Skip to content

Commit 7b9d99c

Browse files
clydinangular-robot[bot]
authored andcommitted
refactor(@angular-devkit/build-angular): use esbuild 0.17 incremental API in esbuild builder
Due to the update of the experimental esbuild-based browser application builder to use esbuild 0.17, the watch mode has been changed to use the new incremental API. The previous API has been removed from esbuild. The new API involves creating a build context object that can then be used to perform rebuilds of the configured application bundler as needed. All watch mode usage has been updated to use this new approach. An effort was made to minimize the amount of changes made to support this API update and limit the changeset. However, further refactoring will be possible as additional capabilities are added in the future. esbuild API reference: https://esbuild.github.io/api/#rebuild
1 parent b5dcb29 commit 7b9d99c

File tree

7 files changed

+295
-95
lines changed

7 files changed

+295
-95
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@
141141
"cross-env": "^7.0.3",
142142
"css-loader": "6.7.3",
143143
"debug": "^4.1.1",
144-
"esbuild": "0.16.17",
145-
"esbuild-wasm": "0.16.17",
144+
"esbuild": "0.17.2",
145+
"esbuild-wasm": "0.17.2",
146146
"eslint": "8.31.0",
147147
"eslint-config-prettier": "8.6.0",
148148
"eslint-plugin-header": "3.1.1",

packages/angular_devkit/build_angular/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"copy-webpack-plugin": "11.0.0",
3232
"critters": "0.0.16",
3333
"css-loader": "6.7.3",
34-
"esbuild-wasm": "0.16.17",
34+
"esbuild-wasm": "0.17.2",
3535
"glob": "8.0.3",
3636
"https-proxy-agent": "5.0.1",
3737
"inquirer": "8.2.4",
@@ -67,7 +67,7 @@
6767
"webpack-subresource-integrity": "5.1.0"
6868
},
6969
"optionalDependencies": {
70-
"esbuild": "0.16.17"
70+
"esbuild": "0.17.2"
7171
},
7272
"peerDependencies": {
7373
"@angular/compiler-cli": "^15.0.0 || ^15.2.0-next",

packages/angular_devkit/build_angular/src/builders/browser-esbuild/compiler-plugin.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,9 @@ export function createCompilerPlugin(
277277
);
278278

279279
const { contents, resourceFiles, errors, warnings } = stylesheetResult;
280-
(result.errors ??= []).push(...errors);
280+
if (errors) {
281+
(result.errors ??= []).push(...errors);
282+
}
281283
(result.warnings ??= []).push(...warnings);
282284
stylesheetResourceFiles.push(...resourceFiles);
283285
if (stylesheetResult.metafile) {

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

Lines changed: 97 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
import { BuilderContext } from '@angular-devkit/architect';
1010
import {
11+
BuildContext,
1112
BuildFailure,
12-
BuildInvalidate,
1313
BuildOptions,
14-
BuildResult,
14+
Message,
15+
Metafile,
1516
OutputFile,
1617
PartialMessage,
1718
build,
19+
context,
1820
formatMessages,
1921
} from 'esbuild';
2022
import { basename, extname, relative } from 'node:path';
@@ -29,76 +31,116 @@ export function isEsBuildFailure(value: unknown): value is BuildFailure {
2931
return !!value && typeof value === 'object' && 'errors' in value && 'warnings' in value;
3032
}
3133

32-
/**
33-
* Executes the esbuild build function and normalizes the build result in the event of a
34-
* build failure that results in no output being generated.
35-
* All builds use the `write` option with a value of `false` to allow for the output files
36-
* build result array to be populated.
37-
*
38-
* @param optionsOrInvalidate The esbuild options object to use when building or the invalidate object
39-
* returned from an incremental build to perform an additional incremental build.
40-
* @returns If output files are generated, the full esbuild BuildResult; if not, the
41-
* warnings and errors for the attempted build.
42-
*/
43-
export async function bundle(
44-
workspaceRoot: string,
45-
optionsOrInvalidate: BuildOptions | BuildInvalidate,
46-
): Promise<
47-
| (BuildResult & { outputFiles: OutputFile[]; initialFiles: FileInfo[] })
48-
| (BuildFailure & { outputFiles?: never })
49-
> {
50-
let result;
51-
try {
52-
if (typeof optionsOrInvalidate === 'function') {
53-
result = (await optionsOrInvalidate()) as BuildResult & { outputFiles: OutputFile[] };
54-
} else {
55-
result = await build({
56-
...optionsOrInvalidate,
57-
metafile: true,
58-
write: false,
59-
});
34+
export class BundlerContext {
35+
#esbuildContext?: BuildContext<{ metafile: true; write: false }>;
36+
#esbuildOptions: BuildOptions & { metafile: true; write: false };
37+
38+
constructor(private workspaceRoot: string, private incremental: boolean, options: BuildOptions) {
39+
this.#esbuildOptions = {
40+
...options,
41+
metafile: true,
42+
write: false,
43+
};
44+
}
45+
46+
/**
47+
* Executes the esbuild build function and normalizes the build result in the event of a
48+
* build failure that results in no output being generated.
49+
* All builds use the `write` option with a value of `false` to allow for the output files
50+
* build result array to be populated.
51+
*
52+
* @returns If output files are generated, the full esbuild BuildResult; if not, the
53+
* warnings and errors for the attempted build.
54+
*/
55+
async bundle(): Promise<
56+
| { errors: Message[]; warnings: Message[] }
57+
| {
58+
errors: undefined;
59+
warnings: Message[];
60+
metafile: Metafile;
61+
outputFiles: OutputFile[];
62+
initialFiles: FileInfo[];
63+
}
64+
> {
65+
let result;
66+
try {
67+
if (this.#esbuildContext) {
68+
// Rebuild using the existing incremental build context
69+
result = await this.#esbuildContext.rebuild();
70+
} else if (this.incremental) {
71+
// Create an incremental build context and perform the first build.
72+
// Context creation does not perform a build.
73+
this.#esbuildContext = await context(this.#esbuildOptions);
74+
result = await this.#esbuildContext.rebuild();
75+
} else {
76+
// For non-incremental builds, perform a single build
77+
result = await build(this.#esbuildOptions);
78+
}
79+
} catch (failure) {
80+
// Build failures will throw an exception which contains errors/warnings
81+
if (isEsBuildFailure(failure)) {
82+
return failure;
83+
} else {
84+
throw failure;
85+
}
6086
}
61-
} catch (failure) {
62-
// Build failures will throw an exception which contains errors/warnings
63-
if (isEsBuildFailure(failure)) {
64-
return failure;
65-
} else {
66-
throw failure;
87+
88+
// Return if the build encountered any errors
89+
if (result.errors.length) {
90+
return {
91+
errors: result.errors,
92+
warnings: result.warnings,
93+
};
6794
}
68-
}
6995

70-
const initialFiles: FileInfo[] = [];
71-
for (const outputFile of result.outputFiles) {
72-
// Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
73-
const relativeFilePath = relative(workspaceRoot, outputFile.path);
74-
const entryPoint = result.metafile?.outputs[relativeFilePath]?.entryPoint;
96+
// Find all initial files
97+
const initialFiles: FileInfo[] = [];
98+
for (const outputFile of result.outputFiles) {
99+
// Entries in the metafile are relative to the `absWorkingDir` option which is set to the workspaceRoot
100+
const relativeFilePath = relative(this.workspaceRoot, outputFile.path);
101+
const entryPoint = result.metafile?.outputs[relativeFilePath]?.entryPoint;
75102

76-
outputFile.path = relativeFilePath;
103+
outputFile.path = relativeFilePath;
77104

78-
if (entryPoint) {
79-
// An entryPoint value indicates an initial file
80-
initialFiles.push({
81-
file: outputFile.path,
82-
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
83-
name: basename(outputFile.path).split('.')[0],
84-
extension: extname(outputFile.path),
85-
});
105+
if (entryPoint) {
106+
// An entryPoint value indicates an initial file
107+
initialFiles.push({
108+
file: outputFile.path,
109+
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
110+
name: basename(outputFile.path).split('.')[0],
111+
extension: extname(outputFile.path),
112+
});
113+
}
86114
}
115+
116+
// Return the successful build results
117+
return { ...result, initialFiles, errors: undefined };
87118
}
88119

89-
return { ...result, initialFiles };
120+
/**
121+
* Disposes incremental build resources present in the context.
122+
*
123+
* @returns A promise that resolves when disposal is complete.
124+
*/
125+
async dispose(): Promise<void> {
126+
try {
127+
return this.#esbuildContext?.dispose();
128+
} finally {
129+
this.#esbuildContext = undefined;
130+
}
131+
}
90132
}
91133

92134
export async function logMessages(
93135
context: BuilderContext,
94-
{ errors, warnings }: { errors: PartialMessage[]; warnings: PartialMessage[] },
136+
{ errors, warnings }: { errors?: PartialMessage[]; warnings?: PartialMessage[] },
95137
): Promise<void> {
96-
if (warnings.length) {
138+
if (warnings?.length) {
97139
const warningMessages = await formatMessages(warnings, { kind: 'warning', color: true });
98140
context.logger.warn(warningMessages.join('\n'));
99141
}
100142

101-
if (errors.length) {
143+
if (errors?.length) {
102144
const errorMessages = await formatMessages(errors, { kind: 'error', color: true });
103145
context.logger.error(errorMessages.join('\n'));
104146
}

0 commit comments

Comments
 (0)