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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions