mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 23:15:56 +08:00
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.
This commit is contained in:
parent
4e42261609
commit
3f193be775
@ -7,10 +7,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
|
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
|
||||||
import * as assert from 'assert';
|
import type { BuildInvalidate, BuildOptions, OutputFile } from 'esbuild';
|
||||||
import type { BuildInvalidate, BuildOptions, Message, OutputFile } from 'esbuild';
|
import assert from 'node:assert';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'node:fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'node:path';
|
||||||
import { deleteOutputDir } from '../../utils';
|
import { deleteOutputDir } from '../../utils';
|
||||||
import { copyAssets } from '../../utils/copy-assets';
|
import { copyAssets } from '../../utils/copy-assets';
|
||||||
import { assertIsError } from '../../utils/error';
|
import { assertIsError } from '../../utils/error';
|
||||||
@ -25,11 +25,12 @@ import { logExperimentalWarnings } from './experimental-warnings';
|
|||||||
import { NormalizedBrowserOptions, normalizeOptions } from './options';
|
import { NormalizedBrowserOptions, normalizeOptions } from './options';
|
||||||
import { shutdownSassWorkerPool } from './sass-plugin';
|
import { shutdownSassWorkerPool } from './sass-plugin';
|
||||||
import { Schema as BrowserBuilderOptions } from './schema';
|
import { Schema as BrowserBuilderOptions } from './schema';
|
||||||
import { bundleStylesheetText } from './stylesheets';
|
import { createStylesheetBundleOptions } from './stylesheets';
|
||||||
import { ChangedFiles, createWatcher } from './watcher';
|
import { ChangedFiles, createWatcher } from './watcher';
|
||||||
|
|
||||||
interface RebuildState {
|
interface RebuildState {
|
||||||
codeRebuild?: BuildInvalidate;
|
codeRebuild?: BuildInvalidate;
|
||||||
|
globalStylesRebuild?: BuildInvalidate;
|
||||||
codeBundleCache?: SourceFileCache;
|
codeBundleCache?: SourceFileCache;
|
||||||
fileChanges: ChangedFiles;
|
fileChanges: ChangedFiles;
|
||||||
}
|
}
|
||||||
@ -41,6 +42,7 @@ class ExecutionResult {
|
|||||||
constructor(
|
constructor(
|
||||||
private success: boolean,
|
private success: boolean,
|
||||||
private codeRebuild?: BuildInvalidate,
|
private codeRebuild?: BuildInvalidate,
|
||||||
|
private globalStylesRebuild?: BuildInvalidate,
|
||||||
private codeBundleCache?: SourceFileCache,
|
private codeBundleCache?: SourceFileCache,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -55,6 +57,7 @@ class ExecutionResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
codeRebuild: this.codeRebuild,
|
codeRebuild: this.codeRebuild,
|
||||||
|
globalStylesRebuild: this.globalStylesRebuild,
|
||||||
codeBundleCache: this.codeBundleCache,
|
codeBundleCache: this.codeBundleCache,
|
||||||
fileChanges,
|
fileChanges,
|
||||||
};
|
};
|
||||||
@ -97,7 +100,10 @@ async function execute(
|
|||||||
rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache),
|
rebuildState?.codeRebuild ?? createCodeBundleOptions(options, target, codeBundleCache),
|
||||||
),
|
),
|
||||||
// Execute esbuild to bundle the global stylesheets
|
// 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
|
// Log all warnings and errors generated during bundling
|
||||||
@ -108,18 +114,33 @@ async function execute(
|
|||||||
|
|
||||||
// Return if the bundling failed to generate output files or there are errors
|
// Return if the bundling failed to generate output files or there are errors
|
||||||
if (!codeResults.outputFiles || codeResults.errors.length) {
|
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
|
// Combine the bundling output files
|
||||||
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
|
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
|
||||||
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];
|
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
|
// Generate index HTML file
|
||||||
if (indexHtmlOptions) {
|
if (indexHtmlOptions) {
|
||||||
// Create an index HTML generator that reads from the in-memory output files
|
// Create an index HTML generator that reads from the in-memory output files
|
||||||
@ -184,14 +205,14 @@ async function execute(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
context.logger.error(error instanceof Error ? error.message : `${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;
|
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
|
||||||
context.logger.info(`Complete. [${buildTime.toFixed(3)} seconds]`);
|
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 {
|
function createOutputFileFromText(path: string, text: string): OutputFile {
|
||||||
@ -293,7 +314,10 @@ function createCodeBundleOptions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target: string[]) {
|
function createGlobalStylesBundleOptions(
|
||||||
|
options: NormalizedBrowserOptions,
|
||||||
|
target: string[],
|
||||||
|
): BuildOptions {
|
||||||
const {
|
const {
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
optimizationOptions,
|
optimizationOptions,
|
||||||
@ -303,70 +327,54 @@ async function bundleGlobalStylesheets(options: NormalizedBrowserOptions, target
|
|||||||
preserveSymlinks,
|
preserveSymlinks,
|
||||||
externalDependencies,
|
externalDependencies,
|
||||||
stylePreprocessorOptions,
|
stylePreprocessorOptions,
|
||||||
|
watch,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const outputFiles: OutputFile[] = [];
|
const buildOptions = createStylesheetBundleOptions({
|
||||||
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,
|
workspaceRoot,
|
||||||
optimization: !!optimizationOptions.styles.minify,
|
optimization: !!optimizationOptions.styles.minify,
|
||||||
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
|
sourcemap: !!sourcemapOptions.styles,
|
||||||
outputNames: initial ? outputNames : { media: outputNames.media },
|
|
||||||
includePaths: stylePreprocessorOptions?.includePaths,
|
|
||||||
preserveSymlinks,
|
preserveSymlinks,
|
||||||
externalDependencies,
|
|
||||||
target,
|
target,
|
||||||
},
|
externalDependencies,
|
||||||
);
|
outputNames,
|
||||||
|
includePaths: stylePreprocessorOptions?.includePaths,
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
|
|
||||||
if (initial) {
|
|
||||||
initialFiles.push({
|
|
||||||
file: sheetPath,
|
|
||||||
name,
|
|
||||||
extension: '.css',
|
|
||||||
});
|
});
|
||||||
}
|
buildOptions.incremental = watch;
|
||||||
outputFiles.push(...sheetResult.resourceFiles);
|
|
||||||
|
const namespace = 'angular:styles/global';
|
||||||
|
buildOptions.entryPoints = {};
|
||||||
|
for (const { name } of globalStyles) {
|
||||||
|
buildOptions.entryPoints[name] = `${namespace};${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { outputFiles, initialFiles, errors, warnings };
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
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 buildOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,13 +23,10 @@ export interface BundleStylesheetOptions {
|
|||||||
target: string[];
|
target: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bundleStylesheet(
|
export function createStylesheetBundleOptions(
|
||||||
entry: Required<Pick<BuildOptions, 'stdin'> | Pick<BuildOptions, 'entryPoints'>>,
|
|
||||||
options: BundleStylesheetOptions,
|
options: BundleStylesheetOptions,
|
||||||
) {
|
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
|
||||||
// Execute esbuild
|
return {
|
||||||
const result = await bundle(options.workspaceRoot, {
|
|
||||||
...entry,
|
|
||||||
absWorkingDir: options.workspaceRoot,
|
absWorkingDir: options.workspaceRoot,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
entryNames: options.outputNames?.bundles,
|
entryNames: options.outputNames?.bundles,
|
||||||
@ -49,6 +46,17 @@ async function bundleStylesheet(
|
|||||||
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths: options.includePaths }),
|
createSassPlugin({ sourcemap: !!options.sourcemap, loadPaths: options.includePaths }),
|
||||||
createCssResourcePlugin(),
|
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
|
// Extract the result of the bundling from the output files
|
||||||
|
Loading…
x
Reference in New Issue
Block a user