From 1be2260cad0c701cd2899a1d4e1ba238a21a91e6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 17 Apr 2023 11:04:48 -0400 Subject: [PATCH] refactor(@angular-devkit/build-angular): use helper to bundle all elements of esbuild builder The code to execute the individual bundling elements of the esbuild-based browser application builder has been consolidated within a bundler context helper method. This moves the execution and result merging code into a single location and allows for more flexibility to control which elements should be executed per build. The global styles and global scripts bundling is now fully skipped if the options are not present in the build configuration. --- .../src/builders/browser-esbuild/esbuild.ts | 65 +++++++++++-- .../src/builders/browser-esbuild/index.ts | 94 ++++++++----------- 2 files changed, 95 insertions(+), 64 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/esbuild.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/esbuild.ts index 102c19643397..f913cd70273e 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/esbuild.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/esbuild.ts @@ -22,6 +22,16 @@ import { import { basename, extname, relative } from 'node:path'; import { FileInfo } from '../../utils/index-file/augment-index-html'; +export type BundleContextResult = + | { errors: Message[]; warnings: Message[] } + | { + errors: undefined; + warnings: Message[]; + metafile: Metafile; + outputFiles: OutputFile[]; + initialFiles: FileInfo[]; + }; + /** * Determines if an unknown value is an esbuild BuildFailure error object thrown by esbuild. * @param value A potential esbuild BuildFailure error object. @@ -43,6 +53,50 @@ export class BundlerContext { }; } + static async bundleAll(contexts: Iterable): Promise { + const individualResults = await Promise.all([...contexts].map((context) => context.bundle())); + + // Return directly if only one result + if (individualResults.length === 1) { + return individualResults[0]; + } + + let errors: Message[] | undefined; + const warnings: Message[] = []; + const metafile: Metafile = { inputs: {}, outputs: {} }; + const initialFiles = []; + const outputFiles = []; + for (const result of individualResults) { + warnings.push(...result.warnings); + if (result.errors) { + errors ??= []; + errors.push(...result.errors); + continue; + } + + // Combine metafiles used for the stats option as well as bundle budgets and console output + if (result.metafile) { + metafile.inputs = { ...metafile.inputs, ...result.metafile.inputs }; + metafile.outputs = { ...metafile.outputs, ...result.metafile.outputs }; + } + + initialFiles.push(...result.initialFiles); + outputFiles.push(...result.outputFiles); + } + + if (errors !== undefined) { + return { errors, warnings }; + } + + return { + errors, + warnings, + metafile, + initialFiles, + outputFiles, + }; + } + /** * Executes the esbuild build function and normalizes the build result in the event of a * build failure that results in no output being generated. @@ -52,16 +106,7 @@ export class BundlerContext { * @returns If output files are generated, the full esbuild BuildResult; if not, the * warnings and errors for the attempted build. */ - async bundle(): Promise< - | { errors: Message[]; warnings: Message[] } - | { - errors: undefined; - warnings: Message[]; - metafile: Metafile; - outputFiles: OutputFile[]; - initialFiles: FileInfo[]; - } - > { + async bundle(): Promise { let result; try { if (this.#esbuildContext) { diff --git a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts index b14e2cfdf58e..e595e0247538 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts @@ -108,6 +108,9 @@ async function execute( const target = transformSupportedBrowsersToTargets(browsers); // Reuse rebuild state or create new bundle contexts for code and global stylesheets + const bundlerContexts = []; + + // Application code const codeBundleCache = options.watch ? rebuildState?.codeBundleCache ?? new SourceFileCache() : undefined; @@ -118,37 +121,38 @@ async function execute( !!options.watch, createCodeBundleOptions(options, target, browsers, codeBundleCache), ); - const globalStylesBundleContext = - rebuildState?.globalStylesRebuild ?? - new BundlerContext( + bundlerContexts.push(codeBundleContext); + // Global Stylesheets + let globalStylesBundleContext; + if (options.globalStyles.length > 0) { + globalStylesBundleContext = + rebuildState?.globalStylesRebuild ?? + new BundlerContext( + workspaceRoot, + !!options.watch, + createGlobalStylesBundleOptions( + options, + target, + browsers, + codeBundleCache?.loadResultCache, + ), + ); + bundlerContexts.push(globalStylesBundleContext); + } + // Global Scripts + if (options.globalScripts.length > 0) { + const globalScriptsBundleContext = new BundlerContext( workspaceRoot, !!options.watch, - createGlobalStylesBundleOptions(options, target, browsers, codeBundleCache?.loadResultCache), + createGlobalScriptsBundleOptions(options), ); + bundlerContexts.push(globalScriptsBundleContext); + } - const globalScriptsBundleContext = new BundlerContext( - workspaceRoot, - !!options.watch, - createGlobalScriptsBundleOptions(options), - ); - - const [codeResults, styleResults, scriptResults] = await Promise.all([ - // Execute esbuild to bundle the application code - codeBundleContext.bundle(), - // Execute esbuild to bundle the global stylesheets - globalStylesBundleContext.bundle(), - globalScriptsBundleContext.bundle(), - ]); + const bundlingResult = await BundlerContext.bundleAll(bundlerContexts); // Log all warnings and errors generated during bundling - await logMessages(context, { - errors: [ - ...(codeResults.errors || []), - ...(styleResults.errors || []), - ...(scriptResults.errors || []), - ], - warnings: [...codeResults.warnings, ...styleResults.warnings, ...scriptResults.warnings], - }); + await logMessages(context, bundlingResult); const executionResult = new ExecutionResult( codeBundleContext, @@ -157,40 +161,22 @@ async function execute( ); // Return if the bundling has errors - if (codeResults.errors || styleResults.errors || scriptResults.errors) { + if (bundlingResult.errors) { return executionResult; } - // Filter global stylesheet initial files - styleResults.initialFiles = styleResults.initialFiles.filter( - ({ name }) => options.globalStyles.find((style) => style.name === name)?.initial, - ); + // Filter global stylesheet initial files. Currently all initial CSS files are from the global styles option. + if (options.globalScripts.length > 0) { + bundlingResult.initialFiles = bundlingResult.initialFiles.filter( + ({ file, name }) => + !file.endsWith('.css') || + options.globalStyles.find((style) => style.name === name)?.initial, + ); + } - // Combine the bundling output files - const initialFiles: FileInfo[] = [ - ...codeResults.initialFiles, - ...styleResults.initialFiles, - ...scriptResults.initialFiles, - ]; - executionResult.outputFiles.push( - ...codeResults.outputFiles, - ...styleResults.outputFiles, - ...scriptResults.outputFiles, - ); + const { metafile, initialFiles, outputFiles } = bundlingResult; - // Combine metafiles used for the stats option as well as bundle budgets and console output - const metafile = { - inputs: { - ...codeResults.metafile?.inputs, - ...styleResults.metafile?.inputs, - ...scriptResults.metafile?.inputs, - }, - outputs: { - ...codeResults.metafile?.outputs, - ...styleResults.metafile?.outputs, - ...scriptResults.metafile?.outputs, - }, - }; + executionResult.outputFiles.push(...outputFiles); // Check metafile for CommonJS module usage if optimizing scripts if (optimizationOptions.scripts) {