Skip to content

Node.js support - #939

Closed
justjake wants to merge 58 commits into
anomalyco:mainfrom
justjake:justjake--node-compat-static
Closed

justjake wants to merge 58 commits into
anomalyco:mainfrom
justjake:justjake--node-compat-static

Conversation

@justjake

@justjake justjake commented Apr 10, 2026 •

Copy link
Copy Markdown
Contributor

Partially addresses #2

This PR brings Node.js compatibility. Bun-only imports and APIs have been replaced by compatibility interface modules, which pick a runtime-appropriate implementation. Most codepaths when running in bun are the same as before the PR.

Node.js differenes

  • bun:ffi -> @opentui/core/compat/ffi: I re-implemented relevant bun:ffi APIs on top of koffi. There's more copying than in bun, and I'm unsure about correctness, but I think it's a worthwhile start. The upcoming version of koffi offer capabilities much closer to bun, but I'm not sure when the author will release it.
  • bun:test -> @opentui/core/compat/test: I know the AGENTS.md says no vitest, but vitest is the most popular Node.js test runner, and its api is very similar to bun:test. We use vite alias to replace bun:test with the compat module based on vitest. Source code still says "bun:test" because "bun:test" only works when your test file directly imports it.
    • to avoid snapshots from different test runners thrashing, we create independent snapshots for nodejs. Perhaps this is avoidable somehow? I didn't investigate.
  • globalThis.Worker -> @opentui/core/compat/Worker. Node.js doesn't have a global worker, and its worker_threads worker isn't web-compatible without this shim. Warning: there's a bit of weird node module loader shenanigans here to work around node not understanding import "./foo.js" means import "./foo.ts", but it's pretty benign for opentui's use.
  • bun-ffi-structs: oh boy. I ended up having codex vendor this because module-loader tricks won't work once we're bundled, and it wasn't immediately obvious how to do as a build step. Bun runtime still uses upstream, nodejs gets this hack.
  • Bun.spawnSync -> @opentui/core/compat/testHelpers. This is only used in test, so I put in own file to avoid bundling it.
  • Bun.* -> opentui/core/compat/runtime: Re-implemented Bun.sleep, Bun.writeFile, and installed the typical npm packages for Bun.stringWidth, Bun.stripANSI
  • import ... with { ... } -> new URL(..., import.meta.url). These were pretty mechanically easy to switch to the more universal option.

Testing

Test scripts:

  • test:bun: the old bun run test | bun run test:js. Unit tests w/ bun:test.
  • test:nodejs: npx vitest run. Maybe I could use bunx but I want to run under node!
  • test:dist: New type of integration test that tests that the npm pack'd artifact actually works in a few very simple example projects. Node is not as forgiving as Bun, so this caught a few goofs that don't show up in vitest or bun test. For example, for some reason Bun@1.2 would emit invalid bundles with duplicate exports, detected by this test. Another case was needing to import react-reconciler/something.js w/ the extension. ¯_(ツ)_/¯ node.

Thanks

This is a cool project, and thank you in advance for your attention to this matter.

Let me know if you'd prefer I split up the changes. I personally prefer to review 1 big pr rather than 10 small prs without context, but I know its not the common opinion. Lots of those lines of code are duplicate snapshots!

@justjake
justjake force-pushed the justjake--node-compat-static branch from e7ed943 to c7ebd25 Compare April 11, 2026 08:43
@justjake

justjake commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Implemented bun:ffi ptr and toArrayBuffer as a Node.js native module unsafe-pointer, npm. Includes prebuilds for linux-${arm64 | x64}-${glibc | musl}, windows-${arm64 | x64}, darwin-${arm64 | x64}. All builds done in Github Actions using zig cc cross compilation, tested on the target platform, then published to NPM with attestation w/ OIDC connect auth. It's about 500 lines of simple c.

With this, compat/nodejs/ffi.ts is now zero-copy and less complex.

@justjake

Copy link
Copy Markdown
Contributor Author

We can port from koffi and node-unsafe-pointer to node:ffi once it's stable and upstream merges this PR: nodejs/node#62762

Is there anything I can do to make this PR easier to review or merge?

@simonklee

Copy link
Copy Markdown
Member

Is there anything I can do to make this PR easier to review or merge?

We have a call with the Node.js/Platformatic people that are working on the node ffi things next week. I'll probably have a look at this around that time. For now its mostly a priorities kind of thing.

@MBearo

MBearo commented May 13, 2026

Copy link
Copy Markdown

Is there anything I can do to make this PR easier to review or merge?

We have a call with the Node.js/Platformatic people that are working on the node ffi things next week. I'll probably have a look at this around that time. For now its mostly a priorities kind of thing.

What is the current progress on this?

@simonklee

Copy link
Copy Markdown
Member

Is there anything I can do to make this PR easier to review or merge?

We have a call with the Node.js/Platformatic people that are working on the node ffi things next week. I'll probably have a look at this around that time. For now its mostly a priorities kind of thing.

What is the current progress on this?

#1055

@justjake

Copy link
Copy Markdown
Contributor Author

@MBearo

MBearo commented May 25, 2026

Copy link
Copy Markdown

@justjake justjake closed this Jun 19, 2026
@justjake

Copy link
Copy Markdown
Contributor Author

Closing as upstream has merged Node 26 support.

@kommander or @simonklee, are you interested in a more reasonably scoped PR to add FFI support for older node? We've moved several Notion internal tools from Ink to my node22 compat fork, works great!

@remorses

Copy link
Copy Markdown
Contributor

I think making opentui work in older Node.js versions would be great. It's the only way to ship TUIs on npm that can work for most users without bundling Node.js 26

felixonmars added a commit to felixonmars/archriscv-packages that referenced this pull request Sep 12, 2026
Build the missing OpenTUI 0.4.5 RISC-V renderer with Zig 0.15.2 and
install it under /usr/lib/opencode.

Use Koffi for native calls and callbacks because Bun's interpreter-only
RISC-V runtime rejects its engine FFI with "bun:ffi requires the JIT".
Preserve pointer and integer representations, callback ownership and raw
memory access. Fix the bundled struct code treating RISC-V pointers as
32-bit.

Build Koffi 3.2.1 from source with rv64gc/lp64d for C, C++ and assembly.
Its prebuilt addon executes RVV instructions during module initialization,
causing "Illegal instruction" while loading the embedded .node module on
baseline CPUs. Drop the prebuilt dependency and embed the locally built
addon through a static import. Keep its libgcc/libstdc++ dependencies and
libopentui.so on disk because Koffi cannot resolve Bun's virtual filesystem.

Copy the addon into packages/opencode/node_modules/@opentui/core for
Bun's isolated workspace layout. The root node_modules destination is
absent, making build() fail with "No such file or directory" after Koffi
has compiled successfully.

No upstream fix for the prebuilt CPU mismatch was found. Koffi documents
source builds, and OpenTUI's earlier Node.js proposal provides precedent
for the backend.
https://koffi.dev/contribute#build-from-source
oven-sh/bun#35246
anomalyco/opentui#939
anomalyco/opentui#1445
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants