Skip to content

Commit 1de04b1

Browse files
clydinatscott
authored andcommitted
feat(compiler-cli): support transforming component style resources (#41307)
This change introduces a new hook on the `ResourceHost` interface named `transformResource`. Resource transformation allows both external and inline resources to be transformed prior to compilation by the AOT compiler. This provides support for tooling integrations to enable features such as preprocessor support for inline styles. Only style resources are currently supported. However, the infrastructure is in place to add template support in the future. PR Close #41307
1 parent dc65526 commit 1de04b1

File tree

9 files changed

+231
-32
lines changed

9 files changed

+231
-32
lines changed

packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ import {isWithinPackage, NOOP_DEPENDENCY_TRACKER} from './util';
4040
class NgccResourceLoader implements ResourceLoader {
4141
constructor(private fs: ReadonlyFileSystem) {}
4242
canPreload = false;
43+
canPreprocess = false;
4344
preload(): undefined|Promise<void> {
4445
throw new Error('Not implemented.');
4546
}
47+
preprocessInline(): Promise<string> {
48+
throw new Error('Not implemented.');
49+
}
4650
load(url: string): string {
4751
return this.fs.readFile(this.fs.resolve(url));
4852
}

packages/compiler-cli/src/ngtsc/annotations/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/// <reference types="node" />
1010

11-
export {ResourceLoader} from './src/api';
11+
export {ResourceLoader, ResourceLoaderContext} from './src/api';
1212
export {ComponentDecoratorHandler} from './src/component';
1313
export {DirectiveDecoratorHandler} from './src/directive';
1414
export {InjectableDecoratorHandler} from './src/injectable';

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export interface ResourceLoader {
2121
*/
2222
canPreload: boolean;
2323

24+
/**
25+
* If true, the resource loader is able to preprocess inline resources.
26+
*/
27+
canPreprocess: boolean;
28+
2429
/**
2530
* Resolve the url of a resource relative to the file that contains the reference to it.
2631
* The return value of this method can be used in the `load()` and `preload()` methods.
@@ -37,11 +42,22 @@ export interface ResourceLoader {
3742
* should be cached so it can be accessed synchronously via the `load()` method.
3843
*
3944
* @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to preload.
45+
* @param context Information regarding the resource such as the type and containing file.
4046
* @returns A Promise that is resolved once the resource has been loaded or `undefined`
4147
* if the file has already been loaded.
4248
* @throws An Error if pre-loading is not available.
4349
*/
44-
preload(resolvedUrl: string): Promise<void>|undefined;
50+
preload(resolvedUrl: string, context: ResourceLoaderContext): Promise<void>|undefined;
51+
52+
/**
53+
* Preprocess the content data of an inline resource, asynchronously.
54+
*
55+
* @param data The existing content data from the inline resource.
56+
* @param context Information regarding the resource such as the type and containing file.
57+
* @returns A Promise that resolves to the processed data. If no processing occurs, the
58+
* same data string that was passed to the function will be resolved.
59+
*/
60+
preprocessInline(data: string, context: ResourceLoaderContext): Promise<string>;
4561

4662
/**
4763
* Load the resource at the given url, synchronously.
@@ -53,3 +69,22 @@ export interface ResourceLoader {
5369
*/
5470
load(resolvedUrl: string): string;
5571
}
72+
73+
/**
74+
* Contextual information used by members of the ResourceLoader interface.
75+
*/
76+
export interface ResourceLoaderContext {
77+
/**
78+
* The type of the component resource.
79+
* * Resources referenced via a component's `styles` or `styleUrls` properties are of
80+
* type `style`.
81+
* * Resources referenced via a component's `template` or `templateUrl` properties are of type
82+
* `template`.
83+
*/
84+
type: 'style'|'template';
85+
86+
/**
87+
* The absolute path to the file that contains the resource or reference to the resource.
88+
*/
89+
containingFile: string;
90+
}

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

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export class ComponentDecoratorHandler implements
220220
* thrown away, and the parsed template is reused during the analyze phase.
221221
*/
222222
private preanalyzeTemplateCache = new Map<DeclarationNode, ParsedTemplateWithSource>();
223+
private preanalyzeStylesCache = new Map<DeclarationNode, string[]|null>();
223224

224225
readonly precedence = HandlerPrecedence.PRIMARY;
225226
readonly name = ComponentDecoratorHandler.name;
@@ -266,7 +267,7 @@ export class ComponentDecoratorHandler implements
266267
resourceType: ResourceTypeForDiagnostics): Promise<void>|undefined => {
267268
const resourceUrl =
268269
this._resolveResourceOrThrow(styleUrl, containingFile, nodeForError, resourceType);
269-
return this.resourceLoader.preload(resourceUrl);
270+
return this.resourceLoader.preload(resourceUrl, {type: 'style', containingFile});
270271
};
271272

272273
// A Promise that waits for the template and all <link>ed styles within it to be preloaded.
@@ -289,22 +290,33 @@ export class ComponentDecoratorHandler implements
289290
// Extract all the styleUrls in the decorator.
290291
const componentStyleUrls = this._extractComponentStyleUrls(component);
291292

292-
if (componentStyleUrls === null) {
293-
// A fast path exists if there are no styleUrls, to just wait for
294-
// templateAndTemplateStyleResources.
295-
return templateAndTemplateStyleResources;
296-
} else {
297-
// Wait for both the template and all styleUrl resources to resolve.
298-
return Promise
299-
.all([
300-
templateAndTemplateStyleResources,
301-
...componentStyleUrls.map(
302-
styleUrl => resolveStyleUrl(
303-
styleUrl.url, styleUrl.nodeForError,
304-
ResourceTypeForDiagnostics.StylesheetFromDecorator))
305-
])
306-
.then(() => undefined);
293+
// Extract inline styles, process, and cache for use in synchronous analyze phase
294+
let inlineStyles;
295+
if (component.has('styles')) {
296+
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
297+
if (litStyles === null) {
298+
this.preanalyzeStylesCache.set(node, null);
299+
} else {
300+
inlineStyles = Promise
301+
.all(litStyles.map(
302+
style => this.resourceLoader.preprocessInline(
303+
style, {type: 'style', containingFile})))
304+
.then(styles => {
305+
this.preanalyzeStylesCache.set(node, styles);
306+
});
307+
}
307308
}
309+
310+
// Wait for both the template and all styleUrl resources to resolve.
311+
return Promise
312+
.all([
313+
templateAndTemplateStyleResources, inlineStyles,
314+
...componentStyleUrls.map(
315+
styleUrl => resolveStyleUrl(
316+
styleUrl.url, styleUrl.nodeForError,
317+
ResourceTypeForDiagnostics.StylesheetFromDecorator))
318+
])
319+
.then(() => undefined);
308320
}
309321

310322
analyze(
@@ -409,12 +421,29 @@ export class ComponentDecoratorHandler implements
409421
}
410422
}
411423

424+
// If inline styles were preprocessed use those
412425
let inlineStyles: string[]|null = null;
413-
if (component.has('styles')) {
414-
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
415-
if (litStyles !== null) {
416-
inlineStyles = [...litStyles];
417-
styles.push(...litStyles);
426+
if (this.preanalyzeStylesCache.has(node)) {
427+
inlineStyles = this.preanalyzeStylesCache.get(node)!;
428+
this.preanalyzeStylesCache.delete(node);
429+
if (inlineStyles !== null) {
430+
styles.push(...inlineStyles);
431+
}
432+
} else {
433+
// Preprocessing is only supported asynchronously
434+
// If no style cache entry is present asynchronous preanalyze was not executed.
435+
// This protects against accidental differences in resource contents when preanalysis
436+
// is not used with a provided transformResource hook on the ResourceHost.
437+
if (this.resourceLoader.canPreprocess) {
438+
throw new Error('Inline resource processing requires asynchronous preanalyze.');
439+
}
440+
441+
if (component.has('styles')) {
442+
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
443+
if (litStyles !== null) {
444+
inlineStyles = [...litStyles];
445+
styles.push(...litStyles);
446+
}
418447
}
419448
}
420449
if (template.styles.length > 0) {
@@ -979,7 +1008,8 @@ export class ComponentDecoratorHandler implements
9791008
}
9801009
const resourceUrl = this._resolveResourceOrThrow(
9811010
templateUrl, containingFile, templateUrlExpr, ResourceTypeForDiagnostics.Template);
982-
const templatePromise = this.resourceLoader.preload(resourceUrl);
1011+
const templatePromise =
1012+
this.resourceLoader.preload(resourceUrl, {type: 'template', containingFile});
9831013

9841014
// If the preload worked, then actually load and parse the template, and wait for any style
9851015
// URLs to resolve.

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,24 @@ import {NOOP_PERF_RECORDER} from '../../perf';
2020
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
2121
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope';
2222
import {getDeclaration, makeProgram} from '../../testing';
23-
import {ResourceLoader} from '../src/api';
23+
import {ResourceLoader, ResourceLoaderContext} from '../src/api';
2424
import {ComponentDecoratorHandler} from '../src/component';
2525

2626
export class StubResourceLoader implements ResourceLoader {
2727
resolve(v: string): string {
2828
return v;
2929
}
3030
canPreload = false;
31+
canPreprocess = false;
3132
load(v: string): string {
3233
return '';
3334
}
3435
preload(): Promise<void>|undefined {
3536
throw new Error('Not implemented');
3637
}
38+
preprocessInline(_data: string, _context: ResourceLoaderContext): Promise<string> {
39+
throw new Error('Not implemented');
40+
}
3741
}
3842

3943
function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.CompilerHost) {
@@ -54,6 +58,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
5458
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
5559
const resourceRegistry = new ResourceRegistry();
5660
const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeRegistry, metaReader);
61+
const resourceLoader = new StubResourceLoader();
5762

5863
const handler = new ComponentDecoratorHandler(
5964
reflectionHost,
@@ -65,7 +70,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
6570
typeCheckScopeRegistry,
6671
resourceRegistry,
6772
/* isCore */ false,
68-
new StubResourceLoader(),
73+
resourceLoader,
6974
/* rootDirs */['/'],
7075
/* defaultPreserveWhitespaces */ false,
7176
/* i18nUseExternalIds */ true,
@@ -83,7 +88,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
8388
/* annotateForClosureCompiler */ false,
8489
NOOP_PERF_RECORDER,
8590
);
86-
return {reflectionHost, handler};
91+
return {reflectionHost, handler, resourceLoader};
8792
}
8893

8994
runInEachFileSystem(() => {
@@ -257,6 +262,47 @@ runInEachFileSystem(() => {
257262
handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool());
258263
expect(compileResult).toEqual([]);
259264
});
265+
266+
it('should replace inline style content with transformed content', async () => {
267+
const {program, options, host} = makeProgram([
268+
{
269+
name: _('/node_modules/@angular/core/index.d.ts'),
270+
contents: 'export const Component: any;',
271+
},
272+
{
273+
name: _('/entry.ts'),
274+
contents: `
275+
import {Component} from '@angular/core';
276+
277+
@Component({
278+
template: '',
279+
styles: ['.abc {}']
280+
}) class TestCmp {}
281+
`
282+
},
283+
]);
284+
const {reflectionHost, handler, resourceLoader} = setup(program, options, host);
285+
resourceLoader.canPreload = true;
286+
resourceLoader.canPreprocess = true;
287+
resourceLoader.preprocessInline = async function(data, context) {
288+
expect(data).toBe('.abc {}');
289+
expect(context.containingFile).toBe(_('/entry.ts').toLowerCase());
290+
expect(context.type).toBe('style');
291+
292+
return '.xyz {}';
293+
};
294+
295+
const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration);
296+
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
297+
if (detected === undefined) {
298+
return fail('Failed to recognize @Component');
299+
}
300+
301+
await handler.preanalyze(TestCmp, detected.metadata);
302+
303+
const {analysis} = handler.analyze(TestCmp, detected.metadata);
304+
expect(analysis?.inlineStyles).toEqual(jasmine.arrayWithExactContents(['.xyz {}']));
305+
});
260306
});
261307

262308
function ivyCode(code: ErrorCode): number {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export type ExtendedCompilerHostMethods =
2828
'getCurrentDirectory'|
2929
// Additional methods of `ExtendedTsCompilerHost` related to resource files (e.g. HTML
3030
// templates). These are optional.
31-
'getModifiedResourceFiles'|'readResource'|'resourceNameToFileName';
31+
'getModifiedResourceFiles'|'readResource'|'resourceNameToFileName'|'transformResource';
3232

3333
/**
3434
* Adapter for `NgCompiler` that allows it to be used in various circumstances, such as

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,52 @@ export interface ResourceHost {
4949
* or `undefined` if this is not an incremental build.
5050
*/
5151
getModifiedResourceFiles?(): Set<string>|undefined;
52+
53+
/**
54+
* Transform an inline or external resource asynchronously.
55+
* It is assumed the consumer of the corresponding `Program` will call
56+
* `loadNgStructureAsync()`. Using outside `loadNgStructureAsync()` will
57+
* cause a diagnostics error or an exception to be thrown.
58+
* Only style resources are currently supported.
59+
*
60+
* @param data The resource data to transform.
61+
* @param context Information regarding the resource such as the type and containing file.
62+
* @returns A promise of either the transformed resource data or null if no transformation occurs.
63+
*/
64+
transformResource?
65+
(data: string, context: ResourceHostContext): Promise<TransformResourceResult|null>;
66+
}
67+
68+
/**
69+
* Contextual information used by members of the ResourceHost interface.
70+
*/
71+
export interface ResourceHostContext {
72+
/**
73+
* The type of the component resource. Templates are not yet supported.
74+
* * Resources referenced via a component's `styles` or `styleUrls` properties are of
75+
* type `style`.
76+
*/
77+
readonly type: 'style';
78+
/**
79+
* The absolute path to the resource file. If the resource is inline, the value will be null.
80+
*/
81+
readonly resourceFile: string|null;
82+
/**
83+
* The absolute path to the file that contains the resource or reference to the resource.
84+
*/
85+
readonly containingFile: string;
86+
}
87+
88+
/**
89+
* The successful transformation result of the `ResourceHost.transformResource` function.
90+
* This interface may be expanded in the future to include diagnostic information and source mapping
91+
* support.
92+
*/
93+
export interface TransformResourceResult {
94+
/**
95+
* The content generated by the transformation.
96+
*/
97+
content: string;
5298
}
5399

54100
/**

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class DelegatingCompilerHost implements
5959
readDirectory = this.delegateMethod('readDirectory');
6060
readFile = this.delegateMethod('readFile');
6161
readResource = this.delegateMethod('readResource');
62+
transformResource = this.delegateMethod('transformResource');
6263
realpath = this.delegateMethod('realpath');
6364
resolveModuleNames = this.delegateMethod('resolveModuleNames');
6465
resolveTypeReferenceDirectives = this.delegateMethod('resolveTypeReferenceDirectives');

0 commit comments

Comments
 (0)