janela

janela

/ʒaˈnɛlɐ/ — Portuguese for window

Desktop and mobile apps in pure TypeScript, compiled to native.

No Rust. No Node. No Electron. Your backend is TypeScript compiled to a real native binary; your frontend runs in the OS webview. macOS, Linux, Windows, iOS and Android — one runtime, and desktop binaries around 190–390 KB.

$ pnpm create janela
TypeScriptfront and back
scriptcTS → native compiler
native binaryno runtime shipped
+
OS webviewfive platforms, one API

The only JavaScript engine in the room is the one already inside the system webview — no V8, no Node, no bundled Chromium.

Five targets, one runtime

The same main.ts, the same typed contract and the same frontend build for every platform. Only the shell underneath differs — each one drives the webview the OS already ships.

PlatformWebviewBuildOutput
macOSWKWebViewjanela buildbinary + .app
LinuxWebKitGTKjanela buildbinary
WindowsWebView2janela build.exe (GUI subsystem)
iOSUIKit + WKWebViewjanela build --target iossimulator .app
Androidandroid.webkit.WebViewjanela build --target android.apk

Commands, the typed contract, events, async commands and file I/O behave the same on all five — the shell owns the clock and the file queue on each. Native file dialogs and runtime window control are desktop-only for now; on mobile they report clearly when called.

One language, both sides of the window

Register commands in TypeScript that compiles to native code. Call them from the page with a promise. Push events back. That's the whole API — and it will feel familiar if you've used Tauri.

src-host/main.ts — compiled to native
import type { JanelaApp } from "janela/host";

// one declaration; the page is checked against it
export type AppCommands = {
  add: (args: { a: number; b: number }) => number;
};
export type AppEvents = { added: number };

// the app, carrying its contract
export type App = JanelaApp<AppCommands, AppEvents>;

export function setup(app: App) {
  app.command("add", (args) => {
    // args inferred; return type checked
    app.emit("added", args.a + args.b);
    return args.a + args.b;
  });
}
src/App.tsx — runs in the OS webview
import { createClient } from "janela/api";
// type-only: erased, no host code in the bundle
import type { App } from "../src-host/main";

const client = createClient<App>();

const sum = await client.invoke("add", { a: 2, b: 40 });
// sum: number — checked against the host's contract

const off = client.on("added", (payload) => {
  console.log("host says:", payload);  // payload: number
});

Features

A worked example — commands, events, and a file reader — lives in examples/demo.

Tiny binaries

Roughly 190–390 KB per desktop app and 195–388 KB packaged for a phone. No bundled browser, no bundled runtime — the window is the webview your OS already ships. Sizes below.

Pure TypeScript, front and back

The backend is TypeScript compiled to a native binary by scriptc. No Rust to learn, no Node process to ship.

Tauri-shaped API

invoke() and listen() in the page; app.command() and app.emit() in the host — or declare a contract and have command names, arguments, results and event payloads checked by the compiler, with no code generation.

Non-blocking file I/O

app.readFileAsync does the work off the UI thread and drains under a time budget, so a 100 MB read holds page round-trips at a 1 ms p99 instead of freezing the window.

Real app bundles

janela build produces the native binary — plus an ad-hoc-signed .app on macOS, a GUI-subsystem .exe on Windows, and an .apk or simulator .app for mobile.

MIT licensed

Free to use, fork, and ship. Builds on webview/webview (MIT) and scriptc.

Quick start

Node 24+ and a C++ toolchain for the platform you are building: Xcode Command Line Tools on macOS, g++ + libwebkit2gtk-4.1-dev on Linux, an llvm-mingw clang on Windows. iOS additionally needs Xcode and zig; Android needs a JDK, the Android SDK, the NDK and zig.

$ pnpm create janela                # prompts for a name and a template
$ cd my-app                         # deps are already installed
$ janela dev                        # Vite dev server + HMR, in a native window
$ janela build                      # .janela/out/my-app  (+ .app on macOS)
$ janela build --target ios         # simulator .app
$ janela build --target android     # .apk

What comes out, measured on an Apple Silicon Mac:

TargetVanillaWith Vue
Desktop (macOS arm64)224 KB289 KB
iOS (simulator)195 KB259 KB
Android (APK)325 KB349 KB

All figures measured directly at 0.14.1 on scriptc 0.0.36 — the desktop row roughly halved when 0.0.36 added stdlib tree-shaking, and the mobile rows fell 43% and 31% once janela dead-stripped the two library-mode links that tree-shaking does not reach. Every one of the five templates — vanilla, Vue, React, Svelte, Solid — is built and run on all three targets; the full matrix is in docs/frontend.md. A React frontend adds roughly 130 KB over Vue on desktop, far less in an APK.

Where it stands

janela is young and pre-1.0. Desktop is the most exercised path; iOS and Android are new, and iOS is simulator-only so far. Async commands interleave rather than run in parallel — host code is single-threaded by design, so a CPU-bound handler still needs slicing. File dialogs do work on mobile — openFileDialog landed on iOS and Android in 0.13.0. Not there yet: saveFileDialog and window control on mobile (both deliberate — a phone has no window, and mobile “save” means exporting a file that already exists), iOS device builds and code signing, icons, installers and notarization, tray icons and menus, multi-window, and an async HTTP client. Directory picking is unsupported on Windows and iOS. The design notes and the compiler findings behind all of it are in docs/findings.md, with per-platform notes in docs/ios.md and docs/android.md.