Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/yarn v2 #75

Merged
merged 2 commits into from
Jul 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/errors/out-of-sync-error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LockfileType } from '../parsers';

const LOCK_FILE_NAME = {
npm: 'package-lock.json',
yarn: 'yarn.lock',
Expand All @@ -14,7 +16,7 @@ export class OutOfSyncError extends Error {
public dependencyName: string;
public lockFileType: string;

constructor(dependencyName: string, lockFileType: 'yarn' | 'npm') {
constructor(dependencyName: string, lockFileType: LockfileType) {
super(
`Dependency ${dependencyName} was not found in ` +
`${LOCK_FILE_NAME[lockFileType]}. Your package.json and ` +
Expand Down
45 changes: 29 additions & 16 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './parsers';
import { PackageLockParser } from './parsers/package-lock-parser';
import { YarnLockParser } from './parsers/yarn-lock-parse';
import { Yarn2LockParser } from './parsers/yarn2-lock-parse';
import getRuntimeVersion from './get-node-runtime-version';
import {
UnsupportedRuntimeError,
Expand Down Expand Up @@ -51,13 +52,16 @@ async function buildDepTree(
lockfileParser = new PackageLockParser();
break;
case LockfileType.yarn:
// parsing yarn.lock is supported for Node.js v6 and higher
if (getRuntimeVersion() >= 6) {
lockfileParser = new YarnLockParser();
lockfileParser = new YarnLockParser();
break;
case LockfileType.yarn2:
// parsing yarn.lock is supported for Node.js v10 and higher
if (getRuntimeVersion() >= 10) {
lockfileParser = new Yarn2LockParser();
} else {
throw new UnsupportedRuntimeError(
'Parsing `yarn.lock` is not ' +
'supported on Node.js version less than 6. Please upgrade your ' +
'supported on Node.js version less than 10. Please upgrade your ' +
'Node.js environment or use `package-lock.json`',
);
}
Expand Down Expand Up @@ -95,18 +99,6 @@ async function buildDepTreeFromFiles(
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 InvalidUserInputError(
`Unknown lockfile ${lockFilePath}. ` +
'Please provide either package-lock.json or yarn.lock.',
);
}

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

Expand All @@ -125,6 +117,27 @@ async function buildDepTreeFromFiles(
const manifestFileContents = fs.readFileSync(manifestFileFullPath, 'utf-8');
const lockFileContents = fs.readFileSync(lockFileFullPath, 'utf-8');

let lockFileType: LockfileType;
if (lockFilePath.endsWith('package-lock.json')) {
lockFileType = LockfileType.npm;
} else if (lockFilePath.endsWith('yarn.lock')) {
if (
lockFileContents.includes('__metadata') ||
fs.existsSync(
path.resolve(root, lockFilePath.replace('yarn.lock', '.yarnrc.yml')),
)
) {
lockFileType = LockfileType.yarn2;
} else {
lockFileType = LockfileType.yarn;
}
} else {
throw new InvalidUserInputError(
`Unknown lockfile ${lockFilePath}. ` +
'Please provide either package-lock.json or yarn.lock.',
);
}

return await buildDepTree(
manifestFileContents,
lockFileContents,
Expand Down
5 changes: 4 additions & 1 deletion lib/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PackageLock } from './package-lock-parser';
import { YarnLock } from './yarn-lock-parse';
import { InvalidUserInputError } from '../errors';
import { Yarn2Lock } from './yarn2-lock-parse';

export interface Dep {
name: string;
Expand Down Expand Up @@ -53,6 +54,7 @@ export interface PkgTree extends DepTreeDep {
};
meta?: {
nodeVersion: string;
packageManagerVersion?: string;
};
hasDevDependencies?: boolean;
cyclic?: boolean;
Expand All @@ -67,6 +69,7 @@ export enum Scope {
export enum LockfileType {
npm = 'npm',
yarn = 'yarn',
yarn2 = 'yarn2',
}

export interface LockfileParser {
Expand All @@ -79,7 +82,7 @@ export interface LockfileParser {
) => Promise<PkgTree>;
}

export type Lockfile = PackageLock | YarnLock;
export type Lockfile = PackageLock | YarnLock | Yarn2Lock;

export function parseManifestFile(manifestFileContents: string): ManifestFile {
try {
Expand Down
4 changes: 2 additions & 2 deletions lib/parsers/package-lock-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class PackageLockParser implements LockfileParser {
// TODO: also check the package version
// for a stricter check
if (strict) {
throw new OutOfSyncError(depName, 'npm');
throw new OutOfSyncError(depName, LockfileType.npm);
}
depTree.dependencies[dep.name] = createDepTreeDepFromDep(dep);
if (!depTree.dependencies[dep.name].labels) {
Expand Down Expand Up @@ -363,7 +363,7 @@ export class PackageLockParser implements LockfileParser {
}

if (!depMap[depName]) {
throw new OutOfSyncError(depName, 'npm');
throw new OutOfSyncError(depName, LockfileType.npm);
}

return depName;
Expand Down
6 changes: 5 additions & 1 deletion lib/parsers/yarn-lock-parse-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { config } from '../config';

const EVENT_PROCESSING_CONCURRENCY = 5;

export type YarnLockFileTypes = LockfileType.yarn;
export type YarnLockFileTypes = LockfileType.yarn | LockfileType.yarn2;

export interface YarnLockDeps {
[depName: string]: YarnLockDep;
Expand Down Expand Up @@ -84,6 +84,10 @@ export abstract class YarnLockParseBase<T extends YarnLockFileTypes>
_set(depTree, 'meta.nodeVersion', nodeVersion);
}

const packageManagerVersion =
lockfile.type === LockfileType.yarn ? '1' : '2';
_set(depTree, 'meta.packageManagerVersion', packageManagerVersion);

const topLevelDeps: Dep[] = getTopLevelDeps(manifestFile, includeDev);
// asked to process empty deps
if (_isEmpty(manifestFile.dependencies) && !includeDev) {
Expand Down
87 changes: 87 additions & 0 deletions lib/parsers/yarn-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { structUtils } from '@yarnpkg/core';
import * as _ from 'lodash';

const BUILTIN_PLACEHOLDER = 'builtin';
const MULTIPLE_KEYS_REGEXP = / *, */g;

export type ParseDescriptor = typeof structUtils.parseDescriptor;
export type ParseRange = typeof structUtils.parseRange;

const keyNormalizer = (
parseDescriptor: ParseDescriptor,
parseRange: ParseRange,
) => (rawDescriptor: string): string[] => {
// See https://yarnpkg.com/features/protocols
const descriptors: string[] = [rawDescriptor];
const descriptor = parseDescriptor(rawDescriptor);
const name = `${descriptor.scope ? '@' + descriptor.scope + '/' : ''}${
descriptor.name
}`;
const range = parseRange(descriptor.range);
const protocol = range.protocol;
switch (protocol) {
case 'npm:':
case 'file:':
descriptors.push(`${name}@${range.selector}`);
descriptors.push(`${name}@${protocol}${range.selector}`);
break;
case 'git:':
case 'git+ssh:':
case 'git+http:':
case 'git+https:':
case 'github:':
if (range.source) {
descriptors.push(
`${name}@${protocol}${range.source}${
range.selector ? '#' + range.selector : ''
}`,
);
} else {
descriptors.push(`${name}@${protocol}${range.selector}`);
}
break;
case 'patch:':
if (range.source && range.selector.indexOf(BUILTIN_PLACEHOLDER) === 0) {
descriptors.push(range.source);
} else {
descriptors.push(
`${name}@${protocol}${range.source}${
range.selector ? '#' + range.selector : ''
}`,
);
}
break;
case null:
case undefined:
if (range.source) {
descriptors.push(`${name}@${range.source}#${range.selector}`);
} else {
descriptors.push(`${name}@${range.selector}`);
}
break;
case 'http:':
case 'https:':
case 'link:':
case 'portal:':
case 'exec:':
case 'workspace:':
case 'virtual:':
default:
// For user defined plugins
descriptors.push(`${name}@${protocol}${range.selector}`);
break;
}
return descriptors;
};

export type YarnLockFileKeyNormalizer = (fullDescriptor: string) => Set<string>;

export const yarnLockFileKeyNormalizer = (
parseDescriptor: ParseDescriptor,
parseRange: ParseRange,
): YarnLockFileKeyNormalizer => (fullDescriptor: string) => {
const allKeys = fullDescriptor
.split(MULTIPLE_KEYS_REGEXP)
.map(keyNormalizer(parseDescriptor, parseRange));
return new Set<string>(_.flatMap(allKeys));
};
55 changes: 55 additions & 0 deletions lib/parsers/yarn2-lock-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as _ from 'lodash';
import * as yaml from 'yaml';

import { LockfileType } from './';
import getRuntimeVersion from '../get-node-runtime-version';
import { InvalidUserInputError, UnsupportedRuntimeError } from '../errors';
import { YarnLockBase, YarnLockDeps } from './yarn-lock-parse-base';
import { YarnLockParseBase } from './yarn-lock-parse-base';
import {
YarnLockFileKeyNormalizer,
yarnLockFileKeyNormalizer,
} from './yarn-utils';

export type Yarn2Lock = YarnLockBase<LockfileType.yarn2>;

export class Yarn2LockParser extends YarnLockParseBase<LockfileType.yarn2> {
private keyNormalizer: YarnLockFileKeyNormalizer;

constructor() {
super(LockfileType.yarn2);
// @yarnpkg/core doesn't work with Node.js < 10
if (getRuntimeVersion() < 10) {
throw new UnsupportedRuntimeError(
`yarn.lock parsing is supported for Node.js v10 and higher.`,
);
}
const structUtils = require('@yarnpkg/core').structUtils;
const parseDescriptor = structUtils.parseDescriptor;
const parseRange = structUtils.parseRange;
this.keyNormalizer = yarnLockFileKeyNormalizer(parseDescriptor, parseRange);
}

public parseLockFile(lockFileContents: string): Yarn2Lock {
try {
const rawYarnLock: any = yaml.parse(lockFileContents);
delete rawYarnLock.__metadata;
const dependencies: YarnLockDeps = {};
_.forEach(rawYarnLock, (versionData, fullDescriptor) => {
this.keyNormalizer(fullDescriptor).forEach((descriptor) => {
dependencies[descriptor] = versionData;
});
});
return {
dependencies,
lockfileType: LockfileType.yarn2,
object: dependencies,
type: LockfileType.yarn2,
};
} catch (e) {
throw new InvalidUserInputError(
`yarn.lock parsing failed with an error: ${e.message}`,
);
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"homepage": "https://github.com/snyk/nodejs-lockfile-parser#readme",
"dependencies": {
"@snyk/graphlib": "2.1.9-patch",
"@yarnpkg/core": "^2.0.0-rc.26",
"@yarnpkg/core": "^2.0.0-rc.29",
"@yarnpkg/lockfile": "^1.1.0",
"event-loop-spinner": "^2.0.0",
"lodash.clonedeep": "^4.5.0",
Expand All @@ -46,7 +46,7 @@
"yaml": "^1.9.2"
},
"devDependencies": {
"@types/node": "^6.14.7",
"@types/node": "^10.17.26",
"@types/uuid": "^3.4.4",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"@typescript-eslint/parser": "^2.29.0",
Expand Down
1 change: 1 addition & 0 deletions test/lib/fixtures/cyclic-dep-simple/yarn2/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-rc.js
23 changes: 23 additions & 0 deletions test/lib/fixtures/cyclic-dep-simple/yarn2/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!

__metadata:
version: 4

"debug@npm:2.0.x":
version: 2.0.0
resolution: "debug@npm:2.0.0"
dependencies:
ms: 0.6.2
checksum: 2/0d3aafea6f4d2fac3e1ad295c17927aeb1188b51184cc1a39a061911fde969c1ff9139a14f895b592f2a82035fc507ef9ec78ebddcba6aba4be0fe6e195abde7
languageName: node
linkType: hard

"ms@npm:0.6.2":
version: 0.6.2
resolution: "ms@npm:0.6.2"
checksum: 2/58b15f75a33f042ce241d435c6eb218f46cd835f74db4f85b4dacf4143a92638cc0887be38423e79411da27dae103db3048a0d9aa4629607ab10c7d037b6f9e7
languageName: node
linkType: hard
dependencies:
debug: 2.0.x
2 changes: 1 addition & 1 deletion test/lib/fixtures/dev-deps-only/expected-tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
}
}
}
}
}
27 changes: 27 additions & 0 deletions test/lib/fixtures/dev-deps-only/yarn1/expected-tree.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "pkg-dev-deps-only",
"size": 3,
"version": "0.0.1",
"hasDevDependencies": true,
"dependencies": {
"debug": {
"name": "debug",
"version": "2.6.9",
"labels": {
"scope": "dev"
},
"dependencies": {
"ms": {
"name": "ms",
"version": "2.0.0",
"labels": {
"scope": "dev"
}
}
}
}
},
"meta": {
"packageManagerVersion": "1"
}
}
1 change: 1 addition & 0 deletions test/lib/fixtures/dev-deps-only/yarn2/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-rc.js
Loading