feat(eslint-plugin-nx): new package, ESLint enforce-module-boundaries rule

This commit is contained in:
James Henry 2019-08-12 18:54:29 +01:00 committed by Victor Savkin
parent 821891457f
commit 49dcacfd1a
17 changed files with 1476 additions and 189 deletions

View File

@ -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",

View 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"
}
}

View File

@ -0,0 +1,9 @@
import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName
} from './rules/enforce-module-boundaries';
module.exports = {
rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries
}
};

View 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;
}
}
}
}
}
};
}
});

View File

@ -0,0 +1,3 @@
import { ESLintUtils } from '@typescript-eslint/experimental-utils';
export const createESLintRule = ESLintUtils.RuleCreator(() => ``);

View File

@ -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;
}

View 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/');
}

View File

@ -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'

View File

@ -42,6 +42,7 @@
"dotenv": "6.2.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"eslint": "<%= eslintVersion %>",
"typescript": "<%= typescriptVersion %>",
"prettier": "<%= prettierVersion %>"
}

View File

@ -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),

View File

@ -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);
}

View File

@ -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

View 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);
}

View File

@ -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:"

View File

@ -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,

View File

@ -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

View File

@ -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