feat: add createRequire support for ESM modules#20497
feat: add createRequire support for ESM modules#20497alexander-akait merged 8 commits intowebpack:mainfrom
createRequire support for ESM modules#20497Conversation
🦋 Changeset detectedLatest commit: f281412 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| javascript: { | ||
| createRequire: true | ||
| }, | ||
| "javascript/esm": { |
There was a problem hiding this comment.
Config explicitly sets commonjs: false for ESM while enabling createRequire: true. This ensures only createRequire is enough for bundling to work correctly
| if (options.createRequire === true) { | ||
| moduleName = ["module", "node:module"]; | ||
| specifierName = "createRequire"; | ||
| } else if (typeof options.createRequire === "string") { |
There was a problem hiding this comment.
typeof options.createRequire === "string" was added because it the next line of matching regex it was causing type error
error messge:
Argument of type 'string | false | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.
| @@ -53,6 +53,298 @@ const createdRequireIdentifierTag = Symbol("createRequire()"); | |||
|
|
|||
| const PLUGIN_NAME = "CommonJsImportsParserPlugin"; | |||
|
|
|||
There was a problem hiding this comment.
extracting helper functions from parent class so that we can reuse them for new createRequireParserPlugin
| // #region Create require | ||
|
|
||
| if (!options.createRequire) return; | ||
|
|
||
| /** @type {ImportSource[]} */ | ||
| let moduleName = []; | ||
| /** @type {string | undefined} */ | ||
| let specifierName; | ||
|
|
||
| if (options.createRequire === true) { | ||
| moduleName = ["module", "node:module"]; | ||
| specifierName = "createRequire"; | ||
| } else { | ||
| /** @type {undefined | string} */ | ||
| let moduleName; | ||
| const match = /^(.*) from (.*)$/.exec(options.createRequire); | ||
| if (match) { | ||
| [, specifierName, moduleName] = match; | ||
| } | ||
| if (!specifierName || !moduleName) { | ||
| const err = new WebpackError( | ||
| `Parsing javascript parser option "createRequire" failed, got ${JSON.stringify( | ||
| options.createRequire | ||
| )}` | ||
| ); | ||
| err.details = | ||
| 'Expected string in format "createRequire from module", where "createRequire" is specifier name and "module" name of the module'; | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| tapRequireExpressionTag(createdRequireIdentifierTag); | ||
| tapRequireExpressionTag(createRequireSpecifierTag); | ||
| parser.hooks.evaluateCallExpression | ||
| .for(createRequireSpecifierTag) | ||
| .tap(PLUGIN_NAME, (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(/** @type {Range} */ (expr.range)); | ||
| }); | ||
| parser.hooks.unhandledExpressionMemberChain | ||
| .for(createdRequireIdentifierTag) | ||
| .tap(PLUGIN_NAME, (expr, members) => | ||
| expressionIsUnsupported( | ||
| parser, | ||
| `createRequire().${members.join(".")} is not supported by webpack.` | ||
| )(expr) | ||
| ); | ||
| parser.hooks.canRename | ||
| .for(createdRequireIdentifierTag) | ||
| .tap(PLUGIN_NAME, () => true); | ||
| parser.hooks.canRename | ||
| .for(createRequireSpecifierTag) | ||
| .tap(PLUGIN_NAME, () => true); | ||
| parser.hooks.rename | ||
| .for(createRequireSpecifierTag) | ||
| .tap(PLUGIN_NAME, defineUndefined); | ||
| parser.hooks.expression | ||
| .for(createdRequireIdentifierTag) | ||
| .tap(PLUGIN_NAME, requireAsExpressionHandler); | ||
| parser.hooks.call | ||
| .for(createdRequireIdentifierTag) | ||
| .tap(PLUGIN_NAME, createRequireHandler(false)); | ||
| /** | ||
| * @param {CallExpression} expr call expression | ||
| * @returns {string | void} context | ||
| */ | ||
| const parseCreateRequireArguments = (expr) => { | ||
| const args = expr.arguments; | ||
| if (args.length !== 1) { | ||
| const err = new WebpackError( | ||
| "module.createRequire supports only one argument." | ||
| ); | ||
| err.loc = /** @type {DependencyLocation} */ (expr.loc); | ||
| parser.state.module.addWarning(err); | ||
| return; | ||
| } | ||
| const arg = args[0]; | ||
| const evaluated = parser.evaluateExpression(arg); | ||
| if (!evaluated.isString()) { | ||
| const err = new WebpackError( | ||
| "module.createRequire failed parsing argument." | ||
| ); | ||
| err.loc = /** @type {DependencyLocation} */ (arg.loc); | ||
| parser.state.module.addWarning(err); | ||
| return; | ||
| } | ||
| const ctx = /** @type {string} */ (evaluated.string).startsWith("file://") | ||
| ? fileURLToPath(/** @type {string} */ (evaluated.string)) | ||
| : /** @type {string} */ (evaluated.string); | ||
| // argument always should be a filename | ||
| return ctx.slice(0, ctx.lastIndexOf(ctx.startsWith("/") ? "/" : "\\")); | ||
| }; | ||
|
|
||
| parser.hooks.import.tap( | ||
| { | ||
| name: PLUGIN_NAME, | ||
| stage: -10 | ||
| }, | ||
| (statement, source) => { | ||
| if ( | ||
| !moduleName.includes(source) || | ||
| statement.specifiers.length !== 1 || | ||
| statement.specifiers[0].type !== "ImportSpecifier" || | ||
| statement.specifiers[0].imported.type !== "Identifier" || | ||
| statement.specifiers[0].imported.name !== specifierName | ||
| ) { | ||
| return; | ||
| } | ||
| // clear for 'import { createRequire as x } from "module"' | ||
| // if any other specifier was used import module | ||
| const clearDep = new ConstDependency( | ||
| parser.isAsiPosition(/** @type {Range} */ (statement.range)[0]) | ||
| ? ";" | ||
| : "", | ||
| /** @type {Range} */ (statement.range) | ||
| ); | ||
| clearDep.loc = /** @type {DependencyLocation} */ (statement.loc); | ||
| parser.state.module.addPresentationalDependency(clearDep); | ||
| parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]); | ||
| return true; | ||
| } | ||
| ); | ||
| parser.hooks.importSpecifier.tap( | ||
| { | ||
| name: PLUGIN_NAME, | ||
| stage: -10 | ||
| }, | ||
| (statement, source, id, name) => { | ||
| if (!moduleName.includes(source) || id !== specifierName) return; | ||
| parser.tagVariable(name, createRequireSpecifierTag); | ||
| return true; | ||
| } | ||
| ); | ||
| parser.hooks.preDeclarator.tap(PLUGIN_NAME, (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 instanceof 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(PLUGIN_NAME, (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(PLUGIN_NAME, (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(PLUGIN_NAME, (expr, members) => { | ||
| // require.cache | ||
| if (members.length === 1 && members[0] === "cache") { | ||
| return requireCache(expr); | ||
| } | ||
| }); | ||
| parser.hooks.callMemberChain | ||
| .for(createdRequireIdentifierTag) | ||
| .tap(PLUGIN_NAME, (expr, members) => { | ||
| // require.resolve() | ||
| if (members.length === 1 && members[0] === "resolve") { | ||
| return processResolve(expr, false); | ||
| } | ||
| }); | ||
| parser.hooks.call | ||
| .for(createRequireSpecifierTag) | ||
| .tap(PLUGIN_NAME, (expr) => { | ||
| const clearDep = new ConstDependency( | ||
| "/* createRequire() */ undefined", | ||
| /** @type {Range} */ (expr.range) | ||
| ); | ||
| clearDep.loc = /** @type {DependencyLocation} */ (expr.loc); | ||
| parser.state.module.addPresentationalDependency(clearDep); | ||
| return true; | ||
| }); | ||
| // #endregion |
There was a problem hiding this comment.
moved this createRequire logic to new CreateRequireParserPlugin file
| module.exports.createProcessResolveHandler = createProcessResolveHandler; | ||
| module.exports.createRequireAsExpressionHandler = | ||
| createRequireAsExpressionHandler; | ||
| module.exports.createRequireCacheDependency = createRequireCacheDependency; | ||
| module.exports.createRequireHandler = createRequireCallHandlerFactory; |
There was a problem hiding this comment.
exporting so that we can re-use in CreateRequireParserPlugin
| if (parserOptions.createRequire) { | ||
| new CreateRequireParserPlugin(parserOptions).apply(parser); | ||
| } |
There was a problem hiding this comment.
this is where actual fix happens
alexander-akait
left a comment
There was a problem hiding this comment.
Do you use only AI to generate this?
|
yes i have used Ai for initial drafting of these changes, but made sure to review through it and seemed fine. If there are any issues/problems with this code I'm really sorry i will try to pick up smaller issues instead and build up knowledge base before attempting huge ones. |
|
@stefanbinoj I ask this because this issue requires manual transfer of code into its own plugin and the extraction of functions that can be reused, AI often does this with errors or rewriting of code and this could lead to new hidden problems. Webpack is quite large and complex and often requires the attention of real people. I'm asking this because if I start checking everything manually, I'll spend exactly the same amount of time as fixing it myself, and I wouldn't want to deal with that. AI should be an assistant, not |
|
Thanks for the clarification I got the point, will do a deep check on this and will make sure that the refactoring to helper function and new plugin parser code is exactly similar. Will ping you once i have done it. |
|
@stefanbinoj thanks |
|
Hi @alexander-akait I have did a thorough manual review and have manually redid the refactoring of helper funtions and new plugin. All the functions that have been refactored are identically to before and has no code changes similarly for plugin parser class as well. The only notable changes are:
const processRequireItem = (arg1) => {
// code
}
const processRequireContext = (arg2) => {
// code
}
const createRequireHandler = (arg3) => {
// calls both processRequireItem() and processRequireContext() (both of these function are used here only!)
}Now i have refactored it to be like: const createRequireCallHandler = (arg1, arg2, arg3) => {
const processRequireItem = (arg1) => {
// same exact code
}
const processRequireContext = (arg2) => {
// same exact code
}
return (arg3) => (expr) => {
// same exact code as in prev createRequireHandler()
}
}similarly for the case of
Reason: helper functions was private, captures inputs ( |
Merging this PR will degrade performance by 32.61%
Performance Changes
Comparing |
|
Hi @alexander-akait just wanted to ask whether should i add a changeset-patch for this? |
|
@stefanbinoj This is not necessary, in the future you can do this, but if you don't, I will add it before merging |
|
This PR is packaged and the instant preview is available (4479861). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@4479861
yarn add -D webpack@https://pkg.pr.new/webpack@4479861
pnpm add -D webpack@https://pkg.pr.new/webpack@4479861 |
Fixes #20477
Summary
This PR fixes
module.parser.javascript.createRequirefor pure ESM modules when usingcreateRequire(import.meta.url)to load local CommonJS modules.Previously,
createRequirelogic lived insideCommonJsImportsParserPlugin, which is wired to CommonJS parser flows (javascript/autoandjavascript/dynamic). As a result, pure ESM modules did not reliably receivecreateRequirehandling.Changes
CreateRequireParserPlugin.jswith extractedcreateRequireparsing logic.createRequireblock fromCommonJsImportsParserPlugin.jsHarmonyModulesPlugin.jswhencreateRequireis enabled.Notes
createRequirehandling,module.parser.javascript.createRequireis now sufficient.module.parser.javascript.commonjsis not required for this ESM interop path.Screenshots
Main
fix/commonjs_bundling
Before
After (webpack + webpack-cli linked to local fix branch)
What kind of change does this PR introduce?
Fixes an issue where
module.parser.javascript.createRequire: truedoes not work for pure ES module scripts.Did you add tests for your changes?
Yes.
Does this PR introduce a breaking change?
No.
AI Disclosure
Since I was new to this repo I have used AI (GPT-5.3 Codex via Codex) to help navigate and understand the issue and draft an initial fix. I then manually tested, updated, and corrected the resulting changes.
Also PR description was primarily written by me and then formatted and fixed typos by using gpt