@@ -36,8 +36,8 @@ use crate::patch::apply::PatchSources;
3636use crate :: patch:: copy_tree:: remove_tree;
3737use crate :: utils:: fs:: atomic_write_bytes_preserving_mode;
3838use crate :: vendor:: bun_lock_text:: {
39- check_lock_version, decode_json_string, packages_bounds , parse_entry_line ,
40- parse_packages_section, split_name_spec, BunEntry ,
39+ check_lock_version, decode_json_string, has_workspace_packages , lock_version , packages_bounds ,
40+ parse_entry_line , parse_packages_section, split_name_spec, BunEntry ,
4141} ;
4242
4343use super :: common:: { already_patched_result, refused} ;
@@ -56,6 +56,47 @@ const BUN_LOCK: &str = "bun.lock";
5656/// original/new = the verbatim entry LINE.
5757const KIND_LOCK_PACKAGE : & str = "bun_lock_package" ;
5858
59+ fn check_workspace_compatibility (
60+ text : & str ,
61+ entries : & [ BunEntry ] ,
62+ ) -> Result < ( ) , ( & ' static str , String ) > {
63+ if lock_version ( text) != Some ( 2 ) && has_workspace_packages ( entries) {
64+ return Err ( (
65+ "vendor_bun_workspace_unsupported" ,
66+ "Bun text locks before version 2 resolve workspace tarballs relative to the \
67+ workspace rather than the lockfile; upgrade to Bun >= 1.4 and run `bun install` \
68+ before vendoring workspace dependencies"
69+ . to_string ( ) ,
70+ ) ) ;
71+ }
72+ Ok ( ( ) )
73+ }
74+
75+ /// Refuse incompatible Bun projects before downloading records into the manifest.
76+ /// Other package managers are left to their own backends.
77+ pub async fn preflight_vendor ( project_root : & Path ) -> Result < ( ) , ( & ' static str , String ) > {
78+ let path = project_root. join ( BUN_LOCK ) ;
79+ let text = match tokio:: fs:: read_to_string ( & path) . await {
80+ Ok ( text) => text,
81+ Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => {
82+ if project_root. join ( "bun.lockb" ) . exists ( ) {
83+ return Err ( (
84+ "vendor_bun_lockb_unsupported" ,
85+ "Bun binary lockfiles cannot be vendored; upgrade Bun and generate bun.lock"
86+ . to_string ( ) ,
87+ ) ) ;
88+ }
89+ return Ok ( ( ) ) ;
90+ }
91+ Err ( error) => return Err ( ( "vendor_lockfile_missing" , error. to_string ( ) ) ) ,
92+ } ;
93+ check_lock_version ( & text) . map_err ( |detail| ( "vendor_lockfile_version_unsupported" , detail) ) ?;
94+ let lines = text. split ( '\n' ) . map ( str:: to_string) . collect :: < Vec < _ > > ( ) ;
95+ let entries = parse_packages_section ( & lines)
96+ . map_err ( |detail| ( "vendor_lockfile_version_unsupported" , detail) ) ?;
97+ check_workspace_compatibility ( & text, & entries)
98+ }
99+
59100/// Vendor one installed npm package into a bun project (see the module doc).
60101/// Same contract as `npm_lock::vendor_npm`: refuse-early / wire-last,
61102/// `entry` present iff `result.success` and not a dry run, and an in-sync
@@ -107,6 +148,10 @@ pub(crate) async fn vendor_bun(
107148 }
108149 } ;
109150
151+ if let Err ( ( code, detail) ) = check_workspace_compatibility ( & lock_text, & entries) {
152+ return refused ( code, detail) ;
153+ }
154+
110155 // ── 3. Pre-flight: at least one rewritable instance ──────────────────
111156 let target_spec = format ! ( "{name}@{version}" ) ;
112157 let target_leaf = tgz_rel_leaf ( name, version) ;
@@ -1226,6 +1271,66 @@ mod tests {
12261271 ) ;
12271272 }
12281273
1274+ #[ tokio:: test]
1275+ async fn legacy_workspace_tarballs_refuse_before_writes ( ) {
1276+ for version in [ 0 , 1 , 2 ] {
1277+ let lock = BN3_BEFORE_LOCK
1278+ . replace ( "\" lockfileVersion\" : 1" , & format ! ( "\" lockfileVersion\" : {version}" ) )
1279+ . replace ( " \" packages\" : {" , " \" packages\" : {\n \" consumer\" : [\" consumer@workspace:packages/consumer\" ]," ) ;
1280+ let fx = fixture_with ( & lock, "node_modules/left-pad" ) . await ;
1281+ if version < 2 {
1282+ assert_eq ! (
1283+ preflight_vendor( fx. root( ) ) . await . unwrap_err( ) . 0 ,
1284+ "vendor_bun_workspace_unsupported"
1285+ ) ;
1286+ expect_refused ( fx. vendor ( false ) . await , "vendor_bun_workspace_unsupported" ) ;
1287+ assert_eq ! ( fx. read_lock( ) . await , lock) ;
1288+ assert ! ( !fx. root( ) . join( ".socket/vendor" ) . exists( ) ) ;
1289+ } else {
1290+ assert ! ( preflight_vendor( fx. root( ) ) . await . is_ok( ) ) ;
1291+ let ( _, entry, _) = expect_done ( fx. vendor ( false ) . await ) ;
1292+ assert ! ( entry. is_some( ) ) ;
1293+ }
1294+ }
1295+ }
1296+
1297+ #[ tokio:: test]
1298+ async fn lock_v0_vendor_and_revert_preserve_bytes ( ) {
1299+ let lock = BN3_BEFORE_LOCK . replace ( "\" lockfileVersion\" : 1" , "\" lockfileVersion\" : 0" ) ;
1300+ let fx = fixture_with ( & lock, "node_modules/left-pad" ) . await ;
1301+ assert ! ( preflight_vendor( fx. root( ) ) . await . is_ok( ) ) ;
1302+ let ( _, entry, _) = expect_done ( fx. vendor ( false ) . await ) ;
1303+ assert ! ( fx. read_lock( ) . await . contains( ".socket/vendor/npm/" ) ) ;
1304+ let entry = entry. unwrap ( ) ;
1305+ let result = revert_bun ( & entry, fx. root ( ) , false ) . await ;
1306+ assert ! ( result. success) ;
1307+ assert_eq ! ( fx. read_lock( ) . await , lock) ;
1308+ }
1309+
1310+ #[ tokio:: test]
1311+ async fn download_preflight_refuses_binary_and_malformed_bun_locks ( ) {
1312+ let root = tempfile:: tempdir ( ) . unwrap ( ) ;
1313+ assert ! ( preflight_vendor( root. path( ) ) . await . is_ok( ) ) ;
1314+ tokio:: fs:: write ( root. path ( ) . join ( "bun.lockb" ) , b"binary" )
1315+ . await
1316+ . unwrap ( ) ;
1317+ assert_eq ! (
1318+ preflight_vendor( root. path( ) ) . await . unwrap_err( ) . 0 ,
1319+ "vendor_bun_lockb_unsupported"
1320+ ) ;
1321+ tokio:: fs:: write ( root. path ( ) . join ( BUN_LOCK ) , BN3_BEFORE_LOCK )
1322+ . await
1323+ . unwrap ( ) ;
1324+ assert ! ( preflight_vendor( root. path( ) ) . await . is_ok( ) ) ;
1325+ tokio:: fs:: write ( root. path ( ) . join ( BUN_LOCK ) , "{}" )
1326+ . await
1327+ . unwrap ( ) ;
1328+ assert_eq ! (
1329+ preflight_vendor( root. path( ) ) . await . unwrap_err( ) . 0 ,
1330+ "vendor_lockfile_version_unsupported"
1331+ ) ;
1332+ }
1333+
12291334 /// Build a scoped-package fixture and vendor it once (not dry).
12301335 async fn scoped_fixture ( ) -> Fixture {
12311336 let fx = fixture_with ( SCOPED_BEFORE_LOCK , "node_modules/@scope/pkg" ) . await ;
0 commit comments