Skip to content

ink-as-compilePackages — Phase B follow-ups after #348 CJS support landed #360

@proggeramlug

Description

@proggeramlug

Tracks the next set of work after PR #359 (issue #348 Phase A — native CJS support for compilePackages) lands. CJS unlocked the React-class blocker; this issue captures the remaining gaps between today (46/67 modules native on the ink sandbox) and interactive ink working end-to-end. Each sub-item below is small enough to be its own issue if anyone wants to pick one up — file as separate issues if scope justifies, or fold into umbrella PRs.

1. Cross-module CJS-call return-value typeof bug

The smallest concrete bug I observed during #348 verification. The repro:

import { createContext } from "react";
const Ctx = createContext(null);
console.log(typeof Ctx);
  • Node: object (correct — createContext returns { Provider, Consumer, $$typeof, ... })
  • Perry: function (wrong)

createContext itself is typeof === "function" correctly. The bug is in how Perry handles the return value of a cross-module call into a CJS-wrapped function. Likely candidates: the wrapped export export const createContext = _cjs.createContext resolves at codegen to a function reference rather than a callable that returns a value, so the return value inherits the function's typeof. Or the NaN-box tag on the return value isn't being decoded correctly when the call goes through the IIFE's bound exports.

Acceptance: the three-line repro above prints object matching Node.

2. process import resolution from inside node_modules

Surfaced repeatedly in #348's scoping pass:

Warning: Could not resolve import 'process' from render.js
Warning: Could not resolve import 'process' from ink.js
Warning: Could not resolve import 'process' from reconciler.js
Warning: Could not resolve import 'process' from App.js
... (8 sites in ink alone)

process.argv / process.env / process.stdout work fine when accessed as the implicit global from user code. But ink's modules do import process from "node:process" (or the equivalent bare specifier import process from "process") explicitly. Perry's stdlib already implements the process surface; it just needs the import path to map to it from inside compilePackages targets.

Acceptance: the eight Could not resolve import 'process' warnings disappear from the ink sandbox compile.

3. Add ink's transitive deps to compilePackages

After #348 lands, the ink sandbox compile shows 46/67 modules native, 21 still on V8 fallback. The 21 are ink's transitive deps that aren't in the user's compilePackages list:

  • chalk (ANSI colors — pure ESM, should be a clean port)
  • scheduler (React's scheduler primitives — CJS, should benefit from Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348)
  • react-reconciler (the React fiber reconciler that ink uses — CJS, large surface)
  • yoga-layout (flexbox engine — ~3.2.1, native-binding-or-WASM)
  • cli-cursor / cli-truncate / cli-boxes / slice-ansi / string-width / wrap-ansi / widest-line / ansi-escapes / ansi-styles / indent-string / auto-bind / signal-exit / stack-utils / code-excerpt / is-in-ci / patch-console / es-toolkit / type-fest / @alcalzone/ansi-tokenize
  • ws (websocket — odd dep for ink; probably for devtools panel)

Two paths:

  • (a) Document a package.json recipe — "to use ink, add these N packages to compilePackages". Manual but works today.
  • (b) Auto-include transitive deps of compilePackages entries. More magic, but matches the user expectation that opting one package into native compile pulls its dep tree along. Open question: should this be opt-in (compilePackagesTransitive: true) or default? Default opens up more failure modes; opt-in is safer.

The hardest dep here is yoga-layout — it ships C++ via WASM bindings (yoga-wasm-web) or native bindings. Either path needs separate evaluation:

  • Compile the JS port of yoga via compilePackages (does one exist that's pure JS?)
  • Link libyoga natively (cross-compile story for every Perry target)
  • Implement a Perry-native flexbox layout engine (largest, lowest leverage)

Acceptance: the ink sandbox compile reports Found N module(s): N native, 0 JavaScript for an interactive non-trivial ink program (counter component).

4. Dynamic import() for ink's devtools

Warning: Dynamic import('./devtools.js') not fully supported, returning undefined

ink does runtime import(\"./devtools.js\") to lazy-load its devtools panel. Two options:

  • (a) Wrap the call in try/catch + treat undefined as "devtools off" (works today, lossy).
  • (b) Wire dynamic-import support in the native compiler — non-trivial but unblocks any package using lazy-loading.

The (a) workaround is fine for #348's MVP; (b) is its own issue.

Acceptance: the warning disappears, OR ink runs without it firing.

5. Bare-identifier downstream noise

Warning: unknown identifier 'props' / 'style' / 'customId' / 'autoFocus' / 'isActive' / 'showCursor'

I didn't fully classify these in the #348 scoping pass — possibly downstream symptoms of React not loading (which is now fixed by #359), or independent destructuring issues. Worth re-running the ink compile post-#359-merge and seeing which warnings remain. If they're gone, close. If they persist, file individually.

Acceptance: rerun ink compile post-#359, decide per-warning whether each is a real gap or downstream noise.

6. process.env.NODE_ENV static branch evaluation

#348's CJS wrap hoists both branches of if (process.env.NODE_ENV === 'production') { module.exports = require('./X') } else { module.exports = require('./Y') } as ESM imports. Both files compile, only one runs at module init — wasteful but correct. Cleaning this up would let react.development.js (the heavier branch) skip native compilation entirely when building production binaries.

Conditional source-level constant folding before the wrap, or a lazy import() fallback with compile-time env resolution. Lower priority than 1–4.

Acceptance: in a Perry-compiled production binary, react.development.js is not in the Found N module(s) list.

7. #347 — TUI primitives (raw stdin, readline, setRawMode)

Hard dependency for interactive ink. Already filed as #347, called out here for traceability. Until #347 ships at least its Phase 1 (line-buffered readline) and Phase 2 (raw-mode + keypress events), useInput / useApp can't function and ink is render-only.

Order of operations

  1. PR feat(compile): #348 Phase A — native CommonJS support for compilePackages #359 lands → 46/67 modules native. ✅
  2. Support custom menu bar items #1 typeof bug → small, isolated, unlocks confidence in CJS interop.
  3. linux compilation and README #2 process import → disappears 8 warnings, low-risk.
  4. Using the fetch API #3 transitive deps strategy decision → write up the (a) vs (b) tradeoff, pick one. Yoga is its own sub-issue regardless.
  5. Using the fetch API #3 yoga sub-issue → independently scope; this is the next Big Rock after Compile ink (React-based TUI framework) end-to-end via perry.compilePackages #348.
  6. fetch linker error on macOS ARM64 with perry-react — _js_fetch_with_options not found #5 re-classification → 5 minutes of re-running compile after feat(compile): #348 Phase A — native CommonJS support for compilePackages #359 merges.
  7. useEffect + setState panics with RefCell already borrowed on macOS ARM64 #4 dynamic import workaround → patch ink locally with try/catch around the devtools import as a stopgap.
  8. TUI gap: readline + tty.setRawMode + raw-mode stdin reader #347 Phase 1+2 → unblocks interactivity. Already its own tracking issue.
  9. End-to-end verification → counter + todo list ink examples compile, run, match Node.

Phase B definition of done: ink's useInput example compiles to a single-file native binary, runs interactively (keystrokes increment/decrement a counter), and matches Node modulo terminal cursor timing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions