Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6a67970
fix(enhanced): preserve shared implementations across scalar scopes
ScriptedAlchemy Sep 14, 2026
4db5927
test(enhanced): cover distinct scopes in a common layer
ScriptedAlchemy Sep 14, 2026
cb742f5
Merge commit 'b69c99fbdf966fdbf36bb7ad07d7a1639f06a272' into fix/scal…
ScriptedAlchemy Sep 14, 2026
d8844a6
Merge commit 'b830b22456c1ad6ad043ad3fcf5af2522cc7deeb' into fix/scal…
ScriptedAlchemy Sep 15, 2026
f197661
fix(manifest): preserve scalar scopes in graph collection
ScriptedAlchemy Sep 15, 2026
fedba4f
chore: merge latest layers into scalar scope fix
ScriptedAlchemy Sep 15, 2026
d688c41
test(manifest): merge graph compiler test isolation fix
ScriptedAlchemy Sep 15, 2026
47f5b0f
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
fe284a1
test(enhanced): check layered scope registration through runtime
ScriptedAlchemy Sep 30, 2026
84dc87d
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
ee3f65a
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
f4ab496
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
a69cd25
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
956c19d
Merge branch 'feat/rspack-layered-shared-metadata' into fix/scalar-sh…
ScriptedAlchemy Sep 30, 2026
c4521e5
chore: merge final preview checksum refresh
ScriptedAlchemy Sep 30, 2026
06aa6be
chore: merge manifest rollback follow-up
ScriptedAlchemy Sep 30, 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
7 changes: 7 additions & 0 deletions .changeset/preserve-scalar-shared-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@module-federation/enhanced': patch
'@module-federation/manifest': patch
'@module-federation/sdk': patch
---

Preserve same-version shared implementations in different scalar share scopes during Webpack runtime registration and manifest generation. Keep each scope’s assets separate while retaining legacy public shared IDs. Resolve aliased shared configuration by share key, scope, and layer, and preserve disabled version requirements.
8 changes: 5 additions & 3 deletions packages/enhanced/src/lib/sharing/ShareRuntimeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,16 @@ class ShareRuntimeModule extends RuntimeModule {
if (sharedOption) {
sharedInitOptions[sharedOption.name] =
sharedInitOptions[sharedOption.name] || [];
const isSameVersionAndLayer = sharedInitOptions[
const isSameVersionLayerAndScope = sharedInitOptions[
sharedOption.name
].find(
(s) =>
s.version === sharedOption.version &&
s.shareConfig?.layer === sharedOption.shareConfig?.layer,
s.shareConfig?.layer === sharedOption.shareConfig?.layer &&
JSON.stringify([s.shareScope].flat()) ===
JSON.stringify([sharedOption.shareScope].flat()),
);
if (!isSameVersionAndLayer) {
if (!isSameVersionLayerAndScope) {
sharedInitOptions[sharedOption.name].push(sharedOption);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { scope: 'a' };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { scope: 'b' };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as a } from 'react-a';
export { default as b } from 'react-b';
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const fs = __non_webpack_require__('fs');
const path = __non_webpack_require__('path');

it('retains and selects both same-version singletons in scalar scopes', async () => {
const { a, b } = await import('./bootstrap.js');
expect(a).toEqual({ scope: 'a' });
expect(b).toEqual({ scope: 'b' });
expect(a).not.toBe(b);

// A consumer without local fallbacks verifies that both providers registered.
const consumer = new __webpack_require__.federation.instance.constructor({
name: `scalar_scopes_consumer_${TEST_LAYER ?? 'unlayered'}`,
remotes: [],
});
const registered = [];
for (const scope of ['a', 'b']) {
consumer.initShareScopeMap(scope, __webpack_share_scopes__[scope]);
const factory = await consumer.loadShare('react', {
customShareInfo: {
scope: [scope],
shareConfig: { layer: TEST_LAYER, requiredVersion: false },
},
});
registered.push(factory?.().default);
}
const artifacts = ['mf-stats.json', 'mf.json'].map((filename) => {
const artifact = JSON.parse(
fs.readFileSync(
path.join(__dirname, `${TEST_PREFIX}${filename}`),
'utf8',
),
);
return artifact.shared
.filter((item) => item.name === 'react')
.map((item) => ({
scope: item.shareScope,
layer: item.layer,
version: item.version,
singleton: item.singleton,
requiredVersion: item.requiredVersion,
assets: item.assets.js.sync,
}));
});
const expectedRows = [
{
scope: 'a',
layer: TEST_LAYER,
version: '1.0.0',
singleton: true,
requiredVersion: false,
assets: [TEST_LAYER ? 'common-_common_a_js.js' : 'a_js.js'],
},
{
scope: 'b',
layer: TEST_LAYER,
version: '1.0.0',
singleton: true,
requiredVersion: '1.0.0',
assets: [TEST_LAYER ? 'common-_common_b_js.js' : 'b_js.js'],
},
];
for (const row of expectedRows) {
for (const filename of row.assets) {
expect(fs.existsSync(path.join(__dirname, filename))).toBe(true);
}
}
expect({ registered, stats: artifacts[0], manifest: artifacts[1] }).toEqual({
registered: [a, b],
stats: expectedRows,
manifest: expectedRows,
});
expect(registered[0]).toBe(a);
expect(registered[1]).toBe(b);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const { ModuleFederationPlugin } = require('../../../../dist/src');
const { DefinePlugin } = require('webpack');

module.exports = [undefined, 'common'].map((layer) => ({
experiments: { layers: true },
module: { rules: layer ? [{ test: /bootstrap\.js$/, layer }] : [] },
optimization: { chunkIds: 'named', moduleIds: 'named' },
output: {
publicPath: '/',
chunkFilename: `${layer ? 'common-' : ''}[id].js`,
},
plugins: [
new DefinePlugin({
TEST_LAYER: layer === undefined ? 'undefined' : JSON.stringify(layer),
TEST_PREFIX: JSON.stringify(layer ? 'common-' : ''),
}),
new ModuleFederationPlugin({
name: layer ? 'scalar_scopes_common' : 'scalar_scopes',
manifest: { fileName: `${layer ? 'common-' : ''}mf.json` },
dts: false,
shared: {
'react-a': {
import: './a.js',
shareKey: 'react',
...(layer === undefined ? {} : { layer, issuerLayer: layer }),
shareScope: 'a',
version: '1.0.0',
requiredVersion: false,
singleton: true,
},
'react-b': {
import: './b.js',
shareKey: 'react',
...(layer === undefined ? {} : { layer, issuerLayer: layer }),
shareScope: 'b',
version: '1.0.0',
requiredVersion: '1.0.0',
singleton: true,
},
},
}),
],
}));
18 changes: 13 additions & 5 deletions packages/manifest/src/ModuleHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export const getShareItem = ({
...normalizedShareOptions,
id: `${hostName}:${pkgName}`,
requiredVersion:
normalizedShareOptions?.requiredVersion || `^${pkgVersion}`,
normalizedShareOptions?.requiredVersion ?? `^${pkgVersion}`,
name: pkgName,
version: pkgVersion,
assets: {
Expand Down Expand Up @@ -331,7 +331,8 @@ class ModuleHandler {
);
const layered =
identity.layer !== undefined || Array.isArray(identity.shareScope);
const sharedKey = (name: string) => (layered ? identity.key : name);
const sharedKey = (name: string) =>
layered || identity.shareScope !== 'default' ? identity.key : name;
const initShared = (pkgName: string, pkgVersion: string) => {
const key = sharedKey(pkgName);
if (sharedMap[key]) {
Expand All @@ -340,15 +341,22 @@ class ModuleHandler {
sharedMap[key] = getShareItem({
pkgName,
pkgVersion,
normalizedShareOptions: sharedManagerNormalizedOptions[pkgName],
normalizedShareOptions:
Object.entries(sharedManagerNormalizedOptions).find(
([request, options]) =>
(options.shareKey ?? request) === pkgName &&
options.layer === identity.layer &&
JSON.stringify([options.shareScope ?? 'default'].flat()) ===
JSON.stringify([identity.shareScope].flat()),
)?.[1] ?? sharedManagerNormalizedOptions[pkgName],
hostName: this._options.name,
});
if (layered) {
sharedMap[key].id = `${this._options.name}:shared:${identity.key}`;
if (identity.layer !== undefined) sharedMap[key].layer = identity.layer;
if (identity.shareScope !== 'default')
sharedMap[key].shareScope = identity.shareScope;
}
if (identity.shareScope !== 'default')
sharedMap[key].shareScope = identity.shareScope;
};

const collectRelationshipMap = (mod: StatsModule, pkgName: string) => {
Expand Down
10 changes: 7 additions & 3 deletions packages/manifest/src/collectGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ export function collectGraph(
const scope = data.shareScope;
const layer = data.shareConfig.layer ?? undefined;
const layered = layer !== undefined || Array.isArray(scope);
const key = layered ? getSharedIdentityKey(name, scope, layer) : name;
const key =
layered || scope !== 'default'
? getSharedIdentityKey(name, scope, layer)
: name;
const configured = configuredShared.filter(([key, value]) => {
const shareKey = value.shareKey || key;
return (
Expand All @@ -135,14 +138,15 @@ export function collectGraph(
const row = getShareItem({
pkgName: name,
pkgVersion: version,
normalizedShareOptions: layered ? normalized : shared[name],
normalizedShareOptions:
layered || scope !== 'default' ? normalized : shared[name],
hostName,
});
if (layered) {
row.id = `${hostName}:shared:${key}`;
if (layer !== undefined) row.layer = layer;
if (scope !== 'default') row.shareScope = scope;
}
if (scope !== 'default') row.shareScope = scope;
sharedMap[key] = row;
}
if (consume) {
Expand Down
2 changes: 1 addition & 1 deletion packages/manifest/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function getSharedModuleName(name: string, identifier?: string): string {
: name,
3,
);
return identity.layer !== undefined || Array.isArray(identity.shareScope)
return identity.layer !== undefined || identity.shareScope !== 'default'
? identity.key
: identity.name;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/types/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface ManifestShared {
name: string;
version: string;
singleton: boolean;
requiredVersion: string;
requiredVersion: string | false | null;
hash: string;
assets: StatsAssets;
fallback?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/types/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export interface StatsShared {
name: string;
version: string;
singleton: boolean;
requiredVersion: string;
requiredVersion: string | false | null;
hash: string;
assets: StatsAssets;
deps: string[];
Expand Down
Loading