TypeScript module resolution and lint updates (#2309)
* Revert "Revert "fix(core): sort node names for module resolution"" This reverts commit 0b77072fcfe63f8d02fccb73ba138aca99cb7f66. * fix(core): add target project locator This will sort nodes by length of the root (high to low) then nodes that have no root. It also uses TypeScript to first try and resolve a module. If it is not found via TypeScript, it will fall back to using a string match.
This commit is contained in:
parent
c4ea49c3c6
commit
1c4078986f
@ -44,7 +44,6 @@ forEachCli(() => {
|
||||
`
|
||||
import '../../../libs/${mylib}';
|
||||
import '@proj/${lazylib}';
|
||||
import '@proj/${mylib}/deep';
|
||||
import '@proj/${myapp2}';
|
||||
import '@proj/${invalidtaglib}';
|
||||
import '@proj/${validtaglib}';
|
||||
@ -56,7 +55,6 @@ forEachCli(() => {
|
||||
const out = runCLI(`lint ${myapp}`, { silenceError: true });
|
||||
expect(out).toContain('library imports must start with @proj/');
|
||||
expect(out).toContain('imports of lazy-loaded libraries are forbidden');
|
||||
expect(out).toContain('deep imports into libraries are forbidden');
|
||||
expect(out).toContain('imports of apps are forbidden');
|
||||
expect(out).toContain(
|
||||
'A project tagged with "validtag" can only depend on libs tagged with "validtag"'
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
readNxJson,
|
||||
readWorkspaceJson
|
||||
} from '@nrwl/workspace/src/core/file-utils';
|
||||
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
|
||||
|
||||
type Options = [
|
||||
{
|
||||
@ -107,6 +108,15 @@ export default createESLintRule<Options, MessageIds>({
|
||||
}
|
||||
const npmScope = (global as any).npmScope;
|
||||
const projectGraph = (global as any).projectGraph as ProjectGraph;
|
||||
|
||||
if (!(global as any).targetProjectLocator) {
|
||||
(global as any).targetProjectLocator = new TargetProjectLocator(
|
||||
projectGraph.nodes
|
||||
);
|
||||
}
|
||||
const targetProjectLocator = (global as any)
|
||||
.targetProjectLocator as TargetProjectLocator;
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: TSESTree.ImportDeclaration) {
|
||||
const imp = (node.source as TSESTree.Literal).value as string;
|
||||
@ -148,8 +158,10 @@ export default createESLintRule<Options, MessageIds>({
|
||||
// findProjectUsingImport to take care of same prefix
|
||||
const targetProject = findProjectUsingImport(
|
||||
projectGraph,
|
||||
npmScope,
|
||||
imp
|
||||
targetProjectLocator,
|
||||
sourceFilePath,
|
||||
imp,
|
||||
npmScope
|
||||
);
|
||||
|
||||
// something went wrong => return.
|
||||
|
||||
@ -1,19 +1,73 @@
|
||||
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';
|
||||
import {
|
||||
DependencyType,
|
||||
ProjectGraph,
|
||||
ProjectType
|
||||
} from '@nrwl/workspace/src/core/project-graph';
|
||||
import { TSESLint } from '@typescript-eslint/experimental-utils';
|
||||
import * as parser from '@typescript-eslint/parser';
|
||||
import { vol } from 'memfs';
|
||||
import { extname } from 'path';
|
||||
import enforceModuleBoundaries, {
|
||||
RULE_NAME as enforceModuleBoundariesRuleName
|
||||
} from '../../src/rules/enforce-module-boundaries';
|
||||
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
jest.mock('@nrwl/workspace/src/utils/app-root', () => ({
|
||||
appRootPath: '/root'
|
||||
}));
|
||||
|
||||
const tsconfig = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@mycompany/impl': ['libs/impl/src/index.ts'],
|
||||
'@mycompany/untagged': ['libs/untagged/src/index.ts'],
|
||||
'@mycompany/api': ['libs/api/src/index.ts'],
|
||||
'@mycompany/impl-domain2': ['libs/impl-domain2/src/index.ts'],
|
||||
'@mycompany/impl-both-domains': ['libs/impl-both-domains/src/index.ts'],
|
||||
'@mycompany/impl2': ['libs/impl2/src/index.ts'],
|
||||
'@mycompany/other': ['libs/other/src/index.ts'],
|
||||
'@mycompany/other/a/b': ['libs/other/src/a/b.ts'],
|
||||
'@mycompany/other/a': ['libs/other/src/a/index.ts'],
|
||||
'@mycompany/another/a/b': ['libs/another/a/b.ts'],
|
||||
'@mycompany/myapp': ['apps/myapp/src/index.ts'],
|
||||
'@mycompany/mylib': ['libs/mylib/src/index.ts'],
|
||||
'@mycompany/mylibName': ['libs/mylibName/src/index.ts'],
|
||||
'@mycompany/anotherlibName': ['libs/anotherlibName/src/index.ts'],
|
||||
'@mycompany/badcirclelib': ['libs/badcirclelib/src/index.ts'],
|
||||
'@mycompany/domain1': ['libs/domain1/src/index.ts'],
|
||||
'@mycompany/domain2': ['libs/domain2/src/index.ts']
|
||||
},
|
||||
types: ['node']
|
||||
},
|
||||
exclude: ['**/*.spec.ts'],
|
||||
include: ['**/*.ts']
|
||||
};
|
||||
|
||||
const fileSys = {
|
||||
'./libs/impl/src/index.ts': '',
|
||||
'./libs/untagged/src/index.ts': '',
|
||||
'./libs/api/src/index.ts': '',
|
||||
'./libs/impl-domain2/src/index.ts': '',
|
||||
'./libs/impl-both-domains/src/index.ts': '',
|
||||
'./libs/impl2/src/index.ts': '',
|
||||
'./libs/other/src/index.ts': '',
|
||||
'./libs/other/src/a/b.ts': '',
|
||||
'./libs/other/src/a/index.ts': '',
|
||||
'./libs/another/a/b.ts': '',
|
||||
'./apps/myapp/src/index.ts': '',
|
||||
'./libs/mylib/src/index.ts': '',
|
||||
'./libs/mylibName/src/index.ts': '',
|
||||
'./libs/anotherlibName/src/index.ts': '',
|
||||
'./libs/badcirclelib/src/index.ts': '',
|
||||
'./libs/domain1/src/index.ts': '',
|
||||
'./libs/domain2/src/index.ts': '',
|
||||
'./tsconfig.json': JSON.stringify(tsconfig)
|
||||
};
|
||||
|
||||
describe('Enforce Module Boundaries', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(fs, 'writeFileSync');
|
||||
vol.fromJSON(fileSys, '/root');
|
||||
});
|
||||
|
||||
it('should not error when everything is in order', () => {
|
||||
@ -131,26 +185,15 @@ describe('Enforce Module Boundaries', () => {
|
||||
files: [createFile(`libs/api/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
implName: {
|
||||
name: 'implName',
|
||||
'impl-both-domainsName': {
|
||||
name: 'impl-both-domainsName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl',
|
||||
tags: ['impl', 'domain1'],
|
||||
root: 'libs/impl-both-domains',
|
||||
tags: ['impl', 'domain1', 'domain2'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
impl2Name: {
|
||||
name: 'impl2Name',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl2',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl2/src/index.ts`)]
|
||||
files: [createFile(`libs/impl-both-domains/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
'impl-domain2Name': {
|
||||
@ -164,15 +207,26 @@ describe('Enforce Module Boundaries', () => {
|
||||
files: [createFile(`libs/impl-domain2/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
'impl-both-domainsName': {
|
||||
name: 'impl-both-domainsName',
|
||||
impl2Name: {
|
||||
name: 'impl2Name',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl-both-domains',
|
||||
tags: ['impl', 'domain1', 'domain2'],
|
||||
root: 'libs/impl2',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl-both-domains/src/index.ts`)]
|
||||
files: [createFile(`libs/impl2/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
implName: {
|
||||
name: 'implName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
untaggedName: {
|
||||
@ -199,6 +253,10 @@ describe('Enforce Module Boundaries', () => {
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vol.fromJSON(fileSys, '/root');
|
||||
});
|
||||
|
||||
it('should error when the target library does not have the right tag', () => {
|
||||
const failures = runRule(
|
||||
depConstraints,
|
||||
@ -480,211 +538,6 @@ describe('Enforce Module Boundaries', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/mylib/src/main.ts`),
|
||||
createFile(`libs/mylib/src/another-file.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
},
|
||||
otherSublibName: {
|
||||
name: 'otherSublibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other/sublib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/sublib/src/blah.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
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"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/mylib/src/main.ts`),
|
||||
createFile(`libs/mylib/src/another-file.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
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/**', '@mycompany/**/testing'] },
|
||||
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||
`
|
||||
import "@mycompany/other/src/blah"
|
||||
import "@mycompany/another/testing"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/mylib/src/main.ts`)]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
},
|
||||
anotherName: {
|
||||
name: 'anotherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/another',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/anotherlib/testing.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
expect(failures.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not error about one level 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/a/b";
|
||||
import "@mycompany/other/a";
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/mylib/src/main.ts`)]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/other/a/index.ts`),
|
||||
createFile(`libs/other/a/b.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
anotherName: {
|
||||
name: 'anotherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/another',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/another/a/index.ts`),
|
||||
createFile(`libs/another/a/b.ts`)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
expect(failures.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should respect regexp in allow option', () => {
|
||||
const failures = runRule(
|
||||
{ allow: ['^.*/utils/.*$'] },
|
||||
@ -1146,6 +999,9 @@ function runRule(
|
||||
(global as any).projectPath = `${process.cwd()}/proj`;
|
||||
(global as any).npmScope = 'mycompany';
|
||||
(global as any).projectGraph = projectGraph;
|
||||
(global as any).targetProjectLocator = new TargetProjectLocator(
|
||||
projectGraph.nodes
|
||||
);
|
||||
|
||||
const config = {
|
||||
...baseConfig,
|
||||
|
||||
@ -13,6 +13,7 @@ jest.mock('../../utils/app-root', () => ({ appRootPath: '/root' }));
|
||||
describe('project graph', () => {
|
||||
let packageJson: any;
|
||||
let workspaceJson: any;
|
||||
let tsConfigJson: any;
|
||||
let nxJson: NxJson;
|
||||
let filesJson: any;
|
||||
let filesAtMasterJson: any;
|
||||
@ -82,6 +83,15 @@ describe('project graph', () => {
|
||||
util: { tags: [] }
|
||||
}
|
||||
};
|
||||
tsConfigJson = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@nrwl/ui': ['libs/ui/src/index.ts'],
|
||||
'@nrwl/util': ['libs/util/src/index.ts']
|
||||
}
|
||||
}
|
||||
};
|
||||
filesJson = {
|
||||
'./apps/api/src/index.ts': stripIndents`
|
||||
console.log('starting server');
|
||||
@ -100,7 +110,8 @@ describe('project graph', () => {
|
||||
`,
|
||||
'./package.json': JSON.stringify(packageJson),
|
||||
'./nx.json': JSON.stringify(nxJson),
|
||||
'./workspace.json': JSON.stringify(workspaceJson)
|
||||
'./workspace.json': JSON.stringify(workspaceJson),
|
||||
'./tsconfig.json': JSON.stringify(tsConfigJson)
|
||||
};
|
||||
files = Object.keys(filesJson).map(f => ({
|
||||
file: f,
|
||||
|
||||
@ -0,0 +1,205 @@
|
||||
jest.mock('../../../utils/app-root', () => ({
|
||||
appRootPath: '/root'
|
||||
}));
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
|
||||
import { fs, vol } from 'memfs';
|
||||
import {
|
||||
AddProjectDependency,
|
||||
ProjectGraphContext,
|
||||
ProjectGraphNode,
|
||||
DependencyType
|
||||
} from '../project-graph-models';
|
||||
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
|
||||
import { createFileMap } from '../../file-graph';
|
||||
import { readWorkspaceFiles } from '../../file-utils';
|
||||
import { appRootPath } from '../../../utils/app-root';
|
||||
import { string } from 'prop-types';
|
||||
|
||||
describe('explicit project dependencies', () => {
|
||||
let ctx: ProjectGraphContext;
|
||||
let projects: Record<string, ProjectGraphNode>;
|
||||
let fsJson;
|
||||
beforeEach(() => {
|
||||
const workspaceJson = {
|
||||
projects: {
|
||||
proj: {
|
||||
root: 'libs/proj'
|
||||
},
|
||||
proj2: {
|
||||
root: 'libs/proj2'
|
||||
},
|
||||
proj3a: {
|
||||
root: 'libs/proj3a'
|
||||
},
|
||||
proj123: {
|
||||
root: 'libs/proj123'
|
||||
},
|
||||
proj1234: {
|
||||
root: 'libs/proj1234'
|
||||
},
|
||||
'proj1234-child': {
|
||||
root: 'libs/proj1234-child'
|
||||
}
|
||||
}
|
||||
};
|
||||
const nxJson = {
|
||||
npmScope: 'proj',
|
||||
projects: {
|
||||
proj1: {}
|
||||
}
|
||||
};
|
||||
const tsConfig = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@proj/proj': ['libs/proj/index.ts'],
|
||||
'@proj/my-second-proj': ['libs/proj2/index.ts'],
|
||||
'@proj/project-3': ['libs/proj3a/index.ts'],
|
||||
'@proj/proj123': ['libs/proj123/index.ts'],
|
||||
'@proj/proj1234': ['libs/proj1234/index.ts'],
|
||||
'@proj/proj1234-child': ['libs/proj1234-child/index.ts']
|
||||
}
|
||||
}
|
||||
};
|
||||
fsJson = {
|
||||
'./package.json': `{
|
||||
"name": "test",
|
||||
"dependencies": [],
|
||||
"devDependencies": []
|
||||
}`,
|
||||
'./workspace.json': JSON.stringify(workspaceJson),
|
||||
'./nx.json': JSON.stringify(nxJson),
|
||||
'./tsconfig.json': JSON.stringify(tsConfig),
|
||||
'./libs/proj/index.ts': `import {a} from '@proj/my-second-proj';
|
||||
import('@proj/project-3');
|
||||
const a = { loadChildren: '@proj/proj4ab#a' };
|
||||
`,
|
||||
'./libs/proj2/index.ts': `export const a = 2;`,
|
||||
'./libs/proj3a/index.ts': `export const a = 3;`,
|
||||
'./libs/proj4ab/index.ts': `export const a = 4;`,
|
||||
'./libs/proj123/index.ts': 'export const a = 5',
|
||||
'./libs/proj1234/index.ts': `export const a = 6
|
||||
import { a } from '@proj/proj1234-child'
|
||||
`,
|
||||
'./libs/proj1234-child/index.ts': 'export const a = 7'
|
||||
};
|
||||
vol.fromJSON(fsJson, '/root');
|
||||
|
||||
ctx = {
|
||||
workspaceJson,
|
||||
nxJson,
|
||||
fileMap: createFileMap(workspaceJson, readWorkspaceFiles())
|
||||
};
|
||||
|
||||
projects = {
|
||||
proj3a: {
|
||||
name: 'proj3a',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj3a',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj2: {
|
||||
name: 'proj2',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj2',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj: {
|
||||
name: 'proj',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj1234: {
|
||||
name: 'proj1234',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj1234',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj123: {
|
||||
name: 'proj123',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj123',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj4ab: {
|
||||
name: 'proj4ab',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj4ab',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
'proj1234-child': {
|
||||
name: 'proj1234-child',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj1234-child',
|
||||
files: []
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it(`should add dependencies for projects based on file imports`, () => {
|
||||
const dependencyMap = {};
|
||||
const addDependency = jest
|
||||
.fn<ReturnType<AddProjectDependency>, Parameters<AddProjectDependency>>()
|
||||
.mockImplementation(
|
||||
(type: DependencyType, source: string, target: string) => {
|
||||
const depObj = {
|
||||
type,
|
||||
source,
|
||||
target
|
||||
};
|
||||
if (dependencyMap[source]) {
|
||||
dependencyMap[source].push(depObj);
|
||||
} else {
|
||||
dependencyMap[source] = [depObj];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
buildExplicitTypeScriptDependencies(ctx, projects, addDependency, s => {
|
||||
return fs.readFileSync(`${appRootPath}/${s}`).toString();
|
||||
});
|
||||
|
||||
expect(dependencyMap).toEqual({
|
||||
proj1234: [
|
||||
{
|
||||
source: 'proj1234',
|
||||
target: 'proj1234-child',
|
||||
type: DependencyType.static
|
||||
}
|
||||
],
|
||||
proj: [
|
||||
{
|
||||
source: 'proj',
|
||||
target: 'proj2',
|
||||
type: DependencyType.static
|
||||
},
|
||||
{
|
||||
source: 'proj',
|
||||
target: 'proj3a',
|
||||
type: DependencyType.dynamic
|
||||
},
|
||||
{
|
||||
source: 'proj',
|
||||
target: 'proj4ab',
|
||||
type: DependencyType.dynamic
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,7 @@ import {
|
||||
ProjectGraphNodeRecords
|
||||
} from '../project-graph-models';
|
||||
import { TypeScriptImportLocator } from './typescript-import-locator';
|
||||
import { normalizedProjectRoot } from '../../file-utils';
|
||||
import { TargetProjectLocator } from '../../target-project-locator';
|
||||
|
||||
export function buildExplicitTypeScriptDependencies(
|
||||
ctx: ProjectGraphContext,
|
||||
@ -14,13 +14,17 @@ export function buildExplicitTypeScriptDependencies(
|
||||
fileRead: (s: string) => string
|
||||
) {
|
||||
const importLocator = new TypeScriptImportLocator(fileRead);
|
||||
|
||||
const targetProjectLocator = new TargetProjectLocator(nodes);
|
||||
Object.keys(ctx.fileMap).forEach(source => {
|
||||
Object.values(ctx.fileMap[source]).forEach(f => {
|
||||
importLocator.fromFile(
|
||||
f.file,
|
||||
(importExpr: string, filePath: string, type: DependencyType) => {
|
||||
const target = findTargetProject(importExpr, nodes);
|
||||
const target = targetProjectLocator.findProjectWithImport(
|
||||
importExpr,
|
||||
f.file,
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
if (source && target) {
|
||||
addDependency(type, source, target);
|
||||
}
|
||||
@ -28,16 +32,4 @@ export function buildExplicitTypeScriptDependencies(
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function findTargetProject(importExpr, nodes) {
|
||||
return Object.keys(nodes).find(projectName => {
|
||||
const p = nodes[projectName];
|
||||
const normalizedRoot = normalizedProjectRoot(p);
|
||||
return (
|
||||
importExpr === `@${ctx.nxJson.npmScope}/${normalizedRoot}` ||
|
||||
importExpr.startsWith(`@${ctx.nxJson.npmScope}/${normalizedRoot}#`) ||
|
||||
importExpr.startsWith(`@${ctx.nxJson.npmScope}/${normalizedRoot}/`)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ describe('project graph', () => {
|
||||
let packageJson: any;
|
||||
let workspaceJson: any;
|
||||
let nxJson: NxJson;
|
||||
let tsConfigJson: any;
|
||||
let filesJson: any;
|
||||
let files: FileData[];
|
||||
|
||||
@ -48,6 +49,16 @@ describe('project graph', () => {
|
||||
sourceRoot: 'libs/shared/util/src',
|
||||
projectType: 'library'
|
||||
},
|
||||
'shared-util-data': {
|
||||
root: 'libs/shared/util/data',
|
||||
sourceRoot: 'libs/shared/util/data/src',
|
||||
projectType: 'library'
|
||||
},
|
||||
'lazy-lib': {
|
||||
root: 'libs/lazy-lib',
|
||||
sourceRoot: 'libs/lazy-lib',
|
||||
projectType: 'library'
|
||||
},
|
||||
api: {
|
||||
root: 'apps/api/',
|
||||
sourceRoot: 'apps/api/src',
|
||||
@ -69,7 +80,20 @@ describe('project graph', () => {
|
||||
demo: { tags: [], implicitDependencies: ['api'] },
|
||||
'demo-e2e': { tags: [] },
|
||||
ui: { tags: [] },
|
||||
'shared-util': { tags: [] }
|
||||
'shared-util': { tags: [] },
|
||||
'shared-util-data': { tags: [] },
|
||||
'lazy-lib': { tags: [] }
|
||||
}
|
||||
};
|
||||
tsConfigJson = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@nrwl/shared/util': ['libs/shared/util/src/index.ts'],
|
||||
'@nrwl/shared-util-data': ['libs/shared/util/data/src/index.ts'],
|
||||
'@nrwl/ui': ['libs/ui/src/index.ts'],
|
||||
'@nrwl/lazy-lib': ['libs/lazy-lib/src/index.ts']
|
||||
}
|
||||
}
|
||||
};
|
||||
filesJson = {
|
||||
@ -78,6 +102,9 @@ describe('project graph', () => {
|
||||
`,
|
||||
'./apps/demo/src/index.ts': stripIndents`
|
||||
import * as ui from '@nrwl/ui';
|
||||
import * as data from '@nrwl/shared-util-data;
|
||||
|
||||
const s = { loadChildren: '@nrwl/lazy-lib#LAZY' }
|
||||
`,
|
||||
'./apps/demo-e2e/src/integration/app.spec.ts': stripIndents`
|
||||
describe('whatever', () => {});
|
||||
@ -88,9 +115,16 @@ describe('project graph', () => {
|
||||
'./libs/shared/util/src/index.ts': stripIndents`
|
||||
import * as happyNrwl from 'happy-nrwl/a/b/c';
|
||||
`,
|
||||
'./libs/shared/util/data/src/index.ts': stripIndents`
|
||||
export const SHARED_DATA = 'shared data';
|
||||
`,
|
||||
'./libs/lazy-lib/src/index.ts': stripIndents`
|
||||
export const LAZY = 'lazy lib';
|
||||
`,
|
||||
'./package.json': JSON.stringify(packageJson),
|
||||
'./nx.json': JSON.stringify(nxJson),
|
||||
'./workspace.json': JSON.stringify(workspaceJson)
|
||||
'./workspace.json': JSON.stringify(workspaceJson),
|
||||
'./tsconfig.json': JSON.stringify(tsConfigJson)
|
||||
};
|
||||
files = Object.keys(filesJson).map(f => ({
|
||||
file: f,
|
||||
@ -108,18 +142,39 @@ describe('project graph', () => {
|
||||
'demo-e2e': { name: 'demo-e2e', type: 'e2e' },
|
||||
demo: { name: 'demo', type: 'app' },
|
||||
ui: { name: 'ui', type: 'lib' },
|
||||
'shared-util': { name: 'shared-util', type: 'lib' }
|
||||
'shared-util': { name: 'shared-util', type: 'lib' },
|
||||
'shared-util-data': { name: 'shared-util-data', type: 'lib' },
|
||||
'lazy-lib': { name: 'lazy-lib', type: 'lib' },
|
||||
'happy-nrwl': { name: 'happy-nrwl', type: 'npm' }
|
||||
});
|
||||
|
||||
expect(graph.dependencies).toMatchObject({
|
||||
'demo-e2e': [
|
||||
{ type: DependencyType.implicit, source: 'demo-e2e', target: 'demo' }
|
||||
],
|
||||
demo: expect.arrayContaining([
|
||||
{ type: DependencyType.implicit, source: 'demo', target: 'api' },
|
||||
{ type: DependencyType.static, source: 'demo', target: 'ui' }
|
||||
]),
|
||||
ui: [{ type: DependencyType.static, source: 'ui', target: 'shared-util' }]
|
||||
demo: [
|
||||
{ type: DependencyType.static, source: 'demo', target: 'ui' },
|
||||
{
|
||||
type: DependencyType.static,
|
||||
source: 'demo',
|
||||
target: 'shared-util-data'
|
||||
},
|
||||
{
|
||||
type: DependencyType.dynamic,
|
||||
source: 'demo',
|
||||
target: 'lazy-lib'
|
||||
},
|
||||
{ type: DependencyType.implicit, source: 'demo', target: 'api' }
|
||||
],
|
||||
ui: [
|
||||
{ type: DependencyType.static, source: 'ui', target: 'shared-util' }
|
||||
],
|
||||
'shared-util': [
|
||||
{
|
||||
type: DependencyType.static,
|
||||
source: 'shared-util',
|
||||
target: 'happy-nrwl'
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
261
packages/workspace/src/core/target-project-locator.spec.ts
Normal file
261
packages/workspace/src/core/target-project-locator.spec.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import { fs, vol } from 'memfs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
ProjectGraphContext,
|
||||
ProjectGraphNode
|
||||
} from './project-graph/project-graph-models';
|
||||
import { TargetProjectLocator } from './target-project-locator';
|
||||
|
||||
jest.mock('../utils/app-root', () => ({
|
||||
appRootPath: '/root'
|
||||
}));
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
|
||||
describe('findTargetProjectWithImport', () => {
|
||||
let ctx: ProjectGraphContext;
|
||||
let projects: Record<string, ProjectGraphNode>;
|
||||
let fsJson;
|
||||
let targetProjectLocator: TargetProjectLocator;
|
||||
beforeEach(() => {
|
||||
const workspaceJson = {
|
||||
projects: {
|
||||
proj1: {}
|
||||
}
|
||||
};
|
||||
const nxJson = {
|
||||
npmScope: 'proj',
|
||||
projects: {
|
||||
proj1: {}
|
||||
}
|
||||
};
|
||||
const tsConfig = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@proj/proj': ['libs/proj/index.ts'],
|
||||
'@proj/my-second-proj': ['libs/proj2/index.ts'],
|
||||
'@proj/project-3': ['libs/proj3a/index.ts'],
|
||||
'@proj/proj123': ['libs/proj123/index.ts'],
|
||||
'@proj/proj1234': ['libs/proj1234/index.ts'],
|
||||
'@proj/proj1234-child': ['libs/proj1234-child/index.ts']
|
||||
}
|
||||
}
|
||||
};
|
||||
fsJson = {
|
||||
'./workspace.json': JSON.stringify(workspaceJson),
|
||||
'./nx.json': JSON.stringify(nxJson),
|
||||
'./tsconfig.json': JSON.stringify(tsConfig),
|
||||
'./libs/proj/index.ts': `import {a} from '@proj/my-second-proj';
|
||||
import('@proj/project-3');
|
||||
const a = { loadChildren: '@proj/proj4ab#a' };
|
||||
`,
|
||||
'./libs/proj2/index.ts': `export const a = 2;`,
|
||||
'./libs/proj3a/index.ts': `export const a = 3;`,
|
||||
'./libs/proj4ab/index.ts': `export const a = 4;`,
|
||||
'./libs/proj123/index.ts': 'export const a = 5',
|
||||
'./libs/proj1234/index.ts': 'export const a = 6',
|
||||
'./libs/proj1234-child/index.ts': 'export const a = 7'
|
||||
};
|
||||
vol.fromJSON(fsJson, '/root');
|
||||
|
||||
ctx = {
|
||||
workspaceJson,
|
||||
nxJson,
|
||||
fileMap: {
|
||||
proj: [
|
||||
{
|
||||
file: 'libs/proj/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
proj2: [
|
||||
{
|
||||
file: 'libs/proj2/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
proj3a: [
|
||||
{
|
||||
file: 'libs/proj3a/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
proj4ab: [
|
||||
{
|
||||
file: 'libs/proj4ab/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
proj123: [
|
||||
{
|
||||
file: 'libs/proj123/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
proj1234: [
|
||||
{
|
||||
file: 'libs/proj1234/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
],
|
||||
'proj1234-child': [
|
||||
{
|
||||
file: 'libs/proj1234-child/index.ts',
|
||||
mtime: 0,
|
||||
ext: '.ts'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
projects = {
|
||||
proj3a: {
|
||||
name: 'proj3a',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj3a',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj2: {
|
||||
name: 'proj2',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj2',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj: {
|
||||
name: 'proj',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj1234: {
|
||||
name: 'proj1234',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj1234',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj123: {
|
||||
name: 'proj123',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj123',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
proj4ab: {
|
||||
name: 'proj4ab',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj4ab',
|
||||
files: []
|
||||
}
|
||||
},
|
||||
'@ng/core': {
|
||||
name: '@ng/core',
|
||||
type: 'npm',
|
||||
data: {
|
||||
files: []
|
||||
}
|
||||
},
|
||||
'@ng/common': {
|
||||
name: '@ng/common',
|
||||
type: 'npm',
|
||||
data: {
|
||||
files: []
|
||||
}
|
||||
},
|
||||
'npm-package': {
|
||||
name: 'npm-package',
|
||||
type: 'npm',
|
||||
data: {
|
||||
files: []
|
||||
}
|
||||
},
|
||||
'proj1234-child': {
|
||||
name: 'proj1234-child',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'libs/proj1234-child',
|
||||
files: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
targetProjectLocator = new TargetProjectLocator(projects);
|
||||
});
|
||||
|
||||
it('should be able to resolve a module by using tsConfig paths', () => {
|
||||
const proj2 = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/my-second-proj',
|
||||
'libs/proj1/index.ts',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
const proj3a = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/project-3',
|
||||
'libs/proj1/index.ts',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
|
||||
expect(proj2).toEqual('proj2');
|
||||
expect(proj3a).toEqual('proj3a');
|
||||
});
|
||||
it('should be able to resolve a module using a normalized path', () => {
|
||||
const proj4ab = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/proj4ab',
|
||||
'libs/proj1/index.ts',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
|
||||
expect(proj4ab).toEqual('proj4ab');
|
||||
});
|
||||
it('should be able to resolve paths that have similar names', () => {
|
||||
const proj = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/proj123',
|
||||
'',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
expect(proj).toEqual('proj123');
|
||||
|
||||
const childProj = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/proj1234-child',
|
||||
'',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
expect(childProj).toEqual('proj1234-child');
|
||||
|
||||
const parentProj = targetProjectLocator.findProjectWithImport(
|
||||
'@proj/proj1234',
|
||||
'',
|
||||
ctx.nxJson.npmScope
|
||||
);
|
||||
expect(parentProj).toEqual('proj1234');
|
||||
});
|
||||
|
||||
it('should be able to sort graph nodes', () => {
|
||||
expect(targetProjectLocator._sortedNodeNames).toEqual([
|
||||
'proj1234-child',
|
||||
'proj1234',
|
||||
'proj123',
|
||||
'proj4ab',
|
||||
'proj3a',
|
||||
'proj2',
|
||||
'proj',
|
||||
'@ng/core',
|
||||
'@ng/common',
|
||||
'npm-package'
|
||||
]);
|
||||
});
|
||||
});
|
||||
74
packages/workspace/src/core/target-project-locator.ts
Normal file
74
packages/workspace/src/core/target-project-locator.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { resolveModuleByImport } from '../utils/typescript';
|
||||
import { normalizedProjectRoot } from './file-utils';
|
||||
import {
|
||||
ProjectGraphNodeRecords,
|
||||
ProjectType
|
||||
} from './project-graph/project-graph-models';
|
||||
|
||||
export class TargetProjectLocator {
|
||||
_sortedNodeNames = [];
|
||||
|
||||
constructor(private nodes: ProjectGraphNodeRecords) {
|
||||
this._sortedNodeNames = Object.keys(nodes).sort((a, b) => {
|
||||
// If a or b is not a nx project, leave them in the same spot
|
||||
if (
|
||||
!this._isNxProject(nodes[a].type) &&
|
||||
!this._isNxProject(nodes[b].type)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
// sort all non-projects lower
|
||||
if (
|
||||
!this._isNxProject(nodes[a].type) &&
|
||||
this._isNxProject(nodes[b].type)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
if (
|
||||
this._isNxProject(nodes[a].type) &&
|
||||
!this._isNxProject(nodes[b].type)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return nodes[a].data.root.length > nodes[b].data.root.length ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
findProjectWithImport(
|
||||
importExpr: string,
|
||||
filePath: string,
|
||||
npmScope: string
|
||||
) {
|
||||
const normalizedImportExpr = importExpr.split('#')[0];
|
||||
|
||||
const resolvedModule = resolveModuleByImport(
|
||||
normalizedImportExpr,
|
||||
filePath
|
||||
);
|
||||
|
||||
return this._sortedNodeNames.find(projectName => {
|
||||
const p = this.nodes[projectName];
|
||||
|
||||
if (!this._isNxProject(p.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resolvedModule && resolvedModule.startsWith(p.data.root)) {
|
||||
return true;
|
||||
} else {
|
||||
const normalizedRoot = normalizedProjectRoot(p);
|
||||
const projectImport = `@${npmScope}/${normalizedRoot}`;
|
||||
return normalizedImportExpr.startsWith(projectImport);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _isNxProject(type: string) {
|
||||
return (
|
||||
type === ProjectType.app ||
|
||||
type === ProjectType.lib ||
|
||||
type === ProjectType.e2e
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,70 @@
|
||||
import { vol } from 'memfs';
|
||||
import { extname } from 'path';
|
||||
import { RuleFailure } from 'tslint';
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import {
|
||||
DependencyType,
|
||||
ProjectGraph,
|
||||
ProjectType
|
||||
} from '../core/project-graph';
|
||||
import { Rule } from './nxEnforceModuleBoundariesRule';
|
||||
import { DependencyType, ProjectGraph } from '../core/project-graph';
|
||||
import { ProjectType } from '../core/project-graph';
|
||||
import { extname } from 'path';
|
||||
import { TargetProjectLocator } from '../core/target-project-locator';
|
||||
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
jest.mock('../utils/app-root', () => ({ appRootPath: '/root' }));
|
||||
|
||||
const tsconfig = {
|
||||
compilerOptions: {
|
||||
baseUrl: '.',
|
||||
paths: {
|
||||
'@mycompany/impl': ['libs/impl/src/index.ts'],
|
||||
'@mycompany/untagged': ['libs/untagged/src/index.ts'],
|
||||
'@mycompany/api': ['libs/api/src/index.ts'],
|
||||
'@mycompany/impl-domain2': ['libs/impl-domain2/src/index.ts'],
|
||||
'@mycompany/impl-both-domains': ['libs/impl-both-domains/src/index.ts'],
|
||||
'@mycompany/impl2': ['libs/impl2/src/index.ts'],
|
||||
'@mycompany/other': ['libs/other/src/index.ts'],
|
||||
'@mycompany/other/a/b': ['libs/other/src/a/b.ts'],
|
||||
'@mycompany/other/a': ['libs/other/src/a/index.ts'],
|
||||
'@mycompany/another/a/b': ['libs/another/a/b.ts'],
|
||||
'@mycompany/myapp': ['apps/myapp/src/index.ts'],
|
||||
'@mycompany/mylib': ['libs/mylib/src/index.ts'],
|
||||
'@mycompany/mylibName': ['libs/mylibName/src/index.ts'],
|
||||
'@mycompany/anotherlibName': ['libs/anotherlibName/src/index.ts'],
|
||||
'@mycompany/badcirclelib': ['libs/badcirclelib/src/index.ts'],
|
||||
'@mycompany/domain1': ['libs/domain1/src/index.ts'],
|
||||
'@mycompany/domain2': ['libs/domain2/src/index.ts']
|
||||
},
|
||||
types: ['node']
|
||||
},
|
||||
exclude: ['**/*.spec.ts'],
|
||||
include: ['**/*.ts']
|
||||
};
|
||||
|
||||
const fileSys = {
|
||||
'./libs/impl/src/index.ts': '',
|
||||
'./libs/untagged/src/index.ts': '',
|
||||
'./libs/api/src/index.ts': '',
|
||||
'./libs/impl-domain2/src/index.ts': '',
|
||||
'./libs/impl-both-domains/src/index.ts': '',
|
||||
'./libs/impl2/src/index.ts': '',
|
||||
'./libs/other/src/index.ts': '',
|
||||
'./libs/other/src/a/b.ts': '',
|
||||
'./libs/other/src/a/index.ts': '',
|
||||
'./libs/another/a/b.ts': '',
|
||||
'./apps/myapp/src/index.ts': '',
|
||||
'./libs/mylib/src/index.ts': '',
|
||||
'./libs/mylibName/src/index.ts': '',
|
||||
'./libs/anotherlibName/src/index.ts': '',
|
||||
'./libs/badcirclelib/src/index.ts': '',
|
||||
'./libs/domain1/src/index.ts': '',
|
||||
'./libs/domain2/src/index.ts': '',
|
||||
'./tsconfig.json': JSON.stringify(tsconfig)
|
||||
};
|
||||
|
||||
describe('Enforce Module Boundaries', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(fs, 'writeFileSync');
|
||||
vol.fromJSON(fileSys, '/root');
|
||||
});
|
||||
|
||||
it('should not error when everything is in order', () => {
|
||||
@ -126,26 +181,15 @@ describe('Enforce Module Boundaries', () => {
|
||||
files: [createFile(`libs/api/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
implName: {
|
||||
name: 'implName',
|
||||
'impl-both-domainsName': {
|
||||
name: 'impl-both-domainsName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl',
|
||||
tags: ['impl', 'domain1'],
|
||||
root: 'libs/impl-both-domains',
|
||||
tags: ['impl', 'domain1', 'domain2'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
impl2Name: {
|
||||
name: 'impl2Name',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl2',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl2/src/index.ts`)]
|
||||
files: [createFile(`libs/impl-both-domains/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
'impl-domain2Name': {
|
||||
@ -159,15 +203,26 @@ describe('Enforce Module Boundaries', () => {
|
||||
files: [createFile(`libs/impl-domain2/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
'impl-both-domainsName': {
|
||||
name: 'impl-both-domainsName',
|
||||
impl2Name: {
|
||||
name: 'impl2Name',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl-both-domains',
|
||||
tags: ['impl', 'domain1', 'domain2'],
|
||||
root: 'libs/impl2',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl-both-domains/src/index.ts`)]
|
||||
files: [createFile(`libs/impl2/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
implName: {
|
||||
name: 'implName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/impl',
|
||||
tags: ['impl', 'domain1'],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/impl/src/index.ts`)]
|
||||
}
|
||||
},
|
||||
untaggedName: {
|
||||
@ -194,6 +249,10 @@ describe('Enforce Module Boundaries', () => {
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vol.fromJSON(fileSys, '/root');
|
||||
});
|
||||
|
||||
it('should error when the target library does not have the right tag', () => {
|
||||
const failures = runRule(
|
||||
depConstraints,
|
||||
@ -476,212 +535,6 @@ describe('Enforce Module Boundaries', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/mylib/src/main.ts`),
|
||||
createFile(`libs/mylib/src/another-file.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
},
|
||||
otherSublibName: {
|
||||
name: 'otherSublibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other/sublib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/sublib/src/blah.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
expect(failures[0].getFailure()).toEqual(
|
||||
'deep imports into libraries are forbidden'
|
||||
);
|
||||
expect(failures[1].getFailure()).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"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/mylib/src/main.ts`),
|
||||
createFile(`libs/mylib/src/another-file.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
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/**', '@mycompany/**/testing'] },
|
||||
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||
`
|
||||
import "@mycompany/other/src/blah"
|
||||
import "@mycompany/another/testing"
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/mylib/src/main.ts`)]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/other/src/blah.ts`)]
|
||||
}
|
||||
},
|
||||
anotherName: {
|
||||
name: 'anotherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/another',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/anotherlib/testing.ts`)]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
expect(failures.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should not error about one level deep imports into library when exception is specified with a wildcard', () => {
|
||||
const failures = runRule(
|
||||
{ allow: ['@mycompany/other/*', '@mycompany/another/*'] },
|
||||
`${process.cwd()}/proj/libs/mylib/src/main.ts`,
|
||||
`
|
||||
import "@mycompany/other/a/b";
|
||||
import "@mycompany/other/a";
|
||||
import "@mycompany/another/a/b";
|
||||
`,
|
||||
{
|
||||
nodes: {
|
||||
mylibName: {
|
||||
name: 'mylibName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/mylib',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [createFile(`libs/mylib/src/main.ts`)]
|
||||
}
|
||||
},
|
||||
otherName: {
|
||||
name: 'otherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/other',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/other/a/index.ts`),
|
||||
createFile(`libs/other/a/b.ts`)
|
||||
]
|
||||
}
|
||||
},
|
||||
anotherName: {
|
||||
name: 'anotherName',
|
||||
type: ProjectType.lib,
|
||||
data: {
|
||||
root: 'libs/another',
|
||||
tags: [],
|
||||
implicitDependencies: [],
|
||||
architect: {},
|
||||
files: [
|
||||
createFile(`libs/another/a/index.ts`),
|
||||
createFile(`libs/another/a/b.ts`)
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
dependencies: {}
|
||||
}
|
||||
);
|
||||
expect(failures.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should respect regexp in allow option', () => {
|
||||
const failures = runRule(
|
||||
{ allow: ['^.*/utils/.*$'] },
|
||||
@ -1143,7 +996,8 @@ function runRule(
|
||||
options,
|
||||
`${process.cwd()}/proj`,
|
||||
'mycompany',
|
||||
projectGraph
|
||||
projectGraph,
|
||||
new TargetProjectLocator(projectGraph.nodes)
|
||||
);
|
||||
return rule.apply(sourceFile);
|
||||
}
|
||||
|
||||
@ -29,13 +29,15 @@ import {
|
||||
readNxJson,
|
||||
readWorkspaceJson
|
||||
} from '@nrwl/workspace/src/core/file-utils';
|
||||
import { TargetProjectLocator } from '../core/target-project-locator';
|
||||
|
||||
export class Rule extends Lint.Rules.AbstractRule {
|
||||
constructor(
|
||||
options: IOptions,
|
||||
private readonly projectPath?: string,
|
||||
private readonly npmScope?: string,
|
||||
private readonly projectGraph?: ProjectGraph
|
||||
private readonly projectGraph?: ProjectGraph,
|
||||
private readonly targetProjectLocator?: TargetProjectLocator
|
||||
) {
|
||||
super(options);
|
||||
|
||||
@ -52,6 +54,13 @@ export class Rule extends Lint.Rules.AbstractRule {
|
||||
}
|
||||
this.npmScope = (global as any).npmScope;
|
||||
this.projectGraph = (global as any).projectGraph;
|
||||
|
||||
if (!(global as any).targetProjectLocator) {
|
||||
(global as any).targetProjectLocator = new TargetProjectLocator(
|
||||
this.projectGraph.nodes
|
||||
);
|
||||
}
|
||||
this.targetProjectLocator = (global as any).targetProjectLocator;
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +71,8 @@ export class Rule extends Lint.Rules.AbstractRule {
|
||||
this.getOptions(),
|
||||
this.projectPath,
|
||||
this.npmScope,
|
||||
this.projectGraph
|
||||
this.projectGraph,
|
||||
this.targetProjectLocator
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -78,7 +88,8 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
options: IOptions,
|
||||
private readonly projectPath: string,
|
||||
private readonly npmScope: string,
|
||||
private readonly projectGraph: ProjectGraph
|
||||
private readonly projectGraph: ProjectGraph,
|
||||
private readonly targetProjectLocator: TargetProjectLocator
|
||||
) {
|
||||
super(sourceFile, options);
|
||||
|
||||
@ -129,15 +140,18 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
// check constraints between libs and apps
|
||||
if (imp.startsWith(`@${this.npmScope}/`)) {
|
||||
// we should find the name
|
||||
const sourceProject = findSourceProject(
|
||||
this.projectGraph,
|
||||
getSourceFilePath(this.getSourceFile().fileName, this.projectPath)
|
||||
const filePath = getSourceFilePath(
|
||||
this.getSourceFile().fileName,
|
||||
this.projectPath
|
||||
);
|
||||
const sourceProject = findSourceProject(this.projectGraph, filePath);
|
||||
// findProjectUsingImport to take care of same prefix
|
||||
const targetProject = findProjectUsingImport(
|
||||
this.projectGraph,
|
||||
this.npmScope,
|
||||
imp
|
||||
this.targetProjectLocator,
|
||||
filePath,
|
||||
imp,
|
||||
this.npmScope
|
||||
);
|
||||
|
||||
// something went wrong => return.
|
||||
@ -169,16 +183,6 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
return;
|
||||
}
|
||||
|
||||
// deep imports aren't allowed
|
||||
if (imp !== `@${this.npmScope}/${normalizedProjectRoot(targetProject)}`) {
|
||||
this.addFailureAt(
|
||||
node.getStart(),
|
||||
node.getWidth(),
|
||||
'deep imports into libraries are forbidden'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// buildable-lib is not allowed to import non-buildable-lib
|
||||
if (
|
||||
this.enforceBuildableLibDependency === true &&
|
||||
@ -198,16 +202,6 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
|
||||
}
|
||||
}
|
||||
|
||||
// deep imports aren't allowed
|
||||
if (imp !== `@${this.npmScope}/${normalizedProjectRoot(targetProject)}`) {
|
||||
this.addFailureAt(
|
||||
node.getStart(),
|
||||
node.getWidth(),
|
||||
'deep imports into libraries are forbidden'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if we import a library using loadChildren, we should not import it using es6imports
|
||||
if (
|
||||
onlyLoadChildren(
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { normalize } from '@angular-devkit/core';
|
||||
import * as path from 'path';
|
||||
import { FileData } from '../core/file-utils';
|
||||
import {
|
||||
DependencyType,
|
||||
ProjectGraphNode,
|
||||
ProjectGraph,
|
||||
ProjectGraphDependency,
|
||||
ProjectGraph
|
||||
ProjectGraphNode
|
||||
} from '../core/project-graph';
|
||||
import { normalize } from '@angular-devkit/core';
|
||||
import { FileData, normalizedProjectRoot } from '../core/file-utils';
|
||||
import { TargetProjectLocator } from '../core/target-project-locator';
|
||||
|
||||
export type Deps = { [projectName: string]: ProjectGraphDependency[] };
|
||||
export type DepConstraint = {
|
||||
@ -133,25 +134,17 @@ export function isAbsoluteImportIntoAnotherProject(imp: string) {
|
||||
|
||||
export function findProjectUsingImport(
|
||||
projectGraph: ProjectGraph,
|
||||
npmScope: string,
|
||||
imp: string
|
||||
targetProjectLocator: TargetProjectLocator,
|
||||
filePath: string,
|
||||
imp: string,
|
||||
npmScope: string
|
||||
) {
|
||||
const unscopedImport = imp.substring(npmScope.length + 2);
|
||||
let bestMatchedRoot: string = '';
|
||||
let bestMatch: ProjectGraphNode = undefined;
|
||||
Object.values(projectGraph.nodes).forEach(n => {
|
||||
const normalizedRoot = normalizedProjectRoot(n);
|
||||
if (
|
||||
unscopedImport === normalizedRoot ||
|
||||
unscopedImport.startsWith(`${normalizedRoot}/`)
|
||||
) {
|
||||
if (normalizedRoot.length > bestMatchedRoot.length) {
|
||||
bestMatchedRoot = normalizedRoot;
|
||||
bestMatch = n;
|
||||
}
|
||||
}
|
||||
});
|
||||
return bestMatch;
|
||||
const target = targetProjectLocator.findProjectWithImport(
|
||||
imp,
|
||||
filePath,
|
||||
npmScope
|
||||
);
|
||||
return projectGraph.nodes[target];
|
||||
}
|
||||
|
||||
export function isCircular(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as ts from 'typescript';
|
||||
import { dirname } from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import { appRootPath } from './app-root';
|
||||
|
||||
export function readTsConfig(tsConfigPath: string) {
|
||||
const readResult = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
||||
@ -9,3 +10,44 @@ export function readTsConfig(tsConfigPath: string) {
|
||||
dirname(tsConfigPath)
|
||||
);
|
||||
}
|
||||
|
||||
let compilerHost: {
|
||||
host: ts.CompilerHost;
|
||||
options: ts.CompilerOptions;
|
||||
moduleResolutionCache: ts.ModuleResolutionCache;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a module based on it's import
|
||||
*
|
||||
* @param importExpr Import used to resolve to a module
|
||||
* @param filePath
|
||||
*/
|
||||
export function resolveModuleByImport(importExpr: string, filePath: string) {
|
||||
compilerHost = compilerHost || getCompilerHost();
|
||||
const { options, host, moduleResolutionCache } = compilerHost;
|
||||
|
||||
const { resolvedModule } = ts.resolveModuleName(
|
||||
importExpr,
|
||||
filePath,
|
||||
options,
|
||||
host,
|
||||
moduleResolutionCache
|
||||
);
|
||||
|
||||
if (!resolvedModule) {
|
||||
return;
|
||||
} else {
|
||||
return resolvedModule.resolvedFileName.replace(`${appRootPath}/`, '');
|
||||
}
|
||||
}
|
||||
|
||||
function getCompilerHost() {
|
||||
const { options } = readTsConfig(`${appRootPath}/tsconfig.json`);
|
||||
const host = ts.createCompilerHost(options, true);
|
||||
const moduleResolutionCache = ts.createModuleResolutionCache(
|
||||
appRootPath,
|
||||
host.getCanonicalFileName
|
||||
);
|
||||
return { options, host, moduleResolutionCache };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user