Skip to content

Commit 421b6e7

Browse files
committed
refactor(@angular/build): provide structured application builder result types
The application builder now provides structured output types to its internal consumers. The architect builders themselves and the programmatic API is not changed. These output result types allow for the development server to receive additional information regarding the build and update the active browser appropriately. This functionality is not yet implemented but the additional result types provide the base infrastructure to enable future features. The result types also allow for reduced complexity inside other builders such as i18n extraction and the browser compatibility builder. The usage is not yet fully optimized and will be refined in future changes.
1 parent 37a2138 commit 421b6e7

File tree

18 files changed

+368
-247
lines changed

18 files changed

+368
-247
lines changed

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

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

9-
import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9+
import { BuilderContext } from '@angular-devkit/architect';
1010
import { existsSync } from 'node:fs';
1111
import path from 'node:path';
12-
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
12+
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
1313
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1414
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
1515
import {
@@ -22,6 +22,7 @@ import { deleteOutputDir } from '../../utils/delete-output-dir';
2222
import { shouldWatchRoot } from '../../utils/environment-options';
2323
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
2424
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
25+
import { FullResult, Result, ResultKind, ResultMessage } from './results';
2526

2627
// Watch workspace for package manager changes
2728
const packageWatchFiles = [
@@ -37,9 +38,6 @@ const packageWatchFiles = [
3738
'.pnp.data.json',
3839
];
3940

40-
type BuildActionOutput = (ExecutionResult['outputWithFiles'] | ExecutionResult['output']) &
41-
BuilderOutput;
42-
4341
export async function* runEsBuildBuildAction(
4442
action: (rebuildState?: RebuildState) => Promise<ExecutionResult>,
4543
options: {
@@ -61,7 +59,7 @@ export async function* runEsBuildBuildAction(
6159
colors?: boolean;
6260
jsonLogs?: boolean;
6361
},
64-
): AsyncIterable<BuildActionOutput> {
62+
): AsyncIterable<Result> {
6563
const {
6664
writeToFileSystemFilter,
6765
writeToFileSystem,
@@ -226,10 +224,24 @@ export async function* runEsBuildBuildAction(
226224

227225
async function writeAndEmitOutput(
228226
writeToFileSystem: boolean,
229-
{ outputFiles, output, outputWithFiles, assetFiles }: ExecutionResult,
227+
{
228+
outputFiles,
229+
outputWithFiles,
230+
assetFiles,
231+
externalMetadata,
232+
htmlIndexPath,
233+
htmlBaseHref,
234+
}: ExecutionResult,
230235
outputOptions: NormalizedApplicationBuildOptions['outputOptions'],
231236
writeToFileSystemFilter: ((file: BuildOutputFile) => boolean) | undefined,
232-
): Promise<BuildActionOutput> {
237+
): Promise<Result> {
238+
if (!outputWithFiles.success) {
239+
return {
240+
kind: ResultKind.Failure,
241+
errors: outputWithFiles.errors as ResultMessage[],
242+
};
243+
}
244+
233245
if (writeToFileSystem) {
234246
// Write output files
235247
const outputFilesToWrite = writeToFileSystemFilter
@@ -238,10 +250,37 @@ async function writeAndEmitOutput(
238250

239251
await writeResultFiles(outputFilesToWrite, assetFiles, outputOptions);
240252

241-
return output;
253+
// Currently unused other than indicating success if writing to disk.
254+
return {
255+
kind: ResultKind.Full,
256+
files: {},
257+
};
242258
} else {
243-
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
244-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
245-
return outputWithFiles as any;
259+
const result: FullResult = {
260+
kind: ResultKind.Full,
261+
files: {},
262+
detail: {
263+
externalMetadata,
264+
htmlIndexPath,
265+
htmlBaseHref,
266+
},
267+
};
268+
for (const file of outputWithFiles.assetFiles) {
269+
result.files[file.destination] = {
270+
type: BuildOutputFileType.Browser,
271+
inputPath: file.source,
272+
origin: 'disk',
273+
};
274+
}
275+
for (const file of outputWithFiles.outputFiles) {
276+
result.files[file.path] = {
277+
type: file.type,
278+
contents: file.contents,
279+
origin: 'memory',
280+
hash: file.hash,
281+
};
282+
}
283+
284+
return result;
246285
}
247286
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ export async function executeBuild(
156156
// Watch input index HTML file if configured
157157
if (options.indexHtmlOptions) {
158158
executionResult.extraWatchFiles.push(options.indexHtmlOptions.input);
159+
executionResult.htmlIndexPath = options.indexHtmlOptions.output;
160+
executionResult.htmlBaseHref = options.baseHref;
159161
}
160162

161163
// Perform i18n translation inlining if enabled

packages/angular/build/src/builders/application/index.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ApplicationBuilderInternalOptions,
2121
normalizeOptions,
2222
} from './options';
23+
import { Result, ResultKind } from './results';
2324
import { Schema as ApplicationBuilderOptions } from './schema';
2425

2526
export type { ApplicationBuilderOptions };
@@ -32,7 +33,7 @@ export async function* buildApplicationInternal(
3233
write?: boolean;
3334
},
3435
extensions?: ApplicationBuilderExtensions,
35-
): AsyncIterable<ApplicationBuilderOutput> {
36+
): AsyncIterable<Result> {
3637
const { workspaceRoot, logger, target } = context;
3738

3839
// Check Angular version.
@@ -44,7 +45,9 @@ export async function* buildApplicationInternal(
4445
// Determine project name from builder context target
4546
const projectName = target?.project;
4647
if (!projectName) {
47-
yield { success: false, error: `The 'application' builder requires a target to be specified.` };
48+
context.logger.error(`The 'application' builder requires a target to be specified.`);
49+
// Only the vite-based dev server current uses the errors value
50+
yield { kind: ResultKind.Failure, errors: [] };
4851

4952
return;
5053
}
@@ -57,19 +60,19 @@ export async function* buildApplicationInternal(
5760
if (writeServerBundles) {
5861
const { browser, server } = normalizedOptions.outputOptions;
5962
if (browser === '') {
60-
yield {
61-
success: false,
62-
error: `'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`,
63-
};
63+
context.logger.error(
64+
`'outputPath.browser' cannot be configured to an empty string when SSR is enabled.`,
65+
);
66+
yield { kind: ResultKind.Failure, errors: [] };
6467

6568
return;
6669
}
6770

6871
if (browser === server) {
69-
yield {
70-
success: false,
71-
error: `'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`,
72-
};
72+
context.logger.error(
73+
`'outputPath.browser' and 'outputPath.server' cannot be configured to the same value.`,
74+
);
75+
yield { kind: ResultKind.Failure, errors: [] };
7376

7477
return;
7578
}
@@ -185,7 +188,7 @@ export function buildApplication(
185188
extensions?: ApplicationBuilderExtensions,
186189
): AsyncIterable<ApplicationBuilderOutput>;
187190

188-
export function buildApplication(
191+
export async function* buildApplication(
189192
options: ApplicationBuilderOptions,
190193
context: BuilderContext,
191194
pluginsOrExtensions?: Plugin[] | ApplicationBuilderExtensions,
@@ -199,7 +202,9 @@ export function buildApplication(
199202
extensions = pluginsOrExtensions;
200203
}
201204

202-
return buildApplicationInternal(options, context, undefined, extensions);
205+
for await (const result of buildApplicationInternal(options, context, undefined, extensions)) {
206+
yield { success: result.kind !== ResultKind.Failure };
207+
}
203208
}
204209

205210
export default createBuilder(buildApplication);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
10+
11+
export enum ResultKind {
12+
Failure,
13+
Full,
14+
Incremental,
15+
ComponentUpdate,
16+
}
17+
18+
export type Result = FailureResult | FullResult | IncrementalResult | ComponentUpdateResult;
19+
20+
export interface BaseResult {
21+
kind: ResultKind;
22+
warnings?: ResultMessage[];
23+
duration?: number;
24+
detail?: Record<string, unknown>;
25+
}
26+
27+
export interface FailureResult extends BaseResult {
28+
kind: ResultKind.Failure;
29+
errors: ResultMessage[];
30+
}
31+
32+
export interface FullResult extends BaseResult {
33+
kind: ResultKind.Full;
34+
files: Record<string, ResultFile>;
35+
}
36+
37+
export interface IncrementalResult extends BaseResult {
38+
kind: ResultKind.Incremental;
39+
added: string[];
40+
removed: string[];
41+
modified: string[];
42+
files: Record<string, ResultFile>;
43+
}
44+
45+
export type ResultFile = DiskFile | MemoryFile;
46+
47+
export interface BaseResultFile {
48+
origin: 'memory' | 'disk';
49+
type: BuildOutputFileType;
50+
}
51+
52+
export interface DiskFile extends BaseResultFile {
53+
origin: 'disk';
54+
inputPath: string;
55+
}
56+
57+
export interface MemoryFile extends BaseResultFile {
58+
origin: 'memory';
59+
hash: string;
60+
contents: Uint8Array;
61+
}
62+
63+
export interface ResultMessage {
64+
text: string;
65+
location?: { file: string; line: number; column: number } | null;
66+
notes?: { text: string }[];
67+
}
68+
69+
export interface ComponentUpdateResult extends BaseResult {
70+
kind: ResultKind.ComponentUpdate;
71+
id: string;
72+
type: 'style' | 'template';
73+
content: string;
74+
}

packages/angular/build/src/builders/application/tests/options/output-path_spec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,14 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
286286
},
287287
});
288288

289-
const { result } = await harness.executeOnce();
289+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
290290
expect(result?.success).toBeFalse();
291-
expect(result?.error).toContain(
292-
`'outputPath.browser' cannot be configured to an empty string when SSR is enabled`,
291+
expect(logs).toContain(
292+
jasmine.objectContaining({
293+
message: jasmine.stringMatching(
294+
`'outputPath.browser' cannot be configured to an empty string when SSR is enabled`,
295+
),
296+
}),
293297
);
294298
});
295299
});
@@ -349,10 +353,14 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
349353
},
350354
});
351355

352-
const { result } = await harness.executeOnce();
356+
const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
353357
expect(result?.success).toBeFalse();
354-
expect(result?.error).toContain(
355-
`'outputPath.browser' and 'outputPath.server' cannot be configured to the same value`,
358+
expect(logs).toContain(
359+
jasmine.objectContaining({
360+
message: jasmine.stringMatching(
361+
`'outputPath.browser' and 'outputPath.server' cannot be configured to the same value`,
362+
),
363+
}),
356364
);
357365
});
358366
});

0 commit comments

Comments
 (0)