Skip to content

Commit 1548827

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 9502d46 commit 1548827

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';
@@ -178,7 +179,7 @@ export function createCompilerPlugin(
178179
fileReplacements: pluginOptions.fileReplacements,
179180
modifiedFiles,
180181
sourceFileCache: pluginOptions.sourceFileCache,
181-
async transformStylesheet(data, containingFile, stylesheetFile) {
182+
async transformStylesheet(data, containingFile, stylesheetFile, order) {
182183
let stylesheetResult;
183184

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

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)