Repository navigation
bun create: keep the template's package.json until the rewrite, and exit 1 when the rewrite fails #46420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
bun create: keep the template's package.json until the rewrite, and exit 1 when the rewrite fails #46420
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -332,6 +332,12 @@ fn exit_with_conflicts<'a>(destination: &[u8], paths: impl Iterator<Item = &'a [ | |||||||||||||||||||||||||||||||||
| Global::exit(1); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #[cold] | ||||||||||||||||||||||||||||||||||
| fn exit_package_json_not_written(error: impl core::fmt::Display) -> ! { | ||||||||||||||||||||||||||||||||||
| pretty_errorln!("package.json failed to write due to error {}", error); | ||||||||||||||||||||||||||||||||||
| Global::exit(1); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| pub(crate) struct CreateCommand; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| impl CreateCommand { | ||||||||||||||||||||||||||||||||||
|
|
@@ -576,7 +582,7 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let _ = Archiver::extract_to_disk( | ||||||||||||||||||||||||||||||||||
| if let Err(err) = Archiver::extract_to_disk( | ||||||||||||||||||||||||||||||||||
| &tarball_buf_list, | ||||||||||||||||||||||||||||||||||
| destination, | ||||||||||||||||||||||||||||||||||
| Some(&mut archive_context), | ||||||||||||||||||||||||||||||||||
|
|
@@ -585,16 +591,30 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
| depth_to_skip: 1, | ||||||||||||||||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| )?; | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| // The extractor read package.json and did not get it into its file. | ||||||||||||||||||||||||||||||||||
| if archive_context | ||||||||||||||||||||||||||||||||||
| .pluckers | ||||||||||||||||||||||||||||||||||
| .first() | ||||||||||||||||||||||||||||||||||
| .is_some_and(|plucker| plucker.found && !plucker.fd.is_valid()) | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| node.end(); | ||||||||||||||||||||||||||||||||||
| progress.refresh(); | ||||||||||||||||||||||||||||||||||
| exit_package_json_not_written(bstr::BStr::new( | ||||||||||||||||||||||||||||||||||
| bun_core::output::ErrName::name(&err), | ||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return Err(err.into()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if !create_options.skip_package_json { | ||||||||||||||||||||||||||||||||||
| let plucker = &archive_context.pluckers[0]; | ||||||||||||||||||||||||||||||||||
| let plucker = &mut archive_context.pluckers[0]; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if plucker.found && plucker.fd.is_valid() { | ||||||||||||||||||||||||||||||||||
| node.name = b"Updating package.json"; | ||||||||||||||||||||||||||||||||||
| progress.refresh(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| package_json_contents = plucker.contents.clone()?; | ||||||||||||||||||||||||||||||||||
| package_json_contents = core::mem::take(&mut plucker.contents); | ||||||||||||||||||||||||||||||||||
| package_json_file = Some(bun_sys::File::from_fd(plucker.fd)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -706,7 +726,9 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| template_has_dot_gitignore = | ||||||||||||||||||||||||||||||||||
| bun_sys::exists_at(&template_dir, bun_core::zstr!(".gitignore")); | ||||||||||||||||||||||||||||||||||
| if bun_sys::exists_at(&template_dir, bun_core::zstr!("package.json")) { | ||||||||||||||||||||||||||||||||||
| if !create_options.skip_package_json | ||||||||||||||||||||||||||||||||||
| && bun_sys::exists_at(&template_dir, bun_core::zstr!("package.json")) | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| package_json_file = destination_dir | ||||||||||||||||||||||||||||||||||
| .open_file(b"package.json", bun_sys::O::RDWR, 0) | ||||||||||||||||||||||||||||||||||
| .ok(); | ||||||||||||||||||||||||||||||||||
|
|
@@ -756,9 +778,6 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
| let cap = package_json_contents.list.capacity(); | ||||||||||||||||||||||||||||||||||
| package_json_contents.list.resize(cap, 0); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #[cfg(windows)] | ||||||||||||||||||||||||||||||||||
| let prev_file_pos = pkg.get_pos()?; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if let Err(err) = | ||||||||||||||||||||||||||||||||||
| pkg.pread_all(package_json_contents.list.as_mut_slice(), 0) | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
|
Comment on lines
781
to
783
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 pre-existing, not blocking: pre-existing: a local template whose package.json cannot be opened, stat'ed or read still ends with exit 0, while the write side of the same rewrite now exits 1. At create_command.rs:732-734 an open failure is swallowed with Why this was flaggedA local template directory with a package.json, run through Verification: pre-existing. |
||||||||||||||||||||||||||||||||||
|
|
@@ -772,10 +791,6 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| break 'read_package_json; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| #[cfg(windows)] | ||||||||||||||||||||||||||||||||||
| pkg.seek_to(prev_file_pos)?; | ||||||||||||||||||||||||||||||||||
| // The printer doesn't truncate, so we must do so manually | ||||||||||||||||||||||||||||||||||
| let _ = bun_sys::ftruncate(pkg.handle(), 0); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| bun_ast::initialize_store(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -827,52 +842,47 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let mut start_command: &[u8] = b"bun dev"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 'process_package_json: { | ||||||||||||||||||||||||||||||||||
| if create_options.skip_package_json { | ||||||||||||||||||||||||||||||||||
| package_json_file = None; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| if let Some(package_json_file) = &package_json_file { | ||||||||||||||||||||||||||||||||||
| bun_ast::initialize_store(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let source = bun_ast::Source::init_path_string( | ||||||||||||||||||||||||||||||||||
| b"package.json", | ||||||||||||||||||||||||||||||||||
| crate::cli::cli_dupe(bun_paths::resolve_path::join_abs::< | ||||||||||||||||||||||||||||||||||
| bun_paths::platform::Auto, | ||||||||||||||||||||||||||||||||||
| >(destination, b"package.json")), | ||||||||||||||||||||||||||||||||||
| package_json_contents.list.as_slice(), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // SAFETY: single-threaded CLI dispatch; no other borrow of `Cli::LOG_` is live in this scope. | ||||||||||||||||||||||||||||||||||
| let log: &mut bun_ast::Log = unsafe { ctx.log_mut() }; | ||||||||||||||||||||||||||||||||||
| let mut log = bun_ast::Log::init(); | ||||||||||||||||||||||||||||||||||
| let bump: &'static bun_alloc::Arena = crate::cli::cli_arena(); | ||||||||||||||||||||||||||||||||||
| let parsed = match JSON::parse_package_json_utf8_with_opts( | ||||||||||||||||||||||||||||||||||
| JSON::JSONOptions { | ||||||||||||||||||||||||||||||||||
| guess_indentation: true, | ||||||||||||||||||||||||||||||||||
| ..JSON::JSONOptions::DEFAULT | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| &source, | ||||||||||||||||||||||||||||||||||
| log, | ||||||||||||||||||||||||||||||||||
| &mut log, | ||||||||||||||||||||||||||||||||||
| bump, | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| Ok(r) => r, | ||||||||||||||||||||||||||||||||||
| Err(_) => { | ||||||||||||||||||||||||||||||||||
| if log.errors > 0 { | ||||||||||||||||||||||||||||||||||
| let _ = log.print(std::ptr::from_mut(Output::error_writer())); | ||||||||||||||||||||||||||||||||||
| Ok(parsed) if parsed.root.data.is_e_object() => parsed, | ||||||||||||||||||||||||||||||||||
| result => { | ||||||||||||||||||||||||||||||||||
| if let Ok(parsed) = result { | ||||||||||||||||||||||||||||||||||
| log.add_error( | ||||||||||||||||||||||||||||||||||
| Some(&source), | ||||||||||||||||||||||||||||||||||
| parsed.root.loc, | ||||||||||||||||||||||||||||||||||
| "package.json root must be an object", | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| break 'process_package_json; | ||||||||||||||||||||||||||||||||||
| let _ = log.print(std::ptr::from_mut(Output::error_writer())); | ||||||||||||||||||||||||||||||||||
| pretty_errorln!( | ||||||||||||||||||||||||||||||||||
| "<blue>note<r><d>:<r> template files were written. <b>package.json<r> is as the template has it", | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| Global::exit(1); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+867
to
+880
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Users whose template's package.json has a comment or trailing comma now get exit 1, no git repo, no install and no tasks, where base finished with exit 0. The parse at create_command.rs:858-866 uses Why this was flaggedA local or GitHub template whose package.json contains a Verification: create_command.rs:858-862 parses with |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| let indentation = parsed.indentation; | ||||||||||||||||||||||||||||||||||
| let mut package_json_expr = parsed.root; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if package_json_expr.data.e_object().is_none() { | ||||||||||||||||||||||||||||||||||
| break 'process_package_json; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if log.errors > 0 { | ||||||||||||||||||||||||||||||||||
| let _ = log.print(std::ptr::from_mut(Output::error_writer())); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| break 'process_package_json; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if let Some(name_expr) = package_json_expr.as_property(b"name") | ||||||||||||||||||||||||||||||||||
| && let Some(mut s) = name_expr.expr.data.e_string() | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
|
|
@@ -1025,17 +1035,13 @@ impl CreateCommand { | |||||||||||||||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| bun_core::pretty_errorln!("package.json failed to write due to error {}", err,); | ||||||||||||||||||||||||||||||||||
| break 'process_package_json; | ||||||||||||||||||||||||||||||||||
| exit_package_json_not_written(err); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| let written = package_json_writer.ctx.get_written(); | ||||||||||||||||||||||||||||||||||
| if let Err(err) = package_json_file.write_all(written).and_then(|()| { | ||||||||||||||||||||||||||||||||||
| if let Err(err) = package_json_file.pwrite_all(written, 0).and_then(|()| { | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 pre-existing, not blocking: On a filesystem whose pwrite can return 0 for a non-empty buffer (FUSE, some network mounts), users get a package.json padded with NUL bytes, a success message, and exit 0. pwrite_all at src/sys/file.rs:229-231 returns Ok(()) when pwrite returns 0, so create_command.rs:1041 then runs ftruncate(len) and extends the file with zeros past the unwritten part. The same helper is now also used in take_file at src/libarchive/lib.rs:1300, so the extracted template copy can be zero-filled the same way. Fix: make pwrite_all (and write_all) return an error when a write returns 0 with bytes remaining, so both the extractor and the rewrite exit 1 instead of reporting success. Why this was flaggedTrigger: Verification: pre-existing. Trigger: pwrite(2) returning 0 for a non-empty buffer. |
||||||||||||||||||||||||||||||||||
| bun_sys::ftruncate(package_json_file.handle, written.len() as i64) | ||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||
| bun_core::pretty_errorln!( | ||||||||||||||||||||||||||||||||||
| "package.json failed to write due to error {}", | ||||||||||||||||||||||||||||||||||
| bstr::BStr::new(err.name()), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| exit_package_json_not_written(bstr::BStr::new(err.name())); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+1041
to
+1044
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win A failed rewrite can leave
🛡️ Proposed fix: best-effort restore of the template text--- "a/src/runtime/cli/create_command.rs"
+++ "b/src/runtime/cli/create_command.rs"
@@ -1038,11 +1038,15 @@
exit_package_json_not_written(err);
}
let written = package_json_writer.ctx.get_written();
if let Err(err) = package_json_file.pwrite_all(written, 0).and_then(|()| {
bun_sys::ftruncate(package_json_file.handle, written.len() as i64)
}) {
+ let original = package_json_contents.list.as_slice();
+ let _ = package_json_file.pwrite_all(original, 0).and_then(|()| {
+ bun_sys::ftruncate(package_json_file.handle, original.len() as i64)
+ });
exit_package_json_not_written(bstr::BStr::new(err.name()));
}
}
}
Also assert in the test at 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+1041
to
+1044
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Users whose disk fills during the final package.json rewrite now get a file holding the start of the new text followed by the tail of the template's old text, with no message saying the file is corrupt. At create_command.rs:1041 pwrite_all(written, 0) overwrites in place and ftruncate(len) runs only after it succeeds, so a partial pwrite followed by ENOSPC leaves mixed content. The helper at create_command.rs:336 prints only "package.json failed to write due to error ENOSPC". On the base branch the local arm truncated to 0 first, so a partial write left only new-text bytes. Fix: write the new text to a temp file and rename over package.json, or on write failure restore the template bytes still in package_json_contents and say so on a note: line. Why this was flaggedTrigger: any Verification: nit — triggered when the final rewrite's pwrite at src/runtime/cli/create_command.rs:1041 writes part of the new text and then fails. |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 (optional) Users whose package.json write fails during a remote create now get a half-extracted template and a message that blames only package.json. The extractor aborts at src/libarchive/lib.rs:1911 when take_file fails, so every tarball entry after package.json is never written. create_command.rs:603 then prints "package.json failed to write due to error EFBIG" and exits 1 without saying the rest of the template is missing. On the base branch the extractor kept the bytes in memory, extracted every entry, and only the final rewrite failed. Fix: either keep extracting after a failed take_file and report the write error after the loop, or add a note: line stating the template is incomplete and which entry stopped it.
Why this was flagged
Trigger:
bun create owner/repo dest(or an@ bun-examplesname) where writing the plucked package.json fails (EFBIG underulimit -f, ENOSPC, EIO) while later entries would have written fine. At src/libarchive/lib.rs:1911plucker_.take_file(output.fd)?returns from extract_to_disk, so entries after package.json in the tarball are never created. create_command.rs:596-606 sees found && !fd.is_valid() and prints only "package.json failed to write due to error {}" then exits 1. The base at the same site assigned the fd and continued, so dest held the complete template with a 0-byte package.json. The user now has a partial dest and a message that points only at package.json; fixing package.json by hand leaves a broken template. The dismissal folded this into the wording nit at create_command.rs:338, but the missing-files consequence needs a different fix (defer the error or state the incompleteness).Verification: Trigger: a remote
bun createwhere the pwrite of package.json fails while later entries would have written. src/libarchive/lib.rs:1911take_file(output.fd)?returns on the write error, so every entry after package.json is never written. src/runtime/cli/create_command.rs:596-606 exits 1 without mentioning the rest of the template was not extracted. On the base every entry was extracted.