diff --git a/packages/bun-plugin-svelte/bun.lock b/packages/bun-plugin-svelte/bun.lock index 75ce55975526..9167e93e7d21 100644 --- a/packages/bun-plugin-svelte/bun.lock +++ b/packages/bun-plugin-svelte/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "bun-plugin-svelte", @@ -28,14 +29,12 @@ "@threlte/core": ["@threlte/core@8.0.1", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.155" } }, "sha512-vy1xRQppJFNmfPTeiRQue+KmYFsbPgVhwuYXRTvVrwPeD2oYz43gxUeOpe1FACeGKxrxZykeKJF5ebVvl7gBxw=="], - "@types/bun": ["bun-types@file:../bun-types", { "dependencies": { "@types/node": "*" }, "devDependencies": { "@types/react": "^19" }, "peerDependencies": { "@types/react": "^19" } }], + "@types/bun": ["bun-types@file:../bun-types", { "dependencies": { "@types/node": "*" } }], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], - "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], - "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-typescript": ["acorn-typescript@1.4.13", "", { "peerDependencies": { "acorn": ">=8.9.0" } }, "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q=="], @@ -46,8 +45,6 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], "esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="], diff --git a/src/install/PackageManager/install_with_manager.rs b/src/install/PackageManager/install_with_manager.rs index 89da786c5647..1879bccad432 100644 --- a/src/install/PackageManager/install_with_manager.rs +++ b/src/install/PackageManager/install_with_manager.rs @@ -1424,6 +1424,8 @@ fn frozen_changed_section( Some(overrides_field_name(manager, root_package_json_path)) } else if manager.summary.catalogs_changed { Some("the catalog") + } else if manager.summary.bins_changed { + Some("the \"bin\" field of a file: dependency") } else { None } diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index d4c6e5770873..2f450060c6f6 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -916,6 +916,8 @@ pub struct DiffSummary { /// A workspace's `version` changed. No edge changed with it (those count as updates), but the /// lockfile records the version, so it is rewritten. pub(crate) workspace_versions_changed: bool, + /// A `file:` package's `bin` changed in place (`Lockfile::eql` cannot see it); fails `--frozen-lockfile`. + pub(crate) bins_changed: bool, pub(crate) pruned_workspaces: Vec, } @@ -978,6 +980,73 @@ impl Diff { ) } + /// Diffs the package.json of the `file:` directory a root or workspace dependency is locked to, like a workspace. + fn generate_folder_dependency( + pm: &mut PackageManager, + log: &mut bun_ast::Log, + from_lockfile: &mut Lockfile, + to_lockfile: &mut Lockfile, + from: &Package, + from_package_id: PackageID, + update_requests: Option<&[UpdateRequest]>, + removed_names: &mut Vec, + ) -> crate::Result> { + if !matches!( + from.resolution.tag, + ResolutionTag::Root | ResolutionTag::Workspace + ) || from_package_id as usize >= from_lockfile.packages.len() + { + return Ok(None); + } + let resolution = from_lockfile.packages.items_resolution()[from_package_id as usize]; + if resolution.tag != ResolutionTag::Folder { + return Ok(None); + } + // Relative to the top level directory, or absolute. + let folder_path: Box<[u8]> = Box::from( + resolution + .folder() + .slice(from_lockfile.buffers.string_bytes.as_slice()), + ); + + let folder_pkg = match crate::_folder_resolver::parse_folder_dependency_package_json( + to_lockfile, + pm, + log, + &folder_path, + ) { + Ok(pkg) => pkg, + // Not on disk (or unreadable): nothing to compare, the locked entry stands like any other. + Err(crate::Error::Sys(_)) => return Ok(None), + Err(err) => return Err(err), + }; + + let from_pkg = from_lockfile.packages.get(from_package_id as usize); + let diff = Self::generate_inner( + pm, + log, + from_lockfile, + to_lockfile, + &from_pkg, + &folder_pkg, + update_requests, + None, + removed_names, + )?; + + if pm.options.log_level.is_verbose() && (diff.add + diff.remove + diff.update) > 0 { + bun_core::pretty_errorln!( + "Package \"file:{}\" has added {} dependencies, removed {} dependencies, and updated {} dependencies", + bstr::BStr::new(&folder_path), + diff.add, + diff.remove, + diff.update, + ); + } + + Ok(Some(diff)) + } + // The root summary's `remove` is the count of distinct names removed across root + workspaces. fn generate_inner( pm: &mut PackageManager, @@ -1002,13 +1071,7 @@ impl Diff { ), _ => true, }; - // `parseWithJSON` may grow `to_lockfile.buffers.dependencies` and - // invalidate the old slice, so `to_deps` is re-derived after it. Held as raw fat - // pointers so the `&mut to_lockfile`/`&mut from_lockfile` reborrows below - // (sort, recursive `generate`) don't conflict with these read views; the - // recursive call only sorts `overrides`/`catalogs` and never reallocates - // either lockfile's `buffers.dependencies`/`resolutions`, so the raw - // pointers remain valid for the loop body. + // Raw fat pointer so the `&mut to_lockfile` reborrows below don't conflict; re-derived after every parse. let mut to_deps: bun_ptr::RawSlice = to .dependencies .get(to_lockfile.buffers.dependencies.as_slice()) @@ -1026,8 +1089,7 @@ impl Diff { .resolutions .get(from_lockfile.buffers.resolutions.as_slice()) .into(); - // See note above — `from_lockfile.buffers` is not reallocated for - // the lifetime of these references. + // Nothing below grows `from_lockfile.buffers`, so these stay valid for the whole loop. let (from_deps, from_resolutions) = (from_deps.slice(), from_resolutions.slice()); let mut to_i: usize = 0; @@ -1461,9 +1523,33 @@ impl Diff { } } + let folder_diff = Self::generate_folder_dependency( + pm, + log, + from_lockfile, + to_lockfile, + from, + from_resolutions[i], + update_requests, + removed_names, + )?; + if let Some(diff) = &folder_diff { + // re-derive the slice, `to_lockfile.buffers.dependencies` may have grown. + to_deps = to + .dependencies + .get(to_lockfile.buffers.dependencies.as_slice()) + .into(); + summary.bins_changed |= diff.bins_changed; + } + if let Some(mapping) = id_mapping.as_deref_mut() { let mut workspace_hooks_only = false; let update_mapping = 'update_mapping: { + if let Some(diff) = &folder_diff { + workspace_hooks_only = !diff.changes_dependencies(); + break 'update_mapping !diff.changes_resolutions(); + } + if !is_root || !from_dep.behavior.is_workspace() { break 'update_mapping true; } @@ -1522,12 +1608,6 @@ impl Diff { Features::WORKSPACE, )?; - // `parse_with_json` may have grown `to_lockfile.buffers - // .dependencies` — re-derive the slice. - to_deps = to - .dependencies - .get(to_lockfile.buffers.dependencies.as_slice()) - .into(); survivors.push((workspace_pkg.name, workspace_pkg.dependencies)); let from_pkg = from_lockfile.packages.get(from_resolutions[i] as usize); @@ -1542,6 +1622,11 @@ impl Diff { None, removed_names, )?; + // Both calls above may have grown `to_lockfile.buffers.dependencies`. + to_deps = to + .dependencies + .get(to_lockfile.buffers.dependencies.as_slice()) + .into(); if pm.options.log_level.is_verbose() && (diff.add + diff.remove + diff.update) > 0 @@ -1558,6 +1643,7 @@ impl Diff { ); } + summary.bins_changed |= diff.bins_changed; workspace_hooks_only = !diff.changes_dependencies(); !diff.changes_resolutions() }; @@ -1570,7 +1656,14 @@ impl Diff { summary.script_only_updates += 1; } } else { - continue; + match &folder_diff { + Some(diff) if diff.changes_resolutions() => { + if !diff.changes_dependencies() { + summary.script_only_updates += 1; + } + } + _ => continue, + } } } @@ -1662,7 +1755,13 @@ impl Diff { ); } - if from.resolution.tag != ResolutionTag::Root { + // bun.lock records no scripts; a `file:` package's are read from its package.json at install. + let compare_scripts = match from.resolution.tag { + ResolutionTag::Root => false, + ResolutionTag::Folder => from.scripts.filled, + _ => true, + }; + if compare_scripts { for (to_hook, from_hook) in to.scripts.hooks().iter().zip(from.scripts.hooks().iter()) { if !String::eql( **to_hook, @@ -1677,6 +1776,21 @@ impl Diff { } } + // Not workspaces: `turbo prune` writes bun.lock without their `bin` and must pass --frozen-lockfile. + if from.resolution.tag == ResolutionTag::Folder + && !Bin::eql( + &to.bin, + &from.bin, + to_lockfile.buffers.string_bytes.as_slice(), + to_lockfile.buffers.extern_strings.as_slice(), + from_lockfile.buffers.string_bytes.as_slice(), + from_lockfile.buffers.extern_strings.as_slice(), + ) + { + summary.update += 1; + summary.bins_changed = true; + } + Ok(summary) } } diff --git a/src/install/resolvers/folder_resolver.rs b/src/install/resolvers/folder_resolver.rs index e7593b61e31c..9fa133f74fa7 100644 --- a/src/install/resolvers/folder_resolver.rs +++ b/src/install/resolvers/folder_resolver.rs @@ -257,17 +257,90 @@ fn normalize_package_json_path<'a>( } } -fn read_package_json_from_disk( +/// `normalize_package_json_path` with posix separators on Windows; `rel` is copied into `rel_buf`. +fn package_json_paths<'a>( + global_or_relative: GlobalOrRelative<'_>, + joined: &'a mut PathBuffer, + rel_buf: &'a mut PathBuffer, + non_normalized_path: &[u8], +) -> (&'a ZStr, &'a [u8]) { + let paths = normalize_package_json_path(global_or_relative, joined, non_normalized_path); + // Take the lengths, drop the read-only borrows, then mutate `joined` and our own copy of the thread-local `rel`. + let abs_len = paths.abs.len(); + let rel_len = paths.rel.len(); + rel_buf[..rel_len].copy_from_slice(paths.rel); + #[cfg(windows)] + { + bun_paths::dangerously_convert_path_to_posix_in_place::(&mut joined[..abs_len]); + bun_paths::dangerously_convert_path_to_posix_in_place::(&mut rel_buf[..rel_len]); + } + // `joined[abs_len]` is still the NUL that `normalize_package_json_path` wrote. + (ZStr::from_buf(&joined[..], abs_len), &rel_buf[..rel_len]) +} + +/// Reads and parses the package.json at `abs` into `lockfile` without appending the package. +fn parse_package_json_file( + lockfile: &mut Lockfile, manager: &mut PackageManager, + log: &mut bun_ast::Log, abs: &ZStr, - version: &dependency::Version, - features: Features, resolver: &mut R, + features: Features, ) -> crate::Result { let mut body = npm::Registry::BodyPool::get(); + let source = { + let file = File::openat(Fd::cwd(), abs.as_bytes(), O::RDONLY, 0)?; + body.reset(); + let read_result = file + .read_to_end_with_array_list(&mut body.list, bun_sys::SizeHint::ProbablySmall) + .map(|_| ()); + let _ = file.close(); + read_result?; + + bun_ast::Source::init_path_string(abs.as_bytes(), body.list.as_slice()) + }; - let mut package: LockfilePackage = Default::default(); + let mut package = LockfilePackage::default(); + package.parse::(lockfile, manager, log, &source, resolver, features)?; + Ok(package) +} + +/// Parses the package.json of the `file:` directory at `folder_path` into `lockfile` like `get_or_put`, appending nothing. +pub(crate) fn parse_folder_dependency_package_json( + lockfile: &mut Lockfile, + manager: &mut PackageManager, + log: &mut bun_ast::Log, + folder_path: &[u8], +) -> crate::Result { + let mut abs_buf = bun_paths::path_buffer_pool::get(); + let non_normalized_path: &[u8] = if bun_paths::is_absolute(folder_path) { + folder_path + } else { + bun_paths::resolve_path::join_abs_string_buf::( + FileSystem::instance().top_level_dir(), + &mut abs_buf, + &[folder_path], + ) + }; + let mut joined = bun_paths::path_buffer_pool::get(); + let mut rel_buf = bun_paths::path_buffer_pool::get(); + let (abs, rel) = package_json_paths( + GlobalOrRelative::Relative(dependency::version::Tag::Folder), + &mut joined, + &mut rel_buf, + non_normalized_path, + ); + let mut resolver: Resolver = NewResolver { folder_path: rel }; + parse_package_json_file(lockfile, manager, log, abs, &mut resolver, Features::FOLDER) +} +fn read_package_json_from_disk( + manager: &mut PackageManager, + abs: &ZStr, + version: &dependency::Version, + features: Features, + resolver: &mut R, +) -> crate::Result { // Borrow splitting: `manager.lockfile`, `manager`, and `manager.log` are // needed simultaneously; borrowck rejects the overlap on `&mut self`, // so split via raw pointer once here. `lockfile` and `log` are disjoint @@ -283,7 +356,7 @@ fn read_package_json_from_disk( let log: &mut bun_ast::Log = manager.log_mut(); let manager_ptr: *mut PackageManager = manager; - if R::IS_WORKSPACE { + let mut package: LockfilePackage = if R::IS_WORKSPACE { let _tracer = bun_perf::trace(bun_perf::PerfEvent::FolderResolverReadPackageJSONFromDiskWorkspace); @@ -301,6 +374,7 @@ fn read_package_json_from_disk( let root: Expr = json.root; let source: *const bun_ast::Source = &raw const json.source; + let mut package = LockfilePackage::default(); // SAFETY: see the borrow-splitting comment above. unsafe { let lockfile: *mut Lockfile = &raw mut *(*manager_ptr).lockfile; @@ -314,35 +388,24 @@ fn read_package_json_from_disk( features, )?; } + package } else { let _tracer = bun_perf::trace(bun_perf::PerfEvent::FolderResolverReadPackageJSONFromDiskFolder); - let source = { - let file = File::openat(Fd::cwd(), abs.as_bytes(), O::RDONLY, 0)?; - body.reset(); - let read_result = file - .read_to_end_with_array_list(&mut body.list, bun_sys::SizeHint::ProbablySmall) - .map(|_| ()); - let _ = file.close(); - read_result?; - - bun_ast::Source::init_path_string(abs.as_bytes(), body.list.as_slice()) - }; - // SAFETY: see the borrow-splitting comment above. unsafe { let lockfile: *mut Lockfile = &raw mut *(*manager_ptr).lockfile; - package.parse::( + parse_package_json_file::( &mut *lockfile, &mut *manager_ptr, log, - &source, + abs, resolver, features, - )?; + )? } - } + }; let has_scripts = package.scripts.has_any() || 'brk: { @@ -382,43 +445,14 @@ pub(crate) fn get_or_put( manager: &mut PackageManager, ) -> FolderResolution { let mut joined = bun_paths::path_buffer_pool::get(); - #[cfg(windows)] let mut rel_buf = bun_paths::path_buffer_pool::get(); - let paths = normalize_package_json_path(global_or_relative, &mut joined, non_normalized_path); - - #[cfg(not(windows))] - let abs = paths.abs; - #[cfg(not(windows))] - let rel = paths.rel; - - // replace before getting hash. rel may or may not be contained in abs - #[cfg(windows)] - let (abs, rel): (&ZStr, &[u8]) = { - // Writing in place through `(&ZStr).as_ptr().cast_mut()` / - // `(&[u8]).as_ptr().cast_mut()` would be UB under Stacked/Tree - // Borrows: those pointers carry read-only provenance, and the - // optimizer may assume `abs`'s bytes are unchanged when computing - // `hash(abs.as_bytes())` below. - // - // Instead: capture lengths, let the shared borrows of `joined` die, - // then take a fresh `&mut joined[..abs_len]` (write provenance) and - // mutate that. `rel` points into FileSystem's thread-local relative - // buffer which we only ever see as `&[u8]`, so copy it into a local - // we own and convert the copy — same pattern as - // WorkspacePackageJSONCache::get_with_path. - let abs_len = paths.abs.len(); - let rel_len = paths.rel.len(); - rel_buf[..rel_len].copy_from_slice(paths.rel); - // `paths` is dead past this point → `joined` is no longer borrowed. - bun_paths::dangerously_convert_path_to_posix_in_place::(&mut joined[..abs_len]); - bun_paths::dangerously_convert_path_to_posix_in_place::(&mut rel_buf[..rel_len]); - ( - // `normalize_package_json_path` wrote `joined[abs_len] = 0`; the - // separator rewrite above never touches the NUL. - ZStr::from_buf(&joined[..], abs_len), - &rel_buf[..rel_len], - ) - }; + // Separators are replaced before hashing. rel may or may not be contained in abs + let (abs, rel) = package_json_paths( + global_or_relative, + &mut joined, + &mut rel_buf, + non_normalized_path, + ); let abs_hash = hash(abs.as_bytes()); // Check first, compute, then insert, because read_package_json_from_disk diff --git a/test/bun.lock b/test/bun.lock index 0dcc5c65ac03..e70b5353817c 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -123,6 +123,7 @@ }, }, "overrides": { + "@types/bun": "../packages/bun-types", "@types/node": "26.2.0", "react": "../node_modules/react", }, @@ -671,6 +672,8 @@ "@testing-library/react": ["@testing-library/react@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg=="], + "@threlte/core": ["@threlte/core@8.0.1", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "svelte": ">=5", "three": ">=0.155" } }, "sha512-vy1xRQppJFNmfPTeiRQue+KmYFsbPgVhwuYXRTvVrwPeD2oYz43gxUeOpe1FACeGKxrxZykeKJF5ebVvl7gBxw=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -989,12 +992,10 @@ "buffer-writer": ["buffer-writer@2.0.0", "", {}, "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="], - "bun-plugin-svelte": ["bun-plugin-svelte@file:../packages/bun-plugin-svelte", { "dependencies": { "svelte": "^5.20.4", "svelte-hmr": "^0.16.0" }, "devDependencies": { "bun-types": "canary" }, "peerDependencies": { "typescript": "^5" } }], + "bun-plugin-svelte": ["bun-plugin-svelte@file:../packages/bun-plugin-svelte", { "devDependencies": { "@threlte/core": "8.0.1", "@types/bun": "../bun-types", "svelte": "^5.20.4" }, "peerDependencies": { "svelte": "^5" } }], "bun-plugin-yaml": ["bun-plugin-yaml@0.0.1", "", { "dependencies": { "js-yaml": "^4.1.0" } }, "sha512-dAqe0eJu+SGtrwp75hrtDHWRnmMUdBJK365PPeM0skwFqWu6FYouVzaluG2FVVgs0NsKd+sXiWyGqvs0cilrkA=="], - "bun-types": ["bun-types@1.2.4-canary.20250226T140704", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P8b2CGLtbvi/kQ4dPHBhU5qkguIjHMYCjNqjWDTKSnodWDTbcv9reBdktZJ7m5SF4m15JLthfFq2PtwKpA9a+w=="], - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1789,6 +1790,8 @@ "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + "mkdirp": ["mkdirp@2.1.6", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A=="], "mongodb": ["mongodb@6.0.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.0", "bson": "^6.0.0", "mongodb-connection-string-url": "^2.6.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-wUIYesF4DTyDccm0noE5TwGi9ISdXUAi9T2cQ4xPc+EUBZG44bfMVt2ecOG5Ypca7eCz3oRpJm6YI6c7jAnuNw=="], @@ -2303,8 +2306,6 @@ "svelte": ["svelte@5.20.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2Mo/AfObaw9zuD0u1JJ7sOVzRCGcpETEyDkLbtkcctWpCMCIyT0iz83xD8JT29SR7O4SgswuPRIDYReYF/607A=="], - "svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="], - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "table": ["table@6.8.2", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA=="], @@ -2327,6 +2328,8 @@ "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + "three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="], + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], @@ -2629,8 +2632,6 @@ "@remix-run/react/@remix-run/server-runtime": ["@remix-run/server-runtime@2.10.3", "", { "dependencies": { "@remix-run/router": "1.18.0", "@types/cookie": "^0.6.0", "@web3-storage/multipart-parser": "^1.0.0", "cookie": "^0.6.0", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3", "turbo-stream": "2.2.0" }, "peerDependencies": { "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-vUl5jONUI6Lj0ICg9FSRFhoPzQdZ/7dpT1m7ID13DF5BEeF3t/9uCJS61XXWgQ/JEu7YRiwvZiwSRTrgM7zeWw=="], - "@remix-run/react/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - "@remix-run/serve/@remix-run/node": ["@remix-run/node@2.10.3", "", { "dependencies": { "@remix-run/server-runtime": "2.10.3", "@remix-run/web-fetch": "^4.4.2", "@web3-storage/multipart-parser": "^1.0.0", "cookie-signature": "^1.1.0", "source-map-support": "^0.5.21", "stream-slice": "^0.1.2", "undici": "^6.11.1" }, "peerDependencies": { "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-LBqsgADJKW7tYdJZZi2wu20gfMm6UcOXbvb5U70P2jCNxjJvuIw1gXVvNXRJKAdxPKLonjm8cSpfoI6HeQKEDg=="], "@remix-run/serve/express": ["express@4.21.0", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng=="], @@ -2651,8 +2652,6 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@testing-library/react/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - "@verdaccio/auth/@verdaccio/config": ["@verdaccio/config@8.0.0-next-8.1", "", { "dependencies": { "@verdaccio/core": "8.0.0-next-8.1", "@verdaccio/utils": "7.0.1-next-8.1", "debug": "4.3.7", "js-yaml": "4.1.0", "lodash": "4.17.21", "minimatch": "7.4.6" } }, "sha512-goDVOH4e8xMUxjHybJpi5HwIecVFqzJ9jeNFrRUgtUUn0PtFuNMHgxOeqDKRVboZhc5HK90yed8URK/1O6VsUw=="], "@verdaccio/auth/@verdaccio/core": ["@verdaccio/core@8.0.0-next-8.1", "", { "dependencies": { "ajv": "8.17.1", "core-js": "3.37.1", "http-errors": "2.0.0", "http-status-codes": "2.3.0", "process-warning": "1.0.0", "semver": "7.6.3" } }, "sha512-kQRCB2wgXEh8H88G51eQgAFK9IxmnBtkQ8sY5FbmB6PbBkyHrbGcCp+2mtRqqo36j0W1VAlfM3XzoknMy6qQnw=="], @@ -2747,6 +2746,8 @@ "browserify-zlib/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "bun-plugin-svelte/@types/bun": ["@types/bun@file:../packages/bun-types", {}], + "cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -2909,12 +2910,6 @@ "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "react-dom/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - - "react-router/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - - "react-router-dom/react": ["react@file:../node_modules/react", { "dependencies": { "loose-envify": "^1.1.0" } }], - "readable-web-to-node-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap index 0f74856c14a5..dbc9e81d38af 100644 --- a/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install-registry.test.ts.snap @@ -352,3 +352,22 @@ what-bin@1.0.0: integrity sha512-sa99On1k5aDqCvpni/TQ6rLzYprUWBlb8fNwWOzbjDlM24fRr7FKDOuaBO/Y9WEIcZuzoPkCW5EkBCpflj8REQ== " `; + +exports[`binaries text lockfile updates with new bin entry for folder dependencies 1`] = ` +"{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "change-bin": "./change-bin", + }, + }, + }, + "packages": { + "change-bin": ["change-bin@file:change-bin", { "bin": { "change-bin-1": "./change-bin-1.js", "change-bin-2": "./change-bin-2.js" } }], + } +} +" +`; diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index dc76901d5a69..9821504f763d 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -2989,7 +2989,7 @@ describe("binaries", () => { expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); }); - test.todo("text lockfile updates with new bin entry for folder dependencies", async () => { + test("text lockfile updates with new bin entry for folder dependencies", async () => { await Promise.all([ write( packageJson, diff --git a/test/cli/install/bun-lock.test.ts b/test/cli/install/bun-lock.test.ts index c8ba27d17e5f..c2378e870139 100644 --- a/test/cli/install/bun-lock.test.ts +++ b/test/cli/install/bun-lock.test.ts @@ -13,7 +13,7 @@ import { toBeValidBin, VerdaccioRegistry, } from "harness"; -import { join } from "path"; +import { delimiter, join } from "path"; expect.extend({ toBeValidBin, @@ -1826,3 +1826,249 @@ describe.each(["hoisted", "isolated"] as const)("peer no published version satis expect(await file(lockfilePath).text()).toBe(lockfile); }); }); + +// The dependency lists and `bin` of a `file:` directory dependency are recorded in bun.lock when +// it is first resolved. Later edits to that directory's package.json must be diffed against the +// lockfile entry on every install, as they are for workspace packages. +describe.each(["hoisted", "isolated"] as const)( + "file: directory dependency edited after bun.lock was written (%s linker)", + linker => { + const vdir = (extra: Record) => JSON.stringify({ name: "vdir", version: "1.0.0", ...extra }); + // Reports which version of each package `vdir` resolves from wherever the linker placed it. + const vdirIndexJs = `module.exports = Object.fromEntries(["no-deps", "a-dep", "no-deps-bins", "basic-1"].map(name => { + try { + return [name, require(name + "/package.json").version]; + } catch { + return [name, null]; + } + }));`; + + async function createProject( + declaredBy: "root" | "workspace" | "override", + vdirExtra: Record = {}, + ) { + const { packageDir, packageJson } = await registry.createTestDir({ + bunfigOpts: { saveTextLockfile: true, linker }, + files: { + "vendor/vdir/package.json": vdir({ dependencies: { "no-deps": "1.0.0" }, ...vdirExtra }), + "vendor/vdir/index.js": vdirIndexJs, + ...(declaredBy === "workspace" + ? { + "packages/app/package.json": JSON.stringify({ + name: "app", + dependencies: { vdir: "file:../../vendor/vdir" }, + }), + } + : {}), + }, + }); + await write( + packageJson, + JSON.stringify( + { + root: { name: "root", dependencies: { vdir: "file:./vendor/vdir" } }, + // The root diff visits workspaces first; its other dependencies are compared after the + // workspace (and the file: package inside it) were parsed into the same buffers. + workspace: { + name: "root", + workspaces: ["packages/*"], + dependencies: { "basic-1": "1.0.0" }, + devDependencies: { "no-deps-bins": "1.0.0" }, + }, + override: { name: "root", dependencies: { vdir: "1.0.0" }, overrides: { vdir: "file:./vendor/vdir" } }, + }[declaredBy], + ), + ); + const lock = () => file(join(packageDir, "bun.lock")).text(); + const bun = async (...args: string[]) => { + await using proc = spawn({ + cmd: [bunExe(), ...args], + cwd: packageDir, + // A cache per project: the environment's cache dir takes precedence over bunfig. + env: { ...env, BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache") }, + stdout: "pipe", + stderr: "pipe", + }); + const [out, err, code] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { out, err, code }; + }; + const project = { + packageDir, + bun, + lock, + editVdir: (extra: Record) => + write(join(packageDir, "vendor", "vdir", "package.json"), vdir(extra)), + async expectInstallToSaveLockfile() { + const { out, err, code } = await bun("install"); + expect({ out, err, code }).toMatchObject({ code: 0 }); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + }, + async expectFrozenLockfileToFail() { + const { out, err, code } = await bun("install", "--frozen-lockfile"); + expect({ out, err, code }).toMatchObject({ code: 1 }); + expect(err).toContain("error: lockfile had changes, but lockfile is frozen"); + }, + async expectInstallToBeANoop() { + const before = await lock(); + for (const args of [["install", "--frozen-lockfile"], ["install"]]) { + const { out, err, code } = await bun(...args); + expect({ out, err, code }).toMatchObject({ code: 0 }); + expect(err).not.toContain("error:"); + expect(err).not.toContain("Saved lockfile"); + } + expect(await lock()).toBe(before); + }, + async versionsResolvedByVdir() { + const { out, err, code } = await bun("-p", `JSON.stringify(require("vdir"))`); + expect({ out, err, code }).toMatchObject({ err: "", code: 0 }); + return JSON.parse(out); + }, + }; + + await project.expectInstallToSaveLockfile(); + expect(await lock()).toContain(`"vdir@file:vendor/vdir", { "dependencies": { "no-deps": "1.0.0" } }`); + return project; + } + + it.concurrent("an added dependency is installed", async () => { + const { + lock, + editVdir, + expectFrozenLockfileToFail, + expectInstallToSaveLockfile, + expectInstallToBeANoop, + versionsResolvedByVdir, + } = await createProject("root"); + expect(await versionsResolvedByVdir()).toEqual({ + "no-deps": "1.0.0", + "a-dep": null, + "no-deps-bins": null, + "basic-1": null, + }); + + await editVdir({ dependencies: { "no-deps": "1.0.0", "a-dep": "1.0.2" } }); + await expectFrozenLockfileToFail(); + await expectInstallToSaveLockfile(); + + expect(await lock()).toContain( + `"vdir@file:vendor/vdir", { "dependencies": { "a-dep": "1.0.2", "no-deps": "1.0.0" } }`, + ); + expect(await lock()).toContain(`"a-dep@1.0.2"`); + expect(await versionsResolvedByVdir()).toEqual({ + "no-deps": "1.0.0", + "a-dep": "1.0.2", + "no-deps-bins": null, + "basic-1": null, + }); + await expectInstallToBeANoop(); + }); + + it.concurrent("removed, re-ranged, optional and peer dependencies are picked up", async () => { + const { + lock, + editVdir, + expectFrozenLockfileToFail, + expectInstallToSaveLockfile, + expectInstallToBeANoop, + versionsResolvedByVdir, + } = await createProject("root"); + + // `no-deps` leaves `dependencies`, and the locked a-dep (none) cannot satisfy the new range + await editVdir({ + dependencies: { "a-dep": "^1.0.3" }, + optionalDependencies: { "no-deps-bins": "1.0.0" }, + peerDependencies: { "basic-1": "^1.0.0" }, + }); + await expectFrozenLockfileToFail(); + await expectInstallToSaveLockfile(); + + expect(await lock()).toContain( + `"vdir@file:vendor/vdir", { "dependencies": { "a-dep": "^1.0.3" }, "optionalDependencies": { "no-deps-bins": "1.0.0" }, "peerDependencies": { "basic-1": "^1.0.0" } }`, + ); + expect(await lock()).not.toContain(`"no-deps@`); + expect(await versionsResolvedByVdir()).toMatchObject({ + "a-dep": "1.0.10", + "no-deps-bins": "1.0.0", + "basic-1": "1.0.0", + }); + await expectInstallToBeANoop(); + }); + + it.concurrent("an added bin is recorded and linked", async () => { + const { + packageDir, + lock, + editVdir, + expectFrozenLockfileToFail, + expectInstallToSaveLockfile, + expectInstallToBeANoop, + } = await createProject("root"); + + await editVdir({ bin: { "vdir-cli": "index.js" }, dependencies: { "no-deps": "1.0.0" } }); + await expectFrozenLockfileToFail(); + await expectInstallToSaveLockfile(); + + expect(await lock()).toContain( + `"vdir@file:vendor/vdir", { "dependencies": { "no-deps": "1.0.0" }, "bin": { "vdir-cli": "index.js" } }`, + ); + expect(Bun.which("vdir-cli", { PATH: join(packageDir, "node_modules", ".bin") })).not.toBeNull(); + await expectInstallToBeANoop(); + }); + + it.concurrent("when declared by a workspace package", async () => { + const { + packageDir, + lock, + editVdir, + expectFrozenLockfileToFail, + expectInstallToSaveLockfile, + expectInstallToBeANoop, + } = await createProject("workspace"); + + await editVdir({ bin: "index.js", dependencies: { "no-deps": "1.0.0", "a-dep": "1.0.2" } }); + await expectFrozenLockfileToFail(); + await expectInstallToSaveLockfile(); + + expect(await lock()).toContain( + `"vdir@file:vendor/vdir", { "dependencies": { "a-dep": "1.0.2", "no-deps": "1.0.0" }, "bin": "index.js" }`, + ); + expect(await lock()).toContain(`"a-dep@1.0.2"`); + // the hoisted linker hoists vdir and its bin to the root, the isolated linker links them into the workspace + const binDirs = [ + join(packageDir, "node_modules", ".bin"), + join(packageDir, "packages", "app", "node_modules", ".bin"), + ]; + expect(Bun.which("vdir", { PATH: binDirs.join(delimiter) })).not.toBeNull(); + await expectInstallToBeANoop(); + }); + + it.concurrent("when an override routes a registry dependency to the directory", async () => { + const { lock, editVdir, expectFrozenLockfileToFail, expectInstallToSaveLockfile, expectInstallToBeANoop } = + await createProject("override"); + + await editVdir({ dependencies: { "no-deps": "1.0.0", "a-dep": "1.0.2" } }); + await expectFrozenLockfileToFail(); + await expectInstallToSaveLockfile(); + + expect(await lock()).toContain( + `"vdir@file:vendor/vdir", { "dependencies": { "a-dep": "1.0.2", "no-deps": "1.0.0" } }`, + ); + expect(await lock()).toContain(`"a-dep@1.0.2"`); + await expectInstallToBeANoop(); + }); + + // bun.lock does not record lifecycle scripts, so they must not count as a difference. + it.concurrent("an unchanged package.json with a lifecycle script is not resolved again", async () => { + const { bun, expectInstallToBeANoop } = await createProject("root", { + scripts: { postinstall: "echo postinstall" }, + }); + + await expectInstallToBeANoop(); + const { err, code } = await bun("install", "--verbose"); + expect(err).not.toContain(`"file:vendor/vdir" has added`); + expect(err).not.toContain("Saved lockfile"); + expect(code).toBe(0); + }); + }, +); diff --git a/test/cli/install/migration/__snapshots__/migrate.test.ts.snap b/test/cli/install/migration/__snapshots__/migrate.test.ts.snap index ba6aeab1da8c..fb5f72d8e3f3 100644 --- a/test/cli/install/migration/__snapshots__/migrate.test.ts.snap +++ b/test/cli/install/migration/__snapshots__/migrate.test.ts.snap @@ -3765,7 +3765,7 @@ bun install --frozen-lockfile exit code: 1 exports[`package-lock.json migration fixes arborist fixtures external-link-dep 1`] = ` "migrated lockfile from package-lock.json bun pm migrate exit code: 0 -bun install --frozen-lockfile exit code: 0 +bun install --frozen-lockfile exit code: 1 { "lockfileVersion": 2, @@ -3799,7 +3799,7 @@ warn: skipped "b" from package-lock.json: transitive folder dependency "../a/nod warn: skipped 3 package-lock.json entries not depended on by any package: "../a", "../i", "../m" migrated lockfile from package-lock.json bun pm migrate exit code: 0 -bun install --frozen-lockfile exit code: 0 +bun install --frozen-lockfile exit code: 1 { "lockfileVersion": 1, diff --git a/test/cli/install/migration/migrate.test.ts b/test/cli/install/migration/migrate.test.ts index 096021f8ecf3..03a0fcef82ec 100644 --- a/test/cli/install/migration/migrate.test.ts +++ b/test/cli/install/migration/migrate.test.ts @@ -1131,7 +1131,8 @@ describe("package-lock.json migration fixes", () => { }); test.concurrent("file: specs into a registry package's folder (B4)", async () => { - using dir = fixture("external-link-dep"); + // cli-750's package.json declares a folder dependency that its lockfile entry never had, which --frozen-lockfile rejects. + using dir = fixture("external-link-dep", { "../cli-750/package.json": JSON.stringify({ name: "monorepo" }) }); const { text, lock } = await migrate(dir); for (const key of ["aaaaaa", "abbrev", "zzzzzz"]) { expect(lock.packages[key][0]).toBe("abbrev@https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz"); @@ -1184,7 +1185,7 @@ describe("package-lock.json migration fixes", () => { expect(text).not.toContain("../m/node_modules/p"); expect(text).not.toContain("../a/node_modules/b"); expect(exitCode).toBe(0); - await frozen(dir); + await expectNextInstallToResolveSkippedDependency(dir, "p"); }); test.concurrent("optional peer that is present keeps optionalPeers (B8)", async () => { @@ -1590,35 +1591,39 @@ describe("package-lock.json migration fixes", () => { }); // Folder dep `o` gets `p` from the node_modules beside it; `m` sits next to the project (external-link--root shape) or inside it. - function folderLinkProject(name: string, opts: { outOfTree: boolean; directP?: boolean }) { + function folderLinkProject( + name: string, + opts: { outOfTree: boolean; directP?: boolean; p?: string; registry?: string }, + ) { const m = opts.outOfTree ? "../m" : "m"; const project = opts.outOfTree ? "root" : "."; + const p = opts.p ?? "p"; const dependencies: Record = { o: `file:${m}/node_modules/n/o` }; - if (opts.directP) dependencies.p = `file:${m}/node_modules/p`; + if (opts.directP) dependencies[p] = `file:${m}/node_modules/${p}`; const copy = tempDir(name, { [`${project}/${m}/node_modules/n/o/package.json`]: JSON.stringify({ name: "o", version: "1.0.0", - dependencies: { p: "" }, + dependencies: { [p]: "" }, }), - [`${project}/${m}/node_modules/n/o/index.js`]: `module.exports = require("p/package.json").name;`, - [`${project}/${m}/node_modules/p/package.json`]: JSON.stringify({ name: "p", version: "1.0.0" }), + [`${project}/${m}/node_modules/n/o/index.js`]: `module.exports = require("${p}/package.json").name;`, + [`${project}/${m}/node_modules/${p}/package.json`]: JSON.stringify({ name: p, version: "1.0.0" }), [`${project}/package.json`]: JSON.stringify({ name: "root", dependencies }), [`${project}/package-lock.json`]: npmLock("root", { "": { name: "root", dependencies }, [m]: {}, - [`${m}/node_modules/n/o`]: { version: "1.0.0", dependencies: { p: "" } }, - [`${m}/node_modules/p`]: {}, + [`${m}/node_modules/n/o`]: { version: "1.0.0", dependencies: { [p]: "" } }, + [`${m}/node_modules/${p}`]: {}, "node_modules/o": { resolved: `${m}/node_modules/n/o`, link: true }, - ...(opts.directP ? { "node_modules/p": { resolved: `${m}/node_modules/p`, link: true } } : {}), + ...(opts.directP ? { [`node_modules/${p}`]: { resolved: `${m}/node_modules/${p}`, link: true } } : {}), }), }); const dir = join(String(copy), project); - writeExtra(dir, {}); + writeExtra(dir, {}, opts.registry); return { dir, o: `o@file:${m}/node_modules/n/o`, - p: `p@file:${m}/node_modules/p`, + p: `${p}@file:${m}/node_modules/${p}`, oSource: join(dir, m, "node_modules", "n", "o"), [Symbol.dispose]() { copy[Symbol.dispose](); @@ -1626,6 +1631,14 @@ describe("package-lock.json migration fixes", () => { }; } + // The migration cannot keep an out-of-tree `p`, but `o`'s package.json still declares it, so the + // next install reads that package.json again and resolves `p` from the (offline) registry. + async function expectNextInstallToResolveSkippedDependency(dir: string, name: string) { + const { stderr, exitCode } = await run(dir, "install", "--frozen-lockfile", "--lockfile-only"); + expect(stderr).toContain(`downloading package manifest ${name}`); + expect(exitCode).toBe(1); + } + async function expectFolderLinkInstalled(project: ReturnType, linker: string) { const install = await run(project.dir, "install", "--frozen-lockfile", "--linker", linker); expect(install.stderr).not.toContain("error"); @@ -1654,7 +1667,7 @@ describe("package-lock.json migration fixes", () => { o: [project.o, {}], p: [project.p, {}], }); - await frozen(project.dir); + await expectNextInstallToResolveSkippedDependency(project.dir, "p"); }, ); @@ -1669,24 +1682,37 @@ describe("package-lock.json migration fixes", () => { await expectFolderLinkInstalled(project, linker); }); - // A transitive folder target outside the project is skipped with a warning, so both linkers install the same lockfile. + // A transitive folder target outside the project is skipped with a warning. `o`'s package.json still declares it, + // so the next install reads it again and resolves the dependency from the registry, with either linker. test.concurrent.each(linkers)("out-of-tree link target and its node_modules install (%s)", async linker => { - using project = folderLinkProject(`npm-migrate-folder-link-out-of-tree-${linker}`, { outOfTree: true }); + using registry = localRegistry(); + using project = folderLinkProject(`npm-migrate-folder-link-out-of-tree-${linker}`, { + outOfTree: true, + p: "no-deps", + registry: registry.url, + }); const { stderr, exitCode, text, lock } = await migrate(project.dir); - expect(stderr).toContain("../m/node_modules/p"); + expect(stderr).toContain("../m/node_modules/no-deps"); expect(stderr).toContain("outside"); expect(exitCode).toBe(0); expect(Object.keys(lock.packages)).toStrictEqual(["o"]); expect(lock.packages.o[0]).toBe(project.o); - expect(text).not.toContain("../m/node_modules/p"); + expect(text).not.toContain("../m/node_modules/no-deps"); + expect(registry.requests).toStrictEqual([]); const install = await run(project.dir, "install", "--linker", linker); expect(install.stderr).not.toContain("error"); + expect(install.stderr).toContain("Saved lockfile"); expect(install.exitCode).toBe(0); + expect((await readLock(project.dir)).lock.packages).toStrictEqual({ + o: [project.o, { dependencies: { "no-deps": "" } }], + "no-deps": ["no-deps@2.0.0", registry.tarball("no-deps", "2.0.0"), {}, registry.integrity("no-deps", "2.0.0")], + }); expect(await Bun.file(join(project.dir, "node_modules", "o", "package.json")).json()).toHaveProperty("name", "o"); - expect(fs.existsSync(join(project.dir, "node_modules", "o", "node_modules", "p"))).toBeFalse(); expect(fs.existsSync(join(project.oSource, "node_modules"))).toBeFalse(); - expect(storeEntries(project.dir)).toStrictEqual(linker === "isolated" ? [project.o.replaceAll(/[/:]/g, "+")] : []); + expect(storeEntries(project.dir)).toStrictEqual( + linker === "isolated" ? ["no-deps@2.0.0", project.o.replaceAll(/[/:]/g, "+")] : [], + ); }); test.concurrent("lockfileVersion 5 is refused, and install falls back to a fresh resolve", async () => { diff --git a/test/package.json b/test/package.json index b9bc51f5a99d..c7c7f27bbfbf 100644 --- a/test/package.json +++ b/test/package.json @@ -123,6 +123,7 @@ "msgpackr-extract": "3.0.2" }, "resolutions": { + "@types/bun": "../packages/bun-types", "react": "../node_modules/react", "@types/node": "26.2.0" }