feat(eslint-plugin-nx): new package, ESLint enforce-module-boundaries rule
This commit is contained in:
parent
821891457f
commit
49dcacfd1a
@ -68,6 +68,7 @@
|
|||||||
"@types/webpack": "^4.4.24",
|
"@types/webpack": "^4.4.24",
|
||||||
"@types/yargs": "^11.0.0",
|
"@types/yargs": "^11.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "2.0.0-alpha.4",
|
"@typescript-eslint/eslint-plugin": "2.0.0-alpha.4",
|
||||||
|
"@typescript-eslint/experimental-utils": "2.0.0-alpha.4",
|
||||||
"@typescript-eslint/parser": "2.0.0-alpha.4",
|
"@typescript-eslint/parser": "2.0.0-alpha.4",
|
||||||
"angular": "1.6.6",
|
"angular": "1.6.6",
|
||||||
"app-root-path": "^2.0.1",
|
"app-root-path": "^2.0.1",
|
||||||
|
|||||||
37
packages/eslint-plugin-nx/package.json
Normal file
37
packages/eslint-plugin-nx/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@nrwl/eslint-plugin-nx",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "ESLint Plugin for Nx",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/nrwl/nx.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Monorepo",
|
||||||
|
"Web",
|
||||||
|
"Lint",
|
||||||
|
"ESLint",
|
||||||
|
"CLI"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"main": "src/index.js",
|
||||||
|
"types": "src/index.d.ts",
|
||||||
|
"author": "Victor Savkin",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nrwl/nx/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://nx.dev",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nrwl/workspace": "*",
|
||||||
|
"@typescript-eslint/parser": "^2.0.0-alpha.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/experimental-utils": "2.0.0-alpha.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/eslint-plugin-nx/src/index.ts
Normal file
9
packages/eslint-plugin-nx/src/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import enforceModuleBoundaries, {
|
||||||
|
RULE_NAME as enforceModuleBoundariesRuleName
|
||||||
|
} from './rules/enforce-module-boundaries';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries
|
||||||
|
}
|
||||||
|
};
|
||||||
250
packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts
Normal file
250
packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import { ProjectType } from '@nrwl/workspace/src/command-line/affected-apps';
|
||||||
|
import { readDependencies } from '@nrwl/workspace/src/command-line/deps-calculator';
|
||||||
|
import {
|
||||||
|
getProjectNodes,
|
||||||
|
normalizedProjectRoot,
|
||||||
|
readNxJson,
|
||||||
|
readWorkspaceJson
|
||||||
|
} from '@nrwl/workspace/src/command-line/shared';
|
||||||
|
import { appRootPath } from '@nrwl/workspace/src/utils/app-root';
|
||||||
|
import {
|
||||||
|
DepConstraint,
|
||||||
|
findConstraintsFor,
|
||||||
|
findProjectUsingImport,
|
||||||
|
findSourceProject,
|
||||||
|
getSourceFilePath,
|
||||||
|
hasNoneOfTheseTags,
|
||||||
|
isAbsoluteImportIntoAnotherProject,
|
||||||
|
isCircular,
|
||||||
|
isRelativeImportIntoAnotherProject,
|
||||||
|
matchImportWithWildcard,
|
||||||
|
onlyLoadChildren
|
||||||
|
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
|
||||||
|
import { TSESTree } from '@typescript-eslint/experimental-utils';
|
||||||
|
import { createESLintRule } from '../utils/create-eslint-rule';
|
||||||
|
|
||||||
|
type Options = [
|
||||||
|
{
|
||||||
|
allow: string[];
|
||||||
|
depConstraints: DepConstraint[];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
export type MessageIds =
|
||||||
|
| 'noRelativeOrAbsoluteImportsAcrossLibraries'
|
||||||
|
| 'noCircularDependencies'
|
||||||
|
| 'noImportsOfApps'
|
||||||
|
| 'noDeepImportsIntoLibraries'
|
||||||
|
| 'noImportsOfLazyLoadedLibraries'
|
||||||
|
| 'projectWithoutTagsCannotHaveDependencies'
|
||||||
|
| 'tagConstraintViolation';
|
||||||
|
export const RULE_NAME = 'enforce-module-boundaries';
|
||||||
|
|
||||||
|
export default createESLintRule<Options, MessageIds>({
|
||||||
|
name: RULE_NAME,
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description: `Ensure that module boundaries are respected within the monorepo`,
|
||||||
|
category: 'Best Practices',
|
||||||
|
recommended: 'error'
|
||||||
|
},
|
||||||
|
fixable: 'code',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
allow: [{ type: 'string' }],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sourceTag: { type: 'string' },
|
||||||
|
onlyDependOnLibsWithTags: [{ type: 'string' }]
|
||||||
|
},
|
||||||
|
additionalProperties: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
additionalProperties: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
messages: {
|
||||||
|
noRelativeOrAbsoluteImportsAcrossLibraries: `Library imports must start with @{{npmScope}}/`,
|
||||||
|
noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected`,
|
||||||
|
noImportsOfApps: 'Imports of apps are forbidden',
|
||||||
|
noDeepImportsIntoLibraries: 'Deep imports into libraries are forbidden',
|
||||||
|
noImportsOfLazyLoadedLibraries: `Imports of lazy-loaded libraries are forbidden`,
|
||||||
|
projectWithoutTagsCannotHaveDependencies: `A project without tags cannot depend on any libraries`,
|
||||||
|
tagConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{allowedTags}}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultOptions: [
|
||||||
|
{
|
||||||
|
allow: [],
|
||||||
|
depConstraints: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
create(context, [{ allow, depConstraints }]) {
|
||||||
|
/**
|
||||||
|
* Globally cached info about workspace
|
||||||
|
*/
|
||||||
|
const projectPath = (global as any).projectPath || appRootPath;
|
||||||
|
if (!(global as any).projectNodes) {
|
||||||
|
const workspaceJson = readWorkspaceJson();
|
||||||
|
const nxJson = readNxJson();
|
||||||
|
(global as any).npmScope = nxJson.npmScope;
|
||||||
|
(global as any).projectNodes = getProjectNodes(workspaceJson, nxJson);
|
||||||
|
(global as any).deps = readDependencies(
|
||||||
|
(global as any).npmScope,
|
||||||
|
(global as any).projectNodes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const npmScope = (global as any).npmScope;
|
||||||
|
const projectNodes = (global as any).projectNodes;
|
||||||
|
const deps = (global as any).deps;
|
||||||
|
|
||||||
|
projectNodes.sort((a, b) => {
|
||||||
|
if (!a.root) return -1;
|
||||||
|
if (!b.root) return -1;
|
||||||
|
return a.root.length > b.root.length ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node: TSESTree.ImportDeclaration) {
|
||||||
|
const imp = (node.source as TSESTree.Literal).value as string;
|
||||||
|
|
||||||
|
const sourceFilePath = getSourceFilePath(
|
||||||
|
context.getFilename(),
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
// whitelisted import
|
||||||
|
if (allow.some(a => matchImportWithWildcard(a, imp))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for relative and absolute imports
|
||||||
|
if (
|
||||||
|
isRelativeImportIntoAnotherProject(
|
||||||
|
imp,
|
||||||
|
projectPath,
|
||||||
|
projectNodes,
|
||||||
|
sourceFilePath
|
||||||
|
) ||
|
||||||
|
isAbsoluteImportIntoAnotherProject(imp)
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noRelativeOrAbsoluteImportsAcrossLibraries',
|
||||||
|
data: {
|
||||||
|
npmScope
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check constraints between libs and apps
|
||||||
|
if (imp.startsWith(`@${npmScope}/`)) {
|
||||||
|
// we should find the name
|
||||||
|
const sourceProject = findSourceProject(projectNodes, sourceFilePath);
|
||||||
|
// findProjectUsingImport to take care of same prefix
|
||||||
|
const targetProject = findProjectUsingImport(
|
||||||
|
projectNodes,
|
||||||
|
npmScope,
|
||||||
|
imp
|
||||||
|
);
|
||||||
|
|
||||||
|
// something went wrong => return.
|
||||||
|
if (!sourceProject || !targetProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for circular dependency
|
||||||
|
if (isCircular(deps, sourceProject, targetProject)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noCircularDependencies',
|
||||||
|
data: {
|
||||||
|
sourceProjectName: sourceProject.name,
|
||||||
|
targetProjectName: targetProject.name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same project => allow
|
||||||
|
if (sourceProject === targetProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot import apps
|
||||||
|
if (targetProject.type !== ProjectType.lib) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noImportsOfApps'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deep imports aren't allowed
|
||||||
|
if (imp !== `@${npmScope}/${normalizedProjectRoot(targetProject)}`) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noDeepImportsIntoLibraries'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we import a library using loadChildren, we should not import it using es6imports
|
||||||
|
if (
|
||||||
|
onlyLoadChildren(deps, sourceProject.name, targetProject.name, [])
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noImportsOfLazyLoadedLibraries'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that dependency constraints are satisfied
|
||||||
|
if (depConstraints.length > 0) {
|
||||||
|
const constraints = findConstraintsFor(
|
||||||
|
depConstraints,
|
||||||
|
sourceProject
|
||||||
|
);
|
||||||
|
// when no constrains found => error. Force the user to provision them.
|
||||||
|
if (constraints.length === 0) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'projectWithoutTagsCannotHaveDependencies'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let constraint of constraints) {
|
||||||
|
if (
|
||||||
|
hasNoneOfTheseTags(
|
||||||
|
targetProject,
|
||||||
|
constraint.onlyDependOnLibsWithTags || []
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const allowedTags = constraint.onlyDependOnLibsWithTags
|
||||||
|
.map(s => `"${s}"`)
|
||||||
|
.join(', ');
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'tagConstraintViolation',
|
||||||
|
data: {
|
||||||
|
sourceTag: constraint.sourceTag,
|
||||||
|
allowedTags
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
|
||||||
|
|
||||||
|
export const createESLintRule = ESLintUtils.RuleCreator(() => ``);
|
||||||
@ -0,0 +1,834 @@
|
|||||||
|
import {
|
||||||
|
ProjectNode,
|
||||||
|
ProjectType
|
||||||
|
} from '@nrwl/workspace/src/command-line/affected-apps';
|
||||||
|
import {
|
||||||
|
Dependency,
|
||||||
|
DependencyType
|
||||||
|
} from '@nrwl/workspace/src/command-line/deps-calculator';
|
||||||
|
import { TSESLint } from '@typescript-eslint/experimental-utils';
|
||||||
|
import * as parser from '@typescript-eslint/parser';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import enforceModuleBoundaries, {
|
||||||
|
RULE_NAME as enforceModuleBoundariesRuleName
|
||||||
|
} from '../../src/rules/enforce-module-boundaries';
|
||||||
|
|
||||||
|
describe('Enforce Module Boundaries', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(fs, 'writeFileSync');
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'myappName',
|
||||||
|
root: 'libs/myapp',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`apps/myapp/src/main.ts`, `apps/myapp/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/myapp/src/main.ts': 0,
|
||||||
|
'apps/myapp/blah.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/index.ts`, `libs/mylib/src/deep.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/mylib/src/index.ts': 0,
|
||||||
|
'apps/mylib/src/deep.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'myappName',
|
||||||
|
root: 'libs/myapp',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`apps/myapp/src/main.ts`, `apps/myapp/src/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/myapp/src/main.ts': 0,
|
||||||
|
'apps/myapp/src/blah.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'myapp2Name',
|
||||||
|
root: 'libs/myapp2',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [],
|
||||||
|
fileMTimes: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'myapp2-mylib',
|
||||||
|
root: 'libs/myapp2/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: ['libs/myapp2/mylib/src/index.ts'],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/myapp2/mylib/src/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('depConstraints', () => {
|
||||||
|
const projectNodes: ProjectNode[] = [
|
||||||
|
{
|
||||||
|
name: 'apiName',
|
||||||
|
root: 'libs/api',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: ['api', 'domain1'],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/api/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/api/src/index.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'implName',
|
||||||
|
root: 'libs/impl',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: ['impl', 'domain1'],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/impl/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/impl/src/index.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'impl2Name',
|
||||||
|
root: 'libs/impl2',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: ['impl', 'domain1'],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/impl2/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/impl2/src/index.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'impl-domain2Name',
|
||||||
|
root: 'libs/impl-domain2',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: ['impl', 'domain2'],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/impl-domain2/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/impl-domain2/src/index.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'impl-both-domainsName',
|
||||||
|
root: 'libs/impl-both-domains',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: ['impl', 'domain1', 'domain2'],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/impl-both-domains/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/impl-both-domains/src/index.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'untaggedName',
|
||||||
|
root: 'libs/untagged',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/untagged/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/untagged/src/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const depConstraints = {
|
||||||
|
depConstraints: [
|
||||||
|
{ sourceTag: 'api', onlyDependOnLibsWithTags: ['api'] },
|
||||||
|
{ sourceTag: 'impl', onlyDependOnLibsWithTags: ['api', 'impl'] },
|
||||||
|
{ sourceTag: 'domain1', onlyDependOnLibsWithTags: ['domain1'] },
|
||||||
|
{ sourceTag: 'domain2', onlyDependOnLibsWithTags: ['domain2'] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures[0].message).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/src/index.ts`,
|
||||||
|
`
|
||||||
|
import '@mycompany/untagged';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures[0].message).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/src/index.ts`,
|
||||||
|
`
|
||||||
|
import '@mycompany/api';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'A project without tags cannot depend on any libraries'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check all tags', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
depConstraints,
|
||||||
|
`${process.cwd()}/proj/libs/impl/src/index.ts`,
|
||||||
|
`
|
||||||
|
import '@mycompany/impl-domain2';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'A project tagged with "domain1" can only depend on libs tagged with "domain1"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
`,
|
||||||
|
projectNodes
|
||||||
|
);
|
||||||
|
|
||||||
|
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"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/other.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/other.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
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"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/other/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/other/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
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"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: ['libs/other/src/index.ts'],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/src/main.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Library imports must start with @mycompany/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: ['libs/other/src/index.ts'],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/src/main.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Library imports must start with @mycompany/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.ts"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/src/other.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/src/other/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(failures.length).toEqual(1);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Library imports must start with @mycompany/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error about deep imports into libraries', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{},
|
||||||
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||||
|
`
|
||||||
|
import "@mycompany/other/src/blah"
|
||||||
|
import "@mycompany/other/src/sublib/blah"
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/src/another-file.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/src/another-file.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/other/src/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/src/blah.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherSublibName',
|
||||||
|
root: 'libs/other/sublib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/other/sublib/src/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/sublib/src/blah.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Deep imports into libraries are forbidden'
|
||||||
|
);
|
||||||
|
expect(failures[1].message).toEqual(
|
||||||
|
'Deep imports into libraries are forbidden'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not error about deep imports into library when fixed exception is set', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{ allow: ['@mycompany/other/src/blah'] },
|
||||||
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||||
|
`
|
||||||
|
import "@mycompany/other/src/blah"
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/src/another-file.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/src/another-file.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/other/src/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/src/blah.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not error about deep imports into library when exception is specified with a wildcard', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{ allow: ['@mycompany/other/*'] },
|
||||||
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||||
|
`
|
||||||
|
import "@mycompany/other/src/blah"
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`, `libs/mylib/src/another-file.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1,
|
||||||
|
'libs/mylib/src/another-file.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/other/src/blah.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/src/blah.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error on importing a lazy-loaded library', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{},
|
||||||
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||||
|
'import "@mycompany/other";',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'otherName',
|
||||||
|
root: 'libs/other',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/other/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/other/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
mylibName: [
|
||||||
|
{ projectName: 'otherName', type: DependencyType.loadChildren }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'myappName',
|
||||||
|
root: 'apps/myapp',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`apps/myapp/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/myapp/src/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual('Imports of apps are forbidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when circular dependency detected', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{},
|
||||||
|
`${process.cwd()}/proj/libs/anotherlib/src/main.ts`,
|
||||||
|
'import "@mycompany/mylib"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'anotherlibName',
|
||||||
|
root: 'libs/anotherlib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/anotherlib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/anotherlib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'myappName',
|
||||||
|
root: 'apps/myapp',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`apps/myapp/src/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/myapp/src/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
mylibName: [
|
||||||
|
{ projectName: 'anotherlibName', type: DependencyType.es6Import }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Circular dependency between "anotherlibName" and "mylibName" detected'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when circular dependency detected (indirect)', () => {
|
||||||
|
const failures = runRule(
|
||||||
|
{},
|
||||||
|
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||||
|
'import "@mycompany/badcirclelib"',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'mylibName',
|
||||||
|
root: 'libs/mylib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/mylib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'anotherlibName',
|
||||||
|
root: 'libs/anotherlib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/anotherlib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/mylib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'badcirclelibName',
|
||||||
|
root: 'libs/badcirclelib',
|
||||||
|
type: ProjectType.lib,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`libs/badcirclelib/src/main.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'libs/badcirclelib/src/main.ts': 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'myappName',
|
||||||
|
root: 'apps/myapp',
|
||||||
|
type: ProjectType.app,
|
||||||
|
tags: [],
|
||||||
|
implicitDependencies: [],
|
||||||
|
architect: {},
|
||||||
|
files: [`apps/myapp/index.ts`],
|
||||||
|
fileMTimes: {
|
||||||
|
'apps/myapp/index.ts': 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
mylibName: [
|
||||||
|
{ projectName: 'badcirclelibName', type: DependencyType.es6Import }
|
||||||
|
],
|
||||||
|
badcirclelibName: [
|
||||||
|
{ projectName: 'anotherlibName', type: DependencyType.es6Import }
|
||||||
|
],
|
||||||
|
anotherlibName: [
|
||||||
|
{ projectName: 'mylibName', type: DependencyType.es6Import }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(failures[0].message).toEqual(
|
||||||
|
'Circular dependency between "mylibName" and "badcirclelibName" detected'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 runRule(
|
||||||
|
ruleArguments: any,
|
||||||
|
contentPath: string,
|
||||||
|
content: string,
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
deps: { [projectName: string]: Dependency[] } = {}
|
||||||
|
): TSESLint.Linter.LintMessage[] {
|
||||||
|
(global as any).projectPath = `${process.cwd()}/proj`;
|
||||||
|
(global as any).npmScope = 'mycompany';
|
||||||
|
(global as any).projectNodes = projectNodes;
|
||||||
|
(global as any).deps = deps;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...baseConfig,
|
||||||
|
rules: {
|
||||||
|
[enforceModuleBoundariesRuleName]: ['error', ruleArguments]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return linter.verifyAndFix(content, config as any, contentPath).messages;
|
||||||
|
}
|
||||||
74
packages/eslint-plugin-nx/tests/test-helper.ts
Normal file
74
packages/eslint-plugin-nx/tests/test-helper.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { TSESLint } from '@typescript-eslint/experimental-utils';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const parser = '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
type RuleTesterConfig = Exclude<TSESLint.RuleTesterConfig, 'parser'> & {
|
||||||
|
parser: typeof parser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RuleTester extends TSESLint.RuleTester {
|
||||||
|
private filename: string | undefined = undefined;
|
||||||
|
|
||||||
|
// as of eslint 6 you have to provide an absolute path to the parser
|
||||||
|
// but that's not as clean to type, this saves us trying to manually enforce
|
||||||
|
// that contributors require.resolve everything
|
||||||
|
constructor(options: RuleTesterConfig) {
|
||||||
|
super({
|
||||||
|
...options,
|
||||||
|
parser: require.resolve(options.parser)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.parserOptions && options.parserOptions.project) {
|
||||||
|
this.filename = path.join(getFixturesRootDir(), 'file.ts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// as of eslint 6 you have to provide an absolute path to the parser
|
||||||
|
// If you don't do that at the test level, the test will fail somewhat cryptically...
|
||||||
|
// This is a lot more explicit
|
||||||
|
run<TMessageIds extends string, TOptions extends Readonly<unknown[]>>(
|
||||||
|
name: string,
|
||||||
|
rule: TSESLint.RuleModule<TMessageIds, TOptions>,
|
||||||
|
tests: TSESLint.RunTests<TMessageIds, TOptions>
|
||||||
|
): void {
|
||||||
|
const errorMessage = `Do not set the parser at the test level unless you want to use a parser other than ${parser}`;
|
||||||
|
|
||||||
|
if (this.filename) {
|
||||||
|
tests.valid = tests.valid.map(test => {
|
||||||
|
if (typeof test === 'string') {
|
||||||
|
return {
|
||||||
|
code: test,
|
||||||
|
filename: this.filename
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return test;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.valid.forEach(test => {
|
||||||
|
if (typeof test !== 'string') {
|
||||||
|
if (test.parser === parser) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
if (!test.filename) {
|
||||||
|
test.filename = this.filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tests.invalid.forEach(test => {
|
||||||
|
if (test.parser === parser) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
if (!test.filename) {
|
||||||
|
test.filename = this.filename;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
super.run(name, rule, tests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFixturesRootDir() {
|
||||||
|
return path.join(process.cwd(), 'tests/fixtures/');
|
||||||
|
}
|
||||||
@ -174,7 +174,6 @@ function addTasks(options: Schema) {
|
|||||||
new NodePackageInstallTask(options.directory)
|
new NodePackageInstallTask(options.directory)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.preset !== 'empty') {
|
|
||||||
const createPresetTask = context.addTask(new RunPresetTask(), [
|
const createPresetTask = context.addTask(new RunPresetTask(), [
|
||||||
packageTask
|
packageTask
|
||||||
]);
|
]);
|
||||||
@ -183,7 +182,6 @@ function addTasks(options: Schema) {
|
|||||||
new NodePackageInstallTask(options.directory),
|
new NodePackageInstallTask(options.directory),
|
||||||
[createPresetTask]
|
[createPresetTask]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if (!options.skipGit) {
|
if (!options.skipGit) {
|
||||||
const commit =
|
const commit =
|
||||||
typeof options.commit == 'object'
|
typeof options.commit == 'object'
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"dotenv": "6.2.0",
|
"dotenv": "6.2.0",
|
||||||
"ts-node": "~7.0.0",
|
"ts-node": "~7.0.0",
|
||||||
"tslint": "~5.11.0",
|
"tslint": "~5.11.0",
|
||||||
|
"eslint": "<%= eslintVersion %>",
|
||||||
"typescript": "<%= typescriptVersion %>",
|
"typescript": "<%= typescriptVersion %>",
|
||||||
"prettier": "<%= prettierVersion %>"
|
"prettier": "<%= prettierVersion %>"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
angularCliVersion,
|
angularCliVersion,
|
||||||
prettierVersion,
|
prettierVersion,
|
||||||
typescriptVersion,
|
typescriptVersion,
|
||||||
|
eslintVersion,
|
||||||
nxVersion
|
nxVersion
|
||||||
} from '../../utils/versions';
|
} from '../../utils/versions';
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ export default function(options: Schema): Rule {
|
|||||||
nxCli: false,
|
nxCli: false,
|
||||||
typescriptVersion,
|
typescriptVersion,
|
||||||
prettierVersion,
|
prettierVersion,
|
||||||
|
eslintVersion,
|
||||||
// angular cli is used only when workspace schematics is added to angular cli
|
// angular cli is used only when workspace schematics is added to angular cli
|
||||||
angularCliVersion,
|
angularCliVersion,
|
||||||
...(options as object),
|
...(options as object),
|
||||||
|
|||||||
@ -1,20 +1,29 @@
|
|||||||
import * as path from 'path';
|
|
||||||
import * as Lint from 'tslint';
|
import * as Lint from 'tslint';
|
||||||
import { IOptions } from 'tslint';
|
import { IOptions } from 'tslint';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
import { ProjectNode, ProjectType } from '../command-line/affected-apps';
|
||||||
|
import { readDependencies } from '../command-line/deps-calculator';
|
||||||
import {
|
import {
|
||||||
getProjectNodes,
|
getProjectNodes,
|
||||||
normalizedProjectRoot,
|
normalizedProjectRoot,
|
||||||
readWorkspaceJson,
|
readNxJson,
|
||||||
readNxJson
|
readWorkspaceJson
|
||||||
} from '../command-line/shared';
|
} from '../command-line/shared';
|
||||||
import { ProjectNode, ProjectType } from '../command-line/affected-apps';
|
|
||||||
import {
|
|
||||||
Dependency,
|
|
||||||
DependencyType,
|
|
||||||
readDependencies
|
|
||||||
} from '../command-line/deps-calculator';
|
|
||||||
import { appRootPath } from '../utils/app-root';
|
import { appRootPath } from '../utils/app-root';
|
||||||
|
import {
|
||||||
|
DepConstraint,
|
||||||
|
Deps,
|
||||||
|
findConstraintsFor,
|
||||||
|
findProjectUsingImport,
|
||||||
|
findSourceProject,
|
||||||
|
getSourceFilePath,
|
||||||
|
hasNoneOfTheseTags,
|
||||||
|
isAbsoluteImportIntoAnotherProject,
|
||||||
|
isCircular,
|
||||||
|
isRelativeImportIntoAnotherProject,
|
||||||
|
matchImportWithWildcard,
|
||||||
|
onlyLoadChildren
|
||||||
|
} from '../utils/runtime-lint-utils';
|
||||||
|
|
||||||
export class Rule extends Lint.Rules.AbstractRule {
|
export class Rule extends Lint.Rules.AbstractRule {
|
||||||
constructor(
|
constructor(
|
||||||
@ -22,7 +31,7 @@ export class Rule extends Lint.Rules.AbstractRule {
|
|||||||
private readonly projectPath?: string,
|
private readonly projectPath?: string,
|
||||||
private readonly npmScope?: string,
|
private readonly npmScope?: string,
|
||||||
private readonly projectNodes?: ProjectNode[],
|
private readonly projectNodes?: ProjectNode[],
|
||||||
private readonly deps?: { [projectName: string]: Dependency[] }
|
private readonly deps?: Deps
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
@ -57,11 +66,6 @@ export class Rule extends Lint.Rules.AbstractRule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type DepConstraint = {
|
|
||||||
sourceTag: string;
|
|
||||||
onlyDependOnLibsWithTags: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||||
private readonly allow: string[];
|
private readonly allow: string[];
|
||||||
private readonly depConstraints: DepConstraint[];
|
private readonly depConstraints: DepConstraint[];
|
||||||
@ -72,7 +76,7 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
private readonly projectPath: string,
|
private readonly projectPath: string,
|
||||||
private readonly npmScope: string,
|
private readonly npmScope: string,
|
||||||
private readonly projectNodes: ProjectNode[],
|
private readonly projectNodes: ProjectNode[],
|
||||||
private readonly deps: { [projectName: string]: Dependency[] }
|
private readonly deps: Deps
|
||||||
) {
|
) {
|
||||||
super(sourceFile, options);
|
super(sourceFile, options);
|
||||||
|
|
||||||
@ -104,8 +108,13 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
|
|
||||||
// check for relative and absolute imports
|
// check for relative and absolute imports
|
||||||
if (
|
if (
|
||||||
this.isRelativeImportIntoAnotherProject(imp) ||
|
isRelativeImportIntoAnotherProject(
|
||||||
this.isAbsoluteImportIntoAnotherProject(imp)
|
imp,
|
||||||
|
this.projectPath,
|
||||||
|
this.projectNodes,
|
||||||
|
getSourceFilePath(this.getSourceFile().fileName, this.projectPath)
|
||||||
|
) ||
|
||||||
|
isAbsoluteImportIntoAnotherProject(imp)
|
||||||
) {
|
) {
|
||||||
this.addFailureAt(
|
this.addFailureAt(
|
||||||
node.getStart(),
|
node.getStart(),
|
||||||
@ -118,8 +127,16 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
// check constraints between libs and apps
|
// check constraints between libs and apps
|
||||||
if (imp.startsWith(`@${this.npmScope}/`)) {
|
if (imp.startsWith(`@${this.npmScope}/`)) {
|
||||||
// we should find the name
|
// we should find the name
|
||||||
const sourceProject = this.findSourceProject();
|
const sourceProject = findSourceProject(
|
||||||
const targetProject = this.findProjectUsingImport(imp); // findProjectUsingImport to take care of same prefix
|
this.projectNodes,
|
||||||
|
getSourceFilePath(this.getSourceFile().fileName, this.projectPath)
|
||||||
|
);
|
||||||
|
// findProjectUsingImport to take care of same prefix
|
||||||
|
const targetProject = findProjectUsingImport(
|
||||||
|
this.projectNodes,
|
||||||
|
this.npmScope,
|
||||||
|
imp
|
||||||
|
);
|
||||||
|
|
||||||
// something went wrong => return.
|
// something went wrong => return.
|
||||||
if (!sourceProject || !targetProject) {
|
if (!sourceProject || !targetProject) {
|
||||||
@ -128,7 +145,7 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for circular dependency
|
// check for circular dependency
|
||||||
if (this.isCircular(sourceProject, targetProject)) {
|
if (isCircular(this.deps, sourceProject, targetProject)) {
|
||||||
const error = `Circular dependency between "${
|
const error = `Circular dependency between "${
|
||||||
sourceProject.name
|
sourceProject.name
|
||||||
}" and "${targetProject.name}" detected`;
|
}" and "${targetProject.name}" detected`;
|
||||||
@ -163,7 +180,9 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if we import a library using loadChildre, we should not import it using es6imports
|
// if we import a library using loadChildre, we should not import it using es6imports
|
||||||
if (this.onlyLoadChildren(sourceProject.name, targetProject.name, [])) {
|
if (
|
||||||
|
onlyLoadChildren(this.deps, sourceProject.name, targetProject.name, [])
|
||||||
|
) {
|
||||||
this.addFailureAt(
|
this.addFailureAt(
|
||||||
node.getStart(),
|
node.getStart(),
|
||||||
node.getWidth(),
|
node.getWidth(),
|
||||||
@ -174,7 +193,10 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
|
|
||||||
// check that dependency constraints are satisfied
|
// check that dependency constraints are satisfied
|
||||||
if (this.depConstraints.length > 0) {
|
if (this.depConstraints.length > 0) {
|
||||||
const constraints = this.findConstraintsFor(sourceProject);
|
const constraints = findConstraintsFor(
|
||||||
|
this.depConstraints,
|
||||||
|
sourceProject
|
||||||
|
);
|
||||||
// when no constrains found => error. Force the user to provision them.
|
// when no constrains found => error. Force the user to provision them.
|
||||||
if (constraints.length === 0) {
|
if (constraints.length === 0) {
|
||||||
this.addFailureAt(
|
this.addFailureAt(
|
||||||
@ -207,152 +229,4 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
|||||||
|
|
||||||
super.visitImportDeclaration(node);
|
super.visitImportDeclaration(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isCircular(
|
|
||||||
sourceProject: ProjectNode,
|
|
||||||
targetProject: ProjectNode
|
|
||||||
): boolean {
|
|
||||||
if (!this.deps[targetProject.name]) return false;
|
|
||||||
return this.isDependingOn(targetProject.name, sourceProject.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isDependingOn(
|
|
||||||
sourceProjectName: string,
|
|
||||||
targetProjectName: string,
|
|
||||||
done: { [projectName: string]: boolean } = {}
|
|
||||||
): boolean {
|
|
||||||
if (done[sourceProjectName]) return false;
|
|
||||||
if (!this.deps[sourceProjectName]) return false;
|
|
||||||
return this.deps[sourceProjectName]
|
|
||||||
.map(dep =>
|
|
||||||
dep.projectName === targetProjectName
|
|
||||||
? true
|
|
||||||
: this.isDependingOn(dep.projectName, targetProjectName, {
|
|
||||||
...done,
|
|
||||||
[`${sourceProjectName}`]: true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.some(result => result);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 === targetProjectName) return true;
|
|
||||||
return this.onlyLoadChildren(d.projectName, targetProjectName, [
|
|
||||||
...visited,
|
|
||||||
sourceProjectName
|
|
||||||
]);
|
|
||||||
}).length > 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isRelativeImportIntoAnotherProject(imp: string): boolean {
|
|
||||||
if (!this.isRelative(imp)) return false;
|
|
||||||
|
|
||||||
const targetFile = normalizePath(
|
|
||||||
path.resolve(
|
|
||||||
path.join(this.projectPath, path.dirname(this.getSourceFilePath())),
|
|
||||||
imp
|
|
||||||
)
|
|
||||||
).substring(this.projectPath.length + 1);
|
|
||||||
|
|
||||||
const sourceProject = this.findSourceProject();
|
|
||||||
const targetProject = this.findTargetProject(targetFile);
|
|
||||||
return sourceProject && targetProject && sourceProject !== targetProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSourceFilePath() {
|
|
||||||
return this.getSourceFile().fileName.substring(this.projectPath.length + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findSourceProject() {
|
|
||||||
const targetFile = removeExt(this.getSourceFilePath());
|
|
||||||
return this.findProjectUsingFile(targetFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private findTargetProject(targetFile: string) {
|
|
||||||
let targetProject = this.findProjectUsingFile(targetFile);
|
|
||||||
if (!targetProject) {
|
|
||||||
targetProject = this.findProjectUsingFile(
|
|
||||||
normalizePath(path.join(targetFile, 'index'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!targetProject) {
|
|
||||||
targetProject = this.findProjectUsingFile(
|
|
||||||
normalizePath(path.join(targetFile, 'src', 'index'))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return targetProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findProjectUsingFile(file: string) {
|
|
||||||
return this.projectNodes.filter(n => containsFile(n.files, file))[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private findProjectUsingImport(imp: string) {
|
|
||||||
const unscopedImport = imp.substring(this.npmScope.length + 2);
|
|
||||||
return this.projectNodes.filter(n => {
|
|
||||||
const normalizedRoot = normalizedProjectRoot(n);
|
|
||||||
return (
|
|
||||||
unscopedImport === normalizedRoot ||
|
|
||||||
unscopedImport.startsWith(`${normalizedRoot}/`)
|
|
||||||
);
|
|
||||||
})[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAbsoluteImportIntoAnotherProject(imp: string) {
|
|
||||||
return (
|
|
||||||
imp.startsWith('libs/') ||
|
|
||||||
imp.startsWith('/libs/') ||
|
|
||||||
imp.startsWith('apps/') ||
|
|
||||||
imp.startsWith('/apps/')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isRelative(s: string) {
|
|
||||||
return s.startsWith('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
private findConstraintsFor(sourceProject: ProjectNode) {
|
|
||||||
return this.depConstraints.filter(f => hasTag(sourceProject, f.sourceTag));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
return file.replace(/\.[^/.]+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePath(osSpecificPath: string): string {
|
|
||||||
return osSpecificPath.split(path.sep).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchImportWithWildcard(
|
|
||||||
// This may or may not contain wildcards ("*")
|
|
||||||
allowableImport: string,
|
|
||||||
extractedImport: string
|
|
||||||
): boolean {
|
|
||||||
const regex = new RegExp('^' + allowableImport.split('*').join('.*') + '$');
|
|
||||||
return regex.test(extractedImport);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import { offsetFromRoot } from './common';
|
|||||||
import {
|
import {
|
||||||
eslintVersion,
|
eslintVersion,
|
||||||
typescriptESLintVersion,
|
typescriptESLintVersion,
|
||||||
eslintConfigPrettierVersion
|
eslintConfigPrettierVersion,
|
||||||
|
nxVersion
|
||||||
} from './versions';
|
} from './versions';
|
||||||
|
|
||||||
export const enum Linter {
|
export const enum Linter {
|
||||||
@ -75,6 +76,7 @@ export function addLintFiles(
|
|||||||
addDepsToPackageJson(
|
addDepsToPackageJson(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
|
'@nrwl/eslint-plugin-nx': nxVersion,
|
||||||
'@typescript-eslint/parser': typescriptESLintVersion,
|
'@typescript-eslint/parser': typescriptESLintVersion,
|
||||||
'@typescript-eslint/eslint-plugin': typescriptESLintVersion,
|
'@typescript-eslint/eslint-plugin': typescriptESLintVersion,
|
||||||
eslint: eslintVersion,
|
eslint: eslintVersion,
|
||||||
@ -179,7 +181,7 @@ const globalESLint = `
|
|||||||
"sourceType": "module",
|
"sourceType": "module",
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint", "@nrwl/nx"],
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
@ -190,7 +192,16 @@ const globalESLint = `
|
|||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/no-parameter-properties": "off"
|
"@typescript-eslint/no-parameter-properties": "off",
|
||||||
|
"@nrwl/nx/enforce-module-boundaries": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allow": [],
|
||||||
|
"depConstraints": [
|
||||||
|
{ "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@ -227,7 +238,7 @@ const globalESLint = `
|
|||||||
// "version": "detect"
|
// "version": "detect"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "plugins": ["@typescript-eslint", "import", "jsx-a11y", "react", "react-hooks"],
|
// "plugins": ["@typescript-eslint", "@nrwl/nx", "import", "jsx-a11y", "react", "react-hooks"],
|
||||||
// "extends": [
|
// "extends": [
|
||||||
// "eslint:recommended",
|
// "eslint:recommended",
|
||||||
// "plugin:@typescript-eslint/eslint-recommended",
|
// "plugin:@typescript-eslint/eslint-recommended",
|
||||||
@ -241,6 +252,15 @@ const globalESLint = `
|
|||||||
// * https://github.com/facebook/create-react-app
|
// * https://github.com/facebook/create-react-app
|
||||||
// */
|
// */
|
||||||
// "rules": {
|
// "rules": {
|
||||||
|
// "@nrwl/nx/enforce-module-boundaries": [
|
||||||
|
// "error",
|
||||||
|
// {
|
||||||
|
// "allow": [],
|
||||||
|
// "depConstraints": [
|
||||||
|
// { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
// /**
|
// /**
|
||||||
// * Standard ESLint rule configurations
|
// * Standard ESLint rule configurations
|
||||||
// * https://eslint.org/docs/rules
|
// * https://eslint.org/docs/rules
|
||||||
|
|||||||
181
packages/workspace/src/utils/runtime-lint-utils.ts
Normal file
181
packages/workspace/src/utils/runtime-lint-utils.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import { ProjectNode } from '../command-line/affected-apps';
|
||||||
|
import { Dependency, DependencyType } from '../command-line/deps-calculator';
|
||||||
|
import { normalizedProjectRoot } from '../command-line/shared';
|
||||||
|
|
||||||
|
export type Deps = { [projectName: string]: Dependency[] };
|
||||||
|
export type DepConstraint = {
|
||||||
|
sourceTag: string;
|
||||||
|
onlyDependOnLibsWithTags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 {
|
||||||
|
return file.replace(/\.[^/.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(osSpecificPath: string): string {
|
||||||
|
return osSpecificPath.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchImportWithWildcard(
|
||||||
|
// This may or may not contain wildcards ("*")
|
||||||
|
allowableImport: string,
|
||||||
|
extractedImport: string
|
||||||
|
): boolean {
|
||||||
|
const regex = new RegExp('^' + allowableImport.split('*').join('.*') + '$');
|
||||||
|
return regex.test(extractedImport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRelative(s: string) {
|
||||||
|
return s.startsWith('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRelativeImportIntoAnotherProject(
|
||||||
|
imp: string,
|
||||||
|
projectPath: string,
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
sourceFilePath: string
|
||||||
|
): boolean {
|
||||||
|
if (!isRelative(imp)) return false;
|
||||||
|
|
||||||
|
const targetFile = normalizePath(
|
||||||
|
path.resolve(path.join(projectPath, path.dirname(sourceFilePath)), imp)
|
||||||
|
).substring(projectPath.length + 1);
|
||||||
|
|
||||||
|
const sourceProject = findSourceProject(projectNodes, sourceFilePath);
|
||||||
|
const targetProject = findTargetProject(projectNodes, targetFile);
|
||||||
|
return sourceProject && targetProject && sourceProject !== targetProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findProjectUsingFile(
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
file: string
|
||||||
|
) {
|
||||||
|
return projectNodes.filter(n => containsFile(n.files, file))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findSourceProject(
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
sourceFilePath: string
|
||||||
|
) {
|
||||||
|
const targetFile = removeExt(sourceFilePath);
|
||||||
|
return findProjectUsingFile(projectNodes, targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findTargetProject(
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
targetFile: string
|
||||||
|
) {
|
||||||
|
let targetProject = findProjectUsingFile(projectNodes, targetFile);
|
||||||
|
if (!targetProject) {
|
||||||
|
targetProject = findProjectUsingFile(
|
||||||
|
projectNodes,
|
||||||
|
normalizePath(path.join(targetFile, 'index'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!targetProject) {
|
||||||
|
targetProject = findProjectUsingFile(
|
||||||
|
projectNodes,
|
||||||
|
normalizePath(path.join(targetFile, 'src', 'index'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return targetProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAbsoluteImportIntoAnotherProject(imp: string) {
|
||||||
|
return (
|
||||||
|
imp.startsWith('libs/') ||
|
||||||
|
imp.startsWith('/libs/') ||
|
||||||
|
imp.startsWith('apps/') ||
|
||||||
|
imp.startsWith('/apps/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findProjectUsingImport(
|
||||||
|
projectNodes: ProjectNode[],
|
||||||
|
npmScope: string,
|
||||||
|
imp: string
|
||||||
|
) {
|
||||||
|
const unscopedImport = imp.substring(npmScope.length + 2);
|
||||||
|
return projectNodes.filter(n => {
|
||||||
|
const normalizedRoot = normalizedProjectRoot(n);
|
||||||
|
return (
|
||||||
|
unscopedImport === normalizedRoot ||
|
||||||
|
unscopedImport.startsWith(`${normalizedRoot}/`)
|
||||||
|
);
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCircular(
|
||||||
|
deps: Deps,
|
||||||
|
sourceProject: ProjectNode,
|
||||||
|
targetProject: ProjectNode
|
||||||
|
): boolean {
|
||||||
|
if (!deps[targetProject.name]) return false;
|
||||||
|
return isDependingOn(deps, targetProject.name, sourceProject.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDependingOn(
|
||||||
|
deps: Deps,
|
||||||
|
sourceProjectName: string,
|
||||||
|
targetProjectName: string,
|
||||||
|
done: { [projectName: string]: boolean } = {}
|
||||||
|
): boolean {
|
||||||
|
if (done[sourceProjectName]) return false;
|
||||||
|
if (!deps[sourceProjectName]) return false;
|
||||||
|
return deps[sourceProjectName]
|
||||||
|
.map(dep =>
|
||||||
|
dep.projectName === targetProjectName
|
||||||
|
? true
|
||||||
|
: isDependingOn(deps, dep.projectName, targetProjectName, {
|
||||||
|
...done,
|
||||||
|
[`${sourceProjectName}`]: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.some(result => result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findConstraintsFor(
|
||||||
|
depConstraints: DepConstraint[],
|
||||||
|
sourceProject: ProjectNode
|
||||||
|
) {
|
||||||
|
return depConstraints.filter(f => hasTag(sourceProject, f.sourceTag));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onlyLoadChildren(
|
||||||
|
deps: Deps,
|
||||||
|
sourceProjectName: string,
|
||||||
|
targetProjectName: string,
|
||||||
|
visited: string[]
|
||||||
|
) {
|
||||||
|
if (visited.indexOf(sourceProjectName) > -1) return false;
|
||||||
|
return (
|
||||||
|
(deps[sourceProjectName] || []).filter(d => {
|
||||||
|
if (d.type !== DependencyType.loadChildren) return false;
|
||||||
|
if (d.projectName === targetProjectName) return true;
|
||||||
|
return onlyLoadChildren(deps, d.projectName, targetProjectName, [
|
||||||
|
...visited,
|
||||||
|
sourceProjectName
|
||||||
|
]);
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSourceFilePath(sourceFileName: string, projectPath: string) {
|
||||||
|
return sourceFileName.substring(projectPath.length + 1);
|
||||||
|
}
|
||||||
@ -50,6 +50,7 @@ cp README.md build/packages/jest
|
|||||||
cp README.md build/packages/cypress
|
cp README.md build/packages/cypress
|
||||||
cp README.md build/packages/cli
|
cp README.md build/packages/cli
|
||||||
cp README.md build/packages/tao
|
cp README.md build/packages/tao
|
||||||
|
cp README.md build/packages/eslint-plugin-nx
|
||||||
cp README.md build/packages/linter
|
cp README.md build/packages/linter
|
||||||
|
|
||||||
cp LICENSE build/packages/builders
|
cp LICENSE build/packages/builders
|
||||||
@ -67,6 +68,7 @@ cp LICENSE build/packages/jest
|
|||||||
cp LICENSE build/packages/cypress
|
cp LICENSE build/packages/cypress
|
||||||
cp LICENSE build/packages/cli
|
cp LICENSE build/packages/cli
|
||||||
cp LICENSE build/packages/tao
|
cp LICENSE build/packages/tao
|
||||||
|
cp LICENSE build/packages/eslint-plugin-nx
|
||||||
cp LICENSE build/packages/linter
|
cp LICENSE build/packages/linter
|
||||||
|
|
||||||
echo "Nx libraries available at build/packages:"
|
echo "Nx libraries available at build/packages:"
|
||||||
|
|||||||
@ -163,6 +163,7 @@ const options = {
|
|||||||
'build/npm/workspace/package.json',
|
'build/npm/workspace/package.json',
|
||||||
'build/npm/cli/package.json',
|
'build/npm/cli/package.json',
|
||||||
'build/npm/tao/package.json',
|
'build/npm/tao/package.json',
|
||||||
|
'build/npm/eslint-plugin-nx/package.json',
|
||||||
'build/npm/linter/package.json'
|
'build/npm/linter/package.json'
|
||||||
],
|
],
|
||||||
increment: parsedVersion.version,
|
increment: parsedVersion.version,
|
||||||
|
|||||||
@ -17,13 +17,13 @@ cd build/packages
|
|||||||
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
sed -i "" "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,web,jest,node,express,nest,cypress,angular,workspace}/src/utils/versions.js
|
sed -i "" "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,web,jest,node,express,nest,cypress,angular,workspace}/src/utils/versions.js
|
||||||
sed -i "" "s|\*|$NX_VERSION|g" {schematics,react,web,jest,node,express,nest,cypress,angular,workspace,cli,tao,create-nx-workspace}/package.json
|
sed -i "" "s|\*|$NX_VERSION|g" {schematics,react,web,jest,node,express,nest,cypress,angular,workspace,cli,linter,tao,eslint-plugin-nx,create-nx-workspace}/package.json
|
||||||
sed -i "" "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "" "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
sed -i "" "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "" "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
sed -i "" "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "" "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
else
|
else
|
||||||
sed -i "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,web,jest,node,express,nest,cypress,angular,workspace}/src/utils/versions.js
|
sed -i "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,web,jest,node,express,nest,cypress,angular,workspace}/src/utils/versions.js
|
||||||
sed -i "s|\*|$NX_VERSION|g" {schematics,react,web,jest,node,express,nest,cypress,angular,workspace,cli,tao,create-nx-workspace}/package.json
|
sed -i "s|\*|$NX_VERSION|g" {schematics,react,web,jest,node,express,nest,cypress,angular,workspace,cli,linter,tao,eslint-plugin-nx,create-nx-workspace}/package.json
|
||||||
sed -i "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
sed -i "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
sed -i "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
sed -i "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js
|
||||||
@ -31,9 +31,9 @@ fi
|
|||||||
|
|
||||||
if [[ $NX_VERSION == "*" ]]; then
|
if [[ $NX_VERSION == "*" ]]; then
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,node,express,nest,cypress,angular,workspace,cli,tao,create-nx-workspace}/package.json
|
sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,node,express,nest,cypress,angular,workspace,linter,cli,tao,eslint-plugin-nx,create-nx-workspace}/package.json
|
||||||
else
|
else
|
||||||
echo $PWD
|
echo $PWD
|
||||||
sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,node,express,nest,cypress,angular,workspace,cli,tao,create-nx-workspace}/package.json
|
sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,node,express,nest,cypress,angular,workspace,linter,cli,tao,eslint-plugin-nx,create-nx-workspace}/package.json
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -3,5 +3,5 @@
|
|||||||
if [ -n "$1" ]; then
|
if [ -n "$1" ]; then
|
||||||
jest --maxWorkers=1 ./build/packages/$1.spec.js
|
jest --maxWorkers=1 ./build/packages/$1.spec.js
|
||||||
else
|
else
|
||||||
jest --maxWorkers=1 ./build/packages/{schematics,bazel,builders,react,jest,web,node,express,nest,cypress,angular,workspace,tao} --passWithNoTests
|
jest --maxWorkers=1 ./build/packages/{schematics,bazel,builders,react,jest,web,node,express,nest,cypress,angular,workspace,tao,eslint-plugin-nx} --passWithNoTests
|
||||||
fi
|
fi
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user