Write an Exact app's native code in TypeScript, calling UIKit, MapKit and every other Objective-C API directly, with no Swift and no native rebuild per edit.
Exact's ~60 packages cover the common platform APIs. This reaches the rest: an app's own native module, or a
package, is a TypeScript file run by NativeScript on a Hermes runtime of its own, made
by the Hermes Exact already links (docs/shared-hermes.md).
Experimental. Runs on the iOS Simulator, an iPhone 16 Pro and macOS; Android, Windows and Linux have a path (below), not yet a build. Not an official Exact or NativeScript release.
What this does not change, so Exact's guarantees hold:
- No JavaScript runs before first pixel. The VM starts when Exact loads native modules, after first pixel.
app.tsis untouched. Its VM stays hardened and deterministic. NativeScript has a VM of its own, because Exact's hardening breaks NativeScript (spike/build.shshows both).- The same trust as Swift. A TypeScript module sits where a Swift module sits and has the same reach. It enforces its own grants, as a Swift module does.
- No code over the air. Exact's signed updates carry plans and assets, not modules. TypeScript modules must stay out of them; this is a rule to enforce, not yet enforced.
- No patches to Exact. It links through Exact's existing
modules/apple/*.xcframeworksupport. NativeScript takes two small patches (patches/).
An app's whole native module, modules/apple/native.ts:
import { defineModule } from 'nativescript-exact';
defineModule({
call(request) { // app.ts: native.call({ op: 'device' })
return { name: UIDevice.currentDevice.name };
},
views: {
'ns-slider': (props, events) => { // Contract: ns-slider level=level message=moved
const slider = UISlider.new();
// …target-action into events.message(String(slider.value))
return { view: slider };
},
},
});Or a package Contract calls directly, beside a Swift module (apps/mixed):
use searchNearby, PlaceResult from "@ns-exact/local-search"
NativeScript's model is the same on every platform: generated metadata describes the platform's own APIs, the runtime binds JavaScript to them directly, and generated typings let a checker refuse a wrong name before anything runs. What differs is the API family and how the runtime attaches to a JavaScript engine. For Exact the question on each platform is the one iOS answered: can NativeScript run on the Hermes Exact already links, and can Exact load it where it loads a native module?
| Platform | NativeScript runtime | What TypeScript reaches | How it binds | What Exact needs | Status |
|---|---|---|---|---|---|
| iOS, macOS | NativeScript/runtimes (ffi/objc) |
Every Objective-C API: UIKit, AppKit, MapKit, CoreHaptics, a vendor's SDK | JSI, on a runtime made by Exact's own Hermes (docs/shared-hermes.md) |
Nothing: it links through modules/apple/*.xcframework |
Done: apps/atlas, apps/nslab, apps/nsmac |
| Android | NativeScript/runtimes (runtime/android/jsi, ffi/jni/jsi), the backend @nativescript/react-native runs on Android |
Every Java and Kotlin API, through JNI, from the app's metadata | JSI, as on Apple: tns::Runtime::Attach takes an existing jsi::Runtime |
Two changes: hand modules its Hermes (makeHermesRootAPI; libexactcanvas.so keeps it local today), and pack a module's native library into the APK (modules/android is Kotlin only) |
A path, measured: docs/shared-hermes.md, "Android, where it stands" |
| Windows | @nativescript/windows-napi (NativeScript/windows) |
The Windows Runtime, .NET, and any DLL's exports (win32.bind('kernel32.dll')) |
Node-API: a native addon for any Node-API host (Node, Bun, Deno, and the hermes.dll React Native for Windows loads, in NativeScript/runtimes#112) |
Node-API on the Hermes that runs it (below) | A path |
| Linux | @nativescript/linux and @nativescript/linux-napi (triniwiz/linux-runtime) |
C libraries, from their headers: GTK 3 and 4, GLib, D-Bus, cairo, Pango, EGL, X11, libc | A V8 runtime, and the same runtime over Node-API (its napi/), on QuickJS, V8, JavaScriptCore or Hermes |
Node-API on the Hermes that runs it (below); Exact's Linux host does not run TypeScript yet (reference.md, "Limits") |
A path |
Windows and Linux meet at one requirement. Both runtimes bind through Node-API, and Exact's Hermes carries none
(no napi_* symbol and no js_native_api.h in its pinned bundle, checked on macOS). Any of three ways gives it to them, in order of preference:
- Exact builds its Hermes with Node-API, as
microsoft/hermes-windowsis built, which linux-runtime's Hermes engine already runs on. One Hermes per process stays true, and both runtimes attach as they are. - A Node-API shim over JSI, carried by the module, in the style of the shims linux-runtime's
engines/puts over QuickJS, V8 and JavaScriptCore. It needs nothing from Exact beyond the Hermes it already hands out on Apple; it does not exist yet. - The module brings a Hermes with Node-API, as NativeScript/runtimes#112 does on React Native for Windows: it works today, at the cost of a second engine in the process (about 1.9 MB on iOS, before it was removed there).
On Android, Windows and Linux, Exact paints its own pixels, so a module there reaches the platform's APIs (calls,
laters, data, devices) rather than placing the platform's views in Exact's layout, as modules/apple views do. Exact
already admits a native library per target for a package (exact.json native.library, pinned by SHA-256), which
is where a NativeScript runtime for those hosts would ship. One TypeScript module per platform
(native.android.ts, native.windows.ts, native.linux.ts, beside native.ios.ts and native.macos.ts) then
gives an app the same reach on every target Exact builds, with the checker, the live loop and the session's clock
as docs/native-in-typescript.md describes them for Apple.
- Reach: any Objective-C API, including ones no Exact package wraps, such as MapKit's local search in
packages/local-search. - The loop: a saved edit runs on the device 0.26 s later on the Mac, 0.24 to 0.39 s on the iOS Simulator and
0.43 to 0.47 s on an iPhone over Wi-Fi, with the app's state kept. The same edit in a Swift module took 12.6 to
13.4 s to reach the simulator's screen, relaunched (
docs/the-case.md). - Mistakes are refused:
nsx.mjs checktype-checks against the whole SDK in under a second, reports incontract build --json's shape, and names the SDK's nearest member for a misremembered one, so a hallucinated selector is a refusal with its fix, notundefinedat run time. - The cost: first pixel unchanged on an iPhone, the module ready about 45 ms later than a Swift one. A release
build keeps only the SDK metadata its bundle names (Atlas: 432 KB of iOS's 4.5 MB), and on the Mac NativeScript
then starts in 3.5 to 4.6 ms and adds 2.4 to 2.6 MB; its modules library is 3.9 MB stripped, 1.0 MB compressed.
A call costs more than Swift's: about 1.3 µs for an Objective-C property where Swift takes 0.27 µs
(
docs/the-case.md). - Tokens: in a small A/B, agents writing TypeScript used 13% fewer tokens and 24% less time than agents
writing Swift (
docs/ab-results.md). Two runs each, so treat it as a direction.
You need macOS, Xcode, Rust, CMake and Bun 1.4.2. scripts/setup.sh fetches the pinned Exact and NativeScript
sources and builds NativeScript's metadata generators and symbol analyzer from them into .gen/metadata-tools
(about a minute; NSX_METADATA_TOOLS names another build of them).
scripts/setup.sh # pinned Exact and NativeScript, patched; the metadata tools
cd apps/nslab
bun ../../scripts/nsx.mjs build . # the bridge, the module's bytecode, NSExact.xcframework
bun exact.mjs ios --run # Exact's own build, on a simulator
bun ../../scripts/nsx.mjs js . # after an edit to native.ts: checked and reloadeddocs/native-in-typescript.md is the recipe an agent reads. docs/next-steps.md is what is left.
| Path | What |
|---|---|
bridge/ |
The C seam (nsx.mm), the JavaScript runtime (runtime.ts), and the Swift adapters for an app module and a package |
scripts/nsx.mjs |
check, js, build, package |
scripts/sessions-ios.mjs |
Two sessions of apps/nslab in Exact's sample host: apart, then one destroyed under the other |
packages/local-search |
A package implemented in TypeScript |
packages/haptic-pattern |
A custom CoreHaptics pattern, which no Exact package plays; apps/nslab uses it |
apps/atlas |
The demo, on iPhone and Mac: a 3D MapKit flyover, NaturalLanguage, haptics; shared and platform files (docs/demo.md) |
apps/nsmac |
A macOS module in TypeScript: a call, a later, an NSSlider view |
apps/nslab |
An app module in TypeScript: a call, a later, a native view, a hatch, diagnostics |
apps/mixed |
A Swift app module beside the TypeScript package |
apps/ab-* |
The A/B's four apps, as the agents left them except the declare lines the checker now refuses; ab-ns-1 and ab-swift-1 are the device comparison |
spike/ |
The bridge on macOS outside any app, in its own VM and under Exact's hardening |
patches/ |
NativeScript patches |
MIT, except patches/, which modify Apache-2.0 NativeScript runtimes
sources and are Apache-2.0.