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.

573 lines (541 loc) 18.7 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin"); const WebpackError = require("../errors/WebpackError"); const { VariableInfo, getImportAttributes } = require("../javascript/JavascriptParser"); const { getInnerGraphUtils } = require("../optimize/InnerGraph"); const ConstDependency = require("./ConstDependency"); const HarmonyAcceptDependency = require("./HarmonyAcceptDependency"); const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency"); const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency"); const HarmonyExports = require("./HarmonyExports"); const { ExportPresenceModes, getNonOptionalPart } = require("./HarmonyImportDependency"); const { attachDependencyGuards, isPresentByGuards } = require("./HarmonyImportGuard"); const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency"); const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency"); const { ImportPhaseUtils, createGetImportPhase } = require("./ImportPhase"); /** @typedef {import("estree").Expression} Expression */ /** @typedef {import("estree").PrivateIdentifier} PrivateIdentifier */ /** @typedef {import("estree").Identifier} Identifier */ /** @typedef {import("estree").MemberExpression} MemberExpression */ /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../javascript/JavascriptParser").ExportAllDeclaration} ExportAllDeclaration */ /** @typedef {import("../javascript/JavascriptParser").ExportNamedDeclaration} ExportNamedDeclaration */ /** @typedef {import("../javascript/JavascriptParser").ImportAttributes} ImportAttributes */ /** @typedef {import("../javascript/JavascriptParser").ImportDeclaration} ImportDeclaration */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("../javascript/JavascriptParser").Members} Members */ /** @typedef {import("../javascript/JavascriptParser").MembersOptionals} MembersOptionals */ /** @typedef {import("./HarmonyImportDependency").Ids} Ids */ /** @typedef {import("./HarmonyImportDependency").ExportPresenceMode} ExportPresenceMode */ /** @typedef {import("./ImportPhase").ImportPhaseType} ImportPhaseType */ /** @typedef {import("./HarmonyImportGuard").GuardFrame} GuardFrame */ const harmonySpecifierTag = Symbol("harmony import"); // Shared placeholder: plain specifier references have no member ranges, so they // can all reuse one (never-mutated) array instead of allocating per dependency. /** @type {Range[]} */ const EMPTY_ID_RANGES = []; /** * Defines the harmony settings type used by this module. * @typedef {object} HarmonySettings * @property {Ids} ids * @property {string} source * @property {number} sourceOrder * @property {string} name * @property {boolean} await * @property {ImportAttributes=} attributes * @property {ImportPhaseType} phase */ const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin"; /** * Gets in operator harmony import info. * @param {JavascriptParser} parser the parser * @param {PrivateIdentifier | Expression} left left expression * @param {Expression} right right expression * @returns {{ leftPart: string, members: Members, settings: HarmonySettings } | undefined} info */ const getInOperatorHarmonyImportInfo = (parser, left, right) => { const leftPartEvaluated = parser.evaluateExpression(left); if (leftPartEvaluated.couldHaveSideEffects()) return; /** @type {string | undefined} */ const leftPart = leftPartEvaluated.asString(); if (!leftPart) return; const rightPart = parser.evaluateExpression(right); if (!rightPart.isIdentifier()) return; const rootInfo = rightPart.rootInfo; const root = typeof rootInfo === "string" ? rootInfo : rootInfo instanceof VariableInfo ? rootInfo.name : undefined; if (!root) return; const settings = /** @type {HarmonySettings | undefined} */ ( parser.getTagData(root, harmonySpecifierTag) ); if (!settings) { return; } return { leftPart, members: /** @type {(() => Members)} */ (rightPart.getMembers)(), settings }; }; /** * Whether the conditional test references an imported binding (so a dependency * guard can possibly gate a dead branch). Cheap pre-scan over the test AST. * @param {JavascriptParser} parser the parser * @param {Expression} node test expression * @returns {boolean} true when the test references a harmony import specifier */ const findImportSpecifier = (parser, node) => { switch (node.type) { case "Identifier": return Boolean(parser.getTagData(node.name, harmonySpecifierTag)); case "UnaryExpression": return findImportSpecifier( parser, /** @type {Expression} */ (node.argument) ); case "LogicalExpression": return ( findImportSpecifier(parser, /** @type {Expression} */ (node.left)) || findImportSpecifier(parser, /** @type {Expression} */ (node.right)) ); case "MemberExpression": return findImportSpecifier( parser, /** @type {Expression} */ (node.object) ); case "BinaryExpression": // `"x" in ns` presence guard return ( node.operator === "in" && findImportSpecifier(parser, /** @type {Expression} */ (node.right)) ); default: return false; } }; module.exports = class HarmonyImportDependencyParserPlugin { /** * Creates an instance of HarmonyImportDependencyParserPlugin. * @param {JavascriptParserOptions} options options */ constructor(options) { /** @type {JavascriptParserOptions} */ this.options = options; /** @type {ExportPresenceMode} */ this.exportPresenceMode = ExportPresenceModes.resolveFromOptions( options.importExportsPresence, options ); /** @type {boolean | undefined} */ this.strictThisContextOnImports = options.strictThisContextOnImports; } /** * Gets export presence mode. * @param {JavascriptParser} parser the parser * @param {HarmonySettings} settings settings * @param {Ids} ids ids * @returns {ExportPresenceMode} exportPresenceMode */ getExportPresenceMode(parser, settings, ids) { // Guards only apply to namespace imports if (settings.ids.length) return this.exportPresenceMode; const harmonySettings = /** @type {HarmonySettings=} */ ( parser.currentTagData ); if (!harmonySettings) return this.exportPresenceMode; if (this.exportPresenceMode === ExportPresenceModes.NONE) { return this.exportPresenceMode; } const stack = /** @type {GuardFrame[] | undefined} */ ( parser.state.guardStack ); if ( stack !== undefined && isPresentByGuards(parser, stack, harmonySettings.name, ids[0]) ) { return ExportPresenceModes.NONE; } return this.exportPresenceMode; } /** * Applies the plugin by registering its hooks on the compiler. * @param {JavascriptParser} parser the parser * @returns {void} */ apply(parser) { const getImportPhase = createGetImportPhase( this.options.deferImport, this.options.sourceImport ); /** * Gets non optional member chain. * @param {MemberExpression} node member expression * @param {number} count count * @returns {Expression} member expression */ function getNonOptionalMemberChain(node, count) { while (count--) node = /** @type {MemberExpression} */ (node.object); return node; } parser.hooks.isPure.for("Identifier").tap(PLUGIN_NAME, (expression) => { const expr = /** @type {Identifier} */ (expression); if ( parser.isVariableDefined(expr.name) || parser.getTagData(expr.name, harmonySpecifierTag) ) { return true; } }); parser.hooks.import.tap(PLUGIN_NAME, (statement, source) => { parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1; const clearDep = new ConstDependency( parser.isAsiPosition(/** @type {Range} */ (statement.range)[0]) ? ";" : "", /** @type {Range} */ (statement.range) ); clearDep.loc = parser.getLocation(statement); parser.state.module.addPresentationalDependency(clearDep); parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]); const attributes = getImportAttributes(statement); const phase = getImportPhase(parser, statement); if ( ImportPhaseUtils.isDefer(phase) && (statement.specifiers.length !== 1 || statement.specifiers[0].type !== "ImportNamespaceSpecifier") ) { const error = new WebpackError( "Deferred import can only be used with `import * as namespace from '...'` syntax." ); error.loc = parser.getLocation(statement) || undefined; parser.state.current.addError(error); } const sideEffectDep = new HarmonyImportSideEffectDependency( /** @type {string} */ (source), parser.state.lastHarmonyImportOrder, phase, attributes ); sideEffectDep.loc = parser.getLocation(statement); parser.state.module.addDependency(sideEffectDep); return true; }); parser.hooks.importSpecifier.tap( PLUGIN_NAME, (statement, source, id, name) => { const ids = id === null ? [] : [id]; const phase = getImportPhase(parser, statement); parser.tagVariable( name, harmonySpecifierTag, /** @type {HarmonySettings} */ ({ name, source, ids, sourceOrder: parser.state.lastHarmonyImportOrder, attributes: getImportAttributes(statement), phase }) ); return true; } ); parser.hooks.binaryExpression.tap(PLUGIN_NAME, (expression) => { if (expression.operator !== "in") return; const info = getInOperatorHarmonyImportInfo( parser, expression.left, expression.right ); if (!info) return; const { leftPart, members, settings } = info; const dep = new HarmonyEvaluatedImportSpecifierDependency( settings.source, settings.sourceOrder, [...settings.ids, ...members, leftPart], settings.name, /** @type {Range} */ (expression.range), settings.attributes, "in" ); dep.directImport = members.length === 0; dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expression.range)[0] ); dep.loc = parser.getLocation(expression); parser.state.module.addDependency(dep); getInnerGraphUtils(parser.state.compilation).onUsage( parser.state, (e) => (dep.usedByExports = e) ); return true; }); parser.hooks.collectDestructuringAssignmentProperties.tap( PLUGIN_NAME, (expr) => { const nameInfo = parser.getNameForExpression(expr); if ( nameInfo && nameInfo.rootInfo instanceof VariableInfo && nameInfo.rootInfo.name && parser.getTagData(nameInfo.rootInfo.name, harmonySpecifierTag) ) { return true; } } ); parser.hooks.expression .for(harmonySpecifierTag) .tap(PLUGIN_NAME, (expr) => { const settings = /** @type {HarmonySettings} */ (parser.currentTagData); const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, settings.ids, settings.name, /** @type {Range} */ (expr.range), this.exportPresenceMode, settings.phase, settings.attributes, EMPTY_ID_RANGES ); dep.referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); dep.shorthand = parser.scope.inShorthand; dep.directImport = true; dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expr.range)[0] ); dep.loc = parser.getLocation(expr); dep.call = parser.scope.inTaggedTemplateTag; parser.state.module.addDependency(dep); attachDependencyGuards(parser, dep); getInnerGraphUtils(parser.state.compilation).onUsage( parser.state, (e) => (dep.usedByExports = e) ); return true; }); parser.hooks.expressionMemberChain .for(harmonySpecifierTag) .tap( PLUGIN_NAME, (expression, members, membersOptionals, memberRanges) => { const settings = /** @type {HarmonySettings} */ (parser.currentTagData); const nonOptionalMembers = getNonOptionalPart( members, membersOptionals ); /** @type {Range[]} */ const ranges = memberRanges.slice( 0, memberRanges.length - (members.length - nonOptionalMembers.length) ); const expr = nonOptionalMembers !== members ? getNonOptionalMemberChain( expression, members.length - nonOptionalMembers.length ) : expression; const ids = [...settings.ids, ...nonOptionalMembers]; const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, ids, settings.name, /** @type {Range} */ (expr.range), this.getExportPresenceMode(parser, settings, ids), settings.phase, settings.attributes, ranges ); dep.referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expr.range)[0] ); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); attachDependencyGuards(parser, dep); getInnerGraphUtils(parser.state.compilation).onUsage( parser.state, (e) => (dep.usedByExports = e) ); return true; } ); parser.hooks.callMemberChain .for(harmonySpecifierTag) .tap( PLUGIN_NAME, (expression, members, membersOptionals, memberRanges) => { const { arguments: args } = expression; const callee = /** @type {MemberExpression} */ (expression.callee); const settings = /** @type {HarmonySettings} */ ( parser.currentTagData ); const nonOptionalMembers = getNonOptionalPart( members, membersOptionals ); /** @type {Range[]} */ const ranges = memberRanges.slice( 0, memberRanges.length - (members.length - nonOptionalMembers.length) ); const expr = nonOptionalMembers !== members ? getNonOptionalMemberChain( callee, members.length - nonOptionalMembers.length ) : callee; const ids = [...settings.ids, ...nonOptionalMembers]; const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, ids, settings.name, /** @type {Range} */ (expr.range), this.getExportPresenceMode(parser, settings, ids), settings.phase, settings.attributes, ranges ); dep.directImport = members.length === 0; dep.call = true; dep.asiSafe = !parser.isAsiPosition( /** @type {Range} */ (expr.range)[0] ); // only in case when we strictly follow the spec we need a special case here dep.namespaceObjectAsContext = members.length > 0 && /** @type {boolean} */ (this.strictThisContextOnImports); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); attachDependencyGuards(parser, dep); if (args) parser.walkExpressions(args); getInnerGraphUtils(parser.state.compilation).onUsage( parser.state, (e) => (dep.usedByExports = e) ); return true; } ); // Per the TC39 import-defer spec, [[Set]] on a Module Namespace // Exotic Object returns false without triggering evaluation. The // default expressionMemberChain path produces `<importVar>.a.foo` // whose `.a` getter eagerly requires (and thus evaluates) the // deferred module. For top-level `ns.foo = value`, walk only the // bare `ns` identifier so it gets replaced with the deferred // namespace proxy (whose set trap returns false), and leave the // `.foo = value` part as plain code. parser.hooks.assignMemberChain .for(harmonySpecifierTag) .tap(PLUGIN_NAME, (expression, members) => { const settings = /** @type {HarmonySettings} */ (parser.currentTagData); if (!ImportPhaseUtils.isDefer(settings.phase)) return; if (expression.operator !== "=") return; if (members.length !== 1) return; const left = /** @type {MemberExpression} */ (expression.left); if (left.object.type !== "Identifier") return; parser.walkExpression(expression.right); parser.walkExpression(left.object); return true; }); const { hotAcceptCallback, hotAcceptWithoutCallback } = HotModuleReplacementPlugin.getParserHooks(parser); hotAcceptCallback.tap(PLUGIN_NAME, (expr, requests) => { if (!HarmonyExports.isEnabled(parser.state)) { // This is not a harmony module, skip it return; } const dependencies = requests.map((request) => { const dep = new HarmonyAcceptImportDependency(request); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); return dep; }); if (dependencies.length > 0) { const dep = new HarmonyAcceptDependency( /** @type {Range} */ (expr.range), dependencies, true ); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); } }); hotAcceptWithoutCallback.tap(PLUGIN_NAME, (expr, requests) => { if (!HarmonyExports.isEnabled(parser.state)) { // This is not a harmony module, skip it return; } const dependencies = requests.map((request) => { const dep = new HarmonyAcceptImportDependency(request); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); return dep; }); if (dependencies.length > 0) { const dep = new HarmonyAcceptDependency( /** @type {Range} */ (expr.range), dependencies, false ); dep.loc = parser.getLocation(expr); parser.state.module.addDependency(dep); } }); parser.hooks.collectGuards.tap(PLUGIN_NAME, (expression) => { if (parser.scope.isAsmJs) return; const hasSpecifier = findImportSpecifier(parser, expression); const depStart = hasSpecifier ? /** @type {Module} */ (parser.state.module).dependencies.length : undefined; if (depStart === undefined) return; /** @type {GuardFrame} */ const consequent = { test: expression, depStart, condition: true }; /** @type {GuardFrame | undefined} */ const alternate = depStart === undefined ? undefined : { test: expression, depStart, condition: false }; return { consequent, alternate }; }); } }; module.exports.harmonySpecifierTag = harmonySpecifierTag;