UNPKG

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