Repository navigation
Conversation
bun init over an existing package.json wrote "type": "module" whenever the file had a "module" field or no "main", and "private": true on every run without --minimal. Both writes replaced an existing value, and both added the key when it was absent. The two keys are now written only to a package.json that bun init did not load: a new file or an empty one.
|
Updated 6:55 AM PT - Oct 15th, 2026
✅ @robobun, your commit 0edeaefa97618b6060010b2e0c0a47bb1e3dc56f passed in 🧪 To try this PR locally: bunx bun-pr 44992That installs a local version of the PR into your bun-44992 --bun |
|
Status Reproduced on main 71d0d43 (linux x64): D=$(mktemp -d); cd "$D"
printf '{"name":"cjs-app","version":"1.0.0","type":"commonjs","private":false}\n' > package.json
printf 'const os=require("os");console.log("require works:",os.platform());\n' > index.js
node index.js # require works: linux
bun init -y
node index.js # ReferenceError: require is not defined in ES module scopeOn main, PR: #44992 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
Walkthrough
ChangesManifest Defaults
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
| const decoder = new TextDecoder(); | ||
| let output = ""; | ||
| // `bun init` prints this note only when its stdin is a TTY. | ||
| const note = Promise.withResolvers<void>(); | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "init"], | ||
| cwd: temp, | ||
| env: initEnv, | ||
| // A terminal that spawn creates reports EOF after the child exits and all of its output arrived. | ||
| terminal: { | ||
| cols: 80, | ||
| rows: 24, | ||
| data(_, chunk: Uint8Array) { | ||
| output += decoder.decode(chunk, { stream: true }); | ||
| if (output.includes("package.json already exists")) note.resolve(); | ||
| }, | ||
| exit() { | ||
| note.reject(new Error(`bun init did not print its TTY note:\n${output}`)); | ||
| }, | ||
| }, | ||
| }); | ||
| const [exitCode] = await Promise.all([proc.exited, note.promise]); | ||
|
|
||
| expect(await Bun.file(path.join(temp, "package.json")).json()).toEqual(commonjsAfterInit); | ||
| expect(exitCode).toBe(0); |
There was a problem hiding this comment.
🟡 nit (optional): the PTY test leaves its Bun.Terminal open after the child exits, unlike every other spawn-with-terminal test, so the PTY master fd leaks until GC. await using proc at test/cli/init/init.test.ts:651 does not close the terminal: Subprocess async dispose returns early once the child has exited (src/runtime/api/bun/subprocess.rs:619-621) and a terminal stays closed === false after exit. Fix: close the terminal on every exit path, e.g. proc.terminal!.close() in a try/finally after awaiting proc.exited, or await using terminal = proc.terminal! right after spawn, matching test/js/bun/terminal/terminal.test.ts:1299.
Why this was flagged
The test at test/cli/init/init.test.ts:642-676 spawns bun init with the terminal option and awaits proc.exited and note.promise at :675. It never calls proc.terminal.close(). await using proc runs Subprocess async dispose, which at src/runtime/api/bun/subprocess.rs:619-621 returns immediately when the child has already exited and leaves everything to GC, and the terminal is not closed by child exit (test/js/bun/terminal/terminal.test.ts:1298 asserts closed is false after exit). Every existing spawn-with-terminal test closes it explicitly (terminal.test.ts:1299, :1367, :1384, :1406, :1425). Under describe.concurrent on a long-lived CI runner the PTY master fd stays open until GC collects the wrapper; the base branch has no such test. Repo review guidance asks that every resource be released via using or try/finally registered before assertions.
Verification: Every run of the new PTY test at test/cli/init/init.test.ts:648-676 hits it: the child exits, nothing closes the terminal, and the master fd stays open until GC. await using proc (:655) runs Subprocess::async_dispose, which at src/runtime/api/bun/subprocess.rs:619-621 returns early once the child has exited. master_fd is only closed in close_internal (Terminal.rs:1745-1750).
Problem
bun initwrites two new-project defaults into apackage.jsonthat it loaded:"type": "module"and"private": true. It replaces"commonjs"andfalse, adds both keys when absent, prints nothing, and exits 0.node index.jsfails withReferenceError: require is not defined in ES module scope, and a Library-template package failsbun publishwitherror: attempted to publish a private package.InitCommand::exec(src/runtime/cli/init_command.rs:662-679) run for every manifest.Fix
bun initdid not load apackage.json(a new or empty file).bun initadds its other fields only when absent.test/cli/init/init.test.ts, 41 pass. Of 24 new or edited tests, main fails 21 and 3 are controls.Background
package.jsonis an existing one that parsed to an object.typedecides how node loads.jsfiles.private: trueblocks publishing.typeto a CommonJS project with notypekey. Keeping indentation and skipping a no-op write are separate follow-ups (Notes).Downsides
package.jsonwith notypeno longer gains"type": "module", including thebun addandbun init -mstubs. One condition restores it.bun initprints a loadedpackage.jsonagain at 2 spaces, also with nothing to add.bun init -y: +40 instructions inInitCommand::exec, +0 allocator calls, +0 syscalls. Release text: -768 bytes.Notes
Where this comes from. A fuzz run of
bun initover existing manifests found it. No user reported it. Related to #16361, which asks for a confirm step or--forcewhen apackage.jsonexists. This PR adds neither.typehas been written since d034c00 (2022-08-04),privatesince #17282 (2025-02-18).Repro (main 71d0d43. The report saw the same on 1.4.2 and 1.4.3):
On main the manifest ends with
"type": "module"and"private": true. With this PR both values stay, andnode index.jsstill works.The decision. The two conditions are separate lines. The
privatecondition also covers an absent key, because the Library template writes noprivateon purpose (fields.private = false) and a laterbun initadded it. Thetypecondition is the open one:typekey keeps working under node. The stubs thatbun add xandbun init -mwrite in an empty directory no longer end like a freshbun init: they get notypeand noprivate.typewhen absent): those stubs converge with a freshbun init, and a typeless CommonJS project with amodulefield or nomainstill breaks under node.typeandprivateonly when the loaded manifest has notype,mainormoduleand the directory has no source file. The two stubs then end like a freshbun init. A package that has onlybinand keeps its CommonJS files in a subdirectory still breaks: with{"bin": {"cli": "./bin/cli.js"}},node bin/cli.jsgoes from working toReferenceError: require is not defined in ES module scope.A matrix of 64 loaded manifests (
typeabsent,"module","commonjs",123, timesprivateabsent,true,false,"yes", times no entry field,module,main, both), run withbun init -yon release builds:typeaddedprivateaddedWhich manifests reach which path on main:
npm init -y(npm 11) writesmainand"type": "commonjs". That manifest keeps itstypeand gains"private": true.bun add ../depin an empty directory writes{"dependencies": {...}}, andbun init -m -ywrites{"devDependencies": {...}}. Both gain"type": "module"and"private": true. With this PR they gainnameandmoduleonly."type": "commonjs"and amodulefield or nomain, or with"private": false, has the value replaced.Tests. A new
describe.concurrentblock holds the offline tests. Every fixture lists@types/bunandtypescript, so no nestedbun installruns.privaterotating. Each asserts the whole manifest.-y, a stdin pipe,-y <folder>and a TTY.private) and thebun addstub (no name).package.jsonstill gets the defaults of a new one, and the expected manifest of the non-object dependencies test losestypeandprivate, because its fixture is a loaded manifest.The 3 controls pass on main:
type: "module"withprivate: trueand no entry field,type: 123withprivate: trueandmain, and the emptypackage.json.Measurements (release builds, linux x64, main 71d0d43 against this PR):
InitCommand::exec18,301 to 17,638 bytes, text 89,103,407 to 89,102,639 (llvm-nm,size).bun init -y:package.jsonand stdout are byte-identical. Calls tohas_property,as_property,put,put_stringandprint_json, allocator entry calls (56) and syscalls (149) do not change (gdb counters, 2 runs each).InitCommand::execto its return on a fresh run: 69,741 to 69,781 (gdbstepi).package.jsonwith nothing to add, in 9 layouts with-yand with-m -y: main and this PR both write 18 of 18, and 16 of 18 change bytes.Not changed here. On a loaded manifest
bun initstill does these things:console.log("Hello via Bun!")file at the entry path when nothing is at that exact path. For"main": "./index"besideindex.jsthat file shadows the real module under node."module": "index.ts"and the file when the directory has no source file, and the CommonJS manifest above gains"module": "index.js".@types/buntodevDependenciesandtypescripttopeerDependencieswhen another dependency group already lists them, and then runsbun install.package.jsonthat exists but does not parse is replaced by the scaffold (init: never write over a package.json that was not loaded #41807).bun init --reactreturns before this code.Open PRs on the same function. This PR adds 0 conflict regions against #41807, #41741, #35445, #44102, #38474, #39689, #36607 and #43489 (
git merge-fileon each head). #41807 removesdid_load_package_json, so whichever lands second changes the two reads to itsLoadedstate. #43489 adds a test whose expected manifest hastypeandprivatefor a loadedpackage.json, and it loses the two keys after this PR.Self-review. The first version of this change also kept the indentation and the trailing-newline state of a loaded manifest, skipped the write when nothing changed, and printed
~ package.json. The review asked for a split: those parts conflict with three open PRs that rewrite the write block (#41807, #41741, #38474), the indentation part has two more sites (bun pm trust, andbun createin #36965), and the skipped write has no report behind it. That version is on the branchrobobun/271725ea/init-keep-type-private(6881131). Four suggestions were not taken:bun initPRs. That is a maintainer's call, and the other PRs are not part of this work.private: truewhen the loaded manifest has noname. It adds a third manifest state, and a stub with no version cannot be published.@types/bunandtypescriptentries here. The dependency block is edited by bun init: put typescript in devDependencies, not peerDependencies #35445 and bun init: add react + @types/react to the blank scaffold so .tsx works #36607.typewhen absent) if the decision above gets no answer. It leaves a typeless CommonJS project broken, so the question is asked here.Windows was not run locally.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/init/init.test.ts