diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json index 9289dfc27248..0d4d97ad1edd 100644 --- a/goldens/circular-deps/packages.json +++ b/goldens/circular-deps/packages.json @@ -3,6 +3,13 @@ "packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts", "packages/angular_devkit/build_angular/src/builders/dev-server/options.ts" ], + [ + "packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts", + "packages/angular/build/src/tools/esbuild/bundler-context.ts", + "packages/angular/build/src/tools/esbuild/utils.ts", + "packages/angular/build/src/utils/server-rendering/manifest.ts", + "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" + ], [ "packages/angular/build/src/tools/esbuild/bundler-context.ts", "packages/angular/build/src/tools/esbuild/utils.ts" diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index ef3bb68d4184..bc78be6aa3e5 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -15,7 +15,11 @@ import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execu import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker'; import { extractLicenses } from '../../tools/esbuild/license-extractor'; import { profileAsync } from '../../tools/esbuild/profiling'; -import { calculateEstimatedTransferSizes, logBuildStats } from '../../tools/esbuild/utils'; +import { + calculateEstimatedTransferSizes, + logBuildStats, + transformSupportedBrowsersToTargets, +} from '../../tools/esbuild/utils'; import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator'; import { shouldOptimizeChunks } from '../../utils/environment-options'; import { resolveAssets } from '../../utils/resolve-assets'; @@ -29,7 +33,7 @@ import { executePostBundleSteps } from './execute-post-bundle'; import { inlineI18n, loadActiveTranslations } from './i18n'; import { NormalizedApplicationBuildOptions } from './options'; import { OutputMode } from './schema'; -import { setupBundlerContexts } from './setup-bundling'; +import { createComponentStyleBundler, setupBundlerContexts } from './setup-bundling'; // eslint-disable-next-line max-lines-per-function export async function executeBuild( @@ -63,12 +67,18 @@ export async function executeBuild( } // Reuse rebuild state or create new bundle contexts for code and global stylesheets - let bundlerContexts = rebuildState?.rebuildContexts; - const codeBundleCache = - rebuildState?.codeBundleCache ?? - new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); - if (bundlerContexts === undefined) { - bundlerContexts = setupBundlerContexts(options, browsers, codeBundleCache); + let bundlerContexts; + let componentStyleBundler; + let codeBundleCache; + if (rebuildState) { + bundlerContexts = rebuildState.rebuildContexts; + componentStyleBundler = rebuildState.componentStyleBundler; + codeBundleCache = rebuildState.codeBundleCache; + } else { + const target = transformSupportedBrowsersToTargets(browsers); + codeBundleCache = new SourceFileCache(cacheOptions.enabled ? cacheOptions.path : undefined); + componentStyleBundler = createComponentStyleBundler(options, target); + bundlerContexts = setupBundlerContexts(options, target, codeBundleCache, componentStyleBundler); } let bundlingResult = await BundlerContext.bundleAll( @@ -85,7 +95,11 @@ export async function executeBuild( ); } - const executionResult = new ExecutionResult(bundlerContexts, codeBundleCache); + const executionResult = new ExecutionResult( + bundlerContexts, + componentStyleBundler, + codeBundleCache, + ); executionResult.addWarnings(bundlingResult.warnings); // Return if the bundling has errors diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index eb4012594f90..8f38137ff3ee 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets'; import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache'; import { createBrowserCodeBundleOptions, @@ -17,10 +18,7 @@ import { import { BundlerContext } from '../../tools/esbuild/bundler-context'; import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts'; import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles'; -import { - getSupportedNodeTargets, - transformSupportedBrowsersToTargets, -} from '../../tools/esbuild/utils'; +import { getSupportedNodeTargets } from '../../tools/esbuild/utils'; import { NormalizedApplicationBuildOptions } from './options'; /** @@ -33,8 +31,9 @@ import { NormalizedApplicationBuildOptions } from './options'; */ export function setupBundlerContexts( options: NormalizedApplicationBuildOptions, - browsers: string[], + target: string[], codeBundleCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, ): BundlerContext[] { const { outputMode, @@ -45,7 +44,6 @@ export function setupBundlerContexts( workspaceRoot, watch = false, } = options; - const target = transformSupportedBrowsersToTargets(browsers); const bundlerContexts = []; // Browser application code @@ -53,7 +51,7 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createBrowserCodeBundleOptions(options, target, codeBundleCache), + createBrowserCodeBundleOptions(options, target, codeBundleCache, stylesheetBundler), ), ); @@ -62,6 +60,7 @@ export function setupBundlerContexts( options, target, codeBundleCache, + stylesheetBundler, ); if (browserPolyfillBundleOptions) { bundlerContexts.push(new BundlerContext(workspaceRoot, watch, browserPolyfillBundleOptions)); @@ -99,7 +98,7 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache), + createServerMainCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), ), ); @@ -109,7 +108,7 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, watch, - createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache), + createSsrEntryCodeBundleOptions(options, nodeTargets, codeBundleCache, stylesheetBundler), ), ); } @@ -128,3 +127,51 @@ export function setupBundlerContexts( return bundlerContexts; } + +export function createComponentStyleBundler( + options: NormalizedApplicationBuildOptions, + target: string[], +): ComponentStylesheetBundler { + const { + workspaceRoot, + optimizationOptions, + sourcemapOptions, + outputNames, + externalDependencies, + preserveSymlinks, + stylePreprocessorOptions, + inlineStyleLanguage, + cacheOptions, + tailwindConfiguration, + postcssConfiguration, + publicPath, + } = options; + const incremental = !!options.watch; + + return new ComponentStylesheetBundler( + { + workspaceRoot, + inlineFonts: !!optimizationOptions.fonts.inline, + optimization: !!optimizationOptions.styles.minify, + sourcemap: + // Hidden component stylesheet sourcemaps are inaccessible which is effectively + // the same as being disabled. Disabling has the advantage of avoiding the overhead + // of sourcemap processing. + sourcemapOptions.styles && !sourcemapOptions.hidden ? 'linked' : false, + outputNames, + includePaths: stylePreprocessorOptions?.includePaths, + // string[] | undefined' is not assignable to type '(Version | DeprecationOrId)[] | undefined'. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sass: stylePreprocessorOptions?.sass as any, + externalDependencies, + target, + preserveSymlinks, + tailwindConfiguration, + postcssConfiguration, + cacheOptions, + publicPath, + }, + inlineStyleLanguage, + incremental, + ); +} diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index 544f03fe31b4..fda63af4b56c 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -532,7 +532,6 @@ export function createCompilerPlugin( build.onDispose(() => { sharedTSCompilationState?.dispose(); - void stylesheetBundler.dispose(); void compilation.close?.(); void cacheStore?.close(); }); diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index 1fa63bdfb4e8..05b61a819fa0 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -18,6 +18,7 @@ import { SERVER_APP_MANIFEST_FILENAME, } from '../../utils/server-rendering/manifest'; import { createCompilerPlugin } from './angular/compiler-plugin'; +import { ComponentStylesheetBundler } from './angular/component-stylesheets'; import { SourceFileCache } from './angular/source-file-cache'; import { BundlerOptionsFactory } from './bundler-context'; import { createCompilerPluginOptions } from './compiler-plugin-options'; @@ -34,15 +35,12 @@ import { createWasmPlugin } from './wasm-plugin'; export function createBrowserCodeBundleOptions( options: NormalizedApplicationBuildOptions, target: string[], - sourceFileCache?: SourceFileCache, + sourceFileCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, ): BuildOptions { const { entryPoints, outputNames, polyfills } = options; - const { pluginOptions, stylesheetBundler } = createCompilerPluginOptions( - options, - target, - sourceFileCache, - ); + const pluginOptions = createCompilerPluginOptions(options, sourceFileCache); const zoneless = isZonelessApp(polyfills); @@ -100,7 +98,8 @@ export function createBrowserCodeBundleOptions( export function createBrowserPolyfillBundleOptions( options: NormalizedApplicationBuildOptions, target: string[], - sourceFileCache?: SourceFileCache, + sourceFileCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, ): BuildOptions | BundlerOptionsFactory | undefined { const namespace = 'angular:polyfills'; const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions( @@ -134,9 +133,9 @@ export function createBrowserPolyfillBundleOptions( // Only add the Angular TypeScript compiler if TypeScript files are provided in the polyfills if (hasTypeScriptEntries) { buildOptions.plugins ??= []; - const { pluginOptions, stylesheetBundler } = createCompilerPluginOptions( + const pluginOptions = createCompilerPluginOptions( options, - target, + sourceFileCache, ); buildOptions.plugins.push( @@ -228,6 +227,7 @@ export function createServerMainCodeBundleOptions( options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, ): BuildOptions { const { serverEntryPoint: mainServerEntryPoint, @@ -243,11 +243,7 @@ export function createServerMainCodeBundleOptions( 'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.', ); - const { pluginOptions, stylesheetBundler } = createCompilerPluginOptions( - options, - target, - sourceFileCache, - ); + const pluginOptions = createCompilerPluginOptions(options, sourceFileCache); const mainServerNamespace = 'angular:main-server'; const mainServerInjectPolyfillsNamespace = 'angular:main-server-inject-polyfills'; @@ -374,6 +370,7 @@ export function createSsrEntryCodeBundleOptions( options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache, + stylesheetBundler: ComponentStylesheetBundler, ): BuildOptions { const { workspaceRoot, ssrOptions, externalPackages } = options; const serverEntryPoint = ssrOptions?.entry; @@ -382,11 +379,7 @@ export function createSsrEntryCodeBundleOptions( 'createSsrEntryCodeBundleOptions should not be called without a defined serverEntryPoint.', ); - const { pluginOptions, stylesheetBundler } = createCompilerPluginOptions( - options, - target, - sourceFileCache, - ); + const pluginOptions = createCompilerPluginOptions(options, sourceFileCache); const ssrEntryNamespace = 'angular:ssr-entry'; const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest'; diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index 10f285e795c9..5cc37c139e5f 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -9,6 +9,7 @@ import type { Message, PartialMessage } from 'esbuild'; import { normalize } from 'node:path'; import type { ChangedFiles } from '../../tools/esbuild/watcher'; +import type { ComponentStylesheetBundler } from './angular/component-stylesheets'; import type { SourceFileCache } from './angular/source-file-cache'; import type { BuildOutputFile, BuildOutputFileType, BundlerContext } from './bundler-context'; import { createOutputFile } from './utils'; @@ -20,6 +21,7 @@ export interface BuildOutputAsset { export interface RebuildState { rebuildContexts: BundlerContext[]; + componentStyleBundler: ComponentStylesheetBundler; codeBundleCache?: SourceFileCache; fileChanges: ChangedFiles; previousOutputHashes: Map; @@ -50,6 +52,7 @@ export class ExecutionResult { constructor( private rebuildContexts: BundlerContext[], + private componentStyleBundler: ComponentStylesheetBundler, private codeBundleCache?: SourceFileCache, ) {} @@ -158,6 +161,7 @@ export class ExecutionResult { return { rebuildContexts: this.rebuildContexts, codeBundleCache: this.codeBundleCache, + componentStyleBundler: this.componentStyleBundler, fileChanges, previousOutputHashes: new Map(this.outputFiles.map((file) => [file.path, file.hash])), }; @@ -177,5 +181,6 @@ export class ExecutionResult { async dispose(): Promise { await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose())); + await this.componentStyleBundler.dispose(); } } diff --git a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts index 568e6f8e0b85..192a7a142acf 100644 --- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts +++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts @@ -15,74 +15,30 @@ type CreateCompilerPluginParameters = Parameters; export function createCompilerPluginOptions( options: NormalizedApplicationBuildOptions, - target: string[], sourceFileCache?: SourceFileCache, -): { - pluginOptions: CreateCompilerPluginParameters[0]; - stylesheetBundler: ComponentStylesheetBundler; -} { +): CreateCompilerPluginParameters[0] { const { - workspaceRoot, - optimizationOptions, sourcemapOptions, tsconfig, - outputNames, fileReplacements, - externalDependencies, - preserveSymlinks, - stylePreprocessorOptions, advancedOptimizations, - inlineStyleLanguage, jit, - cacheOptions, - tailwindConfiguration, - postcssConfiguration, - publicPath, externalRuntimeStyles, instrumentForCoverage, } = options; const incremental = !!options.watch; return { - // JS/TS options - pluginOptions: { - sourcemap: !!sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true), - thirdPartySourcemaps: sourcemapOptions.vendor, - tsconfig, - jit, - advancedOptimizations, - fileReplacements, - sourceFileCache, - loadResultCache: sourceFileCache?.loadResultCache, - incremental, - externalRuntimeStyles, - instrumentForCoverage, - }, - stylesheetBundler: new ComponentStylesheetBundler( - { - workspaceRoot, - inlineFonts: !!optimizationOptions.fonts.inline, - optimization: !!optimizationOptions.styles.minify, - sourcemap: - // Hidden component stylesheet sourcemaps are inaccessible which is effectively - // the same as being disabled. Disabling has the advantage of avoiding the overhead - // of sourcemap processing. - sourcemapOptions.styles && !sourcemapOptions.hidden ? 'linked' : false, - outputNames, - includePaths: stylePreprocessorOptions?.includePaths, - // string[] | undefined' is not assignable to type '(Version | DeprecationOrId)[] | undefined'. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sass: stylePreprocessorOptions?.sass as any, - externalDependencies, - target, - preserveSymlinks, - tailwindConfiguration, - postcssConfiguration, - cacheOptions, - publicPath, - }, - inlineStyleLanguage, - incremental, - ), + sourcemap: !!sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true), + thirdPartySourcemaps: sourcemapOptions.vendor, + tsconfig, + jit, + advancedOptimizations, + fileReplacements, + sourceFileCache, + loadResultCache: sourceFileCache?.loadResultCache, + incremental, + externalRuntimeStyles, + instrumentForCoverage, }; } diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index b75af9435c34..c2c295d74a7a 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -13,7 +13,7 @@ import { getLocaleBaseHref, } from '../../builders/application/options'; import type { BuildOutputFile } from '../../tools/esbuild/bundler-context'; -import { PrerenderedRoutesRecord } from '../../tools/esbuild/bundler-execution-result'; +import type { PrerenderedRoutesRecord } from '../../tools/esbuild/bundler-execution-result'; export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs'; export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs';