feat(schematics): add --routing to the lib schematic

This commit is contained in:
vsavkin 2017-11-13 15:17:42 -05:00
parent 1cadd19752
commit 409bfff878
12 changed files with 384 additions and 31 deletions

View File

@ -43,4 +43,30 @@ describe('Nrwl Workspace', () => {
},
100000
);
it(
'should support router config generation (lazy)',
() => {
ngNew('--collection=@nrwl/schematics --npmScope=nrwl');
copyMissingPackages();
newApp('myapp --routing');
newLib('mylib --ngmodule --routing --lazy --parentModule=apps/myapp/src/app/app.module.ts');
runCLI('build --aot');
},
100000
);
it(
'should support router config generation (eager)',
() => {
ngNew('--collection=@nrwl/schematics --npmScope=nrwl');
copyMissingPackages();
newApp('myapp --routing');
newLib('mylib --ngmodule --routing --parentModule=apps/myapp/src/app/app.module.ts');
runCLI('build --aot');
},
100000
);
});

View File

@ -82,6 +82,20 @@ function addAppToAngularCliJson(options: Schema): Rule {
};
}
function addRouterRootConfiguration(path: string): Rule {
return (host: Tree) => {
const modulePath = `${path}/app/app.module.ts`;
const moduleSource = host.read(modulePath)!.toString('utf-8');
const sourceFile = ts.createSourceFile(modulePath, moduleSource, ts.ScriptTarget.Latest, true);
insert(host, modulePath, [
insertImport(sourceFile, modulePath, 'RouterModule', '@angular/router'),
...addImportToModule(sourceFile, modulePath, `RouterModule.forRoot([], {initialNavigation: 'enabled'})`)
]);
return host;
// add onSameUrlNavigation: 'reload'
};
}
export default function(schema: Schema): Rule {
const options = { ...schema, name: toFileName(schema.name) };
const templateSource = apply(url('./files'), [
@ -95,7 +109,7 @@ export default function(schema: Schema): Rule {
name: 'app',
commonModule: false,
flat: true,
routing: options.routing,
routing: false,
sourceDir: fullPath(options),
spec: false
}),
@ -122,7 +136,8 @@ export default function(schema: Schema): Rule {
),
addBootstrap(fullPath(options)),
addNxModule(fullPath(options)),
addAppToAngularCliJson(options)
addAppToAngularCliJson(options),
options.routing ? addRouterRootConfiguration(fullPath(options)) : noop()
]);
}

View File

@ -49,6 +49,9 @@ describe('application', () => {
it('should set right npmScope', () => {
const tree = schematicRunner.runSchematic('application', { name: 'myApp', directory: 'my-app' }, appTree);
const angularCliJson = JSON.parse(getFileContent(tree, '/my-app/.angular-cli.json'));
expect(angularCliJson.project.npmScope).toEqual('myApp');
const tsconfigJson = JSON.parse(getFileContent(tree, '/my-app/tsconfig.json'));
expect(tsconfigJson.compilerOptions.paths).toEqual({ '@myApp/*': ['libs/*'] });

View File

@ -1,7 +1,8 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "<%= utils.dasherize(name) %>"
"name": "<%= utils.dasherize(name) %>",
"npmScope": "<%= npmScope %>"
},
"apps": [],
"e2e": {

View File

@ -5,24 +5,23 @@ import {
externalSchematic,
mergeWith,
move,
noop,
Rule,
template,
Tree,
url
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import { names, toFileName } from '@nrwl/schematics';
import { addImportToModule, insert, names, toClassName, toFileName, toPropertyName } from '@nrwl/schematics';
import * as path from 'path';
import { serializeJson, addApp } from '../utility/fileutils';
import { serializeJson, addApp, cliConfig } from '../utility/fileutils';
import { insertImport } from '@schematics/angular/utility/route-utils';
import * as ts from 'typescript';
import { addGlobal, addReexport, addRoute } from '../utility/ast-utils';
function addLibToAngularCliJson(options: Schema): Rule {
return (host: Tree) => {
if (!host.exists('.angular-cli.json')) {
throw new Error('Missing .angular-cli.json');
}
const sourceText = host.read('.angular-cli.json')!.toString('utf-8');
const json = JSON.parse(sourceText);
const json = cliConfig(host);
json.apps = addApp(json.apps, {
name: options.name,
root: fullPath(options),
@ -35,11 +34,116 @@ function addLibToAngularCliJson(options: Schema): Rule {
};
}
function addLazyLoadedRouterConfiguration(modulePath: string): Rule {
return (host: Tree) => {
const moduleSource = host.read(modulePath)!.toString('utf-8');
const sourceFile = ts.createSourceFile(modulePath, moduleSource, ts.ScriptTarget.Latest, true);
insert(host, modulePath, [
insertImport(sourceFile, modulePath, 'RouterModule', '@angular/router'),
...addImportToModule(
sourceFile,
modulePath,
`
RouterModule.forChild([
/* {path: '', pathMatch: 'full', component: InsertYourComponentHere} */
]) `
)
]);
return host;
};
}
function addRouterConfiguration(
schema: Schema,
indexFilePath: string,
moduleFileName: string,
modulePath: string
): Rule {
return (host: Tree) => {
const indexSource = host.read(indexFilePath)!.toString('utf-8');
const indexSourceFile = ts.createSourceFile(indexFilePath, indexSource, ts.ScriptTarget.Latest, true);
const moduleSource = host.read(modulePath)!.toString('utf-8');
const moduleSourceFile = ts.createSourceFile(modulePath, moduleSource, ts.ScriptTarget.Latest, true);
const constName = `${toPropertyName(schema.name)}Routes`;
insert(host, modulePath, [
insertImport(moduleSourceFile, modulePath, 'RouterModule, Route', '@angular/router'),
...addImportToModule(moduleSourceFile, modulePath, `RouterModule`),
...addGlobal(moduleSourceFile, modulePath, `export const ${constName}: Route[] = [];`)
]);
insert(host, indexFilePath, [...addReexport(indexSourceFile, indexFilePath, moduleFileName, constName)]);
return host;
};
}
function addLoadChildren(schema: Schema): Rule {
return (host: Tree) => {
const json = cliConfig(host);
const moduleSource = host.read(schema.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true);
const loadChildren = `@${json.project.npmScope}/${toFileName(schema.name)}#${toClassName(schema.name)}Module`;
insert(host, schema.parentModule, [
...addRoute(
schema.parentModule,
sourceFile,
`{path: '${toFileName(schema.name)}', loadChildren: '${loadChildren}'}`
)
]);
return host;
};
}
function addChildren(schema: Schema): Rule {
return (host: Tree) => {
const json = cliConfig(host);
const moduleSource = host.read(schema.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true);
const constName = `${toPropertyName(schema.name)}Routes`;
const importPath = `@${json.project.npmScope}/${toFileName(schema.name)}`;
insert(host, schema.parentModule, [
insertImport(sourceFile, schema.parentModule, constName, importPath),
...addRoute(schema.parentModule, sourceFile, `{path: '${toFileName(schema.name)}', children: ${constName}}`)
]);
return host;
};
}
function updateTsLint(schema: Schema): Rule {
return (host: Tree) => {
const tsLint = JSON.parse(host.read('tslint.json')!.toString('utf-8'));
if (
tsLint['rules'] &&
tsLint['rules']['nx-enforce-module-boundaries'] &&
tsLint['rules']['nx-enforce-module-boundaries'][1] &&
tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad']
) {
tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad'].push(toFileName(schema.name));
host.overwrite('tslint.json', serializeJson(tsLint));
}
return host;
};
}
export default function(schema: Schema): Rule {
const options = { ...schema, name: toFileName(schema.name) };
const fullPath = path.join('libs', toFileName(options.name), options.sourceDir);
const moduleFileName = `${toFileName(schema.name)}.module`;
const modulePath = path.join(fullPath, `${moduleFileName}.ts`);
const indexFile = path.join('libs', toFileName(options.name), 'index.ts');
const templateSource = apply(url(options.ngmodule ? './ngfiles' : './files'), [
if (schema.routing && schema.nomodule) {
throw new Error(`nomodule and routing cannot be used together`);
}
if (!schema.routing && schema.lazy) {
throw new Error(`routing must be set`);
}
const templateSource = apply(url(options.nomodule ? './files' : './ngfiles'), [
template({
...names(options.name),
dot: '.',
@ -48,7 +152,16 @@ export default function(schema: Schema): Rule {
})
]);
return chain([branchAndMerge(chain([mergeWith(templateSource)])), addLibToAngularCliJson(options)]);
return chain([
branchAndMerge(chain([mergeWith(templateSource)])),
addLibToAngularCliJson(options),
schema.routing && schema.lazy ? addLazyLoadedRouterConfiguration(modulePath) : noop(),
schema.routing && schema.lazy ? updateTsLint(schema) : noop(),
schema.routing && schema.lazy && schema.parentModule ? addLoadChildren(schema) : noop(),
schema.routing && !schema.lazy ? addRouterConfiguration(schema, indexFile, moduleFileName, modulePath) : noop(),
schema.routing && !schema.lazy && schema.parentModule ? addChildren(schema) : noop()
]);
}
function fullPath(options: Schema) {

View File

@ -1,7 +1,7 @@
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import * as path from 'path';
import { Tree, VirtualTree } from '@angular-devkit/schematics';
import { createEmptyWorkspace } from '../testing-utils';
import { createApp, createEmptyWorkspace } from '../testing-utils';
import { getFileContent } from '@schematics/angular/utility/test';
describe('lib', () => {
@ -12,6 +12,8 @@ describe('lib', () => {
beforeEach(() => {
appTree = new VirtualTree();
appTree = createEmptyWorkspace(appTree);
schematicRunner.logger.subscribe(s => console.log(s));
});
it('should update angular-cli.json', () => {
@ -28,18 +30,100 @@ describe('lib', () => {
});
it('should generate files', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree);
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', nomodule: true }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/my-lib.spec.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/index.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.ts')).toContain('class MyLib');
});
it('should generate files (--ngmodule)', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', ngmodule: true }, appTree);
it('should generate files', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/my-lib.module.spec.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/index.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('class MyLibModule');
});
describe('router', () => {
it('should error when routing is set with nomodule = true', () => {
expect(() =>
schematicRunner.runSchematic('lib', { name: 'myLib', nomodule: true, routing: true }, appTree)
).toThrow('nomodule and routing cannot be used together');
});
it('should error when lazy is set without routing', () => {
expect(() => schematicRunner.runSchematic('lib', { name: 'myLib', lazy: true }, appTree)).toThrow(
'routing must be set'
);
});
describe('lazy', () => {
it('should add RouterModule.forChild', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true, lazy: true }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('RouterModule.forChild');
});
it('should update the parent module', () => {
appTree = createApp(appTree, 'myapp');
const tree = schematicRunner.runSchematic(
'lib',
{ name: 'myLib', routing: true, lazy: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
appTree
);
expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain(
`RouterModule.forRoot([{path: 'my-lib', loadChildren: '@proj/my-lib#MyLibModule'}])`
);
const tree2 = schematicRunner.runSchematic(
'lib',
{ name: 'myLib2', routing: true, lazy: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
tree
);
expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain(
`RouterModule.forRoot([{path: 'my-lib', loadChildren: '@proj/my-lib#MyLibModule'}, {path: 'my-lib2', loadChildren: '@proj/my-lib2#MyLib2Module'}])`
);
});
it('should register the module as lazy loaded in tslint.json', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true, lazy: true }, appTree);
const tslint = JSON.parse(getFileContent(tree, 'tslint.json'));
expect(tslint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad']).toEqual(['my-lib']);
});
});
describe('eager', () => {
it('should add RouterModule and define an array of routes', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('RouterModule]');
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('const myLibRoutes: Route[] = ');
expect(getFileContent(tree, 'libs/my-lib/index.ts')).toContain('myLibRoutes');
});
it('should update the parent module', () => {
appTree = createApp(appTree, 'myapp');
const tree = schematicRunner.runSchematic(
'lib',
{ name: 'myLib', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
appTree
);
expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain(
`RouterModule.forRoot([{path: 'my-lib', children: myLibRoutes}])`
);
const tree2 = schematicRunner.runSchematic(
'lib',
{ name: 'myLib2', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
tree
);
expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain(
`RouterModule.forRoot([{path: 'my-lib', children: myLibRoutes}, {path: 'my-lib2', children: myLib2Routes}])`
);
});
});
});
// should throw when no --ngmodule
});

View File

@ -1,10 +1,13 @@
export interface Schema {
name: string;
sourceDir?: string;
ngmodule: boolean;
nomodule: boolean;
routing?: boolean;
spec?: boolean;
flat?: boolean;
commonModule?: boolean;
routing?: boolean;
lazy?: boolean;
parentModule?: string;
}

View File

@ -13,9 +13,24 @@
"default": "src",
"alias": "sd"
},
"ngmodule": {
"nomodule": {
"type": "boolean",
"default": false
"default": false,
"description": "Generate a simple TS library when set to true."
},
"routing": {
"type": "boolean",
"default": false,
"description": "Add router configuration. See lazy for more information."
},
"lazy": {
"type": "boolean",
"default": false,
"description": "Add RouterModule.forChild when set to true, and a simple array of routes when set to false."
},
"parentModule": {
"type": "string",
"description": "Update the router configuration of the parent module using loadChildren or children, depending on what `lazy` is set to."
}
},
"required": [

View File

@ -3,6 +3,20 @@ import { Tree } from '@angular-devkit/schematics';
export function createEmptyWorkspace(tree: Tree): Tree {
tree.create('/.angular-cli.json', JSON.stringify({}));
tree.create('/package.json', JSON.stringify({}));
tree.create(
'/tslint.json',
JSON.stringify({
rules: {
'nx-enforce-module-boundaries': [
true,
{
npmScope: '<%= npmScope %>',
lazyLoad: []
}
]
}
})
);
return tree;
}
@ -12,9 +26,10 @@ export function createApp(tree: Tree, appName: string): Tree {
`
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule],
imports: [BrowserModule, RouterModule.forRoot([])],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
@ -42,6 +57,10 @@ export function createApp(tree: Tree, appName: string): Tree {
tree.overwrite(
'/.angular-cli.json',
JSON.stringify({
project: {
name: 'proj',
npmScope: 'proj'
},
apps: [
{
name: appName,

View File

@ -234,6 +234,25 @@ export function addImportToModule(source: ts.SourceFile, modulePath: string, sym
return _addSymbolToNgModuleMetadata(source, modulePath, 'imports', symbolName);
}
export function addReexport(
source: ts.SourceFile,
modulePath: string,
reexportedFileName: string,
token: string
): Change[] {
const allExports = findNodes(source, ts.SyntaxKind.ExportDeclaration);
if (allExports.length > 0) {
const m = allExports.filter(
(e: ts.ExportDeclaration) => e.moduleSpecifier.getText(source).indexOf(reexportedFileName) > -1
);
if (m.length > 0) {
const mm: ts.ExportDeclaration = <any>m[0];
return [new InsertChange(modulePath, mm.exportClause.end - 1, `, ${token} `)];
}
}
return [];
}
export function getBootstrapComponent(source: ts.SourceFile, moduleClassName: string): string {
const bootstrap = getMatchingProperty(source, 'bootstrap');
if (!bootstrap) {
@ -275,6 +294,39 @@ function getMatchingProperty(source: ts.SourceFile, property: string): ts.Object
);
}
export function addRoute(ngModulePath: string, source: ts.SourceFile, route: string): Change[] {
const routes = getListOfRoutes(source);
if (!routes) return [];
if (routes.hasTrailingComma || routes.length === 0) {
return [new InsertChange(ngModulePath, routes.end, route)];
} else {
return [new InsertChange(ngModulePath, routes.end, `, ${route}`)];
}
}
function getListOfRoutes(source: ts.SourceFile): ts.NodeArray<ts.Expression> {
const imports: any = getMatchingProperty(source, 'imports');
if (imports.initializer.kind === ts.SyntaxKind.ArrayLiteralExpression) {
const a = imports.initializer as ts.ArrayLiteralExpression;
for (let e of a.elements) {
if (e.kind === 181) {
const ee = e as ts.CallExpression;
const text = ee.expression.getText(source);
if ((text === 'RouterModule.forRoot' || text === 'RouterModule.forChild') && ee.arguments.length > 0) {
const routes = ee.arguments[0];
if (routes.kind === ts.SyntaxKind.ArrayLiteralExpression) {
return (routes as ts.ArrayLiteralExpression).elements;
}
}
}
}
}
return null;
}
export function getImport(
source: ts.SourceFile,
predicate: (a: any) => boolean
@ -306,6 +358,16 @@ export function addEntryComponents(source: ts.SourceFile, modulePath: string, sy
return _addSymbolToNgModuleMetadata(source, modulePath, 'entryComponents', symbolName);
}
export function addGlobal(source: ts.SourceFile, modulePath: string, statement: string): Change[] {
const allImports = findNodes(source, ts.SyntaxKind.ImportDeclaration);
if (allImports.length > 0) {
const lastImport = allImports[allImports.length - 1];
return [new InsertChange(modulePath, lastImport.end + 1, `\n${statement}\n`)];
} else {
return [new InsertChange(modulePath, 0, `${statement}\n`)];
}
}
export function insert(host: Tree, modulePath: string, changes: Change[]) {
const recorder = host.beginUpdate(modulePath);
for (const change of changes) {

View File

@ -1,4 +1,5 @@
import * as fs from 'fs';
import { Tree } from '@angular-devkit/schematics';
export function updateJsonFile(path: string, callback: (a: any) => any) {
const json = JSON.parse(fs.readFileSync(path, 'utf-8'));
@ -25,3 +26,12 @@ export function addApp(apps: any[] | undefined, newApp: any): any[] {
export function serializeJson(json: any): string {
return `${JSON.stringify(json, null, 2)}\n`;
}
export function cliConfig(host: Tree): any {
if (!host.exists('.angular-cli.json')) {
throw new Error('Missing .angular-cli.json');
}
const sourceText = host.read('.angular-cli.json')!.toString('utf-8');
return JSON.parse(sourceText);
}

View File

@ -70,6 +70,8 @@ function updateAngularCLIJson(options: Schema) {
throw new Error('Cannot find .angular-cli.json');
}
const angularCliJson = JSON.parse(host.read('.angular-cli.json')!.toString('utf-8'));
angularCliJson.project.npmScope = npmScope(options);
if (angularCliJson.apps.length !== 1) {
throw new Error('Can only convert projects with one app');
}
@ -105,15 +107,13 @@ function updateAngularCLIJson(options: Schema) {
function updateTsConfigsJson(options: Schema) {
return (host: Tree) => {
const npmScope = options && options.npmScope ? options.npmScope : options.name;
updateJsonFile('tsconfig.json', json => setUpCompilerOptions(json, npmScope));
updateJsonFile('tsconfig.json', json => setUpCompilerOptions(json, npmScope(options)));
updateJsonFile('tsconfig.app.json', json => {
json['extends'] = './tsconfig.json';
if (!json.exclude) json.exclude = [];
json.exclude = dedup(json.exclude.concat(['**/*.spec.ts', '**/*.e2e-spec.ts', 'node_modules', 'tmp']));
setUpCompilerOptions(json, npmScope);
setUpCompilerOptions(json, npmScope(options));
});
updateJsonFile('tsconfig.spec.json', json => {
@ -121,14 +121,14 @@ function updateTsConfigsJson(options: Schema) {
if (!json.exclude) json.exclude = [];
json.files = ['test.js'];
json.exclude = dedup(json.exclude.concat(['node_modules', 'tmp']));
setUpCompilerOptions(json, npmScope);
setUpCompilerOptions(json, npmScope(options));
});
updateJsonFile('tsconfig.e2e.json', json => {
json['extends'] = './tsconfig.json';
if (!json.exclude) json.exclude = [];
json.exclude = dedup(json.exclude.concat(['**/*.spec.ts', 'node_modules', 'tmp']));
setUpCompilerOptions(json, npmScope);
setUpCompilerOptions(json, npmScope(options));
});
return host;
@ -137,18 +137,20 @@ function updateTsConfigsJson(options: Schema) {
function updateTsLintJson(options: Schema) {
return (host: Tree) => {
const npmScope = options && options.npmScope ? options.npmScope : options.name;
updateJsonFile('tslint.json', json => {
['no-trailing-whitespace', 'one-line', 'quotemark', 'typedef-whitespace', 'whitespace'].forEach(key => {
json[key] = undefined;
});
json['nx-enforce-module-boundaries'] = [true, { npmScope: npmScope, lazyLoad: [] }];
json['nx-enforce-module-boundaries'] = [true, { npmScope: npmScope(options), lazyLoad: [] }];
});
return host;
};
}
function npmScope(options: Schema): string {
return options && options.npmScope ? options.npmScope : options.name;
}
function updateProtractorConf() {
return (host: Tree) => {
if (!host.exists('protractor.conf.js')) {