Skip to main content

CLI Plugins API

The CLI comes with built-in support for plugins. Plugins are used to extend or modify existing webpack config very easily or even run some pre/post processing scripts during the build process.

The CLI plugin is usually a class or an object implementing ImaCliPlugin interface. This instance is then added to the plugins array field in the ima.config.js, which registers the plugin to the build process. Additionally to extending the webpack config, you have ability to provide additional custom CLI arguments.

CLI Plugins API

Each plugin has to comply with the following interface. Even though almost v everything method is not required and marked as optional, your plugin should implement at least one of the following methods in order to be of any use. Otherwise it would still work but the plugin would not do anything.

/**
* Interface for ima/cli plugins that can be defined in plugins field in ima.conf.js. These can be used
* to extend functionality of default CLI with custom cli arguments and webpack config overrides.
*/
export interface ImaCliPlugin {
/**
* Plugin name, used mainly for better debugging messages.
*/
readonly name: string;

/**
* Optional additional CLI arguments to extend the set of existing ones.
*/
readonly cliArgs?: Partial<Record<ImaCliCommand, CommandBuilder>>;

/**
* Optional plugin hook to do some pre processing right after the cli args are processed
* and the imaConfig is loaded, before the webpack config creation and compiler run.
*/
preProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;

/**
* Webpack callback function used by plugins to customize/extend ima webpack config before it's run.
*/
webpack?(
config: Configuration,
ctx: ImaConfigurationContext,
imaConfig: ImaConfig
): Promise<Configuration>;

/**
* Optional plugin hook to do some custom processing after the compilation has finished.
* Attention! This hook runs only for build command.
*/
postProcess?(args: ImaCliArgs, imaConfig: ImaConfig): Promise<void>;
}

Creating a CLI plugin

In this section we're going to create custom plugin, which generates assets manifest json file. To achieve this we'll use WebpackManifestPlugin and extend our webpack config. We'll also define some additional CLI arguments that will enable us to overwrite certain settings on demand.

First we're going to install the webpack-manifest-plugin:

npm install webpack-manifest-plugin -D

Then we need to define base class for our new CLI plugin. To make things easier we're going to work directly in the ima.config.js but in reality you'd be better of creating separate npm package for easier sharing between multiple IMA.js projects.

./ima.config.js
class CliManifestPlugin {
name = 'CliManifestPlugin';

webpack(config, ctx, imaConfig) {}
}

module.exports = {
plugins: [new CliManifestPlugin()],
};

Extending the webpack config

Now we're going to initialize our manifest plugin. But we only want to do this when we are building the final bundle using the build command. For that we can use the ctx: ImaContext variable, which contains multiple flags and values describing current build context. One of those values is ctx.command which can be either dev or build.

We are also going to make sure that we can provide options to our CLI plugin that are in this case passed directly to the webpack plugin.

./ima.config.js
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

class CliManifestPlugin {
//...
#options = {};

constructor(options) {
this.#options = options;
}

webpack(config, ctx, imaConfig) {
if (ctx.command === 'build') {
config.plugins.push(new WebpackManifestPlugin(this.#options));
}

return config;
}
//...
}
tip

Feel free to print the ctx object into the console and examine it's properties.

Similarly to the ctx you can also use the imaConfig parameter, which contains loaded ima.config.js file. You can use this feature to have some additional custom plugin-specific definitions in the ima.config.js file too, or use existing settings for some additional functionality.

We're now going to use these options and pass seed argument to the plugin. The seed object is used to share data between multiple manifest plugin instances (in our case multiple webpack compilations). This makes sure that the final manifest.json file contains paths to all generated assets and is not overwritten by each finished webpack compilation.

./ima.config.js
//...
const manifestSeed = {};

module.exports = {
plugins: [new CliManifestPlugin({ seed: manifestSeed })],
};

Custom CLI arguments

There may be times you'd like to customize or enable/disable certain features on demand using CLI arguments. To demonstrate this we're going to define manifestBasePath CIL argument which will overwrite the basePath plugin option.

You can define CLI arguments for each command separately, in our case, since the plugin does something only in build command, we're gonna do the same for the CLI arguments:

./ima.config.js
class CliManifestPlugin {
//...
cliArgs = {
dev: undefined, // Dev args will go here
build: {
manifestBasePath: {
desc: 'Overwrite basePath default value',
type: 'string',
},
},
};
//...
}

The argument definition is passed directly to the yargs parser, so anything that yargs options accept can be passed here. If you've done everything correctly you should even see the new argument in the command --help option:

npx ima build --help

ima build

Build an application for production

Options:
--manifestBasePath Overwrite basePath default value [string]

Accessing CLI arguments

CLI argument values are merged into the ctx parameter, so you can access them here. In our case we would like to extend the plugin options with the CLI override:

./ima.config.js
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

class CliManifestPlugin {
//...
webpack(config, ctx, imaConfig) {
if (ctx.command === 'build') {
config.plugins.push(
new WebpackManifestPlugin({
...this.#options,
basePath: ctx.manifestBasePath ?? '',
})
);
}

return config;
}
//...
}

Running npx ima build --manifestBasePath=path/prefix should be reflected in the generated manifest.json file in the ./build directory.

Final results

Below is the entire content of the ima.config.js file we've been building so far that you can use as a reference.

./ima.config.js
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

class CliManifestPlugin {
#options = {};

name = 'CliManifestPlugin';

cliArgs = {
dev: undefined, // Dev args will go here
build: {
manifestBasePath: {
desc: 'Overwrite basePath default value',
type: 'string',
},
},
};

constructor(options) {
this.#options = options;
}

webpack(config, ctx, imaConfig) {
if (ctx.command === 'build') {
config.plugins.push(
new WebpackManifestPlugin({
...this.#options,
basePath: ctx.manifestBasePath ?? '',
})
);
}

return config;
}
}

const manifestSeed = {};

module.exports = {
plugins: [new CliManifestPlugin({ seed: manifestSeed })],
};

Using TypeScript

Since the @ima/cli is written in TypeScript, there are TypeScript definitions you can use while defining your plugin. All types and interfaces are available as exports from the @ima/cli package while you can always have a look at our existing plugins, which are also written in TypeScript for an inspiration.

Existing CLI plugins

Currently we maintain 3 distinct CLI plugins that we actively use in our applications. These enables us to extend the feature set of the IMA.js CLI with additional functionality, which is not really suited to be available by default in the original CLI config, since their use is very situational. However you can almost certainly benefit from using these in your application.

Most of these plugins also provide additional functionality that can be used outside of the CLI plugin definition, but it is essential for it to work properly.