Skip to content

Commit 9dd3a5b

Browse files
authored
fix(enhanced): stabilize consume-shared runtime output (#4842)
1 parent 7cfd71b commit 9dd3a5b

3 files changed

Lines changed: 132 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@module-federation/enhanced": patch
3+
---
4+
5+
Fix consume-shared runtime output ordering for stable build hashes.

packages/enhanced/src/lib/sharing/ConsumeSharedRuntimeModule.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { getFederationGlobalScope } from '../container/runtime/utils';
1111
const { Template, RuntimeGlobals, RuntimeModule } = require(
1212
normalizeWebpackPath('webpack'),
1313
) as typeof import('webpack');
14+
const { compareModulesByIdentifier } = require(
15+
normalizeWebpackPath('webpack/lib/util/comparators'),
16+
) as typeof import('webpack/lib/util/comparators');
1417

1518
class ConsumeSharedRuntimeModule extends RuntimeModule {
1619
private _runtimeRequirements: ReadonlySet<string>;
@@ -100,9 +103,10 @@ class ConsumeSharedRuntimeModule extends RuntimeModule {
100103

101104
const allChunks = [...(this.chunk?.getAllReferencedChunks() || [])];
102105
for (const chunk of allChunks) {
103-
const modules = chunkGraph.getChunkModulesIterableBySourceType(
106+
const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
104107
chunk,
105108
'consume-shared',
109+
compareModulesByIdentifier,
106110
);
107111
if (!modules) continue;
108112
// chunk.id may equal 0
@@ -116,9 +120,10 @@ class ConsumeSharedRuntimeModule extends RuntimeModule {
116120
);
117121
}
118122
for (const chunk of [...(this.chunk?.getAllInitialChunks() || [])]) {
119-
const modules = chunkGraph.getChunkModulesIterableBySourceType(
123+
const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
120124
chunk,
121125
'consume-shared',
126+
compareModulesByIdentifier,
122127
);
123128
if (!modules) continue;
124129
addModules(modules, chunk, initialConsumes);

packages/enhanced/test/unit/sharing/ConsumeSharedRuntimeModule.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import { createMockCompilation, createWebpackMock, shareScopes } from './utils';
77
// Use rs.hoisted() to create mock functions that are hoisted along with rs.mock()
88
const mocks = rs.hoisted(() => ({
99
mockNormalizeWebpackPath: rs.fn((path: string) => path),
10+
mockCompareModulesByIdentifier: rs.fn(
11+
(a: { identifier?: () => string }, b: { identifier?: () => string }) => {
12+
if (!a.identifier || !b.identifier) return 0;
13+
return a.identifier().localeCompare(b.identifier());
14+
},
15+
),
1016
}));
1117

1218
// Mock normalize-webpack-path
@@ -17,6 +23,10 @@ rs.mock('@module-federation/sdk/normalize-webpack-path', () => ({
1723
// Mock webpack - createWebpackMock already includes RuntimeModule with STAGE_* constants
1824
rs.mock('webpack', () => createWebpackMock());
1925

26+
rs.mock('webpack/lib/util/comparators', () => ({
27+
compareModulesByIdentifier: mocks.mockCompareModulesByIdentifier,
28+
}));
29+
2030
// Import the real implementation
2131
import ConsumeSharedRuntimeModule from '../../../src/lib/sharing/ConsumeSharedRuntimeModule';
2232

@@ -91,6 +101,14 @@ describe('ConsumeSharedRuntimeModule', () => {
91101
});
92102

93103
describe('generate', () => {
104+
const createChunk = (id: string | number) =>
105+
({
106+
id,
107+
ids: [],
108+
runtime: 'runtime',
109+
getAllInitialChunks: rs.fn().mockReturnValue([]),
110+
}) as any;
111+
94112
it('should generate code with string shareScope', () => {
95113
// Create a set of runtime requirements for the constructor
96114
const runtimeRequirements = new Set(['some-requirement']);
@@ -310,5 +328,107 @@ describe('ConsumeSharedRuntimeModule', () => {
310328
// The result might be null or a string
311329
expect(result === null || typeof result === 'string').toBeTruthy();
312330
});
331+
332+
it('should use ordered consume-shared modules for referenced and initial chunks', () => {
333+
const runtimeRequirements = new Set(['some-requirement']);
334+
const module = new ConsumeSharedRuntimeModule(runtimeRequirements);
335+
const referencedChunk = createChunk('referenced');
336+
const initialChunk = createChunk('initial');
337+
338+
module.chunk = {
339+
id: 'test-chunk',
340+
ids: [],
341+
getAllReferencedChunks: rs.fn().mockReturnValue([referencedChunk]),
342+
getAllInitialChunks: rs.fn().mockReturnValue([initialChunk]),
343+
} as any;
344+
345+
mockCompilation.chunkGraph.getOrderedChunkModulesIterableBySourceType = rs
346+
.fn()
347+
.mockReturnValue([]);
348+
349+
module.compilation = mockCompilation as any;
350+
module.chunkGraph = mockCompilation.chunkGraph as any;
351+
352+
expect(module.generate()).toBeNull();
353+
354+
expect(
355+
mockCompilation.chunkGraph.getOrderedChunkModulesIterableBySourceType,
356+
).toHaveBeenCalledWith(
357+
referencedChunk,
358+
'consume-shared',
359+
expect.any(Function),
360+
);
361+
expect(
362+
mockCompilation.chunkGraph.getOrderedChunkModulesIterableBySourceType,
363+
).toHaveBeenCalledWith(
364+
initialChunk,
365+
'consume-shared',
366+
expect.any(Function),
367+
);
368+
expect(
369+
mockCompilation.chunkGraph.getChunkModulesIterableBySourceType,
370+
).not.toHaveBeenCalled();
371+
});
372+
373+
it('should generate deterministic output when consume-shared insertion order changes', () => {
374+
const createConsumeModule = (identifier: string, id: string) =>
375+
({
376+
id,
377+
identifier: () => identifier,
378+
}) as any;
379+
const moduleA = createConsumeModule('consume ./a', 'a');
380+
const moduleB = createConsumeModule('consume ./b', 'b');
381+
382+
const generate = (modules: any[]) => {
383+
const { mockCompilation: compilation } = createMockCompilation();
384+
const runtimeRequirements = new Set(['__webpack_require__.f']);
385+
const runtimeModule = new ConsumeSharedRuntimeModule(
386+
runtimeRequirements,
387+
);
388+
const chunk = createChunk('async');
389+
390+
runtimeModule.chunk = {
391+
id: 'test-chunk',
392+
ids: [],
393+
getAllReferencedChunks: rs.fn().mockReturnValue([chunk]),
394+
getAllInitialChunks: rs.fn().mockReturnValue([chunk]),
395+
} as any;
396+
397+
compilation.chunkGraph.getChunkModulesIterableBySourceType = rs
398+
.fn()
399+
.mockReturnValue(modules);
400+
compilation.chunkGraph.getOrderedChunkModulesIterableBySourceType = rs
401+
.fn()
402+
.mockImplementation(
403+
(
404+
_chunk: unknown,
405+
_sourceType: string,
406+
compare: (a: unknown, b: unknown) => number,
407+
) => [...modules].sort(compare),
408+
);
409+
compilation.chunkGraph.getModuleId = rs.fn((m: any) => m.id);
410+
compilation.codeGenerationResults.getSource = rs.fn((m: any) => ({
411+
source: () => `fallback_${m.id}`,
412+
}));
413+
compilation.codeGenerationResults.getData = rs.fn((m: any) => ({
414+
shareScope: 'default',
415+
shareConfig: {
416+
singleton: false,
417+
requiredVersion: false,
418+
strictVersion: false,
419+
eager: false,
420+
layer: null,
421+
},
422+
shareKey: m.id,
423+
}));
424+
425+
runtimeModule.compilation = compilation as any;
426+
runtimeModule.chunkGraph = compilation.chunkGraph as any;
427+
428+
return runtimeModule.generate();
429+
};
430+
431+
expect(generate([moduleB, moduleA])).toBe(generate([moduleA, moduleB]));
432+
});
313433
});
314434
});

0 commit comments

Comments
 (0)