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.

836 lines (794 loc) 26.9 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); const CommentCompilationWarning = require("../errors/CommentCompilationWarning"); const UnsupportedFeatureWarning = require("../errors/UnsupportedFeatureWarning"); const { evaluateToIdentifier, evaluateToString, expressionIsUnsupported, toConstantDependency } = require("../javascript/JavascriptParserHelpers"); const memoize = require("../util/memoize"); const traverseDestructuringAssignmentProperties = require("../util/traverseDestructuringAssignmentProperties"); const CommonJsFullRequireDependency = require("./CommonJsFullRequireDependency"); const CommonJsRequireContextDependency = require("./CommonJsRequireContextDependency"); const CommonJsRequireDependency = require("./CommonJsRequireDependency"); const ConstDependency = require("./ConstDependency"); const ContextDependencyHelpers = require("./ContextDependencyHelpers"); const LocalModuleDependency = require("./LocalModuleDependency"); const { getLocalModule } = require("./LocalModulesHelpers"); const RequireHeaderDependency = require("./RequireHeaderDependency"); const RequireResolveContextDependency = require("./RequireResolveContextDependency"); const RequireResolveDependency = require("./RequireResolveDependency"); const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency"); /** @typedef {import("estree").CallExpression} CallExpression */ /** @typedef {import("estree").Expression} Expression */ /** @typedef {import("estree").NewExpression} NewExpression */ /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */ /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */ /** @typedef {import("../javascript/JavascriptParser").ImportSource} ImportSource */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("../javascript/JavascriptParser").Members} Members */ /** @typedef {import("../javascript/JavascriptParser").CalleeMembers} CalleeMembers */ /** @typedef {import("./LocalModule")} LocalModule */ /** * Defines the common js import settings type used by this module. * @typedef {object} CommonJsImportSettings * @property {string=} name * @property {string} context */ /** * Per-`const NAME = require(LITERAL)` binding state used to forward * member-access references on `NAME` to the `CommonJsRequireDependency` * created for the `require()` call. * @typedef {object} RequireBindingData * @property {RawReferencedExports | null} referencedExports mutable list, pushed to as `NAME.x.y` accesses are walked; null means the whole namespace is used. Separate from `dep` so a deferred require (walked after accesses on `NAME`) still collects them; becomes the dep's shared list on creation. * @property {InstanceType<typeof import("./CommonJsRequireDependency")> | null} dep dependency for the `require()` call, null until the `require()` is walked; kept so a later whole-namespace use can null its list directly */ const getHarmonyImportGuard = memoize(() => require("./HarmonyImportGuard")); const REQUIRE_BINDING_TAG = Symbol( "CommonJsImportsParserPlugin require binding" ); const PLUGIN_NAME = "CommonJsImportsParserPlugin"; /** * Checks whether this object is require call expression. * @param {Expression} expression expression * @returns {boolean} true, when expression is `require(...)` or `module.require(...)` */ const isRequireCallExpression = (expression) => { if (expression.type !== "CallExpression") return false; const { callee } = expression; if (callee.type === "Identifier") { return callee.name === "require"; } if (callee.type === "MemberExpression" && !callee.computed) { const object = callee.object; const property = callee.property; return ( object.type === "Identifier" && object.name === "module" && property.type === "Identifier" && property.name === "require" ); } return false; }; /** * Gets require referenced exports from destructuring. * @param {JavascriptParser} parser parser * @param {CallExpression | NewExpression} expr expression * @returns {RawReferencedExports | null} referenced exports from destructuring */ const getRequireReferencedExportsFromDestructuring = (parser, expr) => { const referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); if (!referencedPropertiesInDestructuring) return null; /** @type {RawReferencedExports} */ const referencedExports = []; traverseDestructuringAssignmentProperties( referencedPropertiesInDestructuring, (stack) => referencedExports.push(stack.map((p) => p.id)) ); return referencedExports; }; /** * Creates a require cache dependency. * @param {JavascriptParser} parser parser * @returns {(expr: Expression) => boolean} handler */ const createRequireCacheDependency = (parser) => toConstantDependency(parser, RuntimeGlobals.moduleCache, [ RuntimeGlobals.moduleCache, RuntimeGlobals.moduleId, RuntimeGlobals.moduleLoaded ]); /** * Creates a require as expression handler. * @param {JavascriptParser} parser parser * @param {JavascriptParserOptions} options options * @param {() => undefined | string} getContext context accessor * @returns {(expr: Expression) => boolean} handler */ const createRequireAsExpressionHandler = (parser, options, getContext) => (expr) => { const dep = new CommonJsRequireContextDependency( { request: /** @type {string} */ (options.unknownContextRequest), recursive: /** @type {boolean} */ (options.unknownContextRecursive), regExp: /** @type {RegExp} */ (options.unknownContextRegExp), mode: "sync" }, /** @type {Range} */ (expr.range), undefined, parser.scope.inShorthand, getContext() ); dep.critical = options.unknownContextCritical && "require function is used in a way in which dependencies cannot be statically extracted"; dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); return true; }; /** * Creates a require call handler. * @param {JavascriptParser} parser parser * @param {JavascriptParserOptions} options options * @param {() => undefined | string} getContext context accessor * @param {WeakMap<CallExpression, RequireBindingData>=} requireBindingData per-parser `const NAME = require(LITERAL)` binding state; only the free-`require` plugin populates it * @returns {(callNew: boolean) => (expr: CallExpression | NewExpression) => (boolean | void)} handler factory */ const createRequireCallHandler = ( parser, options, getContext, requireBindingData ) => { /** * Process require item. * @param {CallExpression | NewExpression} expr expression * @param {BasicEvaluatedExpression} param param * @returns {boolean | void} true when handled */ const processRequireItem = (expr, param) => { if (param.isString()) { let referencedExports = getRequireReferencedExportsFromDestructuring( parser, expr ); const binding = requireBindingData && requireBindingData.get(/** @type {CallExpression} */ (expr)); if (binding && !referencedExports) { // `const NAME = require(LITERAL)` — let later member-access walks // on `NAME` populate the dependency's referenced exports. referencedExports = binding.referencedExports; } const dep = new CommonJsRequireDependency( /** @type {string} */ (param.string), /** @type {Range} */ (param.range), getContext(), referencedExports, /** @type {Range} */ (expr.range) ); if (binding) binding.dep = dep; dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); getHarmonyImportGuard().attachDependencyGuards(parser, dep); return true; } }; /** * Process require context. * @param {CallExpression | NewExpression} expr expression * @param {BasicEvaluatedExpression} param param * @returns {boolean | void} true when handled */ const processRequireContext = (expr, param) => { const referencedExports = getRequireReferencedExportsFromDestructuring( parser, expr ); const dep = ContextDependencyHelpers.create( CommonJsRequireContextDependency, /** @type {Range} */ (expr.range), param, expr, options, { category: "commonjs", referencedExports }, parser, undefined, getContext() ); if (!dep) return; dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); return true; }; return (callNew) => (expr) => { if (options.commonjsMagicComments) { const { options: requireOptions, errors: commentErrors } = parser.parseCommentOptions(/** @type {Range} */ (expr.range)); if (commentErrors) { for (const e of commentErrors) { const { comment } = e; parser.state.module.addWarning( new CommentCompilationWarning( `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`, parser.getLocation(comment) ) ); } } if (requireOptions && requireOptions.webpackIgnore !== undefined) { if (typeof requireOptions.webpackIgnore !== "boolean") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`, parser.getLocation(expr) ) ); } else if (requireOptions.webpackIgnore) { // Do not instrument `require()` if `webpackIgnore` is `true` return true; } } } if (expr.arguments.length !== 1) return; /** @type {null | LocalModule} */ let localModule; const param = parser.evaluateExpression(expr.arguments[0]); if (param.isConditional()) { let isExpression = false; for (const p of /** @type {BasicEvaluatedExpression[]} */ ( param.options )) { const result = processRequireItem(expr, p); if (result === undefined) { isExpression = true; } } if (!isExpression) { const dep = new RequireHeaderDependency( /** @type {Range} */ (expr.callee.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); return true; } } if ( param.isString() && (localModule = getLocalModule( parser.state, /** @type {string} */ (param.string) )) ) { localModule.flagUsed(); const dep = new LocalModuleDependency( localModule, /** @type {Range} */ (expr.range), callNew ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); } else { const result = processRequireItem(expr, param); if (result === undefined) { processRequireContext(expr, param); } else { const dep = new RequireHeaderDependency( /** @type {Range} */ (expr.callee.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); } } return true; }; }; /** * Creates a process resolve handler. * @param {JavascriptParser} parser parser * @param {JavascriptParserOptions} options options * @param {() => undefined | string} getContext context accessor * @returns {(expr: CallExpression, weak: boolean) => (boolean | void)} resolver */ const createProcessResolveHandler = (parser, options, getContext) => { /** * Process resolve item. * @param {CallExpression} expr call expression * @param {BasicEvaluatedExpression} param param * @param {boolean} weak weak * @returns {boolean | void} true when handled */ const processResolveItem = (expr, param, weak) => { if (param.isString()) { const dep = new RequireResolveDependency( /** @type {string} */ (param.string), /** @type {Range} */ (param.range), getContext() ); dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); dep.weak = weak; parser.state.current.addDependency(dep); return true; } }; /** * Process resolve context. * @param {CallExpression} expr call expression * @param {BasicEvaluatedExpression} param param * @param {boolean} weak weak * @returns {boolean | void} true when handled */ const processResolveContext = (expr, param, weak) => { const dep = ContextDependencyHelpers.create( RequireResolveContextDependency, /** @type {Range} */ (param.range), param, expr, options, { category: "commonjs", mode: weak ? "weak" : "sync" }, parser, getContext() ); if (!dep) return; dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); return true; }; return (expr, weak) => { if (!weak && options.commonjsMagicComments) { const { options: requireOptions, errors: commentErrors } = parser.parseCommentOptions(/** @type {Range} */ (expr.range)); if (commentErrors) { for (const e of commentErrors) { const { comment } = e; parser.state.module.addWarning( new CommentCompilationWarning( `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`, parser.getLocation(comment) ) ); } } if (requireOptions && requireOptions.webpackIgnore !== undefined) { if (typeof requireOptions.webpackIgnore !== "boolean") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackIgnore\` expected a boolean, but received: ${requireOptions.webpackIgnore}.`, parser.getLocation(expr) ) ); } else if (requireOptions.webpackIgnore) { // Do not instrument `require()` if `webpackIgnore` is `true` return true; } } } if (expr.arguments.length !== 1) return; const param = parser.evaluateExpression(expr.arguments[0]); if (param.isConditional()) { for (const option of /** @type {BasicEvaluatedExpression[]} */ ( param.options )) { const result = processResolveItem(expr, option, weak); if (result === undefined) { processResolveContext(expr, option, weak); } } const dep = new RequireResolveHeaderDependency( /** @type {Range} */ (expr.callee.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); return true; } const result = processResolveItem(expr, param, weak); if (result === undefined) { processResolveContext(expr, param, weak); } const dep = new RequireResolveHeaderDependency( /** @type {Range} */ (expr.callee.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); return true; }; }; class CommonJsImportsParserPlugin { /** * Creates an instance of CommonJsImportsParserPlugin. * @param {JavascriptParserOptions} options parser options */ constructor(options) { /** @type {JavascriptParserOptions} */ this.options = options; } /** * Applies the plugin by registering its hooks on the compiler. * @param {JavascriptParser} parser the parser * @returns {void} */ apply(parser) { const options = this.options; parser.hooks.collectDestructuringAssignmentProperties.tap( PLUGIN_NAME, (expr) => { if (isRequireCallExpression(expr)) return true; } ); const getContext = () => { if (parser.currentTagData) { const { context } = /** @type {CommonJsImportSettings} */ (parser.currentTagData); return context; } }; // #region metadata /** * Tap require expression. * @param {string} expression expression * @param {() => Members} getMembers get members */ const tapRequireExpression = (expression, getMembers) => { parser.hooks.typeof .for(expression) .tap( PLUGIN_NAME, toConstantDependency(parser, JSON.stringify("function")) ); parser.hooks.evaluateTypeof .for(expression) .tap(PLUGIN_NAME, evaluateToString("function")); parser.hooks.evaluateIdentifier .for(expression) .tap( PLUGIN_NAME, evaluateToIdentifier(expression, "require", getMembers, true) ); }; tapRequireExpression("require", () => []); tapRequireExpression("require.resolve", () => ["resolve"]); tapRequireExpression("require.resolveWeak", () => ["resolveWeak"]); // #endregion // Weird stuff // parser.hooks.assign.for("require").tap(PLUGIN_NAME, (expr) => { // to not leak to global "require", we need to define a local require here. const dep = new ConstDependency("var require;", 0); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); return true; }); // #region Unsupported parser.hooks.call .for("require.main.require") .tap( PLUGIN_NAME, expressionIsUnsupported( parser, "require.main.require is not supported by webpack." ) ); parser.hooks.expression .for("module.parent.require") .tap( PLUGIN_NAME, expressionIsUnsupported( parser, "module.parent.require is not supported by webpack." ) ); parser.hooks.call .for("module.parent.require") .tap( PLUGIN_NAME, expressionIsUnsupported( parser, "module.parent.require is not supported by webpack." ) ); // #endregion // #region Renaming /** * Returns true when set undefined. * @param {Expression} expr expression * @returns {boolean} true when set undefined */ const defineUndefined = (expr) => { // To avoid "not defined" error, replace the value with undefined const dep = new ConstDependency( "undefined", /** @type {Range} */ (expr.range) ); dep.loc = parser.getLocation(expr); parser.state.module.addPresentationalDependency(dep); return false; }; parser.hooks.canRename.for("require").tap(PLUGIN_NAME, () => true); parser.hooks.rename.for("require").tap(PLUGIN_NAME, defineUndefined); // #endregion // #region Inspection const requireCache = createRequireCacheDependency(parser); parser.hooks.expression.for("require.cache").tap(PLUGIN_NAME, requireCache); // #endregion // #region Require as expression /** * Require as expression handler. * @param {Expression} expr expression * @returns {boolean} true when handled */ const requireAsExpressionHandler = createRequireAsExpressionHandler( parser, options, getContext ); parser.hooks.expression .for("require") .tap(PLUGIN_NAME, requireAsExpressionHandler); // #endregion // #region Require /** * Creates a require handler. * @param {boolean} callNew true, when require is called with new * @returns {(expr: CallExpression | NewExpression) => (boolean | void)} handler */ /** @type {WeakMap<CallExpression, RequireBindingData>} */ const requireBindingData = new WeakMap(); const createRequireHandler = createRequireCallHandler( parser, options, getContext, requireBindingData ); parser.hooks.call .for("require") .tap(PLUGIN_NAME, createRequireHandler(false)); parser.hooks.new .for("require") .tap(PLUGIN_NAME, createRequireHandler(true)); parser.hooks.call .for("module.require") .tap(PLUGIN_NAME, createRequireHandler(false)); parser.hooks.new .for("module.require") .tap(PLUGIN_NAME, createRequireHandler(true)); // #endregion // #region Require with property access /** * Returns true when handled. * @param {Expression} expr expression * @param {CalleeMembers} calleeMembers callee members * @param {CallExpression} callExpr call expression * @param {Members} members members * @param {Range[]} memberRanges member ranges * @returns {boolean | void} true when handled */ const chainHandler = ( expr, calleeMembers, callExpr, members, memberRanges ) => { if (callExpr.arguments.length !== 1) return; const param = parser.evaluateExpression(callExpr.arguments[0]); if ( param.isString() && !getLocalModule(parser.state, /** @type {string} */ (param.string)) ) { const dep = new CommonJsFullRequireDependency( /** @type {string} */ (param.string), /** @type {Range} */ (expr.range), members, /** @type {Range[]} */ memberRanges ); dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expr.range)[0] ); dep.optional = Boolean(parser.scope.inTry); dep.loc = parser.getLocation(expr); parser.state.current.addDependency(dep); return true; } }; /** * Call chain handler. * @param {CallExpression} expr expression * @param {CalleeMembers} calleeMembers callee members * @param {CallExpression} callExpr call expression * @param {Members} members members * @param {Range[]} memberRanges member ranges * @returns {boolean | void} true when handled */ const callChainHandler = ( expr, calleeMembers, callExpr, members, memberRanges ) => { if (callExpr.arguments.length !== 1) return; const param = parser.evaluateExpression(callExpr.arguments[0]); if ( param.isString() && !getLocalModule(parser.state, /** @type {string} */ (param.string)) ) { const dep = new CommonJsFullRequireDependency( /** @type {string} */ (param.string), /** @type {Range} */ (expr.callee.range), members, /** @type {Range[]} */ memberRanges ); dep.call = true; dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expr.range)[0] ); dep.optional = Boolean(parser.scope.inTry); dep.loc = parser.getLocation(expr.callee); parser.state.current.addDependency(dep); parser.walkExpressions(expr.arguments); return true; } }; parser.hooks.memberChainOfCallMemberChain .for("require") .tap(PLUGIN_NAME, chainHandler); parser.hooks.memberChainOfCallMemberChain .for("module.require") .tap(PLUGIN_NAME, chainHandler); parser.hooks.callMemberChainOfCallMemberChain .for("require") .tap(PLUGIN_NAME, callChainHandler); parser.hooks.callMemberChainOfCallMemberChain .for("module.require") .tap(PLUGIN_NAME, callChainHandler); // #endregion // #region Require bound to a const variable // Track `const NAME = require(LITERAL)` so that static member accesses on // `NAME` (e.g. `NAME.foo`, `NAME.foo()`) are forwarded to the same // `CommonJsRequireDependency` as referenced exports — enabling tree // shaking of CommonJS modules that are imported into a named binding // rather than destructured. parser.hooks.preDeclarator.tap(PLUGIN_NAME, (declarator, statement) => { if (statement.kind !== "const") return; if (declarator.id.type !== "Identifier") return; if (!declarator.init || declarator.init.type !== "CallExpression") { return; } const init = declarator.init; if ( init.callee.type !== "Identifier" || init.callee.name !== "require" || init.arguments.length !== 1 ) { return; } const arg = init.arguments[0]; if (arg.type !== "Literal" || typeof arg.value !== "string") return; // Only attach binding state when `require` resolves to the free // `require` (i.e. it isn't shadowed in the current scope). const requireInfo = parser.getFreeInfoFromVariable("require"); if (!requireInfo || requireInfo.name !== "require") return; /** @type {RequireBindingData} */ const binding = { referencedExports: [], dep: null }; requireBindingData.set(init, binding); parser.tagVariable(declarator.id.name, REQUIRE_BINDING_TAG, binding); return true; }); parser.hooks.expression.for(REQUIRE_BINDING_TAG).tap(PLUGIN_NAME, () => { const binding = /** @type {RequireBindingData} */ (parser.currentTagData); if (binding) { // `NAME` is read as a value (not as the object of a static member // chain), so we have to assume the whole exports object is used. binding.referencedExports = null; if (binding.dep) { binding.dep.referencedExports = null; } } return true; }); parser.hooks.expressionMemberChain .for(REQUIRE_BINDING_TAG) .tap(PLUGIN_NAME, (_expr, members) => { const binding = /** @type {RequireBindingData} */ (parser.currentTagData); if (binding && binding.referencedExports) { binding.referencedExports.push(members); } // Returning truthy suppresses the parser's fallback chain (which // would otherwise walk `NAME` as a bare expression and trigger our // `expression` hook above, marking the whole namespace as used). return true; }); parser.hooks.callMemberChain .for(REQUIRE_BINDING_TAG) .tap(PLUGIN_NAME, (expr, members) => { const binding = /** @type {RequireBindingData} */ (parser.currentTagData); if (binding && binding.referencedExports) { if (members.length === 0) { // `NAME(...)` — calling the require result directly; the // whole exports object is observable. binding.referencedExports = null; if (binding.dep) { binding.dep.referencedExports = null; } } else { binding.referencedExports.push(members); } } if (expr.arguments) parser.walkExpressions(expr.arguments); return true; }); // Re-exporting the binding lets the require result escape, so the whole // exports object is observable. Stage -10 runs before the harmony bail tap. parser.hooks.exportSpecifier.tap( { name: PLUGIN_NAME, stage: -10 }, (_statement, id) => { const binding = /** @type {RequireBindingData | undefined} */ (parser.getTagData(id, REQUIRE_BINDING_TAG)); if (binding) { binding.referencedExports = null; if (binding.dep) { binding.dep.referencedExports = null; } } } ); // #endregion // #region Require.resolve /** * Processes the provided expr. * @param {CallExpression} expr call expression * @param {boolean} weak weak * @returns {boolean | void} true when handled */ const processResolve = createProcessResolveHandler( parser, options, getContext ); parser.hooks.call .for("require.resolve") .tap(PLUGIN_NAME, (expr) => processResolve(expr, false)); parser.hooks.call .for("require.resolveWeak") .tap(PLUGIN_NAME, (expr) => processResolve(expr, true)); // #endregion } } module.exports = CommonJsImportsParserPlugin; module.exports.createProcessResolveHandler = createProcessResolveHandler; module.exports.createRequireAsExpressionHandler = createRequireAsExpressionHandler; module.exports.createRequireCacheDependency = createRequireCacheDependency; module.exports.createRequireHandler = createRequireCallHandler;