Skip to content

Commit b88aaa2

Browse files
committed
fix(angular-rspack): speed up watch-mode rebuilds
Rebuilds transformed ALL Angular-emitted files through JavaScriptTransformer eagerly in buildAndAnalyze, even when only one file changed. Now transform lazily in the loader (esbuild's onLoad pattern) so only files Rspack actually requests are transformed. Also align component stylesheet bundler with Angular CLI by enabling incremental caching in watch mode, and move JavaScriptTransformer.close() from the per-build emit hook to the shutdown hook so worker threads persist across rebuilds.
1 parent ff50d97 commit b88aaa2

7 files changed

Lines changed: 81 additions & 27 deletions

File tree

packages/angular-rspack-compiler/src/compilation/build-and-analyze.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { AngularCompilation } from '../models';
44

55
const JS_TS_FILE_PATTERN = /\.[cm]?[jt]sx?$/;
66

7+
/**
8+
* Cache of raw emitted content from Angular compilation.
9+
* JS/TS transformation happens lazily in the Rspack loader, matching
10+
* esbuild's onLoad architecture where the bundler only transforms
11+
* files its dependency graph requires.
12+
*/
13+
export const rawEmitCache = new Map<string, string>();
14+
715
export async function buildAndAnalyze(
816
angularCompilation: AngularCompilation,
917
typescriptFileCache: Map<string, string | Uint8Array>,
@@ -15,24 +23,25 @@ export async function buildAndAnalyze(
1523
} of await angularCompilation.emitAffectedFiles()) {
1624
const normalizedFilename = normalize(filename.replace(/^[A-Z]:/, ''));
1725

18-
// Skip JavaScript transformation for non-JS/TS files (JSON, CSS, etc.)
19-
// Let Rspack handle these through its native module rules
26+
const text =
27+
typeof contents === 'string'
28+
? contents
29+
: Buffer.from(contents).toString();
30+
31+
// Non-JS/TS files (JSON, CSS, etc.) go directly to the cache.
2032
if (!JS_TS_FILE_PATTERN.test(normalizedFilename)) {
21-
const text =
22-
typeof contents === 'string'
23-
? contents
24-
: Buffer.from(contents).toString();
2533
typescriptFileCache.set(normalizedFilename, text);
2634
continue;
2735
}
2836

29-
await javascriptTransformer
30-
.transformData(normalizedFilename, contents, true, false)
31-
.then((contents) => {
32-
typescriptFileCache.set(
33-
normalizedFilename,
34-
Buffer.from(contents).toString()
35-
);
36-
});
37+
// Store raw emitted content for lazy transformation in the loader.
38+
// Invalidate the transformed cache when raw content changes so the
39+
// loader re-transforms on next access.
40+
const previousRaw = rawEmitCache.get(normalizedFilename);
41+
rawEmitCache.set(normalizedFilename, text);
42+
43+
if (previousRaw !== text) {
44+
typescriptFileCache.delete(normalizedFilename);
45+
}
3746
}
3847
}

packages/angular-rspack-compiler/src/compilation/setup-compilation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface SetupCompilationOptions {
2727
hasServer?: boolean;
2828
includePaths?: string[];
2929
sass?: Sass;
30+
watch?: boolean;
3031
}
3132

3233
export const DEFAULT_NG_COMPILER_OPTIONS: ts.CompilerOptions = {
@@ -113,7 +114,7 @@ export async function setupCompilation(
113114
tailwindConfiguration,
114115
},
115116
options.inlineStyleLanguage,
116-
false
117+
!!options.watch
117118
);
118119

119120
return {

packages/angular-rspack-compiler/src/compilation/setup-with-angular-compilation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export async function setupCompilationWithAngularCompilation(
2525
options.hasServer,
2626
false
2727
);
28+
29+
// Invalidate stylesheet bundler cache for modified files on rebuilds
30+
if (modifiedFiles) {
31+
componentStylesheetBundler.invalidate(modifiedFiles);
32+
}
33+
2834
modifiedFiles ??= new Set(rootNames);
2935

3036
const fileReplacements: Record<string, string> =

packages/angular-rspack/src/lib/models/augmented-compilation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const NG_RSPACK_SYMBOL_NAME = 'NG_RSPACK_BUILD';
1010
export type NG_RSPACK_COMPILATION_STATE = {
1111
javascriptTransformer: JavaScriptTransformer;
1212
typescriptFileCache: SourceFileCache['typeScriptFileCache'];
13+
rawEmitCache: Map<string, string>;
1314
i18n?: I18nOptions;
1415
};
1516
export type NgRspackCompilation = Compilation & {

packages/angular-rspack/src/lib/plugins/angular-rspack-plugin.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@angular/build/private';
77
import {
88
buildAndAnalyze,
9+
rawEmitCache,
910
DiagnosticModes,
1011
JavaScriptTransformer,
1112
SourceFileCache,
@@ -127,7 +128,8 @@ export class AngularRspackPlugin implements RspackPluginInstance {
127128
compiler.options.resolve.tsConfig,
128129
watchingModifiedFiles.size > 0
129130
? watchingModifiedFiles
130-
: undefined
131+
: undefined,
132+
true
131133
);
132134

133135
await buildAndAnalyze(
@@ -266,6 +268,13 @@ export class AngularRspackPlugin implements RspackPluginInstance {
266268
}
267269
}
268270

271+
callback();
272+
});
273+
274+
// Clean up worker threads when the compiler shuts down, not on every rebuild.
275+
// Angular CLI does the same — JavaScriptTransformer.close() is only called in
276+
// esbuild's onDispose, not after each incremental build.
277+
compiler.hooks.shutdown.tapAsync(PLUGIN_NAME, async (callback) => {
269278
await this.#javascriptTransformer.close();
270279
callback();
271280
});
@@ -358,6 +367,7 @@ export class AngularRspackPlugin implements RspackPluginInstance {
358367
javascriptTransformer: this
359368
.#javascriptTransformer as unknown as JavaScriptTransformer,
360369
typescriptFileCache: this.#sourceFileCache.typeScriptFileCache,
370+
rawEmitCache,
361371
i18n: this.#i18n,
362372
});
363373
});
@@ -410,7 +420,8 @@ export class AngularRspackPlugin implements RspackPluginInstance {
410420
private async setupCompilation(
411421
root: string,
412422
tsConfig: RspackOptionsNormalized['resolve']['tsConfig'],
413-
modifiedFiles?: Set<string>
423+
modifiedFiles?: Set<string>,
424+
watch = false
414425
) {
415426
const tsconfigPath = tsConfig
416427
? typeof tsConfig === 'string'
@@ -433,6 +444,7 @@ export class AngularRspackPlugin implements RspackPluginInstance {
433444
hasServer: this.#_options.hasServer,
434445
includePaths: this.#_options.stylePreprocessorOptions?.includePaths,
435446
sass: this.#_options.stylePreprocessorOptions?.sass,
447+
watch,
436448
},
437449
this.#sourceFileCache,
438450
this.#angularCompilation,

packages/angular-rspack/src/lib/plugins/loaders/angular-transform.loader.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ describe('angular-transform.loader', () => {
2323
const callback = vi.fn();
2424
const addDependency = vi.fn();
2525
const typescriptFileCache = new Map<string, string | Buffer>();
26+
const rawEmitCache = new Map<string, string>();
27+
const javascriptTransformer = {
28+
transformData: vi.fn(),
29+
};
2630
const _compilation = {
2731
[NG_RSPACK_SYMBOL_NAME]: () => ({
2832
typescriptFileCache,
33+
rawEmitCache,
34+
javascriptTransformer,
2935
}),
3036
} as unknown as NgRspackCompilation;
3137
const thisValue = {

packages/angular-rspack/src/lib/plugins/loaders/angular-transform.loader.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export default function loader(
2828
) {
2929
callback(null, content);
3030
} else {
31-
const { typescriptFileCache } = (this._compilation as NgRspackCompilation)[
32-
NG_RSPACK_SYMBOL_NAME
33-
]();
31+
const { typescriptFileCache, javascriptTransformer, rawEmitCache } = (
32+
this._compilation as NgRspackCompilation
33+
)[NG_RSPACK_SYMBOL_NAME]();
3434

3535
const request = this.resourcePath.replace(/^[A-Z]:/, '');
3636
const normalizedRequest = normalize(request);
@@ -48,13 +48,32 @@ export default function loader(
4848
this.addDependency(absoluteFileUrl);
4949
}
5050

51-
const contents = typescriptFileCache.get(normalizedRequest);
52-
if (contents === undefined) {
53-
callback(null, content);
54-
} else if (typeof contents === 'string') {
55-
callback(null, contents);
56-
} else {
57-
callback(null, Buffer.from(contents));
51+
// Check transformed cache first (fast path)
52+
const cached = typescriptFileCache.get(normalizedRequest);
53+
if (cached !== undefined) {
54+
if (typeof cached === 'string') {
55+
callback(null, cached);
56+
} else {
57+
callback(null, Buffer.from(cached));
58+
}
59+
return;
5860
}
61+
62+
// Check raw emit cache — transform lazily (like esbuild's onLoad)
63+
const raw = rawEmitCache.get(normalizedRequest);
64+
if (raw !== undefined) {
65+
javascriptTransformer
66+
.transformData(normalizedRequest, raw, true, false)
67+
.then((transformed: Uint8Array) => {
68+
const text = Buffer.from(transformed).toString();
69+
typescriptFileCache.set(normalizedRequest, text);
70+
callback(null, text);
71+
})
72+
.catch((err: Error) => callback(err));
73+
return;
74+
}
75+
76+
// No Angular compilation output — return original source
77+
callback(null, content);
5978
}
6079
}

0 commit comments

Comments
 (0)