feat(core): add full dependency information to project graph file dependencies (#14893)

This commit is contained in:
Miroslav Jonaš 2023-02-21 16:39:44 +01:00 committed by GitHub
parent 00828fd615
commit 8d4855de61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 562 additions and 237 deletions

View File

@ -48,7 +48,6 @@ It only uses language primitives and immutable objects
- [ProjectGraphExternalNode](../../devkit/documents/nrwl_devkit#projectgraphexternalnode)
- [ProjectGraphProcessorContext](../../devkit/documents/nrwl_devkit#projectgraphprocessorcontext)
- [ProjectGraphProjectNode](../../devkit/documents/nrwl_devkit#projectgraphprojectnode)
- [ProjectGraphV4](../../devkit/documents/nrwl_devkit#projectgraphv4)
### Tree Interfaces
@ -301,18 +300,6 @@ A plugin for Nx
---
### ProjectGraphV4
**ProjectGraphV4**<`T`\>: `Object`
#### Type parameters
| Name | Type |
| :--- | :---- |
| `T` | `any` |
---
## Tree Interfaces
### FileChange

View File

@ -48,7 +48,6 @@ It only uses language primitives and immutable objects
- [ProjectGraphExternalNode](../../devkit/documents/nrwl_devkit#projectgraphexternalnode)
- [ProjectGraphProcessorContext](../../devkit/documents/nrwl_devkit#projectgraphprocessorcontext)
- [ProjectGraphProjectNode](../../devkit/documents/nrwl_devkit#projectgraphprojectnode)
- [ProjectGraphV4](../../devkit/documents/nrwl_devkit#projectgraphv4)
### Tree Interfaces
@ -301,18 +300,6 @@ A plugin for Nx
---
### ProjectGraphV4
**ProjectGraphV4**<`T`\>: `Object`
#### Type parameters
| Name | Type |
| :--- | :---- |
| `T` | `any` |
---
## Tree Interfaces
### FileChange

View File

@ -48,7 +48,6 @@ It only uses language primitives and immutable objects
- [ProjectGraphExternalNode](../../devkit/documents/nrwl_devkit#projectgraphexternalnode)
- [ProjectGraphProcessorContext](../../devkit/documents/nrwl_devkit#projectgraphprocessorcontext)
- [ProjectGraphProjectNode](../../devkit/documents/nrwl_devkit#projectgraphprojectnode)
- [ProjectGraphV4](../../devkit/documents/nrwl_devkit#projectgraphv4)
### Tree Interfaces
@ -301,18 +300,6 @@ A plugin for Nx
---
### ProjectGraphV4
**ProjectGraphV4**<`T`\>: `Object`
#### Type parameters
| Name | Type |
| :--- | :---- |
| `T` | `any` |
---
## Tree Interfaces
### FileChange

View File

@ -86,11 +86,9 @@ You can create 2 types of dependencies.
### Implicit Dependencies
An implicit dependency is not associated with any file, and can be crated as follows:
An implicit dependency is not associated with any file, and can be created as follows:
```typescript
import { DependencyType } from '@nrwl/devkit';
// Add a new edge
builder.addImplicitDependency('existing-project', 'new-project');
```
@ -101,23 +99,37 @@ Even though the plugin is written in JavaScript, resolving dependencies of diffe
Because an implicit dependency is not associated with any file, Nx doesn't know when it might change, so it will be recomputed every time.
## Explicit Dependencies
## Static Dependencies
Nx knows what files have changed since the last invocation. Only those files will be present in the provided `filesToProcess`. You can associate a dependency with a particular file (e.g., if that file contains an import).
```typescript
// Add a new edge
builder.addStaticDependency(
'existing-project',
'new-project',
'libs/existing-project/src/index.ts'
);
```
If a file hasn't changed since the last invocation, it doesn't need to be reanalyzed. Nx knows what dependencies are associated with what files, so it will reuse this information for the files that haven't changed.
## Dynamic Dependencies
Dynamic dependencies are a special type of explicit dependencies. In contrast to standard `explicit` dependencies, they are only imported in the runtime under specific conditions.
A typical example would be lazy-loaded routes. Having separation between these two allows us to identify situations where static import breaks the lazy-loading.
```typescript
import { DependencyType } from '@nrwl/devkit';
// Add a new edge
builder.addExplicitDependency(
builder.addDynamicDependency(
'existing-project',
'libs/existing-project/src/index.ts',
'new-project'
'lazy-route',
'libs/existing-project/src/router-setup.ts'
);
```
If a file hasn't changed since the last invocation, it doesn't need to be reanalyzed. Nx knows what dependencies are associated with what files, so it will reuse this information for the files that haven't changed.
## Visualizing the Project Graph
You can then visualize the project graph as described [here](/core-features/explore-graph). However, there is a cache that Nx uses to avoid recalculating the project graph as much as possible. As you develop your project graph plugin, it might be a good idea to set the following environment variable to disable the project graph cache: `NX_CACHE_PROJECT_GRAPH=false`.

View File

@ -472,7 +472,7 @@ describe('Nx Affected and Graph Tests', () => {
target: mylib,
type: 'static',
},
{ source: myapp, target: mylib2, type: 'static' },
{ source: myapp, target: mylib2, type: 'dynamic' },
],
[myappE2e]: [
{

View File

@ -1,5 +1,6 @@
// nx-ignore-next-line
import type {
DependencyType,
ProjectGraphDependency,
ProjectGraphProjectNode,
} from '@nrwl/devkit';
@ -28,7 +29,13 @@ export class MockProjectGraphService implements ProjectGraphService {
{
file: 'some/file.ts',
hash: 'ecccd8481d2e5eae0e59928be1bc4c2d071729d7',
deps: ['existing-lib-1'],
dependencies: [
{
target: 'existing-lib-1',
source: 'existing-app-1',
type: 'static' as DependencyType,
},
],
},
],
},

View File

@ -281,7 +281,9 @@ export class RenderGraph {
.source()
.data('files')
?.filter(
(file) => file.deps && file.deps.includes(edge.target().id())
(file) =>
file.dependencies &&
file.dependencies.find((d) => d.target === edge.target().id())
)
.map((file) => {
return {

View File

@ -1,6 +1,13 @@
jest.mock('@nrwl/devkit');
jest.mock('@nrwl/devkit');
jest.mock('@nrwl/workspace/src/utilities/buildable-libs-utils');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
readCachedProjectGraph: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
import type { ExecutorContext, Target } from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';

View File

@ -22,6 +22,14 @@ jest.mock('@nrwl/devkit', () => ({
.fn()
.mockImplementation(async () => projectGraph),
}));
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
readCachedProjectGraph: jest
.fn()
.mockImplementation(async () => projectGraph),
}));
describe('Cypress Component Testing Configuration', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<

View File

@ -11,6 +11,14 @@ import { storybookConfigurationGenerator } from './storybook-configuration';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
function listFiles(tree: Tree): string[] {
const files = new Set<string>();
tree.listChanges().forEach((change) => {

View File

@ -12,7 +12,16 @@ import {
} from './update-cy-mount-usage';
import { libraryGenerator } from '@nrwl/workspace';
import { cypressComponentProject } from '../../generators/cypress-component-project/cypress-component-project';
jest.mock('../../utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('update cy.mount usage', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<

View File

@ -172,7 +172,6 @@ export type {
ProjectFileMap,
FileData,
ProjectGraph,
ProjectGraphV4,
ProjectGraphDependency,
ProjectGraphNode,
ProjectGraphProjectNode,

View File

@ -1,6 +1,10 @@
import 'nx/src/utils/testing/mock-fs';
import type { FileData, ProjectGraph } from '@nrwl/devkit';
import type {
FileData,
ProjectGraph,
ProjectGraphDependency,
} from '@nrwl/devkit';
import { DependencyType } from '@nrwl/devkit';
import * as parser from '@typescript-eslint/parser';
import { TSESLint } from '@typescript-eslint/utils';
@ -284,7 +288,13 @@ describe('Enforce Module Boundaries (eslint)', () => {
implicitDependencies: [],
targets: {},
files: [
createFile(`libs/dependsOnPrivate/src/index.ts`, ['privateName']),
createFile(`libs/dependsOnPrivate/src/index.ts`, [
{
source: 'dependsOnPrivateName',
type: 'static',
target: 'privateName',
},
]),
],
},
},
@ -298,7 +308,11 @@ describe('Enforce Module Boundaries (eslint)', () => {
targets: {},
files: [
createFile(`libs/dependsOnPrivate2/src/index.ts`, [
'privateName',
{
source: 'dependsOnPrivateName2',
type: 'static',
target: 'privateName',
},
]),
],
},
@ -313,8 +327,12 @@ describe('Enforce Module Boundaries (eslint)', () => {
targets: {},
files: [
createFile(`libs/private/src/index.ts`, [
'untaggedName',
'taggedName',
{
source: 'privateName',
type: 'static',
target: 'untaggedName',
},
{ source: 'privateName', type: 'static', target: 'taggedName' },
]),
],
},
@ -1419,7 +1437,15 @@ Violation detected in:
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/mylib/src/main.ts`, ['anotherlibName'])],
files: [
createFile(`libs/mylib/src/main.ts`, [
{
source: 'mylibName',
type: 'static',
target: 'anotherlibName',
},
]),
],
},
},
anotherlibName: {
@ -1430,7 +1456,15 @@ Violation detected in:
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/anotherlib/src/main.ts`, ['mylibName'])],
files: [
createFile(`libs/anotherlib/src/main.ts`, [
{
source: 'anotherlibName',
type: 'static',
target: 'mylibName',
},
]),
],
},
},
myappName: {
@ -1486,7 +1520,13 @@ Circular file chain:
implicitDependencies: [],
targets: {},
files: [
createFile(`libs/mylib/src/main.ts`, ['badcirclelibName']),
createFile(`libs/mylib/src/main.ts`, [
{
source: 'badcirclelibName',
type: 'static',
target: 'mylibName',
},
]),
],
},
},
@ -1499,8 +1539,20 @@ Circular file chain:
implicitDependencies: [],
targets: {},
files: [
createFile(`libs/anotherlib/src/main.ts`, ['mylibName']),
createFile(`libs/anotherlib/src/index.ts`, ['mylibName']),
createFile(`libs/anotherlib/src/main.ts`, [
{
source: 'anotherlibName',
type: 'static',
target: 'mylibName',
},
]),
createFile(`libs/anotherlib/src/index.ts`, [
{
source: 'anotherlibName',
type: 'static',
target: 'mylibName',
},
]),
],
},
},
@ -1513,7 +1565,13 @@ Circular file chain:
implicitDependencies: [],
targets: {},
files: [
createFile(`libs/badcirclelib/src/main.ts`, ['anotherlibName']),
createFile(`libs/badcirclelib/src/main.ts`, [
{
source: 'badcirclelibName',
type: 'static',
target: 'anotherlibName',
},
]),
],
},
},
@ -2025,8 +2083,11 @@ const baseConfig = {
linter.defineParser('@typescript-eslint/parser', parser);
linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries);
function createFile(f: string, deps?: string[]): FileData {
return { file: f, hash: '', ...(deps && { deps }) };
function createFile(
f: string,
dependencies?: ProjectGraphDependency[]
): FileData {
return { file: f, hash: '', ...(dependencies && { dependencies }) };
}
function runRule(

View File

@ -146,7 +146,9 @@ export function findFilesInCircularPath(
filePathChain.push(
Object.keys(files)
.filter(
(key) => files[key].deps && files[key].deps.indexOf(next) !== -1
(key) =>
files[key].dependencies &&
files[key].dependencies.find((d) => d.target === next)
)
.map((key) => files[key].file)
);

View File

@ -11,7 +11,9 @@ import { NxJsonConfiguration } from './nx-json';
export interface FileData {
file: string;
hash: string;
/** @deprecated this field will be removed in v17. Use {@link dependencies} instead */
deps?: string[];
dependencies?: ProjectGraphDependency[];
}
/**
@ -33,15 +35,6 @@ export interface ProjectGraph {
version?: string;
}
/** @deprecated this type will be removed in v16. Use {@link ProjectGraph} instead */
export interface ProjectGraphV4<T = any> {
nodes: Record<string, ProjectGraphNode>;
dependencies: Record<string, ProjectGraphDependency[]>;
// this is optional otherwise it might break folks who use project graph creation
allWorkspaceFiles?: FileData[];
version?: string;
}
/**
* Type of dependency between projects
*/
@ -125,7 +118,7 @@ export interface ProjectGraphDependency {
export interface ProjectGraphProcessorContext {
/**
* Workspace information such as projects and configuration
* @deprecated use projectsConfigurations or nxJsonConfiguration
* @deprecated use {@link projectsConfigurations} or {@link nxJsonConfiguration} instead
*/
workspace: Workspace;

View File

@ -132,7 +132,6 @@ export type {
ProjectFileMap,
FileData,
ProjectGraph,
ProjectGraphV4,
ProjectGraphDependency,
ProjectGraphNode,
ProjectGraphProjectNode,

View File

@ -285,6 +285,8 @@ class TaskHasher {
visited: string[]
) {
const projectGraphDeps = this.projectGraph.dependencies[projectName] ?? [];
// we don't want random order of dependencies to change the hash
projectGraphDeps.sort((a, b) => a.target.localeCompare(b.target));
const self = await this.hashSelfInputs(projectName, selfInputs);
const deps = await this.hashDepsInputs(

View File

@ -230,7 +230,7 @@ function addDependencies(
Object.entries(section).forEach(([name, versionRange]) => {
const target = findTarget(path, keyMap, name, versionRange);
if (target) {
builder.addExternalNodeDependency(sourceName, target.name);
builder.addStaticDependency(sourceName, target.name);
}
});
}
@ -291,7 +291,7 @@ function addV1NodeDependencies(
Object.entries(snapshot.requires).forEach(([name, versionRange]) => {
const target = findTarget(path, keyMap, name, versionRange);
if (target) {
builder.addExternalNodeDependency(source, target.name);
builder.addStaticDependency(source, target.name);
}
});
}
@ -317,7 +317,7 @@ function addV1NodeDependencies(
) {
const target = findTarget(path, keyMap, depName, depSpec);
if (target) {
builder.addExternalNodeDependency(node.name, target.name);
builder.addStaticDependency(node.name, target.name);
}
}
});

View File

@ -122,7 +122,7 @@ function addDependencies(
builder.graph.externalNodes[`npm:${name}@${version}`] ||
builder.graph.externalNodes[`npm:${name}`];
if (target) {
builder.addExternalNodeDependency(node.name, target.name);
builder.addStaticDependency(node.name, target.name);
}
});
}

View File

@ -111,7 +111,7 @@ function traverseNode(
graph.dependencies[node.name]?.forEach((dep) => {
const depNode = graph.externalNodes[dep.target];
traverseNode(graph, builder, depNode);
builder.addExternalNodeDependency(node.name, dep.target);
builder.addStaticDependency(node.name, dep.target);
});
}
@ -184,11 +184,9 @@ function switchNodeToHoisted(
builder.addExternalNode(node);
targets.forEach((target) => {
builder.addExternalNodeDependency(node.name, target);
builder.addStaticDependency(node.name, target);
});
sources.forEach((source) =>
builder.addExternalNodeDependency(source, node.name)
);
sources.forEach((source) => builder.addStaticDependency(source, node.name));
}
// BFS to find the shortest path to a dependency specified in package.json

View File

@ -168,7 +168,7 @@ function addDependencies(
keyMap.get(`${name}@npm:${versionRange}`) ||
keyMap.get(`${name}@${versionRange}`);
if (target) {
builder.addExternalNodeDependency(node.name, target.name);
builder.addStaticDependency(node.name, target.name);
}
});
}

View File

@ -1,4 +1,7 @@
import { buildExplicitTypeScriptDependencies } from './explicit-project-dependencies';
import {
buildExplicitTypeScriptDependencies,
ExplicitDependency,
} from './explicit-project-dependencies';
import { buildExplicitPackageJsonDependencies } from './explicit-package-json-dependencies';
import { ProjectFileMap, ProjectGraph } from '../../config/project-graph';
import { ProjectsConfigurations } from '../../config/workspace-json-project-json';
@ -14,7 +17,7 @@ export function buildExplicitTypescriptAndPackageJsonDependencies(
projectGraph: ProjectGraph,
filesToProcess: ProjectFileMap
) {
let res = [];
let res: ExplicitDependency[] = [];
if (
jsPluginConfig.analyzeSourceFiles === undefined ||
jsPluginConfig.analyzeSourceFiles === true

View File

@ -5,6 +5,7 @@ import { parseJson } from '../../utils/json';
import { getImportPath, joinPathFragments } from '../../utils/path';
import { ProjectsConfigurations } from '../../config/workspace-json-project-json';
import { NxJsonConfiguration } from '../../config/nx-json';
import { ExplicitDependency } from './explicit-project-dependencies';
class ProjectGraphNodeRecords {}
@ -74,7 +75,7 @@ function processPackageJson(
sourceProject: string,
fileName: string,
graph: ProjectGraph,
collectedDeps: any[],
collectedDeps: ExplicitDependency[],
packageNameMap: { [packageName: string]: string }
) {
try {

View File

@ -46,21 +46,25 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
type: 'dynamic',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'npm:npm-package',
type: 'static',
},
]);
});
@ -91,16 +95,19 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
type: 'static',
},
]);
});
@ -131,16 +138,19 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj2',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj3a',
type: 'static',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/index.ts',
targetProjectName: 'proj4ab',
type: 'static',
},
]);
});
@ -168,7 +178,7 @@ describe('explicit project dependencies', () => {
},
{
path: 'libs/proj/component.tsx',
content: `
content: `
export function App() {
import('@proj/my-second-proj')
return (
@ -192,16 +202,19 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/component.tsx',
targetProjectName: 'proj2',
type: 'dynamic',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/nested-dynamic-import.ts',
targetProjectName: 'proj3a',
type: 'dynamic',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/nested-require.ts',
targetProjectName: 'proj4ab',
type: 'static',
},
]);
});
@ -236,11 +249,13 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/absolute-path.ts',
targetProjectName: 'proj3a',
type: 'dynamic',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/relative-path.ts',
targetProjectName: 'proj4ab',
type: 'dynamic',
},
]);
});
@ -313,15 +328,15 @@ describe('explicit project dependencies', () => {
{
path: 'libs/proj/comments-with-excess-whitespace.ts',
content: `
/*
/*
nx-ignore-next-line
*/
require('@proj/proj4ab');
// nx-ignore-next-line
import('@proj/proj4ab');
/*
/*
nx-ignore-next-line */
import { foo } from '@proj/proj4ab';
`,
@ -464,11 +479,13 @@ describe('explicit project dependencies', () => {
sourceProjectName,
sourceProjectFile: 'libs/proj/file-1.ts',
targetProjectName: 'proj4ab',
type: 'dynamic',
},
{
sourceProjectName,
sourceProjectFile: 'libs/proj/file-2.ts',
targetProjectName: 'proj3a',
type: 'dynamic',
},
]);
});

View File

@ -6,6 +6,13 @@ import {
ProjectGraph,
} from '../../config/project-graph';
export type ExplicitDependency = {
sourceProjectName: string;
targetProjectName: string;
sourceProjectFile: string;
type?: DependencyType.static | DependencyType.dynamic;
};
export function buildExplicitTypeScriptDependencies(
graph: ProjectGraph,
filesToProcess: ProjectFileMap
@ -19,12 +26,16 @@ export function buildExplicitTypeScriptDependencies(
graph.nodes as any,
graph.externalNodes
);
const res = [] as any;
const res: ExplicitDependency[] = [];
Object.keys(filesToProcess).forEach((source) => {
Object.values(filesToProcess[source]).forEach((f) => {
importLocator.fromFile(
f.file,
(importExpr: string, filePath: string, type: DependencyType) => {
(
importExpr: string,
filePath: string,
type: DependencyType.static | DependencyType.dynamic
) => {
const target = targetProjectLocator.findProjectWithImport(
importExpr,
f.file
@ -39,6 +50,7 @@ export function buildExplicitTypeScriptDependencies(
sourceProjectName: source,
targetProjectName: target,
sourceProjectFile: f.file,
type,
});
}
}

View File

@ -243,7 +243,6 @@ describe('project graph', () => {
expect(graph.dependencies).toEqual({
api: [{ source: 'api', target: 'npm:express', type: 'static' }],
demo: [
{ source: 'demo', target: 'api', type: 'implicit' },
{
source: 'demo',
target: 'ui',
@ -253,8 +252,9 @@ describe('project graph', () => {
{
source: 'demo',
target: 'lazy-lib',
type: 'static',
type: 'dynamic',
},
{ source: 'demo', target: 'api', type: 'implicit' },
],
'demo-e2e': [],
'lazy-lib': [],
@ -267,7 +267,7 @@ describe('project graph', () => {
{
source: 'ui',
target: 'lazy-lib',
type: 'static',
type: 'dynamic',
},
],
});
@ -295,7 +295,7 @@ describe('project graph', () => {
target: 'shared-util',
},
{
type: DependencyType.static,
type: DependencyType.dynamic,
source: 'ui',
target: 'lazy-lib',
},

View File

@ -11,7 +11,10 @@ import {
shouldRecomputeWholeGraph,
writeCache,
} from './nx-deps-cache';
import { buildImplicitProjectDependencies } from './build-dependencies';
import {
buildImplicitProjectDependencies,
ExplicitDependency,
} from './build-dependencies';
import { buildWorkspaceProjectNodes } from './build-nodes';
import * as os from 'os';
import { buildExplicitTypescriptAndPackageJsonDependencies } from './build-dependencies/build-explicit-typescript-and-package-json-dependencies';
@ -75,7 +78,7 @@ export async function buildProjectGraphUsingProjectFileMap(
projectGraphCache: ProjectGraphCache;
}> {
const nxJson = readNxJson();
const projectGraphVersion = '5.0';
const projectGraphVersion = '5.1';
assertWorkspaceValidity(projectsConfigurations, nxJson);
const packageJsonDeps = readCombinedDeps();
const rootTsConfig = readRootTsConfig();
@ -203,8 +206,8 @@ async function buildProjectGraphUsingContext(
for (const proj of Object.keys(cachedFileData)) {
for (const f of updatedBuilder.graph.nodes[proj].data.files) {
const cached = cachedFileData[proj][f.file];
if (cached && cached.deps) {
f.deps = [...cached.deps];
if (cached && cached.dependencies) {
f.dependencies = [...cached.dependencies];
}
}
}
@ -346,11 +349,19 @@ function buildExplicitDependenciesWithoutWorkers(
builder.graph,
ctx.filesToProcess
).forEach((r) => {
builder.addExplicitDependency(
r.sourceProjectName,
r.sourceProjectFile,
r.targetProjectName
);
if (r.type === 'static') {
builder.addStaticDependency(
r.sourceProjectName,
r.targetProjectName,
r.sourceProjectFile
);
} else {
builder.addDynamicDependency(
r.sourceProjectName,
r.targetProjectName,
r.sourceProjectFile
);
}
});
}
@ -378,12 +389,20 @@ 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: ExplicitDependency) => {
if (r.type === 'static') {
builder.addStaticDependency(
r.sourceProjectName,
r.targetProjectName,
r.sourceProjectFile
);
} else {
builder.addDynamicDependency(
r.sourceProjectName,
r.targetProjectName,
r.sourceProjectFile
);
}
});
if (bins.length > 0) {
w.postMessage({ filesToProcess: bins.shift() });

View File

@ -13,7 +13,7 @@ describe('nx deps utils', () => {
it('should be false when nothing changes', () => {
expect(
shouldRecomputeWholeGraph(
createCache({ version: '5.0' }),
createCache({ version: '5.1' }),
createPackageJsonDeps({}),
createWorkspaceJson({}),
createNxJson({}),
@ -318,7 +318,7 @@ describe('nx deps utils', () => {
function createCache(p: Partial<ProjectGraphCache>): ProjectGraphCache {
const defaults: ProjectGraphCache = {
version: '5.0',
version: '5.1',
deps: {
'@nrwl/workspace': '12.0.0',
plugin: '1.0.0',

View File

@ -94,7 +94,7 @@ export function createCache(
version: packageJsonDeps[p],
}));
const newValue: ProjectGraphCache = {
version: projectGraph.version || '5.0',
version: projectGraph.version || '5.1',
deps: packageJsonDeps,
lockFileHash,
// compilerOptions may not exist, especially for repos converted through add-nx-to-monorepo
@ -149,7 +149,7 @@ export function shouldRecomputeWholeGraph(
nxJson: NxJsonConfiguration,
tsConfig: { compilerOptions: { paths: { [k: string]: any } } }
): boolean {
if (cache.version !== '5.0') {
if (cache.version !== '5.1') {
return true;
}
if (cache.deps['@nrwl/workspace'] !== packageJsonDeps['@nrwl/workspace']) {

View File

@ -25,7 +25,7 @@ describe('ProjectGraphBuilder', () => {
});
});
it(`should add an implicit dependency`, () => {
it(`should add a dependency`, () => {
expect(() =>
builder.addImplicitDependency('invalid-source', 'target')
).toThrowError();
@ -34,11 +34,40 @@ describe('ProjectGraphBuilder', () => {
).toThrowError();
// ignore the self deps
builder.addImplicitDependency('source', 'source');
builder.addDynamicDependency('source', 'source', 'source/index.ts');
// don't include duplicates
// don't include duplicates of the same type
builder.addImplicitDependency('source', 'target');
builder.addImplicitDependency('source', 'target');
builder.addStaticDependency('source', 'target', 'source/index.ts');
builder.addDynamicDependency('source', 'target', 'source/index.ts');
builder.addStaticDependency('source', 'target', 'source/index.ts');
const graph = builder.getUpdatedProjectGraph();
expect(graph.dependencies).toEqual({
source: [
{
source: 'source',
target: 'target',
type: 'implicit',
},
{
source: 'source',
target: 'target',
type: 'static',
},
{
source: 'source',
target: 'target',
type: 'dynamic',
},
],
target: [],
});
});
it(`should add an implicit dependency`, () => {
builder.addImplicitDependency('source', 'target');
const graph = builder.getUpdatedProjectGraph();
expect(graph.dependencies).toEqual({
@ -96,10 +125,10 @@ describe('ProjectGraphBuilder', () => {
});
});
it(`should use implicit dep when both implicit and explicit deps are available`, () => {
it(`should use both deps when both implicit and explicit deps are available`, () => {
// don't include duplicates
builder.addImplicitDependency('source', 'target');
builder.addExplicitDependency('source', 'source/index.ts', 'target');
builder.addStaticDependency('source', 'target', 'source/index.ts');
const graph = builder.getUpdatedProjectGraph();
expect(graph.dependencies).toEqual({
@ -109,6 +138,11 @@ describe('ProjectGraphBuilder', () => {
target: 'target',
type: 'implicit',
},
{
source: 'source',
target: 'target',
type: 'static',
},
],
target: [],
});
@ -121,7 +155,7 @@ describe('ProjectGraphBuilder', () => {
data: {} as any,
});
builder.addImplicitDependency('source', 'target');
builder.addExplicitDependency('source', 'source/index.ts', 'target');
builder.addStaticDependency('source', 'target', 'source/index.ts');
builder.addImplicitDependency('source', 'target2');
builder.removeDependency('source', 'target');

View File

@ -44,7 +44,6 @@ export class ProjectGraphBuilder {
}
}
this.graph.nodes[node.name] = node;
this.graph.dependencies[node.name] = [];
}
/**
@ -72,29 +71,61 @@ export class ProjectGraphBuilder {
}
/**
* Adds a dependency from source project to target project
* Adds static dependency from source project to target project
*/
addStaticDependency(
sourceProjectName: string,
targetProjectName: string,
sourceProjectFile?: string
): void {
if (this.graph.nodes[sourceProjectName] && !sourceProjectFile) {
throw new Error(`Source project file is required`);
}
this.addDependency(
sourceProjectName,
targetProjectName,
DependencyType.static,
sourceProjectFile
);
}
/**
* Adds dynamic dependency from source project to target project
*/
addDynamicDependency(
sourceProjectName: string,
targetProjectName: string,
sourceProjectFile: string
): void {
if (this.graph.externalNodes[sourceProjectName]) {
throw new Error(`External projects can't have "dynamic" dependencies`);
}
if (!sourceProjectFile) {
throw new Error(`Source project file is required`);
}
this.addDependency(
sourceProjectName,
targetProjectName,
DependencyType.dynamic,
sourceProjectFile
);
}
/**
* Adds implicit dependency from source project to target project
*/
addImplicitDependency(
sourceProjectName: string,
targetProjectName: string
): void {
if (sourceProjectName === targetProjectName) {
return;
if (this.graph.externalNodes[sourceProjectName]) {
throw new Error(`External projects can't have "implicit" dependencies`);
}
if (!this.graph.nodes[sourceProjectName]) {
throw new Error(`Source project does not exist: ${sourceProjectName}`);
}
if (
!this.graph.nodes[targetProjectName] &&
!this.graph.externalNodes[targetProjectName]
) {
throw new Error(`Target project does not exist: ${targetProjectName}`);
}
this.graph.dependencies[sourceProjectName].push({
source: sourceProjectName,
target: targetProjectName,
type: DependencyType.implicit,
});
this.addDependency(
sourceProjectName,
targetProjectName,
DependencyType.implicit
);
}
/**
@ -124,6 +155,7 @@ export class ProjectGraphBuilder {
/**
* Add an explicit dependency from a file in source project to target project
* @deprecated this method will be removed in v17. Use {@link addStaticDependency} or {@link addDynamicDependency} instead
*/
addExplicitDependency(
sourceProjectName: string,
@ -154,46 +186,14 @@ export class ProjectGraphBuilder {
);
}
if (!fileData.deps) {
fileData.deps = [];
if (!fileData.dependencies) {
fileData.dependencies = [];
}
if (!fileData.deps.find((t) => t === targetProjectName)) {
fileData.deps.push(targetProjectName);
}
}
/**
* Add an explicit dependency from a file in source project to target project
*/
addExternalNodeDependency(
sourceProjectName: string,
targetProjectName: string
): void {
if (sourceProjectName === targetProjectName) {
return;
}
const source = this.graph.externalNodes[sourceProjectName];
if (!source) {
throw new Error(`Source project does not exist: ${sourceProjectName}`);
}
if (!this.graph.externalNodes[targetProjectName]) {
throw new Error(`Target project does not exist: ${targetProjectName}`);
}
if (!this.graph.dependencies[sourceProjectName]) {
this.graph.dependencies[sourceProjectName] = [];
}
if (
!this.graph.dependencies[sourceProjectName].some(
(d) => d.target === targetProjectName
)
) {
this.graph.dependencies[sourceProjectName].push({
source: sourceProjectName,
if (!fileData.dependencies.find((t) => t.target === targetProjectName)) {
fileData.dependencies.push({
target: targetProjectName,
source: sourceProjectName,
type: DependencyType.static,
});
}
@ -212,20 +212,25 @@ export class ProjectGraphBuilder {
this.calculateAlreadySetTargetDeps(sourceProject);
this.graph.dependencies[sourceProject] = [
...alreadySetTargetProjects.values(),
];
].flatMap((depsMap) => [...depsMap.values()]);
const fileDeps = this.calculateTargetDepsFromFiles(sourceProject);
for (const targetProject of fileDeps) {
if (!alreadySetTargetProjects.has(targetProject)) {
for (const [targetProject, types] of fileDeps.entries()) {
for (const type of types.values()) {
if (
!this.removedEdges[sourceProject] ||
!this.removedEdges[sourceProject].has(targetProject)
!alreadySetTargetProjects.has(targetProject) ||
!alreadySetTargetProjects.get(targetProject).has(type)
) {
this.graph.dependencies[sourceProject].push({
source: sourceProject,
target: targetProject,
type: DependencyType.static,
});
if (
!this.removedEdges[sourceProject] ||
!this.removedEdges[sourceProject].has(targetProject)
) {
this.graph.dependencies[sourceProject].push({
source: sourceProject,
target: targetProject,
type,
});
}
}
}
}
@ -233,6 +238,78 @@ export class ProjectGraphBuilder {
return this.graph;
}
private addDependency(
sourceProjectName: string,
targetProjectName: string,
type: DependencyType,
sourceProjectFile?: string
): void {
if (sourceProjectName === targetProjectName) {
return;
}
if (
!this.graph.nodes[sourceProjectName] &&
!this.graph.externalNodes[sourceProjectName]
) {
throw new Error(`Source project does not exist: ${sourceProjectName}`);
}
if (
!this.graph.nodes[targetProjectName] &&
!this.graph.externalNodes[targetProjectName]
) {
throw new Error(`Target project does not exist: ${targetProjectName}`);
}
if (
this.graph.externalNodes[sourceProjectName] &&
this.graph.nodes[targetProjectName]
) {
throw new Error(`External projects can't depend on internal projects`);
}
if (!this.graph.dependencies[sourceProjectName]) {
this.graph.dependencies[sourceProjectName] = [];
}
// do not add duplicate
if (
this.graph.dependencies[sourceProjectName].find(
(d) => d.target === targetProjectName && d.type === type
)
) {
return;
}
const dependency = {
source: sourceProjectName,
target: targetProjectName,
type,
};
if (sourceProjectFile) {
const source = this.graph.nodes[sourceProjectName];
if (!source) {
throw new Error(
`Source project is not a project node: ${sourceProjectName}`
);
}
const fileData = source.data.files.find(
(f) => f.file === sourceProjectFile
);
if (!fileData) {
throw new Error(
`Source project ${sourceProjectName} does not have a file: ${sourceProjectFile}`
);
}
if (!fileData.dependencies) {
fileData.dependencies = [];
}
if (!fileData.dependencies.find((t) => t.target === targetProjectName)) {
fileData.dependencies.push(dependency);
}
}
this.graph.dependencies[sourceProjectName].push(dependency);
}
private removeDependenciesWithNode(name: string) {
// remove all source dependencies
delete this.graph.dependencies[name];
@ -254,26 +331,45 @@ export class ProjectGraphBuilder {
}
}
private calculateTargetDepsFromFiles(sourceProject: string) {
const fileDeps = new Set<string>();
private calculateTargetDepsFromFiles(
sourceProject: string
): Map<string, Set<DependencyType | string>> {
const fileDeps = new Map<string, Set<DependencyType | string>>();
const files = this.graph.nodes[sourceProject].data.files;
if (!files) return fileDeps;
if (!files) {
return fileDeps;
}
for (let f of files) {
if (f.deps) {
for (let p of f.deps) {
fileDeps.add(p);
if (f.dependencies) {
for (let d of f.dependencies) {
if (!fileDeps.has(d.target)) {
fileDeps.set(d.target, new Set([d.type]));
} else {
fileDeps.get(d.target).add(d.type);
}
}
}
}
return fileDeps;
}
private calculateAlreadySetTargetDeps(sourceProject: string) {
const alreadySetTargetProjects = new Map<string, ProjectGraphDependency>();
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 calculateAlreadySetTargetDeps(
sourceProject: string
): Map<string, Map<DependencyType | string, ProjectGraphDependency>> {
const alreadySetTargetProjects = new Map<
string,
Map<DependencyType | string, ProjectGraphDependency>
>();
if (this.graph.dependencies[sourceProject]) {
const removed = this.removedEdges[sourceProject];
for (const d of this.graph.dependencies[sourceProject]) {
if (!removed || !removed.has(d.target)) {
if (!alreadySetTargetProjects.has(d.target)) {
alreadySetTargetProjects.set(d.target, new Map([[d.type, d]]));
} else {
alreadySetTargetProjects.get(d.target).set(d.type, d);
}
}
}
}
return alreadySetTargetProjects;

View File

@ -3,7 +3,7 @@ import { buildProjectGraph } from './build-project-graph';
import { output } from '../utils/output';
import { defaultFileHasher } from '../hasher/file-hasher';
import { markDaemonAsDisabled, writeDaemonLogs } from '../daemon/tmp-dir';
import { ProjectGraph, ProjectGraphV4 } from '../config/project-graph';
import { ProjectGraph } from '../config/project-graph';
import { stripIndents } from '../utils/strip-indents';
import {
ProjectConfiguration,
@ -47,7 +47,7 @@ export function readCachedProjectGraph(): ProjectGraph {
return projectGraphAdapter(
projectGraph.version,
'5.0',
'5.1',
projectGraph
) as ProjectGraph;
}
@ -179,43 +179,38 @@ export function projectGraphAdapter(
sourceVersion: string,
targetVersion: string,
projectGraph: ProjectGraph
): ProjectGraph | ProjectGraphV4 {
): ProjectGraph {
if (sourceVersion === targetVersion) {
return projectGraph;
}
if (+sourceVersion >= 5 && targetVersion === '4.0') {
return projectGraphCompat5to4(projectGraph as ProjectGraph);
if (+sourceVersion > 5 && +targetVersion === 5) {
return projectGraphCompatFileDependencies(projectGraph as ProjectGraph);
}
throw new Error(
`Invalid source or target versions. Source: ${sourceVersion}, Target: ${targetVersion}.
Only backwards compatibility between "5.0" and "4.0" is supported.
Only backwards compatibility between "5.1" and "5.0" is supported.
This error can be caused by "@nrwl/..." packages getting out of sync or outdated project graph cache.
Check the versions running "nx report" and/or remove your "nxdeps.json" file (in node_modules/.cache/nx folder).
`
);
}
/**
* Backwards compatibility adapter for project Nodes v4 to v5
* @param {ProjectGraph} projectGraph
* @returns {ProjectGraph}
*/
function projectGraphCompat5to4(projectGraph: ProjectGraph): ProjectGraphV4 {
const { externalNodes, ...rest } = projectGraph;
return {
...rest,
nodes: {
...projectGraph.nodes,
...externalNodes,
},
dependencies: {
...projectGraph.dependencies,
...Object.keys(externalNodes).reduce(
(acc, key) => ({ ...acc, [`npm:${key}`]: [] }),
{}
),
},
version: '4.0',
};
function projectGraphCompatFileDependencies(
projectGraph: ProjectGraph
): ProjectGraph {
Object.values(projectGraph.nodes).forEach(({ data }) => {
if (data.files) {
data.files = data.files.map(({ file, hash, dependencies }) => ({
file,
hash,
// map dependencies to array of targets
...(dependencies &&
dependencies.length && {
deps: [...new Set(dependencies.map((d) => d.target))],
}),
}));
}
});
return projectGraph;
}

View File

@ -8,6 +8,14 @@ import applicationGenerator from '../application/application';
import componentGenerator from '../component/component';
import storybookConfigurationGenerator from './configuration';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('react-native:storybook-configuration', () => {
let appTree;

View File

@ -22,6 +22,14 @@ jest.mock('@nrwl/devkit', () => ({
.mockImplementation(async () => projectGraph),
}));
jest.mock('@nrwl/cypress/src/utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
readCachedProjectGraph: jest
.fn()
.mockImplementation(async () => projectGraph),
}));
describe('React:CypressComponentTestConfiguration', () => {
let tree: Tree;
let mockedAssertCypressVersion: jest.Mock<

View File

@ -9,6 +9,14 @@ import storybookConfigurationGenerator from './configuration';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nrwl/cypress/src/utils/cypress-version');
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('react:storybook-configuration', () => {
let appTree;
let mockedInstalledCypressVersion: jest.Mock<

View File

@ -10,6 +10,14 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import configurationGenerator from './configuration';
import * as workspaceConfiguration from './test-configs/root-workspace-configuration.json';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('@nrwl/storybook:configuration for workspaces with Root project', () => {
describe('basic functionalities', () => {
let tree: Tree;

View File

@ -18,6 +18,14 @@ import { storybook7Version } from '../../utils/versions';
import configurationGenerator from './configuration';
import * as variousProjects from './test-configs/various-projects.json';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('@nrwl/storybook:configuration for Storybook v7', () => {
describe('basic functionalities', () => {
let tree: Tree;

View File

@ -15,6 +15,14 @@ import { TsConfig } from '../../utils/utilities';
import configurationGenerator from './configuration';
import * as workspaceConfiguration from './test-configs/workspace-conifiguration.json';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('@nrwl/storybook:configuration', () => {
describe('basic functionalities', () => {
let tree: Tree;

View File

@ -14,6 +14,14 @@ import {
} from '../../../utils/testing';
import { migrateDefaultsGenerator } from './migrate-defaults-5-to-6';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('migrate-defaults-5-to-6 Generator', () => {
let appTree: Tree;

View File

@ -17,6 +17,14 @@ import {
} from '@nrwl/devkit/ngcli-adapter';
import { getTsSourceFile } from '@nrwl/storybook/src/utils/utilities';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
const componentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'component'

View File

@ -11,6 +11,14 @@ import {
import { nxVersion, storybookVersion } from './versions';
import * as targetVariations from './test-configs/different-target-variations.json';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
const componentSchematic = wrapAngularDevkitSchematic(
'@schematics/angular',
'component'
@ -66,14 +74,14 @@ describe('testing utilities', () => {
`
import { Story, Meta } from '@storybook/react';
import { Button } from './button';
export default {
component: Button,
title: 'Button',
} as Meta;
const Template: Story = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
`
@ -83,7 +91,7 @@ describe('testing utilities', () => {
`test-ui-lib/src/lib/button/button.component.other.ts`,
`
import { Button } from './button';
// test test
`
);
@ -92,7 +100,7 @@ describe('testing utilities', () => {
`test-ui-lib/src/lib/button/button.component.react-native.ts`,
`
import { storiesOf } from '@storybook/react-native';
// test test
`
);
@ -101,7 +109,7 @@ describe('testing utilities', () => {
`test-ui-lib/src/lib/button/button.component.new-syntax.ts`,
`
import { ComponentStory } from '@storybook/react';
// test test
`
);

View File

@ -3,6 +3,14 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Schema } from '../schema';
import { checkTargets } from './check-targets';
// nested code imports graph from the repo, which might have innacurate graph version
jest.mock('nx/src/project-graph/project-graph', () => ({
...jest.requireActual<any>('nx/src/project-graph/project-graph'),
createProjectGraphAsync: jest
.fn()
.mockImplementation(async () => ({ nodes: {}, dependencies: {} })),
}));
describe('checkTargets', () => {
let tree: Tree;
let schema: Schema;