From 608600339b2e150c1c2704446a15c98d16e47670 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:30:41 -0400 Subject: [PATCH 1/2] refactor(@angular/build): support external runtime component stylesheets in application builder To support automatic component style HMR, `application` builder in development mode now provides support for generating external runtime component stylesheets. This capability leverages the upcoming support within the AOT -compiler to emit components that generate `link` elements instead of embedding the stylesheet contents for file-based styles (e.g., `styleUrl`). In combination with support within the development server to handle requests for component stylesheets, file-based component stylesheets will be able to be replaced without a full page reload. The implementation leverages the AOT compiler option `externalRuntimeStyles` which uses the result of the resource handler's resolution and emits new external stylesheet metadata within the component output code. This new metadata works in concert with the Angular runtime to generate `link` elements which can then leverage existing global stylesheet HMR capabilities. This capability is current disabled by default while all elements are integrated across the CLI and framework and can be controlled via the `NG_HMR_CSTYLES=1` environment variable. Once fully integrated the environment variable will unneeded. This feature is only intended for use with the development server. Component styles within in built code including production are not affected by this feature. NOTE: Rebuild times have not yet been optimized. Future improvements will reduce the component stylesheet only rebuild time case. --- .../build/src/builders/application/options.ts | 10 ++++ .../src/builders/dev-server/vite-server.ts | 4 ++ .../build/src/tools/angular/angular-host.ts | 25 ++++++++ .../compilation/angular-compilation.ts | 1 + .../angular/compilation/aot-compilation.ts | 12 +++- .../compilation/parallel-compilation.ts | 1 + .../angular/compilation/parallel-worker.ts | 3 +- .../tools/esbuild/angular/compiler-plugin.ts | 57 ++++++++++++++++++- .../esbuild/angular/component-stylesheets.ts | 55 +++++++++++++++--- .../angular/build/src/tools/esbuild/cache.ts | 8 +++ .../tools/esbuild/compiler-plugin-options.ts | 2 + .../build/src/utils/environment-options.ts | 4 ++ 12 files changed, 170 insertions(+), 12 deletions(-) diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index b3f2c0ddcba8..730a3bffff18 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -90,6 +90,14 @@ interface InternalOptions { * @default false */ disableFullServerManifestGeneration?: boolean; + + /** + * Enables the use of AOT compiler emitted external runtime styles. + * External runtime styles use `link` elements instead of embedded style content in the output JavaScript. + * This option is only intended to be used with a development server that can process and serve component + * styles. + */ + externalRuntimeStyles?: boolean; } /** Full set of options for `application` builder. */ @@ -375,6 +383,7 @@ export async function normalizeOptions( clearScreen, define, disableFullServerManifestGeneration = false, + externalRuntimeStyles, } = options; // Return all the normalized options @@ -436,6 +445,7 @@ export async function normalizeOptions( clearScreen, define, disableFullServerManifestGeneration, + externalRuntimeStyles, }; } diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 36667323251b..0fe72f970e81 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -18,6 +18,7 @@ import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugi import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin'; import { createRemoveIdPrefixPlugin } from '../../tools/vite/id-prefix-plugin'; import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils'; +import { useComponentStyleHmr } from '../../utils/environment-options'; import { loadEsmModule } from '../../utils/load-esm'; import { Result, ResultFile, ResultKind } from '../application/results'; import { @@ -130,6 +131,9 @@ export async function* serveWithVite( process.setSourceMapsEnabled(true); } + // TODO: Enable by default once full support across CLI and FW is integrated + browserOptions.externalRuntimeStyles = useComponentStyleHmr; + // Setup the prebundling transformer that will be shared across Vite prebundling requests const prebundleTransformer = new JavaScriptTransformer( // Always enable JIT linking to support applications built with and without AOT. diff --git a/packages/angular/build/src/tools/angular/angular-host.ts b/packages/angular/build/src/tools/angular/angular-host.ts index beb004a9b274..88184b418cb8 100644 --- a/packages/angular/build/src/tools/angular/angular-host.ts +++ b/packages/angular/build/src/tools/angular/angular-host.ts @@ -7,6 +7,7 @@ */ import type ng from '@angular/compiler-cli'; +import assert from 'node:assert'; import { createHash } from 'node:crypto'; import nodePath from 'node:path'; import type ts from 'typescript'; @@ -18,6 +19,7 @@ export interface AngularHostOptions { fileReplacements?: Record; sourceFileCache?: Map; modifiedFiles?: Set; + externalStylesheets?: Map; transformStylesheet( data: string, containingFile: string, @@ -180,6 +182,11 @@ export function createAngularCompilerHost( return null; } + assert( + !context.resourceFile || !hostOptions.externalStylesheets?.has(context.resourceFile), + 'External runtime stylesheets should not be transformed: ' + context.resourceFile, + ); + // No transformation required if the resource is empty if (data.trim().length === 0) { return { content: '' }; @@ -194,6 +201,24 @@ export function createAngularCompilerHost( return typeof result === 'string' ? { content: result } : null; }; + host.resourceNameToFileName = function (resourceName, containingFile) { + const resolvedPath = nodePath.join(nodePath.dirname(containingFile), resourceName); + + // All resource names that have HTML file extensions are assumed to be templates + if (resourceName.endsWith('.html') || !hostOptions.externalStylesheets) { + return resolvedPath; + } + + // For external stylesheets, create a unique identifier and store the mapping + let externalId = hostOptions.externalStylesheets.get(resolvedPath); + if (externalId === undefined) { + externalId = createHash('sha256').update(resolvedPath).digest('hex'); + hostOptions.externalStylesheets.set(resolvedPath, externalId); + } + + return externalId + '.css'; + }; + // Allow the AOT compiler to request the set of changed templates and styles host.getModifiedResourceFiles = function () { return hostOptions.modifiedFiles; diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts index 4bb9acf18eeb..4cb4852e54f7 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation.ts @@ -75,6 +75,7 @@ export abstract class AngularCompilation { affectedFiles: ReadonlySet; compilerOptions: ng.CompilerOptions; referencedFiles: readonly string[]; + externalStylesheets?: ReadonlyMap; }>; abstract emitAffectedFiles(): Iterable | Promise>; diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index 59de92fb76b1..9e566803fb58 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -46,6 +46,7 @@ export class AotCompilation extends AngularCompilation { affectedFiles: ReadonlySet; compilerOptions: ng.CompilerOptions; referencedFiles: readonly string[]; + externalStylesheets?: ReadonlyMap; }> { // Dynamically load the Angular compiler CLI package const { NgtscProgram, OptimizeFor } = await AngularCompilation.loadCompilerCli(); @@ -59,6 +60,10 @@ export class AotCompilation extends AngularCompilation { const compilerOptions = compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions; + if (compilerOptions.externalRuntimeStyles) { + hostOptions.externalStylesheets ??= new Map(); + } + // Create Angular compiler host const host = createAngularCompilerHost(ts, compilerOptions, hostOptions); @@ -121,7 +126,12 @@ export class AotCompilation extends AngularCompilation { this.#state?.diagnosticCache, ); - return { affectedFiles, compilerOptions, referencedFiles }; + return { + affectedFiles, + compilerOptions, + referencedFiles, + externalStylesheets: hostOptions.externalStylesheets, + }; } *collectDiagnostics(modes: DiagnosticModes): Iterable { diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts index 434ca958b4d1..817c4081ee21 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts @@ -47,6 +47,7 @@ export class ParallelCompilation extends AngularCompilation { affectedFiles: ReadonlySet; compilerOptions: CompilerOptions; referencedFiles: readonly string[]; + externalStylesheets?: ReadonlyMap; }> { const stylesheetChannel = new MessageChannel(); // The request identifier is required because Angular can issue multiple concurrent requests diff --git a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts index 21cdea54c940..38014bc670f9 100644 --- a/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts +++ b/packages/angular/build/src/tools/angular/compilation/parallel-worker.ts @@ -42,7 +42,7 @@ export async function initialize(request: InitRequest) { } }); - const { compilerOptions, referencedFiles } = await compilation.initialize( + const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize( request.tsconfig, { fileReplacements: request.fileReplacements, @@ -93,6 +93,7 @@ export async function initialize(request: InitRequest) { ); return { + externalStylesheets, referencedFiles, // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently. compilerOptions: { 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 344be6fdf838..0c104e3780a8 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -48,6 +48,7 @@ export interface CompilerPluginOptions { sourceFileCache?: SourceFileCache; loadResultCache?: LoadResultCache; incremental: boolean; + externalRuntimeStyles?: boolean; } // eslint-disable-next-line max-lines-per-function @@ -152,6 +153,7 @@ export function createCompilerPlugin( // Angular compiler which does not have direct knowledge of transitive resource // dependencies or web worker processing. let modifiedFiles; + let invalidatedStylesheetEntries; if ( pluginOptions.sourceFileCache?.modifiedFiles.size && referencedFileTracker && @@ -160,7 +162,7 @@ export function createCompilerPlugin( // TODO: Differentiate between changed input files and stale output files modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles); pluginOptions.sourceFileCache.invalidate(modifiedFiles); - stylesheetBundler.invalidate(modifiedFiles); + invalidatedStylesheetEntries = stylesheetBundler.invalidate(modifiedFiles); } if ( @@ -266,6 +268,7 @@ export function createCompilerPlugin( // Initialize the Angular compilation for the current build. // In watch mode, previous build state will be reused. let referencedFiles; + let externalStylesheets; try { const initializationResult = await compilation.initialize( pluginOptions.tsconfig, @@ -280,6 +283,7 @@ export function createCompilerPlugin( !!initializationResult.compilerOptions.sourceMap || !!initializationResult.compilerOptions.inlineSourceMap; referencedFiles = initializationResult.referencedFiles; + externalStylesheets = initializationResult.externalStylesheets; } catch (error) { (result.errors ??= []).push({ text: 'Angular compilation initialization failed.', @@ -304,6 +308,32 @@ export function createCompilerPlugin( return result; } + if (externalStylesheets) { + // Process any new external stylesheets + for (const [stylesheetFile, externalId] of externalStylesheets) { + await bundleExternalStylesheet( + stylesheetBundler, + stylesheetFile, + externalId, + result, + additionalResults, + ); + } + // Process any updated stylesheets + if (invalidatedStylesheetEntries) { + for (const stylesheetFile of invalidatedStylesheetEntries) { + // externalId is already linked in the bundler context so only enabling is required here + await bundleExternalStylesheet( + stylesheetBundler, + stylesheetFile, + true, + result, + additionalResults, + ); + } + } + } + // Update TypeScript file output cache for all affected files try { await profileAsync('NG_EMIT_TS', async () => { @@ -500,6 +530,30 @@ export function createCompilerPlugin( }; } +async function bundleExternalStylesheet( + stylesheetBundler: ComponentStylesheetBundler, + stylesheetFile: string, + externalId: string | boolean, + result: OnStartResult, + additionalResults: Map< + string, + { outputFiles?: OutputFile[]; metafile?: Metafile; errors?: PartialMessage[] } + >, +) { + const { outputFiles, metafile, errors, warnings } = await stylesheetBundler.bundleFile( + stylesheetFile, + externalId, + ); + if (errors) { + (result.errors ??= []).push(...errors); + } + (result.warnings ??= []).push(...warnings); + additionalResults.set(stylesheetFile, { + outputFiles, + metafile, + }); +} + function createCompilerOptionsTransformer( setupWarnings: PartialMessage[] | undefined, pluginOptions: CompilerPluginOptions, @@ -572,6 +626,7 @@ function createCompilerOptionsTransformer( mapRoot: undefined, sourceRoot: undefined, preserveSymlinks, + externalRuntimeStyles: pluginOptions.externalRuntimeStyles, }; }; } diff --git a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts index a4f9143065a5..ae0ff2120d03 100644 --- a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts +++ b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts @@ -7,6 +7,7 @@ */ import { OutputFile } from 'esbuild'; +import assert from 'node:assert'; import { createHash } from 'node:crypto'; import path from 'node:path'; import { BuildOutputFileType, BundleContextResult, BundlerContext } from '../bundler-context'; @@ -35,17 +36,31 @@ export class ComponentStylesheetBundler { private readonly incremental: boolean, ) {} - async bundleFile(entry: string) { + async bundleFile(entry: string, externalId?: string | boolean) { const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => { return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => { const buildOptions = createStylesheetBundleOptions(this.options, loadCache); - buildOptions.entryPoints = [entry]; + if (externalId) { + assert( + typeof externalId === 'string', + 'Initial external component stylesheets must have a string identifier', + ); + + buildOptions.entryPoints = { [externalId]: entry }; + delete buildOptions.publicPath; + } else { + buildOptions.entryPoints = [entry]; + } return buildOptions; }); }); - return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles); + return this.extractResult( + await bundlerContext.bundle(), + bundlerContext.watchFiles, + !!externalId, + ); } async bundleInline(data: string, filename: string, language: string) { @@ -91,22 +106,33 @@ export class ComponentStylesheetBundler { }); // Extract the result of the bundling from the output files - return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles); + return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, false); } - invalidate(files: Iterable) { + /** + * Invalidates both file and inline based component style bundling state for a set of modified files. + * @param files The group of files that have been modified + * @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined. + */ + invalidate(files: Iterable): string[] | undefined { if (!this.incremental) { return; } const normalizedFiles = [...files].map(path.normalize); + let entries: string[] | undefined; - for (const bundler of this.#fileContexts.values()) { - bundler.invalidate(normalizedFiles); + for (const [entry, bundler] of this.#fileContexts.entries()) { + if (bundler.invalidate(normalizedFiles)) { + entries ??= []; + entries.push(entry); + } } for (const bundler of this.#inlineContexts.values()) { bundler.invalidate(normalizedFiles); } + + return entries; } async dispose(): Promise { @@ -117,7 +143,11 @@ export class ComponentStylesheetBundler { await Promise.allSettled(contexts.map((context) => context.dispose())); } - private extractResult(result: BundleContextResult, referencedFiles?: Set) { + private extractResult( + result: BundleContextResult, + referencedFiles: Set | undefined, + external: boolean, + ) { let contents = ''; let metafile; const outputFiles: OutputFile[] = []; @@ -140,7 +170,14 @@ export class ComponentStylesheetBundler { outputFiles.push(clonedOutputFile); } else if (filename.endsWith('.css')) { - contents = outputFile.text; + if (external) { + const clonedOutputFile = outputFile.clone(); + clonedOutputFile.path = path.join(this.options.workspaceRoot, outputFile.path); + outputFiles.push(clonedOutputFile); + contents = path.posix.join(this.options.publicPath ?? '', filename); + } else { + contents = outputFile.text; + } } else { throw new Error( `Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`, diff --git a/packages/angular/build/src/tools/esbuild/cache.ts b/packages/angular/build/src/tools/esbuild/cache.ts index 5b37a6ab0c36..5bd0dc84d73f 100644 --- a/packages/angular/build/src/tools/esbuild/cache.ts +++ b/packages/angular/build/src/tools/esbuild/cache.ts @@ -126,4 +126,12 @@ export class MemoryCache extends Cache> { values() { return this.store.values(); } + + /** + * Provides all the keys/values currently present in the cache instance. + * @returns An iterable of all key/value pairs in the cache. + */ + entries() { + return this.store.entries(); + } } 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 37d9721064ea..4361d7f0cca7 100644 --- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts +++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts @@ -37,6 +37,7 @@ export function createCompilerPluginOptions( tailwindConfiguration, postcssConfiguration, publicPath, + externalRuntimeStyles, } = options; return { @@ -51,6 +52,7 @@ export function createCompilerPluginOptions( sourceFileCache, loadResultCache: sourceFileCache?.loadResultCache, incremental: !!options.watch, + externalRuntimeStyles, }, // Component stylesheet options styleOptions: { diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index cad94ce76129..dd47453cb974 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -100,3 +100,7 @@ export const useJSONBuildLogs = const optimizeChunksVariable = process.env['NG_BUILD_OPTIMIZE_CHUNKS']; export const shouldOptimizeChunks = isPresent(optimizeChunksVariable) && isEnabled(optimizeChunksVariable); + +const hmrComponentStylesVariable = process.env['NG_HMR_CSTYLES']; +export const useComponentStyleHmr = + isPresent(hmrComponentStylesVariable) && isEnabled(hmrComponentStylesVariable); From ca176d4c00d2b61e3a6e67408e227a7ab403f346 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:53:51 -0400 Subject: [PATCH 2/2] refactor(@angular/build): support external runtime styles for inline component styles The build system will now transform inline styles into a corresponding external runtime style with a URL for the Angular AOT compiler when the development server has enabled component HMR for styles. This allows both file-based and inline component styles to be eligible for component style HMR. The inline styles are provided to the development server in an equivalent form to the file-based styles which the Angular runtime will request via `link` elements during development. A unique identifier is produced for each inline style that combines the containing file and order of the style within the containing file to represent the location of the style. This provides an equivalent unique identifier to the full path used by file-based styles. --- .../build/src/tools/angular/angular-host.ts | 4 ++++ .../tools/esbuild/angular/compiler-plugin.ts | 13 +++++++++++- .../esbuild/angular/component-stylesheets.ts | 21 +++++++++++++++---- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/angular/build/src/tools/angular/angular-host.ts b/packages/angular/build/src/tools/angular/angular-host.ts index 88184b418cb8..cb7616424f03 100644 --- a/packages/angular/build/src/tools/angular/angular-host.ts +++ b/packages/angular/build/src/tools/angular/angular-host.ts @@ -24,6 +24,7 @@ export interface AngularHostOptions { data: string, containingFile: string, stylesheetFile?: string, + order?: number, ): Promise; processWebWorker(workerFile: string, containingFile: string): string; } @@ -196,6 +197,9 @@ export function createAngularCompilerHost( data, context.containingFile, context.resourceFile ?? undefined, + // TODO: Remove once available in compiler-cli types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context as any).order, ); return typeof result === 'string' ? { content: result } : null; 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 0c104e3780a8..8d2dd2837530 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -16,6 +16,7 @@ import type { PluginBuild, } from 'esbuild'; import assert from 'node:assert'; +import { createHash } from 'node:crypto'; import * as path from 'node:path'; import { maxWorkers, useTypeChecking } from '../../../utils/environment-options'; import { AngularHostOptions } from '../../angular/angular-host'; @@ -178,7 +179,7 @@ export function createCompilerPlugin( fileReplacements: pluginOptions.fileReplacements, modifiedFiles, sourceFileCache: pluginOptions.sourceFileCache, - async transformStylesheet(data, containingFile, stylesheetFile) { + async transformStylesheet(data, containingFile, stylesheetFile, order) { let stylesheetResult; // Stylesheet file only exists for external stylesheets @@ -190,6 +191,16 @@ export function createCompilerPlugin( containingFile, // Inline stylesheets from a template style element are always CSS containingFile.endsWith('.html') ? 'css' : styleOptions.inlineStyleLanguage, + // When external runtime styles are enabled, an identifier for the style that does not change + // based on the content is required to avoid emitted JS code changes. Any JS code changes will + // invalid the output and force a full page reload for HMR cases. The containing file and order + // of the style within the containing file is used. + pluginOptions.externalRuntimeStyles + ? createHash('sha-256') + .update(containingFile) + .update((order ?? 0).toString()) + .digest('hex') + : undefined, ); } diff --git a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts index ae0ff2120d03..97c1b18ab5e1 100644 --- a/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts +++ b/packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts @@ -63,11 +63,14 @@ export class ComponentStylesheetBundler { ); } - async bundleInline(data: string, filename: string, language: string) { + async bundleInline(data: string, filename: string, language: string, externalId?: string) { // Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve // to the actual stylesheet file path. // TODO: Consider xxhash instead for hashing - const id = createHash('sha256').update(data).digest('hex'); + const id = createHash('sha256') + .update(data) + .update(externalId ?? '') + .digest('hex'); const entry = [language, id, filename].join(';'); const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => { @@ -77,7 +80,13 @@ export class ComponentStylesheetBundler { const buildOptions = createStylesheetBundleOptions(this.options, loadCache, { [entry]: data, }); - buildOptions.entryPoints = [`${namespace};${entry}`]; + if (externalId) { + buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` }; + delete buildOptions.publicPath; + } else { + buildOptions.entryPoints = [`${namespace};${entry}`]; + } + buildOptions.plugins.push({ name: 'angular-component-styles', setup(build) { @@ -106,7 +115,11 @@ export class ComponentStylesheetBundler { }); // Extract the result of the bundling from the output files - return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, false); + return this.extractResult( + await bundlerContext.bundle(), + bundlerContext.watchFiles, + !!externalId, + ); } /**