Skip to content

Commit a977206

Browse files
committed
refactor(@angular/build): directly bundle external component file-based style changes
When using the development server with the application builder, file-based component styles will now be bundled during the main builder execution instead of within the application code bundling step. This allows for these styles to be processed independently from any code bundling steps and will support future changes that will allow the builder to completely skip code bundling if only file-based component stylesheets are changed.
1 parent 8646751 commit a977206

File tree

4 files changed

+134
-72
lines changed

4 files changed

+134
-72
lines changed

packages/angular/build/src/builders/application/execute-build.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { BuilderContext } from '@angular-devkit/architect';
1010
import assert from 'node:assert';
1111
import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
1212
import { generateBudgetStats } from '../../tools/esbuild/budget-stats';
13-
import { BuildOutputFileType, BundlerContext } from '../../tools/esbuild/bundler-context';
13+
import {
14+
BuildOutputFileType,
15+
BundleContextResult,
16+
BundlerContext,
17+
} from '../../tools/esbuild/bundler-context';
1418
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1519
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
1620
import { extractLicenses } from '../../tools/esbuild/license-extractor';
@@ -86,6 +90,23 @@ export async function executeBuild(
8690
rebuildState?.fileChanges.all,
8791
);
8892

93+
if (rebuildState && options.externalRuntimeStyles) {
94+
const invalidatedStylesheetEntries = componentStyleBundler.invalidate(
95+
rebuildState.fileChanges.all,
96+
);
97+
98+
if (invalidatedStylesheetEntries?.length) {
99+
const componentResults: BundleContextResult[] = [];
100+
for (const stylesheetFile of invalidatedStylesheetEntries) {
101+
// externalId is already linked in the bundler context so only enabling is required here
102+
const result = await componentStyleBundler.bundleFile(stylesheetFile, true, true);
103+
componentResults.push(result);
104+
}
105+
106+
bundlingResult = BundlerContext.mergeResults([bundlingResult, ...componentResults]);
107+
}
108+
}
109+
89110
if (options.optimizationOptions.scripts && shouldOptimizeChunks) {
90111
bundlingResult = await profileAsync('OPTIMIZE_CHUNKS', () =>
91112
optimizeChunks(
@@ -102,6 +123,11 @@ export async function executeBuild(
102123
);
103124
executionResult.addWarnings(bundlingResult.warnings);
104125

126+
// Add used external component style referenced files to be watched
127+
if (options.externalRuntimeStyles) {
128+
executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles());
129+
}
130+
105131
// Return if the bundling has errors
106132
if (bundlingResult.errors) {
107133
executionResult.addErrors(bundlingResult.errors);

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ export function createCompilerPlugin(
150150
// Angular compiler which does not have direct knowledge of transitive resource
151151
// dependencies or web worker processing.
152152
let modifiedFiles;
153-
let invalidatedStylesheetEntries;
154153
if (
155154
pluginOptions.sourceFileCache?.modifiedFiles.size &&
156155
referencedFileTracker &&
@@ -159,7 +158,11 @@ export function createCompilerPlugin(
159158
// TODO: Differentiate between changed input files and stale output files
160159
modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles);
161160
pluginOptions.sourceFileCache.invalidate(modifiedFiles);
162-
invalidatedStylesheetEntries = stylesheetBundler.invalidate(modifiedFiles);
161+
// External runtime styles are invalidated and rebuilt at the beginning of a rebuild to avoid
162+
// the need to execute the application bundler for component style only changes.
163+
if (!pluginOptions.externalRuntimeStyles) {
164+
stylesheetBundler.invalidate(modifiedFiles);
165+
}
163166
}
164167

165168
if (
@@ -201,12 +204,14 @@ export function createCompilerPlugin(
201204
);
202205
}
203206

204-
const { contents, outputFiles, metafile, referencedFiles, errors, warnings } =
205-
stylesheetResult;
206-
if (errors) {
207-
(result.errors ??= []).push(...errors);
207+
(result.warnings ??= []).push(...stylesheetResult.warnings);
208+
if (stylesheetResult.errors) {
209+
(result.errors ??= []).push(...stylesheetResult.errors);
210+
211+
return '';
208212
}
209-
(result.warnings ??= []).push(...warnings);
213+
214+
const { contents, outputFiles, metafile, referencedFiles } = stylesheetResult;
210215
additionalResults.set(stylesheetFile ?? containingFile, {
211216
outputFiles,
212217
metafile,
@@ -332,19 +337,6 @@ export function createCompilerPlugin(
332337
additionalResults,
333338
);
334339
}
335-
// Process any updated stylesheets
336-
if (invalidatedStylesheetEntries) {
337-
for (const stylesheetFile of invalidatedStylesheetEntries) {
338-
// externalId is already linked in the bundler context so only enabling is required here
339-
await bundleExternalStylesheet(
340-
stylesheetBundler,
341-
stylesheetFile,
342-
true,
343-
result,
344-
additionalResults,
345-
);
346-
}
347-
}
348340
}
349341

350342
// Update TypeScript file output cache for all affected files
@@ -565,18 +557,23 @@ async function bundleExternalStylesheet(
565557
{ outputFiles?: OutputFile[]; metafile?: Metafile; errors?: PartialMessage[] }
566558
>,
567559
) {
568-
const { outputFiles, metafile, errors, warnings } = await stylesheetBundler.bundleFile(
569-
stylesheetFile,
570-
externalId,
571-
);
572-
if (errors) {
573-
(result.errors ??= []).push(...errors);
560+
const styleResult = await stylesheetBundler.bundleFile(stylesheetFile, externalId);
561+
562+
(result.warnings ??= []).push(...styleResult.warnings);
563+
if (styleResult.errors) {
564+
(result.errors ??= []).push(...styleResult.errors);
565+
} else {
566+
const { outputFiles, metafile } = styleResult;
567+
// Clear inputs to prevent triggering a rebuild of the application code for component
568+
// stylesheet file only changes when the dev server enables the internal-only external
569+
// stylesheet option. This does not affect builds since only the dev server can enable
570+
// the internal option.
571+
metafile.inputs = {};
572+
additionalResults.set(stylesheetFile, {
573+
outputFiles,
574+
metafile,
575+
});
574576
}
575-
(result.warnings ??= []).push(...warnings);
576-
additionalResults.set(stylesheetFile, {
577-
outputFiles,
578-
metafile,
579-
});
580577
}
581578

582579
function createCompilerOptionsTransformer(

packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { OutputFile } from 'esbuild';
109
import assert from 'node:assert';
1110
import { createHash } from 'node:crypto';
1211
import path from 'node:path';
13-
import { BuildOutputFileType, BundleContextResult, BundlerContext } from '../bundler-context';
12+
import {
13+
BuildOutputFile,
14+
BuildOutputFileType,
15+
BundleContextResult,
16+
BundlerContext,
17+
} from '../bundler-context';
1418
import { MemoryCache } from '../cache';
1519
import {
1620
BundleStylesheetOptions,
@@ -37,7 +41,14 @@ export class ComponentStylesheetBundler {
3741
private readonly incremental: boolean,
3842
) {}
3943

40-
async bundleFile(entry: string, externalId?: string | boolean) {
44+
/**
45+
* Bundle a file-based component stylesheet for use within an AOT compiled Angular application.
46+
* @param entry The file path of the stylesheet.
47+
* @param externalId Either an external identifier string for initial bundling or a boolean for rebuilds, if external.
48+
* @param direct If true, the output will be used directly by the builder; false if used inside the compiler plugin.
49+
* @returns A component bundle result object.
50+
*/
51+
async bundleFile(entry: string, externalId?: string | boolean, direct?: boolean) {
4152
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
4253
return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
4354
const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
@@ -62,6 +73,7 @@ export class ComponentStylesheetBundler {
6273
await bundlerContext.bundle(),
6374
bundlerContext.watchFiles,
6475
!!externalId,
76+
!!direct,
6577
);
6678
}
6779

@@ -127,6 +139,7 @@ export class ComponentStylesheetBundler {
127139
await bundlerContext.bundle(),
128140
bundlerContext.watchFiles,
129141
!!externalId,
142+
false,
130143
);
131144
}
132145

@@ -156,6 +169,15 @@ export class ComponentStylesheetBundler {
156169
return entries;
157170
}
158171

172+
collectReferencedFiles(): string[] {
173+
const files = [];
174+
for (const context of this.#fileContexts.values()) {
175+
files.push(...context.watchFiles);
176+
}
177+
178+
return files;
179+
}
180+
159181
async dispose(): Promise<void> {
160182
const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()];
161183
this.#fileContexts.clear();
@@ -168,61 +190,70 @@ export class ComponentStylesheetBundler {
168190
result: BundleContextResult,
169191
referencedFiles: Set<string> | undefined,
170192
external: boolean,
193+
direct: boolean,
171194
) {
172195
let contents = '';
173-
let metafile;
174-
const outputFiles: OutputFile[] = [];
196+
const outputFiles: BuildOutputFile[] = [];
175197

176-
if (!result.errors) {
177-
for (const outputFile of result.outputFiles) {
178-
const filename = path.basename(outputFile.path);
198+
const { errors, warnings } = result;
199+
if (errors) {
200+
return { errors, warnings, referencedFiles };
201+
}
179202

180-
if (outputFile.type === BuildOutputFileType.Media || filename.endsWith('.css.map')) {
181-
// The output files could also contain resources (images/fonts/etc.) that were referenced and the map files.
203+
for (const outputFile of result.outputFiles) {
204+
const filename = path.basename(outputFile.path);
182205

183-
// Clone the output file to avoid amending the original path which would causes problems during rebuild.
184-
const clonedOutputFile = outputFile.clone();
206+
if (outputFile.type === BuildOutputFileType.Media || filename.endsWith('.css.map')) {
207+
// The output files could also contain resources (images/fonts/etc.) that were referenced and the map files.
208+
209+
// Clone the output file to avoid amending the original path which would causes problems during rebuild.
210+
const clonedOutputFile = outputFile.clone();
185211

186-
// Needed for Bazel as otherwise the files will not be written in the correct place,
187-
// this is because esbuild will resolve the output file from the outdir which is currently set to `workspaceRoot` twice,
188-
// once in the stylesheet and the other in the application code bundler.
189-
// Ex: `../../../../../app.component.css.map`.
212+
// Needed for Bazel as otherwise the files will not be written in the correct place,
213+
// this is because esbuild will resolve the output file from the outdir which is currently set to `workspaceRoot` twice,
214+
// once in the stylesheet and the other in the application code bundler.
215+
// Ex: `../../../../../app.component.css.map`.
216+
if (!direct) {
190217
clonedOutputFile.path = path.join(this.options.workspaceRoot, outputFile.path);
218+
}
191219

192-
outputFiles.push(clonedOutputFile);
193-
} else if (filename.endsWith('.css')) {
194-
if (external) {
195-
const clonedOutputFile = outputFile.clone();
220+
outputFiles.push(clonedOutputFile);
221+
} else if (filename.endsWith('.css')) {
222+
if (external) {
223+
const clonedOutputFile = outputFile.clone();
224+
if (!direct) {
196225
clonedOutputFile.path = path.join(this.options.workspaceRoot, outputFile.path);
197-
outputFiles.push(clonedOutputFile);
198-
contents = path.posix.join(this.options.publicPath ?? '', filename);
199-
} else {
200-
contents = outputFile.text;
201226
}
227+
outputFiles.push(clonedOutputFile);
228+
contents = path.posix.join(this.options.publicPath ?? '', filename);
202229
} else {
203-
throw new Error(
204-
`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`,
205-
);
230+
contents = outputFile.text;
206231
}
232+
} else {
233+
throw new Error(
234+
`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`,
235+
);
207236
}
208-
209-
metafile = result.metafile;
210-
// Remove entryPoint fields from outputs to prevent the internal component styles from being
211-
// treated as initial files. Also mark the entry as a component resource for stat reporting.
212-
Object.values(metafile.outputs).forEach((output) => {
213-
delete output.entryPoint;
214-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
215-
(output as any)['ng-component'] = true;
216-
});
217237
}
218238

239+
const metafile = result.metafile;
240+
// Remove entryPoint fields from outputs to prevent the internal component styles from being
241+
// treated as initial files. Also mark the entry as a component resource for stat reporting.
242+
Object.values(metafile.outputs).forEach((output) => {
243+
delete output.entryPoint;
244+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
245+
(output as any)['ng-component'] = true;
246+
});
247+
219248
return {
220-
errors: result.errors,
221-
warnings: result.warnings,
249+
errors,
250+
warnings,
222251
contents,
223252
outputFiles,
224253
metafile,
225254
referencedFiles,
255+
externalImports: result.externalImports,
256+
initialFiles: new Map(),
226257
};
227258
}
228259
}

packages/angular/build/src/tools/esbuild/angular/jit-plugin-callbacks.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,16 @@ export function setupJitPluginCallbacks(
116116
stylesheetResult = await stylesheetBundler.bundleInline(entry.contents, entry.path);
117117
}
118118

119-
const { contents, outputFiles, errors, warnings, metafile, referencedFiles } =
120-
stylesheetResult;
119+
const { errors, warnings, referencedFiles } = stylesheetResult;
120+
if (stylesheetResult.errors) {
121+
return {
122+
errors,
123+
warnings,
124+
watchFiles: referencedFiles && [...referencedFiles],
125+
};
126+
}
127+
128+
const { contents, outputFiles, metafile } = stylesheetResult;
121129

122130
additionalResultFiles.set(entry.path, { outputFiles, metafile });
123131

0 commit comments

Comments
 (0)