Skip to content

Commit e68a662

Browse files
clydinalan-agius4
authored andcommitted
perf(@angular-devkit/build-angular): only rebundle global scripts/styles on explicit changes
The newly introduced incremental bundler result caching is now used for both global styles (`styles` option) and global scripts (`scripts` option). This allows the bundling steps to be skipped in watch mode when no related files for either have been modified. This can be especially beneficial for applications with large global stylesheets. (cherry picked from commit 4bc6deb)
1 parent c0c7dad commit e68a662

File tree

5 files changed

+140
-125
lines changed

5 files changed

+140
-125
lines changed

packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,7 @@ export async function executeBuild(
9595
// Global Stylesheets
9696
if (options.globalStyles.length > 0) {
9797
for (const initial of [true, false]) {
98-
const bundleOptions = createGlobalStylesBundleOptions(
99-
options,
100-
target,
101-
initial,
102-
codeBundleCache?.loadResultCache,
103-
);
98+
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
10499
if (bundleOptions) {
105100
bundlerContexts.push(
106101
new BundlerContext(workspaceRoot, !!options.watch, bundleOptions, () => initial),
@@ -154,7 +149,10 @@ export async function executeBuild(
154149
}
155150
}
156151

157-
const bundlingResult = await BundlerContext.bundleAll(bundlerContexts);
152+
const bundlingResult = await BundlerContext.bundleAll(
153+
bundlerContexts,
154+
rebuildState?.fileChanges.all,
155+
);
158156

159157
// Log all warnings and errors generated during bundling
160158
await logMessages(context, bundlingResult);

packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,19 @@ export class BundlerContext {
9494
};
9595
}
9696

97-
static async bundleAll(contexts: Iterable<BundlerContext>): Promise<BundleContextResult> {
98-
const individualResults = await Promise.all([...contexts].map((context) => context.bundle()));
97+
static async bundleAll(
98+
contexts: Iterable<BundlerContext>,
99+
changedFiles?: Iterable<string>,
100+
): Promise<BundleContextResult> {
101+
const individualResults = await Promise.all(
102+
[...contexts].map((context) => {
103+
if (changedFiles) {
104+
context.invalidate(changedFiles);
105+
}
106+
107+
return context.bundle();
108+
}),
109+
);
99110

100111
// Return directly if only one result
101112
if (individualResults.length === 1) {

packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts

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

9-
import type { BuildOptions } from 'esbuild';
109
import MagicString, { Bundle } from 'magic-string';
1110
import assert from 'node:assert';
1211
import { readFile } from 'node:fs/promises';
1312
import path from 'node:path';
1413
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
1514
import { assertIsError } from '../../utils/error';
16-
import { LoadResultCache, createCachedLoad } from './load-result-cache';
15+
import { BundlerOptionsFactory } from './bundler-context';
16+
import { createCachedLoad } from './load-result-cache';
1717
import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
1818
import { createVirtualModulePlugin } from './virtual-module-plugin';
1919

@@ -26,8 +26,7 @@ import { createVirtualModulePlugin } from './virtual-module-plugin';
2626
export function createGlobalScriptsBundleOptions(
2727
options: NormalizedApplicationBuildOptions,
2828
initial: boolean,
29-
loadCache?: LoadResultCache,
30-
): BuildOptions | undefined {
29+
): BundlerOptionsFactory | undefined {
3130
const {
3231
globalScripts,
3332
optimizationOptions,
@@ -52,83 +51,86 @@ export function createGlobalScriptsBundleOptions(
5251
return;
5352
}
5453

55-
return {
56-
absWorkingDir: workspaceRoot,
57-
bundle: false,
58-
splitting: false,
59-
entryPoints,
60-
entryNames: initial ? outputNames.bundles : '[name]',
61-
assetNames: outputNames.media,
62-
mainFields: ['script', 'browser', 'main'],
63-
conditions: ['script'],
64-
resolveExtensions: ['.mjs', '.js'],
65-
logLevel: options.verbose ? 'debug' : 'silent',
66-
metafile: true,
67-
minify: optimizationOptions.scripts,
68-
outdir: workspaceRoot,
69-
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
70-
write: false,
71-
platform: 'neutral',
72-
preserveSymlinks,
73-
plugins: [
74-
createSourcemapIgnorelistPlugin(),
75-
createVirtualModulePlugin({
76-
namespace,
77-
external: true,
78-
// Add the `js` extension here so that esbuild generates an output file with the extension
79-
transformPath: (path) => path.slice(namespace.length + 1) + '.js',
80-
loadContent: (args, build) =>
81-
createCachedLoad(loadCache, async (args) => {
82-
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))?.files;
83-
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
54+
return (loadCache) => {
55+
return {
56+
absWorkingDir: workspaceRoot,
57+
bundle: false,
58+
splitting: false,
59+
entryPoints,
60+
entryNames: initial ? outputNames.bundles : '[name]',
61+
assetNames: outputNames.media,
62+
mainFields: ['script', 'browser', 'main'],
63+
conditions: ['script'],
64+
resolveExtensions: ['.mjs', '.js'],
65+
logLevel: options.verbose ? 'debug' : 'silent',
66+
metafile: true,
67+
minify: optimizationOptions.scripts,
68+
outdir: workspaceRoot,
69+
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
70+
write: false,
71+
platform: 'neutral',
72+
preserveSymlinks,
73+
plugins: [
74+
createSourcemapIgnorelistPlugin(),
75+
createVirtualModulePlugin({
76+
namespace,
77+
external: true,
78+
// Add the `js` extension here so that esbuild generates an output file with the extension
79+
transformPath: (path) => path.slice(namespace.length + 1) + '.js',
80+
loadContent: (args, build) =>
81+
createCachedLoad(loadCache, async (args) => {
82+
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))
83+
?.files;
84+
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
8485

85-
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
86-
const bundleContent = new Bundle();
87-
const watchFiles = [];
88-
for (const filename of files) {
89-
let fileContent;
90-
try {
91-
// Attempt to read as a relative path from the workspace root
92-
const fullPath = path.join(workspaceRoot, filename);
93-
fileContent = await readFile(fullPath, 'utf-8');
94-
watchFiles.push(fullPath);
95-
} catch (e) {
96-
assertIsError(e);
97-
if (e.code !== 'ENOENT') {
98-
throw e;
99-
}
86+
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
87+
const bundleContent = new Bundle();
88+
const watchFiles = [];
89+
for (const filename of files) {
90+
let fileContent;
91+
try {
92+
// Attempt to read as a relative path from the workspace root
93+
const fullPath = path.join(workspaceRoot, filename);
94+
fileContent = await readFile(fullPath, 'utf-8');
95+
watchFiles.push(fullPath);
96+
} catch (e) {
97+
assertIsError(e);
98+
if (e.code !== 'ENOENT') {
99+
throw e;
100+
}
100101

101-
// If not found, attempt to resolve as a module specifier
102-
const resolveResult = await build.resolve(filename, {
103-
kind: 'entry-point',
104-
resolveDir: workspaceRoot,
105-
});
102+
// If not found, attempt to resolve as a module specifier
103+
const resolveResult = await build.resolve(filename, {
104+
kind: 'entry-point',
105+
resolveDir: workspaceRoot,
106+
});
106107

107-
if (resolveResult.errors.length) {
108-
// Remove resolution failure notes about marking as external since it doesn't apply
109-
// to global scripts.
110-
resolveResult.errors.forEach((error) => (error.notes = []));
108+
if (resolveResult.errors.length) {
109+
// Remove resolution failure notes about marking as external since it doesn't apply
110+
// to global scripts.
111+
resolveResult.errors.forEach((error) => (error.notes = []));
111112

112-
return {
113-
errors: resolveResult.errors,
114-
warnings: resolveResult.warnings,
115-
};
113+
return {
114+
errors: resolveResult.errors,
115+
warnings: resolveResult.warnings,
116+
};
117+
}
118+
119+
watchFiles.push(resolveResult.path);
120+
fileContent = await readFile(resolveResult.path, 'utf-8');
116121
}
117122

118-
watchFiles.push(resolveResult.path);
119-
fileContent = await readFile(resolveResult.path, 'utf-8');
123+
bundleContent.addSource(new MagicString(fileContent, { filename }));
120124
}
121125

122-
bundleContent.addSource(new MagicString(fileContent, { filename }));
123-
}
124-
125-
return {
126-
contents: bundleContent.toString(),
127-
loader: 'js',
128-
watchFiles,
129-
};
130-
}).call(build, args),
131-
}),
132-
],
126+
return {
127+
contents: bundleContent.toString(),
128+
loader: 'js',
129+
watchFiles,
130+
};
131+
}).call(build, args),
132+
}),
133+
],
134+
};
133135
};
134136
}

packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts

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

9-
import type { BuildOptions } from 'esbuild';
109
import assert from 'node:assert';
1110
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
12-
import { LoadResultCache } from './load-result-cache';
11+
import { BundlerOptionsFactory } from './bundler-context';
1312
import { createStylesheetBundleOptions } from './stylesheets/bundle-options';
1413
import { createVirtualModulePlugin } from './virtual-module-plugin';
1514

1615
export function createGlobalStylesBundleOptions(
1716
options: NormalizedApplicationBuildOptions,
1817
target: string[],
1918
initial: boolean,
20-
cache?: LoadResultCache,
21-
): BuildOptions | undefined {
19+
): BundlerOptionsFactory | undefined {
2220
const {
2321
workspaceRoot,
2422
optimizationOptions,
@@ -46,45 +44,47 @@ export function createGlobalStylesBundleOptions(
4644
return;
4745
}
4846

49-
const buildOptions = createStylesheetBundleOptions(
50-
{
51-
workspaceRoot,
52-
optimization: !!optimizationOptions.styles.minify,
53-
sourcemap: !!sourcemapOptions.styles,
54-
preserveSymlinks,
55-
target,
56-
externalDependencies,
57-
outputNames: initial
58-
? outputNames
59-
: {
60-
...outputNames,
61-
bundles: '[name]',
62-
},
63-
includePaths: stylePreprocessorOptions?.includePaths,
64-
tailwindConfiguration,
65-
publicPath: options.publicPath,
66-
},
67-
cache,
68-
);
69-
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
70-
buildOptions.entryPoints = entryPoints;
47+
return (loadCache) => {
48+
const buildOptions = createStylesheetBundleOptions(
49+
{
50+
workspaceRoot,
51+
optimization: !!optimizationOptions.styles.minify,
52+
sourcemap: !!sourcemapOptions.styles,
53+
preserveSymlinks,
54+
target,
55+
externalDependencies,
56+
outputNames: initial
57+
? outputNames
58+
: {
59+
...outputNames,
60+
bundles: '[name]',
61+
},
62+
includePaths: stylePreprocessorOptions?.includePaths,
63+
tailwindConfiguration,
64+
publicPath: options.publicPath,
65+
},
66+
loadCache,
67+
);
68+
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
69+
buildOptions.entryPoints = entryPoints;
7170

72-
buildOptions.plugins.unshift(
73-
createVirtualModulePlugin({
74-
namespace,
75-
transformPath: (path) => path.split(';', 2)[1],
76-
loadContent: (args) => {
77-
const files = globalStyles.find(({ name }) => name === args.path)?.files;
78-
assert(files, `global style name should always be found [${args.path}]`);
71+
buildOptions.plugins.unshift(
72+
createVirtualModulePlugin({
73+
namespace,
74+
transformPath: (path) => path.split(';', 2)[1],
75+
loadContent: (args) => {
76+
const files = globalStyles.find(({ name }) => name === args.path)?.files;
77+
assert(files, `global style name should always be found [${args.path}]`);
7978

80-
return {
81-
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
82-
loader: 'css',
83-
resolveDir: workspaceRoot,
84-
};
85-
},
86-
}),
87-
);
79+
return {
80+
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
81+
loader: 'css',
82+
resolveDir: workspaceRoot,
83+
};
84+
},
85+
}),
86+
);
8887

89-
return buildOptions;
88+
return buildOptions;
89+
};
9090
}

packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export class ChangedFiles {
1313
readonly modified = new Set<string>();
1414
readonly removed = new Set<string>();
1515

16+
get all(): string[] {
17+
return [...this.added, ...this.modified, ...this.removed];
18+
}
19+
1620
toDebugString(): string {
1721
const content = {
1822
added: Array.from(this.added),

0 commit comments

Comments
 (0)