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: add dedupeDependencies utility #175

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist
types
.conf*
# bun.lockb
.idea
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ Installs project dependencies.

Removes dependency from the project.

### `dedupeDependencies(options)`

Dedupe project dependencies.

> [!NOTE]
> This is currently not supported for `bun` and `deno`.

## 💻 Development

- Clone this repository
Expand Down
8 changes: 1 addition & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
doesDependencyExist,
} from "./_utils";
import type { OperationOptions, PackageManagerName } from "./types";
import { consola } from "consola";
import * as fs from "node:fs";
import { resolve } from "pathe";

/**
* Installs project dependencies.
Expand Down Expand Up @@ -232,3 +235,30 @@ export async function ensureDependencyInstalled(

await addDependency(name, resolvedOptions);
}

export async function dedupeDependencies(options: Pick<OperationOptions, 'cwd' | 'silent'> & {recreateLockfile?: boolean} = {}) {
const resolvedOptions = await resolveOperationOptions(options);
const isSupported = ["bun", "deno"].includes(resolvedOptions.packageManager?.name ?? "");
const recreateLockfile = options.recreateLockfile ?? !isSupported;
if(!isSupported) {
consola.log(
`Deduping is currently not supported for \`${resolvedOptions.packageManager?.name ?? "unknown package manager"}\`.`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it might only make users more confused, goal of nypm is transparent compatibility and recreate is already closest behavior + we log about what we do.

Also, it might make implicit issues, if a library passes recreateLockfile: false it will be a no-op function on deno/bun.

I suggest we do fallback without warn by default and in case recreateLockfile: false is passed, throw an error so we cover both max compat (default) and strict (if option explicitly asks to not fallback)

Copy link
Author

@OrbisK OrbisK Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so to summarise:

recreate result
deno or bun undefined recreate lockfile
deno or bun false error
deno or bun true recreate lockfile
any other pm true recreate lockfile
deno or bun undefined dedupe
any other pm false dedupe
  • plus no supported warning

Did I understand that correctly?

Copy link
Member

@pi0 pi0 Dec 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. Without passing recreate (undefined) also:

recreate result
deno or bun undefined recreate lockfile
any other pm undefined dedupe
deno or bun false error
any other pm false dedupe
deno or bun true recreate lockfile
any other pm true recreate lockfile

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to add tests as best as possbile. it was not possible for me to test for deno and bun with recreate false. Because the errors origin is the test.

);
}
if (recreateLockfile) {
consola.log("Removing lockfile(s) and reinstalling dependencies...");
const lockfiles = Array.isArray(resolvedOptions.packageManager.lockFile)
? resolvedOptions.packageManager.lockFile
: [resolvedOptions.packageManager.lockFile];
for (const lockfile of lockfiles) {
if (lockfile)
fs.rmSync(resolve(resolvedOptions.cwd, lockfile), { force: true });
}
await installDependencies(resolvedOptions);
} else if (isSupported) {
await executeCommand(resolvedOptions.packageManager.command, ["dedupe"], {
cwd: resolvedOptions.cwd,
silent: resolvedOptions.silent,
});
}
}
31 changes: 30 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { defineCommand, runMain, ArgsDef } from "citty";
import { resolve } from "pathe";
import { consola } from "consola";
import { name, version, description } from "../package.json";
import { addDependency, installDependencies, removeDependency } from "./api";
import {
addDependency,
installDependencies,
removeDependency,
dedupeDependencies,
} from "./api";
import { detectPackageManager } from "./package-manager";

const operationArgs = {
Expand Down Expand Up @@ -94,6 +99,29 @@ const detect = defineCommand({
},
});

const dedupe = defineCommand({
meta: {
description: "Dedupe dependencies",
},
args: {
cwd: {
type: "string",
description: "Current working directory",
},
silent: {
type: "boolean",
description: "Run in silent mode",
},
recreateLockFile: {
type: "boolean",
description: "Recreate lock file",
},
},
run: async ({ args }) => {
await dedupeDependencies(args);
},
});

const main = defineCommand({
meta: {
name,
Expand All @@ -109,6 +137,7 @@ const main = defineCommand({
uninstall: remove,
un: remove,
detect,
dedupe,
},
});

Expand Down
2 changes: 1 addition & 1 deletion test/api-workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, it, describe, vi } from "vitest";
import { addDependency, removeDependency } from "../src";
import { fixtures } from "./_shared";

describe("api", () => {
describe("api (workspace)", () => {
for (const fixture of fixtures.filter((f) => f.workspace)) {
describe(fixture.name, () => {
it("adds dependency to the workspace root", async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../src";
import { fixtures } from "./_shared";

describe("api (workspace)", () => {
describe("api", () => {
for (const fixture of fixtures.filter((f) => !f.workspace)) {
describe(fixture.name, () => {
it("installs dependencies", async () => {
Expand Down
25 changes: 25 additions & 0 deletions test/dedupe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it, vi } from "vitest";
import { fixtures } from "./_shared";
import { dedupeDependencies } from "../src";

describe("dedupe", () => {
for (const fixture of fixtures.filter((f) => !f.workspace)) {
describe(fixture.name, () => {
it.skipIf(["bun", "deno"].includes(fixture.packageManager))(
"dedupe dependencies",
async () => {
const dedupeDependenciesSpy = vi.fn(dedupeDependencies);
const executeDedupeDependenciesSpy = () =>
dedupeDependencies({
cwd: fixture.dir,
silent: !process.env.DEBUG,
});
await executeDedupeDependenciesSpy();

expect(dedupeDependenciesSpy).not.toThrow();
},
60_000,
);
});
}
});
10 changes: 1 addition & 9 deletions test/fixtures/pnpm/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.