Repository navigation
@ngtools/webpack: elideImports keeps type-only specifiers that are re-exported or extends-ed, so webpack 5.112 (build-angular 21.2.28) fails the build #34580
Description
Command
build (also serve)
Is this a regression?
Yes. The previous version in which this bug was not present was 21.2.27 (webpack 5.105.2). Strictly speaking, 21.2.27 already emits the bad import too (see below). webpack 5.112 just makes it an error.
Description
With @angular-devkit/build-angular:browser 21.2.28 (which updated webpack to 5.112.0), an AOT build fails with:
./src/app/data.service.ts:1:0-33 - Error: export 'Status' (imported as 'Status') was not found in './types' (module has no exports)
Status is an interface. The failure needs all three of these in one file:
- The file has a decorated class, so
removeIvyJitSupportCallsremoves itsɵsetClassMetadatacall and runselideImports. - One
import { ... }statement brings in several interfaces. - Some of those interfaces are used only in type annotations, and at least one is either re-exported (
export type { Status }) or used in an interfaceextends Status.
What happens:
elideImportsdrops the annotation-only specifiers. Its visitor skipsTypeReferenceNodes.- It keeps the others as "used". It records
ExportSpecifiers without checking forexport type, and it only skipsimplementsheritage clauses, so an interface'sextendscounts as a use. - That rewrites the
ImportDeclaration. TypeScript then skips its own import elision for that statement, becauseisElisionBlockedis true when abeforetransformer has replaced the node. - So
import { Status } from './types'survives into the emitted JS, althoughStatushas no runtime value.
With webpack 5.105 this was a silent dead import. The repro's dev build on 21.2.27 still contains __webpack_require__(/*! ./types */ ...) in data.service's module.
webpack 5.112 now link-checks import specifiers even when they are never read, and Angular's strictExportPresence: true makes that an error.
Controls (each of these builds cleanly on 21.2.28):
import type { Item, Status }- no
export type { Status }, so both names are annotation-only and the whole import is removed import { Status }on its own line plusexport type { Status }, so nothing in that statement is partially removed and TypeScript elides it as usual- the same file without the
@Injectabledecorator, soelideImportsnever runs
A possible fix is for elideImports to count a symbol as used only when its alias target has a value meaning (SymbolFlags.Value). Narrower alternatives: skip export type declarations and specifiers, and skip interface heritage clauses.
Minimal Reproduction
Seven files. npm install, then npx ng build (or npx ng build -c development).
package.json
{
"name": "ngtools-webpack-type-reexport-repro",
"private": true,
"dependencies": {
"@angular/common": "21.2.25",
"@angular/compiler": "21.2.25",
"@angular/core": "21.2.25",
"@angular/platform-browser": "21.2.25",
"rxjs": "7.8.2",
"tslib": "2.8.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "21.2.28",
"@angular/cli": "21.2.28",
"@angular/compiler-cli": "21.2.25",
"typescript": "5.9.3"
}
}angular.json
{
"version": 1,
"projects": {
"repro": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/repro",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.json"
},
"configurations": {
"production": { "optimization": true, "outputHashing": "all" },
"development": { "optimization": false, "sourceMap": true }
},
"defaultConfiguration": "production"
}
}
}
}
}tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022", "dom"],
"skipLibCheck": true
},
"files": ["src/main.ts"]
}src/index.html
<!doctype html>
<html><head><meta charset="utf-8"><title>repro</title></head><body><app-root></app-root></body></html>src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent).catch(err => console.error(err));src/app/app.component.ts
import { Component, inject } from '@angular/core';
import { DataService } from './data.service';
@Component({ selector: 'app-root', template: '{{ id }}' })
export class AppComponent {
id = inject(DataService).first().id;
}src/app/types.ts
export interface Item {
id: number;
}
export interface Status {
done: boolean;
}src/app/data.service.ts
import { Injectable } from '@angular/core';
import { Item, Status } from './types';
// Re-export a type for consumers of this service.
export type { Status };
@Injectable({ providedIn: 'root' })
export class DataService {
first(): Item {
return { id: 1 };
}
}The extends variant fails the same way. Replace the export type { Status }; line with:
export interface DetailedStatus extends Status {
note: string;
}Exception or Error
./src/app/data.service.ts:1:0-33 - Error: export 'Status' (imported as 'Status') was not found in './types' (module has no exports)
Your Environment
Angular CLI : 21.2.28
Angular : 21.2.25
Node.js : 20.20.2
Package Manager : npm 10.8.2
Operating System : darwin arm64
@angular-devkit/build-angular 21.2.28
@angular/cli 21.2.28
@angular/common 21.2.25
@angular/compiler 21.2.25
@angular/compiler-cli 21.2.25
@angular/core 21.2.25
@angular/platform-browser 21.2.25
rxjs 7.8.2
typescript 5.9.3
webpack 5.112.0
Also reproduced on Node 22.23.2 (node:22-alpine) in the real app.
Anything else relevant?
- We hit this in a production app when moving from build-angular 21.2.26 to 21.2.28. 7 interface names in 3 files failed, and both
ng buildandng servebroke. - Workaround:
import type. - Neither TypeScript guard catches it.
isolatedModulesdoesn't flag the pattern, andverbatimModuleSyntaxflags every type import in a large codebase. ngcalone emits these files correctly. Only the@ngtools/webpackpipeline leaves the specifier behind.- The
@angular/build(esbuild) builders are not affected, as far as we can tell.
Activity
fix(@ngtools/webpack): elide imports used only in type re-exports or …
fix(@ngtools/webpack): elide imports used only in type re-exports or …