From 4fe15b93deddbe683f945325c2729b9340c781a2 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 22 Jul 2024 15:16:26 +0800 Subject: [PATCH 1/8] feat: support add single environment plugin --- packages/compat/webpack/src/webpackConfig.ts | 10 +- packages/core/src/config.ts | 4 +- packages/core/src/configChain.ts | 17 +- packages/core/src/createRsbuild.ts | 31 +++- packages/core/src/initHooks.ts | 99 ++++++++++-- packages/core/src/initPlugins.ts | 25 +-- packages/core/src/pluginManager.ts | 160 ++++++++++++++----- packages/core/src/plugins/html.ts | 10 +- packages/core/src/provider/initConfigs.ts | 15 +- packages/core/src/provider/rspackConfig.ts | 5 +- packages/core/src/types/config/index.ts | 8 +- packages/core/src/types/context.ts | 9 +- packages/core/src/types/plugin.ts | 48 +++++- packages/core/tests/hooks.test.ts | 26 ++- packages/core/tests/pluginDagSort.test.ts | 151 ++++++++++------- packages/core/tests/pluginStore.test.ts | 75 ++++++++- packages/plugin-react/src/react.ts | 4 +- packages/plugin-react/tests/index.test.ts | 36 ++++- 18 files changed, 553 insertions(+), 180 deletions(-) diff --git a/packages/compat/webpack/src/webpackConfig.ts b/packages/compat/webpack/src/webpackConfig.ts index 69cf6bbdd0..aa7c61c264 100644 --- a/packages/compat/webpack/src/webpackConfig.ts +++ b/packages/compat/webpack/src/webpackConfig.ts @@ -27,9 +27,8 @@ async function modifyWebpackChain( logger.debug('modify webpack chain'); const [modifiedChain] = await context.hooks.modifyWebpackChain.call( - chain, - utils, - ); + utils.environment.name, + )(chain, utils); if (utils.environment.config.tools?.webpackChain) { for (const item of castArray(utils.environment.config.tools.webpackChain)) { @@ -49,9 +48,8 @@ async function modifyWebpackConfig( ): Promise { logger.debug('modify webpack config'); let [modifiedConfig] = await context.hooks.modifyWebpackConfig.call( - webpackConfig, - utils, - ); + utils.environment.name, + )(webpackConfig, utils); if (utils.environment.config.tools?.webpack) { modifiedConfig = reduceConfigsWithContext({ diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 3d6e88a75d..bd71bc11d5 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -458,7 +458,9 @@ export const getRsbuildInspectConfig = ({ for (const [name, config] of Object.entries(environments)) { const debugConfig = { ...config, - pluginNames, + pluginNames: pluginManager + .getPlugins({ environment: name }) + .map((p) => p.name), }; rawEnvironmentConfigs.push({ name, diff --git a/packages/core/src/configChain.ts b/packages/core/src/configChain.ts index 1e6ec0f709..a0bb0ebc87 100644 --- a/packages/core/src/configChain.ts +++ b/packages/core/src/configChain.ts @@ -2,11 +2,8 @@ import RspackChain from 'rspack-chain'; import { castArray, isPlainObject } from './helpers'; import { logger } from './logger'; import type { - CreateAsyncHook, - ModifyBundlerChainFn, + InternalContext, ModifyBundlerChainUtils, - RsbuildConfig, - RsbuildContext, RsbuildEntry, Rspack, RspackConfig, @@ -19,12 +16,7 @@ export function getBundlerChain(): RspackChain { } export async function modifyBundlerChain( - context: RsbuildContext & { - hooks: { - modifyBundlerChain: CreateAsyncHook; - }; - config: Readonly; - }, + context: InternalContext, utils: ModifyBundlerChainUtils, ): Promise { logger.debug('modify bundler chain'); @@ -32,9 +24,8 @@ export async function modifyBundlerChain( const bundlerChain = getBundlerChain(); const [modifiedBundlerChain] = await context.hooks.modifyBundlerChain.call( - bundlerChain, - utils, - ); + utils.environment.name, + )(bundlerChain, utils); if (utils.environment.config.tools?.bundlerChain) { for (const item of castArray(utils.environment.config.tools.bundlerChain)) { diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index 25d603d945..6e2358d857 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -1,6 +1,6 @@ import { createContext } from './createContext'; import { pick } from './helpers'; -import { getPluginAPI } from './initPlugins'; +import { initPluginAPI } from './initPlugins'; import { initRsbuildConfig } from './internal'; import { logger } from './logger'; import { setCssExtractPlugin } from './pluginHelper'; @@ -37,8 +37,10 @@ async function applyDefaultPlugins( ), import('./plugins/asset').then(({ pluginAsset }) => pluginAsset()), import('./plugins/html').then(({ pluginHtml }) => - pluginHtml(async (...args) => { - const result = await context.hooks.modifyHTMLTags.call(...args); + pluginHtml((environment: string) => async (...args) => { + const result = await context.hooks.modifyHTMLTags.call(environment)( + ...args, + ); return result[0]; }), ), @@ -109,8 +111,9 @@ export async function createRsbuild( rsbuildConfig.provider ? 'webpack' : 'rspack', ); - const pluginAPI = getPluginAPI({ context, pluginManager }); - context.pluginAPI = pluginAPI; + const getPluginAPI = initPluginAPI({ context, pluginManager }); + context.getPluginAPI = getPluginAPI; + const globalPluginAPI = getPluginAPI(); logger.debug('add default plugins'); await applyDefaultPlugins(pluginManager, context); @@ -139,7 +142,7 @@ export async function createRsbuild( 'removePlugins', 'isPluginExists', ]), - ...pick(pluginAPI, [ + ...pick(globalPluginAPI, [ 'onBeforeBuild', 'onBeforeCreateCompiler', 'onBeforeStartDevServer', @@ -163,7 +166,7 @@ export async function createRsbuild( 'startDevServer', ]), preview, - context: pluginAPI.context, + context: globalPluginAPI.context, }; if (rsbuildConfig.plugins) { @@ -171,5 +174,19 @@ export async function createRsbuild( rsbuild.addPlugins(plugins); } + // Register environment plugin + if (rsbuildConfig.environments) { + await Promise.all( + Object.entries(rsbuildConfig.environments).map(async ([name, config]) => { + if (config.plugins) { + const plugins = await Promise.all(config.plugins); + rsbuild.addPlugins(plugins, { + environment: name, + }); + } + }), + ); + } + return rsbuild; } diff --git a/packages/core/src/initHooks.ts b/packages/core/src/initHooks.ts index c7358b24e3..ca1326c948 100644 --- a/packages/core/src/initHooks.ts +++ b/packages/core/src/initHooks.ts @@ -1,6 +1,9 @@ import { isFunction } from './helpers'; +import { isPluginMatchEnvironment } from './pluginManager'; import type { AsyncHook, + EnvironmentAsyncHook, + EnvironmentMeta, HookDescriptor, ModifyBundlerChainFn, ModifyEnvironmentConfigFn, @@ -22,6 +25,75 @@ import type { OnExitFn, } from './types'; +export function createEnvironmentAsyncHook< + Callback extends (...args: any[]) => any, +>(): EnvironmentAsyncHook { + type Hook = { + environmentMeta?: EnvironmentMeta; + handler: Callback; + }; + const preGroup: Hook[] = []; + const postGroup: Hook[] = []; + const defaultGroup: Hook[] = []; + + const tap = + (environmentMeta?: EnvironmentMeta) => + (cb: Callback | HookDescriptor) => { + if (isFunction(cb)) { + defaultGroup.push({ + environmentMeta, + handler: cb, + }); + } else if (cb.order === 'pre') { + preGroup.push({ + environmentMeta, + handler: cb.handler, + }); + } else if (cb.order === 'post') { + postGroup.push({ + environmentMeta, + handler: cb.handler, + }); + } else { + defaultGroup.push({ + environmentMeta, + handler: cb.handler, + }); + } + }; + + const call = + (environment?: string) => + async (...args: Parameters) => { + const params = args.slice(0) as Parameters; + const callbacks = [...preGroup, ...defaultGroup, ...postGroup]; + + for (const callback of callbacks) { + // If this callback is not a global callback, the environment info should match + if ( + callback.environmentMeta && + environment && + !isPluginMatchEnvironment(callback.environmentMeta, environment) + ) { + continue; + } + + const result = await callback.handler(...params); + + if (result !== undefined) { + params[0] = result; + } + } + + return params; + }; + + return { + tap, + call, + }; +} + export function createAsyncHook< Callback extends (...args: any[]) => any, >(): AsyncHook { @@ -63,6 +135,7 @@ export function createAsyncHook< } export function initHooks(): { + /** The following hooks are global hooks */ onExit: AsyncHook; onAfterBuild: AsyncHook; onBeforeBuild: AsyncHook; @@ -74,13 +147,14 @@ export function initHooks(): { onBeforeStartProdServer: AsyncHook; onAfterCreateCompiler: AsyncHook; onBeforeCreateCompiler: AsyncHook; - modifyHTMLTags: AsyncHook; - modifyRspackConfig: AsyncHook; - modifyBundlerChain: AsyncHook; - modifyWebpackChain: AsyncHook; - modifyWebpackConfig: AsyncHook; + /** The following hooks are related to the environment */ + modifyHTMLTags: EnvironmentAsyncHook; + modifyRspackConfig: EnvironmentAsyncHook; + modifyBundlerChain: EnvironmentAsyncHook; + modifyWebpackChain: EnvironmentAsyncHook; + modifyWebpackConfig: EnvironmentAsyncHook; modifyRsbuildConfig: AsyncHook; - modifyEnvironmentConfig: AsyncHook; + modifyEnvironmentConfig: EnvironmentAsyncHook; } { return { onExit: createAsyncHook(), @@ -94,13 +168,14 @@ export function initHooks(): { onBeforeStartProdServer: createAsyncHook(), onAfterCreateCompiler: createAsyncHook(), onBeforeCreateCompiler: createAsyncHook(), - modifyHTMLTags: createAsyncHook(), - modifyRspackConfig: createAsyncHook(), - modifyBundlerChain: createAsyncHook(), - modifyWebpackChain: createAsyncHook(), - modifyWebpackConfig: createAsyncHook(), + modifyHTMLTags: createEnvironmentAsyncHook(), + modifyRspackConfig: createEnvironmentAsyncHook(), + modifyBundlerChain: createEnvironmentAsyncHook(), + modifyWebpackChain: createEnvironmentAsyncHook(), + modifyWebpackConfig: createEnvironmentAsyncHook(), modifyRsbuildConfig: createAsyncHook(), - modifyEnvironmentConfig: createAsyncHook(), + modifyEnvironmentConfig: + createEnvironmentAsyncHook(), }; } diff --git a/packages/core/src/initPlugins.ts b/packages/core/src/initPlugins.ts index 442fbbd7ac..7916e15a30 100644 --- a/packages/core/src/initPlugins.ts +++ b/packages/core/src/initPlugins.ts @@ -52,13 +52,13 @@ function applyTransformPlugin( chain.plugin(name).use(RsbuildTransformPlugin); } -export function getPluginAPI({ +export function initPluginAPI({ context, pluginManager, }: { context: InternalContext; pluginManager: PluginManager; -}): RsbuildPluginAPI { +}): (environmentMeta?: { environment: string }) => RsbuildPluginAPI { const { hooks } = context; const publicContext = createPublicContext(context); @@ -118,7 +118,8 @@ export function getPluginAPI({ transformer[id] = handler; - hooks.modifyBundlerChain.tap((chain, { target }) => { + // TODO: support environment + hooks.modifyBundlerChain.tap()((chain, { target }) => { if (descriptor.targets && !descriptor.targets.includes(target)) { return; } @@ -147,9 +148,11 @@ export function getPluginAPI({ hooks.onExit.call(); }); - return { + // Each plugin returns different APIs depending on the registered environment info. + return (environmentMeta?: { environment: string }) => ({ context: publicContext, expose, + // TODO: supports running in specified environment and avoid repeat registry transform, useExposed, getRsbuildConfig, @@ -168,12 +171,12 @@ export function getPluginAPI({ onBeforeStartDevServer: hooks.onBeforeStartDevServer.tap, onAfterStartProdServer: hooks.onAfterStartProdServer.tap, onBeforeStartProdServer: hooks.onBeforeStartProdServer.tap, - modifyHTMLTags: hooks.modifyHTMLTags.tap, - modifyBundlerChain: hooks.modifyBundlerChain.tap, - modifyRspackConfig: hooks.modifyRspackConfig.tap, - modifyWebpackChain: hooks.modifyWebpackChain.tap, - modifyWebpackConfig: hooks.modifyWebpackConfig.tap, modifyRsbuildConfig: hooks.modifyRsbuildConfig.tap, - modifyEnvironmentConfig: hooks.modifyEnvironmentConfig.tap, - }; + modifyHTMLTags: hooks.modifyHTMLTags.tap(environmentMeta), + modifyBundlerChain: hooks.modifyBundlerChain.tap(environmentMeta), + modifyRspackConfig: hooks.modifyRspackConfig.tap(environmentMeta), + modifyWebpackChain: hooks.modifyWebpackChain.tap(environmentMeta), + modifyWebpackConfig: hooks.modifyWebpackConfig.tap(environmentMeta), + modifyEnvironmentConfig: hooks.modifyEnvironmentConfig.tap(environmentMeta), + }); } diff --git a/packages/core/src/pluginManager.ts b/packages/core/src/pluginManager.ts index c648e90e23..73309413b2 100644 --- a/packages/core/src/pluginManager.ts +++ b/packages/core/src/pluginManager.ts @@ -3,8 +3,10 @@ import { isFunction } from './helpers'; import { logger } from './logger'; import type { BundlerPluginInstance, + EnvironmentMeta, Falsy, PluginManager, + PluginMeta, RsbuildPlugin, RsbuildPluginAPI, } from './types'; @@ -52,14 +54,24 @@ function validatePlugin(plugin: unknown) { ); } +export const RSBUILD_ALL_ENVIRONMENT_SYMBOL = 'RSBUILD_ALL_ENVIRONMENT_SYMBOL'; + +export const isPluginMatchEnvironment = ( + meta: { environment: string }, + environment: string, +): boolean => + meta.environment === environment || + meta.environment === RSBUILD_ALL_ENVIRONMENT_SYMBOL; + export function createPluginManager(): PluginManager { - let plugins: RsbuildPlugin[] = []; + let plugins: PluginMeta[] = []; const addPlugins = ( newPlugins: Array, - options?: { before?: string }, + options?: { before?: string; environment?: string }, ) => { - const { before } = options || {}; + const { before, environment = RSBUILD_ALL_ENVIRONMENT_SYMBOL } = + options || {}; for (const newPlugin of newPlugins) { if (!newPlugin) { @@ -68,63 +80,110 @@ export function createPluginManager(): PluginManager { validatePlugin(newPlugin); - if (plugins.find((item) => item.name === newPlugin.name)) { + const existPlugin = plugins.find( + (item) => + item.instance.name === newPlugin.name && + item.environment === environment, + ); + + if (existPlugin) { logger.warn( `Rsbuild plugin "${newPlugin.name}" registered multiple times.`, ); } else if (before) { - const index = plugins.findIndex((item) => item.name === before); + const index = plugins.findIndex( + (item) => item.instance.name === before, + ); if (index === -1) { logger.warn(`Plugin "${before}" does not exist.`); - plugins.push(newPlugin); + plugins.push({ + environment, + instance: newPlugin, + }); } else { - plugins.splice(index, 0, newPlugin); + plugins.splice(index, 0, { + environment, + instance: newPlugin, + }); } } else { - plugins.push(newPlugin); + plugins.push({ + environment, + instance: newPlugin, + }); } } }; - const removePlugins = (pluginNames: string[]) => { - plugins = plugins.filter((plugin) => !pluginNames.includes(plugin.name)); + const removePlugins = ( + pluginNames: string[], + options: { environment?: string } = {}, + ) => { + plugins = plugins.filter( + (plugin) => + !( + pluginNames.includes(plugin.instance.name) && + (!options.environment || plugin.environment === options.environment) + ), + ); }; - const isPluginExists = (pluginName: string) => - Boolean(plugins.find((plugin) => plugin.name === pluginName)); + const isPluginExists = ( + pluginName: string, + options: { environment: string } = { + environment: RSBUILD_ALL_ENVIRONMENT_SYMBOL, + }, + ) => + Boolean( + plugins.find( + (plugin) => + plugin.instance.name === pluginName && + isPluginMatchEnvironment(plugin, options.environment), + ), + ); + const getPlugins = ( + options: { environment: string } = { + environment: RSBUILD_ALL_ENVIRONMENT_SYMBOL, + }, + ) => { + return plugins + .filter((p) => isPluginMatchEnvironment(p, options.environment)) + .map((p) => p.instance); + }; return { - getPlugins: () => plugins, + getPlugins, + getAllPluginsWithMeta: () => plugins, addPlugins, removePlugins, isPluginExists, }; } -export const pluginDagSort = (plugins: RsbuildPlugin[]): RsbuildPlugin[] => { +export const pluginDagSort = (plugins: PluginMeta[]): PluginMeta[] => { let allLines: [string, string][] = []; function getPlugin(name: string) { - const target = plugins.find((item) => item.name === name); - if (!target) { + const targets = plugins.filter((item) => item.instance.name === name); + if (!targets.length) { throw new Error(`plugin ${name} not existed`); } - return target; + return targets; } for (const plugin of plugins) { - if (plugin.pre) { - for (const pre of plugin.pre) { - if (pre && plugins.some((item) => item.name === pre)) { - allLines.push([pre, plugin.name]); + if (plugin.instance.pre) { + for (const pre of plugin.instance.pre) { + if (pre && plugins.some((item) => item.instance.name === pre)) { + allLines.push([pre, plugin.instance.name]); } } } - if (plugin.post) { - for (const post of plugin.post) { - if (post && plugins.some((item) => item.name === post)) { - allLines.push([plugin.name, post]); + if (plugin.instance.post) { + for (const post of plugin.instance.post) { + if (post && plugins.some((item) => item.instance.name === post)) { + allLines.push([plugin.instance.name, post]); } } } @@ -132,21 +191,24 @@ export const pluginDagSort = (plugins: RsbuildPlugin[]): RsbuildPlugin[] => { // search the zero input plugin let zeroEndPoints = plugins.filter( - (item) => !allLines.find((l) => l[1] === item.name), + (item) => !allLines.find((l) => l[1] === item.instance.name), ); - const sortedPoint: RsbuildPlugin[] = []; + const sortedPoint: PluginMeta[] = []; while (zeroEndPoints.length) { const zep = zeroEndPoints.shift()!; - sortedPoint.push(getPlugin(zep.name)); - allLines = allLines.filter((l) => l[0] !== getPlugin(zep.name).name); + sortedPoint.push(...getPlugin(zep.instance.name)); + allLines = allLines.filter( + (l) => l[0] !== getPlugin(zep.instance.name)[0].instance.name, + ); const restPoints = plugins.filter( - (item) => !sortedPoint.find((sp) => sp.name === item.name), + (item) => + !sortedPoint.find((sp) => sp.instance.name === item.instance.name), ); zeroEndPoints = restPoints.filter( - (item) => !allLines.find((l) => l[1] === item.name), + (item) => !allLines.find((l) => l[1] === item.instance.name), ); } @@ -169,28 +231,42 @@ export const pluginDagSort = (plugins: RsbuildPlugin[]): RsbuildPlugin[] => { }; export async function initPlugins({ - pluginAPI, + getPluginAPI, pluginManager, }: { - pluginAPI?: RsbuildPluginAPI; + getPluginAPI: (environmentMeta?: EnvironmentMeta) => RsbuildPluginAPI; pluginManager: PluginManager; }): Promise { logger.debug('init plugins'); - const plugins = pluginDagSort(pluginManager.getPlugins()); + const plugins = pluginDagSort(pluginManager.getAllPluginsWithMeta()); - const removedPlugins = plugins.reduce((ret, plugin) => { - if (plugin.remove) { - return ret.concat(plugin.remove); - } - return ret; - }, []); + const removedPlugins = plugins.reduce>( + (ret, plugin) => { + if (plugin.instance.remove) { + ret[plugin.environment] ??= []; + ret[plugin.environment].push(...plugin.instance.remove); + } + return ret; + }, + {}, + ); for (const plugin of plugins) { - if (removedPlugins.includes(plugin.name)) { + const isGlobalPlugin = + plugin.environment === 'RSBUILD_ALL_ENVIRONMENT_SYMBOL'; + + if ( + removedPlugins[plugin.environment]?.includes(plugin.instance.name) || + (!isGlobalPlugin && + removedPlugins[RSBUILD_ALL_ENVIRONMENT_SYMBOL]?.includes( + plugin.instance.name, + )) + ) { continue; } - await plugin.setup(pluginAPI!); + const { instance, ...environmentMeta } = plugin; + await instance.setup(getPluginAPI!(environmentMeta)); } logger.debug('init plugins done'); diff --git a/packages/core/src/plugins/html.ts b/packages/core/src/plugins/html.ts index 717edb6e1e..479d843f74 100644 --- a/packages/core/src/plugins/html.ts +++ b/packages/core/src/plugins/html.ts @@ -195,7 +195,9 @@ const getTagConfig = ( }; }; -export const pluginHtml = (modifyTagsFn?: ModifyHTMLTagsFn): RsbuildPlugin => ({ +export const pluginHtml = ( + modifyTagsFn?: (environment: string) => ModifyHTMLTagsFn, +): RsbuildPlugin => ({ name: 'rsbuild:html', setup(api) { @@ -310,7 +312,11 @@ export const pluginHtml = (modifyTagsFn?: ModifyHTMLTagsFn): RsbuildPlugin => ({ chain .plugin(CHAIN_ID.PLUGIN.HTML_BASIC) - .use(HtmlBasicPlugin, [htmlInfoMap, environment, modifyTagsFn]); + .use(HtmlBasicPlugin, [ + htmlInfoMap, + environment, + modifyTagsFn?.(environment.name), + ]); if (config.html) { const { appIcon, crossorigin } = config.html; diff --git a/packages/core/src/provider/initConfigs.ts b/packages/core/src/provider/initConfigs.ts index 3d2889baa4..35df1bf01b 100644 --- a/packages/core/src/provider/initConfigs.ts +++ b/packages/core/src/provider/initConfigs.ts @@ -40,11 +40,14 @@ async function modifyEnvironmentConfig( name: string, ) { logger.debug(`modify Rsbuild environment(${name}) config`); - const [modified] = await context.hooks.modifyEnvironmentConfig.call(config, { - name, - mergeEnvironmentConfig: - mergeRsbuildConfig as ModifyEnvironmentConfigUtils['mergeEnvironmentConfig'], - }); + const [modified] = await context.hooks.modifyEnvironmentConfig.call(name)( + config, + { + name, + mergeEnvironmentConfig: + mergeRsbuildConfig as ModifyEnvironmentConfigUtils['mergeEnvironmentConfig'], + }, + ); logger.debug(`modify Rsbuild environment(${name}) config done`); @@ -131,7 +134,7 @@ export async function initRsbuildConfig({ } await initPlugins({ - pluginAPI: context.pluginAPI, + getPluginAPI: context.getPluginAPI!, pluginManager, }); diff --git a/packages/core/src/provider/rspackConfig.ts b/packages/core/src/provider/rspackConfig.ts index c6dfeda59b..e41461d79f 100644 --- a/packages/core/src/provider/rspackConfig.ts +++ b/packages/core/src/provider/rspackConfig.ts @@ -21,9 +21,8 @@ async function modifyRspackConfig( ) { logger.debug('modify Rspack config'); let [modifiedConfig] = await context.hooks.modifyRspackConfig.call( - rspackConfig, - utils, - ); + utils.environment.name, + )(rspackConfig, utils); if (utils.environment.config.tools?.rspack) { modifiedConfig = await reduceConfigsAsyncWithContext({ diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 39ad5161e5..779d08780e 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -66,6 +66,10 @@ export interface EnvironmentConfig { * Options for module federation. */ moduleFederation?: ModuleFederationConfig; + /** + * Configure Rsbuild plugins. + */ + plugins?: RsbuildPlugins; } /** @@ -81,10 +85,6 @@ export interface RsbuildConfig extends EnvironmentConfig { * will take effect during local development and preview. */ server?: ServerConfig; - /** - * Configure Rsbuild plugins. - */ - plugins?: RsbuildPlugins; /** * Configure rsbuild config by environment. */ diff --git a/packages/core/src/types/context.ts b/packages/core/src/types/context.ts index f8e532c8ee..ebd6a131bd 100644 --- a/packages/core/src/types/context.ts +++ b/packages/core/src/types/context.ts @@ -1,7 +1,7 @@ import type { Hooks } from '../initHooks'; import type { NormalizedConfig, RsbuildConfig } from './config'; import type { EnvironmentContext } from './hooks'; -import type { RsbuildPluginAPI } from './plugin'; +import type { EnvironmentMeta, RsbuildPluginAPI } from './plugin'; export type BundlerType = 'rspack' | 'webpack'; @@ -34,8 +34,11 @@ export type InternalContext = RsbuildContext & { originalConfig: Readonly; /** The normalized Rsbuild config. */ normalizedConfig?: NormalizedConfig; - /** The plugin API. */ - pluginAPI?: RsbuildPluginAPI; + /** + * Get the plugin API. + * When environmentMeta is undefined, the global plugin API is returned, which can be used in all environments. + * */ + getPluginAPI?: (environmentMeta?: EnvironmentMeta) => RsbuildPluginAPI; /** The environment context. */ environments: Record; }; diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index 27146a3923..02012e53f6 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -44,6 +44,19 @@ export type HookDescriptor any> = { order: HookOrder; }; +export type EnvironmentMeta = { + environment: string; +}; + +export type EnvironmentAsyncHook any> = { + tap: ( + environmentMeta?: EnvironmentMeta, + ) => (cb: Callback | HookDescriptor) => void; + call: ( + environment?: string, + ) => (...args: Parameters) => Promise>; +}; + export type AsyncHook any> = { tap: (cb: Callback | HookDescriptor) => void; call: (...args: Parameters) => Promise>; @@ -89,16 +102,39 @@ export type ModifyWebpackConfigFn = ( utils: ModifyWebpackConfigUtils, ) => Promise | WebpackConfig | void; +export type PluginMeta = EnvironmentMeta & { + instance: RsbuildPlugin; +}; + export type PluginManager = { - getPlugins: () => RsbuildPlugin[]; + getPlugins: (options?: { + /** Get the plugins in the specified environment. If environment is not specified, get the global plugins. */ + environment: string; + }) => RsbuildPlugin[]; addPlugins: ( plugins: Array, - options?: { before?: string }, + options?: { + before?: string; + /** Add a plugin for the specified environment. If environment is not specified, it will be registered as a global plugin (effective in all environments) */ + environment?: string; + }, + ) => void; + removePlugins: ( + pluginNames: string[], + options?: { + /** Remove the plugin in the specified environment. If environment is not specified, remove it in all environments. */ + environment: string; + }, ) => void; - removePlugins: (pluginNames: string[]) => void; - isPluginExists: (pluginName: string) => boolean; - /** The plugin API. */ - pluginAPI?: RsbuildPluginAPI; + isPluginExists: ( + pluginName: string, + options?: { + /** Whether it exists in the specified environment. If environment is not specified, determine whether the plugin is a global plugin */ + environment: string; + }, + ) => boolean; + /** Get all plugins with environment info */ + getAllPluginsWithMeta: () => PluginMeta[]; }; /** diff --git a/packages/core/tests/hooks.test.ts b/packages/core/tests/hooks.test.ts index 99e1e04f0c..f476416656 100644 --- a/packages/core/tests/hooks.test.ts +++ b/packages/core/tests/hooks.test.ts @@ -1,11 +1,35 @@ import { createStubRsbuild } from '@scripts/test-helper'; -import { initHooks } from '../src/initHooks'; +import { createEnvironmentAsyncHook, initHooks } from '../src/initHooks'; describe('initHooks', () => { test('should init hooks correctly', async () => { const hooks = initHooks(); expect(Object.keys(hooks)).toMatchSnapshot(); }); + + test('createEnvironmentAsyncHook should only works in specified environment', async () => { + const logs: string[] = []; + const hookA = createEnvironmentAsyncHook(); + hookA.tap()((msg) => { + logs.push(`[global] ${msg}`); + }); + + hookA.tap({ + environment: 'a', + })((msg) => { + logs.push(msg); + }); + + await hookA.call('a')('call in a'); + + await hookA.call('b')('call in b'); + + expect(logs).toEqual([ + '[global] call in a', + 'call in a', + '[global] call in b', + ]); + }); }); describe('onExit hook', () => { diff --git a/packages/core/tests/pluginDagSort.test.ts b/packages/core/tests/pluginDagSort.test.ts index 291bb0edc2..db6a1f5b48 100644 --- a/packages/core/tests/pluginDagSort.test.ts +++ b/packages/core/tests/pluginDagSort.test.ts @@ -12,37 +12,47 @@ describe('sort plugins', () => { { name: '6', pre: [], post: [] }, ] as RsbuildPlugin[]; - const result = pluginDagSort(cases); - expect(result).toEqual([ - { - name: '1', - }, - { - name: '4', - post: [], - pre: [], - }, - { - name: '6', - post: [], - pre: [], - }, - { - name: '5', - post: ['3'], - pre: ['6'], - }, - { - name: '3', - post: ['2'], - pre: ['1'], - }, - { - name: '2', - post: [], - pre: [], - }, - ]); + const result = pluginDagSort( + cases.map((c) => ({ + instance: c, + environment: '', + })), + ); + expect(result).toEqual( + [ + { + name: '1', + }, + { + name: '4', + post: [], + pre: [], + }, + { + name: '6', + post: [], + pre: [], + }, + { + name: '5', + post: ['3'], + pre: ['6'], + }, + { + name: '3', + post: ['2'], + pre: ['1'], + }, + { + name: '2', + post: [], + pre: [], + }, + ].map((p) => ({ + environment: '', + instance: p, + })), + ); }); it('should keep the order consistent', () => { @@ -53,13 +63,23 @@ describe('sort plugins', () => { { name: '4' }, ] as RsbuildPlugin[]; - const result = pluginDagSort(cases); - expect(result).toEqual([ - { name: '1' }, - { name: '3' }, - { name: '2', pre: ['3'] }, - { name: '4' }, - ]); + const result = pluginDagSort( + cases.map((c) => ({ + instance: c, + environment: '', + })), + ); + expect(result).toEqual( + [ + { name: '1' }, + { name: '3' }, + { name: '2', pre: ['3'] }, + { name: '4' }, + ].map((p) => ({ + environment: '', + instance: p, + })), + ); }); it('should allow some invalid plugins', () => { @@ -70,25 +90,35 @@ describe('sort plugins', () => { { name: undefined }, ] as RsbuildPlugin[]; - const result = pluginDagSort(cases); - expect(result).toEqual([ - { - name: '1', - }, - { - name: '3', - post: ['2', undefined], - pre: ['1'], - }, - { - name: '2', - post: [], - pre: [undefined], - }, - { - name: undefined, - }, - ]); + const result = pluginDagSort( + cases.map((c) => ({ + instance: c, + environment: '', + })), + ); + expect(result).toEqual( + [ + { + name: '1', + }, + { + name: '3', + post: ['2', undefined], + pre: ['1'], + }, + { + name: '2', + post: [], + pre: [undefined], + }, + { + name: undefined, + }, + ].map((p) => ({ + environment: '', + instance: p, + })), + ); }); it('should throw error when plugin has ring', () => { @@ -102,7 +132,12 @@ describe('sort plugins', () => { ] as RsbuildPlugin[]; expect(() => { - pluginDagSort(cases); + pluginDagSort( + cases.map((c) => ({ + instance: c, + environment: '', + })), + ); }).toThrow(/plugins dependencies has loop: 2,3,5/); }); }); diff --git a/packages/core/tests/pluginStore.test.ts b/packages/core/tests/pluginStore.test.ts index 74b303189f..0c0abe324f 100644 --- a/packages/core/tests/pluginStore.test.ts +++ b/packages/core/tests/pluginStore.test.ts @@ -34,7 +34,7 @@ describe('initPlugins', () => { }, ]); - await initPlugins({ pluginManager }); + await initPlugins({ pluginManager, getPluginAPI: () => ({}) as any }); expect(result).toEqual([2, 0, 3, 1]); }); @@ -65,8 +65,79 @@ describe('initPlugins', () => { }, ]); - await initPlugins({ pluginManager }); + await initPlugins({ pluginManager, getPluginAPI: () => ({}) as any }); expect(result).toEqual([0, 2]); }); }); + +describe('pluginManager', () => { + it('should add / remove / get specific environment plugin correctly', async () => { + const pluginManager = createPluginManager(); + + pluginManager.addPlugins([ + { + name: 'plugin0', + setup() {}, + }, + { + name: 'plugin1', + setup() {}, + }, + { + name: 'plugin2', + setup() {}, + }, + { + name: 'plugin3', + setup() {}, + }, + ]); + + pluginManager.addPlugins( + [ + // Repeat registry + { + name: 'plugin1', + setup() {}, + }, + { + name: 'plugin5', + setup() {}, + }, + ], + { environment: 'b' }, + ); + + pluginManager.removePlugins(['plugin3']); + + expect(pluginManager.isPluginExists('plugin3')).toBeFalsy(); + expect(pluginManager.isPluginExists('plugin5')).toBeFalsy(); + expect( + pluginManager.isPluginExists('plugin5', { + environment: 'b', + }), + ).toBeTruthy(); + + expect(pluginManager.getPlugins().map((p) => p.name)).toEqual([ + 'plugin0', + 'plugin1', + 'plugin2', + ]); + + expect( + pluginManager.getPlugins({ environment: 'a' }).map((p) => p.name), + ).toEqual(['plugin0', 'plugin1', 'plugin2']); + + expect( + pluginManager.getPlugins({ environment: 'b' }).map((p) => p.name), + ).toEqual(['plugin0', 'plugin1', 'plugin2', 'plugin1', 'plugin5']); + + pluginManager.removePlugins(['plugin1'], { environment: 'b' }); + + // should remove environment's plugin1 and keep global plugin1 + expect( + pluginManager.getPlugins({ environment: 'b' }).map((p) => p.name), + ).toEqual(['plugin0', 'plugin1', 'plugin2', 'plugin5']); + }); +}); diff --git a/packages/plugin-react/src/react.ts b/packages/plugin-react/src/react.ts index 917572665c..e3a5e3c0f2 100644 --- a/packages/plugin-react/src/react.ts +++ b/packages/plugin-react/src/react.ts @@ -91,7 +91,7 @@ export const applyBasicReactSupport = ( }; export const applyReactProfiler = (api: RsbuildPluginAPI): void => { - api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => { + api.modifyEnvironmentConfig((config, { mergeEnvironmentConfig }) => { const enableProfilerConfig: RsbuildConfig = { output: { minify: { @@ -105,7 +105,7 @@ export const applyReactProfiler = (api: RsbuildPluginAPI): void => { }, }, }; - return mergeRsbuildConfig(config, enableProfilerConfig); + return mergeEnvironmentConfig(config, enableProfilerConfig); }); api.modifyBundlerChain((chain) => { diff --git a/packages/plugin-react/tests/index.test.ts b/packages/plugin-react/tests/index.test.ts index cc4163dc96..0b63f7627b 100644 --- a/packages/plugin-react/tests/index.test.ts +++ b/packages/plugin-react/tests/index.test.ts @@ -1,5 +1,5 @@ import { createStubRsbuild } from '@scripts/test-helper'; -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { pluginReact } from '../src'; describe('plugins/react', () => { @@ -90,4 +90,38 @@ describe('plugins/react', () => { expect(JSON.stringify(config)).toContain(`"importSource":"@emotion/react"`); }); + + it('should allow to add react plugin as single environment plugin', async () => { + process.env.NODE_ENV = 'production'; + + const rsbuild = await createStubRsbuild({ + rsbuildConfig: { + environments: { + web: {}, + web1: {}, + }, + }, + }); + + rsbuild.addPlugins( + [ + pluginReact({ + enableProfiler: true, + }), + ], + { + environment: 'web', + }, + ); + const { bundlerConfigs, environmentConfigs } = + await rsbuild.inspectConfig(); + + expect(bundlerConfigs[0]).toContain('lib-react'); + expect(environmentConfigs[0]).toContain('keep_classnames'); + + expect(bundlerConfigs[1]).not.toContain('lib-react'); + expect(environmentConfigs[1]).not.toContain('keep_classnames'); + + delete process.env.NODE_ENV; + }); }); From ea9b48e4baa1b33b29dc9f5b885024e63f117e7a Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 22 Jul 2024 16:24:45 +0800 Subject: [PATCH 2/8] chore: add test and doc --- e2e/cases/environments/plugins/index.test.ts | 60 +++++++++++++++++++ e2e/cases/environments/plugins/src/App.tsx | 3 + e2e/cases/environments/plugins/src/index.ts | 9 +++ e2e/cases/environments/plugins/src/index1.ts | 5 ++ website/docs/en/config/environments.mdx | 1 + .../docs/en/guide/advanced/environments.mdx | 26 ++++++++ website/docs/zh/config/environments.mdx | 1 + .../docs/zh/guide/advanced/environments.mdx | 26 ++++++++ 8 files changed, 131 insertions(+) create mode 100644 e2e/cases/environments/plugins/index.test.ts create mode 100644 e2e/cases/environments/plugins/src/App.tsx create mode 100644 e2e/cases/environments/plugins/src/index.ts create mode 100644 e2e/cases/environments/plugins/src/index1.ts diff --git a/e2e/cases/environments/plugins/index.test.ts b/e2e/cases/environments/plugins/index.test.ts new file mode 100644 index 0000000000..aa6fe143ed --- /dev/null +++ b/e2e/cases/environments/plugins/index.test.ts @@ -0,0 +1,60 @@ +import { build, gotoPage } from '@e2e/helper'; +import { expect, test } from '@playwright/test'; + +import { pluginReact } from '@rsbuild/plugin-react'; + +test('should add single environment plugin correctly', async ({ page }) => { + const rsbuild = await build({ + cwd: __dirname, + rsbuildConfig: { + environments: { + web: { + output: { + filenameHash: false, + }, + plugins: [pluginReact()], + }, + web1: { + source: { + entry: { + main: './src/index1.ts', + }, + }, + output: { + assetPrefix: 'auto', + filenameHash: false, + distPath: { + root: 'dist/web1', + }, + }, + }, + }, + }, + runServer: true, + }); + + await gotoPage(page, rsbuild); + + const button = page.locator('#test'); + await expect(button).toHaveText('Hello Rsbuild!'); + + const web1Url = new URL(`http://localhost:${rsbuild.port}/web1/main`); + + await page.goto(web1Url.href); + + await expect(page.locator('#test1')).toHaveText('Hello Rsbuild!'); + + const files = await rsbuild.unwrapOutputJSON(); + const filenames = Object.keys(files); + + expect( + filenames.some((filename) => + filename.includes('dist/static/js/lib-react.js'), + ), + ).toBeTruthy(); + expect( + filenames.some((filename) => + filename.includes('dist/web1/static/js/lib-react.js'), + ), + ).toBeFalsy(); +}); diff --git a/e2e/cases/environments/plugins/src/App.tsx b/e2e/cases/environments/plugins/src/App.tsx new file mode 100644 index 0000000000..639af6a6e2 --- /dev/null +++ b/e2e/cases/environments/plugins/src/App.tsx @@ -0,0 +1,3 @@ +const App = () =>
Hello Rsbuild!
; + +export default App; diff --git a/e2e/cases/environments/plugins/src/index.ts b/e2e/cases/environments/plugins/src/index.ts new file mode 100644 index 0000000000..5ceb026ccc --- /dev/null +++ b/e2e/cases/environments/plugins/src/index.ts @@ -0,0 +1,9 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(React.createElement(App)); +} diff --git a/e2e/cases/environments/plugins/src/index1.ts b/e2e/cases/environments/plugins/src/index1.ts new file mode 100644 index 0000000000..7cbdd06026 --- /dev/null +++ b/e2e/cases/environments/plugins/src/index1.ts @@ -0,0 +1,5 @@ +document.getElementById('root').innerHTML = ` +
+
Hello Rsbuild!
+
+`; diff --git a/website/docs/en/config/environments.mdx b/website/docs/en/config/environments.mdx index b62d19f4a2..56a42ceb77 100644 --- a/website/docs/en/config/environments.mdx +++ b/website/docs/en/config/environments.mdx @@ -16,6 +16,7 @@ interface EnvironmentConfig { security?: SecurityConfig; performance?: PerformanceConfig; moduleFederation?: ModuleFederationConfig; + plugins?: RsbuildPlugins[]; } type Environments = { diff --git a/website/docs/en/guide/advanced/environments.mdx b/website/docs/en/guide/advanced/environments.mdx index 4656b10225..844bbd1ce8 100644 --- a/website/docs/en/guide/advanced/environments.mdx +++ b/website/docs/en/guide/advanced/environments.mdx @@ -100,6 +100,32 @@ export default { }; ``` +## Add plugins for specified environment + +Plugins configured through the [plugins](/config/plugins) field support running in all environments. If you want a plugin to run only in a specified environment, you can configure the plugin in the specified environment. + +For example, enable the react plugin only in the web environment: + +```ts title=rsbuild.config.ts +import { pluginReact } from '@rsbuild/plugin-react'; + +export default { + environments: { + web: { + output: { + target: 'web', + }, + plugins: [pluginReact()], + }, + node: { + output: { + target: 'node', + }, + }, + }, +}; +``` + ## Plugin API ### Update environment config diff --git a/website/docs/zh/config/environments.mdx b/website/docs/zh/config/environments.mdx index ec45529aca..417c8a264d 100644 --- a/website/docs/zh/config/environments.mdx +++ b/website/docs/zh/config/environments.mdx @@ -16,6 +16,7 @@ interface EnvironmentConfig { security?: SecurityConfig; performance?: PerformanceConfig; moduleFederation?: ModuleFederationConfig; + plugins?: RsbuildPlugins[]; } type Environments = { diff --git a/website/docs/zh/guide/advanced/environments.mdx b/website/docs/zh/guide/advanced/environments.mdx index edb0d5bb38..1d78afd8b6 100644 --- a/website/docs/zh/guide/advanced/environments.mdx +++ b/website/docs/zh/guide/advanced/environments.mdx @@ -100,6 +100,32 @@ export default { }; ``` +## 为指定环境添加插件 + +通过 [plugins](/config/plugins) 字段配置的插件支持在所有环境下运行,如果你希望某个插件仅在指定环境下运行时,将该插件配置在指定 environment 下即可。 + +如,仅在 web 环境下开启 react 插件: + +```ts title=rsbuild.config.ts +import { pluginReact } from '@rsbuild/plugin-react'; + +export default { + environments: { + web: { + output: { + target: 'web', + }, + plugins: [pluginReact()], + }, + node: { + output: { + target: 'node', + }, + }, + }, +}; +``` + ## 插件 API ### 更新配置 From 7d5ec8a448a620b020c32b04c86effaa4aff46ba Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Mon, 22 Jul 2024 16:38:03 +0800 Subject: [PATCH 3/8] test: update case --- .../__snapshots__/environments.test.ts.snap | 13 +++++ packages/core/tests/environments.test.ts | 49 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/core/tests/__snapshots__/environments.test.ts.snap b/packages/core/tests/__snapshots__/environments.test.ts.snap index da91c41c38..8c7c2bd9da 100644 --- a/packages/core/tests/__snapshots__/environments.test.ts.snap +++ b/packages/core/tests/__snapshots__/environments.test.ts.snap @@ -555,6 +555,19 @@ exports[`environment config > should print environment config when inspect confi } `; +exports[`environment config > should support add single environment plugin 1`] = ` +{ + "ssr": { + "global": "ssr", + "ssr": "ssr", + }, + "web": { + "global": "web", + "web": "web", + }, +} +`; + exports[`environment config > should support modify environment config by api.modifyRsbuildConfig 1`] = ` { "ssr": { diff --git a/packages/core/tests/environments.test.ts b/packages/core/tests/environments.test.ts index 93d298e18f..cbbb407f87 100644 --- a/packages/core/tests/environments.test.ts +++ b/packages/core/tests/environments.test.ts @@ -1,5 +1,5 @@ import { join } from 'node:path'; -import { createRsbuild } from '../src'; +import { type RsbuildPlugin, createRsbuild } from '../src'; describe('environment config', () => { it('should normalize context correctly', async () => { @@ -154,6 +154,53 @@ describe('environment config', () => { expect(environmentConfigs).toMatchSnapshot(); }); + it('should support add single environment plugin', async () => { + process.env.NODE_ENV = 'development'; + const plugin: (pluginId: string) => RsbuildPlugin = (pluginId) => ({ + name: 'test-environment', + setup: (api) => { + api.modifyEnvironmentConfig( + (config, { name, mergeEnvironmentConfig }) => { + return mergeEnvironmentConfig(config, { + source: { + alias: { + [pluginId]: name, + }, + }, + }); + }, + ); + }, + }); + const rsbuild = await createRsbuild({ + rsbuildConfig: { + environments: { + web: { + plugins: [plugin('web')], + }, + ssr: { + plugins: [plugin('ssr')], + }, + }, + }, + }); + + rsbuild.addPlugins([plugin('global')]); + + const { + origin: { environmentConfigs }, + } = await rsbuild.inspectConfig(); + + expect( + Object.fromEntries( + Object.entries(environmentConfigs).map(([name, config]) => [ + name, + config.source.alias, + ]), + ), + ).toMatchSnapshot(); + }); + it('should normalize environment config correctly', async () => { process.env.NODE_ENV = 'development'; const rsbuild = await createRsbuild({ From 40e47aee2c0277f82fa3e7b7f1a0ceb10db2db71 Mon Sep 17 00:00:00 2001 From: gaoyuan <9aoyuao@gmail.com> Date: Mon, 22 Jul 2024 18:45:51 +0800 Subject: [PATCH 4/8] Update website/docs/en/guide/advanced/environments.mdx Co-authored-by: neverland --- website/docs/en/guide/advanced/environments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/en/guide/advanced/environments.mdx b/website/docs/en/guide/advanced/environments.mdx index 844bbd1ce8..41d0dda1b2 100644 --- a/website/docs/en/guide/advanced/environments.mdx +++ b/website/docs/en/guide/advanced/environments.mdx @@ -104,7 +104,7 @@ export default { Plugins configured through the [plugins](/config/plugins) field support running in all environments. If you want a plugin to run only in a specified environment, you can configure the plugin in the specified environment. -For example, enable the react plugin only in the web environment: +For example, enable the React plugin only in the web environment: ```ts title=rsbuild.config.ts import { pluginReact } from '@rsbuild/plugin-react'; From e2357a9f7849aea92df1010fb36cdc34a9f35cf9 Mon Sep 17 00:00:00 2001 From: gaoyuan <9aoyuao@gmail.com> Date: Mon, 22 Jul 2024 18:46:02 +0800 Subject: [PATCH 5/8] Update website/docs/en/guide/advanced/environments.mdx Co-authored-by: neverland --- website/docs/en/guide/advanced/environments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/en/guide/advanced/environments.mdx b/website/docs/en/guide/advanced/environments.mdx index 41d0dda1b2..658f5b19be 100644 --- a/website/docs/en/guide/advanced/environments.mdx +++ b/website/docs/en/guide/advanced/environments.mdx @@ -102,7 +102,7 @@ export default { ## Add plugins for specified environment -Plugins configured through the [plugins](/config/plugins) field support running in all environments. If you want a plugin to run only in a specified environment, you can configure the plugin in the specified environment. +Plugins configured through the [plugins](/config/plugins) field support running in all environments. If you want a plugin to run only in a specified environment, you can configure the plugin in the specified `environment`. For example, enable the React plugin only in the web environment: From 60664afa5400fbe0fef16b35dcce6fd81395fc18 Mon Sep 17 00:00:00 2001 From: gaoyuan <9aoyuao@gmail.com> Date: Mon, 22 Jul 2024 18:46:11 +0800 Subject: [PATCH 6/8] Update website/docs/zh/guide/advanced/environments.mdx Co-authored-by: neverland --- website/docs/zh/guide/advanced/environments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/zh/guide/advanced/environments.mdx b/website/docs/zh/guide/advanced/environments.mdx index 1d78afd8b6..38bc29ec1b 100644 --- a/website/docs/zh/guide/advanced/environments.mdx +++ b/website/docs/zh/guide/advanced/environments.mdx @@ -104,7 +104,7 @@ export default { 通过 [plugins](/config/plugins) 字段配置的插件支持在所有环境下运行,如果你希望某个插件仅在指定环境下运行时,将该插件配置在指定 environment 下即可。 -如,仅在 web 环境下开启 react 插件: +例如,仅在 web 环境下开启 React 插件: ```ts title=rsbuild.config.ts import { pluginReact } from '@rsbuild/plugin-react'; From f2e74f32ae270f1c039273fd7abbd1e4b872e5b3 Mon Sep 17 00:00:00 2001 From: gaoyuan <9aoyuao@gmail.com> Date: Mon, 22 Jul 2024 18:46:34 +0800 Subject: [PATCH 7/8] Update website/docs/zh/guide/advanced/environments.mdx Co-authored-by: neverland --- website/docs/zh/guide/advanced/environments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/zh/guide/advanced/environments.mdx b/website/docs/zh/guide/advanced/environments.mdx index 38bc29ec1b..9534d78830 100644 --- a/website/docs/zh/guide/advanced/environments.mdx +++ b/website/docs/zh/guide/advanced/environments.mdx @@ -102,7 +102,7 @@ export default { ## 为指定环境添加插件 -通过 [plugins](/config/plugins) 字段配置的插件支持在所有环境下运行,如果你希望某个插件仅在指定环境下运行时,将该插件配置在指定 environment 下即可。 +通过 [plugins](/config/plugins) 字段配置的插件支持在所有环境下运行,如果你希望某个插件仅在指定环境下运行时,将该插件配置在特定 `environment` 下即可。 例如,仅在 web 环境下开启 React 插件: From e8418ba1cd4b76fe8f202dd379432eb30ac1ea22 Mon Sep 17 00:00:00 2001 From: "gaoyuan.1226" Date: Tue, 23 Jul 2024 14:42:09 +0800 Subject: [PATCH 8/8] fix: tapEnvironment --- packages/compat/webpack/src/webpackConfig.ts | 16 ++- packages/core/src/configChain.ts | 8 +- packages/core/src/createRsbuild.ts | 7 +- packages/core/src/initHooks.ts | 115 ++++++++------- packages/core/src/initPlugins.ts | 142 +++++++++++-------- packages/core/src/pluginManager.ts | 21 +-- packages/core/src/provider/initConfigs.ts | 21 +-- packages/core/src/provider/rspackConfig.ts | 8 +- packages/core/src/types/context.ts | 7 +- packages/core/src/types/plugin.ts | 53 +++++-- packages/core/tests/hooks.test.ts | 19 ++- 11 files changed, 246 insertions(+), 171 deletions(-) diff --git a/packages/compat/webpack/src/webpackConfig.ts b/packages/compat/webpack/src/webpackConfig.ts index aa7c61c264..116c01f3fa 100644 --- a/packages/compat/webpack/src/webpackConfig.ts +++ b/packages/compat/webpack/src/webpackConfig.ts @@ -26,9 +26,11 @@ async function modifyWebpackChain( ): Promise { logger.debug('modify webpack chain'); - const [modifiedChain] = await context.hooks.modifyWebpackChain.call( - utils.environment.name, - )(chain, utils); + const [modifiedChain] = + await context.hooks.modifyWebpackChain.callInEnvironment({ + environment: utils.environment.name, + args: [chain, utils], + }); if (utils.environment.config.tools?.webpackChain) { for (const item of castArray(utils.environment.config.tools.webpackChain)) { @@ -47,9 +49,11 @@ async function modifyWebpackConfig( utils: ModifyWebpackConfigUtils, ): Promise { logger.debug('modify webpack config'); - let [modifiedConfig] = await context.hooks.modifyWebpackConfig.call( - utils.environment.name, - )(webpackConfig, utils); + let [modifiedConfig] = + await context.hooks.modifyWebpackConfig.callInEnvironment({ + environment: utils.environment.name, + args: [webpackConfig, utils], + }); if (utils.environment.config.tools?.webpack) { modifiedConfig = reduceConfigsWithContext({ diff --git a/packages/core/src/configChain.ts b/packages/core/src/configChain.ts index fb2815611f..d2c54fde95 100644 --- a/packages/core/src/configChain.ts +++ b/packages/core/src/configChain.ts @@ -23,9 +23,11 @@ export async function modifyBundlerChain( const bundlerChain = getBundlerChain(); - const [modifiedBundlerChain] = await context.hooks.modifyBundlerChain.call( - utils.environment.name, - )(bundlerChain, utils); + const [modifiedBundlerChain] = + await context.hooks.modifyBundlerChain.callInEnvironment({ + environment: utils.environment.name, + args: [bundlerChain, utils], + }); if (utils.environment.config.tools?.bundlerChain) { for (const item of castArray(utils.environment.config.tools.bundlerChain)) { diff --git a/packages/core/src/createRsbuild.ts b/packages/core/src/createRsbuild.ts index 0dad8c1f37..dd948f2e3c 100644 --- a/packages/core/src/createRsbuild.ts +++ b/packages/core/src/createRsbuild.ts @@ -38,9 +38,10 @@ async function applyDefaultPlugins( import('./plugins/asset').then(({ pluginAsset }) => pluginAsset()), import('./plugins/html').then(({ pluginHtml }) => pluginHtml((environment: string) => async (...args) => { - const result = await context.hooks.modifyHTMLTags.call(environment)( - ...args, - ); + const result = await context.hooks.modifyHTMLTags.callInEnvironment({ + environment, + args, + }); return result[0]; }), ), diff --git a/packages/core/src/initHooks.ts b/packages/core/src/initHooks.ts index ca1326c948..5776b66013 100644 --- a/packages/core/src/initHooks.ts +++ b/packages/core/src/initHooks.ts @@ -3,7 +3,6 @@ import { isPluginMatchEnvironment } from './pluginManager'; import type { AsyncHook, EnvironmentAsyncHook, - EnvironmentMeta, HookDescriptor, ModifyBundlerChainFn, ModifyEnvironmentConfigFn, @@ -29,68 +28,77 @@ export function createEnvironmentAsyncHook< Callback extends (...args: any[]) => any, >(): EnvironmentAsyncHook { type Hook = { - environmentMeta?: EnvironmentMeta; + environment?: string; handler: Callback; }; const preGroup: Hook[] = []; const postGroup: Hook[] = []; const defaultGroup: Hook[] = []; - const tap = - (environmentMeta?: EnvironmentMeta) => - (cb: Callback | HookDescriptor) => { - if (isFunction(cb)) { - defaultGroup.push({ - environmentMeta, - handler: cb, - }); - } else if (cb.order === 'pre') { - preGroup.push({ - environmentMeta, - handler: cb.handler, - }); - } else if (cb.order === 'post') { - postGroup.push({ - environmentMeta, - handler: cb.handler, - }); - } else { - defaultGroup.push({ - environmentMeta, - handler: cb.handler, - }); + const tapEnvironment = ({ + environment, + handler: cb, + }: { + environment?: string; + handler: Callback | HookDescriptor; + }) => { + if (isFunction(cb)) { + defaultGroup.push({ + environment, + handler: cb, + }); + } else if (cb.order === 'pre') { + preGroup.push({ + environment, + handler: cb.handler, + }); + } else if (cb.order === 'post') { + postGroup.push({ + environment, + handler: cb.handler, + }); + } else { + defaultGroup.push({ + environment, + handler: cb.handler, + }); + } + }; + + const callInEnvironment = async ({ + environment, + args: params, + }: { + environment?: string; + args: Parameters; + }) => { + const callbacks = [...preGroup, ...defaultGroup, ...postGroup]; + + for (const callback of callbacks) { + // If this callback is not a global callback, the environment info should match + if ( + callback.environment && + environment && + !isPluginMatchEnvironment(callback.environment, environment) + ) { + continue; } - }; - - const call = - (environment?: string) => - async (...args: Parameters) => { - const params = args.slice(0) as Parameters; - const callbacks = [...preGroup, ...defaultGroup, ...postGroup]; - - for (const callback of callbacks) { - // If this callback is not a global callback, the environment info should match - if ( - callback.environmentMeta && - environment && - !isPluginMatchEnvironment(callback.environmentMeta, environment) - ) { - continue; - } - - const result = await callback.handler(...params); - - if (result !== undefined) { - params[0] = result; - } + + const result = await callback.handler(...params); + + if (result !== undefined) { + params[0] = result; } + } - return params; - }; + return params; + }; return { - tap, - call, + tapEnvironment, + tap: (handler: Callback | HookDescriptor) => + tapEnvironment({ handler }), + callInEnvironment, }; } @@ -113,8 +121,7 @@ export function createAsyncHook< } }; - const call = async (...args: Parameters) => { - const params = args.slice(0) as Parameters; + const call = async (...params: Parameters) => { const callbacks = [...preGroup, ...defaultGroup, ...postGroup]; for (const callback of callbacks) { diff --git a/packages/core/src/initPlugins.ts b/packages/core/src/initPlugins.ts index aec368ee33..87d118b9f6 100644 --- a/packages/core/src/initPlugins.ts +++ b/packages/core/src/initPlugins.ts @@ -6,7 +6,6 @@ import { removeLeadingSlash } from './helpers'; import type { TransformLoaderOptions } from './loader/transformLoader'; import { isPluginMatchEnvironment } from './pluginManager'; import type { - EnvironmentMeta, GetRsbuildConfig, InternalContext, NormalizedConfig, @@ -82,7 +81,7 @@ export function initPluginAPI({ }: { context: InternalContext; pluginManager: PluginManager; -}): (environmentMeta?: { environment: string }) => RsbuildPluginAPI { +}): (environment?: string) => RsbuildPluginAPI { const { hooks } = context; const publicContext = createPublicContext(context); @@ -137,12 +136,12 @@ export function initPluginAPI({ let transformId = 0; const transformer: Record = {}; const processAssetsFns: Array<{ - environmentMeta?: EnvironmentMeta; + environment?: string; descriptor: ProcessAssetsDescriptor; handler: ProcessAssetsHandler; }> = []; - hooks.modifyBundlerChain.tap()((chain, { target, environment }) => { + hooks.modifyBundlerChain.tap((chain, { target, environment }) => { const pluginName = 'RsbuildCorePlugin'; /** @@ -164,7 +163,7 @@ export function initPluginAPI({ for (const { descriptor, handler, - environmentMeta, + environment: metaEnvironment, } of processAssetsFns) { // filter by targets if (descriptor.targets && !descriptor.targets.includes(target)) { @@ -173,8 +172,8 @@ export function initPluginAPI({ // filter by environment if ( - environmentMeta && - !isPluginMatchEnvironment(environmentMeta, environment.name) + metaEnvironment && + !isPluginMatchEnvironment(metaEnvironment, environment.name) ) { return; } @@ -201,70 +200,69 @@ export function initPluginAPI({ chain.plugin(pluginName).use(RsbuildCorePlugin); }); - const getTransformFn: (environmentMeta?: { - environment: string; - }) => TransformFn = (environmentMeta) => (descriptor, handler) => { - const id = `rsbuild-transform-${transformId++}`; + const getTransformFn: (environment?: string) => TransformFn = + (environment) => (descriptor, handler) => { + const id = `rsbuild-transform-${transformId++}`; - transformer[id] = handler; + transformer[id] = handler; - hooks.modifyBundlerChain.tap(environmentMeta)( - (chain, { target, environment }) => { - // filter by targets - if (descriptor.targets && !descriptor.targets.includes(target)) { - return; - } + hooks.modifyBundlerChain.tapEnvironment({ + environment, + handler: (chain, { target, environment }) => { + // filter by targets + if (descriptor.targets && !descriptor.targets.includes(target)) { + return; + } - // filter by environments - if ( - descriptor.environments && - !descriptor.environments.includes(environment.name) - ) { - return; - } + // filter by environments + if ( + descriptor.environments && + !descriptor.environments.includes(environment.name) + ) { + return; + } - const rule = chain.module.rule(id); + const rule = chain.module.rule(id); - if (descriptor.test) { - rule.test(descriptor.test); - } - if (descriptor.resourceQuery) { - rule.resourceQuery(descriptor.resourceQuery); - } + if (descriptor.test) { + rule.test(descriptor.test); + } + if (descriptor.resourceQuery) { + rule.resourceQuery(descriptor.resourceQuery); + } - const loaderName = descriptor.raw - ? 'transformRawLoader.cjs' - : 'transformLoader.cjs'; - const loaderPath = join(LOADER_PATH, loaderName); + const loaderName = descriptor.raw + ? 'transformRawLoader.cjs' + : 'transformLoader.cjs'; + const loaderPath = join(LOADER_PATH, loaderName); - rule - .use(id) - .loader(loaderPath) - .options({ - id, - getEnvironment: () => environment, - } satisfies TransformLoaderOptions); - }, - ); - }; + rule + .use(id) + .loader(loaderPath) + .options({ + id, + getEnvironment: () => environment, + } satisfies TransformLoaderOptions); + }, + }); + }; - const setProcessAssets: ( - environmentMeta?: EnvironmentMeta, - ) => ProcessAssetsFn = (environmentMeta) => (descriptor, handler) => { - processAssetsFns.push({ environmentMeta, descriptor, handler }); - }; + const setProcessAssets: (environment?: string) => ProcessAssetsFn = + (environment) => (descriptor, handler) => { + processAssetsFns.push({ environment, descriptor, handler }); + }; process.on('exit', () => { hooks.onExit.call(); }); // Each plugin returns different APIs depending on the registered environment info. - return (environmentMeta?: { environment: string }) => ({ + return (environment?: string) => ({ context: publicContext, expose, - transform: getTransformFn(environmentMeta), + transform: getTransformFn(environment), useExposed, - processAssets: setProcessAssets(environmentMeta), + processAssets: setProcessAssets(environment), getRsbuildConfig, getNormalizedConfig, isPluginExists: pluginManager.isPluginExists, @@ -282,11 +280,35 @@ export function initPluginAPI({ onAfterStartProdServer: hooks.onAfterStartProdServer.tap, onBeforeStartProdServer: hooks.onBeforeStartProdServer.tap, modifyRsbuildConfig: hooks.modifyRsbuildConfig.tap, - modifyHTMLTags: hooks.modifyHTMLTags.tap(environmentMeta), - modifyBundlerChain: hooks.modifyBundlerChain.tap(environmentMeta), - modifyRspackConfig: hooks.modifyRspackConfig.tap(environmentMeta), - modifyWebpackChain: hooks.modifyWebpackChain.tap(environmentMeta), - modifyWebpackConfig: hooks.modifyWebpackConfig.tap(environmentMeta), - modifyEnvironmentConfig: hooks.modifyEnvironmentConfig.tap(environmentMeta), + modifyHTMLTags: (handler) => + hooks.modifyHTMLTags.tapEnvironment({ + environment, + handler, + }), + modifyBundlerChain: (handler) => + hooks.modifyBundlerChain.tapEnvironment({ + environment, + handler, + }), + modifyRspackConfig: (handler) => + hooks.modifyRspackConfig.tapEnvironment({ + environment, + handler, + }), + modifyWebpackChain: (handler) => + hooks.modifyWebpackChain.tapEnvironment({ + environment, + handler, + }), + modifyWebpackConfig: (handler) => + hooks.modifyWebpackConfig.tapEnvironment({ + environment, + handler, + }), + modifyEnvironmentConfig: (handler) => + hooks.modifyEnvironmentConfig.tapEnvironment({ + environment, + handler, + }), }); } diff --git a/packages/core/src/pluginManager.ts b/packages/core/src/pluginManager.ts index 73309413b2..26e87a355e 100644 --- a/packages/core/src/pluginManager.ts +++ b/packages/core/src/pluginManager.ts @@ -3,7 +3,6 @@ import { isFunction } from './helpers'; import { logger } from './logger'; import type { BundlerPluginInstance, - EnvironmentMeta, Falsy, PluginManager, PluginMeta, @@ -57,11 +56,11 @@ function validatePlugin(plugin: unknown) { export const RSBUILD_ALL_ENVIRONMENT_SYMBOL = 'RSBUILD_ALL_ENVIRONMENT_SYMBOL'; export const isPluginMatchEnvironment = ( - meta: { environment: string }, - environment: string, + metaEnvironment: string, + currentEnvironment: string, ): boolean => - meta.environment === environment || - meta.environment === RSBUILD_ALL_ENVIRONMENT_SYMBOL; + metaEnvironment === currentEnvironment || + metaEnvironment === RSBUILD_ALL_ENVIRONMENT_SYMBOL; export function createPluginManager(): PluginManager { let plugins: PluginMeta[] = []; @@ -138,7 +137,7 @@ export function createPluginManager(): PluginManager { plugins.find( (plugin) => plugin.instance.name === pluginName && - isPluginMatchEnvironment(plugin, options.environment), + isPluginMatchEnvironment(plugin.environment, options.environment), ), ); @@ -148,7 +147,9 @@ export function createPluginManager(): PluginManager { }, ) => { return plugins - .filter((p) => isPluginMatchEnvironment(p, options.environment)) + .filter((p) => + isPluginMatchEnvironment(p.environment, options.environment), + ) .map((p) => p.instance); }; return { @@ -234,7 +235,7 @@ export async function initPlugins({ getPluginAPI, pluginManager, }: { - getPluginAPI: (environmentMeta?: EnvironmentMeta) => RsbuildPluginAPI; + getPluginAPI: (environment?: string) => RsbuildPluginAPI; pluginManager: PluginManager; }): Promise { logger.debug('init plugins'); @@ -265,8 +266,8 @@ export async function initPlugins({ ) { continue; } - const { instance, ...environmentMeta } = plugin; - await instance.setup(getPluginAPI!(environmentMeta)); + const { instance, environment } = plugin; + await instance.setup(getPluginAPI!(environment)); } logger.debug('init plugins done'); diff --git a/packages/core/src/provider/initConfigs.ts b/packages/core/src/provider/initConfigs.ts index 35df1bf01b..617f9d16f4 100644 --- a/packages/core/src/provider/initConfigs.ts +++ b/packages/core/src/provider/initConfigs.ts @@ -40,14 +40,19 @@ async function modifyEnvironmentConfig( name: string, ) { logger.debug(`modify Rsbuild environment(${name}) config`); - const [modified] = await context.hooks.modifyEnvironmentConfig.call(name)( - config, - { - name, - mergeEnvironmentConfig: - mergeRsbuildConfig as ModifyEnvironmentConfigUtils['mergeEnvironmentConfig'], - }, - ); + + const [modified] = + await context.hooks.modifyEnvironmentConfig.callInEnvironment({ + environment: name, + args: [ + config, + { + name, + mergeEnvironmentConfig: + mergeRsbuildConfig as ModifyEnvironmentConfigUtils['mergeEnvironmentConfig'], + }, + ], + }); logger.debug(`modify Rsbuild environment(${name}) config done`); diff --git a/packages/core/src/provider/rspackConfig.ts b/packages/core/src/provider/rspackConfig.ts index e41461d79f..61ba01ec70 100644 --- a/packages/core/src/provider/rspackConfig.ts +++ b/packages/core/src/provider/rspackConfig.ts @@ -20,9 +20,11 @@ async function modifyRspackConfig( utils: ModifyRspackConfigUtils, ) { logger.debug('modify Rspack config'); - let [modifiedConfig] = await context.hooks.modifyRspackConfig.call( - utils.environment.name, - )(rspackConfig, utils); + let [modifiedConfig] = + await context.hooks.modifyRspackConfig.callInEnvironment({ + environment: utils.environment.name, + args: [rspackConfig, utils], + }); if (utils.environment.config.tools?.rspack) { modifiedConfig = await reduceConfigsAsyncWithContext({ diff --git a/packages/core/src/types/context.ts b/packages/core/src/types/context.ts index ebd6a131bd..b2da229d07 100644 --- a/packages/core/src/types/context.ts +++ b/packages/core/src/types/context.ts @@ -1,7 +1,7 @@ import type { Hooks } from '../initHooks'; import type { NormalizedConfig, RsbuildConfig } from './config'; import type { EnvironmentContext } from './hooks'; -import type { EnvironmentMeta, RsbuildPluginAPI } from './plugin'; +import type { RsbuildPluginAPI } from './plugin'; export type BundlerType = 'rspack' | 'webpack'; @@ -36,9 +36,10 @@ export type InternalContext = RsbuildContext & { normalizedConfig?: NormalizedConfig; /** * Get the plugin API. - * When environmentMeta is undefined, the global plugin API is returned, which can be used in all environments. + * + * When environment is undefined, the global plugin API is returned, which can be used in all environments. * */ - getPluginAPI?: (environmentMeta?: EnvironmentMeta) => RsbuildPluginAPI; + getPluginAPI?: (environment?: string) => RsbuildPluginAPI; /** The environment context. */ environments: Record; }; diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index f513b3eb4f..45311aeadf 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -45,17 +45,23 @@ export type HookDescriptor any> = { order: HookOrder; }; -export type EnvironmentMeta = { - environment: string; -}; - export type EnvironmentAsyncHook any> = { - tap: ( - environmentMeta?: EnvironmentMeta, - ) => (cb: Callback | HookDescriptor) => void; - call: ( - environment?: string, - ) => (...args: Parameters) => Promise>; + tapEnvironment: (params: { + /** + * Specify that the callback will only be executed under the specified environment + */ + environment?: string; + handler: Callback | HookDescriptor; + }) => void; + /** + * Triggered in all environments by default. + * If you need to specify the environment, please use `tapEnvironment` + */ + tap: (cb: Callback | HookDescriptor) => void; + callInEnvironment: (params: { + environment?: string; + args: Parameters; + }) => Promise>; }; export type AsyncHook any> = { @@ -103,34 +109,51 @@ export type ModifyWebpackConfigFn = ( utils: ModifyWebpackConfigUtils, ) => Promise | WebpackConfig | void; -export type PluginMeta = EnvironmentMeta & { +export type PluginMeta = { + environment: string; instance: RsbuildPlugin; }; export type PluginManager = { getPlugins: (options?: { - /** Get the plugins in the specified environment. If environment is not specified, get the global plugins. */ + /** + * Get the plugins in the specified environment. + * + * If environment is not specified, get the global plugins. + */ environment: string; }) => RsbuildPlugin[]; addPlugins: ( plugins: Array, options?: { before?: string; - /** Add a plugin for the specified environment. If environment is not specified, it will be registered as a global plugin (effective in all environments) */ + /** + * Add a plugin for the specified environment. + * + * If environment is not specified, it will be registered as a global plugin (effective in all environments) + */ environment?: string; }, ) => void; removePlugins: ( pluginNames: string[], options?: { - /** Remove the plugin in the specified environment. If environment is not specified, remove it in all environments. */ + /** + * Remove the plugin in the specified environment. + * + * If environment is not specified, remove it in all environments. + */ environment: string; }, ) => void; isPluginExists: ( pluginName: string, options?: { - /** Whether it exists in the specified environment. If environment is not specified, determine whether the plugin is a global plugin */ + /** + * Whether it exists in the specified environment. + * + * If environment is not specified, determine whether the plugin is a global plugin. + */ environment: string; }, ) => boolean; diff --git a/packages/core/tests/hooks.test.ts b/packages/core/tests/hooks.test.ts index f476416656..22e39ac984 100644 --- a/packages/core/tests/hooks.test.ts +++ b/packages/core/tests/hooks.test.ts @@ -10,19 +10,26 @@ describe('initHooks', () => { test('createEnvironmentAsyncHook should only works in specified environment', async () => { const logs: string[] = []; const hookA = createEnvironmentAsyncHook(); - hookA.tap()((msg) => { + hookA.tap((msg) => { logs.push(`[global] ${msg}`); }); - hookA.tap({ + hookA.tapEnvironment({ environment: 'a', - })((msg) => { - logs.push(msg); + handler: (msg) => { + logs.push(msg); + }, }); - await hookA.call('a')('call in a'); + await hookA.callInEnvironment({ + environment: 'a', + args: ['call in a'], + }); - await hookA.call('b')('call in b'); + await hookA.callInEnvironment({ + environment: 'b', + args: ['call in b'], + }); expect(logs).toEqual([ '[global] call in a',