diff --git a/e2e/schematics/command-line.test.ts b/e2e/schematics/command-line.test.ts index 2db69023bec93..654ac288b048d 100644 --- a/e2e/schematics/command-line.test.ts +++ b/e2e/schematics/command-line.test.ts @@ -5,12 +5,18 @@ describe('Command line', () => { 'lint should ensure module boundaries', () => { newProject(); - newApp('myapp'); + newApp('myapp --tags=validtag'); newApp('myapp2'); newLib('mylib'); newLib('lazylib'); + newLib('invalidtaglib --tags=invalidtag'); + newLib('validtaglib --tags=validtag'); const tslint = JSON.parse(readFile('tslint.json')); + tslint.rules["nx-enforce-module-boundaries"][1].depConstraints = [ + { "sourceTag": "validtag", "onlyDependOnLibsWithTags": ["validtag"] }, + ...tslint.rules["nx-enforce-module-boundaries"][1].depConstraints + ]; updateFile('tslint.json', JSON.stringify(tslint, null, 2)); updateFile( @@ -20,6 +26,8 @@ describe('Command line', () => { import '@proj/lazylib'; import '@proj/mylib/deep'; import '@proj/myapp2'; + import '@proj/invalidtaglib'; + import '@proj/validtaglib'; const s = {loadChildren: '@proj/lazylib'}; ` @@ -30,6 +38,7 @@ describe('Command line', () => { expect(out).toContain('imports of lazy-loaded libraries are forbidden'); expect(out).toContain('deep imports into libraries are forbidden'); expect(out).toContain('imports of apps are forbidden'); + expect(out).toContain('A project tagged with "validtag" can only depend on libs tagged with "validtag"'); }, 1000000 ); diff --git a/packages/schematics/migrations/20180313-add-tags.ts b/packages/schematics/migrations/20180313-add-tags.ts new file mode 100644 index 0000000000000..c9eca9bbcb033 --- /dev/null +++ b/packages/schematics/migrations/20180313-add-tags.ts @@ -0,0 +1,19 @@ +import {updateJsonFile} from '../../shared/fileutils'; + +export default { + description: 'Add tags to all app and libs', + run: async () => { + updateJsonFile('.angular-cli.json', json => { + json.apps = json.apps.map(app => ({...app, tags: []})); + }); + + updateJsonFile('tslint.json', json => { + if (json.rules["nx-enforce-module-boundaries"]) { + json.rules["nx-enforce-module-boundaries"][1].depConstraints = [ + { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } + ]; + json.rules["nx-enforce-module-boundaries"][1].lazyLoad = undefined; + } + }); + } +}; diff --git a/packages/schematics/src/collection/app/app.spec.ts b/packages/schematics/src/collection/app/app.spec.ts index 9bb9823ca11cc..47c278394f164 100644 --- a/packages/schematics/src/collection/app/app.spec.ts +++ b/packages/schematics/src/collection/app/app.spec.ts @@ -33,6 +33,7 @@ describe('app', () => { root: 'apps/my-app/src', scripts: [], styles: ['styles.css'], + tags: [], test: '../../../test.js', testTsconfig: '../../../tsconfig.spec.json', tsconfig: 'tsconfig.app.json' @@ -88,6 +89,7 @@ describe('app', () => { root: 'apps/my-dir/my-app/src', scripts: [], styles: ['styles.css'], + tags: [], test: '../../../../test.js', testTsconfig: '../../../../tsconfig.spec.json', tsconfig: 'tsconfig.app.json' @@ -169,4 +171,12 @@ describe('app', () => { ); }); }); + + describe('tags', () => { + it('should split tags by a comma', () => { + const tree = schematicRunner.runSchematic('app', { name: 'myApp', npmScope: 'nrwl', tags: 'one,two' }, appTree); + const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json')); + expect(updatedAngularCLIJson.apps[0].tags).toEqual(['one', 'two']); + }); + }); }); diff --git a/packages/schematics/src/collection/app/index.ts b/packages/schematics/src/collection/app/index.ts index b54e74fefee52..50e1b0945c27e 100644 --- a/packages/schematics/src/collection/app/index.ts +++ b/packages/schematics/src/collection/app/index.ts @@ -62,6 +62,7 @@ function addAppToAngularCliJson(options: NormalizedSchema): Rule { throw new Error('Missing .angular-cli.json'); } + const tags = options.tags ? options.tags.split(',').map(s => s.trim()) : []; const sourceText = host.read('.angular-cli.json')!.toString('utf-8'); const json = JSON.parse(sourceText); json.apps = addApp(json.apps, { @@ -82,7 +83,8 @@ function addAppToAngularCliJson(options: NormalizedSchema): Rule { environments: { dev: 'environments/environment.ts', prod: 'environments/environment.prod.ts' - } + }, + tags }); json.lint = [ diff --git a/packages/schematics/src/collection/app/schema.d.ts b/packages/schematics/src/collection/app/schema.d.ts index 347f5ed09881b..f3695083501a9 100644 --- a/packages/schematics/src/collection/app/schema.d.ts +++ b/packages/schematics/src/collection/app/schema.d.ts @@ -11,4 +11,5 @@ export interface Schema { skipTests?: boolean; prefix?: string; style?: string; + tags?: string; } diff --git a/packages/schematics/src/collection/app/schema.json b/packages/schematics/src/collection/app/schema.json index f737ca480782b..9b2eb60b56eb2 100644 --- a/packages/schematics/src/collection/app/schema.json +++ b/packages/schematics/src/collection/app/schema.json @@ -59,6 +59,10 @@ "description": "The file extension to be used for style files.", "type": "string", "default": "css" + }, + "tags": { + "type": "string", + "description": "Add tags to the application (used for linting)" } }, "required": [ diff --git a/packages/schematics/src/collection/application/files/__directory__/tslint.json b/packages/schematics/src/collection/application/files/__directory__/tslint.json index d57f2f3c419ba..feb3152e817d5 100644 --- a/packages/schematics/src/collection/application/files/__directory__/tslint.json +++ b/packages/schematics/src/collection/application/files/__directory__/tslint.json @@ -94,7 +94,10 @@ "nx-enforce-module-boundaries": [ true, { - "allow": [] + "allow": [], + "depConstraints": [ + { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } + ] } ] } diff --git a/packages/schematics/src/collection/lib/index.ts b/packages/schematics/src/collection/lib/index.ts index b742b35e77e63..b705e602f214a 100644 --- a/packages/schematics/src/collection/lib/index.ts +++ b/packages/schematics/src/collection/lib/index.ts @@ -17,12 +17,14 @@ interface NormalizedSchema extends Schema { function addLibToAngularCliJson(options: NormalizedSchema): Rule { return (host: Tree) => { + const tags = options.tags ? options.tags.split(',').map(s => s.trim()) : []; const json = cliConfig(host); json.apps = addApp(json.apps, { name: options.fullName, root: options.fullPath, test: `${offsetFromRoot(options.fullPath)}test.js`, - appRoot: '' + appRoot: '', + tags }); host.overwrite('.angular-cli.json', serializeJson(json)); @@ -141,22 +143,6 @@ function addChildren(schema: NormalizedSchema): Rule { }; } -function updateTsLint(schema: NormalizedSchema): 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.fullName)); - host.overwrite('tslint.json', serializeJson(tsLint)); - } - return host; - }; -} - export default function(schema: Schema): Rule { return wrapIntoFormat(() => { const options = normalizeOptions(schema); @@ -185,7 +171,6 @@ export default function(schema: Schema): Rule { branchAndMerge(chain([mergeWith(templateSource)])), addLibToAngularCliJson(options), options.routing && options.lazy ? addLazyLoadedRouterConfiguration(modulePath) : noop(), - options.routing && options.lazy ? updateTsLint(options) : noop(), options.routing && options.lazy && options.parentModule ? addLoadChildren(options) : noop(), options.routing && !options.lazy ? addRouterConfiguration(options, indexFile, moduleFileName, modulePath) : noop(), diff --git a/packages/schematics/src/collection/lib/lib.spec.ts b/packages/schematics/src/collection/lib/lib.spec.ts index 9f6bc7303a640..a286d6fe9e3fe 100644 --- a/packages/schematics/src/collection/lib/lib.spec.ts +++ b/packages/schematics/src/collection/lib/lib.spec.ts @@ -26,6 +26,7 @@ describe('lib', () => { appRoot: '', name: 'my-lib', root: 'libs/my-lib/src', + tags: [], test: '../../../test.js' } ]); @@ -57,6 +58,7 @@ describe('lib', () => { appRoot: '', name: 'my-dir/my-lib', root: 'libs/my-dir/my-lib/src', + tags: [], test: '../../../../test.js' } ]); @@ -148,16 +150,6 @@ describe('lib', () => { '../../../libs/my-dir/my-lib2/index.ts' ]); }); - - it('should register the module as lazy loaded in tslint.json', () => { - 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-dir/my-lib']); - }); }); describe('eager', () => { @@ -192,5 +184,13 @@ describe('lib', () => { ); }); }); + + describe('tags', () => { + it('should split tags by a comma', () => { + const tree = schematicRunner.runSchematic('lib', { name: 'myLib', tags: 'one,two' }, appTree); + const updatedAngularCLIJson = JSON.parse(getFileContent(tree, '/.angular-cli.json')); + expect(updatedAngularCLIJson.apps[0].tags).toEqual(['one', 'two']); + }); + }); }); }); diff --git a/packages/schematics/src/collection/lib/schema.d.ts b/packages/schematics/src/collection/lib/schema.d.ts index 0f62e3cbd1fac..34f47890684f3 100644 --- a/packages/schematics/src/collection/lib/schema.d.ts +++ b/packages/schematics/src/collection/lib/schema.d.ts @@ -11,4 +11,5 @@ export interface Schema { routing?: boolean; lazy?: boolean; parentModule?: string; + tags?: string; } diff --git a/packages/schematics/src/collection/lib/schema.json b/packages/schematics/src/collection/lib/schema.json index de151aa7ec104..50f7e95cccb18 100644 --- a/packages/schematics/src/collection/lib/schema.json +++ b/packages/schematics/src/collection/lib/schema.json @@ -30,6 +30,10 @@ "parentModule": { "type": "string", "description": "Update the router configuration of the parent module using loadChildren or children, depending on what `lazy` is set to." + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting)" } }, "required": [ diff --git a/packages/schematics/src/collection/workspace/index.ts b/packages/schematics/src/collection/workspace/index.ts index 28e442f54b8b6..515e965402cac 100644 --- a/packages/schematics/src/collection/workspace/index.ts +++ b/packages/schematics/src/collection/workspace/index.ts @@ -106,6 +106,7 @@ function updateAngularCLIJson(options: Schema) { app.test = '../../../test.js'; app.testTsconfig = '../../../tsconfig.spec.json'; app.scripts = app.scripts.map(p => path.join('../../', p)); + app.tags = []; if (!angularCliJson.defaults) { angularCliJson.defaults = {}; } @@ -187,7 +188,15 @@ function updateTsLintJson(options: Schema) { json[key] = undefined; }); json.rulesDirectory.push('node_modules/@nrwl/schematics/src/tslint'); - json['nx-enforce-module-boundaries'] = [true, { allow: [] }]; + json['nx-enforce-module-boundaries'] = [ + true, + { + "allow": [], + "depConstraints": [ + { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } + ] + } + ]; }); return host; }; diff --git a/packages/schematics/src/command-line/affected-apps.spec.ts b/packages/schematics/src/command-line/affected-apps.spec.ts index 59078976eddd5..fc4218054d092 100644 --- a/packages/schematics/src/command-line/affected-apps.spec.ts +++ b/packages/schematics/src/command-line/affected-apps.spec.ts @@ -10,12 +10,14 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: [], + tags: [], type: ProjectType.app }, { name: 'app2', root: '', files: [], + tags: [], type: ProjectType.app } ], @@ -33,18 +35,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'lib1', root: '', files: ['lib1.ts'], + tags: [], type: ProjectType.lib }, { name: 'lib2', root: '', files: ['lib2.ts'], + tags: [], type: ProjectType.lib } ], @@ -80,18 +85,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'lib1', root: '', files: ['lib1.ts'], + tags: [], type: ProjectType.lib }, { name: 'lib2', root: '', files: ['lib2.ts'], + tags: [], type: ProjectType.lib } ], @@ -124,6 +132,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['index.html'], + tags: [], type: ProjectType.app } ], @@ -141,12 +150,14 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'aa', root: '', files: ['aa.ts'], + tags: [], type: ProjectType.app }, { name: 'aa/bb', root: '', files: ['bb.ts'], + tags: [], type: ProjectType.app } ], @@ -173,24 +184,28 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'app2', root: '', files: ['app2.ts'], + tags: [], type: ProjectType.app }, { name: 'lib1', root: '', files: ['lib1.ts'], + tags: [], type: ProjectType.lib }, { name: 'lib2', root: '', files: ['lib2.ts'], + tags: [], type: ProjectType.lib } ], @@ -222,18 +237,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'app2', root: '', files: ['app2.ts'], + tags: [], type: ProjectType.app }, { name: 'lib1', root: '', files: ['lib1.ts'], + tags: [], type: ProjectType.lib } ], @@ -263,6 +281,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['one\\app1.ts', 'two/app1.ts'], + tags: [], type: ProjectType.app } ], @@ -288,12 +307,14 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'app2', root: '', files: ['app2.ts'], + tags: [], type: ProjectType.app } ], @@ -320,24 +341,28 @@ describe('Calculates Dependencies Between Apps and Libs', () => { name: 'app1', root: '', files: ['app1.ts'], + tags: [], type: ProjectType.app }, { name: 'app2', root: '', files: ['app2.ts'], + tags: [], type: ProjectType.app }, { name: 'lib1', root: '', files: ['lib1.ts'], + tags: [], type: ProjectType.lib }, { name: 'lib2', root: '', files: ['lib2.ts'], + tags: [], type: ProjectType.lib } ], diff --git a/packages/schematics/src/command-line/affected-apps.ts b/packages/schematics/src/command-line/affected-apps.ts index febe5ffe540ad..c626bef6fe4a6 100644 --- a/packages/schematics/src/command-line/affected-apps.ts +++ b/packages/schematics/src/command-line/affected-apps.ts @@ -11,7 +11,7 @@ export enum DependencyType { loadChildren = 'loadChildren' } -export type ProjectNode = { name: string; root: string; type: ProjectType; files: string[] }; +export type ProjectNode = { name: string; root: string; type: ProjectType; tags: string[]; files: string[] }; export type Dependency = { projectName: string; type: DependencyType }; export function touchedProjects(projects: ProjectNode[], touchedFiles: string[]) { diff --git a/packages/schematics/src/command-line/shared.ts b/packages/schematics/src/command-line/shared.ts index 5b0a6ceb1ae06..d7c1088c34a86 100644 --- a/packages/schematics/src/command-line/shared.ts +++ b/packages/schematics/src/command-line/shared.ts @@ -47,6 +47,7 @@ export function getProjectNodes(config) { name: p.name, root: p.root, type: p.root.startsWith('apps/') ? ProjectType.app : ProjectType.lib, + tags: p.tags, files: allFilesInDir(path.dirname(p.root)) }; }); diff --git a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts index 4edb74a3416d6..f97a8fdd4b26b 100644 --- a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts +++ b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.spec.ts @@ -2,7 +2,12 @@ import { RuleFailure } from 'tslint'; import * as ts from 'typescript'; import { Rule } from './nxEnforceModuleBoundariesRule'; -import {Dependency, DependencyType, ProjectNode, ProjectType} from '../command-line/affected-apps'; +import { + Dependency, + DependencyType, + ProjectNode, + ProjectType +} from '../command-line/affected-apps'; describe('Enforce Module Boundaries', () => { it('should not error when everything is in order', () => { @@ -19,19 +24,15 @@ describe('Enforce Module Boundaries', () => { name: 'myapp', root: 'libs/myapp/src', type: ProjectType.app, - files: [ - `apps/myapp/src/main.ts`, - `apps/myapp/blah.ts` - ] + tags: [], + files: [`apps/myapp/src/main.ts`, `apps/myapp/blah.ts`] }, { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, - files: [ - `libs/mylib/index.ts`, - `libs/mylib/deep.ts` - ] + tags: [], + files: [`libs/mylib/index.ts`, `libs/mylib/deep.ts`] } ] ); @@ -39,6 +40,119 @@ describe('Enforce Module Boundaries', () => { expect(failures.length).toEqual(0); }); + describe('depConstraints', () => { + const projectNodes = [ + { + name: 'api', + root: 'libs/api/src', + type: ProjectType.lib, + tags: ['api'], + files: [`libs/api/index.ts`] + }, + { + name: 'impl', + root: 'libs/impl/src', + type: ProjectType.lib, + tags: ['impl'], + files: [`libs/impl/index.ts`] + }, + { + name: 'impl2', + root: 'libs/impl2/src', + type: ProjectType.lib, + tags: ['impl'], + files: [`libs/impl2/index.ts`] + }, + { + name: 'untagged', + root: 'libs/untagged/src', + type: ProjectType.lib, + tags: [], + files: [`libs/untagged/index.ts`] + } + ]; + + const depConstraints = { + depConstraints: [ + { sourceTag: 'api', onlyDependOnLibsWithTags: ['api'] }, + { sourceTag: 'impl', onlyDependOnLibsWithTags: ['api', 'impl'] } + ] + }; + + it('should error when the target library does not have the right tag', () => { + const failures = runRule( + depConstraints, + `${process.cwd()}/proj/libs/api/index.ts`, + ` + import '@mycompany/impl'; + `, + projectNodes + ); + + expect(failures[0].getFailure()).toEqual( + 'A project tagged with "api" can only depend on libs tagged with "api"' + ); + }); + + it('should error when the target library is untagged', () => { + const failures = runRule( + depConstraints, + `${process.cwd()}/proj/libs/api/index.ts`, + ` + import '@mycompany/untagged'; + `, + projectNodes + ); + + expect(failures[0].getFailure()).toEqual( + 'A project tagged with "api" can only depend on libs tagged with "api"' + ); + }); + + it('should error when the source library is untagged', () => { + const failures = runRule( + depConstraints, + `${process.cwd()}/proj/libs/untagged/index.ts`, + ` + import '@mycompany/api'; + `, + projectNodes + ); + + expect(failures[0].getFailure()).toEqual( + 'A project without tags cannot depend on any libraries' + ); + }); + + it('should not error when the constraints are satisfied', () => { + const failures = runRule( + depConstraints, + `${process.cwd()}/proj/libs/impl/index.ts`, + ` + import '@mycompany/impl2'; + `, + projectNodes + ); + + expect(failures.length).toEqual(0); + }); + + it('should support wild cards', () => { + const failures = runRule( + { + depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }] + }, + `${process.cwd()}/proj/libs/api/index.ts`, + ` + import '@mycompany/impl'; + `, + projectNodes + ); + + expect(failures.length).toEqual(0); + }); + }); + describe('relative imports', () => { it('should not error when relatively importing the same library', () => { const failures = runRule( @@ -50,10 +164,8 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, - files: [ - `libs/mylib/src/main.ts`, - `libs/mylib/other.ts` - ] + tags: [], + files: [`libs/mylib/src/main.ts`, `libs/mylib/other.ts`] } ] ); @@ -70,10 +182,8 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, - files: [ - `libs/mylib/src/main.ts`, - `libs/mylib/other/index.ts` - ] + tags: [], + files: [`libs/mylib/src/main.ts`, `libs/mylib/other/index.ts`] } ] ); @@ -84,19 +194,21 @@ describe('Enforce Module Boundaries', () => { const failures = runRule( {}, `${process.cwd()}/proj/libs/mylib/src/main.ts`, - 'import "../other"', + 'import "../../other"', [ { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, + tags: [], files: [`libs/mylib/src/main.ts`] }, { name: 'other', root: 'libs/other/src', type: ProjectType.lib, - files: [] + tags: [], + files: ['libs/other/index.ts'] } ] ); @@ -116,10 +228,8 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, - files: [ - `libs/mylib/src/main.ts`, - `libs/mylib/src/other.ts` - ] + tags: [], + files: [`libs/mylib/src/main.ts`, `libs/mylib/src/other.ts`] } ] ); @@ -140,12 +250,14 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, + tags: [], files: [`libs/mylib/src/main.ts`] }, { name: 'other', root: 'libs/other/src', type: ProjectType.lib, + tags: [], files: [`libs/other/blah.ts`] } ] @@ -165,20 +277,24 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, + tags: [], files: [`libs/mylib/src/main.ts`] }, { name: 'other', root: 'libs/other/src', type: ProjectType.lib, + tags: [], files: [`libs/other/index.ts`] } ], { - mylib: [{projectName: 'other', type: DependencyType.loadChildren}] + mylib: [{ projectName: 'other', type: DependencyType.loadChildren }] } ); - expect(failures[0].getFailure()).toEqual('imports of lazy-loaded libraries are forbidden'); + expect(failures[0].getFailure()).toEqual( + 'imports of lazy-loaded libraries are forbidden' + ); }); it('should error on importing an app', () => { @@ -191,19 +307,19 @@ describe('Enforce Module Boundaries', () => { name: 'mylib', root: 'libs/mylib/src', type: ProjectType.lib, + tags: [], files: [`libs/mylib/src/main.ts`] }, { name: 'myapp', root: 'apps/myapp/src', type: ProjectType.app, + tags: [], files: [`apps/myapp/index.ts`] } ] ); - expect(failures[0].getFailure()).toEqual( - 'imports of apps are forbidden' - ); + expect(failures[0].getFailure()).toEqual('imports of apps are forbidden'); }); }); @@ -226,6 +342,12 @@ function runRule( ts.ScriptTarget.Latest, true ); - const rule = new Rule(options, `${process.cwd()}/proj`, 'mycompany', projectNodes, deps); + const rule = new Rule( + options, + `${process.cwd()}/proj`, + 'mycompany', + projectNodes, + deps + ); return rule.apply(sourceFile); -} \ No newline at end of file +} diff --git a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts index 0e2633c8e4860..b050664b87e0d 100644 --- a/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts +++ b/packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts @@ -43,7 +43,15 @@ export class Rule extends Lint.Rules.AbstractRule { } } +type DepConstraint = { + sourceTag: string; + onlyDependOnLibsWithTags: string[]; +}; + class EnforceModuleBoundariesWalker extends Lint.RuleWalker { + private readonly allow: string[]; + private readonly depConstraints: DepConstraint[]; + constructor( sourceFile: ts.SourceFile, options: IOptions, @@ -53,60 +61,97 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker { private readonly deps: { [projectName: string]: Dependency[] } ) { super(sourceFile, options); + + this.allow = Array.isArray(this.getOptions()[0].allow) + ? this.getOptions()[0].allow.map(a => `${a}`) + : []; + + this.depConstraints = Array.isArray(this.getOptions()[0].depConstraints) + ? this.getOptions()[0].depConstraints + : []; } public visitImportDeclaration(node: ts.ImportDeclaration) { 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}`) - : []; - // whitelisted import => return - if (allow.indexOf(imp) > -1) { + // whitelisted import + if (this.allow.indexOf(imp) > -1) { super.visitImportDeclaration(node); return; } + // check for relative and absolute imports if (this.isRelativeImportIntoAnotherProject(imp) || this.isAbsoluteImportIntoAnotherProject(imp)) { this.addFailureAt(node.getStart(), node.getWidth(), `library imports must start with @${this.npmScope}/`); return; } + // check constraints between libs and apps if (imp.startsWith(`@${this.npmScope}/`)) { const name = imp.split('/')[1]; const sourceProject = this.findSourceProject(); const targetProject = this.findProjectUsingName(name); - if (sourceProject === targetProject || !targetProject) { + // something went wrong => return. + if (!sourceProject || !targetProject) { super.visitImportDeclaration(node); return; } + // same project => allow + if (sourceProject === targetProject) { + super.visitImportDeclaration(node); + return; + } + + // cannot import apps if (targetProject.type === ProjectType.app) { this.addFailureAt(node.getStart(), node.getWidth(), 'imports of apps are forbidden'); return; } + // deep imports aren't allowed if (imp.split('/').length > 2) { this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden'); return; } + // if we import a library using loadChildre, we should not import it using es6imports if (this.onlyLoadChildren(sourceProject.name, targetProject.name, [])) { this.addFailureAt(node.getStart(), node.getWidth(), 'imports of lazy-loaded libraries are forbidden'); return; } + + // check that dependency constraints are satisfied + if (this.depConstraints.length > 0) { + const constraint = this.findConstraintFor(sourceProject); + + // when no constrains found => error. Force the user to provision them. + if (!constraint) { + this.addFailureAt(node.getStart(), node.getWidth(), `A project without tags cannot depend on any libraries`); + return; + } + + if (hasNoneOfTheseTags(targetProject, constraint.onlyDependOnLibsWithTags || [])) { + const allowedTags = constraint.onlyDependOnLibsWithTags + .map(s => `"${s}"`) + .join(', '); + const error = `A project tagged with "${constraint.sourceTag}" can only depend on libs tagged with ${allowedTags}`; + this.addFailureAt(node.getStart(), node.getWidth(), error); + return; + } + } } super.visitImportDeclaration(node); } - private onlyLoadChildren(source: string, target: string, path: string[]) { - if (path.indexOf(source) > -1) return false; - return (this.deps[source] || []).filter(d => { + private onlyLoadChildren(sourceProjectName: string, targetProjectName: string, visited: string[]) { + if (visited.indexOf(sourceProjectName) > -1) return false; + return (this.deps[sourceProjectName] || []).filter(d => { if (d.type !== DependencyType.loadChildren) return false; - if (d.projectName === target) return true; - return this.onlyLoadChildren(d.projectName, target, [...path, source]); + if (d.projectName === targetProjectName) return true; + return this.onlyLoadChildren(d.projectName, targetProjectName, [...visited, sourceProjectName]); }).length > 0; } @@ -118,42 +163,60 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker { .split(path.sep) .join('/') .substring(this.projectPath.length + 1); + const sourceProject = this.findSourceProject(); + const targetProject = this.findTargetProject(targetFile); + return sourceProject && targetProject && sourceProject !== targetProject; + } - let targetProject = this.findProjectUsingFile(targetFile); - if (!targetProject) { - targetProject = this.findProjectUsingFile(path.join(targetFile, 'index')); - } - return sourceProject !== targetProject || targetProject === undefined; + private getSourceFilePath() { + return this.getSourceFile().fileName.substring(this.projectPath.length + 1); } private findSourceProject() { - return this.projectNodes.filter(n => { - return n.files.filter(f => removeExt(f) === removeExt(this.getSourceFilePath()))[0]; - })[0]; + const targetFile = removeExt(this.getSourceFilePath()); + return this.findProjectUsingFile(targetFile); } - private getSourceFilePath() { - return this.getSourceFile().fileName.substring(this.projectPath.length + 1); + private findTargetProject(targetFile: string) { + let targetProject = this.findProjectUsingFile(targetFile); + if (!targetProject) { + targetProject = this.findProjectUsingFile(path.join(targetFile, 'index')); + } + return targetProject; } private findProjectUsingFile(file: string) { - return this.projectNodes.filter(n => { - return n.files.filter(f => removeExt(f) === file)[0]; - })[0]; + return this.projectNodes.filter(n => containsFile(n.files, file))[0]; } private findProjectUsingName(name: string) { return this.projectNodes.filter(n => n.name === name)[0]; } - private isAbsoluteImportIntoAnotherProject(imp: string): boolean { + private isAbsoluteImportIntoAnotherProject(imp: string) { return imp.startsWith('libs/') || (imp.startsWith('/libs/') && imp.startsWith('apps/')) || imp.startsWith('/apps/'); } - private isRelative(s: string): boolean { + private isRelative(s: string) { return s.startsWith('.'); } + + private findConstraintFor(sourceProject: ProjectNode) { + return this.depConstraints.filter(f => hasTag(sourceProject, f.sourceTag))[0]; + } +} + +function hasNoneOfTheseTags(proj: ProjectNode, tags: string[]) { + return tags.filter(allowedTag => hasTag(proj, allowedTag)).length === 0; +} + +function hasTag(proj: ProjectNode, tag: string) { + return (proj.tags || []).indexOf(tag) > -1 || tag === '*'; +} + +function containsFile(files: string[], targetFileWithoutExtension: string): boolean { + return !!files.filter(f => removeExt(f) === targetFileWithoutExtension)[0]; } function removeExt(file:string): string {