|
1 |
| -import * as ts from 'typescript'; |
2 |
| -import * as fs from 'fs'; |
3 |
| -import {Symbols} from '@angular/tsc-wrapped/src/symbols'; |
4 |
| -import { |
5 |
| - isMetadataImportedSymbolReferenceExpression, |
6 |
| - isMetadataModuleReferenceExpression |
7 |
| -} from '@angular/tsc-wrapped'; |
8 |
| -import {Change, InsertChange, NoopChange, MultiChange} from './change'; |
9 |
| -import {insertImport} from './route-utils'; |
10 |
| - |
11 |
| -import {Observable} from 'rxjs/Observable'; |
12 |
| -import {ReplaySubject} from 'rxjs/ReplaySubject'; |
13 |
| -import 'rxjs/add/observable/of'; |
14 |
| -import 'rxjs/add/operator/do'; |
15 |
| -import 'rxjs/add/operator/filter'; |
16 |
| -import 'rxjs/add/operator/last'; |
17 |
| -import 'rxjs/add/operator/map'; |
18 |
| -import 'rxjs/add/operator/mergeMap'; |
19 |
| -import 'rxjs/add/operator/toArray'; |
20 |
| -import 'rxjs/add/operator/toPromise'; |
21 |
| - |
22 |
| - |
23 |
| -/** |
24 |
| -* Get TS source file based on path. |
25 |
| -* @param filePath |
26 |
| -* @return source file of ts.SourceFile kind |
27 |
| -*/ |
28 |
| -export function getSource(filePath: string): ts.SourceFile { |
29 |
| - return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(), |
30 |
| - ts.ScriptTarget.ES6, true); |
31 |
| -} |
32 |
| - |
33 |
| - |
34 |
| -/** |
35 |
| - * Get all the nodes from a source, as an observable. |
36 |
| - * @param sourceFile The source file object. |
37 |
| - * @returns {Observable<ts.Node>} An observable of all the nodes in the source. |
38 |
| - */ |
39 |
| -export function getSourceNodes(sourceFile: ts.SourceFile): Observable<ts.Node> { |
40 |
| - const subject = new ReplaySubject<ts.Node>(); |
41 |
| - let nodes: ts.Node[] = [sourceFile]; |
42 |
| - |
43 |
| - while(nodes.length > 0) { |
44 |
| - const node = nodes.shift(); |
45 |
| - |
46 |
| - if (node) { |
47 |
| - subject.next(node); |
48 |
| - if (node.getChildCount(sourceFile) >= 0) { |
49 |
| - nodes.unshift(...node.getChildren()); |
50 |
| - } |
51 |
| - } |
52 |
| - } |
53 |
| - |
54 |
| - subject.complete(); |
55 |
| - return subject.asObservable(); |
56 |
| -} |
57 |
| - |
58 |
| - |
59 |
| -/** |
60 |
| - * Find all nodes from the AST in the subtree of node of SyntaxKind kind. |
61 |
| - * @param node |
62 |
| - * @param kind |
63 |
| - * @param max The maximum number of items to return. |
64 |
| - * @return all nodes of kind, or [] if none is found |
65 |
| -*/ |
66 |
| -export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max: number = Infinity): ts.Node[] { |
67 |
| - if (!node || max == 0) { |
68 |
| - return []; |
69 |
| - } |
70 |
| - |
71 |
| - let arr: ts.Node[] = []; |
72 |
| - if (node.kind === kind) { |
73 |
| - arr.push(node); |
74 |
| - max--; |
75 |
| - } |
76 |
| - if (max > 0) { |
77 |
| - for (const child of node.getChildren()) { |
78 |
| - findNodes(child, kind, max).forEach(node => { |
79 |
| - if (max > 0) { |
80 |
| - arr.push(node); |
81 |
| - } |
82 |
| - max--; |
83 |
| - }); |
84 |
| - |
85 |
| - if (max <= 0) { |
86 |
| - break; |
87 |
| - } |
88 |
| - } |
89 |
| - } |
90 |
| - return arr; |
91 |
| -} |
92 |
| - |
93 |
| - |
94 |
| -/** |
95 |
| - * Helper for sorting nodes. |
96 |
| - * @return function to sort nodes in increasing order of position in sourceFile |
97 |
| - */ |
98 |
| -function nodesByPosition(first: ts.Node, second: ts.Node): number { |
99 |
| - return first.pos - second.pos; |
100 |
| -} |
101 |
| - |
102 |
| - |
103 |
| -/** |
104 |
| - * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` |
105 |
| - * or after the last of occurence of `syntaxKind` if the last occurence is a sub child |
106 |
| - * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. |
107 |
| - * |
108 |
| - * @param nodes insert after the last occurence of nodes |
109 |
| - * @param toInsert string to insert |
110 |
| - * @param file file to insert changes into |
111 |
| - * @param fallbackPos position to insert if toInsert happens to be the first occurence |
112 |
| - * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after |
113 |
| - * @return Change instance |
114 |
| - * @throw Error if toInsert is first occurence but fall back is not set |
115 |
| - */ |
116 |
| -export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string, |
117 |
| - file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change { |
118 |
| - var lastItem = nodes.sort(nodesByPosition).pop(); |
119 |
| - if (syntaxKind) { |
120 |
| - lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); |
121 |
| - } |
122 |
| - if (!lastItem && fallbackPos == undefined) { |
123 |
| - throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`); |
124 |
| - } |
125 |
| - let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; |
126 |
| - return new InsertChange(file, lastItemPosition, toInsert); |
127 |
| -} |
128 |
| - |
129 |
| - |
130 |
| -export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { |
131 |
| - if (node.kind == ts.SyntaxKind.Identifier) { |
132 |
| - return (<ts.Identifier>node).text; |
133 |
| - } else if (node.kind == ts.SyntaxKind.StringLiteral) { |
134 |
| - try { |
135 |
| - return JSON.parse(node.getFullText(source)) |
136 |
| - } catch (e) { |
137 |
| - return null; |
138 |
| - } |
139 |
| - } else { |
140 |
| - return null; |
141 |
| - } |
142 |
| -} |
143 |
| - |
144 |
| - |
145 |
| -export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, |
146 |
| - module: string): Observable<ts.Node> { |
147 |
| - const symbols = new Symbols(source); |
148 |
| - |
149 |
| - return getSourceNodes(source) |
150 |
| - .filter(node => { |
151 |
| - return node.kind == ts.SyntaxKind.Decorator |
152 |
| - && (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression; |
153 |
| - }) |
154 |
| - .map(node => <ts.CallExpression>(<ts.Decorator>node).expression) |
155 |
| - .filter(expr => { |
156 |
| - if (expr.expression.kind == ts.SyntaxKind.Identifier) { |
157 |
| - const id = <ts.Identifier>expr.expression; |
158 |
| - const metaData = symbols.resolve(id.getFullText(source)); |
159 |
| - if (isMetadataImportedSymbolReferenceExpression(metaData)) { |
160 |
| - return metaData.name == identifier && metaData.module == module; |
161 |
| - } |
162 |
| - } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { |
163 |
| - // This covers foo.NgModule when importing * as foo. |
164 |
| - const paExpr = <ts.PropertyAccessExpression>expr.expression; |
165 |
| - // If the left expression is not an identifier, just give up at that point. |
166 |
| - if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { |
167 |
| - return false; |
168 |
| - } |
169 |
| - |
170 |
| - const id = paExpr.name; |
171 |
| - const moduleId = <ts.Identifier>paExpr.expression; |
172 |
| - const moduleMetaData = symbols.resolve(moduleId.getFullText(source)); |
173 |
| - if (isMetadataModuleReferenceExpression(moduleMetaData)) { |
174 |
| - return moduleMetaData.module == module && id.getFullText(source) == identifier; |
175 |
| - } |
176 |
| - } |
177 |
| - return false; |
178 |
| - }) |
179 |
| - .filter(expr => expr.arguments[0] |
180 |
| - && expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression) |
181 |
| - .map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]); |
182 |
| -} |
183 |
| - |
184 |
| - |
185 |
| -function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string, |
186 |
| - symbolName: string, importPath: string) { |
187 |
| - const source: ts.SourceFile = getSource(ngModulePath); |
188 |
| - let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core'); |
189 |
| - |
190 |
| - // Find the decorator declaration. |
191 |
| - return metadata |
192 |
| - .toPromise() |
193 |
| - .then((node: ts.ObjectLiteralExpression) => { |
194 |
| - if (!node) { |
195 |
| - return null; |
196 |
| - } |
197 |
| - |
198 |
| - // Get all the children property assignment of object literals. |
199 |
| - return node.properties |
200 |
| - .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) |
201 |
| - // Filter out every fields that's not "metadataField". Also handles string literals |
202 |
| - // (but not expressions). |
203 |
| - .filter(prop => { |
204 |
| - switch (prop.name.kind) { |
205 |
| - case ts.SyntaxKind.Identifier: |
206 |
| - return prop.name.getText(source) == metadataField; |
207 |
| - case ts.SyntaxKind.StringLiteral: |
208 |
| - return prop.name.text == metadataField; |
209 |
| - } |
210 |
| - |
211 |
| - return false; |
212 |
| - }); |
213 |
| - }) |
214 |
| - // Get the last node of the array literal. |
215 |
| - .then(matchingProperties => { |
216 |
| - if (!matchingProperties) { |
217 |
| - return; |
218 |
| - } |
219 |
| - if (matchingProperties.length == 0) { |
220 |
| - return metadata |
221 |
| - .toPromise(); |
222 |
| - } |
223 |
| - |
224 |
| - const assignment = <ts.PropertyAssignment>matchingProperties[0]; |
225 |
| - |
226 |
| - // If it's not an array, nothing we can do really. |
227 |
| - if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { |
228 |
| - return Observable.empty(); |
229 |
| - } |
230 |
| - |
231 |
| - const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer; |
232 |
| - if (arrLiteral.elements.length == 0) { |
233 |
| - // Forward the property. |
234 |
| - return arrLiteral; |
235 |
| - } |
236 |
| - return arrLiteral.elements; |
237 |
| - }) |
238 |
| - .then((node: ts.Node) => { |
239 |
| - if (!node) { |
240 |
| - console.log('No app module found. Please add your new class to your component.'); |
241 |
| - return new NoopChange(); |
242 |
| - } |
243 |
| - if (Array.isArray(node)) { |
244 |
| - node = node[node.length - 1]; |
245 |
| - } |
246 |
| - |
247 |
| - let toInsert; |
248 |
| - let position = node.getEnd(); |
249 |
| - if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { |
250 |
| - // We haven't found the field in the metadata declaration. Insert a new |
251 |
| - // field. |
252 |
| - let expr = <ts.ObjectLiteralExpression>node; |
253 |
| - if (expr.properties.length == 0) { |
254 |
| - position = expr.getEnd() - 1; |
255 |
| - toInsert = ` ${metadataField}: [${symbolName}]\n`; |
256 |
| - } else { |
257 |
| - node = expr.properties[expr.properties.length - 1]; |
258 |
| - position = node.getEnd(); |
259 |
| - // Get the indentation of the last element, if any. |
260 |
| - const text = node.getFullText(source); |
261 |
| - if (text.startsWith('\n')) { |
262 |
| - toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${metadataField}: [${symbolName}]`; |
263 |
| - } else { |
264 |
| - toInsert = `, ${metadataField}: [${symbolName}]`; |
265 |
| - } |
266 |
| - } |
267 |
| - } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { |
268 |
| - // We found the field but it's empty. Insert it just before the `]`. |
269 |
| - position--; |
270 |
| - toInsert = `${symbolName}`; |
271 |
| - } else { |
272 |
| - // Get the indentation of the last element, if any. |
273 |
| - const text = node.getFullText(source); |
274 |
| - if (text.startsWith('\n')) { |
275 |
| - toInsert = `,${text.match(/^\n(\r?)\s+/)[0]}${symbolName}`; |
276 |
| - } else { |
277 |
| - toInsert = `, ${symbolName}`; |
278 |
| - } |
279 |
| - } |
280 |
| - |
281 |
| - const insert = new InsertChange(ngModulePath, position, toInsert); |
282 |
| - const importInsert: Change = insertImport(ngModulePath, symbolName, importPath); |
283 |
| - return new MultiChange([insert, importInsert]); |
284 |
| - }); |
285 |
| -} |
286 |
| - |
287 |
| -/** |
288 |
| -* Custom function to insert a declaration (component, pipe, directive) |
289 |
| -* into NgModule declarations. It also imports the component. |
290 |
| -*/ |
291 |
| -export function addComponentToModule(modulePath: string, classifiedName: string, |
292 |
| - importPath: string): Promise<Change> { |
293 |
| - |
294 |
| - return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath); |
295 |
| -} |
296 |
| - |
297 |
| -/** |
298 |
| - * Custom function to insert a provider into NgModule. It also imports it. |
299 |
| - */ |
300 |
| -export function addProviderToModule(modulePath: string, classifiedName: string, |
301 |
| - importPath: string): Promise<Change> { |
302 |
| - return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath); |
303 |
| -} |
304 |
| - |
| 1 | +// In order to keep refactoring low, simply export from ast-tools. |
| 2 | +// TODO: move all dependencies of this file to ast-tools directly. |
| 3 | +export { |
| 4 | + getSource, |
| 5 | + getSourceNodes, |
| 6 | + findNodes, |
| 7 | + insertAfterLastOccurrence, |
| 8 | + getContentOfKeyLiteral, |
| 9 | + getDecoratorMetadata, |
| 10 | + addComponentToModule, |
| 11 | + addProviderToModule |
| 12 | +} from '@angular-cli/ast-tools'; |
0 commit comments