Skip to content

-I library include roots for build and run, and an m0 session example - #120

Merged
codetalcott merged 2 commits into
mainfrom
claude/stoic-volta-z29i0t
Sep 26, 2026
Merged

codetalcott merged 2 commits into
mainfrom
claude/stoic-volta-z29i0t

Conversation

@codetalcott

Copy link
Copy Markdown
Owner

Two commits, reviewable separately. The second can be dropped without affecting the first.

1. feat(cli): -I library include roots for build and run

napi-mojo build and run passed exactly one -I (the framework's), so nothing built through the CLI could import another Mojo library.

  • -I <dir> is now repeatable on both commands.
  • Library roots always go to the compiler after the framework root. Mojo takes the first root holding a package of the imported name, silently, so a library placed first could replace napi.
  • Refused with exit 1:
    • a root that is missing or not a directory;
    • the framework root passed again, or any root given twice;
    • a root holding its own napi (directory, .mojopkg or .mojoc).
  • Warned: a root that is itself a package, since -I wants its parent directory.

run's build cache now hashes every root, in order. Writing those tests showed the cache was already incomplete:

  • its walk skipped symlinked directories;
  • it hashed only .mojo files.

So a library symlinked beside the entry (the obvious workaround before -I) kept serving the old binary after every edit behind the link. The walk now:

  • follows symlinks, with a realpath cycle guard;
  • also hashes .🔥, .mojopkg and .mojoc;
  • skips node_modules.

The compiler's version is still not in the key. CLAUDE.md now says so instead of calling the input set complete.

Tests

  • tests/cli.test.js: 19 new tests drive the CLI with a stub compiler passed via --mojo, which records each compile. Root order and every cache decision are tested without a Mojo toolchain.
  • Each rule was reverted once and its test failed, 15 of 15. The entry-tree symlink test also fails on main as it stands.
  • New step in the required test job, "Host-mode program with library include roots", does a real compile:
    • an import resolved through -I;
    • a cache hit;
    • an edit reaching the next run, both through the root and through a symlink beside the entry.

2. examples: m0-session, an existing Mojo library compiled through -I

m0 is mojo-http's application framework, and its wheel ships the framework's Mojo source. examples/m0-session/lib.mojo compiles m0's session and grant code, unchanged, into an addon with napi-mojo build -I <wheel>/m0/_mojo. A Node service beside an m0 app can then check m0 cookies and stream grants with m0's own code. m0 0.2.0 and the framework compile together on Mojo 1.1.0, --Werror-clean, in about 9 s.

parity.mjs runs under node --test and is named so Jest never collects it. It judges the addon against an issuer written on node:crypto, after first proving that issuer reproduces mojo-http's pinned vectors. It checks:

  • those pinned vectors;
  • 300 seeded random grants and sessions with non-ASCII keys and cookies, with issueSession byte-equal to the oracle;
  • every refusal class, and m0's refusal messages compared whole;
  • that text crosses as its UTF-8 bytes: a string past 256 bytes, an embedded NUL, a lone surrogate.

Eight reverted rules each fail it: three on the oracle side, five on the addon side. A build without the -I root fails naming m0_core.

The new m0-interop job is non-required and PR-only, like runtimes.

  • It downloads the wheel from PyPI at a pinned M0_VERSION, so neither repo gains a build-time dependency on the other.
  • Before compiling, it checks the wheel's gated_mojo against mojo --version, so a drifted pin fails with a message instead of a compile error.

The example ships in the npm package as a template, minus its build output (!examples/m0-session/build). Deviation from the plan: the hash exports take strings, not a Uint8Array. m0_core's public fnv1a/xxhash32 take String; the byte-pointer forms are underscore-private.

Checked locally (Linux, pinned pixi toolchain)

  • build.sh; npm test: 835 passed, 12 skipped.
  • The new required step's script: 4 compiles, 21 s.
  • Every run: block of the new m0-interop job, taken from the YAML: all pass. Its mismatch branch exits 1 with its message.
  • check-doc-samples, generate-rules-extract --check, sync-versions --check, generate-api-reference --check (in pixi).
  • npm pack --dry-run ships examples/m0-session/{lib.mojo,parity.mjs,README.md} only.

Not checked locally: macOS. CI's required macOS job covers the stub tests and the new e2e step.

🤖 Generated with Claude Code

https://claude.ai/code/session_013tXV7MkRfP97g5hPfNNXHC


Generated by Claude Code

`napi-mojo build` and `run` passed exactly one -I, the framework's, so
nothing built through the CLI could import another Mojo library. -I <dir>
is now repeatable on both, and every root goes to the compiler AFTER the
framework's: Mojo takes the first root holding a package of the imported
name and says nothing about the rest, so a library placed first could
replace `napi` silently. For the same reason a root holding its own
`napi` (directory, .mojopkg or .mojoc) is refused rather than ordered,
along with a root that is missing, not a directory, the framework root
again, or given twice. A root that is itself a package gets a warning
that -I wants its parent.

`run`'s build cache now hashes every root, in order. Writing the tests
for that found the cache was already incomplete: its walk skipped
symlinked directories and hashed only .mojo files, so a library
symlinked beside the entry — the obvious workaround before -I — served
the old binary after every edit behind the link. The walk now follows
symlinks behind a realpath cycle guard, hashes .🔥, .mojopkg and .mojoc
too, and skips node_modules (Mojo never imports through it, and pnpm's
symlinks would otherwise walk every package). The compiler's version is
still not in the key; CLAUDE.md now says so instead of calling the set
complete.

tests/cli.test.js drives the CLI with a stub compiler passed through
--mojo, which records each compile, so the order of roots and every
cache decision are tested with no toolchain. Each rule was reverted once
and its test failed (15 of 15). The new "Host-mode program with library
include roots" step proves the same with a real compile: an import
resolved through -I, a cache hit, and an edit reaching the next run
through the root and through a symlink beside the entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tXV7MkRfP97g5hPfNNXHC
m0 is mojo-http's application framework; its PyPI wheel ships the
framework's Mojo source under m0/_mojo. examples/m0-session/lib.mojo
compiles m0's session and grant code into an addon with
`napi-mojo build -I <wheel>/m0/_mojo`, unchanged, so a Node service beside
an m0 application can check m0's cookies and stream grants with m0's own
code rather than a JavaScript copy of the format kept in step by hand.
m0 0.2.0 and the framework compile together on Mojo 1.1.0 with --Werror
clean, in about 9 s.

parity.mjs (node --test; named so Jest never collects it) judges the
addon with an issuer written on node:crypto, the role grant.py and
notes_session.py play in mojo-http, after first proving that oracle
reproduces mojo-http's pinned vectors. It then checks those vectors, 300
seeded random grants and sessions with non-ASCII keys and cookies
(issueSession byte-equal to the oracle), every refusal class, m0's own
refusal messages whole, and that text crosses as its UTF-8 bytes past the
256-byte fast path, with a NUL and with a lone surrogate. Eight reverted
rules, three on the oracle's side and five on the addon's, each fail it;
a build without the -I root fails naming m0_core.

The new m0-interop job is non-required and PR-only, like runtimes. It
downloads the wheel from PyPI at a pinned M0_VERSION, so neither repo gains
a build-time dependency on the other, and checks the wheel's gated_mojo
against `mojo --version` before compiling, so a drifted pin fails with a
sentence. The example ships in the npm package as a template, minus its
build output.

The hashes take strings rather than a Uint8Array: m0_core's public
fnv1a/xxhash32 are String-based, and the byte-pointer forms are
underscore-private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013tXV7MkRfP97g5hPfNNXHC
@codetalcott
codetalcott marked this pull request as ready for review September 26, 2026 15:43
@codetalcott
codetalcott merged commit 6dbf7e6 into main Sep 26, 2026
11 checks passed
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.

2 participants