X Tutup
Skip to content

Commit ae876d1

Browse files
committed
feat(build): Persisting decorator metadata
This allows determing what the runtime metadata will be for a class without having to loading and running the corresponding .js file.
1 parent 6de68e2 commit ae876d1

File tree

4 files changed

+467
-2
lines changed

4 files changed

+467
-2
lines changed

tools/broccoli/broccoli-typescript.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fse = require('fs-extra');
55
import path = require('path');
66
import * as ts from 'typescript';
77
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
8-
8+
import {MetadataExtractor} from '../metadata/extractor';
99

1010
type FileRegistry = ts.Map<{version: number}>;
1111

@@ -50,6 +50,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
5050
private rootFilePaths: string[];
5151
private tsServiceHost: ts.LanguageServiceHost;
5252
private tsService: ts.LanguageService;
53+
private metadataExtractor: MetadataExtractor;
5354
private firstRun: boolean = true;
5455
private previousRunFailed: boolean = false;
5556
// Whether to generate the @internal typing files (they are only generated when `stripInternal` is
@@ -92,6 +93,7 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
9293
this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths,
9394
this.fileRegistry, this.inputPath);
9495
this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry());
96+
this.metadataExtractor = new MetadataExtractor(this.tsService);
9597
}
9698

9799

@@ -124,6 +126,8 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
124126
this.firstRun = false;
125127
this.doFullBuild();
126128
} else {
129+
let program = this.tsService.getProgram();
130+
let typeChecker = program.getTypeChecker();
127131
tsEmitInternal = false;
128132
pathsToEmit.forEach((tsFilePath) => {
129133
let output = this.tsService.getEmitOutput(tsFilePath);
@@ -139,6 +143,10 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
139143
let destDirPath = path.dirname(o.name);
140144
fse.mkdirsSync(destDirPath);
141145
fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS);
146+
if (endsWith(o.name, '.d.ts')) {
147+
const sourceFile = program.getSourceFile(tsFilePath);
148+
this.emitMetadata(o.name, sourceFile, typeChecker);
149+
}
142150
});
143151
}
144152
});
@@ -194,11 +202,22 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
194202

195203
private doFullBuild() {
196204
let program = this.tsService.getProgram();
197-
205+
let typeChecker = program.getTypeChecker();
206+
let diagnostics: ts.Diagnostic[] = [];
198207
tsEmitInternal = false;
208+
199209
let emitResult = program.emit(undefined, (absoluteFilePath, fileContent) => {
200210
fse.mkdirsSync(path.dirname(absoluteFilePath));
201211
fs.writeFileSync(absoluteFilePath, this.fixSourceMapSources(fileContent), FS_OPTS);
212+
if (endsWith(absoluteFilePath, '.d.ts')) {
213+
// TODO: Use sourceFile from the callback if
214+
// https://github.com/Microsoft/TypeScript/issues/7438
215+
// is taken
216+
const originalFile = absoluteFilePath.replace(this.tsOpts.outDir, this.tsOpts.rootDir)
217+
.replace(/\.d\.ts$/, '.ts');
218+
const sourceFile = program.getSourceFile(originalFile);
219+
this.emitMetadata(absoluteFilePath, sourceFile, typeChecker);
220+
}
202221
});
203222

204223
if (this.genInternalTypings) {
@@ -239,6 +258,22 @@ class DiffingTSCompiler implements DiffingBroccoliPlugin {
239258
}
240259
}
241260

261+
/**
262+
* Emit a .metadata.json file to correspond to the .d.ts file if the module contains classes that
263+
* use decorators or exported constants.
264+
*/
265+
private emitMetadata(dtsFileName: string, sourceFile: ts.SourceFile,
266+
typeChecker: ts.TypeChecker) {
267+
if (sourceFile) {
268+
const metadata = this.metadataExtractor.getMetadata(sourceFile, typeChecker);
269+
if (metadata && metadata.metadata) {
270+
const metadataText = JSON.stringify(metadata);
271+
const metadataFileName = dtsFileName.replace(/\.d.ts$/, '.metadata.json');
272+
fs.writeFileSync(metadataFileName, metadataText, FS_OPTS);
273+
}
274+
}
275+
}
276+
242277
/**
243278
* There is a bug in TypeScript 1.6, where the sourceRoot and inlineSourceMap properties
244279
* are exclusive. This means that the sources property always contains relative paths

tools/metadata/evaluator.ts

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import * as ts from 'typescript';
2+
import {Symbols} from './symbols';
3+
4+
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
5+
const expression = callExpression.expression;
6+
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
7+
const propertyAccessExpression = <ts.PropertyAccessExpression>expression;
8+
const name = propertyAccessExpression.name;
9+
if (name.kind == ts.SyntaxKind.Identifier) {
10+
return name.text === memberName;
11+
}
12+
}
13+
return false;
14+
}
15+
16+
function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
17+
const expression = callExpression.expression;
18+
if (expression.kind === ts.SyntaxKind.Identifier) {
19+
const identifier = <ts.Identifier>expression;
20+
return identifier.text === ident;
21+
}
22+
return false;
23+
}
24+
25+
/**
26+
* ts.forEachChild stops iterating children when the callback return a truthy value.
27+
* This method inverts this to implement an `every` style iterator. It will return
28+
* true if every call to `cb` returns `true`.
29+
*/
30+
function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
31+
return !ts.forEachChild(node, node => !cb(node));
32+
}
33+
34+
export interface SymbolReference {
35+
__symbolic: string; // TODO: Change this to type "reference" when we move to TypeScript 1.8
36+
name: string;
37+
module: string;
38+
}
39+
40+
function isPrimitive(value: any): boolean {
41+
return Object(value) !== value;
42+
}
43+
44+
function isDefined(obj: any): boolean {
45+
return obj !== undefined;
46+
}
47+
48+
/**
49+
* Produce a symbolic representation of an expression folding values into their final value when
50+
* possible.
51+
*/
52+
export class Evaluator {
53+
constructor(private service: ts.LanguageService, private typeChecker: ts.TypeChecker,
54+
private symbols: Symbols, private moduleNameOf: (fileName: string) => string) {}
55+
56+
// TODO: Determine if the first declaration is deterministic.
57+
private symbolFileName(symbol: ts.Symbol): string {
58+
if (symbol) {
59+
if (symbol.flags & ts.SymbolFlags.Alias) {
60+
symbol = this.typeChecker.getAliasedSymbol(symbol);
61+
}
62+
const declarations = symbol.getDeclarations();
63+
if (declarations && declarations.length > 0) {
64+
const sourceFile = declarations[0].getSourceFile();
65+
if (sourceFile) {
66+
return sourceFile.fileName;
67+
}
68+
}
69+
}
70+
return undefined;
71+
}
72+
73+
private symbolReference(symbol: ts.Symbol): SymbolReference {
74+
if (symbol) {
75+
const name = symbol.name;
76+
const module = this.moduleNameOf(this.symbolFileName(symbol));
77+
return {__symbolic: "reference", name, module};
78+
}
79+
}
80+
81+
private nodeSymbolReference(node: ts.Node): SymbolReference {
82+
return this.symbolReference(this.typeChecker.getSymbolAtLocation(node));
83+
}
84+
85+
nameOf(node: ts.Node): string {
86+
if (node.kind == ts.SyntaxKind.Identifier) {
87+
return (<ts.Identifier>node).text;
88+
}
89+
return this.evaluateNode(node);
90+
}
91+
92+
/**
93+
* Returns true if the expression represented by `node` can be folded into a literal expression.
94+
*
95+
* For example, a literal is always foldable. This means that literal expressions such as `1.2`
96+
* `"Some value"` `true` `false` are foldable.
97+
*
98+
* - An object literal is foldable if all the properties in the literal are foldable.
99+
* - An array literal is foldable if all the elements are foldable.
100+
* - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR.
101+
* - A property access is foldable if the object is foldable.
102+
* - A array index is foldable if index expression is foldable and the array is foldable.
103+
* - Binary operator expressions are foldable if the left and right expressions are foldable and
104+
* it is one of '+', '-', '*', '/', '%', '||', and '&&'.
105+
* - An identifier is foldable if a value can be found for its symbol is in the evaluator symbol
106+
* table.
107+
*/
108+
public isFoldable(node: ts.Node) {
109+
if (node) {
110+
switch (node.kind) {
111+
case ts.SyntaxKind.ObjectLiteralExpression:
112+
return everyNodeChild(node, child => {
113+
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
114+
const propertyAssignment = <ts.PropertyAssignment>child;
115+
return this.isFoldable(propertyAssignment.initializer)
116+
}
117+
return false;
118+
});
119+
case ts.SyntaxKind.ArrayLiteralExpression:
120+
return everyNodeChild(node, child => this.isFoldable(child));
121+
case ts.SyntaxKind.CallExpression:
122+
const callExpression = <ts.CallExpression>node;
123+
// We can fold a <array>.concat(<v>).
124+
if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) {
125+
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
126+
if (this.isFoldable(arrayNode) && this.isFoldable(callExpression.arguments[0])) {
127+
// It needs to be an array.
128+
const arrayValue = this.evaluateNode(arrayNode);
129+
if (arrayValue && Array.isArray(arrayValue)) {
130+
return true;
131+
}
132+
}
133+
}
134+
// We can fold a call to CONST_EXPR
135+
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1)
136+
return this.isFoldable(callExpression.arguments[0]);
137+
return false;
138+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
139+
case ts.SyntaxKind.StringLiteral:
140+
case ts.SyntaxKind.NumericLiteral:
141+
case ts.SyntaxKind.NullKeyword:
142+
case ts.SyntaxKind.TrueKeyword:
143+
case ts.SyntaxKind.FalseKeyword:
144+
return true;
145+
case ts.SyntaxKind.BinaryExpression:
146+
const binaryExpression = <ts.BinaryExpression>node;
147+
switch (binaryExpression.operatorToken.kind) {
148+
case ts.SyntaxKind.PlusToken:
149+
case ts.SyntaxKind.MinusToken:
150+
case ts.SyntaxKind.AsteriskToken:
151+
case ts.SyntaxKind.SlashToken:
152+
case ts.SyntaxKind.PercentToken:
153+
case ts.SyntaxKind.AmpersandAmpersandToken:
154+
case ts.SyntaxKind.BarBarToken:
155+
return this.isFoldable(binaryExpression.left) &&
156+
this.isFoldable(binaryExpression.right);
157+
}
158+
case ts.SyntaxKind.PropertyAccessExpression:
159+
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
160+
return this.isFoldable(propertyAccessExpression.expression);
161+
case ts.SyntaxKind.ElementAccessExpression:
162+
const elementAccessExpression = <ts.ElementAccessExpression>node;
163+
return this.isFoldable(elementAccessExpression.expression) &&
164+
this.isFoldable(elementAccessExpression.argumentExpression);
165+
case ts.SyntaxKind.Identifier:
166+
const symbol = this.typeChecker.getSymbolAtLocation(node);
167+
if (this.symbols.has(symbol)) return true;
168+
break;
169+
}
170+
}
171+
return false;
172+
}
173+
174+
/**
175+
* Produce a JSON serialiable object representing `node`. The foldable values in the expression
176+
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
177+
*/
178+
public evaluateNode(node: ts.Node): any {
179+
switch (node.kind) {
180+
case ts.SyntaxKind.ObjectLiteralExpression:
181+
let obj = {};
182+
let allPropertiesDefined = true;
183+
ts.forEachChild(node, child => {
184+
switch (child.kind) {
185+
case ts.SyntaxKind.PropertyAssignment:
186+
const assignment = <ts.PropertyAssignment>child;
187+
const propertyName = this.nameOf(assignment.name);
188+
const propertyValue = this.evaluateNode(assignment.initializer);
189+
obj[propertyName] = propertyValue;
190+
allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined;
191+
}
192+
});
193+
if (allPropertiesDefined) return obj;
194+
break;
195+
case ts.SyntaxKind.ArrayLiteralExpression:
196+
let arr = [];
197+
let allElementsDefined = true;
198+
ts.forEachChild(node, child => {
199+
const value = this.evaluateNode(child);
200+
arr.push(value);
201+
allElementsDefined = isDefined(value) && allElementsDefined;
202+
});
203+
if (allElementsDefined) return arr;
204+
break;
205+
case ts.SyntaxKind.CallExpression:
206+
const callExpression = <ts.CallExpression>node;
207+
const args = callExpression.arguments.map(arg => this.evaluateNode(arg));
208+
if (this.isFoldable(callExpression)) {
209+
if (isMethodCallOf(callExpression, "concat")) {
210+
const arrayValue = this.evaluateNode(
211+
(<ts.PropertyAccessExpression>callExpression.expression).expression);
212+
return arrayValue.concat(args[0]);
213+
}
214+
}
215+
// Always fold a CONST_EXPR even if the argument is not foldable.
216+
if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) {
217+
return args[0];
218+
}
219+
const expression = this.evaluateNode(callExpression.expression);
220+
if (isDefined(expression) && args.every(isDefined)) {
221+
return {
222+
__symbolic: "call",
223+
expression: this.evaluateNode(callExpression.expression),
224+
arguments: args
225+
};
226+
}
227+
break;
228+
case ts.SyntaxKind.PropertyAccessExpression: {
229+
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
230+
const expression = this.evaluateNode(propertyAccessExpression.expression);
231+
const member = this.nameOf(propertyAccessExpression.name);
232+
if (this.isFoldable(propertyAccessExpression.expression)) return expression[member];
233+
if (isDefined(expression)) {
234+
return {__symbolic: "select", expression, member};
235+
}
236+
break;
237+
}
238+
case ts.SyntaxKind.ElementAccessExpression: {
239+
const elementAccessExpression = <ts.ElementAccessExpression>node;
240+
const expression = this.evaluateNode(elementAccessExpression.expression);
241+
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
242+
if (this.isFoldable(elementAccessExpression.expression) &&
243+
this.isFoldable(elementAccessExpression.argumentExpression))
244+
return expression[index];
245+
if (isDefined(expression) && isDefined(index)) {
246+
return {
247+
__symbolic: "index",
248+
expression,
249+
index: this.evaluateNode(elementAccessExpression.argumentExpression)
250+
};
251+
}
252+
break;
253+
}
254+
case ts.SyntaxKind.Identifier:
255+
const symbol = this.typeChecker.getSymbolAtLocation(node);
256+
if (this.symbols.has(symbol)) return this.symbols.get(symbol);
257+
return this.nodeSymbolReference(node);
258+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
259+
return (<ts.LiteralExpression>node).text;
260+
case ts.SyntaxKind.StringLiteral:
261+
return (<ts.StringLiteral>node).text;
262+
case ts.SyntaxKind.NumericLiteral:
263+
return parseFloat((<ts.LiteralExpression>node).text);
264+
case ts.SyntaxKind.NullKeyword:
265+
return null;
266+
case ts.SyntaxKind.TrueKeyword:
267+
return true;
268+
case ts.SyntaxKind.FalseKeyword:
269+
return false;
270+
271+
case ts.SyntaxKind.BinaryExpression:
272+
const binaryExpression = <ts.BinaryExpression>node;
273+
const left = this.evaluateNode(binaryExpression.left);
274+
const right = this.evaluateNode(binaryExpression.right);
275+
if (isDefined(left) && isDefined(right)) {
276+
if (isPrimitive(left) && isPrimitive(right))
277+
switch (binaryExpression.operatorToken.kind) {
278+
case ts.SyntaxKind.PlusToken:
279+
return left + right;
280+
case ts.SyntaxKind.MinusToken:
281+
return left - right;
282+
case ts.SyntaxKind.AsteriskToken:
283+
return left * right;
284+
case ts.SyntaxKind.SlashToken:
285+
return left / right;
286+
case ts.SyntaxKind.PercentToken:
287+
return left % right;
288+
case ts.SyntaxKind.AmpersandAmpersandToken:
289+
return left && right;
290+
case ts.SyntaxKind.BarBarToken:
291+
return left || right;
292+
}
293+
return {
294+
__symbolic: "binop",
295+
operator: binaryExpression.operatorToken.getText(),
296+
left: left,
297+
right: right
298+
};
299+
}
300+
break;
301+
}
302+
return undefined;
303+
}
304+
}

0 commit comments

Comments
 (0)
X Tutup