Skip to content

Parenthesized accessor call breaks ASI between adjacent require() statements in concatenated modules (regression in 5.110.3) #21956

Closed as not planned

Description

@blankse

Filed via the API, so the issue-form dropdowns could not be selected. Their answers:

Field Answer
Have you used AI? Yes
Which part of webpack is affected? optimization (tree-shaking, splitChunks, concatenation)
Is this a regression? Yes
Last Working Version v5.110.2

Bug Description

Parenthesizing a wrapped module's accessor call (#21876, shipped in v5.110.3) defeats automatic semicolon insertion between two adjacent require() expression statements inside a concatenated (scope-hoisted) module.

Two require() calls on separate lines, where the first line has no trailing semicolon, are valid JavaScript: ASI terminates the first statement. Since v5.110.3, webpack emits each replaced accessor call wrapped in parentheses. (...) followed by (...) on the next line is a call expression, so the two statements collapse into one, and the bundle throws at runtime:

Uncaught TypeError: __webpack_require__(...) is not a function

The module reference is emitted as (reference), and the ; guard is only added when asiSafe === false:

https://github.com/webpack/webpack/blob/main/lib/optimize/ConcatenatedModule.js#L984

return asiSafe === false ? `;(${reference})` : `(${reference})`;

At this position asiSafe is not false, so no guard is emitted. The asiSafe check describes whether the position itself is safe, but the newly added parentheses make the position unsafe for the following statement — a leading ( can continue the preceding expression. So the condition no longer covers the cases the parenthesization creates.

This is not a "you forgot a semicolon" problem: the input is valid JavaScript that v5.110.2 and every earlier version compiled correctly, and nothing warns.

Link to Minimal Reproduction and steps to reproduce

Four files, no dependencies beyond webpack and webpack-cli.

src/index.js — the import is what makes this module ESM and therefore scope-hoisted:

import X from "./x";

window.X = X;

require("./a")
require("./b");

src/x.js:

export default function X() {
	return 42;
}

src/a.js:

window.a = true;

src/b.js:

window.b = true;

webpack.config.js:

const path = require("path");

module.exports = {
	mode: "production",
	entry: path.resolve(__dirname, "src/index.js"),
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: "bundle.js"
	}
};

Steps:

  1. npm i webpack@5.110.3 webpack-cli
  2. npx webpack
  3. Load dist/bundle.js in a browser, or run
    node -e "global.window={}; require('./dist/bundle.js')"
  4. It throws TypeError: __webpack_require__(...) is not a function (minified: o(...) is not a function)
  5. Repeat with webpack@5.110.2 — no error

Expected Behavior

The two require() calls stay two separate statements, as in v5.110.2 and earlier:

window.X = X;

a_namespaceFn()
b_namespaceFn();

Both modules are evaluated, the bundle runs.

Actual Behavior

Since v5.110.3 the accessor calls are parenthesized without an ASI guard, so the second becomes an argument to the first:

window.X = X;

(a_namespaceFn())
(b_namespaceFn());

Minified, this is the end of the bundle:

(()=>{"use strict";window.X=function(){return 42},o(967)(o(170))})()

o(967) returns the module exports object of a side-effect-only module, calling it throws, and because this happens in the entry module nothing after it is initialized.

Environment

System:
  OS:      Linux 6.18 Ubuntu 24.04.4 LTS (Noble Numbat)
  CPU:     (20) x64 12th Gen Intel(R) Core(TM) i7-12700
  Memory:  13.53 GB / 23.47 GB
Binaries:
  Node:  20.20.2
  npm:   10.8.2
  Yarn:  1.22.22
Packages:
  webpack:                5.110.3
  webpack-cli:            7.2.3
  terser-webpack-plugin:  5.6.1

Additional Context

Bisect

Same reproduction, only the webpack version changed:

webpack collapsed
5.109.2 no
5.110.0 no
5.110.1 no
5.110.2 no
5.110.3 yes

Terser is not involved — the collapse is already present in the unminified output, and crossing the versions confirms it:

webpack terser collapsed
5.110.3 5.51.2 yes
5.110.3 5.49.0 yes
5.109.2 5.51.2 no
5.109.2 5.49.0 no

Attribution

Patching the line above in an installed v5.110.3 isolates the cause to #21876:

variant collapsed
unchanged v5.110.3 yes
`(${reference})` → `${reference}` (parenthesization reverted) no
always `;(${reference})` no

For completeness: #21907 landed in the same release and removed the ConcatenationScope.registerReplacedRequire / isInsideReplacedRequire containment guard, which looked related in the source diff but turned out not to cause this — reverting only the parenthesization is sufficient.

Impact

Any codebase with two adjacent require() statements where the first lacks a semicolon, inside a module that contains at least one import, silently produces a bundle that throws in the entry module. In our case it took down a production site after a routine dependency update; the source had been unchanged for over two years. There is no build error or warning.

Note on AI usage

The reproduction, the bisect, the version crossing and the attribution were produced with Claude Code. Every number in the tables comes from an actual build in an isolated directory, not from reading the source.

Activity

  1. added
    area: optimizationTree-shaking, splitChunks, concatenation, ids (lib/optimize, lib/ids)
    area: runtimeEmitted runtime and chunk loading across targets (lib/runtime, lib/web, lib/node, lib/esm)
    regressionWorked in a previous release; pair with the Affected version field
    on Sep 4, 2026
  2. blankse commented on Sep 4, 2026

    @blankse
    Author

    Duplicate of #21955, which was filed about twenty minutes earlier and describes the same defect. I verified it is not merely the same line but the same branch: ConcatenatedModule.js contains the parenthesizing expression twice, and neutralizing them individually in an installed 5.110.3 shows that only line 989, else if (accessorCall && !asCall), is responsible — both reports land there.

    Closing in favour of #21955. The parts of my investigation that report does not cover are now a comment over there: a four-file reproduction with no third-party dependency, the fact that the trigger needs no eliminated re-export (two adjacent require() statements are enough, so a plain CommonJsRequireDependency is affected as well), the 5.109.2–5.110.2 bisect, and the version crossing that rules out terser.

  3. added 3 commits that reference this issue on Sep 4, 2026
    6205eea
    7988deb
    c17742f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: optimizationTree-shaking, splitChunks, concatenation, ids (lib/optimize, lib/ids)area: runtimeEmitted runtime and chunk loading across targets (lib/runtime, lib/web, lib/node, lib/esm)regressionWorked in a previous release; pair with the Affected version field

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions