diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index c326d277ec61..857ef564f52a 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -15,6 +15,7 @@ import { BundleContextResult, BundlerContext } from '../../tools/esbuild/bundler import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFileType } from '../../tools/esbuild/bundler-files'; import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; +import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin'; import { extractLicenses } from '../../tools/esbuild/license-extractor'; import { profileAsync } from '../../tools/esbuild/profiling'; import { @@ -210,6 +211,12 @@ export async function executeBuild( const explicitExternal = new Set(); const isExplicitExternal = (dep: string): boolean => { + // Locale-data imports are injected by the builder itself (see i18n-locale-plugin) and must stay resolvable + // 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; } diff --git a/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts b/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts index deb55e172109..bdb8f9428f56 100644 --- a/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/external-dependencies_spec.ts @@ -74,5 +74,33 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // If not externalized, build will fail with a Node.js platform builtin error expect(result?.success).toBeTrue(); }); + + it('should not externalize builder-injected i18n locale-data imports when @angular/common is external', async () => { + harness.useProject('test', { + root: '.', + sourceRoot: 'src', + cli: { + cache: { + enabled: false, + }, + }, + i18n: { + sourceLocale: 'fr', + }, + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + externalDependencies: ['@angular/common'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/polyfills.js').toExist(); + harness + .expectFile('dist/browser/polyfills.js') + .content.not.toMatch(/['"]@angular\/common\/locales\/global\/fr['"]/); + }); }); }); diff --git a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts index ae94b62ca16d..603c65677b00 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts @@ -7,7 +7,7 @@ */ import type { Plugin, ResolveResult } from 'esbuild'; -import { createRequire } from 'node:module'; +import { createProjectResolver } from '../../utils/resolve-project'; /** * The internal namespace used by generated locale import statements and Angular locale data plugin. @@ -50,7 +50,7 @@ export function createAngularLocaleDataPlugin(): Plugin { } let exact = true; - let localeRequire: NodeJS.Require | undefined; + let projectResolve: ((packageName: string) => string) | undefined; while (partialLocaleTag) { // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. // 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 { let result: ResolveResult | undefined; const { packages, absWorkingDir } = build.initialOptions; if (packages === 'external' && absWorkingDir) { - localeRequire ??= createRequire(absWorkingDir + '/'); + projectResolve ??= createProjectResolver(absWorkingDir); try { - localeRequire.resolve(potentialPath); + projectResolve(potentialPath); result = { errors: [], @@ -94,6 +94,17 @@ export function createAngularLocaleDataPlugin(): Plugin { kind: 'import-statement', resolveDir: absWorkingDir, }); + if (result && result.external && absWorkingDir) { + projectResolve ??= createProjectResolver(absWorkingDir); + try { + const resolvedPath = projectResolve(potentialPath); + result = { + ...result, + external: false, + path: resolvedPath, + }; + } catch {} + } } if (result?.path) {