diff --git a/lib/dependencies/CommonJsImportsParserPlugin.js b/lib/dependencies/CommonJsImportsParserPlugin.js index 47013f4269f..cabdab961e5 100644 --- a/lib/dependencies/CommonJsImportsParserPlugin.js +++ b/lib/dependencies/CommonJsImportsParserPlugin.js @@ -8,6 +8,7 @@ const CommentCompilationWarning = require("../CommentCompilationWarning"); const RuntimeGlobals = require("../RuntimeGlobals"); const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning"); +const WebpackError = require("../WebpackError"); const { evaluateToIdentifier, evaluateToString, @@ -25,9 +26,14 @@ const RequireHeaderDependency = require("./RequireHeaderDependency"); const RequireResolveContextDependency = require("./RequireResolveContextDependency"); const RequireResolveDependency = require("./RequireResolveDependency"); const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency"); +const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression"); +/** @typedef {import("estree").CallExpression} CallExpressionNode */ /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */ +const createRequireSpecifierTag = Symbol("createRequire"); +const createdRequireIdentifierTag = Symbol("createRequire()"); + class CommonJsImportsParserPlugin { /** * @param {JavascriptParserOptions} options parser options @@ -39,51 +45,76 @@ class CommonJsImportsParserPlugin { apply(parser) { const options = this.options; - // metadata // + //#region metadata const tapRequireExpression = (expression, getMembers) => { parser.hooks.typeof .for(expression) .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", toConstantDependency(parser, JSON.stringify("function")) ); parser.hooks.evaluateTypeof .for(expression) - .tap("CommonJsPlugin", evaluateToString("function")); + .tap("CommonJsImportsParserPlugin", evaluateToString("function")); parser.hooks.evaluateIdentifier .for(expression) .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", evaluateToIdentifier(expression, "require", getMembers, true) ); }; + const tapRequireExpressionTag = tag => { + parser.hooks.typeof + .for(tag) + .tap( + "CommonJsImportsParserPlugin", + toConstantDependency(parser, JSON.stringify("function")) + ); + parser.hooks.evaluateTypeof + .for(tag) + .tap("CommonJsImportsParserPlugin", evaluateToString("function")); + parser.hooks.evaluateIdentifier + .for(tag) + .tap("CommonJsImportsParserPlugin", expr => { + const info = parser.getVariableInfo(expr.name); + return new BasicEvaluatedExpression() + .setIdentifier(info, info, () => []) + .setSideEffects(false) + .setRange(expr.range); + }); + }; tapRequireExpression("require", () => []); tapRequireExpression("require.resolve", () => ["resolve"]); tapRequireExpression("require.resolveWeak", () => ["resolveWeak"]); + tapRequireExpressionTag(createdRequireIdentifierTag); + tapRequireExpressionTag(createRequireSpecifierTag); + //#endregion // Weird stuff // - parser.hooks.assign.for("require").tap("CommonJsPlugin", expr => { - // to not leak to global "require", we need to define a local require here. - const dep = new ConstDependency("var require;", 0); - dep.loc = expr.loc; - parser.state.module.addPresentationalDependency(dep); - return true; - }); + parser.hooks.assign + .for("require") + .tap("CommonJsImportsParserPlugin", expr => { + // to not leak to global "require", we need to define a local require here. + const dep = new ConstDependency("var require;", 0); + dep.loc = expr.loc; + parser.state.module.addPresentationalDependency(dep); + return true; + }); - // Unsupported // + //#region Unsupported parser.hooks.expression - .for("require.main.require") + .for("require.main") .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", expressionIsUnsupported( parser, - "require.main.require is not supported by webpack." + "require.main is not supported by webpack." ) ); parser.hooks.call .for("require.main.require") .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", expressionIsUnsupported( parser, "require.main.require is not supported by webpack." @@ -92,7 +123,7 @@ class CommonJsImportsParserPlugin { parser.hooks.expression .for("module.parent.require") .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", expressionIsUnsupported( parser, "module.parent.require is not supported by webpack." @@ -101,60 +132,93 @@ class CommonJsImportsParserPlugin { parser.hooks.call .for("module.parent.require") .tap( - "CommonJsPlugin", + "CommonJsImportsParserPlugin", expressionIsUnsupported( parser, "module.parent.require is not supported by webpack." ) ); + parser.hooks.unhandledExpressionMemberChain + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", (expr, members) => { + return expressionIsUnsupported( + parser, + `createRequire().${members.join(".")} is not supported by webpack.` + )(expr); + }); + //#endregion - // renaming // - parser.hooks.canRename.for("require").tap("CommonJsPlugin", () => true); - parser.hooks.rename.for("require").tap("CommonJsPlugin", expr => { + //#region Renaming + const defineUndefined = expr => { // To avoid "not defined" error, replace the value with undefined const dep = new ConstDependency("undefined", expr.range); dep.loc = expr.loc; parser.state.module.addPresentationalDependency(dep); return false; - }); + }; + parser.hooks.canRename + .for("require") + .tap("CommonJsImportsParserPlugin", () => true); + parser.hooks.canRename + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", () => true); + parser.hooks.canRename + .for(createRequireSpecifierTag) + .tap("CommonJsImportsParserPlugin", () => true); + parser.hooks.rename + .for("require") + .tap("CommonJsImportsParserPlugin", defineUndefined); + parser.hooks.rename + .for(createRequireSpecifierTag) + .tap("CommonJsImportsParserPlugin", defineUndefined); + //#endregion + + //#region Inspection + const requireCache = toConstantDependency( + parser, + RuntimeGlobals.moduleCache, + [ + RuntimeGlobals.moduleCache, + RuntimeGlobals.moduleId, + RuntimeGlobals.moduleLoaded + ] + ); - // inspection // parser.hooks.expression .for("require.cache") - .tap( - "CommonJsImportsParserPlugin", - toConstantDependency(parser, RuntimeGlobals.moduleCache, [ - RuntimeGlobals.moduleCache, - RuntimeGlobals.moduleId, - RuntimeGlobals.moduleLoaded - ]) - ); + .tap("CommonJsImportsParserPlugin", requireCache); + //#endregion - // require as expression // + //#region Require as expression + const requireHandler = expr => { + const dep = new CommonJsRequireContextDependency( + { + request: options.unknownContextRequest, + recursive: options.unknownContextRecursive, + regExp: options.unknownContextRegExp, + mode: "sync" + }, + expr.range, + undefined, + parser.scope.inShorthand + ); + dep.critical = + options.unknownContextCritical && + "require function is used in a way in which dependencies cannot be statically extracted"; + dep.loc = expr.loc; + dep.optional = !!parser.scope.inTry; + parser.state.current.addDependency(dep); + return true; + }; parser.hooks.expression .for("require") - .tap("CommonJsImportsParserPlugin", expr => { - const dep = new CommonJsRequireContextDependency( - { - request: options.unknownContextRequest, - recursive: options.unknownContextRecursive, - regExp: options.unknownContextRegExp, - mode: "sync" - }, - expr.range, - undefined, - parser.scope.inShorthand - ); - dep.critical = - options.unknownContextCritical && - "require function is used in a way in which dependencies cannot be statically extracted"; - dep.loc = expr.loc; - dep.optional = !!parser.scope.inTry; - parser.state.current.addDependency(dep); - return true; - }); + .tap("CommonJsImportsParserPlugin", requireHandler); + parser.hooks.expression + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", requireHandler); + //#endregion - // require // + //#region Require const processRequireItem = (expr, param) => { if (param.isString()) { const dep = new CommonJsRequireDependency(param.string, param.range); @@ -259,6 +323,9 @@ class CommonJsImportsParserPlugin { parser.hooks.call .for("require") .tap("CommonJsImportsParserPlugin", createRequireHandler(false)); + parser.hooks.call + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", createRequireHandler(false)); parser.hooks.new .for("require") .tap("CommonJsImportsParserPlugin", createRequireHandler(true)); @@ -268,8 +335,9 @@ class CommonJsImportsParserPlugin { parser.hooks.new .for("module.require") .tap("CommonJsImportsParserPlugin", createRequireHandler(true)); + //#endregion - // require with property access // + //#region Require with property access const chainHandler = (expr, calleeMembers, callExpr, members) => { if (callExpr.arguments.length !== 1) return; const param = parser.evaluateExpression(callExpr.arguments[0]); @@ -316,8 +384,9 @@ class CommonJsImportsParserPlugin { parser.hooks.callMemberChainOfCallMemberChain .for("module.require") .tap("CommonJsImportsParserPlugin", callChainHandler); + //#endregion - // require.resolve // + //#region Require.resolve const processResolve = (expr, weak) => { if (expr.arguments.length !== 1) return; const param = parser.evaluateExpression(expr.arguments[0]); @@ -375,14 +444,192 @@ class CommonJsImportsParserPlugin { parser.hooks.call .for("require.resolve") - .tap("RequireResolveDependencyParserPlugin", expr => { + .tap("CommonJsImportsParserPlugin", expr => { return processResolve(expr, false); }); parser.hooks.call .for("require.resolveWeak") - .tap("RequireResolveDependencyParserPlugin", expr => { + .tap("CommonJsImportsParserPlugin", expr => { return processResolve(expr, true); }); + //#endregion + + //#region Create require + /** + * @param {CallExpressionNode} expr call expression + * @returns {string|boolean} context + */ + const parseCreateRequireArguments = expr => { + const args = expr.arguments; + if (args.length !== 1) { + const err = new WebpackError( + "module.createRequire supports only one argument." + ); + err.loc = expr.loc; + parser.state.module.addWarning(err); + return; + } + const createParsingError = node => { + const err = new WebpackError( + "module.createRequire() failed parsing argument." + ); + err.loc = node.loc; + parser.state.module.addWarning(err); + }; + const arg = args[0]; + if ( + arg.type === "MemberExpression" && + arg.object.type === "MetaProperty" + ) { + if ( + arg.object.meta.type === "Identifier" && + arg.object.meta.name === "import" && + arg.object.property.type === "Identifier" && + arg.object.property.name === "meta" && + arg.property.type === "Identifier" && + arg.property.name === "url" + ) { + // same module context + return false; + } else { + createParsingError(arg); + } + } else { + const evaluated = parser.evaluateExpression(arg); + if (!evaluated.isString()) { + createParsingError(arg); + return; + } + return evaluated.string; + } + }; + + parser.hooks.import.tap( + { + name: "CommonJsImportsParserPlugin", + stage: -10 + }, + (statement, source) => { + if ( + source !== "module" || + statement.specifiers.length !== 1 || + statement.specifiers[0].type !== "ImportSpecifier" || + statement.specifiers[0].imported.type !== "Identifier" || + statement.specifiers[0].imported.name !== "createRequire" + ) + return; + // clear for 'import { createRequire as x } from "module"' + // if any other specifier was used import module + const clearDep = new ConstDependency( + parser.isAsiPosition(statement.range[0]) ? ";" : "", + statement.range + ); + clearDep.loc = statement.loc; + parser.state.module.addPresentationalDependency(clearDep); + parser.unsetAsiPosition(statement.range[1]); + return true; + } + ); + parser.hooks.importSpecifier.tap( + { + name: "CommonJsImportsParserPlugin", + stage: -10 + }, + (statement, source, id, name) => { + if (source !== "module" || id !== "createRequire") return; + parser.tagVariable(name, createRequireSpecifierTag); + return true; + } + ); + parser.hooks.preDeclarator.tap( + "CommonJsImportsParserPlugin", + declarator => { + if ( + declarator.id.type !== "Identifier" || + declarator.init.type !== "CallExpression" || + declarator.init.callee.type !== "Identifier" + ) + return; + const variableInfo = parser.getVariableInfo( + declarator.init.callee.name + ); + if ( + "tagInfo" in variableInfo && + variableInfo.tagInfo.tag === createRequireSpecifierTag + ) { + const context = parseCreateRequireArguments(declarator.init); + if (context === undefined) return; + parser.tagVariable(declarator.id.name, createdRequireIdentifierTag, { + name: declarator.id.name, + context + }); + return true; + } + } + ); + + parser.hooks.memberChainOfCallMemberChain + .for(createRequireSpecifierTag) + .tap( + "CommonJsImportsParserPlugin", + (expr, calleeMembers, callExpr, members) => { + if ( + calleeMembers.length !== 0 || + members.length !== 1 || + members[0] !== "cache" + ) + return; + // createRequire().cache + const context = parseCreateRequireArguments(callExpr); + if (context === undefined) return; + return requireCache(expr); + } + ); + parser.hooks.callMemberChainOfCallMemberChain + .for(createRequireSpecifierTag) + .tap( + "CommonJsImportsParserPlugin", + (expr, calleeMembers, innerCallExpression, members) => { + if (calleeMembers.length !== 0) return; + // createRequire().resolve() + if (members.length === 1 && members[0] === "resolve") { + return processResolve(expr, false); + } else if (members.length === 0) { + // createRequire()() + const context = parseCreateRequireArguments(innerCallExpression); + if (context === undefined) return; + return createRequireHandler(false)(expr); + } + } + ); + parser.hooks.expressionMemberChain + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", (expr, members) => { + // require.cache + if (members.length === 1 && members[0] === "cache") { + return requireCache(expr); + } + }); + parser.hooks.callMemberChain + .for(createdRequireIdentifierTag) + .tap("CommonJsImportsParserPlugin", (expr, members) => { + // require.resolve() + if (members.length === 1 && members[0] === "resolve") { + return processResolve(expr, false); + } + }); + parser.hooks.call + .for(createRequireSpecifierTag) + .tap("CommonJsImportsParserPlugin", expr => { + const clearDep = new ConstDependency( + "/* createRequire() */ undefined", + expr.range + ); + clearDep.loc = expr.loc; + parser.state.module.addPresentationalDependency(clearDep); + return true; + }); + //#endregion } } module.exports = CommonJsImportsParserPlugin; diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 0273c5fc6e0..21e800911ae 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -2716,7 +2716,10 @@ class JavascriptParser extends Parser { // (function(…) { }(…)) this._walkIIFE(expression.callee, expression.arguments, null); } else { - if (expression.callee.type === "MemberExpression") { + if ( + expression.callee.type === "MemberExpression" || + expression.callee.type === "CallExpression" + ) { const exprInfo = this.getMemberExpressionInfo( expression.callee, ALLOWED_MEMBER_TYPES_CALL_EXPRESSION diff --git a/test/configCases/require/module-require/a.js b/test/configCases/require/module-require/a.js new file mode 100644 index 00000000000..bd816eaba4c --- /dev/null +++ b/test/configCases/require/module-require/a.js @@ -0,0 +1 @@ +module.exports = 1; diff --git a/test/configCases/require/module-require/b.js b/test/configCases/require/module-require/b.js new file mode 100644 index 00000000000..4bbffde1044 --- /dev/null +++ b/test/configCases/require/module-require/b.js @@ -0,0 +1 @@ +module.exports = 2; diff --git a/test/configCases/require/module-require/c.js b/test/configCases/require/module-require/c.js new file mode 100644 index 00000000000..690aad34a46 --- /dev/null +++ b/test/configCases/require/module-require/c.js @@ -0,0 +1 @@ +module.exports = 3; diff --git a/test/configCases/require/module-require/index.js b/test/configCases/require/module-require/index.js new file mode 100644 index 00000000000..7ec60fd315e --- /dev/null +++ b/test/configCases/require/module-require/index.js @@ -0,0 +1,36 @@ +import { createRequire as _createRequire } from "module"; +import { createRequire as __createRequire } from "module"; + +it("should evaluate require/createRequire", () => { + expect( + (function() { return typeof _createRequire; }).toString() + ).toBe('function() { return "function"; }'); + expect( + (function() { if (typeof _createRequire); }).toString() + ).toBe('function() { if (true); }'); + const require = __createRequire(import.meta.url); + expect( + (function() { return typeof require; }).toString() + ).toBe('function() { return "function"; }'); + expect( + (function() { if (typeof require); }).toString() + ).toBe('function() { if (true); }'); +}); + +it("should create require", () => { + const require = _createRequire(import.meta.url); + expect(require("./a")).toBe(1); + expect(_createRequire(import.meta.url)("./c")).toBe(3); +}); + +it("should resolve using created require", () => { + const require = _createRequire(import.meta.url); + expect(require.resolve("./b")).toBe("./b.js"); + expect(_createRequire(import.meta.url).resolve("./b")).toBe("./b.js"); +}); + +it("should provide require.cache", () => { + const _require = _createRequire(import.meta.url); + expect(require.cache).toBe(_require.cache); + expect(require.cache).toBe(_createRequire(import.meta.url).cache); +}); diff --git a/test/configCases/require/module-require/webpack.config.js b/test/configCases/require/module-require/webpack.config.js new file mode 100644 index 00000000000..fe99e3d1745 --- /dev/null +++ b/test/configCases/require/module-require/webpack.config.js @@ -0,0 +1,7 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + target: "node", + optimization: { + moduleIds: "named" + } +};