From d93a5b7fecc4c31fe137014c012260ba9cbcbfe0 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 17 Jan 2018 20:44:47 -0600 Subject: [PATCH 1/5] feat(schematics): add initial schematics utils --- .gitignore | 4 + schematics/collection.json | 18 + schematics/package.json | 39 ++ schematics/tsconfig.json | 27 + schematics/utils/ast.ts | 57 +++ schematics/utils/devkit-utils/README.md | 2 + schematics/utils/devkit-utils/ast-utils.ts | 448 +++++++++++++++++ schematics/utils/devkit-utils/change.ts | 127 +++++ schematics/utils/devkit-utils/component.ts | 126 +++++ schematics/utils/devkit-utils/config.ts | 465 ++++++++++++++++++ schematics/utils/devkit-utils/find-module.ts | 111 +++++ schematics/utils/devkit-utils/ng-ast-utils.ts | 84 ++++ schematics/utils/devkit-utils/route-utils.ts | 90 ++++ schematics/utils/html.ts | 56 +++ schematics/utils/lib-versions.ts | 3 + schematics/utils/package.ts | 22 + schematics/utils/testing.ts | 25 + 17 files changed, 1704 insertions(+) create mode 100644 schematics/collection.json create mode 100644 schematics/package.json create mode 100644 schematics/tsconfig.json create mode 100644 schematics/utils/ast.ts create mode 100644 schematics/utils/devkit-utils/README.md create mode 100755 schematics/utils/devkit-utils/ast-utils.ts create mode 100755 schematics/utils/devkit-utils/change.ts create mode 100644 schematics/utils/devkit-utils/component.ts create mode 100755 schematics/utils/devkit-utils/config.ts create mode 100755 schematics/utils/devkit-utils/find-module.ts create mode 100755 schematics/utils/devkit-utils/ng-ast-utils.ts create mode 100755 schematics/utils/devkit-utils/route-utils.ts create mode 100644 schematics/utils/html.ts create mode 100644 schematics/utils/lib-versions.ts create mode 100644 schematics/utils/package.ts create mode 100644 schematics/utils/testing.ts diff --git a/.gitignore b/.gitignore index 9804dad5288e..d14edc307fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ npm-debug.log testem.log /.chrome /.git + +# schematics +/schematics/**/*.js +/schematics/**/*.map \ No newline at end of file diff --git a/schematics/collection.json b/schematics/collection.json new file mode 100644 index 000000000000..a3f3ab4d4605 --- /dev/null +++ b/schematics/collection.json @@ -0,0 +1,18 @@ +// By default, collection.json is a Loose-format JSON5 format, which means it's loaded using a +// special loader and you can use comments, as well as single quotes or no-quotes for standard +// JavaScript identifiers. +// Note that this is only true for collection.json and it depends on the tooling itself. +// We read package.json using a require() call, which is standard JSON. +{ + // This is just to indicate to your IDE that there is a schema for collection.json. + "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", + + // Schematics are listed as a map of schematicName => schematicDescription. + // Each description contains a description field which is required, a factory reference, + // an extends field and a schema reference. + // The extends field points to another schematic (either in the same collection or a + // separate collection using the format collectionName:schematicName). + // The factory is required, except when using the extends field. The the factory can + // overwrite the extended schematic factory. + "schematics": {} +} diff --git a/schematics/package.json b/schematics/package.json new file mode 100644 index 000000000000..df160f94e305 --- /dev/null +++ b/schematics/package.json @@ -0,0 +1,39 @@ +{ + "name": "@angular/material-schematics", + "version": "1.0.23", + "description": "Material Schematics", + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run build && jasmine **/*_spec.js" + }, + "keywords": [ + "schematics", + "angular", + "material", + "angular-cli" + ], + "files": [ + "**/*.json", + "**/*.d.ts", + "**/*.ts", + "**/*.__styleext__", + "**/*.js", + "**/*.html", + "**/*.map" + ], + "author": "", + "license": "MIT", + "schematics": "./collection.json", + "dependencies": { + "@angular-devkit/core": "^0.0.22", + "@angular-devkit/schematics": "^0.0.42", + "@schematics/angular": "^0.1.11", + "parse5": "^3.0.3", + "typescript": "^2.5.2" + }, + "devDependencies": { + "@types/jasmine": "^2.6.0", + "@types/node": "^8.0.31", + "jasmine": "^2.8.0" + } +} diff --git a/schematics/tsconfig.json b/schematics/tsconfig.json new file mode 100644 index 000000000000..219cec2afd28 --- /dev/null +++ b/schematics/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": "tsconfig", + "lib": [ + "es2017", + "dom" + ], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": false, + "rootDir": "src/", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "target": "es6", + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/*/files/**/*" + ] +} diff --git a/schematics/utils/ast.ts b/schematics/utils/ast.ts new file mode 100644 index 000000000000..cdaf19ce1641 --- /dev/null +++ b/schematics/utils/ast.ts @@ -0,0 +1,57 @@ +import { SchematicsException } from '@angular-devkit/schematics'; +import { Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { addImportToModule } from './devkit-utils/ast-utils'; +import { getAppModulePath } from './devkit-utils/ng-ast-utils'; +import { InsertChange } from './devkit-utils/change'; +import { getConfig, getAppFromConfig } from './devkit-utils/config'; +import { normalize } from '@angular-devkit/core'; + +/** + * Reads file given path and returns TypeScript source file. + */ +export function getSourceFile(host: Tree, path: string): ts.SourceFile { + const buffer = host.read(path); + if (!buffer) { + throw new SchematicsException(`Could not find file for path: ${path}`); + } + const content = buffer.toString(); + const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); + return source; +} + +/** + * Import and add module to root app module. + */ +export function addToRootModule(host: Tree, moduleName: string, src: string) { + const config = getConfig(host); + const app = getAppFromConfig(config, '0'); + const modulePath = getAppModulePath(host, app); + addToModule(host, modulePath, moduleName, src); +} + +/** + * Import and add module to specific module path. + */ +export function addToModule(host: Tree, modulePath: string, moduleName: string, src: string) { + const moduleSource = getSourceFile(host, modulePath); + const changes = addImportToModule(moduleSource, modulePath, moduleName, src); + const recorder = host.beginUpdate(modulePath); + + changes.forEach((change) => { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + }); + + host.commitUpdate(recorder); +} + +/** + * Gets the app index.html file + */ +export function getIndexPath(host: Tree) { + const config = getConfig(host); + const app = getAppFromConfig(config, '0'); + return normalize(`/${app.root}/${app.index}`); +} diff --git a/schematics/utils/devkit-utils/README.md b/schematics/utils/devkit-utils/README.md new file mode 100644 index 000000000000..7c6ca2b68b9d --- /dev/null +++ b/schematics/utils/devkit-utils/README.md @@ -0,0 +1,2 @@ +# NOTE +This code is directly taken from [angular schematics package](https://github.com/angular/devkit/tree/master/packages/schematics/angular/utility). \ No newline at end of file diff --git a/schematics/utils/devkit-utils/ast-utils.ts b/schematics/utils/devkit-utils/ast-utils.ts new file mode 100755 index 000000000000..2f34665f5c7b --- /dev/null +++ b/schematics/utils/devkit-utils/ast-utils.ts @@ -0,0 +1,448 @@ +/** + * @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 + */ +import * as ts from 'typescript'; +import { Change, InsertChange } from './change'; +import { insertImport } from './route-utils'; + + +/** + * Find all nodes from the AST in the subtree of node of SyntaxKind kind. + * @param node + * @param kind + * @param max The maximum number of items to return. + * @return all nodes of kind, or [] if none is found + */ +export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): ts.Node[] { + if (!node || max == 0) { + return []; + } + + const arr: ts.Node[] = []; + if (node.kind === kind) { + arr.push(node); + max--; + } + if (max > 0) { + for (const child of node.getChildren()) { + findNodes(child, kind, max).forEach(node => { + if (max > 0) { + arr.push(node); + } + max--; + }); + + if (max <= 0) { + break; + } + } + } + + return arr; +} + + +/** + * Get all the nodes from a source. + * @param sourceFile The source file object. + * @returns {Observable} An observable of all the nodes in the source. + */ +export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { + const nodes: ts.Node[] = [sourceFile]; + const result = []; + + while (nodes.length > 0) { + const node = nodes.shift(); + + if (node) { + result.push(node); + if (node.getChildCount(sourceFile) >= 0) { + nodes.unshift(...node.getChildren()); + } + } + } + + return result; +} + +export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null { + if (node.kind === kind && node.getText() === text) { + // throw new Error(node.getText()); + return node; + } + + let foundNode: ts.Node | null = null; + ts.forEachChild(node, childNode => { + foundNode = foundNode || findNode(childNode, kind, text); + }); + + return foundNode; +} + + +/** + * Helper for sorting nodes. + * @return function to sort nodes in increasing order of position in sourceFile + */ +function nodesByPosition(first: ts.Node, second: ts.Node): number { + return first.pos - second.pos; +} + + +/** + * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` + * or after the last of occurence of `syntaxKind` if the last occurence is a sub child + * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. + * + * @param nodes insert after the last occurence of nodes + * @param toInsert string to insert + * @param file file to insert changes into + * @param fallbackPos position to insert if toInsert happens to be the first occurence + * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after + * @return Change instance + * @throw Error if toInsert is first occurence but fall back is not set + */ +export function insertAfterLastOccurrence(nodes: ts.Node[], + toInsert: string, + file: string, + fallbackPos: number, + syntaxKind?: ts.SyntaxKind): Change { + let lastItem = nodes.sort(nodesByPosition).pop(); + if (!lastItem) { + throw new Error(); + } + if (syntaxKind) { + lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); + } + if (!lastItem && fallbackPos == undefined) { + throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`); + } + const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; + + return new InsertChange(file, lastItemPosition, toInsert); +} + + +export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null { + if (node.kind == ts.SyntaxKind.Identifier) { + return (node as ts.Identifier).text; + } else if (node.kind == ts.SyntaxKind.StringLiteral) { + return (node as ts.StringLiteral).text; + } else { + return null; + } +} + + +function _angularImportsFromNode(node: ts.ImportDeclaration, + _sourceFile: ts.SourceFile): {[name: string]: string} { + const ms = node.moduleSpecifier; + let modulePath: string; + switch (ms.kind) { + case ts.SyntaxKind.StringLiteral: + modulePath = (ms as ts.StringLiteral).text; + break; + default: + return {}; + } + + if (!modulePath.startsWith('@angular/')) { + return {}; + } + + if (node.importClause) { + if (node.importClause.name) { + // This is of the form `import Name from 'path'`. Ignore. + return {}; + } else if (node.importClause.namedBindings) { + const nb = node.importClause.namedBindings; + if (nb.kind == ts.SyntaxKind.NamespaceImport) { + // This is of the form `import * as name from 'path'`. Return `name.`. + return { + [(nb as ts.NamespaceImport).name.text + '.']: modulePath, + }; + } else { + // This is of the form `import {a,b,c} from 'path'` + const namedImports = nb as ts.NamedImports; + + return namedImports.elements + .map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) + .reduce((acc: {[name: string]: string}, curr: string) => { + acc[curr] = modulePath; + + return acc; + }, {}); + } + } + + return {}; + } else { + // This is of the form `import 'path';`. Nothing to do. + return {}; + } +} + + +export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, + module: string): ts.Node[] { + const angularImports: {[name: string]: string} + = findNodes(source, ts.SyntaxKind.ImportDeclaration) + .map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source)) + .reduce((acc: {[name: string]: string}, current: {[name: string]: string}) => { + for (const key of Object.keys(current)) { + acc[key] = current[key]; + } + + return acc; + }, {}); + + return getSourceNodes(source) + .filter(node => { + return node.kind == ts.SyntaxKind.Decorator + && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; + }) + .map(node => (node as ts.Decorator).expression as ts.CallExpression) + .filter(expr => { + if (expr.expression.kind == ts.SyntaxKind.Identifier) { + const id = expr.expression as ts.Identifier; + + return id.getFullText(source) == identifier + && angularImports[id.getFullText(source)] === module; + } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { + // This covers foo.NgModule when importing * as foo. + const paExpr = expr.expression as ts.PropertyAccessExpression; + // If the left expression is not an identifier, just give up at that point. + if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { + return false; + } + + const id = paExpr.name.text; + const moduleId = (paExpr.expression as ts.Identifier).getText(source); + + return id === identifier && (angularImports[moduleId + '.'] === module); + } + + return false; + }) + .filter(expr => expr.arguments[0] + && expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression) + .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); +} + +export function addSymbolToNgModuleMetadata( + source: ts.SourceFile, + ngModulePath: string, + metadataField: string, + symbolName: string, + importPath: string | null = null, +): Change[] { + const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); + let node: any = 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: ts.ObjectLiteralElement[] = + (node as ts.ObjectLiteralExpression).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: ts.PropertyAssignment) => { + const name = prop.name; + switch (name.kind) { + case ts.SyntaxKind.Identifier: + return (name as ts.Identifier).getText(source) == metadataField; + case ts.SyntaxKind.StringLiteral: + return (name as ts.StringLiteral).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 as ts.ObjectLiteralExpression; + let position: number; + let toInsert: string; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\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); + const matches = text.match(/^\r?\n\s*/); + if (matches.length > 0) { + toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + if (importPath !== null) { + return [ + new InsertChange(ngModulePath, position, toInsert), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), + ]; + } else { + return [new InsertChange(ngModulePath, position, toInsert)]; + } + } + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + // If it's not an array, nothing we can do really. + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return []; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + 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 as {} as Array; + const symbolsArray = nodeArray.map(node => node.getText()); + if (symbolsArray.includes(symbolName)) { + return []; + } + + node = node[node.length - 1]; + } + + let toInsert: string; + 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 as ts.ObjectLiteralExpression; + if (expr.properties.length == 0) { + position = expr.getEnd() - 1; + toInsert = ` ${metadataField}: [${symbolName}]\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}: [${symbolName}]`; + } else { + toInsert = `, ${metadataField}: [${symbolName}]`; + } + } + } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { + // We found the field but it's empty. Insert it just before the `]`. + position--; + toInsert = `${symbolName}`; + } 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]}${symbolName}`; + } else { + toInsert = `, ${symbolName}`; + } + } + if (importPath !== null) { + return [ + new InsertChange(ngModulePath, position, toInsert), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), + ]; + } + + return [new InsertChange(ngModulePath, position, toInsert)]; +} + +/** + * Custom function to insert a declaration (component, pipe, directive) + * into NgModule declarations. It also imports the component. + */ +export function addDeclarationToModule(source: ts.SourceFile, + modulePath: string, classifiedName: string, + importPath: string): Change[] { + return addSymbolToNgModuleMetadata( + source, modulePath, 'declarations', classifiedName, importPath); +} + +/** + * Custom function to insert an NgModule into NgModule imports. It also imports the module. + */ +export function addImportToModule(source: ts.SourceFile, + modulePath: string, classifiedName: string, + importPath: string): Change[] { + + return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath); +} + +/** + * Custom function to insert a provider into NgModule. It also imports it. + */ +export function addProviderToModule(source: ts.SourceFile, + modulePath: string, classifiedName: string, + importPath: string): Change[] { + return addSymbolToNgModuleMetadata(source, modulePath, 'providers', classifiedName, importPath); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addExportToModule(source: ts.SourceFile, + modulePath: string, classifiedName: string, + importPath: string): Change[] { + return addSymbolToNgModuleMetadata(source, modulePath, 'exports', classifiedName, importPath); +} + +/** + * Custom function to insert an export into NgModule. It also imports it. + */ +export function addBootstrapToModule(source: ts.SourceFile, + modulePath: string, classifiedName: string, + importPath: string): Change[] { + return addSymbolToNgModuleMetadata(source, modulePath, 'bootstrap', classifiedName, importPath); +} + +/** + * Determine if an import already exists. + */ +export function isImported(source: ts.SourceFile, + classifiedName: string, + importPath: string): boolean { + const allNodes = getSourceNodes(source); + const matchingNodes = allNodes + .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) + .filter((imp: ts.ImportDeclaration) => imp.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) + .filter((imp: ts.ImportDeclaration) => { + return ( imp.moduleSpecifier).text === importPath; + }) + .filter((imp: ts.ImportDeclaration) => { + if (!imp.importClause) { + return false; + } + const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier) + .filter(n => n.getText() === classifiedName); + + return nodes.length > 0; + }); + + return matchingNodes.length > 0; +} diff --git a/schematics/utils/devkit-utils/change.ts b/schematics/utils/devkit-utils/change.ts new file mode 100755 index 000000000000..12556352abda --- /dev/null +++ b/schematics/utils/devkit-utils/change.ts @@ -0,0 +1,127 @@ +/** + * @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 + */ +export interface Host { + write(path: string, content: string): Promise; + read(path: string): Promise; +} + + +export interface Change { + apply(host: Host): Promise; + + // The file this change should be applied to. Some changes might not apply to + // a file (maybe the config). + readonly path: string | null; + + // The order this change should be applied. Normally the position inside the file. + // Changes are applied from the bottom of a file to the top. + readonly order: number; + + // The description of this change. This will be outputted in a dry or verbose run. + readonly description: string; +} + + +/** + * An operation that does nothing. + */ +export class NoopChange implements Change { + description = 'No operation.'; + order = Infinity; + path = null; + apply() { return Promise.resolve(); } +} + + +/** + * Will add text to the source code. + */ +export class InsertChange implements Change { + + order: number; + description: string; + + constructor(public path: string, public pos: number, public toAdd: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; + this.order = pos; + } + + /** + * This method does not insert spaces if there is none in the original string. + */ + apply(host: Host) { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos); + + return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); + }); + } +} + +/** + * Will remove text from the source code. + */ +export class RemoveChange implements Change { + + order: number; + description: string; + + constructor(public path: string, private pos: number, private toRemove: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Removed ${toRemove} into position ${pos} of ${path}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.toRemove.length); + + // TODO: throw error if toRemove doesn't match removed string. + return host.write(this.path, `${prefix}${suffix}`); + }); + } +} + +/** + * Will replace text from the source code. + */ +export class ReplaceChange implements Change { + order: number; + description: string; + + constructor(public path: string, private pos: number, private oldText: string, + private newText: string) { + if (pos < 0) { + throw new Error('Negative positions are invalid'); + } + this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; + this.order = pos; + } + + apply(host: Host): Promise { + return host.read(this.path).then(content => { + const prefix = content.substring(0, this.pos); + const suffix = content.substring(this.pos + this.oldText.length); + const text = content.substring(this.pos, this.pos + this.oldText.length); + + if (text !== this.oldText) { + return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`)); + } + + // TODO: throw error if oldText doesn't match removed string. + return host.write(this.path, `${prefix}${this.newText}${suffix}`); + }); + } +} diff --git a/schematics/utils/devkit-utils/component.ts b/schematics/utils/devkit-utils/component.ts new file mode 100644 index 000000000000..05294da0cbde --- /dev/null +++ b/schematics/utils/devkit-utils/component.ts @@ -0,0 +1,126 @@ +import { normalize } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + apply, + branchAndMerge, + chain, + filter, + mergeWith, + move, + noop, + template, + url, +} from '@angular-devkit/schematics'; +import 'rxjs/add/operator/merge'; +import * as ts from 'typescript'; +import * as stringUtils from '@schematics/angular/strings'; +import { addDeclarationToModule, addExportToModule } from './ast-utils'; +import { InsertChange } from './change'; +import { buildRelativePath, findModuleFromOptions } from './find-module'; + + +function addDeclarationToNgModule(options: any): Rule { + return (host: Tree) => { + if (options.skipImport || !options.module) { + return host; + } + + const modulePath = options.module; + const text = host.read(modulePath); + if (text === null) { + throw new SchematicsException(`File ${modulePath} does not exist.`); + } + const sourceText = text.toString('utf-8'); + const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); + + const componentPath = `/${options.sourceDir}/${options.path}/` + + (options.flat ? '' : stringUtils.dasherize(options.name) + '/') + + stringUtils.dasherize(options.name) + + '.component'; + const relativePath = buildRelativePath(modulePath, componentPath); + const classifiedName = stringUtils.classify(`${options.name}Component`); + const declarationChanges = addDeclarationToModule(source, + modulePath, + classifiedName, + relativePath); + + const declarationRecorder = host.beginUpdate(modulePath); + for (const change of declarationChanges) { + if (change instanceof InsertChange) { + declarationRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(declarationRecorder); + + if (options.export) { + // Need to refresh the AST because we overwrote the file in the host. + const text = host.read(modulePath); + if (text === null) { + throw new SchematicsException(`File ${modulePath} does not exist.`); + } + const sourceText = text.toString('utf-8'); + const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); + + const exportRecorder = host.beginUpdate(modulePath); + const exportChanges = addExportToModule(source, modulePath, + stringUtils.classify(`${options.name}Component`), + relativePath); + + for (const change of exportChanges) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(exportRecorder); + } + + + return host; + }; +} + + +function buildSelector(options: any) { + let selector = stringUtils.dasherize(options.name); + if (options.prefix) { + selector = `${options.prefix}-${selector}`; + } + + return selector; +} + + +export function buildComponent(options: any): Rule { + const sourceDir = options.sourceDir; + if (!sourceDir) { + throw new SchematicsException(`sourceDir option is required.`); + } + + return (host: Tree, context: SchematicContext) => { + options.selector = options.selector || buildSelector(options); + options.path = options.path ? normalize(options.path) : options.path; + options.module = findModuleFromOptions(host, options); + + const templateSource = apply(url('./files'), [ + options.spec ? noop() : filter(path => !path.endsWith('.spec.ts')), + options.inlineStyle ? filter(path => !path.endsWith('.__styleext__')) : noop(), + options.inlineTemplate ? filter(path => !path.endsWith('.html')) : noop(), + template({ + ...stringUtils, + 'if-flat': (s: string) => options.flat ? '' : s, + ...options, + }), + move(sourceDir), + ]); + + return chain([ + branchAndMerge(chain([ + addDeclarationToNgModule(options), + mergeWith(templateSource), + ])), + ])(host, context); + }; +} diff --git a/schematics/utils/devkit-utils/config.ts b/schematics/utils/devkit-utils/config.ts new file mode 100755 index 000000000000..f80490dcd711 --- /dev/null +++ b/schematics/utils/devkit-utils/config.ts @@ -0,0 +1,465 @@ +/** + * @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 + */ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; + + +// The interfaces below are generated from the Angular CLI configuration schema +// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json +export interface AppConfig { + /** + * Name of the app. + */ + name?: string; + /** + * Directory where app files are placed. + */ + appRoot?: string; + /** + * The root directory of the app. + */ + root?: string; + /** + * The output directory for build results. + */ + outDir?: string; + /** + * List of application assets. + */ + assets?: (string | { + /** + * The pattern to match. + */ + glob?: string; + /** + * The dir to search within. + */ + input?: string; + /** + * The output path (relative to the outDir). + */ + output?: string; + })[]; + /** + * URL where files will be deployed. + */ + deployUrl?: string; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The runtime platform of the app. + */ + platform?: ('browser' | 'server'); + /** + * The name of the start HTML file. + */ + index?: string; + /** + * The name of the main entry-point file. + */ + main?: string; + /** + * The name of the polyfills file. + */ + polyfills?: string; + /** + * The name of the test entry-point file. + */ + test?: string; + /** + * The name of the TypeScript configuration file. + */ + tsconfig?: string; + /** + * The name of the TypeScript configuration file for unit tests. + */ + testTsconfig?: string; + /** + * The prefix to apply to generated selectors. + */ + prefix?: string; + /** + * Experimental support for a service worker from @angular/service-worker. + */ + serviceWorker?: boolean; + /** + * Global styles to be included in the build. + */ + styles?: (string | { + input?: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: { + /** + * Paths to include. Paths will be resolved to project root. + */ + includePaths?: string[]; + }; + /** + * Global scripts to be included in the build. + */ + scripts?: (string | { + input: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Source file for environment config. + */ + environmentSource?: string; + /** + * Name and corresponding file for environment config. + */ + environments?: { + [name: string]: any; // tslint:disable-line:no-any + }; + appShell?: { + app: string; + route: string; + }; +} + +export interface CliConfig { + $schema?: string; + /** + * The global configuration of the project. + */ + project?: { + /** + * The name of the project. + */ + name?: string; + /** + * Whether or not this project was ejected. + */ + ejected?: boolean; + }; + /** + * Properties of the different applications in this project. + */ + apps?: AppConfig[]; + /** + * Configuration for end-to-end tests. + */ + e2e?: { + protractor?: { + /** + * Path to the config file. + */ + config?: string; + }; + }; + /** + * Properties to be passed to TSLint. + */ + lint?: { + /** + * File glob(s) to lint. + */ + files?: (string | string[]); + /** + * Location of the tsconfig.json project file. + * Will also use as files to lint if 'files' property not present. + */ + project: string; + /** + * Location of the tslint.json configuration. + */ + tslintConfig?: string; + /** + * File glob(s) to ignore. + */ + exclude?: (string | string[]); + }[]; + /** + * Configuration for unit tests. + */ + test?: { + karma?: { + /** + * Path to the karma config file. + */ + config?: string; + }; + codeCoverage?: { + /** + * Globs to exclude from code coverage. + */ + exclude?: string[]; + }; + }; + /** + * Specify the default values for generating. + */ + defaults?: { + /** + * The file extension to be used for style files. + */ + styleExt?: string; + /** + * How often to check for file updates. + */ + poll?: number; + /** + * Use lint to fix files after generation + */ + lintFix?: boolean; + /** + * Options for generating a class. + */ + class?: { + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a component. + */ + component?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + /** + * Specifies if the style will be in the ts file. + */ + inlineStyle?: boolean; + /** + * Specifies if the template will be in the ts file. + */ + inlineTemplate?: boolean; + /** + * Specifies the view encapsulation strategy. + */ + viewEncapsulation?: ('Emulated' | 'Native' | 'None'); + /** + * Specifies the change detection strategy. + */ + changeDetection?: ('Default' | 'OnPush'); + }; + /** + * Options for generating a directive. + */ + directive?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a guard. + */ + guard?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating an interface. + */ + interface?: { + /** + * Prefix to apply to interface names. (i.e. I) + */ + prefix?: string; + }; + /** + * Options for generating a module. + */ + module?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a pipe. + */ + pipe?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a service. + */ + service?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Properties to be passed to the build command. + */ + build?: { + /** + * Output sourcemaps. + */ + sourcemaps?: boolean; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The ssl key used by the server. + */ + progress?: boolean; + /** + * Enable and define the file watching poll time period (milliseconds). + */ + poll?: number; + /** + * Delete output path before build. + */ + deleteOutputPath?: boolean; + /** + * Do not use the real path when resolving modules. + */ + preserveSymlinks?: boolean; + /** + * Show circular dependency warnings on builds. + */ + showCircularDependencies?: boolean; + /** + * Use a separate bundle containing code used across multiple bundles. + */ + commonChunk?: boolean; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + }; + /** + * Properties to be passed to the serve command. + */ + serve?: { + /** + * The port the application will be served on. + */ + port?: number; + /** + * The host the application will be served on. + */ + host?: string; + /** + * Enables ssl for the application. + */ + ssl?: boolean; + /** + * The ssl key used by the server. + */ + sslKey?: string; + /** + * The ssl certificate used by the server. + */ + sslCert?: string; + /** + * Proxy configuration file. + */ + proxyConfig?: string; + }; + /** + * Properties about schematics. + */ + schematics?: { + /** + * The schematics collection to use. + */ + collection?: string; + /** + * The new app schematic. + */ + newApp?: string; + }; + }; + /** + * Specify which package manager tool to use. + */ + packageManager?: ('npm' | 'cnpm' | 'yarn' | 'default'); + /** + * Allow people to disable console warnings. + */ + warnings?: { + /** + * Show a warning when the user enabled the --hmr option. + */ + hmrWarning?: boolean; + /** + * Show a warning when the node version is incompatible. + */ + nodeDeprecation?: boolean; + /** + * Show a warning when the user installed angular-cli. + */ + packageDeprecation?: boolean; + /** + * Show a warning when the global version is newer than the local one. + */ + versionMismatch?: boolean; + /** + * Show a warning when the TypeScript version is incompatible + */ + typescriptMismatch?: boolean; + }; +} + +export const configPath = '/.angular-cli.json'; + +export function getConfig(host: Tree): CliConfig { + const configBuffer = host.read(configPath); + if (configBuffer === null) { + throw new SchematicsException('Could not find .angular-cli.json'); + } + + const config = JSON.parse(configBuffer.toString()); + + return config; +} + +export function getAppFromConfig(config: CliConfig, appIndexOrName: string): AppConfig | null { + if (!config.apps) { + return null; + } + + if (parseInt(appIndexOrName) >= 0) { + return config.apps[parseInt(appIndexOrName)]; + } + + return config.apps.filter((app) => app.name === appIndexOrName)[0]; +} diff --git a/schematics/utils/devkit-utils/find-module.ts b/schematics/utils/devkit-utils/find-module.ts new file mode 100755 index 000000000000..ce56226d582c --- /dev/null +++ b/schematics/utils/devkit-utils/find-module.ts @@ -0,0 +1,111 @@ +/** + * @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 + */ +import { Path, join, normalize, relative } from '@angular-devkit/core'; +import { DirEntry, Tree } from '@angular-devkit/schematics'; +import { dasherize } from '@schematics/angular/strings'; + + +export interface ModuleOptions { + module?: string; + name: string; + flat?: boolean; + sourceDir?: string; + path?: string; + skipImport?: boolean; + appRoot?: string; +} + + +/** + * Find the module referred by a set of options passed to the schematics. + */ +export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path | undefined { + if (options.hasOwnProperty('skipImport') && options.skipImport) { + return undefined; + } + + if (!options.module) { + const pathToCheck = (options.sourceDir || '') + '/' + (options.path || '') + + (options.flat ? '' : '/' + dasherize(options.name)); + + return normalize(findModule(host, pathToCheck)); + } else { + const modulePath = normalize( + '/' + options.sourceDir + '/' + (options.appRoot || options.path) + '/' + options.module); + const moduleBaseName = normalize(modulePath).split('/').pop(); + + if (host.exists(modulePath)) { + return normalize(modulePath); + } else if (host.exists(modulePath + '.ts')) { + return normalize(modulePath + '.ts'); + } else if (host.exists(modulePath + '.module.ts')) { + return normalize(modulePath + '.module.ts'); + } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) { + return normalize(modulePath + '/' + moduleBaseName + '.module.ts'); + } else { + throw new Error('Specified module does not exist'); + } + } +} + +/** + * Function to find the "closest" module to a generated file's path. + */ +export function findModule(host: Tree, generateDir: string): Path { + let dir: DirEntry | null = host.getDir('/' + generateDir); + + const moduleRe = /\.module\.ts$/; + const routingModuleRe = /-routing\.module\.ts/; + + while (dir) { + const matches = dir.subfiles.filter(p => moduleRe.test(p) && !routingModuleRe.test(p)); + + if (matches.length == 1) { + return join(dir.path, matches[0]); + } else if (matches.length > 1) { + throw new Error('More than one module matches. Use skip-import option to skip importing ' + + 'the component into the closest module.'); + } + + dir = dir.parent; + } + + throw new Error('Could not find an NgModule for the new component. Use the skip-import ' + + 'option to skip importing components in NgModule.'); +} + +/** + * Build a relative path from one file path to another file path. + */ +export function buildRelativePath(from: string, to: string): string { + from = normalize(from); + to = normalize(to); + + // Convert to arrays. + const fromParts = from.split('/'); + const toParts = to.split('/'); + + // Remove file names (preserving destination) + fromParts.pop(); + const toFileName = toParts.pop(); + + const relativePath = relative(normalize(fromParts.join('/')), normalize(toParts.join('/'))); + let pathPrefix = ''; + + // Set the path prefix for same dir or child dir, parent dir starts with `..` + if (!relativePath) { + pathPrefix = '.'; + } else if (!relativePath.startsWith('.')) { + pathPrefix = `./`; + } + if (pathPrefix && !pathPrefix.endsWith('/')) { + pathPrefix += '/'; + } + + return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; +} diff --git a/schematics/utils/devkit-utils/ng-ast-utils.ts b/schematics/utils/devkit-utils/ng-ast-utils.ts new file mode 100755 index 000000000000..208ec6616485 --- /dev/null +++ b/schematics/utils/devkit-utils/ng-ast-utils.ts @@ -0,0 +1,84 @@ +/** + * @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 + */ +import { normalize } from '@angular-devkit/core'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { findNode, getSourceNodes } from './ast-utils'; +import { AppConfig } from './config'; + +export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { + const mainBuffer = host.read(mainPath); + if (!mainBuffer) { + throw new SchematicsException(`Main file (${mainPath}) not found`); + } + const mainText = mainBuffer.toString('utf-8'); + const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); + + const allNodes = getSourceNodes(source); + + let bootstrapCall: ts.CallExpression | null = null; + + for (const node of allNodes) { + + let bootstrapCallNode: ts.Node | null = null; + bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, 'bootstrapModule'); + + // Walk up the parent until CallExpression is found. + while (bootstrapCallNode && bootstrapCallNode.parent + && bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression) { + + bootstrapCallNode = bootstrapCallNode.parent; + } + + if (bootstrapCallNode !== null && + bootstrapCallNode.parent !== undefined && + bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression) { + bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; + break; + } + } + + return bootstrapCall; +} + +export function findBootstrapModulePath(host: Tree, mainPath: string): string { + const bootstrapCall = findBootstrapModuleCall(host, mainPath); + if (!bootstrapCall) { + throw new SchematicsException('Bootstrap call not found'); + } + + const bootstrapModule = bootstrapCall.arguments[0]; + + const mainBuffer = host.read(mainPath); + if (!mainBuffer) { + throw new SchematicsException(`Client app main file (${mainPath}) not found`); + } + const mainText = mainBuffer.toString('utf-8'); + const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); + const allNodes = getSourceNodes(source); + const bootstrapModuleRelativePath = allNodes + .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) + .filter(imp => { + return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); + }) + .map((imp: ts.ImportDeclaration) => { + const modulePathStringLiteral = imp.moduleSpecifier; + + return modulePathStringLiteral.text; + })[0]; + + return bootstrapModuleRelativePath; +} + +export function getAppModulePath(host: Tree, app: AppConfig) { + const mainPath = normalize(`/${app.root}/${app.main}`); + const moduleRelativePath = findBootstrapModulePath(host, mainPath); + const modulePath = normalize(`/${app.root}/${moduleRelativePath}.ts`); + + return modulePath; +} diff --git a/schematics/utils/devkit-utils/route-utils.ts b/schematics/utils/devkit-utils/route-utils.ts new file mode 100755 index 000000000000..67e7e4974d9d --- /dev/null +++ b/schematics/utils/devkit-utils/route-utils.ts @@ -0,0 +1,90 @@ +/** + * @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 + */ +import * as ts from 'typescript'; +import { findNodes, insertAfterLastOccurrence } from './ast-utils'; +import { Change, NoopChange } from './change'; + + +/** +* Add Import `import { symbolName } from fileName` if the import doesn't exit +* already. Assumes fileToEdit can be resolved and accessed. +* @param fileToEdit (file we want to add import to) +* @param symbolName (item to import) +* @param fileName (path to the file) +* @param isDefault (if true, import follows style for importing default exports) +* @return Change +*/ + +export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolName: string, + fileName: string, isDefault = false): Change { + const rootNode = source; + const allImports = 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 as ts.StringLiteral).text); + + return importFiles.filter(file => file === fileName).length === 1; + }); + + if (relevantImports.length > 0) { + let importsAsterisk = false; + // imports from import file + const imports: ts.Node[] = []; + relevantImports.forEach(n => { + Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier)); + if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { + importsAsterisk = true; + } + }); + + // if imports * from fileName, don't add symbolName + if (importsAsterisk) { + return new NoopChange(); + } + + const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName); + + // insert import if it's not there + if (importTextNodes.length === 0) { + const fallbackPos = + findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() || + findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); + + return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); + } + + return new NoopChange(); + } + + // no such import declaration exists + const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral) + .filter((n: ts.StringLiteral) => 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 insertAfterLastOccurrence( + allImports, + toInsert, + fileToEdit, + fallbackPos, + ts.SyntaxKind.StringLiteral, + ); +} diff --git a/schematics/utils/html.ts b/schematics/utils/html.ts new file mode 100644 index 000000000000..b1d0e0694c2a --- /dev/null +++ b/schematics/utils/html.ts @@ -0,0 +1,56 @@ +import { Tree, SchematicsException } from '@angular-devkit/schematics'; +import * as parse5 from 'parse5'; +import { getIndexPath } from './ast'; +import { InsertChange } from './devkit-utils/change'; + +/** + * Parses the index.html file to get the HEAD tag position. + */ +export function getHeadTag(host: Tree, src: string) { + const document = parse5.parse(src, + { locationInfo: true }) as parse5.AST.Default.Document; + + let head; + const visit = (nodes: parse5.AST.Default.Node[]) => { + nodes.forEach(node => { + const element = node; + if (element.tagName === 'head') { + head = element; + } else { + if (element.childNodes) { + visit(element.childNodes); + } + } + }); + }; + + visit(document.childNodes); + + if (!head) { + throw new SchematicsException('Head element not found!'); + } + + return { + position: head.__location.startTag.endOffset + }; +} + +/** + * Adds a link to the index.html head tag + */ +export function addHeadLink(host: Tree, link: string) { + const indexPath = getIndexPath(host); + const buffer = host.read(indexPath); + if (!buffer) { + throw new SchematicsException(`Could not find file for path: ${indexPath}`); + } + + const src = buffer.toString(); + if (src.indexOf(link) === -1) { + const node = getHeadTag(host, src); + const chng = new InsertChange(indexPath, node.position, link); + const recorder = host.beginUpdate(indexPath); + recorder.insertLeft(chng.pos, chng.toAdd); + host.commitUpdate(recorder); + } +} diff --git a/schematics/utils/lib-versions.ts b/schematics/utils/lib-versions.ts new file mode 100644 index 000000000000..5c1a6ddf7a62 --- /dev/null +++ b/schematics/utils/lib-versions.ts @@ -0,0 +1,3 @@ +export const materialVersion = '^5.0.1'; +export const cdkVersion = '^5.0.1'; +export const angularVersion = '5.0.1'; diff --git a/schematics/utils/package.ts b/schematics/utils/package.ts new file mode 100644 index 000000000000..31802113e55d --- /dev/null +++ b/schematics/utils/package.ts @@ -0,0 +1,22 @@ +import { Tree } from '@angular-devkit/schematics'; + +/** + * Adds a package to the package.json + */ +export function addPackageToPackageJson(host: Tree, type: string, pkg: string, version: string) { + if (host.exists('package.json')) { + const sourceText = host.read('package.json')!.toString('utf-8'); + const json = JSON.parse(sourceText); + if (!json[type]) { + json[type] = {}; + } + + if (!json[type][pkg]) { + json[type][pkg] = version; + } + + host.overwrite('package.json', JSON.stringify(json, null, 2)); + } + + return host; +} \ No newline at end of file diff --git a/schematics/utils/testing.ts b/schematics/utils/testing.ts new file mode 100644 index 000000000000..58de51be718a --- /dev/null +++ b/schematics/utils/testing.ts @@ -0,0 +1,25 @@ +import * as path from 'path'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; + +const collectionPath = path.join('./node_modules/@schematics/angular/collection.json'); + +/** + * Create a base app used for testing. + */ +export function baseApp() { + const baseRunner = new SchematicTestRunner('schematics', collectionPath); + return baseRunner.runSchematic('application', { + directory: '', + name: 'app', + prefix: 'app', + sourceDir: 'src', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'None', + version: '1.2.3', + routing: true, + style: 'css', + skipTests: false, + minimal: false, + }); +} \ No newline at end of file From 418e049b58bd3f4c6da6821c3586bb5789b101d3 Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 18 Jan 2018 17:23:05 -0600 Subject: [PATCH 2/5] chore: nit --- schematics/collection.json | 15 +-------------- schematics/utils/ast.ts | 21 +++++++++++---------- schematics/utils/html.ts | 6 +++--- schematics/utils/lib-versions.ts | 6 +++--- schematics/utils/package.ts | 2 +- schematics/utils/testing.ts | 8 ++++---- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/schematics/collection.json b/schematics/collection.json index a3f3ab4d4605..7768476c5e4c 100644 --- a/schematics/collection.json +++ b/schematics/collection.json @@ -1,18 +1,5 @@ -// By default, collection.json is a Loose-format JSON5 format, which means it's loaded using a -// special loader and you can use comments, as well as single quotes or no-quotes for standard -// JavaScript identifiers. -// Note that this is only true for collection.json and it depends on the tooling itself. -// We read package.json using a require() call, which is standard JSON. +// This is the root config file where the schematics are defined. { - // This is just to indicate to your IDE that there is a schema for collection.json. "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", - - // Schematics are listed as a map of schematicName => schematicDescription. - // Each description contains a description field which is required, a factory reference, - // an extends field and a schema reference. - // The extends field points to another schematic (either in the same collection or a - // separate collection using the format collectionName:schematicName). - // The factory is required, except when using the extends field. The the factory can - // overwrite the extended schematic factory. "schematics": {} } diff --git a/schematics/utils/ast.ts b/schematics/utils/ast.ts index cdaf19ce1641..1b55b21418c6 100644 --- a/schematics/utils/ast.ts +++ b/schematics/utils/ast.ts @@ -1,11 +1,11 @@ -import { SchematicsException } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics'; +import {SchematicsException} from '@angular-devkit/schematics'; +import {Tree} from '@angular-devkit/schematics'; import * as ts from 'typescript'; -import { addImportToModule } from './devkit-utils/ast-utils'; -import { getAppModulePath } from './devkit-utils/ng-ast-utils'; -import { InsertChange } from './devkit-utils/change'; -import { getConfig, getAppFromConfig } from './devkit-utils/config'; -import { normalize } from '@angular-devkit/core'; +import {addImportToModule} from './devkit-utils/ast-utils'; +import {getAppModulePath} from './devkit-utils/ng-ast-utils'; +import {InsertChange} from './devkit-utils/change'; +import {getConfig, getAppFromConfig} from './devkit-utils/config'; +import {normalize} from '@angular-devkit/core'; /** * Reads file given path and returns TypeScript source file. @@ -27,13 +27,14 @@ export function addToRootModule(host: Tree, moduleName: string, src: string) { const config = getConfig(host); const app = getAppFromConfig(config, '0'); const modulePath = getAppModulePath(host, app); - addToModule(host, modulePath, moduleName, src); + addModuleImportToModule(host, modulePath, moduleName, src); } /** * Import and add module to specific module path. */ -export function addToModule(host: Tree, modulePath: string, moduleName: string, src: string) { +export function addModuleImportToModule( + host: Tree, modulePath: string, moduleName: string, src: string) { const moduleSource = getSourceFile(host, modulePath); const changes = addImportToModule(moduleSource, modulePath, moduleName, src); const recorder = host.beginUpdate(modulePath); @@ -50,7 +51,7 @@ export function addToModule(host: Tree, modulePath: string, moduleName: string, /** * Gets the app index.html file */ -export function getIndexPath(host: Tree) { +export function getIndexHtmlPath(host: Tree) { const config = getConfig(host); const app = getAppFromConfig(config, '0'); return normalize(`/${app.root}/${app.index}`); diff --git a/schematics/utils/html.ts b/schematics/utils/html.ts index b1d0e0694c2a..904423b7cfa5 100644 --- a/schematics/utils/html.ts +++ b/schematics/utils/html.ts @@ -1,7 +1,7 @@ -import { Tree, SchematicsException } from '@angular-devkit/schematics'; +import {Tree, SchematicsException} from '@angular-devkit/schematics'; import * as parse5 from 'parse5'; -import { getIndexPath } from './ast'; -import { InsertChange } from './devkit-utils/change'; +import {getIndexPath} from './ast'; +import {InsertChange} from './devkit-utils/change'; /** * Parses the index.html file to get the HEAD tag position. diff --git a/schematics/utils/lib-versions.ts b/schematics/utils/lib-versions.ts index 5c1a6ddf7a62..5046b92e16d7 100644 --- a/schematics/utils/lib-versions.ts +++ b/schematics/utils/lib-versions.ts @@ -1,3 +1,3 @@ -export const materialVersion = '^5.0.1'; -export const cdkVersion = '^5.0.1'; -export const angularVersion = '5.0.1'; +export const materialVersion = '^5.0.0'; +export const cdkVersion = '^5.0.0'; +export const angularVersion = '5.0.0'; diff --git a/schematics/utils/package.ts b/schematics/utils/package.ts index 31802113e55d..cb3b9a33b0d6 100644 --- a/schematics/utils/package.ts +++ b/schematics/utils/package.ts @@ -1,4 +1,4 @@ -import { Tree } from '@angular-devkit/schematics'; +import {Tree} from '@angular-devkit/schematics'; /** * Adds a package to the package.json diff --git a/schematics/utils/testing.ts b/schematics/utils/testing.ts index 58de51be718a..34b4378ef47e 100644 --- a/schematics/utils/testing.ts +++ b/schematics/utils/testing.ts @@ -1,12 +1,12 @@ -import * as path from 'path'; -import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import {join} from 'path'; +import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; -const collectionPath = path.join('./node_modules/@schematics/angular/collection.json'); +const collectionPath = join('./node_modules/@schematics/angular/collection.json'); /** * Create a base app used for testing. */ -export function baseApp() { +export function createTestApp() { const baseRunner = new SchematicTestRunner('schematics', collectionPath); return baseRunner.runSchematic('application', { directory: '', From ca0f8212483227dfa718ed15239ab3be93ed99da Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 21 Jan 2018 11:01:14 -0600 Subject: [PATCH 3/5] chore: pr feedback --- schematics/utils/ast.ts | 6 +++++- schematics/utils/html.ts | 13 +++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/schematics/utils/ast.ts b/schematics/utils/ast.ts index 1b55b21418c6..741e134b315b 100644 --- a/schematics/utils/ast.ts +++ b/schematics/utils/ast.ts @@ -23,7 +23,7 @@ export function getSourceFile(host: Tree, path: string): ts.SourceFile { /** * Import and add module to root app module. */ -export function addToRootModule(host: Tree, moduleName: string, src: string) { +export function addModuleImportToRootModule(host: Tree, moduleName: string, src: string) { const config = getConfig(host); const app = getAppFromConfig(config, '0'); const modulePath = getAppModulePath(host, app); @@ -32,6 +32,10 @@ export function addToRootModule(host: Tree, moduleName: string, src: string) { /** * Import and add module to specific module path. + * @param host the tree we are updating + * @param modulePath src location of the module + * @param moduleName name of module to import + * @param src src location to import */ export function addModuleImportToModule( host: Tree, modulePath: string, moduleName: string, src: string) { diff --git a/schematics/utils/html.ts b/schematics/utils/html.ts index 904423b7cfa5..f7b321f9aede 100644 --- a/schematics/utils/html.ts +++ b/schematics/utils/html.ts @@ -5,10 +5,12 @@ import {InsertChange} from './devkit-utils/change'; /** * Parses the index.html file to get the HEAD tag position. + * @param host the tree we are traversing + * @param src the src path of the html file to parse */ export function getHeadTag(host: Tree, src: string) { const document = parse5.parse(src, - { locationInfo: true }) as parse5.AST.Default.Document; + {locationInfo: true}) as parse5.AST.Default.Document; let head; const visit = (nodes: parse5.AST.Default.Node[]) => { @@ -36,7 +38,10 @@ export function getHeadTag(host: Tree, src: string) { } /** - * Adds a link to the index.html head tag + * Adds a link to the index.html head tag Example: + * `` + * @param host the tree we are updating + * @param link html element string we are inserting. */ export function addHeadLink(host: Tree, link: string) { const indexPath = getIndexPath(host); @@ -48,9 +53,9 @@ export function addHeadLink(host: Tree, link: string) { const src = buffer.toString(); if (src.indexOf(link) === -1) { const node = getHeadTag(host, src); - const chng = new InsertChange(indexPath, node.position, link); + const insertion = new InsertChange(indexPath, node.position, link); const recorder = host.beginUpdate(indexPath); - recorder.insertLeft(chng.pos, chng.toAdd); + recorder.insertLeft(insertion.pos, insertion.toAdd); host.commitUpdate(recorder); } } From 2d2d56231dfb484cffa7e582fcc9a7946e340a49 Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 26 Jan 2018 06:46:27 -0600 Subject: [PATCH 4/5] chore: pr feedback --- schematics/utils/ast.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schematics/utils/ast.ts b/schematics/utils/ast.ts index 741e134b315b..7625181eaa9e 100644 --- a/schematics/utils/ast.ts +++ b/schematics/utils/ast.ts @@ -33,7 +33,7 @@ export function addModuleImportToRootModule(host: Tree, moduleName: string, src: /** * Import and add module to specific module path. * @param host the tree we are updating - * @param modulePath src location of the module + * @param modulePath src location of the module to import * @param moduleName name of module to import * @param src src location to import */ From 5f000ea2be064491af4b5aa9b8013141b613fae1 Mon Sep 17 00:00:00 2001 From: Austin Date: Sun, 4 Feb 2018 09:39:06 -0600 Subject: [PATCH 5/5] chore: add code owner --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e027a0978d6..972cf5ef68f2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -170,6 +170,9 @@ /src/e2e-app/slide-toggle/** @devversion /src/e2e-app/tabs/** @andrewseguin +# Schematics +/schematics/** @amcdnl + # Universal app /src/universal-app/** @jelbourn