diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index b62f9a2fef61..ea5f440e245a 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -197,6 +197,33 @@ pub struct BundledDep { // exec // ─────────────────────────────────────────────────────────────────────────── +pub(crate) fn warn_lockfile_unreadable( + cause: &bun_install::lockfile::LoadResultErr, + log: &mut bun_ast::Log, + log_level: LogLevel, +) { + use bun_install::lockfile::LoadStep; + if log_level != LogLevel::Silent { + let step = match cause.step { + LoadStep::OpenFile => "open", + LoadStep::ParseFile => "parse", + LoadStep::ReadFile => "read", + LoadStep::Migrating => "migrate", + }; + bun_core::warn!( + "failed to {} {}: {}, continuing without it", + step, + bstr::BStr::new(cause.lockfile_path.as_bytes()), + cause.value.name(), + ); + if log.has_errors() { + let _ = log.print(std::ptr::from_mut(Output::error_writer())); + } + Output::flush(); + } + log.reset(); +} + impl PackCommand { pub fn exec_with_manager( ctx: Command::Context<'_>, @@ -204,9 +231,8 @@ impl PackCommand { ) -> crate::Result<()> { use bun_install::lockfile::{LoadResult, LoadStep}; - if manager.options.log_level != LogLevel::Silent - && manager.options.log_level != LogLevel::Quiet - { + let log_level = manager.options.log_level; + if log_level != LogLevel::Silent && log_level != LogLevel::Quiet { bun_core::prettyln!( "bun pack v{}", Global::package_json_version_with_sha, @@ -227,39 +253,13 @@ impl PackCommand { let lockfile_ref: Option<&Lockfile> = match load_from_disk_result { LoadResult::Ok(ok) => Some(&*ok.lockfile), LoadResult::Err(cause) => 'err: { - match cause.step { - LoadStep::OpenFile => { - if cause.value == bun_install::Error::Sys(bun_errno::SystemErrno::ENOENT) { - break 'err None; - } - Output::err_generic( - "failed to open lockfile: {}", - format_args!("{}", cause.value.name()), - ); - } - LoadStep::ParseFile => { - Output::err_generic( - "failed to parse lockfile: {}", - format_args!("{}", cause.value.name()), - ); - } - LoadStep::ReadFile => { - Output::err_generic( - "failed to read lockfile: {}", - format_args!("{}", cause.value.name()), - ); - } - LoadStep::Migrating => { - Output::err_generic( - "failed to migrate lockfile: {}", - format_args!("{}", cause.value.name()), - ); - } - } - if pm_log(manager_ptr).has_errors() { - let _ = pm_log(manager_ptr).print(std::ptr::from_mut(Output::error_writer())); + if matches!(cause.step, LoadStep::OpenFile) + && cause.value == bun_install::Error::Sys(bun_errno::SystemErrno::ENOENT) + { + break 'err None; } - Global::crash(); + warn_lockfile_unreadable(&cause, pm_log(manager_ptr), log_level); + None } LoadResult::NotFound => None, }; diff --git a/src/runtime/cli/publish_command.rs b/src/runtime/cli/publish_command.rs index 5e374af7599b..eb88956fcd5a 100644 --- a/src/runtime/cli/publish_command.rs +++ b/src/runtime/cli/publish_command.rs @@ -465,6 +465,7 @@ impl<'a, const DIRECTORY_PUBLISH: bool> Context<'a, DIRECTORY_PUBLISH> { manager: &'a mut PackageManager, ) -> Result, FromWorkspaceError> { let mut lockfile = Lockfile::default(); + let log_level = manager.options.log_level; let manager_ptr: *mut PackageManager = manager; let log: &mut bun_ast::Log = manager.log_mut(); // SAFETY: `manager_ptr` was just derived from `manager: &'a mut PackageManager`; @@ -477,32 +478,13 @@ impl<'a, const DIRECTORY_PUBLISH: bool> Context<'a, DIRECTORY_PUBLISH> { LoadResult::Ok(ok) => Some(&*ok.lockfile), LoadResult::NotFound => None, LoadResult::Err(cause) => 'err: { - match cause.step { - LoadStep::OpenFile => { - if cause.value == bun_install::Error::Sys(bun_errno::SystemErrno::ENOENT) { - break 'err None; - } - Output::err_generic("failed to open lockfile: {}", (cause.value.name(),)); - } - LoadStep::ParseFile => { - Output::err_generic("failed to parse lockfile: {}", (cause.value.name(),)); - } - LoadStep::ReadFile => { - Output::err_generic("failed to read lockfile: {}", (cause.value.name(),)); - } - LoadStep::Migrating => { - Output::err_generic( - "failed to migrate lockfile: {}", - (cause.value.name(),), - ); - } - } - - if log.has_errors() { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); + if matches!(cause.step, LoadStep::OpenFile) + && cause.value == bun_install::Error::Sys(bun_errno::SystemErrno::ENOENT) + { + break 'err None; } - - Global::crash(); + pack::warn_lockfile_unreadable(&cause, log, log_level); + None } }; diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 8e349f865f35..18e6e94d9e4a 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -51,6 +51,138 @@ test("basic", async () => { expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]); }); +describe("unreadable lockfile", () => { + const mergeConflict = `{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "pack-bad-lockfile", +<<<<<<< HEAD + "dependencies": {}, +======= + "devDependencies": {}, +>>>>>>> feature + }, + }, + "packages": {}, +} +`; + + for (const { label, file, contents, diagnostic } of [ + { label: "empty bun.lock", file: "bun.lock", contents: "", diagnostic: "Missing lockfile version" }, + { + label: "bun.lock with git conflict markers", + file: "bun.lock", + contents: mergeConflict, + diagnostic: `Expected string but found "<<<<<<<"`, + }, + { label: "corrupt bun.lockb", file: "bun.lockb", contents: "not a lockfile" }, + ]) { + test(`packs with ${label}`, async () => { + await Promise.all([ + write(join(packageDir, "package.json"), JSON.stringify({ name: "pack-bad-lockfile", version: "1.0.0" })), + write(join(packageDir, "index.js"), "module.exports = 1"), + write(join(packageDir, file), contents), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "pack"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env: bunEnv, + }); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + + expect(err).toContain("warn:"); + expect(err).toContain(`failed to parse ${file}`); + expect(err).toContain("continuing without it"); + if (diagnostic) expect(err).toContain(diagnostic); + expect(out).toContain("Total files: 2"); + expect(exitCode).toBe(0); + + const tarball = readTarball(join(packageDir, "pack-bad-lockfile-1.0.0.tgz")); + expect(tarball.entries).toMatchObject([ + { "pathname": "package/package.json" }, + { "pathname": "package/index.js" }, + ]); + }); + } + + test("--silent suppresses the warning", async () => { + await Promise.all([ + write(join(packageDir, "package.json"), JSON.stringify({ name: "pack-bad-lockfile-silent", version: "1.0.0" })), + write(join(packageDir, "index.js"), "module.exports = 1"), + write(join(packageDir, "bun.lock"), ""), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "pm", "pack", "--silent"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env: bunEnv, + }); + const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]); + + expect(err).toBe(""); + expect(out).toContain("pack-bad-lockfile-silent-1.0.0.tgz"); + expect(exitCode).toBe(0); + expect(await exists(join(packageDir, "pack-bad-lockfile-silent-1.0.0.tgz"))).toBeTrue(); + }); + + test("still errors when a workspace version must be resolved", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-bad-lockfile-workspace", + version: "1.0.0", + workspaces: ["pkgs/*"], + dependencies: { pkg1: "workspace:*" }, + }), + ), + write(join(packageDir, "root.js"), "console.log('root')"), + write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.1.1" })), + write(join(packageDir, "bun.lock"), ""), + ]); + + const { err } = await packExpectError(packageDir, bunEnv); + expect(err).toContain("warn:"); + expect(err).toContain("failed to parse bun.lock"); + expect(err).toContain( + 'error: Failed to resolve workspace version for "pkg1" in `dependencies`. Run `bun install` and try again.', + ); + expect(await exists(join(packageDir, "pack-bad-lockfile-workspace-1.0.0.tgz"))).toBeFalse(); + }); + + test("bun publish --dry-run packs with corrupt lockfile", async () => { + await Promise.all([ + write(join(packageDir, "package.json"), JSON.stringify({ name: "publish-bad-lockfile", version: "1.0.0" })), + write(join(packageDir, "index.js"), "module.exports = 1"), + write(join(packageDir, "bun.lock"), ""), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "publish", "--dry-run"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + env: bunEnv, + }); + const [out, err] = await Promise.all([stdout.text(), stderr.text(), exited]); + + expect(err).toContain("warn:"); + expect(err).toContain("failed to parse bun.lock"); + expect(err).toContain("continuing without it"); + // Packing completed (auth may still fail afterwards, but packing is the behavior under test). + expect(out).toContain("Total files: 2"); + }); +}); + test("in subdirectory", async () => { await Promise.all([ write(