This document details the API for Modern.js CLI plugins. CLI plugins allow you to extend and customize the functionality of Modern.js projects during the build and development process.
CLI plugins need to be configured via the plugins field in modern.config.ts.
A typical CLI plugin structure is as follows:
import type { CliPlugin, AppTools } from '@modern-js/app-tools';
const myCliPlugin = (): CliPlugin<AppTools> => ({
name: '@my-org/my-plugin', // Plugin name, ensure uniqueness
setup: api => {
// Use the API here to register hooks, add commands, etc.
api.onBeforeBuild(() => {
console.log('Build is about to start...');
});
},
});
export default myCliPlugin;The setup function receives an api object, which provides all available CLI plugin APIs.
api.getAppContextGets the context information of the Modern.js application.
AppContext object containing the following fields:| Field Name | Type | Description | When Available |
|---|---|---|---|
command | string | The currently executing command (e.g., dev, build, deploy) | - |
port | number | The development server port number | After onPrepare |
configFile | string | The absolute path to the configuration file | - |
isProd | boolean | Whether it is in production mode | - |
appDirectory | string | The absolute path to the project root directory | - |
srcDirectory | string | The absolute path to the project source code directory | - |
distDirectory | string | The absolute path to the project output directory | After modifyResolvedConfig |
sharedDirectory | string | The absolute path to the shared modules directory | - |
nodeModulesDirectory | string | The absolute path to the node_modules directory | - |
ip | string | The IPv4 address of the current machine | - |
packageName | string | The name field in the project's package.json | - |
plugins | object[] | The list of currently registered plugins | - |
entrypoints | object[] | Information about page entry points | - |
serverRoutes | object[] | Server-side routing information | - |
bundlerType | webpack | rspack | The type of bundler currently in use (webpack or rspack) | After onPrepare |
metaName | string | The internal name of the framework | - |
apiDirectory | string | The absolute path to the API module directory (used by BFF) | - |
lambdaDirectory | string | The absolute path to the Lambda module directory (used by BFF) | - |
runtimeConfigFile | string | The name of the runtime configuration file | - |
checkedEntries | string[] | Specified entry information | - |
apiOnly | boolean | Whether it is in apiOnly mode | - |
api.onPrepare(() => {
const appContext = api.getAppContext();
console.log(
`The current project is running in ${
appContext.isProd ? 'production' : 'development'
} mode`,
);
console.log(`Bundler: ${appContext.bundlerType}`);
});
The context information returned by getAppContext is read-only and cannot be modified directly.
api.getConfigGets the user-defined configuration from the modern.config.ts file.
api.onPrepare(() => {
const userConfig = api.getConfig();
if (userConfig.output) {
console.log('User has customized the output configuration');
}
});api.getNormalizedConfigGets the final configuration after internal processing by Modern.js and modifications by plugins (normalized configuration).
modifyResolvedConfig hook.api.modifyResolvedConfig(resolvedConfig => {
// ... Modify the configuration ...
return resolvedConfig;
});
api.onBeforeBuild(() => {
const finalConfig = api.getNormalizedConfig();
console.log('Final build configuration:', finalConfig);
});api.isPluginExistsChecks if a specified plugin is registered.
pluginName: string: The name of the plugin to check.boolean value indicating whether the plugin exists.if (api.isPluginExists('@modern-js/plugin-bff')) {
console.log('BFF plugin is enabled');
}api.getHooksGets all registered hook functions.
const hooks = api.getHooks();
// Manually trigger the onPrepare hook
hooks.onPrepare.call();In custom plugins, you can only manually call the hooks registered by the corresponding plugin and cannot call official hooks to avoid affecting the normal execution order of the application.
api.configModify the initial configuration of Modern.js.
api.config(configFn: () => UserConfig | Promise<UserConfig>)configFn: A function that returns a configuration object or a Promise.modern.config.ts.api.config(() => {
return {
output: {
disableTsChecker: true, // Disable TypeScript type checking
},
};
});Configuration Merging Priority (from highest to lowest):
modern.config.* file.api.config().api.modifyBundlerChainModify Webpack or Rspack configuration using the chain API.
api.modifyBundlerChain(modifyFn: (chain: WebpackChain | RspackChain, utils: WebpackUtils | RspackUtils) => void | Promise<void>)modifyFn: A modification function that receives a webpack-chain or RspackChain instance and utility functions as parameters.api.modifyBundlerChain((chain, utils) => {
if (utils.env === 'development') {
chain.devtool('eval');
}
chain.plugin('bundle-analyze').use(BundleAnalyzerPlugin);
});api.modifyRsbuildConfigModify the Rsbuild configuration.
api.modifyRsbuildConfig(modifyFn: (config: RsbuildConfig, utils: RsbuildUtils) => RsbuildConfig | Promise<RsbuildConfig> | void)modifyFn: A modification function that receives the Rsbuild configuration object and utility functions as parameters. It can return the modified configuration object, a Promise, or nothing (modifying the original object directly).api.modifyRsbuildConfig((config, utils) => {
// Add a custom Rsbuild plugin
config.plugins.push(myCustomRsbuildPlugin());
});api.modifyRspackConfigModify the Rspack configuration (when using Rspack as the bundler).
api.modifyRspackConfig(modifyFn: (config: RspackConfig, utils: RspackUtils) => RspackConfig | Promise<RspackConfig> | void)modifyFn: A modification function that receives the Rspack configuration object and utility functions as parameters. It can return the modified configuration object, a Promise, or nothing (modifying the original object directly).api.modifyRspackConfig((config, utils) => {
config.builtins.minify = {
enable: true,
implementation: utils.rspack.SwcJsMinimizerRspackPlugin,
};
});api.modifyWebpackChainModify the Webpack configuration using webpack-chain (when using Webpack as the bundler).
api.modifyWebpackChain(modifyFn: (chain: WebpackChain, utils: WebpackUtils) => void | Promise<void>)modifyFn: A modification function that receives a webpack-chain instance and utility functions as parameters.api.modifyWebpackChain((chain, utils) => {
// Add a custom Webpack loader
chain.module
.rule('my-loader')
.test(/\.my-ext$/)
.use('my-loader')
.loader(require.resolve('./my-loader'));
});api.modifyWebpackConfigDirectly modify the Webpack configuration object (when using Webpack as the bundler).
api.modifyWebpackConfig(modifyFn: (config: WebpackConfig, utils: WebpackUtils) => WebpackConfig | Promise<WebpackConfig> | void)modifyFn: A modification function that receives the Webpack configuration object and utility functions as parameters. It can return the modified configuration object, a Promise, or nothing (modifying the original object directly).api.modifyWebpackConfig((config, utils) => {
// Disable source map
config.devtool = false;
});Build Configuration Modification Order
modifyRsbuildConfig
modifyBundlerChain
tools.bundlerChain
modifyRspackConfig
tools.rspackmodifyBundlerChain
tools.bundlerChain
modifyWebpackChain
tools.webpackChain
modifyWebpackConfig
tools.webpackapi.modifyServerRoutesModify the server routing configuration.
api.modifyServerRoutes(transformFn: (routes: ServerRoute[]) => ServerRoute[])transformFn: A transformation function that receives the current server routes array as a parameter and returns the modified array.prepare phase).api.modifyServerRoutes(routes => {
// Add a new API route
routes.push({
urlPath: '/api',
isApi: true,
entryPath: '',
isSPA: false,
isSSR: false,
});
return routes;
});api.modifyHtmlPartialsModify HTML template partials.
api.modifyHtmlPartials(modifyFn: (partials: HtmlPartials, entrypoint: Entrypoint) => void)modifyFn: A modification function that receives the HTML template partials object and the current entry point information as parameters.
partials: Contains top, head, and body sections, each with append, prepend, and replace methods.prepare phase).api.modifyHtmlPartials(({ entrypoint, partials }) => {
// Add a meta tag to the <head> section of all pages
if (partials.head && partials.head.append) {
partials.head.append(`<meta name="my-custom-meta" content="value">`);
}
});This hook function will not be executed when using a completely custom HTML template.
api.onPrepareAdd additional logic during the Modern.js preparation phase.
api.onPrepare(prepareFn: () => void | Promise<void>)prepareFn: A preparation function, without parameters, can be asynchronous.api.onPrepare(async () => {
// Perform some initialization operations, such as checking the environment, downloading dependencies, etc.
await prepareMyCustomEnvironment();
});api.addCommandAdd a custom CLI command.
api.addCommand(commandFn: ({ program: Command }) => void)commandFn: Receives the program object from commander as a parameter, used to define new commands.prepare hook has completed.api.addCommand(({ program }) => {
program
.command('my-command')
.description('My custom command')
.action(async () => {
// Execute command logic
console.log('Executing custom command...');
});
});api.addWatchFilesAdd additional files to the watch list (for development mode).
api.addWatchFiles(watchFn: () => string[] | { files: string[]; isPrivate: boolean; })watchFn: Returns an array of file paths or an object containing files and isPrivate properties.
files: An array of file paths to watch.isPrivate: Whether the files are framework-internal (affects behavior when files change).addCommand hook has completed.api.addWatchFiles(() => {
// Watch the .env file in the project root directory
return [path.resolve(api.getAppContext().appDirectory, '.env')];
});api.onFileChangedAdd additional logic when a watched file changes (for development mode).
api.onFileChanged(changeFn: (params: { filename: string; eventType: 'add' | 'change' | 'unlink'; isPrivate: boolean; }) => void)changeFn: A file change handler function that receives the following parameters:
filename: The path of the changed file.eventType: The type of change (add, change, unlink).isPrivate: Whether the file is framework-internal.api.onFileChanged(({ filename, eventType }) => {
if (eventType === 'change' && filename.endsWith('.ts')) {
console.log('TypeScript file changed, may need to recompile...');
}
});api.onBeforeBuildAdd additional logic before the build starts.
api.onBeforeBuild(buildFn: () => void | Promise<void>)buildFn: A function to be executed before the build, without parameters, can be asynchronous.api.onBeforeBuild(() => {
// Perform some environment checks before building
});api.onAfterBuildAdd additional logic after the build is complete.
api.onAfterBuild(buildFn: () => void | Promise<void>)buildFn: A function to be executed after the build, without parameters, can be asynchronous.api.onAfterBuild(() => {
// Upload sourceMap after building
});api.onDevCompileDoneAdd additional logic after the development server compilation is complete.
api.onDevCompileDone(compileFn: () => void | Promise<void>)compileFn: A function to be executed after compilation is complete.api.onDevCompileDone(() => {
// Open the browser after the initial compilation is complete
});api.onBeforeCreateCompilerAdd additional logic before creating the compiler instance.
api.onBeforeCreateCompiler(createFn: () => void | Promise<void>)createFn: A function to be executed before creation, without parameters, can be asynchronous.api.onBeforeCreateCompiler(() => {
// Can get compiler related configuration
});api.onAfterCreateCompilerAdd additional logic after creating the compiler instance.
api.onAfterCreateCompiler(createFn: () => void | Promise<void>)createFn: A function to be executed after creation, without parameters, can be asynchronous.api.onAfterCreateCompiler(() => {
// Can get the compiler instance
});api.onBeforeDevAdd additional logic before starting the development server.
api.onBeforeDev(devFn: () => void | Promise<void>)devFn: A function to be executed before starting the development server, without parameters, can be asynchronous.dev command to start the development server.api.onBeforeDev(async () => {
// Check if the port is occupied
await checkPortAvailability(3000);
});api.onAfterDevAdd additional logic after starting the development server.
api.onAfterDev(devFn: () => void | Promise<void>)devFn: A function to be executed after the development server starts.api.onAfterDev(() => {
// Report dev related information
});api.onBeforeExitAdd additional logic before the process exits.
api.onBeforeExit(exitFn: () => void | Promise<void>)exitFn: A function to be executed before the process exits, without parameters, can be asynchronous.api.onBeforeExit(async () => {
// Perform some cleanup operations, such as closing database connections, deleting temporary files, etc.
await cleanupMyResources();
});api.onBeforePrintInstructionsAdd additional logic before printing success information.
api.onBeforePrintInstructions(printFn: ({instructions: string}) => {instructions: string} | Promise<{instructions: string}>)printFn: Function to modify the printed information, returns the modified information.api.onBeforePrintInstructions(({ instructions }) => {
// do something
return { instructions };
});