From c69e37c39d12c8e3c761c883069d89f90cac817c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 25 Nov 2021 13:21:40 +0100 Subject: [PATCH] fix outputModule with initial splitChunks --- lib/esm/ModuleChunkFormatPlugin.js | 123 +++++++++++------- lib/javascript/ChunkHelpers.js | 33 +++++ lib/javascript/StartupHelpers.js | 26 +--- test/ConfigTestCases.template.js | 87 +++++++------ .../module/runtime-chunk/test.config.js | 2 +- .../module/runtime-chunk/webpack.config.js | 4 +- test/configCases/module/split-chunks/index.js | 16 +++ .../module/split-chunks/separate.js | 1 + .../module/split-chunks/test.config.js | 5 + .../module/split-chunks/webpack.config.js | 30 +++++ 10 files changed, 207 insertions(+), 120 deletions(-) create mode 100644 lib/javascript/ChunkHelpers.js create mode 100644 test/configCases/module/split-chunks/index.js create mode 100644 test/configCases/module/split-chunks/separate.js create mode 100644 test/configCases/module/split-chunks/test.config.js create mode 100644 test/configCases/module/split-chunks/webpack.config.js diff --git a/lib/esm/ModuleChunkFormatPlugin.js b/lib/esm/ModuleChunkFormatPlugin.js index 8045fe404b6..e17d1053063 100644 --- a/lib/esm/ModuleChunkFormatPlugin.js +++ b/lib/esm/ModuleChunkFormatPlugin.js @@ -5,18 +5,16 @@ "use strict"; -const { ConcatSource, RawSource } = require("webpack-sources"); +const { ConcatSource } = require("webpack-sources"); const { RuntimeGlobals } = require(".."); const HotUpdateChunk = require("../HotUpdateChunk"); const Template = require("../Template"); +const { getAllChunks } = require("../javascript/ChunkHelpers"); const { getCompilationHooks, getChunkFilenameTemplate } = require("../javascript/JavascriptModulesPlugin"); -const { - generateEntryStartup, - updateHashForEntryStartup -} = require("../javascript/StartupHelpers"); +const { updateHashForEntryStartup } = require("../javascript/StartupHelpers"); /** @typedef {import("../Compiler")} Compiler */ @@ -84,63 +82,90 @@ class ModuleChunkFormatPlugin { } ) .split("/"); - const runtimeOutputName = compilation - .getPath( - getChunkFilenameTemplate( - runtimeChunk, - compilation.outputOptions - ), - { - chunk: runtimeChunk, - contentHashType: "javascript" - } - ) - .split("/"); // remove filename, we only need the directory - const outputFilename = currentOutputName.pop(); + currentOutputName.pop(); - // remove common parts - while ( - currentOutputName.length > 0 && - runtimeOutputName.length > 0 && - currentOutputName[0] === runtimeOutputName[0] - ) { - currentOutputName.shift(); - runtimeOutputName.shift(); - } + const getRelativePath = chunk => { + const baseOutputName = currentOutputName.slice(); + const chunkOutputName = compilation + .getPath( + getChunkFilenameTemplate( + chunk, + compilation.outputOptions + ), + { + chunk: chunk, + contentHashType: "javascript" + } + ) + .split("/"); - // create final path - const runtimePath = - (currentOutputName.length > 0 - ? "../".repeat(currentOutputName.length) - : "./") + runtimeOutputName.join("/"); + // remove common parts + while ( + baseOutputName.length > 0 && + chunkOutputName.length > 0 && + baseOutputName[0] === chunkOutputName[0] + ) { + baseOutputName.shift(); + chunkOutputName.shift(); + } + // create final path + return ( + (baseOutputName.length > 0 + ? "../".repeat(baseOutputName.length) + : "./") + chunkOutputName.join("/") + ); + }; const entrySource = new ConcatSource(); entrySource.add(source); entrySource.add(";\n\n// load runtime\n"); entrySource.add( `import __webpack_require__ from ${JSON.stringify( - runtimePath - )};\n` - ); - entrySource.add( - `import * as __webpack_self_exports__ from ${JSON.stringify( - "./" + outputFilename + getRelativePath(runtimeChunk) )};\n` ); - entrySource.add( - `${RuntimeGlobals.externalInstallChunk}(__webpack_self_exports__);\n` - ); - const startupSource = new RawSource( - generateEntryStartup( - chunkGraph, - runtimeTemplate, - entries, - chunk, - false - ) + + const startupSource = new ConcatSource(); + startupSource.add( + `var __webpack_exec__ = ${runtimeTemplate.returningFunction( + `__webpack_require__(${RuntimeGlobals.entryModuleId} = moduleId)`, + "moduleId" + )}\n` ); + + const loadedChunks = new Set(); + let index = 0; + for (let i = 0; i < entries.length; i++) { + const [module, entrypoint] = entries[i]; + const final = i + 1 === entries.length; + const moduleId = chunkGraph.getModuleId(module); + const chunks = getAllChunks( + entrypoint, + runtimeChunk, + undefined + ); + for (const chunk of chunks) { + if (loadedChunks.has(chunk)) continue; + loadedChunks.add(chunk); + startupSource.add( + `import * as __webpack_chunk_${index}__ from ${JSON.stringify( + getRelativePath(chunk) + )};\n` + ); + startupSource.add( + `${RuntimeGlobals.externalInstallChunk}(__webpack_chunk_${index}__);\n` + ); + index++; + } + startupSource.add( + `${ + final ? "var __webpack_exports__ = " : "" + }__webpack_exec__(${JSON.stringify(moduleId)});\n` + ); + } + entrySource.add( hooks.renderStartup.call( startupSource, diff --git a/lib/javascript/ChunkHelpers.js b/lib/javascript/ChunkHelpers.js new file mode 100644 index 00000000000..8e057049603 --- /dev/null +++ b/lib/javascript/ChunkHelpers.js @@ -0,0 +1,33 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const Entrypoint = require("../Entrypoint"); + +/** @typedef {import("../Chunk")} Chunk */ + +/** + * @param {Entrypoint} entrypoint a chunk group + * @param {Chunk} excludedChunk1 current chunk which is excluded + * @param {Chunk} excludedChunk2 runtime chunk which is excluded + * @returns {Set} chunks + */ +const getAllChunks = (entrypoint, excludedChunk1, excludedChunk2) => { + const queue = new Set([entrypoint]); + const chunks = new Set(); + for (const entrypoint of queue) { + for (const chunk of entrypoint.chunks) { + if (chunk === excludedChunk1) continue; + if (chunk === excludedChunk2) continue; + chunks.add(chunk); + } + for (const parent of entrypoint.parentsIterable) { + if (parent instanceof Entrypoint) queue.add(parent); + } + } + return chunks; +}; +exports.getAllChunks = getAllChunks; diff --git a/lib/javascript/StartupHelpers.js b/lib/javascript/StartupHelpers.js index 3e6bdb5c4f9..0fa60eca4ab 100644 --- a/lib/javascript/StartupHelpers.js +++ b/lib/javascript/StartupHelpers.js @@ -5,10 +5,10 @@ "use strict"; -const Entrypoint = require("../Entrypoint"); const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const { isSubset } = require("../util/SetHelpers"); +const { getAllChunks } = require("./ChunkHelpers"); const { chunkHasJs } = require("./JavascriptModulesPlugin"); /** @typedef {import("../util/Hash")} Hash */ @@ -19,30 +19,6 @@ const { chunkHasJs } = require("./JavascriptModulesPlugin"); /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ /** @typedef {(string|number)[]} EntryItem */ -// TODO move to this file to ../javascript/ChunkHelpers.js - -/** - * @param {Entrypoint} entrypoint a chunk group - * @param {Chunk} excludedChunk1 current chunk which is excluded - * @param {Chunk} excludedChunk2 runtime chunk which is excluded - * @returns {Set} chunks - */ -const getAllChunks = (entrypoint, excludedChunk1, excludedChunk2) => { - const queue = new Set([entrypoint]); - const chunks = new Set(); - for (const entrypoint of queue) { - for (const chunk of entrypoint.chunks) { - if (chunk === excludedChunk1) continue; - if (chunk === excludedChunk2) continue; - chunks.add(chunk); - } - for (const parent of entrypoint.parentsIterable) { - if (parent instanceof Entrypoint) queue.add(parent); - } - } - return chunks; -}; - const EXPORT_PREFIX = "var __webpack_exports__ = "; /** diff --git a/test/ConfigTestCases.template.js b/test/ConfigTestCases.template.js index 3dcdbecb89c..0b20c35e363 100644 --- a/test/ConfigTestCases.template.js +++ b/test/ConfigTestCases.template.js @@ -305,6 +305,7 @@ const describeCases = config => { if (testConfig.beforeExecute) testConfig.beforeExecute(); const results = []; for (let i = 0; i < optionsArr.length; i++) { + const options = optionsArr[i]; const bundlePath = testConfig.findBundle(i, optionsArr[i]); if (bundlePath) { filesCount++; @@ -327,6 +328,43 @@ const describeCases = config => { const requireCache = Object.create(null); const esmCache = new Map(); const esmIdentifier = `${category.name}-${testName}-${i}`; + const baseModuleScope = { + console: console, + it: _it, + beforeEach: _beforeEach, + afterEach: _afterEach, + expect, + jest, + __STATS__: jsonStats, + nsObj: m => { + Object.defineProperty(m, Symbol.toStringTag, { + value: "Module" + }); + return m; + } + }; + + let runInNewContext = false; + if ( + options.target === "web" || + options.target === "webworker" + ) { + baseModuleScope.window = globalContext; + baseModuleScope.self = globalContext; + baseModuleScope.URL = URL; + baseModuleScope.Worker = + require("./helpers/createFakeWorker")({ + outputDirectory + }); + runInNewContext = true; + } + if (testConfig.moduleScope) { + testConfig.moduleScope(baseModuleScope); + } + const esmContext = vm.createContext(baseModuleScope, { + name: "context for esm" + }); + // eslint-disable-next-line no-loop-func const _require = ( currentDirectory, @@ -380,41 +418,7 @@ const describeCases = config => { options.experiments && options.experiments.outputModule; - let runInNewContext = false; - - const moduleScope = { - console: console, - it: _it, - beforeEach: _beforeEach, - afterEach: _afterEach, - expect, - jest, - __STATS__: jsonStats, - nsObj: m => { - Object.defineProperty(m, Symbol.toStringTag, { - value: "Module" - }); - return m; - } - }; - - if ( - options.target === "web" || - options.target === "webworker" - ) { - moduleScope.window = globalContext; - moduleScope.self = globalContext; - moduleScope.URL = URL; - moduleScope.Worker = - require("./helpers/createFakeWorker")({ - outputDirectory - }); - runInNewContext = true; - } if (isModule) { - if (testConfig.moduleScope) { - testConfig.moduleScope(moduleScope); - } if (!vm.SourceTextModule) throw new Error( "Running this test requires '--experimental-vm-modules'.\nRun with 'node --experimental-vm-modules node_modules/jest-cli/bin/jest'." @@ -424,11 +428,7 @@ const describeCases = config => { esm = new vm.SourceTextModule(content, { identifier: esmIdentifier + "-" + p, url: pathToFileURL(p).href + "?" + esmIdentifier, - context: - (parentModule && parentModule.context) || - vm.createContext(moduleScope, { - name: `context for ${p}` - }), + context: esmContext, initializeImportMeta: (meta, module) => { meta.url = pathToFileURL(p).href; }, @@ -488,7 +488,8 @@ const describeCases = config => { exports: {} }; requireCache[p] = m; - Object.assign(moduleScope, { + const moduleScope = { + ...baseModuleScope, require: _require.bind( null, path.dirname(p), @@ -511,7 +512,7 @@ const describeCases = config => { __dirname: path.dirname(p), __filename: p, _globalAssign: { expect } - }); + }; if (testConfig.moduleScope) { testConfig.moduleScope(moduleScope); } @@ -549,14 +550,14 @@ const describeCases = config => { results.push( _require( outputDirectory, - optionsArr[i], + options, "./" + bundlePathItem ) ); } } else { results.push( - _require(outputDirectory, optionsArr[i], bundlePath) + _require(outputDirectory, options, bundlePath) ); } } diff --git a/test/configCases/module/runtime-chunk/test.config.js b/test/configCases/module/runtime-chunk/test.config.js index d46441fe453..59523928e81 100644 --- a/test/configCases/module/runtime-chunk/test.config.js +++ b/test/configCases/module/runtime-chunk/test.config.js @@ -1,5 +1,5 @@ module.exports = { findBundle: function () { - return ["./runtime.js", "./main.js"]; + return ["./runtime.mjs", "./main.mjs"]; } }; diff --git a/test/configCases/module/runtime-chunk/webpack.config.js b/test/configCases/module/runtime-chunk/webpack.config.js index 78978ef2048..b1bdf750f81 100644 --- a/test/configCases/module/runtime-chunk/webpack.config.js +++ b/test/configCases/module/runtime-chunk/webpack.config.js @@ -1,9 +1,9 @@ /** @type {import("../../../../").Configuration} */ module.exports = { output: { - filename: "[name].js" + filename: "[name].mjs" }, - target: "web", + target: ["web", "es2020"], experiments: { outputModule: true }, diff --git a/test/configCases/module/split-chunks/index.js b/test/configCases/module/split-chunks/index.js new file mode 100644 index 00000000000..d5753c14d6b --- /dev/null +++ b/test/configCases/module/split-chunks/index.js @@ -0,0 +1,16 @@ +import value from "./separate"; +import { test as t } from "external-self"; + +it("should compile", () => { + expect(value).toBe(42); +}); +it("should circular depend on itself external", () => { + expect(test()).toBe(42); + expect(t()).toBe(42); +}); + +function test() { + return 42; +} + +export { test }; diff --git a/test/configCases/module/split-chunks/separate.js b/test/configCases/module/split-chunks/separate.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/configCases/module/split-chunks/separate.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/configCases/module/split-chunks/test.config.js b/test/configCases/module/split-chunks/test.config.js new file mode 100644 index 00000000000..b15222e4489 --- /dev/null +++ b/test/configCases/module/split-chunks/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function () { + return ["./runtime.mjs", "./separate.mjs", "./main.mjs"]; + } +}; diff --git a/test/configCases/module/split-chunks/webpack.config.js b/test/configCases/module/split-chunks/webpack.config.js new file mode 100644 index 00000000000..e8a91725c45 --- /dev/null +++ b/test/configCases/module/split-chunks/webpack.config.js @@ -0,0 +1,30 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + output: { + filename: "[name].mjs", + library: { + type: "module" + } + }, + target: ["web", "es2020"], + experiments: { + outputModule: true + }, + optimization: { + minimize: true, + runtimeChunk: "single", + splitChunks: { + cacheGroups: { + separate: { + test: /separate/, + chunks: "all", + filename: "separate.mjs", + enforce: true + } + } + } + }, + externals: { + "external-self": "./main.mjs" + } +};