Skip to content

Commit

Permalink
perf(@angular-devkit/build-angular): add initial global styles increm…
Browse files Browse the repository at this point in the history
…ental 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.

(cherry picked from commit 3f193be)
  • Loading branch information
clydin authored and dgp1130 committed Nov 14, 2022
1 parent 444475f commit 9d0872f
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*/

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

interface RebuildState {
codeRebuild?: BuildInvalidate;
globalStylesRebuild?: BuildInvalidate;
codeBundleCache?: SourceFileCache;
fileChanges: ChangedFiles;
}
Expand All @@ -41,6 +42,7 @@ class ExecutionResult {
constructor(
private success: boolean,
private codeRebuild?: BuildInvalidate,
private globalStylesRebuild?: BuildInvalidate,
private codeBundleCache?: SourceFileCache,
) {}

Expand All @@ -55,6 +57,7 @@ class ExecutionResult {

return {
codeRebuild: this.codeRebuild,
globalStylesRebuild: this.globalStylesRebuild,
codeBundleCache: this.codeBundleCache,
fileChanges,
};
Expand Down Expand Up @@ -97,7 +100,10 @@ async function execute(
rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache),
),
// Execute esbuild to bundle the global stylesheets
bundleGlobalStylesheets(options, target),
bundle(
workspaceRoot,
rebuildState?.globalStylesRebuild ?? createGlobalStylesBundleOptions(options, target),
),
]);

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

// Return if the bundling failed to generate output files or there are errors
if (!codeResults.outputFiles || codeResults.errors.length) {
return new ExecutionResult(false, rebuildState?.codeRebuild, codeBundleCache);
return new ExecutionResult(
false,
rebuildState?.codeRebuild,
(styleResults.outputFiles && styleResults.rebuild) ?? rebuildState?.globalStylesRebuild,
codeBundleCache,
);
}

// Return if the global stylesheet bundling has errors
if (!styleResults.outputFiles || styleResults.errors.length) {
return new ExecutionResult(
false,
codeResults.rebuild,
rebuildState?.globalStylesRebuild,
codeBundleCache,
);
}

// Filter global stylesheet initial files
styleResults.initialFiles = styleResults.initialFiles.filter(
({ name }) => options.globalStyles.find((style) => style.name === name)?.initial,
);

// Combine the bundling output files
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];

// Return if the global stylesheet bundling has errors
if (styleResults.errors.length) {
return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
}

// Generate index HTML file
if (indexHtmlOptions) {
// Create an index HTML generator that reads from the in-memory output files
Expand Down Expand Up @@ -184,14 +205,14 @@ async function execute(
} catch (error) {
context.logger.error(error instanceof Error ? error.message : `${error}`);

return new ExecutionResult(false, codeResults.rebuild, codeBundleCache);
return new ExecutionResult(false, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
}
}

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

return new ExecutionResult(true, codeResults.rebuild, codeBundleCache);
return new ExecutionResult(true, codeResults.rebuild, styleResults.rebuild, codeBundleCache);
}

function createOutputFileFromText(path: string, text: string): OutputFile {
Expand Down Expand Up @@ -293,7 +314,10 @@ function createCodeBundleOptions(
};
}

async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target: string[]) {
function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
): BuildOptions {
const {
workspaceRoot,
optimizationOptions,
Expand All @@ -303,70 +327,54 @@ async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target
preserveSymlinks,
externalDependencies,
stylePreprocessorOptions,
watch,
} = options;

const outputFiles: OutputFile[] = [];
const initialFiles: FileInfo[] = [];
const errors: Message[] = [];
const warnings: Message[] = [];

for (const { name, files, initial } of globalStyles) {
const virtualEntryData = files
.map((file) => `@import '${file.replace(/\\/g, '/')}';`)
.join('\n');
const sheetResult = await bundleStylesheetText(
virtualEntryData,
{ virtualName: `angular:style/global;${name}`, resolvePath: workspaceRoot },
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
outputNames: initial ? outputNames : { media: outputNames.media },
includePaths: stylePreprocessorOptions?.includePaths,
preserveSymlinks,
externalDependencies,
target,
},
);

errors.push(...sheetResult.errors);
warnings.push(...sheetResult.warnings);

if (!sheetResult.path) {
// Failed to process the stylesheet
assert.ok(
sheetResult.errors.length,
`Global stylesheet processing for '${name}' failed with no errors.`,
);

continue;
}
const buildOptions = createStylesheetBundleOptions({
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
});
buildOptions.incremental = watch;

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

if (initial) {
initialFiles.push({
file: sheetPath,
name,
extension: '.css',
buildOptions.plugins.unshift({
name: 'angular-global-styles',
setup(build) {
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
if (args.kind !== 'entry-point') {
return null;
}

return {
path: args.path.split(';', 2)[1],
namespace,
};
});
}
outputFiles.push(...sheetResult.resourceFiles);
}
build.onLoad({ filter: /./, namespace }, (args) => {
const files = globalStyles.find(({ name }) => name === args.path)?.files;
assert(files, `global style name should always be found [${args.path}]`);

return {
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
loader: 'css',
resolveDir: workspaceRoot,
};
});
},
});

return { outputFiles, initialFiles, errors, warnings };
return buildOptions;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,10 @@ export interface BundleStylesheetOptions {
target: string[];
}

async function bundleStylesheet(
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
export function createStylesheetBundleOptions(
options: BundleStylesheetOptions,
) {
// Execute esbuild
const result = await bundle(options.workspaceRoot, {
...entry,
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
return {
absWorkingDir: options.workspaceRoot,
bundle: true,
entryNames: options.outputNames?.bundles,
Expand All @@ -49,6 +46,17 @@ async function bundleStylesheet(
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths: options.includePaths }),
createCssResourcePlugin(),
],
};
}

async function bundleStylesheet(
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
options: BundleStylesheetOptions,
) {
// Execute esbuild
const result = await bundle(options.workspaceRoot, {
...createStylesheetBundleOptions(options),
...entry,
});

// Extract the result of the bundling from the output files
Expand Down

0 comments on commit 9d0872f

Please sign in to comment.