Skip to content

Commit

Permalink
Merge pull request #18 from snyk/feat/support-yarn
Browse files Browse the repository at this point in the history
Add support for yarn
  • Loading branch information
miiila authored Aug 24, 2018
2 parents a9a6255 + 8348438 commit ad723de
Show file tree
Hide file tree
Showing 25 changed files with 34,024 additions and 12,774 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
node_js: "8"
script: skip
after_success:
- npm run semantic-release
- npx semantic-release
branches:
only:
- master
4 changes: 4 additions & 0 deletions lib/get-node-runtime-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export default function getRuntimeVersion(): number {
return parseInt(process.version.slice(1).split('.')[0], 10);
}
180 changes: 52 additions & 128 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,167 +1,91 @@
import 'source-map-support/register';
import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';

enum DepType {
prod = 'prod',
dev = 'dev',
}

interface PkgTree {
name: string;
version: string;
dependencies?: {
[dep: string]: PkgTree;
};
depType?: DepType;
hasDevDependencies?: boolean;
cyclic?: boolean;
}

interface ManifestFile {
name?: string;
dependencies?: {
[dep: string]: string;
};
devDependencies?: {
[dep: string]: string;
};
version?: string;
}

interface Lockfile {
name: string;
version: string;
dependencies?: LockfileDeps;
}

interface LockfileDeps {
[depName: string]: LockfileDep;
}

interface LockfileDep {
version: string;
requires?: {
[depName: string]: string;
};
dependencies?: LockfileDeps;
dev?: boolean;
import {LockfileParser, Lockfile, ManifestFile, PkgTree,
DepType, parseManifestFile} from './parsers';
import {PackageLockParser} from './parsers/package-lock-parser';
import {YarnLockParser} from './parsers/yarn-lock-parse';
import getRuntimeVersion from './get-node-runtime-version';

enum LockfileType {
npm = 'npm',
yarn = 'yarn',
}

export {
buildDepTree,
buildDepTreeFromFiles,
PkgTree,
DepType,
LockfileType,
};

async function buildDepTree(
manifestFileContents: string, lockFileContents: string, includeDev = false): Promise<PkgTree> {

const lockFile: Lockfile = JSON.parse(lockFileContents);
const manifestFile: ManifestFile = JSON.parse(manifestFileContents);
manifestFileContents: string, lockFileContents: string,
includeDev = false, lockfileType?: LockfileType): Promise<PkgTree> {

if (!manifestFile.dependencies && !includeDev) {
throw new Error("No 'dependencies' property in package.json");
if (!lockfileType) {
lockfileType = LockfileType.npm;
}

const depTree: PkgTree = {
dependencies: {},
hasDevDependencies: !_.isEmpty(manifestFile.devDependencies),
name: manifestFile.name,
version: manifestFile.version,
};

// asked to process empty deps
if (_.isEmpty(manifestFile.dependencies) && !includeDev) {
return depTree;
let lockfileParser: LockfileParser;
switch (lockfileType) {
case LockfileType.npm:
lockfileParser = new PackageLockParser();
break;
case LockfileType.yarn:
// parsing yarn.lock is supported for Node.js v6 and higher
if (getRuntimeVersion() >= 6) {
lockfileParser = new YarnLockParser();
} else {
const unsupportedRuntimeError = new Error();
unsupportedRuntimeError.name = 'UnsupportedRuntimeError';
// tslint:disable:max-line-length
unsupportedRuntimeError.message = 'Parsing `yarn.lock` is not supported on Node.js version less than 6. Please upgrade your Node.js environment or use `package-lock.json`';
throw unsupportedRuntimeError;
}
break;
default:
throw new Error(`Unsupported lockfile type ${lockfileType} provided.
Only 'npm' or 'yarn' is currently supported.`);
}

if (!lockFile.dependencies && !includeDev) {
throw new Error("No 'dependencies' property in package-lock.json");
}
const topLevelDeps = getTopLevelDeps(manifestFile, includeDev);

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

return depTree;
}

function getTopLevelDeps(targetFile: ManifestFile, includeDev: boolean): string[] {
return Object.keys({
...targetFile.dependencies,
...(includeDev ? targetFile.devDependencies : null),
});
}

async function buildSubTreeRecursive(
depName: string, lockfilePath: string[], lockFile: Lockfile, depPath: string[]): Promise<PkgTree> {

const depSubTree: PkgTree = {
depType: undefined,
dependencies: {},
name: depName,
version: undefined,
};

// try to get list of deps on the path
const deps: LockfileDeps = _.get(lockFile, lockfilePath);
const dep: LockfileDep = _.get(deps, depName);
// If exists and looked-up dep is there
if (dep) {
// update the tree
depSubTree.version = dep.version;
depSubTree.depType = dep.dev ? DepType.dev : DepType.prod;
// check if we already have a package at particular version in the traversed path
const depKey = `${depName}@${dep.version}`;
if (depPath.indexOf(depKey) >= 0) {
depSubTree.cyclic = true;
} else {
// if not, add it
depPath.push(depKey);
// repeat the process for dependencies of looked-up dep
const newDeps = dep.requires ? Object.keys(dep.requires) : [];

await Promise.all(newDeps.map(async (subDep) => {
depSubTree.dependencies[subDep] = await buildSubTreeRecursive(
subDep, [...lockfilePath, depName, 'dependencies'], lockFile, depPath.slice());
}));
}
return depSubTree;
} else {
// tree was walked to the root and dependency was not found
if (!lockfilePath.length) {
throw new Error(`Dependency ${depName} 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
// visitedDepPaths can be passed by a reference, because traversing up doesn't update it
return buildSubTreeRecursive(depName, lockfilePath.slice(0, -1), lockFile, depPath);
const manifestFile: ManifestFile = parseManifestFile(manifestFileContents);
if (!manifestFile.dependencies && !includeDev) {
throw new Error("No 'dependencies' property in package.json");
}
const lockFile: Lockfile = lockfileParser.parseLockFile(lockFileContents);
return lockfileParser.getDependencyTree(manifestFile, lockFile, includeDev);
}

async function buildDepTreeFromFiles(
root: string, manifestFilePath: string, lockFilePath: string, includeDev = false): Promise<PkgTree> {
if (!root || !lockFilePath || !lockFilePath) {
if (!root || !manifestFilePath || !lockFilePath) {
throw new Error('Missing required parameters for buildDepTreeFromFiles()');
}

let lockFileType: LockfileType;
if (lockFilePath.endsWith('package-lock.json')) {
lockFileType = LockfileType.npm;
} else if (lockFilePath.endsWith('yarn.lock')) {
lockFileType = LockfileType.yarn;
} else {
throw new Error(`Unknown lockfile ${lockFilePath}.
Please provide either package-lock.json or yarn.lock.`);
}

const manifestFileFullPath = path.resolve(root, manifestFilePath);
const lockFileFullPath = path.resolve(root, lockFilePath);

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

const manifestFileContents = fs.readFileSync(manifestFileFullPath, 'utf-8');
const lockFileContents = fs.readFileSync(lockFileFullPath, 'utf-8');

return await buildDepTree(manifestFileContents, lockFileContents, includeDev);
return await buildDepTree(manifestFileContents, lockFileContents, includeDev, lockFileType);
}
73 changes: 73 additions & 0 deletions lib/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as _ from 'lodash';
import {PackageLock} from './package-lock-parser';
import {YarnLock} from './yarn-lock-parse';

export interface Dep {
name: string;
version: string;
dev?: boolean;
}

export interface ManifestFile {
name?: string;
dependencies?: {
[dep: string]: string;
};
devDependencies?: {
[dep: string]: string;
};
version?: string;
}

export interface PkgTree {
name: string;
version: string;
dependencies?: {
[dep: string]: PkgTree;
};
depType?: DepType;
hasDevDependencies?: boolean;
cyclic?: boolean;
}

export enum DepType {
prod = 'prod',
dev = 'dev',
}

export interface LockfileParser {
parseLockFile: (lockFileContents: string)
=> Lockfile;
getDependencyTree: (manifestFile: ManifestFile, lockfile: Lockfile, includeDev?: boolean)
=> Promise<PkgTree>;
}

export type Lockfile = PackageLock | YarnLock;

export function parseManifestFile(manifestFileContents: string): ManifestFile {
try {
return JSON.parse(manifestFileContents);
} catch (e) {
throw new Error(`package.json parsing failed with error ${e.message}`);
}
}

export function getTopLevelDeps(targetFile: ManifestFile, includeDev: boolean): Dep[] {
const dependencies: Dep[] = [];

const dependenciesIterator = _.entries({
...targetFile.dependencies,
...(includeDev ? targetFile.devDependencies : null),
});

for (const [name, version] of dependenciesIterator) {
dependencies.push({
dev: (includeDev && targetFile.devDependencies) ?
!!targetFile.devDependencies[name] : false,
name,
version,
});
}

return dependencies;
}
Loading

0 comments on commit ad723de

Please sign in to comment.