feat(schematics): add support for generating apps and libs in nested dirs

This commit is contained in:
vsavkin 2017-12-05 16:13:38 -05:00 committed by Victor Savkin
parent fa5d5bcace
commit 013a828d1e
36 changed files with 327 additions and 151 deletions

View File

@ -5,19 +5,19 @@ describe('Nrwl Workspace', () => {
'should work', 'should work',
() => { () => {
newProject(); newProject();
newApp('myapp'); newApp('myApp --directory=myDir');
newLib('mylib --ngmodule'); newLib('myLib --directory=myDir --ngmodule');
updateFile( updateFile(
'apps/myapp/src/app/app.module.ts', 'apps/my-dir/my-app/src/app/app.module.ts',
` `
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { MylibModule } from '@nrwl/mylib'; import { MyLibModule } from '@nrwl/my-dir/my-lib';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@NgModule({ @NgModule({
imports: [BrowserModule, MylibModule], imports: [BrowserModule, MyLibModule],
declarations: [AppComponent], declarations: [AppComponent],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
@ -35,8 +35,8 @@ describe('Nrwl Workspace', () => {
'should support router config generation (lazy)', 'should support router config generation (lazy)',
() => { () => {
newProject(); newProject();
newApp('myapp --routing'); newApp('myApp --directory=myDir --routing');
newLib('mylib --routing --lazy --parentModule=apps/myapp/src/app/app.module.ts'); newLib('myLib --directory=myDir --routing --lazy --parentModule=apps/my-dir/my-app/src/app/app.module.ts');
runCLI('build --aot'); runCLI('build --aot');
expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS'); expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS');
@ -48,8 +48,8 @@ describe('Nrwl Workspace', () => {
'should support router config generation (eager)', 'should support router config generation (eager)',
() => { () => {
newProject(); newProject();
newApp('myapp --routing'); newApp('myApp --directory=myDir --routing');
newLib('mylib --routing --parentModule=apps/myapp/src/app/app.module.ts'); newLib('myLib --directory=myDir --routing --parentModule=apps/my-dir/my-app/src/app/app.module.ts');
runCLI('build --aot'); runCLI('build --aot');
expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS'); expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS');

View File

@ -18,12 +18,14 @@ describe('Lint', () => {
` `
import '../../../libs/mylib'; import '../../../libs/mylib';
import '@nrwl/lazylib'; import '@nrwl/lazylib';
import '@nrwl/mylib/deep';
` `
); );
const out = runCLI('lint --type-check', { silenceError: true }); const out = runCLI('lint --type-check', { silenceError: true });
expect(out).toContain('relative imports of libraries are forbidden'); expect(out).toContain('relative imports of libraries are forbidden');
expect(out).toContain('import of lazy-loaded libraries are forbidden'); expect(out).toContain('import of lazy-loaded libraries are forbidden');
expect(out).toContain('deep imports into libraries are forbidden');
}, },
1000000 1000000
); );

View File

@ -9,8 +9,8 @@ export default {
const rule = ruleName in json.rules ? json.rules[ruleName] : null; const rule = ruleName in json.rules ? json.rules[ruleName] : null;
// Only modify when the rule is configured with optional arguments // Only modify when the rule is configured with optional arguments
if (Array.isArray(rule) && typeof rule[2] === 'object' && rule[2] !== null) { if (Array.isArray(rule) && typeof rule[1] === 'object' && rule[1] !== null) {
rule[2][ruleOptionName] = []; rule[1][ruleOptionName] = [];
} }
}); });
} }

View File

@ -0,0 +1,16 @@
import { updateJsonFile } from '../src/collection/utility/fileutils';
export default {
description: 'Remove npmScope from tslint.json',
run: () => {
updateJsonFile('tslint.json', json => {
const ruleName = 'nx-enforce-module-boundaries';
const rule = ruleName in json.rules ? json.rules[ruleName] : null;
// Only modify when the rule is configured with optional arguments
if (Array.isArray(rule) && typeof rule[1] === 'object' && rule[1] !== null) {
rule[1].npmScope = undefined;
}
});
}
};

View File

@ -14,49 +14,86 @@ describe('app', () => {
appTree = createEmptyWorkspace(appTree); appTree = createEmptyWorkspace(appTree);
}); });
it('should update angular-cli.json', () => { describe('not nested', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree); it('should update angular-cli.json', () => {
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json')); const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree);
expect(updatedAngularCLIJson.apps).toEqual([ const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
{ expect(updatedAngularCLIJson.apps).toEqual([
assets: ['assets', 'favicon.ico'], {
environmentSource: 'environments/environment.ts', assets: ['assets', 'favicon.ico'],
environments: { dev: 'environments/environment.ts', prod: 'environments/environment.prod.ts' }, environmentSource: 'environments/environment.ts',
index: 'index.html', environments: { dev: 'environments/environment.ts', prod: 'environments/environment.prod.ts' },
main: 'main.ts', index: 'index.html',
name: 'my-app', main: 'main.ts',
outDir: 'dist/apps/my-app', name: 'my-app',
polyfills: 'polyfills.ts', outDir: 'dist/apps/my-app',
prefix: 'app', polyfills: 'polyfills.ts',
root: 'apps/my-app/src', prefix: 'app',
scripts: [], root: 'apps/my-app/src',
styles: ['styles.css'], scripts: [],
test: '../../../test.js', styles: ['styles.css'],
testTsconfig: '../../../tsconfig.spec.json', test: '../../../test.js',
tsconfig: '../../../tsconfig.app.json' testTsconfig: '../../../tsconfig.spec.json',
} tsconfig: '../../../tsconfig.app.json'
]); }
]);
});
it('should generate files', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree);
expect(tree.exists('apps/my-app/src/main.ts')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.module.ts')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.component.ts')).toBeTruthy();
expect(tree.exists('apps/my-app/e2e/app.po.ts')).toBeTruthy();
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('class AppModule');
});
}); });
it('should generate files', () => { describe('nested', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree); it('should update angular-cli.json', () => {
expect(tree.exists('apps/my-app/src/main.ts')).toBeTruthy(); const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir' }, appTree);
expect(tree.exists('apps/my-app/src/app/app.module.ts')).toBeTruthy(); const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
expect(tree.exists('apps/my-app/src/app/app.component.ts')).toBeTruthy(); expect(updatedAngularCLIJson.apps).toEqual([
expect(tree.exists('apps/my-app/e2e/app.po.ts')).toBeTruthy(); {
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('class AppModule'); assets: ['assets', 'favicon.ico'],
environmentSource: 'environments/environment.ts',
environments: { dev: 'environments/environment.ts', prod: 'environments/environment.prod.ts' },
index: 'index.html',
main: 'main.ts',
name: 'my-dir/my-app',
outDir: 'dist/apps/my-dir/my-app',
polyfills: 'polyfills.ts',
prefix: 'app',
root: 'apps/my-dir/my-app/src',
scripts: [],
styles: ['styles.css'],
test: '../../../../test.js',
testTsconfig: '../../../../tsconfig.spec.json',
tsconfig: '../../../../tsconfig.app.json'
}
]);
});
it('should generate files', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir' }, appTree);
expect(tree.exists('apps/my-dir/my-app/src/main.ts')).toBeTruthy();
expect(tree.exists('apps/my-dir/my-app/src/app/app.module.ts')).toBeTruthy();
expect(tree.exists('apps/my-dir/my-app/src/app/app.component.ts')).toBeTruthy();
expect(tree.exists('apps/my-dir/my-app/e2e/app.po.ts')).toBeTruthy();
expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')).toContain('class AppModule');
});
}); });
it('should import NgModule', () => { it('should import NgModule', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree); const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir' }, appTree);
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('NxModule.forRoot()'); expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')).toContain('NxModule.forRoot()');
}); });
describe('routing', () => { describe('routing', () => {
it('should include RouterTestingModule', () => { it('should include RouterTestingModule', () => {
const tree = schematicRunner.runSchematic('app', { name: 'myApp', routing: true }, appTree); const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir', routing: true }, appTree);
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('RouterModule.forRoot'); expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')).toContain('RouterModule.forRoot');
expect(getFileContent(tree, 'apps/my-app/src/app/app.component.spec.ts')).toContain( expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.component.spec.ts')).toContain(
'imports: [RouterTestingModule]' 'imports: [RouterTestingModule]'
); );
}); });

View File

@ -11,4 +11,4 @@
<body> <body>
<<%= prefix %>-root></<%= prefix %>-root> <<%= prefix %>-root></<%= prefix %>-root>
</body> </body>
</html> </html>

View File

@ -21,6 +21,12 @@ import { addBootstrapToModule } from '@schematics/angular/utility/ast-utils';
import { insertImport } from '@schematics/angular/utility/route-utils'; import { insertImport } from '@schematics/angular/utility/route-utils';
import { addApp, serializeJson } from '../utility/fileutils'; import { addApp, serializeJson } from '../utility/fileutils';
import { addImportToTestBed } from '../utility/ast-utils'; import { addImportToTestBed } from '../utility/ast-utils';
import { offsetFromRoot } from '../utility/common';
interface NormalizedSchema extends Schema {
fullName: string;
fullPath: string;
}
function addBootstrap(path: string): Rule { function addBootstrap(path: string): Rule {
return (host: Tree) => { return (host: Tree) => {
@ -48,7 +54,7 @@ function addNxModule(path: string): Rule {
return host; return host;
}; };
} }
function addAppToAngularCliJson(options: Schema): Rule { function addAppToAngularCliJson(options: NormalizedSchema): Rule {
return (host: Tree) => { return (host: Tree) => {
if (!host.exists('.angular-cli.json')) { if (!host.exists('.angular-cli.json')) {
throw new Error('Missing .angular-cli.json'); throw new Error('Missing .angular-cli.json');
@ -57,16 +63,16 @@ function addAppToAngularCliJson(options: Schema): Rule {
const sourceText = host.read('.angular-cli.json')!.toString('utf-8'); const sourceText = host.read('.angular-cli.json')!.toString('utf-8');
const json = JSON.parse(sourceText); const json = JSON.parse(sourceText);
json.apps = addApp(json.apps, { json.apps = addApp(json.apps, {
name: options.name, name: options.fullName,
root: fullPath(options), root: options.fullPath,
outDir: `dist/apps/${options.name}`, outDir: `dist/apps/${options.fullName}`,
assets: ['assets', 'favicon.ico'], assets: ['assets', 'favicon.ico'],
index: 'index.html', index: 'index.html',
main: 'main.ts', main: 'main.ts',
polyfills: 'polyfills.ts', polyfills: 'polyfills.ts',
test: '../../../test.js', test: `${offsetFromRoot(options)}test.js`,
tsconfig: '../../../tsconfig.app.json', tsconfig: `${offsetFromRoot(options)}tsconfig.app.json`,
testTsconfig: '../../../tsconfig.spec.json', testTsconfig: `${offsetFromRoot(options)}tsconfig.spec.json`,
prefix: options.prefix, prefix: options.prefix,
styles: [`styles.${options.style}`], styles: [`styles.${options.style}`],
scripts: [], scripts: [],
@ -109,7 +115,8 @@ function addRouterRootConfiguration(path: string): Rule {
} }
export default function(schema: Schema): Rule { export default function(schema: Schema): Rule {
const options = { ...schema, name: toFileName(schema.name) }; const options = normalizeOptions(schema);
const templateSource = apply(url('./files'), [ const templateSource = apply(url('./files'), [
template({ utils: stringUtils, dot: '.', tmpl: '', ...(options as object) }) template({ utils: stringUtils, dot: '.', tmpl: '', ...(options as object) })
]); ]);
@ -122,13 +129,13 @@ export default function(schema: Schema): Rule {
commonModule: false, commonModule: false,
flat: true, flat: true,
routing: false, routing: false,
sourceDir: fullPath(options), sourceDir: options.fullPath,
spec: false spec: false
}), }),
externalSchematic('@schematics/angular', 'component', { externalSchematic('@schematics/angular', 'component', {
name: 'app', name: 'app',
selector: selector, selector: selector,
sourceDir: fullPath(options), sourceDir: options.fullPath,
flat: true, flat: true,
inlineStyle: options.inlineStyle, inlineStyle: options.inlineStyle,
inlineTemplate: options.inlineTemplate, inlineTemplate: options.inlineTemplate,
@ -142,17 +149,20 @@ export default function(schema: Schema): Rule {
apply(url('./component-files'), [ apply(url('./component-files'), [
options.inlineTemplate ? filter(path => !path.endsWith('.html')) : noop(), options.inlineTemplate ? filter(path => !path.endsWith('.html')) : noop(),
template({ ...options, tmpl: '' }), template({ ...options, tmpl: '' }),
move(`${fullPath(options)}/app`) move(`${options.fullPath}/app`)
]), ]),
MergeStrategy.Overwrite MergeStrategy.Overwrite
), ),
addBootstrap(fullPath(options)), addBootstrap(options.fullPath),
addNxModule(fullPath(options)), addNxModule(options.fullPath),
addAppToAngularCliJson(options), addAppToAngularCliJson(options),
options.routing ? addRouterRootConfiguration(fullPath(options)) : noop() options.routing ? addRouterRootConfiguration(options.fullPath) : noop()
]); ]);
} }
function fullPath(options: Schema) { function normalizeOptions(options: Schema): NormalizedSchema {
return `apps/${options.name}/${options.sourceDir}`; const name = toFileName(options.name);
const fullName = options.directory ? `${toFileName(options.directory)}/${name}` : name;
const fullPath = `apps/${fullName}/${options.sourceDir}`;
return { ...options, name, fullName, fullPath };
} }

View File

@ -1,5 +1,6 @@
export interface Schema { export interface Schema {
name: string; name: string;
directory?: string;
sourceDir?: string; sourceDir?: string;
inlineStyle?: boolean; inlineStyle?: boolean;
inlineTemplate?: boolean; inlineTemplate?: boolean;

View File

@ -8,6 +8,10 @@
"type": "string", "type": "string",
"description": "Application name" "description": "Application name"
}, },
"directory": {
"type": "string",
"description": "A directory where the app is placed"
},
"sourceDir": { "sourceDir": {
"type": "string", "type": "string",
"default": "src", "default": "src",

View File

@ -54,11 +54,5 @@ describe('application', () => {
const tsconfigJson = JSON.parse(getFileContent(tree, '/my-app/tsconfig.json')); const tsconfigJson = JSON.parse(getFileContent(tree, '/my-app/tsconfig.json'));
expect(tsconfigJson.compilerOptions.paths).toEqual({ '@myApp/*': ['libs/*'] }); expect(tsconfigJson.compilerOptions.paths).toEqual({ '@myApp/*': ['libs/*'] });
const tslintJson = JSON.parse(getFileContent(tree, '/my-app/tslint.json'));
expect(tslintJson.rules['nx-enforce-module-boundaries']).toEqual([
true,
{ allow: [], lazyLoad: [], npmScope: 'myApp' }
]);
}); });
}); });

View File

@ -113,7 +113,6 @@
"nx-enforce-module-boundaries": [ "nx-enforce-module-boundaries": [
true, true,
{ {
"npmScope": "<%= npmScope %>",
"lazyLoad": [], "lazyLoad": [],
"allow": [] "allow": []
} }

View File

@ -18,14 +18,21 @@ import { serializeJson, addApp, cliConfig } from '../utility/fileutils';
import { insertImport } from '@schematics/angular/utility/route-utils'; import { insertImport } from '@schematics/angular/utility/route-utils';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { addGlobal, addReexport, addRoute } from '../utility/ast-utils'; import { addGlobal, addReexport, addRoute } from '../utility/ast-utils';
import { offsetFromRoot } from '../utility/common';
function addLibToAngularCliJson(options: Schema): Rule { interface NormalizedSchema extends Schema {
name: string;
fullName: string;
fullPath: string;
}
function addLibToAngularCliJson(options: NormalizedSchema): Rule {
return (host: Tree) => { return (host: Tree) => {
const json = cliConfig(host); const json = cliConfig(host);
json.apps = addApp(json.apps, { json.apps = addApp(json.apps, {
name: options.name, name: options.fullName,
root: fullPath(options), root: options.fullPath,
test: '../../../test.js', test: `${offsetFromRoot(options)}test.js`,
appRoot: '' appRoot: ''
}); });
@ -54,7 +61,7 @@ function addLazyLoadedRouterConfiguration(modulePath: string): Rule {
} }
function addRouterConfiguration( function addRouterConfiguration(
schema: Schema, schema: NormalizedSchema,
indexFilePath: string, indexFilePath: string,
moduleFileName: string, moduleFileName: string,
modulePath: string modulePath: string
@ -76,14 +83,14 @@ function addRouterConfiguration(
}; };
} }
function addLoadChildren(schema: Schema): Rule { function addLoadChildren(schema: NormalizedSchema): Rule {
return (host: Tree) => { return (host: Tree) => {
const json = cliConfig(host); const json = cliConfig(host);
const moduleSource = host.read(schema.parentModule)!.toString('utf-8'); const moduleSource = host.read(schema.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true); const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true);
const loadChildren = `@${json.project.npmScope}/${toFileName(schema.name)}#${toClassName(schema.name)}Module`; const loadChildren = `@${json.project.npmScope}/${toFileName(schema.fullName)}#${toClassName(schema.name)}Module`;
insert(host, schema.parentModule, [ insert(host, schema.parentModule, [
...addRoute( ...addRoute(
schema.parentModule, schema.parentModule,
@ -95,14 +102,14 @@ function addLoadChildren(schema: Schema): Rule {
}; };
} }
function addChildren(schema: Schema): Rule { function addChildren(schema: NormalizedSchema): Rule {
return (host: Tree) => { return (host: Tree) => {
const json = cliConfig(host); const json = cliConfig(host);
const moduleSource = host.read(schema.parentModule)!.toString('utf-8'); const moduleSource = host.read(schema.parentModule)!.toString('utf-8');
const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true); const sourceFile = ts.createSourceFile(schema.parentModule, moduleSource, ts.ScriptTarget.Latest, true);
const constName = `${toPropertyName(schema.name)}Routes`; const constName = `${toPropertyName(schema.name)}Routes`;
const importPath = `@${json.project.npmScope}/${toFileName(schema.name)}`; const importPath = `@${json.project.npmScope}/${toFileName(schema.fullName)}`;
insert(host, schema.parentModule, [ insert(host, schema.parentModule, [
insertImport(sourceFile, schema.parentModule, constName, importPath), insertImport(sourceFile, schema.parentModule, constName, importPath),
@ -112,7 +119,7 @@ function addChildren(schema: Schema): Rule {
}; };
} }
function updateTsLint(schema: Schema): Rule { function updateTsLint(schema: NormalizedSchema): Rule {
return (host: Tree) => { return (host: Tree) => {
const tsLint = JSON.parse(host.read('tslint.json')!.toString('utf-8')); const tsLint = JSON.parse(host.read('tslint.json')!.toString('utf-8'));
if ( if (
@ -121,7 +128,7 @@ function updateTsLint(schema: Schema): Rule {
tsLint['rules']['nx-enforce-module-boundaries'][1] && tsLint['rules']['nx-enforce-module-boundaries'][1] &&
tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad'] tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad']
) { ) {
tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad'].push(toFileName(schema.name)); tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad'].push(toFileName(schema.fullName));
host.overwrite('tslint.json', serializeJson(tsLint)); host.overwrite('tslint.json', serializeJson(tsLint));
} }
return host; return host;
@ -129,16 +136,16 @@ function updateTsLint(schema: Schema): Rule {
} }
export default function(schema: Schema): Rule { export default function(schema: Schema): Rule {
const options = { ...schema, name: toFileName(schema.name) }; const options = normalizeOptions(schema);
const moduleFileName = `${toFileName(schema.name)}.module`; const moduleFileName = `${toFileName(options.name)}.module`;
const modulePath = `${fullPath(schema)}/${moduleFileName}.ts`; const modulePath = `${options.fullPath}/${moduleFileName}.ts`;
const indexFile = `libs/${toFileName(options.name)}/index.ts`; const indexFile = `libs/${toFileName(options.fullName)}/index.ts`;
if (schema.routing && schema.nomodule) { if (options.routing && options.nomodule) {
throw new Error(`nomodule and routing cannot be used together`); throw new Error(`nomodule and routing cannot be used together`);
} }
if (!schema.routing && schema.lazy) { if (!options.routing && options.lazy) {
throw new Error(`routing must be set`); throw new Error(`routing must be set`);
} }
@ -154,15 +161,18 @@ export default function(schema: Schema): Rule {
return chain([ return chain([
branchAndMerge(chain([mergeWith(templateSource)])), branchAndMerge(chain([mergeWith(templateSource)])),
addLibToAngularCliJson(options), addLibToAngularCliJson(options),
schema.routing && schema.lazy ? addLazyLoadedRouterConfiguration(modulePath) : noop(), options.routing && options.lazy ? addLazyLoadedRouterConfiguration(modulePath) : noop(),
schema.routing && schema.lazy ? updateTsLint(schema) : noop(), options.routing && options.lazy ? updateTsLint(options) : noop(),
schema.routing && schema.lazy && schema.parentModule ? addLoadChildren(schema) : noop(), options.routing && options.lazy && options.parentModule ? addLoadChildren(options) : noop(),
schema.routing && !schema.lazy ? addRouterConfiguration(schema, indexFile, moduleFileName, modulePath) : noop(), options.routing && !options.lazy ? addRouterConfiguration(options, indexFile, moduleFileName, modulePath) : noop(),
schema.routing && !schema.lazy && schema.parentModule ? addChildren(schema) : noop() options.routing && !options.lazy && options.parentModule ? addChildren(options) : noop()
]); ]);
} }
function fullPath(options: Schema) { function normalizeOptions(options: Schema): NormalizedSchema {
return `libs/${toFileName(options.name)}/${options.sourceDir}`; const name = toFileName(options.name);
const fullName = options.directory ? `${toFileName(options.directory)}/${name}` : name;
const fullPath = `libs/${fullName}/${options.sourceDir}`;
return { ...options, name, fullName, fullPath };
} }

View File

@ -16,33 +16,58 @@ describe('lib', () => {
schematicRunner.logger.subscribe(s => console.log(s)); schematicRunner.logger.subscribe(s => console.log(s));
}); });
it('should update angular-cli.json', () => { describe('not nested', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree); it('should update angular-cli.json', () => {
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json')); const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree);
expect(updatedAngularCLIJson.apps).toEqual([ const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
{ expect(updatedAngularCLIJson.apps).toEqual([
appRoot: '', {
name: 'my-lib', appRoot: '',
root: 'libs/my-lib/src', name: 'my-lib',
test: '../../../test.js' root: 'libs/my-lib/src',
} test: '../../../test.js'
]); }
]);
});
it('should generate files', () => {
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', () => {
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');
});
}); });
it('should generate files', () => { describe('nested', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', nomodule: true }, appTree); it('should update angular-cli.json', () => {
expect(tree.exists('libs/my-lib/src/my-lib.ts')).toBeTruthy(); const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir' }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.spec.ts')).toBeTruthy(); const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
expect(tree.exists('libs/my-lib/index.ts')).toBeTruthy(); expect(updatedAngularCLIJson.apps).toEqual([
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.ts')).toContain('class MyLib'); {
}); appRoot: '',
name: 'my-dir/my-lib',
root: 'libs/my-dir/my-lib/src',
test: '../../../../test.js'
}
]);
});
it('should generate files', () => { it('should generate files', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree); const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir', nomodule: true }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/src/my-lib.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/my-lib.module.spec.ts')).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/src/my-lib.spec.ts')).toBeTruthy();
expect(tree.exists('libs/my-lib/index.ts')).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/index.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('class MyLibModule'); expect(getFileContent(tree, 'libs/my-dir/my-lib/src/my-lib.ts')).toContain('class MyLib');
});
}); });
describe('router', () => { describe('router', () => {
@ -60,53 +85,75 @@ describe('lib', () => {
describe('lazy', () => { describe('lazy', () => {
it('should add RouterModule.forChild', () => { it('should add RouterModule.forChild', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true, lazy: true }, appTree); const tree = schematicRunner.runSchematic(
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy(); 'lib',
expect(getFileContent(tree, 'libs/my-lib/src/my-lib.module.ts')).toContain('RouterModule.forChild'); { name: 'myLib', directory: 'myDir', routing: true, lazy: true },
appTree
);
expect(tree.exists('libs/my-dir/my-lib/src/my-lib.module.ts')).toBeTruthy();
expect(getFileContent(tree, 'libs/my-dir/my-lib/src/my-lib.module.ts')).toContain('RouterModule.forChild');
}); });
it('should update the parent module', () => { it('should update the parent module', () => {
appTree = createApp(appTree, 'myapp'); appTree = createApp(appTree, 'myapp');
const tree = schematicRunner.runSchematic( const tree = schematicRunner.runSchematic(
'lib', 'lib',
{ name: 'myLib', routing: true, lazy: true, parentModule: 'apps/myapp/src/app/app.module.ts' }, {
name: 'myLib',
directory: 'myDir',
routing: true,
lazy: true,
parentModule: 'apps/myapp/src/app/app.module.ts'
},
appTree appTree
); );
expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain( expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain(
`RouterModule.forRoot([{path: 'my-lib', loadChildren: '@proj/my-lib#MyLibModule'}])` `RouterModule.forRoot([{path: 'my-lib', loadChildren: '@proj/my-dir/my-lib#MyLibModule'}])`
); );
const tree2 = schematicRunner.runSchematic( const tree2 = schematicRunner.runSchematic(
'lib', 'lib',
{ name: 'myLib2', routing: true, lazy: true, parentModule: 'apps/myapp/src/app/app.module.ts' }, {
name: 'myLib2',
directory: 'myDir',
routing: true,
lazy: true,
parentModule: 'apps/myapp/src/app/app.module.ts'
},
tree tree
); );
expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain( 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'}])` `RouterModule.forRoot([{path: 'my-lib', loadChildren: '@proj/my-dir/my-lib#MyLibModule'}, {path: 'my-lib2', loadChildren: '@proj/my-dir/my-lib2#MyLib2Module'}])`
); );
}); });
it('should register the module as lazy loaded in tslint.json', () => { it('should register the module as lazy loaded in tslint.json', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true, lazy: true }, appTree); const tree = schematicRunner.runSchematic(
'lib',
{ name: 'myLib', directory: 'myDir', routing: true, lazy: true },
appTree
);
const tslint = JSON.parse(getFileContent(tree, 'tslint.json')); const tslint = JSON.parse(getFileContent(tree, 'tslint.json'));
expect(tslint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad']).toEqual(['my-lib']); expect(tslint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad']).toEqual(['my-dir/my-lib']);
}); });
}); });
describe('eager', () => { describe('eager', () => {
it('should add RouterModule and define an array of routes', () => { it('should add RouterModule and define an array of routes', () => {
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', routing: true }, appTree); const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir', routing: true }, appTree);
expect(tree.exists('libs/my-lib/src/my-lib.module.ts')).toBeTruthy(); expect(tree.exists('libs/my-dir/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-dir/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-dir/my-lib/src/my-lib.module.ts')).toContain(
expect(getFileContent(tree, 'libs/my-lib/index.ts')).toContain('myLibRoutes'); 'const myLibRoutes: Route[] = '
);
expect(getFileContent(tree, 'libs/my-dir/my-lib/index.ts')).toContain('myLibRoutes');
}); });
it('should update the parent module', () => { it('should update the parent module', () => {
appTree = createApp(appTree, 'myapp'); appTree = createApp(appTree, 'myapp');
const tree = schematicRunner.runSchematic( const tree = schematicRunner.runSchematic(
'lib', 'lib',
{ name: 'myLib', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' }, { name: 'myLib', directory: 'myDir', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
appTree appTree
); );
expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain( expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain(
@ -115,7 +162,7 @@ describe('lib', () => {
const tree2 = schematicRunner.runSchematic( const tree2 = schematicRunner.runSchematic(
'lib', 'lib',
{ name: 'myLib2', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' }, { name: 'myLib2', directory: 'myDir', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
tree tree
); );
expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain( expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain(

View File

@ -1,5 +1,6 @@
export interface Schema { export interface Schema {
name: string; name: string;
directory?: string;
sourceDir?: string; sourceDir?: string;
nomodule: boolean; nomodule: boolean;

View File

@ -8,6 +8,10 @@
"type": "string", "type": "string",
"description": "Library name" "description": "Library name"
}, },
"directory": {
"type": "string",
"description": "A directory where the app is placed"
},
"sourceDir": { "sourceDir": {
"type": "string", "type": "string",
"default": "src", "default": "src",

View File

@ -1,6 +1,7 @@
import { Tree, Rule } from '@angular-devkit/schematics'; import { Tree, Rule } from '@angular-devkit/schematics';
import { angularJsVersion } from './lib-versions'; import { angularJsVersion } from './lib-versions';
import { serializeJson } from './fileutils'; import { serializeJson } from './fileutils';
import { Schema } from '../app/schema';
export function addUpgradeToPackageJson(): Rule { export function addUpgradeToPackageJson(): Rule {
return (host: Tree) => { return (host: Tree) => {
@ -23,3 +24,14 @@ export function addUpgradeToPackageJson(): Rule {
return host; return host;
}; };
} }
export function offsetFromRoot(options: Schema): string {
let offset = '../../../';
if (options.directory) {
const parts = options.directory.split('/').length;
for (let i = 0; i < parts; ++i) {
offset += '../';
}
}
return offset;
}

View File

@ -4,7 +4,7 @@ export const angularJsVersion = '1.6.6';
export const ngrxVersion = '^4.1.0'; export const ngrxVersion = '^4.1.0';
export const nxVersion = '*'; export const nxVersion = '*';
export const schematicsVersion = '*'; export const schematicsVersion = '*';
export const latestMigration = '20171129-change-schema'; export const latestMigration = '20171205-remove-npmscope-from-tslintjson';
export const prettierVersion = '1.8.2'; export const prettierVersion = '1.8.2';
export const libVersions = { export const libVersions = {

View File

@ -13,7 +13,14 @@ import {
} from '@angular-devkit/schematics'; } from '@angular-devkit/schematics';
import { Schema } from './schema'; import { Schema } from './schema';
import * as path from 'path'; import * as path from 'path';
import { angularCliVersion, ngrxVersion, nxVersion, prettierVersion, schematicsVersion } from '../utility/lib-versions'; import {
angularCliVersion,
latestMigration,
ngrxVersion,
nxVersion,
prettierVersion,
schematicsVersion
} from '../utility/lib-versions';
import * as fs from 'fs'; import * as fs from 'fs';
import { join } from 'path'; import { join } from 'path';
import { serializeJson, updateJsonFile } from '../utility/fileutils'; import { serializeJson, updateJsonFile } from '../utility/fileutils';
@ -71,6 +78,7 @@ function updateAngularCLIJson(options: Schema) {
} }
const angularCliJson = JSON.parse(host.read('.angular-cli.json')!.toString('utf-8')); const angularCliJson = JSON.parse(host.read('.angular-cli.json')!.toString('utf-8'));
angularCliJson.project.npmScope = npmScope(options); angularCliJson.project.npmScope = npmScope(options);
angularCliJson.project.latestMigration = latestMigration;
if (angularCliJson.apps.length !== 1) { if (angularCliJson.apps.length !== 1) {
throw new Error('Can only convert projects with one app'); throw new Error('Can only convert projects with one app');
@ -141,7 +149,7 @@ function updateTsLintJson(options: Schema) {
['no-trailing-whitespace', 'one-line', 'quotemark', 'typedef-whitespace', 'whitespace'].forEach(key => { ['no-trailing-whitespace', 'one-line', 'quotemark', 'typedef-whitespace', 'whitespace'].forEach(key => {
json[key] = undefined; json[key] = undefined;
}); });
json['nx-enforce-module-boundaries'] = [true, { npmScope: npmScope(options), lazyLoad: [], allow: [] }]; json['nx-enforce-module-boundaries'] = [true, { lazyLoad: [], allow: [] }];
}); });
return host; return host;
}; };

View File

@ -6,7 +6,7 @@ import { Rule } from './nxEnforceModuleBoundariesRule';
describe('Enforce Module Boundaries', () => { describe('Enforce Module Boundaries', () => {
it('should not error when everything is in order', () => { it('should not error when everything is in order', () => {
const failures = runRule( const failures = runRule(
{ npmScope: 'mycompany', allow: ['@mycompany/mylib/deep'] }, { allow: ['@mycompany/mylib/deep'] },
` `
import '@mycompany/mylib'; import '@mycompany/mylib';
import '@mycompany/mylib/deep'; import '@mycompany/mylib/deep';
@ -25,14 +25,14 @@ describe('Enforce Module Boundaries', () => {
}); });
it('should error about deep imports into libraries', () => { it('should error about deep imports into libraries', () => {
const failures = runRule({ npmScope: 'mycompany' }, `import '@mycompany/mylib/blah';`); const failures = runRule({}, `import '@mycompany/mylib/blah';`);
expect(failures.length).toEqual(1); expect(failures.length).toEqual(1);
expect(failures[0].getFailure()).toEqual('deep imports into libraries are forbidden'); expect(failures[0].getFailure()).toEqual('deep imports into libraries are forbidden');
}); });
it('should error on importing a lazy-loaded library', () => { it('should error on importing a lazy-loaded library', () => {
const failures = runRule({ npmScope: 'mycompany', lazyLoad: ['mylib'] }, `import '@mycompany/mylib';`); const failures = runRule({ lazyLoad: ['mylib'] }, `import '@mycompany/mylib';`);
expect(failures.length).toEqual(1); expect(failures.length).toEqual(1);
expect(failures[0].getFailure()).toEqual('import of lazy-loaded libraries are forbidden'); expect(failures[0].getFailure()).toEqual('import of lazy-loaded libraries are forbidden');
@ -47,6 +47,6 @@ function runRule(ruleArguments: any, content: string): RuleFailure[] {
}; };
const sourceFile = ts.createSourceFile('proj/apps/myapp/src/main.ts', content, ts.ScriptTarget.Latest, true); const sourceFile = ts.createSourceFile('proj/apps/myapp/src/main.ts', content, ts.ScriptTarget.Latest, true);
const rule = new Rule(options, 'proj'); const rule = new Rule(options, 'proj', 'mycompany', ['mylib']);
return rule.apply(sourceFile); return rule.apply(sourceFile);
} }

View File

@ -2,40 +2,71 @@ import * as path from 'path';
import * as Lint from 'tslint'; import * as Lint from 'tslint';
import { IOptions } from 'tslint'; import { IOptions } from 'tslint';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { readFileSync } from 'fs';
export class Rule extends Lint.Rules.AbstractRule { export class Rule extends Lint.Rules.AbstractRule {
constructor(options: IOptions, private path?: string) { constructor(options: IOptions, private path?: string, private npmScope?: string, private appNames?: string[]) {
super(options); super(options);
if (!path) { if (!path) {
const cliConfig = this.readCliConfig();
this.path = process.cwd(); this.path = process.cwd();
this.npmScope = cliConfig.project.npmScope;
this.appNames = cliConfig.apps.map(a => a.name);
} }
} }
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new EnforceModuleBoundariesWalker(sourceFile, this.getOptions(), this.path)); return this.applyWithWalker(
new EnforceModuleBoundariesWalker(sourceFile, this.getOptions(), this.path, this.npmScope, this.appNames)
);
}
private readCliConfig(): any {
return JSON.parse(readFileSync(`.angular-cli.json`, 'UTF-8'));
} }
} }
class EnforceModuleBoundariesWalker extends Lint.RuleWalker { class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
constructor(sourceFile: ts.SourceFile, options: IOptions, private projectPath: string) { constructor(
sourceFile: ts.SourceFile,
options: IOptions,
private projectPath: string,
private npmScope: string,
private appNames: string[]
) {
super(sourceFile, options); super(sourceFile, options);
} }
public visitImportDeclaration(node: ts.ImportDeclaration) { public visitImportDeclaration(node: ts.ImportDeclaration) {
const npmScope = `@${this.getOptions()[0].npmScope}`; const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1);
const lazyLoad = this.getOptions()[0].lazyLoad;
const allow: string[] = Array.isArray(this.getOptions()[0].allow) const allow: string[] = Array.isArray(this.getOptions()[0].allow)
? this.getOptions()[0].allow.map(a => `${a}`) ? this.getOptions()[0].allow.map(a => `${a}`)
: []; : [];
const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1); const lazyLoad: string[] = Array.isArray(this.getOptions()[0].lazyLoad)
const impParts = imp.split(path.sep); ? this.getOptions()[0].lazyLoad.map(a => `${a}`)
: [];
if (impParts[0] === npmScope && allow.indexOf(imp) === -1 && impParts.length > 2) { // whitelisted import => return
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden'); if (allow.indexOf(imp) > -1) {
} else if (impParts[0] === npmScope && impParts.length === 2 && lazyLoad && lazyLoad.indexOf(impParts[1]) > -1) { super.visitImportDeclaration(node);
return;
}
const lazyLoaded = lazyLoad.filter(a => imp.startsWith(`@${this.npmScope}/${a}`))[0];
if (lazyLoaded) {
this.addFailureAt(node.getStart(), node.getWidth(), 'import of lazy-loaded libraries are forbidden'); this.addFailureAt(node.getStart(), node.getWidth(), 'import of lazy-loaded libraries are forbidden');
} else if (this.isRelative(imp) && this.isRelativeImportIntoAnotherProject(imp)) { return;
}
if (this.isRelative(imp) && this.isRelativeImportIntoAnotherProject(imp)) {
this.addFailureAt(node.getStart(), node.getWidth(), 'relative imports of libraries are forbidden'); this.addFailureAt(node.getStart(), node.getWidth(), 'relative imports of libraries are forbidden');
return;
}
const app = this.appNames.filter(a => imp.startsWith(`@${this.npmScope}/${a}`))[0];
if (app && imp !== `@${this.npmScope}/${app}`) {
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden');
return;
} }
super.visitImportDeclaration(node); super.visitImportDeclaration(node);