Skip to content

Added pnpm parser #217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pnpm 9.0.0-beta.1
nodejs 20.12.0
1 change: 1 addition & 0 deletions lib/dep-graph-builders/pnpm-lock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**NOTE**
71 changes: 71 additions & 0 deletions lib/dep-graph-builders/pnpm-lock/build-depgraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DepGraphBuilder } from '@snyk/dep-graph';
import { addPkgNodeToGraph, Dependencies, PkgNode } from '../util';
import type { PnpmLockV7ProjectParseOptions } from '../types';
import type { PackageJsonBase } from '../types';
import { eventLoopSpinner } from 'event-loop-spinner';
import { getChildNodePnpmLockV7Workspace } from './utils';
import type { PnpmNormalisedPkgs, PnpmNormalisedProject } from './type';

function mapWorkspaceToNode(
name: string,
version: string,
workspace: PnpmNormalisedPkgs,
): PkgNode {
const deps: Dependencies = {};
console.log(`workspace:`, workspace);

const nodeId = name === '.' ? 'root-node' : name;
return {
id: nodeId,
name,
version,
dependencies: deps,
isDev: false,
};
}

export const buildDepGraphPnpmLockV7Project = async (
extractedPnpmLockV7Project: PnpmNormalisedProject,
pkgJson: PackageJsonBase,
options: PnpmLockV7ProjectParseOptions,
) => {
const { includeDevDeps, includeOptionalDeps, strictOutOfSync } = options;

const depGraphBuilder = new DepGraphBuilder(
{ name: 'pnpm' },
{ name: pkgJson.name, version: pkgJson.version },
);

const workspaceNames = Object.keys(extractedPnpmLockV7Project);
for (const workspaceName of workspaceNames) {
const workspacePkg = extractedPnpmLockV7Project[workspaceName];
const workspaceNode = mapWorkspaceToNode(
pkgJson.name,
pkgJson.version,
workspacePkg,
);

await dfsVisit(
depGraphBuilder,
workspaceNode,
workspacePkg,
includeOptionalDeps,
strictOutOfSync,
);
}

return depGraphBuilder.build();
};

const dfsVisit = async (
depGraphBuilder: DepGraphBuilder,
node: PkgNode,
extractedPnpmLockV7Pkgs: PnpmNormalisedPkgs,
includeOptionalDeps: boolean,
strictOutOfSync: boolean,
visited?: Set<string>,
): Promise<void> => {
const localVisited = visited || new Set<string>();
console.log(localVisited);
console.log(node);
};
89 changes: 89 additions & 0 deletions lib/dep-graph-builders/pnpm-lock/extract-pnpmlock-v7-pkgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ProjectSnapshot, ResolvedDependencies } from '@pnpm/lockfile-types';
import {
SpecifierAndResolution,
parsePnpm7lockfile,
} from './parse-pnpm7-lock-file';
import { PnpmNormalisedPkgs, PnpmNormalisedProject } from './type';

function normaliseDependencies(deps): ResolvedDependencies {
if (!deps) {
return {};
}
return deps;
}

export const extractPkgsFromPnpmLockV7 = (
pnpmLockContent: string,
): PnpmNormalisedProject => {
const parsedLockFile = parsePnpm7lockfile(pnpmLockContent);
const { packages = {}, snapshots = {}, importers = {} } = parsedLockFile;

function retrieveDependencies(
deps: Record<string, SpecifierAndResolution> | undefined,
info: { optional: boolean; dev: boolean },
dependencies: PnpmNormalisedPkgs,
) {
if (deps) {
Object.keys(deps).forEach((pkgId) => {
const dependencyInfo: SpecifierAndResolution = deps?.[
pkgId
] as unknown as SpecifierAndResolution;
if (!dependencyInfo) {
return;
}

const packageSpecifier = `${pkgId}@${dependencyInfo.version}`;
const snapshotInfo = snapshots[packageSpecifier];
dependencies[pkgId] = {
version: dependencyInfo.version,
dependencies: normaliseDependencies(snapshotInfo.dependencies),
optionalDependencies: normaliseDependencies(
snapshotInfo.optionalDependencies,
),
dev: info.dev,
};
});
}
}

function normaliseWorkspacePackages(
workspace: ProjectSnapshot,
): PnpmNormalisedPkgs {
const workspaceDependencies: PnpmNormalisedPkgs = {};

retrieveDependencies(
workspace.dependencies as
| Record<string, SpecifierAndResolution>
| undefined,
{ optional: false, dev: false },
workspaceDependencies,
);

retrieveDependencies(
workspace.devDependencies as
| Record<string, SpecifierAndResolution>
| undefined,
{ optional: false, dev: true },
workspaceDependencies,
);

retrieveDependencies(
workspace.optionalDependencies as
| Record<string, SpecifierAndResolution>
| undefined,
{ optional: true, dev: false },
workspaceDependencies,
);

return workspaceDependencies;
}

const project: PnpmNormalisedProject = {};
Object.keys(importers).forEach((importerId) => {
const importerInfo = importers[importerId];
const normalisedPkgs = normaliseWorkspacePackages(importerInfo);
project[importerId] = normalisedPkgs;
});

return project;
};
9 changes: 9 additions & 0 deletions lib/dep-graph-builders/pnpm-lock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { buildDepGraphPnpmLockV7Project } from './build-depgraph';
import { extractPkgsFromPnpmLockV7 } from './extract-pnpmlock-v7-pkgs';
import { parsePnpmLockV7Project } from './project';

export {
parsePnpmLockV7Project,
buildDepGraphPnpmLockV7Project,
extractPkgsFromPnpmLockV7,
};
186 changes: 186 additions & 0 deletions lib/dep-graph-builders/pnpm-lock/parse-pnpm7-lock-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { load, FAILSAFE_SCHEMA } from 'js-yaml';
import {
LockfileResolution,
LockfileSettings,
PackageSnapshot,
PackageSnapshots,
PatchFile,
ProjectSnapshot,
} from '@pnpm/lockfile-types';
import * as semver from 'semver';

export const ROOT_PACKAGE_NAME = '.';

export type PackageInfo = Pick<
PackageSnapshot,
| 'id'
| 'patched'
| 'hasBin'
| 'name'
| 'version'
| 'resolution'
| 'peerDependencies'
| 'peerDependenciesMeta'
| 'bundledDependencies'
| 'engines'
| 'cpu'
| 'os'
| 'libc'
| 'deprecated'
>;

export type PackageSnapshotV7 = Pick<
PackageSnapshot,
| 'dev'
| 'optional'
| 'dependencies'
| 'optionalDependencies'
| 'transitivePeerDependencies'
>;

export interface InlineSpecifiersResolvedDependencies {
[depName: string]: SpecifierAndResolution;
}

export interface SpecifierAndResolution {
specifier: string;
version: string;
}

export interface LockfileV7 {
importers: Record<string, ProjectSnapshot>;
lockfileVersion: number | string;
time?: Record<string, string>;
snapshots?: Record<string, PackageSnapshotV7>;
packages?: Record<string, PackageInfo>;
neverBuiltDependencies?: string[];
onlyBuiltDependencies?: string[];
overrides?: Record<string, string>;
packageExtensionsChecksum?: string;
patchedDependencies?: Record<string, PatchFile>;
settings?: LockfileSettings;
}

function parsePkgSnapshot(
packageSpecifier: { name: string; version: string },
entry: any,
): PackageSnapshot {
return {
name: packageSpecifier.name,
version: packageSpecifier.version,
resolution: parseResolution(entry.resolution),
dependencies: entry.dependencies || {},
peerDependencies: entry.peerDependencies || {},
optionalDependencies: entry.optionalDependencies || {},
hasBin: entry.hasBin,
optional: entry.optional,
engines: entry.engines,
};
}

function parsePkgImporter(entry: any): ProjectSnapshot {
return {
specifiers: entry.specifiers || {},
dependencies: entry.dependencies || {},
optionalDependencies: entry.optionalDependencies || {},
devDependencies: entry.devDependencies || {},
dependenciesMeta: entry.dependenciesMeta || {},
publishDirectory: entry.publishDirectory,
};
}

function parseResolution(entry: any): LockfileResolution {
return {
integrity: entry.integrity,
tarball: entry.tarball,
directory: entry.directory,
repo: entry.repo,
commit: entry.commit,
};
}

function parseSnapshot(entry: any): PackageSnapshotV7 {
return {
dependencies: entry.dependencies || {},
};
}

export const parsePnpm7lockfile = (pnpmLockContent: string): LockfileV7 => {
const rawPnpmLock: any = load(pnpmLockContent, {
json: true,
schema: FAILSAFE_SCHEMA,
});

const { lockfileVersion, settings = {}, ...lockfileContents } = rawPnpmLock;

const packageSnapshots: Record<string, PackageSnapshotV7> = {};
if (typeof lockfileContents.snapshots !== 'undefined') {
Object.keys(lockfileContents.snapshots).forEach((snapshotId) => {
const pkgSnapshot = lockfileContents.snapshots[snapshotId];
packageSnapshots[snapshotId] = parseSnapshot(pkgSnapshot);
});
}

const packages: PackageSnapshots = {};
if (typeof lockfileContents.packages !== 'undefined') {
Object.keys(lockfileContents.packages).forEach((packageId) => {
const pkgInfo = lockfileContents.packages[packageId];
const packageSpecifier = parsePackageId(packageId);
if (packageSpecifier.name && packageSpecifier.version) {
packages[packageId] = parsePkgSnapshot(packageSpecifier, pkgInfo);
}
});
}

const importers: Record<string, ProjectSnapshot> = {};
if (typeof lockfileContents.importers !== 'undefined') {
Object.keys(lockfileContents.importers).forEach((importerId) => {
const pkgImport = lockfileContents.importers[importerId];
importers[importerId] = parsePkgImporter(pkgImport);
});
}

const lockfile: LockfileV7 = {
lockfileVersion,
settings,
importers,
packages: packages,
snapshots: packageSnapshots,
};

return lockfile;
};

function parsePackageId(dependencyPath: string) {
const sepIndex = dependencyPath.indexOf('@', 1);
if (sepIndex === -1) {
return {};
}

const name = dependencyPath.substring(0, sepIndex);
let version = dependencyPath.substring(sepIndex + 1);
if (version) {
let peerSepIndex!: number;
let peersSuffix: string | undefined;
if (version.includes('(') && version.endsWith(')')) {
peerSepIndex = version.indexOf('(');
if (peerSepIndex !== -1) {
peersSuffix = version.substring(peerSepIndex);
version = version.substring(0, peerSepIndex);
}
}
if (semver.valid(version)) {
return {
name,
peersSuffix,
version,
};
}
return {
name,
nonSemverVersion: version,
peersSuffix,
};
}
return {};
}
31 changes: 31 additions & 0 deletions lib/dep-graph-builders/pnpm-lock/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { extractPkgsFromPnpmLockV7 } from './extract-pnpmlock-v7-pkgs';
import { parsePkgJson } from '../util';
import { PackageJsonBase, PnpmLockV7ProjectParseOptions } from '../types';
import { buildDepGraphPnpmLockV7Project } from './build-depgraph';
import { DepGraph } from '@snyk/dep-graph';

export const parsePnpmLockV7Project = async (
pkgJsonContent: string,
pnpmLockContent: string,
options: PnpmLockV7ProjectParseOptions,
): Promise<DepGraph> => {
const {
includeDevDeps,
includeOptionalDeps,
strictOutOfSync,
pruneWithinTopLevelDeps,
} = options;

const pkgs = extractPkgsFromPnpmLockV7(pnpmLockContent);

const pkgJson: PackageJsonBase = parsePkgJson(pkgJsonContent);

const depgraph = await buildDepGraphPnpmLockV7Project(pkgs, pkgJson, {
includeDevDeps,
includeOptionalDeps,
strictOutOfSync,
pruneWithinTopLevelDeps,
});

return depgraph;
};
Loading