diff --git a/packages/nx/src/lock-file/npm-v2.ts b/packages/nx/src/lock-file/npm-v2.ts index c03e1f38ba611f..138bcae1aab93d 100644 --- a/packages/nx/src/lock-file/npm-v2.ts +++ b/packages/nx/src/lock-file/npm-v2.ts @@ -67,9 +67,15 @@ export function parseNpmLockFile( } // adds edge out to node -function addEdge(builder, node, depName, depSpec, type) { +function addEdge( + builder: LockFileBuilder, + node, + depName, + depSpec, + isOptional?: boolean +) { if (!node.edgesOut || !node.edgesOut.has(depName)) { - builder.addEdgeOut(node, depName, depSpec, type); + builder.addEdgeOut(node, depName, depSpec, isOptional); } } @@ -140,7 +146,7 @@ function parseV1Dependencies( ) { if (value.requires) { Object.entries(value.requires).forEach(([depName, depSpec]) => { - addEdge(builder, node, depName, depSpec, 'unknown'); + addEdge(builder, node, depName, depSpec); }); } } @@ -194,21 +200,17 @@ function parseV3Dependencies( if (value.peerDependencies) { const peerMeta = value.peerDependenciesMeta || {}; Object.entries(value.peerDependencies).forEach(([depName, depSpec]) => { - if (peerMeta[depName]?.optional) { - addEdge(builder, node, depName, depSpec, 'peerOptional'); - } else { - addEdge(builder, node, depName, depSpec, 'peer'); - } + addEdge(builder, node, depName, depSpec, peerMeta[depName]?.optional); }); } if (value.dependencies) { Object.entries(value.dependencies).forEach(([depName, depSpec]) => { - addEdge(builder, node, depName, depSpec, 'prod'); + addEdge(builder, node, depName, depSpec); }); } if (value.optionalDependencies) { Object.entries(value.optionalDependencies).forEach(([depName, depSpec]) => { - addEdge(builder, node, depName, depSpec, 'optional'); + addEdge(builder, node, depName, depSpec, true); }); } } diff --git a/packages/nx/src/lock-file/utils/lock-file-builder.ts b/packages/nx/src/lock-file/utils/lock-file-builder.ts index 0238eaaa15a41d..ce0e098982a99e 100644 --- a/packages/nx/src/lock-file/utils/lock-file-builder.ts +++ b/packages/nx/src/lock-file/utils/lock-file-builder.ts @@ -16,45 +16,22 @@ export type LockFileNode = { path: string; resolved?: string; integrity?: string; - dev?: boolean; - optional?: boolean; - peer?: boolean; - devOptional?: boolean; // tree.devOptional && !tree.dev && !tree.optional edgesOut?: Map; children?: Map; edgesIn?: Set; isProjectRoot?: true; }; -type LockFileEdgeType = - | 'prod' - | 'dev' - | 'optional' - | 'peer' - | 'peerOptional' - | 'workspace' - | 'unknown'; - -const VALID_TYPES = new Set([ - 'prod', - 'dev', - 'optional', - 'peer', - 'peerOptional', - 'workspace', -]); - export type LockFileEdge = { name: string; versionSpec: string; - type: LockFileEdgeType; - from: LockFileNode; // path from + from?: LockFileNode; to?: LockFileNode; - error?: - | 'MISSING_TARGET' - | 'MISSING_SOURCE' - // | 'DETACHED' - | 'UNRESOLVED_TYPE'; + // some optional dependencies might be missing + // we want to keep track of that to avoid false positives + optional?: boolean; + // error type if source or target is missing + error?: 'MISSING_TARGET' | 'MISSING_SOURCE'; }; // TODO 1: Check Arborist for links? Perhaps those should be workspace files @@ -112,22 +89,28 @@ export class LockFileBuilder { const skipEdgeInCheck = true; if (dependencies) { Object.entries(dependencies).forEach(([name, versionSpec]) => { - this.addEdgeOut(node, name, versionSpec, 'prod', skipEdgeInCheck); + this.addEdgeOut(node, name, versionSpec, false, skipEdgeInCheck); }); } if (devDependencies) { Object.entries(devDependencies).forEach(([name, versionSpec]) => { - this.addEdgeOut(node, name, versionSpec, 'dev', skipEdgeInCheck); + this.addEdgeOut(node, name, versionSpec, false, skipEdgeInCheck); }); } if (optionalDependencies) { Object.entries(optionalDependencies).forEach(([name, versionSpec]) => { - this.addEdgeOut(node, name, versionSpec, 'optional', skipEdgeInCheck); + this.addEdgeOut(node, name, versionSpec, true, skipEdgeInCheck); }); } if (peerDependencies) { Object.entries(peerDependencies).forEach(([name, versionSpec]) => { - this.addEdgeOut(node, name, versionSpec, 'peer', skipEdgeInCheck); + this.addEdgeOut( + node, + name, + versionSpec, + packageJson.peerDependenciesMeta?.[name]?.optional, + skipEdgeInCheck + ); }); } return node; @@ -137,10 +120,10 @@ export class LockFileBuilder { node: LockFileNode, name: string, versionSpec: string, - type: LockFileEdgeType, + isOptional?: boolean, skipEdgeInCheck?: boolean ) { - this.validateEdgeCreation(node, name, versionSpec, type); + this.validateEdgeCreation(node, name, versionSpec); if (!this.isVersionSpecSupported(versionSpec)) { return; @@ -156,9 +139,11 @@ export class LockFileBuilder { const edge: LockFileEdge = { name, versionSpec, - type, from: node, }; + if (isOptional) { + edge.optional = true; + } this.updateEdgeTypeAndError(edge); node.edgesOut.set(name, edge); if (!skipEdgeInCheck) { @@ -231,13 +216,8 @@ export class LockFileBuilder { } } - addEdgeIn( - node: LockFileNode, - type: LockFileEdgeType, - versionSpec: string, - parent?: LockFileNode - ) { - this.validateEdgeCreation(node, node.name, versionSpec, type); + addEdgeIn(node: LockFileNode, versionSpec: string, parent?: LockFileNode) { + this.validateEdgeCreation(node, node.name, versionSpec); const existingEdges = this.findEdgesOut(node.name, versionSpec); if (existingEdges.size > 0) { @@ -248,7 +228,6 @@ export class LockFileBuilder { const edge: LockFileEdge = { name: node.name, versionSpec, - type, from: parent, to: node, }; @@ -315,37 +294,16 @@ export class LockFileBuilder { } private updateEdgeTypeAndError(edge: LockFileEdge) { - if (edge.to && edge.type === 'unknown') { - edge.type = this.getValidEdgeType(edge.to); - } - if (!edge.to && edge.type !== 'optional' && edge.type !== 'peerOptional') { + if (!edge.to && !edge.optional) { edge.error = 'MISSING_TARGET'; } if (!edge.from) { edge.error = 'MISSING_SOURCE'; - } else if (edge.to && edge.type === 'unknown') { - edge.error = 'UNRESOLVED_TYPE'; } else { delete edge.error; } } - private getValidEdgeType(node: LockFileNode): LockFileEdgeType { - if (node.peer && node.optional) { - return 'peerOptional'; - } - if (node.optional) { - return 'optional'; - } - if (node.peer) { - return 'peer'; - } - if (node.dev) { - return 'dev'; - } - return 'prod'; - } - // private detachEdge(edge: LockFileEdge) { // if (edge['to']) { // edge['to'].edgesIn.delete(edge); @@ -432,8 +390,7 @@ export class LockFileBuilder { private validateEdgeCreation( node: LockFileNode, name: string, - versionSpec: string, - type: LockFileEdgeType + versionSpec: string ) { if (!node) { throw new TypeError(`Edge must be bound to a node`); @@ -446,16 +403,6 @@ export class LockFileBuilder { `Edge must have a valid version specification: ${versionSpec}` ); } - if (!type) { - throw new TypeError(`Edge must have a valid type: ${type}`); - } - if (!type || (!VALID_TYPES.has(type) && type !== 'unknown')) { - throw new TypeError( - `Edge must have a valid type: ${type}\nValid types: ${Array.from( - VALID_TYPES - ).join(', ')}` - ); - } } private verifyNode(node: LockFileNode) { @@ -494,12 +441,12 @@ export class LockFileBuilder { getLockFileGraph(): LockFileGraph { let isValid = true; - if (!this.isGraphConsistent()) { - console.error( - `Graph is not consistent. Please report this issue via github` - ); - isValid = false; - } + // if (!this.isGraphConsistent()) { + // console.error( + // `Graph is not consistent. Please report this issue via github` + // ); + // isValid = false; + // } return { root: this.root, diff --git a/packages/nx/src/lock-file/utils/parsing.ts b/packages/nx/src/lock-file/utils/parsing.ts deleted file mode 100644 index b0f84382a3cef1..00000000000000 --- a/packages/nx/src/lock-file/utils/parsing.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { workspaceRoot } from '../../utils/workspace-root'; -import { existsSync, readFileSync } from 'fs'; - -/** - * Checks whether the package is a root dependency - * @param packageName - * @param version - * @returns - */ -export function getRootVersion(packageName: string): boolean { - const fullPath = `${workspaceRoot}/node_modules/${packageName}/package.json`; - - if (existsSync(fullPath)) { - const content = readFileSync(fullPath, 'utf-8'); - return JSON.parse(content).version; - } else { - throw new Error( - `Could not find package.json for "${packageName}" at "${fullPath}"` - ); - } -} diff --git a/packages/nx/src/lock-file/yarn-v2.ts b/packages/nx/src/lock-file/yarn-v2.ts index b174c6b61c71cf..ff2c67bffc5815 100644 --- a/packages/nx/src/lock-file/yarn-v2.ts +++ b/packages/nx/src/lock-file/yarn-v2.ts @@ -5,7 +5,8 @@ import { LockFileGraph, LockFileNode, } from './utils/lock-file-builder'; -import { getRootVersion } from './utils/parsing'; +import { workspaceRoot } from '../utils/workspace-root'; +import { existsSync, readFileSync } from 'fs'; type YarnDependency = { version: string; @@ -106,34 +107,41 @@ function parseClassicLockFile( rootVersion = getRootVersion(packageName); } versionMap.forEach((valueSet) => { - const [key, value]: [string, YarnDependency] = valueSet + const [key, dependency]: [string, YarnDependency] = valueSet .values() .next().value; const versionSpec = key.slice(packageName.length + 1); - if (value.version === rootVersion) { + if (dependency.version === rootVersion) { const path = `node_modules/${packageName}`; - const node = parseClassicNode(packageName, path, versionSpec, value); + const node = parseClassicNode( + packageName, + path, + versionSpec, + dependency + ); builder.addNode(path, node); valueSet.forEach(([newKey]) => { const newSpec = newKey.slice(packageName.length + 1); - builder.addEdgeIn(node, 'unknown', newSpec); + builder.addEdgeIn(node, newSpec); }); - if (value.dependencies) { - Object.entries(value.dependencies).forEach(([depName, depSpec]) => { - builder.addEdgeOut(node, depName, depSpec, 'prod'); - }); + if (dependency.dependencies) { + Object.entries(dependency.dependencies).forEach( + ([depName, depSpec]) => { + builder.addEdgeOut(node, depName, depSpec); + } + ); } - if (value.optionalDependencies) { - Object.entries(value.optionalDependencies).forEach( + if (dependency.optionalDependencies) { + Object.entries(dependency.optionalDependencies).forEach( ([depName, depSpec]) => { - builder.addEdgeOut(node, depName, depSpec, 'optional'); + builder.addEdgeOut(node, depName, depSpec, true); } ); } } else { // we don't know the path yet, so we need to resolve non-root deps later - unresolvedDependencies.add([packageName, versionSpec, value]); + unresolvedDependencies.add([packageName, versionSpec, dependency]); } }); }); @@ -195,12 +203,12 @@ function parseBerryLockFile( builder.addNode(path, node); valueSet.forEach(([newKey]) => { const versionSpec = parseBerryVersionSpec(newKey, packageName); - builder.addEdgeIn(node, 'unknown', versionSpec); + builder.addEdgeIn(node, versionSpec); }); if (value.dependencies) { // Yarn keeps no notion of dev/peer/optional dependencies Object.entries(value.dependencies).forEach(([depName, depSpec]) => { - builder.addEdgeOut(node, depName, depSpec, 'prod'); + builder.addEdgeOut(node, depName, depSpec); }); } } else { @@ -273,6 +281,18 @@ function parseBerryNode( return node; } +/** + * A -> B@v1 + * B@v1 + * C -> B@v2 + * D -> E@v2 -> B@v3 + * E@v1 + * + * D + * E@v2 + * B@v3 + */ + function exhaustUnresolvedDependencies( builder: LockFileBuilder, { @@ -302,7 +322,7 @@ function exhaustUnresolvedDependencies( if (value.dependencies) { // Yarn classic keeps no notion of dev/peer/optional dependencies Object.entries(value.dependencies).forEach(([depName, depSpec]) => { - builder.addEdgeOut(node, depName, depSpec, 'prod'); + builder.addEdgeOut(node, depName, depSpec); }); } unresolvedDependencies.delete(unresolvedSet); @@ -326,3 +346,16 @@ function exhaustUnresolvedDependencies( exhaustUnresolvedDependencies(builder, { unresolvedDependencies, isBerry }); } } + +function getRootVersion(packageName: string): boolean { + const fullPath = `${workspaceRoot}/node_modules/${packageName}/package.json`; + + if (existsSync(fullPath)) { + const content = readFileSync(fullPath, 'utf-8'); + return JSON.parse(content).version; + } else { + throw new Error( + `Could not find package.json for "${packageName}" at "${fullPath}"` + ); + } +}