Skip to content

Commit

Permalink
Prioritize direct dependency if available
Browse files Browse the repository at this point in the history
  • Loading branch information
thatsmydoing committed Sep 9, 2022
1 parent 4488225 commit 0412618
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 2 deletions.
7 changes: 6 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>', 'path to package.json, used to prioritize direct dependencies');

program.parse(process.argv);

Expand All @@ -47,6 +48,7 @@ const {
includePrerelease,
print,
noStats,
packageJson: packageJsonPath,
} = program.opts();

const file = program.args.length ? program.args[0] : 'yarn.lock';
Expand All @@ -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,
Expand All @@ -81,6 +85,7 @@ try {
}
} else {
let dedupedYarnLock = fixDuplicates(yarnLock, {
packageJson,
useMostCommon,
includeScopes: scopes,
includePackages: packages,
Expand Down
45 changes: 44 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as lockfile from '@yarnpkg/lockfile';
import semver from 'semver';

type PackageJson = {
dependencies?: Record<string, string>,
devDependencies?: Record<string, string>,
optionalDependencies?: Record<string, string>
}

type YarnEntry = {
resolved: string
version: string
Expand All @@ -14,6 +20,7 @@ type Package = {
installedVersion:string,
name: string,
pkg: YarnEntry,
isDirectDependency: boolean,
satisfiedBy: Set<string>
candidateVersions?: string[],
requestedVersion: string,
Expand All @@ -23,12 +30,14 @@ type Package = {

type Version = {
pkg: YarnEntry,
isDirectDependency: boolean,
satisfies: Set<Package>
}

type Versions = Map<string, Version>;

type Options = {
packageJson?: string;
includeScopes?: string[];
includePackages?: string[];
excludePackages?: string[];
Expand All @@ -37,10 +46,30 @@ type Options = {
includePrerelease?: boolean;
}

const getDirectRequirements = (file: string | undefined): Set<string> => {
const result = new Set<string>();
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<string>,
includeScopes:string[] = [],
includePackages:string[] = [],
excludePackages:string[] = [],
Expand Down Expand Up @@ -93,6 +122,7 @@ const extractPackages = (
name: packageName,
requestedVersion,
installedVersion: entry.version,
isDirectDependency: directDependencies.has(entryName),
satisfiedBy: new Set(),
versions: new Map()
});
Expand All @@ -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(),
})
}
Expand Down Expand Up @@ -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.
Expand All @@ -160,6 +201,7 @@ const computePackageInstances = (packages: Packages, name: string, useMostCommon
export const getDuplicates = (
yarnEntries: YarnEntries,
{
packageJson,
includeScopes = [],
includePackages = [],
excludePackages = [],
Expand All @@ -170,6 +212,7 @@ export const getDuplicates = (
): Package[] => {
const packages = extractPackages(
yarnEntries,
getDirectRequirements(packageJson),
includeScopes,
includePackages,
excludePackages,
Expand Down
41 changes: 41 additions & 0 deletions tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

0 comments on commit 0412618

Please sign in to comment.