From 3bcd4b245c79d9b8e2d1626fe996d94586c13425 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 15 Apr 2025 23:19:53 -0700 Subject: [PATCH 1/8] feat(enhanced): manual Tree shake markers --- .../lib/container/ModuleFederationPlugin.ts | 41 ++- packages/runtime-core/src/core.ts | 14 +- packages/runtime-core/src/utils/load.ts | 76 ++--- packages/sdk/src/node.ts | 305 ++++++++++-------- .../types/plugins/ModuleFederationPlugin.ts | 13 + 5 files changed, 278 insertions(+), 171 deletions(-) diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts index a34720cdaab..f871b693178 100644 --- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts @@ -57,18 +57,47 @@ class ModuleFederationPlugin implements WebpackPluginInstance { } private _patchBundlerConfig(compiler: Compiler): void { - const { name } = this._options; + const { name, experiments } = this._options; + const definePluginOptions: Record = {}; + const MFPluginNum = compiler.options.plugins.filter( (p): p is WebpackPluginInstance => !!p && (p as any).name === 'ModuleFederationPlugin', ).length; + if (name && MFPluginNum < 2) { - new compiler.webpack.DefinePlugin({ - FEDERATION_BUILD_IDENTIFIER: JSON.stringify( - composeKeyWithSeparator(name, utils.getBuildVersion()), - ), - }).apply(compiler); + definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify( + composeKeyWithSeparator(name, utils.getBuildVersion()), + ); } + + const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false; + definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] = + disableSnapshot; + + let targetEnv: 'web' | 'node'; + if (experiments?.optimization?.target) { + targetEnv = experiments.optimization.target; + } else { + targetEnv = 'web'; + const webpackTarget = compiler.options.target; + if (typeof webpackTarget === 'string') { + if (webpackTarget.startsWith('node')) { + targetEnv = 'node'; + } + } else if (Array.isArray(webpackTarget)) { + if ( + webpackTarget.some( + (t) => typeof t === 'string' && t.startsWith('node'), + ) + ) { + targetEnv = 'node'; + } + } + } + definePluginOptions['ENV_TARGET'] = JSON.stringify(targetEnv); + + new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler); } /** diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index 6ed436b1bb1..c8398cf5fbc 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -35,6 +35,15 @@ import { SharedHandler } from './shared'; import { RemoteHandler } from './remote'; import { formatShareConfigs } from './utils/share'; +// Declare the global constant that will be defined by DefinePlugin +// Default to true if not defined (e.g., when runtime-core is used outside of webpack) +// so that snapshot functionality is included by default. +declare const FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN: boolean; +const USE_SNAPSHOT = + typeof FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN === 'boolean' + ? !FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN + : true; + export class FederationHost { options: Options; hooks = new PluginSystem({ @@ -160,12 +169,15 @@ export class FederationHost { }); constructor(userOptions: UserOptions) { + const plugins = USE_SNAPSHOT + ? [snapshotPlugin(), generatePreloadAssetsPlugin()] + : []; // TODO: Validate the details of the options // Initialize options with default values const defaultOptions: Options = { id: getBuilderId(), name: userOptions.name, - plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()], + plugins, remotes: [], shared: {}, inBrowser: isBrowserEnv(), diff --git a/packages/runtime-core/src/utils/load.ts b/packages/runtime-core/src/utils/load.ts index f26b74a5c9b..50cc67bb236 100644 --- a/packages/runtime-core/src/utils/load.ts +++ b/packages/runtime-core/src/utils/load.ts @@ -16,6 +16,10 @@ import { runtimeDescMap, } from '@module-federation/error-codes'; +// Declare the ENV_TARGET constant that will be defined by DefinePlugin +declare const ENV_TARGET: 'web' | 'node'; +const importCallback = '.then(callbacks[0]).catch(callbacks[1])'; + async function loadEsmEntry({ entry, remoteEntryExports, @@ -27,10 +31,10 @@ async function loadEsmEntry({ try { if (!remoteEntryExports) { if (typeof FEDERATION_ALLOW_NEW_FUNCTION !== 'undefined') { - new Function( - 'callbacks', - `import("${entry}").then(callbacks[0]).catch(callbacks[1])`, - )([resolve, reject]); + new Function('callbacks', `import("${entry}")${importCallback}`)([ + resolve, + reject, + ]); } else { import(/* webpackIgnore: true */ /* @vite-ignore */ entry) .then(resolve) @@ -62,7 +66,7 @@ async function loadSystemJsEntry({ } else { new Function( 'callbacks', - `System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`, + `System.import("${entry}")${importCallback}`, )([resolve, reject]); } } else { @@ -74,6 +78,28 @@ async function loadSystemJsEntry({ }); } +function handleRemoteEntryLoaded( + name: string, + globalName: string, + entry: string, +): RemoteEntryExports { + const { remoteEntryKey, entryExports } = getRemoteEntryExports( + name, + globalName, + ); + + assert( + entryExports, + getShortErrorMsg(RUNTIME_001, runtimeDescMap, { + remoteName: name, + remoteEntryUrl: entry, + remoteEntryKey, + }), + ); + + return entryExports; +} + async function loadEntryScript({ name, globalName, @@ -113,21 +139,7 @@ async function loadEntryScript({ }, }) .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - getShortErrorMsg(RUNTIME_001, runtimeDescMap, { - remoteName: name, - remoteEntryUrl: entry, - remoteEntryKey, - }), - ); - - return entryExports; + return handleRemoteEntryLoaded(name, globalName, entry); }) .catch((e) => { assert( @@ -196,21 +208,7 @@ async function loadEntryNode({ }, }) .then(() => { - const { remoteEntryKey, entryExports } = getRemoteEntryExports( - name, - globalName, - ); - - assert( - entryExports, - getShortErrorMsg(RUNTIME_001, runtimeDescMap, { - remoteName: name, - remoteEntryUrl: entry, - remoteEntryKey, - }), - ); - - return entryExports; + return handleRemoteEntryLoaded(name, globalName, entry); }) .catch((e) => { throw e; @@ -250,7 +248,13 @@ export async function getRemoteEntry({ if (res) { return res; } - return isBrowserEnv() + // Use ENV_TARGET if defined, otherwise fallback to isBrowserEnv, must keep this + const isWebEnvironment = + typeof ENV_TARGET !== 'undefined' + ? ENV_TARGET === 'web' + : isBrowserEnv(); + + return isWebEnvironment ? loadEntryDom({ remoteInfo, remoteEntryExports, loaderHook }) : loadEntryNode({ remoteInfo, loaderHook }); }); diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index d955b21eb00..a72a4b05a86 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -1,5 +1,8 @@ import { CreateScriptHookNode, FetchHook } from './types'; +// Declare the ENV_TARGET constant that will be defined by DefinePlugin +declare const ENV_TARGET: 'web' | 'node'; + function importNodeModule(name: string): Promise { if (!name) { throw new Error('import specifier is required'); @@ -38,144 +41,190 @@ const lazyLoaderHookFetch = async ( return res; }; -export function createScriptNode( - url: string, - cb: (error?: Error, scriptContext?: any) => void, - attrs?: Record, - loaderHook?: { - createScriptHook?: CreateScriptHookNode; - fetch?: FetchHook; - }, -) { - if (loaderHook?.createScriptHook) { - const hookResult = loaderHook.createScriptHook(url); - if (hookResult && typeof hookResult === 'object' && 'url' in hookResult) { - url = hookResult.url; - } - } - - let urlObj: URL; - try { - urlObj = new URL(url); - } catch (e) { - console.error('Error constructing URL:', e); - cb(new Error(`Invalid URL: ${e}`)); - return; - } - - const getFetch = async (): Promise => { - if (loaderHook?.fetch) { - return (input: RequestInfo | URL, init?: RequestInit) => - lazyLoaderHookFetch(input, init, loaderHook); - } - - return typeof fetch === 'undefined' ? loadNodeFetch() : fetch; - }; - - const handleScriptFetch = async (f: typeof fetch, urlObj: URL) => { - try { - const res = await f(urlObj.href); - const data = await res.text(); - const [path, vm] = await Promise.all([ - importNodeModule('path'), - importNodeModule('vm'), - ]); - - const scriptContext = { exports: {}, module: { exports: {} } }; - const urlDirname = urlObj.pathname.split('/').slice(0, -1).join('/'); - const filename = path.basename(urlObj.pathname); - - const script = new vm.Script( - `(function(exports, module, require, __dirname, __filename) {${data}\n})`, - { - filename, - importModuleDynamically: - //@ts-ignore - vm.constants?.USE_MAIN_CONTEXT_DEFAULT_LOADER ?? importNodeModule, +export const createScriptNode = + typeof ENV_TARGET !== 'undefined' && ENV_TARGET !== 'web' + ? ( + url: string, + cb: (error?: Error, scriptContext?: any) => void, + attrs?: Record, + loaderHook?: { + createScriptHook?: CreateScriptHookNode; + fetch?: FetchHook; }, - ); - - script.runInThisContext()( - scriptContext.exports, - scriptContext.module, - eval('require'), - urlDirname, - filename, - ); - const exportedInterface: Record = - scriptContext.module.exports || scriptContext.exports; - - if (attrs && exportedInterface && attrs['globalName']) { - const container = - exportedInterface[attrs['globalName']] || exportedInterface; - cb(undefined, container as keyof typeof scriptContext.module.exports); - return; - } + ) => { + if (loaderHook?.createScriptHook) { + const hookResult = loaderHook.createScriptHook(url); + if ( + hookResult && + typeof hookResult === 'object' && + 'url' in hookResult + ) { + url = hookResult.url; + } + } + console.log('zack was here'); + + let urlObj: URL; + try { + urlObj = new URL(url); + } catch (e) { + console.error('Error constructing URL:', e); + cb(new Error(`Invalid URL: ${e}`)); + return; + } - cb( - undefined, - exportedInterface as keyof typeof scriptContext.module.exports, - ); - } catch (e) { - cb(e instanceof Error ? e : new Error(`Script execution error: ${e}`)); - } - }; + const getFetch = async (): Promise => { + if (loaderHook?.fetch) { + return (input: RequestInfo | URL, init?: RequestInit) => + lazyLoaderHookFetch(input, init, loaderHook); + } + + return typeof fetch === 'undefined' ? loadNodeFetch() : fetch; + }; + + const handleScriptFetch = async (f: typeof fetch, urlObj: URL) => { + try { + const res = await f(urlObj.href); + const data = await res.text(); + const [path, vm] = await Promise.all([ + importNodeModule('path'), + importNodeModule('vm'), + ]); + + const scriptContext = { exports: {}, module: { exports: {} } }; + const urlDirname = urlObj.pathname + .split('/') + .slice(0, -1) + .join('/'); + const filename = path.basename(urlObj.pathname); + + const script = new vm.Script( + `(function(exports, module, require, __dirname, __filename) {${data}\n})`, + { + filename, + importModuleDynamically: + //@ts-ignore + vm.constants?.USE_MAIN_CONTEXT_DEFAULT_LOADER ?? + importNodeModule, + }, + ); - getFetch() - .then(async (f) => { - if (attrs?.['type'] === 'esm' || attrs?.['type'] === 'module') { - return loadModule(urlObj.href, { - fetch: f, - vm: await importNodeModule('vm'), - }) - .then(async (module) => { - await module.evaluate(); - cb(undefined, module.namespace); - }) - .catch((e) => { + script.runInThisContext()( + scriptContext.exports, + scriptContext.module, + eval('require'), + urlDirname, + filename, + ); + const exportedInterface: Record = + scriptContext.module.exports || scriptContext.exports; + + if (attrs && exportedInterface && attrs['globalName']) { + const container = + exportedInterface[attrs['globalName']] || exportedInterface; + cb( + undefined, + container as keyof typeof scriptContext.module.exports, + ); + return; + } + + cb( + undefined, + exportedInterface as keyof typeof scriptContext.module.exports, + ); + } catch (e) { cb( e instanceof Error ? e : new Error(`Script execution error: ${e}`), ); + } + }; + + getFetch() + .then(async (f) => { + if (attrs?.['type'] === 'esm' || attrs?.['type'] === 'module') { + return loadModule(urlObj.href, { + fetch: f, + vm: await importNodeModule('vm'), + }) + .then(async (module) => { + await module.evaluate(); + cb(undefined, module.namespace); + }) + .catch((e) => { + cb( + e instanceof Error + ? e + : new Error(`Script execution error: ${e}`), + ); + }); + } + handleScriptFetch(f, urlObj); + }) + .catch((err) => { + cb(err); }); } - handleScriptFetch(f, urlObj); - }) - .catch((err) => { - cb(err); - }); -} - -export function loadScriptNode( - url: string, - info: { - attrs?: Record; - loaderHook?: { - createScriptHook?: CreateScriptHookNode; - }; - }, -) { - return new Promise((resolve, reject) => { - createScriptNode( - url, - (error, scriptContext) => { - if (error) { - reject(error); - } else { - const remoteEntryKey = - info?.attrs?.['globalName'] || - `__FEDERATION_${info?.attrs?.['name']}:custom__`; - const entryExports = ((globalThis as any)[remoteEntryKey] = - scriptContext); - resolve(entryExports); - } - }, - info.attrs, - info.loaderHook, - ); - }); -} + : ( + url: string, + cb: (error?: Error, scriptContext?: any) => void, + attrs?: Record, + loaderHook?: { + createScriptHook?: CreateScriptHookNode; + fetch?: FetchHook; + }, + ) => { + cb( + new Error('createScriptNode is disabled in non-Node.js environment'), + ); + }; + +export const loadScriptNode = + typeof ENV_TARGET !== 'undefined' && ENV_TARGET !== 'web' + ? ( + url: string, + info: { + attrs?: Record; + loaderHook?: { + createScriptHook?: CreateScriptHookNode; + }; + }, + ) => { + return new Promise((resolve, reject) => { + createScriptNode( + url, + (error, scriptContext) => { + if (error) { + reject(error); + } else { + const remoteEntryKey = + info?.attrs?.['globalName'] || + `__FEDERATION_${info?.attrs?.['name']}:custom__`; + const entryExports = ((globalThis as any)[remoteEntryKey] = + scriptContext); + resolve(entryExports); + } + }, + info.attrs, + info.loaderHook, + ); + }); + } + : ( + url: string, + info: { + attrs?: Record; + loaderHook?: { + createScriptHook?: CreateScriptHookNode; + }; + }, + ) => { + throw new Error( + 'loadScriptNode is disabled in non-Node.js environment', + ); + }; async function loadModule( url: string, diff --git a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts index 6a73b0d038f..597f3fdcc0c 100644 --- a/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts +++ b/packages/sdk/src/types/plugins/ModuleFederationPlugin.ts @@ -254,6 +254,19 @@ export interface ModuleFederationPluginOptions { externalRuntime?: boolean; provideExternalRuntime?: boolean; asyncStartup?: boolean; + /** + * Options related to build optimizations. + */ + optimization?: { + /** + * Enable optimization to skip snapshot plugin + */ + disableSnapshot?: boolean; + /** + * Target environment for the build + */ + target?: 'web' | 'node'; + }; }; bridge?: { /** From f9b5ed950aa4fe9d86f3282f4a20ab1a9451f97c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 00:38:09 -0700 Subject: [PATCH 2/8] chore: add docs about optimization --- .../docs/en/configure/experiments.mdx | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/website-new/docs/en/configure/experiments.mdx b/apps/website-new/docs/en/configure/experiments.mdx index 190302a504d..920434c4140 100644 --- a/apps/website-new/docs/en/configure/experiments.mdx +++ b/apps/website-new/docs/en/configure/experiments.mdx @@ -10,7 +10,11 @@ new ModuleFederationPlugin({ experiments: { asyncStartup: true, externalRuntime: false, - provideExternalRuntime: false + provideExternalRuntime: false, + optimization: { + disableSnapshot: false, + target: 'web', + }, }, shared: { react: { @@ -59,3 +63,40 @@ Make sure to only configure it on the topmost consumer! If multiple consumers in ::: Setting `true` will inject the MF runtime at the consumer. + +## optimization + +This object contains flags related to build-time optimizations that can affect the Module Federation runtime's size and behavior. + +### disableSnapshot + +- Type: `boolean` +- Required: No +- Default: `false` + +When set to `true`, this option defines the `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` global constant as `true` during the build. In the `@module-federation/runtime-core`, this prevents the `snapshotPlugin()` and `generatePreloadAssetsPlugin()` from being included and initialized within the FederationHost. + +**Impact:** +* **Benefit:** Can reduce the overall bundle size of the Module Federation runtime by excluding the code for these two plugins. +* **Cost:** Disables the functionality provided by these plugins. The `snapshotPlugin` is crucial for the "mf-manifest protocol" – it's responsible for generating or providing runtime access to a build manifest (e.g., `mf-manifest.json`) containing metadata about exposed modules, shared dependencies, versions, and remotes. Disabling it means: + * The runtime loses access to this build manifest data. + * Features relying on the manifest, such as dynamic remote discovery, manifest-based version compatibility checks, advanced asset preloading (also handled by the removed `generatePreloadAssetsPlugin`), and potentially runtime debugging/introspection tools, will not function correctly or will be unavailable. + * Use this option only if you do not rely on these manifest-driven features and prioritize a minimal runtime footprint. + +### target + +- Type: `'web' | 'node'` +- Required: No +- Default: Inferred from Webpack's `target` option (usually `'web'`) + +This option defines the `ENV_TARGET` global constant during the build, specifying the intended execution environment. + +**Impact:** +* **`target: 'web'`**: Optimizes the build for browser environments. + * Ensures browser-specific remote entry loading mechanisms are used (`loadEntryDom`). + * Crucially, enables tree-shaking/dead-code elimination for Node.js-specific code within the `@module-federation/sdk`. Functions like `createScriptNode` and `loadScriptNode`, along with their required Node.js built-in modules (e.g., `vm`, `path`, `http`), are completely removed from the bundle, significantly reducing its size. +* **`target: 'node'`**: Optimizes the build for Node.js environments. + * Ensures Node.js-specific remote entry loading mechanisms are used (`loadEntryNode`). + * Includes the necessary Node.js-specific functions from the SDK (`createScriptNode`, `loadScriptNode`) in the bundle, allowing the federated application to function correctly in Node.js. + +Explicitly setting this value is recommended to ensure the intended optimizations are applied, especially in universal or server-side rendering scenarios. From 430cf40f696c1a6f22cfecde78260d389b45131f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 00:42:52 -0700 Subject: [PATCH 3/8] chore: changesets --- .changeset/ai-calm-dog.md | 10 ++++++++++ .changeset/ai-sleepy-cat.md | 13 +++++++++++++ .changeset/ai-sleepy-lion.md | 12 ++++++++++++ package.json | 2 +- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .changeset/ai-calm-dog.md create mode 100644 .changeset/ai-sleepy-cat.md create mode 100644 .changeset/ai-sleepy-lion.md diff --git a/.changeset/ai-calm-dog.md b/.changeset/ai-calm-dog.md new file mode 100644 index 00000000000..369d66284f7 --- /dev/null +++ b/.changeset/ai-calm-dog.md @@ -0,0 +1,10 @@ +--- +"@module-federation/enhanced": patch +--- + +Updated ModuleFederationPlugin to enhance configuration capabilities and target environment identification. + +- Introduced `definePluginOptions` to manage DefinePlugin settings. +- Added `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` to handle disabling of snapshot optimizations via experiments. +- Implemented environment target detection (`web` or `node`) based on compiler options and experiments. +- Consolidated DefinePlugin application with the newly constructed `definePluginOptions`. \ No newline at end of file diff --git a/.changeset/ai-sleepy-cat.md b/.changeset/ai-sleepy-cat.md new file mode 100644 index 00000000000..ed76d938485 --- /dev/null +++ b/.changeset/ai-sleepy-cat.md @@ -0,0 +1,13 @@ +--- +"@module-federation/sdk": patch +--- + +Introduced environment-specific handling for `createScriptNode` and `loadScriptNode` functions and added build optimization options. + +- Declared `ENV_TARGET` constant to differentiate between 'web' and 'node' environments. +- Modified `createScriptNode` and `loadScriptNode` to execute only in Node.js environment. + - Throws an error if attempted in a non-Node.js environment. +- Added logging for debugging purposes. +- Introduced `optimization` options in `ModuleFederationPluginOptions`. + - Added config for `disableSnapshot` and `target` environment optimizations. +``` \ No newline at end of file diff --git a/.changeset/ai-sleepy-lion.md b/.changeset/ai-sleepy-lion.md new file mode 100644 index 00000000000..aafaf7b6839 --- /dev/null +++ b/.changeset/ai-sleepy-lion.md @@ -0,0 +1,12 @@ +"@module-federation/runtime-core": patch +--- + +Add conditional functionality for snapshots and optimize entry loading. + +- Introduced FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN constant to control snapshot functionality. + - Default to include snapshot functionality if constant is not defined. +- Simplified plugin loading logic to check USE_SNAPSHOT flag. +- Added ENV_TARGET constant to differentiate between web and node environments. +- Extracted duplicated logic for handling remote entry loaded into `handleRemoteEntryLoaded` function. +- Refactored entry loading to use conditional environment checks with `ENV_TARGET`. +``` \ No newline at end of file diff --git a/package.json b/package.json index ff64508d7ce..b953ed4a85f 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "prepare": "husky install", "changeset": "changeset", "build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:type:pkg'", - "changegen": "./changeset-gen.js --path ./packages/enhanced --staged && ./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged && ./changeset-gen.js --path ./packages/dts-plugin --staged", + "changegen": "./changeset-gen.js --path ./packages/runtime && ./changeset-gen.js --path ./packages/runtime-core && ./changeset-gen.js --path ./packages/sdk && ./changeset-gen.js --path ./packages/enhanced && ./changeset-gen.js --path ./packages/node && ./changeset-gen.js --path ./packages/data-prefetch && ./changeset-gen.js --path ./packages/nextjs-mf && ./changeset-gen.js --path ./packages/dts-plugin", "commitgen:staged": "./commit-gen.js --path ./packages --staged", "commitgen:main": "./commit-gen.js --path ./packages", "changeset:status": "changeset status", From 87d2bf34604c4217002c446a442784e8523e6cd8 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 02:26:52 -0700 Subject: [PATCH 4/8] fix(enhanced): refine target environment detection logic --- .../src/lib/container/ModuleFederationPlugin.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts index f871b693178..2a8a233ab38 100644 --- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts @@ -75,21 +75,19 @@ class ModuleFederationPlugin implements WebpackPluginInstance { definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] = disableSnapshot; - let targetEnv: 'web' | 'node'; - if (experiments?.optimization?.target) { + let targetEnv: 'web' | 'node' | undefined; + if (experiments?.optimization && 'target' in experiments.optimization) { targetEnv = experiments.optimization.target; } else { targetEnv = 'web'; const webpackTarget = compiler.options.target; if (typeof webpackTarget === 'string') { - if (webpackTarget.startsWith('node')) { + if (webpackTarget.includes('node')) { targetEnv = 'node'; } } else if (Array.isArray(webpackTarget)) { if ( - webpackTarget.some( - (t) => typeof t === 'string' && t.startsWith('node'), - ) + webpackTarget.some((t) => typeof t === 'string' && t.includes('node')) ) { targetEnv = 'node'; } From 4ba61afbb66f5179c5073b9e08b6159e2731a864 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 12:50:07 -0700 Subject: [PATCH 5/8] chore(chrome-devtools): add dependsOn to build target for dependency builds --- packages/chrome-devtools/project.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/chrome-devtools/project.json b/packages/chrome-devtools/project.json index 1ed9f5ce85e..b4be33ce8bb 100644 --- a/packages/chrome-devtools/project.json +++ b/packages/chrome-devtools/project.json @@ -9,7 +9,13 @@ "executor": "nx:run-commands", "options": { "commands": ["npm run build:lib --prefix packages/chrome-devtools"] - } + }, + "dependsOn": [ + { + "target": "build", + "projects": "dependencies" + } + ] }, "build:chrome-plugins": { "executor": "nx:run-commands", From 79094cd21ce42fc2fd2762b0a62f890600183925 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 13:00:42 -0700 Subject: [PATCH 6/8] chore: remove debug logs --- packages/sdk/src/node.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index a72a4b05a86..b0060ff448f 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -62,7 +62,6 @@ export const createScriptNode = url = hookResult.url; } } - console.log('zack was here'); let urlObj: URL; try { From eb01df9fa294c4e1d56972042998c0e9ce418bcf Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 13:12:25 -0700 Subject: [PATCH 7/8] chore: depednon devtools --- packages/chrome-devtools/project.json | 10 ++++++++-- packages/runtime-core/src/core.ts | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/chrome-devtools/project.json b/packages/chrome-devtools/project.json index b4be33ce8bb..be8f4781833 100644 --- a/packages/chrome-devtools/project.json +++ b/packages/chrome-devtools/project.json @@ -13,7 +13,7 @@ "dependsOn": [ { "target": "build", - "projects": "dependencies" + "dependencies": true } ] }, @@ -21,7 +21,13 @@ "executor": "nx:run-commands", "options": { "commands": ["npm run build --prefix packages/chrome-devtools"] - } + }, + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ] }, "test": { "executor": "nx:run-commands", diff --git a/packages/runtime-core/src/core.ts b/packages/runtime-core/src/core.ts index c8398cf5fbc..4e3f02fdda3 100644 --- a/packages/runtime-core/src/core.ts +++ b/packages/runtime-core/src/core.ts @@ -340,7 +340,6 @@ export class FederationHost { return res; }, pluginRes || []); } - registerRemotes(remotes: Remote[], options?: { force?: boolean }): void { return this.remoteHandler.registerRemotes(remotes, options); } From 03cb0173e2d64a4d968ff1545f9e319f7d073dcc Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Apr 2025 13:40:34 -0700 Subject: [PATCH 8/8] feat(rspack): inject new env constants via DefinePlugin for optimization parity Adds FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN and ENV_TARGET support. Enables new optimization and environment targeting options. Ensures parity with webpack plugin. Enables runtime feature toggling. Enables tree-shaking in runtime and SDK. --- .changeset/rspack-define-env-constants.md | 9 +++++ packages/rspack/src/ModuleFederationPlugin.ts | 38 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 .changeset/rspack-define-env-constants.md diff --git a/.changeset/rspack-define-env-constants.md b/.changeset/rspack-define-env-constants.md new file mode 100644 index 00000000000..91e2a9cf84d --- /dev/null +++ b/.changeset/rspack-define-env-constants.md @@ -0,0 +1,9 @@ +--- +"@module-federation/rspack": patch +--- + +Update Rspack ModuleFederationPlugin to support enhanced configuration capabilities and environment targeting. + +- Injects `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` and `ENV_TARGET` as global constants using DefinePlugin, based on the new `experiments.optimization` options. +- Ensures parity with the Webpack plugin for build-time optimizations and environment-specific code paths. +- Enables tree-shaking and feature toggling in the runtime and SDK for both Rspack and Webpack builds. diff --git a/packages/rspack/src/ModuleFederationPlugin.ts b/packages/rspack/src/ModuleFederationPlugin.ts index 4d1b42d4b8c..1e2bd1587e6 100644 --- a/packages/rspack/src/ModuleFederationPlugin.ts +++ b/packages/rspack/src/ModuleFederationPlugin.ts @@ -37,14 +37,40 @@ export class ModuleFederationPlugin implements RspackPluginInstance { } private _patchBundlerConfig(compiler: Compiler): void { - const { name } = this._options; + const { name, experiments } = this._options; + const definePluginOptions: Record = {}; if (name) { - new compiler.webpack.DefinePlugin({ - FEDERATION_BUILD_IDENTIFIER: JSON.stringify( - composeKeyWithSeparator(name, utils.getBuildVersion()), - ), - }).apply(compiler); + definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify( + composeKeyWithSeparator(name, utils.getBuildVersion()), + ); } + // Add FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN + const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false; + definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] = + disableSnapshot; + + // Add ENV_TARGET + let targetEnv: 'web' | 'node' | undefined; + if (experiments?.optimization && 'target' in experiments.optimization) { + targetEnv = experiments.optimization.target; + } else { + targetEnv = 'web'; + const rspackTarget = compiler.options.target; + if (typeof rspackTarget === 'string') { + if (rspackTarget.includes('node')) { + targetEnv = 'node'; + } + } else if (Array.isArray(rspackTarget)) { + if ( + rspackTarget.some((t) => typeof t === 'string' && t.includes('node')) + ) { + targetEnv = 'node'; + } + } + } + definePluginOptions['ENV_TARGET'] = JSON.stringify(targetEnv); + + new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler); } private _checkSingleton(compiler: Compiler): void {