Skip to content

Commit c257e6e

Browse files
committed
refactor(@angular-devkit/build-angular): support cache option with JavaScript transformer
The previously unused `reuseResults` option for the JavaScript transformer used by the `application` builder has been removed and replaced with an optional cache option. This option is currently unused by will allow the caching of JavaScript transformations including the Angular linker.
1 parent 8786daa commit c257e6e

File tree

3 files changed

+67
-38
lines changed

3 files changed

+67
-38
lines changed

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ export async function* serveWithVite(
117117
// have a negative effect unlike production where final output size is relevant.
118118
{ sourcemap: true, jit: true, thirdPartySourcemaps },
119119
1,
120-
true,
121120
);
122121

123122
// Extract output index from options

packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts

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

99
import { transformAsync } from '@babel/core';
10-
import { readFile } from 'node:fs/promises';
10+
import Piscina from 'piscina';
1111
import angularApplicationPreset, { requiresLinking } from '../../tools/babel/presets/application';
1212
import { loadEsmModule } from '../../utils/load-esm';
1313

1414
interface JavaScriptTransformRequest {
1515
filename: string;
16-
data: string;
16+
data: string | Uint8Array;
1717
sourcemap: boolean;
1818
thirdPartySourcemaps: boolean;
1919
advancedOptimizations: boolean;
@@ -22,24 +22,28 @@ interface JavaScriptTransformRequest {
2222
jit: boolean;
2323
}
2424

25-
export default async function transformJavaScript(
26-
request: JavaScriptTransformRequest,
27-
): Promise<Uint8Array> {
28-
request.data ??= await readFile(request.filename, 'utf-8');
29-
const transformedData = await transformWithBabel(request);
25+
const textDecoder = new TextDecoder();
26+
const textEncoder = new TextEncoder();
3027

31-
return Buffer.from(transformedData, 'utf-8');
28+
export default async function transformJavaScript(request: JavaScriptTransformRequest) {
29+
const { filename, data, ...options } = request;
30+
const textData = typeof data === 'string' ? data : textDecoder.decode(data);
31+
32+
const transformedData = await transformWithBabel(filename, textData, options);
33+
34+
// Transfer the data via `move` instead of cloning
35+
return Piscina.move(textEncoder.encode(transformedData));
3236
}
3337

3438
let linkerPluginCreator:
3539
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
3640
| undefined;
3741

38-
async function transformWithBabel({
39-
filename,
40-
data,
41-
...options
42-
}: JavaScriptTransformRequest): Promise<string> {
42+
async function transformWithBabel(
43+
filename: string,
44+
data: string,
45+
options: Omit<JavaScriptTransformRequest, 'filename' | 'data'>,
46+
): Promise<string> {
4347
const shouldLink = !options.skipLinker && (await requiresLinking(filename, data));
4448
const useInputSourcemap =
4549
options.sourcemap &&

packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import { createHash } from 'node:crypto';
10+
import { readFile } from 'node:fs/promises';
911
import Piscina from 'piscina';
12+
import { Cache } from './cache';
1013

1114
/**
1215
* Transformation options that should apply to all transformed files and data.
@@ -28,12 +31,12 @@ export interface JavaScriptTransformerOptions {
2831
export class JavaScriptTransformer {
2932
#workerPool: Piscina | undefined;
3033
#commonOptions: Required<JavaScriptTransformerOptions>;
31-
#pendingfileResults?: Map<string, Promise<Uint8Array>>;
34+
#fileCacheKeyBase: Uint8Array;
3235

3336
constructor(
3437
options: JavaScriptTransformerOptions,
3538
readonly maxThreads: number,
36-
reuseResults?: boolean,
39+
private readonly cache?: Cache<Uint8Array>,
3740
) {
3841
// Extract options to ensure only the named options are serialized and sent to the worker
3942
const {
@@ -48,11 +51,7 @@ export class JavaScriptTransformer {
4851
advancedOptimizations,
4952
jit,
5053
};
51-
52-
// Currently only tracks pending file transform results
53-
if (reuseResults) {
54-
this.#pendingfileResults = new Map();
55-
}
54+
this.#fileCacheKeyBase = Buffer.from(JSON.stringify(this.#commonOptions), 'utf-8');
5655
}
5756

5857
#ensureWorkerPool(): Piscina {
@@ -75,27 +74,56 @@ export class JavaScriptTransformer {
7574
* @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
7675
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
7776
*/
78-
transformFile(
77+
async transformFile(
7978
filename: string,
8079
skipLinker?: boolean,
8180
sideEffects?: boolean,
8281
): Promise<Uint8Array> {
83-
const pendingKey = `${!!skipLinker}--${filename}`;
84-
let pending = this.#pendingfileResults?.get(pendingKey);
85-
if (pending === undefined) {
86-
// Always send the request to a worker. Files are almost always from node modules which means
87-
// they may need linking. The data is also not yet available to perform most transformation checks.
88-
pending = this.#ensureWorkerPool().run({
89-
filename,
90-
skipLinker,
91-
sideEffects,
92-
...this.#commonOptions,
93-
});
94-
95-
this.#pendingfileResults?.set(pendingKey, pending);
82+
const data = await readFile(filename);
83+
84+
let result;
85+
let cacheKey;
86+
if (this.cache) {
87+
// Create a cache key from the file data and options that effect the output.
88+
// NOTE: If additional options are added, this may need to be updated.
89+
// TODO: Consider xxhash or similar instead of SHA256
90+
const hash = createHash('sha256');
91+
hash.update(`${!!skipLinker}--${!!sideEffects}`);
92+
hash.update(data);
93+
hash.update(this.#fileCacheKeyBase);
94+
cacheKey = hash.digest('hex');
95+
96+
try {
97+
result = await this.cache?.get(cacheKey);
98+
} catch {
99+
// Failure to get the value should not fail the transform
100+
}
96101
}
97102

98-
return pending;
103+
if (result === undefined) {
104+
// If there is no cache or no cached entry, process the file
105+
result = (await this.#ensureWorkerPool().run(
106+
{
107+
filename,
108+
data,
109+
skipLinker,
110+
sideEffects,
111+
...this.#commonOptions,
112+
},
113+
{ transferList: [data.buffer] },
114+
)) as Uint8Array;
115+
116+
// If there is a cache then store the result
117+
if (this.cache && cacheKey) {
118+
try {
119+
await this.cache.put(cacheKey, result);
120+
} catch {
121+
// Failure to store the value in the cache should not fail the transform
122+
}
123+
}
124+
}
125+
126+
return result;
99127
}
100128

101129
/**
@@ -140,8 +168,6 @@ export class JavaScriptTransformer {
140168
* @returns A void promise that resolves when closing is complete.
141169
*/
142170
async close(): Promise<void> {
143-
this.#pendingfileResults?.clear();
144-
145171
if (this.#workerPool) {
146172
try {
147173
await this.#workerPool.destroy();

0 commit comments

Comments
 (0)