From 6b6f21c5f0a4a4af7725ec07c9bb24490f1b2581 Mon Sep 17 00:00:00 2001 From: xhayper Date: Thu, 24 Nov 2022 11:38:22 +0700 Subject: [PATCH 1/5] feat: initial support for yarn2 --- src/npm.ts | 80 +++++++++++++++++++++++++++++++++++++++++++---------- src/util.ts | 4 +++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/npm.ts b/src/npm.ts index c355cbef..59340eb3 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as cp from 'child_process'; import parseSemver from 'parse-semver'; -import { CancellationToken, log, nonnull } from './util'; +import { CancellationToken, log, ndjsonToJson, nonnull } from './util'; const exists = (file: string) => fs.promises.stat(file).then( @@ -75,31 +75,63 @@ interface YarnTreeNode { children: YarnTreeNode[]; } +interface Yarn2TreeNode { + value: string; + children: Yarn2TreeNodeChildren; +} + +interface Yarn2TreeNodeChildren { + Version: string; + Dependencies: Yarn2Dependency[]; +} + +interface Yarn2Dependency { + descriptor: string; + locator: string; +} + export interface YarnDependency { name: string; path: string; children: YarnDependency[]; } -function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null { - if (prune && /@[\^~]/.test(tree.name)) { +function isYarn2(tree: YarnTreeNode | Yarn2TreeNode | Yarn2Dependency): tree is Yarn2TreeNode | Yarn2Dependency { + return (tree as Yarn2TreeNode).value !== undefined || (tree as Yarn2Dependency).descriptor !== undefined; +} + +function asYarnDependency(prefix: string, tree: YarnTreeNode | Yarn2TreeNode, prune: boolean): YarnDependency | null { + const isY2 = isYarn2(tree); + const packageName = isY2 ? tree.value : tree.name; + + if (prune && /@[\^~]/.test(packageName)) { return null; } let name: string; try { - const parseResult = parseSemver(tree.name); + const parseResult = parseSemver(packageName); name = parseResult.name; } catch (err) { - name = tree.name.replace(/^([^@+])@.*$/, '$1'); + name = packageName.replace(/^(.*)@.*$/, '$1'); } const dependencyPath = path.join(prefix, name); const children: YarnDependency[] = []; - for (const child of tree.children || []) { - const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child, prune); + for (const child of (isY2 ? tree.children.Dependencies : (tree.children as YarnTreeNode[])) || []) { + const isChildY2 = isYarn2(child); + const childDep = isChildY2 + ? ({ + value: child.locator, + children: {}, + } as Yarn2TreeNode) + : ({ + name: child.name, + } as YarnTreeNode); + + const dep = asYarnDependency(prefix, childDep, prune); if (dep) { children.push(dep); @@ -155,21 +187,39 @@ function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: st } async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise { - const raw = await new Promise((c, e) => - cp.exec( - 'yarn list --prod --json', - { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, - (err, stdout) => (err ? e(err) : c(stdout)) + const [major] = ( + await new Promise((c, e) => + cp.exec( + 'yarn --version', + { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, + (err, stdout) => (err ? e(err) : c(stdout)) + ) + ) + ) + .split('.') + .map(parseInt); + + const isY2 = major > 1; + const listCommand = isY2 ? 'yarn info --recursive --dependents --json' : 'yarn list --prod --json --depth 99999'; + + let raw = await new Promise((c, e) => + cp.exec(listCommand, { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, (err, stdout) => + err ? e(err) : c(stdout) ) ); - const match = /^{"type":"tree".*$/m.exec(raw); + + if (isY2) { + raw = ndjsonToJson(raw); + } + + const match = (isY2 ? /^\[{"value":".*$/m : /^{"type":"tree".*$/m).exec(raw); if (!match || match.length !== 1) { - throw new Error('Could not parse result of `yarn list --json`'); + throw new Error(`Could not parse result of \`${listCommand}\``); } const usingPackagedDependencies = Array.isArray(packagedDependencies); - const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[]; + const trees = isY2 ? (JSON.parse(match[0]) as Yarn2TreeNode[]) : (JSON.parse(match[0]).data.trees as YarnTreeNode[]); let result = trees .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree, !usingPackagedDependencies)) diff --git a/src/util.ts b/src/util.ts index e5023dd0..f2f99bd3 100644 --- a/src/util.ts +++ b/src/util.ts @@ -179,3 +179,7 @@ export function patchOptionsWithManifest(options: any, manifest: Manifest): void } } } + +export function ndjsonToJson(data: string): any { + return `[${data.split('\n').filter(nonnull).join(',')}]`; +} From 04900c773bf76e628d59ac4e9013ced6162ba52f Mon Sep 17 00:00:00 2001 From: xhayper Date: Thu, 24 Nov 2022 12:03:55 +0700 Subject: [PATCH 2/5] fix: not including not-hoisted deep dependency --- src/npm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/npm.ts b/src/npm.ts index 59340eb3..64e399b0 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -131,7 +131,7 @@ function asYarnDependency(prefix: string, tree: YarnTreeNode | Yarn2TreeNode, pr name: child.name, } as YarnTreeNode); - const dep = asYarnDependency(prefix, childDep, prune); + const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), childDep, prune); if (dep) { children.push(dep); From 4eb15cb33ef7561fbba5d85784d24577ea017404 Mon Sep 17 00:00:00 2001 From: xhayper Date: Thu, 24 Nov 2022 12:16:19 +0700 Subject: [PATCH 3/5] chore: clean up unneeded parameter --- src/npm.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/npm.ts b/src/npm.ts index 64e399b0..4502ef1a 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -264,6 +264,8 @@ export async function getDependencies( dependencies: 'npm' | 'yarn' | 'none' | undefined, packagedDependencies?: string[] ): Promise { + console.log(await getYarnDependencies(cwd, packagedDependencies)); + console.log(await getNpmDependencies(cwd)); if (dependencies === 'none') { return [cwd]; } else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(cwd)))) { From 17593dfe728fbfb6d690cda4993147a7cec4201c Mon Sep 17 00:00:00 2001 From: xhayper Date: Thu, 24 Nov 2022 12:17:36 +0700 Subject: [PATCH 4/5] chore: remove debug code --- src/npm.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/npm.ts b/src/npm.ts index 4502ef1a..a81714f5 100644 --- a/src/npm.ts +++ b/src/npm.ts @@ -200,7 +200,7 @@ async function getYarnProductionDependencies(cwd: string, packagedDependencies?: .map(parseInt); const isY2 = major > 1; - const listCommand = isY2 ? 'yarn info --recursive --dependents --json' : 'yarn list --prod --json --depth 99999'; + const listCommand = isY2 ? 'yarn info --recursive --dependents --json' : 'yarn list --prod --json'; let raw = await new Promise((c, e) => cp.exec(listCommand, { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, (err, stdout) => @@ -264,8 +264,6 @@ export async function getDependencies( dependencies: 'npm' | 'yarn' | 'none' | undefined, packagedDependencies?: string[] ): Promise { - console.log(await getYarnDependencies(cwd, packagedDependencies)); - console.log(await getNpmDependencies(cwd)); if (dependencies === 'none') { return [cwd]; } else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(cwd)))) { From 21c83bc0ce8a7208f28db6c8e1545af00400364e Mon Sep 17 00:00:00 2001 From: hayper Date: Wed, 30 Nov 2022 19:13:52 +0700 Subject: [PATCH 5/5] chore: fix the return type --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index f2f99bd3..c1f9a117 100644 --- a/src/util.ts +++ b/src/util.ts @@ -180,6 +180,6 @@ export function patchOptionsWithManifest(options: any, manifest: Manifest): void } } -export function ndjsonToJson(data: string): any { +export function ndjsonToJson(data: string): string { return `[${data.split('\n').filter(nonnull).join(',')}]`; }