/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const AsyncDependenciesBlock = require("../AsyncDependenciesBlock"); const CommentCompilationWarning = require("../CommentCompilationWarning"); const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning"); const { VariableInfo, getImportAttributes } = require("../javascript/JavascriptParser"); const traverseDestructuringAssignmentProperties = require("../util/traverseDestructuringAssignmentProperties"); const ContextDependencyHelpers = require("./ContextDependencyHelpers"); const ImportContextDependency = require("./ImportContextDependency"); const ImportDependency = require("./ImportDependency"); const ImportEagerDependency = require("./ImportEagerDependency"); const ImportWeakDependency = require("./ImportWeakDependency"); /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */ /** @typedef {import("../ChunkGroup").RawChunkGroupOptions} RawChunkGroupOptions */ /** @typedef {import("../ContextModule").ContextMode} ContextMode */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */ /** @typedef {import("../Module").BuildMeta} BuildMeta */ /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ /** @typedef {import("../javascript/JavascriptParser").ImportExpression} ImportExpression */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @typedef {import("../javascript/JavascriptParser").ParserState} ParserState */ /** @typedef {import("../javascript/JavascriptParser").Members} Members */ /** @typedef {import("../javascript/JavascriptParser").MembersOptionals} MembersOptionals */ /** @typedef {import("../javascript/JavascriptParser").ArrowFunctionExpression} ArrowFunctionExpression */ /** @typedef {import("../javascript/JavascriptParser").FunctionExpression} FunctionExpression */ /** @typedef {import("../javascript/JavascriptParser").Identifier} Identifier */ /** @typedef {import("../javascript/JavascriptParser").ObjectPattern} ObjectPattern */ /** @typedef {import("../javascript/JavascriptParser").CallExpression} CallExpression */ /** @typedef {{ references: RawReferencedExports, expression: ImportExpression }} ImportSettings */ /** @typedef {WeakMap} State */ /** @type {WeakMap} */ const parserStateMap = new WeakMap(); const dynamicImportTag = Symbol("import()"); /** * @param {JavascriptParser} parser javascript parser * @returns {State} import parser plugin state */ function getState(parser) { if (!parserStateMap.has(parser.state)) { parserStateMap.set(parser.state, new WeakMap()); } return /** @type {State} */ (parserStateMap.get(parser.state)); } /** * @param {JavascriptParser} parser javascript parser * @param {ImportExpression} importCall import expression * @param {string} variableName variable name */ function tagDynamicImportReferenced(parser, importCall, variableName) { const state = getState(parser); /** @type {RawReferencedExports} */ const references = state.get(importCall) || []; state.set(importCall, references); parser.tagVariable( variableName, dynamicImportTag, /** @type {ImportSettings} */ ({ references, expression: importCall }) ); } /** * @param {CallExpression} importThen import().then() call * @returns {Identifier | ObjectPattern | undefined} the dynamic imported namespace obj */ function getFulfilledCallbackNamespaceObj(importThen) { const fulfilledCallback = importThen.arguments[0]; if ( fulfilledCallback && (fulfilledCallback.type === "ArrowFunctionExpression" || fulfilledCallback.type === "FunctionExpression") && fulfilledCallback.params[0] && (fulfilledCallback.params[0].type === "Identifier" || fulfilledCallback.params[0].type === "ObjectPattern") ) { return fulfilledCallback.params[0]; } } /** * @param {JavascriptParser} parser javascript parser * @param {ImportExpression} importCall import expression * @param {ArrowFunctionExpression | FunctionExpression} fulfilledCallback the fulfilled callback * @param {Identifier | ObjectPattern} namespaceObjArg the argument of namespace object= */ function walkImportThenFulfilledCallback( parser, importCall, fulfilledCallback, namespaceObjArg ) { const arrow = fulfilledCallback.type === "ArrowFunctionExpression"; const wasTopLevel = parser.scope.topLevelScope; parser.scope.topLevelScope = arrow ? (wasTopLevel ? "arrow" : false) : false; const scopeParams = [...fulfilledCallback.params]; // Add function name in scope for recursive calls if (!arrow && fulfilledCallback.id) { scopeParams.push(fulfilledCallback.id); } parser.inFunctionScope(!arrow, scopeParams, () => { if (namespaceObjArg.type === "Identifier") { tagDynamicImportReferenced(parser, importCall, namespaceObjArg.name); } else { parser.enterDestructuringAssignment(namespaceObjArg, importCall); const referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(importCall); if (referencedPropertiesInDestructuring) { const state = getState(parser); const references = /** @type {RawReferencedExports} */ ( state.get(importCall) ); /** @type {RawReferencedExports} */ const refsInDestructuring = []; traverseDestructuringAssignmentProperties( referencedPropertiesInDestructuring, (stack) => refsInDestructuring.push(stack.map((p) => p.id)) ); for (const ids of refsInDestructuring) { references.push(ids); } } } for (const param of fulfilledCallback.params) { parser.walkPattern(param); } if (fulfilledCallback.body.type === "BlockStatement") { parser.detectMode(fulfilledCallback.body.body); const prev = parser.prevStatement; parser.preWalkStatement(fulfilledCallback.body); parser.prevStatement = prev; parser.walkStatement(fulfilledCallback.body); } else { parser.walkExpression(fulfilledCallback.body); } }); parser.scope.topLevelScope = wasTopLevel; } /** * @template T * @param {Iterable} enumerable enumerable * @returns {T[][]} array of array */ const exportsFromEnumerable = (enumerable) => Array.from(enumerable, (e) => [e]); const PLUGIN_NAME = "ImportParserPlugin"; class ImportParserPlugin { /** * @param {JavascriptParserOptions} options options */ constructor(options) { this.options = options; } /** * @param {JavascriptParser} parser the parser * @returns {void} */ apply(parser) { /** * @param {Members} members members * @param {MembersOptionals} membersOptionals members Optionals * @returns {string[]} a non optional part */ function getNonOptionalPart(members, membersOptionals) { let i = 0; while (i < members.length && membersOptionals[i] === false) i++; return i !== members.length ? members.slice(0, i) : members; } parser.hooks.collectDestructuringAssignmentProperties.tap( PLUGIN_NAME, (expr) => { if (expr.type === "ImportExpression") return true; const nameInfo = parser.getNameForExpression(expr); if ( nameInfo && nameInfo.rootInfo instanceof VariableInfo && nameInfo.rootInfo.name && parser.getTagData(nameInfo.rootInfo.name, dynamicImportTag) ) { return true; } } ); parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl) => { if ( decl.init && decl.init.type === "AwaitExpression" && decl.init.argument.type === "ImportExpression" && decl.id.type === "Identifier" ) { parser.defineVariable(decl.id.name); tagDynamicImportReferenced(parser, decl.init.argument, decl.id.name); } }); parser.hooks.expression.for(dynamicImportTag).tap(PLUGIN_NAME, (expr) => { const settings = /** @type {ImportSettings} */ (parser.currentTagData); const referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); if (referencedPropertiesInDestructuring) { /** @type {RawReferencedExports} */ const refsInDestructuring = []; traverseDestructuringAssignmentProperties( referencedPropertiesInDestructuring, (stack) => refsInDestructuring.push(stack.map((p) => p.id)) ); for (const ids of refsInDestructuring) { settings.references.push(ids); } } else { settings.references.push([]); } return true; }); parser.hooks.expressionMemberChain .for(dynamicImportTag) .tap(PLUGIN_NAME, (_expression, members, membersOptionals) => { const settings = /** @type {ImportSettings} */ (parser.currentTagData); const ids = getNonOptionalPart(members, membersOptionals); settings.references.push(ids); return true; }); parser.hooks.callMemberChain .for(dynamicImportTag) .tap(PLUGIN_NAME, (expression, members, membersOptionals) => { const { arguments: args } = expression; const settings = /** @type {ImportSettings} */ (parser.currentTagData); let ids = getNonOptionalPart(members, membersOptionals); const directImport = members.length === 0; if ( !directImport && (this.options.strictThisContextOnImports || ids.length > 1) ) { ids = ids.slice(0, -1); } settings.references.push(ids); if (args) parser.walkExpressions(args); return true; }); parser.hooks.importCall.tap(PLUGIN_NAME, (expr, importThen) => { const param = parser.evaluateExpression(expr.source); let chunkName = null; let mode = /** @type {ContextMode} */ (this.options.dynamicImportMode); let include = null; let exclude = null; /** @type {RawReferencedExports | null} */ let exports = null; /** @type {RawChunkGroupOptions} */ const groupOptions = {}; const { dynamicImportPreload, dynamicImportPrefetch, dynamicImportFetchPriority } = this.options; if ( dynamicImportPreload !== undefined && dynamicImportPreload !== false ) { groupOptions.preloadOrder = dynamicImportPreload === true ? 0 : dynamicImportPreload; } if ( dynamicImportPrefetch !== undefined && dynamicImportPrefetch !== false ) { groupOptions.prefetchOrder = dynamicImportPrefetch === true ? 0 : dynamicImportPrefetch; } if ( dynamicImportFetchPriority !== undefined && dynamicImportFetchPriority !== false ) { groupOptions.fetchPriority = dynamicImportFetchPriority; } 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 CommentCompilationWarning( `Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`, /** @type {DependencyLocation} */ (comment.loc) ) ); } } let phase = expr.phase; if (!phase && importOptions && importOptions.webpackDefer !== undefined) { if (typeof importOptions.webpackDefer !== "boolean") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackDefer\` expected a boolean, but received: ${importOptions.webpackDefer}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else if (importOptions.webpackDefer) { phase = "defer"; } } if (phase === "defer") { parser.state.module.addWarning( new UnsupportedFeatureWarning( "import.defer() is not implemented yet.", /** @type {DependencyLocation} */ (expr.loc) ) ); } if (importOptions) { if (importOptions.webpackIgnore !== undefined) { if (typeof importOptions.webpackIgnore !== "boolean") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else if (importOptions.webpackIgnore) { // Do not instrument `import()` if `webpackIgnore` is `true` return false; } } if (importOptions.webpackChunkName !== undefined) { if (typeof importOptions.webpackChunkName !== "string") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackChunkName\` expected a string, but received: ${importOptions.webpackChunkName}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else { chunkName = importOptions.webpackChunkName; } } if (importOptions.webpackMode !== undefined) { if (typeof importOptions.webpackMode !== "string") { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackMode\` expected a string, but received: ${importOptions.webpackMode}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else { mode = /** @type {ContextMode} */ (importOptions.webpackMode); } } if (importOptions.webpackPrefetch !== undefined) { if (importOptions.webpackPrefetch === true) { groupOptions.prefetchOrder = 0; } else if (typeof importOptions.webpackPrefetch === "number") { groupOptions.prefetchOrder = importOptions.webpackPrefetch; } else { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } } if (importOptions.webpackPreload !== undefined) { if (importOptions.webpackPreload === true) { groupOptions.preloadOrder = 0; } else if (typeof importOptions.webpackPreload === "number") { groupOptions.preloadOrder = importOptions.webpackPreload; } else { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } } if (importOptions.webpackFetchPriority !== undefined) { if ( typeof importOptions.webpackFetchPriority === "string" && ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) ) { groupOptions.fetchPriority = /** @type {"low" | "high" | "auto"} */ (importOptions.webpackFetchPriority); } else { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackFetchPriority\` expected true or "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } } if (importOptions.webpackInclude !== undefined) { if ( !importOptions.webpackInclude || !(importOptions.webpackInclude instanceof RegExp) ) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackInclude\` expected a regular expression, but received: ${importOptions.webpackInclude}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else { include = importOptions.webpackInclude; } } if (importOptions.webpackExclude !== undefined) { if ( !importOptions.webpackExclude || !(importOptions.webpackExclude instanceof RegExp) ) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackExclude\` expected a regular expression, but received: ${importOptions.webpackExclude}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else { exclude = importOptions.webpackExclude; } } if (importOptions.webpackExports !== undefined) { if ( !( typeof importOptions.webpackExports === "string" || (Array.isArray(importOptions.webpackExports) && importOptions.webpackExports.every( (item) => typeof item === "string" )) ) ) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackExports\` expected a string or an array of strings, but received: ${importOptions.webpackExports}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); } else if (typeof importOptions.webpackExports === "string") { exports = [[importOptions.webpackExports]]; } else { exports = exportsFromEnumerable(importOptions.webpackExports); } } } if ( mode !== "lazy" && mode !== "lazy-once" && mode !== "eager" && mode !== "weak" ) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackMode\` expected 'lazy', 'lazy-once', 'eager' or 'weak', but received: ${mode}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); mode = "lazy"; } const referencedPropertiesInDestructuring = parser.destructuringAssignmentPropertiesFor(expr); const state = getState(parser); const referencedPropertiesInMember = state.get(expr); const fulfilledNamespaceObj = importThen && getFulfilledCallbackNamespaceObj(importThen); if ( referencedPropertiesInDestructuring || referencedPropertiesInMember || fulfilledNamespaceObj ) { if (exports) { parser.state.module.addWarning( new UnsupportedFeatureWarning( "You don't need `webpackExports` if the usage of dynamic import is statically analyse-able. You can safely remove the `webpackExports` magic comment.", /** @type {DependencyLocation} */ (expr.loc) ) ); } if (referencedPropertiesInDestructuring) { /** @type {RawReferencedExports} */ const refsInDestructuring = []; traverseDestructuringAssignmentProperties( referencedPropertiesInDestructuring, (stack) => refsInDestructuring.push(stack.map((p) => p.id)) ); exports = refsInDestructuring; } else if (referencedPropertiesInMember) { exports = referencedPropertiesInMember; } else { /** @type {RawReferencedExports} */ const references = []; state.set(expr, references); exports = references; } } if (param.isString()) { const attributes = getImportAttributes(expr); if (mode === "eager") { const dep = new ImportEagerDependency( /** @type {string} */ (param.string), /** @type {Range} */ (expr.range), exports, attributes ); parser.state.current.addDependency(dep); } else if (mode === "weak") { const dep = new ImportWeakDependency( /** @type {string} */ (param.string), /** @type {Range} */ (expr.range), exports, attributes ); parser.state.current.addDependency(dep); } else { const depBlock = new AsyncDependenciesBlock( { ...groupOptions, name: chunkName }, /** @type {DependencyLocation} */ (expr.loc), param.string ); const dep = new ImportDependency( /** @type {string} */ (param.string), /** @type {Range} */ (expr.range), exports, attributes ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); dep.optional = Boolean(parser.scope.inTry); depBlock.addDependency(dep); parser.state.current.addBlock(depBlock); } } else { if (mode === "weak") { mode = "async-weak"; } const dep = ContextDependencyHelpers.create( ImportContextDependency, /** @type {Range} */ (expr.range), param, expr, this.options, { chunkName, groupOptions, include, exclude, mode, namespaceObject: /** @type {BuildMeta} */ (parser.state.module.buildMeta).strictHarmonyModule ? "strict" : true, typePrefix: "import()", category: "esm", referencedExports: exports, attributes: getImportAttributes(expr) }, parser ); if (!dep) return; dep.loc = /** @type {DependencyLocation} */ (expr.loc); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); } if (fulfilledNamespaceObj) { walkImportThenFulfilledCallback( parser, expr, /** @type {ArrowFunctionExpression | FunctionExpression} */ (importThen.arguments[0]), fulfilledNamespaceObj ); parser.walkExpressions(importThen.arguments.slice(1)); } else if (importThen) { parser.walkExpressions(importThen.arguments); } return true; }); } } module.exports = ImportParserPlugin;