Skip to content

perf: add tree shake markers to enable/disable capabilities to reduce bundle size #3704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
May 15, 2025
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3bcd4b2
feat(enhanced): manual Tree shake markers
ScriptedAlchemy Apr 16, 2025
f9b5ed9
chore: add docs about optimization
ScriptedAlchemy Apr 16, 2025
430cf40
chore: changesets
ScriptedAlchemy Apr 16, 2025
87d2bf3
fix(enhanced): refine target environment detection logic
ScriptedAlchemy Apr 16, 2025
65dff8e
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy Apr 16, 2025
0d21411
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy Apr 16, 2025
9c8d93e
Merge remote-tracking branch 'origin/perf/tree-shake-flags' into perf…
ScriptedAlchemy Apr 16, 2025
4ba61af
chore(chrome-devtools): add dependsOn to build target for dependency …
ScriptedAlchemy Apr 16, 2025
79094cd
chore: remove debug logs
ScriptedAlchemy Apr 16, 2025
eb01df9
chore: depednon devtools
ScriptedAlchemy Apr 16, 2025
03cb017
feat(rspack): inject new env constants via DefinePlugin for optimizat…
ScriptedAlchemy Apr 16, 2025
1ff85e9
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy Apr 17, 2025
5bbcca6
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy May 13, 2025
be54b58
refactor(enhanced): streamline ENV_TARGET determination logic
ScriptedAlchemy May 13, 2025
0dd1d3a
fix: only define ENV_TARGET and FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGI…
ScriptedAlchemy May 13, 2025
ea05c2c
Update ai-sleepy-cat.md
ScriptedAlchemy May 15, 2025
64ea9c1
Update ai-sleepy-lion.md
ScriptedAlchemy May 15, 2025
08ae524
Update ai-sleepy-lion.md
ScriptedAlchemy May 15, 2025
da7809b
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy May 15, 2025
e45311b
Merge branch 'main' into perf/tree-shake-flags
ScriptedAlchemy May 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/ai-calm-dog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@module-federation/enhanced": patch
---

Updated ModuleFederationPlugin to enhance configuration capabilities and target environment identification.

- Introduced `definePluginOptions` to manage DefinePlugin settings.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This format changeset can not be consumed . need to fix https://github.com/module-federation/core/actions/runs/15035515868/job/42256409157

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed there was ``` in another file that broke it

- 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`.
12 changes: 12 additions & 0 deletions .changeset/ai-sleepy-cat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@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.
12 changes: 12 additions & 0 deletions .changeset/ai-sleepy-lion.md
Original file line number Diff line number Diff line change
@@ -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`.
9 changes: 9 additions & 0 deletions .changeset/rspack-define-env-constants.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 42 additions & 1 deletion apps/website-new/docs/en/configure/experiments.mdx
Original file line number Diff line number Diff line change
@@ -10,7 +10,11 @@ new ModuleFederationPlugin({
experiments: {
asyncStartup: true,
externalRuntime: false,
provideExternalRuntime: false
provideExternalRuntime: false,
optimization: {
disableSnapshot: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks better to change it to "disable manifest"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also includes the inability to use the ts type

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but there are many users who come from webpack v1 - they want direct switch to v2 and dont use any new capability - first introduction to v2 is a v70kb increase in entry file - and many companies took advantage of small remote entry size of v1 where its like 10kb, they will load 80+ remote modules, but now they are 7X larger and remoteEntry becomes 5.7mb of eager chunk loading required to initialize the system. So these users would prefer to turn off features they do not yet adopt, then explore wha they need and slowly increase the payload size while communicating the values of the additional runtime code. Or if user is in production - they may want no json http call and prefer js entry with minimal payload overhead, so they can use lite federaiton in prod and full in lower environment if they needed to balance the overhead at the expense of capabilities.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any user not using json protocol has no ts sync already. So if user only uses js remote they already are on ~v1.5 level capabilities. So they have no requirement for the code and already do not leverage all of v2 features. Some user wants v1 + runtime plugin. to keep payload cost of adoption small but valuable to busniess case.

target: 'web',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this designed for other bundler like vite ? Becasue we can infer it from automatically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, will it automatically be apply in webpack/rspack ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not designed for other bundler - but i was worried about automatic inference because case like web worker or custom react native target, the infer target may not always be accurate - and you end up removing part of the system from a environment acting like another. Like in react native or lynx - it use commonjs chunk loader - but may be target of web worker or worker, so this may compile to web target but use other mechanics and assuming that may have breaking implications. Yes we can detect, but custom environment or mixed output could exist that i dont foresee right now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the feature will work in webpack and rspack, but currently user must opt into it explicitly - so if there is problem, they understand why because they enabled it. We could add a 'auto' option where we add attempt to infer, so user still must use flag to enable and we can add other enums to it if needed like react-native for custom env target code that we may need to support within the runtime core - without making the core larger - and if user want to tree shake the vanillia runtime themselves - they can just set these in define plugins of their own bundler and customize the library for their own needs as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha cool

},
},
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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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/cli --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/cli --staged && ./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",
8 changes: 7 additions & 1 deletion packages/chrome-devtools/project.json
Original file line number Diff line number Diff line change
@@ -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",
37 changes: 31 additions & 6 deletions packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
@@ -57,18 +57,43 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
}

private _patchBundlerConfig(compiler: Compiler): void {
const { name } = this._options;
const { name, experiments } = this._options;
const definePluginOptions: Record<string, string | boolean> = {};

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;

// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
if (
experiments?.optimization &&
typeof experiments.optimization === 'object' &&
experiments.optimization !== null &&
'target' in experiments.optimization
) {
const manualTarget = experiments.optimization.target as
| 'web'
| 'node'
| undefined;
// Ensure the target is one of the expected values before setting
if (manualTarget === 'web' || manualTarget === 'node') {
definePluginOptions['ENV_TARGET'] = JSON.stringify(manualTarget);
}
}
// No inference for ENV_TARGET. If not manually set and valid, it's not defined.

new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
}

/**
35 changes: 29 additions & 6 deletions packages/rspack/src/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
@@ -39,14 +39,37 @@ export class ModuleFederationPlugin implements RspackPluginInstance {
}

private _patchBundlerConfig(compiler: Compiler): void {
const { name } = this._options;
const { name, experiments } = this._options;
const definePluginOptions: Record<string, string | boolean> = {};
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;

// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
if (
experiments?.optimization &&
typeof experiments.optimization === 'object' &&
experiments.optimization !== null &&
'target' in experiments.optimization
) {
const manualTarget = experiments.optimization.target as
| 'web'
| 'node'
| undefined;
// Ensure the target is one of the expected values before setting
if (manualTarget === 'web' || manualTarget === 'node') {
definePluginOptions['ENV_TARGET'] = JSON.stringify(manualTarget);
}
}
// No inference for ENV_TARGET. If not manually set and valid, it's not defined.

new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
}

private _checkSingleton(compiler: Compiler): void {
15 changes: 13 additions & 2 deletions packages/runtime-core/src/core.ts
Original file line number Diff line number Diff line change
@@ -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; // Default to true (use snapshot) when not explicitly defined

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(),
@@ -328,7 +340,6 @@ export class FederationHost {
return res;
}, pluginRes || []);
}

registerRemotes(remotes: Remote[], options?: { force?: boolean }): void {
return this.remoteHandler.registerRemotes(remotes, options);
}
76 changes: 40 additions & 36 deletions packages/runtime-core/src/utils/load.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
302 changes: 175 additions & 127 deletions packages/sdk/src/node.ts
Original file line number Diff line number Diff line change
@@ -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<T>(name: string): Promise<T> {
if (!name) {
throw new Error('import specifier is required');
@@ -38,144 +41,189 @@ const lazyLoaderHookFetch = async (
return res;
};

export function createScriptNode(
url: string,
cb: (error?: Error, scriptContext?: any) => void,
attrs?: Record<string, any>,
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<typeof fetch> => {
if (loaderHook?.fetch) {
return (input: RequestInfo | URL, init?: RequestInit) =>
lazyLoaderHookFetch(input, init, loaderHook);
}
export const createScriptNode =
typeof ENV_TARGET === 'undefined' || ENV_TARGET !== 'web'
? (
url: string,
cb: (error?: Error, scriptContext?: any) => void,
attrs?: Record<string, any>,
loaderHook?: {
createScriptHook?: CreateScriptHookNode;
fetch?: FetchHook;
},
) => {
if (loaderHook?.createScriptHook) {
const hookResult = loaderHook.createScriptHook(url);
if (
hookResult &&
typeof hookResult === 'object' &&
'url' in hookResult
) {
url = hookResult.url;
}
}

return typeof fetch === 'undefined' ? loadNodeFetch() : fetch;
};
let urlObj: URL;
try {
urlObj = new URL(url);
} catch (e) {
console.error('Error constructing URL:', e);
cb(new Error(`Invalid URL: ${e}`));
return;
}

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<typeof import('path')>('path'),
importNodeModule<typeof import('vm')>('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,
},
);

script.runInThisContext()(
scriptContext.exports,
scriptContext.module,
eval('require'),
urlDirname,
filename,
);
const exportedInterface: Record<string, any> =
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;
}
const getFetch = async (): Promise<typeof fetch> => {
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<typeof import('path')>('path'),
importNodeModule<typeof import('vm')>('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,
},
);

cb(
undefined,
exportedInterface as keyof typeof scriptContext.module.exports,
);
} catch (e) {
cb(e instanceof Error ? e : new Error(`Script execution error: ${e}`));
}
};
script.runInThisContext()(
scriptContext.exports,
scriptContext.module,
eval('require'),
urlDirname,
filename,
);
const exportedInterface: Record<string, any> =
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;
}

getFetch()
.then(async (f) => {
if (attrs?.['type'] === 'esm' || attrs?.['type'] === 'module') {
return loadModule(urlObj.href, {
fetch: f,
vm: await importNodeModule<typeof import('vm')>('vm'),
})
.then(async (module) => {
await module.evaluate();
cb(undefined, module.namespace);
})
.catch((e) => {
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<typeof import('vm')>('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<string, any>;
loaderHook?: {
createScriptHook?: CreateScriptHookNode;
};
},
) {
return new Promise<void>((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<string, any>,
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<string, any>;
loaderHook?: {
createScriptHook?: CreateScriptHookNode;
};
},
) => {
return new Promise<void>((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<string, any>;
loaderHook?: {
createScriptHook?: CreateScriptHookNode;
};
},
) => {
throw new Error(
'loadScriptNode is disabled in non-Node.js environment',
);
};

async function loadModule(
url: string,
13 changes: 13 additions & 0 deletions packages/sdk/src/types/plugins/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
@@ -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?: {
/**