Skip to content
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

feat: [OSM-1024] pnpm dep graph builder #218

Merged
merged 1 commit into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
128 changes: 72 additions & 56 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ orbs:
win: circleci/windows@2.4.0
prodsec: snyk/prodsec-orb@1.0

filters_branches_ignore_master: &filters_branches_ignore_master
filters:
branches:
ignore:
- master

defaults: &defaults
parameters:
node_version:
Expand All @@ -17,6 +23,9 @@ windows_defaults: &windows_defaults
executor:
name: win/default

test_matrix: &test_matrix
node_version: ['12.22.12', '14.17.6', '16.13.2']

commands:
install_deps:
description: Install dependencies
Expand Down Expand Up @@ -66,7 +75,7 @@ jobs:
name: Run lint
command: npm run lint

test-windows:
test-windows-jest:
<<: *defaults
<<: *windows_defaults
steps:
Expand All @@ -80,9 +89,39 @@ jobs:
- show_node_version
- run:
name: Run tests
command: npm test
command: npm run test:jest

test-unix:
test-windows-tap:
<<: *defaults
<<: *windows_defaults
steps:
- run: git config --global core.autocrlf false
- install_node_npm:
node_version: << parameters.node_version >>
- checkout
- attach_workspace:
at: ~/nodejs-lockfile-parser
- install_deps
- show_node_version
- run:
name: Run tests
command: npm run unit-test

test-unix-jest:
<<: *defaults
docker:
- image: cimg/node:<< parameters.node_version >>
steps:
- checkout
- attach_workspace:
at: ~/nodejs-lockfile-parser
- install_deps
- show_node_version
- run:
name: Run tests
command: npm run test:jest

test-unix-tap:
<<: *defaults
docker:
- image: cimg/node:<< parameters.node_version >>
Expand All @@ -94,7 +133,7 @@ jobs:
- show_node_version
- run:
name: Run tests
command: npm test
command: npm run unit-test

release:
<<: *defaults
Expand All @@ -121,70 +160,47 @@ workflows:
name: Lint
context: nodejs-install
node_version: "16.13.2"
filters:
branches:
ignore:
- master
- test-windows:
name: Windows Tests for Node v16 support
context: nodejs-install
node_version: "16.13.2"
requires:
- Lint
filters:
branches:
ignore:
- master
- test-windows:
name: Windows Tests for Node v14 support
context: nodejs-install
node_version: "14.17.6"
requires:
- Lint
filters:
branches:
ignore:
- master
- test-unix:
name: Unix Tests for Node v16 support
<<: *filters_branches_ignore_master
- test-windows-jest:
matrix:
alias: test-windows-jest
parameters:
<<: *test_matrix
name: Windows Tests (Jest) for Node=<< matrix.node_version >> support
context: nodejs-install
node_version: "16.13.2"
requires:
- Lint
filters:
branches:
ignore:
- master
- test-unix:
name: Unix Tests for Node v14 support
<<: *filters_branches_ignore_master
- test-windows-tap:
matrix:
alias: test-windows-tap
parameters:
<<: *test_matrix
name: Windows Tests (Tap) for Node=<< matrix.node_version >> support
context: nodejs-install
node_version: "14.17.6"
requires:
- Lint
filters:
branches:
ignore:
- master
- test-windows:
name: Windows Tests for Node v12 support
<<: *filters_branches_ignore_master
- test-unix-jest:
matrix:
alias: test-unix-jest
parameters:
<<: *test_matrix
name: Unix Tests (Jest) for Node=<< matrix.node_version >> support
context: nodejs-install
node_version: "12.22.12"
requires:
- Lint
filters:
branches:
ignore:
- master
- test-unix:
name: Unix Tests for Node v12 support
<<: *filters_branches_ignore_master
- test-unix-tap:
matrix:
alias: test-unix-tap
parameters:
<<: *test_matrix
name: Unix Tests (Tap) for Node=<< matrix.node_version >> support
context: nodejs-install
node_version: "12.22.12"
requires:
- Lint
filters:
branches:
ignore:
- master
<<: *filters_branches_ignore_master
- release:
name: Release
context: nodejs-app-release
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ dist
.idea
.nyc_output/
.dccache
coverage/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Dep graph generation supported for:

- `package-lock.json` (at Versions 2 and 3)
- `yarn.lock`
- `pnpm-lock.yaml` (lockfileVersion 5.x or 6.x)

Legacy dep tree supported for:

Expand Down
4 changes: 4 additions & 0 deletions lib/dep-graph-builders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
extractPkgsFromYarnLockV2,
} from './yarn-lock-v2';
import { parseNpmLockV2Project } from './npm-lock-v2';
import { parsePnpmProject } from './pnpm';
import { parsePkgJson } from './util';

export {
parseNpmLockV2Project,
Expand All @@ -26,4 +28,6 @@ export {
buildDepGraphYarnLockV2Simple,
parseYarnLockV2Project,
extractPkgsFromYarnLockV2,
parsePnpmProject,
parsePkgJson,
};
147 changes: 147 additions & 0 deletions lib/dep-graph-builders/pnpm/build-dep-graph-pnpm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { DepGraphBuilder } from '@snyk/dep-graph';
import { getTopLevelDeps } from '../util';
import type {
Overrides,
PnpmProjectParseOptions,
PnpmWorkspaceArgs,
} from '../types';
import type { PackageJsonBase } from '../types';
import { getPnpmChildNode } from './utils';
import { eventLoopSpinner } from 'event-loop-spinner';
import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser';
import { NormalisedPnpmPkgs, PnpmNode } from './types';

export const buildDepGraphPnpm = async (
lockFileParser: PnpmLockfileParser,
pkgJson: PackageJsonBase,
options: PnpmProjectParseOptions,
workspaceArgs?: PnpmWorkspaceArgs,
) => {
const { strictOutOfSync, includeOptionalDeps, pruneWithinTopLevelDeps } =
options;

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

const extractedPnpmPkgs: NormalisedPnpmPkgs =
lockFileParser.extractedPackages;

const topLevelDeps = getTopLevelDeps(pkgJson, options);

const extractedTopLevelDeps =
lockFileParser.extractTopLevelDependencies(options) || {};

for (const name of Object.keys(topLevelDeps)) {
topLevelDeps[name].version = extractedTopLevelDeps[name].version;
}

const rootNode: PnpmNode = {
id: 'root-node',
name: pkgJson.name,
version: pkgJson.version,
dependencies: topLevelDeps,
isDev: false,
};

await dfsVisit(
depGraphBuilder,
rootNode,
extractedPnpmPkgs,
strictOutOfSync,
includeOptionalDeps,
// 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 || {},
pruneWithinTopLevelDeps,
lockFileParser,
);

return depGraphBuilder.build();
};

/**
* Use DFS to add all nodes and edges to the depGraphBuilder and prune cyclic nodes.
* The visitedMap keep track of which nodes have already been discovered during traversal.
* - If a node doesn't exist in the map, it means it hasn't been visited.
* - If a node is already visited, simply connect the new node with this node.
*/
const dfsVisit = async (
depGraphBuilder: DepGraphBuilder,
node: PnpmNode,
extractedPnpmPkgs: NormalisedPnpmPkgs,
strictOutOfSync: boolean,
includeOptionalDeps: boolean,
overrides: Overrides,
pruneWithinTopLevel: boolean,
lockFileParser: PnpmLockfileParser,
visited?: Set<string>,
): Promise<void> => {
for (const [name, depInfo] of Object.entries(node.dependencies || {})) {
if (eventLoopSpinner.isStarving()) {
await eventLoopSpinner.spin();
}

const localVisited = visited || new Set<string>();

const childNode: PnpmNode = getPnpmChildNode(
name,
depInfo,
extractedPnpmPkgs,
strictOutOfSync,
includeOptionalDeps,
lockFileParser,
);

if (localVisited.has(childNode.id)) {
if (pruneWithinTopLevel) {
const prunedId = `${childNode.id}:pruned`;
depGraphBuilder.addPkgNode(
{ name: childNode.name, version: childNode.version },
prunedId,
{
labels: {
scope: childNode.isDev ? 'dev' : 'prod',
pruned: 'true',
...(node.missingLockFileEntry && {
missingLockFileEntry: 'true',
}),
},
},
);
depGraphBuilder.connectDep(node.id, prunedId);
} else {
depGraphBuilder.connectDep(node.id, childNode.id);
}
continue;
}

depGraphBuilder.addPkgNode(
{ name: childNode.name, version: childNode.version },
childNode.id,
{
labels: {
scope: childNode.isDev ? 'dev' : 'prod',
...(node.missingLockFileEntry && {
missingLockFileEntry: 'true',
}),
},
},
);
depGraphBuilder.connectDep(node.id, childNode.id);
localVisited.add(childNode.id);
await dfsVisit(
depGraphBuilder,
childNode,
extractedPnpmPkgs,
strictOutOfSync,
includeOptionalDeps,
overrides,
pruneWithinTopLevel,
lockFileParser,
localVisited,
);
}
};