604bf4eba4d9
fix mobile entry.ts module-evaluation ordering: dynamic-import everything after chrome-shim so Rollup can't reorder a bundled dependency's top-level chrome.* access ahead of the shim install
- time
- 2026-07-02 00:05 UTC · 3 months ago
- machine
- koh2
- agent
- claude-sonnet-5
- session
- ring 1 chrome shim
- parent
- 8bf8725baf33
↓ peach-browser-604bf4eb.tar.gzatlas at this save
files changed1 file
src/mobile/entry.ts+55 −32src/mobile/entry.ts+55 −32
@@ -4,38 +4,61 @@44 // before any other module evaluates, and the brain boots before the UI55 // mounts so `bridge.sendMessage`'s first call has a handler waiting.66 //7-// `background/entry.ts` already self-invokes `start()` at its own module8-// bottom (verified against source — this diverges from the god document's9-// illustrative sketch, which shows calling `start()` again explicitly; doing10-// that here would run session-restore, setupAutoLock, and11-// setupClipboardClearAlarm a second time). Importing it for the side effect12-// is sufficient. `popup/index.tsx` similarly self-mounts `<App/>` and, on13-// native platforms, also calls `setupMessageHandler()` again — harmless,14-// since that registration is idempotent (see chrome-shim.ts's dispatch15-// comment for why running it twice doesn't invoke `handleMessage` twice).7+// Found on a real device (emulator run, 2026-07-01): a plain static8+// `import "./chrome-shim"` followed by other static imports does NOT9+// guarantee this ordering once bundled. ES module *evaluation* order follows10+// the import graph, not source-line order within one file — and Rollup11+// inlined chrome-shim.ts (small, single-use) directly into this chunk's own12+// body, positioned AFTER this chunk's own `import` statements for13+// background/entry's heavier dependencies (which get hoisted regardless of14+// where the `import "../background/entry"` line sits in the source). Net15+// effect: those dependencies evaluated and threw `chrome is not defined`16+// before chrome-shim's inlined code ever ran.17+//18+// The fix: keep `./chrome-shim` as the ONLY static import in this file, and19+// load everything else via dynamic `import()` — a dynamic import is a plain20+// expression evaluated in this module's own top-level synchronous flow, not21+// a hoisted declaration, so it genuinely cannot start until chrome-shim's22+// static import has fully finished (chrome-shim has no dependencies of its23+// own to delay it).1624 import "./chrome-shim";17-import "../background/entry";18-import "../popup/index";1925 20-import { ensureSettingsLoaded, isUnlocked, lock } from "../background/vault";21-import { isRecoveryConfirmPending } from "../background/recovery-pending";22-import { denyAllPendingPasskeyRequests } from "../background/passkey";23-import { nativePlugin } from "../shared/native";26+async function boot(): Promise<void> {27+ // `background/entry.ts` self-invokes `start()` at its own module bottom28+ // (verified against source — diverges from the god document's illustrative29+ // sketch, which shows calling `start()` again explicitly; doing that would30+ // run session-restore, setupAutoLock, and setupClipboardClearAlarm a second31+ // time). Importing it for the side effect is sufficient.32+ await import("../background/entry");2433 25-// Lock-on-close, mobile-shaped (§4.4 point 1): entry.ts's real lock-on-close26-// listens for the popup's `chrome.runtime.onConnect` port disconnecting —27-// there is no such port here, so peach-android's NativeBridge pushes28-// foreground/background over the bridge instead. Same intent (mirrors29-// entry.ts's onDisconnect body exactly), native trigger.30-nativePlugin<{ onLifecycle(cb: (s: "foreground" | "background") => void): void }>("PeachNative")?.onLifecycle(31- (state) => {32- if (state !== "background") return;33- void (async () => {34- await denyAllPendingPasskeyRequests();35- const settings = await ensureSettingsLoaded();36- if (!isUnlocked() || settings.lockOnClose === false) return;37- if (await isRecoveryConfirmPending()) return;38- lock();39- })();40- }41-);34+ // `popup/index.tsx` self-mounts `<App/>` and, on native platforms, also35+ // calls `setupMessageHandler()` again — harmless, since that registration36+ // is idempotent (see chrome-shim.ts's dispatch comment for why running it37+ // twice doesn't invoke `handleMessage` twice).38+ await import("../popup/index");39+40+ const { ensureSettingsLoaded, isUnlocked, lock } = await import("../background/vault");41+ const { isRecoveryConfirmPending } = await import("../background/recovery-pending");42+ const { denyAllPendingPasskeyRequests } = await import("../background/passkey");43+ const { nativePlugin } = await import("../shared/native");44+45+ // Lock-on-close, mobile-shaped (§4.4 point 1): entry.ts's real lock-on-close46+ // listens for the popup's `chrome.runtime.onConnect` port disconnecting —47+ // there is no such port here, so peach-android's NativeBridge pushes48+ // foreground/background over the bridge instead. Same intent (mirrors49+ // entry.ts's onDisconnect body exactly), native trigger.50+ nativePlugin<{ onLifecycle(cb: (s: "foreground" | "background") => void): void }>("PeachNative")?.onLifecycle(51+ (state) => {52+ if (state !== "background") return;53+ void (async () => {54+ await denyAllPendingPasskeyRequests();55+ const settings = await ensureSettingsLoaded();56+ if (!isUnlocked() || settings.lockOnClose === false) return;57+ if (await isRecoveryConfirmPending()) return;58+ lock();59+ })();60+ }61+ );62+}63+64+void boot();