feat(schematics): add support for generating apps and libs in nested dirs
This commit is contained in:
parent
fa5d5bcace
commit
013a828d1e
@ -5,19 +5,19 @@ describe('Nrwl Workspace', () => {
|
||||
'should work',
|
||||
() => {
|
||||
newProject();
|
||||
newApp('myapp');
|
||||
newLib('mylib --ngmodule');
|
||||
newApp('myApp --directory=myDir');
|
||||
newLib('myLib --directory=myDir --ngmodule');
|
||||
|
||||
updateFile(
|
||||
'apps/myapp/src/app/app.module.ts',
|
||||
'apps/my-dir/my-app/src/app/app.module.ts',
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { MylibModule } from '@nrwl/mylib';
|
||||
import { MyLibModule } from '@nrwl/my-dir/my-lib';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, MylibModule],
|
||||
imports: [BrowserModule, MyLibModule],
|
||||
declarations: [AppComponent],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
@ -35,8 +35,8 @@ describe('Nrwl Workspace', () => {
|
||||
'should support router config generation (lazy)',
|
||||
() => {
|
||||
newProject();
|
||||
newApp('myapp --routing');
|
||||
newLib('mylib --routing --lazy --parentModule=apps/myapp/src/app/app.module.ts');
|
||||
newApp('myApp --directory=myDir --routing');
|
||||
newLib('myLib --directory=myDir --routing --lazy --parentModule=apps/my-dir/my-app/src/app/app.module.ts');
|
||||
|
||||
runCLI('build --aot');
|
||||
expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS');
|
||||
@ -48,8 +48,8 @@ describe('Nrwl Workspace', () => {
|
||||
'should support router config generation (eager)',
|
||||
() => {
|
||||
newProject();
|
||||
newApp('myapp --routing');
|
||||
newLib('mylib --routing --parentModule=apps/myapp/src/app/app.module.ts');
|
||||
newApp('myApp --directory=myDir --routing');
|
||||
newLib('myLib --directory=myDir --routing --parentModule=apps/my-dir/my-app/src/app/app.module.ts');
|
||||
|
||||
runCLI('build --aot');
|
||||
expect(runCLI('test --single-run')).toContain('Executed 2 of 2 SUCCESS');
|
||||
|
||||
@ -18,12 +18,14 @@ describe('Lint', () => {
|
||||
`
|
||||
import '../../../libs/mylib';
|
||||
import '@nrwl/lazylib';
|
||||
import '@nrwl/mylib/deep';
|
||||
`
|
||||
);
|
||||
|
||||
const out = runCLI('lint --type-check', { silenceError: true });
|
||||
expect(out).toContain('relative imports of libraries are forbidden');
|
||||
expect(out).toContain('import of lazy-loaded libraries are forbidden');
|
||||
expect(out).toContain('deep imports into libraries are forbidden');
|
||||
},
|
||||
1000000
|
||||
);
|
||||
|
||||
@ -9,8 +9,8 @@ export default {
|
||||
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[2] === 'object' && rule[2] !== null) {
|
||||
rule[2][ruleOptionName] = [];
|
||||
if (Array.isArray(rule) && typeof rule[1] === 'object' && rule[1] !== null) {
|
||||
rule[1][ruleOptionName] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -14,49 +14,86 @@ describe('app', () => {
|
||||
appTree = createEmptyWorkspace(appTree);
|
||||
});
|
||||
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
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-app',
|
||||
outDir: 'dist/apps/my-app',
|
||||
polyfills: 'polyfills.ts',
|
||||
prefix: 'app',
|
||||
root: 'apps/my-app/src',
|
||||
scripts: [],
|
||||
styles: ['styles.css'],
|
||||
test: '../../../test.js',
|
||||
testTsconfig: '../../../tsconfig.spec.json',
|
||||
tsconfig: '../../../tsconfig.app.json'
|
||||
}
|
||||
]);
|
||||
describe('not nested', () => {
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
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-app',
|
||||
outDir: 'dist/apps/my-app',
|
||||
polyfills: 'polyfills.ts',
|
||||
prefix: 'app',
|
||||
root: 'apps/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' }, 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', () => {
|
||||
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');
|
||||
describe('nested', () => {
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
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', () => {
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp' }, appTree);
|
||||
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('NxModule.forRoot()');
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir' }, appTree);
|
||||
expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')).toContain('NxModule.forRoot()');
|
||||
});
|
||||
|
||||
describe('routing', () => {
|
||||
it('should include RouterTestingModule', () => {
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp', routing: true }, appTree);
|
||||
expect(getFileContent(tree, 'apps/my-app/src/app/app.module.ts')).toContain('RouterModule.forRoot');
|
||||
expect(getFileContent(tree, 'apps/my-app/src/app/app.component.spec.ts')).toContain(
|
||||
const tree = schematicRunner.runSchematic('app', { name: 'myApp', directory: 'myDir', routing: true }, appTree);
|
||||
expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')).toContain('RouterModule.forRoot');
|
||||
expect(getFileContent(tree, 'apps/my-dir/my-app/src/app/app.component.spec.ts')).toContain(
|
||||
'imports: [RouterTestingModule]'
|
||||
);
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -11,4 +11,4 @@
|
||||
<body>
|
||||
<<%= prefix %>-root></<%= prefix %>-root>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@ -21,6 +21,12 @@ import { addBootstrapToModule } from '@schematics/angular/utility/ast-utils';
|
||||
import { insertImport } from '@schematics/angular/utility/route-utils';
|
||||
import { addApp, serializeJson } from '../utility/fileutils';
|
||||
import { addImportToTestBed } from '../utility/ast-utils';
|
||||
import { offsetFromRoot } from '../utility/common';
|
||||
|
||||
interface NormalizedSchema extends Schema {
|
||||
fullName: string;
|
||||
fullPath: string;
|
||||
}
|
||||
|
||||
function addBootstrap(path: string): Rule {
|
||||
return (host: Tree) => {
|
||||
@ -48,7 +54,7 @@ function addNxModule(path: string): Rule {
|
||||
return host;
|
||||
};
|
||||
}
|
||||
function addAppToAngularCliJson(options: Schema): Rule {
|
||||
function addAppToAngularCliJson(options: NormalizedSchema): Rule {
|
||||
return (host: Tree) => {
|
||||
if (!host.exists('.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 json = JSON.parse(sourceText);
|
||||
json.apps = addApp(json.apps, {
|
||||
name: options.name,
|
||||
root: fullPath(options),
|
||||
outDir: `dist/apps/${options.name}`,
|
||||
name: options.fullName,
|
||||
root: options.fullPath,
|
||||
outDir: `dist/apps/${options.fullName}`,
|
||||
assets: ['assets', 'favicon.ico'],
|
||||
index: 'index.html',
|
||||
main: 'main.ts',
|
||||
polyfills: 'polyfills.ts',
|
||||
test: '../../../test.js',
|
||||
tsconfig: '../../../tsconfig.app.json',
|
||||
testTsconfig: '../../../tsconfig.spec.json',
|
||||
test: `${offsetFromRoot(options)}test.js`,
|
||||
tsconfig: `${offsetFromRoot(options)}tsconfig.app.json`,
|
||||
testTsconfig: `${offsetFromRoot(options)}tsconfig.spec.json`,
|
||||
prefix: options.prefix,
|
||||
styles: [`styles.${options.style}`],
|
||||
scripts: [],
|
||||
@ -109,7 +115,8 @@ function addRouterRootConfiguration(path: string): Rule {
|
||||
}
|
||||
|
||||
export default function(schema: Schema): Rule {
|
||||
const options = { ...schema, name: toFileName(schema.name) };
|
||||
const options = normalizeOptions(schema);
|
||||
|
||||
const templateSource = apply(url('./files'), [
|
||||
template({ utils: stringUtils, dot: '.', tmpl: '', ...(options as object) })
|
||||
]);
|
||||
@ -122,13 +129,13 @@ export default function(schema: Schema): Rule {
|
||||
commonModule: false,
|
||||
flat: true,
|
||||
routing: false,
|
||||
sourceDir: fullPath(options),
|
||||
sourceDir: options.fullPath,
|
||||
spec: false
|
||||
}),
|
||||
externalSchematic('@schematics/angular', 'component', {
|
||||
name: 'app',
|
||||
selector: selector,
|
||||
sourceDir: fullPath(options),
|
||||
sourceDir: options.fullPath,
|
||||
flat: true,
|
||||
inlineStyle: options.inlineStyle,
|
||||
inlineTemplate: options.inlineTemplate,
|
||||
@ -142,17 +149,20 @@ export default function(schema: Schema): Rule {
|
||||
apply(url('./component-files'), [
|
||||
options.inlineTemplate ? filter(path => !path.endsWith('.html')) : noop(),
|
||||
template({ ...options, tmpl: '' }),
|
||||
move(`${fullPath(options)}/app`)
|
||||
move(`${options.fullPath}/app`)
|
||||
]),
|
||||
MergeStrategy.Overwrite
|
||||
),
|
||||
addBootstrap(fullPath(options)),
|
||||
addNxModule(fullPath(options)),
|
||||
addBootstrap(options.fullPath),
|
||||
addNxModule(options.fullPath),
|
||||
addAppToAngularCliJson(options),
|
||||
options.routing ? addRouterRootConfiguration(fullPath(options)) : noop()
|
||||
options.routing ? addRouterRootConfiguration(options.fullPath) : noop()
|
||||
]);
|
||||
}
|
||||
|
||||
function fullPath(options: Schema) {
|
||||
return `apps/${options.name}/${options.sourceDir}`;
|
||||
function normalizeOptions(options: Schema): NormalizedSchema {
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export interface Schema {
|
||||
name: string;
|
||||
directory?: string;
|
||||
sourceDir?: string;
|
||||
inlineStyle?: boolean;
|
||||
inlineTemplate?: boolean;
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
"type": "string",
|
||||
"description": "Application name"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "A directory where the app is placed"
|
||||
},
|
||||
"sourceDir": {
|
||||
"type": "string",
|
||||
"default": "src",
|
||||
|
||||
@ -54,11 +54,5 @@ describe('application', () => {
|
||||
|
||||
const tsconfigJson = JSON.parse(getFileContent(tree, '/my-app/tsconfig.json'));
|
||||
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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -113,7 +113,6 @@
|
||||
"nx-enforce-module-boundaries": [
|
||||
true,
|
||||
{
|
||||
"npmScope": "<%= npmScope %>",
|
||||
"lazyLoad": [],
|
||||
"allow": []
|
||||
}
|
||||
|
||||
@ -18,14 +18,21 @@ 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';
|
||||
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) => {
|
||||
const json = cliConfig(host);
|
||||
json.apps = addApp(json.apps, {
|
||||
name: options.name,
|
||||
root: fullPath(options),
|
||||
test: '../../../test.js',
|
||||
name: options.fullName,
|
||||
root: options.fullPath,
|
||||
test: `${offsetFromRoot(options)}test.js`,
|
||||
appRoot: ''
|
||||
});
|
||||
|
||||
@ -54,7 +61,7 @@ function addLazyLoadedRouterConfiguration(modulePath: string): Rule {
|
||||
}
|
||||
|
||||
function addRouterConfiguration(
|
||||
schema: Schema,
|
||||
schema: NormalizedSchema,
|
||||
indexFilePath: string,
|
||||
moduleFileName: string,
|
||||
modulePath: string
|
||||
@ -76,14 +83,14 @@ function addRouterConfiguration(
|
||||
};
|
||||
}
|
||||
|
||||
function addLoadChildren(schema: Schema): Rule {
|
||||
function addLoadChildren(schema: NormalizedSchema): 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`;
|
||||
const loadChildren = `@${json.project.npmScope}/${toFileName(schema.fullName)}#${toClassName(schema.name)}Module`;
|
||||
insert(host, schema.parentModule, [
|
||||
...addRoute(
|
||||
schema.parentModule,
|
||||
@ -95,14 +102,14 @@ function addLoadChildren(schema: Schema): Rule {
|
||||
};
|
||||
}
|
||||
|
||||
function addChildren(schema: Schema): Rule {
|
||||
function addChildren(schema: NormalizedSchema): 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)}`;
|
||||
const importPath = `@${json.project.npmScope}/${toFileName(schema.fullName)}`;
|
||||
|
||||
insert(host, schema.parentModule, [
|
||||
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) => {
|
||||
const tsLint = JSON.parse(host.read('tslint.json')!.toString('utf-8'));
|
||||
if (
|
||||
@ -121,7 +128,7 @@ function updateTsLint(schema: Schema): Rule {
|
||||
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));
|
||||
tsLint['rules']['nx-enforce-module-boundaries'][1]['lazyLoad'].push(toFileName(schema.fullName));
|
||||
host.overwrite('tslint.json', serializeJson(tsLint));
|
||||
}
|
||||
return host;
|
||||
@ -129,16 +136,16 @@ function updateTsLint(schema: Schema): Rule {
|
||||
}
|
||||
|
||||
export default function(schema: Schema): Rule {
|
||||
const options = { ...schema, name: toFileName(schema.name) };
|
||||
const moduleFileName = `${toFileName(schema.name)}.module`;
|
||||
const modulePath = `${fullPath(schema)}/${moduleFileName}.ts`;
|
||||
const indexFile = `libs/${toFileName(options.name)}/index.ts`;
|
||||
const options = normalizeOptions(schema);
|
||||
const moduleFileName = `${toFileName(options.name)}.module`;
|
||||
const modulePath = `${options.fullPath}/${moduleFileName}.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`);
|
||||
}
|
||||
|
||||
if (!schema.routing && schema.lazy) {
|
||||
if (!options.routing && options.lazy) {
|
||||
throw new Error(`routing must be set`);
|
||||
}
|
||||
|
||||
@ -154,15 +161,18 @@ export default function(schema: Schema): Rule {
|
||||
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(),
|
||||
options.routing && options.lazy ? addLazyLoadedRouterConfiguration(modulePath) : noop(),
|
||||
options.routing && options.lazy ? updateTsLint(options) : noop(),
|
||||
options.routing && options.lazy && options.parentModule ? addLoadChildren(options) : noop(),
|
||||
|
||||
schema.routing && !schema.lazy ? addRouterConfiguration(schema, indexFile, moduleFileName, modulePath) : noop(),
|
||||
schema.routing && !schema.lazy && schema.parentModule ? addChildren(schema) : noop()
|
||||
options.routing && !options.lazy ? addRouterConfiguration(options, indexFile, moduleFileName, modulePath) : noop(),
|
||||
options.routing && !options.lazy && options.parentModule ? addChildren(options) : noop()
|
||||
]);
|
||||
}
|
||||
|
||||
function fullPath(options: Schema) {
|
||||
return `libs/${toFileName(options.name)}/${options.sourceDir}`;
|
||||
function normalizeOptions(options: Schema): NormalizedSchema {
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -16,33 +16,58 @@ describe('lib', () => {
|
||||
schematicRunner.logger.subscribe(s => console.log(s));
|
||||
});
|
||||
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
appRoot: '',
|
||||
name: 'my-lib',
|
||||
root: 'libs/my-lib/src',
|
||||
test: '../../../test.js'
|
||||
}
|
||||
]);
|
||||
describe('not nested', () => {
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('lib', { name: 'myLib' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
appRoot: '',
|
||||
name: 'my-lib',
|
||||
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', () => {
|
||||
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');
|
||||
});
|
||||
describe('nested', () => {
|
||||
it('should update angular-cli.json', () => {
|
||||
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir' }, appTree);
|
||||
const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json'));
|
||||
expect(updatedAngularCLIJson.apps).toEqual([
|
||||
{
|
||||
appRoot: '',
|
||||
name: 'my-dir/my-lib',
|
||||
root: 'libs/my-dir/my-lib/src',
|
||||
test: '../../../../test.js'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir', nomodule: true }, appTree);
|
||||
expect(tree.exists('libs/my-dir/my-lib/src/my-lib.ts')).toBeTruthy();
|
||||
expect(tree.exists('libs/my-dir/my-lib/src/my-lib.spec.ts')).toBeTruthy();
|
||||
expect(tree.exists('libs/my-dir/my-lib/index.ts')).toBeTruthy();
|
||||
expect(getFileContent(tree, 'libs/my-dir/my-lib/src/my-lib.ts')).toContain('class MyLib');
|
||||
});
|
||||
});
|
||||
|
||||
describe('router', () => {
|
||||
@ -60,53 +85,75 @@ describe('lib', () => {
|
||||
|
||||
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');
|
||||
const tree = schematicRunner.runSchematic(
|
||||
'lib',
|
||||
{ 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', () => {
|
||||
appTree = createApp(appTree, 'myapp');
|
||||
const tree = schematicRunner.runSchematic(
|
||||
'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
|
||||
);
|
||||
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(
|
||||
'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
|
||||
);
|
||||
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', () => {
|
||||
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'));
|
||||
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', () => {
|
||||
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');
|
||||
const tree = schematicRunner.runSchematic('lib', { name: 'myLib', directory: 'myDir', routing: 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]');
|
||||
expect(getFileContent(tree, 'libs/my-dir/my-lib/src/my-lib.module.ts')).toContain(
|
||||
'const myLibRoutes: Route[] = '
|
||||
);
|
||||
expect(getFileContent(tree, 'libs/my-dir/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' },
|
||||
{ name: 'myLib', directory: 'myDir', routing: true, parentModule: 'apps/myapp/src/app/app.module.ts' },
|
||||
appTree
|
||||
);
|
||||
expect(getFileContent(tree, 'apps/myapp/src/app/app.module.ts')).toContain(
|
||||
@ -115,7 +162,7 @@ describe('lib', () => {
|
||||
|
||||
const tree2 = schematicRunner.runSchematic(
|
||||
'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
|
||||
);
|
||||
expect(getFileContent(tree2, 'apps/myapp/src/app/app.module.ts')).toContain(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export interface Schema {
|
||||
name: string;
|
||||
directory?: string;
|
||||
sourceDir?: string;
|
||||
nomodule: boolean;
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
"type": "string",
|
||||
"description": "Library name"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "A directory where the app is placed"
|
||||
},
|
||||
"sourceDir": {
|
||||
"type": "string",
|
||||
"default": "src",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Tree, Rule } from '@angular-devkit/schematics';
|
||||
import { angularJsVersion } from './lib-versions';
|
||||
import { serializeJson } from './fileutils';
|
||||
import { Schema } from '../app/schema';
|
||||
|
||||
export function addUpgradeToPackageJson(): Rule {
|
||||
return (host: Tree) => {
|
||||
@ -23,3 +24,14 @@ export function addUpgradeToPackageJson(): Rule {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ export const angularJsVersion = '1.6.6';
|
||||
export const ngrxVersion = '^4.1.0';
|
||||
export const nxVersion = '*';
|
||||
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 libVersions = {
|
||||
|
||||
@ -13,7 +13,14 @@ import {
|
||||
} from '@angular-devkit/schematics';
|
||||
import { Schema } from './schema';
|
||||
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 { join } from 'path';
|
||||
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'));
|
||||
angularCliJson.project.npmScope = npmScope(options);
|
||||
angularCliJson.project.latestMigration = latestMigration;
|
||||
|
||||
if (angularCliJson.apps.length !== 1) {
|
||||
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 => {
|
||||
json[key] = undefined;
|
||||
});
|
||||
json['nx-enforce-module-boundaries'] = [true, { npmScope: npmScope(options), lazyLoad: [], allow: [] }];
|
||||
json['nx-enforce-module-boundaries'] = [true, { lazyLoad: [], allow: [] }];
|
||||
});
|
||||
return host;
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { Rule } from './nxEnforceModuleBoundariesRule';
|
||||
describe('Enforce Module Boundaries', () => {
|
||||
it('should not error when everything is in order', () => {
|
||||
const failures = runRule(
|
||||
{ npmScope: 'mycompany', allow: ['@mycompany/mylib/deep'] },
|
||||
{ allow: ['@mycompany/mylib/deep'] },
|
||||
`
|
||||
import '@mycompany/mylib';
|
||||
import '@mycompany/mylib/deep';
|
||||
@ -25,14 +25,14 @@ describe('Enforce Module Boundaries', () => {
|
||||
});
|
||||
|
||||
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[0].getFailure()).toEqual('deep imports into libraries are forbidden');
|
||||
});
|
||||
|
||||
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[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 rule = new Rule(options, 'proj');
|
||||
const rule = new Rule(options, 'proj', 'mycompany', ['mylib']);
|
||||
return rule.apply(sourceFile);
|
||||
}
|
||||
|
||||
@ -2,40 +2,71 @@ import * as path from 'path';
|
||||
import * as Lint from 'tslint';
|
||||
import { IOptions } from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
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);
|
||||
if (!path) {
|
||||
const cliConfig = this.readCliConfig();
|
||||
this.path = process.cwd();
|
||||
this.npmScope = cliConfig.project.npmScope;
|
||||
this.appNames = cliConfig.apps.map(a => a.name);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
public visitImportDeclaration(node: ts.ImportDeclaration) {
|
||||
const npmScope = `@${this.getOptions()[0].npmScope}`;
|
||||
const lazyLoad = this.getOptions()[0].lazyLoad;
|
||||
const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1);
|
||||
const allow: string[] = Array.isArray(this.getOptions()[0].allow)
|
||||
? this.getOptions()[0].allow.map(a => `${a}`)
|
||||
: [];
|
||||
const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1);
|
||||
const impParts = imp.split(path.sep);
|
||||
const lazyLoad: string[] = Array.isArray(this.getOptions()[0].lazyLoad)
|
||||
? this.getOptions()[0].lazyLoad.map(a => `${a}`)
|
||||
: [];
|
||||
|
||||
if (impParts[0] === npmScope && allow.indexOf(imp) === -1 && impParts.length > 2) {
|
||||
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden');
|
||||
} else if (impParts[0] === npmScope && impParts.length === 2 && lazyLoad && lazyLoad.indexOf(impParts[1]) > -1) {
|
||||
// whitelisted import => return
|
||||
if (allow.indexOf(imp) > -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');
|
||||
} 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');
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user