项目文件夹

文件
2026-07-13 12:22:59 +08:00

10 KiB

Natives Binding Contract (JavaScript/TypeScript Side)

This document defines the JS/TS contract between @oh-my-pi/pi-natives callers and the loaded N-API addon.

Current package shape is direct-to-native: there is no packages/natives/src/<module> TypeScript wrapper layer. The public API is the generated packages/natives/native/index.d.ts declaration file, the ESM loader/export wrapper in packages/natives/native/index.js, and the Rust #[napi] exports in crates/pi-natives/src.

Implementation files

  • packages/natives/native/index.js
  • packages/natives/native/index.d.ts
  • packages/natives/native/loader-state.js
  • packages/natives/scripts/build-native.ts
  • packages/natives/scripts/gen-enums.ts
  • packages/natives/package.json
  • crates/pi-natives/src/lib.rs
  • Rust modules under crates/pi-natives/src/*.rs

Contract model

The contract has three parts:

  1. ESM runtime loader/export wrapper (native/index.js)
    • calls loadNative() from loader-state.js, which require(...)s the .node addon;
    • binds generated classes/functions as explicit named ESM exports;
    • emits enum runtime objects generated by scripts/gen-enums.ts.
  2. Generated TypeScript declarations (native/index.d.ts)
    • generated by napi-rs during scripts/build-native.ts;
    • declares exported functions, classes, object interfaces, and native enums;
    • is the package types entry.
  3. Rust N-API exports (crates/pi-natives/src)
    • #[napi] functions/classes/objects/enums are the source of generated declarations and runtime symbols;
    • snake_case Rust names become camelCase JavaScript names by napi-rs convention.

There is no current NativeBindings declaration-merging lifecycle and no full required-export list in the loader. Install/compiled loads do validate the package-version sentinel export; workspace-dev loads skip that check.

Public export surface organization

packages/natives/package.json exposes the package root only:

{
  "main": "./native/index.js",
  "types": "./native/index.d.ts",
  "exports": {
    ".": {
      "types": "./native/index.d.ts",
      "import": "./native/index.js"
    }
  }
}

Consumers in packages/coding-agent and packages/tui import directly from @oh-my-pi/pi-natives.

JS API ↔ native export mapping (representative)

Category Public JS API Rust source Return style
Grep grep(options, onMatch?) grep.rs Promise<GrepResult>
Grep search(content, options) grep.rs SearchResult
Grep hasMatch(content, pattern, ignoreCase?, multiline?) grep.rs boolean
Fuzzy path search fuzzyFind(options) fd.rs Promise<FuzzyFindResult>
Glob/workspace glob(options, onMatch?), listWorkspace(options) glob.rs, workspace.rs Promise<...>
Glob cache invalidateFsScanCache(path?) fs_cache.rs void
AST/block/summary astGrep(options), astMatch(options), astEdit(options), blockRangeAt(options), enclosingBlockBoundaries(options), summarizeCode(options) ast.rs, block.rs, summary.rs mixed
Shell executeShell(options, onChunk?) shell.rs Promise<ShellRunResult>
Shell new Shell(options?), shell.run(...), shell.abort() shell.rs class / promises
Shell applyBashFixups(command) shell.rs BashFixupResult
PTY new PtySession(), start/write/resize/kill pty.rs class / promises
Process Process.fromPid/fromPath, status/children/killTree/terminate/waitForExit ps.rs class / mixed
Keys parseKey, matchesKey, Kitty/legacy helpers keys.rs sync
Text wrapTextWithAnsi, truncateToWidth, sliceWithWidth, extractSegments, visibleWidth text.rs sync
Highlight highlightCode, supportsLanguage, getSupportedLanguages highlight.rs sync
HTML htmlToMarkdown(html, options?) html.rs Promise<string>
SIXEL encodeSixel sixel.rs sync
Snapcompact renderSnapcompactPng(text, options) snapcompact.rs sync
Clipboard copyToClipboard, readImageFromClipboard clipboard.rs sync / promise
Tokens countTokens(input, encoding?) tokens.rs sync
System/isolation detectMacOSAppearance, MacAppearanceObserver, MacOSPowerAssertion, getWorkProfile, iso* helpers appearance.rs, power.rs, prof.rs, iso.rs mixed

Sync vs async contract differences

The contract preserves Rust/N-API call style:

  • Promise-returning exports for worker-thread or async runtime work (grep, glob, fuzzyFind, astGrep, astMatch, astEdit, htmlToMarkdown, shell/PTY runs, isoStart/isoStop/isoDiff, clipboard image read, workspace scan).
  • Synchronous exports for deterministic in-memory transforms/parsers or direct system calls (search, hasMatch, highlighting, text utilities, token counting, process construction/status, copyToClipboard, encodeSixel, isolation probe/resolve helpers).
  • Constructor exports for stateful runtime objects (Shell, PtySession, Process, macOS observer/power handles).

Changing sync ↔ async for an existing export is a breaking public API change because consumers call these exports directly.

Object and enum typing patterns

Object patterns

#[napi(object)] Rust structs become TS interfaces, for example:

  • GrepResult, SearchResult, GlobResult, FuzzyFindResult
  • ShellRunResult, PtyRunResult, MinimizerResult
  • AstFindResult, AstReplaceResult, BlockRange, SummaryResult
  • System/media/isolation payloads such as ClipboardImage, WorkProfile, ParsedKittyResult, IsoResolveResult

Runtime shape correctness is owned by napi-rs and the Rust implementation.

Enum patterns

Native enums are represented in generated declarations and also emitted as runtime objects by scripts/gen-enums.ts, because napi-rs string enums are TS-only without explicit JS exports. Current enum objects include:

  • AstMatchStrictness
  • Ellipsis
  • Encoding
  • FileType
  • GrepOutputMode
  • IsoBackendKind
  • IsoChangeKind
  • KeyEventType
  • MacOSAppearance
  • ProcessStatus

Error behavior and caveats

  • Addon load failure or unsupported platform throws during package import from native/index.js.
  • The loader rejects install/compiled candidates that lack the package-version sentinel export. It does not verify the full export set after require(...); stale same-version or incomplete binaries surface as native load errors or missing members at use sites.
  • N-API conversion validates basic argument conversion, but TS optional fields do not guarantee semantic validity for untyped callers.
  • Numeric enum declarations do not prevent out-of-range numeric values from untyped callers unless the Rust function rejects them during conversion.
  • Callback exports use napi-rs ThreadsafeFunction shape: (error: Error | null, value) => void. Native code generally emits successful values; hard failures reject/throw through the owning call.

Maintainer checklist for binding changes

When adding/changing an export, update all of:

  1. Rust #[napi] implementation in the owning crates/pi-natives/src/<module>.rs.
  2. crates/pi-natives/src/lib.rs if a new module is added.
  3. Any consumer imports/callsites in packages/coding-agent or packages/tui.
  4. Build output by running the natives build so native/index.d.ts and native/index.js stay in sync.
  5. scripts/gen-enums.ts if enum runtime export patching needs to change.

Do not add a parallel TS wrapper convention unless the package design intentionally moves back to wrappers; current consumers depend on the direct generated API.