diff --git a/dep-graph/client/src/app/machines/dep-graph.spec.ts b/dep-graph/client/src/app/machines/dep-graph.spec.ts index ae639c4760..8a78667270 100644 --- a/dep-graph/client/src/app/machines/dep-graph.spec.ts +++ b/dep-graph/client/src/app/machines/dep-graph.spec.ts @@ -1,5 +1,9 @@ // nx-ignore-next-line -import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; +import { + DependencyType, + ProjectGraphDependency, + ProjectGraphNode, +} from '@nrwl/devkit'; import { depGraphMachine } from './dep-graph.machine'; import { interpret } from 'xstate'; @@ -51,38 +55,38 @@ export const mockProjects: ProjectGraphNode[] = [ export const mockDependencies: Record = { app1: [ { - type: 'static', + type: DependencyType.static, source: 'app1', target: 'auth-lib', }, { - type: 'static', + type: DependencyType.static, source: 'app1', target: 'feature-lib1', }, ], app2: [ { - type: 'static', + type: DependencyType.static, source: 'app2', target: 'auth-lib', }, { - type: 'static', + type: DependencyType.static, source: 'app2', target: 'feature-lib2', }, ], 'feature-lib1': [ { - type: 'static', + type: DependencyType.static, source: 'feature-lib1', target: 'ui-lib', }, ], 'feature-lib2': [ { - type: 'static', + type: DependencyType.static, source: 'feature-lib2', target: 'ui-lib', }, diff --git a/dep-graph/client/src/app/mock-project-graph-service.ts b/dep-graph/client/src/app/mock-project-graph-service.ts index 7911fd6574..5ab60e9ac7 100644 --- a/dep-graph/client/src/app/mock-project-graph-service.ts +++ b/dep-graph/client/src/app/mock-project-graph-service.ts @@ -33,7 +33,7 @@ export class MockProjectGraphService implements ProjectGraphService { { source: 'existing-app-1', target: 'existing-lib-1', - type: 'statis', + type: 'static' as any, }, ], 'existing-lib-1': [], @@ -82,7 +82,7 @@ export class MockProjectGraphService implements ProjectGraphService { { source: newProject.name, target: targetDependency.name, - type: 'static', + type: 'static' as any, }, ]; diff --git a/dep-graph/client/src/app/styles-graph/edges.ts b/dep-graph/client/src/app/styles-graph/edges.ts index 6b7804fbf1..d0ffaf680e 100644 --- a/dep-graph/client/src/app/styles-graph/edges.ts +++ b/dep-graph/client/src/app/styles-graph/edges.ts @@ -41,9 +41,22 @@ const dynamicEdges: Stylesheet = { }, }; +const typeOnlyEdges: Stylesheet = { + selector: 'edge.typeOnly', + style: { + label: 'type only', + 'font-size': '16px', + 'edge-text-rotation': 'autorotate', + 'curve-style': 'unbundled-bezier', + 'line-dash-pattern': [5, 5], + 'line-style': 'dashed', + }, +}; + export const edgeStyles: Stylesheet[] = [ allEdges, affectedEdges, implicitEdges, dynamicEdges, + typeOnlyEdges, ]; diff --git a/dep-graph/client/src/assets/graphs/nx-examples.json b/dep-graph/client/src/assets/graphs/nx-examples.json index 6cda624303..3895d0c004 100644 --- a/dep-graph/client/src/assets/graphs/nx-examples.json +++ b/dep-graph/client/src/assets/graphs/nx-examples.json @@ -185,7 +185,7 @@ { "source": "shared-product-data", "target": "shared-product-types", - "type": "static" + "type": "typeOnly" } ], "products-home-page": [ diff --git a/docs/generated/api-nx-devkit/index.md b/docs/generated/api-nx-devkit/index.md index 159e7c53b5..151481ec68 100644 --- a/docs/generated/api-nx-devkit/index.md +++ b/docs/generated/api-nx-devkit/index.md @@ -36,6 +36,7 @@ It only uses language primitives and immutable objects - [FileData](../../generated/nx-devkit/index#filedata) - [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap) - [ProjectGraph](../../generated/nx-devkit/index#projectgraph) +- [ProjectGraphBuilderExplicitDependency](../../generated/nx-devkit/index#projectgraphbuilderexplicitdependency) - [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency) - [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode) - [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext) @@ -210,6 +211,12 @@ A plugin for Nx --- +### ProjectGraphBuilderExplicitDependency + +• **ProjectGraphBuilderExplicitDependency**: `Object` + +--- + ### ProjectGraphDependency • **ProjectGraphDependency**: `Object` diff --git a/e2e/workspace-integrations/src/workspace-aux-commands.test.ts b/e2e/workspace-integrations/src/workspace-aux-commands.test.ts index 0747957414..e337791abb 100644 --- a/e2e/workspace-integrations/src/workspace-aux-commands.test.ts +++ b/e2e/workspace-integrations/src/workspace-aux-commands.test.ts @@ -273,7 +273,7 @@ describe('dep-graph', () => { target: mylib, type: 'static', }, - { source: myapp, target: mylib2, type: 'static' }, + { source: myapp, target: mylib2, type: 'dynamic' }, ], [myappE2e]: [ { diff --git a/nx-dev/nx-dev/public/documentation/generated/api-nx-devkit/index.md b/nx-dev/nx-dev/public/documentation/generated/api-nx-devkit/index.md index 159e7c53b5..151481ec68 100644 --- a/nx-dev/nx-dev/public/documentation/generated/api-nx-devkit/index.md +++ b/nx-dev/nx-dev/public/documentation/generated/api-nx-devkit/index.md @@ -36,6 +36,7 @@ It only uses language primitives and immutable objects - [FileData](../../generated/nx-devkit/index#filedata) - [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap) - [ProjectGraph](../../generated/nx-devkit/index#projectgraph) +- [ProjectGraphBuilderExplicitDependency](../../generated/nx-devkit/index#projectgraphbuilderexplicitdependency) - [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency) - [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode) - [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext) @@ -210,6 +211,12 @@ A plugin for Nx --- +### ProjectGraphBuilderExplicitDependency + +• **ProjectGraphBuilderExplicitDependency**: `Object` + +--- + ### ProjectGraphDependency • **ProjectGraphDependency**: `Object` diff --git a/packages/devkit/index.ts b/packages/devkit/index.ts index a306950c24..57c2b9f9ce 100644 --- a/packages/devkit/index.ts +++ b/packages/devkit/index.ts @@ -144,6 +144,7 @@ export type { ProjectFileMap, FileData, ProjectGraph, + ProjectGraphBuilderExplicitDependency, ProjectGraphDependency, ProjectGraphNode, ProjectGraphProjectNode, diff --git a/packages/devkit/src/project-graph/project-graph-builder.spec.ts b/packages/devkit/src/project-graph/project-graph-builder.spec.ts index c86dce4c11..36e44eb86f 100644 --- a/packages/devkit/src/project-graph/project-graph-builder.spec.ts +++ b/packages/devkit/src/project-graph/project-graph-builder.spec.ts @@ -1,3 +1,4 @@ +import { DependencyType } from './interfaces'; import { ProjectGraphBuilder } from './project-graph-builder'; describe('ProjectGraphBuilder', () => { @@ -96,21 +97,77 @@ describe('ProjectGraphBuilder', () => { }); }); - it(`should use implicit dep when both implicit and explicit deps are available`, () => { - // don't include duplicates - builder.addImplicitDependency('source', 'target'); - builder.addExplicitDependency('source', 'source/index.ts', 'target'); + describe('dependency type priority', () => { + it(`should use implicit dep when both implicit and explicit deps are available`, () => { + // don't include duplicates + builder.addImplicitDependency('source', 'target'); + builder.addExplicitDependency('source', 'source/index.ts', 'target'); - const graph = builder.getUpdatedProjectGraph(); - expect(graph.dependencies).toEqual({ - source: [ - { - source: 'source', - target: 'target', - type: 'implicit', - }, - ], - target: [], + const graph = builder.getUpdatedProjectGraph(); + expect(graph.dependencies).toEqual({ + source: [ + { + source: 'source', + target: 'target', + type: 'implicit', + }, + ], + target: [], + }); + }); + + it(`should use explicit deps in priority order "static > dynamic"`, () => { + builder.addExplicitDependency( + 'source', + 'source/index.ts', + 'target', + DependencyType.dynamic + ); + builder.addExplicitDependency( + 'source', + 'source/index.ts', + 'target', + DependencyType.static + ); + + const graph = builder.getUpdatedProjectGraph(); + expect(graph.dependencies).toEqual({ + source: [ + { + source: 'source', + target: 'target', + type: DependencyType.static, + }, + ], + target: [], + }); + }); + + it(`should use explicit deps in priority order "dynamic > type-only"`, () => { + builder.addExplicitDependency( + 'source', + 'source/index.ts', + 'target', + DependencyType.dynamic + ); + builder.addExplicitDependency( + 'source', + 'source/second.ts', + 'target', + DependencyType.typeOnly + ); + + const graph = builder.getUpdatedProjectGraph(); + expect(graph.dependencies).toEqual({ + source: [ + { + source: 'source', + target: 'target', + type: DependencyType.dynamic, + }, + ], + target: [], + }); }); }); diff --git a/packages/devkit/src/project-graph/project-graph-builder.ts b/packages/devkit/src/project-graph/project-graph-builder.ts index b97bafc316..e0e13440d3 100644 --- a/packages/devkit/src/project-graph/project-graph-builder.ts +++ b/packages/devkit/src/project-graph/project-graph-builder.ts @@ -1,4 +1,5 @@ import type { + FileData, ProjectGraph, ProjectGraphDependency, ProjectGraphExternalNode, @@ -110,7 +111,8 @@ export class ProjectGraphBuilder { addExplicitDependency( sourceProjectName: string, sourceProjectFile: string, - targetProjectName: string + targetProjectName: string, + dependencyType: DependencyType = DependencyType.static // TODO: Make this argument required ): void { if (sourceProjectName === targetProjectName) { return; @@ -127,9 +129,8 @@ export class ProjectGraphBuilder { throw new Error(`Target project does not exist: ${targetProjectName}`); } - const fileData = source.data.files.find( - (f) => f.file === sourceProjectFile - ); + const files = source.data.files as FileData[]; + const fileData = files.find((f) => f.file === sourceProjectFile); if (!fileData) { throw new Error( `Source project ${sourceProjectName} does not have a file: ${sourceProjectFile}` @@ -140,8 +141,19 @@ export class ProjectGraphBuilder { fileData.deps = []; } - if (!fileData.deps.find((t) => t === targetProjectName)) { - fileData.deps.push(targetProjectName); + const existingFileDep = fileData.deps.find( + (t) => t.projectName === targetProjectName + ); + if (existingFileDep) { + existingFileDep.dependencyType = this.getHigherPriorityDepType( + existingFileDep.dependencyType, + dependencyType + ); + } else { + fileData.deps.push({ + projectName: targetProjectName, + dependencyType, + }); } } @@ -153,54 +165,76 @@ export class ProjectGraphBuilder { } getUpdatedProjectGraph(): ProjectGraph { + const isRemoved = (sourceProject: string, targetProject: string) => + this.removedEdges[sourceProject] && + this.removedEdges[sourceProject].has(targetProject); for (const sourceProject of Object.keys(this.graph.nodes)) { - const alreadySetTargetProjects = - this.calculateAlreadySetTargetDeps(sourceProject); - this.graph.dependencies[sourceProject] = [ - ...alreadySetTargetProjects.values(), - ]; + const sourceProjectDepMap = new Map( + this.graph.dependencies[sourceProject] + .map((dep) => [dep.target, dep] as const) + .filter(([targetProject]) => !isRemoved(sourceProject, targetProject)) + ); const fileDeps = this.calculateTargetDepsFromFiles(sourceProject); - for (const targetProject of fileDeps) { - if (!alreadySetTargetProjects.has(targetProject)) { - if ( - !this.removedEdges[sourceProject] || - !this.removedEdges[sourceProject].has(targetProject) - ) { - this.graph.dependencies[sourceProject].push({ - source: sourceProject, - target: targetProject, - type: DependencyType.static, - }); - } + for (const [targetProject, targetProjectDepType] of fileDeps) { + if (sourceProjectDepMap.has(targetProject)) { + const existingDep = sourceProjectDepMap.get(targetProject); + existingDep.type = this.getHigherPriorityDepType( + existingDep.type, + targetProjectDepType + ); + } else if (!isRemoved(sourceProject, targetProject)) { + sourceProjectDepMap.set(targetProject, { + source: sourceProject, + target: targetProject, + type: targetProjectDepType, + }); } } + + this.graph.dependencies[sourceProject] = [ + ...sourceProjectDepMap.values(), + ]; } return this.graph; } private calculateTargetDepsFromFiles(sourceProject: string) { - const fileDeps = new Set(); - const files = this.graph.nodes[sourceProject].data.files; + const fileDeps = new Map(); + const files: FileData[] = this.graph.nodes[sourceProject].data.files; if (!files) return fileDeps; for (let f of files) { if (f.deps) { for (let p of f.deps) { - fileDeps.add(p); + if (fileDeps.has(p.projectName)) { + const existingDepType = fileDeps.get(p.projectName); + const priorityDepType = this.getHigherPriorityDepType( + p.dependencyType, + existingDepType + ); + fileDeps.set(p.projectName, priorityDepType); + } else { + fileDeps.set(p.projectName, p.dependencyType); + } } } } return fileDeps; } - private calculateAlreadySetTargetDeps(sourceProject: string) { - const alreadySetTargetProjects = new Map(); - const removed = this.removedEdges[sourceProject]; - for (const d of this.graph.dependencies[sourceProject]) { - if (!removed || !removed.has(d.target)) { - alreadySetTargetProjects.set(d.target, d); + private getHigherPriorityDepType( + depTypeA: DependencyType, + depTypeB: DependencyType + ): DependencyType { + for (const priorityDepType of [ + DependencyType.implicit, + DependencyType.static, + DependencyType.dynamic, + DependencyType.typeOnly, + ]) { + if (depTypeA === priorityDepType || depTypeB === priorityDepType) { + return priorityDepType; } } - return alreadySetTargetProjects; } } diff --git a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts index 9768cd19fb..89e1853045 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts @@ -1661,7 +1661,16 @@ linter.defineParser('@typescript-eslint/parser', parser); linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries); function createFile(f: string, deps?: string[]): FileData { - return { file: f, hash: '', ...(deps && { deps }) }; + return { + file: f, + hash: '', + ...(deps && { + deps: deps.map((dep) => ({ + projectName: dep, + dependencyType: DependencyType.static, + })), + }), + }; } function runRule( diff --git a/packages/node/src/executors/package/package.impl.spec.ts b/packages/node/src/executors/package/package.impl.spec.ts index e60cd07e12..262b643b38 100644 --- a/packages/node/src/executors/package/package.impl.spec.ts +++ b/packages/node/src/executors/package/package.impl.spec.ts @@ -1,4 +1,4 @@ -import { ExecutorContext } from '@nrwl/devkit'; +import { DependencyType, ExecutorContext } from '@nrwl/devkit'; import { join } from 'path'; import { mocked } from 'ts-jest/utils'; @@ -309,7 +309,7 @@ describe('NodePackageBuilder', () => { dependencies: { nodelib: [ { - type: ProjectType.lib, + type: DependencyType.static, target: 'nodelib-child', source: null, }, diff --git a/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts b/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts index 440a43d405..d2e565713e 100644 --- a/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts +++ b/packages/react-native/src/utils/find-all-npm-dependencies.spec.ts @@ -1,5 +1,5 @@ import { findAllNpmDependencies } from './find-all-npm-dependencies'; -import { ProjectGraph } from '@nrwl/devkit'; +import { DependencyType, ProjectGraph } from '@nrwl/devkit'; test('findAllNpmDependencies', () => { const graph: ProjectGraph = { @@ -61,26 +61,34 @@ test('findAllNpmDependencies', () => { }, dependencies: { myapp: [ - { type: 'static', source: 'myapp', target: 'lib1' }, - { type: 'static', source: 'myapp', target: 'lib2' }, + { type: DependencyType.static, source: 'myapp', target: 'lib1' }, + { type: DependencyType.static, source: 'myapp', target: 'lib2' }, { - type: 'static', + type: DependencyType.static, source: 'myapp', target: 'npm:react-native-image-picker', }, { - type: 'static', + type: DependencyType.static, source: 'myapp', target: 'npm:@nrwl/react-native', }, ], lib1: [ - { type: 'static', source: 'lib1', target: 'lib2' }, - { type: 'static', source: 'lib3', target: 'npm:react-native-snackbar' }, + { type: DependencyType.static, source: 'lib1', target: 'lib2' }, + { + type: DependencyType.static, + source: 'lib3', + target: 'npm:react-native-snackbar', + }, ], - lib2: [{ type: 'static', source: 'lib2', target: 'lib3' }], + lib2: [{ type: DependencyType.static, source: 'lib2', target: 'lib3' }], lib3: [ - { type: 'static', source: 'lib3', target: 'npm:react-native-dialog' }, + { + type: DependencyType.static, + source: 'lib3', + target: 'npm:react-native-dialog', + }, ], }, }; diff --git a/packages/tao/src/shared/project-graph.ts b/packages/tao/src/shared/project-graph.ts index ebc03b7e1e..4a8ae5c3f0 100644 --- a/packages/tao/src/shared/project-graph.ts +++ b/packages/tao/src/shared/project-graph.ts @@ -11,7 +11,19 @@ export interface FileData { hash: string; /** @deprecated this field will be removed in v13. Use {@link path.extname} to parse extension */ ext?: string; - deps?: string[]; + deps?: FileDependency[]; +} + +export interface FileDependency { + projectName: string; + dependencyType: DependencyType; +} + +export interface ProjectGraphBuilderExplicitDependency { + sourceProjectName: string; + targetProjectName: string; + sourceProjectFile: string; + dependencyType: DependencyType; } /** @@ -49,6 +61,10 @@ export enum DependencyType { * Implicit dependencies are inferred */ implicit = 'implicit', + /** + * Type-only dependencies are those of the form `import type ...` + */ + typeOnly = 'typeOnly', } /** @@ -108,7 +124,7 @@ export interface ProjectGraphExternalNode { * A dependency between two projects */ export interface ProjectGraphDependency { - type: DependencyType | string; + type: DependencyType; /** * The project being imported by the other */ diff --git a/packages/workspace/src/core/hasher/hasher.spec.ts b/packages/workspace/src/core/hasher/hasher.spec.ts index facc0e25cf..65457da9c2 100644 --- a/packages/workspace/src/core/hasher/hasher.spec.ts +++ b/packages/workspace/src/core/hasher/hasher.spec.ts @@ -246,7 +246,9 @@ describe('Hasher', () => { }, }, dependencies: { - parent: [{ source: 'parent', target: 'child', type: 'static' }], + parent: [ + { source: 'parent', target: 'child', type: DependencyType.static }, + ], }, }, {} as any, @@ -343,8 +345,12 @@ describe('Hasher', () => { }, }, dependencies: { - parent: [{ source: 'parent', target: 'child', type: 'static' }], - child: [{ source: 'child', target: 'parent', type: 'static' }], + parent: [ + { source: 'parent', target: 'child', type: DependencyType.static }, + ], + child: [ + { source: 'child', target: 'parent', type: DependencyType.static }, + ], }, }, {} as any, diff --git a/packages/workspace/src/core/project-graph/build-dependencies/build-explicit-typescript-and-package-json-dependencies.ts b/packages/workspace/src/core/project-graph/build-dependencies/build-explicit-typescript-and-package-json-dependencies.ts index b4c50f43b3..90750bf00e 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/build-explicit-typescript-and-package-json-dependencies.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/build-explicit-typescript-and-package-json-dependencies.ts @@ -1,4 +1,9 @@ -import { ProjectFileMap, ProjectGraph, Workspace } from '@nrwl/devkit'; +import { + ProjectFileMap, + ProjectGraph, + ProjectGraphBuilderExplicitDependency, + Workspace, +} from '@nrwl/devkit'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies'; @@ -10,7 +15,7 @@ export function buildExplicitTypescriptAndPackageJsonDependencies( workspace: Workspace, projectGraph: ProjectGraph, filesToProcess: ProjectFileMap -) { +): ProjectGraphBuilderExplicitDependency[] { let res = []; if ( jsPluginConfig.analyzeSourceFiles === undefined || diff --git a/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts b/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts index 5dda36f4ee..98998f6855 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.spec.ts @@ -2,6 +2,7 @@ import { buildExplicitPackageJsonDependencies } from './explicit-package-json-de import { vol } from 'memfs'; import { ProjectGraphNode } from '../project-graph-models'; import { + DependencyType, ProjectGraphBuilder, ProjectGraphProcessorContext, } from '@nrwl/devkit'; @@ -125,16 +126,19 @@ describe('explicit package json dependencies', () => { sourceProjectName: 'proj', targetProjectName: 'proj2', sourceProjectFile: 'libs/proj/package.json', + dependencyType: DependencyType.static, }, { sourceProjectFile: 'libs/proj/package.json', sourceProjectName: 'proj', targetProjectName: 'npm:external', + dependencyType: DependencyType.static, }, { sourceProjectName: 'proj', targetProjectName: 'proj3', sourceProjectFile: 'libs/proj/package.json', + dependencyType: DependencyType.static, }, ]); }); diff --git a/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.ts b/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.ts index ed32e260bb..0828e01d34 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/explicit-package-json-dependencies.ts @@ -1,9 +1,11 @@ import { ProjectGraph, ProjectGraphNodeRecords } from '../project-graph-models'; import { defaultFileRead } from '../../file-utils'; import { + DependencyType, joinPathFragments, parseJson, ProjectFileMap, + ProjectGraphBuilderExplicitDependency, Workspace, } from '@nrwl/devkit'; import { join } from 'path'; @@ -12,8 +14,8 @@ export function buildExplicitPackageJsonDependencies( workspace: Workspace, graph: ProjectGraph, filesToProcess: ProjectFileMap -) { - const res = [] as any; +): ProjectGraphBuilderExplicitDependency[] { + const res: ProjectGraphBuilderExplicitDependency[] = []; let packageNameMap = undefined; Object.keys(filesToProcess).forEach((source) => { Object.values(filesToProcess[source]).forEach((f) => { @@ -55,7 +57,7 @@ function processPackageJson( sourceProject: string, fileName: string, graph: ProjectGraph, - collectedDeps: any[], + collectedDeps: ProjectGraphBuilderExplicitDependency[], packageNameMap: { [packageName: string]: string } ) { try { @@ -68,12 +70,14 @@ function processPackageJson( sourceProjectName: sourceProject, targetProjectName: packageNameMap[d], sourceProjectFile: fileName, + dependencyType: DependencyType.static, }); } else if (graph.externalNodes[`npm:${d}`]) { collectedDeps.push({ sourceProjectName: sourceProject, targetProjectName: `npm:${d}`, sourceProjectFile: fileName, + dependencyType: DependencyType.static, }); } }); 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 index 7228829862..5b18802b94 100644 --- 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 @@ -9,6 +9,7 @@ import { vol } from 'memfs'; import { ProjectGraphNode } from '../project-graph-models'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { + DependencyType, ProjectGraphBuilder, ProjectGraphProcessorContext, } from '@nrwl/devkit'; @@ -201,21 +202,25 @@ describe('explicit project dependencies', () => { sourceProjectFile: 'libs/proj1234/index.ts', sourceProjectName: 'proj1234', targetProjectName: 'proj1234-child', + dependencyType: DependencyType.static, }, { sourceProjectFile: 'libs/proj/index.ts', sourceProjectName: 'proj', targetProjectName: 'proj2', + dependencyType: DependencyType.static, }, { sourceProjectFile: 'libs/proj/index.ts', sourceProjectName: 'proj', targetProjectName: 'proj3a', + dependencyType: DependencyType.dynamic, }, { sourceProjectFile: 'libs/proj/index.ts', sourceProjectName: 'proj', targetProjectName: 'proj4ab', + dependencyType: 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 7b49d2d5bb..07b489d5d0 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 @@ -3,8 +3,7 @@ import { TypeScriptImportLocator } from './typescript-import-locator'; import { TargetProjectLocator } from '../../target-project-locator'; import { ProjectFileMap, - ProjectGraphBuilder, - ProjectGraphProcessorContext, + ProjectGraphBuilderExplicitDependency, Workspace, } from '@nrwl/devkit'; @@ -12,13 +11,13 @@ export function buildExplicitTypeScriptDependencies( workspace: Workspace, graph: ProjectGraph, filesToProcess: ProjectFileMap -) { +): ProjectGraphBuilderExplicitDependency[] { const importLocator = new TypeScriptImportLocator(); const targetProjectLocator = new TargetProjectLocator( graph.nodes, graph.externalNodes ); - const res = [] as any; + const res: ProjectGraphBuilderExplicitDependency[] = []; Object.keys(filesToProcess).forEach((source) => { Object.values(filesToProcess[source]).forEach((f) => { importLocator.fromFile( @@ -34,6 +33,7 @@ export function buildExplicitTypeScriptDependencies( sourceProjectName: source, targetProjectName: target, sourceProjectFile: f.file, + dependencyType: type, }); } } diff --git a/packages/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.ts b/packages/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.ts index 89ab49303b..51640da705 100644 --- a/packages/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.ts +++ b/packages/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.ts @@ -4,7 +4,7 @@ import { DependencyType } from '@nrwl/devkit'; import { stripSourceCode } from '../../../utilities/strip-source-code'; import { defaultFileRead } from '../../file-utils'; -let tsModule: any; +let tsModule: typeof ts; export class TypeScriptImportLocator { private readonly scanner: ts.Scanner; @@ -46,7 +46,7 @@ export class TypeScriptImportLocator { fromNode( filePath: string, - node: any, + node: ts.Node, visitor: ( importExpr: string, filePath: string, @@ -59,7 +59,15 @@ export class TypeScriptImportLocator { ) { if (!this.ignoreStatement(node)) { const imp = this.getStringLiteralValue(node.moduleSpecifier); - visitor(imp, filePath, DependencyType.static); + const isTypeOnly = + (tsModule.isImportDeclaration(node) && + node.importClause?.isTypeOnly) || + (tsModule.isExportDeclaration(node) && node.isTypeOnly); + visitor( + imp, + filePath, + isTypeOnly ? DependencyType.typeOnly : DependencyType.static + ); } return; // stop traversing downwards } diff --git a/packages/workspace/src/core/project-graph/build-project-graph.spec.ts b/packages/workspace/src/core/project-graph/build-project-graph.spec.ts index b0cdbcb16f..7163fa630e 100644 --- a/packages/workspace/src/core/project-graph/build-project-graph.spec.ts +++ b/packages/workspace/src/core/project-graph/build-project-graph.spec.ts @@ -72,6 +72,12 @@ describe('project graph', () => { projectType: 'library', targets: {}, }, + 'types-lib': { + root: 'libs/types-lib', + sourceRoot: 'libs/types-lib', + projectType: 'library', + targets: {}, + }, api: { root: 'apps/api/', sourceRoot: 'apps/api/src', @@ -98,6 +104,7 @@ describe('project graph', () => { '@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'], + '@nrwl/types-lib': ['libs/types-lib/src/index.ts'], }, }, }; @@ -108,6 +115,7 @@ describe('project graph', () => { './apps/demo/src/index.ts': stripIndents` import * as ui from '@nrwl/ui'; import * as data from '@nrwl/shared-util-data; + import type { MyType } from '@nrwl/types-lib; const s = { loadChildren: '@nrwl/lazy-lib#LAZY' } `, './apps/demo-e2e/src/integration/app.spec.ts': stripIndents` @@ -126,6 +134,9 @@ describe('project graph', () => { './libs/lazy-lib/src/index.ts': stripIndents` export const LAZY = 'lazy lib'; `, + './libs/types-lib/src/index.ts': stripIndents` + export type MyType = {}; + `, './package.json': JSON.stringify(packageJson), './nx.json': JSON.stringify(nxJson), './workspace.json': JSON.stringify(workspaceJson), @@ -142,7 +153,9 @@ describe('project graph', () => { fail('Invalid tsconfigs should cause project graph to throw error'); } catch (e) { expect(e.message).toMatchInlineSnapshot( - `"InvalidSymbol in /root/tsconfig.base.json at position 247"` + `"InvalidSymbol in /root/tsconfig.base.json at position ${ + JSON.stringify(tsConfigJson).length + }"` ); } }); @@ -176,23 +189,35 @@ describe('project graph', () => { }, }); expect(graph.dependencies).toEqual({ - api: [{ source: 'api', target: 'npm:express', type: 'static' }], + api: [ + { source: 'api', target: 'npm:express', type: DependencyType.static }, + ], demo: [ - { source: 'demo', target: 'api', type: 'implicit' }, + { source: 'demo', target: 'api', type: DependencyType.implicit }, { source: 'demo', target: 'ui', - type: 'static', + type: DependencyType.static, + }, + { + source: 'demo', + target: 'shared-util-data', + type: DependencyType.static, + }, + { + source: 'demo', + target: 'types-lib', + type: DependencyType.typeOnly, }, - { source: 'demo', target: 'shared-util-data', type: 'static' }, { source: 'demo', target: 'lazy-lib', - type: 'static', + type: DependencyType.dynamic, }, ], 'demo-e2e': [], 'lazy-lib': [], + 'types-lib': [], 'shared-util': [ { source: 'shared-util', target: 'npm:happy-nrwl', type: 'static' }, ], @@ -202,7 +227,7 @@ describe('project graph', () => { { source: 'ui', target: 'lazy-lib', - type: 'static', + type: 'dynamic', }, ], }); @@ -244,7 +269,7 @@ describe('project graph', () => { target: 'shared-util', }, { - type: DependencyType.static, + type: DependencyType.dynamic, source: 'ui', target: 'lazy-lib', }, diff --git a/packages/workspace/src/core/project-graph/build-project-graph.ts b/packages/workspace/src/core/project-graph/build-project-graph.ts index 41a8c1433d..f77b8b5a19 100644 --- a/packages/workspace/src/core/project-graph/build-project-graph.ts +++ b/packages/workspace/src/core/project-graph/build-project-graph.ts @@ -8,6 +8,7 @@ import { ProjectFileMap, ProjectGraph, ProjectGraphBuilder, + ProjectGraphBuilderExplicitDependency, ProjectGraphProcessorContext, readJsonFile, WorkspaceJsonConfiguration, @@ -280,7 +281,8 @@ function buildExplicitDependenciesWithoutWorkers( builder.addExplicitDependency( r.sourceProjectName, r.sourceProjectFile, - r.targetProjectName + r.targetProjectName, + r.dependencyType ); }); } @@ -306,13 +308,16 @@ function buildExplicitDependenciesUsingWorkers( return new Promise((res, reject) => { for (let w of workers) { w.on('message', (explicitDependencies) => { - explicitDependencies.forEach((r) => { - builder.addExplicitDependency( - r.sourceProjectName, - r.sourceProjectFile, - r.targetProjectName - ); - }); + explicitDependencies.forEach( + (r: ProjectGraphBuilderExplicitDependency) => { + builder.addExplicitDependency( + r.sourceProjectName, + r.sourceProjectFile, + r.targetProjectName, + r.dependencyType + ); + } + ); if (bins.length > 0) { w.postMessage({ filesToProcess: bins.shift() }); } diff --git a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts index a523559442..043e13566b 100644 --- a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts +++ b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts @@ -1170,7 +1170,16 @@ Circular file chain: }); function createFile(f: string, deps?: string[]): FileData { - return { file: f, hash: '', ...(deps && { deps }) }; + return { + file: f, + hash: '', + ...(deps && { + deps: deps.map((dep) => ({ + projectName: dep, + dependencyType: DependencyType.static, + })), + }), + }; } function runRule( diff --git a/packages/workspace/src/utilities/project-graph-utils.spec.ts b/packages/workspace/src/utilities/project-graph-utils.spec.ts index d866429af2..4b87c596ee 100644 --- a/packages/workspace/src/utilities/project-graph-utils.spec.ts +++ b/packages/workspace/src/utilities/project-graph-utils.spec.ts @@ -1,5 +1,5 @@ import { PackageJson } from '@nrwl/tao/src/shared/package-json'; -import { ProjectGraph } from '../core/project-graph'; +import { DependencyType, ProjectGraph } from '../core/project-graph'; import { getProjectNameFromDirPath, getSourceDirOfDependentProjects, @@ -52,17 +52,17 @@ describe('project graph utils', () => { dependencies: { 'demo-app': [ { - type: 'static', + type: DependencyType.static, source: 'demo-app', target: 'ui', }, { - type: 'static', + type: DependencyType.static, source: 'demo-app', target: 'npm:chalk', }, { - type: 'static', + type: DependencyType.static, source: 'demo-app', target: 'core', }, diff --git a/packages/workspace/src/utils/graph-utils.ts b/packages/workspace/src/utils/graph-utils.ts index 2846d32688..358a5e13a6 100644 --- a/packages/workspace/src/utils/graph-utils.ts +++ b/packages/workspace/src/utils/graph-utils.ts @@ -116,17 +116,19 @@ export function checkCircularPath( export function findFilesInCircularPath( circularPath: ProjectGraphNode[] ): Array { - const filePathChain = []; + const filePathChain: string[][] = []; for (let i = 0; i < circularPath.length - 1; i++) { const next = circularPath[i + 1].name; - const files: FileData[] = circularPath[i].data.files; + const files: Record = circularPath[i].data.files; filePathChain.push( Object.keys(files) .filter( - (key) => files[key].deps && files[key].deps.indexOf(next) !== -1 + (key) => + files[key].deps && + files[key].deps.some((dep) => dep.projectName === next) ) - .map((key) => files[key].file) + .map((key) => (files[key] as FileData).file) ); } diff --git a/packages/workspace/src/utils/runtime-lint-utils.ts b/packages/workspace/src/utils/runtime-lint-utils.ts index 212a161b95..d8140b29d9 100644 --- a/packages/workspace/src/utils/runtime-lint-utils.ts +++ b/packages/workspace/src/utils/runtime-lint-utils.ts @@ -254,7 +254,7 @@ export function mapProjectGraphFiles( projectGraph.nodes as Record ).forEach(([name, node]) => { const files: Record = {}; - node.data.files.forEach(({ file, hash, deps }) => { + node.data.files.forEach(({ file, hash, deps }: FileData) => { files[removeExt(file)] = { file, hash, ...(deps && { deps }) }; }); const data = { ...node.data, files };