feat(ivy): generator of setClassMetadata statements for Angular types (#26860)
This commit introduces generateSetClassMetadataCall(), an API in ngtsc for generating calls to setClassMetadata() for a given declaration. The reflection API is used to enumerate Angular decorators on the declaration, which are converted to a format that ReflectionCapabilities can understand. The reflection metadata is then patched onto the declared type via a call to setClassMetadata(). This is simply a utility, a future commit invokes this utility for each DecoratorHandler. Testing strategy: tests are included which exercise generateSetClassMetadata in isolation. PR Close #26860
This commit is contained in:
parent
ca1e538752
commit
492576114d
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {ExternalExpr, Identifiers, InvokeFunctionExpr, Statement, WrappedNodeExpr} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {CtorParameter, Decorator, ReflectionHost} from '../../host';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
|
||||||
|
* present on the class or its member fields.
|
||||||
|
*
|
||||||
|
* If no such metadata is present, this function returns `null`. Otherwise, the call is returned
|
||||||
|
* as a `Statement` for inclusion along with the class.
|
||||||
|
*/
|
||||||
|
export function generateSetClassMetadataCall(
|
||||||
|
clazz: ts.Declaration, reflection: ReflectionHost, isCore: boolean): Statement|null {
|
||||||
|
// Classes come in two flavors, class declarations (ES2015) and variable declarations (ES5).
|
||||||
|
// Both must have a declared name to have metadata set on them.
|
||||||
|
if ((!ts.isClassDeclaration(clazz) && !ts.isVariableDeclaration(clazz)) ||
|
||||||
|
clazz.name === undefined || !ts.isIdentifier(clazz.name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const id = ts.updateIdentifier(clazz.name);
|
||||||
|
|
||||||
|
// Reflect over the class decorators. If none are present, or those that are aren't from
|
||||||
|
// Angular, then return null. Otherwise, turn them into metadata.
|
||||||
|
const classDecorators = reflection.getDecoratorsOfDeclaration(clazz);
|
||||||
|
if (classDecorators === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ngClassDecorators =
|
||||||
|
classDecorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
|
||||||
|
if (ngClassDecorators.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const metaDecorators = ts.createArrayLiteral(ngClassDecorators);
|
||||||
|
|
||||||
|
// Convert the constructor parameters to metadata, passing null if none are present.
|
||||||
|
let metaCtorParameters: ts.Expression = ts.createNull();
|
||||||
|
const classCtorParameters = reflection.getConstructorParameters(clazz);
|
||||||
|
if (classCtorParameters !== null) {
|
||||||
|
metaCtorParameters = ts.createArrayLiteral(
|
||||||
|
classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the same for property decorators.
|
||||||
|
let metaPropDecorators: ts.Expression = ts.createNull();
|
||||||
|
const decoratedMembers =
|
||||||
|
reflection.getMembersOfClass(clazz)
|
||||||
|
.filter(member => !member.isStatic && member.decorators !== null)
|
||||||
|
.map(member => classMemberToMetadata(member.name, member.decorators !, isCore));
|
||||||
|
if (decoratedMembers.length > 0) {
|
||||||
|
metaPropDecorators = ts.createObjectLiteral(decoratedMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a pure call to setClassMetadata with the class identifier and its metadata.
|
||||||
|
const setClassMetadata = new ExternalExpr(Identifiers.setClassMetadata);
|
||||||
|
const fnCall = new InvokeFunctionExpr(
|
||||||
|
/* fn */ setClassMetadata,
|
||||||
|
/* args */
|
||||||
|
[
|
||||||
|
new WrappedNodeExpr(id),
|
||||||
|
new WrappedNodeExpr(metaDecorators),
|
||||||
|
new WrappedNodeExpr(metaCtorParameters),
|
||||||
|
new WrappedNodeExpr(metaPropDecorators),
|
||||||
|
],
|
||||||
|
/* type */ undefined,
|
||||||
|
/* sourceSpan */ undefined,
|
||||||
|
/* pure */ true);
|
||||||
|
return fnCall.toStmt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a reflected constructor parameter to metadata.
|
||||||
|
*/
|
||||||
|
function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): ts.Expression {
|
||||||
|
// Parameters sometimes have a type that can be referenced. If so, then use it, otherwise
|
||||||
|
// its type is undefined.
|
||||||
|
const type = param.type !== null ? param.type : ts.createIdentifier('undefined');
|
||||||
|
const properties: ts.ObjectLiteralElementLike[] = [
|
||||||
|
ts.createPropertyAssignment('type', type),
|
||||||
|
];
|
||||||
|
|
||||||
|
// If the parameter has decorators, include the ones from Angular.
|
||||||
|
if (param.decorators !== null) {
|
||||||
|
const ngDecorators =
|
||||||
|
param.decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
|
||||||
|
properties.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(ngDecorators)));
|
||||||
|
}
|
||||||
|
return ts.createObjectLiteral(properties, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a reflected class member to metadata.
|
||||||
|
*/
|
||||||
|
function classMemberToMetadata(
|
||||||
|
name: string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment {
|
||||||
|
const ngDecorators =
|
||||||
|
decorators.filter(dec => isAngularDecorator(dec, isCore)).map(decoratorToMetadata);
|
||||||
|
const decoratorMeta = ts.createArrayLiteral(ngDecorators);
|
||||||
|
return ts.createPropertyAssignment(name, decoratorMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a reflected decorator to metadata.
|
||||||
|
*/
|
||||||
|
function decoratorToMetadata(decorator: Decorator): ts.ObjectLiteralExpression {
|
||||||
|
// Decorators have a type.
|
||||||
|
const properties: ts.ObjectLiteralElementLike[] = [
|
||||||
|
ts.createPropertyAssignment('type', ts.updateIdentifier(decorator.identifier)),
|
||||||
|
];
|
||||||
|
// Sometimes they have arguments.
|
||||||
|
if (decorator.args !== null && decorator.args.length > 0) {
|
||||||
|
const args = decorator.args.map(arg => ts.getMutableClone(arg));
|
||||||
|
properties.push(ts.createPropertyAssignment('args', ts.createArrayLiteral(args)));
|
||||||
|
}
|
||||||
|
return ts.createObjectLiteral(properties, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a given decorator should be treated as an Angular decorator.
|
||||||
|
*
|
||||||
|
* Either it's used in @angular/core, or it's imported from there.
|
||||||
|
*/
|
||||||
|
function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean {
|
||||||
|
return isCore || (decorator.import !== null && decorator.import.from === '@angular/core');
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ ts_library(
|
||||||
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
"//packages/compiler-cli/src/ngtsc/diagnostics",
|
||||||
"//packages/compiler-cli/src/ngtsc/metadata",
|
"//packages/compiler-cli/src/ngtsc/metadata",
|
||||||
"//packages/compiler-cli/src/ngtsc/testing",
|
"//packages/compiler-cli/src/ngtsc/testing",
|
||||||
|
"//packages/compiler-cli/src/ngtsc/translator",
|
||||||
"@ngdeps//typescript",
|
"@ngdeps//typescript",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Statement} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {TypeScriptReflectionHost} from '../../metadata';
|
||||||
|
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||||
|
import {ImportManager, translateStatement} from '../../translator';
|
||||||
|
import {generateSetClassMetadataCall} from '../src/metadata';
|
||||||
|
|
||||||
|
const CORE = {
|
||||||
|
name: 'node_modules/@angular/core/index.d.ts',
|
||||||
|
contents: `
|
||||||
|
export declare function Input(...args: any[]): any;
|
||||||
|
export declare function Inject(...args: any[]): any;
|
||||||
|
export declare function Component(...args: any[]): any;
|
||||||
|
export declare class Injector {}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ngtsc setClassMetadata converter', () => {
|
||||||
|
it('should convert decorated class metadata', () => {
|
||||||
|
const res = compileAndPrint(`
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component('metadata') class Target {}
|
||||||
|
`);
|
||||||
|
expect(res).toEqual(
|
||||||
|
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert decorated class construtor parameter metadata', () => {
|
||||||
|
const res = compileAndPrint(`
|
||||||
|
import {Component, Inject, Injector} from '@angular/core';
|
||||||
|
const FOO = 'foo';
|
||||||
|
|
||||||
|
@Component('metadata') class Target {
|
||||||
|
constructor(@Inject(FOO) foo: any, bar: Injector) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(res).toContain(
|
||||||
|
`[{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }], null);`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert decorated field metadata', () => {
|
||||||
|
const res = compileAndPrint(`
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Component('metadata') class Target {
|
||||||
|
@Input() foo: string;
|
||||||
|
|
||||||
|
@Input('value') bar: string;
|
||||||
|
|
||||||
|
notDecorated: string;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not convert non-angular decorators to metadata', () => {
|
||||||
|
const res = compileAndPrint(`
|
||||||
|
declare function NotAComponent(...args: any[]): any;
|
||||||
|
|
||||||
|
@NotAComponent('metadata') class Target {}
|
||||||
|
`);
|
||||||
|
expect(res).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function compileAndPrint(contents: string): string {
|
||||||
|
const {program} = makeProgram([
|
||||||
|
CORE, {
|
||||||
|
name: 'index.ts',
|
||||||
|
contents,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const host = new TypeScriptReflectionHost(program.getTypeChecker());
|
||||||
|
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration);
|
||||||
|
const call = generateSetClassMetadataCall(target, host, false);
|
||||||
|
if (call === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const sf = program.getSourceFile('index.ts') !;
|
||||||
|
const im = new ImportManager(false, 'i');
|
||||||
|
const tsStatement = translateStatement(call, im);
|
||||||
|
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
|
||||||
|
return res.replace(/\s+/g, ' ');
|
||||||
|
}
|
Loading…
Reference in New Issue