2118 lines
61 KiB
TypeScript
2118 lines
61 KiB
TypeScript
import 'nx/src/utils/testing/mock-fs';
|
|
|
|
import type {
|
|
FileData,
|
|
ProjectGraph,
|
|
ProjectGraphDependency,
|
|
} from '@nrwl/devkit';
|
|
import { DependencyType } from '@nrwl/devkit';
|
|
import * as parser from '@typescript-eslint/parser';
|
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
import { vol } from 'memfs';
|
|
import { TargetProjectLocator } from 'nx/src/utils/target-project-locator';
|
|
import enforceModuleBoundaries, {
|
|
RULE_NAME as enforceModuleBoundariesRuleName,
|
|
} from '../../src/rules/enforce-module-boundaries';
|
|
import { createProjectRootMappings } from 'nx/src/project-graph/utils/find-project-for-path';
|
|
|
|
jest.mock('@nrwl/devkit', () => ({
|
|
...jest.requireActual<any>('@nrwl/devkit'),
|
|
workspaceRoot: '/root',
|
|
}));
|
|
|
|
jest.mock('nx/src/utils/workspace-root', () => ({
|
|
workspaceRoot: '/root',
|
|
}));
|
|
|
|
const tsconfig = {
|
|
compilerOptions: {
|
|
baseUrl: '.',
|
|
paths: {
|
|
'@mycompany/impl': ['libs/impl/src/index.ts'],
|
|
'@mycompany/untagged': ['libs/untagged/src/index.ts'],
|
|
'@mycompany/tagged': ['libs/tagged/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/public': ['libs/public/src/index.ts'],
|
|
'@mycompany/dependsOnPrivate': ['libs/dependsOnPrivate/src/index.ts'],
|
|
'@mycompany/dependsOnPrivate2': ['libs/dependsOnPrivate2/src/index.ts'],
|
|
'@mycompany/private': ['libs/private/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 packageJson = {
|
|
dependencies: {
|
|
'npm-package': '2.3.4',
|
|
},
|
|
devDependencies: {
|
|
'npm-awesome-package': '1.2.3',
|
|
},
|
|
};
|
|
|
|
const fileSys = {
|
|
'./libs/impl/src/index.ts': '',
|
|
'./libs/untagged/src/index.ts': '',
|
|
'./libs/tagged/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': '',
|
|
'./libs/public/src/index.ts': '',
|
|
'./libs/dependsOnPrivate/src/index.ts': '',
|
|
'./libs/dependsOnPrivate2/src/index.ts': '',
|
|
'./libs/private/src/index.ts': '',
|
|
'./tsconfig.base.json': JSON.stringify(tsconfig),
|
|
'./package.json': JSON.stringify(packageJson),
|
|
'./nx.json': JSON.stringify({ npmScope: 'happyorg' }),
|
|
};
|
|
|
|
describe('Enforce Module Boundaries (eslint)', () => {
|
|
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: 'app',
|
|
data: {
|
|
root: 'libs/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`apps/myapp/src/main.ts`),
|
|
createFile(`apps/myapp/blah.ts`),
|
|
],
|
|
},
|
|
},
|
|
mylibName: {
|
|
name: 'mylibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'app',
|
|
data: {
|
|
root: 'libs/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`apps/myapp/src/main.ts`),
|
|
createFile(`apps/myapp/src/blah.ts`),
|
|
],
|
|
},
|
|
},
|
|
myapp2Name: {
|
|
name: 'myapp2Name',
|
|
type: 'app',
|
|
data: {
|
|
root: 'libs/myapp2',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [],
|
|
},
|
|
},
|
|
'myapp2-mylib': {
|
|
name: 'myapp2-mylib',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/myapp2/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile('libs/myapp2/mylib/src/index.ts')],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {},
|
|
}
|
|
);
|
|
|
|
expect(failures.length).toEqual(0);
|
|
});
|
|
|
|
describe('depConstraints', () => {
|
|
const graph: ProjectGraph = {
|
|
nodes: {
|
|
apiName: {
|
|
name: 'apiName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/api',
|
|
tags: ['api', 'domain1'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/api/src/index.ts`)],
|
|
},
|
|
},
|
|
'impl-both-domainsName': {
|
|
name: 'impl-both-domainsName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/impl-both-domains',
|
|
tags: ['impl', 'domain1', 'domain2'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/impl-both-domains/src/index.ts`)],
|
|
},
|
|
},
|
|
'impl-domain2Name': {
|
|
name: 'impl-domain2Name',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/impl-domain2',
|
|
tags: ['impl', 'domain2'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/impl-domain2/src/index.ts`)],
|
|
},
|
|
},
|
|
impl2Name: {
|
|
name: 'impl2Name',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/impl2',
|
|
tags: ['impl', 'domain1'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/impl2/src/index.ts`)],
|
|
},
|
|
},
|
|
implName: {
|
|
name: 'implName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/impl',
|
|
tags: ['impl', 'domain1'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/impl/src/index.ts`)],
|
|
},
|
|
},
|
|
publicName: {
|
|
name: 'publicName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/public',
|
|
tags: ['public'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/public/src/index.ts`)],
|
|
},
|
|
},
|
|
dependsOnPrivateName: {
|
|
name: 'dependsOnPrivateName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/dependsOnPrivate',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/dependsOnPrivate/src/index.ts`, [
|
|
{
|
|
source: 'dependsOnPrivateName',
|
|
type: 'static',
|
|
target: 'privateName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
dependsOnPrivateName2: {
|
|
name: 'dependsOnPrivateName2',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/dependsOnPrivate2',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/dependsOnPrivate2/src/index.ts`, [
|
|
{
|
|
source: 'dependsOnPrivateName2',
|
|
type: 'static',
|
|
target: 'privateName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
privateName: {
|
|
name: 'privateName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/private',
|
|
tags: ['private'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/private/src/index.ts`, [
|
|
{
|
|
source: 'privateName',
|
|
type: 'static',
|
|
target: 'untaggedName',
|
|
},
|
|
{ source: 'privateName', type: 'static', target: 'taggedName' },
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
untaggedName: {
|
|
name: 'untaggedName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/untagged',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/untagged/src/index.ts`)],
|
|
},
|
|
},
|
|
taggedName: {
|
|
name: 'taggedName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/tagged',
|
|
tags: ['some-tag'],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/tagged/src/index.ts`)],
|
|
},
|
|
},
|
|
},
|
|
externalNodes: {
|
|
'npm:npm-package': {
|
|
name: 'npm:npm-package',
|
|
type: 'npm',
|
|
data: {
|
|
packageName: 'npm-package',
|
|
version: '2.3.4',
|
|
},
|
|
},
|
|
'npm:npm-package2': {
|
|
name: 'npm:npm-package2',
|
|
type: 'npm',
|
|
data: {
|
|
packageName: 'npm-package2',
|
|
version: '0.0.0',
|
|
},
|
|
},
|
|
'npm:1npm-package': {
|
|
name: 'npm:1npm-package',
|
|
type: 'npm',
|
|
data: {
|
|
packageName: '1npm-package',
|
|
version: '0.0.0',
|
|
},
|
|
},
|
|
'npm:npm-awesome-package': {
|
|
name: 'npm:npm-awesome-package',
|
|
type: 'npm',
|
|
data: {
|
|
packageName: 'npm-awesome-package',
|
|
version: '1.2.3',
|
|
},
|
|
},
|
|
},
|
|
dependencies: {
|
|
dependsOnPrivateName: [
|
|
{
|
|
source: 'dependsOnPrivateName',
|
|
target: 'privateName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
dependsOnPrivateName2: [
|
|
{
|
|
source: 'dependsOnPrivateName2',
|
|
target: 'privateName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const depConstraints = {
|
|
depConstraints: [
|
|
{ sourceTag: 'api', onlyDependOnLibsWithTags: ['api'] },
|
|
{ sourceTag: 'impl', onlyDependOnLibsWithTags: ['api', 'impl'] },
|
|
{ sourceTag: 'domain1', onlyDependOnLibsWithTags: ['domain1'] },
|
|
{ sourceTag: 'domain2', onlyDependOnLibsWithTags: ['domain2'] },
|
|
{ sourceTag: 'public', notDependOnLibsWithTags: ['private'] },
|
|
{ sourceTag: 'private', onlyDependOnLibsWithTags: [] },
|
|
],
|
|
};
|
|
|
|
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 of 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 importing forbidden npm packages', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', bannedExternalImports: ['npm-package'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import('npm-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should not error when importing npm packages matching allowed external imports', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', allowedExternalImports: ['npm-package'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import('npm-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
expect(failures.length).toEqual(0);
|
|
});
|
|
|
|
it('should error when importing npm packages not matching allowed external imports', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', allowedExternalImports: ['npm-package'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-awesome-package';
|
|
import('npm-awesome-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'A project tagged with "api" is not allowed to import the "npm-awesome-package" package';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should not error when importing npm packages matching allowed glob pattern', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', allowedExternalImports: ['npm-awesome-*'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-awesome-package';
|
|
import('npm-awesome-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
expect(failures.length).toEqual(0);
|
|
});
|
|
|
|
it('should error when importing npm packages not matching allowed glob pattern', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', allowedExternalImports: ['npm-awesome-*'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import('npm-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should error when importing any npm package if none is allowed', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [{ sourceTag: 'api', allowedExternalImports: [] }],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import('npm-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'A project tagged with "api" is not allowed to import the "npm-package" package';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should error when importing transitive npm packages', () => {
|
|
const failures = runRule(
|
|
{
|
|
...depConstraints,
|
|
banTransitiveDependencies: true,
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package2';
|
|
import('npm-package2');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'Transitive dependencies are not allowed. Only packages defined in the "package.json" can be imported';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should not error when importing direct npm dependencies', () => {
|
|
const failures = runRule(
|
|
{
|
|
...depConstraints,
|
|
banTransitiveDependencies: true,
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import('npm-package');
|
|
import 'npm-awesome-package';
|
|
import('npm-awesome-package');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
expect(failures.length).toEqual(0);
|
|
});
|
|
|
|
it('should allow wildcards for defining forbidden npm packages', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{ sourceTag: 'api', bannedExternalImports: ['npm-*ge'] },
|
|
],
|
|
},
|
|
`${process.cwd()}/proj/libs/api/src/index.ts`,
|
|
`
|
|
import 'npm-package';
|
|
import 'npm-awesome-package';
|
|
import 'npm-package2';
|
|
import '1npm-package';
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message = (packageName) =>
|
|
`A project tagged with "api" is not allowed to import the "${packageName}" package`;
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message('npm-package'));
|
|
expect(failures[1].message).toEqual(message('npm-awesome-package'));
|
|
});
|
|
|
|
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 not error when the target library is untagged, if source expects it', () => {
|
|
const failures = runRule(
|
|
depConstraints,
|
|
`${process.cwd()}/proj/libs/private/src/index.ts`,
|
|
`
|
|
import '@mycompany/untagged';
|
|
import('@mycompany/untagged');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
expect(failures.length).toEqual(0);
|
|
});
|
|
|
|
it('should error when the target library is tagged, if source does not expect it', () => {
|
|
const failures = runRule(
|
|
depConstraints,
|
|
`${process.cwd()}/proj/libs/private/src/index.ts`,
|
|
`
|
|
import '@mycompany/tagged';
|
|
import('@mycompany/tagged');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
const message =
|
|
'A project tagged with "private" cannot depend on any libs with tags';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should error when the target library has a disallowed tag', () => {
|
|
const failures = runRule(
|
|
depConstraints,
|
|
`${process.cwd()}/proj/libs/public/src/index.ts`,
|
|
`
|
|
import '@mycompany/private';
|
|
import('@mycompany/private');
|
|
`,
|
|
{
|
|
...graph,
|
|
dependencies: {
|
|
...graph.dependencies,
|
|
publicName: [
|
|
{
|
|
source: 'publicName',
|
|
target: 'privateName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
);
|
|
|
|
const message = `A project tagged with "public" can not depend on libs tagged with "private"
|
|
|
|
Violation detected in:
|
|
- privateName`;
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should error when there is a disallowed tag in the dependency tree', () => {
|
|
const failures = runRule(
|
|
depConstraints,
|
|
`${process.cwd()}/proj/libs/public/src/index.ts`,
|
|
`
|
|
import '@mycompany/dependsOnPrivate';
|
|
import '@mycompany/dependsOnPrivate2';
|
|
import '@mycompany/private';
|
|
`,
|
|
{
|
|
...graph,
|
|
dependencies: {
|
|
...graph.dependencies,
|
|
publicName: [
|
|
{
|
|
source: 'publicName',
|
|
target: 'dependsOnPrivateName',
|
|
type: DependencyType.static,
|
|
},
|
|
{
|
|
source: 'publicName',
|
|
target: 'dependsOnPrivateName2',
|
|
type: DependencyType.static,
|
|
},
|
|
{
|
|
source: 'publicName',
|
|
target: 'privateName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(failures.length).toEqual(3);
|
|
// TODO: Add project dependency path to message
|
|
const message = (
|
|
prefix
|
|
) => `A project tagged with "public" can not depend on libs tagged with "private"
|
|
|
|
Violation detected in:
|
|
- ${prefix}privateName`;
|
|
expect(failures[0].message).toEqual(message('dependsOnPrivateName -> '));
|
|
expect(failures[1].message).toEqual(message('dependsOnPrivateName2 -> '));
|
|
expect(failures[2].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 matching at least one constraint 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);
|
|
});
|
|
|
|
it('should report errors for combo source tags', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{
|
|
allSourceTags: ['impl', 'domain1'],
|
|
onlyDependOnLibsWithTags: ['impl'],
|
|
},
|
|
{ sourceTag: 'impl', onlyDependOnLibsWithTags: ['api'] },
|
|
],
|
|
},
|
|
// ['impl', 'domain1']
|
|
`${process.cwd()}/proj/libs/impl/src/index.ts`,
|
|
// ['impl', 'domain1', 'domain2']
|
|
`
|
|
import '@mycompany/api';
|
|
import('@mycompany/api');
|
|
`,
|
|
graph
|
|
);
|
|
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(
|
|
'A project tagged with "impl" and "domain1" can only depend on libs tagged with "impl"'
|
|
);
|
|
expect(failures[1].message).toEqual(
|
|
'A project tagged with "impl" and "domain1" can only depend on libs tagged with "impl"'
|
|
);
|
|
});
|
|
|
|
it('should properly map combo source tags', () => {
|
|
const failures = runRule(
|
|
{
|
|
depConstraints: [
|
|
{
|
|
allSourceTags: ['impl', 'domain1'],
|
|
onlyDependOnLibsWithTags: ['api'],
|
|
},
|
|
],
|
|
},
|
|
// ['impl', 'domain1']
|
|
`${process.cwd()}/proj/libs/impl/src/index.ts`,
|
|
// ['impl', 'domain1', 'domain2']
|
|
`
|
|
import '@mycompany/api';
|
|
import('@mycompany/api');
|
|
`,
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
otherName: {
|
|
name: 'otherName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/other',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile('libs/other/src/index.ts')],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {},
|
|
}
|
|
);
|
|
|
|
const message =
|
|
'Projects 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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
otherName: {
|
|
name: 'otherName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/other',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile('libs/other/src/index.ts')],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {},
|
|
}
|
|
);
|
|
|
|
const message =
|
|
'Projects 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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/mylib/src/main.ts`),
|
|
createFile(`libs/mylib/src/other.ts`),
|
|
],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {},
|
|
}
|
|
);
|
|
|
|
const message =
|
|
'Projects 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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
utils: {
|
|
name: 'utils',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/utils',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
otherName: {
|
|
name: 'otherName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/other',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
myappName: {
|
|
name: 'myappName',
|
|
type: 'app',
|
|
data: {
|
|
root: 'apps/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
myappE2eName: {
|
|
name: 'myappE2eName',
|
|
type: 'e2e',
|
|
data: {
|
|
root: 'apps/myapp-e2e',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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 absolute path within project detected', () => {
|
|
const failures = runRule(
|
|
{},
|
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
|
`
|
|
import '@mycompany/mylib';
|
|
import('@mycompany/mylib');
|
|
`,
|
|
{
|
|
nodes: {
|
|
mylibName: {
|
|
name: 'mylibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
anotherlibName: {
|
|
name: 'anotherlibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherlib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/anotherlib/src/main.ts`)],
|
|
},
|
|
},
|
|
myappName: {
|
|
name: 'myappName',
|
|
type: 'app',
|
|
data: {
|
|
root: 'apps/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`apps/myapp/src/index.ts`)],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {
|
|
mylibName: [
|
|
{
|
|
source: 'mylibName',
|
|
target: 'anotherlibName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
);
|
|
|
|
const message =
|
|
'Projects should use relative imports to import from other files within the same project. Use "./path/to/file" instead of import from "@mycompany/mylib"';
|
|
expect(failures.length).toEqual(2);
|
|
expect(failures[0].message).toEqual(message);
|
|
expect(failures[1].message).toEqual(message);
|
|
});
|
|
|
|
it('should ignore detected absolute path within project if allowCircularSelfDependency flag is set', () => {
|
|
const failures = runRule(
|
|
{
|
|
allowCircularSelfDependency: true,
|
|
},
|
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
|
`
|
|
import '@mycompany/mylib';
|
|
import('@mycompany/mylib');
|
|
`,
|
|
{
|
|
nodes: {
|
|
mylibName: {
|
|
name: 'mylibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/mylib/src/main.ts`)],
|
|
},
|
|
},
|
|
anotherlibName: {
|
|
name: 'anotherlibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherlib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`libs/anotherlib/src/main.ts`)],
|
|
},
|
|
},
|
|
myappName: {
|
|
name: 'myappName',
|
|
type: 'app',
|
|
data: {
|
|
root: 'apps/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [createFile(`apps/myapp/src/index.ts`)],
|
|
},
|
|
},
|
|
},
|
|
dependencies: {
|
|
mylibName: [
|
|
{
|
|
source: 'mylibName',
|
|
target: 'anotherlibName',
|
|
type: DependencyType.static,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
);
|
|
|
|
expect(failures.length).toBe(0);
|
|
});
|
|
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/mylib/src/main.ts`, [
|
|
{
|
|
source: 'mylibName',
|
|
type: 'static',
|
|
target: 'anotherlibName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
anotherlibName: {
|
|
name: 'anotherlibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherlib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/anotherlib/src/main.ts`, [
|
|
{
|
|
source: 'anotherlibName',
|
|
type: 'static',
|
|
target: 'mylibName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
myappName: {
|
|
name: 'myappName',
|
|
type: 'app',
|
|
data: {
|
|
root: 'apps/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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
|
|
|
|
Circular file chain:
|
|
- libs/anotherlib/src/main.ts
|
|
- libs/mylib/src/main.ts`;
|
|
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: 'lib',
|
|
data: {
|
|
root: 'libs/mylib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/mylib/src/main.ts`, [
|
|
{
|
|
source: 'badcirclelibName',
|
|
type: 'static',
|
|
target: 'mylibName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
anotherlibName: {
|
|
name: 'anotherlibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherlib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/anotherlib/src/main.ts`, [
|
|
{
|
|
source: 'anotherlibName',
|
|
type: 'static',
|
|
target: 'mylibName',
|
|
},
|
|
]),
|
|
createFile(`libs/anotherlib/src/index.ts`, [
|
|
{
|
|
source: 'anotherlibName',
|
|
type: 'static',
|
|
target: 'mylibName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
badcirclelibName: {
|
|
name: 'badcirclelibName',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/badcirclelib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
files: [
|
|
createFile(`libs/badcirclelib/src/main.ts`, [
|
|
{
|
|
source: 'badcirclelibName',
|
|
type: 'static',
|
|
target: 'anotherlibName',
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
},
|
|
myappName: {
|
|
name: 'myappName',
|
|
type: 'app',
|
|
data: {
|
|
root: 'apps/myapp',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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
|
|
|
|
Circular file chain:
|
|
- libs/mylib/src/main.ts
|
|
- libs/badcirclelib/src/main.ts
|
|
- [
|
|
libs/anotherlib/src/main.ts,
|
|
libs/anotherlib/src/index.ts
|
|
]`;
|
|
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: '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: 'lib',
|
|
data: {
|
|
root: 'libs/nonBuildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {},
|
|
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: '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: '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: '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`)],
|
|
},
|
|
},
|
|
anotherBuildableLib: {
|
|
name: 'anotherBuildableLib',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherBuildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {
|
|
build: {
|
|
// defines a buildable lib
|
|
executor: '@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: 'lib',
|
|
data: {
|
|
root: 'libs/buildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
files: [createFile(`libs/buildableLib/src/main.ts`)],
|
|
},
|
|
},
|
|
nonBuildableLib: {
|
|
name: 'nonBuildableLib',
|
|
type: '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: '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: '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: '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`)],
|
|
},
|
|
},
|
|
anotherBuildableLib: {
|
|
name: 'anotherBuildableLib',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherBuildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {
|
|
build: {
|
|
// defines a buildable lib
|
|
executor: '@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: '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: '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: '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`)],
|
|
},
|
|
},
|
|
anotherBuildableLib: {
|
|
name: 'anotherBuildableLib',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherBuildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {
|
|
build: {
|
|
// defines a buildable lib
|
|
executor: '@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: '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`)],
|
|
},
|
|
},
|
|
anotherBuildableLib: {
|
|
name: 'anotherBuildableLib',
|
|
type: 'lib',
|
|
data: {
|
|
root: 'libs/anotherBuildableLib',
|
|
tags: [],
|
|
implicitDependencies: [],
|
|
targets: {
|
|
build: {
|
|
// defines a buildable lib
|
|
executor: '@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: string,
|
|
dependencies?: ProjectGraphDependency[]
|
|
): FileData {
|
|
return { file: f, hash: '', ...(dependencies && { dependencies }) };
|
|
}
|
|
|
|
function runRule(
|
|
ruleArguments: any,
|
|
contentPath: string,
|
|
content: string,
|
|
projectGraph: ProjectGraph
|
|
): TSESLint.Linter.LintMessage[] {
|
|
(global as any).projectPath = `${process.cwd()}/proj`;
|
|
(global as any).projectGraph = projectGraph;
|
|
(global as any).projectRootMappings = createProjectRootMappings(
|
|
projectGraph.nodes
|
|
);
|
|
(global as any).targetProjectLocator = new TargetProjectLocator(
|
|
projectGraph.nodes,
|
|
projectGraph.externalNodes
|
|
);
|
|
|
|
const config = {
|
|
...baseConfig,
|
|
rules: {
|
|
[enforceModuleBoundariesRuleName]: ['error', ruleArguments],
|
|
},
|
|
};
|
|
|
|
return linter.verify(content, config as any, contentPath);
|
|
}
|