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