Which @angular/* package(s) are the source of the bug?
build
Is this a regression?
Yes — this worked in @angular/build 22.0.7 and broke in 22.1.0.
Description
09d0a11a8 ("perf(@angular/build): enable fast-path AST printing with sourcemaps in AotCompilation") changed how useTypeScriptTranspilation is computed.
22.0.7 — aot-compilation.js:
const useTypeScriptTranspilation =
!compilerOptions.isolatedModules ||
!!compilerOptions.sourceMap ||
!!compilerOptions.inlineSourceMap;
22.1.x — aot-compilation.js + compiler-plugin.js:
const useTypeScriptTranspilation =
compilerOptions['_useTypeScriptTranspilation'] ?? !compilerOptions.isolatedModules;
// _useTypeScriptTranspilation: !compilerOptions.isolatedModules || !!pluginOptions.instrumentForCoverage
The sourcemap condition is gone. For any project with isolatedModules: true, TS→JS emit moved from TypeScript to esbuild.
The commit message frames this as improving "rebuild performance for dev server builds", but the change is not scoped to the dev server — it applies to ng build production too, where sourcemaps are commonly enabled ("sourceMap": { "hidden": true, "scripts": true } is a normal production setting for uploading to an error tracker).
The problem
esbuild's TypeScript transpilation is not semantically identical to tsc's, and where it diverges the divergence surfaces as a silent miscompile rather than a build failure.
Concretely, this shipped a ReferenceError to our production app. A module contained:
import Fuse from 'fuse.js';
import FuseOptionKey = Fuse.FuseOptionKey; // type-only alias
// ...
const fuse = new Fuse(items, { /* ... */ });
Under 22.0.7 (tsc emit) this was correct. Under 22.1 (esbuild transpilation) esbuild drops the fuse.js import while keeping new Fuse(...), so the page throws ReferenceError: Fuse is not defined. I've filed the underlying esbuild bug separately: evanw/esbuild#4507.
I'm not arguing Angular should work around that specific esbuild bug. The issue here is the change in blast radius:
- The emit path for production builds changed as a side effect of a dev-server performance optimization.
isolatedModules is normally the contract that says "each file can be transpiled independently". It is enforced by TypeScript, so it feels like a sufficient guarantee — but it only rejects constructs TypeScript knows are unsafe to emit per-file. import X = Default.Y is perfectly fine for tsc, so nothing flags it. isolatedModules therefore does not actually guarantee that a third-party transpiler will agree with tsc.
- There is no supported opt-out.
_useTypeScriptTranspilation is internal and derived; the only lever a user has is turning off isolatedModules entirely, which is a large and unrelated change.
- The failure is invisible pre-merge: the build exits 0 with no warning,
tsc/ng build type-check clean, and no unit or component test can observe it because every single-file compilation path (ts.transpileModule, full ts.Program.emit, esbuild.transform) emits correct code. Only the bundled artifact is wrong.
Reproduction
esbuild entry.ts --bundle is enough to show the underlying miscompile (see evanw/esbuild#4507). To see it through Angular: any app with isolatedModules: true and sourcemaps enabled, containing a module that imports a package's default export and also declares import Alias = ThatDefault.SomeType. Build with 22.0.7 → import preserved. Build with 22.1.x → import dropped, ReferenceError at runtime.
What I'd suggest
Any of these would have prevented this, roughly in order of preference:
- Expose a supported opt-out (a builder option or a documented env var) to force TypeScript transpilation, so a project hitting a transpiler divergence has a same-day escape hatch that isn't "disable
isolatedModules".
- Scope the fast path to the dev server, which is where the commit's stated benefit ("rebuild performance") actually applies, and keep production on
tsc emit where correctness matters more than rebuild speed.
- If it stays on for production, call it out in the release notes / migration guide as a change of transpiler for production output, not just an internal perf tweak. It's currently indistinguishable from a no-op change from a consumer's point of view.
Please provide the environment you discovered this bug in
Angular CLI: 22.1.2
Node: 24.16.0
Package Manager: pnpm 11.7.0
@angular/build: 22.1.2 (regressed) / 22.0.7 (working)
TypeScript: 6.0.3
esbuild: 0.28.1
Relevant tsconfig:
Production builder config includes "sourceMap": { "hidden": true, "scripts": true, "styles": true }.
Which @angular/* package(s) are the source of the bug?
build
Is this a regression?
Yes — this worked in
@angular/build22.0.7 and broke in 22.1.0.Description
09d0a11a8("perf(@angular/build): enable fast-path AST printing with sourcemaps in AotCompilation") changed howuseTypeScriptTranspilationis computed.22.0.7 —
aot-compilation.js:22.1.x —
aot-compilation.js+compiler-plugin.js:The sourcemap condition is gone. For any project with
isolatedModules: true, TS→JS emit moved from TypeScript to esbuild.The commit message frames this as improving "rebuild performance for dev server builds", but the change is not scoped to the dev server — it applies to
ng buildproduction too, where sourcemaps are commonly enabled ("sourceMap": { "hidden": true, "scripts": true }is a normal production setting for uploading to an error tracker).The problem
esbuild's TypeScript transpilation is not semantically identical to
tsc's, and where it diverges the divergence surfaces as a silent miscompile rather than a build failure.Concretely, this shipped a
ReferenceErrorto our production app. A module contained:Under 22.0.7 (tsc emit) this was correct. Under 22.1 (esbuild transpilation) esbuild drops the
fuse.jsimport while keepingnew Fuse(...), so the page throwsReferenceError: Fuse is not defined. I've filed the underlying esbuild bug separately: evanw/esbuild#4507.I'm not arguing Angular should work around that specific esbuild bug. The issue here is the change in blast radius:
isolatedModulesis normally the contract that says "each file can be transpiled independently". It is enforced by TypeScript, so it feels like a sufficient guarantee — but it only rejects constructs TypeScript knows are unsafe to emit per-file.import X = Default.Yis perfectly fine fortsc, so nothing flags it.isolatedModulestherefore does not actually guarantee that a third-party transpiler will agree withtsc._useTypeScriptTranspilationis internal and derived; the only lever a user has is turning offisolatedModulesentirely, which is a large and unrelated change.tsc/ng buildtype-check clean, and no unit or component test can observe it because every single-file compilation path (ts.transpileModule, fullts.Program.emit,esbuild.transform) emits correct code. Only the bundled artifact is wrong.Reproduction
esbuild entry.ts --bundleis enough to show the underlying miscompile (see evanw/esbuild#4507). To see it through Angular: any app withisolatedModules: trueand sourcemaps enabled, containing a module that imports a package's default export and also declaresimport Alias = ThatDefault.SomeType. Build with 22.0.7 → import preserved. Build with 22.1.x → import dropped,ReferenceErrorat runtime.What I'd suggest
Any of these would have prevented this, roughly in order of preference:
isolatedModules".tscemit where correctness matters more than rebuild speed.Please provide the environment you discovered this bug in
Relevant tsconfig:
{ "isolatedModules": true, "module": "preserve", "moduleResolution": "bundler", "target": "ES2022" }Production builder config includes
"sourceMap": { "hidden": true, "scripts": true, "styles": true }.