-
Notifications
You must be signed in to change notification settings - Fork 28
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
Changes from 8 commits
df2b206
abec409
9911b5b
c1ab3d4
3cf3bbe
69290b1
d95b01a
e48b713
c04aeee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) => { | ||
console.log(e); | ||
}); |
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); | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe 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$/'", | ||
|
@@ -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", | ||
|
There was a problem hiding this comment.
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