关于 moonbit-js-binding
通过 MoonBit 的 JS 后端连接 JavaScript API 的分步技能,指导开发者处理 FFI 声明、类型映射、Promise 异步桥接和软件包输出配置。
主要功能
- 01映射 JavaScript 与 MoonBit 类型
- 02声明 JS FFI 函数
- 03导入具名模块导出
- 04桥接 Promise 与异步调用
- 05配置 MoonBit JS 软件包输出
- 06处理 null 与 undefined
适用场景
- 01绑定浏览器 DOM 或 Node.js API
- 02为 MoonBit 封装 npm 软件包函数
- 03配置 ESM、CJS 或 IIFE 输出
- 04生成 TypeScript 声明文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ skills --skill moonbit-js-binding安装后在 Claude Code 中输入/moonbit-js-binding确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdassets/js_binding_proj/moon.modassets/js_binding_proj/src/async_test.mbtasync.mbtcross_target_test.mbtcross_target.mbtffi.mbtlib_test.mbtlib.mbtmodules_test.mbtmodules.mbtmoon.pkgreferences/cfg-and-target-gating.mderror-handling.mdinterop-patterns.mdpromise-bridging.mdtypescript-integration.md详细介绍
MoonBit JS 绑定指南
通过 js 后端将 JavaScript API(浏览器 DOM、Node 内置模块、npm 包)绑定到 MoonBit 的分步工作流。
已针对 MoonBit v0.10.12 和
moonbitlang/async0.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分别映射到 MoonBitOption - 通过
require(...)或 ESM 导入使用 npm 包 - 为 TypeScript 使用者生成
.d.ts
计划模式说明
如果你处于计划模式,并且正在为 moonbit-js-binding 工作创建计划:
- 在最终计划文件开头加入明确指示,要求执行 Agent 在开始实现前阅读 moonbit-js-binding 技能。
- 指示内容应如下:
**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 类型。
| JavaScript | MoonBit | 说明 |
|---|---|---|
boolean | Bool | 直接映射 |
number(整数) | Int、UInt | JS 使用 IEEE-754;较大的 Int64 会失去精度,改用 BigInt |
number(浮点数) | Float、Double | 直接映射 |
bigint | BigInt | 直接映射 |
string | String | 直接映射(两者都使用 UTF-16) |
Uint8Array | Bytes、FixedArray[Byte] | 直接映射,无需复制 |
Array<T> | Array[T]、FixedArray[T] | 直接映射 |
Function | FuncRef[(...) -> R] 或闭包 | 直接映射 |
any(不透明类型) | #external pub type JsValue | 使用名义类型包装 |
T | undefined | 通过 is_undefined 检查映射为 T? | 参见 Null/Undefined 部分 |
T | null | Nullable[T] 包装类型 | null 不等于 undefined |
Promise<T> | #external pub type Promise[T] | 使用 async fn/wait() 桥接 |
常量 enum | number | pub(all) enum E { A; B } |
警告 — Number 精度: JavaScript 没有整数类型。跨越 FFI 边界的值会舍入为 IEEE-754 双精度数。
|x| < 2^53时使用Int没问题;超过此范围时请使用BigInt。UInt64/Int64目前会映射为 JSnumber,超过2^53的部分会丢失精度。
警告 — Trait 对象与 MoonBit 枚举: 将
Map[K, V]、Result[_]、Json或非#externaltrait 对象传递过 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 readmoon.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 风格说明符。
限制:
- 仅支持命名导出。 RHS 必须是模块导出的键。对于默认导出或顶层值本身就是函数的模块,需要在 JS 端通过桥接重新导出,或者在 FFI 函数体中内联
import/require。 - 每个声明只能有一个导出。 不能在多个
#moduleFFI 之间共用一个解构;每个注解都会生成自己的导入行。编译器会对模块说明符去重,因此每个唯一模块只需执行一次require,而不是每个函数执行一次。 - 模块名字面量必须是编译时常量。 不支持动态模块路径(
#module(some_variable))。对于动态选择的模块,可在内联 JS 中使用require(name)。 - 不允许使用内联函数体。 唯一支持的形式是
#module("pkg")+extern "js" fn f() = "name"。与#| (...) => ...混用会导致编译错误。 - 浏览器全局变量不需要使用它。 对于
window、document、fetch、Math等,请使用内联 JS 或模块+名称形式(= "Math" "max");它们不会被导入,只会被访问。 - 有副作用的导入仍然会产生副作用。 编译后的模块加载时会执行
import/require。如果pkg有顶层副作用(CLI 横幅、全局修补),那么即使从未调用 FFI 函数,这些副作用也会在导入时执行。 - 测试与发布格式。 无论你的
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.suspendABI)。已通过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,而非 Int64 | JS number 在超过 2^53 后会丢失精度 |
| 调用 npm 包 | 包装 require("pkg") → JsValue.get("fn") | 参见 references/interop-patterns.md |
| 捕获 JS 异常 | 在 JS 端用 try { ... } catch { ... } 包装调用 | 参见 references/error-handling.md |
常见陷阱
-
忘记用
targets限定文件。 包含extern "js"的文件若未限定到["js"],运行moon check --target wasm-gc和native时就会出错。始终为 FFI 文件设置目标。 -
用
T?表示T | null | undefined。 MoonBit 的None不是 JSnull,而Some(null)是无意义的值。提升类型之前,先用is_undefined和is_null分别处理。 -
在 FFI 中依赖
Int64。 JSnumber是 IEEE-754 双精度数。任何大于 2^53 的 64 位整数都会被静默舍入。请使用BigInt,或将其拆分为两个Int。 -
直接传递
Map/Result/trait 对象,从而暴露 MoonBit 内部表示。 这些类型使用 MoonBit 特有的运行时布局。在边界处将其转换为普通结构体、Array[(K, V)]或JsValue。 -
运行
async test时遗漏moonbitlang/async。 很容易忘记仅用于测试的导入。在编写第一个async test前,将import { "moonbitlang/async" } for "test"添加到moon.pkg。 -
从同步 JS 调用异步代码时忘记使用
%async.run。 导出的同步函数无法使用await。如果需要从同步导出函数触发异步操作,请用run_async(async fn() noraise { ... })包装,并显式返回Promise。 -
不在
async上下文中调用.wait()。.wait()是异步的,只能在async fn或async test中编译。非异步调用方需要直接使用 Promise。 -
在需要实际转换时使用
%identity。%identity是类型检查器的逃逸机制,不会产生任何运行时效果。用它转换Int↔String会导致运行时错误,且不会发出警告。仅当 JS 运行时表示确实匹配时才使用。 -
导出未列入
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