Skip to content

Commit 4b8cdc4

Browse files
feat(bun): bun prune support (#10175)
### Description Resolves #9058 and #7456 Building on #9783 to add `prune` support for bun. This is a simple implementation that allow turbo to properly prune a `bun.lock` file Basically, it just creates a new `BunLockfileData` with the filtered dependencies/workspaces and serializes it to `bun.lock` via `serde_json`. This is made simple since the internal layout matches the bun.lock format, so the only special serialization case I had to do was `PackageEntry`. This also required updating the _deserialization_, since it skipped parsing some parts that we need to write back out. ### Testing Instructions 1. Clone https://github.com/mugencraft/turbobun (or any other turbo + bun repo) 2. `bun install --save-text-lockfile` (we need the new `.lock` version and not `.lockb`) 3. I then do ```sh rm -rf ./out && cp ../turborepo/target/debug/turbo ./node_modules/.bin/turbo # Copy debug build of turbo bunx turbo prune @turbobun/backend --docker --skip-infer # turbo prune 🚀 ``` 4. Open the generated `out/bun.lock`. It's currently outputted in an unformatted json, so I format it. 5. Observe that the dependencies include backend (`elysia`) and root workspace (`commitizen`) dependencies, but not e.g. `@turbobun/website` dependencies (like `next`) 6. `cd ./out/json && bun install` -> Bun install can read the lockfile and works! 7. You can also patch a random package using `bun patch` and test that the patches are propagated correctly ### Screenshots ![image](https://github.com/user-attachments/assets/8e059425-8e87-4d6e-9360-d49fbf65d105) --------- Co-authored-by: Chris Olszewski <[email protected]>
1 parent b2417df commit 4b8cdc4

File tree

4 files changed

+413
-37
lines changed

4 files changed

+413
-37
lines changed

crates/turborepo-lib/src/commands/prune.rs

-9
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ pub enum Error {
4747
MissingWorkspace(PackageName),
4848
#[error("Cannot prune without parsed lockfile.")]
4949
MissingLockfile,
50-
#[error("`prune` is not supported for Bun.")]
51-
BunUnsupported,
5250
#[error("Unable to read config: {0}")]
5351
Config(#[from] crate::config::Error),
5452
}
@@ -106,13 +104,6 @@ pub async fn prune(
106104

107105
let prune = Prune::new(base, scope, docker, output_dir, use_gitignore, telemetry).await?;
108106

109-
if matches!(
110-
prune.package_graph.package_manager(),
111-
turborepo_repository::package_manager::PackageManager::Bun
112-
) {
113-
return Err(Error::BunUnsupported);
114-
}
115-
116107
println!(
117108
"Generating pruned monorepo for {} in {}",
118109
base.color_config.apply(BOLD.apply_to(scope.join(", "))),

crates/turborepo-lockfiles/src/bun/de.rs

+60-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::collections::VecDeque;
33
use serde::Deserialize;
44

55
use super::{PackageEntry, PackageInfo};
6+
use crate::bun::RootInfo;
67
// Comment explaining entry schemas taken from bun.lock.zig
78
// first index is resolution for each type of package
89
// npm -> [
@@ -32,6 +33,8 @@ impl<'de> Deserialize<'de> for PackageEntry {
3233
Info(Box<PackageInfo>),
3334
}
3435
let mut vals = VecDeque::<Vals>::deserialize(deserializer)?;
36+
37+
// First value is always the package key
3538
let key = vals
3639
.pop_front()
3740
.ok_or_else(|| de::Error::custom("expected package entry to not be empty"))?;
@@ -44,22 +47,53 @@ impl<'de> Deserialize<'de> for PackageEntry {
4447
Vals::Str(_) => None,
4548
Vals::Info(package_info) => Some(*package_info),
4649
};
47-
// For workspace packages deps are second element, rest have them as third
48-
// element
49-
let info = vals
50-
.pop_front()
51-
.and_then(val_to_info)
52-
.or_else(|| vals.pop_front().and_then(val_to_info));
50+
51+
let mut registry = None;
52+
let mut info = None;
53+
54+
// Special case: root packages have a unique second value, so we handle it here
55+
if key.ends_with("@root:") {
56+
let root = vals.pop_front().and_then(|val| {
57+
serde_json::from_value::<RootInfo>(match val {
58+
Vals::Info(info) => {
59+
serde_json::to_value(info.other).expect("failed to convert info to value")
60+
}
61+
_ => return None,
62+
})
63+
.ok()
64+
});
65+
return Ok(Self {
66+
ident: key,
67+
info,
68+
registry,
69+
checksum: None,
70+
root,
71+
});
72+
}
73+
74+
// The second value can be either registry (string) or info (object)
75+
if let Some(val) = vals.pop_front() {
76+
match val {
77+
Vals::Str(reg) => registry = Some(reg),
78+
Vals::Info(package_info) => info = Some(*package_info),
79+
}
80+
};
81+
82+
// Info will be next if we haven't already found it
83+
if info.is_none() {
84+
info = vals.pop_front().and_then(val_to_info);
85+
}
86+
87+
// Checksum is last
5388
let checksum = vals.pop_front().and_then(|val| match val {
5489
Vals::Str(sha) => Some(sha),
5590
Vals::Info(_) => None,
5691
});
92+
5793
Ok(Self {
5894
ident: key,
5995
info,
60-
// The rest are only necessary for serializing a lockfile and aren't needed until adding
61-
// `prune` support
62-
registry: None,
96+
registry,
6397
checksum,
6498
root: None,
6599
})
@@ -114,7 +148,7 @@ mod test {
114148
PackageEntry,
115149
PackageEntry {
116150
ident: "[email protected]".into(),
117-
registry: None,
151+
registry: Some("".into()),
118152
info: Some(PackageInfo {
119153
dependencies: Some(("is-number".into(), "^6.0.0".into()))
120154
.into_iter()
@@ -142,10 +176,26 @@ mod test {
142176
root: None,
143177
}
144178
);
179+
180+
fixture!(
181+
root_pkg,
182+
PackageEntry,
183+
PackageEntry {
184+
ident: "some-package@root:".into(),
185+
root: Some(RootInfo {
186+
bin: Some("bin".into()),
187+
bin_dir: Some("binDir".into()),
188+
}),
189+
info: None,
190+
registry: None,
191+
checksum: None,
192+
}
193+
);
145194
#[test_case(json!({"name": "bun-test", "devDependencies": {"turbo": "^2.3.3"}}), basic_workspace() ; "basic")]
146195
#[test_case(json!({"name": "docs", "version": "0.1.0"}), workspace_with_version() ; "with version")]
147196
#[test_case(json!(["[email protected]", "", {"dependencies": {"is-number": "^6.0.0"}}, "sha"]), registry_pkg() ; "registry package")]
148197
#[test_case(json!(["docs", {"dependencies": {"is-odd": "3.0.1"}}]), workspace_pkg() ; "workspace package")]
198+
#[test_case(json!(["some-package@root:", {"bin": "bin", "binDir": "binDir"}]), root_pkg() ; "root package")]
149199
fn test_deserialization<T: for<'a> Deserialize<'a> + PartialEq + std::fmt::Debug>(
150200
input: serde_json::Value,
151201
expected: &T,

0 commit comments

Comments
 (0)