Skip to main content

MEP 72. Mochi and TypeScript/JavaScript package bridge

FieldValue
MEP72
TitleMochi and TypeScript/JavaScript package bridge
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-30 03:50 (GMT+7)
DependsMEP-1 (Grammar, for the import ts extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for Promise<T> and discriminated-union surface), MEP-45 (C transpiler, for the FFI sidecar pattern), MEP-52 (TypeScript transpiler, for the lowering pipeline, the runtime stub layer, the existing TargetNpmPackage / TargetDenoJsr / TargetBrowserBundle / TargetReleaseWorkflow targets, the npm Trusted-Publishing workflow emit, and the existing import typescript "..." FFI surface), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, the content-addressed object store, the capability declaration model, and the Sigstore-keyless Trusted-Publishing OIDC infrastructure)
Research/docs/research/0072/
Tracking/docs/implementation/0072/

Abstract

Mochi today (May 2026, immediately after MEP-52's TypeScript transpiler landed at 18 phases inclusive of the npm + JSR + Jupyter + browser-bundle + Trusted-Publishing release-workflow surfaces, and MEP-57's source-level package system reached Draft) has nine code-emitting targets, four bidirectional package bridges (Rust via MEP-73, Go via MEP-74, plus the two pending one-direction surfaces for Python and TypeScript / Deno), and the beginnings of a source-level package management story. MEP-52 phase 12 wired a minimal import typescript "..." FFI surface that the TS target understands (the import becomes a TS import statement, then the host TS / Deno / Bun toolchain resolves it), but it does not version-pin the imported package, does not record a checksum, does not gate on a capability declaration, does not cross-check against the registry's signature transparency log (npm Sigstore provenance or JSR's own integrity manifest), and does not run on any target other than the TS target. The Mochi-to-TypeScript path emits a package.json that lists dev.mochilang/runtime-ts as its only first-party dependency; user-written Mochi programs cannot pull in arbitrary npm packages with a version constraint, cannot pull in JSR packages at all (the existing import typescript "..." surface is npm-only), and a Mochi package author cannot publish their work as a typed npm library that downstream TS users npm install plus import { ... } from "@scope/pkg". The 3.5M+ packages indexed on npmjs.org (April 2026 snapshot from the npmjs.org public stats endpoint) and the 12K+ packages on jsr.io (April 2026 snapshot from the JSR public stats endpoint) remain only partially reachable to Mochi authors, and the TS / JS ecosystem cannot consume Mochi packages directly without hand-writing the public TS API surface.

MEP-72 specifies the bidirectional TypeScript / JavaScript package bridge: Mochi packages can consume any well-formed npm OR JSR package via import ts "<pkg>@<semver>" as <alias> with no user-written FFI boilerplate beyond what MEP-52 phase 12 already requires, with registry-integrity cross-check on the consume path, with a per-package capability declaration, and with a Mochi-side type binding driven by the TypeScript compiler API. Mochi packages can publish to the TS / JS ecosystem via mochi pkg publish --to=npm (npm Trusted Publishing, GA April 2024, via the MEP-52 Phase 18 emitted release workflow) AND mochi pkg publish --to=jsr (JSR Trusted Publishing, GA mid-2024, parallel OIDC + Sigstore flow). The bridge is the third bidirectional source-language interop story Mochi ships (after the Rust bridge of MEP-73 and the Go bridge of MEP-74), the first one that targets two parallel registries on the consume side (registry.npmjs.org and jsr.io) AND on the publish side (npm Trusted Publishing and JSR Trusted Publishing in the same workflow), and the first one where the runtime cost of consuming a foreign package is structurally zero (TS and Mochi-emitted JS share the same V8 / JavaScriptCore / SpiderMonkey / QuickJS runtime; the consumed package is loaded as a plain ESM module with no FFI boundary, no wrapper crate, no cgo cost, no marshaling layer).

The proposal builds on MEP-57's manifest / lockfile / capability infrastructure, MEP-52's emit pipeline (especially Phase 15's npm package scaffold, Phase 17's JSR package scaffold, and Phase 18's Trusted-Publishing workflow emit), and the philosophy of MEP-73 + MEP-74's bidirectional bridges, and adds a new self-contained component under package3/typescript/. The Mochi grammar gains semver-pinned forms for a new ts <lang> token in the FFI-import production (the existing typescript keyword from MEP-52 phase 12 remains accepted as a backwards-compatible alias; new code is expected to use ts). The Mochi build pipeline gains one new target (TargetNpmLibrary in MEP-52's build driver, parallel to the existing TargetNpmPackage which emits a binary-style executable npm package) and one new build target (TargetJsrLibrary in MEP-52's build driver, parallel to the existing TargetDenoJsr which emits a binary-style executable JSR package). The lockfile gains two new repeated tables ([[npm-package]] and [[jsr-package]], kept separate because the two registries have different integrity-hash formats). No existing transpiler MEP needs structural change; MEP-52 phase 12's existing FFI mangling pass is extended (not replaced) to honour the new lockfile pin.

The system is anchored on eight load-bearing decisions, each justified in §Rationale and surveyed in the companion research notes:

  1. TypeScript compiler API (typescript npm package, ts.createProgram + ts.TypeChecker + ts.SourceFile) as the canonical machine-readable TS / JS package surface, ingested via a small Node-side helper binary, .d.ts is the primary input and .ts source is the fallback only for JSR packages where the registry transpiles server-side and never publishes a separate .d.ts. The TypeScript team has shipped tsc --emitDeclarationOnly since TypeScript 2.0 (2016) and the compiler API has been API-stable across major versions since 3.0 (2018); the same API is what tsserver, ts-morph, tslint, typedoc, and dts-bundle-generator consume. The bridge ingests the .d.ts surface (or the source .ts for JSR-source-not-dist packages) via a Node-side helper binary (package3/typescript/cmd/ts-ingest/main.ts distributed as a tiny self-contained Bun script bundled into the Mochi binary), which loads the package's distributed types under node_modules/<pkg>/ (npm) or jsr_cache/<scope>/<pkg>/ (JSR) and emits an ApiSurface JSON document via ts.createProgram + checker.getTypeAtLocation walks. Parsing .d.ts with a hand-written Go parser is rejected (the TypeScript grammar is the single largest language grammar in mainstream production use; a passable subset is tens of thousands of LOC in Go and immediately stale). Reading the package.json's types field with a string-match parse is rejected (the types field points at a file; the actual type information is in the file). Using @types/<pkg> companion packages from DefinitelyTyped is rejected as the PRIMARY path (DT lags upstream by months and is unmaintained for many packages); the bridge accepts DT companion types only as a fallback when the source package ships no .d.ts. Using tsd or @typescript-eslint's API is rejected (both wrap the official compiler API; the bridge calls the official API directly). See 01-language-surface §2, 04-tsdoc-dts-ingest §1, 02-design-philosophy §1.

  2. No synthesized wrapper package on the consume side; runtime-native ESM linkage is the link surface. This is the single largest structural departure from MEP-73 (Rust) and MEP-74 (Go). The Mochi-to-TypeScript path of MEP-52 already targets the same JavaScript runtime (V8 on Node 22 LTS and Bun 1.1, V8 on Deno 2 via the Deno runtime, and V8 / JavaScriptCore / SpiderMonkey / QuickJS on browsers) that hosts the consumed npm / JSR packages. There is no FFI boundary to cross: the Mochi-emitted JS code imports the consumed package via a plain ESM import { foo } from "@scope/pkg" statement, the host runtime resolves the import through its normal node_modules / jsr_cache / import-map resolution, and the call is a direct property access plus function call at runtime. No extern "C" wrapper, no cgo c-archive, no cgo.Handle pool, no runtime.KeepAlive, no marshaling, no copy. The bridge's only job at link time is to write the right package.json dependencies entry (or the right Deno imports import-map entry, or the right Bun bunfig.toml entry) so the runtime resolves to the cached version. This decision is unique to TS / JS and turns out to compound the bridge's audit story (less synthesized code), its compile-time cost (zero wrapper compile pass per dep), and its runtime cost (zero per-call FFI overhead). The Mochi-side type binding remains a real synthesis: an extern fn declaration per imported item, a Mochi shim file per package, and a per-package import { ... } from "<pkg>" statement in the emitted JS. The bridge does NOT synthesize a .d.ts mirror of the consumed package; it relies on the upstream .d.ts for downstream tsc consumers of the emitted Mochi package. See 02-design-philosophy §2, 03-prior-art-bridges §3, 09-esm-cjs-interop §1.

  3. Closed TypeScript-to-Mochi type translation table, with explicit refusal on out-of-table cases. The translation covers number↔float (with overflow-guarded coercion at the boundary for integer arguments), bigint↔int (Mochi's int is bigint-or-number-discriminated under MEP-52 phase 2.1), string↔string, boolean↔bool, void↔unit, null↔nil, undefined↔nil (coalesced with null for Mochi-side ergonomics; documented in 05-type-mapping §3 as a known asymmetry the user can override per-binding), T[]↔list<T>, ReadonlyArray<T>↔readonly list<T> (Mochi readonly list view from MEP-52 phase 3.1's deferred work; until that ships, ReadonlyArray<T> translates to list<T> with a comment marker), Record<K, V>↔map<K, V> (when K is string or number and V is in-table), Map<K, V>↔map<K, V> (where the Mochi map's iteration-order discipline is "first-seen" matching MEP-52 phase 3.2's Map), Set<T>↔set<T>, T | null↔T?, T | undefined↔T?, T | null | undefined↔T? (the null and undefined are coalesced; documented asymmetry), tagged-union string-literal unions {kind: "a", ...} | {kind: "b", ...}↔ Mochi sum type with aandb variants (matches MEP-52 phase 5), plain object types and interfaces {a: A, b: B}↔ record { a: A, b: B } (only when all fields are in-table; a single out-of-table field skips the binding with a SkipReport entry), class types class C { method(...)... }↔ extern type C (opaque handle with method-call thunks; class instance state is Mochi-opaque), Promise<T>↔async fun(): T (async-colour pass on the Mochi side; matches MEP-52 phase 11.3), AsyncIterable<T>↔stream<T> (matches MEP-52 phase 10), Iterable<T>↔iter<T>, generic function types <T>(arg: T) => T (refused by default; monomorphised via [ts.monomorphise]), tuple types [A, B, ...]↔ tuple<A, B, ...>, function types (a: A) => B↔ fun(a: A) -> B, the Pick<T, K> / Omit<T, K> / Partial<T> / Required<T> / Readonly<T> / Record<K, V> mapped-type built-ins (each eagerly resolved at bind time), and TypeScript-only enum E { A, B }↔ Mochi sum type with A and B nullary variants (the bridge requires the enum to be const-enum or fully-numeric-literal so the runtime representation is statically knowable). Items outside the table (branded types whose witnesses are not exported, conditional types beyond the eager-resolution table, mapped types beyond the named built-ins, intersections whose merged shape has overlapping non-identical members, declaration-merging across multiple .d.ts files where the merged shape is not a strict union, ambient module declarations with no corresponding runtime export, this parameter polymorphism, default exports that are arrow-function literals whose inferred type the checker prints as () => any, decorator-injected metadata, keyof T projected through a generic, typeof <runtime-expression> in a type position, infer clauses in a generic position) are skipped with a SkipReport entry that names the item and the reason. The user can override with a hand-written extern fn declaration that takes responsibility for the type at the FFI boundary. Aggressive translation (e.g., synthesising a Mochi wrapper for every conditional type) is rejected: the bridge promises zero boilerplate, not full generality. See 05-type-mapping §1, 02-design-philosophy §3.

  4. import ts "<pkg>@<semver>" as <alias> as the surface, with <semver> resolved through mochi.lock, extending MEP-52 phase 12's existing keyword. The grammar's typescript <lang> token already exists from MEP-52 phase 12. MEP-72 adds the alias ts as the canonical short form (the typescript form remains accepted) and extends the <spec> production to admit:

    • <pkg-name>: bare package name (e.g., zod, @scope/pkg), resolves through [ts-dependencies] plus mochi.lock.
    • <pkg-name>@<semver-req>: explicit version constraint (1.8.0, ^1.8, ~1.8.0, >=1.0.0 <2.0.0, latest, next).
    • npm:<pkg-name>@<version>: explicit npm-registry selector (avoids JSR resolution).
    • jsr:@scope/<pkg-name>@<version>: explicit JSR selector (matches the Deno jsr: import specifier the Deno toolchain ships).
    • <pkg-name>@git+<url>#<rev>: git source override (escape hatch for forks).
    • <pkg-name>@path+<relative-path>: path source for replace-directive-like local development.

    Bare names resolve npm-first (the registry that hosts 99%+ of the TS / JS ecosystem at MEP-72 spec authoring time); the user can flip the default via [ts] default-registry = "jsr" for projects whose dep tree skews JSR-heavy. The mochi.lock lockfile records the resolved registry, the resolved version, BLAKE3-256 + SHA-512 (npm) or BLAKE3-256 + JSR-integrity-hash (JSR) of the tarball, the TypeScript compiler-API surface SHA-256 (a stable hash of the public d.ts shape used at bind time), the SHA-256 of the synthesised Mochi shim file, and the capability surface declared for the package. No other Mochi syntax changes are introduced; import ts participates in the same module-resolution flow as import rust / import go / import python. See 01-language-surface §1, 06-npm-jsr-publish-flow §2.

  5. Mochi → npm / JSR library emission via two new MEP-52 targets, TargetNpmLibrary and TargetJsrLibrary, parallel to the existing TargetNpmPackage and TargetDenoJsr (which are binary-shaped). MEP-52 today exposes (post-Phase 18) TargetNpmPackage (binary npm package with bin/ entry + shebang + cli scaffolding), TargetDenoJsr (binary JSR package with bin/ entry), TargetDenoJupyter (Jupyter kernelspec), TargetBrowserBundle (ESM bundle via bun build or esbuild), and TargetReleaseWorkflow (the GitHub Actions Trusted-Publishing pipeline). MEP-72 adds TargetNpmLibrary and TargetJsrLibrary. Where the binary npm path emits a package.json with a bin field, a dist/cli/index.js entry, and an executable shebang line, the library path emits a package.json with the exports map (the "import", "require", "types" conditions all wired), no bin field, no shebang, tsc --declaration --emitDeclarationOnly generated .d.ts files for every public export of the Mochi package, the "type": "module" field, the "files" allowlist, the "sideEffects": false flag for the tree-shaking-friendly subset, and the publish-side metadata (description, license, repository, keywords, author, homepage, bugs) harvested from the Mochi package's mochi.toml. The parallel TargetJsrLibrary emits the same package shape but pointed at jsr.json (source-not-dist per MEP-52 Phase 17's invariant), with the "exports" field pointed at ./src/index.ts rather than ./dist/index.js, and with JSR's own publish.include allowlist. The driver's Build function gates the library paths on Driver.LibraryMode=true plus the matching target. See 01-language-surface §3, 06-npm-jsr-publish-flow §1.

  6. Sigstore-keyless OIDC Trusted Publishing to BOTH npm AND jsr.io as the sole publish path, via the MEP-52 Phase 18 emitted release workflow; long-lived NPM_TOKEN / JSR_TOKEN paths are not supported. Trusted Publishing on the npm registry GA'd in April 2024 (Sigstore + OIDC + provenance attestation; consumers verify via npm audit signatures); Trusted Publishing on jsr.io GA'd mid-2024 (Sigstore + OIDC + Deno-side verification via deno publish --token-source=github-actions). The MEP-52 Phase 18 emitter already writes a .github/workflows/release.yml that exercises both flows in one CI job. MEP-72's publish flow is:

    1. Builds the library package via TargetNpmLibrary (or TargetJsrLibrary).
    2. Validates tsc --noEmit produces zero diagnostics against the emitted .d.ts surface.
    3. Runs npm pack --dry-run (or deno publish --dry-run --allow-dirty) to verify the manifest.
    4. The Phase 18 emitted GitHub Actions workflow then:
      • Acquires a GitHub OIDC token via the workflow's id-token: write permission.
      • Runs npm publish --provenance --access=public (which submits the OIDC token to npm's Trusted-Publishing endpoint, the npm side mints a short-lived publish credential, signs the tarball via Sigstore Fulcio, and posts the attestation to the registry).
      • In the same job, runs deno publish --token-source=github-actions (which submits the OIDC token to JSR's Trusted-Publishing endpoint, JSR side mints a short-lived publish credential, signs the manifest via the JSR signing infrastructure, posts the attestation to the JSR registry).
    5. The workflow also invokes actions/attest-build-provenance@v2 to record a SLSA-style attestation for the browser bundle artefact (if one was emitted).

    The legacy NPM_TOKEN + JSR_TOKEN paths via secrets.NPM_TOKEN / secrets.JSR_TOKEN are explicitly refused by the Phase 18 emitter (its TestPhase18NoLongLivedTokens gate fails any workflow body referencing those secrets). The mochi cli's mochi pkg publish --to=npm / --to=jsr exists as a developer-facing convenience that invokes the same code path locally with --dry-run, and --to=both runs the npm + JSR side-by-side. See 07-sigstore-npm-jsr-trusted-publishing §1, 02-design-philosophy §5.

  7. Promise / async bridge via the host JS runtime's built-in event loop; no runtime singleton, no block_on analogue. Unlike MEP-73's Rust bridge (which needs a tokio::runtime::Runtime singleton because Rust's async fn requires a runtime) and unlike MEP-74's Go bridge (which needs the cgo handle pool because the goroutine scheduler lives inside the c-archive), the TS / JS bridge's async story is structural: every JS runtime (Node 22 LTS, Deno 2, Bun 1.1, every modern browser, every JS-engine-based serverless edge runtime) ships with a built-in microtask queue plus an event loop, and TypeScript's Promise<T> is the canonical async return type. The Mochi-emitted JS code calls await foo() directly; the consumed package's async function foo() returns a Promise<T> that the runtime schedules through the same event loop the Mochi-emitted async code uses. The bridge has no per-import async runtime to construct, no thread pool, no shared state, no enter_runtime() ceremony. The only async-specific work the bridge does is at the type-binding side: a TS Promise<T> return becomes a Mochi async fun(...): T, which triggers the MEP-52 phase 11.3 async colour pass on the Mochi caller. The runtime cost of crossing the boundary is the JS engine's normal microtask scheduling cost (~50ns on V8 16.x, May 2026 microbenchmark on darwin-arm64), materially below cgo's 200ns / call and below tokio's block_on (~3μs / call cold, ~500ns / call warm). See 08-promise-async-bridge §1, 02-design-philosophy §4.

  8. Dual-registry consume + dual-registry publish + ESM-CJS dual-export emit, with the bridge treating npm and JSR as parallel first-class registries rather than primary + mirror. The TS / JS ecosystem has fragmented across two registries: npmjs.org (3.5M+ packages, the historical default, supports both CJS and ESM, requires package.json) and jsr.io (12K+ packages, the typed-by-default modern alternative, supports ESM only and TypeScript source publishing, requires jsr.json). MEP-72 treats both as first-class: a Mochi package's dep graph can mix npm and JSR packages freely; a Mochi-emitted library can publish to both in the same release workflow; a Mochi consumer can resolve a JSR package via jsr:@scope/pkg AND consume an npm package via npm:foo in the same source file. The emitted library is structured to satisfy both registries' shape requirements simultaneously: package.json AND jsr.json ship in the same root directory, with the exports map pointing at the same source tree (Mochi emits TS source for JSR consumption; tsc emits compiled JS + .d.ts for npm consumption; the source tree under src/ is the single source of truth). This is materially different from typical npm-first packages, which either ignore JSR or maintain a separate JSR-side fork. The motivation is to ship one library, not two. See 06-npm-jsr-publish-flow §1, 09-esm-cjs-interop §1.

The gate for each delivery phase is empirical: the bridge must successfully ingest a curated 24-package fixture corpus (drawn from the April 2026 npmjs.org weekly downloads top-25 list minus the Mochi-incompatible node-sass standalone usage: typescript, zod, lodash, lodash-es, react, react-dom, vue, axios, dayjs, date-fns, valibot, drizzle-orm, prisma, hono, express, fastify, undici, nanoid, uuid, ts-pattern, immer, effect, neverthrow, ts-toolbelt); generate a translatable surface for every public item the closed type-table covers; emit a SkipReport for every item out of table; produce a Mochi extern fn corpus that parses cleanly; and emit a Mochi binary whose Node 22 LTS + Deno 2 + Bun 1.1 run produces identical stdout (the MEP-52 phase 1+ "run on all three" gate). A separate publish gate exercises the TargetNpmLibrary plus TargetJsrLibrary paths against in-tree mock npm + JSR registries (the same verdaccio-mock harness MEP-52 Phase 18 already uses), asserts that the published packages npm install AND deno add cleanly into a synthetic downstream consumer, and asserts that the Sigstore attestations on both sides verify against the public root of trust.

Motivation

Mochi today (May 2026) integrates with foreign ecosystems through four FFI surfaces: Rust (via MEP-73 import rust "<crate>@<semver>", full bidirectional bridge with semver pinning and capability and crates.io publish), Go (via MEP-74 import go "<module>@<semver>", full bidirectional bridge with semver pinning and sum.golang.org cross-check and git-tag publish), Python (via the partial import python "..." surface, no version pinning), and TypeScript / Deno (via MEP-52 phase 12's import typescript "..." surface, no version pinning, no checksum verification, no capability declaration, no JSR support, no publish path). The TS / JS surface is the most-used of the foreign-ecosystem surfaces and the second-least-developed (after Python). The MEP-72 motivation is to close that gap and bring the TS / JS bridge to parity with the MEP-73 + MEP-74 pair:

  1. The npm + JSR ecosystem is the largest software package ecosystem in production use at MEP-72 spec time (May 2026). npmjs.org indexes 3.5M+ packages and serves 2.5T+ weekly downloads (April 2026 npmjs.org public stats endpoint). The top-1000 most-downloaded packages account for 90%+ of the npm download bandwidth observed by the Node Foundation's CDN telemetry (Q1 2026 stats). jsr.io, GA'd in March 2024, has reached 12K+ published packages (April 2026 JSR public stats endpoint) including the canonical reference implementations of the Deno standard library, the Hono web framework's typed surface, and the @std/*-prefixed JSR-native standard library. A Mochi program needing modern HTTP routing (hono, fastify, express), zod-based schema validation (zod, valibot), ORM access (drizzle-orm, prisma), modern UI (react, vue, solid-js), date handling (dayjs, date-fns), uuid generation (nanoid, uuid), or typed pattern-matching utilities (ts-pattern, effect, neverthrow) has no path to those packages today via a version-pinned + capability-gated + integrity-verified flow.

  2. The TS / JS ecosystem expects typed library packages as the unit of distribution; Mochi must learn to emit one. When a Mochi package author writes a useful library, the natural distribution channel for a TS / JS audience is npm (or, for the typed-native audience, JSR). MEP-52 Phase 15 (the npm-package target) writes a CLI-shaped npm tarball but does not produce a publish-ready library package: there is no exports map condition wiring (import / require / types), no tsc --declaration pass, no "sideEffects": false annotation for tree-shaking, no JSR-side jsr.json companion, no "files" allowlist that excludes test fixtures, no per-publish-target metadata harvest. MEP-72 extends MEP-52 to emit user packages as publish-ready library packages with the right metadata for BOTH registries simultaneously.

  3. The TS / JS ecosystem has finally converged on Sigstore-keyless OIDC publishing for both registries (April 2024 npm GA + mid-2024 JSR GA), and MEP-52 Phase 18 already shipped the emitted GitHub Actions workflow that exercises both flows. The 2024-2026 supply-chain landscape is unambiguous: npm Trusted Publishing, JSR Trusted Publishing, PyPI PEP 740, Cargo RFC #3724, Maven Central Sigstore all converged on the same OIDC-token + Sigstore-keyless-signing model. A package system released in 2026 that does not ship Trusted Publishing on day one is shipping a decade-out-of-date supply-chain story. MEP-72 inherits the MEP-52 Phase 18 workflow emit without modification (the npm publish --provenance --access=public + deno publish --token-source=github-actions job already lives in the emitted YAML); the bridge layer only adds the library-target build orchestration so the workflow has the right artefacts to upload.

  4. The "import ts" surface has zero learning curve for Mochi users. import ts "zod@^3.23" as zod is the same shape as import rust "tokio@^1.42", import go "github.com/spf13/cobra@^v1.8", and import python "numpy". Mochi users do not need to know what a package.json is, what a node_modules directory is, what a peer-dep is, what an exports map is, what a jsr.json is, what an import-map is, what a bundler resolution rule is, what --enable-source-maps does, what a Promise<T> is, what an AsyncIterable<T> is, what a SIGSTORE-Fulcio Sigstore cert is, what a CJS-vs-ESM dual-package hazard is, what __esModule is, what an interop helper is, what a tsconfig.json's moduleResolution field controls. They write import ts "..." as ... and the bridge does the rest.

  5. The TypeScript compiler API + .d.ts give Mochi a turn-key stable, machine-readable, in-package API surface for every well-formed package. Unlike MEP-73's rustdoc-JSON ingest (nightly-only at MEP-73 spec authoring time) and unlike MEP-74's go/packages ingest (requires a Go toolchain installed, which Mochi's host language already provides), the TS / JS ingest path needs only the typescript npm package as a build-time dep (plus Node 22 LTS or Bun 1.1 on PATH to execute the helper script). The bridge ingest binary is a ~400 LOC TypeScript program bundled via bun build into a single self-contained .js file embedded in the Mochi binary at build time; the helper loads the consumed package's .d.ts files via ts.createProgram, walks every exported declaration via checker.getSymbolsInScope, and emits an ApiSurface JSON document the bridge consumes. The .d.ts surface is stable by design (TypeScript's compatibility promise covers .d.ts consumption across major versions), and the compiler API has been stable since 2018. No nightly toolchain. No experimental flag. No schema-version pinning needed (the compiler API itself is the schema). This is materially less risky than MEP-73's rustdoc-JSON ingest and roughly on par with MEP-74's go/packages ingest.

  6. MEP-52 + MEP-57 + MEP-73 + MEP-74 already shipped the prerequisite manifest / lockfile / capability / publish / build-driver infrastructure. The mochi.toml table layout, the mochi.lock serialisation, the BLAKE3-256 + secondary-hash dual-hashing, the trusted-publishing OIDC token exchange model, the content-addressed object store, the capability declaration scheme, the npm-side Trusted-Publishing workflow emit (MEP-52 Phase 18), the JSR-side manifest emit (MEP-52 Phase 17), the browser-bundle emit (MEP-52 Phase 17), and the reproducible-build infrastructure (MEP-52 Phase 16) all transfer to the TS / JS bridge with no architectural duplication. MEP-72 is additive on top of MEP-52 + MEP-57 + MEP-73 + MEP-74: one new manifest section ([ts-dependencies]), two new lockfile repeated tables ([[npm-package]] and [[jsr-package]]), two new CLI surfaces (mochi pkg publish --to=npm, mochi pkg publish --to=jsr), and zero new transpiler MEP changes.

  7. The bridge stays small and audit-friendly. The reference implementation under package3/typescript/ is targeted at ~5,000 LOC of Go across the npm-registry client, the JSR-registry client, the .d.ts ingest helper invocation, the ApiSurface JSON parser, the type-mapping pass, the Mochi extern emitter, the lockfile integrator, the publish flow, and the ESM-CJS interop tracker. There is no JS-side dynamic codegen at user-machine time (the Mochi shim file is fully synthesised by the Go-side bridge binary; the user's Node / Deno / Bun execution only runs the emitted JS). There is no synthesized wrapper package on the consume side at all (the unique TS / JS bridge structural advantage). There is no native code in the bridge layer (the host runtime is purely JS engines on the consume side).

  8. The bridge closes a competitive gap with the Rust and Go bridges and makes Mochi a viable target for the largest pool of working developers. MEP-73 + MEP-74 shipped full bidirectional Rust and Go bridges with semver pinning, capability declaration, lockfile cross-check, and Trusted-Publishing on the publish side. The TS / JS bridge under MEP-52 phase 12 lagged on every one of those axes despite the TS / JS pool being the largest working-developer pool of the three. Closing the gap is a one-MEP investment; deferring it would push the Mochi-TS interop story behind the Mochi-Rust and Mochi-Go interop stories, which is the wrong relative ordering given that TS / JS is the most-deployed of the three and the cheapest of the three to bridge (no wrapper synthesis, no FFI cost, no async runtime).

Specification

This section is normative. Sub-notes under /docs/research/0072/ are informative.

1. Pipeline overview

MEP-72 introduces a per-import TS / JS dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-52 build driver:

mochi.toml [ts-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved TS dep tree (registry + pkg name + version + source URL)
| package3/typescript/npmregistry.Fetch (registry.npmjs.org tarball + integrity)
| package3/typescript/jsrregistry.Fetch (jsr.io meta.json + tarball)
v
.tgz / source tree in ~/.cache/mochi/ts-deps/<blake3-hex>/
| package3/typescript/integrity.Verify (npm SHA-512 + JSR BLAKE3 cross-check)
v
hash-verified package sources
| package3/typescript/cmd/ts-ingest (TS compiler API + checker walk, JSON emit)
v
ApiSurface JSON document per package
| package3/typescript/typemap.Translate (closed TS-to-Mochi table)
v
TranslatedSurface + SkipReport per package
| package3/typescript/emit.MochiShim (extern fn corpus, no wrapper synthesis)
v
synthesised .mochi shim file per package, imported by the user's source
| MEP-52 Driver.Build (TargetNpmPackage / TargetNpmLibrary / TargetDenoJsr / TargetJsrLibrary / TargetBrowserBundle)
v
binary or library package

The bridge does not run the TypeScript compiler at build orchestration time outside the ingest pass. The only TS / JS toolchain invocation is the ts-ingest helper binary, which calls ts.createProgram on the cached package sources to produce the ApiSurface JSON. The Mochi-emitted JS imports the consumed package via a plain ESM import statement; the host runtime resolves the import through its normal node_modules / jsr_cache / import-map resolution.

2. Manifest extension: [ts-dependencies] and [ts]

The MEP-57 mochi.toml gains two new optional top-level tables:

[ts-dependencies]
zod = "^3.23.0"
"@hono/zod-validator" = "^0.4"
"jsr:@std/path" = "^1.0"
typescript = { version = "^5.6", dev = true }
react = { version = "^18", peer = true }
"my-local-pkg" = { path = "../my-pkg" }
"my-git-fork" = { git = "https://github.com/example/my-fork", rev = "abc123" }

[ts]
runtime = "node22"
module = "esm"
target = "es2024"
default-registry = "npm"
import-map = "imports.json"
monomorphise = [
{ item = "zod.ZodObject", T = "MyShape" },
]

[ts.publish]
shape = "library"
exports = { "." = { "types" = "./dist/index.d.ts", "import" = "./dist/index.js" } }
sideEffects = false
files = ["dist/**", "src/**", "README.md", "LICENSE"]
license = "Apache-2.0 OR MIT"
publish-to = ["npm", "jsr"]

[ts.capabilities]
net = true
fs = false
proc = false
worker = false
wasm = false
eval = false

[ts.private]
packages = ["@corp/internal-*"]
sigstore-skip = ["@corp/internal-*"]

[ts-dependencies] follows npm's package.json dependencies grammar verbatim: simple string for version, table for inline version + dev / peer / optional / registry / path / git source. The jsr: prefix selects the JSR registry; otherwise the registry defaults to whatever [ts] default-registry says (default "npm").

The [ts] table holds Mochi-specific knobs:

  • runtime: the JS runtime target. Valid values: "node22", "deno2", "bun1.1", "browser", "edge" (Cloudflare Workers / Vercel Edge / Deno Deploy). Default "node22".
  • module: the module format the Mochi-emitted JS uses. Valid values: "esm" (default), "cjs" (legacy node compatibility).
  • target: the ECMAScript target. Default "es2024".
  • default-registry: "npm" (default) or "jsr".
  • import-map: the Deno / browser import-map filename. Default "imports.json" for Deno; absent for Node.
  • monomorphise: a list of explicit generic instantiations. Required for generic items the user wants to import.

The [ts.publish] table holds Mochi-as-library knobs:

  • shape: "library" (default for TargetNpmLibrary / TargetJsrLibrary) or "binary" (for TargetNpmPackage / TargetDenoJsr).
  • exports: the npm exports map. The bridge fills in standard defaults when omitted.
  • sideEffects: false for tree-shaking-friendly packages, true for packages with module-load-time side effects. Default false.
  • files: the npm files allowlist.
  • license: SPDX licence expression.
  • publish-to: a list of registries to publish to. Subset of ["npm", "jsr"]. Default ["npm"].

The [ts.capabilities] table holds TS-bridge-specific capability flags (a strict refinement of MEP-57's [capabilities] table):

  • net: the dep graph contains packages that open network sockets (undici, node:fetch, node:net, node:http).
  • fs: the dep graph reads or writes files (node:fs, node:fs/promises).
  • proc: the dep graph spawns processes (node:child_process).
  • worker: the dep graph spawns worker threads (node:worker_threads, Worker, SharedWorker).
  • wasm: the dep graph instantiates WebAssembly modules (WebAssembly.instantiate).
  • eval: the dep graph calls eval() or new Function().

The [ts.private] table opts packages out of the Sigstore provenance cross-check:

  • packages: glob patterns of package names the bridge does not query for Sigstore attestations.
  • sigstore-skip: glob patterns for which the bridge skips the Sigstore-attestation lookup at lock time.

3. Lockfile extension: [[npm-package]] and [[jsr-package]]

The MEP-57 mochi.lock gains two new repeated tables (split because npm and JSR use different integrity-hash formats):

[[npm-package]]
name = "zod"
version = "3.23.8"
source = { kind = "npm-registry", registry = "https://registry.npmjs.org" }
tarball-blake3 = "0123456789abcdef..."
tarball-integrity = "sha512-2OdJ8gIxQAH+8XbLN..."
sigstore-attested = true
sigstore-bundle-hash = "..."
api-surface-sha256 = "..."
shim-sha256 = "..."
capabilities-declared = []
dependencies = []
peer-dependencies = []

[[jsr-package]]
scope = "std"
name = "path"
version = "1.0.6"
source = { kind = "jsr-registry", registry = "https://jsr.io" }
tarball-blake3 = "fedcba..."
jsr-integrity = "..."
sigstore-attested = true
api-surface-sha256 = "..."
shim-sha256 = "..."
capabilities-declared = ["fs"]
dependencies = []

tarball-blake3 is the BLAKE3-256 of the package tarball (the bridge's primary verification hash, matches MEP-57's broader BLAKE3 + secondary-hash rule).

tarball-integrity (npm only) is the SRI-formatted sha512-... string the npm registry publishes in the package's dist.integrity field. The bridge cross-checks this against the downloaded tarball at lock time.

jsr-integrity (JSR only) is the JSR-published BLAKE3 manifest hash. The bridge cross-checks this against the downloaded source tree at lock time.

sigstore-attested records whether the bridge found a Sigstore attestation for the published version (npm Trusted Publishing produces attestations; JSR Trusted Publishing produces attestations; un-attested versions are accepted with a warning unless [ts.capabilities] sigstore-required = true).

sigstore-bundle-hash records the SHA-256 of the Sigstore bundle the bridge retrieved at lock time. A drift here at --check time is a hard error (the attestation was rotated, which is normal for re-publishes but rare otherwise).

api-surface-sha256 records the SHA-256 of the ApiSurface JSON the bridge ingested. A drift here at --check time is a hard error.

shim-sha256 records the SHA-256 of the synthesised Mochi shim file.

capabilities-declared is the capability set the manifest declared at lock time.

dependencies is the resolved transitive dep tree.

peer-dependencies is the npm peer-dep graph; the bridge records these separately so a downstream consumer can verify their host package provides the peer.

4. Surface syntax: import ts "..." as <alias>

The Mochi grammar's existing FFI-import production:

ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "ts" | "rust"

gains ts as a Lang alternative. The typescript form (already shipping from MEP-52 phase 12) remains accepted as a backwards-compatible alias. The <spec> form admits:

  • <pkg-name>: bare name, resolves through [ts-dependencies] plus mochi.lock. Bare <pkg-name> defaults to the npm registry; jsr:@scope/name forces JSR.
  • <pkg-name>@<semver-req>: explicit version constraint.
  • npm:<pkg-name>@<version>: explicit npm-registry selector.
  • jsr:@scope/<pkg-name>@<version>: explicit JSR selector.
  • <pkg-name>@git+<url>#<rev>: git source.
  • <pkg-name>@path+<relative-path>: path source.

Example surface:

import ts "zod@^3.23" as z
import ts "jsr:@std/path@^1.0" as path
import ts "hono@^4.0" as hono

fn route(req: hono.Context): async hono.Response {
let schema = z.object({ "name": z.string() })
let body = await req.json()
let parsed = schema.safeParse(body)
if parsed.success {
return req.json({ "greeting": "hello, " + parsed.data.name })
}
return req.json({ "error": parsed.error.message }, 400)
}

The <alias> introduces a Mochi namespace. Symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <pkg-name>.<item>. Item names: TypeScript's exported items follow camelCase or PascalCase; the bridge preserves the original casing (because the runtime resolution targets the original identifier; no rename is performed).

The auto modifier (already accepted by MEP-52 phase 12 for import typescript ... auto) carries forward unchanged.

5. CLI surface

The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:

  • mochi pkg add ts <pkg>[@<semver>]: adds an entry to [ts-dependencies] and runs mochi pkg lock.
  • mochi pkg add ts npm:<pkg>[@<semver>] / jsr:@scope/<pkg>[@<version>]: explicit registry selector.
  • mochi pkg lock: as today (MEP-57), now extended to walk [ts-dependencies], query the npm registry for resolution AND query the JSR registry for any jsr:-prefixed entries, fetch each tarball into the content-addressed cache, cross-check against the registry's integrity hash plus (optionally) Sigstore attestation, run ts-ingest to produce ApiSurface JSON, synthesise the Mochi shim, and write [[npm-package]] / [[jsr-package]] entries.
  • mochi pkg lock --check: as today, now extended to verify tarball-blake3, tarball-integrity / jsr-integrity, sigstore-bundle-hash, api-surface-sha256, shim-sha256, and capabilities-declared for every entry.
  • mochi pkg lock --sigstore-required: hardens the lock to refuse un-attested versions on both registries.
  • mochi pkg publish --to=npm [--dry-run]: builds the package as a typed npm library via TargetNpmLibrary, validates with tsc --noEmit, runs npm pack --dry-run to verify the manifest, hands off to the MEP-52 Phase 18 emitted GitHub Actions workflow for the actual publish (the local CLI invocation is the developer-side dry-run; the CI workflow does the real publish).
  • mochi pkg publish --to=jsr [--dry-run]: parallel to --to=npm, targets JSR Trusted Publishing.
  • mochi pkg publish --to=both: publishes to both registries from the same workflow run, recording the per-registry attestation in the release notes.
  • mochi pkg sync ts: regenerates the Mochi shim file from scratch (without changing the lockfile). Used after manual edits to the synthesised shim.

6. Build orchestration

When a Mochi program contains one or more import ts "..." declarations, the MEP-52 build driver gains the following extensions:

  1. Before invoking the JS bundler, the driver invokes package3/typescript/Bridge.PrepareWorkspace(workdir, mochiLock) which:

    • For each [[npm-package]] in mochi.lock, materialises the npm tarball from the content-addressed cache into <workdir>/node_modules/<pkg>/.
    • For each [[jsr-package]] in mochi.lock, materialises the JSR source tree from the content-addressed cache into <workdir>/jsr_cache/<scope>/<pkg>/.
    • Writes a <workdir>/package.json dependencies field listing every npm dep with the exact version pinned (matches the existing TargetNpmPackage pattern).
    • Writes a <workdir>/imports.json import-map listing every JSR dep mapped to its source path (Deno / Bun read this map directly; for Node 22 LTS, the imports are routed through tsconfig.json's paths field or via the runtime's --experimental-vm-modules shim).
  2. The MEP-52 emit pass treats each import ts "<pkg>" as <alias> as a Mochi import "./ts_shims/<pkg>/shim.mochi" as <alias> shim, where the shim file is the extern fn corpus the bridge emitted. The corresponding emitted JS contains a normal import { ... } from "<pkg>" statement; the host runtime resolves it through the synthesised node_modules / imports.json.

  3. No wrapper package is built. The host JS runtime is the link layer.

  4. The MEP-52 driver runs the existing target build (e.g., tsc + npm pack for TargetNpmPackage, or bun build for TargetBrowserBundle) against the workspace.

  5. The driver invalidates the cache when any [[npm-package]] or [[jsr-package]] shim-sha256 changes (per MEP-52 phase 0's cache-key construction).

7. Promise / async bridge

The TS / JS bridge has no per-import runtime construction. Every consumed package's async function and Promise<T> return runs through the host JS runtime's existing event loop. The bridge's only async-specific work is at the type-binding side: a TS Promise<T> return is translated to a Mochi async fun(...): T, which triggers the MEP-52 phase 11.3 async colour pass on the Mochi caller. The synthesised extern fn carries an async modifier in the Mochi declaration:

extern async fn fetch(url: string, opts: Options): Response from ts "node:fetch"

The Mochi caller writes await fetch(...) and the emitted JS produces a direct await fetch(...) call. There is no await ceremony in the bridge layer, no Promise.resolve wrapping, no microtask scheduling delta.

For TS AsyncIterable<T> (used by for await (const x of source) consumers), the bridge translates to Mochi stream<T> (matches MEP-52 phase 10). The Mochi caller writes for x in source { ... } and the emitted JS produces a for await loop.

8. ESM vs CJS interop

A consumed package may ship CJS-only (require()-style, exports.foo = ...), ESM-only (import { foo } style, export const foo = ...), or dual-export (both, via the exports map). The bridge's emit-side default is ESM (MEP-52 phase 1's emit is ESM by default since the Node 22 LTS + Deno 2 + Bun 1.1 + browser quad-target makes ESM the lowest-common-denominator).

When a consumed package is CJS-only:

  • Node 22 LTS supports import foo from "cjs-pkg" via the runtime's built-in interop (default-import maps to the CJS module.exports).
  • Deno 2 supports CJS-only npm packages via the npm: specifier with built-in interop.
  • Bun 1.1 supports CJS imports natively via the runtime's interop.
  • Browser does NOT support CJS without a bundler; the bridge's TargetBrowserBundle path catches CJS-only deps with a build-time error and prompts the user to add an ESM-side wrapper.

The bridge records the consumed package's module shape in the lockfile (package.json "type" field or "main" vs "module" vs "exports" resolution path) and surfaces a SkipReport entry naming the shape so the user knows what to expect.

Phases

See /docs/implementation/0072/ for the per-phase tracking matrix. Eighteen phases cover skeleton (0), npm-registry client (1), JSR-registry client (2), .d.ts ingest helper (3), ApiSurface JSON schema (4), type-mapping table (5), Mochi extern emitter (6), import-ts grammar extension (7), build orchestration (8), mochi.lock integration (9), TargetNpmLibrary emit (10), TargetJsrLibrary emit (11), npm Trusted-Publishing publish flow (12), JSR Trusted-Publishing publish flow (13), Promise / async bridge (14), monomorphisation of generics (15), ESM vs CJS interop pass (16), and edge-runtime + Jupyter consume-side gate (17).

A phase is LANDED only when its gate is green for every target in the matrix below.

Target matrix

Phasehost node 22 LTS (darwin-arm64)deno 2 (linux-amd64)bun 1.1 (linux-amd64)browser es2024edge (workers/edge/deploy)
0. skeletonNOT STARTEDn/an/an/an/a
1. npm registry clientNOT STARTEDn/an/an/an/a
2. JSR registry clientNOT STARTEDn/an/an/an/a
3. .d.ts ingest helperNOT STARTEDn/an/an/an/a
4. ApiSurface JSONNOT STARTEDn/an/an/an/a
5. type-mapping tableNOT STARTEDrequiredrequiredrequiredrequired
6. extern emitterNOT STARTEDrequiredrequiredrequiredrequired
7. import-ts grammarNOT STARTEDrequiredrequiredrequiredrequired
8. build orchestrationNOT STARTEDrequiredrequiredrequiredrequired
9. mochi.lock integrationNOT STARTEDrequiredrequiredrequiredrequired
10. TargetNpmLibrary emitNOT STARTEDrequiredrequiredn/a (no library shape in browser)required
11. TargetJsrLibrary emitNOT STARTEDrequiredrequiredn/arequired
12. npm publishNOT STARTEDn/a (publish is host-only)n/an/an/a
13. JSR publishNOT STARTEDn/an/an/an/a
14. Promise / async bridgeNOT STARTEDrequiredrequiredrequiredrequired
15. monomorphisationNOT STARTEDrequiredrequiredrequiredrequired
16. ESM/CJS interopNOT STARTEDrequiredrequiredrequired (no CJS)required (no CJS)
17. edge + Jupyter consumeNOT STARTEDrequiredrequiredrequiredrequired

A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.

Alternatives considered

  1. Parse .d.ts directly with a hand-written Go parser instead of via the TypeScript compiler API. Rejected: the TypeScript grammar is the single largest language grammar in mainstream production use (rough lower bound 1.5K production rules). A passable subset is tens of thousands of LOC in Go and immediately stale; the TypeScript team ships new syntax (satisfies, const assertions, using declarations, accessor keyword, ambient namespaces) every release. The compiler API is the authoritative source.

  2. Use the tsserver protocol over stdin/stdout instead of the in-process compiler API. Rejected: tsserver is intended for editor integration and has an editor-shaped request/response loop with state. The in-process compiler API is the correct level for a batch ingest pass.

  3. Use DefinitelyTyped @types/<pkg> as the primary type source. Rejected: DT lags upstream by months for many packages and is unmaintained for many others. The bridge accepts DT as a fallback only when the source package ships no .d.ts.

  4. Use dts-bundle-generator or api-extractor to produce a single rolled-up .d.ts and parse that. Acknowledged: this is the right tool when the consumed package has many internal files; the bridge invokes api-extractor style rollup internally as part of the ingest pass when the package's .d.ts tree is multi-file.

  5. Synthesize a wrapper TS package per imported package (mirroring MEP-73 and MEP-74). Rejected: this is the structural simplification MEP-72 is designed to take advantage of. The host JS runtime IS the link layer; no wrapper is needed. The Mochi shim file IS the binding layer.

  6. Synthesize a Sigstore attestation in the Mochi shim file itself. Rejected: attestations are registry-side records, not source-side artefacts. The Mochi shim file is a binding; the attestation lives in the lockfile.

  7. Use Deno's deno info JSON output as the ingest source instead of the TypeScript compiler API. Acknowledged: deno info --json <module> produces a dep graph + type tree the bridge could consume on the JSR side. The bridge prefers the TS compiler API for unified npm + JSR ingest because the same code path covers both registries; a future sub-phase could add a --ingest=deno mode for JSR-only projects.

  8. Allow long-lived NPM_TOKEN / JSR_TOKEN API tokens for publish (mirror the legacy npm publish path). Rejected: matches MEP-52 phase 18's stance and MEP-73 + MEP-74's broader principle that long-lived tokens are deprecated. The xz-utils, event-stream, and 2025 npm reflected-string flood incidents trace to compromised long-lived tokens; the industry direction is unambiguously toward Sigstore-keyless OIDC. MEP-72 ships exclusively on that path.

  9. Use a Mochi-native TypeScript-AST emitter to produce .d.ts for published packages (instead of tsc --declaration). Rejected for v1: tsc's declaration emit is the authoritative source for .d.ts (downstream tsc consumers expect tsc-emitted declarations; a hand-written emitter would diverge subtly). MEP-72 invokes tsc --declaration --emitDeclarationOnly against the MEP-52-emitted TS source as part of TargetNpmLibrary.

  10. Translate TypeScript decorators (@decorator) into Mochi macros. Rejected: TypeScript's runtime decorator implementation requires runtime metadata reflection (Reflect.metadata, the reflect-metadata polyfill), which is not Mochi's surface. The bridge surfaces decorators as opaque-side-effects on declarations and skips the decorator semantics from the type-mapping table; the consumed package's own runtime handles decorator execution.

  11. Use the npm provenance field on package.json as the trust boundary. Acknowledged: the npm provenance field is the publish-side attestation. The bridge already reads this from the npm registry's package metadata; the lockfile records sigstore-attested = true/false accordingly.

  12. Hand-author the type-mapping table per-package rather than have one shared closed table. Rejected: this is the same boilerplate-violation argument from MEP-73 §A13 and MEP-74 §13. The closed table is a one-time investment that covers every package.

  13. Use a single registry endpoint (npmjs.org only, with JSR as a future addition). Rejected: JSR is the typed-native modern alternative and is GA. Treating it as a future addition would push the Mochi-Deno story behind the Mochi-Node story by a major release; both registries ship at once.

  14. Use the import attributes proposal (import zod from "zod" with { type: "json" }) instead of import ts "...". Rejected: import attributes are for module-level metadata (e.g., JSON modules), not for FFI-level metadata. The Mochi import ts form is the FFI surface; the host JS runtime's import is the link surface; these are two different concerns.

Risks

  1. TypeScript compiler API memory footprint. Loading a large package's .d.ts tree (e.g., typescript itself, ~50K LOC of .d.ts) into ts.createProgram uses ~200 MB of RAM per program instance. The ingest helper is a separate process and exits after the JSON emit, so the RSS is bounded to a single package at a time. Mitigation: the ingest pass is sequential per package; parallelism is bounded by runtime.NumCPU() / 4 to keep total RSS under 1 GB on a typical laptop.

  2. .d.ts quality varies widely across npm packages. Some packages ship .d.ts with any returns, unknown parameters, or unresolved generic parameters (where the type would require running the package's own build script to resolve). Mitigation: the bridge's ApiSurface JSON records the any / unknown markers; the type-mapping pass treats them as "skip with SkipReport". The user can add a hand-written extern fn with a refined signature.

  3. Conditional types are notoriously hard to evaluate eagerly. The TS compiler API's checker.getTypeAtLocation returns a resolved type, but the resolution depends on the call site. The bridge resolves conditional types at the export-position type-binding site, which catches the most common case (function foo<T>(x: T): ConditionalReturn<T> resolves to a wildcard the bridge represents as a generic, surfaced via SkipReport). The user opts into monomorphisation per-call-site.

  4. CJS-only packages on the browser target. The browser target cannot consume CJS without a bundler shim. The bridge's TargetBrowserBundle path catches CJS-only deps with a build-time error and prompts the user to add an ESM-side wrapper. The lockfile records each package's module shape.

  5. Package squatting and typosquatting on npm. The npm registry has a known history of typosquatting attacks (e.g., lodahs instead of lodash). Mitigation: the bridge logs every resolved package name in the lockfile; a mochi pkg audit --provenance run cross-checks every package against the registry's Sigstore attestation status. Un-attested packages produce a warning.

  6. Sigstore attestation gap. Trusted Publishing on npm GA'd April 2024; many packages published before that date have no attestation. Mitigation: [ts.capabilities] sigstore-required = false is the default (un-attested packages accepted with a warning); a stricter mode rejects them. The fraction of attested npm packages was ~3% in May 2025; projected to ~25% by May 2027 as --provenance adoption rolls out.

  7. JSR is still small (12K+ packages, April 2026). Most TS / JS ecosystem coverage is npm-only. The dual-registry surface adds complexity for an ecosystem that is currently npm-dominant. Mitigation: JSR support is built-in but not required; projects can opt-out via [ts] default-registry = "npm" and never import a jsr: prefix. As JSR coverage grows (@std/* for the Deno stdlib is JSR-native; the Hono framework's typed surface is JSR-native), this risk decays.

  8. ESM-CJS dual-package hazard. Some packages ship CJS + ESM duals where the CJS path and the ESM path have different module state (the "dual package hazard"). The bridge surfaces this via a SkipReport when both paths exist and their resolved ApiSurface differs; the user picks one.

  9. TypeScript major-version compatibility. TypeScript 5.x and 6.x (projected late 2026) may break compiler-API users. The bridge bundles the TypeScript version it was built against (currently 5.6) into the ingest helper binary; upgrades to the compiler API are MEP-72 sub-phase concerns.

  10. Bun's TS support is not 100% compatible with tsc. Bun's bundler and runtime ship their own TS implementation that diverges from tsc on edge cases (e.g., enum const-folding, decorator transforms). Mitigation: the bridge invokes the official typescript package's compiler API for the canonical ingest; Bun is one of the runtime targets but not the ingest authority.

  11. Edge runtimes have a restricted API surface (no node:fs, no node:net, no node:child_process, no node:worker_threads). A package's package.json engines field may declare incompatibility with edge. Mitigation: the bridge's edge-runtime gate (phase 17) checks the consumed package's engines.node and engines.workerd declarations at lock time; incompatible packages produce a clear diagnostic.

  12. The Mochi shim file contains every public extern fn for the consumed package; large packages produce large shim files. A lodash shim with 270+ exported functions produces a ~15K-LOC Mochi shim. Mitigation: the bridge writes shim files into target/ts_shims/<pkg>/shim.mochi; they are .gitignored by default and regenerated on lock. The user does not normally read or hand-edit them.

  13. Deno + Node + Bun + browser runtime drift. Subtle behaviour differences across the four targets (e.g., setImmediate is Node-only, globalThis.process is Node + Deno + Bun but not browser) mean the consumed package's behaviour can be runtime-conditional. Mitigation: the bridge does not promise runtime-conditional behaviour resolution; the user's host runtime is the authority. The capability declaration model surfaces the runtime requirements as Mochi-visible capabilities.

Acknowledgements

This MEP builds on MEP-52 (TypeScript transpiler) for the tstree IR, the build driver, the runtime stub layer, the cgo-equivalent (which TS / JS does not have because the runtime is shared), the TargetNpmPackage / TargetDenoJsr / TargetBrowserBundle / TargetReleaseWorkflow targets, the existing import typescript "..." FFI surface, and Phase 18's Trusted-Publishing workflow emit; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the BLAKE3 + secondary-hash dual-hashing, the content-addressed object store, and the capability declaration scheme; on MEP-45 (C transpiler) for the FFI sidecar pattern; on MEP-73 (Mochi+Rust package bridge) and MEP-74 (Mochi+Go package bridge) for the bidirectional-bridge spec template, the closed-type-table philosophy, the lockfile-cross-check rule, and the capability-monotonicity rule; on the TypeScript compiler API maintained by the Microsoft TypeScript team for the canonical ApiSurface, with stability guarantees since 3.0 (2018); on the typescript npm package's tsc, tsserver, and programmatic ts.createProgram + ts.TypeChecker API for the ingest path; on npm's Trusted Publishing GA (April 2024) for the Sigstore-keyless OIDC publish flow; on JSR's Trusted Publishing GA (mid-2024) for the parallel publish flow; on JSR's source-not-dist invariant and server-side transpilation for the simplified library shape; on the Sigstore project and the OpenSSF Trusted Publishing initiative for the keyless OIDC signing flow; on Deno's npm: specifier and jsr: specifier for the dual-registry import resolution model; on Bun's runtime and bundler for the third major JS runtime target; on the node: built-in module namespace for the runtime API discrimination model; on the import-map proposal (TC39 stage 3) for the Mochi-side import-map synthesis; on the package.json exports map (Node 12+, May 2019) for the conditional-exports model; on the tsd and expect-type projects for prior art on .d.ts test harnesses; on the Hono, Drizzle, Effect, ts-pattern, and Zod projects for representative typed-TS-package fixtures; on the api-extractor and dts-bundle-generator projects for .d.ts rollup prior art; and on the broader TypeScript-Deno-Bun ecosystem for the four-runtime convergence (Node 22 LTS + Deno 2 + Bun 1.1 + browser ES2024) MEP-72 ships against.