From a66290a15fc7474b4ffacd1680387cfee290d35c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 15 May 2020 16:24:11 +0200 Subject: [PATCH] add support for async externals add `promise` external add `import` external (uses import()) add `output.importFunctionName` option to change the `import()` function name allow for inline external type when using arrays fix some typings fix namespace object behavior when using system external and accessing nested property fix interop behavior for async dynamic modules --- declarations/WebpackOptions.d.ts | 19 +++- .../container/ContainerReferencePlugin.d.ts | 10 ++- .../container/ModuleFederationPlugin.d.ts | 24 ++++- lib/DllReferencePlugin.js | 2 + lib/ExternalModule.js | 53 +++++++++-- lib/ExternalModuleFactoryPlugin.js | 49 ++++++++-- lib/ExternalsPlugin.js | 5 ++ lib/InitFragment.js | 1 + lib/RuntimeTemplate.js | 89 +++++++++++++------ lib/config/defaults.js | 1 + lib/config/normalization.js | 1 + lib/dependencies/HarmonyAcceptDependency.js | 5 +- lib/dependencies/HarmonyImportDependency.js | 36 ++++++-- lib/library/SystemLibraryPlugin.js | 11 ++- .../AsyncWebAssemblyJavascriptGenerator.js | 32 ++++--- lib/wasm/WebAssemblyJavascriptGenerator.js | 2 +- schemas/WebpackOptions.json | 16 +++- .../container/ContainerReferencePlugin.json | 10 ++- .../container/ModuleFederationPlugin.json | 25 +++++- .../__snapshots__/StatsTestCases.test.js.snap | 2 +- .../externals/async-externals/index.js | 15 ++++ .../async-externals/webpack.config.js | 16 ++++ types.d.ts | 55 +++++++++--- 23 files changed, 381 insertions(+), 98 deletions(-) create mode 100644 test/configCases/externals/async-externals/index.js create mode 100644 test/configCases/externals/async-externals/webpack.config.js diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index 6f53b672b..8c265488d 100644 --- a/declarations/WebpackOptions.d.ts +++ b/declarations/WebpackOptions.d.ts @@ -127,8 +127,7 @@ export type ExternalItem = }; } | (( - context: string, - request: string, + data: {context: string; request: string}, callback: (err?: Error, result?: string) => void ) => void); /** @@ -150,7 +149,9 @@ export type ExternalsType = | "umd" | "umd2" | "jsonp" - | "system"; + | "system" + | "promise" + | "import"; /** * Filtering values. */ @@ -397,6 +398,10 @@ export type HotUpdateMainFilename = string; * Wrap javascript code into IIFE's to avoid leaking into global scope. */ export type Iife = boolean; +/** + * The name of the native import() function (can be exchanged for a polyfill). + */ +export type ImportFunctionName = string; /** * The JSONP function used by webpack for async loading of chunks. */ @@ -1586,6 +1591,10 @@ export interface Output { * Wrap javascript code into IIFE's to avoid leaking into global scope. */ iife?: Iife; + /** + * The name of the native import() function (can be exchanged for a polyfill). + */ + importFunctionName?: ImportFunctionName; /** * The JSONP function used by webpack for async loading of chunks. */ @@ -2043,6 +2052,10 @@ export interface OutputNormalized { * Wrap javascript code into IIFE's to avoid leaking into global scope. */ iife?: Iife; + /** + * The name of the native import() function (can be exchanged for a polyfill). + */ + importFunctionName?: ImportFunctionName; /** * The JSONP function used by webpack for async loading of chunks. */ diff --git a/declarations/plugins/container/ContainerReferencePlugin.d.ts b/declarations/plugins/container/ContainerReferencePlugin.d.ts index 64b069893..ee523741d 100644 --- a/declarations/plugins/container/ContainerReferencePlugin.d.ts +++ b/declarations/plugins/container/ContainerReferencePlugin.d.ts @@ -13,9 +13,9 @@ export type Overrides = (OverridesItem | OverridesObject)[] | OverridesObject; */ export type OverridesItem = string; /** - * Type of library. + * Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value). */ -export type LibraryType = +export type ExternalsType = | "var" | "module" | "assign" @@ -31,7 +31,9 @@ export type LibraryType = | "umd" | "umd2" | "jsonp" - | "system"; + | "system" + | "promise" + | "import"; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. */ @@ -53,7 +55,7 @@ export interface ContainerReferencePluginOptions { /** * The external type of the remote containers. */ - remoteType: LibraryType; + remoteType: ExternalsType; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. */ diff --git a/declarations/plugins/container/ModuleFederationPlugin.d.ts b/declarations/plugins/container/ModuleFederationPlugin.d.ts index 33e7cdeea..f43de301c 100644 --- a/declarations/plugins/container/ModuleFederationPlugin.d.ts +++ b/declarations/plugins/container/ModuleFederationPlugin.d.ts @@ -74,6 +74,28 @@ export type Overrides = (OverridesItem | OverridesObject)[] | OverridesObject; * Request to a module in this container that should override overridable modules in the remote container. */ export type OverridesItem = string; +/** + * Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value). + */ +export type ExternalsType = + | "var" + | "module" + | "assign" + | "this" + | "window" + | "self" + | "global" + | "commonjs" + | "commonjs2" + | "commonjs-module" + | "amd" + | "amd-require" + | "umd" + | "umd2" + | "jsonp" + | "system" + | "promise" + | "import"; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. */ @@ -123,7 +145,7 @@ export interface ModuleFederationPluginOptions { /** * The external type of the remote containers. */ - remoteType?: LibraryType; + remoteType?: ExternalsType; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. */ diff --git a/lib/DllReferencePlugin.js b/lib/DllReferencePlugin.js index 89aa9645e..73cbccdb3 100644 --- a/lib/DllReferencePlugin.js +++ b/lib/DllReferencePlugin.js @@ -15,6 +15,7 @@ const makePathsRelative = require("./util/identifier").makePathsRelative; const validateOptions = require("schema-utils"); const schema = require("../schemas/plugins/DllReferencePlugin.json"); +/** @typedef {import("../declarations/WebpackOptions").Externals} Externals */ /** @typedef {import("../declarations/plugins/DllReferencePlugin").DllReferencePluginOptions} DllReferencePluginOptions */ /** @typedef {import("../declarations/plugins/DllReferencePlugin").DllReferencePluginOptionsManifest} DllReferencePluginOptionsManifest */ @@ -106,6 +107,7 @@ class DllReferencePlugin { if (!content) content = manifest.content; } } + /** @type {Externals} */ const externals = {}; const source = "dll-reference " + name; externals[source] = name; diff --git a/lib/ExternalModule.js b/lib/ExternalModule.js index ca74438b7..602b24ca7 100644 --- a/lib/ExternalModule.js +++ b/lib/ExternalModule.js @@ -55,13 +55,35 @@ const getSourceForCommonJsExternal = moduleAndSpecifiers => { return `module.exports = require(${JSON.stringify(moduleAndSpecifiers)});`; } const moduleName = moduleAndSpecifiers[0]; - const objectLookup = moduleAndSpecifiers - .slice(1) - .map(r => `[${JSON.stringify(r)}]`) - .join(""); return `module.exports = require(${JSON.stringify( moduleName - )})${objectLookup};`; + )})${propertyAccess(moduleAndSpecifiers, 1)};`; +}; + +/** + * @param {string|string[]} moduleAndSpecifiers the module request + * @param {RuntimeTemplate} runtimeTemplate the runtime template + * @returns {string} the generated source + */ +const getSourceForImportExternal = (moduleAndSpecifiers, runtimeTemplate) => { + const importName = runtimeTemplate.outputOptions.importFunctionName; + if (!Array.isArray(moduleAndSpecifiers)) { + return `module.exports = ${importName}(${JSON.stringify( + moduleAndSpecifiers + )});`; + } + if (moduleAndSpecifiers.length === 1) { + return `module.exports = ${importName}(${JSON.stringify( + moduleAndSpecifiers[0] + )});`; + } + const moduleName = moduleAndSpecifiers[0]; + return `module.exports = ${importName}(${JSON.stringify( + moduleName + )}).then(${runtimeTemplate.returningFunction( + `module${propertyAccess(moduleAndSpecifiers, 1)}`, + "module" + )});`; }; /** @@ -196,15 +218,27 @@ class ExternalModule extends Module { */ build(options, compilation, resolver, fs, callback) { this.buildMeta = { + async: false, exportsType: undefined }; this.buildInfo = { strict: true }; this.clearDependenciesAndBlocks(); - if (this.externalType === "system") { - this.buildMeta.exportsType = "namespace"; - this.addDependency(new StaticExportsDependency(true, true)); + switch (this.externalType) { + case "system": + if (!Array.isArray(this.request) || this.request.length === 1) + this.buildMeta.exportsType = "namespace"; + this.addDependency(new StaticExportsDependency(true, true)); + break; + case "promise": + this.buildMeta.async = true; + break; + case "import": + this.buildMeta.async = true; + if (!Array.isArray(this.request) || this.request.length === 1) + this.buildMeta.exportsType = "namespace"; + break; } callback(); } @@ -240,9 +274,12 @@ class ExternalModule extends Module { request, runtimeTemplate ); + case "import": + return getSourceForImportExternal(request, runtimeTemplate); case "module": throw new Error("Module external type is not implemented yet"); case "var": + case "promise": case "const": case "let": case "assign": diff --git a/lib/ExternalModuleFactoryPlugin.js b/lib/ExternalModuleFactoryPlugin.js index fab1cfdbb..b1e2caa45 100644 --- a/lib/ExternalModuleFactoryPlugin.js +++ b/lib/ExternalModuleFactoryPlugin.js @@ -7,6 +7,10 @@ const util = require("util"); const ExternalModule = require("./ExternalModule"); + +/** @typedef {import("../declarations/WebpackOptions").Externals} Externals */ +/** @typedef {import("./NormalModuleFactory")} NormalModuleFactory */ + const UNSPECIFIED_EXTERNAL_TYPE_REGEXP = /^[a-z0-9]+ /; // TODO webpack 6 remove this @@ -19,11 +23,19 @@ const callDeprecatedExternals = util.deprecate( ); class ExternalModuleFactoryPlugin { + /** + * @param {string | undefined} type default external type + * @param {Externals} externals externals config + */ constructor(type, externals) { this.type = type; this.externals = externals; } + /** + * @param {NormalModuleFactory} normalModuleFactory the normal module factory + * @returns {void} + */ apply(normalModuleFactory) { const globalType = this.type; normalModuleFactory.hooks.factorize.tapAsync( @@ -33,7 +45,7 @@ class ExternalModuleFactoryPlugin { const dependency = data.dependencies[0]; /** - * @param {string|boolean} value the external config + * @param {string|string[]|boolean|Record} value the external config * @param {string|undefined} type type of external * @param {function(Error=, ExternalModule=): void} callback callback * @returns {void} @@ -43,7 +55,7 @@ class ExternalModuleFactoryPlugin { // Not externals, fallback to original factory return callback(); } - /** @type {string} */ + /** @type {string | string[] | Record} */ let externalConfig; if (value === true) { externalConfig = dependency.request; @@ -51,13 +63,27 @@ class ExternalModuleFactoryPlugin { externalConfig = value; } // When no explicit type is specified, extract it from the externalConfig - if ( - type === undefined && - UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig) - ) { - const idx = externalConfig.indexOf(" "); - type = externalConfig.substr(0, idx); - externalConfig = externalConfig.substr(idx + 1); + if (type === undefined) { + if ( + typeof externalConfig === "string" && + UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig) + ) { + const idx = externalConfig.indexOf(" "); + type = externalConfig.substr(0, idx); + externalConfig = externalConfig.substr(idx + 1); + } else if ( + Array.isArray(externalConfig) && + externalConfig.length > 0 && + UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0]) + ) { + const firstItem = externalConfig[0]; + const idx = firstItem.indexOf(" "); + type = firstItem.substr(0, idx); + externalConfig = [ + firstItem.substr(idx + 1), + ...externalConfig.slice(1) + ]; + } } callback( null, @@ -69,6 +95,11 @@ class ExternalModuleFactoryPlugin { ); }; + /** + * @param {Externals} externals externals config + * @param {function(Error=, ExternalModule=): void} callback callback + * @returns {void} + */ const handleExternals = (externals, callback) => { if (typeof externals === "string") { if (externals === dependency.request) { diff --git a/lib/ExternalsPlugin.js b/lib/ExternalsPlugin.js index 1dadccc7c..01e746907 100644 --- a/lib/ExternalsPlugin.js +++ b/lib/ExternalsPlugin.js @@ -7,9 +7,14 @@ const ExternalModuleFactoryPlugin = require("./ExternalModuleFactoryPlugin"); +/** @typedef {import("../declarations/WebpackOptions").Externals} Externals */ /** @typedef {import("./Compiler")} Compiler */ class ExternalsPlugin { + /** + * @param {string | undefined} type default external type + * @param {Externals} externals externals config + */ constructor(type, externals) { this.type = type; this.externals = externals; diff --git a/lib/InitFragment.js b/lib/InitFragment.js index 11d73d518..ea97de437 100644 --- a/lib/InitFragment.js +++ b/lib/InitFragment.js @@ -115,5 +115,6 @@ InitFragment.STAGE_HARMONY_EXPORTS = 30; InitFragment.STAGE_HARMONY_IMPORTS = 40; InitFragment.STAGE_PROVIDES = 50; InitFragment.STAGE_ASYNC_DEPENDENCIES = 60; +InitFragment.STAGE_ASYNC_HARMONY_IMPORTS = 70; module.exports = InitFragment; diff --git a/lib/RuntimeTemplate.js b/lib/RuntimeTemplate.js index 7a26af7da..9569ec358 100644 --- a/lib/RuntimeTemplate.js +++ b/lib/RuntimeTemplate.js @@ -10,7 +10,7 @@ const RuntimeGlobals = require("./RuntimeGlobals"); const Template = require("./Template"); const propertyAccess = require("./util/propertyAccess"); -/** @typedef {import("../declarations/WebpackOptions").Output} OutputOptions */ +/** @typedef {import("../declarations/WebpackOptions").OutputNormalized} OutputOptions */ /** @typedef {import("./AsyncDependenciesBlock")} AsyncDependenciesBlock */ /** @typedef {import("./ChunkGraph")} ChunkGraph */ /** @typedef {import("./InitFragment")} InitFragment */ @@ -398,7 +398,7 @@ class RuntimeTemplate { runtimeRequirements }); - let getModuleFunction; + let appending; let idExpr = JSON.stringify(chunkGraph.getModuleId(module)); const comment = this.comment({ request @@ -439,13 +439,13 @@ class RuntimeTemplate { weak, runtimeRequirements }); - getModuleFunction = this.basicFunction( + appending = `.then(${this.basicFunction( "", `${header}return ${rawModule};` - ); + )})`; } else { runtimeRequirements.add(RuntimeGlobals.require); - getModuleFunction = `__webpack_require__.bind(__webpack_require__, ${comment}${idExpr})`; + appending = `.then(__webpack_require__.bind(__webpack_require__, ${comment}${idExpr}))`; } break; case "dynamic": @@ -457,18 +457,42 @@ class RuntimeTemplate { case "default-only": fakeType |= 1; runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); - if (header) { - const returnExpression = `${RuntimeGlobals.createFakeNamespaceObject}(${moduleIdExpr}, ${fakeType})`; - getModuleFunction = header - ? this.basicFunction("", `${header}return ${returnExpression};`) - : this.returningFunction(returnExpression); + if (chunkGraph.moduleGraph.isAsync(module)) { + if (header) { + const rawModule = this.moduleRaw({ + module, + chunkGraph, + request, + weak, + runtimeRequirements + }); + appending = `.then(${this.basicFunction( + "", + `${header}return ${rawModule};` + )})`; + } else { + runtimeRequirements.add(RuntimeGlobals.require); + appending = `.then(__webpack_require__.bind(__webpack_require__, ${comment}${idExpr}))`; + } + appending += `.then(${this.returningFunction( + `${RuntimeGlobals.createFakeNamespaceObject}(m, ${fakeType})`, + "m" + )})`; } else { - getModuleFunction = `${RuntimeGlobals.createFakeNamespaceObject}.bind(__webpack_require__, ${comment}${idExpr}, ${fakeType})`; + if (header) { + const returnExpression = `${RuntimeGlobals.createFakeNamespaceObject}(${moduleIdExpr}, ${fakeType})`; + appending = `.then(${this.basicFunction( + "", + `${header}return ${returnExpression};` + )})`; + } else { + appending = `.then(${RuntimeGlobals.createFakeNamespaceObject}.bind(__webpack_require__, ${comment}${idExpr}, ${fakeType}))`; + } } break; } - return `${promise || "Promise.resolve()"}.then(${getModuleFunction})`; + return `${promise || "Promise.resolve()"}${appending}`; } /** @@ -482,7 +506,7 @@ class RuntimeTemplate { * @param {Module} options.originModule module in which the statement is emitted * @param {boolean=} options.weak true, if this is a weak dependency * @param {Set} options.runtimeRequirements if set, will be filled with runtime requirements - * @returns {string} the import statement + * @returns {[string, string]} the import statement and the compat statement */ importStatement({ update, @@ -495,20 +519,26 @@ class RuntimeTemplate { runtimeRequirements }) { if (!module) { - return this.missingModuleStatement({ - request - }); + return [ + this.missingModuleStatement({ + request + }), + "" + ]; } if (chunkGraph.getModuleId(module) === null) { if (weak) { // only weak referenced modules don't get an id // we can always emit an error emitting code here - return this.weakError({ - module, - chunkGraph, - request, - type: "statements" - }); + return [ + this.weakError({ + module, + chunkGraph, + request, + type: "statements" + }), + "" + ]; } throw new Error( `RuntimeTemplate.importStatement(): Module ${module.identifier()} has no id. This should not happen.` @@ -522,15 +552,20 @@ class RuntimeTemplate { }); const optDeclaration = update ? "" : "var "; - const exportsType = module.buildMeta && module.buildMeta.exportsType; + const exportsType = module.getExportsType( + originModule.buildMeta.strictHarmonyModule + ); runtimeRequirements.add(RuntimeGlobals.require); - let content = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`; + const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`; - if (!exportsType && !originModule.buildMeta.strictHarmonyModule) { + if (exportsType === "dynamic") { runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport); - content += `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n`; + return [ + importContent, + `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n` + ]; } - return content; + return [importContent, ""]; } /** diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 14fb73ab4..0934a8a1a 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -379,6 +379,7 @@ const applyOutputDefaults = ( F(output, "module", () => !!outputModule); F(output, "iife", () => !output.module); D(output, "ecmaVersion", 6); + D(output, "importFunctionName", "import"); F(output, "chunkFilename", () => { const filename = output.filename; if (typeof filename !== "function") { diff --git a/lib/config/normalization.js b/lib/config/normalization.js index 18075b418..4c7373a80 100644 --- a/lib/config/normalization.js +++ b/lib/config/normalization.js @@ -202,6 +202,7 @@ const getNormalizedWebpackOptions = config => { hotUpdateFunction: output.hotUpdateFunction, hotUpdateMainFilename: output.hotUpdateMainFilename, iife: output.iife, + importFunctionName: output.importFunctionName, jsonpFunction: output.jsonpFunction, jsonpScriptType: output.jsonpScriptType, library: libraryBase && { diff --git a/lib/dependencies/HarmonyAcceptDependency.js b/lib/dependencies/HarmonyAcceptDependency.js index 906a45e89..665998e17 100644 --- a/lib/dependencies/HarmonyAcceptDependency.js +++ b/lib/dependencies/HarmonyAcceptDependency.js @@ -67,7 +67,10 @@ HarmonyAcceptDependency.Template = class HarmonyAcceptDependencyTemplate extends .filter(dependency => HarmonyImportDependency.Template.isImportEmitted(dependency, module) ) - .map(dependency => dependency.getImportStatement(true, templateContext)) + .map(dependency => { + const s = dependency.getImportStatement(true, templateContext); + return s[0] + s[1]; + }) .join(""); if (dep.hasCallback) { diff --git a/lib/dependencies/HarmonyImportDependency.js b/lib/dependencies/HarmonyImportDependency.js index 27af25597..dbbafe3e9 100644 --- a/lib/dependencies/HarmonyImportDependency.js +++ b/lib/dependencies/HarmonyImportDependency.js @@ -65,7 +65,7 @@ class HarmonyImportDependency extends ModuleDependency { /** * @param {boolean} update create new variables or update existing one * @param {DependencyTemplateContext} templateContext the template context - * @returns {string} name of the variable for the import + * @returns {[string, string]} the import statement and the compat statement */ getImportStatement( update, @@ -253,20 +253,38 @@ HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends emittedModules.add(module); } - templateContext.initFragments.push( - new InitFragment( - dep.getImportStatement(false, templateContext), - InitFragment.STAGE_HARMONY_IMPORTS, - dep.sourceOrder, - key - ) - ); + const importStatement = dep.getImportStatement(false, templateContext); if (templateContext.moduleGraph.isAsync(referencedModule)) { + templateContext.initFragments.push( + new InitFragment( + importStatement[0], + InitFragment.STAGE_HARMONY_IMPORTS, + dep.sourceOrder, + key + ) + ); templateContext.initFragments.push( new AwaitDependenciesInitFragment( new Set([dep.getImportVar(templateContext.moduleGraph)]) ) ); + templateContext.initFragments.push( + new InitFragment( + importStatement[1], + InitFragment.STAGE_ASYNC_HARMONY_IMPORTS, + dep.sourceOrder, + key + " compat" + ) + ); + } else { + templateContext.initFragments.push( + new InitFragment( + importStatement[0] + importStatement[1], + InitFragment.STAGE_HARMONY_IMPORTS, + dep.sourceOrder, + key + ) + ); } } diff --git a/lib/library/SystemLibraryPlugin.js b/lib/library/SystemLibraryPlugin.js index c0a845890..cbdd8c178 100644 --- a/lib/library/SystemLibraryPlugin.js +++ b/lib/library/SystemLibraryPlugin.js @@ -139,9 +139,14 @@ class SystemLibraryPlugin extends AbstractLibraryPlugin { } } if (!otherUnused) { - externalVarInitialization.push( - `Object.defineProperty(${external}, "__esModule", { value: true });` - ); + if ( + !Array.isArray(module.request) || + module.request.length === 1 + ) { + externalVarInitialization.push( + `Object.defineProperty(${external}, "__esModule", { value: true });` + ); + } if (handledNames.length > 0) { const name = `${external}handledNames`; externalVarInitialization.push( diff --git a/lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js b/lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js index bc2a8a0f1..20eaed2ce 100644 --- a/lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js +++ b/lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js @@ -103,6 +103,8 @@ class AsyncWebAssemblyJavascriptGenerator extends Generator { }); } ); + const importsCode = importStatements.map(([x]) => x).join(""); + const importsCompatCode = importStatements.map(([_, x]) => x).join(""); const importObjRequestItems = Array.from( wasmDepsByRequest, @@ -149,18 +151,28 @@ class AsyncWebAssemblyJavascriptGenerator extends Generator { (importsObj ? `, ${importsObj})` : `)`); const source = new RawSource( - Template.asString([ - ...importStatements, + `${importsCode}${ promises.length > 1 - ? `${module.moduleArgument}.exports = Promise.all([${promises.join( - ", " - )}]).then(function([${promises.join( - ", " - )}]) { return ${instantiateCall}; })` + ? Template.asString([ + `${module.moduleArgument}.exports = Promise.all([${promises.join( + ", " + )}]).then(${runtimeTemplate.basicFunction( + `[${promises.join(", ")}]`, + `${importsCompatCode}return ${instantiateCall};` + )})` + ]) : promises.length === 1 - ? `${module.moduleArgument}.exports = Promise.resolve(${promises[0]}).then(function(${promises[0]}) { return ${instantiateCall}; })` - : `${module.moduleArgument}.exports = ${instantiateCall}` - ]) + ? Template.asString([ + `${module.moduleArgument}.exports = Promise.resolve(${ + promises[0] + }).then(${runtimeTemplate.basicFunction( + promises[0], + + `${importsCompatCode}return ${instantiateCall};` + )})` + ]) + : `${importsCompatCode}${module.moduleArgument}.exports = ${instantiateCall}` + }` ); return InitFragment.addToSource(source, initFragments, generateContext); } diff --git a/lib/wasm/WebAssemblyJavascriptGenerator.js b/lib/wasm/WebAssemblyJavascriptGenerator.js index a0912689c..51db952d2 100644 --- a/lib/wasm/WebAssemblyJavascriptGenerator.js +++ b/lib/wasm/WebAssemblyJavascriptGenerator.js @@ -155,7 +155,7 @@ class WebAssemblyJavascriptGenerator extends Generator { originModule: module, runtimeRequirements }); - return importStatement + reexports.join("\n"); + return importStatement[0] + importStatement[1] + reexports.join("\n"); } ) ); diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index 4fcfe6b60..81a292714 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -429,7 +429,7 @@ { "description": "The function is called on each dependency (`function(context, request, callback(err, result))`).", "instanceof": "Function", - "tsType": "((context: string, request: string, callback: (err?: Error, result?: string) => void) => void)" + "tsType": "((data: { context: string, request: string }, callback: (err?: Error, result?: string) => void) => void)" } ] }, @@ -465,7 +465,9 @@ "umd", "umd2", "jsonp", - "system" + "system", + "promise", + "import" ] }, "FileCacheOptions": { @@ -658,6 +660,10 @@ "description": "Wrap javascript code into IIFE's to avoid leaking into global scope.", "type": "boolean" }, + "ImportFunctionName": { + "description": "The name of the native import() function (can be exchanged for a polyfill).", + "type": "string" + }, "InfrastructureLogging": { "description": "Options for infrastructure level logging.", "type": "object", @@ -1694,6 +1700,9 @@ "iife": { "$ref": "#/definitions/Iife" }, + "importFunctionName": { + "$ref": "#/definitions/ImportFunctionName" + }, "jsonpFunction": { "$ref": "#/definitions/JsonpFunction" }, @@ -1834,6 +1843,9 @@ "iife": { "$ref": "#/definitions/Iife" }, + "importFunctionName": { + "$ref": "#/definitions/ImportFunctionName" + }, "jsonpFunction": { "$ref": "#/definitions/JsonpFunction" }, diff --git a/schemas/plugins/container/ContainerReferencePlugin.json b/schemas/plugins/container/ContainerReferencePlugin.json index 0d48d2286..d2e916f52 100644 --- a/schemas/plugins/container/ContainerReferencePlugin.json +++ b/schemas/plugins/container/ContainerReferencePlugin.json @@ -1,7 +1,7 @@ { "definitions": { - "LibraryType": { - "description": "Type of library.", + "ExternalsType": { + "description": "Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value).", "enum": [ "var", "module", @@ -18,7 +18,9 @@ "umd", "umd2", "jsonp", - "system" + "system", + "promise", + "import" ] }, "Overrides": { @@ -157,7 +159,7 @@ "description": "The external type of the remote containers.", "oneOf": [ { - "$ref": "#/definitions/LibraryType" + "$ref": "#/definitions/ExternalsType" } ] }, diff --git a/schemas/plugins/container/ModuleFederationPlugin.json b/schemas/plugins/container/ModuleFederationPlugin.json index 28590c908..e805aa6a8 100644 --- a/schemas/plugins/container/ModuleFederationPlugin.json +++ b/schemas/plugins/container/ModuleFederationPlugin.json @@ -83,6 +83,29 @@ ] } }, + "ExternalsType": { + "description": "Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value).", + "enum": [ + "var", + "module", + "assign", + "this", + "window", + "self", + "global", + "commonjs", + "commonjs2", + "commonjs-module", + "amd", + "amd-require", + "umd", + "umd2", + "jsonp", + "system", + "promise", + "import" + ] + }, "LibraryCustomUmdCommentObject": { "description": "Set explicit comments for `commonjs`, `commonjs2`, `amd`, and `root`.", "type": "object", @@ -503,7 +526,7 @@ "description": "The external type of the remote containers.", "oneOf": [ { - "$ref": "#/definitions/LibraryType" + "$ref": "#/definitions/ExternalsType" } ] }, diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index c18d60198..8f8f7868d 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -3917,7 +3917,7 @@ Built at: 1970-04-20 12:42:42 256e72dd8b9a83a6e45b.module.wasm 120 bytes [emitted] [immutable] 325.bundle.js 3.74 KiB [emitted] 526.bundle.js 368 bytes [emitted] [id hint: vendors] - 780.bundle.js 526 bytes [emitted] + 780.bundle.js 525 bytes [emitted] 99.bundle.js 220 bytes [emitted] a0e9dd97d7ced35a5b2c.module.wasm 154 bytes [emitted] [immutable] bundle.js 11 KiB [emitted] [name: main-1df31ce3] diff --git a/test/configCases/externals/async-externals/index.js b/test/configCases/externals/async-externals/index.js new file mode 100644 index 000000000..e3f038c3f --- /dev/null +++ b/test/configCases/externals/async-externals/index.js @@ -0,0 +1,15 @@ +import value from "promise-external"; +import request from "import-external"; + +it("should allow async externals", () => { + expect(value).toBe(42); + expect(request).toBe("/hello/world.js"); +}); + +it("should allow to catch errors of async externals", () => { + return expect(() => import("failing-promise-external")).rejects.toEqual( + expect.objectContaining({ + message: "external reject" + }) + ); +}); diff --git a/test/configCases/externals/async-externals/webpack.config.js b/test/configCases/externals/async-externals/webpack.config.js new file mode 100644 index 000000000..09e46b87a --- /dev/null +++ b/test/configCases/externals/async-externals/webpack.config.js @@ -0,0 +1,16 @@ +module.exports = { + output: { + libraryTarget: "commonjs-module", + importFunctionName: "((name) => Promise.resolve({ request: name }))" + }, + externals: { + "promise-external": + "promise new Promise(resolve => setTimeout(() => resolve(42), 100))", + "failing-promise-external": + "promise new Promise((resolve, reject) => setTimeout(() => reject(new Error('external reject')), 100))", + "import-external": ["import /hello/world.js", "request"] + }, + experiments: { + importAsync: true + } +}; diff --git a/types.d.ts b/types.d.ts index 464af8fc0..63371f4d0 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1337,7 +1337,7 @@ declare interface Configuration { /** * Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value). */ - externalsType?: LibraryType; + externalsType?: ExternalsType; /** * Options for infrastructure level logging. @@ -1498,7 +1498,7 @@ declare interface ContainerReferencePluginOptions { /** * The external type of the remote containers. */ - remoteType: LibraryType; + remoteType: ExternalsType; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. @@ -2310,8 +2310,7 @@ type ExternalItem = | RegExp | { [index: string]: string | boolean | string[] | { [index: string]: any } } | (( - context: string, - request: string, + data: { context: string; request: string }, callback: (err: Error, result: string) => void ) => void); declare class ExternalModule extends Module { @@ -2331,20 +2330,38 @@ type Externals = | ExternalItem[] | { [index: string]: string | boolean | string[] | { [index: string]: any } } | (( - context: string, - request: string, + data: { context: string; request: string }, callback: (err: Error, result: string) => void ) => void); declare class ExternalsPlugin { - constructor(type?: any, externals?: any); - type: any; - externals: any; + constructor(type: string, externals: Externals); + type: string; + externals: Externals; /** * Apply the plugin */ apply(compiler: Compiler): void; } +type ExternalsType = + | "var" + | "module" + | "assign" + | "this" + | "window" + | "self" + | "global" + | "commonjs" + | "commonjs2" + | "commonjs-module" + | "amd" + | "amd-require" + | "umd" + | "umd2" + | "jsonp" + | "system" + | "promise" + | "import"; declare interface FactorizeModuleOptions { currentProfile: ModuleProfile; factory: ModuleFactory; @@ -3748,7 +3765,7 @@ declare interface ModuleFederationPluginOptions { /** * The external type of the remote containers. */ - remoteType?: LibraryType; + remoteType?: ExternalsType; /** * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. @@ -4767,6 +4784,11 @@ declare interface Output { */ iife?: boolean; + /** + * The name of the native import() function (can be exchanged for a polyfill). + */ + importFunctionName?: string; + /** * The JSONP function used by webpack for async loading of chunks. */ @@ -4971,6 +4993,11 @@ declare interface OutputNormalized { */ iife?: boolean; + /** + * The name of the native import() function (can be exchanged for a polyfill). + */ + importFunctionName?: string; + /** * The JSONP function used by webpack for async loading of chunks. */ @@ -5875,7 +5902,7 @@ declare class RuntimeModule extends Module { getGeneratedCode(): string; } declare abstract class RuntimeTemplate { - outputOptions: Output; + outputOptions: OutputNormalized; requestShortener: RequestShortener; isIIFE(): boolean; supportsConst(): boolean; @@ -5961,7 +5988,7 @@ declare abstract class RuntimeTemplate { /** * which kind of code should be returned */ - type: "expression" | "promise" | "statements"; + type: "promise" | "expression" | "statements"; }): string; moduleId(__0: { /** @@ -6118,7 +6145,7 @@ declare abstract class RuntimeTemplate { * if set, will be filled with runtime requirements */ runtimeRequirements: Set; - }): string; + }): [string, string]; exportFromImport(__0: { /** * the module graph @@ -7075,7 +7102,7 @@ declare interface WebpackOptionsNormalized { /** * Specifies the default type of externals ('amd*', 'umd*', 'system' and 'jsonp' depend on output.libraryTarget set to the same value). */ - externalsType?: LibraryType; + externalsType?: ExternalsType; /** * Options for infrastructure level logging.