Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Parse lockfile recursively #5

Merged
merged 9 commits into from
Aug 9, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
const inspect = require('../dist/index')

inspect.buildDepTreeFromFiles('./', 'package.json', 'package-lock.json')
.then((tree) => {
console.log(JSON.stringify(tree));
})
.catch((e) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.catch(console.error); might be better here

console.log(e);
});
130 changes: 71 additions & 59 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
import 'source-map-support/register';
import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';

export default function parseLockFile(root, targetFilePath, lockFilePath, options) {
if (!root || !lockFilePath || !lockFilePath) {
throw new Error('Missing required parameters for parseLockFile()');
}
// TODO: validate only valid options were passed in

const targetFileFullPath = path.resolve(root, targetFilePath);
const lockFileFullPath = path.resolve(root, lockFilePath);

if (!fs.existsSync(targetFilePath)) {
throw new Error(`Target file package.json not found at location: ${targetFileFullPath}`);
}
if (!fs.existsSync(lockFilePath)) {
throw new Error(`LockFile package-lock.json not found at location: ${lockFileFullPath}`);
}

const targetFile = fs.readFileSync(targetFilePath);
const lockFile = fs.readFileSync(lockFilePath);

return buildDepTree(targetFile, lockFile, options);
}

function buildDepTree(targetFileRaw, lockFileRaw, options) {
export async function buildDepTree(targetFileRaw, lockFileRaw, options) {

const lockFile = JSON.parse(lockFileRaw);
const targetFile = JSON.parse(targetFileRaw);
Expand All @@ -40,46 +20,78 @@ function buildDepTree(targetFileRaw, lockFileRaw, options) {
name: targetFile.name || undefined,
version: targetFile.version || undefined,
};
const parentDepList = targetFile.dependencies;
const fullDepList = lockFile.dependencies;

const parentDepsMap = Object.keys(parentDepList).reduce((acc, depName) => {
const version = parentDepList[depName];
const name = `${depName}@${version}`;
acc[name] = {
name: depName,
version,
};
return acc;
}, {});

const depsMap = Object.keys(fullDepList).reduce((acc, dep) => {
const version = fullDepList[dep];
const name = `${dep}@${version}`;
acc[name] = dep;
return acc;
}, {});

for (const dep in depsMap) {
if (depsMap.hasOwnProperty(dep)) {
const subTree = buildSubTreeRecursive(dep, new Set(), depsMap);

if (subTree) {
depTree.dependencies[subTree.name] = subTree;
}
}
}

const topLevelDeps = Object.keys(targetFile.dependencies);

await Promise.all(topLevelDeps.map(async (dep) => {
depTree.dependencies[dep] = await buildSubTreeRecursive(dep, [], lockFile);
}));

return depTree;
}

function buildSubTreeRecursive(dep, ancestors, depsMap) {
const newAncestors = (new Set(ancestors)).add(dep);
// TODO
const tree = {
name: depsMap[dep].name,
version: depsMap[dep].version,
async function buildSubTreeRecursive(dep: string, depKeys: string[], lockFile: object) {

const depSubTree = {
dependencies: {},
name: dep,
version: undefined,
};

return tree;
// Get path to the nested dependencies from list ['package1', 'package2']
// to ['dependencies', 'package1', 'dependencies', 'package2', 'dependencies']
const depPath = getDepPath(depKeys);
// try to get list of deps on the path
const deps = _.get(lockFile, depPath);

// If exists and looked-up dep is there
if (deps && deps[dep]) {
// update the tree
depSubTree.version = deps[dep].version;
// repeat the process for dependencies of looked-up dep
const newDeps = deps[dep].requires ? Object.keys(deps[dep].requires) : [];
await Promise.all(newDeps.map(async (subDep) => {
depSubTree.dependencies[subDep] = await buildSubTreeRecursive(subDep, [...depKeys, dep], lockFile);
}));
return depSubTree;
} else {
// tree was walked to the root and dependency was not found
if (!depKeys.length) {
throw new Error(`Dependency ${dep} was not found in package-lock.json.
Your package.json and package-lock.json are probably out of sync.
Please run npm install and try to parse the log again.`);
}
// dependency was not found on a current path, remove last key (move closer to the root) and try again
return buildSubTreeRecursive(dep, depKeys.slice(0, -1), lockFile);
}
}

function getDepPath(depKeys: string[]) {
const depPath = depKeys.reduce((acc, key) => {
return acc.concat([key, 'dependencies']);
}, ['dependencies']);

return depPath;
}

export function buildDepTreeFromFiles(root, targetFilePath, lockFilePath, options) {
if (!root || !lockFilePath || !lockFilePath) {
throw new Error('Missing required parameters for parseLockFile()');
}
// TODO: validate only valid options were passed in
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might drop this TODO.. just noise


const targetFileFullPath = path.resolve(root, targetFilePath);
const lockFileFullPath = path.resolve(root, lockFilePath);

if (!fs.existsSync(targetFilePath)) {
throw new Error(`Target file package.json not found at location: ${targetFileFullPath}`);
}
if (!fs.existsSync(lockFilePath)) {
throw new Error(`LockFile package-lock.json not found at location: ${lockFileFullPath}`);
}

const targetFile = fs.readFileSync(targetFileFullPath);
const lockFile = fs.readFileSync(lockFileFullPath);

return buildDepTree(targetFile, lockFile, options);
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "snyk-nodejs-lockfile-parser",
"description": "Generate a dep tree given a lockfile",
"main": "dist/lib/index.js",
"bin": {
"parse": "./bin/index.js"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "parse" is probably too generic for a bin that will be installed globally

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what a better name might be.. maybe "parse-npm-lockfile"??

},
"scripts": {
"test": "npm run lint && npm run unit-test",
"unit-test": "tap test/lib -R=spec --timeout=300 --node-path ts-node --test-file-pattern '/\\.[tj]s$/'",
Expand All @@ -26,6 +29,7 @@
"@types/node": "10.5.5",
"@types/sinon": "5.0.1",
"sinon": "6.1.4",
"source-map-support": "^0.5.6",
"tap": "github:snyk/node-tap#alternative-runtimes",
"ts-node": "7.0.0",
"tslint": "5.11.0",
Expand Down
Loading