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 bun to package managers #16

Merged
merged 3 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 8
- uses: oven-sh/setup-bun@v1

- run: npm i
- run: npm run build --if-present
Expand Down
4 changes: 4 additions & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ ${prettyPrintRow([
["--npm", "Use npm to remove and install packages."],
["--yarn", "Use yarn to remove and install packages."],
["--pnpm", "Use pnpm to remove and install packages."],
["--bun", "Use bun to remove and install packages."],
["--verbose", "Show additional debugging information."],
["-h, --help", "Show this help text."],
["--version", "Print the version number."],
Expand Down Expand Up @@ -82,6 +83,7 @@ if (args.length === 0) {
npm: { type: "boolean", default: false },
yarn: { type: "boolean", default: false },
pnpm: { type: "boolean", default: false },
bun: { type: "boolean", default: false },
debug: { type: "boolean", default: false },
help: { type: "boolean", default: false, short: "h" },
version: { type: "boolean", default: false },
Expand Down Expand Up @@ -110,6 +112,8 @@ if (args.length === 0) {
? "pnpm"
: options.values.yarn
? "yarn"
: options.values.bun
? "bun"
: null;

const cmd = options.positionals[0];
Expand Down
64 changes: 56 additions & 8 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,93 @@ import * as path from "node:path";
import * as fs from "node:fs";
import * as kl from "kolorist";
import { JsrPackage } from "./utils";
import { getPkgManager } from "./pkg_manager";
import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager";

const NPMRC_FILE = ".npmrc";
const BUNFIG_FILE = "bunfig.toml";
const JSR_NPMRC = `@jsr:registry=https://npm.jsr.io\n`;
const JSR_BUNFIG = `[install.scopes]\n"@jsr" = "https://npm.jsr.io/"\n`;

async function wrapWithStatus(msg: string, fn: () => Promise<void>) {
process.stdout.write(msg + "...");

try {
await fn();
process.stdout.write(kl.green("ok") + "\n");
} catch (err) {
process.stdout.write(kl.red("error") + "\n");
throw err;
}
}

export async function setupNpmRc(dir: string) {
const npmRcPath = path.join(dir, ".npmrc");
const npmRcPath = path.join(dir, NPMRC_FILE);
const msg = `Setting up ${NPMRC_FILE}`;
try {
let content = await fs.promises.readFile(npmRcPath, "utf-8");
if (!content.includes(JSR_NPMRC)) {
content += JSR_NPMRC;
await fs.promises.writeFile(npmRcPath, content);
await wrapWithStatus(msg, async () => {
await fs.promises.writeFile(npmRcPath, content);
});
}
} catch (err) {
if (err instanceof Error && (err as any).code === "ENOENT") {
await wrapWithStatus(msg, async () => {
await fs.promises.writeFile(npmRcPath, JSR_NPMRC);
});
} else {
throw err;
}
}
}

export async function setupBunfigToml(dir: string) {
const bunfigPath = path.join(dir, BUNFIG_FILE);
const msg = `Setting up ${BUNFIG_FILE}`;
try {
let content = await fs.promises.readFile(bunfigPath, "utf-8");
if (!/^"@myorg1"\s+=/gm.test(content)) {
content += JSR_BUNFIG;
await wrapWithStatus(msg, async () => {
await fs.promises.writeFile(bunfigPath, content);
});
}
} catch (err) {
if (err instanceof Error && (err as any).code === "ENOENT") {
await fs.promises.writeFile(npmRcPath, JSR_NPMRC);
await wrapWithStatus(msg, async () => {
await fs.promises.writeFile(bunfigPath, JSR_BUNFIG);
});
} else {
throw err;
}
}
}

export interface BaseOptions {
pkgManagerName: "npm" | "yarn" | "pnpm" | null;
pkgManagerName: PkgManagerName | null;
}

export interface InstallOptions extends BaseOptions {
mode: "dev" | "prod" | "optional";
}

export async function install(packages: JsrPackage[], options: InstallOptions) {
console.log(`Installing ${kl.cyan(packages.join(", "))}...`);
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);
await setupNpmRc(pkgManager.cwd);

if (pkgManager instanceof Bun) {
// Bun doesn't support reading from .npmrc yet
await setupBunfigToml(pkgManager.cwd);
} else {
await setupNpmRc(pkgManager.cwd);
}

console.log(`Installing ${kl.cyan(packages.join(", "))}...`);
await pkgManager.install(packages, options);
}

export async function remove(packages: JsrPackage[], options: BaseOptions) {
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
await pkgManager.remove(packages);
}
33 changes: 29 additions & 4 deletions src/pkg_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,35 @@ class Pnpm implements PackageManager {
}
}

export type PkgManagerName = "npm" | "yarn" | "pnpm";
export class Bun implements PackageManager {
constructor(public cwd: string) {}

async install(packages: JsrPackage[], options: InstallOptions) {
const args = ["add"];
const mode = modeToFlag(options.mode);
if (mode !== "") {
args.push(mode);
}
args.push(...toPackageArgs(packages));
await execWithLog("bun", args, this.cwd);
}

async remove(packages: JsrPackage[]) {
await execWithLog(
"bun",
["remove", ...packages.map((pkg) => pkg.toString())],
this.cwd
);
}
}

export type PkgManagerName = "npm" | "yarn" | "pnpm" | "bun";

function getPkgManagerFromEnv(value: string): PkgManagerName | null {
if (value.includes("pnpm/")) return "pnpm";
else if (value.includes("yarn/")) return "yarn";
else if (value.includes("npm/")) return "npm";
if (value.startsWith("pnpm/")) return "pnpm";
else if (value.startsWith("yarn/")) return "yarn";
else if (value.startsWith("npm/")) return "npm";
else if (value.startsWith("bun/")) return "bun";
else return null;
}

Expand All @@ -121,6 +144,8 @@ export async function getPkgManager(
return new Yarn(projectDir);
} else if (result === "pnpm") {
return new Pnpm(projectDir);
} else if (result === "bun") {
return new Bun(projectDir);
}

return new Npm(projectDir);
Expand Down
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ export async function findProjectDir(
return result;
}

const bunLockfile = path.join(dir, "bun.lockb");
if (await fileExists(bunLockfile)) {
logDebug(`Detected bun from lockfile ${bunLockfile}`);
result.projectDir = dir;
result.pkgManagerName = "bun";
return result;
}

const pkgJsonPath = path.join(dir, "package.json");
if (await fileExists(pkgJsonPath)) {
logDebug(`Found package.json at ${pkgJsonPath}`);
Expand Down
72 changes: 57 additions & 15 deletions test/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,64 @@ describe("install", () => {
);
});

it("detect pnpm from npm_config_user_agent", async () => {
await withTempEnv(
["i", "@std/[email protected]"],
async (_, dir) => {
assert.ok(
await isFile(path.join(dir, "pnpm-lock.yaml")),
"pnpm lockfile not created"
);
},
{
env: {
...process.env,
npm_config_user_agent: `pnpm/8.14.3 ${process.env.npm_config_user_agent}`,
if (process.platform !== "win32") {
it("jsr add --bun @std/[email protected] - forces bun", async () => {
await withTempEnv(
["i", "--bun", "@std/[email protected]"],
async (_, dir) => {
assert.ok(
await isFile(path.join(dir, "bun.lockb")),
"bun lockfile not created"
);

const config = await fs.promises.readFile(
path.join(dir, "bunfig.toml"),
"utf-8"
);
assert.match(config, /"@jsr"\s+=/, "bunfig.toml not created");
}
);
});
}

describe("env detection", () => {
it("detect pnpm from npm_config_user_agent", async () => {
await withTempEnv(
["i", "@std/[email protected]"],
async (_, dir) => {
assert.ok(
await isFile(path.join(dir, "pnpm-lock.yaml")),
"pnpm lockfile not created"
);
},
}
);
{
env: {
...process.env,
npm_config_user_agent: `pnpm/8.14.3 ${process.env.npm_config_user_agent}`,
},
}
);
});

if (process.platform !== "win32") {
it("detect bun from npm_config_user_agent", async () => {
await withTempEnv(
["i", "@std/[email protected]"],
async (_, dir) => {
assert.ok(
await isFile(path.join(dir, "bun.lockb")),
"bun lockfile not created"
);
},
{
env: {
...process.env,
npm_config_user_agent: `bun/1.0.29 ${process.env.npm_config_user_agent}`,
},
}
);
});
}
});
});

Expand Down
Loading