Skip to content

Commit 3f193be

Browse files
clydindgp1130
authored andcommitted
perf(@angular-devkit/build-angular): add initial global styles incremental rebuilds with esbuild builder
When using the experimental esbuild-based browser application builder in watch mode, global stylesheets configured with the `styles` option will now use the incremental rebuild mode of esbuild. This allows for a reduction in processing when rebuilding the global styles. CSS stylesheets benefit the most currently. Sass stylesheets will benefit more once preprocessor output caching is implemented.
1 parent 4e42261 commit 3f193be

File tree

2 files changed

+95
-79
lines changed

2 files changed

+95
-79
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

+81-73
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
*/
88

99
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
10-
import * as assert from 'assert';
11-
import type { BuildInvalidate, BuildOptions, Message, OutputFile } from 'esbuild';
12-
import * as fs from 'fs/promises';
13-
import * as path from 'path';
10+
import type { BuildInvalidate, BuildOptions, OutputFile } from 'esbuild';
11+
import assert from 'node:assert';
12+
import * as fs from 'node:fs/promises';
13+
import * as path from 'node:path';
1414
import { deleteOutputDir } from '../../utils';
1515
import { copyAssets } from '../../utils/copy-assets';
1616
import { assertIsError } from '../../utils/error';
@@ -25,11 +25,12 @@ import { logExperimentalWarnings } from './experimental-warnings';
2525
import { NormalizedBrowserOptions, normalizeOptions } from './options';
2626
import { shutdownSassWorkerPool } from './sass-plugin';
2727
import { Schema as BrowserBuilderOptions } from './schema';
28-
import { bundleStylesheetText } from './stylesheets';
28+
import { createStylesheetBundleOptions } from './stylesheets';
2929
import { ChangedFiles, createWatcher } from './watcher';
3030

3131
interface RebuildState {
3232
codeRebuild?: BuildInvalidate;
33+
globalStylesRebuild?: BuildInvalidate;
3334
codeBundleCache?: SourceFileCache;
3435
fileChanges: ChangedFiles;
3536
}
@@ -41,6 +42,7 @@ class ExecutionResult {
4142
constructor(
4243
private success: boolean,
4344
private codeRebuild?: BuildInvalidate,
45+
private globalStylesRebuild?: BuildInvalidate,
4446
private codeBundleCache?: SourceFileCache,
4547
) {}
4648

@@ -55,6 +57,7 @@ class ExecutionResult {
5557

5658
return {
5759
codeRebuild: this.codeRebuild,
60+
globalStylesRebuild: this.globalStylesRebuild,
5861
codeBundleCache: this.codeBundleCache,
5962
fileChanges,
6063
};
@@ -97,7 +100,10 @@ async function execute(
97100
rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache),
98101
),
99102
// Execute esbuild to bundle the global stylesheets
100-
bundleGlobalStylesheets(options, target),
103+
bundle(
104+
workspaceRoot,
105+
rebuildState?.globalStylesRebuild ?? createGlobalStylesBundleOptions(options, target),
106+
),
101107
]);
102108

103109
// Log all warnings and errors generated during bundling
@@ -108,18 +114,33 @@ async function execute(
108114

109115
// Return if the bundling failed to generate output files or there are errors
110116
if (!codeResults.outputFiles || codeResults.errors.length) {
111-
return new ExecutionResult(false, rebuildState?.codeRebuild, codeBundleCache);
117+
return new ExecutionResult(
118+
false,
119+
rebuildState?.codeRebuild,
120+
(styleResults.outputFiles && styleResults.rebuild) ?? rebuildState?.globalStylesRebuild,
121+
codeBundleCache,
122+
);
123+
}
124+
125+
// Return if the global stylesheet bundling has errors
126+
if (!styleResults.outputFiles || styleResults.errors.length) {
127+
return new ExecutionResult(
128+
false,
129+
codeResults.rebuild,
130+
rebuildState?.globalStylesRebuild,
131+
codeBundleCache,
132+
);
112133
}
113134

135+
// Filter global stylesheet initial files
136+
styleResults.initialFiles = styleResults.initialFiles.filter(
137+
({ name }) => options.globalStyles.find((style) => style.name === name)?.initial,
138+
);
139+
114140
// Combine the bundling output files
115141
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
116142
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];
117143

118-
// Return if the global stylesheet bundling has errors
119-
if (styleResults.errors.length) {
120-
return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
121-
}
122-
123144
// Generate index HTML file
124145
if (indexHtmlOptions) {
125146
// Create an index HTML generator that reads from the in-memory output files
@@ -184,14 +205,14 @@ async function execute(
184205
} catch (error) {
185206
context.logger.error(error instanceof Error ? error.message : `${error}`);
186207

187-
return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
208+
return new ExecutionResult(false, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
188209
}
189210
}
190211

191212
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
192213
context.logger.info(`Complete. [${buildTime.toFixed(3)} seconds]`);
193214

194-
return new ExecutionResult(true, codeResults.rebuild, codeBundleCache);
215+
return new ExecutionResult(true, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
195216
}
196217

197218
function createOutputFileFromText(path: string, text: string): OutputFile {
@@ -293,7 +314,10 @@ function createCodeBundleOptions(
293314
};
294315
}
295316

296-
async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target: string[]) {
317+
function createGlobalStylesBundleOptions(
318+
options: NormalizedBrowserOptions,
319+
target: string[],
320+
): BuildOptions {
297321
const {
298322
workspaceRoot,
299323
optimizationOptions,
@@ -303,70 +327,54 @@ async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target
303327
preserveSymlinks,
304328
externalDependencies,
305329
stylePreprocessorOptions,
330+
watch,
306331
} = options;
307332

308-
const outputFiles: OutputFile[] = [];
309-
const initialFiles: FileInfo[] = [];
310-
const errors: Message[] = [];
311-
const warnings: Message[] = [];
312-
313-
for (const { name, files, initial } of globalStyles) {
314-
const virtualEntryData = files
315-
.map((file) => `@import '${file.replace(/\\/g, '/')}';`)
316-
.join('\n');
317-
const sheetResult = await bundleStylesheetText(
318-
virtualEntryData,
319-
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
320-
{
321-
workspaceRoot,
322-
optimization: !!optimizationOptions.styles.minify,
323-
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
324-
outputNames: initial ? outputNames : { media: outputNames.media },
325-
includePaths: stylePreprocessorOptions?.includePaths,
326-
preserveSymlinks,
327-
externalDependencies,
328-
target,
329-
},
330-
);
331-
332-
errors.push(...sheetResult.errors);
333-
warnings.push(...sheetResult.warnings);
334-
335-
if (!sheetResult.path) {
336-
// Failed to process the stylesheet
337-
assert.ok(
338-
sheetResult.errors.length,
339-
`Global stylesheet processing for '${name}' failed with no errors.`,
340-
);
341-
342-
continue;
343-
}
333+
const buildOptions = createStylesheetBundleOptions({
334+
workspaceRoot,
335+
optimization: !!optimizationOptions.styles.minify,
336+
sourcemap: !!sourcemapOptions.styles,
337+
preserveSymlinks,
338+
target,
339+
externalDependencies,
340+
outputNames,
341+
includePaths: stylePreprocessorOptions?.includePaths,
342+
});
343+
buildOptions.incremental = watch;
344344

345-
// The virtual stylesheets will be named `stdin` by esbuild. This must be replaced
346-
// with the actual name of the global style and the leading directory separator must
347-
// also be removed to make the path relative.
348-
const sheetPath = sheetResult.path.replace('stdin', name);
349-
let sheetContents = sheetResult.contents;
350-
if (sheetResult.map) {
351-
outputFiles.push(createOutputFileFromText(sheetPath + '.map', sheetResult.map));
352-
sheetContents = sheetContents.replace(
353-
'sourceMappingURL=stdin.css.map',
354-
`sourceMappingURL=${name}.css.map`,
355-
);
356-
}
357-
outputFiles.push(createOutputFileFromText(sheetPath, sheetContents));
345+
const namespace = 'angular:styles/global';
346+
buildOptions.entryPoints = {};
347+
for (const { name } of globalStyles) {
348+
buildOptions.entryPoints[name] = `${namespace};${name}`;
349+
}
358350

359-
if (initial) {
360-
initialFiles.push({
361-
file: sheetPath,
362-
name,
363-
extension: '.css',
351+
buildOptions.plugins.unshift({
352+
name: 'angular-global-styles',
353+
setup(build) {
354+
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
355+
if (args.kind !== 'entry-point') {
356+
return null;
357+
}
358+
359+
return {
360+
path: args.path.split(';', 2)[1],
361+
namespace,
362+
};
364363
});
365-
}
366-
outputFiles.push(...sheetResult.resourceFiles);
367-
}
364+
build.onLoad({ filter: /./, namespace }, (args) => {
365+
const files = globalStyles.find(({ name }) => name === args.path)?.files;
366+
assert(files, `global style name should always be found [${args.path}]`);
367+
368+
return {
369+
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
370+
loader: 'css',
371+
resolveDir: workspaceRoot,
372+
};
373+
});
374+
},
375+
});
368376

369-
return { outputFiles, initialFiles, errors, warnings };
377+
return buildOptions;
370378
}
371379

372380
/**

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,10 @@ export interface BundleStylesheetOptions {
2323
target: string[];
2424
}
2525

26-
async function bundleStylesheet(
27-
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
26+
export function createStylesheetBundleOptions(
2827
options: BundleStylesheetOptions,
29-
) {
30-
// Execute esbuild
31-
const result = await bundle(options.workspaceRoot, {
32-
...entry,
28+
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
29+
return {
3330
absWorkingDir: options.workspaceRoot,
3431
bundle: true,
3532
entryNames: options.outputNames?.bundles,
@@ -49,6 +46,17 @@ async function bundleStylesheet(
4946
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths: options.includePaths }),
5047
createCssResourcePlugin(),
5148
],
49+
};
50+
}
51+
52+
async function bundleStylesheet(
53+
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
54+
options: BundleStylesheetOptions,
55+
) {
56+
// Execute esbuild
57+
const result = await bundle(options.workspaceRoot, {
58+
...createStylesheetBundleOptions(options),
59+
...entry,
5260
});
5361

5462
// Extract the result of the bundling from the output files

0 commit comments

Comments
 (0)