Skip to content

Commit 40023cb

Browse files
authored
feat: allow external modules place in async chunks in some case (#20632)
1 parent 214f361 commit 40023cb

9 files changed

Lines changed: 105 additions & 7 deletions

File tree

lib/ExternalModule.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"use strict";
77

8+
const { SyncBailHook } = require("tapable");
89
const { OriginalSource, RawSource } = require("webpack-sources");
910
const ConcatenationScope = require("./ConcatenationScope");
1011
const EnvironmentNotSupportAsyncWarning = require("./EnvironmentNotSupportAsyncWarning");
@@ -629,6 +630,14 @@ const getSourceForDefaultCase = (optional, request, runtimeTemplate) => {
629630
/** @typedef {Record<string, string | string[]>} RequestRecord */
630631
/** @typedef {string | string[] | RequestRecord} ExternalModuleRequest */
631632

633+
/**
634+
* @typedef {object} ExternalModuleHooks
635+
* @property {SyncBailHook<[Chunk, Compilation], boolean>} chunkCondition
636+
*/
637+
638+
/** @type {WeakMap<Compilation, ExternalModuleHooks>} */
639+
const compilationHooksMap = new WeakMap();
640+
632641
class ExternalModule extends Module {
633642
/**
634643
* @param {ExternalModuleRequest} request request
@@ -650,6 +659,21 @@ class ExternalModule extends Module {
650659
this.dependencyMeta = dependencyMeta;
651660
}
652661

662+
/**
663+
* @param {Compilation} compilation the compilation
664+
* @returns {ExternalModuleHooks} the attached hooks
665+
*/
666+
static getCompilationHooks(compilation) {
667+
let hooks = compilationHooksMap.get(compilation);
668+
if (hooks === undefined) {
669+
hooks = {
670+
chunkCondition: new SyncBailHook(["chunk", "compilation"])
671+
};
672+
compilationHooksMap.set(compilation, hooks);
673+
}
674+
return hooks;
675+
}
676+
653677
/**
654678
* @returns {SourceTypes} types available (do not mutate)
655679
*/
@@ -679,12 +703,21 @@ class ExternalModule extends Module {
679703
/**
680704
* @param {Chunk} chunk the chunk which condition should be checked
681705
* @param {Compilation} compilation the compilation
682-
* @returns {boolean} true, if the chunk is ok for the module
706+
* @returns {boolean} true if the module can be placed in the chunk
683707
*/
684-
chunkCondition(chunk, { chunkGraph }) {
685-
return this.externalType === "css-import"
686-
? true
687-
: chunkGraph.getNumberOfEntryModules(chunk) > 0;
708+
chunkCondition(chunk, compilation) {
709+
const { chunkCondition } = ExternalModule.getCompilationHooks(compilation);
710+
const condition = chunkCondition.call(chunk, compilation);
711+
if (condition !== undefined) return condition;
712+
713+
const type = this._resolveExternalType(this.externalType);
714+
715+
// For `import()` externals, keep them in the initial chunk to avoid loading
716+
// them asynchronously twice and to improve runtime performance.
717+
if (["css-import", "module"].includes(type)) {
718+
return true;
719+
}
720+
return compilation.chunkGraph.getNumberOfEntryModules(chunk) > 0;
688721
}
689722

690723
/**

lib/Module.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@ class Module extends DependenciesBlock {
10951095
/**
10961096
* @param {Chunk} chunk the chunk which condition should be checked
10971097
* @param {Compilation} compilation the compilation
1098-
* @returns {boolean} true, if the chunk is ok for the module
1098+
* @returns {boolean} true if the module can be placed in the chunk
10991099
*/
11001100
chunkCondition(chunk, compilation) {
11011101
return true;

lib/container/ContainerReferencePlugin.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"use strict";
77

8+
const ExternalModule = require("../ExternalModule");
89
const ExternalsPlugin = require("../ExternalsPlugin");
910
const RuntimeGlobals = require("../RuntimeGlobals");
1011
const FallbackDependency = require("./FallbackDependency");
@@ -135,6 +136,17 @@ class ContainerReferencePlugin {
135136
set.add(RuntimeGlobals.shareScopeMap);
136137
compilation.addRuntimeModule(chunk, new RemoteRuntimeModule());
137138
});
139+
140+
const { chunkCondition } =
141+
ExternalModule.getCompilationHooks(compilation);
142+
143+
// External modules issued by remote modules should be placed in entry chunks
144+
// to ensure they are loaded and initialize first
145+
chunkCondition.tap(
146+
PLUGIN_NAME,
147+
(chunk, compilation) =>
148+
compilation.chunkGraph.getNumberOfEntryModules(chunk) > 0
149+
);
138150
}
139151
);
140152
}

lib/container/FallbackModule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class FallbackModule extends Module {
7979
/**
8080
* @param {Chunk} chunk the chunk which condition should be checked
8181
* @param {Compilation} compilation the compilation
82-
* @returns {boolean} true, if the chunk is ok for the module
82+
* @returns {boolean} true if the module can be placed in the chunk
8383
*/
8484
chunkCondition(chunk, { chunkGraph }) {
8585
return chunkGraph.getNumberOfEntryModules(chunk) > 0;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import external from "external";
2+
import external2 from "external2";
3+
4+
export const readFile = external.readFile;
5+
export const readFileSync = external2.readFileSync;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
it("Should place externals into its own chunks when externalsType is module", function (done) {
2+
var fs = require("fs");
3+
4+
expect(__webpack_modules__["external"]).toBeUndefined();
5+
expect(__webpack_modules__["external2"]).toBeUndefined();
6+
expect(__webpack_modules__["external3"]).toBeDefined();
7+
8+
import(/* webpackChunkName: 'async' */ "./chunk").then((ns) => {
9+
expect(__webpack_modules__["external"]).toBeDefined();
10+
expect(__webpack_modules__["external2"]).toBeDefined();
11+
expect(ns.readFileSync).toBe(fs.readFileSync);
12+
expect(ns.readFile).toBe(fs.readFile);
13+
14+
import("external3").then((ns) => {
15+
expect(ns.writeFile).toBe(fs.writeFile);
16+
17+
done();
18+
});
19+
});
20+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"use strict";
2+
3+
const supportsRequireInModule = require("../../../helpers/supportsRequireInModule");
4+
5+
module.exports = () => supportsRequireInModule();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use strict";
2+
3+
/** @type {import("../../../../").Configuration} */
4+
module.exports = {
5+
externals: {
6+
external: "fs",
7+
external2: "node:fs",
8+
external3: "fs"
9+
},
10+
externalsType: "module-import",
11+
experiments: {
12+
outputModule: true
13+
},
14+
output: { chunkFilename: "[name].mjs" },
15+
optimization: {
16+
moduleIds: "named",
17+
concatenateModules: false
18+
}
19+
};

types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5711,6 +5711,7 @@ declare class ExternalModule extends Module {
57115711
unsafeCacheData: UnsafeCacheData,
57125712
normalModuleFactory: NormalModuleFactory
57135713
): void;
5714+
static getCompilationHooks(compilation: Compilation): ExternalModuleHooks;
57145715
static ModuleExternalInitFragment: typeof ModuleExternalInitFragment;
57155716
static getExternalModuleNodeCommonjsInitFragment: (
57165717
runtimeTemplate: RuntimeTemplate
@@ -5722,6 +5723,9 @@ declare class ExternalModule extends Module {
57225723
*/
57235724
static getSourceBasicTypes(module: Module): ReadonlySet<string>;
57245725
}
5726+
declare interface ExternalModuleHooks {
5727+
chunkCondition: SyncBailHook<[Chunk, Compilation], boolean>;
5728+
}
57255729
declare interface ExternalModuleInfo {
57265730
type: "external";
57275731
module: Module;

0 commit comments

Comments
 (0)