/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { ConcatSource } = require("webpack-sources"); const { UsageState } = require("../ExportsInfo"); const ExternalModule = require("../ExternalModule"); const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants"); const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const HarmonyExportExpressionDependency = require("../dependencies/HarmonyExportExpressionDependency"); const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency"); const HarmonyExportSpecifierDependency = require("../dependencies/HarmonyExportSpecifierDependency"); const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin"); const ConcatenatedModule = require("../optimize/ConcatenatedModule"); const { InlinedUsedName } = require("../optimize/InlineExports"); const { RESERVED_IDENTIFIER, SAFE_IDENTIFIER, propertyAccess } = require("../util/property"); const { getEntryRuntime, getRuntimeKey } = require("../util/runtime"); const AbstractLibraryPlugin = require("./AbstractLibraryPlugin"); /** @import { Source } from "webpack-sources" */ /** * @import { * LibraryOptions, * LibraryType, * LibraryExport * } from "../../declarations/WebpackOptions" */ /** @import Chunk from "../Chunk" */ /** @import Compilation from "../Compilation" */ /** @import Compiler from "../Compiler" */ /** @import ModuleGraph from "../ModuleGraph" */ /** @import Module, { BuildMeta } from "../Module" */ /** * @import { * StartupRenderContext, * ModuleRenderContext * } from "../javascript/JavascriptModulesPlugin" */ /** @import { RuntimeSpec } from "../util/runtime" */ /** * @import { * JavascriptModuleBuildMeta * } from "../javascript/JavascriptModule" */ /** * Defines the shared type used by this module. * @template T * @typedef {import("./AbstractLibraryPlugin").LibraryContext} LibraryContext */ /** * Defines the module library plugin options type used by this module. * @typedef {object} ModuleLibraryPluginOptions * @property {LibraryType} type */ /** * Defines the module library plugin parsed type used by this module. * @typedef {object} ModuleLibraryPluginParsed * @property {string} name * @property {LibraryExport=} export */ const PLUGIN_NAME = "ModuleLibraryPlugin"; /** * The entry modules this plugin renders `export { … }` for, per compilation — the * only ones whose export definitions may be taken over. * @type {WeakMap>} */ const libraryEntryModules = new WeakMap(); /** * Whether every export the module defines is a binding the library can export * natively, so the definitions written into the exports object are read by nobody. * A re-export or a renamed import is not: `renderStartup` reads those back off the * object, and `treatAsCommonJs` hands the object out as the default export. * @param {Module} module the entry module * @param {ModuleGraph} moduleGraph the module graph * @returns {boolean} true when the definitions may be taken over */ const definesOnlyNativeExports = (module, moduleGraph) => { const buildMeta = /** @type {JavascriptModuleBuildMeta} */ (module.buildMeta); if (buildMeta && buildMeta.treatAsCommonJs) return false; // A top-level `await` wraps the body in `__webpack_require__.a`, and the // definitions read declarations inside that wrapper — re-emitting them after the // module would put them out of scope. if (moduleGraph.isAsync(module)) return false; const topLevelDeclarations = module.buildInfo && module.buildInfo.topLevelDeclarations; if (!topLevelDeclarations) return false; for (const dep of module.dependencies) { // A re-export or an `export default` has no declaration of its own to bind to, // so `renderStartup` reads its value back off the exports object. if ( dep instanceof HarmonyExportImportedSpecifierDependency || dep instanceof HarmonyExportExpressionDependency ) { return false; } if (!(dep instanceof HarmonyExportSpecifierDependency)) continue; if (!topLevelDeclarations.has(dep.id)) return false; } return true; }; /** * An export name is either an identifier name (reserved words included, e.g. * `default`) or a string literal (arbitrary module namespace names). * @param {string} name export name * @returns {string} name as it can be written after `as` */ const toExportName = (name) => SAFE_IDENTIFIER.test(name) ? name : JSON.stringify(name); /** * Whether the export name can also be used as the declared binding name. * @param {string} name export name * @returns {boolean} true, when the name can be declared */ const isDeclarableExportName = (name) => SAFE_IDENTIFIER.test(name) && !RESERVED_IDENTIFIER.has(name); /** * Represents the module library plugin runtime component. * @typedef {ModuleLibraryPluginParsed} T * @extends {AbstractLibraryPlugin} */ class ModuleLibraryPlugin extends AbstractLibraryPlugin { /** * Creates an instance of ModuleLibraryPlugin. * @param {ModuleLibraryPluginOptions} options the plugin options */ constructor(options) { super({ pluginName: "ModuleLibraryPlugin", type: options.type }); } /** * Applies the plugin by registering its hooks on the compiler. * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { super.apply(compiler); compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { const { onDemandExportsGeneration } = ConcatenatedModule.getCompilationHooks(compilation); const javascriptHooks = JavascriptModulesPlugin.getCompilationHooks(compilation); onDemandExportsGeneration.tap( PLUGIN_NAME, (module, runtimes, source, finalName) => { /** @type {BuildMeta} */ const buildMeta = module.buildMeta || (module.buildMeta = {}); /** @type {BuildMeta["exportsSourceByRuntime"]} */ const exportsSourceByRuntime = buildMeta.exportsSourceByRuntime || (buildMeta.exportsSourceByRuntime = new Map()); /** @type {BuildMeta["exportsFinalNameByRuntime"]} */ const exportsFinalNameByRuntime = buildMeta.exportsFinalNameByRuntime || (buildMeta.exportsFinalNameByRuntime = new Map()); for (const runtime of runtimes) { const key = getRuntimeKey(runtime); exportsSourceByRuntime.set(key, source); exportsFinalNameByRuntime.set(key, finalName); } return true; } ); // The same for a plain entry, whose definitions come from a dependency template. // Placed after the module body, which its value bindings read. javascriptHooks.onDemandExportsGeneration.tap( PLUGIN_NAME, (module, runtime, source, placeAtEnd) => { const entries = libraryEntryModules.get(compilation); if (!entries || !entries.has(module)) return; if (!definesOnlyNativeExports(module, compilation.moduleGraph)) { return; } /** @type {BuildMeta} */ const buildMeta = module.buildMeta || (module.buildMeta = {}); const key = getRuntimeKey(runtime); if (placeAtEnd) { const bindings = buildMeta.exportsBindingSourceByRuntime || (buildMeta.exportsBindingSourceByRuntime = new Map()); bindings.set(key, source); } else { const sources = buildMeta.exportsSourceByRuntime || (buildMeta.exportsSourceByRuntime = new Map()); // The `__esModule` marker and the definitions both land here, so // keep whichever arrived first rather than replacing it. const before = sources.get(key); sources.set(key, before ? `${before}${source}` : source); } return true; } ); // `ModuleLibraryPlugin` stashes the on-demand exports source via // `onDemandExportsGeneration` and only re-emits it when the // module is wrapped in an IIFE/factory. When the entry is // inlined directly, the stashed source — and the // `definePropertyGetters` / `requireScope` runtime helpers it // pulled in — never make it into the output. Drop those helpers // from the chunk's set so the bundle stays clean. compilation.hooks.additionalChunkRuntimeRequirements.tap( PLUGIN_NAME, (chunk, set, { chunkGraph, codeGenerationResults }) => { if (!set.has(RuntimeGlobals.definePropertyGetters)) return; if (chunkGraph.getNumberOfEntryModules(chunk) !== 1) return; if ( set.has(RuntimeGlobals.moduleFactories) || set.has(RuntimeGlobals.moduleCache) || set.has(RuntimeGlobals.interceptModuleExecution) || set.has(RuntimeGlobals.module) || set.has(RuntimeGlobals.thisAsExports) ) { return; } if (javascriptHooks.inlineInRuntimeBailout.isUsed()) return; const [module] = chunkGraph.getChunkEntryModulesIterable(chunk); const runtimeKey = getRuntimeKey(chunk.runtime); const buildMeta = module.buildMeta; const takenOver = (buildMeta && buildMeta.exportsSourceByRuntime && buildMeta.exportsSourceByRuntime.has(runtimeKey)) || (buildMeta && buildMeta.exportsBindingSourceByRuntime && buildMeta.exportsBindingSourceByRuntime.has(runtimeKey)); if (!takenOver) return; const codeGenResult = codeGenerationResults.get( module, chunk.runtime ); const jsSource = codeGenResult && codeGenResult.sources.get("javascript"); if ( jsSource && String(jsSource.source()).includes(`${RuntimeGlobals.require}.`) ) { return; } // Without `avoidEntryIife`, another rendered module wraps the entry in // an IIFE, which re-emits the stashed source — its helpers stay. if (!compilation.options.optimization.avoidEntryIife) { for (const m of chunkGraph.getChunkModulesIterableBySourceType( chunk, JAVASCRIPT_TYPE ) || []) { if (m !== module) return; } } // The helpers land in the runtime, not in this chunk, so what still // needs them is asked of every chunk sharing the entrypoint — the // runtime chunk, and whatever `splitChunks` moved out of the entry. /** @type {Set} */ const siblings = new Set([chunk]); for (const [ , entrypoint ] of chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk)) { if (!entrypoint) continue; for (const sibling of entrypoint.chunks) siblings.add(sibling); } let otherNeedsDefine = false; let otherNeedsNamespaceObject = false; let otherNeedsExports = false; let otherNeedsRequireScope = false; outer: for (const sibling of siblings) { for (const m of chunkGraph.getChunkModulesIterable(sibling)) { if (m === module) continue; const requirements = chunkGraph.getModuleRuntimeRequirements( m, sibling.runtime ); if (!requirements) continue; if (requirements.has(RuntimeGlobals.definePropertyGetters)) { otherNeedsDefine = true; } if (requirements.has(RuntimeGlobals.makeNamespaceObject)) { otherNeedsNamespaceObject = true; } if (requirements.has(RuntimeGlobals.exports)) { otherNeedsExports = true; } if (requirements.has(RuntimeGlobals.requireScope)) { otherNeedsRequireScope = true; } if (otherNeedsDefine) break outer; } } if (otherNeedsDefine) return; set.delete(RuntimeGlobals.definePropertyGetters); if (!otherNeedsNamespaceObject) { set.delete(RuntimeGlobals.makeNamespaceObject); } if (!otherNeedsExports) set.delete(RuntimeGlobals.exports); if (!otherNeedsRequireScope) set.delete(RuntimeGlobals.requireScope); } ); }); } /** * Finish entry module. * @param {Module} module the exporting entry module * @param {string} entryName the name of the entrypoint * @param {LibraryContext} libraryContext context * @returns {void} */ finishEntryModule( module, entryName, { options, compilation, compilation: { moduleGraph } } ) { const runtime = getEntryRuntime(compilation, entryName); if (options.export) { const exportsInfo = moduleGraph.getExportInfo( module, Array.isArray(options.export) ? options.export[0] : options.export ); exportsInfo.setUsed(UsageState.Used, runtime); exportsInfo.canMangleUse = false; exportsInfo.canInlineUse = false; } else { const exportsInfo = moduleGraph.getExportsInfo(module); if ( // If the entry module is commonjs, its exports cannot be mangled (module.buildMeta && /** @type {JavascriptModuleBuildMeta} */ (module.buildMeta) .treatAsCommonJs) || // The entry module provides unknown exports exportsInfo._otherExportsInfo.provided === null ) { exportsInfo.setUsedInUnknownWay(runtime); } else { exportsInfo.setAllKnownExportsUsed(runtime); for (const exportInfo of exportsInfo.ownedExports) { exportInfo.canInlineUse = false; } } } moduleGraph.addExtraReason(module, "used as library export"); let entries = libraryEntryModules.get(compilation); if (entries === undefined) { entries = new WeakSet(); libraryEntryModules.set(compilation, entries); } entries.add(module); } /** * Returns preprocess as needed by overriding. * @param {LibraryOptions} library normalized library option * @returns {T} preprocess as needed by overriding */ parseOptions(library) { const { name } = library; if (name) { throw new Error( `Library name must be unset. ${AbstractLibraryPlugin.COMMON_LIBRARY_NAME_MESSAGE}` ); } const _name = /** @type {string} */ (name); return { name: _name, export: library.export }; } /** * Analyze unknown provided exports. * @param {Source} source source * @param {Module} module module * @param {ModuleGraph} moduleGraph moduleGraph * @param {RuntimeSpec} runtime chunk runtime * @param {[string, string][]} exports exports * @param {Set} alreadyRenderedExports already rendered exports * @returns {ConcatSource} source with null provided exports */ _analyzeUnknownProvidedExports( source, module, moduleGraph, runtime, exports, alreadyRenderedExports ) { const result = new ConcatSource(source); /** @type {Set} */ const moduleRequests = new Set(); /** @type {Map} */ const unknownProvidedExports = new Map(); /** * Resolves dynamic star reexport. * @param {Module} module the module * @param {boolean} isDynamicReexport if module is dynamic reexported */ const resolveDynamicStarReexport = (module, isDynamicReexport) => { for (const connection of moduleGraph.getOutgoingConnections(module)) { const dep = connection.dependency; // Only handle star-reexport statement if ( dep instanceof HarmonyExportImportedSpecifierDependency && dep.name === null ) { const importedModule = connection.resolvedModule; const importedModuleExportsInfo = moduleGraph.getExportsInfo(importedModule); // The imported module provides unknown exports // So keep the reexports rendered in the bundle if ( dep.getMode(moduleGraph, runtime).type === "dynamic-reexport" && importedModuleExportsInfo._otherExportsInfo.provided === null ) { // Handle export * from 'external' if (importedModule instanceof ExternalModule) { moduleRequests.add(importedModule.userRequest); } else { resolveDynamicStarReexport(importedModule, true); } } // If importer modules existing `dynamic-reexport` dependency // We should keep export statement rendered in the bundle else if (isDynamicReexport) { for (const exportInfo of importedModuleExportsInfo.orderedExports) { if (!exportInfo.provided || exportInfo.name === "default") { continue; } const originalName = exportInfo.name; const usedName = /** @type {string | InlinedUsedName | false} */ (exportInfo.getUsedName(originalName, runtime)); if (!alreadyRenderedExports.has(originalName) && usedName) { unknownProvidedExports.set(originalName, usedName); } } } } } }; resolveDynamicStarReexport(module, false); for (const request of moduleRequests) { result.add(`export * from "${request}";\n`); } for (const [origin, used] of unknownProvidedExports) { exports.push([ origin, used instanceof InlinedUsedName ? used.render( Template.toNormalComment( `inlined export ${propertyAccess([origin])}` ) ) : `${RuntimeGlobals.exports}${propertyAccess([used])}` ]); } return result; } /** * Renders source with library export. * @param {Source} source source * @param {Module} module module * @param {StartupRenderContext} renderContext render context * @param {LibraryContext} libraryContext context * @returns {Source} source with library export */ renderStartup(source, module, renderContext, { options, compilation }) { const { moduleGraph, chunk, codeGenerationResults, inlined, inlinedInIIFE, runtimeTemplate } = renderContext; let result = new ConcatSource(source); const exportInfos = options.export ? [ moduleGraph.getExportInfo( module, Array.isArray(options.export) ? options.export[0] : options.export ) ] : moduleGraph.getExportsInfo(module).orderedExports; const exportsFinalNameByRuntime = (module.buildMeta && module.buildMeta.exportsFinalNameByRuntime && module.buildMeta.exportsFinalNameByRuntime.get( getRuntimeKey(chunk.runtime) )) || {}; const isInlinedEntryWithoutIIFE = inlined && !inlinedInIIFE; // Direct export bindings from on-demand concatenation const definitions = isInlinedEntryWithoutIIFE ? exportsFinalNameByRuntime : {}; /** @type {string[]} */ const shortHandedExports = []; /** @type {[string, string][]} */ const exports = []; /** @type {Set} */ const alreadyRenderedExports = new Set(); /** @type {Set} */ const usedLocalNames = new Set(); /** * `toIdentifier` is lossy, so distinct export names (`"a b"` and `"a-b"`) * would otherwise be bound to the same generated name twice. * @param {string} exportName export name * @returns {string} unique local name for the export */ const toLocalName = (exportName) => { const base = `${RuntimeGlobals.exports}${Template.toIdentifier(exportName)}`; let name = base; let i = 0; while (usedLocalNames.has(name)) name = `${base}_${i++}`; usedLocalNames.add(name); return name; }; const isAsync = moduleGraph.isAsync(module); const treatAsCommonJs = module.buildMeta && /** @type {JavascriptModuleBuildMeta} */ (module.buildMeta) .treatAsCommonJs; const skipRenderDefaultExport = Boolean(treatAsCommonJs); // A single (non-concatenated) inlined entry is rendered at the top // level, so its own declarations are top-level bindings we can export // directly — keeping ESM live bindings instead of snapshotting // `__webpack_exports__.x` into a `const`. Concatenated entries already // get direct bindings via `definitions` above. /** @type {Map | undefined} */ let directBindings; if (isInlinedEntryWithoutIIFE && !treatAsCommonJs) { const { data } = codeGenerationResults.get(module, chunk.runtime); const topLevelDeclarations = (data && data.get("topLevelDeclarations")) || (module.buildInfo && module.buildInfo.topLevelDeclarations); if (topLevelDeclarations) { for (const dep of module.dependencies) { if ( dep instanceof HarmonyExportSpecifierDependency && topLevelDeclarations.has(dep.id) ) { (directBindings || (directBindings = new Map())).set( dep.name, dep.id ); } } } } // Taken over by `onDemandExportsGeneration` above, and re-emitted by // `renderModuleContent` only where the module is wrapped — which is not here. const bindingsTakenOver = Boolean( module.buildMeta && module.buildMeta.exportsBindingSourceByRuntime && module.buildMeta.exportsBindingSourceByRuntime.has( getRuntimeKey(chunk.runtime) ) ); const moduleExportsInfo = moduleGraph.getExportsInfo(module); // Define ESM compatibility flag will rely on `__webpack_exports__` const needHarmonyCompatibilityFlag = moduleExportsInfo.otherExportsInfo.getUsed(chunk.runtime) !== UsageState.Unused || moduleExportsInfo .getReadOnlyExportInfo("__esModule") .getUsed(chunk.runtime) !== UsageState.Unused; let needExportsDeclaration = !isInlinedEntryWithoutIIFE || isAsync || needHarmonyCompatibilityFlag; if (isAsync) { result.add( `${RuntimeGlobals.exports} = await ${RuntimeGlobals.exports};\n` ); } // Try to find all known exports of the entry module outer: for (const exportInfo of exportInfos) { if (!exportInfo.provided) continue; const originalName = exportInfo.name; // Skip rendering the default export in some cases if (skipRenderDefaultExport && originalName === "default") continue; // Try to find all exports from the reexported modules const target = exportInfo.findTarget(moduleGraph, (_m) => true); // A namespace object target (`export * as ns from "./module"`) has no // export name, so there is nothing that could be missing if (target && target.export) { const targetExport = target.export[0]; const reexportsInfo = moduleGraph.getExportsInfo(target.module); for (const reexportInfo of reexportsInfo.orderedExports) { if ( reexportInfo.provided === false && reexportInfo.name !== "default" && reexportInfo.name === targetExport ) { continue outer; } } } // The exports of entry module are never inlined const usedName = /** @type {string} */ (exportInfo.getUsedName(originalName, chunk.runtime)); /** @type {string | undefined} */ const definition = definitions[usedName]; /** @type {string | undefined} */ const directLocal = definition === undefined && directBindings ? directBindings.get(originalName) : undefined; /** @type {string | undefined} */ let finalName; if (definition) { finalName = definition; } else if (directLocal !== undefined) { // Live binding to the inlined entry's own top-level declaration finalName = directLocal; // The declaration is only there for the definitions written into it; with // those taken over (and not re-emitted here) nothing reads the object. if (!bindingsTakenOver) needExportsDeclaration = true; } else { // Fallback to `__webpack_exports__` property access // when no direct export binding was found finalName = toLocalName(originalName); needExportsDeclaration = true; result.add( `${runtimeTemplate.renderConst()} ${finalName} = ${RuntimeGlobals.exports}${propertyAccess( [usedName] )};\n` ); } if ( // If the name includes `property access` and `call expressions` finalName && (finalName.includes(".") || finalName.includes("[") || finalName.includes("(")) ) { if (exportInfo.isReexport()) { const { data } = codeGenerationResults.get(module, chunk.runtime); const topLevelDeclarations = (data && data.get("topLevelDeclarations")) || (module.buildInfo && module.buildInfo.topLevelDeclarations); if (topLevelDeclarations && topLevelDeclarations.has(originalName)) { const name = toLocalName(originalName); result.add( `${runtimeTemplate.renderConst()} ${name} = ${finalName};\n` ); shortHandedExports.push(`${name} as ${toExportName(originalName)}`); } else { exports.push([originalName, finalName]); } } else { exports.push([originalName, finalName]); } } else { shortHandedExports.push( finalName === originalName ? finalName : `${finalName} as ${toExportName(originalName)}` ); } alreadyRenderedExports.add(originalName); } if (shortHandedExports.length > 0) { result.add(`export { ${shortHandedExports.join(", ")} };\n`); } // Add default export `__webpack_exports__` statement to keep better compatibility if (treatAsCommonJs) { needExportsDeclaration = true; result.add(`export default ${RuntimeGlobals.exports};\n`); } result = this._analyzeUnknownProvidedExports( result, module, moduleGraph, chunk.runtime, exports, alreadyRenderedExports ); for (const [exportName, final] of exports) { if (isDeclarableExportName(exportName)) { result.add( `export ${runtimeTemplate.renderConst()} ${exportName} = ${final};\n` ); continue; } // Bind to a generated name, the export name can't be declared const name = toLocalName(exportName); result.add( `${runtimeTemplate.renderConst()} ${name} = ${final};\nexport { ${name} as ${toExportName(exportName)} };\n` ); } if (!needExportsDeclaration) { renderContext.needExportsDeclaration = false; } return result; } /** * Renders module content. * @param {Source} source source * @param {Module} module module * @param {ModuleRenderContext} renderContext render context * @param {Omit, "options">} libraryContext context * @returns {Source} source with library export */ renderModuleContent( source, module, { factory, inlinedInIIFE, chunk }, libraryContext ) { const runtimeKey = getRuntimeKey(chunk.runtime); const buildMeta = module.buildMeta; const exportsSource = buildMeta && buildMeta.exportsSourceByRuntime && buildMeta.exportsSourceByRuntime.get(runtimeKey); const bindingSource = buildMeta && buildMeta.exportsBindingSourceByRuntime && buildMeta.exportsBindingSourceByRuntime.get(runtimeKey); // Re-add the exports source in a factory or IIFE-wrapped startup module; value // bindings read the declarations above them, so those go after the body. if ((inlinedInIIFE || factory) && (exportsSource || bindingSource)) { return new ConcatSource( ...(exportsSource ? [exportsSource] : []), source, ...(bindingSource ? ["\n", bindingSource] : []) ); } return source; } } module.exports = ModuleLibraryPlugin;