Skip to content

dev-server: /@id/ prefix not stripped from minified bundles when using externalDependencies (greedy regex in createRemoveIdPrefixPlugin) #33524

Description

@FestimShyti

Command

serve

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

No response

Description

When the dev server serves an optimized/minified build (ng serve with a configuration
where optimization is enabled) and the build has externalDependencies, most external imports
reach the browser with Vite's internal /@id/ prefix still attached instead of being restored
to bare specifiers.

Vite's import-analysis rewrites external bare imports to <base>@id/<pkg>. The
angular-plugin-remove-id-prefix transform (id-prefix-plugin.ts)
is supposed to strip that prefix, but its regex appends a greedy (?:/.+)? to each external
name. On minified (single-line) output, the first match on an external with a deep import path
(e.g. @angular/common/http) consumes the remainder of the line into the capture group, so
every later /@id/ occurrence on that line is never matched and is served unstripped. The
browser then requests e.g. http://localhost:4200/@id/@angular/router, receives the
index.html fallback, and module loading fails.

Serving an unoptimized build of the same app works, because esbuild emits one import per line
and the greedy match is clipped by the newline.

A detailed root-cause analysis, a suggested one-line fix (verified end-to-end; PR prepared with
a regression spec), and a patch-package workaround are in the attached additional-info.md.

Minimal Reproduction

The defect is deterministic at the regex level (no app needed):

// node repro.js — the exact regex construction from id-prefix-plugin.ts
const escapeRegexSpecialChars = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const externals = ['@angular/common', '@angular/common/http', '@angular/core', '@angular/router'].sort();
const escaped = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?');
const reg = new RegExp('/@id/(' + escaped.join('|') + ')', 'g');

// one line, as in any minified bundle
const code =
  'import{a}from"/@id/@angular/common/http";import{b}from"/@id/@angular/router";import{c}from"/@id/@angular/core";';

console.log(code.replace(reg, (_, name) => name));
// Actual:   import{a}from"@angular/common/http";import{b}from"/@id/@angular/router";import{c}from"/@id/@angular/core";
// Expected: import{a}from"@angular/common/http";import{b}from"@angular/router";import{c}from"@angular/core";

Steps to observe it in an app:

  1. ng new repro-app --minimal --skip-git && cd repro-app
  2. In angular.json, add to projects.repro-app.architect.build.options:
    "externalDependencies": ["@angular/common", "@angular/common/http", "@angular/core", "@angular/router"]
  3. Make the app reference a deep entry point next to other externals, e.g. in app.config.ts:
    import { provideHttpClient } from '@angular/common/http';
    // add provideHttpClient() to providers (provideRouter is already there in a default app)
  4. Serve with optimization: ng serve --configuration production
  5. Inspect the served main bundle:
    main=$(curl -s http://localhost:4200/ | grep -o 'src="main[^"]*"' | cut -d'"' -f2)
    curl -s "http://localhost:4200/$main" | grep -o '"/@id/[^"]*"' | sort -u

Actual: unstripped specifiers such as "/@id/@angular/router" are listed, and the browser
console shows the module-loading error below.
Expected: no /@id/ occurrences — all externals restored to bare specifiers (as happens
when serving without optimization, step 4 with the default configuration).

Exception or Error

Error: Unsupported Content-Type "text/html" loading http://localhost:4200/@id/@angular/router
imported from http://localhost:4200/chunk-XXXXXXXX.js.
Modules must be served with a valid MIME type like application/javascript.

Your Environment

Angular CLI       : 21.2.0
Angular           : 21.2.1
Node.js           : 26.4.0
Package Manager   : npm 11.17.0
Operating System  : darwin arm64

┌───────────────────────────────────┬───────────────────┬───────────────────┐
│ Package                           │ Installed Version │ Requested Version │
├───────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular-devkit/build-angular     │ 21.2.0            │ 21.2.0            │
│ @angular-devkit/core              │ 21.2.0            │ 21.2.0            │
│ @angular-devkit/schematics        │ 21.2.0            │ 21.2.0            │
│ @angular/animations               │ 21.2.1            │ 21.2.1            │
│ @angular/build                    │ 21.2.0            │ 21.2.0            │
│ @angular/cdk                      │ 21.2.1            │ 21.2.1            │
│ @angular/cdk-experimental         │ 21.2.1            │ 21.2.1            │
│ @angular/cli                      │ 21.2.0            │ 21.2.0            │
│ @angular/common                   │ 21.2.1            │ 21.2.1            │
│ @angular/compiler                 │ 21.2.1            │ 21.2.1            │
│ @angular/compiler-cli             │ 21.2.1            │ 21.2.1            │
│ @angular/core                     │ 21.2.1            │ 21.2.1            │
│ @angular/elements                 │ 21.2.1            │ 21.2.1            │
│ @angular/forms                    │ 21.2.1            │ 21.2.1            │
│ @angular/language-service         │ 21.2.1            │ 21.2.1            │
│ @angular/localize                 │ 21.2.1            │ 21.2.1            │
│ @angular/platform-browser         │ 21.2.1            │ 21.2.1            │
│ @angular/platform-browser-dynamic │ 21.2.1            │ 21.2.1            │
│ @angular/router                   │ 21.2.1            │ 21.2.1            │
│ @schematics/angular               │ 21.2.0            │ 21.2.0            │
│ ng-packagr                        │ 21.2.0            │ 21.2.0            │
│ rxjs                              │ 7.8.2             │ 7.8.2             │
│ typescript                        │ 5.9.3             │ 5.9.3             │
│ vitest                            │ 4.0.18            │ 4.0.18            │
│ zone.js                           │ 0.16.1            │ 0.16.1            │
└───────────────────────────────────┴───────────────────┴───────────────────┘

Anything else relevant?

  • Not browser specific (the served JS is already wrong on the wire); observed on macOS, but the
    defect is platform-independent.
  • The regex is unchanged in @angular/build 21.2.0, 21.2.18, 22.0.5, 22.1.0-next.2, and
    current main.
  • vite's @id prefix isn't removed everywhere #30642 reported the same symptom and was closed as not reproducible — presumably because a
    default (unoptimized) ng serve never triggers it; optimization must be enabled.
  • Root-cause analysis, suggested one-line fix (verified; PR with regression spec prepared), and
    a patch-package workaround: see attached additional-info.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions