Skip to content

Commit 3e088ed

Browse files
committed
fix(@angular/build): prevent externalizing builder-injected i18n locale data
When a user externalizes `@angular/common` using the `externalDependencies` option, esbuild's wildcard external rule also marks any paths under `@angular/common/*` as external. This causes the builder-injected locale data imports (`@angular/common/locales/global/<code>`) inside the polyfills bundle to be externalized as bare imports, which fails at runtime because these files are not emitted as standalone public assets.
1 parent 92da0d6 commit 3e088ed

3 files changed

Lines changed: 50 additions & 4 deletions

File tree

packages/angular/build/src/builders/application/execute-build.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { BundleContextResult, BundlerContext } from '../../tools/esbuild/bundler
1515
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1616
import { BuildOutputFileType } from '../../tools/esbuild/bundler-files';
1717
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
18+
import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin';
1819
import { extractLicenses } from '../../tools/esbuild/license-extractor';
1920
import { profileAsync } from '../../tools/esbuild/profiling';
2021
import {
@@ -210,6 +211,12 @@ export async function executeBuild(
210211
const explicitExternal = new Set<string>();
211212

212213
const isExplicitExternal = (dep: string): boolean => {
214+
// Locale-data imports are injected by the builder itself (see i18n-locale-plugin) and must stay resolvable
215+
// even when the containing package is externalized by the user.
216+
if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) {
217+
return false;
218+
}
219+
213220
if (exclusions.has(dep)) {
214221
return true;
215222
}

packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,33 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
7474
// If not externalized, build will fail with a Node.js platform builtin error
7575
expect(result?.success).toBeTrue();
7676
});
77+
78+
it('should not externalize builder-injected i18n locale-data imports when @angular/common is external', async () => {
79+
harness.useProject('test', {
80+
root: '.',
81+
sourceRoot: 'src',
82+
cli: {
83+
cache: {
84+
enabled: false,
85+
},
86+
},
87+
i18n: {
88+
sourceLocale: 'fr',
89+
},
90+
});
91+
92+
harness.useTarget('build', {
93+
...BASE_OPTIONS,
94+
externalDependencies: ['@angular/common'],
95+
});
96+
97+
const { result } = await harness.executeOnce();
98+
expect(result?.success).toBeTrue();
99+
100+
harness.expectFile('dist/browser/polyfills.js').toExist();
101+
harness
102+
.expectFile('dist/browser/polyfills.js')
103+
.content.not.toMatch(/['"]@angular\/common\/locales\/global\/fr['"]/);
104+
});
77105
});
78106
});

packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import type { Plugin, ResolveResult } from 'esbuild';
10-
import { createRequire } from 'node:module';
10+
import { createProjectResolver } from '../../utils/resolve-project';
1111

1212
/**
1313
* The internal namespace used by generated locale import statements and Angular locale data plugin.
@@ -50,7 +50,7 @@ export function createAngularLocaleDataPlugin(): Plugin {
5050
}
5151

5252
let exact = true;
53-
let localeRequire: NodeJS.Require | undefined;
53+
let projectResolve: ((packageName: string) => string) | undefined;
5454
while (partialLocaleTag) {
5555
// Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here.
5656
// The onLoad hook below for the locale data namespace has an `empty` loader that will prevent inclusion.
@@ -73,10 +73,10 @@ export function createAngularLocaleDataPlugin(): Plugin {
7373
let result: ResolveResult | undefined;
7474
const { packages, absWorkingDir } = build.initialOptions;
7575
if (packages === 'external' && absWorkingDir) {
76-
localeRequire ??= createRequire(absWorkingDir + '/');
76+
projectResolve ??= createProjectResolver(absWorkingDir);
7777

7878
try {
79-
localeRequire.resolve(potentialPath);
79+
projectResolve(potentialPath);
8080

8181
result = {
8282
errors: [],
@@ -94,6 +94,17 @@ export function createAngularLocaleDataPlugin(): Plugin {
9494
kind: 'import-statement',
9595
resolveDir: absWorkingDir,
9696
});
97+
if (result && result.external && absWorkingDir) {
98+
projectResolve ??= createProjectResolver(absWorkingDir);
99+
try {
100+
const resolvedPath = projectResolve(potentialPath);
101+
result = {
102+
...result,
103+
external: false,
104+
path: resolvedPath,
105+
};
106+
} catch {}
107+
}
97108
}
98109

99110
if (result?.path) {

0 commit comments

Comments
 (0)