From 0fd43e0e87055828cc95b95d099ccd51768bd6dd Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Sat, 26 Feb 2022 23:16:21 +0300 Subject: [PATCH] import.meta.webpackContext --- .../ImportMetaContextDependency.js | 51 ++++++++++ ...ImportMetaContextDependencyParserPlugin.js | 98 +++++++++++++++++++ lib/dependencies/RequireContextPlugin.js | 26 +++++ lib/util/internalSerializables.js | 2 + module.d.ts | 8 ++ test/cases/chunks/context-weak/index.js | 15 ++- 6 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 lib/dependencies/ImportMetaContextDependency.js create mode 100644 lib/dependencies/ImportMetaContextDependencyParserPlugin.js diff --git a/lib/dependencies/ImportMetaContextDependency.js b/lib/dependencies/ImportMetaContextDependency.js new file mode 100644 index 00000000000..672e26fca25 --- /dev/null +++ b/lib/dependencies/ImportMetaContextDependency.js @@ -0,0 +1,51 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const makeSerializable = require("../util/makeSerializable"); +const ContextDependency = require("./ContextDependency"); +const ModuleDependencyTemplateAsRequireId = require("./ModuleDependencyTemplateAsRequireId"); + +class ImportMetaContextDependency extends ContextDependency { + constructor(options, range) { + super(options); + + this.range = range; + } + + get category() { + return "esm"; + } + + get type() { + return "import.meta.webpackContext"; + } + + serialize(context) { + const { write } = context; + + write(this.range); + + super.serialize(context); + } + + deserialize(context) { + const { read } = context; + + this.range = read(); + + super.deserialize(context); + } +} + +makeSerializable( + ImportMetaContextDependency, + "webpack/lib/dependencies/ImportMetaContextDependency" +); + +ImportMetaContextDependency.Template = ModuleDependencyTemplateAsRequireId; + +module.exports = ImportMetaContextDependency; diff --git a/lib/dependencies/ImportMetaContextDependencyParserPlugin.js b/lib/dependencies/ImportMetaContextDependencyParserPlugin.js new file mode 100644 index 00000000000..aa0fc0b8a6a --- /dev/null +++ b/lib/dependencies/ImportMetaContextDependencyParserPlugin.js @@ -0,0 +1,98 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +const { + evaluateToIdentifier +} = require("../javascript/JavascriptParserHelpers"); +const ImportMetaContextDependency = require("./ImportMetaContextDependency"); + +/** @typedef {import("estree").Expression} ExpressionNode */ +/** @typedef {import("estree").ObjectExpression} ObjectExpressionNode */ +/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ + +/** + * @param {JavascriptParser} parser parser + * @param {ObjectExpressionNode} optionsNode node + * @returns {{mode: string, recursive: boolean, regExp: RegExp}} options + */ +function getOptions(parser, optionsNode) { + let regExp = /^\.\/.*$/; + let recursive = true; + let mode = "sync"; + if (optionsNode) { + for (const prop of optionsNode.properties) { + if (prop.type !== "Property" || prop.key.type !== "Identifier") return; + switch (prop.key.name) { + case "regExp": { + const regExpExpr = parser.evaluateExpression( + /** @type {ExpressionNode} */ (prop.value) + ); + if (!regExpExpr.isRegExp()) return; + regExp = regExpExpr.regExp; + break; + } + case "mode": { + const modeExpr = parser.evaluateExpression( + /** @type {ExpressionNode} */ (prop.value) + ); + if (!modeExpr.isString()) return; + mode = modeExpr.string; + break; + } + case "recursive": { + const recursiveExpr = parser.evaluateExpression( + /** @type {ExpressionNode} */ (prop.value) + ); + if (!recursiveExpr.isBoolean()) return; + recursive = recursiveExpr.bool; + } + } + } + } + + return { recursive, regExp, mode }; +} + +module.exports = class ImportMetaContextDependencyParserPlugin { + apply(parser) { + parser.hooks.evaluateIdentifier + .for("import.meta.webpackContext") + .tap("HotModuleReplacementPlugin", expr => { + return evaluateToIdentifier( + "import.meta.webpackContext", + "import.meta", + () => ["webpackContext"], + true + )(expr); + }); + parser.hooks.call + .for("import.meta.webpackContext") + .tap("ImportMetaContextDependencyParserPlugin", expr => { + if (expr.arguments.length < 1 || expr.arguments.length > 2) return; + const [directoryNode, optionsNode] = expr.arguments; + if (optionsNode && optionsNode.type !== "ObjectExpression") return; + const requestExpr = parser.evaluateExpression(directoryNode); + if (!requestExpr.isString()) return; + const request = requestExpr.string; + const options = getOptions(parser, optionsNode); + if (!options) return; + + const dep = new ImportMetaContextDependency( + { + request, + ...options, + category: "esm" + }, + expr.range + ); + dep.loc = expr.loc; + dep.optional = !!parser.scope.inTry; + parser.state.current.addDependency(dep); + return true; + }); + } +}; diff --git a/lib/dependencies/RequireContextPlugin.js b/lib/dependencies/RequireContextPlugin.js index d34c85e452a..6b7d1403e00 100644 --- a/lib/dependencies/RequireContextPlugin.js +++ b/lib/dependencies/RequireContextPlugin.js @@ -7,6 +7,8 @@ const { cachedSetProperty } = require("../util/cleverMerge"); const ContextElementDependency = require("./ContextElementDependency"); +const ImportMetaContextDependency = require("./ImportMetaContextDependency"); +const ImportMetaContextDependencyParserPlugin = require("./ImportMetaContextDependencyParserPlugin"); const RequireContextDependency = require("./RequireContextDependency"); const RequireContextDependencyParserPlugin = require("./RequireContextDependencyParserPlugin"); @@ -34,6 +36,14 @@ class RequireContextPlugin { RequireContextDependency, new RequireContextDependency.Template() ); + compilation.dependencyFactories.set( + ImportMetaContextDependency, + contextModuleFactory + ); + compilation.dependencyTemplates.set( + ImportMetaContextDependency, + new ImportMetaContextDependency.Template() + ); compilation.dependencyFactories.set( ContextElementDependency, @@ -50,6 +60,22 @@ class RequireContextPlugin { new RequireContextDependencyParserPlugin().apply(parser); }; + const handlerImportMeta = (parser, parserOptions) => { + if ( + parserOptions.requireContext !== undefined && + !parserOptions.requireContext + ) + return; + + new ImportMetaContextDependencyParserPlugin().apply(parser); + }; + + normalModuleFactory.hooks.parser + .for("javascript/auto") + .tap("RequireContextPlugin", handlerImportMeta); + normalModuleFactory.hooks.parser + .for("javascript/esm") + .tap("RequireContextPlugin", handlerImportMeta); normalModuleFactory.hooks.parser .for("javascript/auto") .tap("RequireContextPlugin", handler); diff --git a/lib/util/internalSerializables.js b/lib/util/internalSerializables.js index 9264c1c00c8..eabbb581bb6 100644 --- a/lib/util/internalSerializables.js +++ b/lib/util/internalSerializables.js @@ -126,6 +126,8 @@ module.exports = { require("../dependencies/ImportMetaHotAcceptDependency"), "dependencies/ImportMetaHotDeclineDependency": () => require("../dependencies/ImportMetaHotDeclineDependency"), + "dependencies/ImportMetaContextDependency": () => + require("../dependencies/ImportMetaContextDependency"), "dependencies/ProvidedDependency": () => require("../dependencies/ProvidedDependency"), "dependencies/PureExpressionDependency": () => diff --git a/module.d.ts b/module.d.ts index f7f9ac120da..4af90d7d32a 100644 --- a/module.d.ts +++ b/module.d.ts @@ -147,6 +147,14 @@ interface ImportMeta { url: string; webpack: number; webpackHot: webpack.Hot; + webpackContext: ( + request: string, + options?: { + recursive?: boolean; + regExp?: RegExp; + mode?: "sync" | "eager" | "weak" | "lazy" | "lazy-once"; + } + ) => webpack.Context; } declare const __resourceQuery: string; diff --git a/test/cases/chunks/context-weak/index.js b/test/cases/chunks/context-weak/index.js index 65aa0c58c1d..5ea896d645a 100644 --- a/test/cases/chunks/context-weak/index.js +++ b/test/cases/chunks/context-weak/index.js @@ -1,18 +1,27 @@ +it("import.meta.webpackContext without arguments should work", function() { + const contextRequire = import.meta.webpackContext("./dir"); + expect(contextRequire("./four")).toBe(4); +}); + it("should not bundle context requires with asyncMode === 'weak'", function() { - var contextRequire = require.context(".", false, /two/, "weak"); + const contextRequire = import.meta.webpackContext(".", { + recursive: false, + regExp: /two/, + mode: "weak" + }); expect(function() { contextRequire("./two") }).toThrowError(/not available/); }); it("should find module with asyncMode === 'weak' when required elsewhere", function() { - var contextRequire = require.context(".", false, /.+/, "weak"); + const contextRequire = require.context(".", false, /.+/, "weak"); expect(contextRequire("./three")).toBe(3); require("./three"); // in a real app would be served as a separate chunk }); it("should find module with asyncMode === 'weak' when required elsewhere (recursive)", function() { - var contextRequire = require.context(".", true, /.+/, "weak"); + const contextRequire = require.context(".", true, /.+/, "weak"); expect(contextRequire("./dir/four")).toBe(4); require("./dir/four"); // in a real app would be served as a separate chunk });