mcpField
M
moonbit-js-binding

moonbit-js-binding

github.com/mizchi开发41655 次安装1 周前更新
代码仓库

关于 moonbit-js-binding

通过 MoonBit 的 JS 后端连接 JavaScript API 的分步技能,指导开发者处理 FFI 声明、类型映射、Promise 异步桥接和软件包输出配置。

主要功能

  1. 01映射 JavaScript 与 MoonBit 类型
  2. 02声明 JS FFI 函数
  3. 03导入具名模块导出
  4. 04桥接 Promise 与异步调用
  5. 05配置 MoonBit JS 软件包输出
  6. 06处理 null 与 undefined

适用场景

  1. 01绑定浏览器 DOM 或 Node.js API
  2. 02为 MoonBit 封装 npm 软件包函数
  3. 03配置 ESM、CJS 或 IIFE 输出
  4. 04生成 TypeScript 声明文件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mizchi/skills --skill moonbit-js-binding

安装后在 Claude Code 中输入/moonbit-js-binding确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
assets/js_binding_proj/
moon.mod
assets/js_binding_proj/src/
async_test.mbt
async.mbt
cross_target_test.mbt
cross_target.mbt
ffi.mbt
lib_test.mbt
lib.mbt
modules_test.mbt
modules.mbt
moon.pkg
references/
cfg-and-target-gating.md
error-handling.md
interop-patterns.md
promise-bridging.md
typescript-integration.md

详细介绍

MoonBit JS 绑定指南

通过 js 后端将 JavaScript API(浏览器 DOM、Node 内置模块、npm 包)绑定到 MoonBit 的分步工作流。

已针对 MoonBit v0.10.12 和 moonbitlang/async 0.21.0 验证。

适用场景

在以下情况下使用此技能:

  • 为 JS API 添加 extern "js" fn 声明
  • 嵌入内联 JS(#| (x) => ...),或按模块名和名称导入(= "Math" "max")
  • 使用 link.js 导出和 format: esm/cjs/iife 配置 moon.pkg
  • 使用 #external pub type 和 %identity 转换包装 JS 对象
  • 使用 async fn + %async.suspend + %async.run 桥接 Promise
  • 将 undefined / null 分别映射到 MoonBit Option
  • 通过 require(...) 或 ESM 导入使用 npm 包
  • 为 TypeScript 使用者生成 .d.ts

计划模式说明

如果你处于计划模式,并且正在为 moonbit-js-binding 工作创建计划:

  1. 在最终计划文件开头加入明确指示,要求执行 Agent 在开始实现前阅读 moonbit-js-binding 技能。
  2. 指示内容应如下:
    **Before starting implementation:** Use the Skill tool to load the moonbit-js-binding skill, which provides comprehensive guidance on `extern "js"` declarations, JS value wrapping, Promise bridging, and moon.pkg export configuration.
    

类型映射

编写声明前,先将 JS 类型映射到 MoonBit 类型。

JavaScriptMoonBit说明
booleanBool直接映射
number(整数)Int、UIntJS 使用 IEEE-754;较大的 Int64 会失去精度,改用 BigInt
number(浮点数)Float、Double直接映射
bigintBigInt直接映射
stringString直接映射(两者都使用 UTF-16)
Uint8ArrayBytes、FixedArray[Byte]直接映射,无需复制
Array<T>Array[T]、FixedArray[T]直接映射
FunctionFuncRef[(...) -> R] 或闭包直接映射
any(不透明类型)#external pub type JsValue使用名义类型包装
T | undefined通过 is_undefined 检查映射为 T?参见 Null/Undefined 部分
T | nullNullable[T] 包装类型null 不等于 undefined
Promise<T>#external pub type Promise[T]使用 async fn/wait() 桥接
常量 enumnumberpub(all) enum E { A; B }

警告 — Number 精度: JavaScript 没有整数类型。跨越 FFI 边界的值会舍入为 IEEE-754 双精度数。|x| < 2^53 时使用 Int 没问题;超过此范围时请使用 BigInt。UInt64 / Int64 目前会映射为 JS number,超过 2^53 的部分会丢失精度。

警告 — Trait 对象与 MoonBit 枚举: 将 Map[K, V]、Result[_]、Json 或非 #external trait 对象传递过 FFI 边界,会暴露 MoonBit 的内部运行时结构。请先转换为普通结构体、JsValue 或基本类型。

工作流

按顺序执行以下 4 个阶段。

阶段 1:项目设置

配置模块和软件包以生成 JS 输出。

模块(moon.mod) — 设置 preferred_target,使 moon check、moon build、moon test 默认使用 js。从 v0.10.9 起,编译器自身的默认目标是 wasm,因此不设置的话,每条命令都需要加上 --target js:

name = "user/pkg"

version = "0.1.0"

source = "src"

preferred_target = "js"

import {
  "moonbitlang/[email protected]",
}

JSON 格式的 moon.mod.json(使用带连字符的 preferred-target、source 和 deps 键)已弃用,计划移除。moon fmt 会将现有项目迁移到 DSL。

软件包(src/moon.pkg) — 限定 .mbt 文件仅用于 js 后端,并配置链接输出。

文件格式 — moon.pkg(DSL)与 moon.pkg.json(JSON): MoonBit 仍可读取这两个文件名,但 JSON 支持已弃用,计划移除。本技能全程使用 DSL 格式的 moon.pkg(无扩展名)。请勿编写 moon.pkg.json;它采用不同语法(纯 JSON,不使用 options(...) 包装,也没有尾随逗号)。在项目中混用两种格式会导致“Unable to read moon.pkg”解析错误。如果你克隆的是使用旧版 .pkg.json 的模板,请运行 moon fmt 进行转换。 targets: — 仅后端特定文件需要: targets: 块将单个 .mbt 文件限定为部分后端。包含 extern "js"、extern "c" 或后端特定 #cfg 块的文件必须列出。纯 MoonBit 文件(不含 FFI)不需要 targets: 条目——如果包中的所有文件都是纯 MoonBit,则完全省略 targets: 块。上面的示例列出了 ffi.mbt / async.mbt,因为它们包含 extern "js";不含 FFI 的简单库只需要 link: { ... }。

import {
  "moonbitlang/async",
} for "test"

options(
  targets: {
    "ffi.mbt": ["js"],
    "async.mbt": ["js"],
    "async_test.mbt": ["js"],
  },
  link: {
    "js": {
      "exports": ["add", "greet"],
      "format": "esm",
    },
  },
)

关键字段:

字段用途
targets将 .mbt 文件限定为 ["js"]。仅支持 JS 的 FFI 文件必须受此限制,否则会导致其他后端出错。
link.js.exports列出要从输出模块重新导出的公共函数。使用 "name:alias" 可重命名。
link.js.format"esm"(ES 模块,现代打包工具的默认格式)、"cjs"(Node require)或 "iife"(浏览器脚本标签)。
import ... for "test"moonbitlang/async 仅在测试中需要(async test)——不要将其加入生产构建。

警告 — supported-targets 与 targets: 不要在包级别使用 supported-targets: ["js"]。这会阻止下游使用者使用该包。请改用 targets 限定单个文件。

警告 — preferred_target = "js": 这只是默认设置,并非锁定目标。对于支持该目标的文件,moon test --target wasm-gc 仍然有效——这对跨后端库很有用。

按后端限定代码的两种方式——targets:(文件级)与 #cfg()(声明级):

方式粒度使用场景
在 moon.pkg 中使用 targets: { "foo.mbt": ["js"] }整个文件文件完全针对 JS(仅含 FFI、专属于某个后端的逻辑)。如果 JS 代码不需要与其他后端共享状态,这是最简洁的方式。
在声明上使用 #cfg(target="js")单个 fn、const 或代码块大部分代码共用的模块需要为某个符号提供特定后端实现。可将相关代码保留在同一个文件中。

你会在 #cfg(...) 中看到以下条件:

#cfg(target="js")                          // JS only
#cfg(target="native")                      // native only
#cfg(not(target="js"))                     // everything except JS
#cfg(any(target="wasm", target="wasm-gc")) // either wasm backend
#cfg(any(target="native", target="llvm"))  // either non-GC backend
#cfg(false)                                // always exclude (quick disable)

moonbitlang/core 广泛使用这一模式——例如,Int16::from_int64 在 native/wasm 上使用 %i64_to_i16 内部函数,而在 JS 上使用备用实现,因为 64 位整数没有原生表示:

///|
#cfg(not(target="js"))
pub fn Int16::from_int64(self : Int64) -> Int16 = "%i64_to_i16"

///|
#cfg(target="js")
pub fn Int16::from_int64(self : Int64) -> Int16 {
  Int16::from_int(self.to_int())
}

文件级规则同样适用于 #cfg:你的函数可从每个目标调用时,该目标必须且只能有一个匹配的定义。重叠的 #cfg 保护条件会导致编译错误。#cfg(target="js") + #cfg(not(target="js")) 覆盖所有情况;如果仅在非内部函数声明上使用 #cfg(target="js"),则该函数在其他后端不存在,任何调用它的代码本身都必须受 #cfg 限定(或位于通过 targets: 限定的文件中)。

如何选择: 如果整个文件都是 extern "js" 包装器,请通过 targets: 限定该文件——这样更简单,而且 moon check --target wasm-gc 会完全跳过该文件。如果你在编写跨后端库,90% 的逻辑共用,只有少数函数不同,则使用 #cfg。

阶段 2:FFI 层

编写 extern "js" 声明。将它们设为私有(或添加 ffi_ 前缀);在阶段 3 中公开安全的包装器。

内联 JS(#| 字面量):

///|
extern "js" fn ffi_console_log(msg : String) -> Unit =
  #| (msg) => console.log(msg)

///|
extern "js" fn ffi_sqrt(x : Double) -> Double =
  #| (x) => Math.sqrt(x)

#| 字面量是函数体的原始 JS 源码。MoonBit 编译器会在调用点将其内联——运行时不会分配闭包。

模块和名称形式(直接导入现有 JS 函数):

///|
extern "js" fn math_max(a : Double, b : Double) -> Double = "Math" "max"

当你想引用可全局访问的函数而不通过闭包包装它时,使用此形式。速度稍快,体积也稍小。

#module("pkg") — 编译时 ESM / CJS 导入:

对于 JS 模块(Node 内置模块、npm 包、本地 .js 文件)中的具名导出,可为 extern "js" 添加 #module("module-id") 注解。右侧是该模块中的导出名称(而非内联 JS 函数体):

///|
#module("node:path")
extern "js" fn path_basename(p : String) -> String = "basename"

///|
#module("node:path")
extern "js" fn path_extname(p : String) -> String = "extname"

编译器生成的内容:

link.js.format生成的代码
"esm"import { basename as basename$7 } from "node:path";
"cjs"const { basename: basename$1548 } = require("node:path");
"iife"与 CJS 风格的解构相同(封装在 IIFE 中)

为什么 #module() 优于手动编写的 require("...") 封装:

  • 可静态分析。 Vite / esbuild / Rollup 可以追踪依赖、进行摇树优化,并正确解析路径。
  • 与格式无关的源代码。 同一个 .mbt 文件既能用于 ESM(其中未定义 require),也能用于 CJS,无需编写第二条代码路径。
  • 生成的代码更少。 无需为每次调用创建闭包;解构出的局部变量可以重复使用。
  • 兼容打包器的路径。 支持 npm 包说明符("lodash")、Node 内置模块("node:path")、相对文件("./helpers.js"),以及运行时支持的 URL 风格说明符。

限制:

  1. 仅支持命名导出。 RHS 必须是模块导出的键。对于默认导出或顶层值本身就是函数的模块,需要在 JS 端通过桥接重新导出,或者在 FFI 函数体中内联 import / require。
  2. 每个声明只能有一个导出。 不能在多个 #module FFI 之间共用一个解构;每个注解都会生成自己的导入行。编译器会对模块说明符去重,因此每个唯一模块只需执行一次 require,而不是每个函数执行一次。
  3. 模块名字面量必须是编译时常量。 不支持动态模块路径(#module(some_variable))。对于动态选择的模块,可在内联 JS 中使用 require(name)。
  4. 不允许使用内联函数体。 唯一支持的形式是 #module("pkg") + extern "js" fn f() = "name"。与 #| (...) => ... 混用会导致编译错误。
  5. 浏览器全局变量不需要使用它。 对于 window、document、fetch、Math 等,请使用内联 JS 或模块+名称形式(= "Math" "max");它们不会被导入,只会被访问。
  6. 有副作用的导入仍然会产生副作用。 编译后的模块加载时会执行 import / require。如果 pkg 有顶层副作用(CLI 横幅、全局修补),那么即使从未调用 FFI 函数,这些副作用也会在导入时执行。
  7. 测试与发布格式。 无论你的 link.js.format 是什么,moon test 都会使用 CJS 测试框架。如果模块仅支持 ESM(package.json 的 exports 中没有 CJS 入口),即使 moon build --release 可以正常工作,其测试也可能失败。请对这些测试进行条件控制,或使用双格式包。

何时使用哪种形式:

形式使用场景
#|(x) => ... 内联简单表达式、复杂逻辑,或需要在一个封装中组合多个 JS 调用
= "Math" "max" 模块+名称调用预定义的全局函数(无需导入模块)
#module("pkg") + = "name"调用可导入模块中的命名导出(Node 内置模块、npm 包、本地文件)
require("pkg").get("name") 辅助函数模块名是动态的,或只有运行时才能确定模块名

多行内联 JS:

///|
extern "js" fn ffi_do_work(x : Int) -> Int =
  #| (x) => {
  #|   const y = x * 2;
  #|   return y + 1;
  #| }

不透明 JS 值模式:

对于 MoonBit 中没有对应类型的值(DOM 节点、npm 对象、undefined、null),请使用 #external pub type 封装:

///|
#external
pub type JsValue

///|
/// Zero-cost bitcast — compiles to nothing.
pub fn[A, B] identity(a : A) -> B = "%identity"

%identity 是编译器内置操作:它在类型检查器层面转换值的类型,不会生成任何 JS 代码。可用它将基本类型提升为 JsValue,再转换回来。它在运行时不进行检查——错误使用会导致静默的类型错误。

针对 JsValue 的通用 FFI 操作:

///|
pub extern "js" fn JsValue::get(self : JsValue, key : String) -> JsValue =
  #| (obj, key) => obj[key]

///|
pub extern "js" fn JsValue::set(
  self : JsValue,
  key : String,
  value : JsValue,
) -> Unit =
  #| (obj, key, value) => { obj[key] = value }

///|
pub extern "js" fn JsValue::call_method(
  self : JsValue,
  name : String,
  args : Array[JsValue],
) -> JsValue =
  #| (obj, name, args) => obj[name](...args)

这三个基本操作(get / set / call_method)几乎可以表示任何 JS 对象交互。

null / undefined 的区分:

MoonBit 的 T? 无法安全表示 T | null | undefined——你会得到 Some(null)。请使用 is_undefined / is_null 进行显式检查:

///|
pub extern "js" fn is_undefined(v : JsValue) -> Bool =
  #| (v) => v === undefined

///|
pub fn[T] js_get_opt(obj : JsValue, key : String) -> T? {
  let v = obj.get(key)
  if is_undefined(v) {
    None
  } else {
    Some(identity(v))
  }
}

对于区分 null 和 undefined 的库(例如 FileReader.error),请引入 Nullable[T] 和 Nullish[T] 包装类型,并通过显式检查缩小类型范围。

第 3 阶段:MoonBit API

在原始 ffi_* extern 上构建安全、类型化的 pub 封装。

///|
pub fn console_log(msg : String) -> Unit {
  ffi_console_log(msg)
}

///|
pub fn sqrt(x : Double) -> Double {
  if x < 0 { panic() }
  ffi_sqrt(x)
}

导出给 JS 消费者:

moon.pkg 的 link.js.exports 中列出的内容都必须是 pub。编译器会为每个内容生成一个具名 ESM 导出(或 CJS 属性,或 IIFE 全局变量)。系统会根据公开签名自动生成 .d.ts。

#export_name(v0.10.4) 可指定单个 pub 非泛型函数生成的符号名称。对于新导出,现在推荐使用它,而不是特定后端的 exports 链接配置。它不适用于泛型函数或带可选参数的函数,且名称必须是包中唯一的有效 C 标识符。

#export_name("add_ints")
pub fn add(a : Int, b : Int) -> Int { a + b }

导出配置的作用范围仅限于生成构件的包——依赖项中的属性不会向你的输出添加符号。

第 4 阶段:Promise / Async 桥接

MoonBit async 使用两个编译器内置函数,将其转换为 JS Promise 内部机制:

内置函数作用
%async.suspend将 resume/reject 回调对转换为 await 点
%async.run在同步上下文中启动异步计算

标准设置(完整详情请参阅 references/promise-bridging.md)。按以下顺序声明——每一项都依赖于前一项:

///|
#external
pub type Promise[T]

///|
/// Cast Promise[T] to JsValue so JsValue methods (get/call_method) apply.
/// Required by `Promise::wait` below.
pub fn[T] Promise::to_any(self : Promise[T]) -> JsValue = "%identity"

///|
pub async fn[T, E : Error] suspend(
  f : ((T) -> Unit, (E) -> Unit) -> Unit,
) -> T raise E = "%async.suspend"

///|
pub fn run_async(f : async () -> Unit noraise) -> Unit = "%async.run"

///|
/// The bridge. `await promise` in JS = `promise.wait()` in MoonBit.
pub async fn[T] Promise::wait(self : Promise[T]) -> T {
  suspend(fn(ok, err) {
    self.to_any().call_method("then", [identity(fn(v : JsValue) { ok(identity(v)) })])
        .call_method("catch", [identity(fn(e : JsValue) { err(identity(e)) })])
    |> ignore
  })
}

什么时候需要 %async.run,什么时候只需 %async.suspend?

情况需要 run_async?原因
async test "..." { ... } 代码块否测试框架已在异步上下文中运行代码块
调用 .wait() 的 pub async fn否调用方已处于异步上下文;await 链会自然衔接
启动 Promise 并将其返回给 JS 的 MoonBit 导出同步函数是你正在跨越同步 → 异步边界;run_async 会调度函数体
从 async fn executor 构造 Promise[T]是即使 new Promise(...) 是同步的,executor 函数体也必须运行

简而言之:使用 async test 的用户永远不需要编写 run_async。只有在同步上下文中通过 %identity 将异步计算转换到 JS Promise 环境时才需要它。

标准模式——返回 Promise 的 FFI + async 封装:

///|
extern "js" fn ffi_fetch_text(url : String) -> Promise[String] =
  #| (url) => fetch(url).then(r => r.text())

///|
pub async fn fetch_text(url : String) -> String {
  ffi_fetch_text(url).wait()
}

调用方可在 async test 或 async fn 中编写 let body = fetch_text(url)——调用处不需要 .wait()。测试:

async test "fetch_text resolves" {
  let body = fetch_text("https://example.com")
  inspect(body.length() > 0, content="true")
}

注意——moonbitlang/async: async test 代码块需要在 moon.pkg 中添加 import { "moonbitlang/async" } for "test"。此依赖仅在测试时需要。使用 moon add moonbitlang/async 添加——必须使用 0.17.0 或更高版本(较旧的 0.1.x 版本不支持当前的 %async.suspend ABI)。已通过 0.21.0 验证。

升级到 0.20 之后时需留意以下破坏性变更:@http.Headers 现在是大小写不敏感的映射,而不是 Map[String, String];@fs 移除了 create / truncate 参数,且 @fs.write_file 现在默认使用 CreateOrTruncate;@stdio.{Input,Output}::fd 现在会抛出异常,而不是返回;@async.protect_from_cancel 的默认值变为 resume_on_cancel=true。运行时还会在检测到死锁时中止(可通过 @async.set_deadlock_handler 配置)。

第 5 阶段:测试
moon check                           # type-check, fast
moon test                            # run all tests (js is default)
moon test src --filter "greet"       # filter by name
moon build                           # emit .js + .d.ts to _build/js/release/build/

构建输出路径——debug 与 release:

命令输出目录
moon build(默认)_build/js/debug/build/<pkg>.js + .d.ts
moon build --release_build/js/release/build/<pkg>.js + .d.ts
moon test(自动)_build/js/debug/test/<pkg>.{internal,blackbox}_test.js

两种模式都会生成 .d.ts。发布到 npm 时,请使用 --release。本地试验时,默认的 debug 输出已足够,且链接速度更快。

从 Node 运行生成的模块:

# debug build
node --input-type=module -e 'import("./_build/js/debug/build/<pkg>.js").then(m => console.log(m.add(2,3)))'

# release build (after `moon build --release`)
node --input-type=module -e 'import("./_build/js/release/build/<pkg>.js").then(m => console.log(m.add(2,3)))'

此技能的 assets/js_binding_proj/ 目录中包含一个可运行的端到端示例——其中有一个最小项目,包含 8 个通过的测试(同步 + 异步),你可以复制、修改,并使用 moon test 运行。

决策表

情况模式关键操作
调用 JS 全局对象(单行代码)使用箭头函数的内联 #|extern "js" fn ... = #| (x) => ...
引用现有全局对象(不包装)模块名 + 名称形式= "Math" "max"
从 npm / Node 内置模块导入命名导出#module("pkg") + = "exportName"编译器生成 import { exportName } 或 require,便于打包工具处理
不透明的 JS 对象#external pub type T使用 %identity 包装并转换类型
JS any 容器使用单一 JsValue 类型及 get/set/call_method只需构建一次对象协议辅助函数,即可重复使用
T | undefined检查 is_undefined(v),返回 T?切勿假设 Some(null) 有意义
T | null(与 undefined 不同)Nullable[T] 包装类型显式处理 is_null 分支
JS Promise#external type Promise[T] + wait()通过 %async.suspend 桥接
导出给 JS 使用者在 moon.pkg 中使用 link.js.exports列出 pub 函数;自动生成 .d.ts
将整个文件限定到一个后端在 moon.pkg 中使用 targets: { "f.mbt": ["js"] }整个文件都特定于某个后端时,这是最简洁的做法
将单个函数限定到一个后端在 fn 上方添加 #cfg(target="js")当文件中 90% 的内容共用时使用;为其他后端配合添加 #cfg(not(target="js"))
大整数(|x| > 2^53)使用 BigInt,而非 Int64JS number 在超过 2^53 后会丢失精度
调用 npm 包包装 require("pkg") → JsValue.get("fn")参见 references/interop-patterns.md
捕获 JS 异常在 JS 端用 try { ... } catch { ... } 包装调用参见 references/error-handling.md

常见陷阱

  1. 忘记用 targets 限定文件。 包含 extern "js" 的文件若未限定到 ["js"],运行 moon check --target wasm-gc 和 native 时就会出错。始终为 FFI 文件设置目标。

  2. 用 T? 表示 T | null | undefined。 MoonBit 的 None 不是 JS null,而 Some(null) 是无意义的值。提升类型之前,先用 is_undefined 和 is_null 分别处理。

  3. 在 FFI 中依赖 Int64。 JS number 是 IEEE-754 双精度数。任何大于 2^53 的 64 位整数都会被静默舍入。请使用 BigInt,或将其拆分为两个 Int。

  4. 直接传递 Map/Result/trait 对象,从而暴露 MoonBit 内部表示。 这些类型使用 MoonBit 特有的运行时布局。在边界处将其转换为普通结构体、Array[(K, V)] 或 JsValue。

  5. 运行 async test 时遗漏 moonbitlang/async。 很容易忘记仅用于测试的导入。在编写第一个 async test 前,将 import { "moonbitlang/async" } for "test" 添加到 moon.pkg。

  6. 从同步 JS 调用异步代码时忘记使用 %async.run。 导出的同步函数无法使用 await。如果需要从同步导出函数触发异步操作,请用 run_async(async fn() noraise { ... }) 包装,并显式返回 Promise。

  7. 不在 async 上下文中调用 .wait()。 .wait() 是异步的,只能在 async fn 或 async test 中编译。非异步调用方需要直接使用 Promise。

  8. 在需要实际转换时使用 %identity。 %identity 是类型检查器的逃逸机制,不会产生任何运行时效果。用它转换 Int ↔ String 会导致运行时错误,且不会发出警告。仅当 JS 运行时表示确实匹配时才使用。

  9. 导出未列入 link.js.exports 的 pub 函数。 其他 MoonBit 包可以调用它,但它不会从编译后的 JS 模块重新导出。请将其名称添加到 exports。

示例项目

一个可运行的最小项目,演示上述所有模式:

assets/js_binding_proj/
├── moon.mod                # preferred_target = "js", moonbitlang/async dep
└── src/
    ├── moon.pkg            # targets + link.js.exports
    ├── ffi.mbt             # extern "js" + JsValue + inline JS
    ├── lib.mbt             # Safe public wrappers (add, greet, js_get_opt)
    ├── lib_test.mbt        # Sync tests
    ├── async.mbt           # Promise[T] + suspend + wait
    ├── async_test.mbt      # async test blocks
    ├── modules.mbt         # #module("node:path") ESM/CJS import demo
    ├── modules_test.mbt
    ├── cross_target.mbt    # #cfg(target="...") per-declaration gating demo
    └── cross_target_test.mbt

运行:

cd assets/js_binding_proj
moon test              # 12 tests pass (sync + async + #cfg + #module)
moon build             # emits ESM + .d.ts
node --input-type=module -e 'import("./_build/js/debug/build/js_binding_proj.js").then(m => console.log(m.add(2,3), m.greet("world"), m.basename("/a/b/c.html")))'
# → 5 Hello, world! c.html

参考资料

@references/promise-bridging.md @references/interop-patterns.md @references/error-handling.md @references/typescript-integration.md @references/cfg-and-target-gating.md

评论

支持 Markdown