UNPKG

@nstudio/schematics

Version:

Cross-platform (xplat) tools for Nx workspaces.

624 lines 25.1 kB
"use strict"; /** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT- style license that can be * found in the LICENSE file at https://angular.io/license */ Object.defineProperty(exports, "__esModule", { value: true }); const ast_utils_1 = require("@schematics/angular/utility/ast-utils"); const change_1 = require("@schematics/angular/utility/change"); const ts = require("typescript"); const path = require("path"); const name_utils_1 = require("./name-utils"); const general_1 = require("./general"); // This should be moved to @schematics/angular once it allows to pass custom expressions as providers function _addSymbolToNgModuleMetadata(source, ngModulePath, metadataField, expression) { const nodes = ast_utils_1.getDecoratorMetadata(source, 'NgModule', '@angular/core'); let node = nodes[0]; // tslint:disable-line:no-any // Find the decorator declaration. if (!node) { return []; } // Get all the children property assignment of object literals. const matchingProperties = node.properties .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) // Filter out every fields that's not "metadataField". Also handles string literals // (but not expressions). .filter((prop) => { const name = prop.name; switch (name.kind) { case ts.SyntaxKind.Identifier: return name.getText(source) == metadataField; case ts.SyntaxKind.StringLiteral: return name.text == metadataField; } return false; }); // Get the last node of the array literal. if (!matchingProperties) { return []; } if (matchingProperties.length == 0) { // We haven't found the field in the metadata declaration. Insert a new field. const expr = node; let position; let toInsert; if (expr.properties.length == 0) { position = expr.getEnd() - 1; toInsert = ` ${metadataField}: [${expression}]\n`; } else { node = expr.properties[expr.properties.length - 1]; position = node.getEnd(); // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match('^\r?\r?\n')) { toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${expression}]`; } else { toInsert = `, ${metadataField}: [${expression}]`; } } const newMetadataProperty = new change_1.InsertChange(ngModulePath, position, toInsert); return [newMetadataProperty]; } const assignment = matchingProperties[0]; // If it's not an array, nothing we can do really. if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { return []; } const arrLiteral = assignment.initializer; if (arrLiteral.elements.length == 0) { // Forward the property. node = arrLiteral; } else { node = arrLiteral.elements; } if (!node) { console.log('No app module found. Please add your new class to your component.'); return []; } if (Array.isArray(node)) { const nodeArray = node; const symbolsArray = nodeArray.map(node => node.getText()); if (symbolsArray.includes(expression)) { return []; } node = node[node.length - 1]; } let toInsert; let position = node.getEnd(); if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { // We haven't found the field in the metadata declaration. Insert a new // field. const expr = node; if (expr.properties.length == 0) { position = expr.getEnd() - 1; toInsert = ` ${metadataField}: [${expression}]\n`; } else { node = expr.properties[expr.properties.length - 1]; position = node.getEnd(); // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match('^\r?\r?\n')) { toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${expression}]`; } else { toInsert = `, ${metadataField}: [${expression}]`; } } } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { // We found the field but it's empty. Insert it just before the `]`. position--; toInsert = `${expression}`; } else { // Get the indentation of the last element, if any. const text = node.getFullText(source); if (text.match(/^\r?\n/)) { toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${expression}`; } else { toInsert = `, ${expression}`; } } const insert = new change_1.InsertChange(ngModulePath, position, toInsert); return [insert]; } exports._addSymbolToNgModuleMetadata = _addSymbolToNgModuleMetadata; function addParameterToConstructor(source, modulePath, opts) { const clazz = findClass(source, opts.className); const constructor = clazz.members.filter(m => m.kind === ts.SyntaxKind.Constructor)[0]; if (constructor) { throw new Error('Should be tested'); } else { const methodHeader = `constructor(${opts.param})`; return addMethod(source, modulePath, { className: opts.className, methodHeader, body: null }); } } exports.addParameterToConstructor = addParameterToConstructor; function addMethod(source, modulePath, opts) { const clazz = findClass(source, opts.className); const body = opts.body ? ` ${opts.methodHeader} { ${offset(opts.body, 1, false)} } ` : ` ${opts.methodHeader} {} `; return [new change_1.InsertChange(modulePath, clazz.end - 1, offset(body, 1, true))]; } exports.addMethod = addMethod; function removeFromNgModule(source, modulePath, property) { const nodes = ast_utils_1.getDecoratorMetadata(source, 'NgModule', '@angular/core'); let node = nodes[0]; // tslint:disable-line:no-any // Find the decorator declaration. if (!node) { return []; } // Get all the children property assignment of object literals. const matchingProperty = getMatchingProperty(source, property); if (matchingProperty) { return [ new change_1.RemoveChange(modulePath, matchingProperty.pos, matchingProperty.getFullText(source)) ]; } else { return []; } } exports.removeFromNgModule = removeFromNgModule; function findClass(source, className, silent = false) { const nodes = ast_utils_1.getSourceNodes(source); const clazz = nodes.filter(n => n.kind === ts.SyntaxKind.ClassDeclaration && n.name.text === className)[0]; if (!clazz && !silent) { throw new Error(`Cannot find class '${className}'`); } return clazz; } exports.findClass = findClass; function offset(text, numberOfTabs, wrap) { const lines = text .trim() .split('\n') .map(line => { let tabs = ''; for (let c = 0; c < numberOfTabs; ++c) { tabs += ' '; } return `${tabs}${line}`; }) .join('\n'); return wrap ? `\n${lines}\n` : lines; } exports.offset = offset; function addImportToModule(source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(source, modulePath, 'imports', symbolName); } exports.addImportToModule = addImportToModule; function addImportToTestBed(source, specPath, symbolName) { const allCalls = ast_utils_1.findNodes(source, ts.SyntaxKind.CallExpression); const configureTestingModuleObjectLiterals = allCalls .filter(c => c.expression.kind === ts.SyntaxKind.PropertyAccessExpression) .filter((c) => c.expression.name.getText(source) === 'configureTestingModule') .map(c => c.arguments[0].kind === ts.SyntaxKind.ObjectLiteralExpression ? c.arguments[0] : null); if (configureTestingModuleObjectLiterals.length > 0) { const startPosition = configureTestingModuleObjectLiterals[0] .getFirstToken(source) .getEnd(); return [ new change_1.InsertChange(specPath, startPosition, `imports: [${symbolName}], `) ]; } else { return []; } } exports.addImportToTestBed = addImportToTestBed; function addReexport(source, modulePath, reexportedFileName, token) { const allExports = ast_utils_1.findNodes(source, ts.SyntaxKind.ExportDeclaration); if (allExports.length > 0) { const m = allExports.filter((e) => e.moduleSpecifier.getText(source).indexOf(reexportedFileName) > -1); if (m.length > 0) { const mm = m[0]; return [ new change_1.InsertChange(modulePath, mm.exportClause.end - 1, `, ${token} `) ]; } } return []; } exports.addReexport = addReexport; function getBootstrapComponent(source, moduleClassName) { const bootstrap = getMatchingProperty(source, 'bootstrap'); if (!bootstrap) { throw new Error(`Cannot find bootstrap components in '${moduleClassName}'`); } const c = bootstrap.getChildren(); const nodes = c[c.length - 1].getChildren(); const bootstrapComponent = nodes.slice(1, nodes.length - 1)[0]; if (!bootstrapComponent) { throw new Error(`Cannot find bootstrap components in '${moduleClassName}'`); } return bootstrapComponent.getText(); } exports.getBootstrapComponent = getBootstrapComponent; function getMatchingObjectLiteralElement(node, source, property) { return (node.properties .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) // Filter out every fields that's not "metadataField". Also handles string literals // (but not expressions). .filter((prop) => { const name = prop.name; switch (name.kind) { case ts.SyntaxKind.Identifier: return name.getText(source) === property; case ts.SyntaxKind.StringLiteral: return name.text === property; } return false; })[0]); } function getMatchingProperty(source, property) { const nodes = ast_utils_1.getDecoratorMetadata(source, 'NgModule', '@angular/core'); let node = nodes[0]; // tslint:disable-line:no-any if (!node) return null; // Get all the children property assignment of object literals. return getMatchingObjectLiteralElement(node, source, property); } function addRoute(ngModulePath, source, route) { const routes = getListOfRoutes(source); if (!routes) return []; if (routes.hasTrailingComma || routes.length === 0) { return [new change_1.InsertChange(ngModulePath, routes.end, route)]; } else { return [new change_1.InsertChange(ngModulePath, routes.end, `, ${route}`)]; } } exports.addRoute = addRoute; function addToCollection(source, barrelIndexPath, symbolName, insertSpaces = '') { const collection = getCollection(source); if (!collection) return []; // if (!collection) return [new NoopChange()]; // return [new NoopChange()]; if (collection.hasTrailingComma || collection.length === 0) { return [new change_1.InsertChange(barrelIndexPath, collection.end, symbolName)]; } else { return [new change_1.InsertChange(barrelIndexPath, collection.end, `,\n${insertSpaces}${symbolName}`)]; } } exports.addToCollection = addToCollection; function addIncludeToTsConfig(tsConfigPath, source, include) { const includeKeywordPos = source.text.indexOf('"include":'); if (includeKeywordPos > -1) { const includeArrayEndPos = source.text.indexOf(']', includeKeywordPos); return [new change_1.InsertChange(tsConfigPath, includeArrayEndPos, include)]; } else { return []; } } exports.addIncludeToTsConfig = addIncludeToTsConfig; function getListOfRoutes(source) { const imports = getMatchingProperty(source, 'imports'); if (imports.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression) { const a = imports.initializer; for (let e of a.elements) { if (e.kind === ts.SyntaxKind.CallExpression) { const ee = e; const text = ee.expression.getText(source); if ((text === 'RouterModule.forRoot' || text === 'RouterModule.forChild') && ee.arguments.length > 0) { const routes = ee.arguments[0]; if (routes.kind === ts.SyntaxKind.ArrayLiteralExpression) { return routes.elements; } } } } } return null; } function getCollection(source) { const allCollections = ast_utils_1.findNodes(source, ts.SyntaxKind.ArrayLiteralExpression); // console.log('allCollections:', allCollections); // allCollections.forEach((i: ts.ArrayLiteralExpression) => { // console.log('getText:',i.getText()); // }); // always assume the first is the standard components collection // other type could be an entry components collection which is not supported (yet) if (allCollections && allCollections.length) { return allCollections[0].elements; } return null; } function getImport(source, predicate) { const allImports = ast_utils_1.findNodes(source, ts.SyntaxKind.ImportDeclaration); const matching = allImports.filter((i) => predicate(i.moduleSpecifier.getText())); return matching.map((i) => { const moduleSpec = i.moduleSpecifier .getText() .substring(1, i.moduleSpecifier.getText().length - 1); const t = i.importClause.namedBindings.getText(); const bindings = t .replace('{', '') .replace('}', '') .split(',') .map(q => q.trim()); return { moduleSpec, bindings }; }); } exports.getImport = getImport; function addProviderToModule(source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(source, modulePath, 'providers', symbolName); } exports.addProviderToModule = addProviderToModule; function addDeclarationToModule(source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(source, modulePath, 'declarations', symbolName); } exports.addDeclarationToModule = addDeclarationToModule; function addEntryComponents(source, modulePath, symbolName) { return _addSymbolToNgModuleMetadata(source, modulePath, 'entryComponents', symbolName); } exports.addEntryComponents = addEntryComponents; function addGlobal(source, modulePath, statement, isExport) { if (isExport) { const allExports = ast_utils_1.findNodes(source, ts.SyntaxKind.ExportDeclaration); // console.log('allExports:', allExports.length); if (allExports.length > 0) { const lastExport = allExports[allExports.length - 1]; // console.log('lastExport.end:', lastExport.end); return [ new change_1.InsertChange(modulePath, lastExport.end, `\n${statement}\n`) ]; } else { return [new change_1.InsertChange(modulePath, 0, `${statement}\n`)]; } } else { const allImports = ast_utils_1.findNodes(source, ts.SyntaxKind.ImportDeclaration); // console.log('allImports:', allImports.length); if (allImports.length > 0) { const lastImport = allImports[allImports.length - 1]; return [ // new InsertChange(modulePath, lastImport.end + 1, `\n${statement}\n`) new change_1.InsertChange(modulePath, lastImport.end, `\n${statement}`) ]; } else { return [new change_1.InsertChange(modulePath, 0, `${statement}`)]; } } } exports.addGlobal = addGlobal; function insert(host, modulePath, changes) { const recorder = host.beginUpdate(modulePath); for (const change of changes) { if (change instanceof change_1.InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } else if (change instanceof change_1.RemoveChange) { recorder.remove(change.pos - 1, change.toRemove.length + 1); } else if (change instanceof change_1.NoopChange) { // do nothing } else if (change instanceof change_1.ReplaceChange) { const action = change; recorder.remove(action.pos + 1, action.oldText.length); recorder.insertLeft(action.pos + 1, action.newText); } else { throw new Error(`Unexpected Change '${change}'`); } } host.commitUpdate(recorder); } exports.insert = insert; /** * This method is specifically for reading JSON files in a Tree * @param host The host tree * @param path The path to the JSON file * @returns The JSON data in the file. */ function readJsonInTree(host, path) { if (!host.exists(path)) { throw new Error(`Cannot find ${path}`); } return general_1.jsonParse(host.read(path).toString('utf-8')); } exports.readJsonInTree = readJsonInTree; /** * This method is specifically for updating JSON in a Tree * @param path Path of JSON file in the Tree * @param callback Manipulation of the JSON data * @returns A rule which updates a JSON file file in a Tree */ function updateJsonInTree(path, callback) { return (host) => { host.overwrite(path, general_1.serializeJson(callback(readJsonInTree(host, path)))); return host; }; } exports.updateJsonInTree = updateJsonInTree; function getProjectConfig(host, name) { const angularJson = readJsonInTree(host, '/angular.json'); const projectConfig = angularJson.projects[name]; if (!projectConfig) { throw new Error(`Cannot find project '${name}'`); } else { return projectConfig; } } exports.getProjectConfig = getProjectConfig; function readBootstrapInfo(host, app) { const config = getProjectConfig(host, app); let mainPath; try { mainPath = config.architect.build.options.main; } catch (e) { throw new Error('Main file cannot be located'); } if (!host.exists(mainPath)) { throw new Error('Main file cannot be located'); } const mainSource = host.read(mainPath).toString('utf-8'); const main = ts.createSourceFile(mainPath, mainSource, ts.ScriptTarget.Latest, true); const moduleImports = getImport(main, (s) => s.indexOf('.module') > -1); if (moduleImports.length !== 1) { throw new Error(`main.ts can only import a single module`); } const moduleImport = moduleImports[0]; const moduleClassName = moduleImport.bindings.filter(b => b.endsWith('Module'))[0]; const modulePath = `${path.join(path.dirname(mainPath), moduleImport.moduleSpec)}.ts`; if (!host.exists(modulePath)) { throw new Error(`Cannot find '${modulePath}'`); } const moduleSourceText = host.read(modulePath).toString('utf-8'); const moduleSource = ts.createSourceFile(modulePath, moduleSourceText, ts.ScriptTarget.Latest, true); const bootstrapComponentClassName = getBootstrapComponent(moduleSource, moduleClassName); const bootstrapComponentFileName = `./${path.join(path.dirname(moduleImport.moduleSpec), `${name_utils_1.toFileName(bootstrapComponentClassName.substring(0, bootstrapComponentClassName.length - 9))}.component`)}`; return { moduleSpec: moduleImport.moduleSpec, mainPath, modulePath, moduleSource, moduleClassName, bootstrapComponentClassName, bootstrapComponentFileName }; } exports.readBootstrapInfo = readBootstrapInfo; function addClass(source, modulePath, clazzName, clazzSrc) { if (!findClass(source, clazzName, true)) { const nodes = ast_utils_1.findNodes(source, ts.SyntaxKind.ClassDeclaration); return ast_utils_1.insertAfterLastOccurrence(nodes, offset(clazzSrc, 0, true), modulePath, 0, ts.SyntaxKind.ClassDeclaration); } return new change_1.NoopChange(); } exports.addClass = addClass; function addUnionTypes(source, modulePath, typeName, typeValues) { const target = findNodesOfType(source, ts.SyntaxKind.TypeAliasDeclaration, it => it.name.getText() === typeName); if (!target) { throw new Error(`Cannot find union type '${typeName}'`); } const node = target.type; // Append new types to create a union type... return new change_1.InsertChange(modulePath, node.end, ['', ...typeValues].join(' | ')); } exports.addUnionTypes = addUnionTypes; /** * Add 1..n enumerators using name + (optional) value pairs */ function addEnumeratorValues(source, modulePath, enumName, pairs = []) { const target = findNodesOfType(source, ts.SyntaxKind.EnumDeclaration, it => it.name.getText() === enumName); const list = target ? target.members : undefined; if (!target) { throw new Error(`Cannot find enum '${enumName}'`); } const addComma = !(list.hasTrailingComma || list.length === 0); return pairs.reduce((buffer, it) => { const member = it.value ? `${it.name} = '${it.value}'` : it.name; const memberExists = () => { return list.filter(m => m.name.getText() === it.name).length; }; if (memberExists()) { throw new Error(`Enum '${enumName}.${it.name}' already exists`); } return [ ...buffer, new change_1.InsertChange(modulePath, list.end, (addComma ? ', ' : '') + member) ]; }, []); } exports.addEnumeratorValues = addEnumeratorValues; /** * Find Enum declaration in source based on name * e.g. * export enum ProductsActionTypes { * ProductsAction = '[Products] Action' * } */ const IDENTITY = a => a; function findNodesOfType(source, kind, predicate, extract = IDENTITY, firstOnly = true) { const nodes = ast_utils_1.findNodes(source, kind); const matching = nodes.filter((i) => predicate(i)).map(extract); return matching.length ? (firstOnly ? matching[0] : matching) : undefined; } exports.findNodesOfType = findNodesOfType; function insertImport(source, fileToEdit, symbolName, fileName, isDefault = false) { const rootNode = source; const allImports = ast_utils_1.findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); // get nodes that map to import statements from the file fileName const relevantImports = allImports.filter(node => { // StringLiteral of the ImportDeclaration is the import file (fileName in this case). const importFiles = node .getChildren() .filter(child => child.kind === ts.SyntaxKind.StringLiteral) .map(n => n.text); return importFiles.filter(file => file === fileName).length === 1; }); if (relevantImports.length > 0) { let importsAsterisk = false; // imports from import file const imports = []; relevantImports.forEach(n => { Array.prototype.push.apply(imports, ast_utils_1.findNodes(n, ts.SyntaxKind.Identifier)); if (ast_utils_1.findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { importsAsterisk = true; } }); // if imports * from fileName, don't add symbolName if (importsAsterisk) { return new change_1.NoopChange(); } const importTextNodes = imports.filter(n => n.text === symbolName); // insert import if it's not there if (importTextNodes.length === 0) { const fallbackPos = ast_utils_1.findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() || ast_utils_1.findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); return ast_utils_1.insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); } return new change_1.NoopChange(); } // no such import declaration exists const useStrict = ast_utils_1.findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter((n) => n.text === 'use strict'); let fallbackPos = 0; if (useStrict.length > 0) { fallbackPos = useStrict[0].end; } const open = isDefault ? '' : '{ '; const close = isDefault ? '' : ' }'; // if there are no imports or 'use strict' statement, insert import at beginning of file const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; const separator = insertAtBeginning ? '' : ';\n'; const toInsert = `${separator}import ${open}${symbolName}${close}` + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; return ast_utils_1.insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral); } exports.insertImport = insertImport; //# sourceMappingURL=ast.js.map