Skip to content

Commit da250b2

Browse files
clydinAndrewKushnir
authored andcommitted
refactor(compiler-cli): add inline stylesheet external runtime style transformation support (angular#57613)
To provide support for HMR of inline component styles (`styles` decorator field), the AOT compiler will now use the resource host transformation API with the Angular CLI to provide external runtime stylesheet URLs when the `externalRuntimeStyles` compiler option is enabled. This allows both a component's file-based and inline styles to be available for HMR when used with a compatible development server such as with the Angular CLI. No behavioral change is present if the `externalRuntimeStyles` option is not enabled or the resource host transformation API is not used. An `order` numeric field is also added to the transformation API which allows consumers such as the Angular CLI to create identifiers for each inline style in a specific containing file. PR Close angular#57613
1 parent c693711 commit da250b2

File tree

6 files changed

+174
-4
lines changed

6 files changed

+174
-4
lines changed

packages/compiler-cli/src/ngtsc/annotations/common/src/api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,20 @@ export interface ResourceLoaderContext {
8787
* The absolute path to the file that contains the resource or reference to the resource.
8888
*/
8989
containingFile: string;
90+
91+
/**
92+
* For style resources, the placement of the style within the containing file with lower numbers
93+
* being before higher numbers.
94+
* The value is primarily used by the Angular CLI to create a deterministic identifier for each
95+
* style in HMR scenarios.
96+
* This is undefined for templates.
97+
*/
98+
order?: number;
99+
100+
/**
101+
* The name of the class that defines the component using the resource.
102+
* This allows identifying the source usage of a resource in cases where multiple components are
103+
* contained in a single source file.
104+
*/
105+
className: string;
90106
}

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,11 @@ export class ComponentDecoratorHandler
332332
const resolveStyleUrl = (styleUrl: string): Promise<void> | undefined => {
333333
try {
334334
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
335-
return this.resourceLoader.preload(resourceUrl, {type: 'style', containingFile});
335+
return this.resourceLoader.preload(resourceUrl, {
336+
type: 'style',
337+
containingFile,
338+
className: node.name.text,
339+
});
336340
} catch {
337341
// Don't worry about failures to preload. We can handle this problem during analysis by
338342
// producing a diagnostic.
@@ -378,11 +382,18 @@ export class ComponentDecoratorHandler
378382
return templateAndTemplateStyleResources.then(async (templateInfo) => {
379383
// Extract inline styles, process, and cache for use in synchronous analyze phase
380384
let styles: string[] | null = null;
385+
// Order plus className allows inline styles to be identified per component by a preprocessor
386+
let orderOffset = 0;
381387
const rawStyles = parseDirectiveStyles(component, this.evaluator, this.compilationMode);
382388
if (rawStyles?.length) {
383389
styles = await Promise.all(
384390
rawStyles.map((style) =>
385-
this.resourceLoader.preprocessInline(style, {type: 'style', containingFile}),
391+
this.resourceLoader.preprocessInline(style, {
392+
type: 'style',
393+
containingFile,
394+
order: orderOffset++,
395+
className: node.name.text,
396+
}),
386397
),
387398
);
388399
}
@@ -394,6 +405,8 @@ export class ComponentDecoratorHandler
394405
this.resourceLoader.preprocessInline(style, {
395406
type: 'style',
396407
containingFile: templateInfo.templateUrl ?? containingFile,
408+
order: orderOffset++,
409+
className: node.name.text,
397410
}),
398411
),
399412
)),
@@ -766,8 +779,13 @@ export class ComponentDecoratorHandler
766779
if (this.preanalyzeStylesCache.has(node)) {
767780
inlineStyles = this.preanalyzeStylesCache.get(node)!;
768781
this.preanalyzeStylesCache.delete(node);
769-
if (inlineStyles !== null) {
770-
styles.push(...inlineStyles);
782+
if (inlineStyles?.length) {
783+
if (this.externalRuntimeStyles) {
784+
// When external runtime styles is enabled, a list of URLs is provided
785+
externalStyles.push(...inlineStyles);
786+
} else {
787+
styles.push(...inlineStyles);
788+
}
771789
}
772790
} else {
773791
// Preprocessing is only supported asynchronously

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ export function preloadAndParseTemplate(
429429
const templatePromise = resourceLoader.preload(resourceUrl, {
430430
type: 'template',
431431
containingFile,
432+
className: node.name.text,
432433
});
433434

434435
// If the preload worked, then actually load and parse the template, and wait for any

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,122 @@ runInEachFileSystem(() => {
547547
]);
548548
});
549549

550+
it('should populate externalStyles from inline style transform when externalRuntimeStyles is enabled', async () => {
551+
const {program, options, host} = makeProgram([
552+
{
553+
name: _('/node_modules/@angular/core/index.d.ts'),
554+
contents: 'export const Component: any;',
555+
},
556+
{
557+
name: _('/entry.ts'),
558+
contents: `
559+
import {Component} from '@angular/core';
560+
561+
@Component({
562+
template: '',
563+
styles: ['.abc {}']
564+
}) class TestCmp {}
565+
`,
566+
},
567+
]);
568+
const {reflectionHost, handler, resourceLoader} = setup(program, options, host, {
569+
externalRuntimeStyles: true,
570+
});
571+
resourceLoader.canPreload = true;
572+
resourceLoader.canPreprocess = true;
573+
resourceLoader.preprocessInline = async function (data, context) {
574+
expect(data).toBe('.abc {}');
575+
expect(context.containingFile).toBe(_('/entry.ts').toLowerCase());
576+
expect(context.type).toBe('style');
577+
expect(context.order).toBe(0);
578+
579+
return 'abc/myInlineStyle.css';
580+
};
581+
582+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
583+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
584+
if (detected === undefined) {
585+
return fail('Failed to recognize @Component');
586+
}
587+
588+
await handler.preanalyze(TestCmp, detected.metadata);
589+
590+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
591+
expect(analysis?.resources.styles.size).toBe(1);
592+
expect(analysis?.meta.externalStyles).toEqual(['abc/myInlineStyle.css']);
593+
expect(analysis?.meta.styles).toEqual([]);
594+
});
595+
596+
it('should not populate externalStyles from inline style when externalRuntimeStyles is enabled and no transform', async () => {
597+
const {program, options, host} = makeProgram([
598+
{
599+
name: _('/node_modules/@angular/core/index.d.ts'),
600+
contents: 'export const Component: any;',
601+
},
602+
{
603+
name: _('/entry.ts'),
604+
contents: `
605+
import {Component} from '@angular/core';
606+
607+
@Component({
608+
template: '',
609+
styles: ['.abc {}']
610+
}) class TestCmp {}
611+
`,
612+
},
613+
]);
614+
const {reflectionHost, handler} = setup(program, options, host, {
615+
externalRuntimeStyles: true,
616+
});
617+
618+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
619+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
620+
if (detected === undefined) {
621+
return fail('Failed to recognize @Component');
622+
}
623+
624+
await handler.preanalyze(TestCmp, detected.metadata);
625+
626+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
627+
expect(analysis?.resources.styles.size).toBe(1);
628+
expect(analysis?.meta.externalStyles).toEqual([]);
629+
expect(analysis?.meta.styles).toEqual(['.abc {}']);
630+
});
631+
632+
it('should not populate externalStyles from inline style when externalRuntimeStyles is enabled and no preanalyze', async () => {
633+
const {program, options, host} = makeProgram([
634+
{
635+
name: _('/node_modules/@angular/core/index.d.ts'),
636+
contents: 'export const Component: any;',
637+
},
638+
{
639+
name: _('/entry.ts'),
640+
contents: `
641+
import {Component} from '@angular/core';
642+
643+
@Component({
644+
template: '',
645+
styles: ['.abc {}']
646+
}) class TestCmp {}
647+
`,
648+
},
649+
]);
650+
const {reflectionHost, handler} = setup(program, options, host, {
651+
externalRuntimeStyles: true,
652+
});
653+
654+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
655+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
656+
if (detected === undefined) {
657+
return fail('Failed to recognize @Component');
658+
}
659+
660+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
661+
expect(analysis?.resources.styles.size).toBe(1);
662+
expect(analysis?.meta.externalStyles).toEqual([]);
663+
expect(analysis?.meta.styles).toEqual(['.abc {}']);
664+
});
665+
550666
it('should replace inline style content with transformed content', async () => {
551667
const {program, options, host} = makeProgram([
552668
{

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ export interface ResourceHostContext {
9292
* The absolute path to the file that contains the resource or reference to the resource.
9393
*/
9494
readonly containingFile: string;
95+
96+
/**
97+
* For style resources, the placement of the style within the containing file with lower numbers
98+
* being before higher numbers.
99+
* The value is primarily used by the Angular CLI to create a deterministic identifier for each
100+
* style in HMR scenarios.
101+
* This is undefined for templates.
102+
*/
103+
readonly order?: number;
104+
105+
/**
106+
* The name of the class that defines the component using the resource.
107+
* This allows identifying the source usage of a resource in cases where multiple components are
108+
* contained in a single source file.
109+
*/
110+
className: string;
95111
}
96112

97113
/**

packages/compiler-cli/src/ngtsc/resource/src/loader.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class AdapterResourceLoader implements ResourceLoader {
9898
type: 'style',
9999
containingFile: context.containingFile,
100100
resourceFile: resolvedUrl,
101+
className: context.className,
101102
};
102103
result = Promise.resolve(result).then(async (str) => {
103104
const transformResult = await this.adapter.transformResource!(str, resourceContext);
@@ -135,6 +136,8 @@ export class AdapterResourceLoader implements ResourceLoader {
135136
type: 'style',
136137
containingFile: context.containingFile,
137138
resourceFile: null,
139+
order: context.order,
140+
className: context.className,
138141
});
139142
if (transformResult === null) {
140143
return data;

0 commit comments

Comments
 (0)