mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 02:31:34 +08:00
feat(): support ng add (#15323)
* feat(angular): support ng add * feat(): add build aditions * chore(): update schematics * chore(): bad style import
This commit is contained in:
@ -0,0 +1,6 @@
|
||||
/* Ionic Variables and Theming. */
|
||||
/* This is just a placeholder file For more info, please see: */
|
||||
/* https://beta.ionicframework.com/docs/theming/basics */
|
||||
|
||||
/* To quickly generate your own theme, check out the color generator */
|
||||
/* https://beta.ionicframework.com/docs/theming/color-generator */
|
136
angular/src/schematics/add/index.ts
Normal file
136
angular/src/schematics/add/index.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import {
|
||||
Rule,
|
||||
SchematicContext,
|
||||
SchematicsException,
|
||||
Tree,
|
||||
apply,
|
||||
chain,
|
||||
mergeWith,
|
||||
move,
|
||||
template,
|
||||
url
|
||||
} from '@angular-devkit/schematics';
|
||||
import { Path, join, normalize } from '@angular-devkit/core';
|
||||
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
|
||||
import { addPackageToPackageJson } from './../utils/package';
|
||||
import { addModuleImportToRootModule } from './../utils/ast';
|
||||
import { addStyle, getWorkspace, addArchitectBuilder } from './../utils/config';
|
||||
import { Schema as IonAddOptions } from './schema';
|
||||
|
||||
function addIonicAngularToPackageJson(): Rule {
|
||||
return (host: Tree) => {
|
||||
addPackageToPackageJson(host, 'dependencies', '@ionic/angular', 'latest');
|
||||
return host;
|
||||
};
|
||||
}
|
||||
|
||||
function addIonicAngularToolkitToPackageJson(): Rule {
|
||||
return (host: Tree) => {
|
||||
addPackageToPackageJson(
|
||||
host,
|
||||
'devDependencies',
|
||||
'@ionic/angular-toolkit',
|
||||
'latest'
|
||||
);
|
||||
return host;
|
||||
};
|
||||
}
|
||||
|
||||
function addIonicAngularModuleToAppModule(): Rule {
|
||||
return (host: Tree) => {
|
||||
addModuleImportToRootModule(
|
||||
host,
|
||||
'IonicModule.forRoot()',
|
||||
'@ionic/angular'
|
||||
);
|
||||
return host;
|
||||
};
|
||||
}
|
||||
|
||||
function addIonicStyles(): Rule {
|
||||
return (host: Tree) => {
|
||||
const ionicStyles = [
|
||||
'node_modules/@ionic/angular/css/normalize.css',
|
||||
'node_modules/@ionic/angular/css/structure.css',
|
||||
'node_modules/@ionic/angular/css/typography.css',
|
||||
'node_modules/@ionic/angular/css/core.css',
|
||||
'node_modules/@ionic/angular/css/padding.css',
|
||||
'node_modules/@ionic/angular/css/float-elements.css',
|
||||
'node_modules/@ionic/angular/css/text-alignment.css',
|
||||
'node_modules/@ionic/angular/css/text-transformation.css',
|
||||
'node_modules/@ionic/angular/css/flex-utils.css',
|
||||
'src/theme/variables.css'
|
||||
].forEach(entry => {
|
||||
addStyle(host, entry);
|
||||
});
|
||||
return host;
|
||||
};
|
||||
}
|
||||
|
||||
function addIonicBuilder(): Rule {
|
||||
return (host: Tree) => {
|
||||
addArchitectBuilder(host, 'ionic-cordova-serve', {
|
||||
builder: '@ionic/angular-toolkit:cordova-serve',
|
||||
options: {
|
||||
cordovaBuildTarget: 'app:ionic-cordova-build',
|
||||
devServerTarget: 'app:serve'
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
cordovaBuildTarget: 'app:ionic-cordova-build:production',
|
||||
devServerTarget: 'app:serve:production'
|
||||
}
|
||||
}
|
||||
});
|
||||
addArchitectBuilder(host, 'ionic-cordova-build', {
|
||||
builder: '@ionic/angular-toolkit:cordova-build',
|
||||
options: {
|
||||
browserTarget: 'app:build'
|
||||
},
|
||||
configurations: {
|
||||
production: {
|
||||
browserTarget: 'app:build:production'
|
||||
}
|
||||
}
|
||||
});
|
||||
return host;
|
||||
};
|
||||
}
|
||||
|
||||
function installNodeDeps() {
|
||||
return (host: Tree, context: SchematicContext) => {
|
||||
context.addTask(new NodePackageInstallTask());
|
||||
};
|
||||
}
|
||||
|
||||
export default function ngAdd(options: IonAddOptions): Rule {
|
||||
return (host: Tree) => {
|
||||
const workspace = getWorkspace(host);
|
||||
if (!options.project) {
|
||||
options.project = Object.keys(workspace.projects)[0];
|
||||
}
|
||||
const project = workspace.projects[options.project];
|
||||
if (project.projectType !== 'application') {
|
||||
throw new SchematicsException(
|
||||
`Ionic Add requires a project type of "application".`
|
||||
);
|
||||
}
|
||||
|
||||
const sourcePath = join(project.root as Path, 'src');
|
||||
const rootTemplateSource = apply(url('./files/root'), [
|
||||
template({ ...options }),
|
||||
move(sourcePath)
|
||||
]);
|
||||
return chain([
|
||||
// @ionic/angular
|
||||
addIonicAngularToPackageJson(),
|
||||
addIonicAngularToolkitToPackageJson(),
|
||||
addIonicAngularModuleToAppModule(),
|
||||
addIonicBuilder(),
|
||||
addIonicStyles(),
|
||||
mergeWith(rootTemplateSource),
|
||||
// install freshly added dependencies
|
||||
installNodeDeps()
|
||||
]);
|
||||
};
|
||||
}
|
3
angular/src/schematics/add/schema.d.ts
vendored
Normal file
3
angular/src/schematics/add/schema.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface Schema {
|
||||
project?: string;
|
||||
}
|
16
angular/src/schematics/add/schema.json
Normal file
16
angular/src/schematics/add/schema.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"id": "ionicNgAdd",
|
||||
"title": "Ionic Add options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The name of the project.",
|
||||
"$default": {
|
||||
"$source": "projectName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
10
angular/src/schematics/collection.json
Normal file
10
angular/src/schematics/collection.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
|
||||
"schematics": {
|
||||
"ng-add": {
|
||||
"description": "Add Ionic to your project",
|
||||
"factory": "./add",
|
||||
"schema": "./add/schema.json"
|
||||
}
|
||||
}
|
||||
}
|
65
angular/src/schematics/utils/ast.ts
Normal file
65
angular/src/schematics/utils/ast.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { SchematicsException, Tree } from '@angular-devkit/schematics';
|
||||
import { normalize } from '@angular-devkit/core';
|
||||
import * as ts from 'typescript';
|
||||
import { addImportToModule } from './devkit-utils/ast-utils';
|
||||
import { InsertChange } from './devkit-utils/change';
|
||||
|
||||
/**
|
||||
* Reads file given path and returns TypeScript source file.
|
||||
*/
|
||||
export function getSourceFile(host: Tree, path: string): ts.SourceFile {
|
||||
const buffer = host.read(path);
|
||||
if (!buffer) {
|
||||
throw new SchematicsException(`Could not find file for path: ${path}`);
|
||||
}
|
||||
const content = buffer.toString();
|
||||
const source = ts.createSourceFile(
|
||||
path,
|
||||
content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import and add module to root app module.
|
||||
*/
|
||||
export function addModuleImportToRootModule(
|
||||
host: Tree,
|
||||
moduleName: string,
|
||||
importSrc: string
|
||||
) {
|
||||
addModuleImportToModule(
|
||||
host,
|
||||
normalize(`src/app/app.module.ts`),
|
||||
moduleName,
|
||||
importSrc
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import and add module to specific module path.
|
||||
* @param host the tree we are updating
|
||||
* @param modulePath src location of the module to import
|
||||
* @param moduleName name of module to import
|
||||
* @param src src location to import
|
||||
*/
|
||||
export function addModuleImportToModule(
|
||||
host: Tree,
|
||||
modulePath: string,
|
||||
moduleName: string,
|
||||
src: string
|
||||
) {
|
||||
const moduleSource = getSourceFile(host, modulePath);
|
||||
const changes = addImportToModule(moduleSource, modulePath, moduleName, src);
|
||||
const recorder = host.beginUpdate(modulePath);
|
||||
|
||||
changes.forEach(change => {
|
||||
if (change instanceof InsertChange) {
|
||||
recorder.insertLeft(change.pos, change.toAdd);
|
||||
}
|
||||
});
|
||||
|
||||
host.commitUpdate(recorder);
|
||||
}
|
97
angular/src/schematics/utils/config.ts
Normal file
97
angular/src/schematics/utils/config.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { SchematicsException, Tree } from '@angular-devkit/schematics';
|
||||
import { experimental, parseJson, JsonParseMode } from '@angular-devkit/core';
|
||||
|
||||
const CONFIG_PATH = 'angular.json';
|
||||
|
||||
export function readConfig(host: Tree) {
|
||||
const sourceText = host.read(CONFIG_PATH)!.toString('utf-8');
|
||||
return JSON.parse(sourceText);
|
||||
}
|
||||
|
||||
export function writeConfig(host: Tree, config: JSON) {
|
||||
host.overwrite(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
function isAngularBrowserProject(projectConfig: any) {
|
||||
if (projectConfig.projectType === 'application') {
|
||||
const buildConfig = projectConfig.architect.build;
|
||||
return buildConfig.builder === '@angular-devkit/build-angular:browser';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getAngularAppName(config: any): string | null {
|
||||
const projects = config.projects;
|
||||
const projectNames = Object.keys(projects);
|
||||
|
||||
for (const projectName of projectNames) {
|
||||
const projectConfig = projects[projectName];
|
||||
if (isAngularBrowserProject(projectConfig)) {
|
||||
return projectName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAngularAppConfig(config: any): any | null {
|
||||
const projects = config.projects;
|
||||
const projectNames = Object.keys(projects);
|
||||
|
||||
for (const projectName of projectNames) {
|
||||
const projectConfig = projects[projectName];
|
||||
if (isAngularBrowserProject(projectConfig)) {
|
||||
return projectConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function addStyle(host: Tree, stylePath: string) {
|
||||
const config = readConfig(host);
|
||||
const appConfig = getAngularAppConfig(config);
|
||||
|
||||
if (appConfig) {
|
||||
appConfig.architect.build.options.styles.push({
|
||||
input: stylePath
|
||||
});
|
||||
|
||||
writeConfig(host, config);
|
||||
} else {
|
||||
throw new SchematicsException(`Cannot find valid app`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addArchitectBuilder(host: Tree, builderName: string, builderOpts: any){
|
||||
const config = readConfig(host);
|
||||
const appConfig = getAngularAppConfig(config);
|
||||
|
||||
if (appConfig) {
|
||||
appConfig.architect[builderName] = builderOpts
|
||||
writeConfig(host, config);
|
||||
} else {
|
||||
throw new SchematicsException(`Cannot find valid app`);
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkspaceSchema = experimental.workspace.WorkspaceSchema;
|
||||
|
||||
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 getWorkspace(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;
|
||||
}
|
571
angular/src/schematics/utils/devkit-utils/ast-utils.ts
Normal file
571
angular/src/schematics/utils/devkit-utils/ast-utils.ts
Normal file
@ -0,0 +1,571 @@
|
||||
/**
|
||||
* @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(child => child.kind === ts.SyntaxKind.StringLiteral)
|
||||
.map(n => (n as ts.StringLiteral).text);
|
||||
|
||||
return importFiles.filter(file => file === fileName).length === 1;
|
||||
});
|
||||
|
||||
if (relevantImports.length > 0) {
|
||||
let importsAsterisk = false;
|
||||
// imports from import file
|
||||
const imports: ts.Node[] = [];
|
||||
relevantImports.forEach(n => {
|
||||
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
|
||||
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
|
||||
importsAsterisk = true;
|
||||
}
|
||||
});
|
||||
|
||||
// if imports * from fileName, don't add symbolName
|
||||
if (importsAsterisk) {
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName);
|
||||
|
||||
// insert import if it's not there
|
||||
if (importTextNodes.length === 0) {
|
||||
const fallbackPos =
|
||||
findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() ||
|
||||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
|
||||
|
||||
return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos);
|
||||
}
|
||||
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
// no such import declaration exists
|
||||
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral)
|
||||
.filter((n: ts.StringLiteral) => n.text === 'use strict');
|
||||
let fallbackPos = 0;
|
||||
if (useStrict.length > 0) {
|
||||
fallbackPos = useStrict[0].end;
|
||||
}
|
||||
const open = isDefault ? '' : '{ ';
|
||||
const close = isDefault ? '' : ' }';
|
||||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
||||
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
|
||||
const separator = insertAtBeginning ? '' : ';\n';
|
||||
const toInsert = `${separator}import ${open}${symbolName}${close}` +
|
||||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
|
||||
|
||||
return insertAfterLastOccurrence(
|
||||
allImports,
|
||||
toInsert,
|
||||
fileToEdit,
|
||||
fallbackPos,
|
||||
ts.SyntaxKind.StringLiteral,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
|
||||
* @param node
|
||||
* @param kind
|
||||
* @param max The maximum number of items to return.
|
||||
* @return all nodes of kind, or [] if none is found
|
||||
*/
|
||||
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): ts.Node[] {
|
||||
if (!node || max == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const arr: ts.Node[] = [];
|
||||
if (node.kind === kind) {
|
||||
arr.push(node);
|
||||
max--;
|
||||
}
|
||||
if (max > 0) {
|
||||
for (const child of node.getChildren()) {
|
||||
findNodes(child, kind, max).forEach(node => {
|
||||
if (max > 0) {
|
||||
arr.push(node);
|
||||
}
|
||||
max--;
|
||||
});
|
||||
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the nodes from a source.
|
||||
* @param sourceFile The source file object.
|
||||
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
|
||||
*/
|
||||
export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
|
||||
const nodes: ts.Node[] = [sourceFile];
|
||||
const result = [];
|
||||
|
||||
while (nodes.length > 0) {
|
||||
const node = nodes.shift();
|
||||
|
||||
if (node) {
|
||||
result.push(node);
|
||||
if (node.getChildCount(sourceFile) >= 0) {
|
||||
nodes.unshift(...node.getChildren());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null {
|
||||
if (node.kind === kind && node.getText() === text) {
|
||||
// throw new Error(node.getText());
|
||||
return node;
|
||||
}
|
||||
|
||||
let foundNode: ts.Node | null = null;
|
||||
ts.forEachChild(node, childNode => {
|
||||
foundNode = foundNode || findNode(childNode, kind, text);
|
||||
});
|
||||
|
||||
return foundNode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper for sorting nodes.
|
||||
* @return function to sort nodes in increasing order of position in sourceFile
|
||||
*/
|
||||
function nodesByPosition(first: ts.Node, second: ts.Node): number {
|
||||
return first.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 {
|
||||
// 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) {
|
||||
throw new Error();
|
||||
}
|
||||
if (syntaxKind) {
|
||||
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
|
||||
}
|
||||
if (!lastItem && fallbackPos == undefined) {
|
||||
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
|
||||
}
|
||||
const lastItemPosition: number = lastItem ? lastItem.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: {[name: string]: string}
|
||||
= findNodes(source, ts.SyntaxKind.ImportDeclaration)
|
||||
.map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source))
|
||||
.reduce((acc: {[name: string]: string}, current: {[name: string]: string}) => {
|
||||
for (const key of Object.keys(current)) {
|
||||
acc[key] = current[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return getSourceNodes(source)
|
||||
.filter(node => {
|
||||
return node.kind == ts.SyntaxKind.Decorator
|
||||
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
|
||||
})
|
||||
.map(node => (node as ts.Decorator).expression as ts.CallExpression)
|
||||
.filter(expr => {
|
||||
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
|
||||
const id = expr.expression as ts.Identifier;
|
||||
|
||||
return id.getFullText(source) == identifier
|
||||
&& angularImports[id.getFullText(source)] === module;
|
||||
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
|
||||
// This covers foo.NgModule when importing * as foo.
|
||||
const paExpr = expr.expression as ts.PropertyAccessExpression;
|
||||
// If the left expression is not an identifier, just give up at that point.
|
||||
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = paExpr.name.text;
|
||||
const moduleId = (paExpr.expression as ts.Identifier).getText(source);
|
||||
|
||||
return id === identifier && (angularImports[moduleId + '.'] === module);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.filter(expr => expr.arguments[0]
|
||||
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
|
||||
.map(expr => expr.arguments[0] as ts.ObjectLiteralExpression);
|
||||
}
|
||||
|
||||
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 addSymbolToNgModuleMetadata(
|
||||
source: ts.SourceFile,
|
||||
ngModulePath: string,
|
||||
metadataField: string,
|
||||
symbolName: string,
|
||||
importPath: string | null = null,
|
||||
): Change[] {
|
||||
const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
|
||||
let node: any = nodes[0]; // tslint:disable-line:no-any
|
||||
|
||||
// Find the decorator declaration.
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all the children property assignment of object literals.
|
||||
const matchingProperties: ts.ObjectLiteralElement[] =
|
||||
(node as ts.ObjectLiteralExpression).properties
|
||||
.filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment)
|
||||
// Filter out every fields that's not "metadataField". Also handles string literals
|
||||
// (but not expressions).
|
||||
.filter((prop: ts.PropertyAssignment) => {
|
||||
const name = prop.name;
|
||||
switch (name.kind) {
|
||||
case ts.SyntaxKind.Identifier:
|
||||
return (name as ts.Identifier).getText(source) == metadataField;
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
return (name as ts.StringLiteral).text == metadataField;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Get the last node of the array literal.
|
||||
if (!matchingProperties) {
|
||||
return [];
|
||||
}
|
||||
if (matchingProperties.length == 0) {
|
||||
// We haven't found the field in the metadata declaration. Insert a new field.
|
||||
const expr = node as ts.ObjectLiteralExpression;
|
||||
let position: number;
|
||||
let toInsert: string;
|
||||
if (expr.properties.length == 0) {
|
||||
position = expr.getEnd() - 1;
|
||||
toInsert = ` ${metadataField}: [${symbolName}]\n`;
|
||||
} else {
|
||||
node = expr.properties[expr.properties.length - 1];
|
||||
position = node.getEnd();
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
const matches = text.match(/^\r?\n\s*/);
|
||||
if (matches.length > 0) {
|
||||
toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
|
||||
} else {
|
||||
toInsert = `, ${metadataField}: [${symbolName}]`;
|
||||
}
|
||||
}
|
||||
if (importPath !== null) {
|
||||
return [
|
||||
new InsertChange(ngModulePath, position, toInsert),
|
||||
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
|
||||
];
|
||||
} else {
|
||||
return [new InsertChange(ngModulePath, position, toInsert)];
|
||||
}
|
||||
}
|
||||
const assignment = matchingProperties[0] as ts.PropertyAssignment;
|
||||
|
||||
// If it's not an array, nothing we can do really.
|
||||
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
|
||||
if (arrLiteral.elements.length == 0) {
|
||||
// Forward the property.
|
||||
node = arrLiteral;
|
||||
} else {
|
||||
node = arrLiteral.elements;
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
console.log('No app module found. Please add your new class to your component.');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
const nodeArray = node as {} as Array<ts.Node>;
|
||||
const symbolsArray = nodeArray.map(node => node.getText());
|
||||
if (symbolsArray.includes(symbolName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
node = node[node.length - 1];
|
||||
}
|
||||
|
||||
let toInsert: string;
|
||||
let position = node.getEnd();
|
||||
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
|
||||
// We haven't found the field in the metadata declaration. Insert a new
|
||||
// field.
|
||||
const expr = node as ts.ObjectLiteralExpression;
|
||||
if (expr.properties.length == 0) {
|
||||
position = expr.getEnd() - 1;
|
||||
toInsert = ` ${metadataField}: [${symbolName}]\n`;
|
||||
} else {
|
||||
node = expr.properties[expr.properties.length - 1];
|
||||
position = node.getEnd();
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.match('^\r?\r?\n')) {
|
||||
toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${symbolName}]`;
|
||||
} else {
|
||||
toInsert = `, ${metadataField}: [${symbolName}]`;
|
||||
}
|
||||
}
|
||||
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
|
||||
// We found the field but it's empty. Insert it just before the `]`.
|
||||
position--;
|
||||
toInsert = `${symbolName}`;
|
||||
} else {
|
||||
// Get the indentation of the last element, if any.
|
||||
const text = node.getFullText(source);
|
||||
if (text.match(/^\r?\n/)) {
|
||||
toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`;
|
||||
} else {
|
||||
toInsert = `, ${symbolName}`;
|
||||
}
|
||||
}
|
||||
if (importPath !== null) {
|
||||
return [
|
||||
new InsertChange(ngModulePath, position, toInsert),
|
||||
insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath),
|
||||
];
|
||||
}
|
||||
|
||||
return [new InsertChange(ngModulePath, position, toInsert)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a declaration (component, pipe, directive)
|
||||
* into NgModule declarations. It also imports the component.
|
||||
*/
|
||||
export function addDeclarationToModule(source: ts.SourceFile,
|
||||
modulePath: string, classifiedName: string,
|
||||
importPath: string): Change[] {
|
||||
return addSymbolToNgModuleMetadata(
|
||||
source, modulePath, 'declarations', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert an NgModule into NgModule imports. It also imports the module.
|
||||
*/
|
||||
export function addImportToModule(source: ts.SourceFile,
|
||||
modulePath: string, classifiedName: string,
|
||||
importPath: string): Change[] {
|
||||
|
||||
return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert a provider into NgModule. It also imports it.
|
||||
*/
|
||||
export function addProviderToModule(source: ts.SourceFile,
|
||||
modulePath: string, classifiedName: string,
|
||||
importPath: string): Change[] {
|
||||
return addSymbolToNgModuleMetadata(source, modulePath, 'providers', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert an export into NgModule. It also imports it.
|
||||
*/
|
||||
export function addExportToModule(source: ts.SourceFile,
|
||||
modulePath: string, classifiedName: string,
|
||||
importPath: string): Change[] {
|
||||
return addSymbolToNgModuleMetadata(source, modulePath, 'exports', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom function to insert an export into NgModule. It also imports it.
|
||||
*/
|
||||
export function addBootstrapToModule(source: ts.SourceFile,
|
||||
modulePath: string, classifiedName: string,
|
||||
importPath: string): Change[] {
|
||||
return addSymbolToNgModuleMetadata(source, modulePath, 'bootstrap', classifiedName, importPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an import already exists.
|
||||
*/
|
||||
export function isImported(source: ts.SourceFile,
|
||||
classifiedName: string,
|
||||
importPath: string): boolean {
|
||||
const allNodes = getSourceNodes(source);
|
||||
const matchingNodes = allNodes
|
||||
.filter(node => node.kind === ts.SyntaxKind.ImportDeclaration)
|
||||
.filter((imp: ts.ImportDeclaration) => imp.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral)
|
||||
.filter((imp: ts.ImportDeclaration) => {
|
||||
return (<ts.StringLiteral> imp.moduleSpecifier).text === importPath;
|
||||
})
|
||||
.filter((imp: ts.ImportDeclaration) => {
|
||||
if (!imp.importClause) {
|
||||
return false;
|
||||
}
|
||||
const nodes = findNodes(imp.importClause, ts.SyntaxKind.ImportSpecifier)
|
||||
.filter(n => n.getText() === classifiedName);
|
||||
|
||||
return nodes.length > 0;
|
||||
});
|
||||
|
||||
return matchingNodes.length > 0;
|
||||
}
|
127
angular/src/schematics/utils/devkit-utils/change.ts
Normal file
127
angular/src/schematics/utils/devkit-utils/change.ts
Normal file
@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
7
angular/src/schematics/utils/devkit-utils/readme.md
Normal file
7
angular/src/schematics/utils/devkit-utils/readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
### Devkit Utils
|
||||
|
||||
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.
|
||||
|
||||
|
110
angular/src/schematics/utils/devkit-utils/route-utils.ts
Normal file
110
angular/src/schematics/utils/devkit-utils/route-utils.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import { findNodes, insertAfterLastOccurrence } from './ast-utils';
|
||||
import { Change, NoopChange } from './change';
|
||||
|
||||
/**
|
||||
* Add Import `import { symbolName } from fileName` if the import doesn't exit
|
||||
* already. Assumes fileToEdit can be resolved and accessed.
|
||||
* @param fileToEdit (file we want to add import to)
|
||||
* @param symbolName (item to import)
|
||||
* @param fileName (path to the file)
|
||||
* @param isDefault (if true, import follows style for importing default exports)
|
||||
* @return Change
|
||||
*/
|
||||
|
||||
export function insertImport(
|
||||
source: ts.SourceFile,
|
||||
fileToEdit: string,
|
||||
symbolName: string,
|
||||
fileName: string,
|
||||
isDefault = false
|
||||
): Change {
|
||||
const rootNode = source;
|
||||
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||
|
||||
// get nodes that map to import statements from the file fileName
|
||||
const relevantImports = allImports.filter(node => {
|
||||
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
|
||||
const importFiles = node
|
||||
.getChildren()
|
||||
.filter(child => child.kind === ts.SyntaxKind.StringLiteral)
|
||||
.map(n => (n as ts.StringLiteral).text);
|
||||
|
||||
return importFiles.filter(file => file === fileName).length === 1;
|
||||
});
|
||||
|
||||
if (relevantImports.length > 0) {
|
||||
let importsAsterisk = false;
|
||||
// imports from import file
|
||||
const imports: ts.Node[] = [];
|
||||
relevantImports.forEach(n => {
|
||||
Array.prototype.push.apply(
|
||||
imports,
|
||||
findNodes(n, ts.SyntaxKind.Identifier)
|
||||
);
|
||||
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
|
||||
importsAsterisk = true;
|
||||
}
|
||||
});
|
||||
|
||||
// if imports * from fileName, don't add symbolName
|
||||
if (importsAsterisk) {
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
const importTextNodes = imports.filter(
|
||||
n => (n as ts.Identifier).text === symbolName
|
||||
);
|
||||
|
||||
// insert import if it's not there
|
||||
if (importTextNodes.length === 0) {
|
||||
const fallbackPos =
|
||||
findNodes(
|
||||
relevantImports[0],
|
||||
ts.SyntaxKind.CloseBraceToken
|
||||
)[0].getStart() ||
|
||||
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
|
||||
|
||||
return insertAfterLastOccurrence(
|
||||
imports,
|
||||
`, ${symbolName}`,
|
||||
fileToEdit,
|
||||
fallbackPos
|
||||
);
|
||||
}
|
||||
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
// no such import declaration exists
|
||||
const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
|
||||
(n: ts.StringLiteral) => n.text === 'use strict'
|
||||
);
|
||||
let fallbackPos = 0;
|
||||
if (useStrict.length > 0) {
|
||||
fallbackPos = useStrict[0].end;
|
||||
}
|
||||
const open = isDefault ? '' : '{ ';
|
||||
const close = isDefault ? '' : ' }';
|
||||
// if there are no imports or 'use strict' statement, insert import at beginning of file
|
||||
const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
|
||||
const separator = insertAtBeginning ? '' : ';\n';
|
||||
const toInsert =
|
||||
`${separator}import ${open}${symbolName}${close}` +
|
||||
` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
|
||||
|
||||
return insertAfterLastOccurrence(
|
||||
allImports,
|
||||
toInsert,
|
||||
fileToEdit,
|
||||
fallbackPos,
|
||||
ts.SyntaxKind.StringLiteral
|
||||
);
|
||||
}
|
22
angular/src/schematics/utils/package.ts
Normal file
22
angular/src/schematics/utils/package.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {Tree} from '@angular-devkit/schematics';
|
||||
|
||||
/**
|
||||
* Adds a package to the package.json
|
||||
*/
|
||||
export function addPackageToPackageJson(host: Tree, type: string, pkg: string, version: string) {
|
||||
if (host.exists('package.json')) {
|
||||
const sourceText = host.read('package.json')!.toString('utf-8');
|
||||
const json = JSON.parse(sourceText);
|
||||
if (!json[type]) {
|
||||
json[type] = {};
|
||||
}
|
||||
|
||||
if (!json[type][pkg]) {
|
||||
json[type][pkg] = version;
|
||||
}
|
||||
|
||||
host.overwrite('package.json', JSON.stringify(json, null, 2));
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
Reference in New Issue
Block a user