Fix empty window on first npm start in fresh workspace - #3191
Conversation
📊 Performance Test ResultsComparing a93bf7a vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
|
So, if I have multiple git workspaces, I still can't have multiple Studio instances open at the same time, right? |
|
So the problem is that macOS keeps a cache of plist bundle IDs mapped to binary paths, and some of those paths may be stale because they belong to another git worktree? |
fredrikekelund
left a comment
There was a problem hiding this comment.
Took me a second to wrap my head around this, but this LGTM 👍 Much better to do this ahead of launch and guarantee that it doesn't run in production
Co-authored-by: Fredrik Rombach Ekelund <fredrik@f26d.dev>
Mostly yes, as far as I could check, if each worktree is registering the same bundle ID, there's no guarantee from Launch Services that it'll route to the latest registration. That's why it's been non-deterministic which worktree gets the protocol redirect.
I haven't considered this angle yet, but I think this change breaks that constraint for dev builds, because we're locking instances based on bundle ID. I'll check how we could still restrict this for dev builds. |
I tested and found that the single instance is still enforced, when starting a second instance it quits at |
Related issues
How AI was used in this PR
Claude Code diagnosed the root cause, proposed the preflight-script approach, and implemented the change. I reviewed the diff and confirmed the fix end-to-end locally.
Proposed Changes
PR #3114 patches
node_modules/electron/dist/Electron.app/Contents/Info.plistwith a per-workspaceCFBundleIdentifiersowp-studio://deep links don't collide across worktrees in Launch Services. The original implementation did this insideappBoot()and calledapp.relaunch(); app.exit(0)to make the new ID take effect. That flow does not surviveelectron-vite dev:npm startspawns Electron viaelectron-vite dev, which injects dev-only wiring (includingELECTRON_RENDERER_URL) into its child process.ensureUniqueDevBundleId()patched the plist and exited with 0.app.relaunch()then spawned a new Electron with the same argv but without electron-vite's dev wiring, so the renderer URL was lost and an empty window appeared.This PR moves the plist patch out of the Electron main process:
scripts/ensure-dev-bundle-id.mjs, that patches the plist before Electron boots. macOS-only, idempotent, no-op on non-darwin or when the plist is already patched.prestartafter the CLI build, sonpm startruns the patch beforeelectron-vite devspawns Electron.ensureUniqueDevBundleId()fromapps/studio/src/index.tsalong with the now-unusedexecFileSync/createHashimports.Electron now boots exactly once with the plist already correct. No relaunch, no lost env, no empty window.
Testing Instructions
/usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier com.github.Electron' node_modules/electron/dist/Electron.app/Contents/Info.plistnpm startPatched Electron.app bundle ID -> com.studio.dev.<hash>during prestartnpm starta second time/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' node_modules/electron/dist/Electron.app/Contents/Info.plistwp-studio://deep links still route correctly to this workspace's running instance (the testing steps from Fix stale protocol handler registration on app boot #3114).Non-macOS:
npm startshould behave exactly as before; the preflight exits immediately.Pre-merge Checklist