From 0412618b16bac6d002662187d3165fbb403b9269 Mon Sep 17 00:00:00 2001 From: Thomas Dy Date: Fri, 9 Sep 2022 21:48:58 +0900 Subject: [PATCH] Prioritize direct dependency if available --- src/cli.ts | 7 ++++++- src/index.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- tests/index.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 8391240c..d42fad81 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,7 +32,8 @@ program .option( '--includePrerelease', 'Include prereleases in version comparisons, e.g. ^1.0.0 will be satisfied by 1.0.1-alpha' - ); + ) + .option('--package-json ', 'path to package.json, used to prioritize direct dependencies'); program.parse(process.argv); @@ -47,6 +48,7 @@ const { includePrerelease, print, noStats, + packageJson: packageJsonPath, } = program.opts(); const file = program.args.length ? program.args[0] : 'yarn.lock'; @@ -63,10 +65,12 @@ if (strategy !== 'highest' && strategy !== 'fewer') { try { const yarnLock = fs.readFileSync(file, 'utf8'); + const packageJson = packageJsonPath ? fs.readFileSync(packageJsonPath, 'utf8') : undefined; const useMostCommon = strategy === 'fewer'; if (list) { const duplicates = listDuplicates(yarnLock, { + packageJson, useMostCommon, includeScopes: scopes, includePackages: packages, @@ -81,6 +85,7 @@ try { } } else { let dedupedYarnLock = fixDuplicates(yarnLock, { + packageJson, useMostCommon, includeScopes: scopes, includePackages: packages, diff --git a/src/index.ts b/src/index.ts index 4f0a245c..d45e2e49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,12 @@ import * as lockfile from '@yarnpkg/lockfile'; import semver from 'semver'; +type PackageJson = { + dependencies?: Record, + devDependencies?: Record, + optionalDependencies?: Record +} + type YarnEntry = { resolved: string version: string @@ -14,6 +20,7 @@ type Package = { installedVersion:string, name: string, pkg: YarnEntry, + isDirectDependency: boolean, satisfiedBy: Set candidateVersions?: string[], requestedVersion: string, @@ -23,12 +30,14 @@ type Package = { type Version = { pkg: YarnEntry, + isDirectDependency: boolean, satisfies: Set } type Versions = Map; type Options = { + packageJson?: string; includeScopes?: string[]; includePackages?: string[]; excludePackages?: string[]; @@ -37,10 +46,30 @@ type Options = { includePrerelease?: boolean; } +const getDirectRequirements = (file: string | undefined): Set => { + const result = new Set(); + if (file === undefined) { + return result; + } + + const packageJson = JSON.parse(file) as PackageJson; + for (const [packageName, requestedVersion] of Object.entries(packageJson.dependencies ?? {})) { + result.add(`${packageName}@${requestedVersion}`); + } + for (const [packageName, requestedVersion] of Object.entries(packageJson.devDependencies ?? {})) { + result.add(`${packageName}@${requestedVersion}`); + } + for (const [packageName, requestedVersion] of Object.entries(packageJson.optionalDependencies ?? {})) { + result.add(`${packageName}@${requestedVersion}`); + } + return result; +} + const parseYarnLock = (file:string) => lockfile.parse(file).object as YarnEntries; const extractPackages = ( yarnEntries: YarnEntries, + directDependencies: Set, includeScopes:string[] = [], includePackages:string[] = [], excludePackages:string[] = [], @@ -93,6 +122,7 @@ const extractPackages = ( name: packageName, requestedVersion, installedVersion: entry.version, + isDirectDependency: directDependencies.has(entryName), satisfiedBy: new Set(), versions: new Map() }); @@ -107,9 +137,15 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon // Extract the list of unique versions for this package const versions:Versions = new Map(); for (const packageInstance of packageInstances) { - if (versions.has(packageInstance.installedVersion)) continue; + if (versions.has(packageInstance.installedVersion)) { + const existingPackage = versions.get(packageInstance.installedVersion)!; + existingPackage.isDirectDependency ||= packageInstance.isDirectDependency; + continue; + }; + versions.set(packageInstance.installedVersion, { pkg: packageInstance.pkg, + isDirectDependency: packageInstance.isDirectDependency, satisfies: new Set(), }) } @@ -139,6 +175,11 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon // Compute the versions that actually satisfy this instance packageInstance.candidateVersions = Array.from(packageInstance.satisfiedBy); packageInstance.candidateVersions.sort((versionA:string, versionB:string) => { + const isDirectA = versions.get(versionA)!.isDirectDependency; + const isDirectB = versions.get(versionB)!.isDirectDependency; + if (isDirectA && !isDirectB) return -1; + if (!isDirectB && isDirectA) return 1; + if (useMostCommon) { // Sort verions based on how many packages it satisfies. In case of a tie, put the // highest version first. @@ -160,6 +201,7 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon export const getDuplicates = ( yarnEntries: YarnEntries, { + packageJson, includeScopes = [], includePackages = [], excludePackages = [], @@ -170,6 +212,7 @@ export const getDuplicates = ( ): Package[] => { const packages = extractPackages( yarnEntries, + getDirectRequirements(packageJson), includeScopes, includePackages, excludePackages, diff --git a/tests/index.ts b/tests/index.ts index 51fac2bb..f4be65f0 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -272,3 +272,44 @@ test('should support the integrity field if present', () => { // We should not have made any change to the order of outputted lines (@yarnpkg/lockfile 1.0.0 had this bug) expect(yarn_lock).toBe(deduped); }); + +test('prioritizes direct requirements if present', () => { + const yarn_lock = outdent` + a-package@*: + version "2.0.0" + resolved "http://example.com/a-package/2.0.0" + + a-package@^1.0.0, a-package@^1.0.1, a-package@^1.0.2: + version "1.0.2" + resolved "http://example.com/a-package/1.0.2" + + a-package@^0.1.0: + version "0.1.0" + resolved "http://example.com/a-package/0.1.0" + + other-package@>=1.0.0: + version "2.0.0" + resolved "http://example.com/other-package/2.0.0" + + other-package@^1.0.0: + version "1.0.12" + resolved "http://example.com/other-package/1.0.12" + `; + const package_json = outdent` + { + "dependencies": { + "a-package": "^1.0.1" + } + } + `; + + const deduped = fixDuplicates(yarn_lock, { + packageJson: package_json, + }); + const json = lockfile.parse(deduped).object; + expect(json['a-package@*']['version']).toEqual('1.0.2'); + expect(json['a-package@^1.0.0']['version']).toEqual('1.0.2'); + expect(json['a-package@^0.1.0']['version']).toEqual('0.1.0'); + expect(json['other-package@>=1.0.0']['version']).toEqual('2.0.0'); + expect(json['other-package@^1.0.0']['version']).toEqual('1.0.12'); +});