From 9fd875324eb70f1e07417eda708b866293e2ae1f Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 21 Sep 2026 09:42:58 +0000 Subject: [PATCH] refactor(@angular/build): relocate non-esbuild utilities out of tools/esbuild Relocate several utilities from `src/tools/esbuild` to more appropriate locations, as they are not specific to esbuild: - `watcher.ts`: moved to `src/utils/watcher.ts`. - `profiling.ts`: moved to `src/utils/profiling.ts`. - `cache.ts`, `sqlite-cache-store.ts`, `lmdb-cache-store.ts`: moved to `src/utils/cache/`. - `javascript-transformer.ts`, `javascript-transformer-worker.ts`: moved to `src/tools/javascript-transformer/`. - `i18n-inliner.ts`, `i18n-inliner-worker.ts`, `i18n-translation-*`: moved to `src/tools/i18n/`, with locale data helpers in `src/tools/i18n/locale-data.ts`. - `index-html-generator.ts`: moved to `src/builders/application/index-html-generator.ts`. --- .../src/builders/application/build-action.ts | 6 +- .../src/builders/application/execute-build.ts | 2 +- .../application/execute-post-bundle.ts | 7 +- .../build/src/builders/application/i18n.ts | 2 +- .../application}/index-html-generator.ts | 8 +- .../build/src/builders/dev-server/internal.ts | 2 +- packages/angular/build/src/private.ts | 4 +- .../angular/compilation/aot-compilation.ts | 2 +- .../angular/compilation/jit-compilation.ts | 2 +- .../compilation/parallel-compilation.ts | 2 +- .../angular/compilation/parallel-worker.ts | 2 +- .../compilation/typescript-compilation.ts | 2 +- .../tools/esbuild/angular/compiler-plugin.ts | 10 +- .../esbuild/angular/component-stylesheets.ts | 2 +- .../tools/esbuild/bundler-execution-result.ts | 2 +- .../src/tools/esbuild/i18n-locale-plugin.ts | 145 ++---------------- .../esbuild/persistent-load-result-cache.ts | 2 +- .../persistent-load-result-cache_spec.ts | 2 +- .../esbuild/stylesheets/sass-language.ts | 2 +- .../{esbuild => i18n}/i18n-inliner-worker.ts | 4 +- .../tools/{esbuild => i18n}/i18n-inliner.ts | 12 +- .../{esbuild => i18n}/i18n-inliner_spec.ts | 8 +- .../i18n-translation-encoder.ts | 0 .../i18n-translation-encoder_spec.ts | 0 .../i18n-translation-reader.ts | 0 .../angular/build/src/tools/i18n/index.ts | 12 ++ .../build/src/tools/i18n/locale-data.ts | 138 +++++++++++++++++ .../src/tools/javascript-transformer/index.ts | 9 ++ .../javascript-transformer-worker.ts | 0 .../javascript-transformer.ts | 2 +- .../javascript-transformer_spec.ts | 0 .../angular/build/src/tools/vite/utils.ts | 2 +- .../{tools/esbuild => utils/cache}/cache.ts | 4 +- .../esbuild => utils/cache}/cache_spec.ts | 0 .../angular/build/src/utils/cache/index.ts | 9 ++ .../cache}/lmdb-cache-store.ts | 0 .../cache}/sqlite-cache-store.ts | 0 .../cache}/sqlite-cache-store_spec.ts | 0 .../src/{tools/esbuild => utils}/profiling.ts | 2 +- .../src/{tools/esbuild => utils}/watcher.ts | 4 +- .../{tools/esbuild => utils}/watcher_spec.ts | 0 scripts/benchmark.mts | 4 +- scripts/benchmarks/i18n/fixtures.mts | 2 +- scripts/benchmarks/i18n/scenarios.mts | 6 +- 44 files changed, 247 insertions(+), 177 deletions(-) rename packages/angular/build/src/{tools/esbuild => builders/application}/index-html-generator.ts (96%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-inliner-worker.ts (99%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-inliner.ts (99%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-inliner_spec.ts (99%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-translation-encoder.ts (100%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-translation-encoder_spec.ts (100%) rename packages/angular/build/src/tools/{esbuild => i18n}/i18n-translation-reader.ts (100%) create mode 100644 packages/angular/build/src/tools/i18n/index.ts create mode 100644 packages/angular/build/src/tools/i18n/locale-data.ts create mode 100644 packages/angular/build/src/tools/javascript-transformer/index.ts rename packages/angular/build/src/tools/{esbuild => javascript-transformer}/javascript-transformer-worker.ts (100%) rename packages/angular/build/src/tools/{esbuild => javascript-transformer}/javascript-transformer.ts (99%) rename packages/angular/build/src/tools/{esbuild => javascript-transformer}/javascript-transformer_spec.ts (100%) rename packages/angular/build/src/{tools/esbuild => utils/cache}/cache.ts (98%) rename packages/angular/build/src/{tools/esbuild => utils/cache}/cache_spec.ts (100%) create mode 100644 packages/angular/build/src/utils/cache/index.ts rename packages/angular/build/src/{tools/esbuild => utils/cache}/lmdb-cache-store.ts (100%) rename packages/angular/build/src/{tools/esbuild => utils/cache}/sqlite-cache-store.ts (100%) rename packages/angular/build/src/{tools/esbuild => utils/cache}/sqlite-cache-store_spec.ts (100%) rename packages/angular/build/src/{tools/esbuild => utils}/profiling.ts (97%) rename packages/angular/build/src/{tools/esbuild => utils}/watcher.ts (99%) rename packages/angular/build/src/{tools/esbuild => utils}/watcher_spec.ts (100%) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index 299d7cf88ef4..52e70ff87e99 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -18,9 +18,9 @@ import { shutdownSassWorkerPool, } from '../../tools/esbuild/stylesheets/sass-language'; import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; -import { ChangedFiles } from '../../tools/esbuild/watcher'; import { initializeHash } from '../../utils/hash'; import { NormalizedCachedOptions } from '../../utils/normalize-cache'; +import { ChangedFiles } from '../../utils/watcher'; import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options'; import { ComponentUpdateResult, @@ -88,7 +88,7 @@ export async function* runEsBuildBuildAction( } } - let watcher: import('../../tools/esbuild/watcher').BuildWatcher | undefined; + let watcher: import('../../utils/watcher').BuildWatcher | undefined; let watchLoopStarted = false; try { // Setup watcher if watch mode enabled @@ -98,7 +98,7 @@ export async function* runEsBuildBuildAction( } // Setup a watcher - const { setupWatcher } = await import('../../tools/esbuild/watcher'); + const { setupWatcher } = await import('../../utils/watcher'); watcher = await setupWatcher({ workspaceRoot, projectRoot, diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 379566e4de4d..d3b628a169e2 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -20,7 +20,6 @@ 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 { transformSupportedBrowsersToTargets } from '../../tools/esbuild/target'; import { calculateEstimatedTransferSizes, @@ -29,6 +28,7 @@ import { } from '../../tools/esbuild/utils'; import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator'; import { optimizeChunksThreshold } from '../../utils/environment-options'; +import { profileAsync } from '../../utils/profiling'; import { resolveAssets } from '../../utils/resolve-assets'; import { SERVER_APP_ENGINE_MANIFEST_FILENAME, diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts index 07639a43c3e5..99e23bcb30c9 100644 --- a/packages/angular/build/src/builders/application/execute-post-bundle.ts +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -18,18 +18,15 @@ import { type InitialFileRecord, createOutputFile, } from '../../tools/esbuild/bundler-files'; -import { generateIndexHtml } from '../../tools/esbuild/index-html-generator'; import { maxWorkers } from '../../utils/environment-options'; import { SERVER_APP_MANIFEST_FILENAME, generateAngularServerAppManifest, } from '../../utils/server-rendering/manifest'; -import { - RouteRenderMode, - WritableSerializableRouteTreeNode, -} from '../../utils/server-rendering/models'; +import { WritableSerializableRouteTreeNode } from '../../utils/server-rendering/models'; import { prerenderPages } from '../../utils/server-rendering/prerender'; import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; +import { generateIndexHtml } from './index-html-generator'; import { injectDebugIds } from './inject-debug-ids'; import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options'; import { OutputMode } from './schema'; diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index 0d6ae77cdea7..839384421f1f 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -16,7 +16,7 @@ import { PrerenderedRoutesRecord, } from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-files'; -import { I18nInliner } from '../../tools/esbuild/i18n-inliner'; +import { I18nInliner } from '../../tools/i18n'; import { maxInlinerWorkers } from '../../utils/environment-options'; import { loadTranslations } from '../../utils/i18n-options'; import { createTranslationLoader } from '../../utils/load-translations'; diff --git a/packages/angular/build/src/tools/esbuild/index-html-generator.ts b/packages/angular/build/src/builders/application/index-html-generator.ts similarity index 96% rename from packages/angular/build/src/tools/esbuild/index-html-generator.ts rename to packages/angular/build/src/builders/application/index-html-generator.ts index a7ad3fd47e7f..d89eadf8b9b0 100644 --- a/packages/angular/build/src/tools/esbuild/index-html-generator.ts +++ b/packages/angular/build/src/builders/application/index-html-generator.ts @@ -9,9 +9,13 @@ import assert from 'node:assert'; import { createHash } from 'node:crypto'; import path from 'node:path'; -import { NormalizedApplicationBuildOptions } from '../../builders/application/options'; +import { + BuildOutputFile, + BuildOutputFileType, + InitialFileRecord, +} from '../../tools/esbuild/bundler-files'; import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator'; -import { BuildOutputFile, BuildOutputFileType, InitialFileRecord } from './bundler-files'; +import { NormalizedApplicationBuildOptions } from './options'; /** * The maximum number of module preload link elements that should be added for diff --git a/packages/angular/build/src/builders/dev-server/internal.ts b/packages/angular/build/src/builders/dev-server/internal.ts index 96587acb5cff..19ca0cf3e92e 100644 --- a/packages/angular/build/src/builders/dev-server/internal.ts +++ b/packages/angular/build/src/builders/dev-server/internal.ts @@ -8,7 +8,7 @@ export { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files'; export { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin'; -export { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer'; +export { JavaScriptTransformer } from '../../tools/javascript-transformer'; export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils'; export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator'; export { purgeStaleBuildCache } from '../../utils/purge-cache'; diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 1fe8765b2646..9db709640b57 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -39,9 +39,9 @@ export { transformSupportedBrowsersToTargets } from './tools/esbuild/target'; export { SassCompiler } from './tools/sass/sass-service'; export { SourceFileCache } from './tools/esbuild/angular/source-file-cache'; -export { Cache } from './tools/esbuild/cache'; +export { Cache } from './utils/cache'; export { createJitResourceTransformer } from './tools/angular/transformers/jit-resource-transformer'; -export { JavaScriptTransformer } from './tools/esbuild/javascript-transformer'; +export { JavaScriptTransformer } from './tools/javascript-transformer'; export function createCompilerPlugin( pluginOptions: CompilerPluginOptions & { diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index ca74e721ba7d..b42b1273592d 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -11,7 +11,7 @@ import assert from 'node:assert'; import { relative } from 'node:path'; import ts from 'typescript'; import { useTypeChecking } from '../../../utils/environment-options'; -import { profileAsync, profileSync } from '../../esbuild/profiling'; +import { profileAsync, profileSync } from '../../../utils/profiling'; import { AngularHostOptions, createAngularCompilerHost, diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index 233f5977e5a6..e89db1e75aa4 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -9,7 +9,7 @@ import type * as ng from '@angular/compiler-cli'; import assert from 'node:assert'; import ts from 'typescript'; -import { profileSync } from '../../esbuild/profiling'; +import { profileSync } from '../../../utils/profiling'; import { AngularHostOptions, createAngularCompilerHost } from '../angular-host'; import { createJitResourceTransformer } from '../transformers/jit-resource-transformer'; import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer'; diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 4df13b3a500e..0b58423a418b 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -9,8 +9,8 @@ import type { PartialMessage } from 'esbuild'; import { createRequire } from 'node:module'; import { MessageChannel } from 'node:worker_threads'; +import { mergeCumulativeDurations } from '../../../utils/profiling'; import { WorkerPool } from '../../../utils/worker-pool'; -import { mergeCumulativeDurations } from '../../esbuild/profiling'; import type { AngularHostOptions } from '../angular-host'; import { AngularCompilation, diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index 71b4d34e2756..5bf293da727d 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -11,7 +11,7 @@ import assert from 'node:assert'; import { randomUUID } from 'node:crypto'; import { type MessagePort, receiveMessageOnPort } from 'node:worker_threads'; import { initializeHash } from '../../../utils/hash'; -import { getAndClearCumulativeDurations } from '../../esbuild/profiling'; +import { getAndClearCumulativeDurations } from '../../../utils/profiling'; import type { AngularCompilation, AngularCompilationResult, diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 71061031f3a4..05403b728fda 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -10,7 +10,7 @@ import type * as ng from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import ts from 'typescript'; import { toPosixPath } from '../../../utils/path'; -import { profileAsync, profileSync } from '../../esbuild/profiling'; +import { profileAsync, profileSync } from '../../../utils/profiling'; import { AngularCompilation, DiagnosticModes } from './angular-compilation'; import { type CompilerOptionOverrides, transformCompilerOptions } from './compiler-options'; import { convertTypeScriptDiagnostic } from './diagnostics'; diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index 00c82b328085..a9c71e8db888 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -20,14 +20,18 @@ import type { import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; +import { type PersistentCacheStore, createPersistentCacheStore } from '../../../utils/cache'; import { maxTransformWorkers, useTypeChecking } from '../../../utils/environment-options'; import { calculateHash, initializeHash } from '../../../utils/hash'; +import { + logCumulativeDurations, + profileAsync, + resetCumulativeDurations, +} from '../../../utils/profiling'; import { AngularHostOptions } from '../../angular/angular-host'; import { AngularCompilation, DiagnosticModes } from '../../angular/compilation'; -import { type PersistentCacheStore, createPersistentCacheStore } from '../cache'; -import { JavaScriptTransformer } from '../javascript-transformer'; +import { JavaScriptTransformer } from '../../javascript-transformer'; import { LoadResultCache, createCachedLoad } from '../load-result-cache'; -import { logCumulativeDurations, profileAsync, resetCumulativeDurations } from '../profiling'; import { AngularCompilationContext, PrimaryCompilationContext } from './compilation-state'; import { ComponentStylesheetBundler } from './component-stylesheets'; import { FileReferenceTracker } from './file-reference-tracker'; diff --git a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts index 75ce354341ad..b5d2e453b57e 100644 --- a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts +++ b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts @@ -8,10 +8,10 @@ import assert from 'node:assert'; import path from 'node:path'; +import { MemoryCache } from '../../../utils/cache'; import { createContentHash } from '../../../utils/hash'; import { BundleContextResult, BundlerContext } from '../bundler-context'; import { type BuildOutputFile, BuildOutputFileType } from '../bundler-files'; -import { MemoryCache } from '../cache'; import { MemoryLoadResultCache } from '../load-result-cache'; import { BundleStylesheetOptions, diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index eeb7045dfb61..991ad988662b 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -8,7 +8,7 @@ import type { Message, PartialMessage } from 'esbuild'; import { normalize } from 'node:path'; -import type { ChangedFiles } from '../../tools/esbuild/watcher'; +import type { ChangedFiles } from '../../utils/watcher'; import type { ComponentStylesheetBundler } from './angular/component-stylesheets'; import type { SourceFileCache } from './angular/source-file-cache'; import type { BundlerContext } from './bundler-context'; 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 8ffddf19adb7..b4c89cf644c1 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-locale-plugin.ts @@ -7,136 +7,23 @@ */ import type { Plugin } from 'esbuild'; -import { readFile } from 'node:fs/promises'; import { createProjectResolver } from '../../utils/resolve-project'; - -/** - * The internal namespace used by generated locale import statements and Angular locale data plugin. - */ -export const LOCALE_DATA_NAMESPACE = 'angular:locale/data'; - -/** - * The base module location used to search for locale specific data. - */ -export const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global'; - -/** - * Result of resolving locale data for a given locale tag. - */ -export interface LocaleDataResolution { - path?: string; - warning?: string; - error?: string; -} - -/** - * Result of loading locale data for a given locale tag. - */ -export interface LoadedLocaleData { - code?: string; - warning?: string; - error?: string; -} - -const localeDataCache = new Map>(); - -/** - * Resolves the path to the Angular locale data file for a given locale tag. - * - * @param rawLocaleTag The raw locale identifier (e.g. "fr-CA", "de", "en-US"). - * @param projectResolve A function that attempts to resolve a path string to an absolute file path. - * @returns Resolution result with file path, or warning/error diagnostics if applicable. - */ -export function resolveLocaleDataPath( - rawLocaleTag: string, - projectResolve: (potentialPath: string) => string | undefined, -): LocaleDataResolution { - let partialLocaleTag: string; - try { - const locale = new Intl.Locale(rawLocaleTag); - partialLocaleTag = locale.baseName; - } catch { - return { - error: `Invalid or unsupported locale provided in configuration: "${rawLocaleTag}"`, - }; - } - - let exact = true; - while (partialLocaleTag) { - // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. - if (partialLocaleTag === 'en' || partialLocaleTag === 'en-US') { - return {}; - } - - const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocaleTag}`; - try { - const resolvedPath = projectResolve(potentialPath); - if (resolvedPath) { - return { - path: resolvedPath, - warning: exact - ? undefined - : `Locale data for '${rawLocaleTag}' cannot be found. Using locale data for '${partialLocaleTag}'.`, - }; - } - } catch {} - - // Remove the last subtag and try again with a less specific locale. - const parts = partialLocaleTag.split('-'); - partialLocaleTag = parts.slice(0, -1).join('-'); - exact = false; - } - - return { - warning: `Locale data for '${rawLocaleTag}' cannot be found. No locale data will be included for this locale.`, - }; -} - -/** - * Loads the Angular global locale data script for a specified locale tag. - * - * @param rawLocaleTag The raw locale identifier (e.g. "fr-CA", "de", "en-US"). - * @param projectRoot Optional project root for module resolution. - * @returns A promise resolving to the loaded locale data script code and any diagnostic warnings. - */ -export function loadLocaleData( - rawLocaleTag: string, - projectRoot?: string, -): Promise { - let cached = localeDataCache.get(rawLocaleTag); - if (!cached) { - cached = (async () => { - const projectResolve = createProjectResolver(projectRoot ?? process.cwd()); - const resolution = resolveLocaleDataPath(rawLocaleTag, (potentialPath) => { - try { - return projectResolve(potentialPath); - } catch { - return undefined; - } - }); - - if (resolution.error) { - return { error: resolution.error }; - } - - if (resolution.path) { - try { - const code = await readFile(resolution.path, 'utf8'); - - return { code, warning: resolution.warning }; - } catch (e) { - return { error: `Failed to read locale data file: ${(e as Error).message}` }; - } - } - - return { warning: resolution.warning }; - })(); - - localeDataCache.set(rawLocaleTag, cached); - } - - return cached; -} +import { + LOCALE_DATA_NAMESPACE, + type LoadedLocaleData, + type LocaleDataResolution, + loadLocaleData, + resolveLocaleDataPath, +} from '../i18n/locale-data'; + +export { + LOCALE_DATA_NAMESPACE, + LOCALE_DATA_BASE_MODULE, + type LoadedLocaleData, + type LocaleDataResolution, + loadLocaleData, + resolveLocaleDataPath, +} from '../i18n/locale-data'; /** * Creates an esbuild plugin that resolves Angular locale data files from `@angular/common`. diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts index 9761ff9aff64..c5dcee90ccf9 100644 --- a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts @@ -31,9 +31,9 @@ import type { Loader, OnLoadResult, PartialMessage } from 'esbuild'; import { readFile, stat } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; +import type { Cache as PersistentCacheStore } from '../../utils/cache'; import { mapConcurrent, runConcurrent } from '../../utils/concurrency'; import { calculateHash, createContentHash } from '../../utils/hash'; -import type { Cache as PersistentCacheStore } from './cache'; import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache'; /** diff --git a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts index f8ecc0611818..b41add55031d 100644 --- a/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts +++ b/packages/angular/build/src/tools/esbuild/persistent-load-result-cache_spec.ts @@ -10,8 +10,8 @@ import type { OnLoadResult } from 'esbuild'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import type { Cache as PersistentCacheStore } from '../../utils/cache'; import { initializeHash } from '../../utils/hash'; -import type { Cache as PersistentCacheStore } from './cache'; import { type CachedLoadResultEntry, PersistentLoadResultCache, diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index cce85f61b9a2..b519a408bfb8 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -10,8 +10,8 @@ import type { OnLoadResult, PartialMessage, PartialNote, ResolveResult } from 'e import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { CanonicalizeContext, CompileResult, Exception, Syntax } from 'sass-embedded'; +import { MemoryCache } from '../../../utils/cache'; import type { SassCompiler } from '../../sass/sass-service'; -import { MemoryCache } from '../cache'; import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory'; let sassService: SassCompiler | undefined; diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts similarity index 99% rename from packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts rename to packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts index ac75ab246d8d..6a7e84b308b7 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts @@ -8,14 +8,12 @@ import remapping, { type DecodedSourceMap, type SourceMapInput } from '@ampproject/remapping'; import type { ɵParsedTranslation } from '@angular/localize'; -import type { Node } from '@oxc-project/types'; import { MagicString } from 'magic-string'; -import assert from 'node:assert'; import { deserialize } from 'node:v8'; import { parseSync } from 'oxc-parser'; import { traversePostOrder } from '../oxc/traversal'; -import { loadLocaleData } from './i18n-locale-plugin'; import { createSharedTranslationProxy } from './i18n-translation-reader'; +import { loadLocaleData } from './locale-data'; /** * The options passed to the inliner for each code request diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/i18n/i18n-inliner.ts similarity index 99% rename from packages/angular/build/src/tools/esbuild/i18n-inliner.ts rename to packages/angular/build/src/tools/i18n/i18n-inliner.ts index e5ec68f48e26..581a2477adc5 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner.ts @@ -11,10 +11,18 @@ import assert from 'node:assert'; import { createRequire } from 'node:module'; import { extname, join } from 'node:path'; import { serialize } from 'node:v8'; +import { + type Cache, + type PersistentCacheStore, + createPersistentCacheStore, +} from '../../utils/cache'; import { calculateHash, createContentHash, initializeHash } from '../../utils/hash'; import { WorkerPool } from '../../utils/worker-pool'; -import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files'; -import { type Cache, type PersistentCacheStore, createPersistentCacheStore } from './cache'; +import { + type BuildOutputFile, + BuildOutputFileType, + createOutputFile, +} from '../esbuild/bundler-files'; import type { InlineCodeRequest, InlineCodeResult, diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts similarity index 99% rename from packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts rename to packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts index 8f73c018e4bd..bbc827b94c7f 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts @@ -11,10 +11,14 @@ import { transform } from 'esbuild'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { createPersistentCacheStore } from '../../utils/cache'; import { initializeHash } from '../../utils/hash'; import { WorkerPool } from '../../utils/worker-pool'; -import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files'; -import { createPersistentCacheStore } from './cache'; +import { + type BuildOutputFile, + BuildOutputFileType, + createOutputFile, +} from '../esbuild/bundler-files'; import { I18nInliner, type I18nInlinerOptions } from './i18n-inliner'; /** diff --git a/packages/angular/build/src/tools/esbuild/i18n-translation-encoder.ts b/packages/angular/build/src/tools/i18n/i18n-translation-encoder.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/i18n-translation-encoder.ts rename to packages/angular/build/src/tools/i18n/i18n-translation-encoder.ts diff --git a/packages/angular/build/src/tools/esbuild/i18n-translation-encoder_spec.ts b/packages/angular/build/src/tools/i18n/i18n-translation-encoder_spec.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/i18n-translation-encoder_spec.ts rename to packages/angular/build/src/tools/i18n/i18n-translation-encoder_spec.ts diff --git a/packages/angular/build/src/tools/esbuild/i18n-translation-reader.ts b/packages/angular/build/src/tools/i18n/i18n-translation-reader.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/i18n-translation-reader.ts rename to packages/angular/build/src/tools/i18n/i18n-translation-reader.ts diff --git a/packages/angular/build/src/tools/i18n/index.ts b/packages/angular/build/src/tools/i18n/index.ts new file mode 100644 index 000000000000..a2f8cdd2d8f3 --- /dev/null +++ b/packages/angular/build/src/tools/i18n/index.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './i18n-inliner'; +export * from './i18n-translation-encoder'; +export * from './i18n-translation-reader'; +export * from './locale-data'; diff --git a/packages/angular/build/src/tools/i18n/locale-data.ts b/packages/angular/build/src/tools/i18n/locale-data.ts new file mode 100644 index 000000000000..0418830b69a8 --- /dev/null +++ b/packages/angular/build/src/tools/i18n/locale-data.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { readFile } from 'node:fs/promises'; +import { createProjectResolver } from '../../utils/resolve-project'; + +/** + * The internal namespace used by generated locale import statements and Angular locale data plugin. + */ +export const LOCALE_DATA_NAMESPACE = 'angular:locale/data'; + +/** + * The base module location used to search for locale specific data. + */ +export const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global'; + +/** + * Result of resolving locale data for a given locale tag. + */ +export interface LocaleDataResolution { + path?: string; + warning?: string; + error?: string; +} + +/** + * Result of loading locale data for a given locale tag. + */ +export interface LoadedLocaleData { + code?: string; + warning?: string; + error?: string; +} + +const localeDataCache = new Map>(); + +/** + * Resolves the path to the Angular locale data file for a given locale tag. + * + * @param rawLocaleTag The raw locale identifier (e.g. "fr-CA", "de", "en-US"). + * @param projectResolve A function that attempts to resolve a path string to an absolute file path. + * @returns Resolution result with file path, or warning/error diagnostics if applicable. + */ +export function resolveLocaleDataPath( + rawLocaleTag: string, + projectResolve: (potentialPath: string) => string | undefined, +): LocaleDataResolution { + let partialLocaleTag: string; + try { + const locale = new Intl.Locale(rawLocaleTag); + partialLocaleTag = locale.baseName; + } catch { + return { + error: `Invalid or unsupported locale provided in configuration: "${rawLocaleTag}"`, + }; + } + + let exact = true; + while (partialLocaleTag) { + // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. + if (partialLocaleTag === 'en' || partialLocaleTag === 'en-US') { + return {}; + } + + const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocaleTag}`; + try { + const resolvedPath = projectResolve(potentialPath); + if (resolvedPath) { + return { + path: resolvedPath, + warning: exact + ? undefined + : `Locale data for '${rawLocaleTag}' cannot be found. Using locale data for '${partialLocaleTag}'.`, + }; + } + } catch {} + + // Remove the last subtag and try again with a less specific locale. + const parts = partialLocaleTag.split('-'); + partialLocaleTag = parts.slice(0, -1).join('-'); + exact = false; + } + + return { + warning: `Locale data for '${rawLocaleTag}' cannot be found. No locale data will be included for this locale.`, + }; +} + +/** + * Loads the Angular global locale data script for a specified locale tag. + * + * @param rawLocaleTag The raw locale identifier (e.g. "fr-CA", "de", "en-US"). + * @param projectRoot Optional project root for module resolution. + * @returns A promise resolving to the loaded locale data script code and any diagnostic warnings. + */ +export function loadLocaleData( + rawLocaleTag: string, + projectRoot?: string, +): Promise { + let cached = localeDataCache.get(rawLocaleTag); + if (!cached) { + cached = (async () => { + const projectResolve = createProjectResolver(projectRoot ?? process.cwd()); + const resolution = resolveLocaleDataPath(rawLocaleTag, (potentialPath) => { + try { + return projectResolve(potentialPath); + } catch { + return undefined; + } + }); + + if (resolution.error) { + return { error: resolution.error }; + } + + if (resolution.path) { + try { + const code = await readFile(resolution.path, 'utf8'); + + return { code, warning: resolution.warning }; + } catch (e) { + return { error: `Failed to read locale data file: ${(e as Error).message}` }; + } + } + + return { warning: resolution.warning }; + })(); + + localeDataCache.set(rawLocaleTag, cached); + } + + return cached; +} diff --git a/packages/angular/build/src/tools/javascript-transformer/index.ts b/packages/angular/build/src/tools/javascript-transformer/index.ts new file mode 100644 index 000000000000..399eea87e644 --- /dev/null +++ b/packages/angular/build/src/tools/javascript-transformer/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './javascript-transformer'; diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/javascript-transformer-worker.ts rename to packages/angular/build/src/tools/javascript-transformer/javascript-transformer-worker.ts diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer.ts similarity index 99% rename from packages/angular/build/src/tools/esbuild/javascript-transformer.ts rename to packages/angular/build/src/tools/javascript-transformer/javascript-transformer.ts index 2917222a3277..56a7b4eb6ed9 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer.ts @@ -7,11 +7,11 @@ */ import { readFile } from 'node:fs/promises'; +import { Cache } from '../../utils/cache'; import { createContentHash } from '../../utils/hash'; import { IMPORT_EXEC_ARGV } from '../../utils/server-rendering/esm-in-memory-loader/utils'; import { removeSourceMappingURL } from '../../utils/source-map'; import { WorkerPool, WorkerPoolOptions } from '../../utils/worker-pool'; -import { Cache } from './cache'; const LINKER_DECLARATION_PREFIX = 'ɵɵngDeclare'; const LINKER_DECLARATION_PREFIX_BYTES = Buffer.from(LINKER_DECLARATION_PREFIX, 'utf-8'); diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts b/packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/javascript-transformer_spec.ts rename to packages/angular/build/src/tools/javascript-transformer/javascript-transformer_spec.ts diff --git a/packages/angular/build/src/tools/vite/utils.ts b/packages/angular/build/src/tools/vite/utils.ts index dc7094b8ffe4..fa833066f8ed 100644 --- a/packages/angular/build/src/tools/vite/utils.ts +++ b/packages/angular/build/src/tools/vite/utils.ts @@ -13,7 +13,7 @@ import type { DepOptimizationConfig } from 'vite' with { 'resolution-mode': 'import', }; import type { ExternalResultMetadata } from '../esbuild/bundler-execution-result'; -import { JavaScriptTransformer } from '../esbuild/javascript-transformer'; +import { JavaScriptTransformer } from '../javascript-transformer'; export type AngularMemoryOutputFiles = Map< string, diff --git a/packages/angular/build/src/tools/esbuild/cache.ts b/packages/angular/build/src/utils/cache/cache.ts similarity index 98% rename from packages/angular/build/src/tools/esbuild/cache.ts rename to packages/angular/build/src/utils/cache/cache.ts index 40c6787abee3..1773caca71ea 100644 --- a/packages/angular/build/src/tools/esbuild/cache.ts +++ b/packages/angular/build/src/utils/cache/cache.ts @@ -11,8 +11,8 @@ * Provides infrastructure for common caching functionality within the build system. */ -import { persistentCacheStoreSetting } from '../../utils/environment-options'; -import { assertIsError } from '../../utils/error'; +import { persistentCacheStoreSetting } from '../environment-options'; +import { assertIsError } from '../error'; /** * A backing data store for one or more Cache instances. diff --git a/packages/angular/build/src/tools/esbuild/cache_spec.ts b/packages/angular/build/src/utils/cache/cache_spec.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/cache_spec.ts rename to packages/angular/build/src/utils/cache/cache_spec.ts diff --git a/packages/angular/build/src/utils/cache/index.ts b/packages/angular/build/src/utils/cache/index.ts new file mode 100644 index 000000000000..417d13326f50 --- /dev/null +++ b/packages/angular/build/src/utils/cache/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './cache'; diff --git a/packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts b/packages/angular/build/src/utils/cache/lmdb-cache-store.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts rename to packages/angular/build/src/utils/cache/lmdb-cache-store.ts diff --git a/packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts b/packages/angular/build/src/utils/cache/sqlite-cache-store.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts rename to packages/angular/build/src/utils/cache/sqlite-cache-store.ts diff --git a/packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts b/packages/angular/build/src/utils/cache/sqlite-cache-store_spec.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts rename to packages/angular/build/src/utils/cache/sqlite-cache-store_spec.ts diff --git a/packages/angular/build/src/tools/esbuild/profiling.ts b/packages/angular/build/src/utils/profiling.ts similarity index 97% rename from packages/angular/build/src/tools/esbuild/profiling.ts rename to packages/angular/build/src/utils/profiling.ts index 80fd26c32d0e..762a739bd318 100644 --- a/packages/angular/build/src/tools/esbuild/profiling.ts +++ b/packages/angular/build/src/utils/profiling.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { debugPerformance } from '../../utils/environment-options'; +import { debugPerformance } from './environment-options'; let cumulativeDurations: Map | undefined; diff --git a/packages/angular/build/src/tools/esbuild/watcher.ts b/packages/angular/build/src/utils/watcher.ts similarity index 99% rename from packages/angular/build/src/tools/esbuild/watcher.ts rename to packages/angular/build/src/utils/watcher.ts index e30909fa6813..be38151f7a48 100644 --- a/packages/angular/build/src/tools/esbuild/watcher.ts +++ b/packages/angular/build/src/utils/watcher.ts @@ -12,8 +12,8 @@ import { once } from 'node:events'; import * as fs from 'node:fs'; import * as path from 'node:path'; import picomatch from 'picomatch'; -import { shouldWatchRoot } from '../../utils/environment-options'; -import { toPosixPath } from '../../utils/path'; +import { shouldWatchRoot } from './environment-options'; +import { toPosixPath } from './path'; export class ChangedFiles { readonly added = new Set(); diff --git a/packages/angular/build/src/tools/esbuild/watcher_spec.ts b/packages/angular/build/src/utils/watcher_spec.ts similarity index 100% rename from packages/angular/build/src/tools/esbuild/watcher_spec.ts rename to packages/angular/build/src/utils/watcher_spec.ts diff --git a/scripts/benchmark.mts b/scripts/benchmark.mts index bf35b97808e3..0237f5101bc4 100644 --- a/scripts/benchmark.mts +++ b/scripts/benchmark.mts @@ -11,8 +11,8 @@ import path from 'node:path'; import { type BenchmarkCliOptions, runI18nBenchmarks } from './benchmarks/i18n/index.mts'; function checkBuildStatus(logger: Console): boolean { - const distFile = 'dist/@angular/build/src/tools/esbuild/i18n-inliner.js'; - const srcFile = 'packages/angular/build/src/tools/esbuild/i18n-inliner.ts'; + const distFile = 'dist/@angular/build/src/tools/i18n/i18n-inliner.js'; + const srcFile = 'packages/angular/build/src/tools/i18n/i18n-inliner.ts'; if (!fs.existsSync(distFile)) { logger.error( diff --git a/scripts/benchmarks/i18n/fixtures.mts b/scripts/benchmarks/i18n/fixtures.mts index 3d53c3dbff5b..6a926d07a29e 100644 --- a/scripts/benchmarks/i18n/fixtures.mts +++ b/scripts/benchmarks/i18n/fixtures.mts @@ -14,7 +14,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import type { BuildOutputFile } from '../../../packages/angular/build/src/tools/esbuild/bundler-files.js'; -import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'; +import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/i18n/i18n-inliner.js'; // Setup module paths to resolve dependencies from packages/angular/build const requireFromBuild = createRequire( diff --git a/scripts/benchmarks/i18n/scenarios.mts b/scripts/benchmarks/i18n/scenarios.mts index c00f34ded8a2..d1bdcb8cfa40 100644 --- a/scripts/benchmarks/i18n/scenarios.mts +++ b/scripts/benchmarks/i18n/scenarios.mts @@ -16,7 +16,7 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import type { BuildOutputFile } from '../../../packages/angular/build/src/tools/esbuild/bundler-files.js'; -import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'; +import type { LocaleInlineOptions } from '../../../packages/angular/build/src/tools/i18n/i18n-inliner.js'; import { generateSyntheticBundle, generateTranslations, initializeFixtures } from './fixtures.mts'; import type { BenchmarkScenario } from './harness.mts'; @@ -25,8 +25,8 @@ const requireFromBuild = createRequire( ); const { I18nInliner } = requireFromBuild( - '../../../dist/@angular/build/src/tools/esbuild/i18n-inliner.js', -) as typeof import('../../../packages/angular/build/src/tools/esbuild/i18n-inliner.js'); + '../../../dist/@angular/build/src/tools/i18n/i18n-inliner.js', +) as typeof import('../../../packages/angular/build/src/tools/i18n/i18n-inliner.js'); export interface ScenarioFactoryOptions { concurrency?: number;