mirror of https://github.com/abpframework/abp.git
20 changed files with 2605 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||
**DISCLAIMER** |
|||
|
|||
This directory is a direct copy of https://github.com/angular/angular-cli/tree/master/packages/schematics/angular/utility and is used under terms and permissions by the MIT license granted by Google, Inc. |
|||
|
|||
All credits go to Angular team for building these utilities. |
|||
@ -0,0 +1,753 @@ |
|||
/** |
|||
* @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, 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(ts.isStringLiteral) |
|||
.map(n => n.text); |
|||
|
|||
return importFiles.filter(file => file === fileName).length === 1; |
|||
}); |
|||
|
|||
if (relevantImports.length > 0) { |
|||
let importsAsterisk = false; |
|||
// imports from import file
|
|||
const imports: 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.isStringLiteral) |
|||
.filter((n) => n.text === 'use strict'); |
|||
let fallbackPos = 0; |
|||
if (useStrict.length > 0) { |
|||
fallbackPos = useStrict[0].end; |
|||
} |
|||
const open = isDefault ? '' : '{ '; |
|||
const close = isDefault ? '' : ' }'; |
|||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
|||
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; |
|||
const separator = insertAtBeginning ? '' : ';\n'; |
|||
const toInsert = `${separator}import ${open}${symbolName}${close}` + |
|||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; |
|||
|
|||
return insertAfterLastOccurrence( |
|||
allImports, |
|||
toInsert, |
|||
fileToEdit, |
|||
fallbackPos, |
|||
ts.SyntaxKind.StringLiteral, |
|||
); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 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. |
|||
* @param recursive Continue looking for nodes of kind recursive until end |
|||
* the last child even when node of kind has been found. |
|||
* @return all nodes of kind, or [] if none is found |
|||
*/ |
|||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max?: number, recursive?: boolean): ts.Node[]; |
|||
|
|||
/** |
|||
* Find all nodes from the AST in the subtree that satisfy a type guard. |
|||
* @param node |
|||
* @param guard |
|||
* @param max The maximum number of items to return. |
|||
* @param recursive Continue looking for nodes of kind recursive until end |
|||
* the last child even when node of kind has been found. |
|||
* @return all nodes that satisfy the type guard, or [] if none is found |
|||
*/ |
|||
export function findNodes<T extends ts.Node>(node: ts.Node, guard: (node: ts.Node) => node is T, max?: number, recursive?: boolean): T[]; |
|||
|
|||
export function findNodes<T extends ts.Node>( |
|||
node: ts.Node, |
|||
kindOrGuard: ts.SyntaxKind | ((node: ts.Node) => node is T), |
|||
max = Infinity, |
|||
recursive = false, |
|||
): T[] { |
|||
if (!node || max == 0) { |
|||
return []; |
|||
} |
|||
|
|||
const test = |
|||
typeof kindOrGuard === 'function' |
|||
? kindOrGuard |
|||
: (node: ts.Node): node is T => node.kind === kindOrGuard; |
|||
|
|||
const arr: T[] = []; |
|||
if (test(node)) { |
|||
arr.push(node); |
|||
max--; |
|||
} |
|||
if (max > 0 && (recursive || !test(node))) { |
|||
for (const child of node.getChildren()) { |
|||
findNodes(child, test, 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 {Array<ts.Node>} An array 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.getStart() - second.getStart(); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 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: ts.Node | undefined; |
|||
for (const node of nodes) { |
|||
if (!lastItem || lastItem.getStart() < node.getStart()) { |
|||
lastItem = node; |
|||
} |
|||
} |
|||
if (syntaxKind && lastItem) { |
|||
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.getEnd() : 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 = findNodes(source, ts.isImportDeclaration) |
|||
.map((node) => _angularImportsFromNode(node, source)) |
|||
.reduce((acc, current) => { |
|||
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.text == identifier && angularImports[id.text] === 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).text; |
|||
|
|||
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); |
|||
} |
|||
|
|||
function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration|undefined { |
|||
if (ts.isClassDeclaration(node)) { |
|||
return node; |
|||
} |
|||
|
|||
return node.parent && findClassDeclarationParent(node.parent); |
|||
} |
|||
|
|||
/** |
|||
* Given a source file with @NgModule class(es), find the name of the first @NgModule class. |
|||
* |
|||
* @param source source file containing one or more @NgModule |
|||
* @returns the name of the first @NgModule, or `undefined` if none is found |
|||
*/ |
|||
export function getFirstNgModuleName(source: ts.SourceFile): string|undefined { |
|||
// First, find the @NgModule decorators.
|
|||
const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core'); |
|||
if (ngModulesMetadata.length === 0) { |
|||
return undefined; |
|||
} |
|||
|
|||
// Then walk parent pointers up the AST, looking for the ClassDeclaration parent of the NgModule
|
|||
// metadata.
|
|||
const moduleClass = findClassDeclarationParent(ngModulesMetadata[0]); |
|||
if (!moduleClass || !moduleClass.name) { |
|||
return undefined; |
|||
} |
|||
|
|||
// Get the class name of the module ClassDeclaration.
|
|||
return moduleClass.name.text; |
|||
} |
|||
|
|||
export function getMetadataField( |
|||
node: ts.ObjectLiteralExpression, |
|||
metadataField: string, |
|||
): ts.ObjectLiteralElement[] { |
|||
return node.properties |
|||
.filter(ts.isPropertyAssignment) |
|||
// Filter out every fields that's not "metadataField". Also handles string literals
|
|||
// (but not expressions).
|
|||
.filter(({ name }) => { |
|||
return (ts.isIdentifier(name) || ts.isStringLiteral(name)) |
|||
&& name.getText() === metadataField; |
|||
}); |
|||
} |
|||
|
|||
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 = getMetadataField( |
|||
node as ts.ObjectLiteralExpression, |
|||
metadataField, |
|||
); |
|||
|
|||
// 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 && 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) { |
|||
// tslint:disable-next-line: no-console
|
|||
console.error('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 = ` ${symbolName}\n`; |
|||
} else { |
|||
// 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]}${symbolName}`; |
|||
} else { |
|||
toInsert = `, ${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); |
|||
} |
|||
|
|||
/** |
|||
* Custom function to insert an entryComponent into NgModule. It also imports it. |
|||
* @deprecated - Since version 9.0.0 with Ivy, entryComponents is no longer necessary. |
|||
*/ |
|||
export function addEntryComponentToModule(source: ts.SourceFile, |
|||
modulePath: string, classifiedName: string, |
|||
importPath: string): Change[] { |
|||
return addSymbolToNgModuleMetadata( |
|||
source, modulePath, |
|||
'entryComponents', 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(ts.isImportDeclaration) |
|||
.filter( |
|||
(imp) => ts.isStringLiteral(imp.moduleSpecifier) && imp.moduleSpecifier.text === importPath, |
|||
) |
|||
.filter((imp) => { |
|||
if (!imp.importClause) { |
|||
return false; |
|||
} |
|||
const nodes = findNodes(imp.importClause, ts.isImportSpecifier).filter( |
|||
(n) => n.getText() === classifiedName, |
|||
); |
|||
|
|||
return nodes.length > 0; |
|||
}); |
|||
|
|||
return matchingNodes.length > 0; |
|||
} |
|||
|
|||
/** |
|||
* This function returns the name of the environment export |
|||
* whether this export is aliased or not. If the environment file |
|||
* is not imported, then it will return `null`. |
|||
*/ |
|||
export function getEnvironmentExportName(source: ts.SourceFile): string | null { |
|||
// Initial value is `null` as we don't know yet if the user
|
|||
// has imported `environment` into the root module or not.
|
|||
let environmentExportName: string | null = null; |
|||
|
|||
const allNodes = getSourceNodes(source); |
|||
|
|||
allNodes |
|||
.filter(ts.isImportDeclaration) |
|||
.filter( |
|||
(declaration) => |
|||
declaration.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral && |
|||
declaration.importClause !== undefined, |
|||
) |
|||
.map((declaration) => |
|||
// If `importClause` property is defined then the first
|
|||
// child will be `NamedImports` object (or `namedBindings`).
|
|||
(declaration.importClause as ts.ImportClause).getChildAt(0), |
|||
) |
|||
// Find those `NamedImports` object that contains `environment` keyword
|
|||
// in its text. E.g. `{ environment as env }`.
|
|||
.filter(ts.isNamedImports) |
|||
.filter((namedImports) => namedImports.getText().includes('environment')) |
|||
.forEach((namedImports) => { |
|||
for (const specifier of namedImports.elements) { |
|||
// `propertyName` is defined if the specifier
|
|||
// has an aliased import.
|
|||
const name = specifier.propertyName || specifier.name; |
|||
|
|||
// Find specifier that contains `environment` keyword in its text.
|
|||
// Whether it's `environment` or `environment as env`.
|
|||
if (name.text.includes('environment')) { |
|||
environmentExportName = specifier.name.text; |
|||
} |
|||
} |
|||
}); |
|||
|
|||
return environmentExportName; |
|||
} |
|||
|
|||
/** |
|||
* Returns the RouterModule declaration from NgModule metadata, if any. |
|||
*/ |
|||
export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression | undefined { |
|||
const result = getDecoratorMetadata(source, 'NgModule', '@angular/core') as ts.Node[]; |
|||
const node = result[0] as ts.ObjectLiteralExpression; |
|||
const matchingProperties = getMetadataField(node, 'imports'); |
|||
|
|||
if (!matchingProperties) { |
|||
return; |
|||
} |
|||
|
|||
const assignment = matchingProperties[0] as ts.PropertyAssignment; |
|||
|
|||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { |
|||
return; |
|||
} |
|||
|
|||
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; |
|||
|
|||
return arrLiteral.elements |
|||
.filter(el => el.kind === ts.SyntaxKind.CallExpression) |
|||
.find(el => (el as ts.Identifier).getText().startsWith('RouterModule')); |
|||
} |
|||
|
|||
/** |
|||
* Adds a new route declaration to a router module (i.e. has a RouterModule declaration) |
|||
*/ |
|||
export function addRouteDeclarationToModule( |
|||
source: ts.SourceFile, |
|||
fileToAdd: string, |
|||
routeLiteral: string, |
|||
): Change { |
|||
const routerModuleExpr = getRouterModuleDeclaration(source); |
|||
if (!routerModuleExpr) { |
|||
throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`); |
|||
} |
|||
const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression).arguments; |
|||
if (!scopeConfigMethodArgs.length) { |
|||
const { line } = source.getLineAndCharacterOfPosition(routerModuleExpr.getStart()); |
|||
throw new Error( |
|||
`The router module method doesn't have arguments ` + |
|||
`at line ${line} in ${fileToAdd}`, |
|||
); |
|||
} |
|||
|
|||
let routesArr: ts.ArrayLiteralExpression | undefined; |
|||
const routesArg = scopeConfigMethodArgs[0]; |
|||
|
|||
// Check if the route declarations array is
|
|||
// an inlined argument of RouterModule or a standalone variable
|
|||
if (ts.isArrayLiteralExpression(routesArg)) { |
|||
routesArr = routesArg; |
|||
} else { |
|||
const routesVarName = routesArg.getText(); |
|||
let routesVar; |
|||
if (routesArg.kind === ts.SyntaxKind.Identifier) { |
|||
routesVar = source.statements |
|||
.filter(ts.isVariableStatement) |
|||
.find((v) => { |
|||
return v.declarationList.declarations[0].name.getText() === routesVarName; |
|||
}); |
|||
} |
|||
|
|||
if (!routesVar) { |
|||
const { line } = source.getLineAndCharacterOfPosition(routesArg.getStart()); |
|||
throw new Error( |
|||
`No route declaration array was found that corresponds ` + |
|||
`to router module at line ${line} in ${fileToAdd}`, |
|||
); |
|||
} |
|||
|
|||
routesArr = findNodes(routesVar, ts.SyntaxKind.ArrayLiteralExpression, 1)[0] as ts.ArrayLiteralExpression; |
|||
} |
|||
|
|||
const occurrencesCount = routesArr.elements.length; |
|||
const text = routesArr.getFullText(source); |
|||
|
|||
let route: string = routeLiteral; |
|||
let insertPos = routesArr.elements.pos; |
|||
|
|||
if (occurrencesCount > 0) { |
|||
const lastRouteLiteral = [...routesArr.elements].pop() as ts.Expression; |
|||
const lastRouteIsWildcard = ts.isObjectLiteralExpression(lastRouteLiteral) |
|||
&& lastRouteLiteral |
|||
.properties |
|||
.some(n => ( |
|||
ts.isPropertyAssignment(n) |
|||
&& ts.isIdentifier(n.name) |
|||
&& n.name.text === 'path' |
|||
&& ts.isStringLiteral(n.initializer) |
|||
&& n.initializer.text === '**' |
|||
)); |
|||
|
|||
const indentation = text.match(/\r?\n(\r?)\s*/) || []; |
|||
const routeText = `${indentation[0] || ' '}${routeLiteral}`; |
|||
|
|||
// Add the new route before the wildcard route
|
|||
// otherwise we'll always redirect to the wildcard route
|
|||
if (lastRouteIsWildcard) { |
|||
insertPos = lastRouteLiteral.pos; |
|||
route = `${routeText},`; |
|||
} else { |
|||
insertPos = lastRouteLiteral.end; |
|||
route = `,${routeText}`; |
|||
} |
|||
} |
|||
|
|||
return new InsertChange(fileToAdd, insertPos, route); |
|||
} |
|||
@ -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,532 @@ |
|||
/** |
|||
* @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 { JsonParseMode, parseJson } from '@angular-devkit/core'; |
|||
import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { ProjectType, WorkspaceProject, WorkspaceSchema } from './workspace-models'; |
|||
|
|||
// 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; |
|||
}; |
|||
budgets?: { |
|||
/** |
|||
* The type of budget |
|||
*/ |
|||
type?: ('bundle' | 'initial' | 'allScript' | 'all' | 'anyScript' | 'any' | 'anyComponentStyle'); |
|||
/** |
|||
* The name of the bundle |
|||
*/ |
|||
name?: string; |
|||
/** |
|||
* The baseline size for comparison. |
|||
*/ |
|||
baseline?: string; |
|||
/** |
|||
* The maximum threshold for warning relative to the baseline. |
|||
*/ |
|||
maximumWarning?: string; |
|||
/** |
|||
* The maximum threshold for error relative to the baseline. |
|||
*/ |
|||
maximumError?: string; |
|||
/** |
|||
* The minimum threshold for warning relative to the baseline. |
|||
*/ |
|||
minimumWarning?: string; |
|||
/** |
|||
* The minimum threshold for error relative to the baseline. |
|||
*/ |
|||
minimumError?: string; |
|||
/** |
|||
* The threshold for warning relative to the baseline (min & max). |
|||
*/ |
|||
warning?: string; |
|||
/** |
|||
* The threshold for error relative to the baseline (min & max). |
|||
*/ |
|||
error?: 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 directory 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 directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a guard. |
|||
*/ |
|||
guard?: { |
|||
/** |
|||
* Flag to indicate if a directory 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 directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a pipe. |
|||
*/ |
|||
pipe?: { |
|||
/** |
|||
* Flag to indicate if a directory is created. |
|||
*/ |
|||
flat?: boolean; |
|||
/** |
|||
* Specifies if a spec file is generated. |
|||
*/ |
|||
spec?: boolean; |
|||
}; |
|||
/** |
|||
* Options for generating a service. |
|||
*/ |
|||
service?: { |
|||
/** |
|||
* Flag to indicate if a directory 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?: { |
|||
versionMismatch?: boolean; |
|||
}; |
|||
} |
|||
|
|||
export function getWorkspacePath(host: Tree): string { |
|||
const possibleFiles = [ '/angular.json', '/.angular.json' ]; |
|||
const path = possibleFiles.filter(path => host.exists(path))[0]; |
|||
|
|||
return path; |
|||
} |
|||
|
|||
export function getWorkspaceSchema(host: Tree): WorkspaceSchema { |
|||
const path = getWorkspacePath(host); |
|||
const configBuffer = host.read(path); |
|||
if (configBuffer === null) { |
|||
throw new SchematicsException(`Could not find (${path})`); |
|||
} |
|||
const content = configBuffer.toString(); |
|||
|
|||
return parseJson(content, JsonParseMode.Loose) as {} as WorkspaceSchema; |
|||
} |
|||
|
|||
export function addProjectToWorkspace<TProjectType extends ProjectType = ProjectType.Application>( |
|||
workspace: WorkspaceSchema, |
|||
name: string, |
|||
project: WorkspaceProject<TProjectType>, |
|||
): Rule { |
|||
return (_host: Tree, _context: SchematicContext) => { |
|||
|
|||
if (workspace.projects[name]) { |
|||
throw new Error(`Project '${name}' already exists in workspace.`); |
|||
} |
|||
|
|||
// Add project to workspace.
|
|||
workspace.projects[name] = project; |
|||
|
|||
if (!workspace.defaultProject && Object.keys(workspace.projects).length === 1) { |
|||
// Make the new project the default one.
|
|||
workspace.defaultProject = name; |
|||
} |
|||
|
|||
return updateWorkspaceSchema(workspace); |
|||
}; |
|||
} |
|||
|
|||
export function updateWorkspaceSchema(workspace: WorkspaceSchema): Rule { |
|||
return (host: Tree, _context: SchematicContext) => { |
|||
host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2)); |
|||
}; |
|||
} |
|||
|
|||
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 = parseJson(configBuffer.toString(), JsonParseMode.Loose) as {} as CliConfig; |
|||
|
|||
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]; |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* @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 { Tree } from '@angular-devkit/schematics'; |
|||
import { JSONFile } from './json-file'; |
|||
|
|||
const PKG_JSON_PATH = '/package.json'; |
|||
export enum NodeDependencyType { |
|||
Default = 'dependencies', |
|||
Dev = 'devDependencies', |
|||
Peer = 'peerDependencies', |
|||
Optional = 'optionalDependencies', |
|||
} |
|||
|
|||
export interface NodeDependency { |
|||
type: NodeDependencyType; |
|||
name: string; |
|||
version: string; |
|||
overwrite?: boolean; |
|||
} |
|||
|
|||
const ALL_DEPENDENCY_TYPE = [ |
|||
NodeDependencyType.Default, |
|||
NodeDependencyType.Dev, |
|||
NodeDependencyType.Optional, |
|||
NodeDependencyType.Peer, |
|||
]; |
|||
|
|||
export function addPackageJsonDependency(tree: Tree, dependency: NodeDependency, pkgJsonPath = PKG_JSON_PATH): void { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
const { overwrite, type, name, version } = dependency; |
|||
const path = [type, name]; |
|||
if (overwrite || !json.get(path)) { |
|||
json.modify(path, version); |
|||
} |
|||
} |
|||
|
|||
export function removePackageJsonDependency(tree: Tree, name: string, pkgJsonPath = PKG_JSON_PATH): void { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
for (const depType of ALL_DEPENDENCY_TYPE) { |
|||
json.remove([depType, name]); |
|||
} |
|||
} |
|||
|
|||
export function getPackageJsonDependency(tree: Tree, name: string, pkgJsonPath = PKG_JSON_PATH): NodeDependency | null { |
|||
const json = new JSONFile(tree, pkgJsonPath); |
|||
if (json.error) { |
|||
throw json.error; |
|||
} |
|||
|
|||
for (const depType of ALL_DEPENDENCY_TYPE) { |
|||
const version = json.get([depType, name]); |
|||
|
|||
if (typeof version === 'string') { |
|||
return { |
|||
type: depType, |
|||
name: name, |
|||
version, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
/** |
|||
* @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 { |
|||
dirname, |
|||
join, |
|||
normalize, NormalizedRoot, |
|||
Path, |
|||
relative |
|||
} from '@angular-devkit/core'; |
|||
import { DirEntry, Tree } from '@angular-devkit/schematics'; |
|||
|
|||
|
|||
export interface ModuleOptions { |
|||
project?: string; // added this
|
|||
module?: string; |
|||
name: string; |
|||
flat?: boolean; |
|||
path?: string; |
|||
route?: string; // added this
|
|||
selector?: string; // added this
|
|||
skipImport?: boolean; |
|||
moduleExt?: string; |
|||
routingModuleExt?: string; |
|||
} |
|||
|
|||
export const MODULE_EXT = '.module.ts'; |
|||
export const ROUTING_MODULE_EXT = '-routing.module.ts'; |
|||
|
|||
/** |
|||
* 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; |
|||
} |
|||
|
|||
const moduleExt = options.moduleExt || MODULE_EXT; |
|||
const routingModuleExt = options.routingModuleExt || ROUTING_MODULE_EXT; |
|||
|
|||
if (!options.module) { |
|||
const pathToCheck = (options.path || '') + '/' + options.name; |
|||
|
|||
return normalize(findModule(host, pathToCheck, moduleExt, routingModuleExt)); |
|||
} else { |
|||
const modulePath = normalize(`/${options.path}/${options.module}`); |
|||
const componentPath = normalize(`/${options.path}/${options.name}`); |
|||
const moduleBaseName = normalize(modulePath).split('/').pop(); |
|||
|
|||
const candidateSet = new Set<Path>([ |
|||
normalize(options.path || '/'), |
|||
]); |
|||
|
|||
for (let dir = modulePath; dir != NormalizedRoot; dir = dirname(dir)) { |
|||
candidateSet.add(dir); |
|||
} |
|||
for (let dir = componentPath; dir != NormalizedRoot; dir = dirname(dir)) { |
|||
candidateSet.add(dir); |
|||
} |
|||
|
|||
const candidatesDirs = [...candidateSet].sort((a, b) => b.length - a.length); |
|||
for (const c of candidatesDirs) { |
|||
const candidateFiles = [ |
|||
'', |
|||
`${moduleBaseName}.ts`, |
|||
`${moduleBaseName}${moduleExt}`, |
|||
].map(x => join(c, x)); |
|||
|
|||
for (const sc of candidateFiles) { |
|||
if (host.exists(sc)) { |
|||
return normalize(sc); |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw new Error( |
|||
`Specified module '${options.module}' does not exist.\n` |
|||
+ `Looked in the following directories:\n ${candidatesDirs.join('\n ')}`, |
|||
); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Function to find the "closest" module to a generated file's path. |
|||
*/ |
|||
export function findModule(host: Tree, generateDir: string, |
|||
moduleExt = MODULE_EXT, routingModuleExt = ROUTING_MODULE_EXT): Path { |
|||
|
|||
let dir: DirEntry | null = host.getDir('/' + generateDir); |
|||
let foundRoutingModule = false; |
|||
|
|||
while (dir) { |
|||
const allMatches = dir.subfiles.filter(p => p.endsWith(moduleExt)); |
|||
const filteredMatches = allMatches.filter(p => !p.endsWith(routingModuleExt)); |
|||
|
|||
foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; |
|||
|
|||
if (filteredMatches.length == 1) { |
|||
return join(dir.path, filteredMatches[0]); |
|||
} else if (filteredMatches.length > 1) { |
|||
throw new Error( |
|||
'More than one module matches. Use the skip-import option to skip importing ' + |
|||
'the component into the closest module or use the module option to specify a module.'); |
|||
} |
|||
|
|||
dir = dir.parent; |
|||
} |
|||
|
|||
const errorMsg = foundRoutingModule ? 'Could not find a non Routing NgModule.' |
|||
+ `\nModules with suffix '${routingModuleExt}' are strictly reserved for routing.` |
|||
+ '\nUse the skip-import option to skip importing in NgModule.' |
|||
: 'Could not find an NgModule. Use the skip-import option to skip importing in NgModule.'; |
|||
|
|||
throw new Error(errorMsg); |
|||
} |
|||
|
|||
/** |
|||
* 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; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
export * from './ast-utils'; |
|||
export * from './change'; |
|||
export * from './config'; |
|||
export * from './dependencies'; |
|||
export * from './find-module'; |
|||
export * from './json-file'; |
|||
export * from './json-utils'; |
|||
export * from './latest-versions'; |
|||
export * from './lint-fix'; |
|||
export * from './ng-ast-utils'; |
|||
export * from './parse-name'; |
|||
export * from './paths'; |
|||
export * from './project-targets'; |
|||
export * from './tsconfig'; |
|||
export * from './validation'; |
|||
export * from './workspace'; |
|||
export * from './workspace-models'; |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* @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 { JsonValue } from '@angular-devkit/core'; |
|||
import { Tree } from '@angular-devkit/schematics'; |
|||
import { Node, applyEdits, findNodeAtLocation, getNodeValue, modify, parseTree } from 'jsonc-parser'; |
|||
|
|||
export type JSONPath = (string | number)[]; |
|||
|
|||
/** @internal */ |
|||
export class JSONFile { |
|||
private content: string; |
|||
error: undefined | Error; |
|||
|
|||
constructor( |
|||
private readonly host: Tree, |
|||
private readonly path: string, |
|||
) { |
|||
const buffer = this.host.read(this.path); |
|||
if (buffer) { |
|||
this.content = buffer.toString(); |
|||
} else { |
|||
this.error = new Error(`Could not read ${path}.`); |
|||
} |
|||
} |
|||
|
|||
private _jsonAst: Node | undefined; |
|||
private get JsonAst(): Node { |
|||
if (this._jsonAst) { |
|||
return this._jsonAst; |
|||
} |
|||
|
|||
this._jsonAst = parseTree(this.content); |
|||
|
|||
return this._jsonAst; |
|||
} |
|||
|
|||
get(jsonPath: JSONPath): unknown { |
|||
if (jsonPath.length === 0) { |
|||
return getNodeValue(this.JsonAst); |
|||
} |
|||
|
|||
const node = findNodeAtLocation(this.JsonAst, jsonPath); |
|||
|
|||
return node === undefined ? undefined : getNodeValue(node); |
|||
} |
|||
|
|||
modify(jsonPath: JSONPath, value: JsonValue | undefined, getInsertionIndex?: (properties: string[]) => number): void { |
|||
if (!getInsertionIndex) { |
|||
const property = jsonPath.slice(-1)[0]; |
|||
getInsertionIndex = properties => [...properties, property].sort().findIndex(p => p === property); |
|||
} |
|||
|
|||
const edits = modify( |
|||
this.content, |
|||
jsonPath, |
|||
value, |
|||
{ |
|||
getInsertionIndex, |
|||
formattingOptions: { |
|||
insertSpaces: true, |
|||
tabSize: 2, |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
this.content = applyEdits(this.content, edits); |
|||
this.host.overwrite(this.path, this.content); |
|||
this._jsonAst = undefined; |
|||
} |
|||
|
|||
remove(jsonPath: JSONPath): void { |
|||
if (this.get(jsonPath) !== undefined) { |
|||
this.modify(jsonPath, undefined); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,231 @@ |
|||
/** |
|||
* @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 { |
|||
JsonAstArray, |
|||
JsonAstKeyValue, |
|||
JsonAstNode, |
|||
JsonAstObject, |
|||
JsonValue, |
|||
} from '@angular-devkit/core'; |
|||
import { UpdateRecorder } from '@angular-devkit/schematics'; |
|||
|
|||
export function appendPropertyInAstObject( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
value: JsonValue, |
|||
indent: number, |
|||
) { |
|||
const indentStr = _buildIndent(indent); |
|||
let index = node.start.offset + 1; |
|||
if (node.properties.length > 0) { |
|||
// Insert comma.
|
|||
const last = node.properties[node.properties.length - 1]; |
|||
const { text, end } = last; |
|||
const commaIndex = text.endsWith('\n') ? end.offset - 1 : end.offset; |
|||
recorder.insertRight(commaIndex, ','); |
|||
index = end.offset; |
|||
} |
|||
const content = _stringifyContent(value, indentStr); |
|||
recorder.insertRight( |
|||
index, |
|||
(node.properties.length === 0 && indent ? '\n' : '') |
|||
+ ' '.repeat(indent) |
|||
+ `"${propertyName}":${indent ? ' ' : ''}${content}` |
|||
+ indentStr.slice(0, -indent), |
|||
); |
|||
} |
|||
|
|||
export function insertPropertyInAstObjectInOrder( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
value: JsonValue, |
|||
indent: number, |
|||
) { |
|||
|
|||
if (node.properties.length === 0) { |
|||
appendPropertyInAstObject(recorder, node, propertyName, value, indent); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Find insertion info.
|
|||
let insertAfterProp: JsonAstKeyValue | null = null; |
|||
let prev: JsonAstKeyValue | null = null; |
|||
let isLastProp = false; |
|||
const last = node.properties[node.properties.length - 1]; |
|||
for (const prop of node.properties) { |
|||
if (prop.key.value > propertyName) { |
|||
if (prev) { |
|||
insertAfterProp = prev; |
|||
} |
|||
break; |
|||
} |
|||
if (prop === last) { |
|||
isLastProp = true; |
|||
insertAfterProp = last; |
|||
} |
|||
prev = prop; |
|||
} |
|||
|
|||
if (isLastProp) { |
|||
appendPropertyInAstObject(recorder, node, propertyName, value, indent); |
|||
|
|||
return; |
|||
} |
|||
|
|||
const indentStr = _buildIndent(indent); |
|||
const insertIndex = insertAfterProp === null |
|||
? node.start.offset + 1 |
|||
: insertAfterProp.end.offset + 1; |
|||
const content = _stringifyContent(value, indentStr); |
|||
recorder.insertRight( |
|||
insertIndex, |
|||
indentStr |
|||
+ `"${propertyName}":${indent ? ' ' : ''}${content}` |
|||
+ ',', |
|||
); |
|||
} |
|||
|
|||
export function removePropertyInAstObject( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
) { |
|||
// Find the property inside the object.
|
|||
const propIdx = node.properties.findIndex(prop => prop.key.value === propertyName); |
|||
|
|||
if (propIdx === -1) { |
|||
// There's nothing to remove.
|
|||
return; |
|||
} |
|||
|
|||
if (node.properties.length === 1) { |
|||
// This is a special case. Everything should be removed, including indentation.
|
|||
recorder.remove(node.start.offset, node.end.offset - node.start.offset); |
|||
recorder.insertRight(node.start.offset, '{}'); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// The AST considers commas and indentation to be part of the preceding property.
|
|||
// To get around messy comma and identation management, we can work over the range between
|
|||
// two properties instead.
|
|||
const previousProp = node.properties[propIdx - 1]; |
|||
const targetProp = node.properties[propIdx]; |
|||
const nextProp = node.properties[propIdx + 1]; |
|||
|
|||
let start, end; |
|||
if (previousProp) { |
|||
// Given the object below, and intending to remove the `m` property:
|
|||
// "{\n \"a\": \"a\",\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
// ^---------------^
|
|||
// Removing the range above results in:
|
|||
// "{\n \"a\": \"a\",\n \"z\": \"z\"\n}"
|
|||
start = previousProp.end; |
|||
end = targetProp.end; |
|||
} else { |
|||
// If there's no previousProp there is a nextProp, since we've specialcased the 1 length case.
|
|||
// Given the object below, and intending to remove the `a` property:
|
|||
// "{\n \"a\": \"a\",\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
// ^---------------^
|
|||
// Removing the range above results in:
|
|||
// "{\n \"m\": \"m\",\n \"z\": \"z\"\n}"
|
|||
start = targetProp.start; |
|||
end = nextProp.start; |
|||
} |
|||
|
|||
recorder.remove(start.offset, end.offset - start.offset); |
|||
if (!nextProp) { |
|||
recorder.insertRight(start.offset, '\n'); |
|||
} |
|||
} |
|||
|
|||
|
|||
export function appendValueInAstArray( |
|||
recorder: UpdateRecorder, |
|||
node: JsonAstArray, |
|||
value: JsonValue, |
|||
indent = 4, |
|||
) { |
|||
let indentStr = _buildIndent(indent); |
|||
let index = node.start.offset + 1; |
|||
// tslint:disable-next-line: no-any
|
|||
let newNodes: any[] | undefined; |
|||
|
|||
if (node.elements.length > 0) { |
|||
// Insert comma.
|
|||
const { end } = node.elements[node.elements.length - 1]; |
|||
const isClosingOnSameLine = node.end.offset - end.offset === 1; |
|||
|
|||
if (isClosingOnSameLine && indent) { |
|||
// Reformat the entire array
|
|||
recorder.remove(node.start.offset, node.end.offset - node.start.offset); |
|||
newNodes = [ |
|||
...node.elements.map(({ value }) => value), |
|||
value, |
|||
]; |
|||
index = node.start.offset; |
|||
// In case we are generating the entire node we need to reduce the spacing as
|
|||
// otherwise we'd end up having incorrect double spacing
|
|||
indent = indent - 2; |
|||
indentStr = _buildIndent(indent); |
|||
} else { |
|||
recorder.insertRight(end.offset, ','); |
|||
index = end.offset; |
|||
} |
|||
} |
|||
|
|||
recorder.insertRight( |
|||
index, |
|||
(newNodes ? '' : indentStr) |
|||
+ _stringifyContent(newNodes || value, indentStr) |
|||
+ (node.elements.length === 0 && indent ? indentStr.substr(0, -indent) + '\n' : ''), |
|||
); |
|||
} |
|||
|
|||
|
|||
export function findPropertyInAstObject( |
|||
node: JsonAstObject, |
|||
propertyName: string, |
|||
): JsonAstNode | null { |
|||
let maybeNode: JsonAstNode | null = null; |
|||
for (const property of node.properties) { |
|||
if (property.key.value == propertyName) { |
|||
maybeNode = property.value; |
|||
} |
|||
} |
|||
|
|||
return maybeNode; |
|||
} |
|||
|
|||
function _buildIndent(count: number): string { |
|||
return count ? '\n' + ' '.repeat(count) : ''; |
|||
} |
|||
|
|||
function _stringifyContent(value: JsonValue, indentStr: string): string { |
|||
// TODO: Add snapshot tests
|
|||
|
|||
// The 'space' value is 2, because we want to add 2 additional
|
|||
// indents from the 'key' node.
|
|||
|
|||
// If we use the indent provided we will have double indents:
|
|||
// "budgets": [
|
|||
// {
|
|||
// "type": "initial",
|
|||
// "maximumWarning": "2mb",
|
|||
// "maximumError": "5mb"
|
|||
// },
|
|||
// {
|
|||
// "type": "anyComponentStyle",
|
|||
// 'maximumWarning": "5kb"
|
|||
// }
|
|||
// ]
|
|||
return JSON.stringify(value, null, 2).replace(/\n/g, indentStr); |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
/** |
|||
* @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 const latestVersions = { |
|||
// These versions should be kept up to date with latest Angular peer dependencies.
|
|||
Angular: '~10.0.0-rc.0', |
|||
RxJs: '~6.6.0', |
|||
ZoneJs: '~0.10.2', |
|||
TypeScript: '~3.9.5', |
|||
TsLib: '^2.0.0', |
|||
|
|||
// The versions below must be manually updated when making a new devkit release.
|
|||
// For our e2e tests, these versions must match the latest tag present on the branch.
|
|||
// During RC periods they will not match the latest RC until there's a new git tag, and
|
|||
// should not be updated.
|
|||
DevkitBuildAngular: '~0.1000.0-rc.0', |
|||
DevkitBuildNgPackagr: '~0.1000.0-rc.0', |
|||
DevkitBuildWebpack: '~0.1000.0-rc.0', |
|||
|
|||
ngPackagr: '^10.0.0', |
|||
}; |
|||
@ -0,0 +1,51 @@ |
|||
/** |
|||
* @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 { |
|||
DirEntry, |
|||
Rule, |
|||
SchematicContext, |
|||
SchematicsException, |
|||
Tree, |
|||
} from '@angular-devkit/schematics'; |
|||
import { TslintFixTask } from '@angular-devkit/schematics/tasks'; |
|||
|
|||
export function applyLintFix(path = '/'): Rule { |
|||
return (tree: Tree, context: SchematicContext) => { |
|||
// Find the closest tslint.json or tslint.yaml
|
|||
let dir: DirEntry | null = tree.getDir(path.substr(0, path.lastIndexOf('/'))); |
|||
|
|||
do { |
|||
if ((dir.subfiles as string[]).some(f => f === 'tslint.json' || f === 'tslint.yaml')) { |
|||
break; |
|||
} |
|||
|
|||
dir = dir.parent; |
|||
} while (dir !== null); |
|||
|
|||
if (dir === null) { |
|||
throw new SchematicsException( |
|||
'Asked to run lint fixes, but could not find a tslint.json or tslint.yaml config file.'); |
|||
} |
|||
|
|||
// Only include files that have been touched.
|
|||
const files = tree.actions.reduce((acc: Set<string>, action) => { |
|||
const path = action.path.substr(1); // Remove the starting '/'.
|
|||
if (path.endsWith('.ts') && dir && action.path.startsWith(dir.path)) { |
|||
acc.add(path); |
|||
} |
|||
|
|||
return acc; |
|||
}, new Set<string>()); |
|||
|
|||
context.addTask(new TslintFixTask({ |
|||
ignoreErrors: true, |
|||
tsConfigPath: 'tsconfig.json', |
|||
files: [...files], |
|||
})); |
|||
}; |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
/** |
|||
* @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 { dirname } from 'path'; |
|||
import * as ts from 'typescript'; |
|||
import { findNode, getSourceNodes } from './ast-utils'; |
|||
|
|||
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 as ts.StringLiteral; |
|||
|
|||
return modulePathStringLiteral.text; |
|||
})[0]; |
|||
|
|||
return bootstrapModuleRelativePath; |
|||
} |
|||
|
|||
export function getAppModulePath(host: Tree, mainPath: string): string { |
|||
const moduleRelativePath = findBootstrapModulePath(host, mainPath); |
|||
const mainDir = dirname(mainPath); |
|||
const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); |
|||
|
|||
return modulePath; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
|
|||
/** |
|||
* @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 { relative, Path } from "../../../angular_devkit/core/src/virtual-fs";
|
|||
import { Path, basename, dirname, join, normalize } from '@angular-devkit/core'; |
|||
|
|||
export interface Location { |
|||
name: string; |
|||
path: Path; |
|||
} |
|||
|
|||
export function parseName(path: string, name: string): Location { |
|||
const nameWithoutPath = basename(normalize(name)); |
|||
const namePath = dirname(join(normalize(path), name) as Path); |
|||
|
|||
return { |
|||
name: nameWithoutPath, |
|||
path: normalize('/' + namePath), |
|||
}; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* @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, split } from '@angular-devkit/core'; |
|||
|
|||
export function relativePathToWorkspaceRoot(projectRoot: string | undefined): string { |
|||
const normalizedPath = split(normalize(projectRoot || '')); |
|||
|
|||
if (normalizedPath.length === 0 || !normalizedPath[0]) { |
|||
return '.'; |
|||
} else { |
|||
return normalizedPath.map(() => '..').join('/'); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
/** |
|||
* @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 } from '@angular-devkit/schematics'; |
|||
|
|||
export function targetBuildNotFoundError(): SchematicsException { |
|||
return new SchematicsException(`Project target "build" not found.`); |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
/** |
|||
* @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 { JsonParseMode, parseJsonAst } from '@angular-devkit/core'; |
|||
import { Rule, SchematicsException, Tree } from '@angular-devkit/schematics'; |
|||
import { appendValueInAstArray, findPropertyInAstObject } from './json-utils'; |
|||
|
|||
const SOLUTION_TSCONFIG_PATH = 'tsconfig.json'; |
|||
|
|||
/** |
|||
* Add project references in "Solution Style" tsconfig. |
|||
*/ |
|||
export function addTsConfigProjectReferences(paths: string[]): Rule { |
|||
return (host, context) => { |
|||
const logger = context.logger; |
|||
|
|||
// We need to read after each write to avoid missing `,` when appending multiple items.
|
|||
for (const path of paths) { |
|||
const source = host.read(SOLUTION_TSCONFIG_PATH); |
|||
if (!source) { |
|||
// Solution tsconfig doesn't exist.
|
|||
logger.warn(`Cannot add reference '${path}' in '${SOLUTION_TSCONFIG_PATH}'. File doesn't exists.`); |
|||
|
|||
return; |
|||
} |
|||
|
|||
const jsonAst = parseJsonAst(source.toString(), JsonParseMode.Loose); |
|||
if (jsonAst?.kind !== 'object') { |
|||
// Invalid JSON
|
|||
throw new SchematicsException(`Invalid JSON AST Object '${SOLUTION_TSCONFIG_PATH}'.`); |
|||
} |
|||
|
|||
// Solutions style tsconfig can contain 2 properties:
|
|||
// - 'files' with a value of empty array
|
|||
// - 'references'
|
|||
const filesAst = findPropertyInAstObject(jsonAst, 'files'); |
|||
const referencesAst = findPropertyInAstObject(jsonAst, 'references'); |
|||
if ( |
|||
filesAst?.kind !== 'array' || |
|||
filesAst.elements.length !== 0 || |
|||
referencesAst?.kind !== 'array' |
|||
) { |
|||
logger.warn(`Cannot add reference '${path}' in '${SOLUTION_TSCONFIG_PATH}'. It appears to be an invalid solution style tsconfig.`); |
|||
|
|||
return; |
|||
} |
|||
|
|||
// Append new paths
|
|||
const recorder = host.beginUpdate(SOLUTION_TSCONFIG_PATH); |
|||
appendValueInAstArray(recorder, referencesAst, { 'path': `./${path}` }, 4); |
|||
host.commitUpdate(recorder); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Throws an exception when the base tsconfig doesn't exists. |
|||
*/ |
|||
export function verifyBaseTsConfigExists(host: Tree): void { |
|||
if (host.exists('tsconfig.base.json')) { |
|||
return; |
|||
} |
|||
|
|||
throw new SchematicsException(`Cannot find base TypeScript configuration file 'tsconfig.base.json'.`); |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
/** |
|||
* @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 { tags } from '@angular-devkit/core'; |
|||
import { SchematicsException } from '@angular-devkit/schematics'; |
|||
|
|||
export function validateName(name: string): void { |
|||
if (name && /^\d/.test(name)) { |
|||
throw new SchematicsException(tags.oneLine`name (${name})
|
|||
can not start with a digit.`);
|
|||
} |
|||
} |
|||
|
|||
// Must start with a letter, and must contain only alphanumeric characters or dashes.
|
|||
// When adding a dash the segment after the dash must also start with a letter.
|
|||
export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; |
|||
|
|||
export function validateHtmlSelector(selector: string): void { |
|||
if (selector && !htmlSelectorRe.test(selector)) { |
|||
throw new SchematicsException(tags.oneLine`Selector (${selector})
|
|||
is invalid.`);
|
|||
} |
|||
} |
|||
|
|||
|
|||
export function validateProjectName(projectName: string) { |
|||
const errorIndex = getRegExpFailPosition(projectName); |
|||
const unsupportedProjectNames: string[] = []; |
|||
const packageNameRegex = /^(?:@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9_-]+$/; |
|||
if (errorIndex !== null) { |
|||
const firstMessage = tags.oneLine` |
|||
Project name "${projectName}" is not valid. New project names must |
|||
start with a letter, and must contain only alphanumeric characters or dashes. |
|||
When adding a dash the segment after the dash must also start with a letter. |
|||
`;
|
|||
const msg = tags.stripIndent` |
|||
${firstMessage} |
|||
${projectName} |
|||
${Array(errorIndex + 1).join(' ') + '^'} |
|||
`;
|
|||
throw new SchematicsException(msg); |
|||
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) { |
|||
throw new SchematicsException( |
|||
`Project name ${JSON.stringify(projectName)} is not a supported name.`); |
|||
} else if (!packageNameRegex.test(projectName)) { |
|||
throw new SchematicsException(`Project name ${JSON.stringify(projectName)} is invalid.`); |
|||
} |
|||
} |
|||
|
|||
function getRegExpFailPosition(str: string): number | null { |
|||
const isScope = /^@.*\/.*/.test(str); |
|||
if (isScope) { |
|||
// Remove starting @
|
|||
str = str.replace(/^@/, ''); |
|||
// Change / to - for validation
|
|||
str = str.replace(/\//g, '-'); |
|||
} |
|||
|
|||
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str]; |
|||
const matched: string[] = []; |
|||
|
|||
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/; |
|||
|
|||
parts.forEach(part => { |
|||
if (part.match(projectNameRegexp)) { |
|||
matched.push(part); |
|||
} |
|||
}); |
|||
|
|||
const compare = matched.join('-'); |
|||
|
|||
return (str !== compare) ? compare.length : null; |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
/** |
|||
* @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 { experimental } from '@angular-devkit/core'; |
|||
|
|||
export enum ProjectType { |
|||
Application = 'application', |
|||
Library = 'library', |
|||
} |
|||
|
|||
export enum Builders { |
|||
AppShell = '@angular-devkit/build-angular:app-shell', |
|||
Server = '@angular-devkit/build-angular:server', |
|||
Browser = '@angular-devkit/build-angular:browser', |
|||
Karma = '@angular-devkit/build-angular:karma', |
|||
TsLint = '@angular-devkit/build-angular:tslint', |
|||
NgPackagr = '@angular-devkit/build-ng-packagr:build', |
|||
DevServer = '@angular-devkit/build-angular:dev-server', |
|||
ExtractI18n = '@angular-devkit/build-angular:extract-i18n', |
|||
Protractor = '@angular-devkit/build-angular:protractor', |
|||
} |
|||
|
|||
export interface FileReplacements { |
|||
replace: string; |
|||
with: string; |
|||
} |
|||
|
|||
export interface BrowserBuilderBaseOptions { |
|||
main: string; |
|||
tsConfig: string; |
|||
fileReplacements?: FileReplacements[]; |
|||
outputPath?: string; |
|||
index?: string; |
|||
polyfills: string; |
|||
assets?: (object|string)[]; |
|||
styles?: (object|string)[]; |
|||
scripts?: (object|string)[]; |
|||
sourceMap?: boolean; |
|||
} |
|||
|
|||
export type OutputHashing = 'all' | 'media' | 'none' | 'bundles'; |
|||
|
|||
export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions { |
|||
serviceWorker?: boolean; |
|||
optimization?: boolean; |
|||
outputHashing?: OutputHashing; |
|||
resourcesOutputPath?: string; |
|||
extractCss?: boolean; |
|||
namedChunks?: boolean; |
|||
aot?: boolean; |
|||
extractLicenses?: boolean; |
|||
vendorChunk?: boolean; |
|||
buildOptimizer?: boolean; |
|||
ngswConfigPath?: string; |
|||
budgets?: { |
|||
type: string; |
|||
maximumWarning?: string; |
|||
maximumError?: string; |
|||
}[]; |
|||
webWorkerTsConfig?: string; |
|||
} |
|||
|
|||
export interface ServeBuilderOptions { |
|||
browserTarget: string; |
|||
} |
|||
export interface LibraryBuilderOptions { |
|||
tsConfig: string; |
|||
project: string; |
|||
} |
|||
|
|||
export interface ServerBuilderOptions { |
|||
outputPath: string; |
|||
tsConfig: string; |
|||
main: string; |
|||
fileReplacements?: FileReplacements[]; |
|||
optimization?: { |
|||
scripts?: boolean; |
|||
styles?: boolean; |
|||
}; |
|||
sourceMap?: boolean | { |
|||
scripts?: boolean; |
|||
styles?: boolean; |
|||
hidden?: boolean; |
|||
vendor?: boolean; |
|||
}; |
|||
} |
|||
|
|||
export interface AppShellBuilderOptions { |
|||
browserTarget: string; |
|||
serverTarget: string; |
|||
route: string; |
|||
} |
|||
|
|||
export interface TestBuilderOptions extends Partial<BrowserBuilderBaseOptions> { |
|||
karmaConfig: string; |
|||
} |
|||
|
|||
export interface LintBuilderOptions { |
|||
tsConfig: string[] | string; |
|||
exclude?: string[]; |
|||
} |
|||
|
|||
export interface ExtractI18nOptions { |
|||
browserTarget: string; |
|||
} |
|||
|
|||
export interface E2EOptions { |
|||
protractorConfig: string; |
|||
devServerTarget: string; |
|||
} |
|||
|
|||
export interface BuilderTarget<TBuilder extends Builders, TOptions> { |
|||
builder: TBuilder; |
|||
options: TOptions; |
|||
configurations?: { |
|||
production: Partial<TOptions>; |
|||
[key: string]: Partial<TOptions>; |
|||
}; |
|||
} |
|||
|
|||
export type LibraryBuilderTarget = BuilderTarget<Builders.NgPackagr, LibraryBuilderOptions>; |
|||
export type BrowserBuilderTarget = BuilderTarget<Builders.Browser, BrowserBuilderOptions>; |
|||
export type ServerBuilderTarget = BuilderTarget<Builders.Server, ServerBuilderOptions>; |
|||
export type AppShellBuilderTarget = BuilderTarget<Builders.AppShell, AppShellBuilderOptions>; |
|||
export type LintBuilderTarget = BuilderTarget<Builders.TsLint, LintBuilderOptions>; |
|||
export type TestBuilderTarget = BuilderTarget<Builders.Karma, TestBuilderOptions>; |
|||
export type ServeBuilderTarget = BuilderTarget<Builders.DevServer, ServeBuilderOptions>; |
|||
export type ExtractI18nBuilderTarget = BuilderTarget<Builders.ExtractI18n, ExtractI18nOptions>; |
|||
export type E2EBuilderTarget = BuilderTarget<Builders.Protractor, E2EOptions>; |
|||
|
|||
export interface WorkspaceSchema extends experimental.workspace.WorkspaceSchema { |
|||
projects: { |
|||
[key: string]: WorkspaceProject<ProjectType.Application | ProjectType.Library>; |
|||
}; |
|||
} |
|||
|
|||
export interface WorkspaceProject<TProjectType extends ProjectType = ProjectType.Application> |
|||
extends experimental.workspace.WorkspaceProject { |
|||
/** |
|||
* Project type. |
|||
*/ |
|||
projectType: ProjectType; |
|||
|
|||
/** |
|||
* Tool options. |
|||
*/ |
|||
architect?: WorkspaceTargets<TProjectType>; |
|||
/** |
|||
* Tool options. |
|||
*/ |
|||
targets?: WorkspaceTargets<TProjectType>; |
|||
} |
|||
|
|||
export interface WorkspaceTargets<TProjectType extends ProjectType = ProjectType.Application> { |
|||
build?: TProjectType extends ProjectType.Library ? LibraryBuilderTarget : BrowserBuilderTarget; |
|||
server?: ServerBuilderTarget; |
|||
lint?: LintBuilderTarget; |
|||
test?: TestBuilderTarget; |
|||
serve?: ServeBuilderTarget; |
|||
e2e?: E2EBuilderTarget; |
|||
'app-shell'?: AppShellBuilderTarget; |
|||
'extract-i18n'?: ExtractI18nBuilderTarget; |
|||
// TODO(hans): change this any to unknown when google3 supports TypeScript 3.0.
|
|||
// tslint:disable-next-line:no-any
|
|||
[key: string]: any; |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
/** |
|||
* @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 { virtualFs, workspaces } from '@angular-devkit/core'; |
|||
import { Rule, Tree } from '@angular-devkit/schematics'; |
|||
import { ProjectType } from './workspace-models'; |
|||
|
|||
function createHost(tree: Tree): workspaces.WorkspaceHost { |
|||
return { |
|||
async readFile(path: string): Promise<string> { |
|||
const data = tree.read(path); |
|||
if (!data) { |
|||
throw new Error('File not found.'); |
|||
} |
|||
|
|||
return virtualFs.fileBufferToString(data); |
|||
}, |
|||
async writeFile(path: string, data: string): Promise<void> { |
|||
return tree.overwrite(path, data); |
|||
}, |
|||
async isDirectory(path: string): Promise<boolean> { |
|||
// approximate a directory check
|
|||
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; |
|||
}, |
|||
async isFile(path: string): Promise<boolean> { |
|||
return tree.exists(path); |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
export function updateWorkspace( |
|||
updater: (workspace: workspaces.WorkspaceDefinition) => void | PromiseLike<void>, |
|||
): Rule; |
|||
export function updateWorkspace( |
|||
workspace: workspaces.WorkspaceDefinition, |
|||
): Rule; |
|||
export function updateWorkspace( |
|||
updaterOrWorkspace: workspaces.WorkspaceDefinition |
|||
| ((workspace: workspaces.WorkspaceDefinition) => void | PromiseLike<void>), |
|||
): Rule { |
|||
return async (tree: Tree) => { |
|||
const host = createHost(tree); |
|||
|
|||
if (typeof updaterOrWorkspace === 'function') { |
|||
|
|||
const { workspace } = await workspaces.readWorkspace('/', host); |
|||
|
|||
const result = updaterOrWorkspace(workspace); |
|||
if (result !== undefined) { |
|||
await result; |
|||
} |
|||
|
|||
await workspaces.writeWorkspace(workspace, host); |
|||
} else { |
|||
await workspaces.writeWorkspace(updaterOrWorkspace, host); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export async function getWorkspace(tree: Tree, path = '/') { |
|||
const host = createHost(tree); |
|||
|
|||
const { workspace } = await workspaces.readWorkspace(path, host); |
|||
|
|||
return workspace; |
|||
} |
|||
|
|||
/** |
|||
* Build a default project path for generating. |
|||
* @param project The project which will have its default path generated. |
|||
*/ |
|||
export function buildDefaultPath(project: workspaces.ProjectDefinition): string { |
|||
const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; |
|||
const projectDirName = project.extensions['projectType'] === ProjectType.Application ? 'app' : 'lib'; |
|||
|
|||
return `${root}${projectDirName}`; |
|||
} |
|||
|
|||
export async function createDefaultPath(tree: Tree, projectName: string): Promise<string> { |
|||
const workspace = await getWorkspace(tree); |
|||
const project = workspace.projects.get(projectName); |
|||
if (!project) { |
|||
throw new Error('Specified project does not exist.'); |
|||
} |
|||
|
|||
return buildDefaultPath(project); |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './angular'; |
|||
Loading…
Reference in new issue