Skip to content

Commit 6391984

Browse files
clydinmgechev
authored andcommitted
fix(@ngtools/webpack): downlevel constructor parameter type information
The TypeScript `emitDecoratorMetadata` option does not support forward references of ES2015 classes due to the TypeScript compiler transforming the metadata into a direct reference of the yet to be defined class. This results in a runtime TDZ error. This is a known limitation of the `emitDecoratorMetadata` option. The change in the PR removes the need for the option by storing the information in a static property function via the use of the tsickle `ctorParameters` transformation. By leveraging the existing functionality, no changes to the framework code is necessary. Also, minimal new code is required within the CLI, as the relevant tsickle code can be extracted and used with several local modifications.
1 parent 7693587 commit 6391984

File tree

3 files changed

+427
-0
lines changed

3 files changed

+427
-0
lines changed

packages/ngtools/webpack/src/angular_compiler_plugin.ts

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
replaceServerBootstrap,
6161
} from './transformers';
6262
import { collectDeepNodes } from './transformers/ast_helpers';
63+
import { downlevelConstructorParameters } from './transformers/ctor-parameters';
6364
import {
6465
AUTO_START_ARG,
6566
} from './type_checker';
@@ -914,6 +915,9 @@ export class AngularCompilerPlugin {
914915
// Replace resources in JIT.
915916
this._transformers.push(
916917
replaceResources(isAppPath, getTypeChecker, this._options.directTemplateLoading));
918+
// Downlevel constructor parameters for DI support
919+
// This is required to support forwardRef in ES2015 due to TDZ issues
920+
this._transformers.push(downlevelConstructorParameters(getTypeChecker));
917921
} else {
918922
// Remove unneeded angular decorators.
919923
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
10+
export function downlevelConstructorParameters(
11+
getTypeChecker: () => ts.TypeChecker,
12+
): ts.TransformerFactory<ts.SourceFile> {
13+
return (context: ts.TransformationContext) => {
14+
const transformer = decoratorDownlevelTransformer(getTypeChecker(), []);
15+
16+
return transformer(context);
17+
};
18+
}
19+
20+
// The following is sourced from tsickle with local modifications
21+
// Only the creation of `ctorParameters` is retained
22+
// tslint:disable-next-line:max-line-length
23+
// https://github.com/angular/tsickle/blob/0ceb7d6bc47f6945a6c4c09689f1388eb48f5c07/src/decorator_downlevel_transformer.ts
24+
//
25+
26+
/**
27+
* Extracts the type of the decorator (the function or expression invoked), as well as all the
28+
* arguments passed to the decorator. Returns an AST with the form:
29+
*
30+
* // For @decorator(arg1, arg2)
31+
* { type: decorator, args: [arg1, arg2] }
32+
*/
33+
function extractMetadataFromSingleDecorator(
34+
decorator: ts.Decorator,
35+
diagnostics: ts.Diagnostic[],
36+
): ts.ObjectLiteralExpression {
37+
const metadataProperties: ts.ObjectLiteralElementLike[] = [];
38+
const expr = decorator.expression;
39+
switch (expr.kind) {
40+
case ts.SyntaxKind.Identifier:
41+
// The decorator was a plain @Foo.
42+
metadataProperties.push(ts.createPropertyAssignment('type', expr));
43+
break;
44+
case ts.SyntaxKind.CallExpression:
45+
// The decorator was a call, like @Foo(bar).
46+
const call = expr as ts.CallExpression;
47+
metadataProperties.push(ts.createPropertyAssignment('type', call.expression));
48+
if (call.arguments.length) {
49+
const args: ts.Expression[] = [];
50+
for (const arg of call.arguments) {
51+
args.push(arg);
52+
}
53+
const argsArrayLiteral = ts.createArrayLiteral(args);
54+
argsArrayLiteral.elements.hasTrailingComma = true;
55+
metadataProperties.push(ts.createPropertyAssignment('args', argsArrayLiteral));
56+
}
57+
break;
58+
default:
59+
diagnostics.push({
60+
file: decorator.getSourceFile(),
61+
start: decorator.getStart(),
62+
length: decorator.getEnd() - decorator.getStart(),
63+
messageText: `${
64+
ts.SyntaxKind[decorator.kind]
65+
} not implemented in gathering decorator metadata`,
66+
category: ts.DiagnosticCategory.Error,
67+
code: 0,
68+
});
69+
break;
70+
}
71+
72+
return ts.createObjectLiteral(metadataProperties);
73+
}
74+
75+
/**
76+
* createCtorParametersClassProperty creates a static 'ctorParameters' property containing
77+
* downleveled decorator information.
78+
*
79+
* The property contains an arrow function that returns an array of object literals of the shape:
80+
* static ctorParameters = () => [{
81+
* type: SomeClass|undefined, // the type of the param that's decorated, if it's a value.
82+
* decorators: [{
83+
* type: DecoratorFn, // the type of the decorator that's invoked.
84+
* args: [ARGS], // the arguments passed to the decorator.
85+
* }]
86+
* }];
87+
*/
88+
function createCtorParametersClassProperty(
89+
diagnostics: ts.Diagnostic[],
90+
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
91+
ctorParameters: ParameterDecorationInfo[],
92+
): ts.PropertyDeclaration {
93+
const params: ts.Expression[] = [];
94+
95+
for (const ctorParam of ctorParameters) {
96+
if (!ctorParam.type && ctorParam.decorators.length === 0) {
97+
params.push(ts.createNull());
98+
continue;
99+
}
100+
101+
const paramType = ctorParam.type
102+
? typeReferenceToExpression(entityNameToExpression, ctorParam.type)
103+
: undefined;
104+
const members = [
105+
ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined')),
106+
];
107+
108+
const decorators: ts.ObjectLiteralExpression[] = [];
109+
for (const deco of ctorParam.decorators) {
110+
decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics));
111+
}
112+
if (decorators.length) {
113+
members.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(decorators)));
114+
}
115+
params.push(ts.createObjectLiteral(members));
116+
}
117+
118+
const initializer = ts.createArrowFunction(
119+
undefined,
120+
undefined,
121+
[],
122+
undefined,
123+
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
124+
ts.createArrayLiteral(params, true),
125+
);
126+
127+
const ctorProp = ts.createProperty(
128+
undefined,
129+
[ts.createToken(ts.SyntaxKind.StaticKeyword)],
130+
'ctorParameters',
131+
undefined,
132+
undefined,
133+
initializer,
134+
);
135+
136+
return ctorProp;
137+
}
138+
139+
/**
140+
* Returns an expression representing the (potentially) value part for the given node.
141+
*
142+
* This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a
143+
* workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode
144+
* not being exposed). In practice this implementation is sufficient for Angular's use of type
145+
* metadata.
146+
*/
147+
function typeReferenceToExpression(
148+
entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined,
149+
node: ts.TypeNode,
150+
): ts.Expression | undefined {
151+
let kind = node.kind;
152+
if (ts.isLiteralTypeNode(node)) {
153+
// Treat literal types like their base type (boolean, string, number).
154+
kind = node.literal.kind;
155+
}
156+
switch (kind) {
157+
case ts.SyntaxKind.FunctionType:
158+
case ts.SyntaxKind.ConstructorType:
159+
return ts.createIdentifier('Function');
160+
case ts.SyntaxKind.ArrayType:
161+
case ts.SyntaxKind.TupleType:
162+
return ts.createIdentifier('Array');
163+
case ts.SyntaxKind.TypePredicate:
164+
case ts.SyntaxKind.TrueKeyword:
165+
case ts.SyntaxKind.FalseKeyword:
166+
case ts.SyntaxKind.BooleanKeyword:
167+
return ts.createIdentifier('Boolean');
168+
case ts.SyntaxKind.StringLiteral:
169+
case ts.SyntaxKind.StringKeyword:
170+
return ts.createIdentifier('String');
171+
case ts.SyntaxKind.ObjectKeyword:
172+
return ts.createIdentifier('Object');
173+
case ts.SyntaxKind.NumberKeyword:
174+
case ts.SyntaxKind.NumericLiteral:
175+
return ts.createIdentifier('Number');
176+
case ts.SyntaxKind.TypeReference:
177+
const typeRef = node as ts.TypeReferenceNode;
178+
179+
// Ignore any generic types, just return the base type.
180+
return entityNameToExpression(typeRef.typeName);
181+
default:
182+
return undefined;
183+
}
184+
}
185+
186+
/** ParameterDecorationInfo describes the information for a single constructor parameter. */
187+
interface ParameterDecorationInfo {
188+
/**
189+
* The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an
190+
* interface).
191+
*/
192+
type: ts.TypeNode | null;
193+
/** The list of decorators found on the parameter, null if none. */
194+
decorators: ts.Decorator[];
195+
}
196+
197+
/**
198+
* Transformer factory for the decorator downlevel transformer. See fileoverview for details.
199+
*/
200+
export function decoratorDownlevelTransformer(
201+
typeChecker: ts.TypeChecker,
202+
diagnostics: ts.Diagnostic[],
203+
): (context: ts.TransformationContext) => ts.Transformer<ts.SourceFile> {
204+
return (context: ts.TransformationContext) => {
205+
/** A map from symbols to the identifier of an import, reset per SourceFile. */
206+
let importNamesBySymbol = new Map<ts.Symbol, ts.Identifier>();
207+
208+
/**
209+
* Converts an EntityName (from a type annotation) to an expression (accessing a value).
210+
*
211+
* For a given ts.EntityName, this walks depth first to find the leftmost ts.Identifier, then
212+
* converts the path into property accesses.
213+
*
214+
* This generally works, but TypeScript's emit pipeline does not serialize identifiers that are
215+
* only used in a type location (such as identifiers in a TypeNode), even if the identifier
216+
* itself points to a value (e.g. a class). To avoid that problem, this method finds the symbol
217+
* representing the identifier (using typeChecker), then looks up where it was imported (using
218+
* importNamesBySymbol), and then uses the imported name instead of the identifier from the type
219+
* expression, if any. Otherwise it'll use the identifier unchanged. This makes sure the
220+
* identifier is not marked as stemming from a "type only" expression, causing it to be emitted
221+
* and causing the import to be retained.
222+
*/
223+
function entityNameToExpression(name: ts.EntityName): ts.Expression | undefined {
224+
const sym = typeChecker.getSymbolAtLocation(name);
225+
if (!sym) {
226+
return undefined;
227+
}
228+
// Check if the entity name references a symbol that is an actual value. If it is not, it
229+
// cannot be referenced by an expression, so return undefined.
230+
let symToCheck = sym;
231+
if (symToCheck.flags & ts.SymbolFlags.Alias) {
232+
symToCheck = typeChecker.getAliasedSymbol(symToCheck);
233+
}
234+
if (!(symToCheck.flags & ts.SymbolFlags.Value)) {
235+
return undefined;
236+
}
237+
238+
if (ts.isIdentifier(name)) {
239+
// If there's a known import name for this symbol, use it so that the import will be
240+
// retained and the value can be referenced.
241+
const value = importNamesBySymbol.get(sym);
242+
if (value) {
243+
return value;
244+
}
245+
246+
// Otherwise this will be a locally declared name, just return that.
247+
return name;
248+
}
249+
const ref = entityNameToExpression(name.left);
250+
if (!ref) {
251+
return undefined;
252+
}
253+
254+
return ts.createPropertyAccess(ref, name.right);
255+
}
256+
257+
/**
258+
* Transforms a constructor. Returns the transformed constructor and the list of parameter
259+
* information collected, consisting of decorators and optional type.
260+
*/
261+
function transformConstructor(
262+
ctor: ts.ConstructorDeclaration,
263+
): [ts.ConstructorDeclaration, ParameterDecorationInfo[]] {
264+
ctor = ts.visitEachChild(ctor, visitor, context);
265+
266+
const parametersInfo: ParameterDecorationInfo[] = [];
267+
for (const param of ctor.parameters) {
268+
const paramInfo: ParameterDecorationInfo = { decorators: [], type: null };
269+
270+
for (const decorator of param.decorators || []) {
271+
paramInfo.decorators.push(decorator);
272+
}
273+
if (param.type) {
274+
// param has a type provided, e.g. "foo: Bar".
275+
// The type will be emitted as a value expression in entityNameToExpression, which takes
276+
// care not to emit anything for types that cannot be expressed as a value (e.g.
277+
// interfaces).
278+
paramInfo.type = param.type;
279+
}
280+
parametersInfo.push(paramInfo);
281+
}
282+
283+
return [ctor, parametersInfo];
284+
}
285+
286+
/**
287+
* Transforms a single class declaration:
288+
* - creates a ctorParameters property
289+
*/
290+
function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration {
291+
if (!classDecl.decorators || classDecl.decorators.length === 0) {
292+
return classDecl;
293+
}
294+
295+
const newMembers: ts.ClassElement[] = [];
296+
let classParameters: ParameterDecorationInfo[] | null = null;
297+
298+
for (const member of classDecl.members) {
299+
switch (member.kind) {
300+
case ts.SyntaxKind.Constructor: {
301+
const ctor = member as ts.ConstructorDeclaration;
302+
if (!ctor.body) {
303+
break;
304+
}
305+
306+
const [newMember, parametersInfo] = transformConstructor(
307+
member as ts.ConstructorDeclaration,
308+
);
309+
classParameters = parametersInfo;
310+
newMembers.push(newMember);
311+
continue;
312+
}
313+
default:
314+
break;
315+
}
316+
newMembers.push(ts.visitEachChild(member, visitor, context));
317+
}
318+
319+
const newClassDeclaration = ts.getMutableClone(classDecl);
320+
321+
if (classParameters) {
322+
newMembers.push(
323+
createCtorParametersClassProperty(diagnostics, entityNameToExpression, classParameters),
324+
);
325+
}
326+
327+
newClassDeclaration.members = ts.setTextRange(
328+
ts.createNodeArray(newMembers, newClassDeclaration.members.hasTrailingComma),
329+
classDecl.members,
330+
);
331+
332+
return newClassDeclaration;
333+
}
334+
335+
function visitor(node: ts.Node): ts.Node {
336+
switch (node.kind) {
337+
case ts.SyntaxKind.SourceFile: {
338+
importNamesBySymbol = new Map<ts.Symbol, ts.Identifier>();
339+
340+
return ts.visitEachChild(node, visitor, context);
341+
}
342+
case ts.SyntaxKind.ImportDeclaration: {
343+
const impDecl = node as ts.ImportDeclaration;
344+
if (impDecl.importClause) {
345+
const importClause = impDecl.importClause;
346+
const names = [];
347+
if (importClause.name) {
348+
names.push(importClause.name);
349+
}
350+
if (
351+
importClause.namedBindings &&
352+
importClause.namedBindings.kind === ts.SyntaxKind.NamedImports
353+
) {
354+
const namedImports = importClause.namedBindings as ts.NamedImports;
355+
names.push(...namedImports.elements.map(e => e.name));
356+
}
357+
for (const name of names) {
358+
const sym = typeChecker.getSymbolAtLocation(name);
359+
if (sym) {
360+
importNamesBySymbol.set(sym, name);
361+
}
362+
}
363+
}
364+
365+
return ts.visitEachChild(node, visitor, context);
366+
}
367+
case ts.SyntaxKind.ClassDeclaration: {
368+
return transformClassDeclaration(node as ts.ClassDeclaration);
369+
}
370+
default:
371+
return ts.visitEachChild(node, visitor, context);
372+
}
373+
}
374+
375+
return (sf: ts.SourceFile) => visitor(sf) as ts.SourceFile;
376+
};
377+
}

0 commit comments

Comments
 (0)