diff --git a/lib/APIPlugin.js b/lib/APIPlugin.js index c71b099b165..d5d7ce50972 100644 --- a/lib/APIPlugin.js +++ b/lib/APIPlugin.js @@ -5,6 +5,7 @@ "use strict"; +const InitFragment = require("./InitFragment"); const { JAVASCRIPT_MODULE_TYPE_AUTO, JAVASCRIPT_MODULE_TYPE_DYNAMIC, @@ -14,6 +15,7 @@ const RuntimeGlobals = require("./RuntimeGlobals"); const WebpackError = require("./WebpackError"); const ConstDependency = require("./dependencies/ConstDependency"); const BasicEvaluatedExpression = require("./javascript/BasicEvaluatedExpression"); +const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const { toConstantDependency, evaluateToString @@ -24,103 +26,121 @@ const GetFullHashRuntimeModule = require("./runtime/GetFullHashRuntimeModule"); /** @typedef {import("./Compiler")} Compiler */ /** @typedef {import("./javascript/JavascriptParser")} JavascriptParser */ -/* eslint-disable camelcase */ -const REPLACEMENTS = { - __webpack_require__: { - expr: RuntimeGlobals.require, - req: [RuntimeGlobals.require], - type: "function", - assign: false - }, - __webpack_public_path__: { - expr: RuntimeGlobals.publicPath, - req: [RuntimeGlobals.publicPath], - type: "string", - assign: true - }, - __webpack_base_uri__: { - expr: RuntimeGlobals.baseURI, - req: [RuntimeGlobals.baseURI], - type: "string", - assign: true - }, - __webpack_modules__: { - expr: RuntimeGlobals.moduleFactories, - req: [RuntimeGlobals.moduleFactories], - type: "object", - assign: false - }, - __webpack_chunk_load__: { - expr: RuntimeGlobals.ensureChunk, - req: [RuntimeGlobals.ensureChunk], - type: "function", - assign: true - }, - __non_webpack_require__: { - expr: "require", - req: null, - type: undefined, // type is not known, depends on environment - assign: true - }, - __webpack_nonce__: { - expr: RuntimeGlobals.scriptNonce, - req: [RuntimeGlobals.scriptNonce], - type: "string", - assign: true - }, - __webpack_hash__: { - expr: `${RuntimeGlobals.getFullHash}()`, - req: [RuntimeGlobals.getFullHash], - type: "string", - assign: false - }, - __webpack_chunkname__: { - expr: RuntimeGlobals.chunkName, - req: [RuntimeGlobals.chunkName], - type: "string", - assign: false - }, - __webpack_get_script_filename__: { - expr: RuntimeGlobals.getChunkScriptFilename, - req: [RuntimeGlobals.getChunkScriptFilename], - type: "function", - assign: true - }, - __webpack_runtime_id__: { - expr: RuntimeGlobals.runtimeId, - req: [RuntimeGlobals.runtimeId], - assign: false - }, - "require.onError": { - expr: RuntimeGlobals.uncaughtErrorHandler, - req: [RuntimeGlobals.uncaughtErrorHandler], - type: undefined, // type is not known, could be function or undefined - assign: true // is never a pattern - }, - __system_context__: { - expr: RuntimeGlobals.systemContext, - req: [RuntimeGlobals.systemContext], - type: "object", - assign: false - }, - __webpack_share_scopes__: { - expr: RuntimeGlobals.shareScopeMap, - req: [RuntimeGlobals.shareScopeMap], - type: "object", - assign: false - }, - __webpack_init_sharing__: { - expr: RuntimeGlobals.initializeSharing, - req: [RuntimeGlobals.initializeSharing], - type: "function", - assign: true - } -}; -/* eslint-enable camelcase */ +/** + * @param {boolean} module true if ES module + * @param {string} importMetaName `import.meta` name + * @returns {Record} replacements + */ +function getReplacements(module, importMetaName) { + return { + __webpack_require__: { + expr: RuntimeGlobals.require, + req: [RuntimeGlobals.require], + type: "function", + assign: false + }, + __webpack_public_path__: { + expr: RuntimeGlobals.publicPath, + req: [RuntimeGlobals.publicPath], + type: "string", + assign: true + }, + __webpack_base_uri__: { + expr: RuntimeGlobals.baseURI, + req: [RuntimeGlobals.baseURI], + type: "string", + assign: true + }, + __webpack_modules__: { + expr: RuntimeGlobals.moduleFactories, + req: [RuntimeGlobals.moduleFactories], + type: "object", + assign: false + }, + __webpack_chunk_load__: { + expr: RuntimeGlobals.ensureChunk, + req: [RuntimeGlobals.ensureChunk], + type: "function", + assign: true + }, + __non_webpack_require__: { + expr: module + ? `__WEBPACK_EXTERNAL_createRequire(${importMetaName}.url)` + : "require", + req: null, + type: undefined, // type is not known, depends on environment + assign: true + }, + __webpack_nonce__: { + expr: RuntimeGlobals.scriptNonce, + req: [RuntimeGlobals.scriptNonce], + type: "string", + assign: true + }, + __webpack_hash__: { + expr: `${RuntimeGlobals.getFullHash}()`, + req: [RuntimeGlobals.getFullHash], + type: "string", + assign: false + }, + __webpack_chunkname__: { + expr: RuntimeGlobals.chunkName, + req: [RuntimeGlobals.chunkName], + type: "string", + assign: false + }, + __webpack_get_script_filename__: { + expr: RuntimeGlobals.getChunkScriptFilename, + req: [RuntimeGlobals.getChunkScriptFilename], + type: "function", + assign: true + }, + __webpack_runtime_id__: { + expr: RuntimeGlobals.runtimeId, + req: [RuntimeGlobals.runtimeId], + assign: false + }, + "require.onError": { + expr: RuntimeGlobals.uncaughtErrorHandler, + req: [RuntimeGlobals.uncaughtErrorHandler], + type: undefined, // type is not known, could be function or undefined + assign: true // is never a pattern + }, + __system_context__: { + expr: RuntimeGlobals.systemContext, + req: [RuntimeGlobals.systemContext], + type: "object", + assign: false + }, + __webpack_share_scopes__: { + expr: RuntimeGlobals.shareScopeMap, + req: [RuntimeGlobals.shareScopeMap], + type: "object", + assign: false + }, + __webpack_init_sharing__: { + expr: RuntimeGlobals.initializeSharing, + req: [RuntimeGlobals.initializeSharing], + type: "function", + assign: true + } + }; +} const PLUGIN_NAME = "APIPlugin"; +/** + * @typedef {Object} APIPluginOptions + * @property {boolean} [module] the output filename + */ + class APIPlugin { + /** + * @param {APIPluginOptions} [options] options + */ + constructor(options = {}) { + this.options = options; + } /** * Apply the plugin * @param {Compiler} compiler the compiler instance @@ -130,6 +150,12 @@ class APIPlugin { compiler.hooks.compilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { + const { importMetaName } = compilation.outputOptions; + const REPLACEMENTS = getReplacements( + this.options.module, + importMetaName + ); + compilation.dependencyTemplates.set( ConstDependency, new ConstDependency.Template() @@ -152,18 +178,43 @@ class APIPlugin { return true; }); + const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation); + + hooks.renderModuleContent.tap( + PLUGIN_NAME, + (source, module, renderContext) => { + if (module.buildInfo.needCreateRequire) { + const chunkInitFragments = [ + new InitFragment( + 'import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "module";\n', + InitFragment.STAGE_HARMONY_IMPORTS, + 0, + "external module node-commonjs" + ) + ]; + + renderContext.chunkInitFragments.push(...chunkInitFragments); + } + + return source; + } + ); + /** * @param {JavascriptParser} parser the parser */ const handler = parser => { Object.keys(REPLACEMENTS).forEach(key => { const info = REPLACEMENTS[key]; - parser.hooks.expression - .for(key) - .tap( - PLUGIN_NAME, - toConstantDependency(parser, info.expr, info.req) - ); + parser.hooks.expression.for(key).tap(PLUGIN_NAME, expression => { + const dep = toConstantDependency(parser, info.expr, info.req); + + if (key === "__non_webpack_require__" && this.options.module) { + parser.state.module.buildInfo.needCreateRequire = true; + } + + return dep(expression); + }); if (info.assign === false) { parser.hooks.assign.for(key).tap(PLUGIN_NAME, expr => { const err = new WebpackError(`${key} must not be assigned`); diff --git a/lib/ExternalModule.js b/lib/ExternalModule.js index b2bf8940949..9cff93a531f 100644 --- a/lib/ExternalModule.js +++ b/lib/ExternalModule.js @@ -104,9 +104,13 @@ const getSourceForCommonJsExternal = moduleAndSpecifiers => { /** * @param {string|string[]} moduleAndSpecifiers the module request + * @param {string} importMetaName import.meta name * @returns {SourceData} the generated source */ -const getSourceForCommonJsExternalInNodeModule = moduleAndSpecifiers => { +const getSourceForCommonJsExternalInNodeModule = ( + moduleAndSpecifiers, + importMetaName +) => { const chunkInitFragments = [ new InitFragment( 'import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "module";\n', @@ -117,18 +121,18 @@ const getSourceForCommonJsExternalInNodeModule = moduleAndSpecifiers => { ]; if (!Array.isArray(moduleAndSpecifiers)) { return { - expression: `__WEBPACK_EXTERNAL_createRequire(import.meta.url)(${JSON.stringify( + chunkInitFragments, + expression: `__WEBPACK_EXTERNAL_createRequire(${importMetaName}.url)(${JSON.stringify( moduleAndSpecifiers - )})`, - chunkInitFragments + )})` }; } const moduleName = moduleAndSpecifiers[0]; return { - expression: `__WEBPACK_EXTERNAL_createRequire(import.meta.url)(${JSON.stringify( + chunkInitFragments, + expression: `__WEBPACK_EXTERNAL_createRequire(${importMetaName}.url)(${JSON.stringify( moduleName - )})${propertyAccess(moduleAndSpecifiers, 1)}`, - chunkInitFragments + )})${propertyAccess(moduleAndSpecifiers, 1)}` }; }; @@ -557,7 +561,10 @@ class ExternalModule extends Module { return getSourceForCommonJsExternal(request); case "node-commonjs": return this.buildInfo.module - ? getSourceForCommonJsExternalInNodeModule(request) + ? getSourceForCommonJsExternalInNodeModule( + request, + runtimeTemplate.outputOptions.importMetaName + ) : getSourceForCommonJsExternal(request); case "amd": case "amd-require": diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 8c9ffa7b09a..ca8f4530b99 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -368,7 +368,9 @@ class WebpackOptionsApply extends OptionsApply { const NodeStuffPlugin = require("./NodeStuffPlugin"); new NodeStuffPlugin(options.node).apply(compiler); } - new APIPlugin().apply(compiler); + new APIPlugin({ + module: options.output.module + }).apply(compiler); new ExportsInfoApiPlugin().apply(compiler); new WebpackIsIncludedPlugin().apply(compiler); new ConstPlugin().apply(compiler); diff --git a/lib/javascript/JavascriptModulesPlugin.js b/lib/javascript/JavascriptModulesPlugin.js index 45666a9a0ad..4d33a29d617 100644 --- a/lib/javascript/JavascriptModulesPlugin.js +++ b/lib/javascript/JavascriptModulesPlugin.js @@ -937,6 +937,7 @@ class JavascriptModulesPlugin { "JavascriptModulesPlugin error: JavascriptModulesPlugin.getCompilationHooks().renderContent plugins should return something" ); } + finalSource = InitFragment.addToSource( finalSource, chunkRenderContext.chunkInitFragments, diff --git a/lib/node/ReadFileCompileAsyncWasmPlugin.js b/lib/node/ReadFileCompileAsyncWasmPlugin.js index 5f0a1709bb5..886ac121e70 100644 --- a/lib/node/ReadFileCompileAsyncWasmPlugin.js +++ b/lib/node/ReadFileCompileAsyncWasmPlugin.js @@ -40,6 +40,7 @@ class ReadFileCompileAsyncWasmPlugin { : globalWasmLoading; return wasmLoading === this._type; }; + const { importMetaName } = compilation.outputOptions; /** * @type {(path: string) => string} */ @@ -48,7 +49,7 @@ class ReadFileCompileAsyncWasmPlugin { Template.asString([ "Promise.all([import('fs'), import('url')]).then(([{ readFile }, { URL }]) => new Promise((resolve, reject) => {", Template.indent([ - `readFile(new URL(${path}, import.meta.url), (err, buffer) => {`, + `readFile(new URL(${path}, ${importMetaName}.url), (err, buffer) => {`, Template.indent([ "if (err) return reject(err);", "", diff --git a/test/ConfigTestCases.template.js b/test/ConfigTestCases.template.js index df68b068fa6..c0c00fb8785 100644 --- a/test/ConfigTestCases.template.js +++ b/test/ConfigTestCases.template.js @@ -441,6 +441,9 @@ const describeCases = config => { ) { baseModuleScope.window = globalContext; baseModuleScope.self = globalContext; + baseModuleScope.document = globalContext.document; + baseModuleScope.setTimeout = globalContext.setTimeout; + baseModuleScope.clearTimeout = globalContext.clearTimeout; baseModuleScope.URL = URL; baseModuleScope.Worker = require("./helpers/createFakeWorker")({ diff --git a/test/configCases/output-module/issue-16040/index.js b/test/configCases/output-module/issue-16040/index.js index cecb68042a2..da656cdd0f0 100644 --- a/test/configCases/output-module/issue-16040/index.js +++ b/test/configCases/output-module/issue-16040/index.js @@ -1,8 +1,6 @@ import foo from "./foo.js"; import bar from "./bar.js"; -console.log(foo + bar); - it("should not contain non javascript chunk in the main bundle", () => { const fs = require("fs"); const source = fs.readFileSync(__STATS__.outputPath + "/main.mjs", "utf-8"); @@ -12,4 +10,5 @@ it("should not contain non javascript chunk in the main bundle", () => { expect(source).not.toMatch( /import\s\*\sas+\s__webpack_chunk_[0-9]+__\sfrom\s"\.\/style\.mjs"/g ); + expect(foo + bar).toBe(12); }); diff --git a/test/configCases/output-module/non-webpack-require/baz.js b/test/configCases/output-module/non-webpack-require/baz.js new file mode 100644 index 00000000000..ab1913ce984 --- /dev/null +++ b/test/configCases/output-module/non-webpack-require/baz.js @@ -0,0 +1 @@ +export default "baz module text"; diff --git a/test/configCases/output-module/non-webpack-require/index.js b/test/configCases/output-module/non-webpack-require/index.js new file mode 100644 index 00000000000..79e36df4e9f --- /dev/null +++ b/test/configCases/output-module/non-webpack-require/index.js @@ -0,0 +1,16 @@ +import { createRequire as func_create_require, builtinModules as builtin } from "module"; +import external from "external-module"; +import externalOther from "external-other-module"; +import baz from "./baz.js"; + +it("should work with __non_webpack_require__ and ES modules", function () { + const foo = __non_webpack_require__("./mod.js"); + + expect(foo).toBe("module text"); + expect(external).toBe("external module text"); + expect(externalOther).toBe("external module text"); + expect(baz).toBe("baz module text"); + expect(typeof func_create_require).toBe("function"); + expect(func_create_require(import.meta.url)("./mod.js")).toBe("module text"); + expect(typeof builtin).toBe("object") +}); diff --git a/test/configCases/output-module/non-webpack-require/mod.js b/test/configCases/output-module/non-webpack-require/mod.js new file mode 100644 index 00000000000..af5c7eea34c --- /dev/null +++ b/test/configCases/output-module/non-webpack-require/mod.js @@ -0,0 +1 @@ +module.exports = "module text"; diff --git a/test/configCases/output-module/non-webpack-require/test.filter.js b/test/configCases/output-module/non-webpack-require/test.filter.js new file mode 100644 index 00000000000..ad4dc826959 --- /dev/null +++ b/test/configCases/output-module/non-webpack-require/test.filter.js @@ -0,0 +1,5 @@ +const supportsRequireInModule = require("../../../helpers/supportsRequireInModule"); + +module.exports = () => { + return supportsRequireInModule(); +}; diff --git a/test/configCases/output-module/non-webpack-require/webpack.config.js b/test/configCases/output-module/non-webpack-require/webpack.config.js new file mode 100644 index 00000000000..44c26e1c34d --- /dev/null +++ b/test/configCases/output-module/non-webpack-require/webpack.config.js @@ -0,0 +1,58 @@ +var webpack = require("../../../../"); + +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: ["node", "es2020"], + experiments: { + outputModule: true + }, + output: { + module: true, + iife: true + }, + externals: { + "external-module": "node-commonjs external-module", + "external-other-module": ["node-commonjs external-module"] + }, + optimization: { + concatenateModules: false + }, + plugins: [ + { + apply(compiler) { + compiler.hooks.compilation.tap("Test", compilation => { + compilation.hooks.processAssets.tap( + { + name: "copy-webpack-plugin", + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL + }, + () => { + compilation.emitAsset( + "mod.js", + new webpack.sources.RawSource( + "module.exports = 'module text';\n" + ) + ); + } + ); + compilation.hooks.processAssets.tap( + { + name: "copy-webpack-plugin", + stage: + compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL + }, + () => { + compilation.emitAsset( + "node_modules/external-module/index.js", + new webpack.sources.RawSource( + "module.exports = 'external module text';\n" + ) + ); + } + ); + }); + } + } + ] +}; diff --git a/test/configCases/web/prefetch-preload-module/index.js b/test/configCases/web/prefetch-preload-module/index.js deleted file mode 100644 index 86c0ff0800c..00000000000 --- a/test/configCases/web/prefetch-preload-module/index.js +++ /dev/null @@ -1,90 +0,0 @@ -// This config need to be set on initial evaluation to be effective -__webpack_nonce__ = "nonce"; -__webpack_public_path__ = "https://example.com/public/path/"; - -it("should prefetch and preload child chunks on chunk load", () => { - let link, script; - - expect(document.head._children).toHaveLength(1); - - // Test prefetch from entry chunk - link = document.head._children[0]; - expect(link._type).toBe("link"); - expect(link.rel).toBe("prefetch"); - expect(link.href).toBe("https://example.com/public/path/chunk1.js"); - - const promise = import( - /* webpackChunkName: "chunk1", webpackPrefetch: true */ "./chunk1" - ); - - expect(document.head._children).toHaveLength(3); - - // Test normal script loading - script = document.head._children[1]; - expect(script._type).toBe("script"); - expect(script.src).toBe("https://example.com/public/path/chunk1.js"); - expect(script.getAttribute("nonce")).toBe("nonce"); - expect(script.crossOrigin).toBe("anonymous"); - expect(script.onload).toBeTypeOf("function"); - - // Test preload of chunk1-b - link = document.head._children[2]; - expect(link._type).toBe("link"); - expect(link.rel).toBe("preload"); - expect(link.as).toBe("script"); - expect(link.href).toBe("https://example.com/public/path/chunk1-b.js"); - expect(link.charset).toBe("utf-8"); - expect(link.getAttribute("nonce")).toBe("nonce"); - expect(link.crossOrigin).toBe("anonymous"); - - // Run the script - __non_webpack_require__("./chunk1.js"); - - script.onload(); - - return promise.then(() => { - expect(document.head._children).toHaveLength(4); - - // Test prefetching for chunk1-c and chunk1-a in this order - link = document.head._children[2]; - expect(link._type).toBe("link"); - expect(link.rel).toBe("prefetch"); - expect(link.href).toBe("https://example.com/public/path/chunk1-c.js"); - expect(link.crossOrigin).toBe("anonymous"); - - link = document.head._children[3]; - expect(link._type).toBe("link"); - expect(link.rel).toBe("prefetch"); - expect(link.href).toBe("https://example.com/public/path/chunk1-a.js"); - expect(link.crossOrigin).toBe("anonymous"); - - const promise2 = import( - /* webpackChunkName: "chunk1", webpackPrefetch: true */ "./chunk1" - ); - - // Loading chunk1 again should not trigger prefetch/preload - expect(document.head._children).toHaveLength(4); - - const promise3 = import(/* webpackChunkName: "chunk2" */ "./chunk2"); - - expect(document.head._children).toHaveLength(5); - - // Test normal script loading - script = document.head._children[4]; - expect(script._type).toBe("script"); - expect(script.src).toBe("https://example.com/public/path/chunk2.js"); - expect(script.getAttribute("nonce")).toBe("nonce"); - expect(script.crossOrigin).toBe("anonymous"); - expect(script.onload).toBeTypeOf("function"); - - // Run the script - __non_webpack_require__("./chunk2.js"); - - script.onload(); - - return promise3.then(() => { - // Loading chunk2 again should not trigger prefetch/preload as it's already prefetch/preloaded - expect(document.head._children).toHaveLength(4); - }); - }); -}); diff --git a/test/configCases/web/prefetch-preload-module/index.mjs b/test/configCases/web/prefetch-preload-module/index.mjs index 63f67a46ead..ebe1e6de07a 100644 --- a/test/configCases/web/prefetch-preload-module/index.mjs +++ b/test/configCases/web/prefetch-preload-module/index.mjs @@ -37,7 +37,7 @@ it("should prefetch and preload child chunks on chunk load", () => { expect(link.crossOrigin).toBe("anonymous"); // Run the script - __non_webpack_require__("./chunk1.js"); + import(/* webpackIgnore: true */ "./chunk1.js"); script.onload(); @@ -77,7 +77,7 @@ it("should prefetch and preload child chunks on chunk load", () => { expect(script.onload).toBeTypeOf("function"); // Run the script - __non_webpack_require__("./chunk2.js"); + import(/* webpackIgnore: true */ "./chunk2.js"); script.onload(); diff --git a/test/configCases/web/prefetch-preload-module/webpack.config.js b/test/configCases/web/prefetch-preload-module/webpack.config.js index cd4e599f156..1bc385d226e 100644 --- a/test/configCases/web/prefetch-preload-module/webpack.config.js +++ b/test/configCases/web/prefetch-preload-module/webpack.config.js @@ -9,7 +9,7 @@ module.exports = { output: { publicPath: "", module: true, - filename: "bundle0.js", + filename: "bundle0.mjs", chunkFilename: "[name].js", crossOriginLoading: "anonymous" },