|
37 | 37 | * process. We close the stream on dispose to release the libuv |
38 | 38 | * handle. |
39 | 39 | * |
40 | | - * **Lazy import.** `ink`, `ink-spinner`, and `react` are all |
41 | | - * dynamically imported by `createInkUI()` so the npm bundle (which |
42 | | - * excludes them from the bundle graph) never sees the imports at |
43 | | - * module-load time. This keeps the `LoggingUI` path cheap to |
44 | | - * instantiate when interactive UI is not needed. |
| 40 | + * **Lazy import.** The Ink app sidecar (`ink-app.js`) is a |
| 41 | + * self-contained ESM bundle with all deps (ink, react, yoga-layout) |
| 42 | + * inlined. It's loaded lazily by `createInkUI()` via dynamic |
| 43 | + * `import()` so the `LoggingUI` path stays cheap to instantiate |
| 44 | + * when interactive UI is not needed. On the Bun binary the sidecar |
| 45 | + * is embedded in `/$bunfs/`; on the npm/Node distribution it ships |
| 46 | + * as `dist/ink-app.js` alongside the CJS bundle. |
45 | 47 | */ |
46 | 48 |
|
47 | 49 | import { openSync } from "node:fs"; |
@@ -170,20 +172,19 @@ function severityForStopCode(code: SpinnerExitCode): LogSeverity { |
170 | 172 | * `text-import-plugin` in `script/build.ts` intercepts this during |
171 | 173 | * esbuild: it pre-bundles the .tsx source into self-contained JS |
172 | 174 | * (stripping TypeScript, inlining local deps and npm packages, |
173 | | - * injecting a `createRequire` banner for CJS deps), then marks the |
174 | | - * import external so Bun.compile picks up the resulting .js file. |
| 175 | + * injecting a `createRequire` banner for CJS deps). |
175 | 176 | * |
176 | 177 | * Why pre-bundle? Bun's `/$bunfs/` virtual FS uses a JavaScript |
177 | 178 | * parser, not TypeScript — raw .tsx fails on `import { type Foo }`. |
178 | 179 | * The `/$bunfs/` environment also has no `node_modules`, so all |
179 | 180 | * deps (ink, react, local modules) must be inlined. |
180 | 181 | * |
181 | | - * The npm/Node distribution never reaches `createInkUI()` (the |
182 | | - * factory routes there only on the Bun binary because Ink uses |
183 | | - * top-level await that esbuild can't emit in our CJS bundle), so |
184 | | - * the embedded file is unused on Node. We still produce it because |
185 | | - * the static import is unconditional; the bundle.ts cleanup step |
186 | | - * `unlink`s the unused sidecar after bundling. |
| 182 | + * For the Bun binary build (ESM), the import is marked external so |
| 183 | + * Bun.compile embeds the file. For the npm CJS bundle, the plugin |
| 184 | + * emits a virtual module that exports the sidecar filename as a |
| 185 | + * string — `createInkUI` then resolves it relative to the bundle |
| 186 | + * and loads it via dynamic `import()`. The sidecar ships as |
| 187 | + * `dist/ink-app.js` in the npm package. |
187 | 188 | */ |
188 | 189 | // @ts-expect-error: `with { type: "file" }` is Bun-specific and not yet typed in @types/bun |
189 | 190 | import inkAppPath from "./ink-app.tsx" with { type: "file" }; |
@@ -213,24 +214,32 @@ function openFreshTtyForInk(): ReadStream | null { |
213 | 214 | export async function createInkUI( |
214 | 215 | opts: CreateInkUIOptions = {} |
215 | 216 | ): Promise<InkUI> { |
216 | | - // Import the Ink App sidecar. In compiled binaries the path |
217 | | - // points to `/$bunfs/root/ink-app-xxx.js` (pre-bundled by |
218 | | - // text-import-plugin). In dev mode it's the raw filesystem path |
219 | | - // to `ink-app.tsx`. |
| 217 | + // Import the Ink App sidecar. Three runtime contexts: |
220 | 218 | // |
221 | | - // Query-string cache-bust: Bun's module loader caches the |
222 | | - // `with { type: "file" }` import result (a path string) under |
223 | | - // the same specifier key. A bare `await import(inkAppPath)` in |
224 | | - // dev mode returns `{ default: "/abs/path" }` instead of the |
225 | | - // module's actual exports. Appending `?bridge=1` forces a |
226 | | - // distinct cache key so the .tsx is evaluated as a module. |
| 219 | + // 1. Bun binary: inkAppPath is "/$bunfs/root/ink-app-xxx.js" |
| 220 | + // (embedded by Bun.compile). Import directly — no query string |
| 221 | + // (/$bunfs/ doesn't support them). |
227 | 222 | // |
228 | | - // In compiled binaries the path lives under `/$bunfs/` which |
229 | | - // does NOT support query strings (ENOENT). There the cache |
230 | | - // collision doesn't occur because Bun.compile resolves the |
231 | | - // `with { type: "file" }` import differently. |
232 | | - const isEmbedded = inkAppPath.startsWith("/$bunfs/"); |
233 | | - const importPath = isEmbedded ? inkAppPath : `${inkAppPath}?bridge=1`; |
| 223 | + // 2. Dev mode (bun run src/bin.ts): inkAppPath is the absolute |
| 224 | + // filesystem path to ink-app.tsx. Append ?bridge=1 to bust |
| 225 | + // Bun's module cache (otherwise the import returns the path |
| 226 | + // string instead of the module's exports). |
| 227 | + // |
| 228 | + // 3. Node/npm (npx sentry@latest): inkAppPath is a relative path |
| 229 | + // like "./ink-app.js" (emitted by text-import-plugin as a |
| 230 | + // string literal). Resolve it to an absolute file:// URL using |
| 231 | + // import.meta.url so Node's dynamic import() can load the |
| 232 | + // self-contained ESM sidecar from the dist/ directory. |
| 233 | + let importPath: string; |
| 234 | + if (inkAppPath.startsWith("/$bunfs/")) { |
| 235 | + importPath = inkAppPath; |
| 236 | + } else if (inkAppPath.startsWith("./")) { |
| 237 | + // Node/npm bundle — resolve relative to the bundle location |
| 238 | + importPath = new URL(inkAppPath, import.meta.url).href; |
| 239 | + } else { |
| 240 | + // Dev mode — absolute filesystem path, cache-bust for Bun |
| 241 | + importPath = `${inkAppPath}?bridge=1`; |
| 242 | + } |
234 | 243 | const app = (await import(importPath)) as typeof import("./ink-app.js"); |
235 | 244 |
|
236 | 245 | const store = new WizardStore({ |
|
0 commit comments