Skip to content

Commit 093eadf

Browse files
authored
Merge pull request #14827 from webpack/bugfix/mf-module
fix and test module federation with ESM
2 parents ac9a2c8 + 041287f commit 093eadf

File tree

7 files changed

+213
-116
lines changed

7 files changed

+213
-116
lines changed

lib/container/ContainerEntryModule.js

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const AsyncDependenciesBlock = require("../AsyncDependenciesBlock");
1010
const Module = require("../Module");
1111
const RuntimeGlobals = require("../RuntimeGlobals");
1212
const Template = require("../Template");
13+
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
1314
const makeSerializable = require("../util/makeSerializable");
1415
const ContainerExposedDependency = require("./ContainerExposedDependency");
1516

@@ -104,6 +105,7 @@ class ContainerEntryModule extends Module {
104105
strict: true,
105106
topLevelDeclarations: new Set(["moduleMap", "get", "init"])
106107
};
108+
this.buildMeta.exportsType = "namespace";
107109

108110
this.clearDependenciesAndBlocks();
109111

@@ -127,6 +129,7 @@ class ContainerEntryModule extends Module {
127129
}
128130
this.addBlock(block);
129131
}
132+
this.addDependency(new StaticExportsDependency(["get", "init"], false));
130133

131134
callback();
132135
}

test/ConfigTestCases.template.js

+80-67
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ const describeCases = config => {
325325
};
326326

327327
const requireCache = Object.create(null);
328+
const esmCache = new Map();
329+
const esmIdentifier = `${category.name}-${testName}-${i}`;
328330
// eslint-disable-next-line no-loop-func
329331
const _require = (
330332
currentDirectory,
@@ -335,7 +337,7 @@ const describeCases = config => {
335337
) => {
336338
if (testConfig === undefined) {
337339
throw new Error(
338-
`_require(${module}) called after all tests have completed`
340+
`_require(${module}) called after all tests from ${category.name} ${testName} have completed`
339341
);
340342
}
341343
if (Array.isArray(module) || /^\.\.?\//.test(module)) {
@@ -373,16 +375,15 @@ const describeCases = config => {
373375
);
374376
}
375377
}
376-
if (p in requireCache) {
377-
return requireCache[p].exports;
378-
}
379-
const m = {
380-
exports: {}
381-
};
382-
requireCache[p] = m;
378+
const isModule =
379+
p.endsWith(".mjs") &&
380+
options.experiments &&
381+
options.experiments.outputModule;
382+
383383
let runInNewContext = false;
384384

385385
const moduleScope = {
386+
console: console,
386387
it: _it,
387388
beforeEach: _beforeEach,
388389
afterEach: _afterEach,
@@ -396,36 +397,7 @@ const describeCases = config => {
396397
return m;
397398
}
398399
};
399-
const isModule =
400-
p.endsWith(".mjs") &&
401-
options.experiments &&
402-
options.experiments.outputModule;
403-
if (!isModule) {
404-
Object.assign(moduleScope, {
405-
require: _require.bind(
406-
null,
407-
path.dirname(p),
408-
options
409-
),
410-
importScripts: url => {
411-
expect(url).toMatch(
412-
/^https:\/\/test\.cases\/path\//
413-
);
414-
_require(
415-
outputDirectory,
416-
options,
417-
`.${url.slice(
418-
"https://test.cases/path".length
419-
)}`
420-
);
421-
},
422-
module: m,
423-
exports: m.exports,
424-
__dirname: path.dirname(p),
425-
__filename: p,
426-
_globalAssign: { expect }
427-
});
428-
}
400+
429401
if (
430402
options.target === "web" ||
431403
options.target === "webworker"
@@ -439,48 +411,55 @@ const describeCases = config => {
439411
});
440412
runInNewContext = true;
441413
}
442-
if (testConfig.moduleScope) {
443-
testConfig.moduleScope(moduleScope);
444-
}
445414
if (isModule) {
415+
if (testConfig.moduleScope) {
416+
testConfig.moduleScope(moduleScope);
417+
}
446418
if (!vm.SourceTextModule)
447419
throw new Error(
448420
"Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'."
449421
);
450-
const esm = new vm.SourceTextModule(content, {
451-
identifier: p,
452-
url: pathToFileURL(p).href,
453-
context:
454-
(parentModule && parentModule.context) ||
455-
vm.createContext(moduleScope, {
456-
name: `context for ${p}`
457-
}),
458-
initializeImportMeta: (meta, module) => {
459-
meta.url = pathToFileURL(p).href;
460-
},
461-
importModuleDynamically: async (
462-
specifier,
463-
module
464-
) => {
465-
const result = await _require(
466-
path.dirname(p),
467-
options,
422+
let esm = esmCache.get(p);
423+
if (!esm) {
424+
esm = new vm.SourceTextModule(content, {
425+
identifier: esmIdentifier + "-" + p,
426+
url: pathToFileURL(p).href + "?" + esmIdentifier,
427+
context:
428+
(parentModule && parentModule.context) ||
429+
vm.createContext(moduleScope, {
430+
name: `context for ${p}`
431+
}),
432+
initializeImportMeta: (meta, module) => {
433+
meta.url = pathToFileURL(p).href;
434+
},
435+
importModuleDynamically: async (
468436
specifier,
469-
"evaluated",
470437
module
471-
);
472-
return await asModule(result, module.context);
473-
}
474-
});
438+
) => {
439+
const result = await _require(
440+
path.dirname(p),
441+
options,
442+
specifier,
443+
"evaluated",
444+
module
445+
);
446+
return await asModule(result, module.context);
447+
}
448+
});
449+
esmCache.set(p, esm);
450+
}
475451
if (esmMode === "unlinked") return esm;
476452
return (async () => {
477453
await esm.link(
478454
async (specifier, referencingModule) => {
479455
return await asModule(
480456
await _require(
481457
path.dirname(
482-
referencingModule.identifier ||
483-
fileURLToPath(referencingModule.url)
458+
referencingModule.identifier
459+
? referencingModule.identifier.slice(
460+
esmIdentifier.length + 1
461+
)
462+
: fileURLToPath(referencingModule.url)
484463
),
485464
options,
486465
specifier,
@@ -502,6 +481,40 @@ const describeCases = config => {
502481
: ns;
503482
})();
504483
} else {
484+
if (p in requireCache) {
485+
return requireCache[p].exports;
486+
}
487+
const m = {
488+
exports: {}
489+
};
490+
requireCache[p] = m;
491+
Object.assign(moduleScope, {
492+
require: _require.bind(
493+
null,
494+
path.dirname(p),
495+
options
496+
),
497+
importScripts: url => {
498+
expect(url).toMatch(
499+
/^https:\/\/test\.cases\/path\//
500+
);
501+
_require(
502+
outputDirectory,
503+
options,
504+
`.${url.slice(
505+
"https://test.cases/path".length
506+
)}`
507+
);
508+
},
509+
module: m,
510+
exports: m.exports,
511+
__dirname: path.dirname(p),
512+
__filename: p,
513+
_globalAssign: { expect }
514+
});
515+
if (testConfig.moduleScope) {
516+
testConfig.moduleScope(moduleScope);
517+
}
505518
if (!runInNewContext)
506519
content = `Object.assign(global, _globalAssign); ${content}`;
507520
const args = Object.keys(moduleScope);
@@ -517,8 +530,8 @@ const describeCases = config => {
517530
: vm.runInThisContext(code, p);
518531
fn.call(m.exports, ...argValues);
519532
document.currentScript = oldCurrentScript;
533+
return m.exports;
520534
}
521-
return m.exports;
522535
} else if (
523536
testConfig.modules &&
524537
module in testConfig.modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
findBundle: function (i, options) {
3+
return i === 0 ? "./main.js" : "./module/main.mjs";
4+
}
5+
};
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,61 @@
11
const { ModuleFederationPlugin } = require("../../../../").container;
22

3-
/** @type {import("../../../../").Configuration} */
4-
module.exports = {
5-
plugins: [
6-
new ModuleFederationPlugin({
7-
name: "container",
8-
library: { type: "commonjs-module" },
9-
filename: "container.js",
10-
exposes: {
11-
"./ComponentA": {
12-
import: "./ComponentA"
13-
}
14-
},
15-
remotes: {
16-
containerA: {
17-
external: "./container.js"
18-
}
19-
},
20-
shared: {
21-
react: {
22-
version: false,
23-
requiredVersion: false
24-
}
25-
}
26-
})
27-
]
3+
/** @type {ConstructorParameters<typeof ModuleFederationPlugin>[0]} */
4+
const common = {
5+
name: "container",
6+
exposes: {
7+
"./ComponentA": {
8+
import: "./ComponentA"
9+
}
10+
},
11+
shared: {
12+
react: {
13+
version: false,
14+
requiredVersion: false
15+
}
16+
}
2817
};
18+
19+
/** @type {import("../../../../").Configuration[]} */
20+
module.exports = [
21+
{
22+
output: {
23+
filename: "[name].js",
24+
uniqueName: "0-container-full"
25+
},
26+
plugins: [
27+
new ModuleFederationPlugin({
28+
library: { type: "commonjs-module" },
29+
filename: "container.js",
30+
remotes: {
31+
containerA: {
32+
external: "./container.js"
33+
}
34+
},
35+
...common
36+
})
37+
]
38+
},
39+
{
40+
experiments: {
41+
outputModule: true
42+
},
43+
output: {
44+
filename: "module/[name].mjs",
45+
uniqueName: "0-container-full-mjs"
46+
},
47+
plugins: [
48+
new ModuleFederationPlugin({
49+
library: { type: "module" },
50+
filename: "module/container.mjs",
51+
remotes: {
52+
containerA: {
53+
external: "./container.mjs"
54+
}
55+
},
56+
...common
57+
})
58+
],
59+
target: "node14"
60+
}
61+
];

test/configCases/container/1-container-full/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
2+
"private": true,
3+
"engines": {
4+
"node": ">=10.13.0"
5+
},
26
"dependencies": {
37
"react": "*"
48
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
findBundle: function (i, options) {
3+
return i === 0 ? "./main.js" : "./module/main.mjs";
4+
}
5+
};

0 commit comments

Comments
 (0)