Skip to content

advancedOptimizations emits duplicate top-level declarations for adjacent classes in minified files (regression in 22.1.0 #33699

Description

@radunicoara-cw

Command

build

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

22.0.9

Description

Since @angular/build 22.1.0, ng build --configuration=production fails with The symbol "X" has already been declared whenever the build graph contains a minified JavaScript file with two or more consecutive top-level classes that qualify for pure-IIFE wrapping.

22.1.0 replaced the Babel-based advancedOptimizations pass with a magic-string + oxc-parser text-splicing implementation in src/tools/babel/plugins/oxc-transform.js. In adjustStaticMembersInStatements, each wrapped class gets:

s.appendLeft(classNode.start,    `let ${classIdName} = /*#__PURE__*/ (() => {\n`);
s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`);

In minified code two sibling class declarations are directly adjacent, so classA.end === classB.start. At a shared index magic-string emits appendLeft content before appendRight content, so class B's opening fragment is inserted ahead of class A's closing fragment. The wrappers interleave and the class escapes the IIFE meant to contain it:

// input
class A{static s=[1]}class B{static s=[2]}

// 22.1.x output
let A = /*#__PURE__*/ (() => {
class A{static s=[1]}let B = /*#__PURE__*/ (() => {

return A;
})();class B{static s=[2]}
return B;
})();

esbuild then reports the duplicated A (the let plus the escaped class) and, in files with an export list, additionally "A" is not declared in this file.

Non-minified files are unaffected, because a newline between the declarations keeps the two insertion points distinct. That is why Angular's own packages, which ship non-minified fesm2022, do not trigger this, while any dependency shipping pre-minified ESM does.

Expected: the build succeeds, as it does on @angular/build 22.0.9.
Actual: Application bundle generation failed with the error below.

Verified on the identical app, changing only @angular/build:

@angular/build Result
22.0.8 success
22.0.9 success
22.1.0 fails
22.1.1 fails

Minimal Reproduction

ng new repro-oxc --defaults --skip-git --style=css --ssr=false
cd repro-oxc
npm i pdfjs-dist@5.6.205

Add one import at the top of src/main.ts:

import 'pdfjs-dist/build/pdf.worker.min.mjs';

Remove the default budgets from the production configuration in angular.json (otherwise the build stops on a bundle-size error before reaching this one), then:

ng build --configuration=production

Exception or Error

X [ERROR] The symbol "SYSTEM_FONT_INFO" has already been declared

    node_modules/pdfjs-dist/build/pdf.worker.min.mjs:32:11:
      32 │ })();class SYSTEM_FONT_INFO{static strings=["css","loadedName","ba...
         ╵            ~~~~~~~~~~~~~~~~

  The symbol "SYSTEM_FONT_INFO" was originally declared here:

    node_modules/pdfjs-dist/build/pdf.worker.min.mjs:29:81:
      29 │ ...ight","italicAngle"]}let SYSTEM_FONT_INFO = /*#__PURE__*/ (() => {
         ╵                             ~~~~~~~~~~~~~~~~

Your Environment

Angular CLI       : 22.1.1
Angular           : 22.1.0
Node.js           : 24.15.0
Package Manager   : npm 11.12.1
Operating System  : win32 x64

Package                     Installed Version   Requested Version
@angular/build              22.1.1              ^22.1.1
@angular/cli                22.1.1              ^22.1.1
@angular/common             22.1.0              ^22.1.0
@angular/compiler           22.1.0              ^22.1.0
@angular/compiler-cli       22.1.0              ^22.1.0
@angular/core               22.1.0              ^22.1.0
@angular/forms              22.1.0              ^22.1.0
@angular/platform-browser   22.1.0              ^22.1.0
@angular/router             22.1.0              ^22.1.0
rxjs                        7.8.2               ~7.8.0
typescript                  6.0.3               ~6.0.2
vitest                      4.1.10              ^4.0.8

Anything else relevant?

  • This appears to have been introduced by feat(@angular/build): migrate advanced optimization Babel plugins to oxc-parser + magic-string #33568 (merged 2026-07-21), which migrated the advanced optimization plugins from Babel to oxc-parser plus magic-string and added oxc-transform.ts. The accompanying adjust-static-class-members_oxc_spec.ts covers the pass fairly thoroughly, but I could not find a case with two adjacent class declarations and no whitespace between them, which is the shape that triggers this. A test on input like class A{static s=[1]}class B{static s=[2]} would cover it.
  • Production builds only, since advancedOptimizations is gated on !!aot && optimizationOptions.scripts. The dev server and ng-packagr library builds are unaffected.
  • pdfjs-dist is just a convenient public trigger. Any pre-minified ESM dependency with adjacent wrappable classes hits this, including libraries that ship minified fesm2022 bundles. In our own case a single production build reported corruption in three separate packages.
  • oxc-transform.js is byte-identical in 22.1.0, 22.1.1 and 22.2.0-next.0, so no released version currently contains a fix.
  • Possible fix: swap the insertion sides so the closing fragment binds to the preceding chunk and the opening fragment to the following one, that is appendRight for the opener and appendLeft for the closer. I confirmed that with that swap the shared-boundary case emits ... })();let B = ... in the correct order. The isExportDefault, isExportNamed and isVariableClass branches use the same pairing and would need the same treatment.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions