diff --git a/lib/dep-graph-builders/index.ts b/lib/dep-graph-builders/index.ts index 43b436df..dcb5d38c 100644 --- a/lib/dep-graph-builders/index.ts +++ b/lib/dep-graph-builders/index.ts @@ -13,7 +13,7 @@ import { extractPkgsFromYarnLockV2, } from './yarn-lock-v2'; import { parseNpmLockV2Project } from './npm-lock-v2'; -import { parsePnpmProject } from './pnpm'; +import { parsePnpmProject, parsePnpmWorkspace } from './pnpm'; import { parsePkgJson } from './util'; export { @@ -29,5 +29,6 @@ export { parseYarnLockV2Project, extractPkgsFromYarnLockV2, parsePnpmProject, + parsePnpmWorkspace, parsePkgJson, }; diff --git a/lib/dep-graph-builders/pnpm/build-dep-graph-pnpm.ts b/lib/dep-graph-builders/pnpm/build-dep-graph-pnpm.ts index adebaa38..928cc103 100644 --- a/lib/dep-graph-builders/pnpm/build-dep-graph-pnpm.ts +++ b/lib/dep-graph-builders/pnpm/build-dep-graph-pnpm.ts @@ -1,10 +1,6 @@ import { DepGraphBuilder } from '@snyk/dep-graph'; import { getTopLevelDeps } from '../util'; -import type { - Overrides, - PnpmProjectParseOptions, - PnpmWorkspaceArgs, -} from '../types'; +import type { Overrides, PnpmProjectParseOptions } from '../types'; import type { PackageJsonBase } from '../types'; import { getPnpmChildNode } from './utils'; import { eventLoopSpinner } from 'event-loop-spinner'; @@ -15,7 +11,7 @@ export const buildDepGraphPnpm = async ( lockFileParser: PnpmLockfileParser, pkgJson: PackageJsonBase, options: PnpmProjectParseOptions, - workspaceArgs?: PnpmWorkspaceArgs, + importer?: string, ) => { const { strictOutOfSync, @@ -37,7 +33,7 @@ export const buildDepGraphPnpm = async ( const topLevelDeps = getTopLevelDeps(pkgJson, options); const extractedTopLevelDeps = - lockFileParser.extractTopLevelDependencies(options) || {}; + lockFileParser.extractTopLevelDependencies(options, importer) || {}; for (const name of Object.keys(topLevelDeps)) { topLevelDeps[name].version = extractedTopLevelDeps[name].version; @@ -58,10 +54,7 @@ export const buildDepGraphPnpm = async ( strictOutOfSync, includeOptionalDeps, includeDevDeps, - // we have rootWorkspaceOverrides if this is workspace pkg with overrides - // at root - therefore it should take precedent - // TODO: inspect if this is needed at all, seems like pnpm resolves everything in lockfile - workspaceArgs?.rootOverrides || pkgJson.pnpm?.overrides || {}, + pkgJson.pnpm?.overrides || {}, pruneWithinTopLevelDeps, lockFileParser, ); diff --git a/lib/dep-graph-builders/pnpm/index.ts b/lib/dep-graph-builders/pnpm/index.ts index d756964a..b2a48b3b 100644 --- a/lib/dep-graph-builders/pnpm/index.ts +++ b/lib/dep-graph-builders/pnpm/index.ts @@ -1,48 +1,4 @@ -import { parsePkgJson } from '../util'; -import { - PackageJsonBase, - PnpmProjectParseOptions, - PnpmWorkspaceArgs, -} from '../types'; -import { buildDepGraphPnpm } from './build-dep-graph-pnpm'; -import { DepGraph } from '@snyk/dep-graph'; -import { getPnpmLockfileParser } from './lockfile-parser/index'; -import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser'; -import { NodeLockfileVersion } from '../../utils'; +import { parsePnpmProject } from './parse-project'; +import { parsePnpmWorkspace } from './parse-workspace'; -export const parsePnpmProject = async ( - pkgJsonContent: string, - pnpmLockContent: string, - options: PnpmProjectParseOptions, - lockfileVersion?: NodeLockfileVersion, - workspaceArgs?: PnpmWorkspaceArgs, -): Promise => { - const { - includeDevDeps, - includeOptionalDeps, - strictOutOfSync, - pruneWithinTopLevelDeps, - } = options; - - const pkgJson: PackageJsonBase = parsePkgJson(pkgJsonContent); - - const lockFileParser: PnpmLockfileParser = getPnpmLockfileParser( - pnpmLockContent, - lockfileVersion, - workspaceArgs, - ); - - const depgraph = await buildDepGraphPnpm( - lockFileParser, - pkgJson, - { - includeDevDeps, - strictOutOfSync, - includeOptionalDeps, - pruneWithinTopLevelDeps, - }, - workspaceArgs, - ); - - return depgraph; -}; +export { parsePnpmProject, parsePnpmWorkspace }; diff --git a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-parser.ts b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-parser.ts index be99f37d..0ffb0037 100644 --- a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-parser.ts +++ b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-parser.ts @@ -15,47 +15,28 @@ export abstract class PnpmLockfileParser { public lockFileVersion: string; public rawPnpmLock: any; public packages: Record; - public dependencies: Record; - public devDependencies: Record; - public optionalDependencies: Record; - public peerDependencies: Record; public extractedPackages: NormalisedPnpmPkgs; public importers: PnpmImporters; public workspaceArgs?: PnpmWorkspaceArgs; public constructor(rawPnpmLock: any, workspaceArgs?: PnpmWorkspaceArgs) { this.rawPnpmLock = rawPnpmLock; - this.lockFileVersion = rawPnpmLock.lockFileVersion; + this.lockFileVersion = rawPnpmLock.lockfileVersion; this.workspaceArgs = workspaceArgs; - const depsRoot = this.getRoot(rawPnpmLock); this.packages = rawPnpmLock.packages || {}; - this.dependencies = depsRoot.dependencies || {}; - this.devDependencies = depsRoot.devDependencies || {}; - this.optionalDependencies = depsRoot.optionalDependencies || {}; - this.peerDependencies = depsRoot.peerDependencies || {}; this.extractedPackages = {}; this.importers = this.normaliseImporters(rawPnpmLock); } public isWorkspaceLockfile() { - return this.workspaceArgs?.isWorkspacePkg; - } - - public getRoot(rawPnpmLock: any) { - let depsRoot = rawPnpmLock; - if (this.workspaceArgs?.isWorkspacePkg) { - depsRoot = rawPnpmLock.importers[this.workspaceArgs.workspacePath]; - } - if (this.workspaceArgs?.isRoot) { - if (!this.workspaceArgs.workspacePath) { - this.workspaceArgs.workspacePath = '.'; - } - depsRoot = rawPnpmLock.importers[this.workspaceArgs.workspacePath]; - } - return depsRoot; + return this.workspaceArgs?.isWorkspace; } public extractPackages() { + // Packages should be parsed only one time for a parser + if (Object.keys(this.extractedPackages).length > 0) { + return this.extractedPackages; + } const packages: NormalisedPnpmPkgs = {}; Object.entries(this.packages).forEach( ([depPath, versionData]: [string, any]) => { @@ -81,42 +62,38 @@ export abstract class PnpmLockfileParser { return packages; } - public extractTopLevelDependencies(options: { - includeDevDeps: boolean; - includeOptionalDeps?: boolean; - includePeerDeps?: boolean; - }): PnpmDeps { - let importerName; - if (this.isWorkspaceLockfile()) { - importerName = this.workspaceArgs?.workspacePath; + public extractTopLevelDependencies( + options: { + includeDevDeps: boolean; + includeOptionalDeps?: boolean; + includePeerDeps?: boolean; + }, + importer?: string, + ): PnpmDeps { + let root = this.rawPnpmLock; + if (importer) { + root = this.rawPnpmLock.importers[importer]; } + const prodDeps = this.normalizeTopLevelDeps( - this.dependencies || {}, + root.dependencies || {}, false, - importerName, + importer, ); const devDeps = options.includeDevDeps - ? this.normalizeTopLevelDeps( - this.devDependencies || {}, - true, - importerName, - ) + ? this.normalizeTopLevelDeps(root.devDependencies || {}, true, importer) : {}; const optionalDeps = options.includeOptionalDeps ? this.normalizeTopLevelDeps( - this.optionalDependencies || {}, + root.optionalDependencies || {}, false, - importerName, + importer, ) : {}; const peerDeps = options.includePeerDeps - ? this.normalizeTopLevelDeps( - this.peerDependencies || {}, - false, - importerName, - ) + ? this.normalizeTopLevelDeps(root.peerDependencies || {}, false, importer) : {}; return { ...prodDeps, ...devDeps, ...optionalDeps, ...peerDeps }; diff --git a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v5.ts b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v5.ts index 583a03d3..ac9f256b 100644 --- a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v5.ts +++ b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v5.ts @@ -4,12 +4,8 @@ import { PnpmLockfileParser } from './lockfile-parser'; import { PnpmWorkspaceArgs } from '../../types'; export class LockfileV5Parser extends PnpmLockfileParser { - public specifiers: Record; - public constructor(rawPnpmLock: any, workspaceArgs?: PnpmWorkspaceArgs) { super(rawPnpmLock, workspaceArgs); - const depsRoot = this.getRoot(rawPnpmLock); - this.specifiers = depsRoot.specifiers; } public parseDepPath(depPath: string): ParsedDepPath { @@ -36,7 +32,6 @@ export class LockfileV5Parser extends PnpmLockfileParser { name, version, isDev, - specifier: this.specifiers[name], }; return pnpmDeps; }, diff --git a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v6.ts b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v6.ts index bc91a201..57dd34d5 100644 --- a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v6.ts +++ b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v6.ts @@ -51,7 +51,6 @@ export class LockfileV6Parser extends PnpmLockfileParser { pnpmDeps[name] = { name, version, - specifier: depInfo.specifier, isDev, }; return pnpmDeps; diff --git a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v9.ts b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v9.ts index a254279a..a00ebdde 100644 --- a/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v9.ts +++ b/lib/dep-graph-builders/pnpm/lockfile-parser/lockfile-v9.ts @@ -2,11 +2,8 @@ import { PnpmWorkspaceArgs } from '../../types'; import { LockfileV6Parser } from './lockfile-v6'; const DEFAULT_WORKSPACE_ARGS: PnpmWorkspaceArgs = { - isWorkspacePkg: true, - isRoot: true, - workspacePath: '.', + isWorkspace: true, projectsVersionMap: {}, - rootOverrides: {}, }; export class LockfileV9Parser extends LockfileV6Parser { public settings; diff --git a/lib/dep-graph-builders/pnpm/parse-project.ts b/lib/dep-graph-builders/pnpm/parse-project.ts new file mode 100644 index 00000000..0abb3d28 --- /dev/null +++ b/lib/dep-graph-builders/pnpm/parse-project.ts @@ -0,0 +1,47 @@ +import { parsePkgJson } from '../util'; +import { PackageJsonBase, PnpmProjectParseOptions } from '../types'; +import { buildDepGraphPnpm } from './build-dep-graph-pnpm'; +import { DepGraph } from '@snyk/dep-graph'; +import { getPnpmLockfileParser } from './lockfile-parser/index'; +import { NodeLockfileVersion } from '../../utils'; + +export const parsePnpmProject = async ( + pkgJsonContent: string, + pnpmLockContent: string, + options: PnpmProjectParseOptions, + lockfileVersion?: NodeLockfileVersion, +): Promise => { + const { + includeDevDeps, + includeOptionalDeps, + strictOutOfSync, + pruneWithinTopLevelDeps, + } = options; + let importer = ''; + + const pkgJson: PackageJsonBase = parsePkgJson(pkgJsonContent); + + const lockFileParser = getPnpmLockfileParser( + pnpmLockContent, + lockfileVersion, + ); + + // Lockfile V9 simple project has the root importer + if (lockFileParser.lockFileVersion.startsWith('9')) { + importer = '.'; + } + + const depgraph = await buildDepGraphPnpm( + lockFileParser, + pkgJson, + { + includeDevDeps, + strictOutOfSync, + includeOptionalDeps, + pruneWithinTopLevelDeps, + }, + importer, + ); + + return depgraph; +}; diff --git a/lib/dep-graph-builders/pnpm/parse-workspace.ts b/lib/dep-graph-builders/pnpm/parse-workspace.ts new file mode 100644 index 00000000..7568ee29 --- /dev/null +++ b/lib/dep-graph-builders/pnpm/parse-workspace.ts @@ -0,0 +1,114 @@ +import * as debugModule from 'debug'; +import * as path from 'path'; +import { parsePkgJson } from '../util'; +import { + PackageJsonBase, + PnpmProjectParseOptions, + ScannedNodeProject, +} from '../types'; +import { buildDepGraphPnpm } from './build-dep-graph-pnpm'; +import { DepGraph } from '@snyk/dep-graph'; +import { getPnpmLockfileParser } from './lockfile-parser/index'; +import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser'; +import { getPnpmLockfileVersion } from '../../utils'; +import { getFileContents } from './utils'; + +const debug = debugModule('snyk-pnpm-workspaces'); + +// Compute project versions map +// This is needed because the lockfile doesn't present the version of +// a project that's part of a workspace, we need to retrieve it from +// its corresponding package.json +function computeProjectVersionMaps(root: string, targets: string[]) { + const projectsVersionMap = {}; + for (const target of targets) { + const directory = path.join(root, target); + const packageJsonFileName = path.join(directory, 'package.json'); + const packageJson = getFileContents(root, packageJsonFileName); + + try { + const parsedPkgJson = parsePkgJson(packageJson.content); + const projectVersion = parsedPkgJson.version; + projectsVersionMap[target] = projectVersion; + } catch (err: any) { + debug( + `Error getting version for project: ${packageJsonFileName}. ERROR: ${err}`, + ); + continue; + } + } + return projectsVersionMap; +} + +export const parsePnpmWorkspace = async ( + root: string, + workspaceDir: string, + options: PnpmProjectParseOptions, +) => { + const scannedProjects: ScannedNodeProject[] = []; + const { + includeDevDeps, + includeOptionalDeps, + strictOutOfSync, + pruneWithinTopLevelDeps, + } = options; + + const pnpmLockfileContents = getFileContents( + root, + path.join(workspaceDir, 'pnpm-lock.yaml'), + ).content; + + const lockfileVersion = getPnpmLockfileVersion(pnpmLockfileContents); + const lockFileParser: PnpmLockfileParser = getPnpmLockfileParser( + pnpmLockfileContents, + lockfileVersion, + ); + + const projectVersionsMaps = computeProjectVersionMaps( + workspaceDir, + Object.keys(lockFileParser.importers), + ); + + for (const importer of Object.keys(lockFileParser.importers)) { + const resolvedImporterPath = path.join(workspaceDir, importer); + const pkgJsonFile = getFileContents( + root, + path.join(resolvedImporterPath, 'package.json'), + ); + + const pkgJson: PackageJsonBase = parsePkgJson(pkgJsonFile.content); + + lockFileParser.workspaceArgs = { + isWorkspace: true, + projectsVersionMap: projectVersionsMaps, + }; + + try { + const depGraph: DepGraph = await buildDepGraphPnpm( + lockFileParser, + pkgJson, + { + includeDevDeps, + strictOutOfSync, + includeOptionalDeps, + pruneWithinTopLevelDeps, + }, + importer, + ); + + const project: ScannedNodeProject = { + packageManager: 'pnpm', + targetFile: path.relative(root, pkgJsonFile.fileName), + depGraph, + plugin: { + name: 'snyk-nodejs-lockfile-parser', + runtime: process.version, + }, + }; + scannedProjects.push(project); + } catch (e) { + debug(`Error process workspace: ${pkgJsonFile.fileName}. ERROR: ${e}`); + } + } + return scannedProjects; +}; diff --git a/lib/dep-graph-builders/pnpm/utils.ts b/lib/dep-graph-builders/pnpm/utils.ts index 1dad4094..6fa31c6a 100644 --- a/lib/dep-graph-builders/pnpm/utils.ts +++ b/lib/dep-graph-builders/pnpm/utils.ts @@ -1,3 +1,5 @@ +import * as path from 'path'; +import * as fs from 'fs'; import { LockfileType } from '../..'; import { getGraphDependencies } from '../util'; import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser'; @@ -77,3 +79,23 @@ export const getPnpmChildNode = ( }; } }; + +export function getFileContents( + root: string, + fileName: string, +): { + content: string; + fileName: string; +} { + const fullPath = path.resolve(root, fileName); + if (!fs.existsSync(fullPath)) { + throw new Error( + 'Manifest ' + fileName + ' not found at location: ' + fileName, + ); + } + const content = fs.readFileSync(fullPath, 'utf-8'); + return { + content, + fileName, + }; +} diff --git a/lib/dep-graph-builders/types.ts b/lib/dep-graph-builders/types.ts index cb00bd22..06c54473 100644 --- a/lib/dep-graph-builders/types.ts +++ b/lib/dep-graph-builders/types.ts @@ -1,3 +1,5 @@ +import { DepGraph } from '@snyk/dep-graph'; + // Common types export type PackageJsonBase = { name: string; @@ -81,11 +83,8 @@ export type Yarn1DepGraphBuildOptions = { }; export type PnpmWorkspaceArgs = { - isWorkspacePkg: boolean; - isRoot: boolean; - workspacePath: string; + isWorkspace: boolean; projectsVersionMap: Record; - rootOverrides: Overrides; }; export type PnpmProjectParseOptions = { @@ -94,3 +93,15 @@ export type PnpmProjectParseOptions = { strictOutOfSync: boolean; pruneWithinTopLevelDeps: boolean; }; + +type NodePkgManagers = 'npm' | 'yarn' | 'pnpm'; + +export type ScannedNodeProject = { + packageManager: NodePkgManagers; + targetFile: string; + depGraph: DepGraph; + plugin: { + name: string; + runtime: string; + }; +}; diff --git a/lib/index.ts b/lib/index.ts index 1fd190a7..8e25d9fe 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -52,6 +52,7 @@ import { parseYarnLockV2Project, buildDepGraphYarnLockV2Simple, parsePnpmProject, + parsePnpmWorkspace, parsePkgJson, } from './dep-graph-builders'; import { getPnpmLockfileParser } from './dep-graph-builders/pnpm/lockfile-parser'; @@ -82,6 +83,7 @@ export { buildDepGraphYarnLockV2Simple, getPnpmLockfileParser, parsePnpmProject, + parsePnpmWorkspace, parsePkgJson, PackageJsonBase, ProjectParseOptions, diff --git a/lib/utils.ts b/lib/utils.ts index a171930b..fbc479cb 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -27,7 +27,7 @@ export const getLockfileVersionFromFile = ( } else { throw new InvalidUserInputError( `Unknown lockfile ${targetFile}. ` + - 'Please provide either package-lock.json or yarn.lock.', + 'Please provide either package-lock.json, yarn.lock or pnpm-lock.yaml', ); } }; diff --git a/test/jest/dep-graph-builders/pnpm-lock.test.ts b/test/jest/dep-graph-builders/pnpm-lock.test.ts index 183aa816..1a0f18d2 100644 --- a/test/jest/dep-graph-builders/pnpm-lock.test.ts +++ b/test/jest/dep-graph-builders/pnpm-lock.test.ts @@ -1,13 +1,7 @@ import { join } from 'path'; import { readFileSync } from 'fs'; import { parsePnpmProject } from '../../../lib/dep-graph-builders'; -import { NodeLockfileVersion } from '../../../lib/utils'; -const LOCK_FILE_VERSIONS = { - 'pnpm-lock-v6': NodeLockfileVersion.PnpmLockV6, - 'pnpm-lock-v5': NodeLockfileVersion.PnpmLockV5, - 'pnpm-lock-v9': NodeLockfileVersion.PnpmLockV9, -}; describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])( 'dep-graph-builder %s', (lockFileVersionPath) => { @@ -76,302 +70,6 @@ describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])( }); }); }); - - describe('[workspaces tests]', () => { - it('isolated packages in workspaces - test workspace package.json', async () => { - const fixtureName = 'workspace-with-isolated-pkgs'; - const pkgJsonContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkg-b/package.json`, - ), - 'utf8', - ); - const pkgLockContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/pnpm-lock.yaml`, - ), - 'utf8', - ); - const newDepGraph = await parsePnpmProject( - pkgJsonContent, - pkgLockContent, - { - includeDevDeps: false, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - LOCK_FILE_VERSIONS[lockFileVersionPath], - { - isWorkspacePkg: true, - isRoot: false, - workspacePath: 'packages/pkg-b', - projectsVersionMap: {}, - rootOverrides: {}, - }, - ); - const expectedDepGraphJson = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkg-b/expected.json`, - ), - 'utf8', - ), - ); - expect( - Buffer.from(JSON.stringify(newDepGraph)).toString('base64'), - ).toBe( - Buffer.from(JSON.stringify(expectedDepGraphJson)).toString( - 'base64', - ), - ); - }); - it('isolated packages in workspaces - test root package.json', async () => { - const fixtureName = 'workspace-with-isolated-pkgs'; - const pkgJsonContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/package.json`, - ), - 'utf8', - ); - const pkgLockContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/pnpm-lock.yaml`, - ), - 'utf8', - ); - const newDepGraph = await parsePnpmProject( - pkgJsonContent, - pkgLockContent, - { - includeDevDeps: false, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - LOCK_FILE_VERSIONS[lockFileVersionPath], - { - isWorkspacePkg: true, - isRoot: true, - workspacePath: '.', - projectsVersionMap: {}, - rootOverrides: {}, - }, - ); - const expectedDepGraphJson = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/expected.json`, - ), - 'utf8', - ), - ); - expect( - Buffer.from(JSON.stringify(newDepGraph)).toString('base64'), - ).toBe( - Buffer.from(JSON.stringify(expectedDepGraphJson)).toString( - 'base64', - ), - ); - }); - it('cross ref packages in workspaces', async () => { - const fixtureName = 'workspace-with-cross-ref'; - const pkgJsonContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/package.json`, - ), - 'utf8', - ); - const pkgLockContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/pnpm-lock.yaml`, - ), - 'utf8', - ); - const newDepGraph = await parsePnpmProject( - pkgJsonContent, - pkgLockContent, - { - includeDevDeps: false, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - LOCK_FILE_VERSIONS[lockFileVersionPath], - { - isWorkspacePkg: true, - isRoot: false, - workspacePath: 'packages/pkgs/pkg-a', - projectsVersionMap: { - '.': '1.0.0', - 'packages/pkgs/pkg-a': '1.0.0', - 'packages/pkgs/pkg-b': '1.0.0', - 'other-packages/pkg-c': '1.0.0', - }, - rootOverrides: {}, - }, - ); - const expectedDepGraphJson = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/expected.json`, - ), - 'utf8', - ), - ); - expect( - Buffer.from(JSON.stringify(newDepGraph)).toString('base64'), - ).toBe( - Buffer.from(JSON.stringify(expectedDepGraphJson)).toString( - 'base64', - ), - ); - }); - - it('undefined versions in cross ref packages in workspaces', async () => { - const fixtureName = 'workspace-undefined-versions'; - const pkgJsonContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/package.json`, - ), - 'utf8', - ); - const pkgLockContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/pnpm-lock.yaml`, - ), - 'utf8', - ); - const newDepGraph = await parsePnpmProject( - pkgJsonContent, - pkgLockContent, - { - includeDevDeps: false, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - LOCK_FILE_VERSIONS[lockFileVersionPath], - { - isWorkspacePkg: true, - isRoot: false, - workspacePath: 'packages/pkgs/pkg-a', - projectsVersionMap: { - '.': '1.0.0', - }, - rootOverrides: {}, - }, - ); - const expectedDepGraphJson = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/expected.json`, - ), - 'utf8', - ), - ); - expect( - Buffer.from(JSON.stringify(newDepGraph)).toString('base64'), - ).toBe( - Buffer.from(JSON.stringify(expectedDepGraphJson)).toString( - 'base64', - ), - ); - }); - - // Dev Dep tests - describe.each(['only-dev-deps', 'empty-dev-deps'])( - '[dev deps tests] project: %s ', - (fixtureName) => { - test('matches expected', async () => { - const pkgJsonContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/package.json`, - ), - 'utf8', - ); - const pnpmLockContent = readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/pnpm-lock.yaml`, - ), - 'utf8', - ); - const newDepGraphDevDepsIncluded = await parsePnpmProject( - pkgJsonContent, - pnpmLockContent, - { - includeDevDeps: true, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - ); - const newDepGraphDevDepsExcluded = await parsePnpmProject( - pkgJsonContent, - pnpmLockContent, - { - includeDevDeps: false, - includeOptionalDeps: true, - pruneWithinTopLevelDeps: true, - strictOutOfSync: false, - }, - ); - const expectedDepGraphJsonDevIncluded = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/expected-dev-deps-included.json`, - ), - 'utf8', - ), - ); - const expectedDepGraphJsonDevExcluded = JSON.parse( - readFileSync( - join( - __dirname, - `./fixtures/${lockFileVersionPath}/${fixtureName}/expected-dev-deps-excluded.json`, - ), - 'utf8', - ), - ); - - expect( - Buffer.from( - JSON.stringify(newDepGraphDevDepsIncluded), - ).toString('base64'), - ).toBe( - Buffer.from( - JSON.stringify(expectedDepGraphJsonDevIncluded), - ).toString('base64'), - ); - - expect( - Buffer.from( - JSON.stringify(newDepGraphDevDepsExcluded), - ).toString('base64'), - ).toBe( - Buffer.from( - JSON.stringify(expectedDepGraphJsonDevExcluded), - ).toString('base64'), - ); - }); - }, - ); - }); }); describe('Unhappy path tests', () => { it('project: invalid-pkg-json -> fails as expected', async () => { diff --git a/test/jest/dep-graph-builders/pnpm-workspaces.test.ts b/test/jest/dep-graph-builders/pnpm-workspaces.test.ts new file mode 100644 index 00000000..7f6b6191 --- /dev/null +++ b/test/jest/dep-graph-builders/pnpm-workspaces.test.ts @@ -0,0 +1,129 @@ +import { join } from 'path'; +import { readFileSync } from 'fs'; +import { parsePnpmWorkspace } from '../../../lib/dep-graph-builders'; + +describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])( + 'dep-graph-builder %s', + (lockFileVersionPath) => { + describe('[workspaces tests]', () => { + it('isolated packages in workspaces - test workspace package.json', async () => { + const fixtureName = 'workspace-with-isolated-pkgs'; + const result = await parsePnpmWorkspace( + __dirname, + join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`), + { + includeDevDeps: false, + includeOptionalDeps: true, + pruneWithinTopLevelDeps: true, + strictOutOfSync: false, + }, + ); + + const expectedRootDepGraphJson = JSON.parse( + readFileSync( + join( + __dirname, + `./fixtures/${lockFileVersionPath}/${fixtureName}/expected.json`, + ), + 'utf8', + ), + ); + + const expectedBDepGraphJson = JSON.parse( + readFileSync( + join( + __dirname, + `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkg-b/expected.json`, + ), + 'utf8', + ), + ); + + expect(result[0].targetFile.replace(/\\/g, '/')).toEqual( + `fixtures/${lockFileVersionPath}/${fixtureName}/package.json`, + ); + expect( + Buffer.from(JSON.stringify(result[0].depGraph)).toString('base64'), + ).toBe( + Buffer.from(JSON.stringify(expectedRootDepGraphJson)).toString( + 'base64', + ), + ); + + expect(result[2].targetFile.replace(/\\/g, '/')).toEqual( + `fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkg-b/package.json`, + ); + expect( + Buffer.from(JSON.stringify(result[2].depGraph)).toString('base64'), + ).toBe( + Buffer.from(JSON.stringify(expectedBDepGraphJson)).toString('base64'), + ); + }); + + it('cross ref packages in workspaces', async () => { + const fixtureName = 'workspace-with-cross-ref'; + const result = await parsePnpmWorkspace( + __dirname, + join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`), + { + includeDevDeps: false, + includeOptionalDeps: true, + pruneWithinTopLevelDeps: true, + strictOutOfSync: false, + }, + ); + + const expectedDepGraphJson = JSON.parse( + readFileSync( + join( + __dirname, + `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/expected.json`, + ), + 'utf8', + ), + ); + + expect(result[2].targetFile.replace(/\\/g, '/')).toEqual( + `fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/package.json`, + ); + expect( + Buffer.from(JSON.stringify(result[2].depGraph)).toString('base64'), + ).toBe( + Buffer.from(JSON.stringify(expectedDepGraphJson)).toString('base64'), + ); + }); + + it('undefined versions in cross ref packages in workspaces', async () => { + const fixtureName = 'workspace-undefined-versions'; + const result = await parsePnpmWorkspace( + __dirname, + join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`), + { + includeDevDeps: false, + includeOptionalDeps: true, + pruneWithinTopLevelDeps: true, + strictOutOfSync: false, + }, + ); + const expectedDepGraphJson = JSON.parse( + readFileSync( + join( + __dirname, + `./fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/expected.json`, + ), + 'utf8', + ), + ); + + expect(result[2].targetFile.replace(/\\/g, '/')).toEqual( + `fixtures/${lockFileVersionPath}/${fixtureName}/packages/pkgs/pkg-a/package.json`, + ); + expect( + Buffer.from(JSON.stringify(result[2].depGraph)).toString('base64'), + ).toBe( + Buffer.from(JSON.stringify(expectedDepGraphJson)).toString('base64'), + ); + }); + }); + }, +);