ast-utils.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. /**
  4. * @license
  5. * Copyright Google Inc. All Rights Reserved.
  6. *
  7. * Use of this source code is governed by an MIT-style license that can be
  8. * found in the LICENSE file at https://angular.io/license
  9. */
  10. const ts = require("../third_party/github.com/Microsoft/TypeScript/lib/typescript");
  11. const change_1 = require("./change");
  12. /**
  13. * Add Import `import { symbolName } from fileName` if the import doesn't exit
  14. * already. Assumes fileToEdit can be resolved and accessed.
  15. * @param fileToEdit (file we want to add import to)
  16. * @param symbolName (item to import)
  17. * @param fileName (path to the file)
  18. * @param isDefault (if true, import follows style for importing default exports)
  19. * @return Change
  20. */
  21. function insertImport(source, fileToEdit, symbolName, fileName, isDefault = false) {
  22. const rootNode = source;
  23. const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
  24. // get nodes that map to import statements from the file fileName
  25. const relevantImports = allImports.filter(node => {
  26. // StringLiteral of the ImportDeclaration is the import file (fileName in this case).
  27. const importFiles = node.getChildren()
  28. .filter(child => child.kind === ts.SyntaxKind.StringLiteral)
  29. .map(n => n.text);
  30. return importFiles.filter(file => file === fileName).length === 1;
  31. });
  32. if (relevantImports.length > 0) {
  33. let importsAsterisk = false;
  34. // imports from import file
  35. const imports = [];
  36. relevantImports.forEach(n => {
  37. Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
  38. if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
  39. importsAsterisk = true;
  40. }
  41. });
  42. // if imports * from fileName, don't add symbolName
  43. if (importsAsterisk) {
  44. return new change_1.NoopChange();
  45. }
  46. const importTextNodes = imports.filter(n => n.text === symbolName);
  47. // insert import if it's not there
  48. if (importTextNodes.length === 0) {
  49. const fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
  50. findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
  51. return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
  52. }
  53. return new change_1.NoopChange();
  54. }
  55. // no such import declaration exists
  56. const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral)
  57. .filter((n) => n.text === 'use strict');
  58. let fallbackPos = 0;
  59. if (useStrict.length > 0) {
  60. fallbackPos = useStrict[0].end;
  61. }
  62. const open = isDefault ? '' : '{ ';
  63. const close = isDefault ? '' : ' }';
  64. // if there are no imports or 'use strict' statement, insert import at beginning of file
  65. const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
  66. const separator = insertAtBeginning ? '' : ';\n';
  67. const toInsert = `${separator}import ${open}${symbolName}${close}` +
  68. ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
  69. return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
  70. }
  71. exports.insertImport = insertImport;
  72. /**
  73. * Find all nodes from the AST in the subtree of node of SyntaxKind kind.
  74. * @param node
  75. * @param kind
  76. * @param max The maximum number of items to return.
  77. * @param recursive Continue looking for nodes of kind recursive until end
  78. * the last child even when node of kind has been found.
  79. * @return all nodes of kind, or [] if none is found
  80. */
  81. function findNodes(node, kind, max = Infinity, recursive = false) {
  82. if (!node || max == 0) {
  83. return [];
  84. }
  85. const arr = [];
  86. if (node.kind === kind) {
  87. arr.push(node);
  88. max--;
  89. }
  90. if (max > 0 && (recursive || node.kind !== kind)) {
  91. for (const child of node.getChildren()) {
  92. findNodes(child, kind, max).forEach(node => {
  93. if (max > 0) {
  94. arr.push(node);
  95. }
  96. max--;
  97. });
  98. if (max <= 0) {
  99. break;
  100. }
  101. }
  102. }
  103. return arr;
  104. }
  105. exports.findNodes = findNodes;
  106. /**
  107. * Get all the nodes from a source.
  108. * @param sourceFile The source file object.
  109. * @returns {Observable<ts.Node>} An observable of all the nodes in the source.
  110. */
  111. function getSourceNodes(sourceFile) {
  112. const nodes = [sourceFile];
  113. const result = [];
  114. while (nodes.length > 0) {
  115. const node = nodes.shift();
  116. if (node) {
  117. result.push(node);
  118. if (node.getChildCount(sourceFile) >= 0) {
  119. nodes.unshift(...node.getChildren());
  120. }
  121. }
  122. }
  123. return result;
  124. }
  125. exports.getSourceNodes = getSourceNodes;
  126. function findNode(node, kind, text) {
  127. if (node.kind === kind && node.getText() === text) {
  128. // throw new Error(node.getText());
  129. return node;
  130. }
  131. let foundNode = null;
  132. ts.forEachChild(node, childNode => {
  133. foundNode = foundNode || findNode(childNode, kind, text);
  134. });
  135. return foundNode;
  136. }
  137. exports.findNode = findNode;
  138. /**
  139. * Helper for sorting nodes.
  140. * @return function to sort nodes in increasing order of position in sourceFile
  141. */
  142. function nodesByPosition(first, second) {
  143. return first.getStart() - second.getStart();
  144. }
  145. /**
  146. * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
  147. * or after the last of occurence of `syntaxKind` if the last occurence is a sub child
  148. * of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
  149. *
  150. * @param nodes insert after the last occurence of nodes
  151. * @param toInsert string to insert
  152. * @param file file to insert changes into
  153. * @param fallbackPos position to insert if toInsert happens to be the first occurence
  154. * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
  155. * @return Change instance
  156. * @throw Error if toInsert is first occurence but fall back is not set
  157. */
  158. function insertAfterLastOccurrence(nodes, toInsert, file, fallbackPos, syntaxKind) {
  159. let lastItem;
  160. for (const node of nodes) {
  161. if (!lastItem || lastItem.getStart() < node.getStart()) {
  162. lastItem = node;
  163. }
  164. }
  165. if (syntaxKind && lastItem) {
  166. lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
  167. }
  168. if (!lastItem && fallbackPos == undefined) {
  169. throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
  170. }
  171. const lastItemPosition = lastItem ? lastItem.getEnd() : fallbackPos;
  172. return new change_1.InsertChange(file, lastItemPosition, toInsert);
  173. }
  174. exports.insertAfterLastOccurrence = insertAfterLastOccurrence;
  175. function getContentOfKeyLiteral(_source, node) {
  176. if (node.kind == ts.SyntaxKind.Identifier) {
  177. return node.text;
  178. }
  179. else if (node.kind == ts.SyntaxKind.StringLiteral) {
  180. return node.text;
  181. }
  182. else {
  183. return null;
  184. }
  185. }
  186. exports.getContentOfKeyLiteral = getContentOfKeyLiteral;
  187. function _angularImportsFromNode(node, _sourceFile) {
  188. const ms = node.moduleSpecifier;
  189. let modulePath;
  190. switch (ms.kind) {
  191. case ts.SyntaxKind.StringLiteral:
  192. modulePath = ms.text;
  193. break;
  194. default:
  195. return {};
  196. }
  197. if (!modulePath.startsWith('@angular/')) {
  198. return {};
  199. }
  200. if (node.importClause) {
  201. if (node.importClause.name) {
  202. // This is of the form `import Name from 'path'`. Ignore.
  203. return {};
  204. }
  205. else if (node.importClause.namedBindings) {
  206. const nb = node.importClause.namedBindings;
  207. if (nb.kind == ts.SyntaxKind.NamespaceImport) {
  208. // This is of the form `import * as name from 'path'`. Return `name.`.
  209. return {
  210. [nb.name.text + '.']: modulePath,
  211. };
  212. }
  213. else {
  214. // This is of the form `import {a,b,c} from 'path'`
  215. const namedImports = nb;
  216. return namedImports.elements
  217. .map((is) => is.propertyName ? is.propertyName.text : is.name.text)
  218. .reduce((acc, curr) => {
  219. acc[curr] = modulePath;
  220. return acc;
  221. }, {});
  222. }
  223. }
  224. return {};
  225. }
  226. else {
  227. // This is of the form `import 'path';`. Nothing to do.
  228. return {};
  229. }
  230. }
  231. function getDecoratorMetadata(source, identifier, module) {
  232. const angularImports = findNodes(source, ts.SyntaxKind.ImportDeclaration)
  233. .map((node) => _angularImportsFromNode(node, source))
  234. .reduce((acc, current) => {
  235. for (const key of Object.keys(current)) {
  236. acc[key] = current[key];
  237. }
  238. return acc;
  239. }, {});
  240. return getSourceNodes(source)
  241. .filter(node => {
  242. return node.kind == ts.SyntaxKind.Decorator
  243. && node.expression.kind == ts.SyntaxKind.CallExpression;
  244. })
  245. .map(node => node.expression)
  246. .filter(expr => {
  247. if (expr.expression.kind == ts.SyntaxKind.Identifier) {
  248. const id = expr.expression;
  249. return id.text == identifier && angularImports[id.text] === module;
  250. }
  251. else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
  252. // This covers foo.NgModule when importing * as foo.
  253. const paExpr = expr.expression;
  254. // If the left expression is not an identifier, just give up at that point.
  255. if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
  256. return false;
  257. }
  258. const id = paExpr.name.text;
  259. const moduleId = paExpr.expression.text;
  260. return id === identifier && (angularImports[moduleId + '.'] === module);
  261. }
  262. return false;
  263. })
  264. .filter(expr => expr.arguments[0]
  265. && expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
  266. .map(expr => expr.arguments[0]);
  267. }
  268. exports.getDecoratorMetadata = getDecoratorMetadata;
  269. function findClassDeclarationParent(node) {
  270. if (ts.isClassDeclaration(node)) {
  271. return node;
  272. }
  273. return node.parent && findClassDeclarationParent(node.parent);
  274. }
  275. /**
  276. * Given a source file with @NgModule class(es), find the name of the first @NgModule class.
  277. *
  278. * @param source source file containing one or more @NgModule
  279. * @returns the name of the first @NgModule, or `undefined` if none is found
  280. */
  281. function getFirstNgModuleName(source) {
  282. // First, find the @NgModule decorators.
  283. const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
  284. if (ngModulesMetadata.length === 0) {
  285. return undefined;
  286. }
  287. // Then walk parent pointers up the AST, looking for the ClassDeclaration parent of the NgModule
  288. // metadata.
  289. const moduleClass = findClassDeclarationParent(ngModulesMetadata[0]);
  290. if (!moduleClass || !moduleClass.name) {
  291. return undefined;
  292. }
  293. // Get the class name of the module ClassDeclaration.
  294. return moduleClass.name.text;
  295. }
  296. exports.getFirstNgModuleName = getFirstNgModuleName;
  297. function getMetadataField(node, metadataField) {
  298. return node.properties
  299. .filter(prop => ts.isPropertyAssignment(prop))
  300. // Filter out every fields that's not "metadataField". Also handles string literals
  301. // (but not expressions).
  302. .filter(({ name }) => {
  303. return (ts.isIdentifier(name) || ts.isStringLiteral(name))
  304. && name.getText() === metadataField;
  305. });
  306. }
  307. exports.getMetadataField = getMetadataField;
  308. function addSymbolToNgModuleMetadata(source, ngModulePath, metadataField, symbolName, importPath = null) {
  309. const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
  310. let node = nodes[0]; // tslint:disable-line:no-any
  311. // Find the decorator declaration.
  312. if (!node) {
  313. return [];
  314. }
  315. // Get all the children property assignment of object literals.
  316. const matchingProperties = getMetadataField(node, metadataField);
  317. // Get the last node of the array literal.
  318. if (!matchingProperties) {
  319. return [];
  320. }
  321. if (matchingProperties.length == 0) {
  322. // We haven't found the field in the metadata declaration. Insert a new field.
  323. const expr = node;
  324. let position;
  325. let toInsert;
  326. if (expr.properties.length == 0) {
  327. position = expr.getEnd() - 1;
  328. toInsert = ` ${metadataField}: [${symbolName}]\n`;
  329. }
  330. else {
  331. node = expr.properties[expr.properties.length - 1];
  332. position = node.getEnd();
  333. // Get the indentation of the last element, if any.
  334. const text = node.getFullText(source);
  335. const matches = text.match(/^\r?\n\s*/);
  336. if (matches && matches.length > 0) {
  337. toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
  338. }
  339. else {
  340. toInsert = `, ${metadataField}: [${symbolName}]`;
  341. }
  342. }
  343. if (importPath !== null) {
  344. return [
  345. new change_1.InsertChange(ngModulePath, position, toInsert),
  346. insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
  347. ];
  348. }
  349. else {
  350. return [new change_1.InsertChange(ngModulePath, position, toInsert)];
  351. }
  352. }
  353. const assignment = matchingProperties[0];
  354. // If it's not an array, nothing we can do really.
  355. if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
  356. return [];
  357. }
  358. const arrLiteral = assignment.initializer;
  359. if (arrLiteral.elements.length == 0) {
  360. // Forward the property.
  361. node = arrLiteral;
  362. }
  363. else {
  364. node = arrLiteral.elements;
  365. }
  366. if (!node) {
  367. // tslint:disable-next-line: no-console
  368. console.error('No app module found. Please add your new class to your component.');
  369. return [];
  370. }
  371. if (Array.isArray(node)) {
  372. const nodeArray = node;
  373. const symbolsArray = nodeArray.map(node => node.getText());
  374. if (symbolsArray.includes(symbolName)) {
  375. return [];
  376. }
  377. node = node[node.length - 1];
  378. }
  379. let toInsert;
  380. let position = node.getEnd();
  381. if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
  382. // We haven't found the field in the metadata declaration. Insert a new
  383. // field.
  384. const expr = node;
  385. if (expr.properties.length == 0) {
  386. position = expr.getEnd() - 1;
  387. toInsert = ` ${symbolName}\n`;
  388. }
  389. else {
  390. // Get the indentation of the last element, if any.
  391. const text = node.getFullText(source);
  392. if (text.match(/^\r?\r?\n/)) {
  393. toInsert = `,${text.match(/^\r?\n\s*/)[0]}${symbolName}`;
  394. }
  395. else {
  396. toInsert = `, ${symbolName}`;
  397. }
  398. }
  399. }
  400. else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
  401. // We found the field but it's empty. Insert it just before the `]`.
  402. position--;
  403. toInsert = `${symbolName}`;
  404. }
  405. else {
  406. // Get the indentation of the last element, if any.
  407. const text = node.getFullText(source);
  408. if (text.match(/^\r?\n/)) {
  409. toInsert = `,${text.match(/^\r?\n(\r?)\s*/)[0]}${symbolName}`;
  410. }
  411. else {
  412. toInsert = `, ${symbolName}`;
  413. }
  414. }
  415. if (importPath !== null) {
  416. return [
  417. new change_1.InsertChange(ngModulePath, position, toInsert),
  418. insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
  419. ];
  420. }
  421. return [new change_1.InsertChange(ngModulePath, position, toInsert)];
  422. }
  423. exports.addSymbolToNgModuleMetadata = addSymbolToNgModuleMetadata;
  424. /**
  425. * Custom function to insert a declaration (component, pipe, directive)
  426. * into NgModule declarations. It also imports the component.
  427. */
  428. function addDeclarationToModule(source, modulePath, classifiedName, importPath) {
  429. return addSymbolToNgModuleMetadata(source, modulePath, 'declarations', classifiedName, importPath);
  430. }
  431. exports.addDeclarationToModule = addDeclarationToModule;
  432. /**
  433. * Custom function to insert an NgModule into NgModule imports. It also imports the module.
  434. */
  435. function addImportToModule(source, modulePath, classifiedName, importPath) {
  436. return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath);
  437. }
  438. exports.addImportToModule = addImportToModule;
  439. /**
  440. * Custom function to insert a provider into NgModule. It also imports it.
  441. */
  442. function addProviderToModule(source, modulePath, classifiedName, importPath) {
  443. return addSymbolToNgModuleMetadata(source, modulePath, 'providers', classifiedName, importPath);
  444. }
  445. exports.addProviderToModule = addProviderToModule;
  446. /**
  447. * Custom function to insert an export into NgModule. It also imports it.
  448. */
  449. function addExportToModule(source, modulePath, classifiedName, importPath) {
  450. return addSymbolToNgModuleMetadata(source, modulePath, 'exports', classifiedName, importPath);
  451. }
  452. exports.addExportToModule = addExportToModule;
  453. /**
  454. * Custom function to insert an export into NgModule. It also imports it.
  455. */
  456. function addBootstrapToModule(source, modulePath, classifiedName, importPath) {
  457. return addSymbolToNgModuleMetadata(source, modulePath, 'bootstrap', classifiedName, importPath);
  458. }
  459. exports.addBootstrapToModule = addBootstrapToModule;
  460. /**
  461. * Custom function to insert an entryComponent into NgModule. It also imports it.
  462. * @deprecated - Since version 9.0.0 with Ivy, entryComponents is no longer necessary.
  463. */
  464. function addEntryComponentToModule(source, modulePath, classifiedName, importPath) {
  465. return addSymbolToNgModuleMetadata(source, modulePath, 'entryComponents', classifiedName, importPath);
  466. }
  467. exports.addEntryComponentToModule = addEntryComponentToModule;
  468. /**
  469. * Determine if an import already exists.
  470. */
  471. function isImported(source, classifiedName, importPath) {
  472. const allNodes = getSourceNodes(source);
  473. const matchingNodes = allNodes
  474. .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
  475. .filter((imp) => imp.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
  476. .filter((imp) => {
  477. return imp.moduleSpecifier.text === importPath;
  478. })
  479. .filter((imp) => {
  480. if (!imp.importClause) {
  481. return false;
  482. }
  483. const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier)
  484. .filter(n => n.getText() === classifiedName);
  485. return nodes.length > 0;
  486. });
  487. return matchingNodes.length > 0;
  488. }
  489. exports.isImported = isImported;
  490. /**
  491. * This function returns the name of the environment export
  492. * whether this export is aliased or not. If the environment file
  493. * is not imported, then it will return `null`.
  494. */
  495. function getEnvironmentExportName(source) {
  496. // Initial value is `null` as we don't know yet if the user
  497. // has imported `environment` into the root module or not.
  498. let environmentExportName = null;
  499. const allNodes = getSourceNodes(source);
  500. allNodes
  501. .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
  502. .filter((declaration) => declaration.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral &&
  503. declaration.importClause !== undefined)
  504. .map((declaration) =>
  505. // If `importClause` property is defined then the first
  506. // child will be `NamedImports` object (or `namedBindings`).
  507. declaration.importClause.getChildAt(0))
  508. // Find those `NamedImports` object that contains `environment` keyword
  509. // in its text. E.g. `{ environment as env }`.
  510. .filter((namedImports) => namedImports.getText().includes('environment'))
  511. .forEach((namedImports) => {
  512. for (const specifier of namedImports.elements) {
  513. // `propertyName` is defined if the specifier
  514. // has an aliased import.
  515. const name = specifier.propertyName || specifier.name;
  516. // Find specifier that contains `environment` keyword in its text.
  517. // Whether it's `environment` or `environment as env`.
  518. if (name.text.includes('environment')) {
  519. environmentExportName = specifier.name.text;
  520. }
  521. }
  522. });
  523. return environmentExportName;
  524. }
  525. exports.getEnvironmentExportName = getEnvironmentExportName;
  526. /**
  527. * Returns the RouterModule declaration from NgModule metadata, if any.
  528. */
  529. function getRouterModuleDeclaration(source) {
  530. const result = getDecoratorMetadata(source, 'NgModule', '@angular/core');
  531. const node = result[0];
  532. const matchingProperties = getMetadataField(node, 'imports');
  533. if (!matchingProperties) {
  534. return;
  535. }
  536. const assignment = matchingProperties[0];
  537. if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
  538. return;
  539. }
  540. const arrLiteral = assignment.initializer;
  541. return arrLiteral.elements
  542. .filter(el => el.kind === ts.SyntaxKind.CallExpression)
  543. .find(el => el.getText().startsWith('RouterModule'));
  544. }
  545. exports.getRouterModuleDeclaration = getRouterModuleDeclaration;
  546. /**
  547. * Adds a new route declaration to a router module (i.e. has a RouterModule declaration)
  548. */
  549. function addRouteDeclarationToModule(source, fileToAdd, routeLiteral) {
  550. const routerModuleExpr = getRouterModuleDeclaration(source);
  551. if (!routerModuleExpr) {
  552. throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`);
  553. }
  554. const scopeConfigMethodArgs = routerModuleExpr.arguments;
  555. if (!scopeConfigMethodArgs.length) {
  556. const { line } = source.getLineAndCharacterOfPosition(routerModuleExpr.getStart());
  557. throw new Error(`The router module method doesn't have arguments ` +
  558. `at line ${line} in ${fileToAdd}`);
  559. }
  560. let routesArr;
  561. const routesArg = scopeConfigMethodArgs[0];
  562. // Check if the route declarations array is
  563. // an inlined argument of RouterModule or a standalone variable
  564. if (ts.isArrayLiteralExpression(routesArg)) {
  565. routesArr = routesArg;
  566. }
  567. else {
  568. const routesVarName = routesArg.getText();
  569. let routesVar;
  570. if (routesArg.kind === ts.SyntaxKind.Identifier) {
  571. routesVar = source.statements
  572. .filter((s) => s.kind === ts.SyntaxKind.VariableStatement)
  573. .find((v) => {
  574. return v.declarationList.declarations[0].name.getText() === routesVarName;
  575. });
  576. }
  577. if (!routesVar) {
  578. const { line } = source.getLineAndCharacterOfPosition(routesArg.getStart());
  579. throw new Error(`No route declaration array was found that corresponds ` +
  580. `to router module at line ${line} in ${fileToAdd}`);
  581. }
  582. routesArr = findNodes(routesVar, ts.SyntaxKind.ArrayLiteralExpression, 1)[0];
  583. }
  584. const occurrencesCount = routesArr.elements.length;
  585. const text = routesArr.getFullText(source);
  586. let route = routeLiteral;
  587. let insertPos = routesArr.elements.pos;
  588. if (occurrencesCount > 0) {
  589. const lastRouteLiteral = [...routesArr.elements].pop();
  590. const lastRouteIsWildcard = ts.isObjectLiteralExpression(lastRouteLiteral)
  591. && lastRouteLiteral
  592. .properties
  593. .some(n => (ts.isPropertyAssignment(n)
  594. && ts.isIdentifier(n.name)
  595. && n.name.text === 'path'
  596. && ts.isStringLiteral(n.initializer)
  597. && n.initializer.text === '**'));
  598. const indentation = text.match(/\r?\n(\r?)\s*/) || [];
  599. const routeText = `${indentation[0] || ' '}${routeLiteral}`;
  600. // Add the new route before the wildcard route
  601. // otherwise we'll always redirect to the wildcard route
  602. if (lastRouteIsWildcard) {
  603. insertPos = lastRouteLiteral.pos;
  604. route = `${routeText},`;
  605. }
  606. else {
  607. insertPos = lastRouteLiteral.end;
  608. route = `,${routeText}`;
  609. }
  610. }
  611. return new change_1.InsertChange(fileToAdd, insertPos, route);
  612. }
  613. exports.addRouteDeclarationToModule = addRouteDeclarationToModule;