Skip to content

Commit c693711

Browse files
clydinAndrewKushnir
authored andcommitted
refactor(compiler): support external runtime component styles for file-based stylesheets (angular#57613)
The AOT compiler now has the capability to handle component stylesheet files as external runtime files. External runtime files are stylesheets that are not embedded within the component code at build time. Instead a URL path is emitted within a component's metadata. When combined with separate updates to the shared style host and DOM renderer, this will allow these stylesheet files to be fetched and processed by a development server on-demand. This behavior is controlled by an internal compiler option `externalRuntimeStyles`. The Angular CLI development server will also be updated to provide the serving functionality once this capability is enabled. This capability enables upcoming features such as automatic component style hot module replacement (HMR) and development server deferred stylesheet processing. The current implementation does not affect the behavior of inline styles. Only the behavior of stylesheet files referenced via component properties `styleUrl`/`styleUrls` and relative template `link` elements are changed by enabling the internal option. PR Close angular#57613
1 parent a36744e commit c693711

File tree

7 files changed

+240
-7
lines changed

7 files changed

+240
-7
lines changed

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {
7777
MetaKind,
7878
NgModuleMeta,
7979
PipeMeta,
80+
Resource,
8081
ResourceRegistry,
8182
} from '../../../metadata';
8283
import {PartialEvaluator} from '../../../partial_evaluator';
@@ -249,6 +250,7 @@ export class ComponentDecoratorHandler
249250
private readonly forbidOrphanRendering: boolean,
250251
private readonly enableBlockSyntax: boolean,
251252
private readonly enableLetSyntax: boolean,
253+
private readonly externalRuntimeStyles: boolean,
252254
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null,
253255
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
254256
private readonly i18nPreserveSignificantWhitespace: boolean,
@@ -400,6 +402,11 @@ export class ComponentDecoratorHandler
400402

401403
this.preanalyzeStylesCache.set(node, styles);
402404

405+
if (this.externalRuntimeStyles) {
406+
// No preanalysis required for style URLs with external runtime styles
407+
return;
408+
}
409+
403410
// Wait for both the template and all styleUrl resources to resolve.
404411
await Promise.all([
405412
...componentStyleUrls.map((styleUrl) => resolveStyleUrl(styleUrl.url)),
@@ -686,6 +693,7 @@ export class ComponentDecoratorHandler
686693
// precede inline styles, and styles defined in the template override styles defined in the
687694
// component.
688695
let styles: string[] = [];
696+
const externalStyles: string[] = [];
689697

690698
const styleResources = extractInlineStyleResources(component);
691699
const styleUrls: StyleUrlMeta[] = [
@@ -696,6 +704,11 @@ export class ComponentDecoratorHandler
696704
for (const styleUrl of styleUrls) {
697705
try {
698706
const resourceUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
707+
if (this.externalRuntimeStyles) {
708+
// External runtime styles are not considered disk-based and may not actually exist on disk
709+
externalStyles.push(resourceUrl);
710+
continue;
711+
}
699712
if (
700713
styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator &&
701714
ts.isStringLiteralLike(styleUrl.expression)
@@ -816,7 +829,7 @@ export class ComponentDecoratorHandler
816829
changeDetection,
817830
interpolation: template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG,
818831
styles,
819-
832+
externalStyles,
820833
// These will be replaced during the compilation step, after all `NgModule`s have been
821834
// analyzed and the full compilation scope for the component can be realized.
822835
animations,

packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts

Lines changed: 193 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ConstantPool} from '@angular/compiler';
9+
import {ConstantPool, ViewEncapsulation} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../cycles';
@@ -67,11 +67,17 @@ function setup(
6767
program: ts.Program,
6868
options: ts.CompilerOptions,
6969
host: ts.CompilerHost,
70-
opts: {compilationMode: CompilationMode; usePoisonedData?: boolean} = {
71-
compilationMode: CompilationMode.FULL,
72-
},
70+
opts: {
71+
compilationMode?: CompilationMode;
72+
usePoisonedData?: boolean;
73+
externalRuntimeStyles?: boolean;
74+
} = {},
7375
) {
74-
const {compilationMode, usePoisonedData} = opts;
76+
const {
77+
compilationMode = CompilationMode.FULL,
78+
usePoisonedData,
79+
externalRuntimeStyles = false,
80+
} = opts;
7581
const checker = program.getTypeChecker();
7682
const reflectionHost = new TypeScriptReflectionHost(checker);
7783
const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null);
@@ -145,6 +151,7 @@ function setup(
145151
/* forbidOrphanRenderering */ false,
146152
/* enableBlockSyntax */ true,
147153
/* enableLetSyntax */ true,
154+
externalRuntimeStyles,
148155
/* localCompilationExtraImportsTracker */ null,
149156
jitDeclarationRegistry,
150157
/* i18nPreserveSignificantWhitespace */ true,
@@ -359,6 +366,187 @@ runInEachFileSystem(() => {
359366
expect(compileResult).toEqual([]);
360367
});
361368

369+
it('should populate externalStyles from styleUrl when externalRuntimeStyles is enabled', () => {
370+
const {program, options, host} = makeProgram([
371+
{
372+
name: _('/node_modules/@angular/core/index.d.ts'),
373+
contents: 'export const Component: any;',
374+
},
375+
{
376+
name: _('/myStyle.css'),
377+
contents: '<div>hello world</div>',
378+
},
379+
{
380+
name: _('/entry.ts'),
381+
contents: `
382+
import {Component} from '@angular/core';
383+
384+
@Component({
385+
template: '',
386+
styleUrl: '/myStyle.css',
387+
styles: ['a { color: red; }', 'b { color: blue; }'],
388+
}) class TestCmp {}
389+
`,
390+
},
391+
]);
392+
const {reflectionHost, handler} = setup(program, options, host, {
393+
externalRuntimeStyles: true,
394+
});
395+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
396+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
397+
if (detected === undefined) {
398+
return fail('Failed to recognize @Component');
399+
}
400+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
401+
expect(analysis?.resources.styles.size).toBe(2);
402+
expect(analysis?.meta.externalStyles).toEqual(['/myStyle.css']);
403+
});
404+
405+
it('should populate externalStyles from styleUrls when externalRuntimeStyles is enabled', () => {
406+
const {program, options, host} = makeProgram([
407+
{
408+
name: _('/node_modules/@angular/core/index.d.ts'),
409+
contents: 'export const Component: any;',
410+
},
411+
{
412+
name: _('/myStyle.css'),
413+
contents: '<div>hello world</div>',
414+
},
415+
{
416+
name: _('/entry.ts'),
417+
contents: `
418+
import {Component} from '@angular/core';
419+
420+
@Component({
421+
template: '',
422+
styleUrls: ['/myStyle.css', '/myOtherStyle.css'],
423+
styles: ['a { color: red; }', 'b { color: blue; }'],
424+
}) class TestCmp {}
425+
`,
426+
},
427+
]);
428+
const {reflectionHost, handler} = setup(program, options, host, {
429+
externalRuntimeStyles: true,
430+
});
431+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
432+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
433+
if (detected === undefined) {
434+
return fail('Failed to recognize @Component');
435+
}
436+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
437+
expect(analysis?.resources.styles.size).toBe(2);
438+
expect(analysis?.meta.externalStyles).toEqual(['/myStyle.css', '/myOtherStyle.css']);
439+
});
440+
441+
it('should keep default emulated view encapsulation with styleUrls when externalRuntimeStyles is enabled', () => {
442+
const {program, options, host} = makeProgram([
443+
{
444+
name: _('/node_modules/@angular/core/index.d.ts'),
445+
contents: 'export const Component: any;',
446+
},
447+
{
448+
name: _('/myStyle.css'),
449+
contents: '<div>hello world</div>',
450+
},
451+
{
452+
name: _('/entry.ts'),
453+
contents: `
454+
import {Component} from '@angular/core';
455+
456+
@Component({
457+
template: '',
458+
styleUrls: ['/myStyle.css', '/myOtherStyle.css'],
459+
}) class TestCmp {}
460+
`,
461+
},
462+
]);
463+
const {reflectionHost, handler} = setup(program, options, host, {
464+
externalRuntimeStyles: true,
465+
});
466+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
467+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
468+
if (detected === undefined) {
469+
return fail('Failed to recognize @Component');
470+
}
471+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
472+
expect(analysis?.meta.encapsulation).toBe(ViewEncapsulation.Emulated);
473+
});
474+
475+
it('should populate externalStyles from template link element when externalRuntimeStyles is enabled', () => {
476+
const {program, options, host} = makeProgram([
477+
{
478+
name: _('/node_modules/@angular/core/index.d.ts'),
479+
contents: 'export const Component: any;',
480+
},
481+
{
482+
name: _('/myStyle.css'),
483+
contents: '<div>hello world</div>',
484+
},
485+
{
486+
name: _('/entry.ts'),
487+
contents: `
488+
import {Component} from '@angular/core';
489+
490+
@Component({
491+
template: '<link rel="stylesheet" href="myTemplateStyle.css" />',
492+
styles: ['a { color: red; }', 'b { color: blue; }'],
493+
}) class TestCmp {}
494+
`,
495+
},
496+
]);
497+
const {reflectionHost, handler} = setup(program, options, host, {
498+
externalRuntimeStyles: true,
499+
});
500+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
501+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
502+
if (detected === undefined) {
503+
return fail('Failed to recognize @Component');
504+
}
505+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
506+
expect(analysis?.resources.styles.size).toBe(2);
507+
expect(analysis?.meta.externalStyles).toEqual(['myTemplateStyle.css']);
508+
});
509+
510+
it('should populate externalStyles with resolve return values when externalRuntimeStyles is enabled', () => {
511+
const {program, options, host} = makeProgram([
512+
{
513+
name: _('/node_modules/@angular/core/index.d.ts'),
514+
contents: 'export const Component: any;',
515+
},
516+
{
517+
name: _('/myStyle.css'),
518+
contents: '<div>hello world</div>',
519+
},
520+
{
521+
name: _('/entry.ts'),
522+
contents: `
523+
import {Component} from '@angular/core';
524+
525+
@Component({
526+
template: '<link rel="stylesheet" href="myTemplateStyle.css" />',
527+
styleUrl: '/myStyle.css',
528+
styles: ['a { color: red; }', 'b { color: blue; }'],
529+
}) class TestCmp {}
530+
`,
531+
},
532+
]);
533+
const {reflectionHost, handler, resourceLoader} = setup(program, options, host, {
534+
externalRuntimeStyles: true,
535+
});
536+
resourceLoader.resolve = (v) => 'abc/' + v;
537+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
538+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
539+
if (detected === undefined) {
540+
return fail('Failed to recognize @Component');
541+
}
542+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
543+
expect(analysis?.resources.styles.size).toBe(2);
544+
expect(analysis?.meta.externalStyles).toEqual([
545+
'abc//myStyle.css',
546+
'abc/myTemplateStyle.css',
547+
]);
548+
});
549+
362550
it('should replace inline style content with transformed content', async () => {
363551
const {program, options, host} = makeProgram([
364552
{

packages/compiler-cli/src/ngtsc/core/api/src/options.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ export interface InternalOptions {
9696
*/
9797
_enableLetSyntax?: boolean;
9898

99+
/**
100+
* Enables the use of `<link>` elements for component styleUrls instead of inlining the file
101+
* content.
102+
* This option is intended to be used with a development server that processes and serves
103+
* the files on-demand for an application.
104+
*
105+
* @internal
106+
*/
107+
externalRuntimeStyles?: boolean;
108+
99109
/**
100110
* Detected version of `@angular/core` in the workspace. Used by the
101111
* compiler to adjust the output depending on the available symbols.

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,7 @@ export class NgCompiler {
13921392
const strictCtorDeps = this.options.strictInjectionParameters || false;
13931393
const supportJitMode = this.options['supportJitMode'] ?? true;
13941394
const supportTestBed = this.options['supportTestBed'] ?? true;
1395+
const externalRuntimeStyles = this.options['externalRuntimeStyles'] ?? false;
13951396

13961397
// Libraries compiled in partial mode could potentially be used with TestBed within an
13971398
// application. Since this is not known at library compilation time, support is required to
@@ -1457,6 +1458,7 @@ export class NgCompiler {
14571458
!!this.options.forbidOrphanComponents,
14581459
this.enableBlockSyntax,
14591460
this.enableLetSyntax,
1461+
externalRuntimeStyles,
14601462
localCompilationExtraImportsTracker,
14611463
jitDeclarationRegistry,
14621464
this.options.i18nPreserveWhitespaceForLegacyExtraction ?? true,

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ export class Identifiers {
553553
moduleName: CORE,
554554
};
555555

556+
static ExternalStylesFeature: o.ExternalReference = {
557+
name: 'ɵɵExternalStylesFeature',
558+
moduleName: CORE,
559+
};
560+
556561
static listener: o.ExternalReference = {name: 'ɵɵlistener', moduleName: CORE};
557562

558563
static getInheritedFactory: o.ExternalReference = {

packages/compiler/src/render3/view/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
236236
*/
237237
styles: string[];
238238

239+
/**
240+
* A collection of style paths for external stylesheets that will be applied and scoped to the component.
241+
*/
242+
externalStyles?: string[];
243+
239244
/**
240245
* An encapsulation policy for the component's styling.
241246
* Possible values:

packages/compiler/src/render3/view/compiler.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ function addFeatures(
152152
if (meta.hasOwnProperty('template') && meta.isStandalone) {
153153
features.push(o.importExpr(R3.StandaloneFeature));
154154
}
155+
if ('externalStyles' in meta && meta.externalStyles?.length) {
156+
const externalStyleNodes = meta.externalStyles.map((externalStyle) => o.literal(externalStyle));
157+
features.push(
158+
o.importExpr(R3.ExternalStylesFeature).callFn([o.literalArr(externalStyleNodes)]),
159+
);
160+
}
155161
if (features.length) {
156162
definitionMap.set('features', o.literalArr(features));
157163
}
@@ -281,8 +287,10 @@ export function compileComponentFromMetadata(
281287
meta.encapsulation = core.ViewEncapsulation.Emulated;
282288
}
283289

290+
let hasStyles = !!meta.externalStyles?.length;
284291
// e.g. `styles: [str1, str2]`
285292
if (meta.styles && meta.styles.length) {
293+
hasStyles = true;
286294
const styleValues =
287295
meta.encapsulation == core.ViewEncapsulation.Emulated
288296
? compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR)
@@ -297,7 +305,9 @@ export function compileComponentFromMetadata(
297305
if (styleNodes.length > 0) {
298306
definitionMap.set('styles', o.literalArr(styleNodes));
299307
}
300-
} else if (meta.encapsulation === core.ViewEncapsulation.Emulated) {
308+
}
309+
310+
if (!hasStyles && meta.encapsulation === core.ViewEncapsulation.Emulated) {
301311
// If there is no style, don't generate css selectors on elements
302312
meta.encapsulation = core.ViewEncapsulation.None;
303313
}

0 commit comments

Comments
 (0)