feat(core): show dep types in dep graph (#2760) (#8132)

This commit is contained in:
MaximSagan 2022-01-19 04:20:09 +11:00 committed by GitHub
parent be8ce09ddb
commit 31bb2f3626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 342 additions and 113 deletions

View File

@ -1,5 +1,9 @@
// nx-ignore-next-line // 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 { depGraphMachine } from './dep-graph.machine';
import { interpret } from 'xstate'; import { interpret } from 'xstate';
@ -51,38 +55,38 @@ export const mockProjects: ProjectGraphNode[] = [
export const mockDependencies: Record<string, ProjectGraphDependency[]> = { export const mockDependencies: Record<string, ProjectGraphDependency[]> = {
app1: [ app1: [
{ {
type: 'static', type: DependencyType.static,
source: 'app1', source: 'app1',
target: 'auth-lib', target: 'auth-lib',
}, },
{ {
type: 'static', type: DependencyType.static,
source: 'app1', source: 'app1',
target: 'feature-lib1', target: 'feature-lib1',
}, },
], ],
app2: [ app2: [
{ {
type: 'static', type: DependencyType.static,
source: 'app2', source: 'app2',
target: 'auth-lib', target: 'auth-lib',
}, },
{ {
type: 'static', type: DependencyType.static,
source: 'app2', source: 'app2',
target: 'feature-lib2', target: 'feature-lib2',
}, },
], ],
'feature-lib1': [ 'feature-lib1': [
{ {
type: 'static', type: DependencyType.static,
source: 'feature-lib1', source: 'feature-lib1',
target: 'ui-lib', target: 'ui-lib',
}, },
], ],
'feature-lib2': [ 'feature-lib2': [
{ {
type: 'static', type: DependencyType.static,
source: 'feature-lib2', source: 'feature-lib2',
target: 'ui-lib', target: 'ui-lib',
}, },

View File

@ -33,7 +33,7 @@ export class MockProjectGraphService implements ProjectGraphService {
{ {
source: 'existing-app-1', source: 'existing-app-1',
target: 'existing-lib-1', target: 'existing-lib-1',
type: 'statis', type: 'static' as any,
}, },
], ],
'existing-lib-1': [], 'existing-lib-1': [],
@ -82,7 +82,7 @@ export class MockProjectGraphService implements ProjectGraphService {
{ {
source: newProject.name, source: newProject.name,
target: targetDependency.name, target: targetDependency.name,
type: 'static', type: 'static' as any,
}, },
]; ];

View File

@ -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[] = [ export const edgeStyles: Stylesheet[] = [
allEdges, allEdges,
affectedEdges, affectedEdges,
implicitEdges, implicitEdges,
dynamicEdges, dynamicEdges,
typeOnlyEdges,
]; ];

View File

@ -185,7 +185,7 @@
{ {
"source": "shared-product-data", "source": "shared-product-data",
"target": "shared-product-types", "target": "shared-product-types",
"type": "static" "type": "typeOnly"
} }
], ],
"products-home-page": [ "products-home-page": [

View File

@ -36,6 +36,7 @@ It only uses language primitives and immutable objects
- [FileData](../../generated/nx-devkit/index#filedata) - [FileData](../../generated/nx-devkit/index#filedata)
- [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap) - [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap)
- [ProjectGraph](../../generated/nx-devkit/index#projectgraph) - [ProjectGraph](../../generated/nx-devkit/index#projectgraph)
- [ProjectGraphBuilderExplicitDependency](../../generated/nx-devkit/index#projectgraphbuilderexplicitdependency)
- [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency) - [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency)
- [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode) - [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode)
- [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext) - [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext)
@ -210,6 +211,12 @@ A plugin for Nx
--- ---
### ProjectGraphBuilderExplicitDependency
**ProjectGraphBuilderExplicitDependency**: `Object`
---
### ProjectGraphDependency ### ProjectGraphDependency
**ProjectGraphDependency**: `Object` **ProjectGraphDependency**: `Object`

View File

@ -273,7 +273,7 @@ describe('dep-graph', () => {
target: mylib, target: mylib,
type: 'static', type: 'static',
}, },
{ source: myapp, target: mylib2, type: 'static' }, { source: myapp, target: mylib2, type: 'dynamic' },
], ],
[myappE2e]: [ [myappE2e]: [
{ {

View File

@ -36,6 +36,7 @@ It only uses language primitives and immutable objects
- [FileData](../../generated/nx-devkit/index#filedata) - [FileData](../../generated/nx-devkit/index#filedata)
- [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap) - [ProjectFileMap](../../generated/nx-devkit/index#projectfilemap)
- [ProjectGraph](../../generated/nx-devkit/index#projectgraph) - [ProjectGraph](../../generated/nx-devkit/index#projectgraph)
- [ProjectGraphBuilderExplicitDependency](../../generated/nx-devkit/index#projectgraphbuilderexplicitdependency)
- [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency) - [ProjectGraphDependency](../../generated/nx-devkit/index#projectgraphdependency)
- [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode) - [ProjectGraphExternalNode](../../generated/nx-devkit/index#projectgraphexternalnode)
- [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext) - [ProjectGraphProcessorContext](../../generated/nx-devkit/index#projectgraphprocessorcontext)
@ -210,6 +211,12 @@ A plugin for Nx
--- ---
### ProjectGraphBuilderExplicitDependency
**ProjectGraphBuilderExplicitDependency**: `Object`
---
### ProjectGraphDependency ### ProjectGraphDependency
**ProjectGraphDependency**: `Object` **ProjectGraphDependency**: `Object`

View File

@ -144,6 +144,7 @@ export type {
ProjectFileMap, ProjectFileMap,
FileData, FileData,
ProjectGraph, ProjectGraph,
ProjectGraphBuilderExplicitDependency,
ProjectGraphDependency, ProjectGraphDependency,
ProjectGraphNode, ProjectGraphNode,
ProjectGraphProjectNode, ProjectGraphProjectNode,

View File

@ -1,3 +1,4 @@
import { DependencyType } from './interfaces';
import { ProjectGraphBuilder } from './project-graph-builder'; import { ProjectGraphBuilder } from './project-graph-builder';
describe('ProjectGraphBuilder', () => { describe('ProjectGraphBuilder', () => {
@ -96,6 +97,7 @@ describe('ProjectGraphBuilder', () => {
}); });
}); });
describe('dependency type priority', () => {
it(`should use implicit dep when both implicit and explicit deps are available`, () => { it(`should use implicit dep when both implicit and explicit deps are available`, () => {
// don't include duplicates // don't include duplicates
builder.addImplicitDependency('source', 'target'); builder.addImplicitDependency('source', 'target');
@ -114,6 +116,61 @@ describe('ProjectGraphBuilder', () => {
}); });
}); });
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: [],
});
});
});
it(`remove dependency`, () => { it(`remove dependency`, () => {
builder.addNode({ builder.addNode({
name: 'target2', name: 'target2',

View File

@ -1,4 +1,5 @@
import type { import type {
FileData,
ProjectGraph, ProjectGraph,
ProjectGraphDependency, ProjectGraphDependency,
ProjectGraphExternalNode, ProjectGraphExternalNode,
@ -110,7 +111,8 @@ export class ProjectGraphBuilder {
addExplicitDependency( addExplicitDependency(
sourceProjectName: string, sourceProjectName: string,
sourceProjectFile: string, sourceProjectFile: string,
targetProjectName: string targetProjectName: string,
dependencyType: DependencyType = DependencyType.static // TODO: Make this argument required
): void { ): void {
if (sourceProjectName === targetProjectName) { if (sourceProjectName === targetProjectName) {
return; return;
@ -127,9 +129,8 @@ export class ProjectGraphBuilder {
throw new Error(`Target project does not exist: ${targetProjectName}`); throw new Error(`Target project does not exist: ${targetProjectName}`);
} }
const fileData = source.data.files.find( const files = source.data.files as FileData[];
(f) => f.file === sourceProjectFile const fileData = files.find((f) => f.file === sourceProjectFile);
);
if (!fileData) { if (!fileData) {
throw new Error( throw new Error(
`Source project ${sourceProjectName} does not have a file: ${sourceProjectFile}` `Source project ${sourceProjectName} does not have a file: ${sourceProjectFile}`
@ -140,8 +141,19 @@ export class ProjectGraphBuilder {
fileData.deps = []; fileData.deps = [];
} }
if (!fileData.deps.find((t) => t === targetProjectName)) { const existingFileDep = fileData.deps.find(
fileData.deps.push(targetProjectName); (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 { 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)) { for (const sourceProject of Object.keys(this.graph.nodes)) {
const alreadySetTargetProjects = const sourceProjectDepMap = new Map<string, ProjectGraphDependency>(
this.calculateAlreadySetTargetDeps(sourceProject); this.graph.dependencies[sourceProject]
this.graph.dependencies[sourceProject] = [ .map((dep) => [dep.target, dep] as const)
...alreadySetTargetProjects.values(), .filter(([targetProject]) => !isRemoved(sourceProject, targetProject))
]; );
const fileDeps = this.calculateTargetDepsFromFiles(sourceProject); const fileDeps = this.calculateTargetDepsFromFiles(sourceProject);
for (const targetProject of fileDeps) { for (const [targetProject, targetProjectDepType] of fileDeps) {
if (!alreadySetTargetProjects.has(targetProject)) { if (sourceProjectDepMap.has(targetProject)) {
if ( const existingDep = sourceProjectDepMap.get(targetProject);
!this.removedEdges[sourceProject] || existingDep.type = this.getHigherPriorityDepType(
!this.removedEdges[sourceProject].has(targetProject) existingDep.type,
) { targetProjectDepType
this.graph.dependencies[sourceProject].push({ );
} else if (!isRemoved(sourceProject, targetProject)) {
sourceProjectDepMap.set(targetProject, {
source: sourceProject, source: sourceProject,
target: targetProject, target: targetProject,
type: DependencyType.static, type: targetProjectDepType,
}); });
} }
} }
}
this.graph.dependencies[sourceProject] = [
...sourceProjectDepMap.values(),
];
} }
return this.graph; return this.graph;
} }
private calculateTargetDepsFromFiles(sourceProject: string) { private calculateTargetDepsFromFiles(sourceProject: string) {
const fileDeps = new Set<string>(); const fileDeps = new Map<string, DependencyType>();
const files = this.graph.nodes[sourceProject].data.files; const files: FileData[] = this.graph.nodes[sourceProject].data.files;
if (!files) return fileDeps; if (!files) return fileDeps;
for (let f of files) { for (let f of files) {
if (f.deps) { if (f.deps) {
for (let p of 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; return fileDeps;
} }
private calculateAlreadySetTargetDeps(sourceProject: string) { private getHigherPriorityDepType(
const alreadySetTargetProjects = new Map<string, ProjectGraphDependency>(); depTypeA: DependencyType,
const removed = this.removedEdges[sourceProject]; depTypeB: DependencyType
for (const d of this.graph.dependencies[sourceProject]) { ): DependencyType {
if (!removed || !removed.has(d.target)) { for (const priorityDepType of [
alreadySetTargetProjects.set(d.target, d); DependencyType.implicit,
DependencyType.static,
DependencyType.dynamic,
DependencyType.typeOnly,
]) {
if (depTypeA === priorityDepType || depTypeB === priorityDepType) {
return priorityDepType;
} }
} }
return alreadySetTargetProjects;
} }
} }

View File

@ -1661,7 +1661,16 @@ linter.defineParser('@typescript-eslint/parser', parser);
linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries); linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries);
function createFile(f: string, deps?: string[]): FileData { 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( function runRule(

View File

@ -1,4 +1,4 @@
import { ExecutorContext } from '@nrwl/devkit'; import { DependencyType, ExecutorContext } from '@nrwl/devkit';
import { join } from 'path'; import { join } from 'path';
import { mocked } from 'ts-jest/utils'; import { mocked } from 'ts-jest/utils';
@ -309,7 +309,7 @@ describe('NodePackageBuilder', () => {
dependencies: { dependencies: {
nodelib: [ nodelib: [
{ {
type: ProjectType.lib, type: DependencyType.static,
target: 'nodelib-child', target: 'nodelib-child',
source: null, source: null,
}, },

View File

@ -1,5 +1,5 @@
import { findAllNpmDependencies } from './find-all-npm-dependencies'; import { findAllNpmDependencies } from './find-all-npm-dependencies';
import { ProjectGraph } from '@nrwl/devkit'; import { DependencyType, ProjectGraph } from '@nrwl/devkit';
test('findAllNpmDependencies', () => { test('findAllNpmDependencies', () => {
const graph: ProjectGraph = { const graph: ProjectGraph = {
@ -61,26 +61,34 @@ test('findAllNpmDependencies', () => {
}, },
dependencies: { dependencies: {
myapp: [ myapp: [
{ type: 'static', source: 'myapp', target: 'lib1' }, { type: DependencyType.static, source: 'myapp', target: 'lib1' },
{ type: 'static', source: 'myapp', target: 'lib2' }, { type: DependencyType.static, source: 'myapp', target: 'lib2' },
{ {
type: 'static', type: DependencyType.static,
source: 'myapp', source: 'myapp',
target: 'npm:react-native-image-picker', target: 'npm:react-native-image-picker',
}, },
{ {
type: 'static', type: DependencyType.static,
source: 'myapp', source: 'myapp',
target: 'npm:@nrwl/react-native', target: 'npm:@nrwl/react-native',
}, },
], ],
lib1: [ lib1: [
{ type: 'static', source: 'lib1', target: 'lib2' }, { type: DependencyType.static, source: 'lib1', target: 'lib2' },
{ type: 'static', source: 'lib3', target: 'npm:react-native-snackbar' }, {
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: [ lib3: [
{ type: 'static', source: 'lib3', target: 'npm:react-native-dialog' }, {
type: DependencyType.static,
source: 'lib3',
target: 'npm:react-native-dialog',
},
], ],
}, },
}; };

View File

@ -11,7 +11,19 @@ export interface FileData {
hash: string; hash: string;
/** @deprecated this field will be removed in v13. Use {@link path.extname} to parse extension */ /** @deprecated this field will be removed in v13. Use {@link path.extname} to parse extension */
ext?: string; 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 dependencies are inferred
*/ */
implicit = 'implicit', 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 * A dependency between two projects
*/ */
export interface ProjectGraphDependency { export interface ProjectGraphDependency {
type: DependencyType | string; type: DependencyType;
/** /**
* The project being imported by the other * The project being imported by the other
*/ */

View File

@ -246,7 +246,9 @@ describe('Hasher', () => {
}, },
}, },
dependencies: { dependencies: {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [
{ source: 'parent', target: 'child', type: DependencyType.static },
],
}, },
}, },
{} as any, {} as any,
@ -343,8 +345,12 @@ describe('Hasher', () => {
}, },
}, },
dependencies: { dependencies: {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [
child: [{ source: 'child', target: 'parent', type: 'static' }], { source: 'parent', target: 'child', type: DependencyType.static },
],
child: [
{ source: 'child', target: 'parent', type: DependencyType.static },
],
}, },
}, },
{} as any, {} as any,

View File

@ -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 { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies'; import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies';
@ -10,7 +15,7 @@ export function buildExplicitTypescriptAndPackageJsonDependencies(
workspace: Workspace, workspace: Workspace,
projectGraph: ProjectGraph, projectGraph: ProjectGraph,
filesToProcess: ProjectFileMap filesToProcess: ProjectFileMap
) { ): ProjectGraphBuilderExplicitDependency[] {
let res = []; let res = [];
if ( if (
jsPluginConfig.analyzeSourceFiles === undefined || jsPluginConfig.analyzeSourceFiles === undefined ||

View File

@ -2,6 +2,7 @@ import { buildExplicitPackageJsonDependencies } from './explicit-package-json-de
import { vol } from 'memfs'; import { vol } from 'memfs';
import { ProjectGraphNode } from '../project-graph-models'; import { ProjectGraphNode } from '../project-graph-models';
import { import {
DependencyType,
ProjectGraphBuilder, ProjectGraphBuilder,
ProjectGraphProcessorContext, ProjectGraphProcessorContext,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
@ -125,16 +126,19 @@ describe('explicit package json dependencies', () => {
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'proj2', targetProjectName: 'proj2',
sourceProjectFile: 'libs/proj/package.json', sourceProjectFile: 'libs/proj/package.json',
dependencyType: DependencyType.static,
}, },
{ {
sourceProjectFile: 'libs/proj/package.json', sourceProjectFile: 'libs/proj/package.json',
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'npm:external', targetProjectName: 'npm:external',
dependencyType: DependencyType.static,
}, },
{ {
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'proj3', targetProjectName: 'proj3',
sourceProjectFile: 'libs/proj/package.json', sourceProjectFile: 'libs/proj/package.json',
dependencyType: DependencyType.static,
}, },
]); ]);
}); });

View File

@ -1,9 +1,11 @@
import { ProjectGraph, ProjectGraphNodeRecords } from '../project-graph-models'; import { ProjectGraph, ProjectGraphNodeRecords } from '../project-graph-models';
import { defaultFileRead } from '../../file-utils'; import { defaultFileRead } from '../../file-utils';
import { import {
DependencyType,
joinPathFragments, joinPathFragments,
parseJson, parseJson,
ProjectFileMap, ProjectFileMap,
ProjectGraphBuilderExplicitDependency,
Workspace, Workspace,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { join } from 'path'; import { join } from 'path';
@ -12,8 +14,8 @@ export function buildExplicitPackageJsonDependencies(
workspace: Workspace, workspace: Workspace,
graph: ProjectGraph, graph: ProjectGraph,
filesToProcess: ProjectFileMap filesToProcess: ProjectFileMap
) { ): ProjectGraphBuilderExplicitDependency[] {
const res = [] as any; const res: ProjectGraphBuilderExplicitDependency[] = [];
let packageNameMap = undefined; let packageNameMap = undefined;
Object.keys(filesToProcess).forEach((source) => { Object.keys(filesToProcess).forEach((source) => {
Object.values(filesToProcess[source]).forEach((f) => { Object.values(filesToProcess[source]).forEach((f) => {
@ -55,7 +57,7 @@ function processPackageJson(
sourceProject: string, sourceProject: string,
fileName: string, fileName: string,
graph: ProjectGraph, graph: ProjectGraph,
collectedDeps: any[], collectedDeps: ProjectGraphBuilderExplicitDependency[],
packageNameMap: { [packageName: string]: string } packageNameMap: { [packageName: string]: string }
) { ) {
try { try {
@ -68,12 +70,14 @@ function processPackageJson(
sourceProjectName: sourceProject, sourceProjectName: sourceProject,
targetProjectName: packageNameMap[d], targetProjectName: packageNameMap[d],
sourceProjectFile: fileName, sourceProjectFile: fileName,
dependencyType: DependencyType.static,
}); });
} else if (graph.externalNodes[`npm:${d}`]) { } else if (graph.externalNodes[`npm:${d}`]) {
collectedDeps.push({ collectedDeps.push({
sourceProjectName: sourceProject, sourceProjectName: sourceProject,
targetProjectName: `npm:${d}`, targetProjectName: `npm:${d}`,
sourceProjectFile: fileName, sourceProjectFile: fileName,
dependencyType: DependencyType.static,
}); });
} }
}); });

View File

@ -9,6 +9,7 @@ import { vol } from 'memfs';
import { ProjectGraphNode } from '../project-graph-models'; import { ProjectGraphNode } from '../project-graph-models';
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies'; import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import { import {
DependencyType,
ProjectGraphBuilder, ProjectGraphBuilder,
ProjectGraphProcessorContext, ProjectGraphProcessorContext,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
@ -201,21 +202,25 @@ describe('explicit project dependencies', () => {
sourceProjectFile: 'libs/proj1234/index.ts', sourceProjectFile: 'libs/proj1234/index.ts',
sourceProjectName: 'proj1234', sourceProjectName: 'proj1234',
targetProjectName: 'proj1234-child', targetProjectName: 'proj1234-child',
dependencyType: DependencyType.static,
}, },
{ {
sourceProjectFile: 'libs/proj/index.ts', sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'proj2', targetProjectName: 'proj2',
dependencyType: DependencyType.static,
}, },
{ {
sourceProjectFile: 'libs/proj/index.ts', sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'proj3a', targetProjectName: 'proj3a',
dependencyType: DependencyType.dynamic,
}, },
{ {
sourceProjectFile: 'libs/proj/index.ts', sourceProjectFile: 'libs/proj/index.ts',
sourceProjectName: 'proj', sourceProjectName: 'proj',
targetProjectName: 'proj4ab', targetProjectName: 'proj4ab',
dependencyType: DependencyType.dynamic,
}, },
]); ]);
}); });

View File

@ -3,8 +3,7 @@ import { TypeScriptImportLocator } from './typescript-import-locator';
import { TargetProjectLocator } from '../../target-project-locator'; import { TargetProjectLocator } from '../../target-project-locator';
import { import {
ProjectFileMap, ProjectFileMap,
ProjectGraphBuilder, ProjectGraphBuilderExplicitDependency,
ProjectGraphProcessorContext,
Workspace, Workspace,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
@ -12,13 +11,13 @@ export function buildExplicitTypeScriptDependencies(
workspace: Workspace, workspace: Workspace,
graph: ProjectGraph, graph: ProjectGraph,
filesToProcess: ProjectFileMap filesToProcess: ProjectFileMap
) { ): ProjectGraphBuilderExplicitDependency[] {
const importLocator = new TypeScriptImportLocator(); const importLocator = new TypeScriptImportLocator();
const targetProjectLocator = new TargetProjectLocator( const targetProjectLocator = new TargetProjectLocator(
graph.nodes, graph.nodes,
graph.externalNodes graph.externalNodes
); );
const res = [] as any; const res: ProjectGraphBuilderExplicitDependency[] = [];
Object.keys(filesToProcess).forEach((source) => { Object.keys(filesToProcess).forEach((source) => {
Object.values(filesToProcess[source]).forEach((f) => { Object.values(filesToProcess[source]).forEach((f) => {
importLocator.fromFile( importLocator.fromFile(
@ -34,6 +33,7 @@ export function buildExplicitTypeScriptDependencies(
sourceProjectName: source, sourceProjectName: source,
targetProjectName: target, targetProjectName: target,
sourceProjectFile: f.file, sourceProjectFile: f.file,
dependencyType: type,
}); });
} }
} }

View File

@ -4,7 +4,7 @@ import { DependencyType } from '@nrwl/devkit';
import { stripSourceCode } from '../../../utilities/strip-source-code'; import { stripSourceCode } from '../../../utilities/strip-source-code';
import { defaultFileRead } from '../../file-utils'; import { defaultFileRead } from '../../file-utils';
let tsModule: any; let tsModule: typeof ts;
export class TypeScriptImportLocator { export class TypeScriptImportLocator {
private readonly scanner: ts.Scanner; private readonly scanner: ts.Scanner;
@ -46,7 +46,7 @@ export class TypeScriptImportLocator {
fromNode( fromNode(
filePath: string, filePath: string,
node: any, node: ts.Node,
visitor: ( visitor: (
importExpr: string, importExpr: string,
filePath: string, filePath: string,
@ -59,7 +59,15 @@ export class TypeScriptImportLocator {
) { ) {
if (!this.ignoreStatement(node)) { if (!this.ignoreStatement(node)) {
const imp = this.getStringLiteralValue(node.moduleSpecifier); 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 return; // stop traversing downwards
} }

View File

@ -72,6 +72,12 @@ describe('project graph', () => {
projectType: 'library', projectType: 'library',
targets: {}, targets: {},
}, },
'types-lib': {
root: 'libs/types-lib',
sourceRoot: 'libs/types-lib',
projectType: 'library',
targets: {},
},
api: { api: {
root: 'apps/api/', root: 'apps/api/',
sourceRoot: 'apps/api/src', sourceRoot: 'apps/api/src',
@ -98,6 +104,7 @@ describe('project graph', () => {
'@nrwl/shared-util-data': ['libs/shared/util/data/src/index.ts'], '@nrwl/shared-util-data': ['libs/shared/util/data/src/index.ts'],
'@nrwl/ui': ['libs/ui/src/index.ts'], '@nrwl/ui': ['libs/ui/src/index.ts'],
'@nrwl/lazy-lib': ['libs/lazy-lib/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` './apps/demo/src/index.ts': stripIndents`
import * as ui from '@nrwl/ui'; import * as ui from '@nrwl/ui';
import * as data from '@nrwl/shared-util-data; import * as data from '@nrwl/shared-util-data;
import type { MyType } from '@nrwl/types-lib;
const s = { loadChildren: '@nrwl/lazy-lib#LAZY' } const s = { loadChildren: '@nrwl/lazy-lib#LAZY' }
`, `,
'./apps/demo-e2e/src/integration/app.spec.ts': stripIndents` './apps/demo-e2e/src/integration/app.spec.ts': stripIndents`
@ -126,6 +134,9 @@ describe('project graph', () => {
'./libs/lazy-lib/src/index.ts': stripIndents` './libs/lazy-lib/src/index.ts': stripIndents`
export const LAZY = 'lazy lib'; export const LAZY = 'lazy lib';
`, `,
'./libs/types-lib/src/index.ts': stripIndents`
export type MyType = {};
`,
'./package.json': JSON.stringify(packageJson), './package.json': JSON.stringify(packageJson),
'./nx.json': JSON.stringify(nxJson), './nx.json': JSON.stringify(nxJson),
'./workspace.json': JSON.stringify(workspaceJson), './workspace.json': JSON.stringify(workspaceJson),
@ -142,7 +153,9 @@ describe('project graph', () => {
fail('Invalid tsconfigs should cause project graph to throw error'); fail('Invalid tsconfigs should cause project graph to throw error');
} catch (e) { } catch (e) {
expect(e.message).toMatchInlineSnapshot( 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({ expect(graph.dependencies).toEqual({
api: [{ source: 'api', target: 'npm:express', type: 'static' }], api: [
{ source: 'api', target: 'npm:express', type: DependencyType.static },
],
demo: [ demo: [
{ source: 'demo', target: 'api', type: 'implicit' }, { source: 'demo', target: 'api', type: DependencyType.implicit },
{ {
source: 'demo', source: 'demo',
target: 'ui', 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', source: 'demo',
target: 'lazy-lib', target: 'lazy-lib',
type: 'static', type: DependencyType.dynamic,
}, },
], ],
'demo-e2e': [], 'demo-e2e': [],
'lazy-lib': [], 'lazy-lib': [],
'types-lib': [],
'shared-util': [ 'shared-util': [
{ source: 'shared-util', target: 'npm:happy-nrwl', type: 'static' }, { source: 'shared-util', target: 'npm:happy-nrwl', type: 'static' },
], ],
@ -202,7 +227,7 @@ describe('project graph', () => {
{ {
source: 'ui', source: 'ui',
target: 'lazy-lib', target: 'lazy-lib',
type: 'static', type: 'dynamic',
}, },
], ],
}); });
@ -244,7 +269,7 @@ describe('project graph', () => {
target: 'shared-util', target: 'shared-util',
}, },
{ {
type: DependencyType.static, type: DependencyType.dynamic,
source: 'ui', source: 'ui',
target: 'lazy-lib', target: 'lazy-lib',
}, },

View File

@ -8,6 +8,7 @@ import {
ProjectFileMap, ProjectFileMap,
ProjectGraph, ProjectGraph,
ProjectGraphBuilder, ProjectGraphBuilder,
ProjectGraphBuilderExplicitDependency,
ProjectGraphProcessorContext, ProjectGraphProcessorContext,
readJsonFile, readJsonFile,
WorkspaceJsonConfiguration, WorkspaceJsonConfiguration,
@ -280,7 +281,8 @@ function buildExplicitDependenciesWithoutWorkers(
builder.addExplicitDependency( builder.addExplicitDependency(
r.sourceProjectName, r.sourceProjectName,
r.sourceProjectFile, r.sourceProjectFile,
r.targetProjectName r.targetProjectName,
r.dependencyType
); );
}); });
} }
@ -306,13 +308,16 @@ function buildExplicitDependenciesUsingWorkers(
return new Promise((res, reject) => { return new Promise((res, reject) => {
for (let w of workers) { for (let w of workers) {
w.on('message', (explicitDependencies) => { w.on('message', (explicitDependencies) => {
explicitDependencies.forEach((r) => { explicitDependencies.forEach(
(r: ProjectGraphBuilderExplicitDependency) => {
builder.addExplicitDependency( builder.addExplicitDependency(
r.sourceProjectName, r.sourceProjectName,
r.sourceProjectFile, r.sourceProjectFile,
r.targetProjectName r.targetProjectName,
r.dependencyType
);
}
); );
});
if (bins.length > 0) { if (bins.length > 0) {
w.postMessage({ filesToProcess: bins.shift() }); w.postMessage({ filesToProcess: bins.shift() });
} }

View File

@ -1170,7 +1170,16 @@ Circular file chain:
}); });
function createFile(f: string, deps?: string[]): FileData { 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( function runRule(

View File

@ -1,5 +1,5 @@
import { PackageJson } from '@nrwl/tao/src/shared/package-json'; import { PackageJson } from '@nrwl/tao/src/shared/package-json';
import { ProjectGraph } from '../core/project-graph'; import { DependencyType, ProjectGraph } from '../core/project-graph';
import { import {
getProjectNameFromDirPath, getProjectNameFromDirPath,
getSourceDirOfDependentProjects, getSourceDirOfDependentProjects,
@ -52,17 +52,17 @@ describe('project graph utils', () => {
dependencies: { dependencies: {
'demo-app': [ 'demo-app': [
{ {
type: 'static', type: DependencyType.static,
source: 'demo-app', source: 'demo-app',
target: 'ui', target: 'ui',
}, },
{ {
type: 'static', type: DependencyType.static,
source: 'demo-app', source: 'demo-app',
target: 'npm:chalk', target: 'npm:chalk',
}, },
{ {
type: 'static', type: DependencyType.static,
source: 'demo-app', source: 'demo-app',
target: 'core', target: 'core',
}, },

View File

@ -116,17 +116,19 @@ export function checkCircularPath(
export function findFilesInCircularPath( export function findFilesInCircularPath(
circularPath: ProjectGraphNode[] circularPath: ProjectGraphNode[]
): Array<string[]> { ): Array<string[]> {
const filePathChain = []; const filePathChain: string[][] = [];
for (let i = 0; i < circularPath.length - 1; i++) { for (let i = 0; i < circularPath.length - 1; i++) {
const next = circularPath[i + 1].name; const next = circularPath[i + 1].name;
const files: FileData[] = circularPath[i].data.files; const files: Record<string, FileData> = circularPath[i].data.files;
filePathChain.push( filePathChain.push(
Object.keys(files) Object.keys(files)
.filter( .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)
); );
} }

View File

@ -254,7 +254,7 @@ export function mapProjectGraphFiles<T>(
projectGraph.nodes as Record<string, ProjectGraphProjectNode> projectGraph.nodes as Record<string, ProjectGraphProjectNode>
).forEach(([name, node]) => { ).forEach(([name, node]) => {
const files: Record<string, FileData> = {}; const files: Record<string, FileData> = {};
node.data.files.forEach(({ file, hash, deps }) => { node.data.files.forEach(({ file, hash, deps }: FileData) => {
files[removeExt(file)] = { file, hash, ...(deps && { deps }) }; files[removeExt(file)] = { file, hash, ...(deps && { deps }) };
}); });
const data = { ...node.data, files }; const data = { ...node.data, files };