diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts index 3c0b045709..d68c2fbd7f 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts @@ -117,7 +117,6 @@ export async function* serveWithVite( // have a negative effect unlike production where final output size is relevant. { sourcemap: true, jit: true, thirdPartySourcemaps }, 1, - true, ); // Extract output index from options diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts index 27e7244774..513b7c40b4 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts @@ -7,13 +7,13 @@ */ import { transformAsync } from '@babel/core'; -import { readFile } from 'node:fs/promises'; +import Piscina from 'piscina'; import angularApplicationPreset, { requiresLinking } from '../../tools/babel/presets/application'; import { loadEsmModule } from '../../utils/load-esm'; interface JavaScriptTransformRequest { filename: string; - data: string; + data: string | Uint8Array; sourcemap: boolean; thirdPartySourcemaps: boolean; advancedOptimizations: boolean; @@ -22,24 +22,28 @@ interface JavaScriptTransformRequest { jit: boolean; } -export default async function transformJavaScript( - request: JavaScriptTransformRequest, -): Promise { - request.data ??= await readFile(request.filename, 'utf-8'); - const transformedData = await transformWithBabel(request); +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); - return Buffer.from(transformedData, 'utf-8'); +export default async function transformJavaScript(request: JavaScriptTransformRequest) { + const { filename, data, ...options } = request; + const textData = typeof data === 'string' ? data : textDecoder.decode(data); + + const transformedData = await transformWithBabel(filename, textData, options); + + // Transfer the data via `move` instead of cloning + return Piscina.move(textEncoder.encode(transformedData)); } let linkerPluginCreator: | typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | undefined; -async function transformWithBabel({ - filename, - data, - ...options -}: JavaScriptTransformRequest): Promise { +async function transformWithBabel( + filename: string, + data: string, + options: Omit, +): Promise { const shouldLink = !options.skipLinker && (await requiresLinking(filename, data)); const useInputSourcemap = options.sourcemap && diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts index 8f686acb5d..3ef95dc794 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts @@ -6,7 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; import Piscina from 'piscina'; +import { Cache } from './cache'; /** * Transformation options that should apply to all transformed files and data. @@ -28,12 +31,12 @@ export interface JavaScriptTransformerOptions { export class JavaScriptTransformer { #workerPool: Piscina | undefined; #commonOptions: Required; - #pendingfileResults?: Map>; + #fileCacheKeyBase: Uint8Array; constructor( options: JavaScriptTransformerOptions, readonly maxThreads: number, - reuseResults?: boolean, + private readonly cache?: Cache, ) { // Extract options to ensure only the named options are serialized and sent to the worker const { @@ -48,11 +51,7 @@ export class JavaScriptTransformer { advancedOptimizations, jit, }; - - // Currently only tracks pending file transform results - if (reuseResults) { - this.#pendingfileResults = new Map(); - } + this.#fileCacheKeyBase = Buffer.from(JSON.stringify(this.#commonOptions), 'utf-8'); } #ensureWorkerPool(): Piscina { @@ -75,27 +74,56 @@ export class JavaScriptTransformer { * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped. * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result. */ - transformFile( + async transformFile( filename: string, skipLinker?: boolean, sideEffects?: boolean, ): Promise { - const pendingKey = `${!!skipLinker}--${filename}`; - let pending = this.#pendingfileResults?.get(pendingKey); - if (pending === undefined) { - // Always send the request to a worker. Files are almost always from node modules which means - // they may need linking. The data is also not yet available to perform most transformation checks. - pending = this.#ensureWorkerPool().run({ - filename, - skipLinker, - sideEffects, - ...this.#commonOptions, - }); + const data = await readFile(filename); - this.#pendingfileResults?.set(pendingKey, pending); + let result; + let cacheKey; + if (this.cache) { + // Create a cache key from the file data and options that effect the output. + // NOTE: If additional options are added, this may need to be updated. + // TODO: Consider xxhash or similar instead of SHA256 + const hash = createHash('sha256'); + hash.update(`${!!skipLinker}--${!!sideEffects}`); + hash.update(data); + hash.update(this.#fileCacheKeyBase); + cacheKey = hash.digest('hex'); + + try { + result = await this.cache?.get(cacheKey); + } catch { + // Failure to get the value should not fail the transform + } } - return pending; + if (result === undefined) { + // If there is no cache or no cached entry, process the file + result = (await this.#ensureWorkerPool().run( + { + filename, + data, + skipLinker, + sideEffects, + ...this.#commonOptions, + }, + { transferList: [data.buffer] }, + )) as Uint8Array; + + // If there is a cache then store the result + if (this.cache && cacheKey) { + try { + await this.cache.put(cacheKey, result); + } catch { + // Failure to store the value in the cache should not fail the transform + } + } + } + + return result; } /** @@ -140,8 +168,6 @@ export class JavaScriptTransformer { * @returns A void promise that resolves when closing is complete. */ async close(): Promise { - this.#pendingfileResults?.clear(); - if (this.#workerPool) { try { await this.#workerPool.destroy();