From de77e69ba6a1ff39da2f30f444e7b275810615d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 8 May 2026 12:37:34 +0200 Subject: [PATCH 1/5] fix(init): fix InkUI loading in dev mode (bun run src/bin.ts) In dev mode, `with { type: "file" }` places ink-app.tsx's path in Bun's module cache as a file resource. Any subsequent import() of that path gets the path string back instead of the module, so mountApp is undefined and the wizard falls back to LoggingUI. Fix: isolate the `with { type: "file" }` import in a separate sidecar module (ink-ui-sidecar.ts) that is only dynamically imported inside the compiled binary. We detect the compiled binary by checking whether import.meta.url contains the /$bunfs/ virtual-filesystem prefix. In dev mode the check is false so the sidecar is never loaded and ink-app.tsx is imported directly without any cache collision. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/lib/init/ui/ink-ui-sidecar.ts | 19 +++++++++++++++++++ src/lib/init/ui/ink-ui.ts | 28 ++++++++++------------------ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 src/lib/init/ui/ink-ui-sidecar.ts diff --git a/src/lib/init/ui/ink-ui-sidecar.ts b/src/lib/init/ui/ink-ui-sidecar.ts new file mode 100644 index 000000000..e39ef8213 --- /dev/null +++ b/src/lib/init/ui/ink-ui-sidecar.ts @@ -0,0 +1,19 @@ +/** + * Compiled-binary sidecar loader for the Ink App. + * + * This module is ONLY imported in the compiled Bun binary. It must + * NOT be imported in dev mode (`bun run src/bin.ts`) because the + * `with { type: "file" }` static import poisons the Bun module cache + * for ink-app.tsx — any later import() of that path gets the path + * string back instead of the module, breaking mountApp. + * + * The text-import-plugin pre-bundles ink-app.tsx → ink-app.js during + * the esbuild step, so the embedded virtual-FS file is self-contained + * plain JS that Bun can evaluate at runtime. + */ +// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun +import inkAppPath from "./ink-app.tsx" with { type: "file" }; + +export async function loadInkApp(): Promise { + return import(inkAppPath as string) as Promise; +} diff --git a/src/lib/init/ui/ink-ui.ts b/src/lib/init/ui/ink-ui.ts index 237ab8c69..5cb7182ae 100644 --- a/src/lib/init/ui/ink-ui.ts +++ b/src/lib/init/ui/ink-ui.ts @@ -185,8 +185,6 @@ function severityForStopCode(code: SpinnerExitCode): LogSeverity { * the static import is unconditional; the bundle.ts cleanup step * `unlink`s the unused sidecar after bundling. */ -// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun -import inkAppPath from "./ink-app.tsx" with { type: "file" }; /** * Open a fresh `/dev/tty` `ReadStream` for Ink to consume. Returns @@ -213,22 +211,16 @@ function openFreshTtyForInk(): ReadStream | null { export async function createInkUI( opts: CreateInkUIOptions = {} ): Promise { - // Import the Ink App sidecar from the embedded file. The - // `with { type: "file" }` import above gives us the virtual path - // (e.g. `/$bunfs/root/ink-app-xxx.js`). The text-import-plugin - // pre-bundles the .tsx source into self-contained JS at build - // time, so the embedded file includes ink, react, and all local - // deps — no external resolution needed at runtime. - // - // NOTE: Do NOT append a query string (e.g. `?bridge=1`) to the - // path. Bun's `/$bunfs/` virtual filesystem does not support - // query strings — the path lookup fails with ENOENT. - // - // `mountApp()` lives inside the sidecar so it uses the same - // ink/react instances as the App's hooks. Importing ink/react - // separately in this module would create a second copy of React, - // causing "Invalid hook call" errors at runtime. - const app = (await import(inkAppPath)) as typeof import("./ink-app.js"); + // In the compiled binary, modules live at `file:///$bunfs/root/…`. + // The `with { type: "file" }` static import in ink-ui-sidecar.ts + // poisons Bun's module cache for ink-app.tsx: any import() that + // resolves to the same path returns the path string instead of the + // module. To avoid this in dev mode (`bun run src/bin.ts`), the + // sidecar is ONLY loaded when running inside a compiled binary. + const isCompiledBinary = import.meta.url.includes("/$bunfs/"); + const app = isCompiledBinary + ? await (await import("./ink-ui-sidecar.js")).loadInkApp() + : ((await import("./ink-app.js")) as typeof import("./ink-app.js")); const store = new WizardStore({ cliVersion: CLI_VERSION, From e227254de6491d0ac679ca9389b32b32c7550f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 8 May 2026 12:43:37 +0200 Subject: [PATCH 2/5] fix(init): remove spurious async from loadInkApp --- src/lib/init/ui/ink-ui-sidecar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/init/ui/ink-ui-sidecar.ts b/src/lib/init/ui/ink-ui-sidecar.ts index e39ef8213..19ff80217 100644 --- a/src/lib/init/ui/ink-ui-sidecar.ts +++ b/src/lib/init/ui/ink-ui-sidecar.ts @@ -14,6 +14,6 @@ // @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun import inkAppPath from "./ink-app.tsx" with { type: "file" }; -export async function loadInkApp(): Promise { +export function loadInkApp(): Promise { return import(inkAppPath as string) as Promise; } From eaaf42fe40762fdc642861c1b7213a739c0a63fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 8 May 2026 14:03:52 +0200 Subject: [PATCH 3/5] fix(init): use Bun.build for ink-app sidecar pre-bundling esbuild wraps CJS packages (signal-exit, parse-keypress, etc.) in __commonJS helpers. When Bun.compile then embeds the resulting file as a with { type: "file" } asset, it injects __promiseAll helpers at wrong positions inside those wrappers, causing: SyntaxError: Unexpected identifier '__promiseAll' at parse-keypress.js:420:1 on all platforms. Switching to Bun.build produces output that Bun.compile recognises natively and processes without the mis-injection. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/lib/init/ui/ink-ui-sidecar.ts | 19 ------------------- src/lib/init/ui/ink-ui.ts | 27 +++++++++++++++++---------- 2 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 src/lib/init/ui/ink-ui-sidecar.ts diff --git a/src/lib/init/ui/ink-ui-sidecar.ts b/src/lib/init/ui/ink-ui-sidecar.ts deleted file mode 100644 index 19ff80217..000000000 --- a/src/lib/init/ui/ink-ui-sidecar.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Compiled-binary sidecar loader for the Ink App. - * - * This module is ONLY imported in the compiled Bun binary. It must - * NOT be imported in dev mode (`bun run src/bin.ts`) because the - * `with { type: "file" }` static import poisons the Bun module cache - * for ink-app.tsx — any later import() of that path gets the path - * string back instead of the module, breaking mountApp. - * - * The text-import-plugin pre-bundles ink-app.tsx → ink-app.js during - * the esbuild step, so the embedded virtual-FS file is self-contained - * plain JS that Bun can evaluate at runtime. - */ -// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun -import inkAppPath from "./ink-app.tsx" with { type: "file" }; - -export function loadInkApp(): Promise { - return import(inkAppPath as string) as Promise; -} diff --git a/src/lib/init/ui/ink-ui.ts b/src/lib/init/ui/ink-ui.ts index 5cb7182ae..4becfa802 100644 --- a/src/lib/init/ui/ink-ui.ts +++ b/src/lib/init/ui/ink-ui.ts @@ -185,6 +185,8 @@ function severityForStopCode(code: SpinnerExitCode): LogSeverity { * the static import is unconditional; the bundle.ts cleanup step * `unlink`s the unused sidecar after bundling. */ +// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun +import inkAppPath from "./ink-app.tsx" with { type: "file" }; /** * Open a fresh `/dev/tty` `ReadStream` for Ink to consume. Returns @@ -211,16 +213,21 @@ function openFreshTtyForInk(): ReadStream | null { export async function createInkUI( opts: CreateInkUIOptions = {} ): Promise { - // In the compiled binary, modules live at `file:///$bunfs/root/…`. - // The `with { type: "file" }` static import in ink-ui-sidecar.ts - // poisons Bun's module cache for ink-app.tsx: any import() that - // resolves to the same path returns the path string instead of the - // module. To avoid this in dev mode (`bun run src/bin.ts`), the - // sidecar is ONLY loaded when running inside a compiled binary. - const isCompiledBinary = import.meta.url.includes("/$bunfs/"); - const app = isCompiledBinary - ? await (await import("./ink-ui-sidecar.js")).loadInkApp() - : ((await import("./ink-app.js")) as typeof import("./ink-app.js")); + // `with { type: "file" }` above gives us the virtual path in the + // compiled binary (e.g. `/$bunfs/root/ink-app-xxx.js`). In dev + // mode (`bun run src/bin.ts`) inkAppPath is the source file path, + // and Bun's module cache already holds it as a path-string from the + // static import — a plain `import(inkAppPath)` would return the + // cached string rather than the module. Importing via the absolute + // file:// URL uses a distinct cache key, bypassing the poison. + // + // NOTE: Do NOT append a query string to the /$bunfs/ path — Bun's + // virtual filesystem does not support query strings (ENOENT). + const isCompiledBinary = String(inkAppPath).includes("/$bunfs/"); + const inkModulePath = isCompiledBinary + ? String(inkAppPath) + : new URL("./ink-app.tsx", import.meta.url).href; + const app = (await import(inkModulePath)) as typeof import("./ink-app.js"); const store = new WizardStore({ cliVersion: CLI_VERSION, From d69b4a7f1053ca6db631fc016202a3d269050ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 8 May 2026 15:41:20 +0200 Subject: [PATCH 4/5] fix(init): restore sidecar and use Bun.build for pre-bundling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes in one: 1. Dev mode (bun run src/bin.ts): the file:// URL approach to bypass the module cache collision doesn't work — Bun normalises file:/// paths to plain paths before cache lookup, so it hits the same poisoned entry. Restoring the sidecar module (ink-ui-sidecar.ts) is the only reliable fix: the with { type: "file" } static import only runs inside the compiled binary (detected via /$bunfs/ in import.meta.url), so the cache is never poisoned in dev mode. 2. Binary CI (__promiseAll on Linux): esbuild wraps CJS packages in __commonJS helpers; Bun.compile injects __promiseAll at wrong positions inside those wrappers. Switching text-import-plugin.ts to use Bun.build instead of esbuild avoids the mis-injection. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- script/text-import-plugin.ts | 59 +++++++++++++------------------ src/lib/init/ui/ink-ui-sidecar.ts | 16 +++++++++ src/lib/init/ui/ink-ui.ts | 53 ++++++--------------------- 3 files changed, 52 insertions(+), 76 deletions(-) create mode 100644 src/lib/init/ui/ink-ui-sidecar.ts diff --git a/script/text-import-plugin.ts b/script/text-import-plugin.ts index c99bdc421..0f69a873f 100644 --- a/script/text-import-plugin.ts +++ b/script/text-import-plugin.ts @@ -26,13 +26,6 @@ * like `ink-frame.tsx` and third-party packages like `ink`, * `react`) so the file is fully self-contained. * - * Some of the inlined packages (e.g. `signal-exit`, used by Ink) - * are CJS modules that call `require("assert")` etc. esbuild - * wraps these in `__require` shims that throw "Dynamic require - * is not supported" at runtime. The banner injects a real - * `require` function via `createRequire` so CJS dependencies - * resolve Node builtins correctly inside the ESM bundle. - * * Non-TypeScript files (plain `.js`) are copied verbatim. * * Used by `script/build.ts` (single-file executable) and @@ -44,7 +37,7 @@ import { copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { basename, dirname, extname, resolve as resolvePath } from "node:path"; -import { build as esbuildBuild, type Plugin } from "esbuild"; +import type { Plugin } from "esbuild"; const TEXT_IMPORT_NS = "text-import"; const ANY_FILTER = /.*/; @@ -53,36 +46,34 @@ const ANY_FILTER = /.*/; const TS_EXTENSIONS = new Set([".ts", ".tsx", ".jsx"]); /** - * Banner injected into the pre-bundled sidecar JS. Provides a real - * `require` function so esbuild's CJS-wrapping `__require` shims - * can resolve Node.js builtins (`assert`, `events`, etc.) at runtime. - * Without this, `signal-exit` and other CJS deps of Ink throw - * "Dynamic require of 'assert' is not supported". - */ -const REQUIRE_BANNER = - 'import { createRequire as ___cr } from "node:module";' + - " var require = ___cr(import.meta.url);"; - -/** - * Pre-bundle a TypeScript/TSX source file into self-contained JS. - * All dependencies (local modules AND npm packages) are inlined; - * only `node:*` builtins are external since Bun resolves them - * natively inside `/$bunfs/`. + * Pre-bundle a TypeScript/TSX source file into a self-contained JS module + * using Bun.build. All dependencies (local modules AND npm packages) are + * inlined; Bun handles node:* builtins natively. + * + * Using Bun.build (rather than esbuild) is critical. esbuild wraps CJS + * packages (e.g. `signal-exit`, `parse-keypress`) in `__commonJS` helpers. + * When Bun.compile later embeds the esbuild output as a `with { type: "file" + * }` asset, it injects `__promiseAll` helpers at wrong positions inside those + * wrappers, causing `SyntaxError: Unexpected identifier '__promiseAll'` at + * runtime on all platforms. Bun.build produces output that Bun.compile + * recognises natively and handles without mis-injecting the helper. */ async function prebundleTs(sourcePath: string, outPath: string): Promise { - await esbuildBuild({ - entryPoints: [sourcePath], - bundle: true, - outfile: outPath, - platform: "node", - target: "esnext", - format: "esm", - jsx: "automatic", - external: ["node:*"], - banner: { js: REQUIRE_BANNER }, + const result = await Bun.build({ + entrypoints: [sourcePath], + target: "bun", + outdir: dirname(outPath), + naming: "[name].js", + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, minify: false, - write: true, }); + if (!result.success) { + throw new Error( + result.logs.map((l) => String(l)).join("\n") || "unknown error" + ); + } } /** Resolve the output directory from the parent esbuild config. */ diff --git a/src/lib/init/ui/ink-ui-sidecar.ts b/src/lib/init/ui/ink-ui-sidecar.ts new file mode 100644 index 000000000..26d058a07 --- /dev/null +++ b/src/lib/init/ui/ink-ui-sidecar.ts @@ -0,0 +1,16 @@ +/** + * Compiled-binary sidecar loader for the Ink App. + * + * Only imported in the compiled Bun binary (when import.meta.url + * contains /$bunfs/). In dev mode this module is never loaded, so the + * `with { type: "file" }` static import never runs and the Bun module + * cache for ink-app.tsx is never poisoned — which would cause any + * subsequent import() of that path to return the path string instead + * of the module. + */ +// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun +import inkAppPath from "./ink-app.tsx" with { type: "file" }; + +export function loadInkApp(): Promise { + return import(inkAppPath as string) as Promise; +} diff --git a/src/lib/init/ui/ink-ui.ts b/src/lib/init/ui/ink-ui.ts index 4becfa802..55f100c97 100644 --- a/src/lib/init/ui/ink-ui.ts +++ b/src/lib/init/ui/ink-ui.ts @@ -161,33 +161,6 @@ function severityForStopCode(code: SpinnerExitCode): LogSeverity { return "success"; } -/** - * Embed the Ink App sidecar as a Bun-compile file resource. - * - * `with { type: "file" }` tells Bun.compile to embed the file into - * the binary's virtual filesystem (`/$bunfs/root/`) and replace the - * import with the embedded path string at runtime. The - * `text-import-plugin` in `script/build.ts` intercepts this during - * esbuild: it pre-bundles the .tsx source into self-contained JS - * (stripping TypeScript, inlining local deps and npm packages, - * injecting a `createRequire` banner for CJS deps), then marks the - * import external so Bun.compile picks up the resulting .js file. - * - * Why pre-bundle? Bun's `/$bunfs/` virtual FS uses a JavaScript - * parser, not TypeScript — raw .tsx fails on `import { type Foo }`. - * The `/$bunfs/` environment also has no `node_modules`, so all - * deps (ink, react, local modules) must be inlined. - * - * The npm/Node distribution never reaches `createInkUI()` (the - * factory routes there only on the Bun binary because Ink uses - * top-level await that esbuild can't emit in our CJS bundle), so - * the embedded file is unused on Node. We still produce it because - * the static import is unconditional; the bundle.ts cleanup step - * `unlink`s the unused sidecar after bundling. - */ -// @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun -import inkAppPath from "./ink-app.tsx" with { type: "file" }; - /** * Open a fresh `/dev/tty` `ReadStream` for Ink to consume. Returns * `null` when `/dev/tty` isn't available (non-TTY environment, or @@ -213,21 +186,17 @@ function openFreshTtyForInk(): ReadStream | null { export async function createInkUI( opts: CreateInkUIOptions = {} ): Promise { - // `with { type: "file" }` above gives us the virtual path in the - // compiled binary (e.g. `/$bunfs/root/ink-app-xxx.js`). In dev - // mode (`bun run src/bin.ts`) inkAppPath is the source file path, - // and Bun's module cache already holds it as a path-string from the - // static import — a plain `import(inkAppPath)` would return the - // cached string rather than the module. Importing via the absolute - // file:// URL uses a distinct cache key, bypassing the poison. - // - // NOTE: Do NOT append a query string to the /$bunfs/ path — Bun's - // virtual filesystem does not support query strings (ENOENT). - const isCompiledBinary = String(inkAppPath).includes("/$bunfs/"); - const inkModulePath = isCompiledBinary - ? String(inkAppPath) - : new URL("./ink-app.tsx", import.meta.url).href; - const app = (await import(inkModulePath)) as typeof import("./ink-app.js"); + // The `with { type: "file" }` static import in ink-ui-sidecar.ts + // poisons Bun's module cache for ink-app.tsx in dev mode — any + // import() of that path returns the path string instead of the + // module. The sidecar is ONLY dynamically imported in the compiled + // binary (detected via /$bunfs/ in import.meta.url), so the cache + // is never poisoned in dev mode and `import("./ink-app.js")` works. + const app = ( + import.meta.url.includes("/$bunfs/") + ? await (await import("./ink-ui-sidecar.js")).loadInkApp() + : ((await import("./ink-app.js")) as typeof import("./ink-app.js")) + ) as typeof import("./ink-app.js"); const store = new WizardStore({ cliVersion: CLI_VERSION, From b57b1f15278e810c79f423cac34cc0a7bbf1c7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Fri, 8 May 2026 16:13:14 +0200 Subject: [PATCH 5/5] fix(init): prevent ink-app.tsx from being inlined into the main bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `import("./ink-app.js")` (static literal) in ink-ui.ts caused esbuild to inline all of ink-app.tsx — including ink's CJS modules — into the main bundle (bin.js). Bun.compile then processes those modules and injects __promiseAll at wrong positions inside esbuild's __commonJS wrappers, producing: SyntaxError: Unexpected identifier '__promiseAll' even for commands like --help that never touch the Ink UI. Fix: use a variable for the dev-mode import path so esbuild cannot statically analyse it and declines to bundle ink-app.tsx into bin.js. esbuild only inlines static string literals; a variable (even one whose value is a constant) blocks the inlining. In the compiled binary the dev-mode branch is never reached (import.meta.url contains /$bunfs/), so the variable import is dead code and no resolution is attempted. In dev mode (bun run src/bin.ts) Bun resolves the variable path at runtime without any bundler involvement. Also reverts text-import-plugin.ts to the clean esbuild-based approach (no Bun.build, no ink-dev-strip plugin) now that the root cause is addressed in ink-ui.ts instead. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- script/text-import-plugin.ts | 46 +++++++++++++++++++----------------- src/lib/init/ui/ink-ui.ts | 20 ++++++++++------ 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/script/text-import-plugin.ts b/script/text-import-plugin.ts index 0f69a873f..455edf697 100644 --- a/script/text-import-plugin.ts +++ b/script/text-import-plugin.ts @@ -37,7 +37,7 @@ import { copyFileSync, mkdirSync, readFileSync } from "node:fs"; import { basename, dirname, extname, resolve as resolvePath } from "node:path"; -import type { Plugin } from "esbuild"; +import { build as esbuildBuild, type Plugin } from "esbuild"; const TEXT_IMPORT_NS = "text-import"; const ANY_FILTER = /.*/; @@ -46,34 +46,36 @@ const ANY_FILTER = /.*/; const TS_EXTENSIONS = new Set([".ts", ".tsx", ".jsx"]); /** - * Pre-bundle a TypeScript/TSX source file into a self-contained JS module - * using Bun.build. All dependencies (local modules AND npm packages) are - * inlined; Bun handles node:* builtins natively. - * - * Using Bun.build (rather than esbuild) is critical. esbuild wraps CJS - * packages (e.g. `signal-exit`, `parse-keypress`) in `__commonJS` helpers. - * When Bun.compile later embeds the esbuild output as a `with { type: "file" - * }` asset, it injects `__promiseAll` helpers at wrong positions inside those - * wrappers, causing `SyntaxError: Unexpected identifier '__promiseAll'` at - * runtime on all platforms. Bun.build produces output that Bun.compile - * recognises natively and handles without mis-injecting the helper. + * Banner injected into the pre-bundled sidecar JS. Provides a real + * `require` function so esbuild's CJS-wrapping `__require` shims can + * resolve Node.js builtins (`assert`, `events`, etc.) at runtime. + */ +const REQUIRE_BANNER = + 'import { createRequire as ___cr } from "node:module";' + + " var require = ___cr(import.meta.url);"; + +/** + * Pre-bundle a TypeScript/TSX source file into a self-contained JS module. + * All dependencies (local modules AND npm packages) are inlined; + * only `node:*` builtins are external since Bun resolves them natively. */ async function prebundleTs(sourcePath: string, outPath: string): Promise { - const result = await Bun.build({ - entrypoints: [sourcePath], - target: "bun", - outdir: dirname(outPath), - naming: "[name].js", + await esbuildBuild({ + entryPoints: [sourcePath], + bundle: true, + outfile: outPath, + platform: "node", + target: "esnext", + format: "esm", + jsx: "automatic", + external: ["node:*"], + banner: { js: REQUIRE_BANNER }, define: { "process.env.NODE_ENV": JSON.stringify("production"), }, minify: false, + write: true, }); - if (!result.success) { - throw new Error( - result.logs.map((l) => String(l)).join("\n") || "unknown error" - ); - } } /** Resolve the output directory from the parent esbuild config. */ diff --git a/src/lib/init/ui/ink-ui.ts b/src/lib/init/ui/ink-ui.ts index 55f100c97..52c2178d8 100644 --- a/src/lib/init/ui/ink-ui.ts +++ b/src/lib/init/ui/ink-ui.ts @@ -186,16 +186,22 @@ function openFreshTtyForInk(): ReadStream | null { export async function createInkUI( opts: CreateInkUIOptions = {} ): Promise { - // The `with { type: "file" }` static import in ink-ui-sidecar.ts - // poisons Bun's module cache for ink-app.tsx in dev mode — any - // import() of that path returns the path string instead of the - // module. The sidecar is ONLY dynamically imported in the compiled - // binary (detected via /$bunfs/ in import.meta.url), so the cache - // is never poisoned in dev mode and `import("./ink-app.js")` works. + // In the compiled binary: sidecar holds the `with { type: "file" }` + // import; detected via /$bunfs/ in import.meta.url. + // + // In dev mode: use a variable import path so esbuild cannot statically + // analyse it and bundle ink-app.tsx into bin.js. A literal + // `import("./ink-app.js")` would cause esbuild to inline ink-app.tsx + // — pulling in ink's CJS modules — and Bun.compile then injects + // __promiseAll at wrong positions inside those modules. The variable + // breaks esbuild's static analysis while still resolving correctly at + // dev-mode runtime. The sidecar is never loaded in dev mode so Bun's + // module cache for ink-app.tsx is never poisoned. + const devPath = /* @__PURE__ */ (() => "./ink-app.js")(); const app = ( import.meta.url.includes("/$bunfs/") ? await (await import("./ink-ui-sidecar.js")).loadInkApp() - : ((await import("./ink-app.js")) as typeof import("./ink-app.js")) + : ((await import(devPath)) as typeof import("./ink-app.js")) ) as typeof import("./ink-app.js"); const store = new WizardStore({