From 34c4edca6ba73b21916b19b93b8dae2d4184448f Mon Sep 17 00:00:00 2001 From: Ivan Kopeykin Date: Wed, 23 Mar 2022 22:02:22 +0300 Subject: [PATCH] add createRequire support --- .../CommonJsImportsParserPlugin.js | 367 +++++++++++++++--- lib/javascript/JavascriptParser.js | 44 ++- test/configCases/require/module-require/a.js | 1 + test/configCases/require/module-require/b.js | 1 + test/configCases/require/module-require/c.js | 1 + .../require/module-require/index.js | 40 ++ .../require/module-require/webpack.config.js | 7 + types.d.ts | 7 + 8 files changed, 395 insertions(+), 73 deletions(-) create mode 100644 test/configCases/require/module-require/a.js create mode 100644 test/configCases/require/module-require/b.js create mode 100644 test/configCases/require/module-require/c.js create mode 100644 test/configCases/require/module-require/index.js create mode 100644 test/configCases/require/module-require/webpack.config.js diff --git a/lib/dependencies/CommonJsImportsParserPlugin.js b/lib/dependencies/CommonJsImportsParserPlugin.js index 47013f4269f..b96a4fc9675 100644 --- a/lib/dependencies/CommonJsImportsParserPlugin.js +++ b/lib/dependencies/CommonJsImportsParserPlugin.js @@ -8,6 +8,8 @@ const CommentCompilationWarning = require("../CommentCompilationWarning"); const RuntimeGlobals = require("../RuntimeGlobals"); const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning"); +const WebpackError = require("../WebpackError"); +const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression"); const { evaluateToIdentifier, evaluateToString, @@ -26,8 +28,12 @@ const RequireResolveContextDependency = require("./RequireResolveContextDependen const RequireResolveDependency = require("./RequireResolveDependency"); const RequireResolveHeaderDependency = require("./RequireResolveHeaderDependency"); +/** @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,82 @@ 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")); + }; tapRequireExpression("require", () => []); tapRequireExpression("require.resolve", () => ["resolve"]); tapRequireExpression("require.resolveWeak", () => ["resolveWeak"]); + tapRequireExpressionTag(createdRequireIdentifierTag); + tapRequireExpressionTag(createRequireSpecifierTag); + parser.hooks.evaluateCallExpression + .for(createRequireSpecifierTag) + .tap("CommonJsImportsParserPlugin", expr => { + const context = parseCreateRequireArguments(expr); + if (context === undefined) return; + const ident = parser.evaluatedVariable({ + tag: createdRequireIdentifierTag, + data: { context }, + next: undefined + }); + return new BasicEvaluatedExpression() + .setIdentifier(ident, ident, () => []) + .setSideEffects(false) + .setRange(expr.range); + }); + //#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 +129,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 +138,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 +329,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 +341,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 +390,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 +450,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 || + declarator.init.type !== "CallExpression" || + declarator.init.callee.type !== "Identifier" + ) + return; + const variableInfo = parser.getVariableInfo( + declarator.init.callee.name + ); + if ( + variableInfo && + variableInfo.tagInfo && + 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 || + members.length !== 1 || + members[0] !== "resolve" + ) + return; + // createRequire().resolve() + return processResolve(expr, false); + } + ); + 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..ecaae863d9f 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -165,6 +165,10 @@ class JavascriptParser extends Parser { evaluateDefinedIdentifier: new HookMap( () => new SyncBailHook(["expression"]) ), + /** @type {HookMap>} */ + evaluateCallExpression: new HookMap( + () => new SyncBailHook(["expression"]) + ), /** @type {HookMap>} */ evaluateCallExpressionMember: new HookMap( () => new SyncBailHook(["expression", "param"]) @@ -1036,24 +1040,28 @@ class JavascriptParser extends Parser { this.hooks.evaluate.for("CallExpression").tap("JavascriptParser", _expr => { const expr = /** @type {CallExpressionNode} */ (_expr); if ( - expr.callee.type !== "MemberExpression" || - expr.callee.property.type !== + expr.callee.type === "MemberExpression" && + expr.callee.property.type === (expr.callee.computed ? "Literal" : "Identifier") ) { - return; - } - - // type Super also possible here - const param = this.evaluateExpression( - /** @type {ExpressionNode} */ (expr.callee.object) - ); - const property = - expr.callee.property.type === "Literal" - ? `${expr.callee.property.value}` - : expr.callee.property.name; - const hook = this.hooks.evaluateCallExpressionMember.get(property); - if (hook !== undefined) { - return hook.call(expr, param); + // type Super also possible here + const param = this.evaluateExpression( + /** @type {ExpressionNode} */ (expr.callee.object) + ); + const property = + expr.callee.property.type === "Literal" + ? `${expr.callee.property.value}` + : expr.callee.property.name; + const hook = this.hooks.evaluateCallExpressionMember.get(property); + if (hook !== undefined) { + return hook.call(expr, param); + } + } else if (expr.callee.type === "Identifier") { + return this.callHooksForName( + this.hooks.evaluateCallExpression, + expr.callee.name, + expr + ); } }); this.hooks.evaluateCallExpressionMember @@ -3603,6 +3611,10 @@ class JavascriptParser extends Parser { } } + evaluatedVariable(tagInfo) { + return new VariableInfo(this.scope, undefined, tagInfo); + } + parseCommentOptions(range) { const comments = this.getComments(range); if (comments.length === 0) { 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..8891d4b09a3 --- /dev/null +++ b/test/configCases/require/module-require/index.js @@ -0,0 +1,40 @@ +import { createRequire as _createRequire } from "module"; +import { createRequire as __createRequire, builtinModules } 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); +}); + +it("should import Node.js module", () => { + expect(Array.isArray(builtinModules)).toBe(true); +}); 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" + } +}; diff --git a/types.d.ts b/types.d.ts index 6329c416fcc..1e1a3006b19 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4884,6 +4884,12 @@ declare class JavascriptParser extends Parser { undefined | null | BasicEvaluatedExpression > >; + evaluateCallExpression: HookMap< + SyncBailHook< + [CallExpression], + undefined | null | BasicEvaluatedExpression + > + >; evaluateCallExpressionMember: HookMap< SyncBailHook< [CallExpression, undefined | BasicEvaluatedExpression], @@ -5392,6 +5398,7 @@ declare class JavascriptParser extends Parser { isVariableDefined(name?: any): boolean; getVariableInfo(name: string): ExportedVariableInfo; setVariable(name: string, variableInfo: ExportedVariableInfo): void; + evaluatedVariable(tagInfo?: any): VariableInfo; parseCommentOptions( range?: any ): { options: null; errors: null } | { options: object; errors: unknown[] };