Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add createRequire support for ESM modules#20497

Merged
alexander-akait merged 8 commits intowebpack:mainfrom
stefanbinoj:fix/commonjs_bundling
Feb 24, 2026
Merged

feat: add createRequire support for ESM modules#20497
alexander-akait merged 8 commits intowebpack:mainfrom
stefanbinoj:fix/commonjs_bundling

Conversation

@stefanbinoj
Copy link
Contributor

@stefanbinoj stefanbinoj commented Feb 22, 2026

Fixes #20477

Summary

This PR fixes module.parser.javascript.createRequire for pure ESM modules when using createRequire(import.meta.url) to load local CommonJS modules.

Previously, createRequire logic lived inside CommonJsImportsParserPlugin, which is wired to CommonJS parser flows (javascript/auto and javascript/dynamic). As a result, pure ESM modules did not reliably receive createRequire handling.

Changes

  • Added CreateRequireParserPlugin.jswith extracted createRequire parsing logic.
  • Removed the createRequire block from CommonJsImportsParserPlugin.js
  • Wired the new plugin in HarmonyModulesPlugin.js when createRequire is enabled.

Notes

  • For ESM createRequire handling, module.parser.javascript.createRequire is now sufficient.
  • module.parser.javascript.commonjs is not required for this ESM interop path.

Screenshots

Test output on main vs this branch of newly added test

Main

Screenshot 2026-02-22 at 11 25 00 AM

fix/commonjs_bundling

Screenshot 2026-02-22 at 11 26 55 AM

Running the build command and executing the bundled JS from the minimal repro repo provided by the issue author: repo

Before

Screenshot 2026-02-20 at 5 00 47 PM

After (webpack + webpack-cli linked to local fix branch)

Screenshot 2026-02-20 at 5 52 56 PM
  • What kind of change does this PR introduce?
    Fixes an issue where module.parser.javascript.createRequire: true does 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

@changeset-bot
Copy link

changeset-bot bot commented Feb 22, 2026

🦋 Changeset detected

Latest commit: f281412

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Patch

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

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Feb 22, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

javascript: {
createRequire: true
},
"javascript/esm": {
Copy link
Contributor Author

@stefanbinoj stefanbinoj Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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") {
Copy link
Contributor Author

@stefanbinoj stefanbinoj Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is AI generated code with a lot of bugs and copy/paste code, you need export helper functions from CommonJsImportParser, no need to duplicate them and then reuse them here

@stefanbinoj stefanbinoj marked this pull request as draft February 23, 2026 01:06
@stefanbinoj stefanbinoj marked this pull request as ready for review February 23, 2026 11:48
@@ -53,6 +53,298 @@ const createdRequireIdentifierTag = Symbol("createRequire()");

const PLUGIN_NAME = "CommonJsImportsParserPlugin";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracting helper functions from parent class so that we can reuse them for new createRequireParserPlugin

Comment on lines -578 to -801
// #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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this createRequire logic to new CreateRequireParserPlugin file

Comment on lines +607 to +611
module.exports.createProcessResolveHandler = createProcessResolveHandler;
module.exports.createRequireAsExpressionHandler =
createRequireAsExpressionHandler;
module.exports.createRequireCacheDependency = createRequireCacheDependency;
module.exports.createRequireHandler = createRequireCallHandlerFactory;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exporting so that we can re-use in CreateRequireParserPlugin

Comment on lines +142 to +144
if (parserOptions.createRequire) {
new CreateRequireParserPlugin(parserOptions).apply(parser);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where actual fix happens

Copy link
Member

@alexander-akait alexander-akait left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you use only AI to generate this?

@stefanbinoj
Copy link
Contributor Author

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.

@alexander-akait
Copy link
Member

@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 please write me code to fix #number issue.

@stefanbinoj
Copy link
Contributor Author

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.

@alexander-akait
Copy link
Member

@stefanbinoj thanks

@stefanbinoj
Copy link
Contributor Author

stefanbinoj commented Feb 23, 2026

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:

  1. Earlier we had code like:
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 createProcessResolveHandler (with helper functions: processResolveItem , processResolveContext

  1. Had to alter the jsdoc of these two since the argument list and output changed

Reason: helper functions was private, captures inputs (parser/options/getContext) once via closure so function calling stay simple and behavior stays identical.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 23, 2026

Merging this PR will degrade performance by 32.61%

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 141 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 7.2 MB 10.7 MB -32.61%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 10 MB 7.6 MB +32.09%
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 555.1 KB 784.5 KB -29.25%

Comparing stefanbinoj:fix/commonjs_bundling (f281412) with main (c8ebcd8)

Open in CodSpeed

@stefanbinoj
Copy link
Contributor Author

Hi @alexander-akait just wanted to ask whether should i add a changeset-patch for this?

@alexander-akait
Copy link
Member

@stefanbinoj This is not necessary, in the future you can do this, but if you don't, I will add it before merging

@alexander-akait alexander-akait merged commit 4479861 into webpack:main Feb 24, 2026
53 of 55 checks passed
@github-actions
Copy link
Contributor

This PR is packaged and the instant preview is available (4479861).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@4479861
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@4479861
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@4479861

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

module.parser.javascript.createRequire: true does not work for ES module script (.mjs) without transpilation

2 participants