UNPKG

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