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.
900 lines (854 loc) • 30.2 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const isDependencyKind = require("../../graph/isDependencyKind");
const {
evaluateToString
} = require("../../javascript/JavascriptParserHelpers");
const RuntimeGlobals = require("../../runtime/RuntimeGlobals");
const { recordOptimizationBailout } = require("../../util/buildDiagnostics");
const formatLocation = require("../../util/formatLocation");
const memoize = require("../../util/memoize");
const { propertyAccess } = require("../../util/property");
const RuntimeRequirementsDependency = require("../core/RuntimeRequirementsDependency");
const ESMExports = require("../esm/ESMExports");
const DynamicExports = require("../helpers/DynamicExports");
const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency");
const CommonJsExportsDependency = require("./CommonJsExportsDependency");
const CommonJsObjectExportDependency = require("./CommonJsObjectExportDependency");
const CommonJsSelfReferenceDependency = require("./CommonJsSelfReferenceDependency");
const ModuleDecoratorDependency = require("./ModuleDecoratorDependency");
const getRequireBindingTag = memoize(
() => require("./CommonJsImportsParserPlugin").REQUIRE_BINDING_TAG
);
const FUNCTION_PROPERTY_DROP_HEAD =
CommonJsObjectExportDependency.FUNCTION_PROPERTY_DROP_HEAD;
/**
* @import {
* AssignmentExpression,
* CallExpression,
* Expression,
* FunctionExpression,
* Node,
* ObjectExpression,
* Property,
* SpreadElement,
* Super,
* ThisExpression
* } from "estree"
*/
/**
* @import {
* JavascriptParserOptions
* } from "../../../declarations/WebpackOptions"
*/
/** @import CommonJsRequireDependency from "./CommonJsRequireDependency" */
/** @import { ExportInfoName } from "../../graph/Dependency" */
/**
* @import JavascriptParser, {
* Range,
* Members,
* StatementPath
* } from "../../javascript/JavascriptParser"
*/
/**
* @import {
* CommonJSDependencyBaseKeywords
* } from "./CommonJsDependencyHelpers"
*/
/**
* @import {
* CommonJsObjectExportKind
* } from "./CommonJsObjectExportDependency"
*/
/** @import { RequireBindingData } from "./CommonJsImportsParserPlugin" */
/**
* This function takes a generic expression and detects whether it is an ObjectExpression.
* This is used in the context of parsing CommonJS exports to get the value of the property descriptor
* when the `exports` object is assigned to `Object.defineProperty`.
*
* In CommonJS modules, the `exports` object can be assigned to `Object.defineProperty` and therefore
* webpack has to detect this case and get the value key of the property descriptor. See the following example
* for more information: https://astexplorer.net/#/gist/83ce51a4e96e59d777df315a6d111da6/8058ead48a1bb53c097738225db0967ef7f70e57
*
* This would be an example of a CommonJS module that exports an object with a property descriptor:
* ```js
* Object.defineProperty(exports, "__esModule", { value: true });
* exports.foo = void 0;
* exports.foo = "bar";
* ```
* @param {Expression} expr expression
* @returns {Expression | undefined} returns the value of property descriptor
*/
const getValueOfPropertyDescription = (expr) => {
if (expr.type !== "ObjectExpression") return;
for (const property of expr.properties) {
if (property.type === "SpreadElement" || property.computed) continue;
const key = property.key;
if (key.type !== "Identifier" || key.name !== "value") continue;
return /** @type {Expression} */ (property.value);
}
};
const DESCRIPTOR_ATTRIBUTES = new Set([
"enumerable",
"configurable",
"writable"
]);
/**
* Whether an attribute value names no binding, like `true` or a minifier's `!0`.
* @param {Expression} expr attribute value
* @returns {boolean} true, when the value is a literal, possibly under a unary operator
*/
const isConstantAttribute = (expr) =>
expr.type === "Literal" ||
(expr.type === "UnaryExpression" && expr.argument.type === "Literal");
/**
* Extracts the re-exportable expression from a property descriptor, handling
* both the eager `{ value: <expr> }` form and the lazy
* `{ get: () => <expr> }` / `{ get() { return <expr>; } }` accessor form used
* by barrel files (e.g. webpack's own `lib/index.js`). Any other property must
* be a literal attribute, since the descriptor is kept as written but not walked.
* @param {Expression} expr property descriptor expression
* @returns {{ expr: Expression, getter: boolean } | undefined} the value expression and whether it is a lazy getter
*/
const getReexportOfPropertyDescriptor = (expr) => {
if (expr.type !== "ObjectExpression") return;
/** @type {Expression | undefined} */
let valueExpr;
/** @type {Expression | undefined} */
let getFn;
for (const property of expr.properties) {
if (property.type === "SpreadElement" || property.computed) return;
const key = property.key;
if (key.type !== "Identifier") return;
if (key.name === "value") {
valueExpr = /** @type {Expression} */ (property.value);
} else if (key.name === "get") {
getFn = /** @type {Expression} */ (property.value);
} else if (
!DESCRIPTOR_ATTRIBUTES.has(key.name) ||
!isConstantAttribute(/** @type {Expression} */ (property.value))
) {
// a setter, an unknown key or a computed attribute: not a plain re-export
return;
}
}
if (valueExpr) return { expr: valueExpr, getter: false };
if (
getFn &&
(getFn.type === "FunctionExpression" ||
getFn.type === "ArrowFunctionExpression")
) {
// A parameter or the function's own name would shadow a `const` binding the body names.
if (getFn.params.length > 0) return;
if (getFn.type === "FunctionExpression" && getFn.id) return;
// An async or generator getter returns a wrapper, not the re-exported value.
if (getFn.async || getFn.generator) return;
// Arrow with expression body: `() => require("./x")`.
if (getFn.body.type !== "BlockStatement") {
return { expr: getFn.body, getter: true };
}
// Function body must be exactly `return <expr>;`.
if (
getFn.body.body.length === 1 &&
getFn.body.body[0].type === "ReturnStatement" &&
getFn.body.body[0].argument
) {
return { expr: getFn.body.body[0].argument, getter: true };
}
}
};
/**
* The purpose of this function is to check whether an expression is a truthy literal or not. This is
* useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy
* values like `null` and `false`. However, exports should only be created if the exported value is truthy.
* @param {Expression} expr expression being checked
* @returns {boolean} true, when the expression is a truthy literal
*/
const isTruthyLiteral = (expr) => {
switch (expr.type) {
case "Literal":
return Boolean(expr.value);
case "UnaryExpression":
if (expr.operator === "!") return isFalsyLiteral(expr.argument);
}
return false;
};
/**
* The purpose of this function is to check whether an expression is a falsy literal or not. This is
* useful when parsing CommonJS exports, because CommonJS modules can export any value, including falsy
* values like `null` and `false`. However, exports should only be created if the exported value is truthy.
* @param {Expression} expr expression being checked
* @returns {boolean} true, when the expression is a falsy literal
*/
const isFalsyLiteral = (expr) => {
switch (expr.type) {
case "Literal":
return !expr.value;
case "UnaryExpression":
if (expr.operator === "!") return isTruthyLiteral(expr.argument);
}
return false;
};
/**
* Parses a `require()` call, or a `const` binding of one, under a static member chain.
* @param {JavascriptParser} parser the parser
* @param {Expression} expr expression
* @returns {{ request: string, ids: ExportInfoName[], binding?: { range: Range, dep: CommonJsRequireDependency } } | undefined} parsed call, with the binding it reads
*/
const parseRequireCall = (parser, expr) => {
/** @type {ExportInfoName[]} */
const ids = [];
while (expr.type === "MemberExpression") {
if (expr.object.type === "Super") return;
if (!expr.property) return;
const prop = expr.property;
if (expr.computed) {
if (prop.type !== "Literal") return;
ids.push(`${prop.value}`);
} else {
if (prop.type !== "Identifier") return;
ids.push(prop.name);
}
expr = expr.object;
}
if (expr.type === "Identifier") {
const binding =
/** @type {RequireBindingData | undefined} */
(parser.getTagData(expr.name, getRequireBindingTag()));
// No dependency: `webpackIgnore`, a local module, or not walked yet.
if (!binding || !binding.dep) return;
return {
request: binding.request,
ids: ids.reverse(),
binding: { range: /** @type {Range} */ (expr.range), dep: binding.dep }
};
}
if (expr.type !== "CallExpression" || expr.arguments.length !== 1) return;
const callee = expr.callee;
if (
callee.type !== "Identifier" ||
parser.getVariableInfo(callee.name) !== "require"
) {
return;
}
const arg = expr.arguments[0];
if (arg.type === "SpreadElement") return;
const argValue = parser.evaluateExpression(arg);
if (!argValue.isString()) return;
return {
request: /** @type {string} */ (argValue.string),
ids: ids.reverse()
};
};
/** @typedef {Node extends infer N ? (N extends Node ? N[keyof N] : never) : never} NodeFieldValue */
/**
* Checks if a value is an AST node.
* @template V
* @param {V} value candidate
* @returns {value is Extract<V, Node>} true when the value is an AST node
*/
const isNode = (value) =>
typeof value === "object" &&
value !== null &&
typeof (/** @type {Partial<Node>} */ (value).type) === "string";
/** Positional/metadata keys that never hold child AST nodes. */
const NON_CHILD_KEYS = new Set([
"type",
"start",
"end",
"loc",
"range",
"leadingComments",
"trailingComments"
]);
/** @type {Map<string, string[]>} per node type: own keys that may hold child nodes */
const childKeysByType = new Map();
/**
* Returns the keys of a node that may hold child nodes, excluding positional
* metadata. Computed once per node type and cached: acorn initializes every
* field of a given type (to `null` when absent), so the key set is stable, and
* the cache stays allocation-free on the hot path after the first node of each
* type is seen.
* @param {Node} node node
* @returns {string[]} candidate child keys
*/
const getChildKeys = (node) => {
let keys = childKeysByType.get(node.type);
if (keys === undefined) {
keys = Object.keys(node).filter((key) => !NON_CHILD_KEYS.has(key));
childKeysByType.set(node.type, keys);
}
return keys;
};
/**
* Searches for a `this` belonging to the scanned function's own `this`-scope.
* Descends into arrow functions, but not into nested functions, static blocks
* and class field values, which have their own `this`. Class method bodies are
* function expressions and are skipped by the same rule, while computed keys
* and `extends` clauses evaluate in the outer scope and are searched. The
* `this`-scope boundary is enforced when a node is popped, so the generic
* branch may enumerate any child without crossing it.
* @param {Node} body function body
* @param {Node[]} params function params
* @returns {ThisExpression | undefined} first own `this` expression
*/
const findOwnThisExpression = (body, params) => {
// the body is popped first, so its `this` wins over a default parameter's
const stack = [];
for (let i = 0; i < params.length; i++) stack.push(params[i]);
stack.push(body);
let node;
while ((node = stack.pop()) !== undefined) {
switch (node.type) {
case "ThisExpression":
return node;
case "FunctionExpression":
case "FunctionDeclaration":
case "StaticBlock":
break;
case "PropertyDefinition":
if (node.computed) stack.push(node.key);
break;
default: {
const children =
/** @type {Node & Record<string, NodeFieldValue>} */
(node);
const keys = getChildKeys(node);
for (let i = 0; i < keys.length; i++) {
const value = children[keys[i]];
if (Array.isArray(value)) {
for (const item of value) {
if (isNode(item)) stack.push(item);
}
} else if (isNode(value)) {
stack.push(value);
}
}
}
}
}
};
/**
* Returns the own `this` of a function-expression export value. When such a
* function is called as a method of the exports object, `this` is the exports
* object and may reach any sibling export (#21178).
* @param {JavascriptParser} parser the parser
* @param {Node} expr the exported value
* @returns {ThisExpression | undefined} first own `this` expression
*/
const getThisAccessInExportedValue = (parser, expr) =>
expr.type === "FunctionExpression" &&
// scanning the body of every exported function is expensive and most of them
// never mention `this`, so filter on the source text first
parser.sourceMayContainIdentifier(/** @type {Range} */ (expr.range), "this")
? findOwnThisExpression(expr.body, expr.params)
: undefined;
/**
* Returns the export name an object-literal property contributes, or
* `undefined` when it cannot contribute one: a spread, a computed or
* non-string key is unknown at compile time, and `__proto__` sets
* [[Prototype]] instead of an own property.
* @param {Property | SpreadElement} property property
* @returns {string | undefined} export name
*/
const getStaticPropertyName = (property) => {
if (property.type === "SpreadElement" || property.computed) return;
const key = property.key;
/** @type {string | undefined} */
let name;
if (key.type === "Identifier") {
name = key.name;
} else if (key.type === "Literal" && typeof key.value === "string") {
name = key.value;
}
return name === "__proto__" ? undefined : name;
};
/**
* @param {Property} property property
* @returns {CommonJsObjectExportKind} property kind
*/
const getObjectExportKind = (property) => {
if (property.kind === "get" || property.kind === "set") return property.kind;
if (property.method) {
const { async, generator } = /** @type {FunctionExpression} */ (
property.value
);
if (async) return generator ? "asyncGeneratorMethod" : "asyncMethod";
return generator ? "generatorMethod" : "method";
}
return property.shorthand ? "shorthand" : "keyValue";
};
const PLUGIN_NAME = "CommonJsExportsParserPlugin";
class CommonJsExportsParserPlugin {
/**
* Creates an instance of CommonJsExportsParserPlugin.
* @param {JavascriptParserOptions["topLevelThis"]} topLevelThis what top-level `this` refers to
*/
constructor(topLevelThis) {
/** @type {boolean} */
this.thisAsExports = topLevelThis !== "global";
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {JavascriptParser} parser the parser
* @returns {void}
*/
apply(parser) {
const { thisAsExports } = this;
const enableStructuredExports = () => {
DynamicExports.enable(parser.state);
};
/**
* Checks namespace.
* @param {boolean} topLevel true, when the export is on top level
* @param {Members} members members of the export
* @param {Expression | undefined} valueExpr expression for the value
* @returns {void}
*/
const checkNamespace = (topLevel, members, valueExpr) => {
if (!DynamicExports.isEnabled(parser.state)) return;
if (members.length > 0 && members[0] === "__esModule") {
if (valueExpr && isTruthyLiteral(valueExpr) && topLevel) {
DynamicExports.setFlagged(parser.state);
} else {
DynamicExports.setDynamic(parser.state);
}
}
};
/**
* Processes the provided reason.
* @param {string=} reason reason
*/
const bailout = (reason) => {
DynamicExports.bailout(parser.state);
if (reason) bailoutHint(reason);
};
/**
* Processes the provided reason.
* @param {string} reason reason
*/
const bailoutHint = (reason) => {
recordOptimizationBailout(
parser.state.module.buildInfo,
`CommonJS bailout: ${reason}`
);
};
/** @type {JavascriptParser["state"] | undefined} */
let keptAllExportsForState;
/**
* An exported function accessing its own `this` may reach any export
* through it when called as a method of the exports object, so the
* whole exports object must be kept (#21178).
* @param {Node} valueExpr the exported value
* @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
* @returns {void}
*/
const keepAllExportsOnThisAccess = (valueExpr, base) => {
// One bailout per module is enough
if (keptAllExportsForState === parser.state) return;
const thisExpr = getThisAccessInExportedValue(parser, valueExpr);
if (thisExpr === undefined) return;
keptAllExportsForState = parser.state;
bailoutHint(
`this in exported function may access any export, so all exports are kept, at ${formatLocation(
parser.getLocation(thisExpr)
)}`
);
const dep = new CommonJsSelfReferenceDependency(
/** @type {Range} */ (thisExpr.range),
base,
[],
false
);
dep.loc = parser.getLocation(thisExpr);
parser.state.module.addDependency(dep);
};
// metadata //
parser.hooks.evaluateTypeof
.for("module")
.tap(PLUGIN_NAME, evaluateToString("object"));
parser.hooks.evaluateTypeof
.for("exports")
.tap(PLUGIN_NAME, evaluateToString("object"));
// exporting //
/**
* Handle assign export.
* @param {AssignmentExpression} expr expression
* @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
* @param {Members} members members of the export
* @returns {boolean | undefined} true, when the expression was handled
*/
const handleAssignExport = (expr, base, members) => {
if (ESMExports.isEnabled(parser.state)) return;
// Handle reexporting; `+=` and the like combine with the current value
const requireCall =
expr.operator === "="
? parseRequireCall(parser, expr.right)
: undefined;
if (
requireCall &&
(members.length === 0 || members[0] !== "__esModule")
) {
enableStructuredExports();
// It's possible to reexport __esModule, so we must convert to a dynamic module
if (members.length === 0) DynamicExports.setDynamic(parser.state);
const dep = new CommonJsExportRequireDependency(
/** @type {Range} */ (expr.range),
null,
base,
members,
requireCall.request,
requireCall.ids,
!parser.isStatementLevelExpression(expr)
);
if (requireCall.binding) {
dep.bindingRange = requireCall.binding.range;
dep.bindingDependency = requireCall.binding.dep;
}
dep.asiSafe = !parser.isAsiPosition(
/** @type {Range} */ (expr.range)[0]
);
dep.loc = parser.getLocation(expr);
dep.optional = Boolean(parser.scope.inTry);
parser.state.module.addDependency(dep);
parser.state.module.buildMeta.treatAsCommonJs = true;
return true;
}
// `module.exports = { … }` — properties are the module's named exports.
if (members.length === 0) {
return handleObjectLiteralExport(expr, base);
}
enableStructuredExports();
const remainingMembers = members;
checkNamespace(
/** @type {StatementPath} */
(parser.statementPath).length === 1 &&
parser.isStatementLevelExpression(expr),
remainingMembers,
expr.right
);
const dep = new CommonJsExportsDependency(
/** @type {Range} */ (expr.left.range),
null,
base,
remainingMembers
);
dep.loc = parser.getLocation(expr);
parser.state.module.addDependency(dep);
parser.state.module.buildMeta.treatAsCommonJs = true;
keepAllExportsOnThisAccess(expr.right, base);
parser.walkExpression(expr.right);
return true;
};
/**
* Handle top-level `module.exports = { … }` object-literal exports.
* @param {AssignmentExpression} expr expression
* @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
* @returns {boolean | undefined} true, when the expression was handled
*/
const handleObjectLiteralExport = (expr, base) => {
// Only `module.exports = { … }` replaces the whole exports object.
if (base !== "module.exports") return;
if (
/** @type {StatementPath} */ (parser.statementPath).length !== 1 ||
!parser.isStatementLevelExpression(expr)
) {
return;
}
const object = expr.right;
if (object.type !== "ObjectExpression") return;
const module = parser.state.module;
module.buildMeta.treatAsCommonJs = true;
// LHS stays as the text `module.exports` (moduleArgument is "module"
// unless a top-level `module` binding shadows it, which prevents this hook).
module.addPresentationalDependency(
new RuntimeRequirementsDependency([RuntimeGlobals.module])
);
// one unknown key makes the whole object opaque, so name them all first
/** @type {string[]} */
const names = [];
for (const property of object.properties) {
const name = getStaticPropertyName(property);
if (name === undefined) {
bailout(
`module.exports = {…} cannot be statically analysed at ${formatLocation(
parser.getLocation(expr)
)}`
);
parser.walkExpression(object);
return true;
}
names.push(name);
}
enableStructuredExports();
for (let i = 0; i < object.properties.length; i++) {
const property = /** @type {Property} */ (object.properties[i]);
const name = names[i];
const value = /** @type {Expression} */ (property.value);
const kind = getObjectExportKind(property);
// a shorthand has no value text of its own to replace
const valueRange = /** @type {Range} */ (
kind === "shorthand" ? property.range : value.range
);
// scans the key/value gap, so `/*#__PURE__*/` is picked up here
const pure =
kind === "keyValue" &&
parser.isPure(value, /** @type {Range} */ (property.key.range)[1]);
if (name === "__esModule") {
checkNamespace(true, ["__esModule"], value);
}
const dep = new CommonJsObjectExportDependency(
/** @type {Range} */ (property.range),
/** @type {Range} */ (property.key.range),
valueRange,
name,
kind,
pure
);
dep.loc = parser.getLocation(property);
module.addDependency(dep);
keepAllExportsOnThisAccess(value, base);
// Tag nested requires so unused get/set/method can disconnect them.
const start = FUNCTION_PROPERTY_DROP_HEAD.has(kind)
? module.dependencies.length
: -1;
parser.walkExpression(value);
if (start !== -1) {
const usedByExports = new Set([name]);
for (let j = start; j < module.dependencies.length; j++) {
const nested = module.dependencies[j];
if (isDependencyKind(nested, "commonjs-require")) {
nested.usedByExports = usedByExports;
}
}
}
}
return true;
};
parser.hooks.assignMemberChain
.for("exports")
.tap(PLUGIN_NAME, (expr, members) =>
handleAssignExport(expr, "exports", members)
);
if (thisAsExports) {
parser.hooks.assignMemberChain
.for("this")
.tap(PLUGIN_NAME, (expr, members) => {
if (!parser.scope.topLevelScope) return;
return handleAssignExport(expr, "this", members);
});
}
parser.hooks.assignMemberChain
.for("module")
.tap(PLUGIN_NAME, (expr, members) => {
if (members[0] !== "exports") return;
return handleAssignExport(expr, "module.exports", members.slice(1));
});
parser.hooks.call
.for("Object.defineProperty")
.tap(PLUGIN_NAME, (expression) => {
const expr = expression;
if (!parser.isStatementLevelExpression(expr)) return;
if (expr.arguments.length !== 3) return;
if (expr.arguments[0].type === "SpreadElement") return;
if (expr.arguments[1].type === "SpreadElement") return;
if (expr.arguments[2].type === "SpreadElement") return;
const exportsArg = parser.evaluateExpression(expr.arguments[0]);
if (!exportsArg.isIdentifier()) return;
if (
exportsArg.identifier !== "exports" &&
exportsArg.identifier !== "module.exports" &&
(exportsArg.identifier !== "this" ||
!thisAsExports ||
!parser.scope.topLevelScope)
) {
return;
}
const propertyArg = parser.evaluateExpression(expr.arguments[1]);
const property = propertyArg.asString();
if (typeof property !== "string") return;
enableStructuredExports();
const descArg = expr.arguments[2];
// Handle reexporting: `Object.defineProperty(exports, "x", { value: require("./y")[.z] })`
// and the lazy getter form `{ get: () => require("./y")[.z] }`.
if (property !== "__esModule") {
const reexport = getReexportOfPropertyDescriptor(descArg);
const requireCall =
reexport && parseRequireCall(parser, reexport.expr);
if (reexport && requireCall) {
const dep = new CommonJsExportRequireDependency(
/** @type {Range} */ (expr.range),
/** @type {Range} */ (reexport.expr.range),
`Object.defineProperty(${exportsArg.identifier})`,
[property],
requireCall.request,
requireCall.ids,
false
);
dep.getter = reexport.getter;
dep.descriptorStart = /** @type {Range} */ (descArg.range)[0];
if (requireCall.binding) {
dep.bindingRange = requireCall.binding.range;
dep.bindingDependency = requireCall.binding.dep;
}
dep.asiSafe = !parser.isAsiPosition(
/** @type {Range} */ (expr.range)[0]
);
dep.loc = parser.getLocation(expr);
dep.optional = Boolean(parser.scope.inTry);
parser.state.module.addDependency(dep);
parser.state.module.buildMeta.treatAsCommonJs = true;
return true;
}
}
checkNamespace(
/** @type {StatementPath} */
(parser.statementPath).length === 1,
[property],
getValueOfPropertyDescription(descArg)
);
const dep = new CommonJsExportsDependency(
/** @type {Range} */ (expr.range),
/** @type {Range} */ (expr.arguments[2].range),
`Object.defineProperty(${exportsArg.identifier})`,
[property]
);
dep.loc = parser.getLocation(expr);
parser.state.module.addDependency(dep);
parser.state.module.buildMeta.treatAsCommonJs = true;
if (descArg.type === "ObjectExpression") {
// `value`, `get` and `set` descriptor functions are all called
// with the exports object as `this`
for (const descProperty of descArg.properties) {
if (descProperty.type === "SpreadElement") continue;
keepAllExportsOnThisAccess(
descProperty.value,
/** @type {CommonJSDependencyBaseKeywords} */
(exportsArg.identifier)
);
}
}
parser.walkExpression(expr.arguments[2]);
return true;
});
// Self reference //
/**
* Handle access export.
* @param {Expression | Super} expr expression
* @param {CommonJSDependencyBaseKeywords} base commonjs base keywords
* @param {Members} members members of the export
* @param {CallExpression=} call call expression
* @returns {boolean | void} true, when the expression was handled
*/
const handleAccessExport = (expr, base, members, call) => {
if (ESMExports.isEnabled(parser.state)) return;
if (members.length === 0) {
bailout(
`${base} is used directly at ${formatLocation(
parser.getLocation(expr)
)}`
);
}
if (call && members.length === 1) {
bailoutHint(
`${base}${propertyAccess(
members
)}(...) prevents optimization as ${base} is passed as call context at ${formatLocation(
parser.getLocation(expr)
)}`
);
}
const dep = new CommonJsSelfReferenceDependency(
/** @type {Range} */ (expr.range),
base,
members,
Boolean(call)
);
dep.loc = parser.getLocation(expr);
parser.state.module.addDependency(dep);
parser.state.module.buildMeta.treatAsCommonJs = true;
if (call) {
parser.walkExpressions(call.arguments);
}
return true;
};
parser.hooks.callMemberChain
.for("exports")
.tap(PLUGIN_NAME, (expr, members) =>
handleAccessExport(expr.callee, "exports", members, expr)
);
parser.hooks.expressionMemberChain
.for("exports")
.tap(PLUGIN_NAME, (expr, members) =>
handleAccessExport(expr, "exports", members)
);
parser.hooks.expression
.for("exports")
.tap(PLUGIN_NAME, (expr) => handleAccessExport(expr, "exports", []));
parser.hooks.callMemberChain
.for("module")
.tap(PLUGIN_NAME, (expr, members) => {
if (members[0] !== "exports") return;
return handleAccessExport(
expr.callee,
"module.exports",
members.slice(1),
expr
);
});
parser.hooks.expressionMemberChain
.for("module")
.tap(PLUGIN_NAME, (expr, members) => {
if (members[0] !== "exports") return;
return handleAccessExport(expr, "module.exports", members.slice(1));
});
parser.hooks.expression
.for("module.exports")
.tap(PLUGIN_NAME, (expr) =>
handleAccessExport(expr, "module.exports", [])
);
if (thisAsExports) {
parser.hooks.callMemberChain
.for("this")
.tap(PLUGIN_NAME, (expr, members) => {
if (!parser.scope.topLevelScope) return;
return handleAccessExport(expr.callee, "this", members, expr);
});
parser.hooks.expressionMemberChain
.for("this")
.tap(PLUGIN_NAME, (expr, members) => {
if (!parser.scope.topLevelScope) return;
return handleAccessExport(expr, "this", members);
});
parser.hooks.expression.for("this").tap(PLUGIN_NAME, (expr) => {
if (!parser.scope.topLevelScope) return;
return handleAccessExport(expr, "this", []);
});
}
// Bailouts //
parser.hooks.expression.for("module").tap(PLUGIN_NAME, (expr) => {
const isHarmony = ESMExports.isEnabled(parser.state);
// Silent under ESM, where `ESMExports` governs and nothing was lost.
bailout(
isHarmony
? undefined
: `module is used directly at ${formatLocation(
parser.getLocation(expr)
)}`
);
const dep = new ModuleDecoratorDependency(
isHarmony
? RuntimeGlobals.esmModuleDecorator
: RuntimeGlobals.nodeModuleDecorator,
!isHarmony
);
dep.loc = parser.getLocation(expr);
parser.state.module.addDependency(dep);
return true;
});
}
}
module.exports = CommonJsExportsParserPlugin;