implement ngrx generation
This commit is contained in:
parent
b97520fd9c
commit
cdb115ddb8
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.idea
|
.idea
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
|
.DS_Store
|
||||||
|
tmp
|
||||||
|
|||||||
@ -1,12 +1,84 @@
|
|||||||
import {cleanup, runCLI, runSchematic} from './utils';
|
import {
|
||||||
|
addNgRx, checkFilesExists, cleanup, newApp, readFile, runCLI, runCommand, runSchematic,
|
||||||
|
updateFile
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
describe('addNgRxToModule', () => {
|
describe('addNgRxToModule', () => {
|
||||||
beforeEach(cleanup);
|
beforeEach(cleanup);
|
||||||
|
|
||||||
it('should add ngrx to module', () => {
|
it('should add root configuration', () => {
|
||||||
runCLI('new proj --skip-install ');
|
newApp('new proj --skipInstall');
|
||||||
runSchematic('@nrwl/ext:addNgRxToModule', {cwd: 'proj'});
|
runSchematic('@nrwl/ext:addNgRxToModule --module=src/app/app.module.ts --root', {cwd: 'proj'});
|
||||||
|
|
||||||
|
checkFilesExists(
|
||||||
|
`proj/src/app/+state/app.actions.ts`,
|
||||||
|
`proj/src/app/+state/app.effects.ts`,
|
||||||
|
`proj/src/app/+state/app.effects.spec.ts`,
|
||||||
|
`proj/src/app/+state/app.init.ts`,
|
||||||
|
`proj/src/app/+state/app.interfaces.ts`,
|
||||||
|
`proj/src/app/+state/app.reducer.ts`,
|
||||||
|
`proj/src/app/+state/app.reducer.spec.ts`
|
||||||
|
);
|
||||||
|
|
||||||
|
const contents = readFile('proj/src/app/app.module.ts');
|
||||||
|
expect(contents).toContain('StoreModule.forRoot');
|
||||||
|
expect(contents).toContain('EffectsModule.forRoot');
|
||||||
|
|
||||||
|
addNgRx('proj');
|
||||||
|
|
||||||
|
runCLI('build', {cwd: 'proj'});
|
||||||
|
runCLI('test --single-run', {cwd: 'proj'});
|
||||||
|
|
||||||
expect(1).toEqual(2);
|
|
||||||
}, 50000);
|
}, 50000);
|
||||||
|
|
||||||
|
it('should add empty root configuration', () => {
|
||||||
|
newApp('new proj2 --skipInstall');
|
||||||
|
runSchematic('@nrwl/ext:addNgRxToModule --module=src/app/app.module.ts --emptyRoot', {cwd: 'proj2'});
|
||||||
|
|
||||||
|
const contents = readFile('proj2/src/app/app.module.ts');
|
||||||
|
expect(contents).toContain('StoreModule.forRoot');
|
||||||
|
expect(contents).not.toContain('EffectsModule.forRoot');
|
||||||
|
|
||||||
|
addNgRx('proj2');
|
||||||
|
|
||||||
|
runCLI('build', {cwd: 'proj2'});
|
||||||
|
}, 50000);
|
||||||
|
|
||||||
|
it('should add feature configuration', () => {
|
||||||
|
newApp('new proj3 --skipInstall');
|
||||||
|
runSchematic('@nrwl/ext:addNgRxToModule --module=src/app/app.module.ts', {cwd: 'proj3'});
|
||||||
|
|
||||||
|
checkFilesExists(
|
||||||
|
`proj3/src/app/+state/app.actions.ts`,
|
||||||
|
`proj3/src/app/+state/app.effects.ts`,
|
||||||
|
`proj3/src/app/+state/app.effects.spec.ts`,
|
||||||
|
`proj3/src/app/+state/app.init.ts`,
|
||||||
|
`proj3/src/app/+state/app.interfaces.ts`,
|
||||||
|
`proj3/src/app/+state/app.reducer.ts`,
|
||||||
|
`proj3/src/app/+state/app.reducer.spec.ts`
|
||||||
|
);
|
||||||
|
|
||||||
|
const contents = readFile('proj3/src/app/app.module.ts');
|
||||||
|
expect(contents).toContain('StoreModule.forFeature');
|
||||||
|
expect(contents).toContain('EffectsModule.forFeature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate files without importing them', () => {
|
||||||
|
newApp('new proj4 --skipInstall');
|
||||||
|
runSchematic('@nrwl/ext:addNgRxToModule --module=src/app/app.module.ts --skipImport', {cwd: 'proj4'});
|
||||||
|
|
||||||
|
checkFilesExists(
|
||||||
|
`proj4/src/app/+state/app.actions.ts`,
|
||||||
|
`proj4/src/app/+state/app.effects.ts`,
|
||||||
|
`proj4/src/app/+state/app.effects.spec.ts`,
|
||||||
|
`proj4/src/app/+state/app.init.ts`,
|
||||||
|
`proj4/src/app/+state/app.interfaces.ts`,
|
||||||
|
`proj4/src/app/+state/app.reducer.ts`,
|
||||||
|
`proj4/src/app/+state/app.reducer.spec.ts`
|
||||||
|
);
|
||||||
|
|
||||||
|
const contents = readFile('proj4/src/app/app.module.ts');
|
||||||
|
expect(contents).not.toContain('StoreModule');
|
||||||
|
expect(contents).not.toContain('EffectsModule');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
80
e2e/utils.ts
80
e2e/utils.ts
@ -1,32 +1,74 @@
|
|||||||
import {execSync} from 'child_process';
|
import {execSync} from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import {readFileSync, statSync, writeFileSync} from 'fs';
|
||||||
|
|
||||||
export function runCLI(command: string, {cwd}: {cwd?: string} = {}): string {
|
export function newApp(command: string): string {
|
||||||
cwd = cwd === undefined ? '' : cwd;
|
return execSync(`../node_modules/.bin/ng ${command}`, {cwd: `./tmp`}).toString();
|
||||||
return execSync(`../node_modules/.bin/ng ${command}`, {cwd: `./tmp/${cwd}`}).toString();
|
|
||||||
}
|
}
|
||||||
export function runSchematic(command: string, {cwd}: {cwd?: string} = {}): string {
|
export function runCLI(command: string, {cwd}: {cwd: string}): string {
|
||||||
|
cwd = cwd === undefined ? '' : cwd;
|
||||||
|
return execSync(`../../node_modules/.bin/ng ${command}`, {cwd: `./tmp/${cwd}`}).toString();
|
||||||
|
}
|
||||||
|
export function runSchematic(command: string, {cwd}: {cwd: string}): string {
|
||||||
cwd = cwd === undefined ? '' : cwd;
|
cwd = cwd === undefined ? '' : cwd;
|
||||||
return execSync(`../../node_modules/.bin/schematics ${command}`, {cwd: `./tmp/${cwd}`}).toString();
|
return execSync(`../../node_modules/.bin/schematics ${command}`, {cwd: `./tmp/${cwd}`}).toString();
|
||||||
}
|
}
|
||||||
|
export function runCommand(command: string, {cwd}: {cwd: string}): string {
|
||||||
|
cwd = cwd === undefined ? '' : cwd;
|
||||||
|
return execSync(command, {cwd: `./tmp/${cwd}`}).toString();
|
||||||
|
}
|
||||||
|
|
||||||
// export function updateFile(f: string, content: string): void {
|
export function updateFile(f: string, content: string): void {
|
||||||
// writeFileSync(path.join(files.getCwd(), 'tmp', f), content);
|
writeFileSync(path.join(getCwd(), 'tmp', f), content);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// export function checkFilesExists(...expectedFiles: string[]) {
|
export function checkFilesExists(...expectedFiles: string[]) {
|
||||||
// expectedFiles.forEach(f => {
|
expectedFiles.forEach(f => {
|
||||||
// const ff = f.startsWith('/') ? f : path.join(files.getCwd(), 'tmp', f);
|
const ff = f.startsWith('/') ? f : path.join(getCwd(), 'tmp', f);
|
||||||
// if (! files.exists(ff)) {
|
if (! exists(ff)) {
|
||||||
// throw new Error(`File '${ff}' does not exist`);
|
throw new Error(`File '${ff}' does not exist`);
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
// export function readFile(f: string) {
|
export function readFile(f: string) {
|
||||||
// const ff = f.startsWith('/') ? f : path.join(files.getCwd(), 'tmp', f);
|
const ff = f.startsWith('/') ? f : path.join(getCwd(), 'tmp', f);
|
||||||
// return readFileSync(ff).toString();
|
return readFileSync(ff).toString();
|
||||||
// }
|
}
|
||||||
|
|
||||||
export function cleanup() {
|
export function cleanup() {
|
||||||
execSync('rm -rf tmp && mkdir tmp');
|
execSync('rm -rf tmp && mkdir tmp');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCwd(): string {
|
||||||
|
return process.cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directoryExists(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(filePath).isDirectory();
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileExists(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
return statSync(filePath).isFile();
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exists(filePath: string): boolean {
|
||||||
|
return directoryExists(filePath) || fileExists(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addNgRx(path: string): string {
|
||||||
|
const p = JSON.parse(readFile(`${path}/package.json`));
|
||||||
|
p['dependencies']['@ngrx/store'] = '4.0.2';
|
||||||
|
p['dependencies']['@ngrx/effects'] = '4.0.2';
|
||||||
|
p['dependencies']['jasmine-marbles'] = '0.1.0';
|
||||||
|
updateFile(`${path}/package.json`, JSON.stringify(p, null, 2));
|
||||||
|
return runCommand('npm install', {cwd: path});
|
||||||
|
}
|
||||||
|
|||||||
@ -7,12 +7,16 @@
|
|||||||
"build": "./scripts/build.sh",
|
"build": "./scripts/build.sh",
|
||||||
"e2e": "yarn build && ./scripts/e2e.sh"
|
"e2e": "yarn build && ./scripts/e2e.sh"
|
||||||
},
|
},
|
||||||
|
"dependencies" :{
|
||||||
|
"rxjs": "5.4.2",
|
||||||
|
"jasmine-marbles": "0.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "2.4.2",
|
"typescript": "2.4.2",
|
||||||
"@types/node": "8.0.7",
|
"@types/node": "8.0.7",
|
||||||
"@types/jasmine": "2.5.53",
|
"@types/jasmine": "2.5.53",
|
||||||
"@angular-devkit/core": "0.0.10",
|
|
||||||
"@angular-devkit/schematics": "0.0.16",
|
"@angular-devkit/schematics": "0.0.16",
|
||||||
|
"@angular/cli": "1.3.0",
|
||||||
"jest": "20.0.4"
|
"jest": "20.0.4"
|
||||||
},
|
},
|
||||||
"author": "Victor Savkin",
|
"author": "Victor Savkin",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
rm -rf build
|
rm -rf build
|
||||||
rm -rf tmp
|
|
||||||
tsc
|
tsc
|
||||||
rsync -a --exclude=*.ts src/ build/src
|
rsync -a --exclude=*.ts src/ build/src
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
export { readAll } from './utils/testing';
|
||||||
|
export { hot, cold } from 'jasmine-marbles';
|
||||||
@ -1,15 +1,15 @@
|
|||||||
import {TalksAndFiltersEffects} from './talks-and-filters.effects';
|
import {Actions} from '@ngrx/effects';
|
||||||
import {readAll} from '@nrwl/ext';
|
import {readAll, hot} from '@nrwl/ext';
|
||||||
|
import {<%= className %>Effects} from './<%= fileName %>.effects';
|
||||||
import {of} from 'rxjs/observable/of';
|
import {of} from 'rxjs/observable/of';
|
||||||
import {hot} from 'jasmine-marbles';
|
|
||||||
|
|
||||||
describe('<%= className %>Effects', () => {
|
describe('<%= className %>Effects', () => {
|
||||||
describe('someEffect', () => {
|
describe('someEffect', () => {
|
||||||
it('should work', async () => {
|
it('should work', async () => {
|
||||||
const actions = new Actions(hot('-a-|', {a: {type: 'SOME_ACTION'}}));
|
const actions = new Actions(hot('-a-|', {a: {type: 'SOME_ACTION'}}));
|
||||||
const effects = new <%= className %>Effects();
|
const effects = new <%= className %>Effects(actions);
|
||||||
|
|
||||||
expect(await readAll(effects.navigateToTalk)).toEqual([
|
expect(await readAll(effects.someEffect)).toEqual([
|
||||||
{type: 'OTHER_ACTION'}
|
{type: 'OTHER_ACTION'}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Effect, Actions} from '@ngrx/effects';
|
import {Effect, Actions} from '@ngrx/effects';
|
||||||
import {of} from 'rxjs/observable/of';
|
import {of} from 'rxjs/observable/of';
|
||||||
|
import 'rxjs/add/operator/switchMap';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class <%= className %>Effects {
|
export class <%= className %>Effects {
|
||||||
|
|||||||
@ -1,15 +1,95 @@
|
|||||||
import {apply, branchAndMerge, chain, mergeWith, Rule, template, Tree, url} from '@angular-devkit/schematics';
|
import {apply, branchAndMerge, chain, mergeWith, move, Rule, template, Tree, url} from '@angular-devkit/schematics';
|
||||||
import { names } from "../name-utils";
|
|
||||||
|
import {names, toClassName, toFileName, toPropertyName} from "../name-utils";
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import {addImportToModule, addProviderToModule} from '../utility/ast-utils';
|
||||||
|
import {InsertChange} from '../utility/change';
|
||||||
|
import {insertImport} from '../utility/route-utils';
|
||||||
|
|
||||||
|
function addImportsToModule(name: string, options: any): Rule {
|
||||||
|
return (host: Tree) => {
|
||||||
|
if (options.skipImport) {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!host.exists(options.module)) {
|
||||||
|
throw new Error('Specified module does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const modulePath = options.module;
|
||||||
|
|
||||||
|
const sourceText = host.read(modulePath) !.toString('utf-8');
|
||||||
|
const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true);
|
||||||
|
|
||||||
|
if (options.emptyRoot) {
|
||||||
|
const reducer = `StoreModule.forRoot({})`;
|
||||||
|
const changes = [
|
||||||
|
insertImport(source, modulePath, 'StoreModule', '@ngrx/store'),
|
||||||
|
...addImportToModule(source, modulePath, reducer)
|
||||||
|
];
|
||||||
|
const declarationRecorder = host.beginUpdate(modulePath);
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change instanceof InsertChange) {
|
||||||
|
declarationRecorder.insertLeft(change.pos, change.toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host.commitUpdate(declarationRecorder);
|
||||||
|
return host;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const reducerPath = `./+state/${toFileName(name)}.reducer`;
|
||||||
|
const effectsPath = `./+state/${toFileName(name)}.effects`;
|
||||||
|
const initPath = `./+state/${toFileName(name)}.init`;
|
||||||
|
|
||||||
|
const reducerName = `${toPropertyName(name)}Reducer`;
|
||||||
|
const effectsName = `${toClassName(name)}Effects`;
|
||||||
|
const initName = `${toPropertyName(name)}InitialState`;
|
||||||
|
|
||||||
|
const effects = options.root ? `EffectsModule.forRoot([${effectsName}])` : `EffectsModule.forFeature([${effectsName}])`;
|
||||||
|
const reducer = options.root ? `StoreModule.forRoot(${reducerName}, {initialState: ${initName}})` : `StoreModule.forFeature('${toPropertyName(name)}', ${reducerName}, {initialState: ${initName}})`;
|
||||||
|
|
||||||
|
const changes = [
|
||||||
|
insertImport(source, modulePath, 'StoreModule', '@ngrx/store'),
|
||||||
|
insertImport(source, modulePath, 'EffectsModule', '@ngrx/effects'),
|
||||||
|
insertImport(source, modulePath, reducerName, reducerPath),
|
||||||
|
insertImport(source, modulePath, initName, initPath),
|
||||||
|
insertImport(source, modulePath, effectsName, effectsPath),
|
||||||
|
...addImportToModule(source, modulePath, reducer),
|
||||||
|
...addImportToModule(source, modulePath, effects),
|
||||||
|
...addProviderToModule(source, modulePath, effectsName)
|
||||||
|
];
|
||||||
|
const declarationRecorder = host.beginUpdate(modulePath);
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change instanceof InsertChange) {
|
||||||
|
declarationRecorder.insertLeft(change.pos, change.toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host.commitUpdate(declarationRecorder);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function (options: any): Rule {
|
export default function (options: any): Rule {
|
||||||
|
const name = path.basename(options.module, ".module.ts");
|
||||||
|
const moduleDir = path.dirname(options.module);
|
||||||
|
|
||||||
const templateSource = apply(url('./files'), [
|
if (options.emptyRoot) {
|
||||||
template({...options, tmpl: '', ...names('some')})
|
return chain([
|
||||||
]);
|
addImportsToModule(name, options)
|
||||||
|
]);
|
||||||
return chain([
|
} else {
|
||||||
branchAndMerge(chain([
|
const templateSource = apply(url('./files'), [
|
||||||
mergeWith(templateSource)
|
template({...options, tmpl: '', ...names(name)}),
|
||||||
])),
|
move(moduleDir)
|
||||||
]);
|
]);
|
||||||
|
return chain([
|
||||||
|
branchAndMerge(chain([
|
||||||
|
mergeWith(templateSource)
|
||||||
|
])),
|
||||||
|
addImportsToModule(name, options)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,20 @@
|
|||||||
"title": "Add NgRx support to a module",
|
"title": "Add NgRx support to a module",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"path": {
|
"module": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"skipImport": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"emptyRoot": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"path"
|
"module"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,17 @@ export function names(name: string): any {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toClassName(str: string): string {
|
export function toClassName(str: string): string {
|
||||||
return toCapitalCase(toPropertyName(str));
|
return toCapitalCase(toPropertyName(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPropertyName(s: string): string {
|
export function toPropertyName(s: string): string {
|
||||||
return s
|
return s
|
||||||
.replace(/(-|_|\.|\s)+(.)?/g, (_, __, chr) => chr ? chr.toUpperCase() : '')
|
.replace(/(-|_|\.|\s)+(.)?/g, (_, __, chr) => chr ? chr.toUpperCase() : '')
|
||||||
.replace(/^([A-Z])/, m => m.toLowerCase());
|
.replace(/^([A-Z])/, m => m.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function toFileName(s: string): string {
|
export function toFileName(s: string): string {
|
||||||
return s.replace(/([a-z\d])([A-Z])/g, '$1_$2').toLowerCase().replace(/[ _]/g, '-');
|
return s.replace(/([a-z\d])([A-Z])/g, '$1_$2').toLowerCase().replace(/[ _]/g, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
353
src/schematics/utility/ast-utils.ts
Normal file
353
src/schematics/utility/ast-utils.ts
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import { Change, InsertChange } from './change';
|
||||||
|
import { insertImport } from './route-utils';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
|
||||||
|
* @param node
|
||||||
|
* @param kind
|
||||||
|
* @param max The maximum number of items to return.
|
||||||
|
* @return all nodes of kind, or [] if none is found
|
||||||
|
*/
|
||||||
|
export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): ts.Node[] {
|
||||||
|
if (!node || max == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: ts.Node[] = [];
|
||||||
|
if (node.kind === kind) {
|
||||||
|
arr.push(node);
|
||||||
|
max--;
|
||||||
|
}
|
||||||
|
if (max > 0) {
|
||||||
|
for (const child of node.getChildren()) {
|
||||||
|
findNodes(child, kind, max).forEach(node => {
|
||||||
|
if (max > 0) {
|
||||||
|
arr.push(node);
|
||||||
|
}
|
||||||
|
max--;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (max <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the nodes from a source.
|
||||||
|
* @param sourceFile The source file object.
|
||||||
|
* @returns {Observable<ts.Node>} An observable of all the nodes in the source.
|
||||||
|
*/
|
||||||
|
export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
|
||||||
|
const nodes: ts.Node[] = [sourceFile];
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
while (nodes.length > 0) {
|
||||||
|
const node = nodes.shift();
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
result.push(node);
|
||||||
|
if (node.getChildCount(sourceFile) >= 0) {
|
||||||
|
nodes.unshift(...node.getChildren());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for sorting nodes.
|
||||||
|
* @return function to sort nodes in increasing order of position in sourceFile
|
||||||
|
*/
|
||||||
|
function nodesByPosition(first: ts.Node, second: ts.Node): number {
|
||||||
|
return first.pos - second.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
|
||||||
|
* or after the last of occurence of `syntaxKind` if the last occurence is a sub child
|
||||||
|
* of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
|
||||||
|
*
|
||||||
|
* @param nodes insert after the last occurence of nodes
|
||||||
|
* @param toInsert string to insert
|
||||||
|
* @param file file to insert changes into
|
||||||
|
* @param fallbackPos position to insert if toInsert happens to be the first occurence
|
||||||
|
* @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
|
||||||
|
* @return Change instance
|
||||||
|
* @throw Error if toInsert is first occurence but fall back is not set
|
||||||
|
*/
|
||||||
|
export function insertAfterLastOccurrence(nodes: ts.Node[],
|
||||||
|
toInsert: string,
|
||||||
|
file: string,
|
||||||
|
fallbackPos: number,
|
||||||
|
syntaxKind?: ts.SyntaxKind): Change {
|
||||||
|
let lastItem = nodes.sort(nodesByPosition).pop();
|
||||||
|
if (syntaxKind) {
|
||||||
|
lastItem = findNodes(lastItem !, syntaxKind).sort(nodesByPosition).pop();
|
||||||
|
}
|
||||||
|
if (!lastItem && fallbackPos == undefined) {
|
||||||
|
throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`);
|
||||||
|
}
|
||||||
|
const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
|
||||||
|
|
||||||
|
return new InsertChange(file, lastItemPosition, toInsert);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string | null {
|
||||||
|
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||||
|
return (node as ts.Identifier).text;
|
||||||
|
} else if (node.kind == ts.SyntaxKind.StringLiteral) {
|
||||||
|
return (node as ts.StringLiteral).text;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _angularImportsFromNode(node: ts.ImportDeclaration,
|
||||||
|
_sourceFile: ts.SourceFile): {[name: string]: string} {
|
||||||
|
const ms = node.moduleSpecifier;
|
||||||
|
let modulePath: string | null = null;
|
||||||
|
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 _addSymbolToNgModuleMetadata(source: ts.SourceFile,
|
||||||
|
ngModulePath: string, metadataField: string,
|
||||||
|
expression: string): 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}: [${expression}]\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}: [${expression}]`;
|
||||||
|
} else {
|
||||||
|
toInsert = `, ${metadataField}: [${expression}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newMetadataProperty = new InsertChange(ngModulePath, position, toInsert);
|
||||||
|
return [newMetadataProperty];
|
||||||
|
}
|
||||||
|
|
||||||
|
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(expression)) {
|
||||||
|
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}: [${expression}]\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}: [${expression}]`;
|
||||||
|
} else {
|
||||||
|
toInsert = `, ${metadataField}: [${expression}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
|
||||||
|
// We found the field but it's empty. Insert it just before the `]`.
|
||||||
|
position--;
|
||||||
|
toInsert = `${expression}`;
|
||||||
|
} 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]}${expression}`;
|
||||||
|
} else {
|
||||||
|
toInsert = `, ${expression}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const insert = new InsertChange(ngModulePath, position, toInsert);
|
||||||
|
return [insert];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addImportToModule(source: ts.SourceFile,
|
||||||
|
modulePath: string, symbolName: string): Change[] {
|
||||||
|
|
||||||
|
return _addSymbolToNgModuleMetadata(source, modulePath, 'imports', symbolName,);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addProviderToModule(source: ts.SourceFile,
|
||||||
|
modulePath: string, symbolName: string): Change[] {
|
||||||
|
return _addSymbolToNgModuleMetadata(source, modulePath, 'providers', symbolName);
|
||||||
|
}
|
||||||
127
src/schematics/utility/change.ts
Normal file
127
src/schematics/utility/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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/schematics/utility/find-module.ts
Normal file
65
src/schematics/utility/find-module.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @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, normalizePath } from '@angular-devkit/schematics';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to find the "closest" module to a generated file's path.
|
||||||
|
*/
|
||||||
|
export function findModule(host: Tree, generateDir: string): string {
|
||||||
|
let closestModule = generateDir;
|
||||||
|
const allFiles = host.files;
|
||||||
|
|
||||||
|
let modulePath: string | null = null;
|
||||||
|
const moduleRe = /\.module\.ts$/;
|
||||||
|
while (closestModule) {
|
||||||
|
const normalizedRoot = normalizePath(closestModule);
|
||||||
|
const matches = allFiles.filter(p => moduleRe.test(p) && p.startsWith(normalizedRoot));
|
||||||
|
|
||||||
|
if (matches.length == 1) {
|
||||||
|
modulePath = matches[0];
|
||||||
|
break;
|
||||||
|
} else if (matches.length > 1) {
|
||||||
|
throw new Error('More than one module matches. Use skip-import option to skip importing '
|
||||||
|
+ 'the component into the closest module.');
|
||||||
|
}
|
||||||
|
closestModule = closestModule.split('/').slice(0, -1).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modulePath) {
|
||||||
|
throw new Error('Could not find an NgModule for the new component. Use the skip-import '
|
||||||
|
+ 'option to skip importing components in NgModule.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return modulePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a relative path from one file path to another file path.
|
||||||
|
*/
|
||||||
|
export function buildRelativePath(from: string, to: string) {
|
||||||
|
// Convert to arrays.
|
||||||
|
const fromParts = from.split('/');
|
||||||
|
const toParts = to.split('/');
|
||||||
|
|
||||||
|
// Remove file names (preserving destination)
|
||||||
|
fromParts.pop();
|
||||||
|
const toFileName = toParts.pop();
|
||||||
|
|
||||||
|
const relativePath = path.relative(fromParts.join('/'), 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 = `./`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${pathPrefix}${relativePath}/${toFileName}`;
|
||||||
|
}
|
||||||
90
src/schematics/utility/route-utils.ts
Normal file
90
src/schematics/utility/route-utils.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import { findNodes, insertAfterLastOccurrence } from './ast-utils';
|
||||||
|
import { Change, NoopChange } from './change';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Import `import { symbolName } from fileName` if the import doesn't exit
|
||||||
|
* already. Assumes fileToEdit can be resolved and accessed.
|
||||||
|
* @param fileToEdit (file we want to add import to)
|
||||||
|
* @param symbolName (item to import)
|
||||||
|
* @param fileName (path to the file)
|
||||||
|
* @param isDefault (if true, import follows style for importing default exports)
|
||||||
|
* @return Change
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function insertImport(source: ts.SourceFile, fileToEdit: string, symbolName: string,
|
||||||
|
fileName: string, isDefault = false): Change {
|
||||||
|
const rootNode = source;
|
||||||
|
const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
|
||||||
|
|
||||||
|
// get nodes that map to import statements from the file fileName
|
||||||
|
const relevantImports = allImports.filter(node => {
|
||||||
|
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
|
||||||
|
const importFiles = node.getChildren()
|
||||||
|
.filter(child => child.kind === ts.SyntaxKind.StringLiteral)
|
||||||
|
.map(n => (n as ts.StringLiteral).text);
|
||||||
|
|
||||||
|
return importFiles.filter(file => file === fileName).length === 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relevantImports.length > 0) {
|
||||||
|
let importsAsterisk = false;
|
||||||
|
// imports from import file
|
||||||
|
const imports: ts.Node[] = [];
|
||||||
|
relevantImports.forEach(n => {
|
||||||
|
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
|
||||||
|
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
|
||||||
|
importsAsterisk = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if imports * from fileName, don't add symbolName
|
||||||
|
if (importsAsterisk) {
|
||||||
|
return new NoopChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName);
|
||||||
|
|
||||||
|
// insert import if it's not there
|
||||||
|
if (importTextNodes.length === 0) {
|
||||||
|
const fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
|
||||||
|
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
7
src/utils/testing.ts
Normal file
7
src/utils/testing.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Observable} from 'rxjs/Observable';
|
||||||
|
import {toPromise} from 'rxjs/operator/toPromise';
|
||||||
|
import {toArray} from 'rxjs/operator/toArray';
|
||||||
|
|
||||||
|
export function readAll<T>(o: Observable<T>): Promise<T[]> {
|
||||||
|
return toPromise.call(toArray.call(o));
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@
|
|||||||
"typeRoots": ["node_modules/@types"],
|
"typeRoots": ["node_modules/@types"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2015"
|
"es2017"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user