UNPKG

22.6 kBJavaScriptView Raw
1"use strict";
2var ts = require('typescript');
3var fs = require('fs');
4var path = require('path');
5var change_1 = require('./change');
6var node_1 = require('./node');
7var ast_utils_1 = require('./ast-utils');
8var change_2 = require('./change');
9/**
10 * Adds imports to mainFile and adds toBootstrap to the array of providers
11 * in bootstrap, if not present
12 * @param mainFile main.ts
13 * @param imports Object { importedClass: ['path/to/import/from', defaultStyleImport?] }
14 * @param toBootstrap
15 */
16function bootstrapItem(mainFile, imports, toBootstrap) {
17 var changes = Object.keys(imports).map(function (importedClass) {
18 var defaultStyleImport = imports[importedClass].length === 2 && !!imports[importedClass][1];
19 return insertImport(mainFile, importedClass, imports[importedClass][0].toString(), defaultStyleImport);
20 });
21 var rootNode = getRootNode(mainFile);
22 // get ExpressionStatements from the top level syntaxList of the sourceFile
23 var bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(function (node) {
24 // get bootstrap expressions
25 return node.kind === ts.SyntaxKind.ExpressionStatement &&
26 node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap';
27 });
28 if (bootstrapNodes.length !== 1) {
29 throw new Error(("Did not bootstrap provideRouter in " + mainFile) +
30 ' because of multiple or no bootstrap calls');
31 }
32 var bootstrapNode = bootstrapNodes[0].getChildAt(0);
33 var isBootstraped = node_1.findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items
34 .reduce(function (a, b) { return a.concat(b.getChildren().map(function (n) { return n.getText(); })); }, [])
35 .filter(function (n) { return n !== ','; })
36 .indexOf(toBootstrap) !== -1;
37 if (isBootstraped) {
38 return changes;
39 }
40 // if bracket exitst already, add configuration template,
41 // otherwise, insert into bootstrap parens
42 var fallBackPos, configurePathsTemplate, separator;
43 var syntaxListNodes;
44 var bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers
45 if (bootstrapProviders) {
46 syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren();
47 fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral
48 separator = syntaxListNodes.length === 0 ? '' : ', ';
49 configurePathsTemplate = "" + separator + toBootstrap;
50 }
51 else {
52 fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral
53 syntaxListNodes = bootstrapNode.getChildAt(2).getChildren();
54 configurePathsTemplate = ", [ " + toBootstrap + " ]";
55 }
56 changes.push(ast_utils_1.insertAfterLastOccurrence(syntaxListNodes, configurePathsTemplate, mainFile, fallBackPos));
57 return changes;
58}
59exports.bootstrapItem = bootstrapItem;
60/**
61* Add Import `import { symbolName } from fileName` if the import doesn't exit
62* already. Assumes fileToEdit can be resolved and accessed.
63* @param fileToEdit (file we want to add import to)
64* @param symbolName (item to import)
65* @param fileName (path to the file)
66* @param isDefault (if true, import follows style for importing default exports)
67* @return Change
68*/
69function insertImport(fileToEdit, symbolName, fileName, isDefault) {
70 if (isDefault === void 0) { isDefault = false; }
71 if (process.platform.startsWith('win')) {
72 fileName = fileName.replace(/\\/g, '/'); // correction in windows
73 }
74 var rootNode = getRootNode(fileToEdit);
75 var allImports = node_1.findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
76 // get nodes that map to import statements from the file fileName
77 var relevantImports = allImports.filter(function (node) {
78 // StringLiteral of the ImportDeclaration is the import file (fileName in this case).
79 var importFiles = node.getChildren().filter(function (child) { return child.kind === ts.SyntaxKind.StringLiteral; })
80 .map(function (n) { return n.text; });
81 return importFiles.filter(function (file) { return file === fileName; }).length === 1;
82 });
83 if (relevantImports.length > 0) {
84 var importsAsterisk_1 = false;
85 // imports from import file
86 var imports_1 = [];
87 relevantImports.forEach(function (n) {
88 Array.prototype.push.apply(imports_1, node_1.findNodes(n, ts.SyntaxKind.Identifier));
89 if (node_1.findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
90 importsAsterisk_1 = true;
91 }
92 });
93 // if imports * from fileName, don't add symbolName
94 if (importsAsterisk_1) {
95 return;
96 }
97 var importTextNodes = imports_1.filter(function (n) { return n.text === symbolName; });
98 // insert import if it's not there
99 if (importTextNodes.length === 0) {
100 var fallbackPos_1 = node_1.findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
101 node_1.findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
102 return ast_utils_1.insertAfterLastOccurrence(imports_1, ", " + symbolName, fileToEdit, fallbackPos_1);
103 }
104 return new change_1.NoopChange();
105 }
106 // no such import declaration exists
107 var useStrict = node_1.findNodes(rootNode, ts.SyntaxKind.StringLiteral)
108 .filter(function (n) { return n.text === 'use strict'; });
109 var fallbackPos = 0;
110 if (useStrict.length > 0) {
111 fallbackPos = useStrict[0].end;
112 }
113 var open = isDefault ? '' : '{ ';
114 var close = isDefault ? '' : ' }';
115 // if there are no imports or 'use strict' statement, insert import at beginning of file
116 var insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
117 var separator = insertAtBeginning ? '' : ';\n';
118 var toInsert = (separator + "import " + open + symbolName + close) +
119 (" from '" + fileName + "'" + (insertAtBeginning ? ';\n' : ''));
120 return ast_utils_1.insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
121}
122exports.insertImport = insertImport;
123;
124/**
125 * Inserts a path to the new route into src/routes.ts if it doesn't exist
126 * @param routesFile
127 * @param pathOptions
128 * @return Change[]
129 * @throws Error if routesFile has multiple export default or none.
130 */
131function addPathToRoutes(routesFile, pathOptions) {
132 var route = pathOptions.route.split('/')
133 .filter(function (n) { return n !== ''; }).join('/'); // change say `/about/:id/` to `about/:id`
134 var isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
135 var outlet = pathOptions.outlet ? ", outlet: '" + pathOptions.outlet + "'" : '';
136 // create route path and resolve component import
137 var positionalRoutes = /\/:[^/]*/g;
138 var routePath = route.replace(positionalRoutes, '');
139 routePath = "./app/" + routePath + "/" + pathOptions.dasherizedName + ".component";
140 var originalComponent = pathOptions.component;
141 pathOptions.component = resolveImportName(pathOptions.component, routePath, pathOptions.routesFile);
142 var content = "{ path: '" + route + "', component: " + pathOptions.component + isDefault + outlet + " }";
143 var rootNode = getRootNode(routesFile);
144 var routesNode = rootNode.getChildAt(0).getChildren().filter(function (n) {
145 // get export statement
146 return n.kind === ts.SyntaxKind.ExportAssignment &&
147 n.getFullText().indexOf('export default') !== -1;
148 });
149 if (routesNode.length !== 1) {
150 throw new Error('Did not insert path in routes.ts because ' +
151 "there were multiple or no 'export default' statements");
152 }
153 var pos = routesNode[0].getChildAt(2).getChildAt(0).end; // openBracketLiteral
154 // all routes in export route array
155 var routesArray = routesNode[0].getChildAt(2).getChildAt(1)
156 .getChildren()
157 .filter(function (n) { return n.kind === ts.SyntaxKind.ObjectLiteralExpression; });
158 if (pathExists(routesArray, route, pathOptions.component)) {
159 // don't duplicate routes
160 throw new Error('Route was not added since it is a duplicate');
161 }
162 var isChild = false;
163 // get parent to insert under
164 var parent;
165 if (pathOptions.parent) {
166 // append '_' to route to find the actual parent (not parent of the parent)
167 parent = getParent(routesArray, pathOptions.parent + "/_");
168 if (!parent) {
169 throw new Error("You specified parent '" + pathOptions.parent + "'' which was not found in routes.ts");
170 }
171 if (route.indexOf(pathOptions.parent) === 0) {
172 route = route.substring(pathOptions.parent.length);
173 }
174 }
175 else {
176 parent = getParent(routesArray, route);
177 }
178 if (parent) {
179 var childrenInfo = addChildPath(parent, pathOptions, route);
180 if (!childrenInfo) {
181 // path exists already
182 throw new Error('Route was not added since it is a duplicate');
183 }
184 content = childrenInfo.newContent;
185 pos = childrenInfo.pos;
186 isChild = true;
187 }
188 var isFirstElement = routesArray.length === 0;
189 if (!isChild) {
190 var separator = isFirstElement ? '\n' : ',';
191 content = "\n " + content + separator;
192 }
193 var changes = [new change_1.InsertChange(routesFile, pos, content)];
194 var component = originalComponent === pathOptions.component ? originalComponent :
195 originalComponent + " as " + pathOptions.component;
196 routePath = routePath.replace(/\\/, '/'); // correction in windows
197 changes.push(insertImport(routesFile, component, routePath));
198 return changes;
199}
200exports.addPathToRoutes = addPathToRoutes;
201/**
202 * Add more properties to the route object in routes.ts
203 * @param routesFile routes.ts
204 * @param routes Object {route: [key, value]}
205 */
206function addItemsToRouteProperties(routesFile, routes) {
207 var rootNode = getRootNode(routesFile);
208 var routesNode = rootNode.getChildAt(0).getChildren().filter(function (n) {
209 // get export statement
210 return n.kind === ts.SyntaxKind.ExportAssignment &&
211 n.getFullText().indexOf('export default') !== -1;
212 });
213 if (routesNode.length !== 1) {
214 throw new Error('Did not insert path in routes.ts because ' +
215 "there were multiple or no 'export default' statements");
216 }
217 var routesArray = routesNode[0].getChildAt(2).getChildAt(1)
218 .getChildren()
219 .filter(function (n) { return n.kind === ts.SyntaxKind.ObjectLiteralExpression; });
220 var changes = Object.keys(routes).reduce(function (result, route) {
221 // let route = routes[guardName][0];
222 var itemKey = routes[route][0];
223 var itemValue = routes[route][1];
224 var currRouteNode = getParent(routesArray, route + "/_");
225 if (!currRouteNode) {
226 throw new Error("Could not find '" + route + "' in routes.ts");
227 }
228 var fallBackPos = node_1.findNodes(currRouteNode, ts.SyntaxKind.CloseBraceToken).pop().pos;
229 var pathPropertiesNodes = currRouteNode.getChildAt(1).getChildren()
230 .filter(function (n) { return n.kind === ts.SyntaxKind.PropertyAssignment; });
231 return result.concat([ast_utils_1.insertAfterLastOccurrence(pathPropertiesNodes, ", " + itemKey + ": " + itemValue, routesFile, fallBackPos)]);
232 }, []);
233 return changes;
234}
235exports.addItemsToRouteProperties = addItemsToRouteProperties;
236/**
237 * Verifies that a component file exports a class of the component
238 * @param file
239 * @param componentName
240 * @return whether file exports componentName
241 */
242function confirmComponentExport(file, componentName) {
243 var rootNode = getRootNode(file);
244 var exportNodes = rootNode.getChildAt(0).getChildren().filter(function (n) {
245 return n.kind === ts.SyntaxKind.ClassDeclaration &&
246 (n.getChildren().filter(function (p) { return p.text === componentName; }).length !== 0);
247 });
248 return exportNodes.length > 0;
249}
250exports.confirmComponentExport = confirmComponentExport;
251/**
252 * Ensures there is no collision between import names. If a collision occurs, resolve by adding
253 * underscore number to the name
254 * @param importName
255 * @param importPath path to import component from
256 * @param fileName (file to add import to)
257 * @return resolved importName
258 */
259function resolveImportName(importName, importPath, fileName) {
260 var rootNode = getRootNode(fileName);
261 // get all the import names
262 var importNodes = rootNode.getChildAt(0).getChildren()
263 .filter(function (n) { return n.kind === ts.SyntaxKind.ImportDeclaration; });
264 // check if imported file is same as current one before updating component name
265 var importNames = importNodes
266 .reduce(function (a, b) {
267 var importFrom = node_1.findNodes(b, ts.SyntaxKind.StringLiteral); // there's only one
268 if (importFrom.pop().text !== importPath) {
269 // importing from different file, add to imported components to inspect
270 // if only one identifier { FooComponent }, if two { FooComponent as FooComponent_1 }
271 // choose last element of identifier array in both cases
272 return a.concat([node_1.findNodes(b, ts.SyntaxKind.Identifier).pop()]);
273 }
274 return a;
275 }, [])
276 .map(function (n) { return n.text; });
277 var index = importNames.indexOf(importName);
278 if (index === -1) {
279 return importName;
280 }
281 var baseName = importNames[index].split('_')[0];
282 var newName = baseName;
283 var resolutionNumber = 1;
284 while (importNames.indexOf(newName) !== -1) {
285 newName = baseName + "_" + resolutionNumber;
286 resolutionNumber++;
287 }
288 return newName;
289}
290/**
291 * Resolve a path to a component file. If the path begins with path.sep, it is treated to be
292 * absolute from the app/ directory. Otherwise, it is relative to currDir
293 * @param projectRoot
294 * @param currentDir
295 * @param filePath componentName or path to componentName
296 * @return component file name
297 * @throw Error if component file referenced by path is not found
298 */
299function resolveComponentPath(projectRoot, currentDir, filePath) {
300 var parsedPath = path.parse(filePath);
301 var componentName = parsedPath.base.split('.')[0];
302 var componentDir = path.parse(parsedPath.dir).base;
303 // correction for a case where path is /**/componentName/componentName(.component.ts)
304 if (componentName === componentDir) {
305 filePath = parsedPath.dir;
306 }
307 if (parsedPath.dir === '') {
308 // only component file name is given
309 filePath = componentName;
310 }
311 var directory = filePath[0] === path.sep ?
312 path.resolve(path.join(projectRoot, 'src', 'app', filePath)) :
313 path.resolve(currentDir, filePath);
314 if (!fs.existsSync(directory)) {
315 throw new Error(("path '" + filePath + "' must be relative to current directory") +
316 " or absolute from project root");
317 }
318 if (directory.indexOf('src' + path.sep + 'app') === -1) {
319 throw new Error('Route must be within app');
320 }
321 var componentFile = path.join(directory, componentName + ".component.ts");
322 if (!fs.existsSync(componentFile)) {
323 throw new Error("could not find component file referenced by " + filePath);
324 }
325 return componentFile;
326}
327exports.resolveComponentPath = resolveComponentPath;
328/**
329 * Sort changes in decreasing order and apply them.
330 * @param changes
331 * @param host
332 * @return Promise
333 */
334function applyChanges(changes, host) {
335 if (host === void 0) { host = change_2.NodeHost; }
336 return changes
337 .filter(function (change) { return !!change; })
338 .sort(function (curr, next) { return next.order - curr.order; })
339 .reduce(function (newChange, change) { return newChange.then(function () { return change.apply(host); }); }, Promise.resolve());
340}
341exports.applyChanges = applyChanges;
342/**
343 * Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file
344 * @return Object (pos, newContent)
345 */
346function addChildPath(parentObject, pathOptions, route) {
347 if (!parentObject) {
348 return;
349 }
350 var pos;
351 var newContent;
352 // get object with 'children' property
353 var childrenNode = parentObject.getChildAt(1).getChildren()
354 .filter(function (n) {
355 return n.kind === ts.SyntaxKind.PropertyAssignment
356 && n.name.text === 'children';
357 });
358 // find number of spaces to pad nested paths
359 var nestingLevel = 1; // for indenting route object in the `children` array
360 var n = parentObject;
361 while (n.parent) {
362 if (n.kind === ts.SyntaxKind.ObjectLiteralExpression
363 || n.kind === ts.SyntaxKind.ArrayLiteralExpression) {
364 nestingLevel++;
365 }
366 n = n.parent;
367 }
368 // strip parent route
369 var parentRoute = parentObject.getChildAt(1).getChildAt(0).getChildAt(2).text;
370 var childRoute = route.substring(route.indexOf(parentRoute) + parentRoute.length + 1);
371 var isDefault = pathOptions.isDefault ? ', useAsDefault: true' : '';
372 var outlet = pathOptions.outlet ? ", outlet: '" + pathOptions.outlet + "'" : '';
373 var content = ("{ path: '" + childRoute + "', component: " + pathOptions.component) +
374 ("" + isDefault + outlet + " }");
375 var spaces = Array(2 * nestingLevel + 1).join(' ');
376 if (childrenNode.length !== 0) {
377 // add to beginning of children array
378 pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket
379 newContent = "\n" + spaces + content + ",";
380 }
381 else {
382 // no children array, add one
383 pos = parentObject.getChildAt(2).pos; // close brace
384 newContent = (",\n" + spaces.substring(2) + "children: [\n" + spaces + content) +
385 ("\n" + spaces.substring(2) + "]\n" + spaces.substring(5));
386 }
387 return { newContent: newContent, pos: pos };
388}
389/**
390 * Helper for addPathToRoutes.
391 * @return parentNode which contains the children array to add a new path to or
392 * undefined if none or the entire route was matched.
393 */
394function getParent(routesArray, route, parent) {
395 if (routesArray.length === 0 && !parent) {
396 return; // no children array and no parent found
397 }
398 if (route.length === 0) {
399 return; // route has been completely matched
400 }
401 var splitRoute = route.split('/');
402 // don't treat positional parameters separately
403 if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
404 var actualRoute = splitRoute.shift();
405 splitRoute[0] = actualRoute + "/" + splitRoute[0];
406 }
407 var potentialParents = routesArray // route nodes with same path as current route
408 .filter(function (n) { return getValueForKey(n, 'path') === splitRoute[0]; });
409 if (potentialParents.length !== 0) {
410 splitRoute.shift(); // matched current parent, move on
411 route = splitRoute.join('/');
412 }
413 // get all children paths
414 var newRouteArray = getChildrenArray(routesArray);
415 if (route && parent && potentialParents.length === 0) {
416 return parent; // final route is not matched. assign parent from here
417 }
418 parent = potentialParents.sort(function (a, b) { return a.pos - b.pos; }).shift();
419 return getParent(newRouteArray, route, parent);
420}
421/**
422 * Helper for addPathToRoutes.
423 * @return whether path with same route and component exists
424 */
425function pathExists(routesArray, route, component, fullRoute) {
426 if (routesArray.length === 0) {
427 return false;
428 }
429 fullRoute = fullRoute ? fullRoute : route;
430 var sameRoute = false;
431 var splitRoute = route.split('/');
432 // don't treat positional parameters separately
433 if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) {
434 var actualRoute = splitRoute.shift();
435 splitRoute[0] = actualRoute + "/" + splitRoute[0];
436 }
437 var repeatedRoutes = routesArray.filter(function (n) {
438 var currentRoute = getValueForKey(n, 'path');
439 var sameComponent = getValueForKey(n, 'component') === component;
440 sameRoute = currentRoute === splitRoute[0];
441 // Confirm that it's parents are the same
442 if (sameRoute && sameComponent) {
443 var path_1 = currentRoute;
444 var objExp = n.parent;
445 while (objExp) {
446 if (objExp.kind === ts.SyntaxKind.ObjectLiteralExpression) {
447 var currentParentPath = getValueForKey(objExp, 'path');
448 path_1 = currentParentPath ? currentParentPath + "/" + path_1 : path_1;
449 }
450 objExp = objExp.parent;
451 }
452 return path_1 === fullRoute;
453 }
454 return false;
455 });
456 if (sameRoute) {
457 splitRoute.shift(); // matched current parent, move on
458 route = splitRoute.join('/');
459 }
460 if (repeatedRoutes.length !== 0) {
461 return true; // new path will be repeating if inserted. report that path already exists
462 }
463 // all children paths
464 var newRouteArray = getChildrenArray(routesArray);
465 return pathExists(newRouteArray, route, component, fullRoute);
466}
467/**
468 * Helper for getParent and pathExists
469 * @return array with all nodes holding children array under routes
470 * in routesArray
471 */
472function getChildrenArray(routesArray) {
473 return routesArray.reduce(function (allRoutes, currRoute) { return allRoutes.concat(currRoute.getChildAt(1).getChildren()
474 .filter(function (n) { return n.kind === ts.SyntaxKind.PropertyAssignment
475 && n.name.text === 'children'; })
476 .map(function (n) { return n.getChildAt(2).getChildAt(1); }) // syntaxList containing chilren paths
477 .reduce(function (childrenArray, currChild) { return childrenArray.concat(currChild.getChildren()
478 .filter(function (p) { return p.kind === ts.SyntaxKind.ObjectLiteralExpression; })); }, [])); }, []);
479}
480/**
481 * Helper method to get the path text or component
482 * @param objectLiteralNode
483 * @param key 'path' or 'component'
484 */
485function getValueForKey(objectLiteralNode, key) {
486 var currentNode = key === 'component' ? objectLiteralNode.getChildAt(1).getChildAt(2) :
487 objectLiteralNode.getChildAt(1).getChildAt(0);
488 return currentNode
489 && currentNode.getChildAt(0)
490 && currentNode.getChildAt(0).text === key
491 && currentNode.getChildAt(2)
492 && currentNode.getChildAt(2).text;
493}
494/**
495 * Helper method to get AST from file
496 * @param file
497 */
498function getRootNode(file) {
499 return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.Latest, true);
500}
501//# sourceMappingURL=/Users/hans/Sources/angular-cli/packages/@angular-cli/ast-tools/src/route-utils.js.map
\No newline at end of file