diff --git a/docs/pm/cli/install.mdx b/docs/pm/cli/install.mdx index 0b219f2b285e..fbf73124eef1 100644 --- a/docs/pm/cli/install.mdx +++ b/docs/pm/cli/install.mdx @@ -583,11 +583,13 @@ Dependencies using pnpm's `catalog:` protocol are preserved: ### Configuration Migration -Bun migrates the following pnpm configuration from both `pnpm-lock.yaml` and `pnpm-workspace.yaml`: +Bun copies the following pnpm configuration into the root `package.json`: -- **Overrides**: Moved from `pnpm.overrides` to root-level `overrides` in `package.json` -- **Patched Dependencies**: Moved from `pnpm.patchedDependencies` to root-level `patchedDependencies` in `package.json` -- **Workspace Overrides**: Applied from `pnpm-workspace.yaml` to root `package.json` +- **Overrides**: Copied from `pnpm.overrides` to root-level `overrides` in `package.json` +- **Patched Dependencies**: Copied from `pnpm.patchedDependencies` to root-level `patchedDependencies` in `package.json` +- **Workspace Overrides**: Copied from `overrides` and `patchedDependencies` in `pnpm-workspace.yaml` to the same root-level fields + +Bun leaves the `pnpm` field of `package.json` as it is. pnpm reads its configuration from that field and ignores the root-level fields, so `pnpm install --frozen-lockfile` keeps working for teammates who still use pnpm. Bun reads the root-level fields and ignores the `pnpm` field. ### Requirements and limitations @@ -598,7 +600,7 @@ Bun migrates the following pnpm configuration from both `pnpm-lock.yaml` and `pn - Relative `link:` dependencies and git dependencies with a sub-directory (`resolution.path`) are not supported - If migration fails for any of these reasons, Bun prints why and resolves from scratch instead -After migration, you can safely remove `pnpm-lock.yaml` and `pnpm-workspace.yaml` files. +Once nobody on the repository uses pnpm anymore, you can remove `pnpm-lock.yaml`, `pnpm-workspace.yaml`, and the `pnpm` field of `package.json`. --- diff --git a/src/install/pnpm.rs b/src/install/pnpm.rs index abf7916c98f8..e3d7e5252407 100644 --- a/src/install/pnpm.rs +++ b/src/install/pnpm.rs @@ -17,6 +17,7 @@ use crate::external_slice::ExternalSlice; use crate::integrity::Integrity; use crate::lockfile::{self, LoadResult, LoadResultOk, Lockfile}; use crate::npm::{self}; +use crate::package_manager_real::update_package_json_and_install::print_package_json_into_cache_entry; use crate::repository::Repository; use crate::resolution::{self, Resolution, TaggedValue}; use crate::{DependencyID, INVALID_PACKAGE_ID, PackageID, PackageManager}; @@ -2368,140 +2369,28 @@ fn update_package_json_after_migration( return Ok(()); } - let mut needs_update = false; - let mut moved_overrides = false; - let mut moved_patched_deps = false; - let mut moved: Vec<&'static str> = Vec::new(); + let mut copied: Vec<&'static str> = Vec::new(); - if let Some(mut pnpm_prop) = json.as_property(b"pnpm") { + // Copied, not moved: pnpm keeps reading this block, bun only reads the root-level fields. + if let Some(pnpm_prop) = json.as_property(b"pnpm") { if pnpm_prop.expr.is_object() { - let pnpm_obj = e_object_mut(&mut pnpm_prop.expr); - - if let Some(overrides_field) = pnpm_obj.get(b"overrides") { - if is_non_empty_object(&overrides_field) { - if let Some(mut existing_prop) = json.as_property(b"overrides") { - if existing_prop.expr.is_object() { - let existing_overrides = e_object_mut(&mut existing_prop.expr); - for prop in e_object(&overrides_field).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - continue; - }; - existing_overrides.put( - &bump, - key, - prop.value.expect("infallible: prop has value"), - )?; - } - } - } else { - e_object_mut(&mut json).put(&bump, b"overrides", overrides_field)?; - } - moved_overrides = true; - needs_update = true; - moved.push("pnpm.overrides to overrides"); - } - } + let pnpm_obj = e_object(&pnpm_prop.expr); - if let Some(mut patched_field) = pnpm_obj.get(b"patchedDependencies") { - if is_non_empty_object(&patched_field) { - rewrite_bare_patch_keys(&mut patched_field, patches)?; - if let Some(mut existing_prop) = json.as_property(b"patchedDependencies") { - if existing_prop.expr.is_object() { - let existing_patches = e_object_mut(&mut existing_prop.expr); - for prop in e_object(&patched_field).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - continue; - }; - existing_patches.put( - &bump, - key, - prop.value.expect("infallible: prop has value"), - )?; - } - } - } else { - e_object_mut(&mut json).put( - &bump, - b"patchedDependencies", - patched_field, - )?; - } - moved_patched_deps = true; - needs_update = true; - moved.push("pnpm.patchedDependencies to patchedDependencies"); + if let Some(overrides) = pnpm_obj.get(b"overrides").filter(is_non_empty_object) { + if copy_into_root(&mut json, &bump, b"overrides", copy_object(&overrides))? { + copied.push("pnpm.overrides to overrides"); } } - if moved_overrides || moved_patched_deps { - let mut remaining_count: usize = 0; - for prop in pnpm_obj.properties.slice() { - let Some(key) = as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - remaining_count += 1; - continue; - }; - if moved_overrides && key == b"overrides" { - continue; - } - if moved_patched_deps && key == b"patchedDependencies" { - continue; - } - remaining_count += 1; - } - - if remaining_count == 0 { - let mut new_root_count: usize = 0; - for prop in e_object(&json).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - new_root_count += 1; - continue; - }; - if key != b"pnpm" { - new_root_count += 1; - } - } - - let mut new_root_props = G::PropertyList::init_capacity(new_root_count); - for prop in e_object(&json).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - VecExt::append(&mut new_root_props, shallow_clone_prop(prop)); - continue; - }; - if key != b"pnpm" { - VecExt::append(&mut new_root_props, shallow_clone_prop(prop)); - } - } - - e_object_mut(&mut json).properties = new_root_props; - } else { - let mut new_pnpm_props = G::PropertyList::init_capacity(remaining_count); - for prop in pnpm_obj.properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - VecExt::append(&mut new_pnpm_props, shallow_clone_prop(prop)); - continue; - }; - if moved_overrides && key == b"overrides" { - continue; - } - if moved_patched_deps && key == b"patchedDependencies" { - continue; - } - VecExt::append(&mut new_pnpm_props, shallow_clone_prop(prop)); - } - - pnpm_obj.properties = new_pnpm_props; + if let Some(patched) = pnpm_obj + .get(b"patchedDependencies") + .filter(is_non_empty_object) + { + let mut patched = copy_object(&patched); + rewrite_bare_patch_keys(&mut patched, patches)?; + if copy_into_root(&mut json, &bump, b"patchedDependencies", patched)? { + copied.push("pnpm.patchedDependencies to patchedDependencies"); } - needs_update = true; } } } @@ -2526,11 +2415,11 @@ fn update_package_json_after_migration( // `Expr::data_store_reset`). let contents: &'static [u8] = js_ast::data_store_dupe_str(&contents); let yaml_source = bun_ast::Source::init_path_string(b"pnpm-workspace.yaml", contents); - let arena = bun_alloc::Arena::new(); + // Quoted scalars are copied into the arena; `bump` lives until the print below. let Ok(ws_root) = bun_parsers::yaml::YAML::parse( &yaml_source, log, - &arena, + &bump, bun_parsers::yaml::CyclicAliases::Reject, ) else { break 'read_pnpm_workspace_yaml; @@ -2659,108 +2548,42 @@ fn update_package_json_after_migration( } } if wrote_workspaces { - needs_update = true; - moved.push("pnpm-workspace.yaml to workspaces"); - } - - // Handle overrides from pnpm-workspace.yaml - if let Some(ws_overrides) = &workspace_overrides_obj { - if ws_overrides.is_object() { - if let Some(mut existing_prop) = json.as_property(b"overrides") { - if existing_prop.expr.is_object() { - let existing_overrides = e_object_mut(&mut existing_prop.expr); - for prop in e_object(ws_overrides).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - continue; - }; - existing_overrides.put( - &bump, - key, - prop.value.expect("infallible: prop has value"), - )?; - } - } - } else { - e_object_mut(&mut json).put(&bump, b"overrides", *ws_overrides)?; - } - needs_update = true; - moved.push("pnpm-workspace.yaml overrides to overrides"); - } + copied.push("pnpm-workspace.yaml to workspaces"); } - // Handle patchedDependencies from pnpm-workspace.yaml - if let Some(ws_patched) = &mut workspace_patched_deps_obj { - if ws_patched.is_object() { - rewrite_bare_patch_keys(ws_patched, patches)?; - if let Some(mut existing_prop) = json.as_property(b"patchedDependencies") { - if existing_prop.expr.is_object() { - let existing_patches = e_object_mut(&mut existing_prop.expr); - for prop in e_object(ws_patched).properties.slice() { - let Some(key) = - as_string(prop.key.as_ref().expect("infallible: prop has key")) - else { - continue; - }; - existing_patches.put( - &bump, - key, - prop.value.expect("infallible: prop has value"), - )?; - } - } - } else { - e_object_mut(&mut json).put(&bump, b"patchedDependencies", *ws_patched)?; - } - needs_update = true; - moved.push("pnpm-workspace.yaml patchedDependencies to patchedDependencies"); + if let Some(ws_overrides) = workspace_overrides_obj { + if copy_into_root(&mut json, &bump, b"overrides", ws_overrides)? { + copied.push("pnpm-workspace.yaml overrides to overrides"); } } - if needs_update { - let mut buffer_writer = bun_js_printer::BufferWriter::init(); - buffer_writer.append_newline = !root_pkg_json.source.contents().is_empty() - && root_pkg_json.source.contents()[root_pkg_json.source.contents().len() - 1] == b'\n'; - let mut package_json_writer = bun_js_printer::BufferPrinter::init(buffer_writer); - - if bun_js_printer::print_json( - &mut package_json_writer, - json, - &root_pkg_json.source, - bun_js_printer::PrintJsonOptions { - indent: root_pkg_json.indentation, - mangled_props: None, - ..Default::default() - }, - ) - .is_err() - { - return Ok(()); + if let Some(mut ws_patched) = workspace_patched_deps_obj { + rewrite_bare_patch_keys(&mut ws_patched, patches)?; + if copy_into_root(&mut json, &bump, b"patchedDependencies", ws_patched)? { + copied.push("pnpm-workspace.yaml patchedDependencies to patchedDependencies"); } + } - if package_json_writer.flush().is_err() { - return Err(AllocError); + if !copied.is_empty() { + print_package_json_into_cache_entry(root_pkg_json, json); + // The printed tree borrows from the replaced contents; `bun update` edits this entry next. + if let Err(err) = root_pkg_json.reparse_root(log) { + bun_core::pretty_errorln!("package.json failed to parse due to error {}", err.name()); + bun_core::Global::crash(); } - root_pkg_json.source.contents = std::borrow::Cow::Owned( - package_json_writer - .ctx - .written_without_trailing_zero() - .to_vec(), - ); - - // Write the updated package.json if sys::File::write_file( dir, bun_core::zstr!("package.json"), root_pkg_json.source.contents(), ) .is_ok() - && !moved.is_empty() && !silent { - bun_core::pretty_errorln!("moved {} in package.json", moved.join(", ")); + bun_core::pretty_errorln!( + "copied {} in package.json", + copied.join(", ") + ); } } @@ -2771,6 +2594,46 @@ fn is_non_empty_object(expr: &Expr) -> bool { matches!(&expr.data, ExprData::EObject(o) if !o.properties.is_empty()) } +/// The root-level copy gets edited further; an `Expr` from `get` would alias the `pnpm` block. +fn copy_object(src: &Expr) -> Expr { + let src_props = e_object(src).properties.slice(); + let mut properties = G::PropertyList::init_capacity(src_props.len()); + for prop in src_props { + VecExt::append(&mut properties, shallow_clone_prop(prop)); + } + Expr::init( + E::Object { + properties, + ..Default::default() + }, + bun_ast::Loc::EMPTY, + ) +} + +/// Merges `src` into the root-level `field` (created when absent); `false` if it is not an object. +fn copy_into_root( + json: &mut Expr, + bump: &bun_alloc::Arena, + field: &[u8], + src: Expr, +) -> Result { + let Some(mut existing) = json.as_property(field) else { + e_object_mut(json).put(bump, field, src)?; + return Ok(true); + }; + if !existing.expr.is_object() { + return Ok(false); + } + let existing_obj = e_object_mut(&mut existing.expr); + for prop in e_object(&src).properties.slice() { + let Some(key) = as_string(prop.key.as_ref().expect("infallible: prop has key")) else { + continue; + }; + existing_obj.put(bump, key, prop.value.expect("infallible: prop has value"))?; + } + Ok(true) +} + fn paths_array(paths: &[&'static [u8]]) -> Expr { let mut items = js_ast::ExprNodeList::init_capacity(paths.len()); for path in paths { diff --git a/test/cli/install/migration/pnpm-lock-v9.test.ts b/test/cli/install/migration/pnpm-lock-v9.test.ts index 9fe4c0434e27..e5d76d805ed9 100644 --- a/test/cli/install/migration/pnpm-lock-v9.test.ts +++ b/test/cli/install/migration/pnpm-lock-v9.test.ts @@ -785,16 +785,19 @@ snapshots: const { stderr, exitCode } = await migrate(packageDir); expect(stderr).not.toContain("is not in patchedDependencies"); + expect(stderr).toContain("copied pnpm.patchedDependencies to patchedDependencies in package.json"); expect(stderr).toContain("migrated lockfile from pnpm-lock.yaml"); expect(exitCode).toBe(0); const bunLock = await bunLockOf(packageDir); expect(bunLock).toContain(`"patchedDependencies": {\n "no-deps@1.0.1": "patches/no-deps.patch",\n }`); + // The versioned key only goes to the root; pnpm keeps reading the bare key from its own block. const packageJson = await Bun.file(join(packageDir, "package.json")).json(); expect(packageJson).toStrictEqual({ name: "patch-path-in-package-json", dependencies: { "no-deps": "^1.0.0" }, + pnpm: { patchedDependencies: { "no-deps": "patches/no-deps.patch" } }, patchedDependencies: { "no-deps@1.0.1": "patches/no-deps.patch" }, }); @@ -807,6 +810,55 @@ snapshots: ); }); + // `bun update` migrates, then edits the cached root package.json that the migration rewrote, so the cached + // copy has to be re-read from the rewritten contents (a stale copy pointed into the freed previous contents). + test("bun update straight from pnpm-lock.yaml edits the package.json the migration rewrote", async () => { + const pnpm = { + patchedDependencies: { "no-deps": "patches/no-deps.patch" }, + overrides: { "a-dep": "1.0.1" }, + }; + const { packageDir } = await verdaccio.createTestDir({ + bunfigOpts: { linker: "hoisted" }, + files: { + "package.json": JSON.stringify({ + name: "update-after-migration", + dependencies: { "no-deps": "^1.0.0" }, + pnpm, + }), + "patches/no-deps.patch": NO_DEPS_INDEX_PATCH, + "pnpm-lock.yaml": bareHashNoDepsLockfile("no-deps").replace( + "importers:", + "overrides:\n a-dep: 1.0.1\n\nimporters:", + ), + }, + }); + + const update = await run(packageDir, "update"); + + expect(update.stderr).toContain( + "copied pnpm.overrides to overrides, pnpm.patchedDependencies to patchedDependencies in package.json", + ); + expect(update.stderr).toContain("migrated lockfile from pnpm-lock.yaml"); + expect(update.stderr).not.toContain("error:"); + expect(update.stdout).toContain("no-deps@1.0.1"); + expect(update.exitCode).toBe(0); + expect(await Bun.file(join(packageDir, "package.json")).json()).toStrictEqual({ + name: "update-after-migration", + dependencies: { "no-deps": "^1.0.1" }, + pnpm, + overrides: pnpm.overrides, + patchedDependencies: { "no-deps@1.0.1": "patches/no-deps.patch" }, + }); + expect(await Bun.file(join(packageDir, "node_modules/no-deps/index.js")).text()).toStartWith( + "globalThis.patchedByMigration = true;\n", + ); + + const frozen = await run(packageDir, "install", "--frozen-lockfile"); + + expect(frozen.stderr).not.toContain("error:"); + expect(frozen.exitCode).toBe(0); + }); + test("versioned lockfile key falls back to the bare config key", async () => { const { packageDir } = await verdaccio.createTestDir({ bunfigOpts: { linker: "hoisted" }, @@ -2419,6 +2471,77 @@ snapshots: }); }); + // #23694: `bun update -i` migrates once to list the outdated packages, edits the root package.json through the + // cache, then installs, which migrates again because bun.lock is still not on disk. The editor and the second + // migration both used the tree the first migration had edited, which pointed into the contents it had freed. + test("bun update -i in a pnpm workspace migrates twice and keeps package.json intact", async () => { + const { packageDir } = await verdaccio.createTestDir({ + bunfigOpts: { linker: "hoisted" }, + files: { + "package.json": JSON.stringify({ + name: "update-interactive", + dependencies: { "no-deps": "^1.0.0" }, + pnpm: { overrides: { "a-dep": "1.0.1" } }, + }), + "packages/a/package.json": JSON.stringify({ name: "a" }), + "pnpm-workspace.yaml": "packages:\n - packages/*\n", + "pnpm-lock.yaml": `lockfileVersion: '9.0' + +overrides: + a-dep: 1.0.1 + +importers: + + .: + dependencies: + no-deps: + specifier: ^1.0.0 + version: 1.0.0 + + packages/a: {} + +packages: + + no-deps@1.0.0: + resolution: {integrity: ${NO_DEPS_1_0_0_INTEGRITY}} + +snapshots: + + no-deps@1.0.0: {} +`, + }, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "update", "-i"], + cwd: packageDir, + env: { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache") }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + proc.stdin.write("a\r"); + proc.stdin.end(); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).not.toContain("error:"); + expect(stdout).toContain("no-deps"); + expect(exitCode).toBe(0); + expect(await Bun.file(join(packageDir, "package.json")).json()).toStrictEqual({ + name: "update-interactive", + dependencies: { "no-deps": "^1.1.0" }, + pnpm: { overrides: { "a-dep": "1.0.1" } }, + overrides: { "a-dep": "1.0.1" }, + workspaces: ["packages/*"], + }); + expect(existsSync(join(packageDir, "bun.lock"))).toBe(true); + + const frozen = await run(packageDir, "install", "--frozen-lockfile"); + + expect(frozen.stderr).not.toContain("error:"); + expect(frozen.exitCode).toBe(0); + }); + describe("overrides", () => { function overridesLockfile(overrides: string) { return `lockfileVersion: '9.0' @@ -2440,7 +2563,7 @@ importers: return bunLock.slice(start, end + "\n },".length); } - // pnpm/pnpm#5928 (`-` removes the dependency) is warned once, with a location, when bun install reads the moved package.json overrides; pnpm/pnpm#6774 (`name@range` keys) migrates as ranged rules + // pnpm/pnpm#5928 (`-` removes the dependency) is warned once, with a location, when bun install reads the copied package.json overrides; pnpm/pnpm#6774 (`name@range` keys) migrates as ranged rules test.concurrent("removal values are dropped; name@range keys migrate as ranged rules", async () => { const overrides = { "left-pad": "-", @@ -2463,12 +2586,13 @@ importers: const { stderr, exitCode } = await migrate(String(dir)); expect(stderr).not.toContain("warn:"); - expect(stderr).toContain("moved pnpm.overrides to overrides in package.json"); + expect(stderr).toContain("copied pnpm.overrides to overrides in package.json"); expect(stderr).toContain("migrated lockfile from pnpm-lock.yaml"); expect(exitCode).toBe(0); expect(await Bun.file(join(String(dir), "package.json")).json()).toStrictEqual({ name: "overrides-unsupported", + pnpm: { overrides }, overrides, }); @@ -2499,6 +2623,103 @@ importers: expect(install.exitCode).toBe(0); }); + // pnpm reads `pnpm.overrides` and `pnpm.patchedDependencies` from package.json and ignores the root-level + // fields, so removing them from the `pnpm` block breaks `pnpm install --frozen-lockfile` for everyone still on + // pnpm (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). The block has to survive the migration as it was. + test.concurrent("the pnpm block in package.json is left as it was", async () => { + const pnpm = { + overrides: { "no-deps": "1.0.0" }, + patchedDependencies: { "one-dep@1.0.0": "patches/one-dep.patch" }, + onlyBuiltDependencies: ["one-dep"], + }; + using dir = tempDir("pnpm-v9-pnpm-block-kept", { + "package.json": JSON.stringify({ name: "pnpm-block-kept", private: true, pnpm }), + "pnpm-lock.yaml": overridesLockfile(" no-deps: 1.0.0"), + }); + using onlyMigratedKeys = tempDir("pnpm-v9-pnpm-block-only-migrated-keys", { + "package.json": JSON.stringify({ name: "pnpm-block-only-migrated-keys", pnpm: { overrides: pnpm.overrides } }), + "pnpm-lock.yaml": overridesLockfile(" no-deps: 1.0.0"), + }); + + const { stderr, exitCode } = await migrate(String(dir)); + + expect(stderr).not.toContain("warn:"); + expect(stderr).toContain( + "copied pnpm.overrides to overrides, pnpm.patchedDependencies to patchedDependencies in package.json", + ); + expect(exitCode).toBe(0); + expect(await Bun.file(join(String(dir), "package.json")).json()).toStrictEqual({ + name: "pnpm-block-kept", + private: true, + pnpm, + overrides: pnpm.overrides, + patchedDependencies: pnpm.patchedDependencies, + }); + + const onlyMigrated = await migrate(String(onlyMigratedKeys)); + + expect(onlyMigrated.stderr).toContain("copied pnpm.overrides to overrides in package.json"); + expect(onlyMigrated.exitCode).toBe(0); + expect(await Bun.file(join(String(onlyMigratedKeys), "package.json")).json()).toStrictEqual({ + name: "pnpm-block-only-migrated-keys", + pnpm: { overrides: pnpm.overrides }, + overrides: pnpm.overrides, + }); + }); + + // pnpm 10 merges `pnpm.overrides` with the `overrides` of pnpm-workspace.yaml, and the lockfile records the + // merged set. The yaml entries belong in the root-level copy only. + test.concurrent("pnpm-workspace.yaml overrides go into the root copy, not into the pnpm block", async () => { + using dir = tempDir("pnpm-v9-overrides-package-json-and-workspace-yaml", { + "package.json": JSON.stringify({ + name: "overrides-package-json-and-workspace-yaml", + pnpm: { overrides: { "no-deps": "1.0.0" } }, + }), + "pnpm-workspace.yaml": "overrides:\n a-dep: 1.0.1\n", + "pnpm-lock.yaml": overridesLockfile(" no-deps: 1.0.0\n a-dep: 1.0.1"), + }); + + const { stderr, exitCode } = await migrate(String(dir)); + + expect(stderr).not.toContain("warn:"); + expect(stderr).toContain( + "copied pnpm.overrides to overrides, pnpm-workspace.yaml overrides to overrides in package.json", + ); + expect(exitCode).toBe(0); + expect(await Bun.file(join(String(dir), "package.json")).json()).toStrictEqual({ + name: "overrides-package-json-and-workspace-yaml", + pnpm: { overrides: { "no-deps": "1.0.0" } }, + overrides: { "no-deps": "1.0.0", "a-dep": "1.0.1" }, + }); + }); + + // Unquoted yaml scalars point into the file contents; quoted ones (the usual spelling for scoped names) are + // copied into the arena the yaml was parsed with, which has to stay alive until package.json is printed. + test.concurrent("quoted pnpm-workspace.yaml scalars are written to package.json", async () => { + using dir = tempDir("pnpm-v9-workspace-yaml-quoted", { + "package.json": JSON.stringify({ name: "workspace-yaml-quoted" }), + "pnpm-workspace.yaml": `overrides: + '@scope/pkg': "1.0.0" +catalog: + '@scope/other': '^2.0.0' +`, + "pnpm-lock.yaml": overridesLockfile(" '@scope/pkg': 1.0.0"), + }); + + const { stderr, exitCode } = await migrate(String(dir)); + + expect(stderr).not.toContain("warn:"); + expect(stderr).toContain( + "copied pnpm-workspace.yaml to workspaces, pnpm-workspace.yaml overrides to overrides in package.json", + ); + expect(exitCode).toBe(0); + expect(await Bun.file(join(String(dir), "package.json")).json()).toStrictEqual({ + name: "workspace-yaml-quoted", + workspaces: { catalog: { "@scope/other": "^2.0.0" } }, + overrides: { "@scope/pkg": "1.0.0" }, + }); + }); + test.concurrent("parent selectors become nested rules", async () => { using dir = tempDir("pnpm-v9-overrides-nested", { "package.json": JSON.stringify({ name: "overrides-nested" }), @@ -2536,7 +2757,7 @@ importers: const deep = await migrate(String(tooDeep)); expect(deep.stderr).not.toContain("warn:"); - expect(deep.stderr).toContain("moved pnpm.overrides to overrides in package.json"); + expect(deep.stderr).toContain("copied pnpm.overrides to overrides in package.json"); expect(deep.stderr).toContain("migrated lockfile from pnpm-lock.yaml"); expect(deep.exitCode).toBe(0); expect(await bunLockOf(String(tooDeep))).not.toContain("a>b"); diff --git a/test/cli/install/nested-overrides.test.ts b/test/cli/install/nested-overrides.test.ts index 9308eb302438..e70335cd2780 100644 --- a/test/cli/install/nested-overrides.test.ts +++ b/test/cli/install/nested-overrides.test.ts @@ -1553,7 +1553,7 @@ snapshots: } const migratedLine = /\[[\d.]+m?s\] migrated lockfile from pnpm-lock\.yaml\n/; - const movedOverridesLine = "moved pnpm.overrides to overrides in package.json"; + const copiedOverridesLine = "copied pnpm.overrides to overrides in package.json"; test("pnpm-lock.yaml parent>child overrides become nested rules that package.json agrees with", async () => { const dir = await project( @@ -1565,7 +1565,7 @@ snapshots: expect(migrated.err).not.toContain("warn:"); expect(migrated.err).not.toContain("error:"); expect(migrated.err).toMatch(migratedLine); - expect(occurrences(migrated.err, movedOverridesLine)).toBe(1); + expect(occurrences(migrated.err, copiedOverridesLine)).toBe(1); expect(migrated.exitCode).toBe(0); const text = await lock(dir); expect(text).toContain('"one-dep": {'); @@ -1575,6 +1575,7 @@ snapshots: expect(JSON.parse(packageJson)).toStrictEqual({ name: "nested-overrides", dependencies: { "one-dep": "1.0.0" }, + pnpm: { overrides: { "one-dep>no-deps": "2.0.0" } }, overrides: { "one-dep>no-deps": "2.0.0" }, }); await installOk(dir, "--frozen-lockfile"); @@ -1589,7 +1590,7 @@ snapshots: const migrated = await migrate(dir); expect(migrated.err).not.toContain("warn:"); expect(migrated.err).not.toContain("error:"); - expect(occurrences(migrated.err, movedOverridesLine)).toBe(1); + expect(occurrences(migrated.err, copiedOverridesLine)).toBe(1); expect(migrated.exitCode).toBe(0); const text = await lock(dir); expect(text).toContain('"one-dep": {'); @@ -1607,7 +1608,7 @@ snapshots: const migrated = await migrate(dir); expect(migrated.err).not.toContain("warn:"); expect(migrated.err).not.toContain("error:"); - expect(occurrences(migrated.err, movedOverridesLine)).toBe(1); + expect(occurrences(migrated.err, copiedOverridesLine)).toBe(1); expect(migrated.exitCode).toBe(0); const text = await lock(dir); expect(text).toContain('"lockfileVersion": 3'); @@ -1638,7 +1639,7 @@ snapshots: }) .then(({ packageDir }) => packageDir); const migrated = await migrate(dir); - expect(occurrences(migrated.err, movedOverridesLine)).toBe(1); + expect(occurrences(migrated.err, copiedOverridesLine)).toBe(1); expect(migrated.err).not.toContain("error:"); expect(migrated.exitCode).toBe(0); const packageJson = await packageJsonText(dir); @@ -1649,10 +1650,11 @@ snapshots: name: "nested-overrides", dependencies: { "one-dep": "1.0.0" }, overrides: { "a-dep": "1.0.1", "one-dep>no-deps": "2.0.0" }, + pnpm: { overrides: { "one-dep>no-deps": "2.0.0" } }, }); }); - test("an empty pnpm.overrides is not moved and package.json is not announced as modified", async () => { + test("an empty pnpm.overrides is not copied and package.json is not announced as modified", async () => { const dir = await project({ dependencies: { "one-dep": "1.0.0" }, pnpm: { overrides: {} } }, "hoisted", { "pnpm-lock.yaml": await pnpmLock({ noDepsVersion: "1.0.1" }), }); @@ -1660,7 +1662,7 @@ snapshots: const migrated = await migrate(dir); expect(migrated.err).not.toContain("warn:"); expect(migrated.err).not.toContain("error:"); - expect(migrated.err).not.toContain(movedOverridesLine); + expect(migrated.err).not.toContain(copiedOverridesLine); expect(migrated.err).toMatch(migratedLine); expect(migrated.exitCode).toBe(0); expect(await packageJsonText(dir)).toBe(before); @@ -1696,14 +1698,14 @@ snapshots: ), ); - test("bun install warns once per rejected rule that the migration moved into package.json", async () => { + test("bun install warns once per rejected rule that the migration copied into package.json", async () => { const dir = await rejectedRulesProject(); const { err, exitCode } = await install(dir); expect(occurrences(err, 'warn: Removing "left-pad" with "-" is not supported')).toBe(1); expect(occurrences(err, 'warn: Bun currently only supports one level of nested "overrides"')).toBe(1); expect(occurrences(err, "warn:")).toBe(2); expect(err).toMatch(migratedLine); - expect(occurrences(err, movedOverridesLine)).toBe(1); + expect(occurrences(err, copiedOverridesLine)).toBe(1); expect(err).not.toContain("error:"); expect(exitCode).toBe(0); expect((await file(join(dir, "package.json")).json()).overrides).toStrictEqual({