Skip to content

Commit

Permalink
refactor(compiler): support external runtime component styles for sty…
Browse files Browse the repository at this point in the history
…leUrl entries

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 when combined
with separate updates to the shared style host and DOM renderer 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 provide this 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.
  • Loading branch information
clydin committed Aug 23, 2024
1 parent d1d4adc commit 251f6b2
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
MetaKind,
NgModuleMeta,
PipeMeta,
Resource,
ResourceRegistry,
} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
Expand Down Expand Up @@ -249,6 +250,7 @@ export class ComponentDecoratorHandler
private readonly forbidOrphanRendering: boolean,
private readonly enableBlockSyntax: boolean,
private readonly enableLetSyntax: boolean,
private readonly externalRuntimeStyles: boolean,
private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null,
private readonly jitDeclarationRegistry: JitDeclarationRegistry,
) {
Expand Down Expand Up @@ -396,6 +398,11 @@ export class ComponentDecoratorHandler

this.preanalyzeStylesCache.set(node, styles);

if (this.externalRuntimeStyles) {
// No preanalysis required for style URLs with external runtime styles
return;
}

// Wait for both the template and all styleUrl resources to resolve.
await Promise.all([
...componentStyleUrls.map((styleUrl) => resolveStyleUrl(styleUrl.url)),
Expand Down Expand Up @@ -680,8 +687,12 @@ export class ComponentDecoratorHandler
// precede inline styles, and styles defined in the template override styles defined in the
// component.
let styles: string[] = [];
const externalStyles: string[] = [];

const styleResources = extractStyleResources(this.resourceLoader, component, containingFile);
// External runtime style do not have an on-disk file to track
const styleResources = this.externalRuntimeStyles
? new Set<Resource>()
: extractStyleResources(this.resourceLoader, component, containingFile);
const styleUrls: StyleUrlMeta[] = [
...extractComponentStyleUrls(this.evaluator, component),
..._extractTemplateStyleUrls(template),
Expand All @@ -690,6 +701,11 @@ export class ComponentDecoratorHandler
for (const styleUrl of styleUrls) {
try {
const resourceUrl = this.resourceLoader.resolve(styleUrl.url, containingFile);
if (this.externalRuntimeStyles) {
externalStyles.push(resourceUrl);
continue;
}

const resourceStr = this.resourceLoader.load(resourceUrl);
styles.push(resourceStr);
if (this.depTracker !== null) {
Expand Down Expand Up @@ -804,7 +820,7 @@ export class ComponentDecoratorHandler
changeDetection,
interpolation: template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG,
styles,

externalStyles,
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
animations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ function setup(
program: ts.Program,
options: ts.CompilerOptions,
host: ts.CompilerHost,
opts: {compilationMode: CompilationMode; usePoisonedData?: boolean} = {
compilationMode: CompilationMode.FULL,
},
opts: {
compilationMode?: CompilationMode;
usePoisonedData?: boolean;
externalRuntimeStyles?: boolean;
} = {},
) {
const {compilationMode, usePoisonedData} = opts;
const {
compilationMode = CompilationMode.FULL,
usePoisonedData,
externalRuntimeStyles = false,
} = opts;
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null);
Expand Down Expand Up @@ -145,6 +151,7 @@ function setup(
/* forbidOrphanRenderering */ false,
/* enableBlockSyntax */ true,
/* enableLetSyntax */ true,
externalRuntimeStyles,
/* localCompilationExtraImportsTracker */ null,
jitDeclarationRegistry,
);
Expand Down Expand Up @@ -357,6 +364,153 @@ runInEachFileSystem(() => {
expect(compileResult).toEqual([]);
});

it('should populate externalStyles from styleUrl when externalRuntimeStyles is enabled', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/myStyle.css'),
contents: '<div>hello world</div>',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '',
styleUrl: '/myStyle.css',
styles: ['a { color: red; }', 'b { color: blue; }'],
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler} = setup(program, options, host, {
externalRuntimeStyles: true,
});
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.styles.size).toBe(0);
expect(analysis?.meta.externalStyles).toEqual(['/myStyle.css']);
});

it('should populate externalStyles from styleUrls when externalRuntimeStyles is enabled', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/myStyle.css'),
contents: '<div>hello world</div>',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '',
styleUrls: ['/myStyle.css', '/myOtherStyle.css'],
styles: ['a { color: red; }', 'b { color: blue; }'],
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler} = setup(program, options, host, {
externalRuntimeStyles: true,
});
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.styles.size).toBe(0);
expect(analysis?.meta.externalStyles).toEqual(['/myStyle.css', '/myOtherStyle.css']);
});

it('should populate externalStyles from template link element when externalRuntimeStyles is enabled', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/myStyle.css'),
contents: '<div>hello world</div>',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '<link rel="stylesheet" href="myTemplateStyle.css" />',
styles: ['a { color: red; }', 'b { color: blue; }'],
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler} = setup(program, options, host, {
externalRuntimeStyles: true,
});
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.styles.size).toBe(0);
expect(analysis?.meta.externalStyles).toEqual(['myTemplateStyle.css']);
});

it('should populate externalStyles with resolve return values when externalRuntimeStyles is enabled', () => {
const {program, options, host} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Component: any;',
},
{
name: _('/myStyle.css'),
contents: '<div>hello world</div>',
},
{
name: _('/entry.ts'),
contents: `
import {Component} from '@angular/core';
@Component({
template: '<link rel="stylesheet" href="myTemplateStyle.css" />',
styleUrl: '/myStyle.css',
styles: ['a { color: red; }', 'b { color: blue; }'],
}) class TestCmp {}
`,
},
]);
const {reflectionHost, handler, resourceLoader} = setup(program, options, host, {
externalRuntimeStyles: true,
});
resourceLoader.resolve = (v) => 'abc/' + v;
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
if (detected === undefined) {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
expect(analysis?.resources.styles.size).toBe(0);
expect(analysis?.meta.externalStyles).toEqual([
'abc//myStyle.css',
'abc/myTemplateStyle.css',
]);
});

it('should replace inline style content with transformed content', async () => {
const {program, options, host} = makeProgram([
{
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-cli/src/ngtsc/core/api/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ export interface InternalOptions {
*/
_enableLetSyntax?: boolean;

/**
* Enables the use of link elements for component styleUrls instead of inlining the file
* content.
* This option is intended to be used with a development server that processes and serves
* the files on-demand for an application.
*
* @internal
*/
externalRuntimeStyles?: boolean;

/**
* Detected version of `@angular/core` in the workspace. Used by the
* compiler to adjust the output depending on the available symbols.
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/core/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,7 @@ export class NgCompiler {
!!this.options.forbidOrphanComponents,
this.enableBlockSyntax,
this.enableLetSyntax,
this.options['externalRuntimeStyles'] ?? false,
localCompilationExtraImportsTracker,
jitDeclarationRegistry,
),
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/render3/view/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
*/
styles: string[];

/**
* A collection of style paths for external stylesheets that will be applied and scoped to the component.
*/
externalStyles?: string[];

/**
* An encapsulation policy for the component's styling.
* Possible values:
Expand Down
13 changes: 12 additions & 1 deletion packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export function compileComponentFromMetadata(
meta.encapsulation = core.ViewEncapsulation.Emulated;
}

let hasStyles = false;
// e.g. `styles: [str1, str2]`
if (meta.styles && meta.styles.length) {
const styleValues =
Expand All @@ -295,9 +296,19 @@ export function compileComponentFromMetadata(
}, [] as o.Expression[]);

if (styleNodes.length > 0) {
hasStyles = true;
definitionMap.set('styles', o.literalArr(styleNodes));
}
} else if (meta.encapsulation === core.ViewEncapsulation.Emulated) {
}
if (meta.externalStyles?.length) {
hasStyles = true;
const externalStyleNodes = meta.externalStyles.map((externalStyle) =>
constantPool.getConstLiteral(o.literal(externalStyle)),
);
definitionMap.set('externalStyles', o.literalArr(externalStyleNodes));
}

if (!hasStyles && meta.encapsulation === core.ViewEncapsulation.Emulated) {
// If there is no style, don't generate css selectors on elements
meta.encapsulation = core.ViewEncapsulation.None;
}
Expand Down

0 comments on commit 251f6b2

Please sign in to comment.