|
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