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 225aa9eb1fa3..434ca958b4d1 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -10,8 +10,8 @@ import type { CompilerOptions } from '@angular/compiler-cli'; import type { PartialMessage } from 'esbuild'; import { createRequire } from 'node:module'; import { MessageChannel } from 'node:worker_threads'; -import Piscina from 'piscina'; import type { SourceFile } from 'typescript'; +import { WorkerPool } from '../../../utils/worker-pool'; import type { AngularHostOptions } from '../angular-host'; import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation'; @@ -24,7 +24,7 @@ import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-c * main Node.js CLI process memory settings with large application code sizes. */ export class ParallelCompilation extends AngularCompilation { - readonly #worker: Piscina; + readonly #worker: WorkerPool; constructor(readonly jit: boolean) { super(); @@ -32,15 +32,10 @@ export class ParallelCompilation extends AngularCompilation { // TODO: Convert to import.meta usage during ESM transition const localRequire = createRequire(__filename); - this.#worker = new Piscina({ - minThreads: 1, + this.#worker = new WorkerPool({ maxThreads: 1, idleTimeout: Infinity, - // Web containers do not support transferable objects with receiveOnMessagePort which - // is used when the Atomics based wait loop is enable. - useAtomics: !process.versions.webcontainer, filename: localRequire.resolve('./parallel-worker'), - recordTiming: false, }); } diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts index b1714efd459f..3075595aee87 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts @@ -7,7 +7,7 @@ */ import assert from 'node:assert'; -import Piscina from 'piscina'; +import { WorkerPool } from '../../utils/worker-pool'; import { BuildOutputFile, BuildOutputFileType } from './bundler-context'; import { createOutputFile } from './utils'; @@ -33,7 +33,7 @@ export interface I18nInlinerOptions { * localize function (`$localize`). */ export class I18nInliner { - #workerPool: Piscina; + #workerPool: WorkerPool; readonly #localizeFiles: ReadonlyMap; readonly #unmodifiedFiles: Array; readonly #fileToType = new Map(); @@ -88,7 +88,7 @@ export class I18nInliner { this.#localizeFiles = files; - this.#workerPool = new Piscina({ + this.#workerPool = new WorkerPool({ filename: require.resolve('./i18n-inliner-worker'), maxThreads, // Extract options to ensure only the named options are serialized and sent to the worker @@ -97,7 +97,6 @@ export class I18nInliner { shouldOptimize: options.shouldOptimize, files, }, - recordTiming: false, }); } diff --git a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts index ce4b0aa91356..f8445ccbef03 100644 --- a/packages/angular/build/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular/build/src/tools/esbuild/javascript-transformer.ts @@ -8,7 +8,7 @@ import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; -import Piscina from 'piscina'; +import { WorkerPool } from '../../utils/worker-pool'; import { Cache } from './cache'; /** @@ -29,7 +29,7 @@ export interface JavaScriptTransformerOptions { * and advanced optimizations. */ export class JavaScriptTransformer { - #workerPool: Piscina | undefined; + #workerPool: WorkerPool | undefined; #commonOptions: Required; #fileCacheKeyBase: Uint8Array; @@ -54,14 +54,10 @@ export class JavaScriptTransformer { this.#fileCacheKeyBase = Buffer.from(JSON.stringify(this.#commonOptions), 'utf-8'); } - #ensureWorkerPool(): Piscina { - this.#workerPool ??= new Piscina({ + #ensureWorkerPool(): WorkerPool { + this.#workerPool ??= new WorkerPool({ filename: require.resolve('./javascript-transformer-worker'), - minThreads: 1, maxThreads: this.maxThreads, - // Shutdown idle threads after 1 second of inactivity - idleTimeout: 1000, - recordTiming: false, }); return this.#workerPool; diff --git a/packages/angular/build/src/tools/sass/sass-service.ts b/packages/angular/build/src/tools/sass/sass-service.ts index 3e54e0ec1520..2df6f85f7a52 100644 --- a/packages/angular/build/src/tools/sass/sass-service.ts +++ b/packages/angular/build/src/tools/sass/sass-service.ts @@ -9,7 +9,6 @@ import assert from 'node:assert'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { MessageChannel } from 'node:worker_threads'; -import { Piscina } from 'piscina'; import type { CanonicalizeContext, CompileResult, @@ -22,6 +21,7 @@ import type { StringOptions, } from 'sass'; import { maxWorkers } from '../../utils/environment-options'; +import { WorkerPool } from '../../utils/worker-pool'; // Polyfill Symbol.dispose if not present // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -84,24 +84,17 @@ interface RenderResponseMessage { * the worker which can be up to two times faster than the asynchronous variant. */ export class SassWorkerImplementation { - #workerPool: Piscina | undefined; + #workerPool: WorkerPool | undefined; constructor( private readonly rebase = false, readonly maxThreads = MAX_RENDER_WORKERS, ) {} - #ensureWorkerPool(): Piscina { - this.#workerPool ??= new Piscina({ + #ensureWorkerPool(): WorkerPool { + this.#workerPool ??= new WorkerPool({ filename: require.resolve('./worker'), - minThreads: 1, maxThreads: this.maxThreads, - // Web containers do not support transferable objects with receiveOnMessagePort which - // is used when the Atomics based wait loop is enable. - useAtomics: !process.versions.webcontainer, - // Shutdown idle threads after 1 second of inactivity - idleTimeout: 1000, - recordTiming: false, }); return this.#workerPool; diff --git a/packages/angular/build/src/typings.d.ts b/packages/angular/build/src/typings.d.ts index c784b3c4220a..7eaadcafc536 100644 --- a/packages/angular/build/src/typings.d.ts +++ b/packages/angular/build/src/typings.d.ts @@ -17,3 +17,10 @@ declare module 'esbuild' { export * from 'esbuild-wasm'; } + +/** + * Augment the Node.js module builtin types to support the v22.8+ compile cache functions + */ +declare module 'node:module' { + function getCompileCacheDir(): string | undefined; +} diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index 1b53fd9ca6d9..55fec73b5210 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -9,10 +9,10 @@ import { readFile } from 'node:fs/promises'; import { extname, join, posix } from 'node:path'; import { pathToFileURL } from 'node:url'; -import Piscina from 'piscina'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result'; import { urlJoin } from '../url'; +import { WorkerPool } from '../worker-pool'; import type { RenderWorkerData } from './render-worker'; import type { RoutersExtractorWorkerResult, @@ -188,7 +188,7 @@ async function renderPages( workerExecArgv.push('--enable-source-maps'); } - const renderWorker = new Piscina({ + const renderWorker = new WorkerPool({ filename: require.resolve('./render-worker'), maxThreads: Math.min(allRoutes.size, maxThreads), workerData: { @@ -197,7 +197,6 @@ async function renderPages( assetFiles: assetFilesForWorker, } as RenderWorkerData, execArgv: workerExecArgv, - recordTiming: false, }); try { @@ -286,7 +285,7 @@ async function getAllRoutes( workerExecArgv.push('--enable-source-maps'); } - const renderWorker = new Piscina({ + const renderWorker = new WorkerPool({ filename: require.resolve('./routes-extractor-worker'), maxThreads: 1, workerData: { @@ -295,7 +294,6 @@ async function getAllRoutes( assetFiles: assetFilesForWorker, } as RoutesExtractorWorkerData, execArgv: workerExecArgv, - recordTiming: false, }); const errors: string[] = []; diff --git a/packages/angular/build/src/utils/worker-pool.ts b/packages/angular/build/src/utils/worker-pool.ts new file mode 100644 index 000000000000..ca35f7edb46b --- /dev/null +++ b/packages/angular/build/src/utils/worker-pool.ts @@ -0,0 +1,44 @@ +/** + * @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 { getCompileCacheDir } from 'node:module'; +import { Piscina } from 'piscina'; + +export type WorkerPoolOptions = ConstructorParameters[0]; + +export class WorkerPool extends Piscina { + constructor(options: WorkerPoolOptions) { + const piscinaOptions: WorkerPoolOptions = { + minThreads: 1, + idleTimeout: 1000, + // Web containers do not support transferable objects with receiveOnMessagePort which + // is used when the Atomics based wait loop is enable. + useAtomics: !process.versions.webcontainer, + recordTiming: false, + ...options, + }; + + // Enable compile code caching if enabled for the main process (only exists on Node.js v22.8+). + // Skip if running inside Bazel via a RUNFILES environment variable check. The cache does not work + // well with Bazel's hermeticity requirements. + const compileCacheDirectory = process.env['RUNFILES'] ? undefined : getCompileCacheDir?.(); + if (compileCacheDirectory) { + if (typeof piscinaOptions.env === 'object') { + piscinaOptions.env['NODE_COMPILE_CACHE'] = compileCacheDirectory; + } else { + // Default behavior of `env` option is to copy current process values + piscinaOptions.env = { + ...process.env, + 'NODE_COMPILE_CACHE': compileCacheDirectory, + }; + } + } + + super(piscinaOptions); + } +} diff --git a/packages/angular/cli/bin/bootstrap.js b/packages/angular/cli/bin/bootstrap.js index 96b978296dcc..1282f906aef2 100644 --- a/packages/angular/cli/bin/bootstrap.js +++ b/packages/angular/cli/bin/bootstrap.js @@ -18,4 +18,16 @@ * range. */ -import('../lib/init.js'); +// Enable on-disk code caching if available (Node.js 22.8+) +// Skip if running inside Bazel via a RUNFILES environment variable check. The cache does not work +// well with Bazel's hermeticity requirements. +if (!process.env['RUNFILES']) { + try { + const { enableCompileCache } = require('node:module'); + + enableCompileCache?.(); + } catch {} +} + +// Initialize the Angular CLI +void import('../lib/init.js');