Skip to content

Commit fe044a0

Browse files
committed
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.
1 parent 95f4e58 commit fe044a0

File tree

3 files changed

+33
-5
lines changed

3 files changed

+33
-5
lines changed

packages/angular/build/src/tools/angular/angular-host.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface AngularHostOptions {
2424
data: string,
2525
containingFile: string,
2626
stylesheetFile?: string,
27+
order?: number,
2728
): Promise<string | null>;
2829
processWebWorker(workerFile: string, containingFile: string): string;
2930
}
@@ -196,6 +197,9 @@ export function createAngularCompilerHost(
196197
data,
197198
context.containingFile,
198199
context.resourceFile ?? undefined,
200+
// TODO: Remove once available in compiler-cli types
201+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
202+
(context as any).order,
199203
);
200204

201205
return typeof result === 'string' ? { content: result } : null;

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
PluginBuild,
1717
} from 'esbuild';
1818
import assert from 'node:assert';
19+
import { createHash } from 'node:crypto';
1920
import * as path from 'node:path';
2021
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
2122
import { AngularHostOptions } from '../../angular/angular-host';
@@ -177,7 +178,7 @@ export function createCompilerPlugin(
177178
fileReplacements: pluginOptions.fileReplacements,
178179
modifiedFiles,
179180
sourceFileCache: pluginOptions.sourceFileCache,
180-
async transformStylesheet(data, containingFile, stylesheetFile) {
181+
async transformStylesheet(data, containingFile, stylesheetFile, order) {
181182
let stylesheetResult;
182183

183184
// Stylesheet file only exists for external stylesheets
@@ -189,6 +190,16 @@ export function createCompilerPlugin(
189190
containingFile,
190191
// Inline stylesheets from a template style element are always CSS
191192
containingFile.endsWith('.html') ? 'css' : styleOptions.inlineStyleLanguage,
193+
// When external runtime styles are enabled, an identifier for the style that does not change
194+
// based on the content is required to avoid emitted JS code changes. Any JS code changes will
195+
// invalid the output and force a full page reload for HMR cases. The containing file and order
196+
// of the style within the containing file is used.
197+
pluginOptions.externalRuntimeStyles
198+
? createHash('sha-256')
199+
.update(containingFile)
200+
.update((order ?? 0).toString())
201+
.digest('hex')
202+
: undefined,
192203
);
193204
}
194205

packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ export class ComponentStylesheetBundler {
6363
);
6464
}
6565

66-
async bundleInline(data: string, filename: string, language: string) {
66+
async bundleInline(data: string, filename: string, language: string, externalId?: string) {
6767
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
6868
// to the actual stylesheet file path.
6969
// TODO: Consider xxhash instead for hashing
70-
const id = createHash('sha256').update(data).digest('hex');
70+
const id = createHash('sha256')
71+
.update(data)
72+
.update(externalId ?? '')
73+
.digest('hex');
7174
const entry = [language, id, filename].join(';');
7275

7376
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
@@ -77,7 +80,13 @@ export class ComponentStylesheetBundler {
7780
const buildOptions = createStylesheetBundleOptions(this.options, loadCache, {
7881
[entry]: data,
7982
});
80-
buildOptions.entryPoints = [`${namespace};${entry}`];
83+
if (externalId) {
84+
buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
85+
delete buildOptions.publicPath;
86+
} else {
87+
buildOptions.entryPoints = [`${namespace};${entry}`];
88+
}
89+
8190
buildOptions.plugins.push({
8291
name: 'angular-component-styles',
8392
setup(build) {
@@ -106,7 +115,11 @@ export class ComponentStylesheetBundler {
106115
});
107116

108117
// Extract the result of the bundling from the output files
109-
return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, false);
118+
return this.extractResult(
119+
await bundlerContext.bundle(),
120+
bundlerContext.watchFiles,
121+
!!externalId,
122+
);
110123
}
111124

112125
/**

0 commit comments

Comments
 (0)