Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1b84bc8
feat(runtime): select capabilities through package conditions
ScriptedAlchemy Sep 18, 2026
7bc07b8
fix(repo): ignore package imports in dependency lint
ScriptedAlchemy Sep 18, 2026
b221eca
test(runtime): stop minifying selector bundles
ScriptedAlchemy Sep 21, 2026
9f98556
test(runtime): give real compiler selector tests a time budget
ScriptedAlchemy Sep 24, 2026
2158c52
fix(runtime): emit selector declarations without #mf imports
ScriptedAlchemy Sep 24, 2026
9533331
fix(sdk): keep the worker module import out of consumer bundles
ScriptedAlchemy Sep 24, 2026
af2ed4c
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 24, 2026
0e0df7b
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 24, 2026
a9f274b
fix(runtime-core): give disabled handlers the members callers use
ScriptedAlchemy Sep 24, 2026
f6d3faf
fix(sdk): register ESM remotes loaded by the worker platform loader
ScriptedAlchemy Sep 24, 2026
5298f6b
docs(changeset): describe the handler and worker selector changes
ScriptedAlchemy Sep 24, 2026
08c617c
test(runtime): run condition probes without inherited color
ScriptedAlchemy Sep 24, 2026
d9814c9
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 24, 2026
119fabf
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 25, 2026
a880a73
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 25, 2026
782bb57
refactor(runtime-core): drop the unused ResourceLoadContext import
ScriptedAlchemy Sep 25, 2026
fcb5c01
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 25, 2026
9fb0f2f
Merge branch 'rfc5036/02-compiler-coordination' into rfc5036/03-runti…
ScriptedAlchemy Sep 25, 2026
6b7dd3a
fix(runtime): preserve named core cjs declarations
ScriptedAlchemy Oct 3, 2026
5e122fb
Merge branch 'update/rfc5036-02' into update/rfc5036-03
ScriptedAlchemy Oct 3, 2026
7efeac0
Merge updated RFC5036 parent update/rfc5036-02
ScriptedAlchemy Oct 3, 2026
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
5 changes: 5 additions & 0 deletions .changeset/packed-runtime-core-declarations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/runtime': patch
---

Preserve named CommonJS declarations for the runtime core entry so strict TypeScript consumers can import its re-exported APIs.
13 changes: 13 additions & 0 deletions .changeset/runtime-capability-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@module-federation/runtime-core': minor
'@module-federation/webpack-bundler-runtime': minor
'@module-federation/sdk': minor
---

Add package-owned selectors for runtime capabilities and runtime targets.
Namespaced package conditions can select disabled modules without adding the
enabled implementations to the module graph. Default modules keep the existing
`FEDERATION_*` define behavior. Disabled handlers keep the hooks and methods
the runtime calls, and the worker loader stores an imported ESM remote under
its global name. Published declarations use the default module types and
contain no `#mf/*` imports, so `node10` resolution keeps working.
72 changes: 72 additions & 0 deletions packages/runtime-core/__tests__/disabled-handler-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import path from 'node:path';
import { runNodeWithConditions } from '../../../tools/testing/runNodeWithConditions';

const packageDir = path.resolve(__dirname, '..');

const REMOTE_DISABLED_MESSAGE =
'Remote loading is disabled by experiments.optimization.disableRemote.';

describe('disabled handler selectors', () => {
it('keeps the public entry loader working when remotes are disabled', () => {
expect(
runNodeWithConditions(
packageDir,
['module-federation:no-remote'],
"globalThis.FEDERATION_OPTIMIZE_NO_REMOTE = true; const { ModuleFederation, getRemoteEntry } = require('./dist/index.cjs'); const origin = new ModuleFederation({ name: 'host', remotes: [], plugins: [{ name: 'fallback-entry', loadEntry: () => ({ init() {}, get: () => () => 'fallback loaded' }) }] }); getRemoteEntry({ origin, remoteInfo: { name: 'fallback', entry: 'http://localhost/fallback.js', type: 'global', entryGlobalName: 'fallback', shareScope: 'default' } }).then((entry) => console.log(entry.get()()));",
),
).toBe('fallback loaded');
});

it('follows the FEDERATION_OPTIMIZE_NO_REMOTE define without a condition', () => {
const code = (value: string) =>
`globalThis.FEDERATION_OPTIMIZE_NO_REMOTE = ${value}; const { ModuleFederation } = require('./dist/index.cjs'); new ModuleFederation({ name: 'host', remotes: [] }).loadRemote('app/value').catch((error) => console.log(error.message.split('\\n')[0]));`;

expect(runNodeWithConditions(packageDir, [], code('true'))).toBe(
REMOTE_DISABLED_MESSAGE,
);
expect(runNodeWithConditions(packageDir, [], code('false'))).toContain(
'#RUNTIME-004',
);
expect(runNodeWithConditions(packageDir, [], code('undefined'))).toContain(
'#RUNTIME-004',
);
});

it('preserves the remote-disabled error through ModuleFederation', () => {
expect(
runNodeWithConditions(
packageDir,
['module-federation:no-remote'],
"const { ModuleFederation } = require('./dist/index.cjs'); new ModuleFederation({ name: 'host', remotes: [] }).loadRemote('app/value').catch((error) => console.log(error.message));",
),
).toBe(REMOTE_DISABLED_MESSAGE);
});

it('keeps share-scope storage and rejects shared loading', () => {
expect(
runNodeWithConditions(
packageDir,
['module-federation:no-shared'],
"const { ModuleFederation } = require('./dist/index.cjs'); const runtime = new ModuleFederation({ name: 'host', remotes: [] }); runtime.initShareScopeMap('custom', { react: {} }); runtime.loadShare('react').catch((error) => console.log(`${Object.keys(runtime.shareScopeMap).join(',')}:${error.message}`));",
),
).toBe(
'custom:Shared dependency loading is disabled by experiments.optimization.disableShared.',
);
});

it('removes default snapshot plugins only for the matching condition', () => {
const code =
"const { ModuleFederation } = require('./dist/index.cjs'); console.log(new ModuleFederation({ name: 'host', remotes: [] }).options.plugins.map((plugin) => plugin.name).join(','));";

expect(runNodeWithConditions(packageDir, [], code)).toBe(
'snapshot-plugin,generate-preload-assets-plugin',
);
expect(
runNodeWithConditions(
packageDir,
['module-federation:no-snapshot-plugins'],
code,
),
).toBe('');
});
});
174 changes: 174 additions & 0 deletions packages/runtime-core/__tests__/remote-module-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { TextDecoder, TextEncoder } from 'node:util';
import type webpack from 'webpack';

const packageDir = path.resolve(__dirname, '..');
type CompilerFactory = typeof webpack;
type StatsModule = {
name?: string;
identifier?: string;
modules?: StatsModule[];
};

function runNode(conditions: string[], code: string): string {
const output = execFileSync(
process.execPath,
[...conditions.map((condition) => `--conditions=${condition}`), '-e', code],
{ cwd: packageDir, encoding: 'utf8' },
);
return output.trim();
}

function compilerCases(): [string, CompilerFactory][] {
Object.defineProperties(globalThis, {
TextDecoder: { configurable: true, value: TextDecoder },
TextEncoder: { configurable: true, value: TextEncoder },
});
return [
[
'webpack',
createRequire(path.resolve(packageDir, '../enhanced/package.json'))(
'webpack',
) as CompilerFactory,
],
[
'rspack',
createRequire(path.resolve(packageDir, '../rspack/package.json'))(
'@rspack/core',
) as CompilerFactory,
],
];
}

function flattenModules(modules: StatsModule[]): StatsModule[] {
return modules.flatMap((module) => [
module,
...flattenModules(module.modules ?? []),
]);
}

function selectedModules(
compilerFactory: CompilerFactory,
name: string,
): Promise<string[]> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mf-selector-'));
fs.writeFileSync(
path.join(root, 'entry.js'),
'import { Module } from "@module-federation/runtime-core";\nexport default Module;\n',
);
return new Promise((resolve, reject) => {
const compiler = compilerFactory({
context: packageDir,
mode: 'none',
entry: path.join(root, 'entry.js'),
output: { path: path.join(root, name), filename: 'out.js' },
resolve: {
alias: {
'@module-federation/runtime-core': path.join(
packageDir,
'dist/index.js',
),
},
conditionNames: ['module-federation:no-remote', 'import', '...'],
},
optimization: {
minimize: false,
},
externals: [
(
{ request }: { request?: string },
callback: (error?: Error | null, result?: string) => void,
) => {
if (
request?.startsWith('@module-federation/') &&
request !== '@module-federation/runtime-core'
) {
callback(null, `commonjs ${request}`);
return;
}
callback();
},
],
});
compiler.run((error, stats) => {
compiler.close(() => undefined);
fs.rmSync(root, { recursive: true, force: true });
if (error) {
reject(error);
return;
}
const info = stats?.toJson({ modules: true });
if (stats?.hasErrors()) {
reject(new Error(info?.errors?.[0]?.message ?? 'compile failed'));
return;
}
const modules = flattenModules(
(info?.modules ?? []) as unknown as StatsModule[],
);
resolve(modules.map((module) => module.name ?? module.identifier ?? ''));
});
});
}

describe('remote module selector packaging', () => {
it('keeps the remote module available by default', () => {
expect(
runNode(
[],
"const { Module } = require('#mf/remote-module'); console.log(new Module({ remoteInfo: { name: 'remote' }, host: {} }).remoteInfo.name)",
),
).toBe('remote');
});

it('exports the unavailable module for the no-remote condition', () => {
expect(
runNode(
['module-federation:no-remote'],
"const { Module } = require('#mf/remote-module'); try { new Module() } catch (error) { console.log(error.message) }",
),
).toBe(
'Remote loading is disabled by experiments.optimization.disableRemote.',
);
});

it('lets webpack and rspack select the disabled leaf by condition name', async () => {
const selected = await Promise.all(
compilerCases().map(([name, compiler]) =>
selectedModules(compiler, name),
),
);
for (const modules of selected) {
expect(modules).toContain('./dist/selectors/remote-module/disabled.js');
expect(modules).toContain('./dist/remote/disabled.js');
expect(modules).not.toContain('./dist/selectors/remote-module/legacy.js');
expect(modules).not.toContain('./dist/module/index.js');
}
}, 60_000);

it('loads the enabled leaf in both module formats', () => {
const cjsPath = path.join(
packageDir,
'dist/selectors/remote-module/enabled.cjs',
);
const esmUrl = pathToFileURL(
path.join(packageDir, 'dist/selectors/remote-module/enabled.js'),
).href;
expect(
runNode(
[],
`const { Module } = require(${JSON.stringify(cjsPath)}); console.log(new Module({ remoteInfo: { name: 'remote' }, host: {} }).remoteInfo.name)`,
),
).toBe('remote');
expect(
runNode(
[],
`import(${JSON.stringify(esmUrl)}).then(({ Module }) => console.log(new Module({ remoteInfo: { name: 'remote' }, host: {} }).remoteInfo.name))`,
),
).toBe('remote');
});
});
16 changes: 16 additions & 0 deletions packages/runtime-core/__tests__/worker-remote-entry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import path from 'node:path';
import { runNodeWithConditions } from '../../../tools/testing/runNodeWithConditions';

const packageDir = path.resolve(__dirname, '..');

describe('worker remote entry', () => {
it('loads an ESM remote entry through the worker platform loader', () => {
expect(
runNodeWithConditions(
packageDir,
['module-federation:target-worker'],
"const { ModuleFederation, getRemoteEntry } = require('./dist/index.cjs'); const origin = new ModuleFederation({ name: 'host', remotes: [] }); getRemoteEntry({ origin, remoteInfo: { name: 'worker-remote', entry: 'data:text/javascript,export const init = () => {}; export const get = () => () => 42;', type: 'module', entryGlobalName: 'workerRemote', shareScope: 'default' } }).then((entry) => console.log(entry.get()()), (error) => console.log(error.message));",
),
).toBe('42');
});
});
Loading
Loading