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/metadata",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@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