nx/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts

1430 lines
42 KiB
TypeScript

import {
DependencyType,
ProjectGraph,
ProjectType,
} from '@nrwl/workspace/src/core/project-graph';
import { TSESLint } from '@typescript-eslint/experimental-utils';
import * as parser from '@typescript-eslint/parser';
import { vol } from 'memfs';
import { extname, join } from 'path';
import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from '../../src/rules/enforce-module-boundaries';
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
import { readFileSync } from 'fs';
jest.mock('fs', () => require('memfs').fs);
jest.mock('../../../workspace/src/utilities/app-root', () => ({
appRootPath: '/root',
}));
const tsconfig = {
compilerOptions: {
baseUrl: '.',
paths: {
'@mycompany/impl': ['libs/impl/src/index.ts'],
'@mycompany/untagged': ['libs/untagged/src/index.ts'],
'@mycompany/api': ['libs/api/src/index.ts'],
'@mycompany/impl-domain2': ['libs/impl-domain2/src/index.ts'],
'@mycompany/impl-both-domains': ['libs/impl-both-domains/src/index.ts'],
'@mycompany/impl2': ['libs/impl2/src/index.ts'],
'@mycompany/other': ['libs/other/src/index.ts'],
'@mycompany/other/a/b': ['libs/other/src/a/b.ts'],
'@mycompany/other/a': ['libs/other/src/a/index.ts'],
'@mycompany/another/a/b': ['libs/another/a/b.ts'],
'@mycompany/myapp': ['apps/myapp/src/index.ts'],
'@mycompany/myapp-e2e': ['apps/myapp-e2e/src/index.ts'],
'@mycompany/mylib': ['libs/mylib/src/index.ts'],
'@mycompany/mylibName': ['libs/mylibName/src/index.ts'],
'@mycompany/anotherlibName': ['libs/anotherlibName/src/index.ts'],
'@mycompany/badcirclelib': ['libs/badcirclelib/src/index.ts'],
'@mycompany/domain1': ['libs/domain1/src/index.ts'],
'@mycompany/domain2': ['libs/domain2/src/index.ts'],
'@mycompany/buildableLib': ['libs/buildableLib/src/main.ts'],
'@nonBuildableScope/nonBuildableLib': [
'libs/nonBuildableLib/src/main.ts',
],
},
types: ['node'],
},
exclude: ['**/*.spec.ts'],
include: ['**/*.ts'],
};
const fileSys = {
'./libs/impl/src/index.ts': '',
'./libs/untagged/src/index.ts': '',
'./libs/api/src/index.ts': '',
'./libs/impl-domain2/src/index.ts': '',
'./libs/impl-both-domains/src/index.ts': '',
'./libs/impl2/src/index.ts': '',
'./libs/other/src/index.ts': '',
'./libs/other/src/a/b.ts': '',
'./libs/other/src/a/index.ts': '',
'./libs/another/a/b.ts': '',
'./apps/myapp/src/index.ts': '',
'./libs/mylib/src/index.ts': '',
'./libs/mylibName/src/index.ts': '',
'./libs/anotherlibName/src/index.ts': '',
'./libs/badcirclelib/src/index.ts': '',
'./libs/domain1/src/index.ts': '',
'./libs/domain2/src/index.ts': '',
'./libs/buildableLib/src/main.ts': '',
'./libs/nonBuildableLib/src/main.ts': '',
'./tsconfig.base.json': JSON.stringify(tsconfig),
};
describe('Enforce Module Boundaries', () => {
beforeEach(() => {
vol.fromJSON(fileSys, '/root');
});
it('should not error when everything is in order', () => {
const failures = runRule(
{ allow: ['@mycompany/mylib/deep'] },
`${process.cwd()}/proj/apps/myapp/src/main.ts`,
`
import '@mycompany/mylib';
import '@mycompany/mylib/deep';
import '../blah';
import('@mycompany/mylib');
import('@mycompany/mylib/deep');
import('../blah');
`,
{
nodes: {
myappName: {
name: 'myappName',
type: ProjectType.app,
data: {
root: 'libs/myapp',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`apps/myapp/src/main.ts`),
createFile(`apps/myapp/blah.ts`),
],
},
},
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`libs/mylib/src/index.ts`),
createFile(`libs/mylib/src/deep.ts`),
],
},
},
},
dependencies: {},
}
);
expect(failures.length).toEqual(0);
});
it('should handle multiple projects starting with the same prefix properly', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/apps/myapp/src/main.ts`,
`
import '@mycompany/myapp2/mylib';
import('@mycompany/myapp2/mylib');
`,
{
nodes: {
myappName: {
name: 'myappName',
type: ProjectType.app,
data: {
root: 'libs/myapp',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`apps/myapp/src/main.ts`),
createFile(`apps/myapp/src/blah.ts`),
],
},
},
myapp2Name: {
name: 'myapp2Name',
type: ProjectType.app,
data: {
root: 'libs/myapp2',
tags: [],
implicitDependencies: [],
architect: {},
files: [],
},
},
'myapp2-mylib': {
name: 'myapp2-mylib',
type: ProjectType.lib,
data: {
root: 'libs/myapp2/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile('libs/myapp2/mylib/src/index.ts')],
},
},
},
dependencies: {},
}
);
expect(failures.length).toEqual(0);
});
describe('depConstraints', () => {
const graph = {
nodes: {
apiName: {
name: 'apiName',
type: ProjectType.lib,
data: {
root: 'libs/api',
tags: ['api', 'domain1'],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/api/src/index.ts`)],
},
},
'impl-both-domainsName': {
name: 'impl-both-domainsName',
type: ProjectType.lib,
data: {
root: 'libs/impl-both-domains',
tags: ['impl', 'domain1', 'domain2'],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/impl-both-domains/src/index.ts`)],
},
},
'impl-domain2Name': {
name: 'impl-domain2Name',
type: ProjectType.lib,
data: {
root: 'libs/impl-domain2',
tags: ['impl', 'domain2'],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/impl-domain2/src/index.ts`)],
},
},
impl2Name: {
name: 'impl2Name',
type: ProjectType.lib,
data: {
root: 'libs/impl2',
tags: ['impl', 'domain1'],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/impl2/src/index.ts`)],
},
},
implName: {
name: 'implName',
type: ProjectType.lib,
data: {
root: 'libs/impl',
tags: ['impl', 'domain1'],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/impl/src/index.ts`)],
},
},
untaggedName: {
name: 'untaggedName',
type: ProjectType.lib,
data: {
root: 'libs/untagged',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/untagged/src/index.ts`)],
},
},
npmPackage: {
name: 'npm:npm-package',
type: 'npm',
data: {
packageName: 'npm-package',
version: '0.0.0',
files: [],
},
},
},
dependencies: {},
};
const depConstraints = {
depConstraints: [
{ sourceTag: 'api', onlyDependOnLibsWithTags: ['api'] },
{ sourceTag: 'impl', onlyDependOnLibsWithTags: ['api', 'impl'] },
{ sourceTag: 'domain1', onlyDependOnLibsWithTags: ['domain1'] },
{ sourceTag: 'domain2', onlyDependOnLibsWithTags: ['domain2'] },
],
};
beforeEach(() => {
vol.fromJSON(fileSys, '/root');
});
it('should error when the target library does not have the right tag', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import '@mycompany/impl';
import('@mycompany/impl');
`,
graph
);
const message =
'A project tagged with "api" can only depend on libs tagged with "api"';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should allow imports to npm packages', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import 'npm-package';
import('npm-package');
`,
graph
);
expect(failures.length).toEqual(0);
});
it('should error when the target library is untagged', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import '@mycompany/untagged';
import('@mycompany/untagged');
`,
graph
);
const message =
'A project tagged with "api" can only depend on libs tagged with "api"';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should error when the source library is untagged', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/untagged/src/index.ts`,
`
import '@mycompany/api';
import('@mycompany/api');
`,
graph
);
const message = 'A project without tags cannot depend on any libraries';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should check all tags', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/impl/src/index.ts`,
`
import '@mycompany/impl-domain2';
import('@mycompany/impl-domain2');
`,
graph
);
const message =
'A project tagged with "domain1" can only depend on libs tagged with "domain1"';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should allow a domain1 project to depend on a project that is tagged with domain1 and domain2', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/impl/src/index.ts`,
`
import '@mycompany/impl-both-domains';
import('@mycompany/impl-both-domains');
`,
graph
);
expect(failures.length).toEqual(0);
});
it('should allow a domain1/domain2 project depend on domain1', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/impl-both-domain/src/index.ts`,
`
import '@mycompany/impl';
import('@mycompany/impl');
`,
graph
);
expect(failures.length).toEqual(0);
});
it('should not error when the constraints are satisfied', () => {
const failures = runRule(
depConstraints,
`${process.cwd()}/proj/libs/impl/src/index.ts`,
`
import '@mycompany/impl2';
import('@mycompany/impl2');
`,
graph
);
expect(failures.length).toEqual(0);
});
it('should support wild cards', () => {
const failures = runRule(
{
depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }],
},
`${process.cwd()}/proj/libs/api/src/index.ts`,
`
import '@mycompany/impl';
import('@mycompany/impl');
`,
graph
);
expect(failures.length).toEqual(0);
});
});
describe('relative imports', () => {
it('should not error when relatively importing the same library', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '../other';
import('../other');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`libs/mylib/src/main.ts`),
createFile(`libs/mylib/other.ts`),
],
},
},
},
dependencies: {},
}
);
expect(failures.length).toEqual(0);
});
it('should not error when relatively importing the same library (index file)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '../other';
import('../other');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`libs/mylib/src/main.ts`),
createFile(`libs/mylib/other/index.ts`),
],
},
},
},
dependencies: {},
}
);
expect(failures.length).toEqual(0);
});
it('should error when relatively importing another library', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '../../other';
import('../../other');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
otherName: {
name: 'otherName',
type: ProjectType.lib,
data: {
root: 'libs/other',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile('libs/other/src/index.ts')],
},
},
},
dependencies: {},
}
);
const message =
'Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should error when relatively importing the src directory of another library', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '../../other/src';
import('../../other/src');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
otherName: {
name: 'otherName',
type: ProjectType.lib,
data: {
root: 'libs/other',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile('libs/other/src/index.ts')],
},
},
},
dependencies: {},
}
);
const message =
'Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
});
it('should error on absolute imports into libraries without using the npm scope', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import 'libs/src/other';
import('libs/src/other');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [
createFile(`libs/mylib/src/main.ts`),
createFile(`libs/mylib/src/other.ts`),
],
},
},
},
dependencies: {},
}
);
const message =
'Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should respect regexp in allow option', () => {
const failures = runRule(
{ allow: ['^.*/utils/.*$'] },
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '../../utils/a';
import('../../utils/a');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
utils: {
name: 'utils',
type: ProjectType.lib,
data: {
root: 'libs/utils',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/utils/a.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toEqual(0);
});
it.each`
importKind | shouldError | importStatement
${'value'} | ${true} | ${'import { someValue } from "@mycompany/other";'}
${'type'} | ${false} | ${'import type { someType } from "@mycompany/other";'}
`(
`when importing a lazy-loaded library:
\t importKind: $importKind
\t shouldError: $shouldError`,
({ importKind, importStatement }) => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
importStatement,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
otherName: {
name: 'otherName',
type: ProjectType.lib,
data: {
root: 'libs/other',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/other/index.ts`)],
},
},
},
dependencies: {
mylibName: [
{
source: 'mylibName',
target: 'otherName',
type: DependencyType.dynamic,
},
],
},
}
);
if (importKind === 'type') {
expect(failures.length).toEqual(0);
} else {
expect(failures[0].message).toEqual(
'Imports of lazy-loaded libraries are forbidden'
);
}
}
);
it('should error on importing an app', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '@mycompany/myapp';
import('@mycompany/myapp');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
myappName: {
name: 'myappName',
type: ProjectType.app,
data: {
root: 'apps/myapp',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`apps/myapp/src/index.ts`)],
},
},
},
dependencies: {},
}
);
const message = 'Imports of apps are forbidden';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should error on importing an e2e project', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '@mycompany/myapp-e2e';
import('@mycompany/myapp-e2e');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
myappE2eName: {
name: 'myappE2eName',
type: ProjectType.e2e,
data: {
root: 'apps/myapp-e2e',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`apps/myapp-e2e/src/index.ts`)],
},
},
},
dependencies: {},
}
);
const message = 'Imports of e2e projects are forbidden';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should error when circular dependency detected', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/anotherlib/src/main.ts`,
`
import '@mycompany/mylib';
import('@mycompany/mylib');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
anotherlibName: {
name: 'anotherlibName',
type: ProjectType.lib,
data: {
root: 'libs/anotherlib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
},
},
myappName: {
name: 'myappName',
type: ProjectType.app,
data: {
root: 'apps/myapp',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`apps/myapp/src/index.ts`)],
},
},
},
dependencies: {
mylibName: [
{
source: 'mylibName',
target: 'anotherlibName',
type: DependencyType.static,
},
],
},
}
);
const message =
'Circular dependency between "anotherlibName" and "mylibName" detected: anotherlibName -> mylibName -> anotherlibName';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should error when circular dependency detected (indirect)', () => {
const failures = runRule(
{},
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
`
import '@mycompany/badcirclelib';
import('@mycompany/badcirclelib');
`,
{
nodes: {
mylibName: {
name: 'mylibName',
type: ProjectType.lib,
data: {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
},
},
anotherlibName: {
name: 'anotherlibName',
type: ProjectType.lib,
data: {
root: 'libs/anotherlib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
},
},
badcirclelibName: {
name: 'badcirclelibName',
type: ProjectType.lib,
data: {
root: 'libs/badcirclelib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/badcirclelib/src/main.ts`)],
},
},
myappName: {
name: 'myappName',
type: ProjectType.app,
data: {
root: 'apps/myapp',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`apps/myapp/index.ts`)],
},
},
},
dependencies: {
mylibName: [
{
source: 'mylibName',
target: 'badcirclelibName',
type: DependencyType.static,
},
],
badcirclelibName: [
{
source: 'badcirclelibName',
target: 'anotherlibName',
type: DependencyType.static,
},
],
anotherlibName: [
{
source: 'anotherlibName',
target: 'mylibName',
type: DependencyType.static,
},
],
},
}
);
const message =
'Circular dependency between "mylibName" and "badcirclelibName" detected: mylibName -> badcirclelibName -> anotherlibName -> mylibName';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
describe('buildable library imports', () => {
it('should ignore the buildable library verification if the enforceBuildableLibDependency is set to false', () => {
const failures = runRule(
{
enforceBuildableLibDependency: false,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
import '@mycompany/nonBuildableLib';
import('@mycompany/nonBuildableLib');
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
nonBuildableLib: {
name: 'nonBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/nonBuildableLib',
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/nonBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
it('should error when buildable libraries import non-buildable libraries', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
import '@nonBuildableScope/nonBuildableLib';
import('@nonBuildableScope/nonBuildableLib');
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
targets: {
build: {
// defines a buildable lib
executor: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
nonBuildableLib: {
name: 'nonBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/nonBuildableLib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/nonBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
const message =
'Buildable libraries cannot import or export from non-buildable libraries';
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
it('should not error when buildable libraries import another buildable libraries', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
import '@mycompany/anotherBuildableLib';
import('@mycompany/anotherBuildableLib');
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
anotherBuildableLib: {
name: 'anotherBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/anotherBuildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/anotherBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
it('should ignore the buildable library verification if no architect is specified', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
import '@mycompany/nonBuildableLib';
import('@mycompany/nonBuildableLib');
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
nonBuildableLib: {
name: 'nonBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/nonBuildableLib',
tags: [],
implicitDependencies: [],
files: [createFile(`libs/nonBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
it('should error when exporting all from a non-buildable library', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
export * from '@nonBuildableScope/nonBuildableLib';
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
targets: {
build: {
// defines a buildable lib
executor: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
nonBuildableLib: {
name: 'nonBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/nonBuildableLib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/nonBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
const message =
'Buildable libraries cannot import or export from non-buildable libraries';
expect(failures[0].message).toEqual(message);
});
it('should not error when exporting all from a buildable library', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
export * from '@mycompany/anotherBuildableLib';
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
anotherBuildableLib: {
name: 'anotherBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/anotherBuildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/anotherBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
it('should error when exporting a named resource from a non-buildable library', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
export { foo } from '@nonBuildableScope/nonBuildableLib';
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
targets: {
build: {
// defines a buildable lib
executor: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
nonBuildableLib: {
name: 'nonBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/nonBuildableLib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/nonBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
const message =
'Buildable libraries cannot import or export from non-buildable libraries';
expect(failures[0].message).toEqual(message);
});
it('should not error when exporting a named resource from a buildable library', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
export { foo } from '@mycompany/anotherBuildableLib';
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
anotherBuildableLib: {
name: 'anotherBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/anotherBuildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/anotherBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
it('should not error when in-line exporting a named resource', () => {
const failures = runRule(
{
enforceBuildableLibDependency: true,
},
`${process.cwd()}/proj/libs/buildableLib/src/main.ts`,
`
export class Foo {};
`,
{
nodes: {
buildableLib: {
name: 'buildableLib',
type: ProjectType.lib,
data: {
root: 'libs/buildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/buildableLib/src/main.ts`)],
},
},
anotherBuildableLib: {
name: 'anotherBuildableLib',
type: ProjectType.lib,
data: {
root: 'libs/anotherBuildableLib',
tags: [],
implicitDependencies: [],
architect: {
build: {
// defines a buildable lib
builder: '@angular-devkit/build-ng-packagr:build',
},
},
files: [createFile(`libs/anotherBuildableLib/src/main.ts`)],
},
},
},
dependencies: {},
}
);
expect(failures.length).toBe(0);
});
});
});
const linter = new TSESLint.Linter();
const baseConfig = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018 as const,
sourceType: 'module' as const,
},
rules: {
[enforceModuleBoundariesRuleName]: 'error',
},
};
linter.defineParser('@typescript-eslint/parser', parser);
linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries);
function createFile(f) {
return { file: f, ext: extname(f), hash: '' };
}
function runRule(
ruleArguments: any,
contentPath: string,
content: string,
projectGraph: ProjectGraph
): TSESLint.Linter.LintMessage[] {
(global as any).projectPath = `${process.cwd()}/proj`;
(global as any).npmScope = 'mycompany';
(global as any).projectGraph = projectGraph;
(global as any).targetProjectLocator = new TargetProjectLocator(
projectGraph.nodes
);
const config = {
...baseConfig,
rules: {
[enforceModuleBoundariesRuleName]: ['error', ruleArguments],
},
};
return linter.verify(content, config as any, contentPath);
}