├── .nvmrc ├── .npmrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── .gitattributes ├── benchmark ├── tsconfig.json ├── .eslintrc.json ├── create-from-json.ts ├── pkg-paths-to-root.ts └── count-paths-to-root.ts ├── test ├── tsconfig.json ├── fixtures │ ├── cyclic-complex-dep-graph.png │ ├── changed-packages-graph │ │ ├── graph.png │ │ ├── graph-root-changed.png │ │ ├── graph-direct-dep-added.png │ │ ├── graph-direct-dep-changed.png │ │ ├── graph-direct-dep-removed.png │ │ ├── graph-root-changed-expected.png │ │ ├── graph-transitive-dep-changed.png │ │ ├── graph-transitive-dep-removed.png │ │ ├── graph-direct-dep-changed-cycle.png │ │ ├── graph-direct-dep-added-expected.png │ │ ├── graph-direct-dep-changed-expected.png │ │ ├── graph-direct-dep-removed-expected.png │ │ ├── graph-root-and-direct-dep-changed.png │ │ ├── graph-transitive-dep-as-direct-dep.png │ │ ├── graph-transitive-dep-changed-cycle.png │ │ ├── graph-transitive-dep-changed-expected.png │ │ ├── graph-transitive-dep-removed-expected.png │ │ ├── graph-direct-dep-changed-cycle-expected.png │ │ ├── graph-root-and-direct-dep-changed-expected.png │ │ ├── graph-transitive-dep-as-direct-dep-expected.png │ │ ├── graph-transitive-dep-changed-cycle-expected.png │ │ ├── graph-direct-dep-with-exiting-transitive-dep-added.png │ │ ├── graph-direct-dep-with-exiting-transitive-dep-added-expected.png │ │ ├── graph-direct-dep-removed-expected.json │ │ ├── graph-root-changed-expected.json │ │ ├── graph-transitive-dep-removed-expected.json │ │ ├── graph-transitive-dep-as-direct-dep-expected.json │ │ ├── graph-direct-dep-changed-cycle-expected.json │ │ ├── graph-root-and-direct-dep-changed-expected.json │ │ ├── graph-direct-dep-with-exiting-transitive-dep-added-expected.json │ │ ├── graph-direct-dep-changed-expected.json │ │ ├── graph-direct-dep-added-expected.json │ │ ├── graph-transitive-dep-changed-expected.json │ │ ├── graph-direct-dep-removed.json │ │ ├── graph-transitive-dep-changed-cycle-expected.json │ │ ├── graph-transitive-dep-removed.json │ │ ├── graph.json │ │ ├── graph-root-changed.json │ │ ├── graph-direct-dep-changed.json │ │ ├── graph-transitive-dep-changed.json │ │ ├── graph-direct-dep-changed-cycle.json │ │ ├── graph-transitive-dep-as-direct-dep.json │ │ ├── graph-transitive-dep-changed-cycle.json │ │ ├── graph-root-and-direct-dep-changed.json │ │ ├── graph-direct-dep-with-exiting-transitive-dep-added.json │ │ └── graph-direct-dep-added.json │ ├── cyclic-complex-dep-graph-expected-optimized-tree.png │ ├── os-linux-scratch-dep-tree.json │ ├── os-linux-scratch-dep-graph.json │ ├── npm-cyclic-dep-tree.json │ ├── equals │ │ ├── different-node-id-a.json │ │ ├── different-node-id-b.json │ │ ├── cycles │ │ │ ├── one-node-cycle-a.json │ │ │ ├── one-node-cycle-b.json │ │ │ ├── simple-a.json │ │ │ └── simple-b.json │ │ ├── simple-wrong-nodes-order-a.json │ │ ├── simple-wrong-nodes-order-b.json │ │ ├── simple.json │ │ ├── simple-different-root.json │ │ ├── simple-different-minor-verion.json │ │ ├── simple-one-more-child.json │ │ └── simple-with-label.json │ ├── unpruneable-tree.json │ ├── pruneable-tree-pruned.json │ ├── pruneable-tree.json │ ├── os-apk-dep-tree.json │ ├── os-apt-dep-tree.json │ ├── os-deb-dep-tree.json │ ├── os-rpm-dep-tree.json │ ├── simple-dep-tree.json │ ├── cyclic-dep-graph.json │ ├── labelled-dep-tree.json │ ├── pruneable-tree-multi-top-level-deps-pruned.json │ ├── pruneable-tree-multi-top-level-deps.json │ ├── cyclic-complex-dep-graph.json │ ├── old-schema-compat │ │ └── simple-graph-1.0.0.json │ ├── os-deb-graph.json │ ├── simple-graph.json │ ├── labelled-graph.json │ └── plain-dep-graph.json ├── core │ ├── is-transitive.test.ts │ ├── stress.test.ts │ ├── direct-deps-leading-to.test.ts │ ├── pkg-paths-to-root.test.ts │ ├── filter-from-graph.test.ts │ ├── create-changed-packages-graph.test.ts │ ├── __snapshots__ │ │ └── pkg-paths-to-root.test.ts.snap │ └── count-paths-to-root.test.ts ├── legacy │ ├── stress.test.ts │ ├── to-dep-tree-prune.test.ts │ └── __snapshots__ │ │ └── to-dep-tree.test.ts.snap └── helpers.ts ├── .prettierrc.json ├── .gitignore ├── src ├── core │ ├── errors │ │ ├── index.ts │ │ ├── validation-error.ts │ │ └── custom-error.ts │ ├── filter-from-graph.ts │ ├── builder.ts │ ├── create-changed-packages-graph.ts │ ├── types.ts │ └── create-from-json.ts ├── graphlib │ ├── alg │ │ ├── postorder.ts │ │ ├── is-acyclic.ts │ │ ├── topsort.ts │ │ └── dfs.ts │ ├── index.ts │ └── LICENSE-graphlib ├── index.ts └── legacy │ ├── cycles.ts │ └── memiozation.ts ├── .pre-commit-config.yaml ├── catalog-info.yaml ├── scripts ├── readme.md ├── to-graph.ts └── to-tree.ts ├── jest.config.js ├── go ├── go.mod ├── pkg │ └── depgraph │ │ ├── testdata │ │ └── snyk_dep_graph.json │ │ ├── builder.go │ │ └── builder_test.go └── go.sum ├── tsconfig.json ├── LICENSE ├── .eslintrc.json ├── package.json └── .circleci └── config.yml /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @snyk/analysis-arch @snyk/codesec_arch 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ignore Go checksum file 2 | go.sum linguist-generated -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | package-lock.json 5 | .idea/ 6 | benchmark/results 7 | isolate-* 8 | -------------------------------------------------------------------------------- /src/core/errors/index.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './validation-error'; 2 | 3 | export { ValidationError }; 4 | -------------------------------------------------------------------------------- /test/fixtures/cyclic-complex-dep-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/cyclic-complex-dep-graph.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph.png -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.16.1 4 | hooks: 5 | - id: gitleaks 6 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-root-changed.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-added.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-changed.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-removed.png -------------------------------------------------------------------------------- /test/fixtures/cyclic-complex-dep-graph-expected-optimized-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/cyclic-complex-dep-graph-expected-optimized-tree.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-changed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-root-changed-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-changed.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-removed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-removed.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle.png -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: dep-graph 5 | spec: 6 | type: external-library 7 | lifecycle: "-" 8 | owner: analysis-arch 9 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-added-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-added-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-changed-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-removed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-removed-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-changed-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-removed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-removed-expected.png -------------------------------------------------------------------------------- /src/graphlib/alg/postorder.ts: -------------------------------------------------------------------------------- 1 | import { dfs } from './dfs'; 2 | import { Graph } from '../graph'; 3 | 4 | export function postorder(g: Graph, vs: string[]): string[] { 5 | return dfs(g, vs, 'post'); 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle-expected.png -------------------------------------------------------------------------------- /benchmark/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc"], 3 | "parserOptions": { 4 | "project": "./benchmark/tsconfig.json" 5 | }, 6 | "rules": { 7 | "no-console": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep-expected.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle-expected.png -------------------------------------------------------------------------------- /src/graphlib/index.ts: -------------------------------------------------------------------------------- 1 | export { Graph } from './graph'; 2 | import { isAcyclic } from './alg/is-acyclic'; 3 | import { postorder } from './alg/postorder'; 4 | 5 | export const alg = { 6 | isAcyclic, 7 | postorder, 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added.png -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added-expected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snyk/dep-graph/HEAD/test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added-expected.png -------------------------------------------------------------------------------- /src/core/errors/validation-error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from './custom-error'; 2 | 3 | export class ValidationError extends CustomError { 4 | constructor(message: string) { 5 | super(message); 6 | Object.setPrototypeOf(this, ValidationError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/os-linux-scratch-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-image|scratch", 3 | "version": "0", 4 | "type": "linux", 5 | "packageFormatVersion": "linux:0.0.1", 6 | "targetOS": { 7 | "name": "unknown", 8 | "version": "0.0" 9 | } 10 | } -------------------------------------------------------------------------------- /src/core/errors/custom-error.ts: -------------------------------------------------------------------------------- 1 | export class CustomError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | Object.setPrototypeOf(this, CustomError.prototype); 5 | Error.captureStackTrace(this, this.constructor); 6 | this.name = this.constructor.name; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | ### Note 2 | 3 | Scripts are written in TypeScript. To run them use `npx ts-node`. 4 | 5 | Examples: 6 | 7 | ```bash 8 | cat test/fixtures/goof-dep-tree.json | npx ts-node ./scripts/to-graph.ts npm 9 | cat test/fixtures/goof-graph.json | npx ts-node ./scripts/to-tree.ts npm 10 | ``` 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - `node -v`: 2 | - `npm -v`: 3 | 4 | ### Expected behaviour 5 | 6 | 7 | ### Actual behaviour 8 | 9 | 10 | ### Steps to reproduce 11 | 12 | 13 | --- 14 | 15 | If applicable, please append any error output here **ensuring to remove any sensitive/personal details or tokens. 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: false, // Enabled by running `npm run test:coverage` 5 | collectCoverageFrom: [ 'src/**/*.ts' ], 6 | coverageReporters: ['text-summary', 'html'], 7 | testPathIgnorePatterns: ['/src/', '/node_modules/'], 8 | }; 9 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/snyk/dep-graph/go 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/elliotchance/orderedmap/v3 v3.1.0 7 | github.com/stretchr/testify v1.8.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /src/graphlib/alg/is-acyclic.ts: -------------------------------------------------------------------------------- 1 | import { topsort, CycleException } from './topsort'; 2 | import { Graph } from '../graph'; 3 | 4 | export function isAcyclic(g: Graph) { 5 | try { 6 | topsort(g); 7 | } catch (e) { 8 | if (e instanceof CycleException) { 9 | return false; 10 | } 11 | throw e; 12 | } 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "pretty": true, 5 | "target": "es2018", 6 | "lib": [ 7 | "es2018" 8 | ], 9 | "allowJs": true, 10 | "module": "commonjs", 11 | "sourceMap": true, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "declaration": true, 15 | "importHelpers": true 16 | }, 17 | "include": [ 18 | "./src/**/*", 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | DepGraph, 3 | DepGraphData, 4 | Pkg, 5 | PkgInfo, 6 | PkgManager, 7 | VersionProvenance, 8 | } from './core/types'; 9 | export { createFromJSON } from './core/create-from-json'; 10 | export { DepGraphBuilder } from './core/builder'; 11 | export { createChangedPackagesGraph } from './core/create-changed-packages-graph'; 12 | 13 | import * as Errors from './core/errors'; 14 | 15 | export { Errors }; 16 | 17 | import * as legacy from './legacy'; 18 | 19 | export { legacy }; 20 | -------------------------------------------------------------------------------- /benchmark/create-from-json.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node -r ts-node/register/transpile-only 2 | 3 | import { createFromJSON } from '../src'; 4 | import benny from 'benny'; 5 | import { readFileSync } from 'fs'; 6 | 7 | const doc = JSON.parse(readFileSync(process.argv[2], { encoding: 'utf-8' })); 8 | 9 | benny.suite( 10 | 'createFromJSON', 11 | 12 | benny.add('parsing arg', () => createFromJSON(doc)), 13 | 14 | benny.cycle(), 15 | benny.complete(), 16 | benny.save({ file: 'reduce', version: '1.0.0' }), 17 | ); 18 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-removed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | } 14 | ], 15 | "graph": { 16 | "rootNodeId": "root-node", 17 | "nodes": [ 18 | { 19 | "nodeId": "root-node", 20 | "pkgId": "a@1", 21 | "deps": [] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-changed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1.1", 9 | "info": { 10 | "name": "a", 11 | "version": "1.1" 12 | } 13 | } 14 | ], 15 | "graph": { 16 | "rootNodeId": "root-node", 17 | "nodes": [ 18 | { 19 | "nodeId": "root-node", 20 | "pkgId": "a@1.1", 21 | "deps": [] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-removed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | } 14 | ], 15 | "graph": { 16 | "rootNodeId": "root-node", 17 | "nodes": [ 18 | { 19 | "nodeId": "root-node", 20 | "pkgId": "a@1", 21 | "deps": [] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Ready for review 2 | - [ ] Follows [CONTRIBUTING](https://github.com/snyk/dep-graph/blob/master/.github/CONTRIBUTING.md) rules 3 | - [ ] Reviewed by Snyk internal team 4 | 5 | #### What does this PR do? 6 | 7 | 8 | #### Where should the reviewer start? 9 | 10 | 11 | #### How should this be manually tested? 12 | 13 | 14 | #### Any background context you want to provide? 15 | 16 | 17 | #### What are the relevant tickets? 18 | 19 | 20 | #### Screenshots 21 | 22 | 23 | #### Additional questions 24 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | } 14 | ], 15 | "graph": { 16 | "rootNodeId": "root-node", 17 | "nodes": [ 18 | { 19 | "nodeId": "root-node", 20 | "pkgId": "a@1", 21 | "deps": [] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Snyk Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test/fixtures/os-linux-scratch-dep-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "linux", 5 | "repositories": [ 6 | { 7 | "alias": "unknown:0.0" 8 | } 9 | ] 10 | }, 11 | "pkgs": [ 12 | { 13 | "id": "docker-image|scratch@0", 14 | "info": { 15 | "name": "docker-image|scratch", 16 | "version": "0" 17 | } 18 | } 19 | ], 20 | "graph": { 21 | "rootNodeId": "root-node", 22 | "nodes": [ 23 | { 24 | "nodeId": "root-node", 25 | "pkgId": "docker-image|scratch@0", 26 | "deps": [] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /scripts/to-graph.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | // tslint:disable:no-console 3 | 4 | import * as fs from 'fs'; 5 | import { legacy } from '../src'; 6 | 7 | const [STDIN, STDOUT] = [0, 1]; 8 | 9 | const pkgManager = process.argv[2]; 10 | if (!pkgManager) { 11 | console.error('requires package manager argument. e.g. npm'); 12 | console.log( 13 | 'usage: cat tree.json | npx ts-node to-graph.ts npm > graph.json', 14 | ); 15 | process.exit(1); 16 | } 17 | 18 | async function go() { 19 | const depTree = JSON.parse(fs.readFileSync(STDIN).toString()); 20 | const graph = await legacy.depTreeToGraph(depTree, pkgManager); 21 | fs.writeSync(STDOUT, JSON.stringify(graph)); 22 | } 23 | 24 | go().catch(console.error); 25 | -------------------------------------------------------------------------------- /scripts/to-tree.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | // tslint:disable:no-console 3 | 4 | import * as fs from 'fs'; 5 | import { legacy, createFromJSON } from '../src'; 6 | 7 | const [STDIN, STDOUT] = [0, 1]; 8 | 9 | const pkgType = process.argv[2]; 10 | if (!pkgType) { 11 | console.error('requires package type argument. e.g. npm'); 12 | console.log('usage: cat graph.json | npx ts-node to-tree.ts npm > tree.json'); 13 | process.exit(1); 14 | } 15 | 16 | async function go() { 17 | const graphData = JSON.parse(fs.readFileSync(STDIN).toString()); 18 | const graph = createFromJSON(graphData); 19 | const tree = await legacy.graphToDepTree(graph, pkgType); 20 | fs.writeSync(STDOUT, JSON.stringify(tree)); 21 | } 22 | 23 | go().catch(console.error); 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "cache": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "project": "./tsconfig.json" 8 | }, 9 | "env": { 10 | "node": true, 11 | "es6": true 12 | }, 13 | "plugins": ["@typescript-eslint"], 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/eslint-recommended", 17 | "plugin:@typescript-eslint/recommended", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/no-use-before-define": "off", 24 | 25 | "@typescript-eslint/no-unused-vars": "error", 26 | "no-buffer-constructor": "error" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/fixtures/npm-cyclic-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "debug": { 4 | "dependencies": { 5 | "ms": { 6 | "dependencies": { 7 | "debug": { 8 | "labels": { 9 | "scope": "prod", 10 | "pruned": "cyclic" 11 | }, 12 | "name": "debug", 13 | "version": "2.0.0" 14 | } 15 | }, 16 | "labels": { 17 | "scope": "prod" 18 | }, 19 | "name": "ms", 20 | "version": "0.6.2" 21 | } 22 | }, 23 | "labels": { 24 | "scope": "prod" 25 | }, 26 | "name": "debug", 27 | "version": "2.0.0" 28 | } 29 | }, 30 | "name": "trucolor", 31 | "version": "0.7.1" 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "c@3.1", 16 | "info": { 17 | "name": "c", 18 | "version": "3.1" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "3", 27 | "pkgId": "c@3.1", 28 | "deps": [] 29 | }, 30 | { 31 | "nodeId": "root-node", 32 | "pkgId": "a@1", 33 | "deps": [ 34 | { 35 | "nodeId": "3" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/graphlib/alg/topsort.ts: -------------------------------------------------------------------------------- 1 | import * as each from 'lodash.foreach'; 2 | import * as size from 'lodash.size'; 3 | 4 | import { Graph } from '../graph'; 5 | 6 | export function topsort(g: Graph): string[] { 7 | const visited = {}; 8 | const stack = {}; 9 | const results: any[] = []; 10 | 11 | function visit(node: string) { 12 | if (node in stack) { 13 | throw new CycleException(); 14 | } 15 | 16 | if (!(node in visited)) { 17 | stack[node] = true; 18 | visited[node] = true; 19 | each(g.predecessors(node), visit); 20 | delete stack[node]; 21 | results.push(node); 22 | } 23 | } 24 | 25 | each(g.sinks(), visit); 26 | 27 | if (size(visited) !== g.nodeCount()) { 28 | throw new CycleException(); 29 | } 30 | 31 | return results; 32 | } 33 | 34 | export class CycleException extends Error {} 35 | -------------------------------------------------------------------------------- /test/fixtures/equals/different-node-id-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "root-node", 27 | "pkgId": "root@0.0.0", 28 | "deps": [ 29 | { 30 | "nodeId": "a@0.0.0" 31 | } 32 | ] 33 | }, 34 | { 35 | "nodeId": "a@0.0.0", 36 | "pkgId": "a@0.0.0", 37 | "deps": [] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1.1", 9 | "info": { 10 | "name": "a", 11 | "version": "1.1" 12 | } 13 | }, 14 | { 15 | "id": "b@2.1", 16 | "info": { 17 | "name": "b", 18 | "version": "2.1" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "2", 27 | "pkgId": "b@2.1", 28 | "deps": [] 29 | }, 30 | { 31 | "nodeId": "root-node", 32 | "pkgId": "a@1.1", 33 | "deps": [ 34 | { 35 | "nodeId": "2" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/equals/different-node-id-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "root-node", 27 | "pkgId": "root@0.0.0", 28 | "deps": [ 29 | { 30 | "nodeId": "a@0.0.0|123" 31 | } 32 | ] 33 | }, 34 | { 35 | "nodeId": "a@0.0.0|123", 36 | "pkgId": "a@0.0.0", 37 | "deps": [] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "l@0.1", 16 | "info": { 17 | "name": "l", 18 | "version": "0.1" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "12", 27 | "pkgId": "l@0.1", 28 | "deps": [] 29 | }, 30 | { 31 | "nodeId": "root-node", 32 | "pkgId": "a@1", 33 | "deps": [ 34 | { 35 | "nodeId": "12" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/equals/cycles/one-node-cycle-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@1", 9 | "info": { 10 | "name": "root", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "A@1", 16 | "info": { 17 | "name": "A", 18 | "version": "1" 19 | } 20 | } 21 | ], 22 | "graph": { 23 | "rootNodeId": "root-node", 24 | "nodes": [ 25 | { 26 | "nodeId": "root-node", 27 | "pkgId": "root@1", 28 | "deps": [ 29 | { 30 | "nodeId": "A@1" 31 | } 32 | ] 33 | }, 34 | { 35 | "nodeId": "A@1", 36 | "pkgId": "A@1", 37 | "deps": [ 38 | { 39 | "nodeId": "A@1" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/legacy/cycles.ts: -------------------------------------------------------------------------------- 1 | type NodeId = string; 2 | type Cycle = NodeId[]; 3 | 4 | export type Cycles = Cycle[]; 5 | 6 | export type PartitionedCycles = { 7 | cyclesStartWithThisNode: Cycle[]; 8 | cyclesWithThisNode: Cycle[]; 9 | }; 10 | 11 | export function getCycle(ancestors: NodeId[], nodeId: NodeId): Cycle | null { 12 | if (!ancestors.includes(nodeId)) { 13 | return null; 14 | } 15 | 16 | // first item is where the cycle starts and ends. 17 | return ancestors.slice(ancestors.indexOf(nodeId)); 18 | } 19 | 20 | export function partitionCycles( 21 | nodeId: NodeId, 22 | allCyclesTheNodeIsPartOf: Cycle[], 23 | ): PartitionedCycles { 24 | const cyclesStartWithThisNode: Cycle[] = []; 25 | const cyclesWithThisNode: Cycle[] = []; 26 | 27 | for (const cycle of allCyclesTheNodeIsPartOf) { 28 | const nodeStartsCycle = cycle[0] === nodeId; 29 | if (nodeStartsCycle) { 30 | cyclesStartWithThisNode.push(cycle); 31 | } else { 32 | cyclesWithThisNode.push(cycle); 33 | } 34 | } 35 | return { cyclesStartWithThisNode, cyclesWithThisNode }; 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/unpruneable-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "b": { 7 | "name": "b", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "c": { 11 | "name": "c", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "v": { 15 | "name": "v", 16 | "version": "5.0.0", 17 | "labels": { 18 | "vuln": "yes" 19 | } 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | "e": { 26 | "name": "e", 27 | "version": "1.0.0", 28 | "dependencies": { 29 | "c": { 30 | "name": "c", 31 | "version": "1.0.0", 32 | "dependencies": { 33 | "v": { 34 | "name": "v", 35 | "version": "5.0.0", 36 | "labels": { 37 | "vuln": "yes" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/graphlib/LICENSE-graphlib: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Chris Pettitt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "c@4", 16 | "info": { 17 | "name": "c", 18 | "version": "4" 19 | } 20 | }, 21 | { 22 | "id": "d@4.1", 23 | "info": { 24 | "name": "d", 25 | "version": "4.1" 26 | } 27 | } 28 | ], 29 | "graph": { 30 | "rootNodeId": "root-node", 31 | "nodes": [ 32 | { 33 | "nodeId": "3", 34 | "pkgId": "c@4", 35 | "deps": [] 36 | }, 37 | { 38 | "nodeId": "4", 39 | "pkgId": "d@4.1", 40 | "deps": [] 41 | }, 42 | { 43 | "nodeId": "root-node", 44 | "pkgId": "a@1", 45 | "deps": [ 46 | { 47 | "nodeId": "3" 48 | }, 49 | { 50 | "nodeId": "4" 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/fixtures/equals/cycles/one-node-cycle-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@1", 9 | "info": { 10 | "name": "root", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "A@1", 16 | "info": { 17 | "name": "A", 18 | "version": "1" 19 | } 20 | }, 21 | { 22 | "id": "B@1", 23 | "info": { 24 | "name": "B", 25 | "version": "1" 26 | } 27 | } 28 | ], 29 | "graph": { 30 | "rootNodeId": "root-node", 31 | "nodes": [ 32 | { 33 | "nodeId": "root-node", 34 | "pkgId": "root@1", 35 | "deps": [ 36 | { 37 | "nodeId": "A@1" 38 | } 39 | ] 40 | }, 41 | { 42 | "nodeId": "A@1", 43 | "pkgId": "A@1", 44 | "deps": [ 45 | { 46 | "nodeId": "B@1" 47 | } 48 | ] 49 | }, 50 | { 51 | "nodeId": "B@1", 52 | "pkgId": "B@1", 53 | "deps": [] 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-added-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "l@0.1", 16 | "info": { 17 | "name": "l", 18 | "version": "0.1" 19 | } 20 | }, 21 | { 22 | "id": "m@1.2", 23 | "info": { 24 | "name": "m", 25 | "version": "1.2" 26 | } 27 | } 28 | ], 29 | "graph": { 30 | "rootNodeId": "root-node", 31 | "nodes": [ 32 | { 33 | "nodeId": "12", 34 | "pkgId": "l@0.1", 35 | "deps": [ 36 | { 37 | "nodeId": "13" 38 | } 39 | ] 40 | }, 41 | { 42 | "nodeId": "13", 43 | "pkgId": "m@1.2", 44 | "deps": [] 45 | }, 46 | { 47 | "nodeId": "root-node", 48 | "pkgId": "a@1", 49 | "deps": [ 50 | { 51 | "nodeId": "12" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/fixtures/pruneable-tree-pruned.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "b": { 11 | "name": "b", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "v": { 19 | "name": "v", 20 | "version": "5.0.0", 21 | "labels": { 22 | "vuln": "yes" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | "d": { 30 | "name": "d", 31 | "version": "1.0.0", 32 | "dependencies": { 33 | "c": { 34 | "name": "c", 35 | "version": "1.0.0", 36 | "labels": { 37 | "pruned": "true" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /go/pkg/depgraph/testdata/snyk_dep_graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.1.0", 3 | "pkgManager": { 4 | "name": "npm" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "demo-app-for-test@1.1.1", 9 | "info": { 10 | "name": "demo-app-for-test", 11 | "version": "1.1.1" 12 | } 13 | }, 14 | { 15 | "id": "express@4.4.0", 16 | "info": { 17 | "name": "express", 18 | "version": "4.4.0" 19 | } 20 | }, 21 | { 22 | "id": "ws@1.0.0", 23 | "info": { 24 | "name": "ws", 25 | "version": "1.0.0" 26 | } 27 | } 28 | ], 29 | "graph": { 30 | "rootNodeId": "root-node", 31 | "nodes": [ 32 | { 33 | "nodeId": "root-node", 34 | "pkgId": "demo-app-for-test@1.1.1", 35 | "deps": [ 36 | { 37 | "nodeId": "express@4.4.0" 38 | }, 39 | { 40 | "nodeId": "ws@1.0.0" 41 | } 42 | ] 43 | }, 44 | { 45 | "nodeId": "express@4.4.0", 46 | "pkgId": "express@4.4.0", 47 | "deps": [] 48 | }, 49 | { 50 | "nodeId": "ws@1.0.0", 51 | "pkgId": "ws@1.0.0", 52 | "deps": [] 53 | } 54 | ] 55 | } 56 | } -------------------------------------------------------------------------------- /benchmark/pkg-paths-to-root.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node -r ts-node/register/transpile-only 2 | 3 | import { createFromJSON } from '../src'; 4 | import benny from 'benny'; 5 | import { readFileSync } from 'fs'; 6 | import { join } from 'path'; 7 | 8 | const fixturePath = join(__dirname, 'fixtures', 'big-golang-graph.json'); 9 | const fixture = JSON.parse(readFileSync(fixturePath, { encoding: 'utf-8' })); 10 | const depGraph = createFromJSON(fixture); 11 | 12 | benny.suite( 13 | 'pkgPathsToRoot', 14 | 15 | benny.add('with limit of 1', () => 16 | depGraph.pkgPathsToRoot( 17 | { 18 | name: 'github.com/hashicorp/golang-lru', 19 | version: 'v0.5.0', 20 | }, 21 | { limit: 1 }, 22 | ), 23 | ), 24 | 25 | benny.add('with limit of 100', () => 26 | depGraph.pkgPathsToRoot( 27 | { 28 | name: 'github.com/hashicorp/golang-lru', 29 | version: 'v0.5.0', 30 | }, 31 | { limit: 100 }, 32 | ), 33 | ), 34 | 35 | benny.add('with limit of 10,000', () => 36 | depGraph.pkgPathsToRoot( 37 | { 38 | name: 'github.com/hashicorp/golang-lru', 39 | version: 'v0.5.0', 40 | }, 41 | { limit: 10_000 }, 42 | ), 43 | ), 44 | 45 | benny.cycle(), 46 | benny.complete(), 47 | benny.save({ file: 'pkgPathsToRoot', version: '1.0.0' }), 48 | ); 49 | -------------------------------------------------------------------------------- /test/fixtures/pruneable-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "b": { 11 | "name": "b", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "v": { 19 | "name": "v", 20 | "version": "5.0.0", 21 | "labels": { 22 | "vuln": "yes" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | "d": { 30 | "name": "d", 31 | "version": "1.0.0", 32 | "dependencies": { 33 | "c": { 34 | "name": "c", 35 | "version": "1.0.0", 36 | "dependencies": { 37 | "v": { 38 | "name": "v", 39 | "version": "5.0.0", 40 | "labels": { 41 | "vuln": "yes" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/fixtures/os-apk-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "apk:0.0.1", 5 | "targetOS": { 6 | "name": "ubuntu", 7 | "version": "18.04" 8 | }, 9 | "dependencies": { 10 | "a": { 11 | "name": "a", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "d": { 19 | "name": "d", 20 | "version": "0.0.1", 21 | "dependencies": { 22 | "e": { 23 | "name": "e", 24 | "version": "5.0.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "b": { 33 | "name": "b", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "dependencies": { 40 | "d": { 41 | "name": "d", 42 | "version": "0.0.2", 43 | "dependencies": { 44 | "e": { 45 | "name": "e", 46 | "version": "5.0.0" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/os-apt-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "apt:0.0.1", 5 | "targetOS": { 6 | "name": "ubuntu", 7 | "version": "18.04" 8 | }, 9 | "dependencies": { 10 | "a": { 11 | "name": "a", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "d": { 19 | "name": "d", 20 | "version": "0.0.1", 21 | "dependencies": { 22 | "e": { 23 | "name": "e", 24 | "version": "5.0.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "b": { 33 | "name": "b", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "dependencies": { 40 | "d": { 41 | "name": "d", 42 | "version": "0.0.2", 43 | "dependencies": { 44 | "e": { 45 | "name": "e", 46 | "version": "5.0.0" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/os-deb-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "deb:0.0.1", 5 | "targetOS": { 6 | "name": "ubuntu", 7 | "version": "18.04" 8 | }, 9 | "dependencies": { 10 | "a": { 11 | "name": "a", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "d": { 19 | "name": "d", 20 | "version": "0.0.1", 21 | "dependencies": { 22 | "e": { 23 | "name": "e", 24 | "version": "5.0.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "b": { 33 | "name": "b", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "dependencies": { 40 | "d": { 41 | "name": "d", 42 | "version": "0.0.2", 43 | "dependencies": { 44 | "e": { 45 | "name": "e", 46 | "version": "5.0.0" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/os-rpm-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "rpm:0.0.1", 5 | "targetOS": { 6 | "name": "ubuntu", 7 | "version": "18.04" 8 | }, 9 | "dependencies": { 10 | "a": { 11 | "name": "a", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "d": { 19 | "name": "d", 20 | "version": "0.0.1", 21 | "dependencies": { 22 | "e": { 23 | "name": "e", 24 | "version": "5.0.0" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | }, 32 | "b": { 33 | "name": "b", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "dependencies": { 40 | "d": { 41 | "name": "d", 42 | "version": "0.0.2", 43 | "dependencies": { 44 | "e": { 45 | "name": "e", 46 | "version": "5.0.0" 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/graphlib/alg/dfs.ts: -------------------------------------------------------------------------------- 1 | import * as each from 'lodash.foreach'; 2 | import { Graph } from '../graph'; 3 | 4 | /* 5 | * A helper that preforms a pre- or post-order traversal on the input graph 6 | * and returns the nodes in the order they were visited. If the graph is 7 | * undirected then this algorithm will navigate using neighbors. If the graph 8 | * is directed then this algorithm will navigate using successors. 9 | * 10 | * Order must be one of "pre" or "post". 11 | */ 12 | export function dfs(g: Graph, vs: string[], order: 'pre' | 'post'): string[] { 13 | if (!Array.isArray(vs)) { 14 | vs = [vs]; 15 | } 16 | 17 | const navigation = (g.isDirected() ? g.successors : g.neighbors).bind(g); 18 | 19 | const acc: string[] = []; 20 | const visited: { [v: string]: boolean } = {}; 21 | each(vs, (v) => { 22 | if (!g.hasNode(v)) { 23 | throw new Error('Graph does not have node: ' + v); 24 | } 25 | 26 | doDfs(g, v, order === 'post', visited, navigation, acc); 27 | }); 28 | return acc; 29 | } 30 | 31 | function doDfs(g, v: string, postorder, visited, navigation, acc) { 32 | if (!(v in visited)) { 33 | visited[v] = true; 34 | 35 | if (!postorder) { 36 | acc.push(v); 37 | } 38 | each(navigation(v), function (w) { 39 | doDfs(g, w, postorder, visited, navigation, acc); 40 | }); 41 | if (postorder) { 42 | acc.push(v); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "j@2", 16 | "info": { 17 | "name": "j", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "i@9", 23 | "info": { 24 | "name": "i", 25 | "version": "9" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | } 35 | ], 36 | "graph": { 37 | "rootNodeId": "root-node", 38 | "nodes": [ 39 | { 40 | "nodeId": "4", 41 | "pkgId": "d@4", 42 | "deps": [ 43 | { 44 | "nodeId": "9" 45 | } 46 | ] 47 | }, 48 | { 49 | "nodeId": "9", 50 | "pkgId": "i@9", 51 | "deps": [ 52 | { 53 | "nodeId": "10" 54 | } 55 | ] 56 | }, 57 | { 58 | "nodeId": "10", 59 | "pkgId": "j@2", 60 | "deps": [] 61 | }, 62 | { 63 | "nodeId": "root-node", 64 | "pkgId": "a@1", 65 | "deps": [ 66 | { 67 | "nodeId": "4" 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/fixtures/simple-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "c": { 11 | "name": "c", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "d": { 15 | "name": "d", 16 | "version": "0.0.1", 17 | "dependencies": { 18 | "e": { 19 | "name": "e", 20 | "version": "5.0.0", 21 | "labels": { 22 | "key": "value" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | }, 31 | "b": { 32 | "name": "b", 33 | "version": "1.0.0", 34 | "dependencies": { 35 | "c": { 36 | "name": "c", 37 | "version": "1.0.0", 38 | "dependencies": { 39 | "d": { 40 | "name": "d", 41 | "version": "0.0.2", 42 | "dependencies": { 43 | "e": { 44 | "name": "e", 45 | "version": "5.0.0", 46 | "labels": { 47 | "key": "value" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /benchmark/count-paths-to-root.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node -r ts-node/register/transpile-only 2 | 3 | import { createFromJSON } from '../src'; 4 | import benny from 'benny'; 5 | import { readFileSync } from 'fs'; 6 | import { join } from 'path'; 7 | 8 | const fixturePath = join(__dirname, 'fixtures', 'big-golang-graph.json'); 9 | const fixture = JSON.parse(readFileSync(fixturePath, { encoding: 'utf-8' })); 10 | const depGraph = createFromJSON(fixture); 11 | 12 | // Get a sample of packages to benchmark 13 | const pkgs = Array.from(depGraph.getPkgs()).slice(0, 5); 14 | 15 | benny.suite('countPathsToRoot', 16 | 17 | benny.add('with limit of 100', () => { 18 | for (const pkg of pkgs) { 19 | depGraph.countPathsToRoot(pkg, { limit: 100 }); 20 | } 21 | }), 22 | 23 | benny.add('with limit of 1,000', () => { 24 | for (const pkg of pkgs) { 25 | depGraph.countPathsToRoot(pkg, { limit: 1000 }); 26 | } 27 | }), 28 | 29 | benny.add('with limit of 10,000', () => { 30 | for (const pkg of pkgs) { 31 | depGraph.countPathsToRoot(pkg, { limit: 10000 }); 32 | } 33 | }), 34 | 35 | benny.add('with limit of 20,000', () => { 36 | for (const pkg of pkgs) { 37 | depGraph.countPathsToRoot(pkg, { limit: 20000 }); 38 | } 39 | }), 40 | 41 | // Note: "no limit" case skipped for hyper-dense-graph as it would never complete 42 | 43 | benny.cycle(), 44 | benny.complete(), 45 | benny.save({ file: 'countPathsToRoot', version: '1.0.0' }), 46 | ); 47 | 48 | -------------------------------------------------------------------------------- /test/fixtures/cyclic-dep-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "toor@1.0.0", 9 | "info": { 10 | "name": "toor", 11 | "version": "1.0.0" 12 | } 13 | }, 14 | { 15 | "id": "foo@2", 16 | "info": { 17 | "name": "foo", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "bar@3", 23 | "info": { 24 | "name": "bar", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "baz@4", 30 | "info": { 31 | "name": "baz", 32 | "version": "4" 33 | } 34 | } 35 | ], 36 | "graph": { 37 | "rootNodeId": "toor", 38 | "nodes": [ 39 | { 40 | "nodeId": "toor", 41 | "pkgId": "toor@1.0.0", 42 | "deps": [ 43 | { 44 | "nodeId": "foo@2|x" 45 | } 46 | ] 47 | }, 48 | { 49 | "nodeId": "foo@2|x", 50 | "pkgId": "foo@2", 51 | "deps": [ 52 | { 53 | "nodeId": "bar@3|x" 54 | } 55 | ] 56 | }, 57 | { 58 | "nodeId": "bar@3|x", 59 | "pkgId": "bar@3", 60 | "deps": [ 61 | { 62 | "nodeId": "baz@4|x" 63 | } 64 | ] 65 | }, 66 | { 67 | "nodeId": "baz@4|x", 68 | "pkgId": "baz@4", 69 | "deps": [ 70 | { 71 | "nodeId": "foo@2|x" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= 5 | github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /test/fixtures/equals/cycles/simple-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | }, 21 | { 22 | "id": "b@0.0.0", 23 | "info": { 24 | "name": "b", 25 | "version": "0.0.0" 26 | } 27 | }, 28 | { 29 | "id": "c@0.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "0.0.0" 33 | } 34 | } 35 | ], 36 | "graph": { 37 | "rootNodeId": "root-node", 38 | "nodes": [ 39 | { 40 | "nodeId": "root-node", 41 | "pkgId": "root@0.0.0", 42 | "deps": [ 43 | { 44 | "nodeId": "a@0.0.0" 45 | } 46 | ] 47 | }, 48 | { 49 | "nodeId": "a@0.0.0", 50 | "pkgId": "a@0.0.0", 51 | "deps": [ 52 | { 53 | "nodeId": "b@0.0.0" 54 | } 55 | ] 56 | }, 57 | { 58 | "nodeId": "b@0.0.0", 59 | "pkgId": "b@0.0.0", 60 | "deps": [ 61 | { 62 | "nodeId": "c@0.0.0" 63 | } 64 | ] 65 | }, 66 | { 67 | "nodeId": "c@0.0.0", 68 | "pkgId": "c@0.0.0", 69 | "deps": [ 70 | { 71 | "nodeId": "a@0.0.0" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/fixtures/labelled-dep-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "c": { 11 | "name": "c", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "d": { 15 | "name": "d", 16 | "version": "2.0.0", 17 | "labels": { 18 | "key": "value1" 19 | }, 20 | "dependencies": { 21 | "e": { 22 | "name": "e", 23 | "version": "5.0.0", 24 | "labels": { 25 | "key": "value" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "b": { 35 | "name": "b", 36 | "version": "1.0.0", 37 | "dependencies": { 38 | "c": { 39 | "name": "c", 40 | "version": "1.0.0", 41 | "dependencies": { 42 | "d": { 43 | "name": "d", 44 | "version": "2.0.0", 45 | "labels": { 46 | "key": "value2" 47 | }, 48 | "dependencies": { 49 | "e": { 50 | "name": "e", 51 | "version": "5.0.0", 52 | "labels": { 53 | "key": "value" 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/legacy/memiozation.ts: -------------------------------------------------------------------------------- 1 | import { DepTree } from './index'; 2 | import { PartitionedCycles } from './cycles'; 3 | 4 | type NodeId = string; 5 | 6 | export type MemoizationMap = Map< 7 | NodeId, 8 | { 9 | depTree: DepTree; 10 | 11 | // the cycleNodeIds holds the nodes ids in a cycle 12 | // i.e. for the cyclic graph "1->2->3->4->2", for nodeId=2 the cycleNodeIds will be "3,4" 13 | // if nodeId exists in cycleNodeIds, don't use memoized depTree version 14 | cycleNodeIds?: Set; 15 | } 16 | >; 17 | 18 | export function memoize( 19 | nodeId: NodeId, 20 | memoizationMap: MemoizationMap, 21 | depTree: DepTree, 22 | partitionedCycles: PartitionedCycles, 23 | ) { 24 | const { cyclesStartWithThisNode, cyclesWithThisNode } = partitionedCycles; 25 | if (cyclesStartWithThisNode.length > 0) { 26 | const cycleNodeIds = new Set(...cyclesStartWithThisNode); 27 | memoizationMap.set(nodeId, { depTree, cycleNodeIds }); 28 | } else if (cyclesWithThisNode.length === 0) { 29 | memoizationMap.set(nodeId, { depTree }); 30 | } 31 | // Don't memoize nodes in cycles (cyclesWithThisNode.length > 0) 32 | } 33 | 34 | export function getMemoizedDepTree( 35 | nodeId: NodeId, 36 | ancestors: NodeId[], 37 | memoizationMap: MemoizationMap, 38 | ): DepTree | null { 39 | if (!memoizationMap.has(nodeId)) return null; 40 | 41 | const { depTree, cycleNodeIds } = memoizationMap.get(nodeId)!; 42 | if (!cycleNodeIds) return depTree; 43 | 44 | const ancestorsArePartOfTheCycle = ancestors.some((nodeId) => 45 | cycleNodeIds.has(nodeId), 46 | ); 47 | 48 | return ancestorsArePartOfTheCycle ? null : depTree; 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/equals/cycles/simple-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | }, 21 | { 22 | "id": "b@0.0.0", 23 | "info": { 24 | "name": "b", 25 | "version": "0.0.0" 26 | } 27 | }, 28 | { 29 | "id": "c@0.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "0.0.0" 33 | } 34 | } 35 | ], 36 | "graph": { 37 | "rootNodeId": "root-node", 38 | "nodes": [ 39 | { 40 | "nodeId": "root-node", 41 | "pkgId": "root@0.0.0", 42 | "deps": [ 43 | { 44 | "nodeId": "a@0.0.0" 45 | } 46 | ] 47 | }, 48 | { 49 | "nodeId": "a@0.0.0", 50 | "pkgId": "a@0.0.0", 51 | "deps": [ 52 | { 53 | "nodeId": "b@0.0.0" 54 | } 55 | ] 56 | }, 57 | { 58 | "nodeId": "b@0.0.0", 59 | "pkgId": "b@0.0.0", 60 | "deps": [ 61 | { 62 | "nodeId": "c@0.0.0" 63 | } 64 | ] 65 | }, 66 | { 67 | "nodeId": "c@0.0.0", 68 | "pkgId": "c@0.0.0", 69 | "deps": [ 70 | { 71 | "nodeId": "a@0.0.0" 72 | }, 73 | { 74 | "nodeId": "b@0.0.0" 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/core/is-transitive.test.ts: -------------------------------------------------------------------------------- 1 | import { createFromJSON, DepGraphData } from '../../src'; 2 | import { loadFixture } from '../helpers'; 3 | import * as depGraphLib from '../../src'; 4 | 5 | describe('isTransitive', () => { 6 | it('checks direct', () => { 7 | const graphJson: DepGraphData = loadFixture('simple-graph.json'); 8 | 9 | const depGraph = createFromJSON(graphJson); 10 | 11 | expect(depGraph.isTransitive({ name: 'a', version: '1.0.0' })).toBe(false); 12 | expect(depGraph.isTransitive({ name: 'b', version: '1.0.0' })).toBe(false); 13 | expect(depGraph.isTransitive({ name: 'e', version: '5.0.0' })).toBe(true); 14 | expect(depGraph.isTransitive({ name: 'd', version: '0.0.1' })).toBe(true); 15 | expect(depGraph.isTransitive({ name: 'd', version: '0.0.2' })).toBe(true); 16 | expect(depGraph.isTransitive({ name: 'c', version: '1.0.0' })).toBe(true); 17 | }); 18 | 19 | it('assume non-transitive if any directly depends', () => { 20 | const builder = new depGraphLib.DepGraphBuilder( 21 | { name: 'npm' }, 22 | { name: 'root', version: '1.2.3' }, 23 | ); 24 | const rootNodeId = 'root-node'; 25 | 26 | // root depends on 'a' and 'b', 'b' depends again on 'a'. 27 | builder.addPkgNode({ name: 'a', version: '1.0.0' }, 'a|1'); 28 | builder.connectDep(rootNodeId, 'a|1'); 29 | 30 | builder.addPkgNode({ name: 'b', version: '1.0.0' }, 'b'); 31 | builder.connectDep(rootNodeId, 'b'); 32 | 33 | builder.addPkgNode({ name: 'a', version: '1.0.0' }, 'a|2'); 34 | builder.connectDep('b', 'a|2'); 35 | 36 | const depGraph = builder.build(); 37 | 38 | expect(depGraph.isTransitive({ name: 'a', version: '1.0.0' })).toBe(false); 39 | expect(depGraph.isTransitive({ name: 'b', version: '1.0.0' })).toBe(false); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/legacy/stress.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import { graphToDepTree } from '../../src/legacy'; 3 | 4 | const dependencyName = 'needle'; 5 | 6 | async function generateLargeGraph(width: number) { 7 | const builder = new depGraphLib.DepGraphBuilder( 8 | { name: 'npm' }, 9 | { name: 'root', version: '1.2.3' }, 10 | ); 11 | const rootNodeId = 'root-node'; 12 | 13 | const deepDependency = { name: dependencyName, version: '1.2.3' }; 14 | const deepDependency2 = { name: dependencyName + 2, version: '1.2.3' }; 15 | 16 | builder.addPkgNode(deepDependency, dependencyName); 17 | builder.addPkgNode(deepDependency2, deepDependency2.name); 18 | builder.connectDep(rootNodeId, dependencyName); 19 | builder.connectDep(deepDependency.name, deepDependency2.name); 20 | 21 | for (let j = 0; j < width / 2; j++) { 22 | const shallowName = `id-${j}`; 23 | const shallowDependency = { name: shallowName, version: '1.2.3' }; 24 | 25 | builder.addPkgNode(shallowDependency, shallowName); 26 | builder.connectDep(rootNodeId, shallowName); 27 | builder.connectDep(shallowName, dependencyName); 28 | } 29 | 30 | for (let j = 0; j < width / 2; j++) { 31 | const shallowName = `second-${j}`; 32 | const shallowDependency = { name: shallowName, version: '1.2.3' }; 33 | 34 | builder.addPkgNode(shallowDependency, shallowName); 35 | builder.connectDep(deepDependency2.name, shallowName); 36 | } 37 | 38 | return builder.build(); 39 | } 40 | 41 | describe('stress tests', () => { 42 | test('graphToDepTree() with memoization (without deduplicateWithinTopLevelDeps) succeed for large dep-graphs', async () => { 43 | const graph = await generateLargeGraph(125000); 44 | 45 | const depTree = await graphToDepTree(graph, 'gomodules', { 46 | deduplicateWithinTopLevelDeps: false, 47 | }); 48 | expect(depTree).toBeDefined(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/core/stress.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | 3 | const dependencyName = 'needle'; 4 | 5 | async function generateLargeGraph(width: number) { 6 | const builder = new depGraphLib.DepGraphBuilder( 7 | { name: 'npm' }, 8 | { name: 'root', version: '1.2.3' }, 9 | ); 10 | const rootNodeId = 'root-node'; 11 | 12 | const deepDependency = { name: dependencyName, version: '1.2.3' }; 13 | 14 | builder.addPkgNode(deepDependency, dependencyName); 15 | builder.connectDep(rootNodeId, dependencyName); 16 | 17 | for (let j = 0; j < width; j++) { 18 | const shallowName = `id-${j}`; 19 | const shallowDependency = { name: shallowName, version: '1.2.3' }; 20 | 21 | builder.addPkgNode(shallowDependency, shallowName); 22 | builder.connectDep(rootNodeId, shallowName); 23 | builder.connectDep(shallowName, dependencyName); 24 | } 25 | 26 | return builder.build(); 27 | } 28 | 29 | describe('stress tests', () => { 30 | test('pkgPathsToRoot() does not cause stack overflow for large dep-graphs', async () => { 31 | const graph = await generateLargeGraph(125000); 32 | 33 | const result = graph.pkgPathsToRoot({ 34 | name: dependencyName, 35 | version: '1.2.3', 36 | }); 37 | expect(result).toBeDefined(); 38 | }); 39 | test('countPathsToRoot() is cached', async () => { 40 | const graph = await generateLargeGraph(100_000); 41 | let start = Date.now(); 42 | graph.countPathsToRoot({ 43 | name: dependencyName, 44 | version: '1.2.3', 45 | }); 46 | const firstCallDuration = Date.now() - start; 47 | start = Date.now(); 48 | graph.countPathsToRoot({ 49 | name: dependencyName, 50 | version: '1.2.3', 51 | }); 52 | const secondCallDuration = Date.now() - start; 53 | // the second call must be *significantly* faster than the first 54 | expect(secondCallDuration).toBeLessThan(firstCallDuration * 0.2); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as _isEqual from 'lodash.isequal'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { PkgInfo } from '../src'; 5 | 6 | export function loadFixture(name: string) { 7 | return JSON.parse( 8 | fs.readFileSync(path.join(__dirname, `fixtures/${name}`), 'utf8'), 9 | ); 10 | } 11 | 12 | function depCompare(a: any, b: any) { 13 | if (a.name < b.name) { 14 | return -1; 15 | } else if (a.name > b.name) { 16 | return 1; 17 | } 18 | if (a.version < b.version) { 19 | return -1; 20 | } else if (a.version > b.version) { 21 | return 1; 22 | } 23 | return 0; 24 | } 25 | 26 | export function expectSamePkgs(actual: PkgInfo[], expected: PkgInfo[]) { 27 | actual = actual.slice().sort(depCompare); 28 | expected = expected.slice().sort(depCompare); 29 | return expect(actual).toEqual(expected); 30 | } 31 | 32 | export function depTreesEqual(a: any, b: any) { 33 | if (a.name !== b.name || a.version !== b.version) { 34 | return false; 35 | } 36 | 37 | if ( 38 | !_isEqual(a.labels, b.labels) || 39 | !_isEqual(a.versionProvenance, b.versionProvenance) 40 | ) { 41 | return false; 42 | } 43 | 44 | const aDeps = a.dependencies || {}; 45 | const bDeps = b.dependencies || {}; 46 | 47 | if ( 48 | Object.keys(aDeps).sort().join(',') !== Object.keys(bDeps).sort().join(',') 49 | ) { 50 | return false; 51 | } 52 | 53 | for (const depName of Object.keys(aDeps)) { 54 | const aSubtree = aDeps[depName]; 55 | const bSubtree = bDeps[depName]; 56 | 57 | const isEq = depTreesEqual(aSubtree, bSubtree); 58 | if (!isEq) { 59 | return false; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | export const sortBy = (arr: any[], p: string) => 67 | arr.slice().sort((x: any, y: any) => { 68 | const a = x[p]; 69 | const b = y[p]; 70 | if (a < b) { 71 | return -1; 72 | } else if (a > b) { 73 | return 1; 74 | } else { 75 | return 0; 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /test/fixtures/pruneable-tree-multi-top-level-deps-pruned.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "b": { 11 | "name": "b", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "v": { 19 | "name": "v", 20 | "version": "5.0.0", 21 | "labels": { 22 | "vuln": "yes" 23 | } 24 | } 25 | }, 26 | "labels": { 27 | "foo": "bar" 28 | } 29 | } 30 | } 31 | }, 32 | "d": { 33 | "name": "d", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "labels": { 40 | "foo": "bar", 41 | "pruned": "true" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | "e": { 49 | "name": "e", 50 | "version": "1.0.0", 51 | "dependencies": { 52 | "f": { 53 | "name": "f", 54 | "version": "1.0.0", 55 | "dependencies": { 56 | "c": { 57 | "name": "c", 58 | "version": "1.0.0", 59 | "dependencies": { 60 | "v": { 61 | "name": "v", 62 | "version": "5.0.0", 63 | "labels": { 64 | "vuln": "yes" 65 | } 66 | } 67 | }, 68 | "labels": { 69 | "foo": "bar" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/core/direct-deps-leading-to.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import * as helpers from '../helpers'; 3 | 4 | describe('directDepsLeadingTo', () => { 5 | const depGraph = depGraphLib.createFromJSON( 6 | helpers.loadFixture('goof-graph.json'), 7 | ); 8 | 9 | test('it gets the package itself if it is a direct dependency', () => { 10 | const pkg = { name: 'consolidate', version: '0.14.5' }; 11 | 12 | expect(depGraph.directDepsLeadingTo(pkg)).toEqual([pkg]); 13 | }); 14 | 15 | test('it gets the direct deps leading to a package, respecting the version', () => { 16 | const pkgA = { name: 'bluebird', version: '2.9.26' }; 17 | const pkgB = { name: 'bluebird', version: '3.5.2' }; 18 | 19 | const expectedDepsA = [{ name: 'mongoose', version: '4.2.4' }]; 20 | const expectedDepsB = [ 21 | { name: 'consolidate', version: '0.14.5' }, 22 | { name: 'tap', version: '5.8.0' }, 23 | ]; 24 | 25 | const directDepsForA = depGraph.directDepsLeadingTo(pkgA); 26 | const directDepsForB = depGraph.directDepsLeadingTo(pkgB); 27 | 28 | expect(directDepsForA).toEqual(expectedDepsA); 29 | expect(directDepsForB).toEqual(expectedDepsB); 30 | expect(directDepsForA).not.toEqual(directDepsForB); 31 | }); 32 | 33 | test('it gets the correct direct deps for a deep dependency', () => { 34 | const pkg = { name: 'isarray', version: '0.0.1' }; 35 | const expected = [ 36 | { name: 'express-fileupload', version: '0.0.5' }, 37 | { name: 'mongoose', version: '4.2.4' }, 38 | { name: 'tap', version: '5.8.0' }, 39 | ]; 40 | 41 | expect(depGraph.directDepsLeadingTo(pkg)).toEqual(expected); 42 | }); 43 | 44 | test('it works with a cyclic dep-graph', () => { 45 | const cyclic = depGraphLib.createFromJSON( 46 | helpers.loadFixture('cyclic-dep-graph.json'), 47 | ); 48 | const pkg = { name: 'baz', version: '4' }; 49 | const expected = [{ name: 'foo', version: '2' }]; 50 | 51 | expect(cyclic.directDepsLeadingTo(pkg)).toEqual(expected); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/fixtures/pruneable-tree-multi-top-level-deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "packageFormatVersion": "mvn:0.0.1", 5 | "dependencies": { 6 | "a": { 7 | "name": "a", 8 | "version": "1.0.0", 9 | "dependencies": { 10 | "b": { 11 | "name": "b", 12 | "version": "1.0.0", 13 | "dependencies": { 14 | "c": { 15 | "name": "c", 16 | "version": "1.0.0", 17 | "dependencies": { 18 | "v": { 19 | "name": "v", 20 | "version": "5.0.0", 21 | "labels": { 22 | "vuln": "yes" 23 | } 24 | } 25 | }, 26 | "labels": { 27 | "foo": "bar" 28 | } 29 | } 30 | } 31 | }, 32 | "d": { 33 | "name": "d", 34 | "version": "1.0.0", 35 | "dependencies": { 36 | "c": { 37 | "name": "c", 38 | "version": "1.0.0", 39 | "dependencies": { 40 | "v": { 41 | "name": "v", 42 | "version": "5.0.0", 43 | "labels": { 44 | "vuln": "yes" 45 | } 46 | } 47 | }, 48 | "labels": { 49 | "foo": "bar" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "e": { 57 | "name": "e", 58 | "version": "1.0.0", 59 | "dependencies": { 60 | "f": { 61 | "name": "f", 62 | "version": "1.0.0", 63 | "dependencies": { 64 | "c": { 65 | "name": "c", 66 | "version": "1.0.0", 67 | "dependencies": { 68 | "v": { 69 | "name": "v", 70 | "version": "5.0.0", 71 | "labels": { 72 | "vuln": "yes" 73 | } 74 | } 75 | }, 76 | "labels": { 77 | "foo": "bar" 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "snyk.io", 3 | "name": "@snyk/dep-graph", 4 | "description": "Snyk dependency graph library", 5 | "homepage": "https://github.com/snyk/dep-graph#readme", 6 | "license": "Apache-2.0", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "build-dev": "tsc -w", 15 | "format": "prettier --write '{src,test,scripts}/**/*.?s'", 16 | "lint:eslint": "eslint 'src/**/*.ts' && (cd test && eslint '**/*.ts')", 17 | "lint:prettier": "prettier --check '{src,test,scripts}/**/*.ts'", 18 | "lint": "npm run lint:eslint && npm run lint:prettier", 19 | "test": "jest --runInBand", 20 | "test:coverage": "npm run test -- --coverage", 21 | "prepare": "npm run build" 22 | }, 23 | "engines": { 24 | "node": ">=10" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/snyk/dep-graph.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/snyk/dep-graph/issues" 32 | }, 33 | "devDependencies": { 34 | "@types/graphlib": "^2.1.7", 35 | "@types/jest": "^29", 36 | "@types/node": "^14", 37 | "@types/object-hash": "^2.1.0", 38 | "@types/semver": "^7.3.6", 39 | "@typescript-eslint/eslint-plugin": "^4.28.0", 40 | "@typescript-eslint/parser": "^4.28.0", 41 | "benny": "^3.6.15", 42 | "eslint": "^7.29.0", 43 | "eslint-config-prettier": "^8.3.0", 44 | "jest": "^29.7.0", 45 | "prettier": "^2.3.1", 46 | "ts-jest": "^29.4.5", 47 | "ts-node": "^10.0.0", 48 | "typescript": "~5.9.3" 49 | }, 50 | "dependencies": { 51 | "event-loop-spinner": "^2.1.0", 52 | "lodash.clone": "^4.5.0", 53 | "lodash.constant": "^3.0.0", 54 | "lodash.filter": "^4.6.0", 55 | "lodash.foreach": "^4.5.0", 56 | "lodash.isempty": "^4.4.0", 57 | "lodash.isequal": "^4.5.0", 58 | "lodash.isfunction": "^3.0.9", 59 | "lodash.isundefined": "^3.0.1", 60 | "lodash.map": "^4.6.0", 61 | "lodash.reduce": "^4.6.0", 62 | "lodash.size": "^4.2.0", 63 | "lodash.transform": "^4.6.0", 64 | "lodash.union": "^4.6.0", 65 | "lodash.values": "^4.3.0", 66 | "object-hash": "^3.0.0", 67 | "packageurl-js": "2.0.1", 68 | "semver": "^7.0.0", 69 | "tslib": "^2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-removed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "e@5", 30 | "info": { 31 | "name": "e", 32 | "version": "5" 33 | } 34 | }, 35 | { 36 | "id": "f@6", 37 | "info": { 38 | "name": "f", 39 | "version": "6" 40 | } 41 | }, 42 | { 43 | "id": "g@7", 44 | "info": { 45 | "name": "g", 46 | "version": "7" 47 | } 48 | } 49 | ], 50 | "graph": { 51 | "rootNodeId": "root-node", 52 | "nodes": [ 53 | { 54 | "nodeId": "root-node", 55 | "pkgId": "a@1", 56 | "deps": [ 57 | { 58 | "nodeId": "2" 59 | }, 60 | { 61 | "nodeId": "3" 62 | } 63 | ] 64 | }, 65 | { 66 | "nodeId": "2", 67 | "pkgId": "b@2", 68 | "deps": [ 69 | { 70 | "nodeId": "5" 71 | } 72 | ] 73 | }, 74 | { 75 | "nodeId": "3", 76 | "pkgId": "c@3", 77 | "deps": [ 78 | { 79 | "nodeId": "5" 80 | } 81 | ] 82 | }, 83 | { 84 | "nodeId": "5", 85 | "pkgId": "e@5", 86 | "deps": [ 87 | { 88 | "nodeId": "6" 89 | } 90 | ] 91 | }, 92 | { 93 | "nodeId": "6", 94 | "pkgId": "f@6", 95 | "deps": [ 96 | { 97 | "nodeId": "7" 98 | } 99 | ] 100 | }, 101 | { 102 | "nodeId": "7", 103 | "pkgId": "g@7", 104 | "deps": [ 105 | { 106 | "nodeId": "5" 107 | } 108 | ] 109 | } 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle-expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "e@6", 16 | "info": { 17 | "name": "e", 18 | "version": "6" 19 | } 20 | }, 21 | { 22 | "id": "g@7", 23 | "info": { 24 | "name": "g", 25 | "version": "7" 26 | } 27 | }, 28 | { 29 | "id": "f@6", 30 | "info": { 31 | "name": "f", 32 | "version": "6" 33 | } 34 | }, 35 | { 36 | "id": "c@3", 37 | "info": { 38 | "name": "c", 39 | "version": "3" 40 | } 41 | }, 42 | { 43 | "id": "b@2", 44 | "info": { 45 | "name": "b", 46 | "version": "2" 47 | } 48 | } 49 | ], 50 | "graph": { 51 | "rootNodeId": "root-node", 52 | "nodes": [ 53 | { 54 | "nodeId": "2", 55 | "pkgId": "b@2", 56 | "deps": [ 57 | { 58 | "nodeId": "5" 59 | } 60 | ] 61 | }, 62 | { 63 | "nodeId": "3", 64 | "pkgId": "c@3", 65 | "deps": [ 66 | { 67 | "nodeId": "5" 68 | } 69 | ] 70 | }, 71 | { 72 | "nodeId": "5", 73 | "pkgId": "e@6", 74 | "deps": [ 75 | { 76 | "nodeId": "6" 77 | } 78 | ] 79 | }, 80 | { 81 | "nodeId": "6", 82 | "pkgId": "f@6", 83 | "deps": [ 84 | { 85 | "nodeId": "7" 86 | } 87 | ] 88 | }, 89 | { 90 | "nodeId": "7", 91 | "pkgId": "g@7", 92 | "deps": [ 93 | { 94 | "nodeId": "5" 95 | } 96 | ] 97 | }, 98 | { 99 | "nodeId": "root-node", 100 | "pkgId": "a@1", 101 | "deps": [ 102 | { 103 | "nodeId": "2" 104 | }, 105 | { 106 | "nodeId": "3" 107 | } 108 | ] 109 | } 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Contributor Agreement 4 | A pull-request will only be considered for merging into the upstream codebase after you have signed our [contributor agreement](https://github.com/snyk/snyk/dep-graph/master/Contributor-Agreement.md), assigning us the rights to the contributed code and granting you a license to use it in return. If you submit a pull request, you will be prompted to review and sign the agreement with one click (we use [CLA assistant](https://cla-assistant.io/)). 5 | 6 | ## Commit messages 7 | 8 | Commit messages must follow the [Angular-style](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) commit format (but excluding the scope). 9 | 10 | i.e: 11 | 12 | ```text 13 | fix: minified scripts being removed 14 | 15 | Also includes tests 16 | ``` 17 | 18 | This will allow for the automatic changelog to generate correctly. 19 | 20 | ### Commit types 21 | 22 | Must be one of the following: 23 | 24 | * **feat**: A new feature 25 | * **fix**: A bug fix 26 | * **docs**: Documentation only changes 27 | * **test**: Adding missing tests 28 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 29 | * **refactor**: A code change that neither fixes a bug nor adds a feature 30 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 31 | * **perf**: A code change that improves performance 32 | 33 | To release a major you need to add `BREAKING CHANGE: ` to the start of the body and the detail of the breaking change. 34 | 35 | ## Code standards 36 | 37 | Ensure that your code adheres to the included `.eslintrc` config by running `npm run lint`. 38 | 39 | ## Sending pull requests 40 | 41 | - new command line options are generally discouraged unless there's a *really* good reason 42 | - add tests for newly added code (and try to mirror directory and file structure if possible) 43 | - spell check 44 | - PRs will not be code reviewed unless all tests are passing 45 | 46 | *Important:* when fixing a bug, please commit a **failing test** first so that Travis CI (or I can) can show the code failing. Once that commit is in place, then commit the bug fix, so that we can test *before* and *after*. 47 | 48 | Remember that you're developing for multiple platforms and versions of node, so if the tests pass on your Mac or Linux or Windows machine, it *may* not pass elsewhere. 49 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-wrong-nodes-order-a.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | }, 21 | { 22 | "id": "b@0.0.0", 23 | "info": { 24 | "name": "b", 25 | "version": "0.0.0" 26 | } 27 | }, 28 | { 29 | "id": "c@0.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "0.0.0" 33 | } 34 | }, 35 | { 36 | "id": "d@0.0.0", 37 | "info": { 38 | "name": "d", 39 | "version": "0.0.0" 40 | } 41 | }, 42 | { 43 | "id": "e@0.0.0", 44 | "info": { 45 | "name": "e", 46 | "version": "0.0.0" 47 | } 48 | } 49 | ], 50 | "graph": { 51 | "rootNodeId": "root-node", 52 | "nodes": [ 53 | { 54 | "nodeId": "root-node", 55 | "pkgId": "root@0.0.0", 56 | "deps": [ 57 | { 58 | "nodeId": "a@0.0.0" 59 | }, 60 | { 61 | "nodeId": "b@0.0.0" 62 | } 63 | ] 64 | }, 65 | { 66 | "nodeId": "a@0.0.0", 67 | "pkgId": "a@0.0.0", 68 | "deps": [ 69 | { 70 | "nodeId": "c@0.0.0|1" 71 | } 72 | ] 73 | }, 74 | { 75 | "nodeId": "b@0.0.0", 76 | "pkgId": "b@0.0.0", 77 | "deps": [ 78 | { 79 | "nodeId": "c@0.0.0|2" 80 | } 81 | ] 82 | }, 83 | { 84 | "nodeId": "c@0.0.0|1", 85 | "pkgId": "c@0.0.0", 86 | "deps": [ 87 | { 88 | "nodeId": "d@0.0.0" 89 | } 90 | ] 91 | }, 92 | { 93 | "nodeId": "d@0.0.0", 94 | "pkgId": "d@0.0.0", 95 | "deps": [] 96 | }, 97 | { 98 | "nodeId": "c@0.0.0|2", 99 | "pkgId": "c@0.0.0", 100 | "deps": [ 101 | { 102 | "nodeId": "e@0.0.0" 103 | } 104 | ] 105 | }, 106 | { 107 | "nodeId": "e@0.0.0", 108 | "pkgId": "e@0.0.0", 109 | "deps": [] 110 | } 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-wrong-nodes-order-b.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "a@0.0.0", 16 | "info": { 17 | "name": "a", 18 | "version": "0.0.0" 19 | } 20 | }, 21 | { 22 | "id": "b@0.0.0", 23 | "info": { 24 | "name": "b", 25 | "version": "0.0.0" 26 | } 27 | }, 28 | { 29 | "id": "c@0.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "0.0.0" 33 | } 34 | }, 35 | { 36 | "id": "d@0.0.0", 37 | "info": { 38 | "name": "d", 39 | "version": "0.0.0" 40 | } 41 | }, 42 | { 43 | "id": "e@0.0.0", 44 | "info": { 45 | "name": "e", 46 | "version": "0.0.0" 47 | } 48 | } 49 | ], 50 | "graph": { 51 | "rootNodeId": "root-node", 52 | "nodes": [ 53 | { 54 | "nodeId": "root-node", 55 | "pkgId": "root@0.0.0", 56 | "deps": [ 57 | { 58 | "nodeId": "a@0.0.0" 59 | }, 60 | { 61 | "nodeId": "b@0.0.0" 62 | } 63 | ] 64 | }, 65 | { 66 | "nodeId": "a@0.0.0", 67 | "pkgId": "a@0.0.0", 68 | "deps": [ 69 | { 70 | "nodeId": "c@0.0.0|2" 71 | } 72 | ] 73 | }, 74 | { 75 | "nodeId": "b@0.0.0", 76 | "pkgId": "b@0.0.0", 77 | "deps": [ 78 | { 79 | "nodeId": "c@0.0.0|1" 80 | } 81 | ] 82 | }, 83 | { 84 | "nodeId": "c@0.0.0|1", 85 | "pkgId": "c@0.0.0", 86 | "deps": [ 87 | { 88 | "nodeId": "d@0.0.0" 89 | } 90 | ] 91 | }, 92 | { 93 | "nodeId": "d@0.0.0", 94 | "pkgId": "d@0.0.0", 95 | "deps": [] 96 | }, 97 | { 98 | "nodeId": "c@0.0.0|2", 99 | "pkgId": "c@0.0.0", 100 | "deps": [ 101 | { 102 | "nodeId": "e@0.0.0" 103 | } 104 | ] 105 | }, 106 | { 107 | "nodeId": "e@0.0.0", 108 | "pkgId": "e@0.0.0", 109 | "deps": [] 110 | } 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/legacy/to-dep-tree-prune.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import * as helpers from '../helpers'; 3 | 4 | test('depTree pruning works as expected with 1 top-level dep', async () => { 5 | const origTree = helpers.loadFixture('pruneable-tree.json'); 6 | const depGraph = await depGraphLib.legacy.depTreeToGraph(origTree, 'maven'); 7 | 8 | const expectedDepTree = helpers.loadFixture('pruneable-tree-pruned.json'); 9 | const depTree = await depGraphLib.legacy.graphToDepTree(depGraph, 'maven', { 10 | deduplicateWithinTopLevelDeps: true, 11 | }); 12 | 13 | expect(depTree.type).toEqual('maven'); 14 | delete depTree.type; 15 | expect(depTree).toEqual(expectedDepTree); 16 | }); 17 | 18 | test('depTree pruning works as expected with multi top-level deps', async () => { 19 | const origTree = helpers.loadFixture( 20 | 'pruneable-tree-multi-top-level-deps.json', 21 | ); 22 | const depGraph = await depGraphLib.legacy.depTreeToGraph(origTree, 'maven'); 23 | 24 | const expectedDepTree = helpers.loadFixture( 25 | 'pruneable-tree-multi-top-level-deps-pruned.json', 26 | ); 27 | const depTree = await depGraphLib.legacy.graphToDepTree(depGraph, 'maven', { 28 | deduplicateWithinTopLevelDeps: true, 29 | }); 30 | 31 | expect(depTree.type).toEqual('maven'); 32 | delete depTree.type; 33 | expect(depTree).toEqual(expectedDepTree); 34 | }); 35 | 36 | test('depTree is a no-op for dysmorphic trees', async () => { 37 | // NOTE: this package tree is "dysmorphic" 38 | // i.e. it has a package that appears twice in the tree 39 | // at the exact same version, but with slightly differently resolved 40 | // dependencies 41 | const origTree = helpers.loadFixture('simple-dep-tree.json'); 42 | const depGraph = await depGraphLib.legacy.depTreeToGraph(origTree, 'maven'); 43 | const depTree = await depGraphLib.legacy.graphToDepTree(depGraph, 'maven', { 44 | deduplicateWithinTopLevelDeps: true, 45 | }); 46 | 47 | expect(depTree.type).toEqual('maven'); 48 | delete depTree.type; 49 | expect(depTree).toEqual(origTree); 50 | }); 51 | 52 | test('subdeps from different direct deps are not deduped', async () => { 53 | const origTree = helpers.loadFixture('unpruneable-tree.json'); 54 | const depGraph = await depGraphLib.legacy.depTreeToGraph(origTree, 'maven'); 55 | const depTree = await depGraphLib.legacy.graphToDepTree(depGraph, 'maven', { 56 | deduplicateWithinTopLevelDeps: true, 57 | }); 58 | 59 | expect(depTree.type).toEqual('maven'); 60 | delete depTree.type; 61 | expect(depTree).toEqual(origTree); 62 | }); 63 | -------------------------------------------------------------------------------- /test/core/pkg-paths-to-root.test.ts: -------------------------------------------------------------------------------- 1 | import { createFromJSON, DepGraphData } from '../../src'; 2 | import { loadFixture } from '../helpers'; 3 | 4 | describe('pkgPathsToRoot', () => { 5 | it('calculates paths from a package to the root', () => { 6 | const graphJson: DepGraphData = loadFixture('simple-graph.json'); 7 | 8 | const depGraph = createFromJSON(graphJson); 9 | 10 | const paths = depGraph.pkgPathsToRoot({ 11 | name: 'e', 12 | version: '5.0.0', 13 | }); 14 | 15 | expect(paths).toEqual([ 16 | [ 17 | { 18 | name: 'e', 19 | version: '5.0.0', 20 | }, 21 | { 22 | name: 'd', 23 | version: '0.0.1', 24 | }, 25 | { 26 | name: 'c', 27 | version: '1.0.0', 28 | }, 29 | { 30 | name: 'a', 31 | version: '1.0.0', 32 | }, 33 | { 34 | name: 'root', 35 | version: '0.0.0', 36 | }, 37 | ], 38 | [ 39 | { 40 | name: 'e', 41 | version: '5.0.0', 42 | }, 43 | { 44 | name: 'd', 45 | version: '0.0.2', 46 | }, 47 | { 48 | name: 'c', 49 | version: '1.0.0', 50 | }, 51 | { 52 | name: 'b', 53 | version: '1.0.0', 54 | }, 55 | { 56 | name: 'root', 57 | version: '0.0.0', 58 | }, 59 | ], 60 | ]); 61 | }); 62 | 63 | it('can limit the number of paths returned', () => { 64 | const graphJson: DepGraphData = loadFixture('simple-graph.json'); 65 | const depGraph = createFromJSON(graphJson); 66 | const limit = 1; 67 | 68 | const pathsWithoutLimit = depGraph.pkgPathsToRoot({ 69 | name: 'e', 70 | version: '5.0.0', 71 | }); 72 | 73 | const pathsWithlimit = depGraph.pkgPathsToRoot( 74 | { 75 | name: 'e', 76 | version: '5.0.0', 77 | }, 78 | { limit }, 79 | ); 80 | 81 | expect(pathsWithoutLimit.length).toBeGreaterThan(limit); 82 | expect(pathsWithlimit).toHaveLength(limit); 83 | }); 84 | 85 | describe('cycles', () => { 86 | const depGraphData = loadFixture('cyclic-complex-dep-graph.json'); 87 | const depGraph = createFromJSON(depGraphData); 88 | it('returns expected paths for all packages', () => { 89 | depGraph.getPkgs().forEach((pkg) => { 90 | const pkgPathsToRoot = depGraph.pkgPathsToRoot(pkg); 91 | expect(pkgPathsToRoot).toMatchSnapshot(`${pkg.name}@${pkg.version}`); 92 | }); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | prodsec: snyk/prodsec-orb@1.1 5 | 6 | defaults: &defaults 7 | resource_class: small 8 | docker: 9 | - image: cimg/node:14.19 10 | 11 | go_image: &go_image 12 | resource_class: small 13 | docker: 14 | - image: cimg/go:1.24.9 15 | 16 | jobs: 17 | security-scans: 18 | <<: *defaults 19 | steps: 20 | - checkout 21 | - run: 22 | name: Install 23 | command: npm install 24 | - prodsec/security_scans: 25 | mode: auto 26 | iac-scan: disabled 27 | release-branch: master 28 | 29 | test_ts: 30 | <<: *defaults 31 | working_directory: ~/work 32 | steps: 33 | - checkout 34 | - run: 35 | name: Install 36 | command: npm install 37 | - run: 38 | name: Lint 39 | command: npm run lint 40 | - run: 41 | name: Test 42 | command: npm test 43 | 44 | test_go: 45 | <<: *go_image 46 | working_directory: ~/work 47 | steps: 48 | - checkout 49 | - run: 50 | name: Test 51 | working_directory: go 52 | command: mkdir -p ../testresults && gotestsum --junitfile ../testresults/test-results.xml ./... 53 | - store_test_results: 54 | path: testresults/ 55 | 56 | release: 57 | <<: *defaults 58 | working_directory: ~/work 59 | steps: 60 | - checkout 61 | - run: 62 | name: Install 63 | command: npm install 64 | - run: 65 | name: Release 66 | command: npx semantic-release@17 67 | 68 | workflows: 69 | version: 2 70 | test: 71 | jobs: 72 | - prodsec/secrets-scan: 73 | name: Scan repository for secrets 74 | context: 75 | - snyk-bot-slack 76 | channel: snyk-vuln-alerts-arch 77 | filters: 78 | branches: 79 | ignore: 80 | - master 81 | 82 | - security-scans: 83 | name: Security Scans 84 | context: 85 | - team-analysis-arch 86 | 87 | - test_ts: 88 | name: Test TS code 89 | requires: 90 | - Scan repository for secrets 91 | - Security Scans 92 | 93 | - test_go: 94 | name: Test Go code 95 | requires: 96 | - Scan repository for secrets 97 | - Security Scans 98 | 99 | - release: 100 | name: Release 101 | requires: 102 | - Test TS code 103 | - Test Go code 104 | context: nodejs-lib-release 105 | filters: 106 | branches: 107 | only: 108 | - master 109 | -------------------------------------------------------------------------------- /test/fixtures/cyclic-complex-dep-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "// graph image": "cyclic-complex-dep-graph.png", 3 | "// expected dep-tree image": "cyclic-complex-dep-graph-expected-optimized-tree.png", 4 | "schemaVersion": "1.2.0", 5 | "pkgManager": { 6 | "name": "pip" 7 | }, 8 | "pkgs": [ 9 | { 10 | "id": "a@1", 11 | "info": { 12 | "name": "a", 13 | "version": "1" 14 | } 15 | }, 16 | { 17 | "id": "b@2", 18 | "info": { 19 | "name": "b", 20 | "version": "2" 21 | } 22 | }, 23 | { 24 | "id": "c@3", 25 | "info": { 26 | "name": "c", 27 | "version": "3" 28 | } 29 | }, 30 | { 31 | "id": "d@4", 32 | "info": { 33 | "name": "d", 34 | "version": "4" 35 | } 36 | }, 37 | { 38 | "id": "e@5", 39 | "info": { 40 | "name": "e", 41 | "version": "5" 42 | } 43 | }, 44 | { 45 | "id": "f@6", 46 | "info": { 47 | "name": "f", 48 | "version": "6" 49 | } 50 | }, 51 | { 52 | "id": "g@7", 53 | "info": { 54 | "name": "g", 55 | "version": "7" 56 | } 57 | } 58 | ], 59 | "graph": { 60 | "rootNodeId": "root-node", 61 | "nodes": [ 62 | { 63 | "nodeId": "root-node", 64 | "pkgId": "a@1", 65 | "deps": [ 66 | { 67 | "nodeId": "2" 68 | }, 69 | { 70 | "nodeId": "3" 71 | }, 72 | { 73 | "nodeId": "4" 74 | } 75 | ] 76 | }, 77 | { 78 | "nodeId": "2", 79 | "pkgId": "b@2", 80 | "deps": [ 81 | { 82 | "nodeId": "5" 83 | } 84 | ] 85 | }, 86 | { 87 | "nodeId": "3", 88 | "pkgId": "c@3", 89 | "deps": [ 90 | { 91 | "nodeId": "5" 92 | } 93 | ] 94 | }, 95 | { 96 | "nodeId": "4", 97 | "pkgId": "d@4", 98 | "deps": [ 99 | { 100 | "nodeId": "6" 101 | } 102 | ] 103 | }, 104 | { 105 | "nodeId": "5", 106 | "pkgId": "e@5", 107 | "deps": [ 108 | { 109 | "nodeId": "6" 110 | } 111 | ] 112 | }, 113 | { 114 | "nodeId": "6", 115 | "pkgId": "f@6", 116 | "deps": [ 117 | { 118 | "nodeId": "7" 119 | } 120 | ] 121 | }, 122 | { 123 | "nodeId": "7", 124 | "pkgId": "g@7", 125 | "deps": [ 126 | { 127 | "nodeId": "5" 128 | } 129 | ] 130 | } 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/fixtures/old-schema-compat/simple-graph-1.0.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [] 76 | }, 77 | { 78 | "nodeId": "d@0.0.1", 79 | "pkgId": "d@0.0.1", 80 | "deps": [ 81 | { 82 | "nodeId": "e@5.0.0" 83 | } 84 | ] 85 | }, 86 | { 87 | "nodeId": "c@1.0.0|1", 88 | "pkgId": "c@1.0.0", 89 | "deps": [ 90 | { 91 | "nodeId": "d@0.0.1" 92 | } 93 | ] 94 | }, 95 | { 96 | "nodeId": "c@1.0.0|2", 97 | "pkgId": "c@1.0.0", 98 | "deps": [ 99 | { 100 | "nodeId": "d@0.0.2" 101 | } 102 | ] 103 | }, 104 | { 105 | "nodeId": "a@1.0.0", 106 | "pkgId": "a@1.0.0", 107 | "deps": [ 108 | { 109 | "nodeId": "c@1.0.0|1" 110 | } 111 | ] 112 | }, 113 | { 114 | "nodeId": "d@0.0.2", 115 | "pkgId": "d@0.0.2", 116 | "deps": [ 117 | { 118 | "nodeId": "e@5.0.0" 119 | } 120 | ] 121 | }, 122 | { 123 | "nodeId": "b@1.0.0", 124 | "pkgId": "b@1.0.0", 125 | "deps": [ 126 | { 127 | "nodeId": "c@1.0.0|2" 128 | } 129 | ] 130 | } 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/core/filter-from-graph.ts: -------------------------------------------------------------------------------- 1 | import { DepGraph, DepGraphInternal, NodeInfo, PkgInfo } from './types'; 2 | import { DepGraphBuilder } from './builder'; 3 | 4 | type NodeId = string; 5 | type PkgName = string; 6 | 7 | export async function filterPackagesFromGraph( 8 | originalDepGraph: DepGraph, 9 | packagesToFilterOut: (PkgName | PkgInfo)[], 10 | ): Promise { 11 | if (!packagesToFilterOut?.length) return originalDepGraph; 12 | 13 | const depGraph = originalDepGraph as DepGraphInternal; 14 | 15 | const packages = depGraph 16 | .getDepPkgs() 17 | .filter((existingPkg) => 18 | packagesToFilterOut.some((pkgToFilter) => 19 | isString(pkgToFilter) 20 | ? existingPkg.name === pkgToFilter 21 | : existingPkg.name === pkgToFilter.name && 22 | existingPkg.version === pkgToFilter.version, 23 | ), 24 | ); 25 | 26 | const nodeIdsToFilterOut: NodeId[] = []; 27 | for (const pkg of packages) { 28 | const nodeIds = depGraph.getPkgNodeIds(pkg); 29 | for (const nodeId of nodeIds) { 30 | nodeIdsToFilterOut.push(nodeId); 31 | } 32 | } 33 | 34 | return filterNodesFromGraph(originalDepGraph, nodeIdsToFilterOut); 35 | } 36 | 37 | export async function filterNodesFromGraph( 38 | originalDepGraph: DepGraph, 39 | nodeIdsToFilterOut: NodeId[], 40 | ): Promise { 41 | if (!nodeIdsToFilterOut?.length) return originalDepGraph; 42 | 43 | const depGraph = originalDepGraph as DepGraphInternal; 44 | const existingNodeIds: Set = new Set(depGraph['_graph'].nodes()); 45 | nodeIdsToFilterOut = nodeIdsToFilterOut.filter((nodeId) => 46 | existingNodeIds.has(nodeId), 47 | ); 48 | if (nodeIdsToFilterOut.length === 0) return originalDepGraph; 49 | 50 | const depGraphBuilder = new DepGraphBuilder( 51 | depGraph.pkgManager, 52 | depGraph.rootPkg, 53 | ); 54 | 55 | const nodeIdsToFilterOutSet = new Set(nodeIdsToFilterOut); 56 | 57 | const queue: [NodeId, NodeId?][] = [[depGraph.rootNodeId, undefined]]; 58 | 59 | while (queue.length > 0) { 60 | const [nodeId, parentNodeId] = queue.pop()!; 61 | 62 | if (nodeIdsToFilterOutSet.has(nodeId)) continue; 63 | 64 | if (parentNodeId) { 65 | const pkgInfo = depGraph.getNodePkg(nodeId); 66 | let nodeInfo: NodeInfo | undefined = depGraph.getNode(nodeId); 67 | if (isEmpty(nodeInfo)) nodeInfo = undefined; 68 | 69 | depGraphBuilder.addPkgNode(pkgInfo, nodeId, nodeInfo); 70 | depGraphBuilder.connectDep(parentNodeId, nodeId); 71 | } 72 | 73 | const dependencies = depGraph.getNodeDepsNodeIds(nodeId).slice().reverse(); 74 | 75 | for (const depNodeId of dependencies) { 76 | queue.push([depNodeId, nodeId]); 77 | } 78 | } 79 | 80 | return depGraphBuilder.build(); 81 | } 82 | 83 | function isString(pkgToFilter: string | PkgInfo): pkgToFilter is string { 84 | return typeof pkgToFilter === 'string'; 85 | } 86 | 87 | function isEmpty(obj) { 88 | return !obj || Object.keys(obj).length === 0; 89 | } 90 | -------------------------------------------------------------------------------- /test/legacy/__snapshots__/to-dep-tree.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`graphs with cycles are supported 1`] = ` 4 | { 5 | "dependencies": { 6 | "b": { 7 | "dependencies": { 8 | "e": { 9 | "dependencies": { 10 | "f": { 11 | "dependencies": { 12 | "g": { 13 | "dependencies": { 14 | "e": { 15 | "labels": { 16 | "pruned": "cyclic", 17 | }, 18 | "name": "e", 19 | "version": "5", 20 | }, 21 | }, 22 | "name": "g", 23 | "version": "7", 24 | }, 25 | }, 26 | "name": "f", 27 | "version": "6", 28 | }, 29 | }, 30 | "name": "e", 31 | "version": "5", 32 | }, 33 | }, 34 | "name": "b", 35 | "version": "2", 36 | }, 37 | "c": { 38 | "dependencies": { 39 | "e": { 40 | "dependencies": { 41 | "f": { 42 | "dependencies": { 43 | "g": { 44 | "dependencies": { 45 | "e": { 46 | "labels": { 47 | "pruned": "cyclic", 48 | }, 49 | "name": "e", 50 | "version": "5", 51 | }, 52 | }, 53 | "name": "g", 54 | "version": "7", 55 | }, 56 | }, 57 | "name": "f", 58 | "version": "6", 59 | }, 60 | }, 61 | "name": "e", 62 | "version": "5", 63 | }, 64 | }, 65 | "name": "c", 66 | "version": "3", 67 | }, 68 | "d": { 69 | "dependencies": { 70 | "f": { 71 | "dependencies": { 72 | "g": { 73 | "dependencies": { 74 | "e": { 75 | "dependencies": { 76 | "f": { 77 | "labels": { 78 | "pruned": "cyclic", 79 | }, 80 | "name": "f", 81 | "version": "6", 82 | }, 83 | }, 84 | "name": "e", 85 | "version": "5", 86 | }, 87 | }, 88 | "name": "g", 89 | "version": "7", 90 | }, 91 | }, 92 | "name": "f", 93 | "version": "6", 94 | }, 95 | }, 96 | "name": "d", 97 | "version": "4", 98 | }, 99 | }, 100 | "name": "a", 101 | "packageFormatVersion": "pip:0.0.1", 102 | "type": "pip", 103 | "version": "1", 104 | } 105 | `; 106 | -------------------------------------------------------------------------------- /test/fixtures/os-deb-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "deb", 5 | "repositories": [ 6 | { 7 | "alias": "ubuntu:18.04" 8 | } 9 | ] 10 | }, 11 | "pkgs": [ 12 | { 13 | "id": "root@0.0.0", 14 | "info": { 15 | "name": "root", 16 | "version": "0.0.0" 17 | } 18 | }, 19 | { 20 | "id": "e@5.0.0", 21 | "info": { 22 | "name": "e", 23 | "version": "5.0.0" 24 | } 25 | }, 26 | { 27 | "id": "d@0.0.1", 28 | "info": { 29 | "name": "d", 30 | "version": "0.0.1" 31 | } 32 | }, 33 | { 34 | "id": "c@1.0.0", 35 | "info": { 36 | "name": "c", 37 | "version": "1.0.0" 38 | } 39 | }, 40 | { 41 | "id": "a@1.0.0", 42 | "info": { 43 | "name": "a", 44 | "version": "1.0.0" 45 | } 46 | }, 47 | { 48 | "id": "d@0.0.2", 49 | "info": { 50 | "name": "d", 51 | "version": "0.0.2" 52 | } 53 | }, 54 | { 55 | "id": "b@1.0.0", 56 | "info": { 57 | "name": "b", 58 | "version": "1.0.0" 59 | } 60 | } 61 | ], 62 | "graph": { 63 | "rootNodeId": "root-node", 64 | "nodes": [ 65 | { 66 | "nodeId": "root-node", 67 | "pkgId": "root@0.0.0", 68 | "deps": [ 69 | { 70 | "nodeId": "a@1.0.0" 71 | }, 72 | { 73 | "nodeId": "b@1.0.0" 74 | } 75 | ] 76 | }, 77 | { 78 | "nodeId": "e@5.0.0", 79 | "pkgId": "e@5.0.0", 80 | "deps": [] 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|1", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|2", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|1" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "deps": [ 131 | { 132 | "nodeId": "c@1.0.0|2" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|2", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|1", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|2" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "deps": [ 131 | { 132 | "nodeId": "c@1.0.0|1" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/fixtures/simple-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|1", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|2", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|1" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "deps": [ 131 | { 132 | "nodeId": "c@1.0.0|2" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-different-root.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.1", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.1" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.1", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|2", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|1", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|2" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "deps": [ 131 | { 132 | "nodeId": "c@1.0.0|1" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-different-minor-verion.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.7", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.7" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.7", 84 | "pkgId": "d@0.0.7", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|2", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.7" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|1", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|2" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "deps": [ 131 | { 132 | "nodeId": "c@1.0.0|1" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/core/builder.ts: -------------------------------------------------------------------------------- 1 | import * as graphlib from '../graphlib'; 2 | import * as types from './types'; 3 | import { DepGraphImpl } from './dep-graph'; 4 | import { validatePackageURL } from './validate-graph'; 5 | 6 | export { DepGraphBuilder }; 7 | 8 | class DepGraphBuilder { 9 | get rootNodeId(): string { 10 | return this._rootNodeId; 11 | } 12 | 13 | private static _getPkgId(pkg: types.Pkg): string { 14 | return `${pkg.name}@${pkg.version || ''}`; 15 | } 16 | 17 | private _pkgs: { [pkgId: string]: types.PkgInfo } = {}; 18 | private _pkgNodes: { [pkgId: string]: Set } = {}; 19 | 20 | private _graph: graphlib.Graph; 21 | private _pkgManager: types.PkgManager; 22 | 23 | private _rootNodeId: string; 24 | private _rootPkgId: string; 25 | 26 | public constructor( 27 | pkgManager: types.PkgManager, 28 | rootPkg?: types.PkgInfo, 29 | nodeInfo?: types.NodeInfo, 30 | ) { 31 | const graph = new graphlib.Graph({ 32 | directed: true, 33 | multigraph: false, 34 | compound: false, 35 | }); 36 | if (!rootPkg) { 37 | rootPkg = { 38 | name: '_root', 39 | version: '0.0.0', 40 | }; 41 | } 42 | 43 | this._rootNodeId = 'root-node'; 44 | this._rootPkgId = DepGraphBuilder._getPkgId(rootPkg); 45 | this._pkgs[this._rootPkgId] = rootPkg; 46 | 47 | graph.setNode(this._rootNodeId, { pkgId: this._rootPkgId, info: nodeInfo }); 48 | this._pkgNodes[this._rootPkgId] = new Set([this._rootNodeId]); 49 | 50 | this._graph = graph; 51 | this._pkgManager = pkgManager; 52 | } 53 | 54 | public getPkgs(): types.PkgInfo[] { 55 | return Object.values(this._pkgs); 56 | } 57 | 58 | // TODO: this can create disconnected nodes 59 | public addPkgNode( 60 | pkgInfo: types.PkgInfo, 61 | nodeId: string, 62 | nodeInfo?: types.NodeInfo, 63 | ) { 64 | if (nodeId === this._rootNodeId) { 65 | throw new Error('DepGraphBuilder.addPkgNode() cant override root node'); 66 | } 67 | 68 | validatePackageURL(pkgInfo); 69 | 70 | const pkgId = DepGraphBuilder._getPkgId(pkgInfo); 71 | 72 | this._pkgs[pkgId] = pkgInfo; 73 | this._pkgNodes[pkgId] = this._pkgNodes[pkgId] || new Set(); 74 | this._pkgNodes[pkgId].add(nodeId); 75 | 76 | this._graph.setNode(nodeId, { pkgId, info: nodeInfo }); 77 | return this; 78 | } 79 | 80 | // TODO: this can create cycles 81 | public connectDep(parentNodeId: string, depNodeId: string) { 82 | if (!this._graph.hasNode(parentNodeId)) { 83 | throw new Error('parentNodeId does not exist'); 84 | } 85 | 86 | if (!this._graph.hasNode(depNodeId)) { 87 | throw new Error('depNodeId does not exist'); 88 | } 89 | 90 | this._graph.setEdge(parentNodeId, depNodeId); 91 | return this; 92 | } 93 | 94 | public build(): types.DepGraph { 95 | return new DepGraphImpl( 96 | this._graph, 97 | this._rootNodeId, 98 | this._pkgs, 99 | this._pkgNodes, 100 | this._pkgManager, 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-one-more-child.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|2", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | }, 98 | { 99 | "nodeId": "e@5.0.0" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "c@1.0.0|1", 105 | "pkgId": "c@1.0.0", 106 | "deps": [ 107 | { 108 | "nodeId": "d@0.0.2" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "a@1.0.0", 114 | "pkgId": "a@1.0.0", 115 | "deps": [ 116 | { 117 | "nodeId": "c@1.0.0|2" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "d@0.0.2", 123 | "pkgId": "d@0.0.2", 124 | "deps": [ 125 | { 126 | "nodeId": "e@5.0.0" 127 | } 128 | ] 129 | }, 130 | { 131 | "nodeId": "b@1.0.0", 132 | "pkgId": "b@1.0.0", 133 | "deps": [ 134 | { 135 | "nodeId": "c@1.0.0|1" 136 | } 137 | ] 138 | } 139 | ] 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/fixtures/labelled-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@2.0.0", 23 | "info": { 24 | "name": "d", 25 | "version": "2.0.0" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "b@1.0.0", 44 | "info": { 45 | "name": "b", 46 | "version": "1.0.0" 47 | } 48 | } 49 | ], 50 | "graph": { 51 | "rootNodeId": "root-node", 52 | "nodes": [ 53 | { 54 | "nodeId": "root-node", 55 | "pkgId": "root@0.0.0", 56 | "deps": [ 57 | { 58 | "nodeId": "a@1.0.0" 59 | }, 60 | { 61 | "nodeId": "b@1.0.0" 62 | } 63 | ] 64 | }, 65 | { 66 | "nodeId": "e@5.0.0", 67 | "pkgId": "e@5.0.0", 68 | "deps": [], 69 | "info": { 70 | "labels": { 71 | "key": "value" 72 | } 73 | } 74 | }, 75 | { 76 | "nodeId": "d@2.0.0|1", 77 | "pkgId": "d@2.0.0", 78 | "deps": [ 79 | { 80 | "nodeId": "e@5.0.0" 81 | } 82 | ], 83 | "info": { 84 | "labels": { 85 | "key": "value1" 86 | } 87 | } 88 | }, 89 | { 90 | "nodeId": "d@2.0.0|2", 91 | "pkgId": "d@2.0.0", 92 | "deps": [ 93 | { 94 | "nodeId": "e@5.0.0" 95 | } 96 | ], 97 | "info": { 98 | "labels": { 99 | "key": "value2" 100 | } 101 | } 102 | }, 103 | { 104 | "nodeId": "c@1.0.0|1", 105 | "pkgId": "c@1.0.0", 106 | "deps": [ 107 | { 108 | "nodeId": "d@2.0.0|1" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "c@1.0.0|2", 114 | "pkgId": "c@1.0.0", 115 | "deps": [ 116 | { 117 | "nodeId": "d@2.0.0|2" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "a@1.0.0", 123 | "pkgId": "a@1.0.0", 124 | "deps": [ 125 | { 126 | "nodeId": "c@1.0.0|1" 127 | } 128 | ] 129 | }, 130 | { 131 | "nodeId": "b@1.0.0", 132 | "pkgId": "b@1.0.0", 133 | "deps": [ 134 | { 135 | "nodeId": "c@1.0.0|2" 136 | } 137 | ] 138 | } 139 | ] 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/core/filter-from-graph.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import * as helpers from '../helpers'; 3 | import { 4 | filterNodesFromGraph, 5 | filterPackagesFromGraph, 6 | } from '../../src/core/filter-from-graph'; 7 | import { PkgInfo } from '../../src'; 8 | 9 | describe('filter-from-graph', function () { 10 | let depGraph; 11 | 12 | beforeEach(async () => { 13 | depGraph = depGraphLib.createFromJSON( 14 | helpers.loadFixture('goof-graph.json'), 15 | ); 16 | }); 17 | 18 | describe('filterNodesFromGraph', () => { 19 | describe('should return same depGraph if nodeIdsToFilterOut is empty', () => { 20 | const testCases = [null, undefined, []]; 21 | 22 | it.each(testCases)('%s', async (nodeIdsToFilterOut) => { 23 | const result = await filterNodesFromGraph( 24 | depGraph, 25 | nodeIdsToFilterOut as any, 26 | ); 27 | 28 | expect(result).toBe(depGraph); 29 | }); 30 | }); 31 | 32 | it('should remove direct dependencies', async () => { 33 | const result = await filterNodesFromGraph(depGraph, ['adm-zip@0.4.7']); 34 | 35 | expect(result.getDepPkgs().length).toEqual( 36 | depGraph.getDepPkgs().length - 1, 37 | ); 38 | 39 | expect(result.toJSON()).toMatchSnapshot({ 40 | schemaVersion: expect.any(String), 41 | }); 42 | }); 43 | 44 | it('should not mutate original depGraph', async () => { 45 | const depGraphCloneJson = JSON.parse(JSON.stringify(depGraph.toJSON())); 46 | 47 | await filterNodesFromGraph(depGraph, ['adm-zip@0.4.7']); 48 | 49 | expect(depGraph.toJSON()).toEqual(depGraphCloneJson); 50 | }); 51 | 52 | it("should return same instance if node ids to filter don't exists", async () => { 53 | const result = await filterNodesFromGraph(depGraph, ['blabla@1.2.3']); 54 | 55 | expect(result).toBe(depGraph); 56 | }); 57 | }); 58 | 59 | describe('filterPackagesFromGraph', () => { 60 | const admZip = { name: 'adm-zip', version: '0.4.7' }; 61 | const bytes = { name: 'bytes', version: '1.0.0' }; 62 | const testCases = [ 63 | ['one existing name', [admZip.name]], 64 | ['one existing PkgInfo', [admZip]], 65 | ['two existing names', [admZip.name, bytes.name]], 66 | ['two existing PkgInfos', [admZip, bytes]], 67 | ] as [string, (string | PkgInfo)[]][]; 68 | 69 | it.each(testCases)('%s', async (_, pkgs) => { 70 | const result = await filterPackagesFromGraph(depGraph, pkgs); 71 | 72 | expect(result.getDepPkgs().length).toEqual( 73 | depGraph.getDepPkgs().length - pkgs.length, 74 | ); 75 | expect(result.toJSON()).toMatchSnapshot({ 76 | schemaVersion: expect.any(String), 77 | }); 78 | }); 79 | 80 | it('should return an empty graph', async () => { 81 | const pkgs = depGraph.getDepPkgs(); 82 | const result = await filterPackagesFromGraph(depGraph, pkgs); 83 | 84 | expect(result.getDepPkgs().length).toBe(0); 85 | expect(result.toJSON()).toMatchSnapshot({ 86 | schemaVersion: expect.any(String), 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/fixtures/equals/simple-with-label.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "maven" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | } 56 | ], 57 | "graph": { 58 | "rootNodeId": "root-node", 59 | "nodes": [ 60 | { 61 | "nodeId": "root-node", 62 | "pkgId": "root@0.0.0", 63 | "deps": [ 64 | { 65 | "nodeId": "a@1.0.0" 66 | }, 67 | { 68 | "nodeId": "b@1.0.0" 69 | } 70 | ] 71 | }, 72 | { 73 | "nodeId": "e@5.0.0", 74 | "pkgId": "e@5.0.0", 75 | "deps": [], 76 | "info": { 77 | "labels": { 78 | "key": "value" 79 | } 80 | } 81 | }, 82 | { 83 | "nodeId": "d@0.0.1", 84 | "pkgId": "d@0.0.1", 85 | "deps": [ 86 | { 87 | "nodeId": "e@5.0.0" 88 | } 89 | ] 90 | }, 91 | { 92 | "nodeId": "c@1.0.0|2", 93 | "pkgId": "c@1.0.0", 94 | "deps": [ 95 | { 96 | "nodeId": "d@0.0.1" 97 | } 98 | ] 99 | }, 100 | { 101 | "nodeId": "c@1.0.0|1", 102 | "pkgId": "c@1.0.0", 103 | "deps": [ 104 | { 105 | "nodeId": "d@0.0.2" 106 | } 107 | ] 108 | }, 109 | { 110 | "nodeId": "a@1.0.0", 111 | "pkgId": "a@1.0.0", 112 | "deps": [ 113 | { 114 | "nodeId": "c@1.0.0|2" 115 | } 116 | ] 117 | }, 118 | { 119 | "nodeId": "d@0.0.2", 120 | "pkgId": "d@0.0.2", 121 | "deps": [ 122 | { 123 | "nodeId": "e@5.0.0" 124 | } 125 | ] 126 | }, 127 | { 128 | "nodeId": "b@1.0.0", 129 | "pkgId": "b@1.0.0", 130 | "info": { 131 | "labels": { 132 | "a": "b" 133 | } 134 | }, 135 | "deps": [ 136 | { 137 | "nodeId": "c@1.0.0|1" 138 | } 139 | ] 140 | } 141 | ] 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/core/create-changed-packages-graph.ts: -------------------------------------------------------------------------------- 1 | import { DepGraph, DepGraphInternal, NodeInfo, PkgInfo } from './types'; 2 | import { DepGraphImpl } from './dep-graph'; 3 | import { DepGraphBuilder } from './builder'; 4 | import { eventLoopSpinner } from 'event-loop-spinner'; 5 | 6 | type NodeId = string; 7 | 8 | /** 9 | * Creates an induced subgraph of {@param graphB} with only packages 10 | * that are not present in {@param graphA} or have a different version. 11 | * 12 | * @param graphA 13 | * @param graphB 14 | */ 15 | export async function createChangedPackagesGraph( 16 | graphA: DepGraph, 17 | graphB: DepGraph, 18 | ): Promise { 19 | const depGraph = graphB as DepGraphInternal; 20 | 21 | const graphAPackageIds = new Set( 22 | graphA.getDepPkgs().map(DepGraphImpl.getPkgId), 23 | ); 24 | 25 | const addedOrUpdatedPackages: PkgInfo[] = depGraph 26 | .getDepPkgs() 27 | .filter((pkg) => !graphAPackageIds.has(DepGraphImpl.getPkgId(pkg))); 28 | 29 | const depGraphBuilder = new DepGraphBuilder( 30 | depGraph.pkgManager, 31 | depGraph.rootPkg, 32 | ); 33 | 34 | const parentQueue: [parentId: NodeId, nodeId: NodeId][] = []; 35 | for (const changedPackage of addedOrUpdatedPackages) { 36 | for (const changedNodeId of depGraph.getPkgNodeIds(changedPackage)) { 37 | //we add all nodes with new and changed packages to the new graph. 38 | //a newly added node will also have its dependencies added here, since they are "new". 39 | depGraphBuilder.addPkgNode( 40 | depGraph.getNodePkg(changedNodeId), 41 | changedNodeId, 42 | getNodeInfo(depGraph, changedNodeId), 43 | ); 44 | 45 | //Push all direct parents of the changed nodes to a queue to later build up a path to root from that node 46 | for (const parentId of depGraph.getNodeParentsNodeIds(changedNodeId)) { 47 | parentQueue.push([parentId, changedNodeId]); 48 | 49 | if (eventLoopSpinner.isStarving()) { 50 | await eventLoopSpinner.spin(); 51 | } 52 | } 53 | } 54 | } 55 | 56 | //add direct and transitive parents for the changed nodes 57 | const visited = new Set([depGraph.rootNodeId]); 58 | 59 | while (parentQueue.length > 0) { 60 | const [nodeId, dependencyNodeId] = parentQueue.pop()!; 61 | if (visited.has(nodeId)) { 62 | //ensure we link parents even if visited through another path 63 | depGraphBuilder.connectDep(nodeId, dependencyNodeId); 64 | continue; 65 | } 66 | 67 | visited.add(nodeId); 68 | 69 | depGraphBuilder.addPkgNode( 70 | depGraph.getNodePkg(nodeId), 71 | nodeId, 72 | getNodeInfo(depGraph, nodeId), 73 | ); 74 | depGraphBuilder.connectDep(nodeId, dependencyNodeId); 75 | 76 | for (const parentId of depGraph.getNodeParentsNodeIds(nodeId)) { 77 | parentQueue.push([parentId, nodeId]); 78 | 79 | if (eventLoopSpinner.isStarving()) { 80 | await eventLoopSpinner.spin(); 81 | } 82 | } 83 | } 84 | 85 | return depGraphBuilder.build(); 86 | } 87 | 88 | function getNodeInfo( 89 | depGraph: DepGraphInternal, 90 | nodeId: string, 91 | ): NodeInfo | undefined { 92 | const nodeInfo: NodeInfo = depGraph.getNode(nodeId); 93 | if (!nodeInfo || Object.keys(nodeInfo).length === 0) { 94 | return undefined; 95 | } 96 | return nodeInfo; 97 | } 98 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | // using .ts instead of .d.ts for this file, 2 | // as otherwise it's referenced from the auto-generated `index.d.ts`, 3 | // but not copied over to the `./dist` folder 4 | // and thus fails the compilation of a typescript project that imports this lib 5 | 6 | export interface Pkg { 7 | name: string; 8 | version?: string; 9 | } 10 | 11 | export type PurlString = string; 12 | 13 | export interface PkgInfo { 14 | name: string; 15 | version?: string; 16 | purl?: PurlString; 17 | 18 | // NOTE: consider adding in the future 19 | // requires?: { 20 | // name: string; 21 | // versionRange: string; 22 | // dev?: boolean; 23 | // exclusions?: string[]; 24 | // }[]; 25 | // resolved?: { 26 | // registry: string; 27 | // url: string; 28 | // }; 29 | // parentManifestUri?: string; 30 | // issues?: { 31 | // id: string; 32 | // type: string; 33 | // title: string; 34 | // severity: string; 35 | // }[]; 36 | } 37 | 38 | export interface VersionProvenance { 39 | type: string; 40 | location: string; 41 | property?: { 42 | name: string; 43 | }; 44 | } 45 | 46 | export interface NodeInfo { 47 | versionProvenance?: VersionProvenance; 48 | labels?: { 49 | [key: string]: string | undefined; 50 | scope?: 'dev' | 'prod'; 51 | pruned?: 'cyclic' | 'true'; 52 | }; 53 | } 54 | 55 | export interface Node { 56 | // id: string; - can be added later once it's useful as input to other methods 57 | // pkg: PkgInfo; - it can be nice to return this, consider when exposing more node related methods 58 | info: NodeInfo; 59 | } 60 | 61 | export interface GraphNode { 62 | nodeId: string; 63 | pkgId: string; 64 | info?: NodeInfo; 65 | deps: Array<{ 66 | nodeId: string; 67 | // NOTE: consider adding later: 68 | // meta?: JsonMap; 69 | }>; 70 | } 71 | 72 | export interface PkgManager { 73 | name: string; 74 | version?: string; 75 | repositories?: Array<{ 76 | alias: string; 77 | }>; 78 | } 79 | 80 | export interface DepGraphData { 81 | schemaVersion: string; 82 | pkgManager: PkgManager; 83 | pkgs: Array<{ 84 | id: string; 85 | info: PkgInfo; 86 | }>; 87 | graph: { 88 | rootNodeId: string; 89 | nodes: GraphNode[]; 90 | }; 91 | } 92 | 93 | export interface DepGraph { 94 | readonly pkgManager: PkgManager; 95 | readonly rootPkg: PkgInfo; 96 | getPkgs(): PkgInfo[]; 97 | getDepPkgs(): PkgInfo[]; 98 | getPkgNodes(pkg: Pkg): Node[]; 99 | toJSON(): DepGraphData; 100 | pkgPathsToRoot(pkg: Pkg, opts?: { limit?: number }): PkgInfo[][]; 101 | isTransitive(pkg: Pkg): boolean; 102 | directDepsLeadingTo(pkg: Pkg): PkgInfo[]; 103 | countPathsToRoot(pkg: Pkg, opts?: { limit?: number }): number; 104 | equals(other: DepGraph, options?: { compareRoot?: boolean }): boolean; 105 | } 106 | 107 | // NOTE/TODO(shaun): deferring any/all design decisions here 108 | // Revisit when we actually start using things 109 | export interface DepGraphInternal extends DepGraph { 110 | readonly rootNodeId: string; 111 | getNode(nodeId: string): NodeInfo; 112 | getNodePkg(nodeId: string): PkgInfo; 113 | getPkgNodeIds(pkg: Pkg): string[]; 114 | getNodeDepsNodeIds(nodeId: string): string[]; 115 | getNodeParentsNodeIds(nodeId: string): string[]; 116 | hasCycles(): boolean; 117 | } 118 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-removed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | } 77 | ], 78 | "graph": { 79 | "rootNodeId": "root-node", 80 | "nodes": [ 81 | { 82 | "nodeId": "root-node", 83 | "pkgId": "a@1", 84 | "deps": [ 85 | { 86 | "nodeId": "2" 87 | }, 88 | { 89 | "nodeId": "3" 90 | }, 91 | { 92 | "nodeId": "4" 93 | } 94 | ] 95 | }, 96 | { 97 | "nodeId": "2", 98 | "pkgId": "b@2", 99 | "deps": [ 100 | { 101 | "nodeId": "5" 102 | } 103 | ] 104 | }, 105 | { 106 | "nodeId": "3", 107 | "pkgId": "c@3", 108 | "deps": [ 109 | { 110 | "nodeId": "5" 111 | } 112 | ] 113 | }, 114 | { 115 | "nodeId": "4", 116 | "pkgId": "d@4", 117 | "deps": [ 118 | { 119 | "nodeId": "8" 120 | }, 121 | { 122 | "nodeId": "9" 123 | } 124 | ] 125 | }, 126 | { 127 | "nodeId": "5", 128 | "pkgId": "e@5", 129 | "deps": [ 130 | { 131 | "nodeId": "6" 132 | } 133 | ] 134 | }, 135 | { 136 | "nodeId": "6", 137 | "pkgId": "f@6", 138 | "deps": [ 139 | { 140 | "nodeId": "7" 141 | } 142 | ] 143 | }, 144 | { 145 | "nodeId": "7", 146 | "pkgId": "g@7", 147 | "deps": [ 148 | { 149 | "nodeId": "5" 150 | } 151 | ] 152 | }, 153 | { 154 | "nodeId": "8", 155 | "pkgId": "h@2.1", 156 | "deps": [] 157 | }, 158 | { 159 | "nodeId": "9", 160 | "pkgId": "i@9", 161 | "deps": [ 162 | { 163 | "nodeId": "10" 164 | } 165 | ] 166 | }, 167 | { 168 | "nodeId": "10", 169 | "pkgId": "j@1", 170 | "deps": [] 171 | } 172 | ] 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test/core/create-changed-packages-graph.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import * as helpers from '../helpers'; 3 | import { createChangedPackagesGraph } from '../../src'; 4 | 5 | describe('filter-unchanged-packages', () => { 6 | it.each` 7 | fixture 8 | ${'equals/simple.json'} 9 | ${'cyclic-complex-dep-graph.json'} 10 | ${'goof-graph.json'} 11 | `( 12 | 'result and $fixture are equals for empty initial graph', 13 | async ({ fixture }) => { 14 | const graphB = depGraphLib.createFromJSON(helpers.loadFixture(fixture)); 15 | 16 | const graphA = new depGraphLib.DepGraphBuilder( 17 | graphB.pkgManager, 18 | graphB.rootPkg, 19 | ).build(); 20 | 21 | const result = await createChangedPackagesGraph(graphA, graphB); 22 | expect(graphB.equals(result)).toBe(true); 23 | }, 24 | ); 25 | 26 | it.each` 27 | fixtureA | fixtureB | expected 28 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-direct-dep-added.json'} | ${'changed-packages-graph/graph-direct-dep-added-expected.json'} 29 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-direct-dep-changed-cycle.json'} | ${'changed-packages-graph/graph-direct-dep-changed-cycle-expected.json'} 30 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-direct-dep-changed.json'} | ${'changed-packages-graph/graph-direct-dep-changed-expected.json'} 31 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-direct-dep-removed.json'} | ${'changed-packages-graph/graph-direct-dep-removed-expected.json'} 32 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added.json'} | ${'changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added-expected.json'} 33 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-root-and-direct-dep-changed.json'} | ${'changed-packages-graph/graph-root-and-direct-dep-changed-expected.json'} 34 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-root-changed-expected.json'} | ${'changed-packages-graph/graph-root-changed-expected.json'} 35 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-transitive-dep-as-direct-dep.json'} | ${'changed-packages-graph/graph-transitive-dep-as-direct-dep-expected.json'} 36 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-transitive-dep-changed-cycle.json'} | ${'changed-packages-graph/graph-transitive-dep-changed-cycle-expected.json'} 37 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-transitive-dep-changed.json'} | ${'changed-packages-graph/graph-transitive-dep-changed-expected.json'} 38 | ${'changed-packages-graph/graph.json'} | ${'changed-packages-graph/graph-transitive-dep-removed.json'} | ${'changed-packages-graph/graph-transitive-dep-removed-expected.json'} 39 | `( 40 | 'result is $expected for $fixtureA and $fixtureB', 41 | async ({ fixtureA, fixtureB, expected }) => { 42 | const graphA = depGraphLib.createFromJSON(helpers.loadFixture(fixtureA)); 43 | 44 | const graphB = depGraphLib.createFromJSON(helpers.loadFixture(fixtureB)); 45 | 46 | const expectedResult = depGraphLib.createFromJSON( 47 | helpers.loadFixture(expected), 48 | ); 49 | 50 | const result = await createChangedPackagesGraph(graphA, graphB); 51 | expect(expectedResult.equals(result, { compareRoot: true })).toBe(true); 52 | }, 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1.1", 9 | "info": { 10 | "name": "a", 11 | "version": "1.1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1.1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@4", 23 | "info": { 24 | "name": "c", 25 | "version": "4" 26 | } 27 | }, 28 | { 29 | "id": "d@4.1", 30 | "info": { 31 | "name": "d", 32 | "version": "4.1" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@4", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4.1", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@2", 72 | "info": { 73 | "name": "j", 74 | "version": "2" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@2", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-changed-cycle.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3.1", 23 | "info": { 24 | "name": "c", 25 | "version": "3.1" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3.1", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-as-direct-dep.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | }, 101 | { 102 | "nodeId": "11" 103 | } 104 | ] 105 | }, 106 | { 107 | "nodeId": "2", 108 | "pkgId": "b@2", 109 | "deps": [ 110 | { 111 | "nodeId": "5" 112 | } 113 | ] 114 | }, 115 | { 116 | "nodeId": "3", 117 | "pkgId": "c@3", 118 | "deps": [ 119 | { 120 | "nodeId": "5" 121 | } 122 | ] 123 | }, 124 | { 125 | "nodeId": "4", 126 | "pkgId": "d@4", 127 | "deps": [ 128 | { 129 | "nodeId": "8" 130 | }, 131 | { 132 | "nodeId": "9" 133 | } 134 | ] 135 | }, 136 | { 137 | "nodeId": "5", 138 | "pkgId": "e@5", 139 | "deps": [ 140 | { 141 | "nodeId": "6" 142 | } 143 | ] 144 | }, 145 | { 146 | "nodeId": "6", 147 | "pkgId": "f@6", 148 | "deps": [ 149 | { 150 | "nodeId": "7" 151 | } 152 | ] 153 | }, 154 | { 155 | "nodeId": "7", 156 | "pkgId": "g@7", 157 | "deps": [ 158 | { 159 | "nodeId": "5" 160 | } 161 | ] 162 | }, 163 | { 164 | "nodeId": "8", 165 | "pkgId": "h@2.1", 166 | "deps": [] 167 | }, 168 | { 169 | "nodeId": "9", 170 | "pkgId": "i@9", 171 | "deps": [ 172 | { 173 | "nodeId": "10" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-transitive-dep-changed-cycle.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@6", 37 | "info": { 38 | "name": "e", 39 | "version": "6" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@6", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-root-and-direct-dep-changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1.1", 9 | "info": { 10 | "name": "a", 11 | "version": "1.1" 12 | } 13 | }, 14 | { 15 | "id": "b@2.1", 16 | "info": { 17 | "name": "b", 18 | "version": "2.1" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "a@1.1", 91 | "deps": [ 92 | { 93 | "nodeId": "2" 94 | }, 95 | { 96 | "nodeId": "3" 97 | }, 98 | { 99 | "nodeId": "4" 100 | } 101 | ] 102 | }, 103 | { 104 | "nodeId": "2", 105 | "pkgId": "b@2.1", 106 | "deps": [ 107 | { 108 | "nodeId": "5" 109 | } 110 | ] 111 | }, 112 | { 113 | "nodeId": "3", 114 | "pkgId": "c@3", 115 | "deps": [ 116 | { 117 | "nodeId": "5" 118 | } 119 | ] 120 | }, 121 | { 122 | "nodeId": "4", 123 | "pkgId": "d@4", 124 | "deps": [ 125 | { 126 | "nodeId": "8" 127 | }, 128 | { 129 | "nodeId": "9" 130 | } 131 | ] 132 | }, 133 | { 134 | "nodeId": "5", 135 | "pkgId": "e@5", 136 | "deps": [ 137 | { 138 | "nodeId": "6" 139 | } 140 | ] 141 | }, 142 | { 143 | "nodeId": "6", 144 | "pkgId": "f@6", 145 | "deps": [ 146 | { 147 | "nodeId": "7" 148 | } 149 | ] 150 | }, 151 | { 152 | "nodeId": "7", 153 | "pkgId": "g@7", 154 | "deps": [ 155 | { 156 | "nodeId": "5" 157 | } 158 | ] 159 | }, 160 | { 161 | "nodeId": "8", 162 | "pkgId": "h@2.1", 163 | "deps": [] 164 | }, 165 | { 166 | "nodeId": "9", 167 | "pkgId": "i@9", 168 | "deps": [ 169 | { 170 | "nodeId": "10" 171 | }, 172 | { 173 | "nodeId": "11" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "10", 179 | "pkgId": "j@1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "11", 184 | "pkgId": "k@3", 185 | "deps": [] 186 | } 187 | ] 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /go/pkg/depgraph/builder.go: -------------------------------------------------------------------------------- 1 | package depgraph 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/elliotchance/orderedmap/v3" 8 | ) 9 | 10 | type Builder struct { 11 | schemaVersion string 12 | rootNodeID string 13 | rootPkgID string 14 | pkgManager *PkgManager 15 | pkgs *orderedmap.OrderedMap[string, *Pkg] 16 | nodes *orderedmap.OrderedMap[string, *Node] 17 | } 18 | 19 | const ( 20 | schemaVersion = "1.3.0" 21 | rootNodeID = "root-node" 22 | ) 23 | 24 | func NewBuilder(pkgManager *PkgManager, rootPkg *PkgInfo) (*Builder, error) { 25 | if pkgManager == nil { 26 | return nil, errors.New("cannot create builder without a package manager") 27 | } 28 | 29 | if rootPkg == nil { 30 | rootPkg = &PkgInfo{ 31 | Name: "_root", 32 | Version: "unknown", 33 | } 34 | } 35 | 36 | b := &Builder{ 37 | schemaVersion: schemaVersion, 38 | pkgManager: pkgManager, 39 | rootNodeID: rootNodeID, 40 | rootPkgID: getPkgID(rootPkg), 41 | pkgs: orderedmap.NewOrderedMap[string, *Pkg](), 42 | nodes: orderedmap.NewOrderedMap[string, *Node](), 43 | } 44 | 45 | b.addNode(b.rootNodeID, rootPkg) 46 | 47 | return b, nil 48 | } 49 | 50 | func (b *Builder) Build() *DepGraph { 51 | dg := &DepGraph{ 52 | SchemaVersion: b.schemaVersion, 53 | PkgManager: *b.pkgManager, 54 | Pkgs: b.GetPkgs(), 55 | rootPkg: b.pkgs.GetOrDefault(b.rootPkgID, nil), 56 | pkgIdx: make(map[string]*Pkg), 57 | Graph: Graph{ 58 | RootNodeID: b.rootNodeID, 59 | Nodes: make([]Node, b.nodes.Len()), 60 | }, 61 | } 62 | 63 | var i int 64 | for _, node := range b.nodes.AllFromFront() { 65 | nodeID := node.NodeID 66 | pkgID := node.PkgID 67 | deps := node.Deps 68 | info := node.Info 69 | pkg := b.pkgs.GetOrDefault(pkgID, nil) 70 | 71 | dg.pkgIdx[pkg.ID] = pkg 72 | dg.Graph.Nodes[i] = Node{ 73 | NodeID: nodeID, 74 | PkgID: pkgID, 75 | Info: info, 76 | Deps: deps, 77 | } 78 | 79 | i++ 80 | } 81 | 82 | return dg 83 | } 84 | 85 | func (b *Builder) GetPkgManager() *PkgManager { 86 | return b.pkgManager 87 | } 88 | 89 | func (b *Builder) GetPkgs() []Pkg { 90 | pkgs := make([]Pkg, 0, b.pkgs.Len()) 91 | 92 | for _, pkg := range b.pkgs.AllFromFront() { 93 | pkgs = append(pkgs, *pkg) 94 | } 95 | 96 | return pkgs 97 | } 98 | 99 | func (b *Builder) GetRootNode() *Node { 100 | return b.nodes.GetOrDefault(b.rootNodeID, nil) 101 | } 102 | 103 | type nodeOpt = func(*Node) 104 | 105 | func WithNodeInfo(info *NodeInfo) nodeOpt { 106 | return func(node *Node) { 107 | node.Info = info 108 | } 109 | } 110 | 111 | func (b *Builder) AddNode(nodeID string, pkgInfo *PkgInfo, opts ...nodeOpt) *Node { 112 | node := b.addNode(nodeID, pkgInfo) 113 | 114 | for _, opt := range opts { 115 | opt(node) 116 | } 117 | 118 | return node 119 | } 120 | 121 | func (b *Builder) addNode(nodeID string, pkgInfo *PkgInfo) *Node { 122 | if n, ok := b.nodes.Get(nodeID); ok { 123 | return n 124 | } 125 | pkgID := getPkgID(pkgInfo) 126 | 127 | b.pkgs.Set(pkgID, &Pkg{ 128 | ID: pkgID, 129 | Info: *pkgInfo, 130 | }) 131 | 132 | b.nodes.Set(nodeID, &Node{ 133 | NodeID: nodeID, 134 | PkgID: pkgID, 135 | Deps: make([]Dependency, 0), 136 | }) 137 | 138 | return b.nodes.GetOrDefault(nodeID, nil) 139 | } 140 | 141 | func (b *Builder) ConnectNodes(parentNodeID, childNodeID string) error { 142 | parentNode, ok := b.nodes.Get(parentNodeID) 143 | if !ok { 144 | return fmt.Errorf("cound not find parent node %s", parentNodeID) 145 | } 146 | 147 | childNode, ok := b.nodes.Get(childNodeID) 148 | if !ok { 149 | return fmt.Errorf("cound not find child node %s", childNodeID) 150 | } 151 | 152 | parentNode.Deps = append(parentNode.Deps, Dependency{ 153 | NodeID: childNode.NodeID, 154 | }) 155 | 156 | return nil 157 | } 158 | 159 | func getPkgID(pkgInfo *PkgInfo) string { 160 | return fmt.Sprintf("%s@%s", pkgInfo.Name, pkgInfo.Version) 161 | } 162 | -------------------------------------------------------------------------------- /src/core/create-from-json.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | import * as graphlib from '../graphlib'; 3 | import * as types from './types'; 4 | 5 | import { DepGraph, DepGraphData, GraphNode } from './types'; 6 | import { ValidationError } from './errors'; 7 | import { validateGraph } from './validate-graph'; 8 | import { DepGraphImpl } from './dep-graph'; 9 | 10 | export const SUPPORTED_SCHEMA_RANGE = '^1.0.0'; 11 | 12 | /** 13 | * Create a DepGraph instance from a JSON representation of a dep graph. This 14 | * is typically used after passing the graph over the wire as `DepGraphData`. 15 | */ 16 | export function createFromJSON(depGraphData: DepGraphData): DepGraph { 17 | validateDepGraphData(depGraphData); 18 | 19 | const graph = new graphlib.Graph({ 20 | directed: true, 21 | multigraph: false, 22 | compound: false, 23 | }); 24 | const pkgs: { [pkgId: string]: types.PkgInfo } = {}; 25 | const pkgNodes: { [pkgId: string]: Set } = {}; 26 | 27 | for (const { id, info } of depGraphData.pkgs) { 28 | pkgs[id] = info.version ? info : { ...info, version: undefined }; 29 | } 30 | 31 | for (const node of depGraphData.graph.nodes) { 32 | const pkgId = node.pkgId; 33 | if (!pkgNodes[pkgId]) { 34 | pkgNodes[pkgId] = new Set(); 35 | } 36 | pkgNodes[pkgId].add(node.nodeId); 37 | 38 | graph.setNode(node.nodeId, { pkgId, info: node.info }); 39 | } 40 | 41 | for (const node of depGraphData.graph.nodes) { 42 | for (const depNodeId of node.deps) { 43 | graph.setEdge(node.nodeId, depNodeId.nodeId); 44 | } 45 | } 46 | 47 | validateGraph(graph, depGraphData.graph.rootNodeId, pkgs, pkgNodes); 48 | 49 | return new DepGraphImpl( 50 | graph, 51 | depGraphData.graph.rootNodeId, 52 | pkgs, 53 | pkgNodes, 54 | depGraphData.pkgManager, 55 | ); 56 | } 57 | 58 | function assert(condition: boolean, msg: string) { 59 | if (!condition) { 60 | throw new ValidationError(msg); 61 | } 62 | } 63 | 64 | function validateDepGraphData(depGraphData: DepGraphData) { 65 | assert( 66 | !!semver.valid(depGraphData.schemaVersion) && 67 | semver.satisfies(depGraphData.schemaVersion, SUPPORTED_SCHEMA_RANGE), 68 | `dep-graph schemaVersion not in "${SUPPORTED_SCHEMA_RANGE}"`, 69 | ); 70 | assert( 71 | depGraphData.pkgManager && !!depGraphData.pkgManager.name, 72 | '.pkgManager.name is missing', 73 | ); 74 | 75 | const pkgsMap = depGraphData.pkgs.reduce((acc, cur) => { 76 | assert(!(cur.id in acc), 'more than one pkg with same id'); 77 | assert(!!cur.info, '.pkgs item missing .info'); 78 | 79 | acc[cur.id] = cur.info; 80 | return acc; 81 | }, {} as { [pkdId: string]: types.PkgInfo }); 82 | 83 | const nodesMap = depGraphData.graph.nodes.reduce((acc, cur) => { 84 | assert(!(cur.nodeId in acc), 'more than on node with same id'); 85 | 86 | acc[cur.nodeId] = cur; 87 | return acc; 88 | }, {} as { [nodeId: string]: GraphNode }); 89 | 90 | const rootNodeId = depGraphData.graph.rootNodeId; 91 | const rootNode = nodesMap[rootNodeId]; 92 | assert(rootNodeId in nodesMap, `.${rootNodeId} root graph node is missing`); 93 | const rootPkgId = rootNode.pkgId; 94 | assert(rootPkgId in pkgsMap, `.${rootPkgId} root pkg missing`); 95 | assert( 96 | nodesMap[rootNodeId].pkgId === rootPkgId, 97 | `the root node .pkgId should be "${rootPkgId}"`, 98 | ); 99 | const pkgIds = Object.keys(pkgsMap); 100 | // NOTE: this name@version check is very strict, 101 | // we can relax it later, it just makes things easier now 102 | assert( 103 | pkgIds.filter((pkgId) => pkgId !== DepGraphImpl.getPkgId(pkgsMap[pkgId])) 104 | .length === 0, 105 | 'pkgs ids should be name@version', 106 | ); 107 | assert( 108 | Object.values(nodesMap).filter((node) => !(node.pkgId in pkgsMap)) 109 | .length === 0, 110 | 'some instance nodes belong to non-existing pkgIds', 111 | ); 112 | assert( 113 | Object.values(pkgsMap).filter((pkg: { name: string }) => !pkg.name) 114 | .length === 0, 115 | 'some .pkgs elements have no .name field', 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-with-exiting-transitive-dep-added.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | }, 84 | { 85 | "id": "l@0.1", 86 | "info": { 87 | "name": "l", 88 | "version": "0.1" 89 | } 90 | } 91 | ], 92 | "graph": { 93 | "rootNodeId": "root-node", 94 | "nodes": [ 95 | { 96 | "nodeId": "root-node", 97 | "pkgId": "a@1", 98 | "deps": [ 99 | { 100 | "nodeId": "2" 101 | }, 102 | { 103 | "nodeId": "3" 104 | }, 105 | { 106 | "nodeId": "4" 107 | }, 108 | { 109 | "nodeId": "12" 110 | } 111 | ] 112 | }, 113 | { 114 | "nodeId": "2", 115 | "pkgId": "b@2", 116 | "deps": [ 117 | { 118 | "nodeId": "5" 119 | } 120 | ] 121 | }, 122 | { 123 | "nodeId": "3", 124 | "pkgId": "c@3", 125 | "deps": [ 126 | { 127 | "nodeId": "5" 128 | } 129 | ] 130 | }, 131 | { 132 | "nodeId": "4", 133 | "pkgId": "d@4", 134 | "deps": [ 135 | { 136 | "nodeId": "8" 137 | }, 138 | { 139 | "nodeId": "9" 140 | } 141 | ] 142 | }, 143 | { 144 | "nodeId": "5", 145 | "pkgId": "e@5", 146 | "deps": [ 147 | { 148 | "nodeId": "6" 149 | } 150 | ] 151 | }, 152 | { 153 | "nodeId": "6", 154 | "pkgId": "f@6", 155 | "deps": [ 156 | { 157 | "nodeId": "7" 158 | } 159 | ] 160 | }, 161 | { 162 | "nodeId": "7", 163 | "pkgId": "g@7", 164 | "deps": [ 165 | { 166 | "nodeId": "5" 167 | } 168 | ] 169 | }, 170 | { 171 | "nodeId": "8", 172 | "pkgId": "h@2.1", 173 | "deps": [] 174 | }, 175 | { 176 | "nodeId": "9", 177 | "pkgId": "i@9", 178 | "deps": [ 179 | { 180 | "nodeId": "11" 181 | } 182 | ] 183 | }, 184 | { 185 | "nodeId": "10", 186 | "pkgId": "j@1", 187 | "deps": [] 188 | }, 189 | { 190 | "nodeId": "11", 191 | "pkgId": "k@3", 192 | "deps": [] 193 | }, 194 | { 195 | "nodeId": "12", 196 | "pkgId": "l@0.1", 197 | "deps": [ 198 | { 199 | "nodeId": "10" 200 | } 201 | ] 202 | } 203 | ] 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /test/core/__snapshots__/pkg-paths-to-root.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: a@1 1`] = ` 4 | [ 5 | [ 6 | { 7 | "name": "a", 8 | "version": "1", 9 | }, 10 | ], 11 | ] 12 | `; 13 | 14 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: b@2 1`] = ` 15 | [ 16 | [ 17 | { 18 | "name": "b", 19 | "version": "2", 20 | }, 21 | { 22 | "name": "a", 23 | "version": "1", 24 | }, 25 | ], 26 | ] 27 | `; 28 | 29 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: c@3 1`] = ` 30 | [ 31 | [ 32 | { 33 | "name": "c", 34 | "version": "3", 35 | }, 36 | { 37 | "name": "a", 38 | "version": "1", 39 | }, 40 | ], 41 | ] 42 | `; 43 | 44 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: d@4 1`] = ` 45 | [ 46 | [ 47 | { 48 | "name": "d", 49 | "version": "4", 50 | }, 51 | { 52 | "name": "a", 53 | "version": "1", 54 | }, 55 | ], 56 | ] 57 | `; 58 | 59 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: e@5 1`] = ` 60 | [ 61 | [ 62 | { 63 | "name": "e", 64 | "version": "5", 65 | }, 66 | { 67 | "name": "b", 68 | "version": "2", 69 | }, 70 | { 71 | "name": "a", 72 | "version": "1", 73 | }, 74 | ], 75 | [ 76 | { 77 | "name": "e", 78 | "version": "5", 79 | }, 80 | { 81 | "name": "c", 82 | "version": "3", 83 | }, 84 | { 85 | "name": "a", 86 | "version": "1", 87 | }, 88 | ], 89 | [ 90 | { 91 | "name": "e", 92 | "version": "5", 93 | }, 94 | { 95 | "name": "g", 96 | "version": "7", 97 | }, 98 | { 99 | "name": "f", 100 | "version": "6", 101 | }, 102 | { 103 | "name": "d", 104 | "version": "4", 105 | }, 106 | { 107 | "name": "a", 108 | "version": "1", 109 | }, 110 | ], 111 | ] 112 | `; 113 | 114 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: f@6 1`] = ` 115 | [ 116 | [ 117 | { 118 | "name": "f", 119 | "version": "6", 120 | }, 121 | { 122 | "name": "d", 123 | "version": "4", 124 | }, 125 | { 126 | "name": "a", 127 | "version": "1", 128 | }, 129 | ], 130 | [ 131 | { 132 | "name": "f", 133 | "version": "6", 134 | }, 135 | { 136 | "name": "e", 137 | "version": "5", 138 | }, 139 | { 140 | "name": "b", 141 | "version": "2", 142 | }, 143 | { 144 | "name": "a", 145 | "version": "1", 146 | }, 147 | ], 148 | [ 149 | { 150 | "name": "f", 151 | "version": "6", 152 | }, 153 | { 154 | "name": "e", 155 | "version": "5", 156 | }, 157 | { 158 | "name": "c", 159 | "version": "3", 160 | }, 161 | { 162 | "name": "a", 163 | "version": "1", 164 | }, 165 | ], 166 | ] 167 | `; 168 | 169 | exports[`pkgPathsToRoot cycles returns expected paths for all packages: g@7 1`] = ` 170 | [ 171 | [ 172 | { 173 | "name": "g", 174 | "version": "7", 175 | }, 176 | { 177 | "name": "f", 178 | "version": "6", 179 | }, 180 | { 181 | "name": "d", 182 | "version": "4", 183 | }, 184 | { 185 | "name": "a", 186 | "version": "1", 187 | }, 188 | ], 189 | [ 190 | { 191 | "name": "g", 192 | "version": "7", 193 | }, 194 | { 195 | "name": "f", 196 | "version": "6", 197 | }, 198 | { 199 | "name": "e", 200 | "version": "5", 201 | }, 202 | { 203 | "name": "b", 204 | "version": "2", 205 | }, 206 | { 207 | "name": "a", 208 | "version": "1", 209 | }, 210 | ], 211 | [ 212 | { 213 | "name": "g", 214 | "version": "7", 215 | }, 216 | { 217 | "name": "f", 218 | "version": "6", 219 | }, 220 | { 221 | "name": "e", 222 | "version": "5", 223 | }, 224 | { 225 | "name": "c", 226 | "version": "3", 227 | }, 228 | { 229 | "name": "a", 230 | "version": "1", 231 | }, 232 | ], 233 | ] 234 | `; 235 | -------------------------------------------------------------------------------- /test/fixtures/changed-packages-graph/graph-direct-dep-added.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.2.0", 3 | "pkgManager": { 4 | "name": "pip" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "a@1", 9 | "info": { 10 | "name": "a", 11 | "version": "1" 12 | } 13 | }, 14 | { 15 | "id": "b@2", 16 | "info": { 17 | "name": "b", 18 | "version": "2" 19 | } 20 | }, 21 | { 22 | "id": "c@3", 23 | "info": { 24 | "name": "c", 25 | "version": "3" 26 | } 27 | }, 28 | { 29 | "id": "d@4", 30 | "info": { 31 | "name": "d", 32 | "version": "4" 33 | } 34 | }, 35 | { 36 | "id": "e@5", 37 | "info": { 38 | "name": "e", 39 | "version": "5" 40 | } 41 | }, 42 | { 43 | "id": "f@6", 44 | "info": { 45 | "name": "f", 46 | "version": "6" 47 | } 48 | }, 49 | { 50 | "id": "g@7", 51 | "info": { 52 | "name": "g", 53 | "version": "7" 54 | } 55 | }, 56 | { 57 | "id": "h@2.1", 58 | "info": { 59 | "name": "h", 60 | "version": "2.1" 61 | } 62 | }, 63 | { 64 | "id": "i@9", 65 | "info": { 66 | "name": "i", 67 | "version": "9" 68 | } 69 | }, 70 | { 71 | "id": "j@1", 72 | "info": { 73 | "name": "j", 74 | "version": "1" 75 | } 76 | }, 77 | { 78 | "id": "k@3", 79 | "info": { 80 | "name": "k", 81 | "version": "3" 82 | } 83 | }, 84 | { 85 | "id": "l@0.1", 86 | "info": { 87 | "name": "l", 88 | "version": "0.1" 89 | } 90 | }, 91 | { 92 | "id": "m@1.2", 93 | "info": { 94 | "name": "m", 95 | "version": "1.2" 96 | } 97 | } 98 | ], 99 | "graph": { 100 | "rootNodeId": "root-node", 101 | "nodes": [ 102 | { 103 | "nodeId": "root-node", 104 | "pkgId": "a@1", 105 | "deps": [ 106 | { 107 | "nodeId": "2" 108 | }, 109 | { 110 | "nodeId": "3" 111 | }, 112 | { 113 | "nodeId": "4" 114 | }, 115 | { 116 | "nodeId": "12" 117 | } 118 | ] 119 | }, 120 | { 121 | "nodeId": "2", 122 | "pkgId": "b@2", 123 | "deps": [ 124 | { 125 | "nodeId": "5" 126 | } 127 | ] 128 | }, 129 | { 130 | "nodeId": "3", 131 | "pkgId": "c@3", 132 | "deps": [ 133 | { 134 | "nodeId": "5" 135 | } 136 | ] 137 | }, 138 | { 139 | "nodeId": "4", 140 | "pkgId": "d@4", 141 | "deps": [ 142 | { 143 | "nodeId": "8" 144 | }, 145 | { 146 | "nodeId": "9" 147 | } 148 | ] 149 | }, 150 | { 151 | "nodeId": "5", 152 | "pkgId": "e@5", 153 | "deps": [ 154 | { 155 | "nodeId": "6" 156 | } 157 | ] 158 | }, 159 | { 160 | "nodeId": "6", 161 | "pkgId": "f@6", 162 | "deps": [ 163 | { 164 | "nodeId": "7" 165 | } 166 | ] 167 | }, 168 | { 169 | "nodeId": "7", 170 | "pkgId": "g@7", 171 | "deps": [ 172 | { 173 | "nodeId": "5" 174 | } 175 | ] 176 | }, 177 | { 178 | "nodeId": "8", 179 | "pkgId": "h@2.1", 180 | "deps": [] 181 | }, 182 | { 183 | "nodeId": "9", 184 | "pkgId": "i@9", 185 | "deps": [ 186 | { 187 | "nodeId": "10" 188 | }, 189 | { 190 | "nodeId": "11" 191 | } 192 | ] 193 | }, 194 | { 195 | "nodeId": "10", 196 | "pkgId": "j@1", 197 | "deps": [] 198 | }, 199 | { 200 | "nodeId": "11", 201 | "pkgId": "k@3", 202 | "deps": [] 203 | }, 204 | { 205 | "nodeId": "12", 206 | "pkgId": "l@0.1", 207 | "deps": [ 208 | { 209 | "nodeId": "13" 210 | } 211 | ] 212 | }, 213 | { 214 | "nodeId": "13", 215 | "pkgId": "m@1.2", 216 | "deps": [] 217 | } 218 | ] 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /test/fixtures/plain-dep-graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.3.0", 3 | "pkgManager": { 4 | "name": "npm" 5 | }, 6 | "pkgs": [ 7 | { 8 | "id": "root@0.0.0", 9 | "info": { 10 | "name": "root", 11 | "version": "0.0.0" 12 | } 13 | }, 14 | { 15 | "id": "e@5.0.0", 16 | "info": { 17 | "name": "e", 18 | "version": "5.0.0" 19 | } 20 | }, 21 | { 22 | "id": "d@0.0.1", 23 | "info": { 24 | "name": "d", 25 | "version": "0.0.1" 26 | } 27 | }, 28 | { 29 | "id": "c@1.0.0", 30 | "info": { 31 | "name": "c", 32 | "version": "1.0.0" 33 | } 34 | }, 35 | { 36 | "id": "a@1.0.0", 37 | "info": { 38 | "name": "a", 39 | "version": "1.0.0" 40 | } 41 | }, 42 | { 43 | "id": "d@0.0.2", 44 | "info": { 45 | "name": "d", 46 | "version": "0.0.2" 47 | } 48 | }, 49 | { 50 | "id": "b@1.0.0", 51 | "info": { 52 | "name": "b", 53 | "version": "1.0.0" 54 | } 55 | }, 56 | { 57 | "id": "i@2.1.0", 58 | "info": { 59 | "name": "i", 60 | "version": "2.1.0" 61 | } 62 | }, 63 | { 64 | "id": "h@0.0.1", 65 | "info": { 66 | "name": "h", 67 | "version": "0.0.1" 68 | } 69 | }, 70 | { 71 | "id": "g@1.0.0", 72 | "info": { 73 | "name": "g", 74 | "version": "1.0.0" 75 | } 76 | }, 77 | { 78 | "id": "f@1.0.0", 79 | "info": { 80 | "name": "f", 81 | "version": "1.0.0" 82 | } 83 | } 84 | ], 85 | "graph": { 86 | "rootNodeId": "root-node", 87 | "nodes": [ 88 | { 89 | "nodeId": "root-node", 90 | "pkgId": "root@0.0.0", 91 | "deps": [ 92 | { 93 | "nodeId": "a@1.0.0" 94 | }, 95 | { 96 | "nodeId": "b@1.0.0" 97 | }, 98 | { 99 | "nodeId": "d@0.0.2" 100 | }, 101 | { 102 | "nodeId": "f@1.0.0" 103 | }, 104 | { 105 | "nodeId": "g@1.0.0" 106 | }, 107 | { 108 | "nodeId": "h@0.0.1" 109 | }, 110 | { 111 | "nodeId": "i@2.1.0" 112 | } 113 | ] 114 | }, 115 | { 116 | "nodeId": "e@5.0.0", 117 | "pkgId": "e@5.0.0", 118 | "deps": [] 119 | }, 120 | { 121 | "nodeId": "d@0.0.1", 122 | "pkgId": "d@0.0.1", 123 | "deps": [ 124 | { 125 | "nodeId": "e@5.0.0" 126 | } 127 | ] 128 | }, 129 | { 130 | "nodeId": "c@1.0.0|1", 131 | "pkgId": "c@1.0.0", 132 | "deps": [ 133 | { 134 | "nodeId": "d@0.0.1" 135 | } 136 | ] 137 | }, 138 | { 139 | "nodeId": "c@1.0.0|2", 140 | "pkgId": "c@1.0.0", 141 | "deps": [ 142 | { 143 | "nodeId": "d@0.0.2" 144 | } 145 | ] 146 | }, 147 | { 148 | "nodeId": "a@1.0.0", 149 | "pkgId": "a@1.0.0", 150 | "deps": [ 151 | { 152 | "nodeId": "c@1.0.0|1" 153 | } 154 | ] 155 | }, 156 | { 157 | "nodeId": "d@0.0.2", 158 | "pkgId": "d@0.0.2", 159 | "deps": [ 160 | { 161 | "nodeId": "e@5.0.0" 162 | } 163 | ] 164 | }, 165 | { 166 | "nodeId": "b@1.0.0", 167 | "pkgId": "b@1.0.0", 168 | "deps": [ 169 | { 170 | "nodeId": "c@1.0.0|2" 171 | } 172 | ] 173 | }, 174 | { 175 | "nodeId": "i@2.1.0", 176 | "pkgId": "i@2.1.0", 177 | "deps": [ 178 | { 179 | "nodeId": "e@5.0.0" 180 | } 181 | ] 182 | }, 183 | { 184 | "nodeId": "h@0.0.1", 185 | "pkgId": "h@0.0.1", 186 | "deps": [ 187 | { 188 | "nodeId": "i@2.1.0" 189 | } 190 | ] 191 | }, 192 | { 193 | "nodeId": "g@1.0.0", 194 | "pkgId": "g@1.0.0", 195 | "deps": [ 196 | { 197 | "nodeId": "h@0.0.1" 198 | } 199 | ] 200 | }, 201 | { 202 | "nodeId": "f@1.0.0", 203 | "pkgId": "f@1.0.0", 204 | "deps": [ 205 | { 206 | "nodeId": "g@1.0.0" 207 | } 208 | ] 209 | } 210 | ] 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /test/core/count-paths-to-root.test.ts: -------------------------------------------------------------------------------- 1 | import * as depGraphLib from '../../src'; 2 | import * as helpers from '../helpers'; 3 | 4 | describe('countPathsToRoot', () => { 5 | describe('basic', () => { 6 | const depGraphData = helpers.loadFixture('plain-dep-graph.json'); 7 | const depGraph = depGraphLib.createFromJSON(depGraphData); 8 | 9 | it('returns expected path counts for all packages', () => { 10 | const counts: Record = {}; 11 | for (const pkg of depGraph.getPkgs()) { 12 | counts[`${pkg.name}@${pkg.version}`] = depGraph.countPathsToRoot(pkg); 13 | } 14 | expect(counts).toEqual({ 15 | 'root@0.0.0': 1, 16 | 'e@5.0.0': 7, 17 | 'd@0.0.1': 1, 18 | 'c@1.0.0': 2, 19 | 'a@1.0.0': 1, 20 | 'd@0.0.2': 2, 21 | 'b@1.0.0': 1, 22 | 'i@2.1.0': 4, 23 | 'h@0.0.1': 3, 24 | 'g@1.0.0': 2, 25 | 'f@1.0.0': 1, 26 | }); 27 | }); 28 | 29 | it('returns limited path counts for all packages', () => { 30 | const counts: Record = {}; 31 | for (const pkg of depGraph.getPkgs()) { 32 | counts[`${pkg.name}@${pkg.version}`] = depGraph.countPathsToRoot(pkg, { 33 | limit: 2, 34 | }); 35 | } 36 | expect(counts).toEqual({ 37 | 'root@0.0.0': 1, 38 | 'e@5.0.0': 2, 39 | 'd@0.0.1': 1, 40 | 'c@1.0.0': 2, 41 | 'a@1.0.0': 1, 42 | 'd@0.0.2': 2, 43 | 'b@1.0.0': 1, 44 | 'i@2.1.0': 2, 45 | 'h@0.0.1': 2, 46 | 'g@1.0.0': 2, 47 | 'f@1.0.0': 1, 48 | }); 49 | }); 50 | }); 51 | 52 | describe('large', () => { 53 | const depGraphData = helpers.loadFixture('goof-graph.json'); 54 | 55 | it('returns expected path counts for all packages', () => { 56 | const depGraph = depGraphLib.createFromJSON(depGraphData); 57 | const counts: Record = {}; 58 | for (const pkg of depGraph.getPkgs()) { 59 | counts[`${pkg.name}@${pkg.version}`] = depGraph.countPathsToRoot(pkg); 60 | } 61 | expect(counts).toMatchSnapshot(); 62 | }); 63 | 64 | it('returns limited path counts for all packages', () => { 65 | const depGraph = depGraphLib.createFromJSON(depGraphData); 66 | for (const pkg of depGraph.getPkgs()) { 67 | expect( 68 | depGraph.countPathsToRoot(pkg, { limit: 2 }), 69 | ).toBeLessThanOrEqual(2); 70 | } 71 | }); 72 | 73 | it('returns identical limited paths counts with and without internal cache', () => { 74 | // One: run with a limit on a fresh dep-graph 75 | const depGraph = depGraphLib.createFromJSON(depGraphData); 76 | const countsWithoutCache: Record = {}; 77 | for (const pkg of depGraph.getPkgs()) { 78 | countsWithoutCache[`${pkg.name}@${pkg.version}`] = 79 | depGraph.countPathsToRoot(pkg, { limit: 2 }); 80 | } 81 | 82 | // Two: run without a limit on a new dep-graph 83 | const depGraphB = depGraphLib.createFromJSON(depGraphData); 84 | for (const pkg of depGraphB.getPkgs()) { 85 | depGraphB.countPathsToRoot(pkg); 86 | } 87 | 88 | // Three: Use that same dep-graph instance to run with a limit 89 | const countsWithCache: Record = {}; 90 | for (const pkg of depGraphB.getPkgs()) { 91 | countsWithCache[`${pkg.name}@${pkg.version}`] = 92 | depGraphB.countPathsToRoot(pkg, { limit: 2 }); 93 | } 94 | expect(countsWithCache).toEqual(countsWithoutCache); 95 | }); 96 | }); 97 | 98 | describe('cycles', () => { 99 | const depGraphData = helpers.loadFixture('cyclic-complex-dep-graph.json'); 100 | const depGraph = depGraphLib.createFromJSON(depGraphData); 101 | 102 | it('returns 1 for the root node', () => { 103 | expect(depGraph.countPathsToRoot(depGraph.rootPkg)).toBe(1); 104 | }); 105 | 106 | it.each` 107 | name | version | expected 108 | ${'a'} | ${'1'} | ${1} 109 | ${'b'} | ${'2'} | ${1} 110 | ${'c'} | ${'3'} | ${1} 111 | ${'d'} | ${'4'} | ${1} 112 | ${'e'} | ${'5'} | ${3} 113 | ${'f'} | ${'6'} | ${3} 114 | ${'g'} | ${'7'} | ${3} 115 | `('returns $expected for $name@$version', ({ name, version, expected }) => { 116 | expect(depGraph.countPathsToRoot({ name, version })).toBe(expected); 117 | }); 118 | 119 | it.each(depGraph.getPkgs())(`equals pkgPathsToRoot(%s).length`, (pkg) => { 120 | expect(depGraph.countPathsToRoot(pkg)).toBe( 121 | depGraph.pkgPathsToRoot(pkg).length, 122 | ); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /go/pkg/depgraph/builder_test.go: -------------------------------------------------------------------------------- 1 | package depgraph 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBuilder_Basics(t *testing.T) { 11 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 12 | require.NoError(t, err) 13 | 14 | dg := builder.Build() 15 | 16 | assert.Equal(t, "1.3.0", dg.SchemaVersion) 17 | assert.Equal(t, "golang", dg.PkgManager.Name) 18 | assert.Len(t, dg.Pkgs, 1) 19 | assert.Equal(t, "_root", dg.Pkgs[0].Info.Name) 20 | assert.Equal(t, "unknown", dg.Pkgs[0].Info.Version) 21 | assert.Equal(t, "root-node", dg.Graph.RootNodeID) 22 | assert.Len(t, dg.Graph.Nodes, 1) 23 | assert.Equal(t, "root-node", dg.Graph.Nodes[0].NodeID) 24 | assert.Equal(t, "_root@unknown", dg.Graph.Nodes[0].PkgID) 25 | assert.Len(t, dg.Graph.Nodes[0].Deps, 0) 26 | } 27 | 28 | func TestBuilder_GetPkgManager(t *testing.T) { 29 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 30 | require.NoError(t, err) 31 | 32 | pkgManager := builder.GetPkgManager() 33 | 34 | assert.Equal(t, "golang", pkgManager.Name) 35 | } 36 | 37 | func TestBuilder_RootPkg(t *testing.T) { 38 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, &PkgInfo{Name: "project", Version: "VERSION"}) 39 | require.NoError(t, err) 40 | 41 | dg := builder.Build() 42 | 43 | assert.Equal(t, "project", dg.Pkgs[0].Info.Name) 44 | assert.Equal(t, "VERSION", dg.Pkgs[0].Info.Version) 45 | } 46 | 47 | func TestBuilder_AddNode(t *testing.T) { 48 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 49 | require.NoError(t, err) 50 | 51 | builder.AddNode("dep@VERSION", &PkgInfo{Name: "dep", Version: "VERSION"}) 52 | dg := builder.Build() 53 | 54 | assert.Len(t, dg.Pkgs, 2) 55 | assert.Len(t, dg.Graph.Nodes, 2) 56 | } 57 | 58 | func TestBuilder_AddNode_Existing(t *testing.T) { 59 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 60 | require.NoError(t, err) 61 | 62 | builder.AddNode("dep@VERSION", &PkgInfo{Name: "dep", Version: "VERSION"}) 63 | firstEntry := builder.nodes.GetOrDefault("dep@VERSION", nil) 64 | require.NotNil(t, firstEntry) 65 | assert.Equal(t, firstEntry, builder.AddNode("dep@VERSION", &PkgInfo{Name: "dep", Version: "VERSION"})) 66 | } 67 | 68 | func TestBuilder_ConnectNodes(t *testing.T) { 69 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 70 | require.NoError(t, err) 71 | 72 | node := builder.AddNode("dep@VERSION", &PkgInfo{Name: "dep", Version: "VERSION"}) 73 | err = builder.ConnectNodes(builder.rootNodeID, node.NodeID) 74 | require.NoError(t, err) 75 | 76 | dg := builder.Build() 77 | 78 | require.Len(t, dg.Graph.Nodes, 2) 79 | require.Len(t, dg.Graph.Nodes[0].Deps, 1) 80 | require.Equal(t, "root-node", dg.Graph.Nodes[0].NodeID) 81 | assert.Equal(t, "dep@VERSION", dg.Graph.Nodes[0].Deps[0].NodeID) 82 | } 83 | 84 | func TestBuilder_GetRootNode(t *testing.T) { 85 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, &PkgInfo{Name: "root-pkg", Version: "1.2.3"}) 86 | require.NoError(t, err) 87 | 88 | root := builder.GetRootNode() 89 | 90 | assert.NotNil(t, root) 91 | assert.Equal(t, "root-node", root.NodeID) 92 | assert.Equal(t, "root-pkg@1.2.3", root.PkgID) 93 | } 94 | 95 | func TestBuilder_Build(t *testing.T) { 96 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, nil) 97 | require.NoError(t, err) 98 | 99 | builder.AddNode("dep@1.0.0", &PkgInfo{Name: "dep", Version: "1.0.0"}, WithNodeInfo(&NodeInfo{ 100 | VersionProvenance: &VersionProvenance{ 101 | Type: "file", 102 | Location: "deps.txt", 103 | Property: &Property{Name: "dep"}, 104 | }, 105 | Labels: map[string]string{ 106 | "scope": "prod", 107 | }, 108 | })) 109 | 110 | dg := builder.Build() 111 | 112 | assert.NotNil(t, dg.rootPkg) 113 | assert.Equal(t, &Pkg{ID: "_root@unknown", Info: PkgInfo{Name: "_root", Version: "unknown"}}, dg.rootPkg) 114 | 115 | require.Len(t, dg.Graph.Nodes, 2) 116 | depNode := dg.Graph.Nodes[1] 117 | require.NotNil(t, depNode.Info) 118 | assert.Equal(t, "file", depNode.Info.VersionProvenance.Type) 119 | assert.Equal(t, "deps.txt", depNode.Info.VersionProvenance.Location) 120 | assert.Equal(t, "dep", depNode.Info.VersionProvenance.Property.Name) 121 | assert.Equal(t, "prod", depNode.Info.Labels["scope"]) 122 | } 123 | 124 | func TestBuilder_Build_GetPkg(t *testing.T) { 125 | builder, err := NewBuilder(&PkgManager{Name: "golang"}, &PkgInfo{Name: "root-pkg", Version: "1.2.3"}) 126 | require.NoError(t, err) 127 | 128 | dg := builder.Build() 129 | 130 | pkg, ok := dg.GetPkg("root-pkg@1.2.3") 131 | require.True(t, ok) 132 | require.NotNil(t, pkg) 133 | 134 | assert.Equal(t, &Pkg{ID: "root-pkg@1.2.3", Info: PkgInfo{Name: "root-pkg", Version: "1.2.3"}}, pkg) 135 | } 136 | --------------------------------------------------------------------------------