Skip to content

Commit 5a4f503

Browse files
authored
fix(manifest): record split expose chunk assets (#4489)
1 parent da4c722 commit 5a4f503

File tree

7 files changed

+142
-10
lines changed

7 files changed

+142
-10
lines changed

.changeset/cozy-worlds-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/manifest': patch
3+
---
4+
5+
fix(manifest): record split expose chunk assets
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const fs = __non_webpack_require__('fs');
2+
const path = __non_webpack_require__('path');
3+
4+
const statsPath = path.join(__dirname, 'mf-stats.json');
5+
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf-8'));
6+
7+
it('should have one expose entry', () => {
8+
expect(stats.exposes).toHaveLength(1);
9+
expect(stats.exposes[0].path).toBe('./expose-a');
10+
});
11+
12+
it('should collect all expose chunk assets even when split by splitChunks.maxSize', () => {
13+
const syncFiles = stats.exposes[0].assets.js.sync;
14+
15+
// Discover every JS file webpack actually emitted for this expose chunk.
16+
// Split chunks produced by maxSize are named "__federation_expose_expose_a-<hash>.js",
17+
// so we match on the expose chunk name prefix.
18+
const exposeChunkPrefix = '__federation_expose_expose_a';
19+
const emittedExposeFiles = fs
20+
.readdirSync(__dirname)
21+
.filter((f) => f.startsWith(exposeChunkPrefix) && f.endsWith('.js'));
22+
23+
// At least the primary expose chunk must exist
24+
expect(emittedExposeFiles.length).toBeGreaterThanOrEqual(1);
25+
26+
// Every emitted expose chunk file must appear in the stats assets.
27+
// This verifies the fix: without it, split chunks named
28+
// "__federation_expose_expose_a-<hash>" would be missed by the direct
29+
// Map lookup in StatsManager._getModuleAssets.
30+
expect(syncFiles.sort()).toEqual(
31+
expect.arrayContaining(emittedExposeFiles.sort()),
32+
);
33+
});
34+
35+
it('should have multiple sync JS files when splitting actually occurred', () => {
36+
const syncFiles = stats.exposes[0].assets.js.sync;
37+
const exposeChunkPrefix = '__federation_expose_expose_a';
38+
const splitChunkFiles = syncFiles.filter((f) =>
39+
f.startsWith(exposeChunkPrefix),
40+
);
41+
42+
// With minSize:0 and maxSize:100 and two modules each >100 bytes,
43+
// webpack must have produced more than one expose-related chunk.
44+
expect(splitChunkFiles.length).toBeGreaterThan(1);
45+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { dataA, extraA } from './part-a';
2+
export { dataB, extraB } from './part-b';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Part A — content intentionally sized to participate in maxSize splitting
2+
export const dataA = 'part-a-primary-export-value-for-split-chunks-test';
3+
export const extraA = 'part-a-secondary-export-value-for-split-chunks-test';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Part B — content intentionally sized to participate in maxSize splitting
2+
export const dataB = 'part-b-primary-export-value-for-split-chunks-test';
3+
export const extraB = 'part-b-secondary-export-value-for-split-chunks-test';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { ModuleFederationPlugin } = require('../../../../dist/src');
2+
3+
module.exports = {
4+
mode: 'production',
5+
optimization: {
6+
chunkIds: 'named',
7+
moduleIds: 'named',
8+
splitChunks: {
9+
cacheGroups: {
10+
'expose-a': {
11+
chunks: /__federation_expose_expose_a/,
12+
minSize: 0,
13+
maxSize: 100,
14+
name: '__federation_expose_expose_a',
15+
},
16+
},
17+
},
18+
},
19+
output: {
20+
publicPath: '/',
21+
},
22+
plugins: [
23+
new ModuleFederationPlugin({
24+
name: 'manifest_split_chunks',
25+
filename: 'container.js',
26+
library: { type: 'commonjs-module' },
27+
exposes: {
28+
'./expose-a': './module.js',
29+
},
30+
manifest: true,
31+
}),
32+
],
33+
};

packages/manifest/src/StatsManager.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,57 @@ class StatsManager {
229229
const assets: Record<string, StatsAssets> = {};
230230

231231
chunks.forEach((chunk) => {
232-
if (
233-
typeof chunk.name === 'string' &&
234-
exposeFileNameImportMap[chunk.name]
235-
) {
236-
// TODO: support multiple import
237-
const exposeKey = exposeFileNameImportMap[chunk.name][0];
238-
assets[getFileNameWithOutExt(exposeKey)] = getAssetsByChunk(
239-
chunk,
240-
entryPointNames,
241-
);
232+
if (typeof chunk.name !== 'string') return;
233+
234+
// Support split chunks caused by splitChunks.maxSize:
235+
// A chunk named "__federation_expose_Foo" may be split into
236+
// "__federation_expose_Foo-<hash>" chunks, so we match both exact
237+
// and prefix+dash patterns.
238+
const matchedKey =
239+
exposeFileNameImportMap[chunk.name] !== undefined
240+
? chunk.name
241+
: Object.keys(exposeFileNameImportMap).find((key) =>
242+
chunk.name!.startsWith(key + '-'),
243+
);
244+
245+
if (!matchedKey) return;
246+
247+
// TODO: support multiple import
248+
const exposeKey = exposeFileNameImportMap[matchedKey][0];
249+
const assetKey = getFileNameWithOutExt(exposeKey);
250+
const chunkAssets = getAssetsByChunk(chunk, entryPointNames);
251+
252+
if (!assets[assetKey]) {
253+
assets[assetKey] = chunkAssets;
254+
} else {
255+
// Merge split chunk assets, deduplicating with Set
256+
assets[assetKey] = {
257+
js: {
258+
sync: [
259+
...new Set([...assets[assetKey].js.sync, ...chunkAssets.js.sync]),
260+
],
261+
async: [
262+
...new Set([
263+
...assets[assetKey].js.async,
264+
...chunkAssets.js.async,
265+
]),
266+
],
267+
},
268+
css: {
269+
sync: [
270+
...new Set([
271+
...assets[assetKey].css.sync,
272+
...chunkAssets.css.sync,
273+
]),
274+
],
275+
async: [
276+
...new Set([
277+
...assets[assetKey].css.async,
278+
...chunkAssets.css.async,
279+
]),
280+
],
281+
},
282+
};
242283
}
243284
});
244285

0 commit comments

Comments
 (0)