feat(angular): build for angular 12.0 (#23970)

This commit is contained in:
Mike Hartington
2021-10-15 16:54:59 -04:00
committed by GitHub
parent e3996cfbd5
commit 3451a34ad0
67 changed files with 55358 additions and 29616 deletions

View File

@ -1,8 +1,21 @@
import { join, Path } from '@angular-devkit/core';
import { apply, chain, mergeWith, move, Rule, SchematicContext, SchematicsException, template, Tree, url } from '@angular-devkit/schematics';
import { Path, join } from '@angular-devkit/core';
import {
Rule,
SchematicContext,
Tree,
apply,
chain,
mergeWith,
move,
SchematicsException,
template,
url,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { getWorkspace } from '@schematics/angular/utility/workspace';
import { addModuleImportToRootModule } from './../utils/ast';
import { addArchitectBuilder, addAsset, addStyle, getDefaultAngularAppName, getWorkspace, WorkspaceProject, WorkspaceSchema } from './../utils/config';
import { addArchitectBuilder, addAsset, addStyle, getDefaultAngularAppName } from './../utils/config';
import { addPackageToPackageJson } from './../utils/package';
import { Schema as IonAddOptions } from './schema';
@ -15,24 +28,14 @@ function addIonicAngularToPackageJson(): Rule {
function addIonicAngularToolkitToPackageJson(): Rule {
return (host: Tree) => {
addPackageToPackageJson(
host,
'devDependencies',
'@ionic/angular-toolkit',
'latest'
);
addPackageToPackageJson(host, 'devDependencies', '@ionic/angular-toolkit', 'latest');
return host;
};
}
function addIonicAngularModuleToAppModule(projectSourceRoot: Path): Rule {
return (host: Tree) => {
addModuleImportToRootModule(
host,
projectSourceRoot,
'IonicModule.forRoot()',
'@ionic/angular'
);
addModuleImportToRootModule(host, projectSourceRoot, 'IonicModule.forRoot()', '@ionic/angular');
return host;
};
}
@ -50,13 +53,13 @@ function addIonicStyles(projectName: string, projectSourceRoot: Path): Rule {
'node_modules/@ionic/angular/css/text-alignment.css',
'node_modules/@ionic/angular/css/text-transformation.css',
'node_modules/@ionic/angular/css/flex-utils.css',
`${projectSourceRoot}/theme/variables.css`
]
`${projectSourceRoot}/theme/variables.css`,
];
ionicStyles.forEach(entry => {
ionicStyles.forEach((entry) => {
addStyle(host, projectName, entry);
});
return host;
return host;
};
}
@ -65,7 +68,7 @@ function addIonicons(projectName: string): Rule {
const ioniconsGlob = {
glob: '**/*.svg',
input: 'node_modules/ionicons/dist/ionicons/svg',
output: './svg'
output: './svg',
};
addAsset(host, projectName, 'build', ioniconsGlob);
addAsset(host, projectName, 'test', ioniconsGlob);
@ -79,25 +82,25 @@ function addIonicBuilder(projectName: string): Rule {
builder: '@ionic/angular-toolkit:cordova-serve',
options: {
cordovaBuildTarget: `${projectName}:ionic-cordova-build`,
devServerTarget: `${projectName}:serve`
devServerTarget: `${projectName}:serve`,
},
configurations: {
production: {
cordovaBuildTarget: `${projectName}:ionic-cordova-build:production`,
devServerTarget: `${projectName}:serve:production`
}
}
devServerTarget: `${projectName}:serve:production`,
},
},
});
addArchitectBuilder(host, projectName, 'ionic-cordova-build', {
builder: '@ionic/angular-toolkit:cordova-build',
options: {
browserTarget: `${projectName}:build`
browserTarget: `${projectName}:build`,
},
configurations: {
production: {
browserTarget: `${projectName}:build:production`
}
}
browserTarget: `${projectName}:build:production`,
},
},
});
return host;
};
@ -110,22 +113,18 @@ function installNodeDeps() {
}
export default function ngAdd(options: IonAddOptions): Rule {
return (host: Tree) => {
const workspace: WorkspaceSchema = getWorkspace(host);
return async (host: Tree) => {
const workspace = await getWorkspace(host);
if (!options.project) {
options.project = getDefaultAngularAppName(workspace);
}
const project: WorkspaceProject = workspace.projects[options.project];
if (project.projectType !== 'application') {
throw new SchematicsException(
`Ionic Add requires a project type of "application".`
);
const project = workspace.projects.get(options.project);
if (!project || project.extensions.projectType !== 'application') {
throw new SchematicsException(`Ionic Add requires a project type of "application".`);
}
const sourcePath: Path = join(project.sourceRoot as Path);
const rootTemplateSource = apply(url('./files/root'), [
template({ ...options }),
move(sourcePath)
]);
const rootTemplateSource = apply(url('./files/root'), [template({ ...options }), move(sourcePath)]);
return chain([
// @ionic/angular
addIonicAngularToPackageJson(),
@ -136,7 +135,7 @@ export default function ngAdd(options: IonAddOptions): Rule {
addIonicons(options.project),
mergeWith(rootTemplateSource),
// install freshly added dependencies
installNodeDeps()
installNodeDeps(),
]);
};
}

View File

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/schema",
"id": "ionicNgAdd",
"$id": "ionicNgAdd",
"title": "Ionic Add options",
"type": "object",
"properties": {

View File

@ -1,6 +1,7 @@
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import { normalize } from '@angular-devkit/core';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { addImportToModule } from './devkit-utils/ast-utils';
import { InsertChange } from './devkit-utils/change';
@ -13,12 +14,7 @@ export function getSourceFile(host: Tree, path: string): ts.SourceFile {
throw new SchematicsException(`Could not find file for path: ${path}`);
}
const content = buffer.toString();
const source = ts.createSourceFile(
path,
content,
ts.ScriptTarget.Latest,
true
);
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
return source;
}
@ -30,13 +26,8 @@ export function addModuleImportToRootModule(
projectSourceRoot: string,
moduleName: string,
importSrc: string
) {
addModuleImportToModule(
host,
normalize(`${projectSourceRoot}/app/app.module.ts`),
moduleName,
importSrc
);
): void {
addModuleImportToModule(host, normalize(`${projectSourceRoot}/app/app.module.ts`), moduleName, importSrc);
}
/**
@ -46,17 +37,12 @@ export function addModuleImportToRootModule(
* @param moduleName name of module to import
* @param src src location to import
*/
export function addModuleImportToModule(
host: Tree,
modulePath: string,
moduleName: string,
src: string
) {
export function addModuleImportToModule(host: Tree, modulePath: string, moduleName: string, src: string): void {
const moduleSource = getSourceFile(host, modulePath);
const changes = addImportToModule(moduleSource, modulePath, moduleName, src);
const recorder = host.beginUpdate(modulePath);
changes.forEach(change => {
changes.forEach((change) => {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}

View File

@ -1,18 +1,19 @@
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import { experimental, parseJson, JsonParseMode } from '@angular-devkit/core';
import { WorkspaceDefinition } from '@angular-devkit/core/src/workspace';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import { parse } from 'jsonc-parser';
const CONFIG_PATH = 'angular.json';
export function readConfig(host: Tree) {
const sourceText = host.read(CONFIG_PATH)!.toString('utf-8');
export function readConfig(host: Tree): any {
const sourceText = host.read(CONFIG_PATH)?.toString('utf-8');
return JSON.parse(sourceText);
}
export function writeConfig(host: Tree, config: JSON) {
export function writeConfig(host: Tree, config: JSON): void {
host.overwrite(CONFIG_PATH, JSON.stringify(config, null, 2));
}
function isAngularBrowserProject(projectConfig: any) {
function isAngularBrowserProject(projectConfig: any): boolean {
if (projectConfig.projectType === 'application') {
const buildConfig = projectConfig.architect.build;
return buildConfig.builder === '@angular-devkit/build-angular:browser';
@ -36,6 +37,7 @@ export function getDefaultAngularAppName(config: any): string {
}
export function getAngularAppConfig(config: any, projectName: string): any | never {
// eslint-disable-next-line no-prototype-builtins
if (!config.projects.hasOwnProperty(projectName)) {
throw new SchematicsException(`Could not find project: ${projectName}`);
}
@ -53,40 +55,50 @@ export function getAngularAppConfig(config: any, projectName: string): any | nev
}
}
export function addStyle(host: Tree, projectName: string, stylePath: string) {
export function addStyle(host: Tree, projectName: string, stylePath: string): void {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect.build.options.styles.push({
input: stylePath
input: stylePath,
});
writeConfig(host, config);
}
export function addAsset(host: Tree, projectName: string, architect: string, asset: string | {glob: string; input: string; output: string}) {
export function addAsset(
host: Tree,
projectName: string,
architect: string,
asset: string | { glob: string; input: string; output: string }
): void {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect[architect].options.assets.push(asset);
writeConfig(host, config);
const target = appConfig.architect[architect];
if (target) {
target.options.assets.push(asset);
writeConfig(host, config);
}
}
export function addArchitectBuilder(host: Tree, projectName: string, builderName: string, builderOpts: any): void | never {
export function addArchitectBuilder(
host: Tree,
projectName: string,
builderName: string,
builderOpts: any
): void | never {
const config = readConfig(host);
const appConfig = getAngularAppConfig(config, projectName);
appConfig.architect[builderName] = builderOpts;
writeConfig(host, config);
}
export type WorkspaceSchema = experimental.workspace.WorkspaceSchema;
export type WorkspaceProject = experimental.workspace.WorkspaceProject;
export function getWorkspacePath(host: Tree): string {
const possibleFiles = ['/angular.json', '/.angular.json'];
const path = possibleFiles.filter(path => host.exists(path))[0];
const path = possibleFiles.filter((path) => host.exists(path))[0];
return path;
}
export function getWorkspace(host: Tree): WorkspaceSchema {
export function getWorkspace(host: Tree): WorkspaceDefinition {
const path = getWorkspacePath(host);
const configBuffer = host.read(path);
if (configBuffer === null) {
@ -94,5 +106,5 @@ export function getWorkspace(host: Tree): WorkspaceSchema {
}
const content = configBuffer.toString();
return (parseJson(content, JsonParseMode.Loose) as {}) as WorkspaceSchema;
return parse(content) as WorkspaceDefinition;
}

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import { Change, InsertChange, NoopChange } from './change';
import { Change, InsertChange, NoopChange } from './change';
/**
* Add Import `import { symbolName } from fileName` if the import doesn't exit
@ -18,26 +18,32 @@ import { Change, InsertChange, NoopChange } from './change';
* @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 {
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 => {
const relevantImports = allImports.filter((node) => {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
const importFiles = node.getChildren()
.filter(child => child.kind === ts.SyntaxKind.StringLiteral)
.map(n => (n as ts.StringLiteral).text);
const importFiles = node
.getChildren()
.filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
.map((n) => (n as ts.StringLiteral).text);
return importFiles.filter(file => file === fileName).length === 1;
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 => {
relevantImports.forEach((n) => {
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk = true;
@ -49,7 +55,7 @@ export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolNa
return new NoopChange();
}
const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName);
const importTextNodes = imports.filter((n) => (n as ts.Identifier).text === symbolName);
// insert import if it's not there
if (importTextNodes.length === 0) {
@ -64,8 +70,9 @@ export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolNa
}
// no such import declaration exists
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral)
.filter((n: ts.StringLiteral) => n.text === 'use strict');
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
(n: ts.StringLiteral) => n.text === 'use strict'
);
let fallbackPos = 0;
if (useStrict.length > 0) {
fallbackPos = useStrict[0].end;
@ -75,19 +82,12 @@ export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolNa
// 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' : ''}`;
const toInsert =
`${separator}import ${open}${symbolName}${close}` + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
return insertAfterLastOccurrence(
allImports,
toInsert,
fileToEdit,
fallbackPos,
ts.SyntaxKind.StringLiteral,
);
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
@ -107,7 +107,7 @@ export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): t
}
if (max > 0) {
for (const child of node.getChildren()) {
findNodes(child, kind, max).forEach(node => {
findNodes(child, kind, max).forEach((node) => {
if (max > 0) {
arr.push(node);
}
@ -123,7 +123,6 @@ export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): t
return arr;
}
/**
* Get all the nodes from a source.
* @param sourceFile The source file object.
@ -154,14 +153,13 @@ export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.N
}
let foundNode: ts.Node | null = null;
ts.forEachChild(node, childNode => {
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
@ -170,7 +168,6 @@ 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
@ -184,11 +181,13 @@ function nodesByPosition(first: ts.Node, second: ts.Node): number {
* @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 {
export function insertAfterLastOccurrence(
nodes: ts.Node[],
toInsert: string,
file: string,
fallbackPos: number,
syntaxKind?: ts.SyntaxKind
): Change {
// sort() has a side effect, so make a copy so that we won't overwrite the parent's object.
let lastItem = [...nodes].sort(nodesByPosition).pop();
if (!lastItem) {
@ -205,7 +204,6 @@ export function insertAfterLastOccurrence(nodes: ts.Node[],
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;
@ -216,9 +214,11 @@ export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): s
}
}
function _angularImportsFromNode(node: ts.ImportDeclaration,
_sourceFile: ts.SourceFile): {[name: string]: string} {
function _angularImportsFromNode(
node: ts.ImportDeclaration,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_sourceFile: ts.SourceFile
): { [name: string]: string } {
const ms = node.moduleSpecifier;
let modulePath: string;
switch (ms.kind) {
@ -249,8 +249,8 @@ function _angularImportsFromNode(node: ts.ImportDeclaration,
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) => {
.map((is: ts.ImportSpecifier) => (is.propertyName ? is.propertyName.text : is.name.text))
.reduce((acc: { [name: string]: string }, curr: string) => {
acc[curr] = modulePath;
return acc;
@ -265,13 +265,10 @@ function _angularImportsFromNode(node: ts.ImportDeclaration,
}
}
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
module: string): ts.Node[] {
const angularImports: {[name: string]: string}
= findNodes(source, ts.SyntaxKind.ImportDeclaration)
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, module: string): ts.Node[] {
const angularImports: { [name: string]: string } = findNodes(source, ts.SyntaxKind.ImportDeclaration)
.map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source))
.reduce((acc: {[name: string]: string}, current: {[name: string]: string}) => {
.reduce((acc: { [name: string]: string }, current: { [name: string]: string }) => {
for (const key of Object.keys(current)) {
acc[key] = current[key];
}
@ -280,17 +277,17 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
}, {});
return getSourceNodes(source)
.filter(node => {
return node.kind == ts.SyntaxKind.Decorator
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
.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 => {
.map((node) => (node as ts.Decorator).expression as ts.CallExpression)
.filter((expr) => {
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
const id = expr.expression as ts.Identifier;
return id.getFullText(source) == identifier
&& angularImports[id.getFullText(source)] === module;
return id.getFullText(source) == identifier && angularImports[id.getFullText(source)] === module;
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
// This covers foo.NgModule when importing * as foo.
const paExpr = expr.expression as ts.PropertyAccessExpression;
@ -302,17 +299,16 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
const id = paExpr.name.text;
const moduleId = (paExpr.expression as ts.Identifier).getText(source);
return id === identifier && (angularImports[moduleId + '.'] === module);
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);
.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 {
function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration | undefined {
if (ts.isClassDeclaration(node)) {
return node;
}
@ -326,7 +322,7 @@ function findClassDeclarationParent(node: ts.Node): ts.ClassDeclaration|undefine
* @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 {
export function getFirstNgModuleName(source: ts.SourceFile): string | undefined {
// First, find the @NgModule decorators.
const ngModulesMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
if (ngModulesMetadata.length === 0) {
@ -349,10 +345,10 @@ export function addSymbolToNgModuleMetadata(
ngModulePath: string,
metadataField: string,
symbolName: string,
importPath: string | null = null,
importPath: string | null = null
): Change[] {
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
let node: any = nodes[0]; // tslint:disable-line:no-any
let node: any = nodes[0]; // tslint:disable-line:no-any
// Find the decorator declaration.
if (!node) {
@ -360,9 +356,8 @@ export function addSymbolToNgModuleMetadata(
}
// Get all the children property assignment of object literals.
const matchingProperties: ts.ObjectLiteralElement[] =
(node as ts.ObjectLiteralExpression).properties
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
const matchingProperties: ts.ObjectLiteralElement[] = (node as ts.ObjectLiteralExpression).properties
.filter((prop) => prop.kind == ts.SyntaxKind.PropertyAssignment)
// Filter out every fields that's not "metadataField". Also handles string literals
// (but not expressions).
.filter((prop: ts.PropertyAssignment) => {
@ -432,8 +427,9 @@ export function addSymbolToNgModuleMetadata(
}
if (Array.isArray(node)) {
const nodeArray = node as {} as Array<ts.Node>;
const symbolsArray = nodeArray.map(node => node.getText());
// eslint-disable-next-line @typescript-eslint/ban-types
const nodeArray = node as {} as ts.Node[];
const symbolsArray = nodeArray.map((node) => node.getText());
if (symbolsArray.includes(symbolName)) {
return [];
}
@ -488,81 +484,93 @@ export function addSymbolToNgModuleMetadata(
* 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);
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[] {
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[] {
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[] {
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[] {
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.
*/
export function addEntryComponentToModule(source: ts.SourceFile,
modulePath: string, classifiedName: string,
importPath: string): Change[] {
return addSymbolToNgModuleMetadata(
source, modulePath,
'entryComponents', classifiedName, importPath,
);
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 {
export function isImported(source: ts.SourceFile, classifiedName: string, importPath: string): boolean {
const allNodes = getSourceNodes(source);
const matchingNodes = allNodes
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
.filter((node) => node.kind === ts.SyntaxKind.ImportDeclaration)
.filter((imp: ts.ImportDeclaration) => imp.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
.filter((imp: ts.ImportDeclaration) => {
return (<ts.StringLiteral> imp.moduleSpecifier).text === importPath;
return (imp.moduleSpecifier as ts.StringLiteral).text === importPath;
})
.filter((imp: ts.ImportDeclaration) => {
if (!imp.importClause) {
return false;
}
const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier)
.filter(n => n.getText() === classifiedName);
const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier).filter(
(n) => n.getText() === classifiedName
);
return nodes.length > 0;
});

View File

@ -10,7 +10,6 @@ export interface Host {
read(path: string): Promise<string>;
}
export interface Change {
apply(host: Host): Promise<void>;
@ -26,7 +25,6 @@ export interface Change {
readonly description: string;
}
/**
* An operation that does nothing.
*/
@ -34,15 +32,15 @@ export class NoopChange implements Change {
description = 'No operation.';
order = Infinity;
path = null;
apply() { return Promise.resolve(); }
apply(): Promise<void> {
return Promise.resolve();
}
}
/**
* Will add text to the source code.
*/
export class InsertChange implements Change {
order: number;
description: string;
@ -57,8 +55,8 @@ export class InsertChange implements Change {
/**
* This method does not insert spaces if there is none in the original string.
*/
apply(host: Host) {
return host.read(this.path).then(content => {
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);
@ -71,7 +69,6 @@ export class InsertChange implements Change {
* Will remove text from the source code.
*/
export class RemoveChange implements Change {
order: number;
description: string;
@ -84,7 +81,7 @@ export class RemoveChange implements Change {
}
apply(host: Host): Promise<void> {
return host.read(this.path).then(content => {
return host.read(this.path).then((content) => {
const prefix = content.substring(0, this.pos);
const suffix = content.substring(this.pos + this.toRemove.length);
@ -101,8 +98,7 @@ export class ReplaceChange implements Change {
order: number;
description: string;
constructor(public path: string, private pos: number, private oldText: string,
private newText: string) {
constructor(public path: string, private pos: number, private oldText: string, private newText: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
@ -111,7 +107,7 @@ export class ReplaceChange implements Change {
}
apply(host: Host): Promise<void> {
return host.read(this.path).then(content => {
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);

View File

@ -3,5 +3,3 @@
These are utility files copied over from `@angular-devkit`.
They are not exported so they need to be manually copied over.
Please do not edit directly.

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import { findNodes, insertAfterLastOccurrence } from './ast-utils';
import { Change, NoopChange } from './change';
@ -30,25 +31,22 @@ export function insertImport(
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
// get nodes that map to import statements from the file fileName
const relevantImports = allImports.filter(node => {
const relevantImports = allImports.filter((node) => {
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
const importFiles = node
.getChildren()
.filter(child => child.kind === ts.SyntaxKind.StringLiteral)
.map(n => (n as ts.StringLiteral).text);
.filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
.map((n) => (n as ts.StringLiteral).text);
return importFiles.filter(file => file === fileName).length === 1;
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)
);
relevantImports.forEach((n) => {
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
importsAsterisk = true;
}
@ -59,25 +57,15 @@ export function insertImport(
return new NoopChange();
}
const importTextNodes = imports.filter(
n => (n as ts.Identifier).text === symbolName
);
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.CloseBraceToken)[0].getStart() ||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
return insertAfterLastOccurrence(
imports,
`, ${symbolName}`,
fileToEdit,
fallbackPos
);
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
}
return new NoopChange();
@ -97,14 +85,7 @@ export function insertImport(
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
const separator = insertAtBeginning ? '' : ';\n';
const toInsert =
`${separator}import ${open}${symbolName}${close}` +
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
`${separator}import ${open}${symbolName}${close}` + ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
return insertAfterLastOccurrence(
allImports,
toInsert,
fileToEdit,
fallbackPos,
ts.SyntaxKind.StringLiteral
);
return insertAfterLastOccurrence(allImports, toInsert, fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
}

View File

@ -1,11 +1,11 @@
import {Tree} from '@angular-devkit/schematics';
import { Tree } from '@angular-devkit/schematics';
/**
* Adds a package to the package.json
*/
export function addPackageToPackageJson(host: Tree, type: string, pkg: string, version: string) {
export function addPackageToPackageJson(host: Tree, type: string, pkg: string, version: string): Tree {
if (host.exists('package.json')) {
const sourceText = host.read('package.json')!.toString('utf-8');
const sourceText = host.read('package.json')?.toString('utf-8');
const json = JSON.parse(sourceText);
if (!json[type]) {
json[type] = {};