Skip to content

Commit 37c0432

Browse files
committed
feat(install): add bun as a package manager (#1005)
## Summary Add bun as the 4th supported package manager alongside pnpm, npm, and yarn. Bun is added only as a package manager and runtime support is not planned. Closes #557 ## Changes ### Rust Core - Add `Bun` variant to `PackageManagerType` enum - Detect bun via `packageManager` field, `bun.lock`, `bun.lockb`, `bunfig.toml` - Download platform-specific native binary from `@oven/bun-{os}-{arch}` npm packages (including musl variants) - Add native binary shim support (non-Node.js wrappers for sh/cmd/ps1) - Add `PackageManagerType::Bun` arms to all 30 command files with correct flag mappings - Add bun to interactive package manager selection menu ### Command Mappings - `bun install` with `--frozen-lockfile`, `--production`, `--ignore-scripts`, `--lockfile-only`, `--omit optional`, `--filter` - `bun add` with `--dev`, `--peer`, `--optional`, `--exact`, `--global` - `bun remove`, `bun update` (with `--latest`, `--interactive`, `--recursive`, `--production`) - `bun outdated` (with `--filter`, `--recursive`, `--production`, `--omit optional`) - `bun why` (with `--depth`), `bun x` (with `--package`), `bun audit` (with `--audit-level`) - `bun pm pack` (with `--filename`, `--gzip-level`), `bun pm ls`, `bun pm cache`, `bun pm whoami` - `bun info` for package view, `bun publish` for publishing - Unsupported commands (deprecate, fund, owner, ping, search, token) fall back to npm silently ### Global CLI & NAPI - Add `"bun"` to `PACKAGE_MANAGER_TOOLS` in shim dispatch - Add `"bun" => PackageManagerType::Bun` in NAPI binding ### TypeScript - Add `bun` to `PackageManager` type and selection prompt - Add `--package-manager` flag to `vp create` for easier testing - Handle bun in monorepo templates (uses `package.json` workspaces, not pnpm-workspace.yaml) - Bun catalog support: write catalog entries to root `package.json` with `catalog:` references in both dependencies and overrides - Migration: bun uses `overrides` with `catalog:` references (not raw versions) - Use `bun x` instead of `bunx` for DLX commands (better cross-platform compatibility) ### Snap Tests & Output Sanitizer - 10 bun command snap tests (add, remove, update, outdated, why, dlx, list, publish, view, cache) - 1 bun monorepo creation snap test (`new-vite-monorepo-bun`) verifying catalog support - Filter unstable bun output: `Resolving dependencies`, `Resolved, downloaded and extracted`, `Resolving...`, `Saved lockfile`, `(vX.Y.Z available)` ### RFCs - Update all 11 package-manager RFCs with bun command mapping tables ### Ecosystem CI - Add `bun-vite-template` (React + Mantine) test case ## Test plan - [x] `cargo test -p vite_install` — 455 tests pass (18 new bun tests) - [x] `cargo test -p vite_global_cli` — 311 tests pass - [x] `pnpm -F vite-plus snap-test-global bun` — all 10 bun snap tests pass - [x] `pnpm -F vite-plus snap-test-global new-vite-monorepo-bun` — catalog test passes - [x] `pnpm -F vite-plus snap-test-global new-vite-monorepo` — existing pnpm test unaffected - [x] Ecosystem CI clone verified locally for `bun-vite-template` - [x] Windows native cmd shim tested (no command echo) - [x] musl Linux binary detection tested 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 2e59346 commit 37c0432

File tree

92 files changed

+2957
-479
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+2957
-479
lines changed

.claude/skills/add-ecosystem-ci/SKILL.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,42 @@ Present the auto-detected configuration and ask user to confirm or modify:
8484
8585
## Step 4: Verify
8686
87-
Test the clone locally:
87+
### 4.1 Build fresh tgz packages
88+
89+
Always rebuild tgz packages from latest source to avoid using stale cached versions:
90+
91+
```bash
92+
# Rebuild the global CLI first (includes Rust binary + NAPI binding)
93+
pnpm bootstrap-cli
94+
95+
# Pack fresh tgz files into tmp/tgz/
96+
rm -rf tmp/tgz && mkdir -p tmp/tgz
97+
cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
98+
cd packages/test && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
99+
cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
100+
ls -la tmp/tgz
101+
```
102+
103+
### 4.2 Clone and test locally
88104

89105
```bash
90106
node ecosystem-ci/clone.ts project-name
91107
```
92108

109+
### 4.3 Patch and run commands
110+
111+
```bash
112+
# Run from the ecosystem-ci temp directory
113+
cd $(node -e "const os=require('os'); console.log(os.tmpdir() + '/vite-plus-ecosystem-ci')")
114+
115+
# Migrate the project (uses tgz files from tmp/tgz/)
116+
node /path/to/vite-plus/ecosystem-ci/patch-project.ts project-name
117+
118+
# Run the configured commands
119+
cd project-name
120+
vp run build
121+
```
122+
93123
3. **Add OS exclusion to `.github/workflows/e2e-test.yml`** (if not running on both):
94124

95125
For ubuntu-only:

.github/workflows/e2e-test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ jobs:
289289
node-version: 24
290290
command: |
291291
vp test run
292+
- name: bun-vite-template
293+
node-version: 24
294+
command: |
295+
vp run build
296+
vp run test
292297
exclude:
293298
# frm-stack uses Docker (testcontainers) which doesn't work the same way on Windows
294299
- os: windows-latest

crates/vite_global_cli/src/shim/dispatch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const RECURSION_ENV_VAR: &str = env_vars::VITE_PLUS_TOOL_RECURSION;
2727

2828
/// Package manager tools that should resolve Node.js version from the project context
2929
/// rather than using the install-time version.
30-
const PACKAGE_MANAGER_TOOLS: &[&str] = &["pnpm", "yarn"];
30+
const PACKAGE_MANAGER_TOOLS: &[&str] = &["pnpm", "yarn", "bun"];
3131

3232
fn is_package_manager_tool(tool: &str) -> bool {
3333
PACKAGE_MANAGER_TOOLS.contains(&tool)

crates/vite_install/src/commands/add.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus};
33
use vite_command::run_command;
44
use vite_error::Error;
55
use vite_path::AbsolutePath;
6+
use vite_shared::output;
67

78
use crate::package_manager::{
89
PackageManager, PackageManagerType, ResolveCommandResult, format_path_env,
@@ -195,6 +196,47 @@ impl PackageManager {
195196
args.push("--save-exact".into());
196197
}
197198
}
199+
PackageManagerType::Bun => {
200+
bin_name = "bun".into();
201+
args.push("add".into());
202+
203+
if let Some(save_dependency_type) = options.save_dependency_type {
204+
match save_dependency_type {
205+
SaveDependencyType::Production => {
206+
// default, no flag needed
207+
}
208+
SaveDependencyType::Dev => {
209+
args.push("--dev".into());
210+
}
211+
SaveDependencyType::Peer => {
212+
args.push("--peer".into());
213+
}
214+
SaveDependencyType::Optional => {
215+
args.push("--optional".into());
216+
}
217+
}
218+
}
219+
if options.save_exact {
220+
args.push("--exact".into());
221+
}
222+
if let Some(filters) = options.filters {
223+
if !filters.is_empty() {
224+
output::warn("bun add does not support --filter");
225+
}
226+
}
227+
if options.workspace_root {
228+
output::warn("bun add does not support --workspace-root");
229+
}
230+
if options.workspace_only {
231+
output::warn("bun add does not support --workspace-only");
232+
}
233+
if options.save_catalog_name.is_some() {
234+
output::warn("bun add does not support --save-catalog-name");
235+
}
236+
if options.allow_build.is_some() {
237+
output::warn("bun add does not support --allow-build");
238+
}
239+
}
198240
}
199241

200242
if let Some(pass_through_args) = options.pass_through_args {

crates/vite_install/src/commands/audit.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ impl PackageManager {
142142
}
143143
}
144144
}
145+
PackageManagerType::Bun => {
146+
bin_name = "bun".into();
147+
args.push("audit".into());
148+
149+
if options.fix {
150+
output::warn("bun audit does not support --fix");
151+
return None;
152+
}
153+
154+
if let Some(level) = options.level {
155+
args.push("--audit-level".into());
156+
args.push(level.to_string());
157+
}
158+
159+
if options.production {
160+
output::warn("--production not supported by bun audit, ignoring flag");
161+
}
162+
163+
if options.json {
164+
args.push("--json".into());
165+
}
166+
}
145167
}
146168

147169
// Add pass-through args
@@ -320,6 +342,70 @@ mod tests {
320342
assert_eq!(result.args, vec!["audit", "--level", "high"]);
321343
}
322344

345+
#[test]
346+
fn test_bun_audit_basic() {
347+
let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11");
348+
let result = pm.resolve_audit_command(&AuditCommandOptions {
349+
fix: false,
350+
json: false,
351+
level: None,
352+
production: false,
353+
pass_through_args: None,
354+
});
355+
assert!(result.is_some());
356+
let result = result.unwrap();
357+
assert_eq!(result.bin_path, "bun");
358+
assert!(result.args.contains(&"audit".to_string()), "should contain 'audit'");
359+
assert!(!result.args.contains(&"pm".to_string()), "should NOT use 'bun pm audit'");
360+
}
361+
362+
#[test]
363+
fn test_bun_audit_level() {
364+
let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11");
365+
let result = pm.resolve_audit_command(&AuditCommandOptions {
366+
fix: false,
367+
json: false,
368+
level: Some("high"),
369+
production: false,
370+
pass_through_args: None,
371+
});
372+
assert!(result.is_some());
373+
let result = result.unwrap();
374+
assert!(
375+
result.args.contains(&"--audit-level".to_string()),
376+
"should use --audit-level not --level"
377+
);
378+
assert!(result.args.contains(&"high".to_string()));
379+
}
380+
381+
#[test]
382+
fn test_bun_audit_fix_not_supported() {
383+
let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11");
384+
let result = pm.resolve_audit_command(&AuditCommandOptions {
385+
fix: true,
386+
json: false,
387+
level: None,
388+
production: false,
389+
pass_through_args: None,
390+
});
391+
assert!(result.is_none());
392+
}
393+
394+
#[test]
395+
fn test_bun_audit_json() {
396+
let pm = create_mock_package_manager(PackageManagerType::Bun, "1.3.11");
397+
let result = pm.resolve_audit_command(&AuditCommandOptions {
398+
fix: false,
399+
json: true,
400+
level: None,
401+
production: false,
402+
pass_through_args: None,
403+
});
404+
assert!(result.is_some());
405+
let result = result.unwrap();
406+
assert_eq!(result.args, vec!["audit", "--json"]);
407+
}
408+
323409
#[test]
324410
fn test_audit_with_level_yarn2() {
325411
let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0");

crates/vite_install/src/commands/cache.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,28 @@ impl PackageManager {
117117
}
118118
}
119119
}
120+
PackageManagerType::Bun => {
121+
bin_name = "bun".into();
122+
123+
match options.subcommand {
124+
"dir" | "path" => {
125+
args.push("pm".into());
126+
args.push("cache".into());
127+
}
128+
"clean" => {
129+
args.push("pm".into());
130+
args.push("cache".into());
131+
args.push("rm".into());
132+
}
133+
_ => {
134+
output::warn(&format!(
135+
"bun pm cache subcommand '{}' not supported",
136+
options.subcommand
137+
));
138+
return None;
139+
}
140+
}
141+
}
120142
}
121143

122144
// Add pass-through args

crates/vite_install/src/commands/config.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,32 @@ impl PackageManager {
127127
}
128128
}
129129
}
130+
PackageManagerType::Bun => {
131+
output::warn(
132+
"bun uses bunfig.toml for configuration, not a config command. Falling back to npm config.",
133+
);
134+
135+
// Fall back to npm config
136+
args.push("config".into());
137+
args.push(options.subcommand.to_string());
138+
139+
if let Some(key) = options.key {
140+
args.push(key.to_string());
141+
}
142+
143+
if let Some(value) = options.value {
144+
args.push(value.to_string());
145+
}
146+
147+
if options.json {
148+
args.push("--json".into());
149+
}
150+
151+
if let Some(location) = options.location {
152+
args.push("--location".into());
153+
args.push(location.to_string());
154+
}
155+
}
130156
}
131157

132158
// Add pass-through args

crates/vite_install/src/commands/dedupe.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus};
33
use vite_command::run_command;
44
use vite_error::Error;
55
use vite_path::AbsolutePath;
6+
use vite_shared::output;
67

78
use crate::package_manager::{
89
PackageManager, PackageManagerType, ResolveCommandResult, format_path_env,
@@ -63,6 +64,11 @@ impl PackageManager {
6364
args.push("--dry-run".into());
6465
}
6566
}
67+
PackageManagerType::Bun => {
68+
bin_name = "bun".into();
69+
output::warn("bun does not support dedupe, falling back to bun install");
70+
args.push("install".into());
71+
}
6672
}
6773

6874
// Add pass-through args

crates/vite_install/src/commands/deprecate.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ use std::{collections::HashMap, process::ExitStatus};
33
use vite_command::run_command;
44
use vite_error::Error;
55
use vite_path::AbsolutePath;
6+
use vite_shared::output;
67

7-
use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env};
8+
use crate::package_manager::{
9+
PackageManager, PackageManagerType, ResolveCommandResult, format_path_env,
10+
};
811

912
/// Options for the deprecate command.
1013
#[derive(Debug, Default)]
@@ -31,6 +34,7 @@ impl PackageManager {
3134

3235
/// Resolve the deprecate command.
3336
/// All package managers delegate to npm deprecate.
37+
/// Bun does not support deprecate, falls back to npm.
3438
#[must_use]
3539
pub fn resolve_deprecate_command(
3640
&self,
@@ -40,6 +44,12 @@ impl PackageManager {
4044
let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]);
4145
let mut args: Vec<String> = Vec::new();
4246

47+
if self.client == PackageManagerType::Bun {
48+
output::warn(
49+
"bun does not support the deprecate command, falling back to npm deprecate",
50+
);
51+
}
52+
4353
args.push("deprecate".into());
4454
args.push(options.package.to_string());
4555
args.push(options.message.to_string());

crates/vite_install/src/commands/dist_tag.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, process::ExitStatus};
33
use vite_command::run_command;
44
use vite_error::Error;
55
use vite_path::AbsolutePath;
6+
use vite_shared::output;
67

78
use crate::package_manager::{
89
PackageManager, PackageManagerType, ResolveCommandResult, format_path_env,
@@ -64,6 +65,11 @@ impl PackageManager {
6465
args.push("tag".into());
6566
}
6667
}
68+
PackageManagerType::Bun => {
69+
output::warn("bun does not support dist-tag, falling back to npm dist-tag");
70+
bin_name = "npm".into();
71+
args.push("dist-tag".into());
72+
}
6773
}
6874

6975
match &options.subcommand {

0 commit comments

Comments
 (0)