mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(schematics): add initial schematics utils (#9451)
* feat(schematics): add initial schematics utils * chore: nit * chore: pr feedback * chore: pr feedback * chore: add code owner
This commit is contained in:
@@ -172,6 +172,9 @@
|
||||
/src/e2e-app/slide-toggle/** @devversion
|
||||
/src/e2e-app/tabs/** @andrewseguin
|
||||
|
||||
# Schematics
|
||||
/schematics/** @amcdnl
|
||||
|
||||
# Universal app
|
||||
/src/universal-app/** @jelbourn
|
||||
|
||||
|
||||
@@ -38,3 +38,7 @@ npm-debug.log
|
||||
testem.log
|
||||
/.chrome
|
||||
/.git
|
||||
|
||||
# schematics
|
||||
/schematics/**/*.js
|
||||
/schematics/**/*.map
|
||||
@@ -0,0 +1,5 @@
|
||||
// This is the root config file where the schematics are defined.
|
||||
{
|
||||
"$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
|
||||
"schematics": {}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 addModuleImportToRootModule(host: Tree, moduleName: string, src: string) {
|
||||
const config = getConfig(host);
|
||||
const app = getAppFromConfig(config, '0');
|
||||
const modulePath = getAppModulePath(host, app);
|
||||
addModuleImportToModule(host, modulePath, moduleName, src);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import and add module to specific module path.
|
||||
* @param host the tree we are updating
|
||||
* @param modulePath src location of the module to import
|
||||
* @param moduleName name of module to import
|
||||
* @param src src location to import
|
||||
*/
|
||||
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);
|
||||
|
||||
changes.forEach((change) => {
|
||||
if (change instanceof InsertChange) {
|
||||
recorder.insertLeft(change.pos, change.toAdd);
|
||||
}
|
||||
});
|
||||
|
||||
host.commitUpdate(recorder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the app index.html file
|
||||
*/
|
||||
export function getIndexHtmlPath(host: Tree) {
|
||||
const config = getConfig(host);
|
||||
const app = getAppFromConfig(config, '0');
|
||||
return normalize(`/${app.root}/${app.index}`);
|
||||
}
|
||||
@@ -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).
|
||||
Executable
+448
@@ -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<ts.Node>} 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<ts.Node>;
|
||||
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 (<ts.StringLiteral> 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;
|
||||
}
|
||||
Executable
+127
@@ -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<void>;
|
||||
read(path: string): Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
export interface Change {
|
||||
apply(host: Host): Promise<void>;
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
Executable
+465
@@ -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];
|
||||
}
|
||||
Executable
+111
@@ -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;
|
||||
}
|
||||
+84
@@ -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 = <ts.StringLiteral> 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;
|
||||
}
|
||||
Executable
+90
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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.
|
||||
* @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;
|
||||
|
||||
let head;
|
||||
const visit = (nodes: parse5.AST.Default.Node[]) => {
|
||||
nodes.forEach(node => {
|
||||
const element = <parse5.AST.Default.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 Example:
|
||||
* `<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">`
|
||||
* @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);
|
||||
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 insertion = new InsertChange(indexPath, node.position, link);
|
||||
const recorder = host.beginUpdate(indexPath);
|
||||
recorder.insertLeft(insertion.pos, insertion.toAdd);
|
||||
host.commitUpdate(recorder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const materialVersion = '^5.0.0';
|
||||
export const cdkVersion = '^5.0.0';
|
||||
export const angularVersion = '5.0.0';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {join} from 'path';
|
||||
import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
|
||||
|
||||
const collectionPath = join('./node_modules/@schematics/angular/collection.json');
|
||||
|
||||
/**
|
||||
* Create a base app used for testing.
|
||||
*/
|
||||
export function createTestApp() {
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user