Skip to content

@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

Closed

Description

@ohItzYosh

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:

  1. The file has a decorated class, so removeIvyJitSupportCalls removes its ɵsetClassMetadata call and runs elideImports.
  2. One import { ... } statement brings in several interfaces.
  3. 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 interface extends Status.

What happens:

  • elideImports drops the annotation-only specifiers. Its visitor skips TypeReferenceNodes.
  • It keeps the others as "used". It records ExportSpecifiers without checking for export type, and it only skips implements heritage clauses, so an interface's extends counts as a use.
  • That rewrites the ImportDeclaration. TypeScript then skips its own import elision for that statement, because isElisionBlocked is true when a before transformer has replaced the node.
  • So import { Status } from './types' survives into the emitted JS, although Status has 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 plus export type { Status }, so nothing in that statement is partially removed and TypeScript elides it as usual
  • the same file without the @Injectable decorator, so elideImports never 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 build and ng serve broke.
  • Workaround: import type.
  • Neither TypeScript guard catches it. isolatedModules doesn't flag the pattern, and verbatimModuleSyntax flags every type import in a large codebase.
  • ngc alone emits these files correctly. Only the @ngtools/webpack pipeline leaves the specifier behind.
  • The @angular/build (esbuild) builders are not affected, as far as we can tell.

Activity

  1. added this to the needsTriage milestone on Oct 29, 2026
  2. added a commit that references this issue on Oct 29, 2026
    4b6caac
  3. added a commit that references this issue on Oct 29, 2026
    b6bfa2c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions