Skip to content

Commit 3f8a0b0

Browse files
feat: add bun to package managers (#16)
* feat: add bun to package managers * chore: skip bun tests on windows * chore: split windows + other jobs
1 parent 0918f88 commit 3f8a0b0

File tree

6 files changed

+178
-28
lines changed

6 files changed

+178
-28
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,30 @@ jobs:
1010
test:
1111
strategy:
1212
matrix:
13-
platform: [ubuntu-latest, macos-latest, windows-latest]
13+
platform: [ubuntu-latest, macos-latest]
14+
node-version: ["20.x"]
15+
16+
runs-on: ${{ matrix.platform }}
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Use Node.js ${{ matrix.node-version }}
21+
uses: actions/setup-node@v3
22+
with:
23+
node-version: ${{ matrix.node-version }}
24+
- uses: pnpm/action-setup@v3
25+
with:
26+
version: 8
27+
- uses: oven-sh/setup-bun@v1
28+
29+
- run: npm i
30+
- run: npm run build --if-present
31+
- run: npm test
32+
33+
test-win:
34+
strategy:
35+
matrix:
36+
platform: [windows-latest]
1437
node-version: ["20.x"]
1538

1639
runs-on: ${{ matrix.platform }}

src/bin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ ${prettyPrintRow([
4747
["--npm", "Use npm to remove and install packages."],
4848
["--yarn", "Use yarn to remove and install packages."],
4949
["--pnpm", "Use pnpm to remove and install packages."],
50+
["--bun", "Use bun to remove and install packages."],
5051
["--verbose", "Show additional debugging information."],
5152
["-h, --help", "Show this help text."],
5253
["--version", "Print the version number."],
@@ -82,6 +83,7 @@ if (args.length === 0) {
8283
npm: { type: "boolean", default: false },
8384
yarn: { type: "boolean", default: false },
8485
pnpm: { type: "boolean", default: false },
86+
bun: { type: "boolean", default: false },
8587
debug: { type: "boolean", default: false },
8688
help: { type: "boolean", default: false, short: "h" },
8789
version: { type: "boolean", default: false },
@@ -110,6 +112,8 @@ if (args.length === 0) {
110112
? "pnpm"
111113
: options.values.yarn
112114
? "yarn"
115+
: options.values.bun
116+
? "bun"
113117
: null;
114118

115119
const cmd = options.positionals[0];

src/commands.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,93 @@ import * as path from "node:path";
22
import * as fs from "node:fs";
33
import * as kl from "kolorist";
44
import { JsrPackage } from "./utils";
5-
import { getPkgManager } from "./pkg_manager";
5+
import { Bun, PkgManagerName, getPkgManager } from "./pkg_manager";
66

7+
const NPMRC_FILE = ".npmrc";
8+
const BUNFIG_FILE = "bunfig.toml";
79
const JSR_NPMRC = `@jsr:registry=https://npm.jsr.io\n`;
10+
const JSR_BUNFIG = `[install.scopes]\n"@jsr" = "https://npm.jsr.io/"\n`;
11+
12+
async function wrapWithStatus(msg: string, fn: () => Promise<void>) {
13+
process.stdout.write(msg + "...");
14+
15+
try {
16+
await fn();
17+
process.stdout.write(kl.green("ok") + "\n");
18+
} catch (err) {
19+
process.stdout.write(kl.red("error") + "\n");
20+
throw err;
21+
}
22+
}
823

924
export async function setupNpmRc(dir: string) {
10-
const npmRcPath = path.join(dir, ".npmrc");
25+
const npmRcPath = path.join(dir, NPMRC_FILE);
26+
const msg = `Setting up ${NPMRC_FILE}`;
1127
try {
1228
let content = await fs.promises.readFile(npmRcPath, "utf-8");
1329
if (!content.includes(JSR_NPMRC)) {
1430
content += JSR_NPMRC;
15-
await fs.promises.writeFile(npmRcPath, content);
31+
await wrapWithStatus(msg, async () => {
32+
await fs.promises.writeFile(npmRcPath, content);
33+
});
34+
}
35+
} catch (err) {
36+
if (err instanceof Error && (err as any).code === "ENOENT") {
37+
await wrapWithStatus(msg, async () => {
38+
await fs.promises.writeFile(npmRcPath, JSR_NPMRC);
39+
});
40+
} else {
41+
throw err;
42+
}
43+
}
44+
}
45+
46+
export async function setupBunfigToml(dir: string) {
47+
const bunfigPath = path.join(dir, BUNFIG_FILE);
48+
const msg = `Setting up ${BUNFIG_FILE}`;
49+
try {
50+
let content = await fs.promises.readFile(bunfigPath, "utf-8");
51+
if (!/^"@myorg1"\s+=/gm.test(content)) {
52+
content += JSR_BUNFIG;
53+
await wrapWithStatus(msg, async () => {
54+
await fs.promises.writeFile(bunfigPath, content);
55+
});
1656
}
1757
} catch (err) {
1858
if (err instanceof Error && (err as any).code === "ENOENT") {
19-
await fs.promises.writeFile(npmRcPath, JSR_NPMRC);
59+
await wrapWithStatus(msg, async () => {
60+
await fs.promises.writeFile(bunfigPath, JSR_BUNFIG);
61+
});
2062
} else {
2163
throw err;
2264
}
2365
}
2466
}
2567

2668
export interface BaseOptions {
27-
pkgManagerName: "npm" | "yarn" | "pnpm" | null;
69+
pkgManagerName: PkgManagerName | null;
2870
}
2971

3072
export interface InstallOptions extends BaseOptions {
3173
mode: "dev" | "prod" | "optional";
3274
}
3375

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

79+
if (pkgManager instanceof Bun) {
80+
// Bun doesn't support reading from .npmrc yet
81+
await setupBunfigToml(pkgManager.cwd);
82+
} else {
83+
await setupNpmRc(pkgManager.cwd);
84+
}
85+
86+
console.log(`Installing ${kl.cyan(packages.join(", "))}...`);
3987
await pkgManager.install(packages, options);
4088
}
4189

4290
export async function remove(packages: JsrPackage[], options: BaseOptions) {
43-
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
4491
const pkgManager = await getPkgManager(process.cwd(), options.pkgManagerName);
92+
console.log(`Removing ${kl.cyan(packages.join(", "))}...`);
4593
await pkgManager.remove(packages);
4694
}

src/pkg_manager.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,35 @@ class Pnpm implements PackageManager {
9494
}
9595
}
9696

97-
export type PkgManagerName = "npm" | "yarn" | "pnpm";
97+
export class Bun implements PackageManager {
98+
constructor(public cwd: string) {}
99+
100+
async install(packages: JsrPackage[], options: InstallOptions) {
101+
const args = ["add"];
102+
const mode = modeToFlag(options.mode);
103+
if (mode !== "") {
104+
args.push(mode);
105+
}
106+
args.push(...toPackageArgs(packages));
107+
await execWithLog("bun", args, this.cwd);
108+
}
109+
110+
async remove(packages: JsrPackage[]) {
111+
await execWithLog(
112+
"bun",
113+
["remove", ...packages.map((pkg) => pkg.toString())],
114+
this.cwd
115+
);
116+
}
117+
}
118+
119+
export type PkgManagerName = "npm" | "yarn" | "pnpm" | "bun";
98120

99121
function getPkgManagerFromEnv(value: string): PkgManagerName | null {
100-
if (value.includes("pnpm/")) return "pnpm";
101-
else if (value.includes("yarn/")) return "yarn";
102-
else if (value.includes("npm/")) return "npm";
122+
if (value.startsWith("pnpm/")) return "pnpm";
123+
else if (value.startsWith("yarn/")) return "yarn";
124+
else if (value.startsWith("npm/")) return "npm";
125+
else if (value.startsWith("bun/")) return "bun";
103126
else return null;
104127
}
105128

@@ -121,6 +144,8 @@ export async function getPkgManager(
121144
return new Yarn(projectDir);
122145
} else if (result === "pnpm") {
123146
return new Pnpm(projectDir);
147+
} else if (result === "bun") {
148+
return new Bun(projectDir);
124149
}
125150

126151
return new Npm(projectDir);

src/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ export async function findProjectDir(
105105
return result;
106106
}
107107

108+
const bunLockfile = path.join(dir, "bun.lockb");
109+
if (await fileExists(bunLockfile)) {
110+
logDebug(`Detected bun from lockfile ${bunLockfile}`);
111+
result.projectDir = dir;
112+
result.pkgManagerName = "bun";
113+
return result;
114+
}
115+
108116
const pkgJsonPath = path.join(dir, "package.json");
109117
if (await fileExists(pkgJsonPath)) {
110118
logDebug(`Found package.json at ${pkgJsonPath}`);

test/commands.test.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,22 +136,64 @@ describe("install", () => {
136136
);
137137
});
138138

139-
it("detect pnpm from npm_config_user_agent", async () => {
140-
await withTempEnv(
141-
["i", "@std/[email protected]"],
142-
async (_, dir) => {
143-
assert.ok(
144-
await isFile(path.join(dir, "pnpm-lock.yaml")),
145-
"pnpm lockfile not created"
146-
);
147-
},
148-
{
149-
env: {
150-
...process.env,
151-
npm_config_user_agent: `pnpm/8.14.3 ${process.env.npm_config_user_agent}`,
139+
if (process.platform !== "win32") {
140+
it("jsr add --bun @std/[email protected] - forces bun", async () => {
141+
await withTempEnv(
142+
["i", "--bun", "@std/[email protected]"],
143+
async (_, dir) => {
144+
assert.ok(
145+
await isFile(path.join(dir, "bun.lockb")),
146+
"bun lockfile not created"
147+
);
148+
149+
const config = await fs.promises.readFile(
150+
path.join(dir, "bunfig.toml"),
151+
"utf-8"
152+
);
153+
assert.match(config, /"@jsr"\s+=/, "bunfig.toml not created");
154+
}
155+
);
156+
});
157+
}
158+
159+
describe("env detection", () => {
160+
it("detect pnpm from npm_config_user_agent", async () => {
161+
await withTempEnv(
162+
["i", "@std/[email protected]"],
163+
async (_, dir) => {
164+
assert.ok(
165+
await isFile(path.join(dir, "pnpm-lock.yaml")),
166+
"pnpm lockfile not created"
167+
);
152168
},
153-
}
154-
);
169+
{
170+
env: {
171+
...process.env,
172+
npm_config_user_agent: `pnpm/8.14.3 ${process.env.npm_config_user_agent}`,
173+
},
174+
}
175+
);
176+
});
177+
178+
if (process.platform !== "win32") {
179+
it("detect bun from npm_config_user_agent", async () => {
180+
await withTempEnv(
181+
["i", "@std/[email protected]"],
182+
async (_, dir) => {
183+
assert.ok(
184+
await isFile(path.join(dir, "bun.lockb")),
185+
"bun lockfile not created"
186+
);
187+
},
188+
{
189+
env: {
190+
...process.env,
191+
npm_config_user_agent: `bun/1.0.29 ${process.env.npm_config_user_agent}`,
192+
},
193+
}
194+
);
195+
});
196+
}
155197
});
156198
});
157199

0 commit comments

Comments
 (0)