UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

858 lines (804 loc) • 27.8 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { pathToFileURL } = require("url"); const AsyncDependenciesBlock = require("../../graph/AsyncDependenciesBlock"); const EnableChunkLoadingPlugin = require("../../javascript/EnableChunkLoadingPlugin"); const { JAVASCRIPT_MODULE_TYPE_AUTO, JAVASCRIPT_MODULE_TYPE_ESM } = require("../../module/ModuleTypeConstants"); const parseResourceHintOptions = require("../../prefetch/parseResourceHintOptions"); const { equals } = require("../../util/ArrayHelpers"); const createHash = require("../../util/createHash"); const { contextify } = require("../../util/identifier"); const memoize = require("../../util/memoize"); const EnableWasmLoadingPlugin = require("../../wasm/EnableWasmLoadingPlugin"); const ConstDependency = require("../core/ConstDependency"); const { harmonySpecifierTag } = require("../esm/ESMImportDependencyParserPlugin"); const { isImportMetaFieldEnabled } = require("../import-meta/ImportMetaPlugin"); const CreateScriptUrlDependency = require("./CreateScriptUrlDependency"); const WorkerDependency = require("./WorkerDependency"); const WorkletDependency = require("./WorkletDependency"); const getUnsupportedFeatureWarning = memoize(() => require("../../errors/UnsupportedFeatureWarning") ); const getCommentCompilationWarning = memoize(() => require("../../errors/CommentCompilationWarning") ); /** * @import { * CallExpression, * Expression, * MemberExpression, * ObjectExpression, * Pattern, * Property, * SpreadElement * } from "estree" */ /** * @import { * ChunkLoading, * JavascriptParserOptions, * OutputModule, * WasmLoading, * WorkerPublicPath * } from "../../../declarations/WebpackOptions" */ /** @import Compiler from "../../Compiler" */ /** @import { DependencyLocation } from "../../graph/Dependency" */ /** @import { EntryOptions } from "../../graph/Entrypoint" */ /** @import NormalModule from "../../module/NormalModule" */ /** * @import JavascriptParser, { * JavascriptParserState, * Range * } from "../../javascript/JavascriptParser" */ /** @import Parser from "../../javascript/JavascriptParser" */ /** @import BasicEvaluatedExpression from "../../javascript/BasicEvaluatedExpression" */ /** @import { ESMSettings } from "../esm/ESMImportDependencyParserPlugin" */ /** @import { ResourceHint } from "./WorkerDependency" */ /** * Returns url. * @param {NormalModule} module module * @returns {string} url */ const getUrl = (module) => pathToFileURL(module.resource).toString(); const WorkerSpecifierTag = Symbol("worker specifier tag"); const WorkletSpecifierTag = Symbol("worklet specifier tag"); const WORKER_DEFAULT_SYNTAX = [ "Worker", "SharedWorker", "navigator.serviceWorker.register()", "Worker from worker_threads" ]; // A worklet is always a module script loaded through `addModule`, and unlike a // Web Worker cannot load further chunks at runtime, so WorkletDependency wraps // the call and pre-adds every chunk from the calling scope. const WORKLET_DEFAULT_SYNTAX = [ "*context.audioWorklet.addModule()", "*audioWorklet.addModule()", "CSS.paintWorklet.addModule()", "CSS.layoutWorklet.addModule()", "CSS.animationWorklet.addModule()" ]; /** @type {WeakMap<JavascriptParserState, number>} */ const workerIndexMap = new WeakMap(); const PLUGIN_NAME = "WorkerAndWorkletPlugin"; class WorkerAndWorkletPlugin { /** * Creates an instance of WorkerAndWorkletPlugin. * @param {ChunkLoading=} chunkLoading chunk loading * @param {WasmLoading=} wasmLoading wasm loading * @param {OutputModule=} module output module * @param {WorkerPublicPath=} workerPublicPath worker public path * @param {boolean=} workletDefault whether worklet parsing is on when `parser.worklet` is unset (enabled by `futureDefaults`) */ constructor( chunkLoading, wasmLoading, module, workerPublicPath, workletDefault ) { /** @type {ChunkLoading | undefined} */ this._chunkLoading = chunkLoading; /** @type {WasmLoading | undefined} */ this._wasmLoading = wasmLoading; /** @type {boolean | undefined} */ this._module = module; /** @type {string | undefined} */ this._workerPublicPath = workerPublicPath; /** @type {boolean} */ this._workletDefault = Boolean(workletDefault); } /** * Applies the plugin by registering its hooks on the compiler. * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { if (this._chunkLoading) { new EnableChunkLoadingPlugin(this._chunkLoading).apply(compiler); } if (this._wasmLoading) { new EnableWasmLoadingPlugin(this._wasmLoading).apply(compiler); } const cachedContextify = contextify.bindContextCache( compiler.context, compiler.root ); compiler.hooks.thisCompilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( WorkerDependency, normalModuleFactory ); compilation.dependencyTemplates.set( WorkerDependency, new WorkerDependency.Template() ); compilation.dependencyFactories.set( WorkletDependency, normalModuleFactory ); compilation.dependencyTemplates.set( WorkletDependency, new WorkletDependency.Template() ); compilation.dependencyTemplates.set( CreateScriptUrlDependency, new CreateScriptUrlDependency.Template() ); /** * Checks whether the expression is `import.meta.url`. * @param {JavascriptParser} parser the parser * @param {MemberExpression} expr expression * @returns {boolean} is `import.meta.url` */ const isMetaUrl = (parser, expr) => { const chain = parser.extractMemberExpressionChain(expr); if ( chain.members.length !== 1 || chain.object.type !== "MetaProperty" || chain.object.meta.name !== "import" || chain.object.property.name !== "meta" || chain.members[0] !== "url" ) { return false; } return true; }; /** * Resolves a `new URL(..., import.meta.url)` argument to its module url. * @param {JavascriptParser} parser the parser * @param {Expression} expr expression * @param {boolean} importMetaUrlEnabled true when import.meta.url should be handled * @returns {[string, Range] | void} parsed */ const parseModuleUrl = (parser, expr, importMetaUrlEnabled) => { if (expr.type !== "NewExpression" || expr.callee.type === "Super") { return; } if ( importMetaUrlEnabled && expr.arguments.length === 1 && expr.arguments[0].type === "MemberExpression" && isMetaUrl(parser, expr.arguments[0]) ) { const arg1 = expr.arguments[0]; return [ getUrl(parser.state.module), [ /** @type {Range} */ (arg1.range)[0], /** @type {Range} */ (arg1.range)[1] ] ]; } else if (expr.arguments.length === 2) { const [arg1, arg2] = expr.arguments; if (arg1.type === "SpreadElement") return; if (arg2.type === "SpreadElement") return; const callee = parser.evaluateExpression(expr.callee); if (!callee.isIdentifier() || callee.identifier !== "URL") return; const arg2Value = parser.evaluateExpression(arg2); if ( !arg2Value.isString() || !( /** @type {string} */ (arg2Value.string).startsWith("file://") ) || arg2Value.string !== getUrl(parser.state.module) ) { return; } const arg1Value = parser.evaluateExpression(arg1); if (!arg1Value.isString()) return; return [ /** @type {string} */ (arg1Value.string), [ /** @type {Range} */ (arg1.range)[0], /** @type {Range} */ (arg2.range)[1] ] ]; } }; /** @typedef {Record<string, ReturnType<BasicEvaluatedExpression["asCompileTimeValue"]>>} Values */ /** * Parses object expression. * @param {JavascriptParser} parser the parser * @param {ObjectExpression} expr expression * @returns {{ expressions: Record<string, Expression | Pattern | undefined>, otherElements: (Property | SpreadElement)[], values: Values, spread: boolean, insertType: "comma" | "single", insertLocation: number }} parsed object */ const parseObjectExpression = (parser, expr) => { /** @type {Values} */ const values = {}; /** @type {Record<string, Expression | Pattern>} */ const expressions = {}; /** @type {(Property | SpreadElement)[]} */ const otherElements = []; let spread = false; for (const prop of expr.properties) { if (prop.type === "SpreadElement") { spread = true; } else if ( prop.type === "Property" && !prop.method && !prop.computed && prop.key.type === "Identifier" ) { expressions[prop.key.name] = prop.value; if (!prop.shorthand && !prop.value.type.endsWith("Pattern")) { const value = parser.evaluateExpression( /** @type {Expression} */ (prop.value) ); if (value.isCompileTimeValue()) { values[prop.key.name] = value.asCompileTimeValue(); } } } else { otherElements.push(prop); } } const insertType = expr.properties.length > 0 ? "comma" : "single"; const insertLocation = expr.properties.length > 0 ? /** @type {Range} */ ( expr.properties[expr.properties.length - 1].range )[1] : /** @type {Range} */ (expr.range)[0] + 1; return { expressions, otherElements, values, spread, insertType, insertLocation }; }; /** * Processes the provided parser. * @param {Parser} parser parser parser * @param {JavascriptParserOptions} parserOptions parserOptions * @returns {void} */ const parserPlugin = (parser, parserOptions) => { const importMetaUrlEnabled = isImportMetaFieldEnabled( parserOptions.importMeta, "url" ); /** * Resolves the url argument (the `new URL(...)` / bare `import.meta.url`). * @param {CallExpression} expr expression * @returns {{ url: string, range: Range, needNewUrl: boolean, arg1: Expression | SpreadElement, arg2: Expression | SpreadElement | undefined } | void} resolved url info */ const resolveWorkerUrl = (expr) => { if (expr.arguments.length === 0 || expr.arguments.length > 2) { return; } const [arg1, arg2] = expr.arguments; if (arg1.type === "SpreadElement") return; if (arg2 && arg2.type === "SpreadElement") return; /** @type {string} */ let url; /** @type {Range} */ let range; let needNewUrl = false; if ( arg1.type === "MemberExpression" && importMetaUrlEnabled && isMetaUrl(parser, arg1) ) { url = getUrl(parser.state.module); range = [ /** @type {Range} */ (arg1.range)[0], /** @type {Range} */ (arg1.range)[1] ]; needNewUrl = true; } else { const parsedUrl = parseModuleUrl( parser, arg1, importMetaUrlEnabled ); if (!parsedUrl) return; [url, range] = parsedUrl; } return { url, range, needNewUrl, arg1, arg2 }; }; /** * Reads the magic-comment entry options for the matched expression. * @param {CallExpression} expr expression * @returns {EntryOptions | false} entry options, or false when `webpackIgnore` */ const parseEntryOptions = (expr) => { const { options: importOptions, errors: commentErrors } = parser.parseCommentOptions(/** @type {Range} */ (expr.range)); if (commentErrors) { for (const e of commentErrors) { const { comment } = e; parser.state.module.addWarning( new (getCommentCompilationWarning())( `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`, parser.getLocation(comment) ) ); } } /** @type {EntryOptions} */ const entryOptions = {}; if (importOptions) { if (importOptions.webpackIgnore !== undefined) { if (typeof importOptions.webpackIgnore !== "boolean") { parser.state.module.addWarning( new (getUnsupportedFeatureWarning())( `\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`, parser.getLocation(expr) ) ); } else if (importOptions.webpackIgnore) { return false; } } if (importOptions.webpackEntryOptions !== undefined) { if ( typeof importOptions.webpackEntryOptions !== "object" || importOptions.webpackEntryOptions === null ) { parser.state.module.addWarning( new (getUnsupportedFeatureWarning())( `\`webpackEntryOptions\` expected a object, but received: ${importOptions.webpackEntryOptions}.`, parser.getLocation(expr) ) ); } else { // `webpackEntryOptions` is user input from a magic comment, so // only safe own keys are copied, against prototype pollution // through `__proto__` or `constructor`. const userEntryOptions = importOptions.webpackEntryOptions; for (const key of Object.keys(userEntryOptions)) { if ( key === "__proto__" || key === "constructor" || key === "prototype" ) { continue; } /** @type {Record<string, EntryOptions[keyof EntryOptions]>} */ (entryOptions)[key] = userEntryOptions[key]; } } } if (importOptions.webpackChunkName !== undefined) { if (typeof importOptions.webpackChunkName !== "string") { parser.state.module.addWarning( new (getUnsupportedFeatureWarning())( `\`webpackChunkName\` expected a string, but received: ${importOptions.webpackChunkName}.`, parser.getLocation(expr) ) ); } else { entryOptions.name = importOptions.webpackChunkName; } } } return entryOptions; }; /** * Assigns a unique runtime to the entry so each worker gets its own runtime chunk. * @param {EntryOptions} entryOptions entry options * @returns {void} */ const ensureRuntime = (entryOptions) => { if (entryOptions.runtime !== undefined) return; const i = workerIndexMap.get(parser.state) || 0; workerIndexMap.set(parser.state, i + 1); const name = `${cachedContextify( parser.state.module.identifier() )}|${i}`; const hash = createHash(compilation.outputOptions.hashFunction); hash.update(name); const digest = hash.digest(compilation.outputOptions.hashDigest); entryOptions.runtime = digest.slice( 0, compilation.outputOptions.hashDigestLength ); }; /** * Handles a matched `new Worker(...)` expression. * @param {CallExpression} expr expression * @param {string=} workerSyntax matched global constructor syntax * @returns {boolean | void} true when handled */ const handleNewWorker = (expr, workerSyntax) => { const parsedUrl = resolveWorkerUrl(expr); if (!parsedUrl) return; const { url, range, needNewUrl, arg1, arg2 } = parsedUrl; const { expressions, otherElements, values: options, spread: hasSpreadInOptions, insertType, insertLocation } = arg2 && arg2.type === "ObjectExpression" ? parseObjectExpression(parser, arg2) : { expressions: /** @type {Record<string, Expression | Pattern | undefined>} */ ({}), otherElements: [], /** @type {Values} */ values: {}, spread: false, insertType: arg2 ? "spread" : "argument", insertLocation: arg2 ? /** @type {Range} */ (arg2.range) : /** @type {Range} */ (arg1.range)[1] }; const entryOptions = parseEntryOptions(expr); if (entryOptions === false) return false; /** @type {ResourceHint | undefined} */ let resourceHint; // Resource-hint comments belong to the asset expression, so they are // read from the inner `new URL(...)` range alone. A bare // `import.meta.url` has none, so its hints are skipped. if (arg1.type === "NewExpression") { const { options: urlImportOptions } = parser.parseCommentOptions( /** @type {Range} */ (arg1.range) ); const hints = parseResourceHintOptions( urlImportOptions, parser.state.module, /** @type {DependencyLocation} */ (expr.loc) ); if (hints.prefetch || hints.preload) { resourceHint = { prefetch: hints.prefetch, preload: hints.preload, fetchPriority: hints.fetchPriority, as: hints.as, type: hints.type, media: hints.media }; } } if ( !Object.prototype.hasOwnProperty.call(entryOptions, "name") && options && typeof options.name === "string" ) { entryOptions.name = options.name; } ensureRuntime(entryOptions); const block = new AsyncDependenciesBlock({ name: entryOptions.name, circular: false, entryOptions: { chunkLoading: this._chunkLoading, wasmLoading: this._wasmLoading, ...entryOptions, worker: true } }); block.loc = parser.getLocation(expr); const dep = new WorkerDependency(url, range, { publicPath: this._workerPublicPath, needNewUrl, workerConstructorRange: workerSyntax === "Worker" ? /** @type {Range} */ (expr.callee.range) : undefined, resourceHint }); dep.loc = parser.getLocation(expr); block.addDependency(dep); parser.state.module.addBlock(block); if (compilation.outputOptions.trustedTypes) { const dep = new CreateScriptUrlDependency( /** @type {Range} */ (expr.arguments[0].range) ); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); } if (expressions.type) { const expr = expressions.type; if (options.type !== false) { const dep = new ConstDependency( this._module ? '"module"' : "undefined", /** @type {Range} */ (expr.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); expressions.type = undefined; } } else if (insertType === "comma") { if (this._module || hasSpreadInOptions) { const dep = new ConstDependency( `, type: ${this._module ? '"module"' : "undefined"}`, insertLocation ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); } } else if (insertType === "spread") { const type = this._module ? '"module"' : "undefined"; // `{ ...(opts), type }` is equivalent to `Object.assign({}, opts, { type })` const useSpread = compilation.outputOptions.environment.spread; let prefix = useSpread ? "{ ...(" : "Object.assign({}, "; let suffix = useSpread ? `), type: ${type} }` : `, { type: ${type} })`; if ( workerSyntax !== undefined && (workerSyntax === "SharedWorker" || workerSyntax.endsWith(".SharedWorker")) ) { const optionsExpression = /** @type {Expression} */ (arg2); if ( optionsExpression.type === "Literal" && typeof optionsExpression.value === "string" ) { prefix = this._module ? "{ name: " : ""; suffix = this._module ? `, type: ${type} }` : ""; } else { const stringOptions = this._module ? `{ name: options, type: ${type} }` : "options"; // Evaluate the argument once, outside the wrapper's scope. prefix = `(${compilation.runtimeTemplate.returningFunction( `typeof options === "string" ? ${stringOptions} : ${prefix}options${suffix}`, "options" )})((`; suffix = "))"; } } if (prefix) { const dep1 = new ConstDependency( prefix, /** @type {Range} */ (insertLocation)[0] ); const dep2 = new ConstDependency( suffix, /** @type {Range} */ (insertLocation)[1] ); dep1.loc = parser.getLocation(expr); dep2.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep1); parser.state.module.addPresentationalDependency(dep2); } } else if (insertType === "argument" && this._module) { const dep = new ConstDependency( ', { type: "module" }', insertLocation ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); } parser.walkExpression(expr.callee); for (const key of Object.keys(expressions)) { if (expressions[key]) { if (expressions[key].type.endsWith("Pattern")) continue; parser.walkExpression( /** @type {Expression} */ (expressions[key]) ); } } for (const prop of otherElements) { parser.walkProperty(prop); } if (insertType === "spread") { parser.walkExpression(/** @type {Expression} */ (arg2)); } return true; }; /** * Handles a matched `addModule(...)` worklet expression. * @param {CallExpression} expr expression * @returns {boolean | void} true when handled */ const handleNewWorklet = (expr) => { const parsedUrl = resolveWorkerUrl(expr); if (!parsedUrl) return; const { url } = parsedUrl; const entryOptions = parseEntryOptions(expr); if (entryOptions === false) return false; const block = new AsyncDependenciesBlock({ name: entryOptions.name, circular: false, entryOptions: { // A module worklet links its split chunks through native `import` // and keeps module chunk loading. A script worklet loads none at // runtime, so its chunks are pre-added from the calling scope. chunkLoading: this._module ? this._chunkLoading : "import-scripts", wasmLoading: false, ...entryOptions, worker: true, // `addModule` always loads the chunk as a module, so its // auto public-path can read `import.meta.url` (no shim needed). worklet: true } }); block.loc = parser.getLocation(expr); // The dependency rewrites the call site: the fast path swaps the URL // argument directly, the multi-chunk path wraps it in a bootstrap. const dep = new WorkletDependency( url, [ /** @type {Range} */ (expr.range)[0], /** @type {Range} */ (expr.range)[0] ], [ /** @type {Range} */ (expr.callee.range)[1], /** @type {Range} */ (expr.range)[1] ], { publicPath: this._workerPublicPath } ); dep.loc = parser.getLocation(expr); block.addDependency(dep); parser.state.module.addBlock(block); parser.walkExpression(expr.callee); return true; }; /** * Registers the parser hooks matching a single syntax item. * @param {string} item item * @param {(expr: CallExpression, workerSyntax?: string) => boolean | void} handle handler invoked on a match * @param {symbol} specifierTag tag used for `*variable` syntax */ const processItem = (item, handle, specifierTag) => { if ( item.startsWith("*") && item.includes(".") && item.endsWith("()") ) { const firstDot = item.indexOf("."); const pattern = item.slice(1, firstDot); const itemMembers = item.slice(firstDot + 1, -2); parser.hooks.preDeclarator.tap( PLUGIN_NAME, (decl, _statement) => { if ( decl.id.type === "Identifier" && decl.id.name === pattern ) { parser.tagVariable(decl.id.name, specifierTag); return true; } } ); parser.hooks.pattern.for(pattern).tap(PLUGIN_NAME, (pattern) => { parser.tagVariable(pattern.name, specifierTag); return true; }); parser.hooks.callMemberChain .for(specifierTag) .tap(PLUGIN_NAME, (expression, members) => { if (itemMembers !== members.join(".")) { return; } return handle(expression); }); } else if (item.endsWith("()")) { parser.hooks.call.for(item.slice(0, -2)).tap(PLUGIN_NAME, handle); } else { const match = /^(.+?)(\(\))?\s+from\s+(.+)$/.exec(item); if (match) { const ids = match[1].split("."); const call = match[2]; const source = match[3]; (call ? parser.hooks.call : parser.hooks.new) .for(harmonySpecifierTag) .tap(PLUGIN_NAME, (expr) => { const settings = /** @type {ESMSettings} */ ( parser.currentTagData ); if ( !settings || settings.source !== source || !equals(settings.ids, ids) ) { return; } settings.used = true; return handle(expr); }); } else { parser.hooks.new .for(item) .tap(PLUGIN_NAME, (expr) => handle(expr, item)); } } }; /** * Expands and registers a syntax list. * @param {string[]} syntaxList syntax list (may contain "...") * @param {string[]} defaultSyntax default syntax used for "..." * @param {(expr: CallExpression, workerSyntax?: string) => boolean | void} handle handler * @param {symbol} specifierTag tag used for `*variable` syntax */ const processList = ( syntaxList, defaultSyntax, handle, specifierTag ) => { for (const item of syntaxList) { if (item === "...") { for (const itemFromDefault of defaultSyntax) { processItem(itemFromDefault, handle, specifierTag); } } else { processItem(item, handle, specifierTag); } } }; if (parserOptions.worker !== false) { const workerSyntax = !Array.isArray(parserOptions.worker) ? ["..."] : parserOptions.worker; processList( workerSyntax, WORKER_DEFAULT_SYNTAX, handleNewWorker, WorkerSpecifierTag ); } // Worklet parsing is opt-in: unset falls back to `futureDefaults` and // `false` disables it. Not defaulted in `config/defaults.js`, which // would splice into a user list's `"..."` sentinel. const workletOption = parserOptions.worklet === undefined ? this._workletDefault : parserOptions.worklet; if (workletOption) { const workletSyntax = !Array.isArray(workletOption) ? ["..."] : workletOption; processList( workletSyntax, WORKLET_DEFAULT_SYNTAX, handleNewWorklet, WorkletSpecifierTag ); } }; normalModuleFactory.hooks.parser .for(JAVASCRIPT_MODULE_TYPE_AUTO) .tap(PLUGIN_NAME, parserPlugin); normalModuleFactory.hooks.parser .for(JAVASCRIPT_MODULE_TYPE_ESM) .tap(PLUGIN_NAME, parserPlugin); } ); } } module.exports = WorkerAndWorkletPlugin;