Skip to content

Commit 385eb77

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular-devkit/build-angular): cache loading of component resources in JIT mode
The load result caching capabilities of the Angular compiler plugin used within the `application` and `browser-esbuild` builders is now used for both stylesheet and template component resources when building in JIT mode. This limits the amount of file system access required during a rebuild in JIT mode and also more accurately captures the full set of watched files. (cherry picked from commit 12f4433)
1 parent 7b8d6cd commit 385eb77

File tree

3 files changed

+107
-88
lines changed

3 files changed

+107
-88
lines changed

packages/angular_devkit/build_angular/src/builders/application/tests/behavior/rebuild-component_styles_spec.ts

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,60 +18,67 @@ export const BUILD_TIMEOUT = 30_000;
1818

1919
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
2020
describe('Behavior: "Rebuilds when component stylesheets change"', () => {
21-
it('updates component when imported sass changes', async () => {
22-
harness.useTarget('build', {
23-
...BASE_OPTIONS,
24-
watch: true,
25-
});
21+
for (const aot of [true, false]) {
22+
it(`updates component when imported sass changes with ${aot ? 'AOT' : 'JIT'}`, async () => {
23+
harness.useTarget('build', {
24+
...BASE_OPTIONS,
25+
watch: true,
26+
aot,
27+
});
28+
harness.useTarget('build', {
29+
...BASE_OPTIONS,
30+
watch: true,
31+
});
2632

27-
await harness.modifyFile('src/app/app.component.ts', (content) =>
28-
content.replace('app.component.css', 'app.component.scss'),
29-
);
30-
await harness.writeFile('src/app/app.component.scss', "@import './a';");
31-
await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }');
33+
await harness.modifyFile('src/app/app.component.ts', (content) =>
34+
content.replace('app.component.css', 'app.component.scss'),
35+
);
36+
await harness.writeFile('src/app/app.component.scss', "@import './a';");
37+
await harness.writeFile('src/app/a.scss', '$primary: aqua;\\nh1 { color: $primary; }');
3238

33-
const builderAbort = new AbortController();
34-
const buildCount = await harness
35-
.execute({ signal: builderAbort.signal })
36-
.pipe(
37-
timeout(30000),
38-
concatMap(async ({ result }, index) => {
39-
expect(result?.success).toBe(true);
39+
const builderAbort = new AbortController();
40+
const buildCount = await harness
41+
.execute({ signal: builderAbort.signal })
42+
.pipe(
43+
timeout(30000),
44+
concatMap(async ({ result }, index) => {
45+
expect(result?.success).toBe(true);
4046

41-
switch (index) {
42-
case 0:
43-
harness.expectFile('dist/browser/main.js').content.toContain('color: aqua');
44-
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
47+
switch (index) {
48+
case 0:
49+
harness.expectFile('dist/browser/main.js').content.toContain('color: aqua');
50+
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
4551

46-
await harness.writeFile(
47-
'src/app/a.scss',
48-
'$primary: blue;\\nh1 { color: $primary; }',
49-
);
50-
break;
51-
case 1:
52-
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
53-
harness.expectFile('dist/browser/main.js').content.toContain('color: blue');
52+
await harness.writeFile(
53+
'src/app/a.scss',
54+
'$primary: blue;\\nh1 { color: $primary; }',
55+
);
56+
break;
57+
case 1:
58+
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
59+
harness.expectFile('dist/browser/main.js').content.toContain('color: blue');
5460

55-
await harness.writeFile(
56-
'src/app/a.scss',
57-
'$primary: green;\\nh1 { color: $primary; }',
58-
);
59-
break;
60-
case 2:
61-
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
62-
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
63-
harness.expectFile('dist/browser/main.js').content.toContain('color: green');
61+
await harness.writeFile(
62+
'src/app/a.scss',
63+
'$primary: green;\\nh1 { color: $primary; }',
64+
);
65+
break;
66+
case 2:
67+
harness.expectFile('dist/browser/main.js').content.not.toContain('color: aqua');
68+
harness.expectFile('dist/browser/main.js').content.not.toContain('color: blue');
69+
harness.expectFile('dist/browser/main.js').content.toContain('color: green');
6470

65-
// Test complete - abort watch mode
66-
builderAbort.abort();
67-
break;
68-
}
69-
}),
70-
count(),
71-
)
72-
.toPromise();
71+
// Test complete - abort watch mode
72+
builderAbort.abort();
73+
break;
74+
}
75+
}),
76+
count(),
77+
)
78+
.toPromise();
7379

74-
expect(buildCount).toBe(3);
75-
});
80+
expect(buildCount).toBe(3);
81+
});
82+
}
7683
});
7784
});

packages/angular_devkit/build_angular/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ export function createCompilerPlugin(
409409
stylesheetBundler,
410410
additionalResults,
411411
styleOptions.inlineStyleLanguage,
412+
pluginOptions.loadResultCache,
412413
);
413414
}
414415

packages/angular_devkit/build_angular/src/tools/esbuild/angular/jit-plugin-callbacks.ts

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
import type { Metafile, OutputFile, PluginBuild } from 'esbuild';
1010
import { readFile } from 'node:fs/promises';
11-
import path from 'node:path';
11+
import { dirname, join, relative } from 'node:path';
12+
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
1213
import { ComponentStylesheetBundler } from './component-stylesheets';
1314
import {
1415
JIT_NAMESPACE_REGEXP,
@@ -34,7 +35,7 @@ async function loadEntry(
3435
skipRead?: boolean,
3536
): Promise<{ path: string; contents?: string }> {
3637
if (entry.startsWith('file:')) {
37-
const specifier = path.join(root, entry.slice(5));
38+
const specifier = join(root, entry.slice(5));
3839

3940
return {
4041
path: specifier,
@@ -44,7 +45,7 @@ async function loadEntry(
4445
const [importer, data] = entry.slice(7).split(';', 2);
4546

4647
return {
47-
path: path.join(root, importer),
48+
path: join(root, importer),
4849
contents: Buffer.from(data, 'base64').toString(),
4950
};
5051
} else {
@@ -66,6 +67,7 @@ export function setupJitPluginCallbacks(
6667
stylesheetBundler: ComponentStylesheetBundler,
6768
additionalResultFiles: Map<string, { outputFiles?: OutputFile[]; metafile?: Metafile }>,
6869
inlineStyleLanguage: string,
70+
loadCache?: LoadResultCache,
6971
): void {
7072
const root = build.initialOptions.absWorkingDir ?? '';
7173

@@ -84,12 +86,12 @@ export function setupJitPluginCallbacks(
8486
return {
8587
// Use a relative path to prevent fully resolved paths in the metafile (JSON stats file).
8688
// This is only necessary for custom namespaces. esbuild will handle the file namespace.
87-
path: 'file:' + path.relative(root, path.join(path.dirname(args.importer), specifier)),
89+
path: 'file:' + relative(root, join(dirname(args.importer), specifier)),
8890
namespace,
8991
};
9092
} else {
9193
// Inline data may need the importer to resolve imports/references within the content
92-
const importer = path.relative(root, args.importer);
94+
const importer = relative(root, args.importer);
9395

9496
return {
9597
path: `inline:${importer};${specifier}`,
@@ -99,45 +101,54 @@ export function setupJitPluginCallbacks(
99101
});
100102

101103
// Add a load callback to handle Component stylesheets (both inline and external)
102-
build.onLoad({ filter: /./, namespace: JIT_STYLE_NAMESPACE }, async (args) => {
103-
// skipRead is used here because the stylesheet bundling will read a file stylesheet
104-
// directly either via a preprocessor or esbuild itself.
105-
const entry = await loadEntry(args.path, root, true /* skipRead */);
104+
build.onLoad(
105+
{ filter: /./, namespace: JIT_STYLE_NAMESPACE },
106+
createCachedLoad(loadCache, async (args) => {
107+
// skipRead is used here because the stylesheet bundling will read a file stylesheet
108+
// directly either via a preprocessor or esbuild itself.
109+
const entry = await loadEntry(args.path, root, true /* skipRead */);
110+
111+
let stylesheetResult;
112+
113+
// Stylesheet contents only exist for internal stylesheets
114+
if (entry.contents === undefined) {
115+
stylesheetResult = await stylesheetBundler.bundleFile(entry.path);
116+
} else {
117+
stylesheetResult = await stylesheetBundler.bundleInline(
118+
entry.contents,
119+
entry.path,
120+
inlineStyleLanguage,
121+
);
122+
}
123+
124+
const { contents, resourceFiles, errors, warnings, metafile, referencedFiles } =
125+
stylesheetResult;
126+
127+
additionalResultFiles.set(entry.path, { outputFiles: resourceFiles, metafile });
106128

107-
let stylesheetResult;
108-
109-
// Stylesheet contents only exist for internal stylesheets
110-
if (entry.contents === undefined) {
111-
stylesheetResult = await stylesheetBundler.bundleFile(entry.path);
112-
} else {
113-
stylesheetResult = await stylesheetBundler.bundleInline(
114-
entry.contents,
115-
entry.path,
116-
inlineStyleLanguage,
117-
);
118-
}
119-
120-
const { contents, resourceFiles, errors, warnings, metafile } = stylesheetResult;
121-
122-
additionalResultFiles.set(entry.path, { outputFiles: resourceFiles, metafile });
123-
124-
return {
125-
errors,
126-
warnings,
127-
contents,
128-
loader: 'text',
129-
};
130-
});
129+
return {
130+
errors,
131+
warnings,
132+
contents,
133+
loader: 'text',
134+
watchFiles: referencedFiles && [...referencedFiles],
135+
};
136+
}),
137+
);
131138

132139
// Add a load callback to handle Component templates
133140
// NOTE: While this callback supports both inline and external templates, the transformer
134141
// currently only supports generating URIs for external templates.
135-
build.onLoad({ filter: /./, namespace: JIT_TEMPLATE_NAMESPACE }, async (args) => {
136-
const { contents } = await loadEntry(args.path, root);
142+
build.onLoad(
143+
{ filter: /./, namespace: JIT_TEMPLATE_NAMESPACE },
144+
createCachedLoad(loadCache, async (args) => {
145+
const { contents, path } = await loadEntry(args.path, root);
137146

138-
return {
139-
contents,
140-
loader: 'text',
141-
};
142-
});
147+
return {
148+
contents,
149+
loader: 'text',
150+
watchFiles: [path],
151+
};
152+
}),
153+
);
143154
}

0 commit comments

Comments
 (0)