@juspay/rescript-bindgen is a deterministic TypeScript→ReScript binding generator (no AI at
runtime, no guessing). It reads .d.ts via the TypeScript compiler API (src/extract.mjs),
builds an IR, and emits ReScript (src/emit.mjs); src/cli.mjs wires it together.
docs/TYPE_MAPPING.md is the source of truth for every TypeScript→ReScript
rule. Consult it before adding or modifying a mapping. It is backed by golden fixtures and is enforced
in CI — treat it as a contract, not just docs.
- No unsafe casts. Never emit
Obj.magic,@unwrap, or a bare%identity. The only allowed%identityforms are (a) the zero-costexternal from*constructor oras*accessor of an opaque-type module (the value passes through unchanged) — used as the fidelity fallback when an exact type or@unboxedvariant can't express the shape (e.g. reverseas*views of an overloaded function) — and (b) the<prop>Fnrender-prop wrapper (((props, state) => React.element) => React.element, #46): the function form of a render prop, precisely typed from the extracted signature; needed becauseReact.elementis abstract so an@unboxed Element | Fncannot compile. Both are enforced by the golden suite'scheckNoStrayIdentity. - Flag, don't fake. If a type can't be modelled exactly, emit a
stringplaceholder + comment and bucket it (⚪ loose / 🔍 review / 🛑 broken). Never emit a plausible-but-wrong type. - Multi-type props →
@unboxeduntagged variant (distinct runtime tags) or an opaque module. unknown→JSON.t(opaque value), never a type variable.'ais only for a genuine generic that round-trips (input ↔ callback).
A new or changed mapping is not done until all three are updated together:
- A row in
docs/TYPE_MAPPING.md. - A fixture under
test/golden/cases/<name>/(self-containedindex.d.tswith inline stubs +lib.dom, no installs — see existing cases; plusargs.json). - Its regenerated golden:
npm run test:golden:update, then verify the diff is correct.
| Command | What it does |
|---|---|
npm test |
smoke test + golden snapshot diff (offline, fast) |
npm run test:golden |
golden snapshot only |
npm run test:golden:update |
regenerate goldens (after an intentional change) |
npm run test:compile |
compile every golden with ReScript in test/sandbox (needs sandbox deps) |
npm run gen -- --pkg <name> --out <dir> --report |
generate bindings for a package |
npm run bench |
real-world benchmark: run the checkout against the pinned packages in benchmark/packages.json, diff vs committed baselines (see benchmark/README.md) |
npm run bench:update |
accept intentional output changes — regenerate benchmark/baselines/ and commit the diff in the same PR |
CI (.github/workflows/ci.yml) runs the smoke+golden diff, the compile check, and a
fixture guard (a PR touching src/extract|emit|resolve.mjs must also touch
test/golden/cases/ or docs/TYPE_MAPPING.md; label no-fixture-needed opts out) on every
PR, all blocking — generated output cannot drift from docs/TYPE_MAPPING.md without a
failing build. .github/workflows/benchmark.yml is the opt-in heavy gate: approve it from
the PR's "Review pending deployments" button to run the real-package benchmark before a
release.
- ESM, Node ≥ 20, single runtime dep (
typescript). Match the terse, heavily-commented style ofsrc/*.mjs.