TS Bridge Generator
Use this skill when changing how .ts / .tsx / .d.ts entrypoints
become MoonBit bridge packages, or when extending the consumer-facing
ts2mbt CLI.
Start here
Read these files first:
src/bridge/moonbit_bridge.mbt— package-level bundle (bridge.mbti,bridge.mbt,bridge.js,moon.pkg.jsoncomposition)src/bridge/moonbit_js_ffi.mbt— extern signature emission, struct converters, tagged-union lowering, opaque-type pubicnesssrc/bridge/moonbit_decl.mbt— declaration emission + checker-driven diagnostics injectionsrc/bridge/tagged_union_lowering.mbt—_to_js/_from_jsIIFE shapes and the inline pass-through fallbacksrc/main.mbt— CLI emit_* entry helpers (returnBool, route exit through cmd dispatchers)src/vendor.mbt—ts2mbt generate/ts2mbt vendorworkflowsrc/cmd/ts2mbt/main.mbt— dispatcher + per-subcommand flag parsingsrc/main_wbtest.mbt— end-to-end fixture coverage
If the local companion plugin repo exists, also read:
/tmp/vite-plugin-moonbit-worktree/src/index.ts/tmp/vite-plugin-moonbit-worktree/test/ts-bridge-tree-shake.test.mjs/tmp/vite-plugin-moonbit-worktree/examples/ts_bridge_project/README.md
Current contract
CLI surface (TypeScript -> MoonBit only; mbt2ts is the reverse
direction and lives in src/cmd/mbt2ts/main.mbt):
ts2mbt --input <ts-entry> --out <dir> [--module-spec <spec>] [--diagnostics <path>] [--strict]— high-level unified flowts2mbt vendor <pkg> [--module-spec <spec>] [--out <dir>]— resolve a single npm package vianode_modulesand emit a sub-package under<moon source>/internal/generated/<safe>/ts2mbt generate [--package-json <path>] [--out <dir>]— generate every dependency / devDependency listed inpackage.jsonts2mbt scaffold <ts-entry> <module-spec> <out-dir>— low-level package writer (always emitsSCAFFOLD_DIAGNOSTICS.md)ts2mbt package <ts-entry> <module-spec> <out-dir>— package writer without the diagnostics filets2mbt bridge / ffi / decl— individual generation stages
Generated package layout:
moon.pkg.json(defaults to{})bridge.mbtibridge.mbt(split intotypes.mbt/converters.mbt/externs.mbt/guards.mbtfor large packages)bridge.jsSCAFFOLD_DIAGNOSTICS.md
Vendor naming:
- Directory slug strips leading
@, replaces/with__, and maps remaining non-[A-Za-z0-9]characters to_. So@types/react->types__react,@scope/foo-bar->scope__foo_bar. @types/<name>packages default the runtime module spec to the unscoped name (react,express, ...) since the runtime ships in the non-types package.
Constraints to preserve
#module(...)only accepts absolute paths — relative is rejected by MoonBit's JS extern. The bridge bakes the absolute scaffold path; this is documented insrc/main.mbtnearrewrite_bridge_package_module_path. Don't try to relativise without changing the upstream extern model.- Self-contained
pub type JSValueandpub type Promise[X]are injected into the bridge package (nomizchi/jsdep). - Opaque external types are emitted as
#external\npub type Xso the matchingdeclare pub type Xin.mbtiactually resolves. - Tagged-union
_to_jsIIFEs are inlined into consumer extern lambdas (the bridge.js function isn't auto-imported into inline JS bodies). Keep the leading "if not a tagged value, pass through" guard sounsafeCast-style call sites work. - Every
emit_*entry returnsBool; cmd dispatchers route failures through@ts.cli_fail("ts2mbt")so CI sees a non-zero exit. - Tree-shake behaviour matters. If adapter exports change, update the companion plugin binding parser and tree-shake test.
Validation
In this repo:
moon check --deny-warn
moon test --target native
just verify-mbti-dts
just verify-scaffolds
just verify-generated-fixtures
just verify-realworld-typescript # optional, requires npm corpus
Smoke the vendor flow when changing src/vendor.mbt:
moon run src/cmd/ts2mbt -- sync --out /tmp/vendor_smoke
In the companion plugin repo when present:
pnpm exec tsc --noEmit
pnpm test