Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
externalDependencies excludes packages from the bundle so the user can
provide them at runtime (e.g. via an import map — the documented contract,
used among others by module-federation tooling). Bare imports of the
externalized packages in main.js are therefore expected.
However, when a non-English i18n.sourceLocale is configured, the builder
itself injects a locale-data import into the polyfills bundle
(application-code-bundle.ts → angular:locale/data:<code> →
@angular/common/locales/global/<code>). If @angular/common is
externalized, this builder-injected import inherits the external
classification through the prefix rule (--external:@foo/bar implies
--external:@foo/bar/*) and ends up as an unresolvable bare specifier in
the polyfills bundle — in ng build output as well as in the dev server.
Unlike the user-authored imports in main.js, this import cannot reasonably
be satisfied by the user: the locale-data file is not emitted or served
under any stable URL, and the polyfills script typically executes before a
runtime-provided import map exists. The builder should treat its own
locale-data injection as internal and always make it resolvable (bundle it,
or let Vite prebundle it in the dev server), regardless of the externals
configuration.
Minimal Reproduction
ng new repro --defaults && cd repro
angular.json: add to the project "i18n": { "sourceLocale": "de-DE" } and to
the build target options "externalDependencies": ["@angular/common"].
ng build
grep -o 'import"@angular/common[^"]*"' dist/repro/browser/polyfills*.js
# → import"@angular/common/locales/global/de-DE" (bare, unresolvable)
Comparison of the polyfills bundle (ng build):
| config |
polyfills output |
de-DE, no externalDependencies |
locale data bundled inline (ng.common.locales…) |
de-DE + externalDependencies |
bare import"@angular/common/locales/global/de-DE" |
en-US + externalDependencies |
no locale reference at all (framework built-in) |
The same happens with the dev server: ng serve, then
curl http://localhost:4200/polyfills.js shows
import "@angular/common/locales/global/de"; while Vite prebundles and
rewrites all other polyfill imports. (Side observation: the dev server falls
back from de-DE to de via the locale plugin's require.resolve path,
while ng build keeps de-DE because the external option matches before
resolution — two different code paths produce different specifiers.)
Note: the browser console additionally shows a failing bare
"@angular/common" from main.js — that one is expected (the user opted
into providing it at runtime). The defect is solely the builder-injected
@angular/common/locales/global/<code> import in the polyfills bundle.
Proposed fix
Exempt the builder's own locale-data imports from the explicit-external
classification, so the dev server prebundles them exactly as in a default
setup:
// packages/angular/build/src/builders/application/execute-build.ts
import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin';
const isExplicitExternal = (dep: string): boolean => {
// Locale-data imports are injected by the builder itself (see
// i18n-locale-plugin) and must stay resolvable via Vite prebundling
// even when the containing package is externalized by the user.
if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) {
return false;
}
if (exclusions.has(dep)) {
return true;
}
for (const prefix of exclusionsPrefixes) {
if (dep.startsWith(prefix)) {
return true;
}
}
return false;
};
Exception or Error
Uncaught TypeError: Failed to resolve module specifier
"@angular/common/locales/global/de". Relative references must start with
either "/", "./", or "../".
Your Environment
Angular CLI: 21.2.x
@angular/build: 21.2.x
Node: 24.x
Anything else relevant?
- Downstream context: module-federation tooling
(@angular-architects/native-federation) externalizes @angular/common via
build plugins and hits exactly this classification; it currently works
around it by forcing the polyfills bundle into packages: 'bundle' mode
(adding a local polyfill entry), which inlines the locale data like a
production build does.
- User-level workaround: add any local file (e.g. an empty
src/polyfills-switch.ts) to the polyfills array — this disables
external packages for the polyfills bundle
(getEsBuildCommonPolyfillsOptions) and bundles the locale data inline.
Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
externalDependenciesexcludes packages from the bundle so the user canprovide them at runtime (e.g. via an import map — the documented contract,
used among others by module-federation tooling). Bare imports of the
externalized packages in
main.jsare therefore expected.However, when a non-English
i18n.sourceLocaleis configured, the builderitself injects a locale-data import into the polyfills bundle
(
application-code-bundle.ts→angular:locale/data:<code>→@angular/common/locales/global/<code>). If@angular/commonisexternalized, this builder-injected import inherits the external
classification through the prefix rule (
--external:@foo/barimplies--external:@foo/bar/*) and ends up as an unresolvable bare specifier inthe polyfills bundle — in
ng buildoutput as well as in the dev server.Unlike the user-authored imports in
main.js, this import cannot reasonablybe satisfied by the user: the locale-data file is not emitted or served
under any stable URL, and the polyfills script typically executes before a
runtime-provided import map exists. The builder should treat its own
locale-data injection as internal and always make it resolvable (bundle it,
or let Vite prebundle it in the dev server), regardless of the externals
configuration.
Minimal Reproduction
angular.json: add to the project
"i18n": { "sourceLocale": "de-DE" }and tothe build target options
"externalDependencies": ["@angular/common"].Comparison of the polyfills bundle (
ng build):de-DE, noexternalDependenciesng.common.locales…)de-DE+externalDependenciesimport"@angular/common/locales/global/de-DE"en-US+externalDependenciesThe same happens with the dev server:
ng serve, thencurl http://localhost:4200/polyfills.jsshowsimport "@angular/common/locales/global/de";while Vite prebundles andrewrites all other polyfill imports. (Side observation: the dev server falls
back from
de-DEtodevia the locale plugin'srequire.resolvepath,while
ng buildkeepsde-DEbecause theexternaloption matches beforeresolution — two different code paths produce different specifiers.)
Note: the browser console additionally shows a failing bare
"@angular/common"frommain.js— that one is expected (the user optedinto providing it at runtime). The defect is solely the builder-injected
@angular/common/locales/global/<code>import in the polyfills bundle.Proposed fix
Exempt the builder's own locale-data imports from the explicit-external
classification, so the dev server prebundles them exactly as in a default
setup:
Exception or Error
Your Environment
Anything else relevant?
(@angular-architects/native-federation) externalizes
@angular/commonviabuild plugins and hits exactly this classification; it currently works
around it by forcing the polyfills bundle into
packages: 'bundle'mode(adding a local polyfill entry), which inlines the locale data like a
production build does.
src/polyfills-switch.ts) to thepolyfillsarray — this disablesexternal packages for the polyfills bundle
(
getEsBuildCommonPolyfillsOptions) and bundles the locale data inline.