跳转到内容

原生绑定契约(TypeScript 侧)

本文档定义了 @f5-sales-demo/pi-natives 调用方与已加载的 N-API 插件之间的 TypeScript 侧契约。

主要聚焦于三个部分:

  1. 契约形状(NativeBindings + 模块增强),
  2. 包装器行为(src/<module>/index.ts),
  3. 公共导出表面(src/index.ts)。
  • packages/natives/src/bindings.ts
  • packages/natives/src/native.ts
  • packages/natives/src/index.ts
  • packages/natives/src/clipboard/types.ts
  • packages/natives/src/clipboard/index.ts
  • packages/natives/src/glob/types.ts
  • packages/natives/src/glob/index.ts
  • packages/natives/src/grep/types.ts
  • packages/natives/src/grep/index.ts
  • packages/natives/src/highlight/types.ts
  • packages/natives/src/highlight/index.ts
  • packages/natives/src/html/types.ts
  • packages/natives/src/html/index.ts
  • packages/natives/src/image/types.ts
  • packages/natives/src/image/index.ts
  • packages/natives/src/keys/types.ts
  • packages/natives/src/keys/index.ts
  • packages/natives/src/ps/types.ts
  • packages/natives/src/ps/index.ts
  • packages/natives/src/pty/types.ts
  • packages/natives/src/pty/index.ts
  • packages/natives/src/shell/types.ts
  • packages/natives/src/shell/index.ts
  • packages/natives/src/system-info/types.ts
  • packages/natives/src/system-info/index.ts
  • packages/natives/src/text/types.ts
  • packages/natives/src/text/index.ts
  • packages/natives/src/work/types.ts
  • packages/natives/src/work/index.ts

packages/natives/src/bindings.ts 定义了基础契约:

  • NativeBindings(基础接口,当前包含 cancelWork(id: number): void
  • CancellabletimeoutMs?: numbersignal?: AbortSignal
  • TsFunc<T> N-API 线程安全回调所使用的回调函数签名

每个模块通过声明合并添加自己的字段:

// packages/natives/src/<module>/types.ts
declare module "../bindings" {
interface NativeBindings {
grep(options: GrepOptions, onMatch?: TsFunc<GrepMatch>): Promise<GrepResult>;
}
}

这使得在没有单一庞大中央类型文件的情况下,维护一个聚合绑定接口。

  • bindings.ts 提供基础 NativeBindings 符号。
  • 每个 src/<module>/types.ts 增强 NativeBindings
  • src/native.ts 为了副作用导入所有 ./<module>/types 文件,使合并后的契约在使用 NativeBindings 的位置处于作用域内。

状态转换:基础契约合并契约

  • src/native.ts 加载候选的 .node 二进制文件。
  • 加载的对象被视为 NativeBindings 并立即通过 validateNative(...) 进行验证。
  • validateNative 通过 typeof bindings[name] === "function" 验证所需的导出键。

状态转换:不可信的插件对象已验证的原生绑定对象(或硬失败)。

  • src/<module>/index.ts 中的模块包装器调用 native.<export>
  • 包装器适配默认值和回调签名(将 (err, value) 转换为 JS API 中仅传值的回调模式)。
  • src/index.ts 将模块包装器/类型作为公共包 API 重新导出。

状态转换:已验证的原始绑定符合人体工程学的公共 API

包装器刻意保持精简;它们不重新实现原生逻辑。

主要职责:

  • 参数规范化/默认值设置
    • glob()options.path 解析为绝对路径,并为 hiddengitignorerecursive 设置默认值。
    • hasMatch() 在调用原生函数前填充默认标志(ignoreCasemultiline)。
  • 回调适配
    • grep()glob()executeShell()TsFunc<T>error, value)转换为用户回调,仅接收成功的值。
  • 围绕原生调用的环境或策略行为
    • 剪贴板包装器添加 OSC52/Termux/无头环境处理,并将复制操作视为尽力而为。
  • 公共命名与重新导出策划
    • searchContent() 映射到原生导出 search

packages/natives/src/index.ts 是规范的公共桶文件。它按功能域分组导出:

  • 搜索/文本:grepglobtexthighlight
  • 执行/进程/终端:shellptypskeys
  • 系统/媒体/转换:imagehtmlclipboardsystem-infowork

维护者规则:如果一个包装器没有从 src/index.ts 重新导出,则它不属于预期的公共包表面。

JS API ↔ 原生导出映射(代表性示例)

Section titled “JS API ↔ 原生导出映射(代表性示例)”

Rust 侧使用 N-API 导出名称(通常通过 #[napi] 将 snake_case 转换为 camelCase,偶尔使用显式别名),这些名称必须与这些绑定键匹配。

类别公共 JS API(包装器)原生绑定键返回类型是否异步?
Grepgrep(options, onMatch?)grepPromise<GrepResult>
GrepsearchContent(content, options)searchSearchResult
GrephasMatch(content, pattern, opts?)hasMatchboolean
GrepfuzzyFind(options)fuzzyFindPromise<FuzzyFindResult>
Globglob(options, onMatch?)globPromise<GlobResult>
GlobinvalidateFsScanCache(path?)invalidateFsScanCachevoid
ShellexecuteShell(options, onChunk?)executeShellPromise<ShellExecuteResult>
ShellShellShell类构造函数N/A
PTYPtySessionPtySession类构造函数N/A
TexttruncateToWidth(...)truncateToWidthstring
TextsliceWithWidth(...)sliceWithWidthSliceWithWidthResult
TextvisibleWidth(text)visibleWidthnumber
HighlighthighlightCode(code, lang, colors)highlightCodestring
HTMLhtmlToMarkdown(html, options?)htmlToMarkdownPromise<string>
SystemgetSystemInfo()getSystemInfoSystemInfo
WorkgetWorkProfile(lastSeconds)getWorkProfileWorkProfile
ProcesskillTree(pid, signal)killTreenumber
ProcesslistDescendants(pid)listDescendantsnumber[]
ClipboardcopyToClipboard(text)copyToClipboardPromise<void>(尽力而为的包装器行为)
ClipboardreadImageFromClipboard()readImageFromClipboardPromise<ClipboardImage | null>
KeysparseKey(data, kittyProtocolActive)parseKeystring | null

契约混合了同步和异步 API;包装器保留原生调用风格,而非强制统一模型:

  • 基于 Promise 的异步导出用于 I/O 或长时间运行的工作(grepglobhtmlToMarkdownexecuteShell、剪贴板、图像操作)。
  • 同步导出用于确定性的内存内转换/解析器(searchhasMatch、高亮、文本宽度/切片、按键解析、进程查询)。
  • 构造函数导出用于有状态的运行时对象(ShellPtySessionPhotonImage)。

对维护者的影响:更改现有导出的同步 ↔ 异步模式是跨包装器和调用方的破坏性 API 和契约变更。

对象模式(#[napi(object)] 风格的 JS 对象)

Section titled “对象模式(#[napi(object)] 风格的 JS 对象)”

TypeScript 将对象形状的原生值建模为接口,例如:

  • GrepResultSearchResultGlobResult
  • SystemInfoWorkProfile
  • ClipboardImageParsedKittyResult

这些是编译时的结构化契约;运行时形状的正确性由原生实现负责。

数值型原生枚举在 TypeScript 中表示为 const enum 值:

  • FileType1=file2=dir3=symlink
  • ImageFormat0=PNG1=JPEG2=WEBP3=GIF
  • SamplingFilterEllipsisKeyEventType

调用方看到命名的枚举成员;绑定边界传递的是数字。

不匹配检测在两个层面进行:

  1. 编译时 TypeScript 契约检查

    • 包装器针对合并后的 NativeBindings 调用 native.<name>
    • 缺失/重命名的绑定键会导致包装器中的 TS 类型检查失败。
  2. validateNative 中的运行时验证

    • 加载后,native.ts 检查所需的导出,如果缺失则抛出异常。
    • 错误消息包含缺失的键和重新构建的指示。

这捕获了常见的二进制文件过期漂移:包装器/类型存在但加载的 .node 缺少该导出。

  • 插件加载失败或不支持的平台会在 native.ts 的模块初始化期间抛出异常。
  • 缺少所需导出会在包装器可用之前抛出异常。

效果:包快速失败,而非将失败推迟到首次调用时。

  • 某些包装器有意地软化失败(copyToClipboard 是尽力而为的,会吞掉原生失败)。
  • 流式回调忽略回调的错误载荷,仅转发成功的值事件。

类型级别的注意事项(运行时比 TS 更严格)

Section titled “类型级别的注意事项(运行时比 TS 更严格)”
  • TS 可选字段不能保证语义有效性;原生层仍然可以拒绝格式错误的值。
  • const enum 类型不能阻止来自运行时未类型化调用方的超出范围的数值。
  • validateNative 仅检查所需导出的存在性/是否为函数,不检查深层的参数/返回值形状兼容性。
  • bindings.ts 在基础接口中包含 cancelWork(id),但当前的运行时验证列表并未强制检查该键。

添加/更改导出时,需更新以下所有内容:

  1. src/<module>/types.ts(增强 + 契约类型)
  2. src/<module>/index.ts(包装器行为)
  3. src/native.ts 中的模块类型导入(如果是新模块)
  4. validateNative 所需导出检查
  5. src/index.ts 公共重新导出

跳过任何步骤都会导致编译时漂移或运行时加载失败。