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/yargs": "^11.0.0",
|
||||
"@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",
|
||||
"angular": "1.6.6",
|
||||
"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,16 +174,14 @@ function addTasks(options: Schema) {
|
||||
new NodePackageInstallTask(options.directory)
|
||||
);
|
||||
}
|
||||
if (options.preset !== 'empty') {
|
||||
const createPresetTask = context.addTask(new RunPresetTask(), [
|
||||
packageTask
|
||||
]);
|
||||
const createPresetTask = context.addTask(new RunPresetTask(), [
|
||||
packageTask
|
||||
]);
|
||||
|
||||
presetInstallTask = context.addTask(
|
||||
new NodePackageInstallTask(options.directory),
|
||||
[createPresetTask]
|
||||
);
|
||||
}
|
||||
presetInstallTask = context.addTask(
|
||||
new NodePackageInstallTask(options.directory),
|
||||
[createPresetTask]
|
||||
);
|
||||
if (!options.skipGit) {
|
||||
const commit =
|
||||
typeof options.commit == 'object'
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"dotenv": "6.2.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"eslint": "<%= eslintVersion %>",
|
||||
"typescript": "<%= typescriptVersion %>",
|
||||
"prettier": "<%= prettierVersion %>"
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
angularCliVersion,
|
||||
prettierVersion,
|
||||
typescriptVersion,
|
||||
eslintVersion,
|
||||
nxVersion
|
||||
} from '../../utils/versions';
|
||||
|
||||
@ -39,6 +40,7 @@ export default function(options: Schema): Rule {
|
||||
nxCli: false,
|
||||
typescriptVersion,
|
||||
prettierVersion,
|
||||
eslintVersion,
|
||||
// angular cli is used only when workspace schematics is added to angular cli
|
||||
angularCliVersion,
|
||||
...(options as object),
|
||||
|
||||
@ -1,20 +1,29 @@
|
||||
import * as path from 'path';
|
||||
import * as Lint from 'tslint';
|
||||
import { IOptions } from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import { ProjectNode, ProjectType } from '../command-line/affected-apps';
|
||||
import { readDependencies } from '../command-line/deps-calculator';
|
||||
import {
|
||||
getProjectNodes,
|
||||
normalizedProjectRoot,
|
||||
readWorkspaceJson,
|
||||
readNxJson
|
||||
readNxJson,
|
||||
readWorkspaceJson
|
||||
} 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 {
|
||||
DepConstraint,
|
||||
Deps,
|
||||
findConstraintsFor,
|
||||
findProjectUsingImport,
|
||||
findSourceProject,
|
||||
getSourceFilePath,
|
||||
hasNoneOfTheseTags,
|
||||
isAbsoluteImportIntoAnotherProject,
|
||||
isCircular,
|
||||
isRelativeImportIntoAnotherProject,
|
||||
matchImportWithWildcard,
|
||||
onlyLoadChildren
|
||||
} from '../utils/runtime-lint-utils';
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
constructor(
|
||||
@ -22,7 +31,7 @@ export class Rule extends Lint.Rules.AbstractRule {
|
||||
private readonly projectPath?: string,
|
||||
private readonly npmScope?: string,
|
||||
private readonly projectNodes?: ProjectNode[],
|
||||
private readonly deps?: { [projectName: string]: Dependency[] }
|
||||
private readonly deps?: Deps
|
||||
) {
|
||||
super(options);
|
||||
if (!projectPath) {
|
||||
@ -57,11 +66,6 @@ export class Rule extends Lint.Rules.AbstractRule {
|
||||
}
|
||||
}
|
||||
|
||||
type DepConstraint = {
|
||||
sourceTag: string;
|
||||
onlyDependOnLibsWithTags: string[];
|
||||
};
|
||||
|
||||
class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
private readonly allow: string[];
|
||||
private readonly depConstraints: DepConstraint[];
|
||||
@ -72,7 +76,7 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
private readonly projectPath: string,
|
||||
private readonly npmScope: string,
|
||||
private readonly projectNodes: ProjectNode[],
|
||||
private readonly deps: { [projectName: string]: Dependency[] }
|
||||
private readonly deps: Deps
|
||||
) {
|
||||
super(sourceFile, options);
|
||||
|
||||
@ -104,8 +108,13 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
|
||||
// check for relative and absolute imports
|
||||
if (
|
||||
this.isRelativeImportIntoAnotherProject(imp) ||
|
||||
this.isAbsoluteImportIntoAnotherProject(imp)
|
||||
isRelativeImportIntoAnotherProject(
|
||||
imp,
|
||||
this.projectPath,
|
||||
this.projectNodes,
|
||||
getSourceFilePath(this.getSourceFile().fileName, this.projectPath)
|
||||
) ||
|
||||
isAbsoluteImportIntoAnotherProject(imp)
|
||||
) {
|
||||
this.addFailureAt(
|
||||
node.getStart(),
|
||||
@ -118,8 +127,16 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
// check constraints between libs and apps
|
||||
if (imp.startsWith(`@${this.npmScope}/`)) {
|
||||
// we should find the name
|
||||
const sourceProject = this.findSourceProject();
|
||||
const targetProject = this.findProjectUsingImport(imp); // findProjectUsingImport to take care of same prefix
|
||||
const sourceProject = findSourceProject(
|
||||
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.
|
||||
if (!sourceProject || !targetProject) {
|
||||
@ -128,7 +145,7 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
}
|
||||
|
||||
// check for circular dependency
|
||||
if (this.isCircular(sourceProject, targetProject)) {
|
||||
if (isCircular(this.deps, sourceProject, targetProject)) {
|
||||
const error = `Circular dependency between "${
|
||||
sourceProject.name
|
||||
}" 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 (this.onlyLoadChildren(sourceProject.name, targetProject.name, [])) {
|
||||
if (
|
||||
onlyLoadChildren(this.deps, sourceProject.name, targetProject.name, [])
|
||||
) {
|
||||
this.addFailureAt(
|
||||
node.getStart(),
|
||||
node.getWidth(),
|
||||
@ -174,7 +193,10 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
|
||||
// check that dependency constraints are satisfied
|
||||
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.
|
||||
if (constraints.length === 0) {
|
||||
this.addFailureAt(
|
||||
@ -207,152 +229,4 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
|
||||
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 {
|
||||
eslintVersion,
|
||||
typescriptESLintVersion,
|
||||
eslintConfigPrettierVersion
|
||||
eslintConfigPrettierVersion,
|
||||
nxVersion
|
||||
} from './versions';
|
||||
|
||||
export const enum Linter {
|
||||
@ -75,6 +76,7 @@ export function addLintFiles(
|
||||
addDepsToPackageJson(
|
||||
{},
|
||||
{
|
||||
'@nrwl/eslint-plugin-nx': nxVersion,
|
||||
'@typescript-eslint/parser': typescriptESLintVersion,
|
||||
'@typescript-eslint/eslint-plugin': typescriptESLintVersion,
|
||||
eslint: eslintVersion,
|
||||
@ -179,7 +181,7 @@ const globalESLint = `
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint", "@nrwl/nx"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
@ -190,7 +192,16 @@ const globalESLint = `
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-member-accessibility": "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": [
|
||||
{
|
||||
@ -227,7 +238,7 @@ const globalESLint = `
|
||||
// "version": "detect"
|
||||
// }
|
||||
// },
|
||||
// "plugins": ["@typescript-eslint", "import", "jsx-a11y", "react", "react-hooks"],
|
||||
// "plugins": ["@typescript-eslint", "@nrwl/nx", "import", "jsx-a11y", "react", "react-hooks"],
|
||||
// "extends": [
|
||||
// "eslint:recommended",
|
||||
// "plugin:@typescript-eslint/eslint-recommended",
|
||||
@ -241,6 +252,15 @@ const globalESLint = `
|
||||
// * https://github.com/facebook/create-react-app
|
||||
// */
|
||||
// "rules": {
|
||||
// "@nrwl/nx/enforce-module-boundaries": [
|
||||
// "error",
|
||||
// {
|
||||
// "allow": [],
|
||||
// "depConstraints": [
|
||||
// { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] }
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
// /**
|
||||
// * Standard ESLint rule configurations
|
||||
// * 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/cli
|
||||
cp README.md build/packages/tao
|
||||
cp README.md build/packages/eslint-plugin-nx
|
||||
cp README.md build/packages/linter
|
||||
|
||||
cp LICENSE build/packages/builders
|
||||
@ -67,6 +68,7 @@ cp LICENSE build/packages/jest
|
||||
cp LICENSE build/packages/cypress
|
||||
cp LICENSE build/packages/cli
|
||||
cp LICENSE build/packages/tao
|
||||
cp LICENSE build/packages/eslint-plugin-nx
|
||||
cp LICENSE build/packages/linter
|
||||
|
||||
echo "Nx libraries available at build/packages:"
|
||||
|
||||
@ -163,6 +163,7 @@ const options = {
|
||||
'build/npm/workspace/package.json',
|
||||
'build/npm/cli/package.json',
|
||||
'build/npm/tao/package.json',
|
||||
'build/npm/eslint-plugin-nx/package.json',
|
||||
'build/npm/linter/package.json'
|
||||
],
|
||||
increment: parsedVersion.version,
|
||||
|
||||
@ -17,13 +17,13 @@ cd build/packages
|
||||
|
||||
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|\*|$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|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
|
||||
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|\*|$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|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
|
||||
@ -31,9 +31,9 @@ fi
|
||||
|
||||
if [[ $NX_VERSION == "*" ]]; 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
|
||||
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
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
if [ -n "$1" ]; then
|
||||
jest --maxWorkers=1 ./build/packages/$1.spec.js
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user