diff --git a/e2e/command-line.test.ts b/e2e/command-line.test.ts index 23fae626fa..20f337bfb4 100644 --- a/e2e/command-line.test.ts +++ b/e2e/command-line.test.ts @@ -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"' diff --git a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts index 75fc3b5517..59a11c19ae 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts @@ -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({ } 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({ // findProjectUsingImport to take care of same prefix const targetProject = findProjectUsingImport( projectGraph, - npmScope, - imp + targetProjectLocator, + sourceFilePath, + imp, + npmScope ); // something went wrong => return. diff --git a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts index fe050f00e3..4dd0cfceb9 100644 --- a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts @@ -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, diff --git a/packages/workspace/src/core/affected-project-graph/affected-project-graph.spec.ts b/packages/workspace/src/core/affected-project-graph/affected-project-graph.spec.ts index 67abd3c762..7181987be3 100644 --- a/packages/workspace/src/core/affected-project-graph/affected-project-graph.spec.ts +++ b/packages/workspace/src/core/affected-project-graph/affected-project-graph.spec.ts @@ -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, diff --git a/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.spec.ts b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.spec.ts new file mode 100644 index 0000000000..bbdacb882a --- /dev/null +++ b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.spec.ts @@ -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; + 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, Parameters>() + .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 + } + ] + }); + }); +}); diff --git a/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts index ebd28a4b37..b3115e428d 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/explicit-project-dependencies.ts @@ -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}/`) - ); - }); - } } diff --git a/packages/workspace/src/core/project-graph/project-graph.spec.ts b/packages/workspace/src/core/project-graph/project-graph.spec.ts index 88ef8e7dc6..bac8bdbd68 100644 --- a/packages/workspace/src/core/project-graph/project-graph.spec.ts +++ b/packages/workspace/src/core/project-graph/project-graph.spec.ts @@ -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' + } + ] }); }); diff --git a/packages/workspace/src/core/target-project-locator.spec.ts b/packages/workspace/src/core/target-project-locator.spec.ts new file mode 100644 index 0000000000..2bbcf407f6 --- /dev/null +++ b/packages/workspace/src/core/target-project-locator.spec.ts @@ -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; + 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' + ]); + }); +}); diff --git a/packages/workspace/src/core/target-project-locator.ts b/packages/workspace/src/core/target-project-locator.ts new file mode 100644 index 0000000000..0495f5283f --- /dev/null +++ b/packages/workspace/src/core/target-project-locator.ts @@ -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 + ); + } +} diff --git a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts index 284da9076b..503e4d090d 100644 --- a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts +++ b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts @@ -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); } diff --git a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts index 2db48dc466..ef86a1b712 100644 --- a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts +++ b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts @@ -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( diff --git a/packages/workspace/src/utils/runtime-lint-utils.ts b/packages/workspace/src/utils/runtime-lint-utils.ts index 8f1a21dc54..35a5e08d14 100644 --- a/packages/workspace/src/utils/runtime-lint-utils.ts +++ b/packages/workspace/src/utils/runtime-lint-utils.ts @@ -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( diff --git a/packages/workspace/src/utils/typescript.ts b/packages/workspace/src/utils/typescript.ts index 228b370456..a28d192e17 100644 --- a/packages/workspace/src/utils/typescript.ts +++ b/packages/workspace/src/utils/typescript.ts @@ -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 }; +}