Skip to content

Commit 0ac5f27

Browse files
clydinangular-robot[bot]
authored andcommitted
refactor(@angular-devkit/build-angular): support in-memory results for esbuild builder
To provide support for development server integration, the esbuild-based builder can now be setup to provide in-memory file results at the completion of a build. This applies to both watch and non-watch modes. The result output object structure is not currently considered part of the public API and is currently only intended to be used by other builders within the package.
1 parent c9e84d0 commit 0ac5f27

File tree

1 file changed

+89
-70
lines changed
  • packages/angular_devkit/build_angular/src/builders/browser-esbuild

1 file changed

+89
-70
lines changed

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

+89-70
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,30 @@ interface RebuildState {
4242
* Represents the result of a single builder execute call.
4343
*/
4444
class ExecutionResult {
45+
readonly outputFiles: OutputFile[] = [];
46+
readonly assetFiles: { source: string; destination: string }[] = [];
47+
4548
constructor(
46-
private success: boolean,
4749
private codeRebuild?: BundlerContext,
4850
private globalStylesRebuild?: BundlerContext,
4951
private codeBundleCache?: SourceFileCache,
5052
) {}
5153

54+
addOutputFile(path: string, content: string): void {
55+
this.outputFiles.push(createOutputFileFromText(path, content));
56+
}
57+
5258
get output() {
5359
return {
54-
success: this.success,
60+
success: this.outputFiles.length > 0,
61+
};
62+
}
63+
64+
get outputWithFiles() {
65+
return {
66+
success: this.outputFiles.length > 0,
67+
outputFiles: this.outputFiles,
68+
assetFiles: this.assetFiles,
5569
};
5670
}
5771

@@ -67,7 +81,7 @@ class ExecutionResult {
6781
}
6882

6983
async dispose(): Promise<void> {
70-
await Promise.all([this.codeRebuild?.dispose(), this.globalStylesRebuild?.dispose()]);
84+
await Promise.allSettled([this.codeRebuild?.dispose(), this.globalStylesRebuild?.dispose()]);
7185
}
7286
}
7387

@@ -82,7 +96,6 @@ async function execute(
8296
projectRoot,
8397
workspaceRoot,
8498
optimizationOptions,
85-
outputPath,
8699
assets,
87100
serviceWorkerOptions,
88101
indexHtmlOptions,
@@ -123,24 +136,15 @@ async function execute(
123136
warnings: [...codeResults.warnings, ...styleResults.warnings],
124137
});
125138

126-
// Return if the bundling failed to generate output files or there are errors
127-
if (codeResults.errors) {
128-
return new ExecutionResult(
129-
false,
130-
codeBundleContext,
131-
globalStylesBundleContext,
132-
codeBundleCache,
133-
);
134-
}
139+
const executionResult = new ExecutionResult(
140+
codeBundleContext,
141+
globalStylesBundleContext,
142+
codeBundleCache,
143+
);
135144

136-
// Return if the global stylesheet bundling has errors
137-
if (styleResults.errors) {
138-
return new ExecutionResult(
139-
false,
140-
codeBundleContext,
141-
globalStylesBundleContext,
142-
codeBundleCache,
143-
);
145+
// Return if the bundling has errors
146+
if (codeResults.errors || styleResults.errors) {
147+
return executionResult;
144148
}
145149

146150
// Filter global stylesheet initial files
@@ -150,7 +154,7 @@ async function execute(
150154

151155
// Combine the bundling output files
152156
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
153-
const outputFiles: OutputFile[] = [...codeResults.outputFiles, ...styleResults.outputFiles];
157+
executionResult.outputFiles.push(...codeResults.outputFiles, ...styleResults.outputFiles);
154158

155159
// Combine metafiles used for the stats option as well as bundle budgets and console output
156160
const metafile = {
@@ -180,7 +184,7 @@ async function execute(
180184
indexHtmlGenerator.readAsset = async function (filePath: string): Promise<string> {
181185
// Remove leading directory separator
182186
const relativefilePath = path.relative(virtualOutputPath, filePath);
183-
const file = outputFiles.find((file) => file.path === relativefilePath);
187+
const file = executionResult.outputFiles.find((file) => file.path === relativefilePath);
184188
if (file) {
185189
return file.text;
186190
}
@@ -202,29 +206,26 @@ async function execute(
202206
context.logger.warn(warning);
203207
}
204208

205-
outputFiles.push(createOutputFileFromText(indexHtmlOptions.output, content));
209+
executionResult.addOutputFile(indexHtmlOptions.output, content);
206210
}
207211

208212
// Copy assets
209-
let assetFiles;
210213
if (assets) {
211214
// The webpack copy assets helper is used with no base paths defined. This prevents the helper
212215
// from directly writing to disk. This should eventually be replaced with a more optimized helper.
213-
assetFiles = await copyAssets(assets, [], workspaceRoot);
216+
executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot)));
214217
}
215218

216219
// Write metafile if stats option is enabled
217220
if (options.stats) {
218-
outputFiles.push(createOutputFileFromText('stats.json', JSON.stringify(metafile, null, 2)));
221+
executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2));
219222
}
220223

221224
// Extract and write licenses for used packages
222225
if (options.extractLicenses) {
223-
outputFiles.push(
224-
createOutputFileFromText(
225-
'3rdpartylicenses.txt',
226-
await extractLicenses(metafile, workspaceRoot),
227-
),
226+
executionResult.addOutputFile(
227+
'3rdpartylicenses.txt',
228+
await extractLicenses(metafile, workspaceRoot),
228229
);
229230
}
230231

@@ -235,31 +236,22 @@ async function execute(
235236
workspaceRoot,
236237
serviceWorkerOptions,
237238
options.baseHref || '/',
238-
outputFiles,
239-
assetFiles || [],
239+
executionResult.outputFiles,
240+
executionResult.assetFiles,
240241
);
241-
outputFiles.push(createOutputFileFromText('ngsw.json', serviceWorkerResult.manifest));
242-
assetFiles ??= [];
243-
assetFiles.push(...serviceWorkerResult.assetFiles);
242+
executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest);
243+
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
244244
} catch (error) {
245245
context.logger.error(error instanceof Error ? error.message : `${error}`);
246246

247-
return new ExecutionResult(
248-
false,
249-
codeBundleContext,
250-
globalStylesBundleContext,
251-
codeBundleCache,
252-
);
247+
return executionResult;
253248
}
254249
}
255250

256-
// Write output files
257-
await writeResultFiles(outputFiles, assetFiles, outputPath);
258-
259251
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
260252
context.logger.info(`Complete. [${buildTime.toFixed(3)} seconds]`);
261253

262-
return new ExecutionResult(true, codeBundleContext, globalStylesBundleContext, codeBundleCache);
254+
return executionResult;
263255
}
264256

265257
async function writeResultFiles(
@@ -521,16 +513,19 @@ function createGlobalStylesBundleOptions(
521513
/**
522514
* Main execution function for the esbuild-based application builder.
523515
* The options are compatible with the Webpack-based builder.
524-
* @param initialOptions The browser builder options to use when setting up the application build
516+
* @param userOptions The browser builder options to use when setting up the application build
525517
* @param context The Architect builder context object
526518
* @returns An async iterable with the builder result output
527519
*/
528520
export async function* buildEsbuildBrowser(
529-
initialOptions: BrowserBuilderOptions,
521+
userOptions: BrowserBuilderOptions,
530522
context: BuilderContext,
531-
): AsyncIterable<BuilderOutput> {
523+
infrastructureSettings?: {
524+
write?: boolean;
525+
},
526+
): AsyncIterable<BuilderOutput & { outputFiles?: OutputFile[] }> {
532527
// Inform user of experimental status of builder and options
533-
logExperimentalWarnings(initialOptions, context);
528+
logExperimentalWarnings(userOptions, context);
534529

535530
// Determine project name from builder context target
536531
const projectName = context.target?.project;
@@ -540,36 +535,50 @@ export async function* buildEsbuildBrowser(
540535
return;
541536
}
542537

543-
const normalizedOptions = await normalizeOptions(context, projectName, initialOptions);
538+
const normalizedOptions = await normalizeOptions(context, projectName, userOptions);
539+
// Writing the result to the filesystem is the default behavior
540+
const shouldWriteResult = infrastructureSettings?.write !== false;
544541

545-
// Clean output path if enabled
546-
if (initialOptions.deleteOutputPath) {
547-
deleteOutputDir(normalizedOptions.workspaceRoot, initialOptions.outputPath);
548-
}
542+
if (shouldWriteResult) {
543+
// Clean output path if enabled
544+
if (userOptions.deleteOutputPath) {
545+
deleteOutputDir(normalizedOptions.workspaceRoot, userOptions.outputPath);
546+
}
549547

550-
// Create output directory if needed
551-
try {
552-
await fs.mkdir(normalizedOptions.outputPath, { recursive: true });
553-
} catch (e) {
554-
assertIsError(e);
555-
context.logger.error('Unable to create output directory: ' + e.message);
548+
// Create output directory if needed
549+
try {
550+
await fs.mkdir(normalizedOptions.outputPath, { recursive: true });
551+
} catch (e) {
552+
assertIsError(e);
553+
context.logger.error('Unable to create output directory: ' + e.message);
556554

557-
return;
555+
return;
556+
}
558557
}
559558

560559
// Initial build
561560
let result: ExecutionResult;
562561
try {
563562
result = await execute(normalizedOptions, context);
564-
yield result.output;
563+
564+
if (shouldWriteResult) {
565+
// Write output files
566+
await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath);
567+
568+
yield result.output;
569+
} else {
570+
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
571+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
572+
yield result.outputWithFiles as any;
573+
}
565574

566575
// Finish if watch mode is not enabled
567-
if (!initialOptions.watch) {
576+
if (!userOptions.watch) {
568577
return;
569578
}
570579
} finally {
571580
// Ensure Sass workers are shutdown if not watching
572-
if (!initialOptions.watch) {
581+
if (!userOptions.watch) {
573582
shutdownSassWorkerPool();
574583
}
575584
}
@@ -578,8 +587,8 @@ export async function* buildEsbuildBrowser(
578587

579588
// Setup a watcher
580589
const watcher = createWatcher({
581-
polling: typeof initialOptions.poll === 'number',
582-
interval: initialOptions.poll,
590+
polling: typeof userOptions.poll === 'number',
591+
interval: userOptions.poll,
583592
// Ignore the output and cache paths to avoid infinite rebuild cycles
584593
ignored: [normalizedOptions.outputPath, normalizedOptions.cacheOptions.basePath],
585594
});
@@ -598,12 +607,22 @@ export async function* buildEsbuildBrowser(
598607
for await (const changes of watcher) {
599608
context.logger.info('Changes detected. Rebuilding...');
600609

601-
if (initialOptions.verbose) {
610+
if (userOptions.verbose) {
602611
context.logger.info(changes.toDebugString());
603612
}
604613

605614
result = await execute(normalizedOptions, context, result.createRebuildState(changes));
606-
yield result.output;
615+
616+
if (shouldWriteResult) {
617+
// Write output files
618+
await writeResultFiles(result.outputFiles, result.assetFiles, normalizedOptions.outputPath);
619+
620+
yield result.output;
621+
} else {
622+
// Requires casting due to unneeded `JsonObject` requirement. Remove once fixed.
623+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
624+
yield result.outputWithFiles as any;
625+
}
607626
}
608627
} finally {
609628
// Stop the watcher

0 commit comments

Comments
 (0)