Skip to content

Commit ffea33f

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): use helper to setup esbuild plugin load caching
Within the esbuild-based browser application builder, a helper function has been introduced to streamline the use of the load result cache within the internal plugins. This removes repeat code that would otherwise be needed. The ability to use a load result cache with the global script processing has also been added but has not yet been enabled.
1 parent 4c82bb8 commit ffea33f

File tree

4 files changed

+101
-70
lines changed

4 files changed

+101
-70
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/global-scripts.ts

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import assert from 'node:assert';
1212
import { readFile } from 'node:fs/promises';
1313
import path from 'node:path';
1414
import { assertIsError } from '../../utils/error';
15-
import { NormalizedBrowserOptions } from './options';
15+
import { LoadResultCache, createCachedLoad } from './load-result-cache';
16+
import type { NormalizedBrowserOptions } from './options';
1617
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
1718

1819
/**
@@ -24,6 +25,7 @@ import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
2425
export function createGlobalScriptsBundleOptions(
2526
options: NormalizedBrowserOptions,
2627
initial: boolean,
28+
loadCache?: LoadResultCache,
2729
): BuildOptions | undefined {
2830
const {
2931
globalScripts,
@@ -91,51 +93,60 @@ export function createGlobalScriptsBundleOptions(
9193
external: true,
9294
};
9395
});
94-
build.onLoad({ filter: /./, namespace }, async (args) => {
95-
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))?.files;
96-
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
96+
build.onLoad(
97+
{ filter: /./, namespace },
98+
createCachedLoad(loadCache, async (args) => {
99+
const files = globalScripts.find(
100+
({ name }) => name === args.path.slice(0, -3),
101+
)?.files;
102+
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
97103

98-
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
99-
const bundleContent = new Bundle();
100-
for (const filename of files) {
101-
let fileContent;
102-
try {
103-
// Attempt to read as a relative path from the workspace root
104-
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
105-
} catch (e) {
106-
assertIsError(e);
107-
if (e.code !== 'ENOENT') {
108-
throw e;
109-
}
104+
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
105+
const bundleContent = new Bundle();
106+
const watchFiles = [];
107+
for (const filename of files) {
108+
let fileContent;
109+
try {
110+
// Attempt to read as a relative path from the workspace root
111+
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
112+
watchFiles.push(filename);
113+
} catch (e) {
114+
assertIsError(e);
115+
if (e.code !== 'ENOENT') {
116+
throw e;
117+
}
118+
119+
// If not found attempt to resolve as a module specifier
120+
const resolveResult = await build.resolve(filename, {
121+
kind: 'entry-point',
122+
resolveDir: workspaceRoot,
123+
});
110124

111-
// If not found attempt to resolve as a module specifier
112-
const resolveResult = await build.resolve(filename, {
113-
kind: 'entry-point',
114-
resolveDir: workspaceRoot,
115-
});
125+
if (resolveResult.errors.length) {
126+
// Remove resolution failure notes about marking as external since it doesn't apply
127+
// to global scripts.
128+
resolveResult.errors.forEach((error) => (error.notes = []));
116129

117-
if (resolveResult.errors.length) {
118-
// Remove resolution failure notes about marking as external since it doesn't apply
119-
// to global scripts.
120-
resolveResult.errors.forEach((error) => (error.notes = []));
130+
return {
131+
errors: resolveResult.errors,
132+
warnings: resolveResult.warnings,
133+
};
134+
}
121135

122-
return {
123-
errors: resolveResult.errors,
124-
warnings: resolveResult.warnings,
125-
};
136+
watchFiles.push(path.relative(resolveResult.path, workspaceRoot));
137+
fileContent = await readFile(resolveResult.path, 'utf-8');
126138
}
127139

128-
fileContent = await readFile(resolveResult.path, 'utf-8');
140+
bundleContent.addSource(new MagicString(fileContent, { filename }));
129141
}
130142

131-
bundleContent.addSource(new MagicString(fileContent, { filename }));
132-
}
133-
134-
return {
135-
contents: bundleContent.toString(),
136-
loader: 'js',
137-
};
138-
});
143+
return {
144+
contents: bundleContent.toString(),
145+
loader: 'js',
146+
watchFiles,
147+
};
148+
}),
149+
);
139150
},
140151
},
141152
],

packages/angular_devkit/build_angular/src/builders/browser-esbuild/load-result-cache.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,38 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import type { OnLoadResult } from 'esbuild';
9+
import type { OnLoadResult, PluginBuild } from 'esbuild';
1010

1111
export interface LoadResultCache {
1212
get(path: string): OnLoadResult | undefined;
1313
put(path: string, result: OnLoadResult): Promise<void>;
1414
}
1515

16+
export function createCachedLoad(
17+
cache: LoadResultCache | undefined,
18+
callback: Parameters<PluginBuild['onLoad']>[1],
19+
): Parameters<PluginBuild['onLoad']>[1] {
20+
if (cache === undefined) {
21+
return callback;
22+
}
23+
24+
return async (args) => {
25+
const loadCacheKey = `${args.namespace}:${args.path}`;
26+
let result: OnLoadResult | null | undefined = cache.get(loadCacheKey);
27+
28+
if (result === undefined) {
29+
result = await callback(args);
30+
31+
// Do not cache null or undefined or results with errors
32+
if (result && result.errors === undefined) {
33+
await cache.put(loadCacheKey, result);
34+
}
35+
}
36+
37+
return result;
38+
};
39+
}
40+
1641
export class MemoryLoadResultCache implements LoadResultCache {
1742
#loadResults = new Map<string, OnLoadResult>();
1843
#fileDependencies = new Map<string, Set<string>>();

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets/bundle-options.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import type { BuildOptions, OutputFile } from 'esbuild';
10+
import { createHash } from 'node:crypto';
1011
import path from 'node:path';
1112
import { BundlerContext } from '../esbuild';
1213
import { LoadResultCache } from '../load-result-cache';
@@ -108,7 +109,11 @@ export async function bundleComponentStylesheet(
108109
cache?: LoadResultCache,
109110
) {
110111
const namespace = 'angular:styles/component';
111-
const entry = [language, componentStyleCounter++, filename].join(';');
112+
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
113+
// to the actual stylesheet file path.
114+
// TODO: Consider xxhash instead for hashing
115+
const id = inline ? createHash('sha256').update(data).digest('hex') : componentStyleCounter++;
116+
const entry = [language, id, filename].join(';');
112117

113118
const buildOptions = createStylesheetBundleOptions(options, cache, { [entry]: data });
114119
buildOptions.entryPoints = [`${namespace};${entry}`];

packages/angular_devkit/build_angular/src/builders/browser-esbuild/stylesheets/sass-plugin.ts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
FileImporterWithRequestContextOptions,
1717
SassWorkerImplementation,
1818
} from '../../../sass/sass-service';
19-
import type { LoadResultCache } from '../load-result-cache';
19+
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
2020

2121
export interface SassPluginOptions {
2222
sourcemap: boolean;
@@ -63,43 +63,33 @@ export function createSassPlugin(options: SassPluginOptions, cache?: LoadResultC
6363
return result;
6464
};
6565

66-
build.onLoad({ filter: /^s[ac]ss;/, namespace: 'angular:styles/component' }, async (args) => {
67-
const data = options.inlineComponentData?.[args.path];
68-
assert(
69-
typeof data === 'string',
70-
`component style name should always be found [${args.path}]`,
71-
);
66+
// Load inline component stylesheets
67+
build.onLoad(
68+
{ filter: /^s[ac]ss;/, namespace: 'angular:styles/component' },
69+
createCachedLoad(cache, async (args) => {
70+
const data = options.inlineComponentData?.[args.path];
71+
assert(
72+
typeof data === 'string',
73+
`component style name should always be found [${args.path}]`,
74+
);
7275

73-
let result = cache?.get(data);
74-
if (result === undefined) {
7576
const [language, , filePath] = args.path.split(';', 3);
7677
const syntax = language === 'sass' ? 'indented' : 'scss';
7778

78-
result = await compileString(data, filePath, syntax, options, resolveUrl);
79-
if (result.errors === undefined) {
80-
// Cache the result if there were no errors
81-
await cache?.put(data, result);
82-
}
83-
}
84-
85-
return result;
86-
});
79+
return compileString(data, filePath, syntax, options, resolveUrl);
80+
}),
81+
);
8782

88-
build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
89-
let result = cache?.get(args.path);
90-
if (result === undefined) {
83+
// Load file stylesheets
84+
build.onLoad(
85+
{ filter: /\.s[ac]ss$/ },
86+
createCachedLoad(cache, async (args) => {
9187
const data = await readFile(args.path, 'utf-8');
9288
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';
9389

94-
result = await compileString(data, args.path, syntax, options, resolveUrl);
95-
if (result.errors === undefined) {
96-
// Cache the result if there were no errors
97-
await cache?.put(args.path, result);
98-
}
99-
}
100-
101-
return result;
102-
});
90+
return compileString(data, args.path, syntax, options, resolveUrl);
91+
}),
92+
);
10393
},
10494
};
10595
}

0 commit comments

Comments
 (0)