# Native Shell > Native Shell: AI-generated UI, native in your app, on every platform. Apps on iOS, Android, macOS, Windows and Linux, GPUI apps and web pages load signed, sandboxed TSX modules at run time and draw them with the platform’s own UI. Apps are TSX modules run by QuickJS on a native Rust runtime; the toolchain is the single `nsh` binary (`nsh init my-app`, `nsh run my-app/App.tsx`). Every page below is also served as Markdown at its URL plus `.md`. --- # Introduction Native Shell (NSH for short) is designed for **AI-generated, dynamically delivered native UI**. A model writes a small module in TSX, and a native app loads it at run time and shows it with the platform's own UI, inside a sandbox the app controls. The same mechanism makes native apps extensible in general — plugins, features, whole screens delivered after release. NSH runs on phones and desktops alike: iOS (UIKit), Android (Android views), macOS (AppKit), Windows (Win32 windows drawn with Direct2D) and Linux (GTK 4). Each platform has a native backend of its own, and the same module runs on all of them ([Platforms](https://nsh.dev/docs/platforms.md)). > [!IMPORTANT] > NSH is the layer inside an app for UI that is generated, changed or added after it ships. On a phone or a desktop, the app stays a native app, built and released the way it always was. ## Why it exists **First: UI generated by AI.** An assistant inside your app can answer with an interface rather than with text. A model writes a small TSX module (a form, a chart, a booking card), your server checks, builds and signs it, and the app downloads it and shows it in an `NshAppView`: native UI, sandboxed, limited to what you granted, and with no app update. NSH is built around this use, so its design is AI friendly throughout: - **A format models write well.** A module is one TSX file using signals, a handful of host elements and Tailwind classes — the vocabulary models already know from the web, with no build configuration and no NPM. - **A precise, generated contract.** Every element, prop, style key and module has TypeScript declarations generated from the Rust sources (`.nsh/types`), so generated code can be type-checked, and a model can be given the exact API instead of guessing. These docs are also served as plain text for models ([`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt)). - **Checkable before it ships.** `nsh trace` runs a module headless, without a device, and reports errors and the views it produced; build errors come with a code frame. A generation loop can compile, run, read the result and retry. The compiler also runs in-process as a library (`nsh_bundler::compile_module`) and in the browser ([Playground](/playground)). - **Safe to run when it is not trusted.** Generated code is untrusted code. Each module runs in its own QuickJS VM with memory, call-time and node limits, reaches only the capabilities the host grants, and arrives as a signed package the app verifies first. - **Native without native code.** The output is the platform's own UI — UIKit, Android views, AppKit, GTK 4 or Win32 with Direct2D — with the system's text, input and accessibility, yet the model never writes Swift, Kotlin, Java or Rust, and the app never needs a new release. ![From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI.](https://nsh.dev/diagrams/ai-flow-light.svg) **Second: extending native apps, and delivering UI dynamically.** The same mechanism serves any module, not only generated ones. The app embeds an `NshAppView` (iOS), `NshView` (Android) or `AppView` (macOS, Windows, Linux) wherever a module should appear, and NSH runs the module inside it: - **Delivered after release.** A module is a signed package (`.nshpkg`) the app downloads at run time. The app checks its signature against the keys it trusts and against a revocation list before running it ([Signed packages](https://nsh.dev/docs/signed-packages.md)). A change needs no new binary and no store release. - **The host stays in control.** Each module runs in its own sandboxed VM, with memory, time and node limits, and reaches only what the host grants: network hosts, web pages, the status bar ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). Several modules can run side by side; there are no global singletons. - **It looks like the rest of the app.** Modules render with the platform's own UI, so text, input, accessibility and scrolling are the system's. The host can register its own native views — a map, a player, a design-system component — for modules to use ([Native components](https://nsh.dev/docs/native-components.md)). **Third: standalone apps.** The same modules can also be a whole app. `nsh package` turns them into an installable iOS, Android, macOS, Windows or Linux app with the prebuilt runtime, which is also the quickest way to try NSH: the [getting started](https://nsh.dev/docs/getting-started.md) guide does exactly that. It is a supported use, but not the one NSH is designed around. ## Why Native Shell, next to GPUI Shell [Longbridge](https://longbridge.com) is building NSH as the plugin layer of its mobile client, and its desktop client needs the same layer. [GPUI Shell](https://gpui-kit.com/shell) makes [GPUI](https://gpui-kit.com) desktop apps extensible in JavaScript, and the first idea was to use it on phones too. It does not fit there, because its modules are drawn by GPUI itself, on the GPU, not with the platform's views: - **System behavior would have to be rebuilt.** A self-drawn surface inside a native screen has none of the platform's text selection, nor the selection menu that comes with it (Copy, Share, Translate, Look Up, Select All), nor the system's input methods, accessibility and new visual materials each iOS release brings. Recreating them in a GPU renderer is a large, never-finished job, and the copies drift from the real thing. - **Mobile teams would have to adopt a new UI stack.** Running a self-drawn layer at scale in an existing mobile client means its iOS and Android teams take on an unfamiliar renderer, its compatibility issues across OS versions and devices, and its bugs. NSH therefore renders modules with each platform's own UI: UIKit on [iOS](https://nsh.dev/docs/embedding-ios.md), Android views on [Android](https://nsh.dev/docs/embedding-android.md), AppKit on [macOS](https://nsh.dev/docs/macos.md), Win32 windows drawn with Direct2D on [Windows](https://nsh.dev/docs/windows.md) and GTK 4 on [Linux](https://nsh.dev/docs/linux.md). One module format and one runtime run in all these hosts. The two are positioned differently. GPUI Shell is for extending GPUI desktop applications; Native Shell is for native apps on every platform, phones and desktops alike. ## A module A module is TypeScript and JSX: ```tsx import { mount, signal, Text, View } from "nsh"; function Counter() { const count = signal(0); return ( {count()} count.update((n) => n + 1)} activeOpacity={0.6} class="rounded-lg bg-blue-600 px-5 py-3" > Tap ); } mount(Counter); ``` `` becomes a `UIView` on iOS, a `ViewGroup` on Android, an `NSView` on macOS and a GTK widget on Linux, `` a native text view, and the button's press feedback (`activeOpacity`) runs on the main thread without a round trip to JavaScript. When you tap, `count.update` changes one signal, and only the text that reads `count()` is updated: the component function does not run again. The host app shows it with a few lines of native code ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app)). On iOS: ```swift let app = NshAppView() // a UIView: place it anywhere try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#) let info = try await app.loadPackage(at: downloadedFile, trust: trust) // verified, then run ``` Only the module's code is JavaScript. Everything it calls into — the reactive graph, the renderer, layout, text and every module it imports — is native, written in Rust. ## How a module runs A module (an "app" in the CLI's terms) is a set of `.tsx` / `.ts` files with an entry, usually `App.tsx`. The toolchain is a single `nsh` binary: 1. **Build.** `nsh` bundles the module's files (with rolldown and Oxc) and compiles the JSX with its own compiler. Imports of `nsh` and `nsh/*` stay imports: the runtime serves them natively, so the bundle holds only your code. The bundle is compiled to QuickJS bytecode. 2. **Run.** On the device, the engine thread runs the bytecode in [QuickJS](https://github.com/quickjs-ng/quickjs). Compiled JSX calls the native renderer directly; changes are collected during one JavaScript turn, laid out with [Taffy](https://github.com/DioxusLabs/taffy) (flexbox and CSS grid), and handed to the main thread as one batch of view operations per frame. 3. **Render.** The main thread applies the batch to native views. Text is measured and laid out once on the engine thread (CoreText on Apple platforms, `StaticLayout` on Android, DirectWrite on Windows, Pango on Linux) and only drawn on the main thread. Because JavaScript only describes the UI and handles events, the work done in the interpreter is small. Parsing styles, resolving Tailwind classes, diffing, layout and text all happen in Rust. > [!NOTE] > The main thread never waits for JavaScript. Questions the system asks synchronously (may this input accept another character, may the user swipe back, may this sheet close) are answered natively from props you set beforehand, such as `maxLength`, `preventBack` or `dismissible`. Events reach your code afterwards. ## What you get - **Embedding.** `NshAppView` (Swift), `NshView` (Java/Kotlin) and the desktops' `AppView` (AppKit, GTK, Win32) put a module in a native app, as many as you like ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app)). - **Signed, revocable delivery.** Ed25519-signed packages, release keys certified by an offline root key, revocation lists ([Signed packages](https://nsh.dev/docs/signed-packages.md)). - **A sandbox the host configures.** VM limits and capabilities granted per view, in the iOS and Android SDKs ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). - **The host's own views in modules.** Register native components from Swift, Java or Rust ([Native components](https://nsh.dev/docs/native-components.md)). - **Native views.** Embedding a native map, web view or video player has no layering problems, accessibility uses the platform's own tree, and text input is the system's. - **Fine-grained reactivity.** [Signals](https://nsh.dev/docs/reactivity.md) like Solid's, implemented natively. Components run once; only the expressions that read a changed signal run again. - **Styles and Tailwind**, resolved natively: flexbox, CSS grid, shadows, gradients, filters, transitions ([Styling](https://nsh.dev/docs/styling.md), [Tailwind](https://nsh.dev/docs/tailwind.md)). - **A mobile UI set** (`nsh/ui/mobile`) whose components are written in Rust and drawn by NSH, so they look the same on every platform: [Button](https://nsh.dev/components/button.md), [Card](https://nsh.dev/components/card.md), [ListView](https://nsh.dev/components/list-view.md), [navigation](https://nsh.dev/components/navigation.md), and more. Desktop layouts are built from the primitives with `desktop={{…}}` props and `Platform.select` ([Platform-specific code](https://nsh.dev/docs/platform-specific.md)), and a desktop UI set is on the way. - **One binary toolchain.** A single `nsh` executable builds modules, writes their type declarations, serves them with hot module replacement, packages and signs them. ## Where it runs | Platform | Status | Backend | Notes | |---|---|---|---| | iOS | Supported (iOS 15.0+) | UIKit | Devices and the simulator; embedded with `NativeShell.xcframework` ([iOS](https://nsh.dev/docs/embedding-ios.md)) | | Android | Supported (API 28, Android 9+) | Android views | `arm64-v8a` and `x86_64`; embedded with `nsh.aar` ([Android](https://nsh.dev/docs/embedding-android.md)) | | macOS | Supported (macOS 14+) | AppKit | Standalone apps; embedded with `AppView` ([macOS](https://nsh.dev/docs/macos.md)) | | Windows | Supported (Windows 10 1809+) | Win32, Direct2D | Standalone apps; embedded with `AppView` ([Windows](https://nsh.dev/docs/windows.md)) | | Linux | Supported (GTK 4.14+) | GTK 4 | Standalone apps; embedded with `AppView` ([Linux](https://nsh.dev/docs/linux.md)) | | GPUI (macOS, Windows, Linux) | Supported | GPUI | Standalone apps; embedded with `AppView` in GPUI and GPUI Kit apps ([GPUI](https://nsh.dev/docs/gpui.md)) | | Web | Supported (modern browsers) | HTML | Static sites; embedded in a page with `createPlayer` or an iframe ([Web](https://nsh.dev/docs/web.md)) | | Mac Catalyst | For development | UIKit | The iPhone-sized window `nsh run` opens on a Mac, to see a module as on a phone | ![One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML.](https://nsh.dev/diagrams/platforms-light.svg) ## How it compares The usual ways to put UI into an app after release are web views, server-driven UI described in JSON, and mini-program containers; for whole apps, cross-platform frameworks such as React Native, Flutter, Electron and Tauri. An NSH module renders native UI where a web view renders a page, carries its own state and logic where a server-driven schema can only recombine built-in components, and runs in a sandbox the host configures in code. [Comparing approaches](https://nsh.dev/docs/comparison.md) goes through each. ## Next steps - [Installation](https://nsh.dev/docs/installation.md) and [Getting started](https://nsh.dev/docs/getting-started.md): get `nsh`, write a first module and run it on each platform. - [Platforms](https://nsh.dev/docs/platforms.md): what runs where, and how to run, package and embed on iOS, Android, macOS, Windows and Linux. - [Signed packages](https://nsh.dev/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md): deliver modules and decide what they may do. - [JSX and components](https://nsh.dev/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/docs/reactivity.md): the two ideas everything else builds on. Source: https://nsh.dev/docs --- # Comparing approaches An app that wants UI it did not ship with — answers generated by a model, features from partners or other teams, screens that change without a release — has a handful of established options, on phones and desktops alike. They differ in what draws the UI, what the delivered code may do, and how much of the app has to change to host them. This page compares them with NSH, and then compares NSH with the cross-platform frameworks used to write whole apps. ## The options | | What draws it | What it can express | Confinement | Fit for generated UI | Cost to the host app | |---|---|---|---|---|---| | **Web view (H5)** | A browser engine | Anything HTML, CSS and JS can | Same-origin rules; native access through a hand-written bridge | Models write HTML well; the result looks and behaves like a web page | A browser engine per view; a bridge to maintain | | **Server-driven UI** | The app's own native views | Only the components and actions the app built in, described in JSON | Total: the schema is the limit | Easy to generate, little to express: no local state or logic beyond what the schema provides | A renderer for every component and its versions, in each app | | **Mini-program containers** | Usually a web engine with a proprietary layer | A platform-specific framework and API | The container's permission model | Proprietary dialect, less known to models | A large container SDK | | **Over-the-air updates of a cross-platform app** (React Native with a code-push service, for example) | That framework's renderer | The whole framework | None: updates are trusted app code | Code generated per request is not what these updates are for | The app has to be written in that framework | | **NSH** | The platform's own UI: UIKit, Android views, AppKit, GTK 4, Win32 with Direct2D | Components with state, logic, animation and layout; host-registered native views | Signed packages, a VM per view with host-set limits, capabilities granted per view | Designed for it: one TSX module, typed API, headless checks | One view and a library in an existing native app | ## How NSH differs - **The platform's own UI.** A module is made of the platform's views, so text selection and its menu, input methods, accessibility and scrolling are the system's own — the same reason NSH exists next to [GPUI Shell](https://nsh.dev/docs.md#why-native-shell-next-to-gpui-shell). - **More than a schema.** Server-driven UI only recombines what the app already ships. An NSH module has its own state, logic, animations and layout, so a model can produce an interface the app's developers never anticipated. - **Confined by the host.** Delivered code is untrusted. NSH checks signatures and revocations before running anything, gives each view its own VM with memory, call-time and node limits, and lets the host decide which network hosts, web pages and system settings a module may reach ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). - **Embedded, not adopted.** The app stays native and keeps its toolchain; NSH is a view placed where a module should appear ([Platforms](https://nsh.dev/docs/platforms.md#embed-in-a-native-app)). - **One module, every platform.** The same bundle runs on iOS, Android, macOS, Windows and Linux, each time with that platform's own UI ([Platforms](https://nsh.dev/docs/platforms.md)). ## Cross-platform frameworks NSH is designed for modules inside native apps, but the same modules can be packaged as whole apps for every platform, so it is natural to compare it with the frameworks people write cross-platform apps in. The comparison is about how each draws and where it runs, not about maturity: these frameworks have large ecosystems that NSH does not have. | | What draws it | Platforms | App code | Code delivered after release | |---|---|---|---|---| | **React Native** | The platform's views on iOS and Android; desktops through separate projects | iOS, Android; Windows and macOS out of tree | JavaScript or TypeScript, with React | Over-the-air updates of trusted app code | | **Flutter** | Its own engine draws every pixel | iOS, Android, macOS, Windows, Linux, web | Dart | Not part of the framework | | **Electron** | Chromium, bundled with the app | macOS, Windows, Linux | HTML, CSS, JavaScript | Web content the app loads | | **Tauri** | The system's web view | macOS, Windows, Linux, iOS, Android | HTML, CSS, JavaScript; Rust for the native side | Web content the app loads | | **NSH** | The platform's own UI on each platform | iOS, Android, macOS, Windows, Linux | TSX modules; the runtime and components are Rust | Signed packages, verified and sandboxed per view | What sets NSH apart: - **Native UI on every platform, not one renderer everywhere.** Flutter draws everything itself, and Electron and Tauri show web pages; NSH builds each platform's own views, so text selection, input methods, accessibility and the system's look come from the platform. - **Small modules inside native apps.** The frameworks above own the whole app. NSH is designed to sit inside an app written with the platform's own tools, as one view among others, and to run modules the app did not ship with, in a sandbox. - **A light runtime.** No browser engine per app or per view; JavaScript only runs the module's own code, and everything it calls is native Rust. ## When another option fits better - The UI is a document or an existing web page: a web view shows it as it is. - The set of screens is fixed and small, and only content and order change: server-driven UI needs no runtime at all. - The whole app is written in a cross-platform framework already: its own update mechanism ships its code. - You need a large ecosystem of third-party packages, or a desktop UI kit with desktop controls today: NSH's components are the mobile set. ## See also - [Introduction](https://nsh.dev/docs.md): why NSH exists and what it is designed for. - [Architecture](https://nsh.dev/docs/architecture.md): what runs where inside NSH. Source: https://nsh.dev/docs/comparison --- # Installation Everything NSH does goes through one command, `nsh`. It bundles and compiles apps, runs the dev server, launches development hosts and packages installable apps. You can use it from a prebuilt distribution, which needs no Rust, or build it from a checkout of the repository. On top of `nsh`, each platform needs its own tools: | Platform | Develop and package on | Needs | |---|---|---| | iOS | macOS | Xcode and an iOS simulator runtime | | Android | macOS, Windows or Linux | The Android SDK and a JDK; an emulator or a device | | macOS | macOS | Xcode's command line tools; apps run on macOS 14+ | | Windows | Windows | Windows 10 1809+; the WebView2 Runtime for web views | | Linux | Linux | GTK 4.14+ (and its development packages to build the runtime) | | [GPUI](https://nsh.dev/docs/gpui.md) | macOS, Windows or Linux, for that OS | A GPU (Metal, DirectX or Vulkan); on Linux, Wayland or X11 | Mac Catalyst, `nsh run`'s default on macOS, needs Xcode like iOS. Details are under [Platform tools](#platform-tools). ## Installing a release Each [GitHub release](https://github.com/longbridge/nsh/releases) carries the prebuilt distribution (below) for macOS (Apple silicon), Linux x86_64 and Windows x86_64. Every way of installing it puts the same `nsh` command on your `PATH`. **macOS and Linux**, with the install script: ```sh curl -fsSL https://nsh.dev/install.sh | sh ``` It installs the distribution to `~/.nsh` and links `~/.local/bin/nsh` to it; running it again updates it. `NSH_VERSION=v0.1.0` pins a release, `NSH_INSTALL_DIR` picks another directory. **Windows**, in PowerShell: ```powershell irm https://nsh.dev/install.ps1 | iex ``` It installs to `%LOCALAPPDATA%\nsh` and adds its `bin` to your user `PATH` (open a new terminal afterwards). `$env:NSH_VERSION` and `$env:NSH_INSTALL_DIR` work as above. **Homebrew** (macOS and Linux): ```sh brew install longbridge/tap/nsh ``` **Scoop** (Windows): ```powershell scoop install https://github.com/longbridge/nsh/releases/latest/download/nsh.json ``` **mise** (all three): ```sh mise use -g github:longbridge/nsh ``` The archives (`nsh-macos-arm64.tar.gz`, `nsh-linux-x86_64.tar.gz`, `nsh-windows-x86_64.zip`, each with a `.sha256`) carry no version in their names, so `https://github.com/longbridge/nsh/releases/latest/download/` always fetches the latest. The embedding SDKs are also released alone, for native apps that need no CLI: `nsh-embed-ios.zip` and `nsh-embed-android.zip`. > [!NOTE] > The binaries are not signed yet. macOS quarantines files downloaded by a browser; the install script, Homebrew and mise download with `curl` and are not affected, otherwise run `xattr -dr com.apple.quarantine `. The Linux runtime needs glibc 2.39 and GTK 4.14 or newer (Ubuntu 24.04 and later). There is no Intel Mac or arm64 Linux / Windows build: build those from a checkout. ## The prebuilt distribution The distribution is a directory, `dist/nsh`, with the CLI and the prebuilt native runtimes it packages apps with: ```sh dist/nsh/bin/nsh # the CLI dist/nsh/runtime/ios/iphoneos/nsh-ios-app # runs packaged apps on iOS devices dist/nsh/runtime/ios/iphonesimulator/{nsh-ios-app,nsh-ios-host} dist/nsh/runtime/ios/maccatalyst/nsh-ios-host # the Mac Catalyst development host dist/nsh/runtime/android/{release,dev}/ # classes.dex, lib//libnsh.so dist/nsh/runtime/macos/{nsh-macos-host,nsh-macos-app} # the macOS (AppKit) development host and release runner dist/nsh/runtime/linux/{nsh-linux-host,nsh-linux-app} # the Linux development host and release runner dist/nsh/runtime/windows/{nsh-windows-host,nsh-windows-app}.exe # the Windows development host and release runner dist/nsh/runtime/gpui/{nsh-gpui-host,nsh-gpui-app}[.exe] # the GPUI development host and release runner, for this OS dist/nsh/runtime/web/{nsh.wasm,nsh-dev.wasm,nsh.js} # the web runtimes (nsh package / run --target web) dist/nsh/embed/ios/{NativeShell,NativeShellDev}.xcframework # embedding in iOS apps dist/nsh/embed/android/{nsh,nsh-dev}.aar # embedding in Android apps ``` Put `dist/nsh/bin` on your `PATH` (or call `dist/nsh/bin/nsh` directly) and keep the directory together: `nsh` finds the runtimes next to itself, in `bin/../runtime` (through a symlink to it, as the installers make, too). ```sh export PATH="$PWD/dist/nsh/bin:$PATH" nsh --version ``` The distribution is made with `cargo xtask dist` in a checkout (below). The `nsh` binary it contains is built for that machine. On a Mac with Xcode it includes the iOS and macOS runtimes (Apple Silicon); on Linux it includes the Linux runtime instead, built for that machine; on Windows the Windows runtime, with `bin\nsh.exe`. Each also includes the [GPUI](https://nsh.dev/docs/gpui.md) runtime, built for that machine. All include the Android runtime (on Windows only when the Android SDK is found). ## Building from a checkout You need Rust through [rustup](https://rustup.rs). The repository's `rust-toolchain.toml` selects the stable toolchain and installs the iOS and Android targets on first use. ```sh git clone https://github.com/longbridge/nsh.git cd nsh cargo build -p nsh-cli # ./target/debug/nsh ``` Then build the development host of each target you want to run on, once, and again after you update the checkout: ```sh cargo build -p nsh-ios-host --target aarch64-apple-ios-macabi # nsh run (Mac Catalyst window) cargo build -p nsh-ios-host --target aarch64-apple-ios-sim # nsh run --target ios cargo xtask android --dev # nsh run --target android cargo build -p nsh-macos-host # nsh run --target macos (built for you in a checkout) cargo build -p nsh-linux-host # nsh run on Linux (built for you in a checkout) cargo build -p nsh-windows-host # nsh run on Windows (built for you in a checkout) cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-host # nsh run --target gpui (built for you in a checkout) cargo build --profile web -p nsh-platform-web --target wasm32-unknown-unknown # nsh run --target web (built for you in a checkout) ``` For `nsh package`, build the release runners as well: ```sh cargo build -p nsh-ios-app --target aarch64-apple-ios-sim # nsh package --target ios-simulator cargo build -p nsh-ios-app --target aarch64-apple-ios # nsh package --target ios cargo xtask android # nsh package --target android cargo build -p nsh-macos-app # nsh package --target macos cargo build -p nsh-linux-app # nsh package --target linux cargo build -p nsh-windows-app # nsh package --target windows cargo build --manifest-path crates/gpui/Cargo.toml --target-dir target -p nsh-gpui-app # nsh package --target gpui # nsh package --target web uses the same web runtime as nsh run --target web ``` A CLI built from a checkout looks for the runtimes in `target//` (the macOS, Linux, Windows and GPUI ones, built for this machine, in `target/`) and `target/android/[-dev]`, using its own profile: `./target/debug/nsh` uses debug runtimes, `./target/release/nsh` release ones. Build both with the same profile (add `--release` to all of them for faster apps). > [!TIP] > `NSH_RUNTIME=/path/to/runtime` makes `nsh` use a runtime directory laid out like the distribution's `runtime/`, wherever the CLI itself lives. ## Platform tools ### iOS and Mac Catalyst - **Xcode** (with its command line tools). `nsh run` uses `xcrun simctl` to boot simulators and install the development host; `nsh package` uses `actool` for app icons and `codesign` for signing. - An iOS simulator runtime. If none is installed, install one with `xcodebuild -downloadPlatform iOS`. - Apps target iOS 15.0 and later. ### Android - **The Android SDK**, found through `ANDROID_HOME`, `ANDROID_SDK_ROOT` or Android Studio's default place (`~/Library/Android/sdk`, `%LOCALAPPDATA%\Android\Sdk` on Windows), with: - `platforms;android-35` and a recent `build-tools` (NSH uses `aapt2`, `zipalign` and `apksigner` from it, no Gradle); - `platform-tools` (`adb`). - **A JDK** (`keytool` creates the debug keystore in `~/.android/debug.keystore` the first time you package, `apksigner` needs Java). - A running emulator or a device connected over `adb`. Apps need Android 9 (API 28) or later. ```sh sdkmanager "platforms;android-35" "build-tools;35.0.1" "platform-tools" ``` ### macOS - **Xcode's command line tools** (`xcode-select --install`): the SDK and clang to build the runtime, `codesign` and `sips` to package apps. - Apps need macOS 14 or later. See [macOS](https://nsh.dev/docs/macos.md). ### Linux - **GTK 4.14** or later, with their development packages and clang to build the runtime: `libgtk-4-dev clang` (Debian, Ubuntu 24.04 or later), `gtk4 clang` (Arch), `gtk4-devel clang` (Fedora). - **WebKitGTK 6.0** (2.40 or later) for web views: `libwebkitgtk-6.0-dev` (Debian, Ubuntu), `webkitgtk-6.0` (Arch), `webkitgtk6.0-devel` (Fedora) to build the runtime; apps then need it where they run. Without it, `cargo xtask dist` builds the runtime with placeholders for web views. - **GStreamer** plugins for the video formats apps play. See [Linux](https://nsh.dev/docs/linux.md). ### Windows - **Windows 10 1809** or later, or Windows 11. The WebView2 Runtime (part of Windows 11, installed with Edge on Windows 10) for web views. - To build the runtime: **Visual Studio Build Tools** with the "Desktop development with C++" workload and the Windows SDK, and **LLVM** (libclang, for QuickJS' bindings; found in `C:\Program Files\LLVM`, else set `LIBCLANG_PATH`). See [Windows](https://nsh.dev/docs/windows.md). ### Building the Android runtime Building the Android runtime from a checkout (`cargo xtask android`) additionally needs an **NDK** from the SDK (`ndk;`; set `ANDROID_NDK_HOME` if the newest one installed is incomplete) and `javac`. ## What works where | Command | macOS | Linux | Windows | |---|---|---|---| | `nsh init`, `nsh build`, `nsh trace`, `nsh dev`, `nsh check` | Yes | Yes | Yes | | `nsh run` (Mac Catalyst, the default on macOS) | Yes | No | No | | `nsh run --target macos`, `nsh package --target macos` | Yes | No | No | | `nsh run --target linux` (the default on Linux), `nsh package --target linux` | No | Yes | No | | `nsh run --target windows` (the default on Windows), `nsh package --target windows` | No | No | Yes | | `nsh run --target gpui`, `nsh package --target gpui` (a package for the OS it is made on) | Yes | Yes | Yes | | `nsh run --target ios`, `nsh package --target ios` / `ios-simulator` | Yes, with Xcode | No | No | | `nsh run --target android`, `nsh package --target android` | Yes | Yes, from a checkout | Yes, with the Android runtime | | `nsh keygen`, `certify`, `publish`, `verify`, `revoke` | Yes | Yes | Yes | On Linux, build the CLI and the Android runtime from a checkout (`cargo build -p nsh-cli` and `cargo xtask android --dev`); the NDK's Linux toolchain is used automatically. On Windows the NDK's Windows toolchain is used the same way, and `cargo xtask dist` includes the Android runtime when it finds the Android SDK. ## Editor setup NSH needs no NPM. `nsh init` writes a `tsconfig.json` and the type declarations of everything NSH provides into `.nsh/types`, and every command that builds the app keeps them up to date. Any editor that uses the TypeScript language server (VS Code, Zed, Neovim, WebStorm) type-checks the app with them. See [Project structure](https://nsh.dev/docs/project-structure.md#tsconfigjson-and-nshtypes). ## Next steps Continue with [Getting started](https://nsh.dev/docs/getting-started.md) to create an app and run it. Source: https://nsh.dev/docs/installation --- # Getting started NSH's main use is hosting modules inside a native app (see [Introduction](https://nsh.dev/docs.md)). This guide follows that path: write a module, develop it with hot reload on the platforms you target, show it in your app, and ship an update as a signed package. It assumes `nsh` is installed and on your `PATH` ([Installation](https://nsh.dev/docs/installation.md)); in a checkout, use `./target/debug/nsh` instead. > [!TIP] > No app project at hand? The first two sections need none: `nsh run` shows the module in NSH's own development host: in a window on your computer, in the iOS simulator or on an Android device. You can stop there, or [package it as a standalone app](#or-ship-it-as-a-standalone-app). ## Create a module ```sh nsh init my-app ``` A module is what a host app loads: one or more `.tsx` files with an entry. `nsh init` creates the directory if it is missing and writes: | File | What it is | |---|---| | `App.tsx` | The entry module: renders the app with `mount` | | `nsh.json` | The app's name, id, version and build number ([Project structure](https://nsh.dev/docs/project-structure.md#nshjson)) | | `tsconfig.json` | TypeScript settings that resolve `nsh` to the declarations in `.nsh/types` | | `.gitignore` | Ignores `.nsh/` and `out/` | | `.nsh/types/` | Generated declarations of every NSH module, for your editor | The display name defaults to the directory's name; pass `--name "My App"` to choose another. The id becomes `com.example.`: change it in `nsh.json` before you ship. Files that already exist are kept, so you can run `nsh init` in a directory that has some of them. ### The entry module ```tsx import { mount, signal, Text, View } from "nsh"; import { SafeAreaView } from "nsh/ui/mobile"; function App() { const count = signal(0); return ( {count()} count.update((c) => c + 1)} activeOpacity={0.6} accessibilityRole="button" style={{ paddingHorizontal: 20, paddingVertical: 12, borderRadius: 10, backgroundColor: "#0969da" }} > Tap ); } mount(App); ``` Line by line: - **`import { mount, signal, Text, View } from "nsh"`**: `nsh` is the core module: [signals](https://nsh.dev/docs/reactivity.md), `mount`, the other reactive primitives, and the host elements `View` and `Text`. It is served by the native runtime, not bundled. - **`import { SafeAreaView } from "nsh/ui/mobile"`**: the mobile UI set. [`SafeAreaView`](https://nsh.dev/components/safe-area-view.md) pads its content so it stays clear of the notch, the status bar and the home indicator; on desktops, where the safe area is 0, it adds no padding. - **`function App()`** is a component: a function that returns JSX. It runs **once**. `signal(0)` makes a value that changes over time. - **`{count()}`** reads the signal inside JSX. The compiler sees a call and makes this text follow the signal: when `count` changes, the runtime updates this one text, nothing else. - **``** is a native view that reacts to presses; `activeOpacity` fades it while pressed, natively. `View`, `Text`, `Image`, `Scroll`, `Input` and the other [host elements](https://nsh.dev/elements.md) are imported from NSH's modules, like any component; only `If` and `Each` need no import. - **`style={{ … }}`** takes an object of camelCase style properties, with numbers in points. Layout is flexbox, and children stack in a column by default ([Layout](https://nsh.dev/docs/layout.md)). - **`mount(App)`** renders the component into the app's surface. > [!IMPORTANT] > Text must be inside ``. A bare string in a `` (`Hello`) is not displayed. ## Develop it ### Run it `nsh run` starts a dev server, builds the app, launches a development host that loads it, and keeps rebuilding while you edit. Logs from the app (`console.log`, errors) appear in the terminal. Without `--target`, `nsh run` uses the machine you are on: a Win32 window on Windows, a GTK window on Linux, and on macOS a Mac Catalyst window that shows the app as an iPhone would. Pick another platform with `--target`: | Target | Command | Runs from | |---|---|---| | iOS simulator | `nsh run my-app/App.tsx --target ios` | macOS with Xcode | | Android | `nsh run my-app/App.tsx --target android` | macOS, Windows, Linux | | macOS (AppKit) | `nsh run my-app/App.tsx --target macos` | macOS | | Windows | `nsh run my-app/App.tsx` | Windows (the default) | | Linux (GTK 4) | `nsh run my-app/App.tsx` | Linux (the default) | | Mac Catalyst | `nsh run my-app/App.tsx` | macOS (the default) | #### iOS simulator ```sh nsh run my-app/App.tsx --target ios nsh run my-app/App.tsx --target ios --device "iPhone 16 Pro" ``` `nsh` uses a booted simulator, or boots the newest iPhone if none is running, and installs the development host into it. `--device` picks a simulator by name or UDID. #### Android ```sh nsh run my-app/App.tsx --target android ``` Start an emulator or connect a device first (`adb devices` lists them; NSH uses the first one). `nsh` installs a development host APK (rebuilt only when the runtime changes) and forwards the dev server's port to the device with `adb reverse`, so it works over USB without a shared network. #### macOS (AppKit) ```sh nsh run my-app/App.tsx --target macos ``` The app opens as a desktop Mac app (AppKit, in a resizable window), with mouse, trackpad and keyboard input instead of touches. It needs macOS 14 or later; see [macOS](https://nsh.dev/docs/macos.md). #### Windows (default on Windows) ```sh nsh run my-app/App.tsx ``` The app opens in a desktop window (1200×800 points), with mouse, touchpad, touch and keyboard input. It needs Windows 10 1809 or later; see [Windows](https://nsh.dev/docs/windows.md). #### Linux (default on Linux) ```sh nsh run my-app/App.tsx ``` The app opens in a GTK window (390×844 points). It needs GTK 4.14 or later; see [Linux](https://nsh.dev/docs/linux.md). #### Mac Catalyst (default on macOS) ```sh nsh run my-app/App.tsx ``` The app opens in an iPhone-sized window (390×844 points) on your Mac. This is the quickest way to see a module as a phone shows it: no simulator to boot. It uses UIKit like iOS, so what you see is what an iPhone shows, apart from Mac-specific behavior such as scrolling with a mouse wheel. For the Mac app itself, use `--target macos`. ### Edit with hot reload Leave `nsh run` running and change the label in `App.tsx`: ```tsx Add one ``` Save. The CLI rebuilds and sends a patch; the host runs the changed module again and renders again only the components whose code changed. Here that is `App`, which owns the `count` signal, so the count starts over. State survives when you edit a component that does not own it. Move the button into a component of its own: ```tsx function TapButton(props: { onPress: () => void }) { return ( Tap ); } ``` Use ` count.update((c) => c + 1)} />` in `App`, tap a few times, then change `TapButton`'s color: the button updates and the count stays. Hot replacement works per top-level component: a function with a capitalized name whose body contains JSX. A component renders again when its own code changes, or when the module's code outside components changes (a constant it uses, a helper). If an edit cannot be applied in place (for example, it imports an NSH module the running app did not import yet, or changes `nsh.json`'s capabilities), the host reloads the whole app. #### Errors A build error is printed in the terminal and shown on the device as a red overlay with the file, line and column; the app keeps running the last good build. The overlay disappears with the next successful build (tap it to dismiss it earlier). An exception thrown at run time is shown the same way, with its stack mapped back to your `.tsx` source. ## Host it in your app Your app shows a module in a view of its own: `NshAppView` (a `UIView`) on iOS, `NshView` (an Android `View`) on Android, `AppView` on macOS, Windows and Linux. Place it like any other view, on any screen, as many times as you need. During development, point it at the dev server; in a release, load a built bundle or a downloaded package. ### iOS Link `NativeShellDev.xcframework` in Debug and `NativeShell.xcframework` in Release ([Embedding in iOS apps](https://nsh.dev/docs/embedding-ios.md)), then: ```swift import NativeShell let app = NshAppView() app.onError = { message in print("nsh error: \(message)") } view.addSubview(app) // and constrain it like any view #if DEBUG try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) // `nsh dev my-app` on your Mac #else try app.loadBundle(at: Bundle.main.url(forResource: "bundle", withExtension: "qbc", subdirectory: "nsh")!) #endif ``` `nsh dev my-app/App.tsx` serves the module to the app and pushes every save, as `nsh run` does with its own host. For the release build, `nsh build my-app/App.tsx --release --out MyApp/nsh` writes the bundle into a folder you add to the app's resources. ### Android Add `nsh-dev.aar` for debug builds and `nsh.aar` for release builds ([Embedding in Android apps](https://nsh.dev/docs/embedding-android.md)), then: ```kotlin val nsh = NshView(this) nsh.setErrorListener { message -> Log.e("nsh", message) } setContentView(nsh) // or add it to any layout if (BuildConfig.DEBUG) { nsh.connectDevServer("ws://127.0.0.1:8790/ws") // with `adb reverse tcp:8790 tcp:8790` } else { nsh.loadAssetBundle("nsh") // assets/nsh, from `nsh build my-app/App.tsx --release --out app/src/main/assets/nsh` } ``` ### macOS, Windows and Linux Desktop apps written in Rust embed modules with `AppView`, from the `nsh-platform-macos`, `nsh-platform-windows`, `nsh-platform-linux` and `nsh-platform-gpui` crates: an `NSView`, a child window, a GTK widget or a GPUI view that loads a built bundle. See [macOS](https://nsh.dev/docs/macos.md#embedding-in-a-mac-app), [Windows](https://nsh.dev/docs/windows.md#embedding-in-a-win32-app), [Linux](https://nsh.dev/docs/linux.md#embedding-in-a-gtk-app) and [GPUI](https://nsh.dev/docs/gpui.md), and [Platforms](https://nsh.dev/docs/platforms.md#embed-in-a-native-app) for every host side by side. ### What the module may do On iOS and Android, the host decides before loading what the module may do: `grantCapabilities` sets the network hosts and other capabilities it may reach, and limits cap its memory, call time and node count ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). To give modules your own native views (a map, a chart, a design-system component), register them as [native components](https://nsh.dev/docs/native-components.md). ## Ship updates as signed packages A module the app downloads after release comes as a signed package. Create a root key once and keep it offline, certify a release key with it, and publish: ```sh nsh keygen keys/root && nsh keygen keys/release nsh certify --root keys/root.secret --key keys/release.pub --apps 'com.example.*' -o keys/release.cert nsh publish my-app/App.tsx --key keys/release.secret --cert keys/release.cert --build 2 ``` The app trusts the root's public key and loads what it downloads; NSH verifies the signature, the certificate and the revocation list before running anything: ```swift let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh")) try trust.addRoot(rootPublicKey) // the contents of keys/root.pub let info = try await app.loadPackage(at: downloadedFile, trust: trust) ``` Android has the same flow with `NshTrust` and `NshPackage`. Hosting, delivery and revocation in detail: [Signed packages](https://nsh.dev/docs/signed-packages.md). ## Or ship it as a standalone app The same module can also be a whole app. `nsh package` builds an installable app around it from the prebuilt runtime, with no Xcode project or Gradle build of your own: ```sh nsh package my-app --target ios-simulator --run nsh package my-app --target android nsh package my-app --target macos nsh package my-app --target linux nsh package my-app --target windows ``` This is a supported use on every platform, and the quickest way to put a module on a device or a desktop, but NSH is designed around hosting modules in native apps. Options, signing and icons: [Packaging](https://nsh.dev/docs/packaging.md). ## Try it in the terminal `nsh trace` runs an app headless (no device) and prints the native view operations it produces and their timings, then taps the first pressable view twice: ```sh nsh trace my-app/App.tsx ``` It is a quick way to check that an app starts without errors, and to see how much work an interaction causes ([Testing and trace](https://nsh.dev/docs/testing-and-trace.md)). ## Next steps - [Platforms](https://nsh.dev/docs/platforms.md): what each platform supports, and its own page ([iOS](https://nsh.dev/docs/embedding-ios.md), [Android](https://nsh.dev/docs/embedding-android.md), [macOS](https://nsh.dev/docs/macos.md), [Windows](https://nsh.dev/docs/windows.md), [Linux](https://nsh.dev/docs/linux.md)). - [Signed packages](https://nsh.dev/docs/signed-packages.md) and [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md): delivery and what modules may do. - [JSX and components](https://nsh.dev/docs/jsx-and-components.md) and [Reactivity](https://nsh.dev/docs/reactivity.md): writing modules. - [Project structure](https://nsh.dev/docs/project-structure.md): what goes in `nsh.json`, assets, files. Source: https://nsh.dev/docs/getting-started --- # Project structure An NSH app is a directory with an entry module and an optional `nsh.json`. Everything else (more modules, images, fonts, JSON files) is found by following the entry's imports, so there is no file list to maintain. ```text my-app/ ├── App.tsx the entry module: calls mount() ├── nsh.json name, id, version, icon, Tailwind theme, capabilities ├── tsconfig.json TypeScript settings for editors ├── .gitignore .nsh/ and out/ ├── components/ your own modules (any layout you like) │ └── Header.tsx ├── assets/ │ ├── logo@2x.png imported images │ └── Inter.ttf imported fonts ├── .nsh/ generated: type declarations, development host builds └── out/ generated: nsh build / nsh package output ``` Every command takes the entry as its argument (`nsh run my-app/App.tsx`). The directory that contains the entry is the app's root: `nsh.json` is read from there, asset paths are relative to it, and `.nsh/` is written there. ## The entry module The entry is a normal module that ends by rendering the root component: ```tsx import { mount } from "nsh"; import { Home } from "./components/Home"; function App() { return ; } mount(App); ``` `mount(component, props?)` renders the component into the app's surface and returns a function that removes it again. An app calls it once. Code at the top level of the entry runs when the bundle loads, before the first frame. ## nsh.json `nsh.json` sits next to the entry. Every key is optional; `nsh init` writes the first four. ```json { "name": "Gallery", "id": "com.example.gallery", "version": "1.0.0", "build": 1, "icon": "icon.png", "tailwind": { "colors": { "brand": { "500": "#7c3aed", "600": "#6d28d9" } }, "radius": { "card": 14 } }, "capabilities": { "network": { "hosts": ["api.example.com", "*.cdn.example.com"] } } } ``` | Key | Type | Used by | Description | |---|---|---|---| | `name` | string | `package` | The display name under the app icon. Default: the entry directory's name. | | `id` | string | `package`, `publish` | Bundle identifier / application id, e.g. `com.example.app`. Default: `com.example.`. | | `version` | string | `package`, `publish` | The user-visible version, e.g. `1.2.0` (`CFBundleShortVersionString`, Android `versionName`). | | `build` | number | `package`, `publish` | The build number (`CFBundleVersion`, Android `versionCode`). Signed packages require it to grow with every release. | | `icon` | string | `package` | A square PNG, ideally 1024×1024, relative to `nsh.json`. | | `tailwind` | object | every build | Extends the Tailwind theme your `class` strings resolve against ([Tailwind](https://nsh.dev/docs/tailwind.md#theme)). | | `appearance` | string | every build, `package` | `"light"`, `"dark"` or `"system"` (the default): an app that is always light or dark says so here. `colorScheme()`, `dark:` classes, the components' theme and the platform's controls follow it from the start ([Device and environment](https://nsh.dev/docs/device-and-environment.md#dark-mode)). | | `theme` | string or object | every build | The theme of the `nsh/ui/mobile` components: a theme file's path relative to `nsh.json`, or the theme inline ([Components](https://nsh.dev/components.md)). | | `capabilities` | object | every build | What the app may reach outside its VM: network hosts, web view hosts, the status bar ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). | The flags of `nsh package` and `nsh publish` (`--name`, `--id`, `--app-version`, `--build`, `--icon`) override the file's values ([CLI](https://nsh.dev/docs/cli.md)). `tailwind`, `theme`, `appearance` and `capabilities` are checked when the app is built. A typo is a build error rather than a setting that silently does nothing: ```text error building App.tsx failed nsh.json: tailwind: unknown theme key `colours` ``` > [!WARNING] > `nsh package` and `nsh publish` read `nsh.json` strictly and currently reject keys they do not know, including `theme`. If you set a component theme, package from a copy of the app without it until this is fixed. ### capabilities Without a `capabilities` key, an app may use the network, web views and the status bar freely: a standalone app is its developer's own. With the key, everything not listed is denied, both in development and in packaged apps: ```json "capabilities": { "network": { "hosts": ["api.example.com", "*.cdn.example.com"], "http": [{ "host": "quotes.example.com", "methods": ["GET"], "pathPrefix": "/v1/" }] }, "webview": { "hosts": ["*.example.com"] }, "statusBar": true } ``` `network` covers `fetch`, remote images and video sources; local assets are always allowed. An app that embeds NSH may grant less than an app declares. The details are in [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). ## tsconfig.json and .nsh/types NSH does not need `node_modules`. Instead, every command that builds the app (`init`, `build`, `trace`, `dev`, `run`, `package`, `publish`) writes the TypeScript declarations of everything NSH provides into `.nsh/types`, and `tsconfig.json` points the `nsh` imports there: ```json { "compilerOptions": { "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", "moduleResolution": "Bundler", "jsx": "preserve", "jsxImportSource": "nsh", "strict": true, "noEmit": true, "isolatedModules": true, "skipLibCheck": true, "types": [], "paths": { "nsh": ["./.nsh/types/nsh/index.d.ts"], "nsh/*": ["./.nsh/types/nsh/*.d.ts"] } }, "include": ["**/*.ts", "**/*.tsx", ".nsh/types/nsh/globals.d.ts", ".nsh/types/nsh/assets.d.ts"] } ``` | File in `.nsh/types/nsh/` | Declares | |---|---| | `index.d.ts` | `nsh`: `signal`, `computed`, `effect`, `mount`, `View`, `Text`, ... and re-exports the types below | | `types.d.ts` | Styles (`ViewStyle`, `TextStyle`, ...), every element's props and events, `YNode` (what a `ref` receives) | | `jsx-runtime.d.ts` | The JSX runtime TypeScript resolves through `jsxImportSource` (the `JSX` namespace) | | `globals.d.ts` | `If` and `Each`; `console`, timers, `requestAnimationFrame`, `fetch` and the other [global APIs](https://nsh.dev/modules/globals.md) | | `assets.d.ts` | What importing an image or font gives | | `list.d.ts`, `animated.d.ts`, `device.d.ts`, ... | One file per module (`nsh/list`, `nsh/animated`, `nsh/device`, ...) | | `ui/mobile.d.ts` | `nsh/ui/mobile` | `lib` is `ES2023` without `DOM`: there is no `document` or `window`. The files are regenerated (only when their contents change), so do not edit them and do not commit them; `.gitignore` already excludes `.nsh/`. If your editor shows `nsh` as missing after cloning a project, run any build command once, e.g. `nsh trace App.tsx`. `jsxImportSource: "nsh"` and `jsx: "preserve"` only matter to the editor: NSH compiles JSX with its own compiler, whatever the `tsconfig.json` says. TypeScript is checked by your editor or by `nsh check`; the other commands strip types without checking them. ## Modules Split the app into as many `.ts` and `.tsx` modules as you like and import them with relative paths; the extension may be left out: ```tsx import { Header } from "./components/Header"; import { formatPrice } from "./lib/format"; import strings from "./strings.json"; ``` All of them are bundled into one script. JSON files import as their parsed value. ### Loading code on demand `import()` of one of the app's modules returns a promise of its exports, as in a browser: ```tsx const open = async () => { const { Settings } = await import("./screens/Settings"); screen.set(() => Settings); }; ``` Apps on devices ship one bundle, so the module is already there and the promise resolves at once. A web app published split (`nsh build --split`) gets the module, and what only it uses, as a separate file that is downloaded the first time `import()` asks for it ([Web](https://nsh.dev/docs/web.md#split-bundles)). The specifier must be a literal relative path: the bundler has to see which module is meant. ### NSH's modules Imports of `nsh` and `nsh/...` are not bundled: the runtime provides them natively. | Module | What it has | |---|---| | `nsh` | Reactivity (`signal`, `computed`, `effect`, ...), `mount`, `split`, `context`; `View`, `Text`, `Scroll`, `Input` ([reference](https://nsh.dev/modules/nsh.md)) | | `nsh/ui/mobile` | The mobile UI set: components, navigation, alerts, safe area ([Components](https://nsh.dev/components.md)) | | `nsh/list` | `List`: virtualized, recycling lists | | `nsh/animated` | Native animations | | `nsh/canvas` | 2D drawing | | `nsh/gesture` | `PanResponder` | | `nsh/device` | `Platform`, `windowSize`, `colorScheme`, `fontScale`, `PixelRatio`, `I18nManager` | | `nsh/accessibility` | Screen reader and reduce motion state, announcements | | `nsh/image` | `Image` (the element), `Image.getSize`, `Image.prefetch` | | `nsh/webview`, `nsh/video` | `WebView`, `Video` | | `nsh/native` | Components the embedding app registers | | `nsh/ios`, `nsh/android` | Platform-only APIs | The [modules reference](https://nsh.dev/modules.md) documents each one. Importing a module NSH does not have (`nsh/foo`) is a build error. ### npm packages NSH needs nothing from npm, but the bundler resolves bare imports from a `node_modules` directory if you install packages with a package manager. Packages resolve their browser builds, because the runtime provides web APIs (`fetch`, streams, `URL`, `TextEncoder`, ...) and not Node's (`fs`, `Buffer`, `process`). Packages that touch the DOM do not work: there is none. `solid-js` cannot be imported: the bundler refuses it and points to `nsh`'s own `signal`, `computed` and `effect`, which replace it. ## Assets Images and fonts are imported like modules. The bundler records each imported file and ships it with the app: `nsh build` copies them to `out/assets/`, `nsh package` puts them in the app, and during development the dev server serves them. ```tsx import { Text } from "nsh"; import { Image } from "nsh/image"; import logo from "./assets/logo@2x.png"; // { uri: "asset:/assets/logo@2x.png", width: 64, height: 64 } import Inter from "./assets/Inter.ttf"; // "Inter" Hello ``` | Extension | Import gives | |---|---| | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.heic` | An `ImageAsset`: `{ uri, width, height }`, sized in points. An `@2x` / `@3x` suffix divides the pixel size by 2 / 3. | | `.ttf`, `.otf` | The font's family name, a string. The font is registered before the first frame. | | `.json` | The parsed JSON | See [Images and assets](https://nsh.dev/docs/images-and-assets.md) and [Fonts](https://nsh.dev/docs/fonts.md). ## Generated directories - **`.nsh/`**: the type declarations (`.nsh/types`) and what `nsh run` installs on simulators and devices (the Catalyst host app, the Android development APK). Safe to delete; it is recreated. - **`out/`**: the default output of `nsh build` (`bundle.js`, `bundle.js.map`, `bundle.nshc`, `capabilities.json`, `assets/`) and `nsh package` (`.app`, `.ipa`, `.apk`). Change it with `-o`. Source: https://nsh.dev/docs/project-structure --- # CLI `nsh` is the only tool an NSH app needs. Every subcommand that builds an app takes `[ENTRY]`: the entry module, or the app's directory, whose `App.tsx` is the entry (default: the current directory, so `cd my-app && nsh run` works). It reads `nsh.json` next to the entry and refreshes the type declarations in `.nsh/types`. ```text nsh init Create an app build Bundle an app and compile it to QuickJS bytecode trace Run an app headless and print the mount instructions and timings check Type check an app with TypeScript's native compiler dev Serve an app to dev hosts (simulators, devices) and push rebuilds run Like dev, and also run the app in a development host package Package an installable app from the prebuilt runtime keygen Create a signing key certify Certify a release key with a root key publish Build an app in release mode and sign it into a package (.nshpkg) verify Verify a package revoke Sign a revocation list ``` `nsh --help` prints the flags of a command; `nsh --version` prints the version. ## Output Every command prints one line per step, and the details only when something fails: ```text ✓ built the Rust crate gallery-native (5.0s) ✓ built App.tsx (500 B js, 809 B bytecode, 21ms) → dev server ws://127.0.0.1:8790/ws devices on this network: ws://192.168.1.20:8790/ws ✓ built the development host for the iOS simulator (12.7s) • iOS Simulator (iPhone 16 Plus) connected, build 1 warn iOS Simulator: image failed: An SSL error has occurred and a secure connection to the server cannot be made. ✓ rebuilt App.tsx (18ms), 1 device ``` `✓` is a finished step, `→` where to connect, `•` what a device did; `warn` and `error` lines are problems, and plain lines are your app's own output (`console.log`). On a terminal a spinner shows the step that is running, and lines are colored (unless `NO_COLOR` is set); redirected to a file, only the result lines are written, without colors. The output of the tools NSH runs (cargo for an app's Rust crate, the development hosts) is printed below an `error` line when one of them fails. `-v, --verbose` (any command; also `NSH_VERBOSE=1`) prints everything as it comes: every log line with its level and source, cargo's output and the hosts' own. ## nsh init ```sh nsh init [DIR] [--name ] ``` Creates an app in `DIR` (default: the current directory, created if missing): `App.tsx`, `nsh.json`, `tsconfig.json`, `.gitignore` and `.nsh/types`. Files that exist are kept; it fails only if all of them exist already. | Flag | Description | |---|---| | `--name ` | Display name written to `nsh.json`. Default: the directory's name. The id becomes `com.example.` (letters and digits only, lower case). | ## nsh run ```sh nsh run [ENTRY] [--target catalyst|ios|android|macos|linux|windows|gpui|web] [--device ] [--port ] [--release] [--trace] [--fps] ``` Starts the dev server (as `nsh dev`), launches a development host that loads the app from it, and rebuilds on every save. Hot module replacement patches the running app in place; logs and errors from the app are printed in the terminal. Stop it with Ctrl-C (or by closing the Catalyst, macOS, Linux, Windows or GPUI window). However it ends (Ctrl-C, `kill` / `pkill`, an error), it stops the host it started: the Catalyst app, the macOS, Linux, Windows or GPUI host, the development host in the simulator, or on the Android device. | Flag | Default | Description | |---|---|---| | `--target ` | `catalyst` on macOS, `linux` on Linux, `windows` on Windows | `catalyst`: the Mac Catalyst host, an iPhone-sized window (390×844). `ios`: the host in an iOS simulator. `android`: the host on an Android device or emulator. `macos`: the AppKit host, a 1200×800 window ([macOS](https://nsh.dev/docs/macos.md)). `linux`: the GTK host, a 390×844 window ([Linux](https://nsh.dev/docs/linux.md)). `windows`: the Win32 host, a 1200×800 window ([Windows](https://nsh.dev/docs/windows.md)). `gpui`: the GPUI host, a 1200×800 window, on macOS, Windows or Linux ([GPUI](https://nsh.dev/docs/gpui.md)). `web`: the dev server's page, opened in the browser ([Web](https://nsh.dev/docs/web.md#develop-in-the-browser)). | | `--device ` | | iOS simulator to use, by name (`"iPhone 16 Pro"`) or UDID. Default: a booted one, else the newest iPhone, which is booted. | | `--port ` | `8790` | Dev server port. If it is taken, the next free one is used. | | `--release` | | Minify and strip debug info. Release bundles have no hot module replacement: every save reloads the app. | | `--trace` | | The host logs the timing of every commit (JS, layout, mount items) to the terminal. | | `--fps` | | Shows the performance monitor over the app: frame rate, what frames and commits cost, CPU, GPU and memory ([macOS](https://nsh.dev/docs/macos.md#performance-monitor); `--target macos` only for now). | With `--target android`, the first device `adb devices` lists is used; the dev server's port is forwarded to it with `adb reverse`. See [Getting started](https://nsh.dev/docs/getting-started.md#run-it) and [Dev server and debugging](https://nsh.dev/docs/dev-server-and-debugging.md). ## nsh dev ```sh nsh dev [ENTRY] [--port ] [--release] ``` Runs only the dev server: it builds the app, watches the entry's directory, and pushes every rebuild to the hosts connected to it. It prints the URL for hosts on this machine and one per LAN address for devices, and, when the web runtime is there, the page browsers open to run the app ([Web](https://nsh.dev/docs/web.md#develop-in-the-browser)): ```text ✓ built App.tsx (3.2 KB js, 9.8 KB bytecode, 21ms) → dev server ws://127.0.0.1:8790/ws browsers: http://127.0.0.1:8790/ devices on this network: ws://192.168.1.20:8790/ws browsers on this network: http://192.168.1.20:8790/ ``` Use it with hosts you start yourself, for example an app that embeds the development SDK (`NativeShellDev.xcframework`, `nsh-dev.aar`). Flags as for `nsh run`. ## nsh trace ```sh nsh trace [ENTRY] [--tap ]... [--taps ] [--idle-ms ] [--wait ] [--duration ] [--scroll ] [--width ] [--height ] [--safe-area ] ``` Runs the app headless, in the CLI's own process with the same core and runtime as a device but no views, and prints every mount instruction (create, insert, style, frame, text) and the time of each commit. Then it taps views (the ones `--tap` names, or the first pressable one) and prints what each tap changed. Use it to check that an app starts without errors, to count the work an interaction causes, and in CI ([Testing and trace](https://nsh.dev/docs/testing-and-trace.md)). ```text ✓ bundled App.tsx (3255 B in 12.6 ms) perf commit: 56 ops, 412µs (list js 6µs), 49 mount items, 21 nodes ── initial render: 49 items Create { id: 2, kind: View } ... ── tap #1 on view 20: 3 items Text { id: 14, text: "1", content: 71x77 @ 0,0 } ``` | Flag | Default | Description | |---|---|---| | `--tap ` | | Tap a view; repeat it for several taps, in order. `id:`, `label:`, `text:`, `view:` (a view id from the output), or a bare string, matched as a `testID`, then a label, then a text. The first match on screen is tapped, or its nearest pressable ancestor (the button around a text). No match is an error. | | `--taps ` | `2` | Without `--tap`: taps sent to the first pressable view. A tap that changes nothing is an error (exit code 1). | | `--idle-ms ` | `1200` | Before tapping, wait until the app renders nothing for this long (at most 10 s, for apps with timers or animations). The default lets the engine's idle garbage collection run first, so retention bugs show up. | | `--wait ` | | Run timers and animations exactly this long before the first tap and after each one, printing what they render, instead of waiting until the app is idle. | | `--duration ` | `0` | Keep running this long after the last tap. | | `--safe-area ` | `0` | Safe area insets in points, `top,right,bottom,left` (or one value for all four), e.g. `47,0,34,0` for a phone with a notch. | | `--scroll ` | | Scrolls the first scroll view or list by this many points, 10 points per frame, and prints each frame's batch. | | `--width ` | `390` | Viewport width in points. | | `--height ` | `844` | Viewport height in points. | `nsh trace` behaves as iOS: `Platform.OS` is `"ios"`, and `ios:` classes and `ios={{…}}` props apply. ## nsh check ```sh nsh check [ENTRY] [--tsc ] [-- ...] ``` Type checks the app: refreshes `.nsh/types`, then runs `tsc -p --noEmit` with the app's `tsconfig.json`. It uses TypeScript 7, the native compiler, which needs no Node: the first run downloads it from the npm registry (about 9 MB), checks it against the checksum NSH records, and keeps it in NSH's cache (`~/Library/Caches/nsh` on macOS, `~/.cache/nsh` on Linux, `%LOCALAPPDATA%\nsh` on Windows, or `NSH_CACHE`). Type errors make it exit with a non-zero code. | Flag | Description | |---|---| | `--tsc ` | Run this `tsc` instead (also `NSH_TSC`), for example one from `node_modules` or a copy for machines without network. | | `-- ` | Passed on to `tsc`, e.g. `nsh check -- --pretty false`. | ## nsh build ```sh nsh build [ENTRY] [-o ] [--release] [--split] ``` Bundles the app and compiles it to QuickJS bytecode, for apps that embed NSH and ship the bundle themselves. | Flag | Default | Description | |---|---|---| | `-o, --out ` | `out` | Output directory. | | `--release` | | Minify and strip debug info. | | `--split` | | Put what `import()` loads into chunk files of its own, downloaded when first imported (web apps, see [Split bundles](https://nsh.dev/docs/web.md#split-bundles)). Without it, the bundle holds the whole app. | It writes: | File | Content | |---|---| | `bundle.js` | The bundle as JavaScript | | `bundle.js.map` | Its source map (stack traces are mapped back to your sources) | | `bundle.nshc` | The bundle as compiled bytecode: what apps load | | `.js`, `.js.map`, `.nshc` | With `--split`: each chunk, its source map and its bytecode (`Screen.js` for `import("./Screen")`) | | `capabilities.json` | The `capabilities` of `nsh.json`, if declared (removed otherwise) | | `assets/` | Every imported image and font, at its path relative to the app root | ## nsh package ```sh nsh package [ENTRY] --target ios|ios-simulator|android|macos|linux|windows|gpui|web [-o ] [flags] ``` Builds an installable app from the prebuilt runtime, in release mode. No Rust and no Xcode project or Gradle build are involved: NSH assembles the app bundle itself and uses Xcode's `actool` and `codesign` (iOS) or the SDK's `aapt2`, `zipalign` and `apksigner` (Android); for macOS it assembles an ad hoc signed `.app` with `codesign` (and `sips` for the icon); for Linux it copies the runner and writes a `.tar.gz` with `tar`; for Windows it copies the runner, embeds the icon and writes a `.zip`, with no tools; for GPUI it does what it does for this machine's OS, with the GPUI runner; for the web it writes a static site, with no tools. Metadata comes from `nsh.json`, overridden by the flags. | Flag | Description | |---|---| | `--target ` | Required. `ios`: iOS devices, a signed `.ipa` (or an unsigned `.app` without `--sign`). `ios-simulator`: an ad hoc signed `.app`. `android`: a signed `.apk`. `macos`: an ad hoc signed `.app` ([macOS](https://nsh.dev/docs/macos.md#packaging)). `linux`: a directory `/` (the executable, `nsh/`, a `.desktop` file) and `.tar.gz` ([Linux](https://nsh.dev/docs/linux.md#packaging)). `windows`: a directory `\` (`.exe`, `nsh\`) and `.zip` ([Windows](https://nsh.dev/docs/windows.md#packaging)). `gpui`: what `macos`, `windows` or `linux` makes on this machine, with the GPUI runner ([GPUI](https://nsh.dev/docs/gpui.md#packaging)). `web`: a directory `/`, a static site (`index.html` and files named by their content) ([Web](https://nsh.dev/docs/web.md#publish-an-app)). | | `-o, --out ` | Output directory (default `out`). | | `--name ` | Display name. | | `--id ` | Bundle identifier, e.g. `com.example.app`. | | `--app-version ` | Version, e.g. `1.2.0`. | | `--build ` | Build number. | | `--icon ` | App icon: a square PNG, ideally 1024×1024. | | `--sign ` | iOS devices: code signing identity, e.g. `"Apple Development: …"`. | | `--profile ` | iOS devices: provisioning profile (`.mobileprovision`). Entitlements come from it. | | `--keystore ` | Android: release keystore. Default: the debug key in `~/.android/debug.keystore` (created if missing). | | `--key-alias ` | Android: key alias in the keystore. | | `--keystore-password ` | Android: keystore password. Also read from `NSH_KEYSTORE_PASSWORD`. | | `--run` | Install and launch the app afterwards: in the simulator (`ios-simulator`) or on the connected Android device; `macos`, `linux`, `windows` and `gpui` launch it; `web` serves the site on this machine and opens it in the browser. | ```sh nsh package App.tsx --target ios-simulator --run nsh package App.tsx --target ios --sign "Apple Development: Jane Doe (ABCDE12345)" --profile dev.mobileprovision nsh package App.tsx --target android --keystore release.jks --key-alias upload nsh package App.tsx --target macos nsh package App.tsx --target linux nsh package App.tsx --target windows nsh package App.tsx --target gpui nsh package App.tsx --target web ``` See [Packaging](https://nsh.dev/docs/packaging.md). ## Signing commands These commands make and check signed packages (`.nshpkg`): app bundles that apps embedding NSH download and verify before running them. [Signed packages](https://nsh.dev/docs/signed-packages.md) explains the model (an offline root key certifies release keys for some app ids). ### nsh keygen ```sh nsh keygen ``` Creates an Ed25519 key pair: `.secret` and `.pub` (e.g. `nsh keygen keys/root`). Keep a root's `.secret` offline; apps trust its `.pub`. ### nsh certify ```sh nsh certify --root --key --apps ... [--days ] -o ``` Certifies a release key with a root key for some app ids and a time window; packages carry the certificate. | Flag | Description | |---|---| | `--root ` | The root's secret key. | | `--key ` | The release key's public key. | | `--apps ` | App ids it may sign for: `*`, `com.example.*` or an id. Repeatable. | | `--days ` | How long it may sign, from now (default 365). | | `-o, --out ` | The certificate to write. | ### nsh publish ```sh nsh publish [ENTRY] --key --cert [-o ] [--id ] [--app-version ] [--build ] ``` Builds the app in release mode and signs it into a package. Metadata comes from `nsh.json`, overridden by the flags. | Flag | Description | |---|---| | `--key ` | The release key's secret key. | | `--cert ` | Its certificate (`nsh certify`). | | `-o, --out ` | The package to write. Default: `out/-.nshpkg`. | | `--id ` | App id. | | `--app-version ` | Version. | | `--build ` | Build number: must grow with every release. | ### nsh verify ```sh nsh verify [--root ]... [--revocations ] ``` Verifies a package as an app trusting the given roots would. Without `--root`, only the package's own signature is checked. It prints the package's SHA-256, which `nsh revoke --package` takes. ### nsh revoke ```sh nsh revoke --root [--extend ] [--sequence ] [--key ]... [--package ]... -o ``` Signs a revocation list: release keys and packages that must not load any more. | Flag | Description | |---|---| | `--root ` | The root's secret key. | | `--extend ` | Keep the entries of this list (the last one published). | | `--sequence ` | The list's number, higher than any published before. Default with `--extend`: that list's plus one. | | `--key ` | A release key id to revoke. Repeatable. | | `--package ` | A package SHA-256 to revoke. Repeatable. | | `-o, --out ` | The list to write. | ## Environment variables | Variable | Used by | Description | |---|---|---| | `ANDROID_HOME`, `ANDROID_SDK_ROOT` | Android targets | The Android SDK. Fallback: `~/Library/Android/sdk`. | | `NSH_KEYSTORE_PASSWORD` | `package --target android` | The keystore password. | | `NSH_RUNTIME` | `run`, `package` | A runtime directory to use instead of the one next to the CLI. | | `NSH_VERBOSE` | every command | `1`: as `--verbose`. | | `NO_COLOR` | every command | Set: no colors on terminals. | Source: https://nsh.dev/docs/cli --- # Platform overview NSH runs on phones, desktops and the web: iOS, Android, macOS, Windows, Linux, GPUI and web pages. Each has a backend of its own that draws with the platform's own UI, so a module is UIKit views on an iPhone, AppKit views on a Mac, GTK 4 widgets on Linux, GPUI elements in a GPUI app and HTML in a browser, with the platform's text, input, accessibility and dark mode. Everything above the backend is shared: QuickJS, the reactive graph, the renderer, Taffy layout and the components are the same Rust code everywhere, so one bundle runs on all of them. On every platform, NSH's main use is the same: modules embedded in a native app of your own, which loads them at run time. You develop them with hot reload (`nsh run`), and the same modules can also be packaged as an installable app (`nsh package`), a supported second use. ## At a glance | Platform | Backend; text | Runs on | Develop | Package | |---|---|---|---|---| | [iOS](https://nsh.dev/docs/embedding-ios.md) | UIKit; CoreText | iOS 15.0+, devices and the simulator | `nsh run --target ios` | `.app` (simulator), signed `.ipa` | | [Android](https://nsh.dev/docs/embedding-android.md) | Android views; `StaticLayout` | Android 9 (API 28)+, `arm64-v8a` and `x86_64` | `nsh run --target android` | signed `.apk` | | [macOS](https://nsh.dev/docs/macos.md) | AppKit and Core Animation; CoreText | macOS 14+ | `nsh run --target macos` | ad hoc signed `.app` | | [Windows](https://nsh.dev/docs/windows.md) | Win32 windows drawn with Direct2D; DirectWrite | Windows 10 1809+, Windows 11 | `nsh run` (the default on Windows) | a directory and a `.zip` | | [Linux](https://nsh.dev/docs/linux.md) | GTK 4 widgets; Pango | GTK 4.14+ | `nsh run` (the default on Linux) | a directory and a `.tar.gz` | | [GPUI](https://nsh.dev/docs/gpui.md) | GPUI; GPUI's text system | macOS, Windows, Linux (Vulkan) | `nsh run --target gpui` | as the desktop it runs on | | [Web](https://nsh.dev/docs/web.md) | HTML and CSS; the browser's fonts | modern browsers, desktop and phone | `nsh run --target web` | a static site | - **[GPUI](https://nsh.dev/docs/gpui.md)** draws modules with [GPUI](https://gpui-kit.com), the framework of the sibling project GPUI Kit, on macOS, Windows and Linux alike: one renderer for all three desktops, and an `AppView` that GPUI and GPUI Kit apps place in their own element tree. - **[Web](https://nsh.dev/docs/web.md)** renders apps as HTML in a browser page: `nsh package --target web` publishes a static site for any web server or CDN, and a page of your own embeds an app with `createPlayer` or an iframe. It also runs the live demos and the [playground](/playground) on this site. - **Mac Catalyst** runs the iOS backend in an iPhone-sized window (390×844) on a Mac, for development. It is `nsh run`'s default target on macOS: the quickest way to see how a module looks on a phone. ## Develop and package `nsh run` picks the machine's own platform when you give no `--target`: Mac Catalyst on macOS, the GTK window on Linux and the Win32 window on Windows. Phones are reached from a computer: iOS from a Mac with Xcode, Android from macOS, Linux or Windows with the Android SDK ([Installation](https://nsh.dev/docs/installation.md#what-works-where)). ```sh nsh run my-app --target macos # or ios, android, windows, linux, gpui, web, catalyst nsh package my-app --target macos # or ios, ios-simulator, android, windows, linux, gpui, web ``` Desktop backends build for the machine they run on: package a Mac app on a Mac, a Windows app on Windows and a Linux app on Linux. [Packaging](https://nsh.dev/docs/packaging.md) covers every target's output, signing and icons. ## Embed in a native app | Platform | Host view | Ships as | Host language | |---|---|---|---| | iOS | `NshAppView` (a `UIView`) | `NativeShell.xcframework` | Swift | | Android | `NshView` (a `FrameLayout`) | `nsh.aar` | Java or Kotlin | | macOS | `AppView` (an `NSView`) | the `nsh-platform-macos` crate | Rust | | Windows | `AppView` (a child window) | the `nsh-platform-windows` crate | Rust | | Linux | `AppView` (a GTK widget) | the `nsh-platform-linux` crate | Rust | | GPUI (macOS, Windows, Linux) | `AppView` (a GPUI view) | the `nsh-platform-gpui` crate | Rust | | Web | `createPlayer` (a page element) or an iframe | the runtime files from `nsh package --target web` | JavaScript or HTML | Each view is one engine with its own VM, and an app can show several. The host registers its own native views for modules to use on every platform ([Native components](https://nsh.dev/docs/native-components.md)). > [!NOTE] > Desktop hosts embed NSH with Rust crates from the NSH repository; their `AppView` loads bundles from the app's own files (or source code), with the default limits. The iOS and Android SDKs also load [signed packages](https://nsh.dev/docs/signed-packages.md) with `NshTrust` and set limits and capabilities per view ([Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)). ## Writing for every platform Most of an app needs no platform checks: elements, styles, layout and modules behave the same everywhere. Where platforms differ, NSH gives the difference one place ([Platform-specific code](https://nsh.dev/docs/platform-specific.md)): - **Platform-only props** go in `ios={{…}}`, `android={{…}}`, `web={{…}}` or `desktop={{…}}` (macOS, Windows, Linux and desktop browsers). - **`Platform.select`** takes a value per OS, or for `mobile` and `desktop` as a whole; `Platform.OS` is `"ios"`, `"android"`, `"macos"`, `"windows"`, `"linux"` or `"web"`. - **Input follows the platform.** Phones get touches and the software keyboard; desktops get the mouse, the trackpad and the keyboard. The components of [`nsh/ui/mobile`](https://nsh.dev/components.md) are drawn by NSH, so they also run on desktops, looking as they do on phones. A desktop layout is built from the primitives (`View`, `Text`, `Scroll`, `List`, …) and `Platform.select({ desktop: … })`, as the gallery's desktop layout is; a desktop UI set is on the way. Each backend lists what it does not support yet: [macOS](https://nsh.dev/docs/macos.md#what-differs-on-macos), [Windows](https://nsh.dev/docs/windows.md#what-differs-on-windows), [Linux](https://nsh.dev/docs/linux.md#what-differs-on-linux), [GPUI](https://nsh.dev/docs/gpui.md#differences). ## See also - [Getting started](https://nsh.dev/docs/getting-started.md): a first module, run on each platform. - [Installation](https://nsh.dev/docs/installation.md): the tools each platform needs. - [Architecture](https://nsh.dev/docs/architecture.md): what is shared and what each backend does. Source: https://nsh.dev/docs/platforms --- # iOS NSH apps run on iOS with UIKit views and CoreText text, on devices and in the simulator, from iOS 15.0. The main use is embedding: an existing app shows modules in an `NshAppView` and loads signed packages it downloads, under limits and capabilities it sets. Running and packaging whole apps work too, as on the other platforms. On iOS, `Platform.OS` is `"ios"` and the `ios={{…}}` props apply. ## Requirements - **Xcode** on a Mac, with an iOS simulator runtime (`xcodebuild -downloadPlatform iOS` installs one). See [Installation](https://nsh.dev/docs/installation.md#ios-and-mac-catalyst). - Apps need **iOS 15.0** or later, on iPhone and iPad. ## Running an app ```sh nsh run my-app --target ios nsh run my-app --target ios --device "iPhone 16 Pro" ``` `nsh` uses a booted simulator, or boots the newest iPhone, installs the development host into it and keeps the app up to date as you save, with logs in the terminal. `--device` picks a simulator by name or UDID. For a quicker loop without a simulator, plain `nsh run` on a Mac opens the same UIKit backend in an iPhone-sized [Mac Catalyst](https://nsh.dev/docs/getting-started.md#mac-catalyst-default-on-macos) window. ## Packaging ```sh nsh package my-app --target ios-simulator --run # out/.app, installed and launched in the simulator nsh package my-app --target ios --sign "Apple Distribution: Example Inc (ABCDE12345)" --profile AppStore.mobileprovision ``` `--target ios` writes a signed `.ipa` for devices, TestFlight or the App Store. Signing identities, provisioning profiles and icons: [Packaging](https://nsh.dev/docs/packaging.md#ios-devices). ## Embedding in an iOS app An existing iOS app can show NSH apps inside its own UI. The runtime ships as `NativeShell.xcframework`, a static framework with a Swift module: you create an `NshAppView` (a `UIView`), add it anywhere in your view hierarchy and load an app into it. Your native screens, navigation and the NSH app live side by side in one UIKit hierarchy. Each `NshAppView` is one independent engine with its own JS VM; there are no global singletons, so an app can show several NSH apps at once, for example one per tab. ![One host app with two NshAppView regions: each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map.](https://nsh.dev/diagrams/host-app-light.svg) ## Get the framework The distribution contains both variants: ```text dist/nsh/embed/ios/NativeShell.xcframework release: bundles and signed packages dist/nsh/embed/ios/NativeShellDev.xcframework also connects to a `nsh dev` server ``` From a checkout of NSH you can build just the frameworks: ```sh cargo xtask ios-embed # writes dist/nsh/embed/ios/{NativeShell,NativeShellDev}.xcframework ``` Each XCFramework has three arm64 slices: iOS devices, the iOS simulator (Apple silicon) and Mac Catalyst. The minimum deployment target is iOS 15.0. The Swift module is compiled with library evolution, so apps built with other Swift compiler versions can import it, and the framework links everything it needs: you add nothing else. ## Add it to an Xcode project 1. Drag `NativeShell.xcframework` (or `NativeShellDev.xcframework` for development builds) into the app target's **Frameworks, Libraries, and Embedded Content**. It is a static framework, so choose **Do Not Embed**. 2. In Swift, `import NativeShell`. > [!TIP] > Link `NativeShellDev.xcframework` in the Debug configuration and `NativeShell.xcframework` in Release, so only > development builds can connect to a dev server. ## Ship a bundle with the app Build the NSH app into a folder and add that folder to the app's resources as a folder reference named `nsh`: ```sh nsh build App.tsx --release --out MyApp/nsh ``` `nsh build` writes: | File | Contents | |---|---| | `bundle.nshc` | The app compiled to bytecode: the fastest to load. | | `bundle.js` | The same app as JS source. | | `bundle.js.map` | Its source map (stack traces point at your `.tsx` files). | | `capabilities.json` | The [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md) the app declares, when `nsh.json` has any. | | `assets/` | Images and fonts the app imports, at their paths relative to the entry. | ## Show the app ```swift import NativeShell import UIKit final class EmbedController: UIViewController { private let app = NshAppView() override func viewDidLoad() { super.viewDidLoad() app.translatesAutoresizingMaskIntoConstraints = false app.onError = { message in print("nsh error: \(message)") } view.addSubview(app) NSLayoutConstraint.activate([ app.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), app.leadingAnchor.constraint(equalTo: view.leadingAnchor), app.trailingAnchor.constraint(equalTo: view.trailingAnchor), app.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) guard let url = Bundle.main.url(forResource: "bundle", withExtension: "qbc", subdirectory: "nsh") else { return } do { try app.loadBundle(at: url) } catch { print("cannot load: \(error)") } } // Let the embedded app's StatusBar decide the status bar. override var preferredStatusBarStyle: UIStatusBarStyle { NshAppView.preferredStatusBarStyle } override var prefersStatusBarHidden: Bool { NshAppView.prefersStatusBarHidden } override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { NshAppView.preferredStatusBarUpdateAnimation } } ``` The view's size is the app's window: NSH lays the app out at the view's bounds and again whenever they change. Loading again replaces the running app; the app stops when the view is deallocated. The complete sample, a UIKit app that shows an NSH app below its own native header, is `examples/embed-ios/main.swift`. ## `NshAppView` `NshAppView` is a `UIView` subclass isolated to the main actor. | Member | Description | |---|---| | `init(frame: CGRect = .zero, trace: Bool = false)` | Creates the view. `trace` logs the time of every commit. `init?(coder:)` supports storyboards and nibs. | | `onError: ((String) -> Void)?` | Receives fatal app errors (a bundle that fails to load, uncaught JS exceptions, a VM stopped by a limit) on the main thread. Errors are always logged; development builds also show them over the app. | | `limits: NshLimits` | The VM limits of the apps loaded from now on. | | `grantCapabilities(_ json: String?) throws` | The most the apps loaded from now on may do, as JSON in the form of `nsh.json`'s `capabilities`; `nil` grants what each app declares. Throws `NshError` for invalid JSON. | | `loadBundle(at url: URL) throws` | Runs a bundle written by `nsh build`. Assets come from `assets/` next to it; `bundle.js.map` and `capabilities.json` next to it are read too. | | `run(_ package: NshPackage)` | Runs a verified signed package (see below). A package runs once. | | `loadPackage(at:trust:) async throws -> NshPackage.Info` | Verifies the package at a URL off the main thread, then runs it. Throws why it was refused; the running app stays. | | `connectDevServer(_ url: URL) throws` | Development: loads from a `nsh dev` server and reloads on every change. Throws with `NativeShell.xcframework`, which has no development support. | | `register(component:_:)` | Registers a [native component](https://nsh.dev/docs/native-components.md). | | `static preferredStatusBarStyle`, `prefersStatusBarHidden`, `preferredStatusBarUpdateAnimation` | What the running apps asked for with `StatusBar`: return them from the hosting view controller's overrides. | > [!IMPORTANT] > `bundle.nshc` (bytecode) loads only from inside the app bundle. QuickJS does not validate bytecode, so bytecode from > anywhere else could corrupt memory: for a file outside the app bundle, load `bundle.js` (JS source), or better, a > [signed package](https://nsh.dev/docs/signed-packages.md). `limits` and `grantCapabilities` apply from the next load: set them before `loadBundle` / `run`. ## Limits ```swift app.limits = NshLimits(memoryMB: 128, callBudget: 1, maxNodes: 50_000) ``` | `NshLimits` field | Default | Meaning | |---|---|---| | `memoryMB: UInt32?` | 256 | JS heap in MB. Running out stops the app. | | `stackKB: UInt32?` | 4096 (also the maximum) | Interpreter stack in KB. Deeper recursion throws a `RangeError`. | | `loadBudget: TimeInterval?` | 10 s | Time to evaluate the bundle. | | `callBudget: TimeInterval?` | 2 s | Time of any other call into JS (one event, one timer, one frame). An overrun call is cut off. | | `maxNodes: UInt32?` | 200 000 | Nodes the app may keep. | `nil` keeps the default. What each limit does when it is hit: [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). ## Capabilities An app declares what it may reach in its `nsh.json`; the embedding app can grant less. A request must be allowed by both: ```swift try app.grantCapabilities(""" { "network": { "hosts": ["api.example.com", "*.cdn.example.com"] }, "webview": { "hosts": ["help.example.com"] } } """) try app.loadBundle(at: url) ``` Here the app may not set the status bar (no `statusBar`), even if its `nsh.json` asks for it. Details: [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). ## Signed packages To run code the app downloads (updates, third-party mini apps), use [signed packages](https://nsh.dev/docs/signed-packages.md): `nsh publish` writes a `.nshpkg` signed by a release key that your root key certified, and the app verifies it before running it. ```swift let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh")) try trust.addRoot(rootPublicKey) // the contents of root.pub from `nsh keygen` Task { do { let info = try await app.loadPackage(at: downloadedFile, trust: trust) print("running \(info.id) \(info.version) (build \(info.build))") } catch { print("refused: \(error)") // the running app stays } } ``` To verify first and run later, split the two steps: ```swift let package = try await trust.openPackage(at: downloadedFile) // verifies off the main thread // ... decide, ask the user, ... app.run(package) ``` | API | Description | |---|---| | `NshTrust(stateDirectory: URL) throws` | Opens or creates the trust store. Its state (newest revocation list, highest build accepted per app) persists there, and packages' assets are extracted under it. Trusts no key until `addRoot`. Thread-safe. | | `addRoot(_ publicKey: String) throws` | Trusts a root public key (base64, the contents of a `.pub` file). Several roots may be trusted. | | `updateRevocations(_ json: String) throws` | Applies a revocation list written by `nsh revoke`, signed by a trusted root and newer than the one the store has. | | `openPackage(at: URL) async throws -> NshPackage` | Verifies, extracts assets and accepts the package (older builds of that app are refused from then on). | | `NshPackage.info` | `id`, `name`, `version`, `build` of the app inside. | | `NshError` | The error every throwing call throws; `message` says why. | ## Development With `NativeShellDev.xcframework`, point the view at a dev server instead of a bundle: ```swift #if DEBUG try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!) #else try app.loadBundle(at: bundledURL) #endif ``` Run `nsh dev App.tsx` on your Mac. The simulator shares the Mac's network, so `127.0.0.1` reaches the server; a device uses the LAN address `nsh dev` prints (`devices on this network: ws://192.168.1.20:8790/ws`). Every save updates the app in place, logs appear in the `nsh dev` terminal, and errors show over the app. See [Dev server and debugging](https://nsh.dev/docs/dev-server-and-debugging.md). > [!NOTE] > On a device, the connection to your Mac is ordinary local network traffic over plain `ws://` and `http://` (assets). > iOS may ask the user for local network access, and the app's App Transport Security settings must allow it. ## Notes - **Threads.** `NshAppView` is main-actor isolated. `NshTrust`, `NshPackage` and `NshComponentContext` are thread-safe. The JS engine runs on its own thread; the main thread never waits for it. - **Status bar.** The embedded app's `StatusBar` changes what `NshAppView.preferredStatusBarStyle` and friends return; the hosting view controller must forward them, as in the example above. - **Safe area.** Apps use `SafeAreaView` or `useSafeAreaInsets()` from [`nsh/ui/mobile`](https://nsh.dev/components/safe-area-view.md) as usual. An `NshAppView` placed below your own header gets the insets that apply to it, not the window's. - **Mac Catalyst.** The same framework works in Catalyst apps. ## See also - [Embedding in Android apps](https://nsh.dev/docs/embedding-android.md) - [Native components](https://nsh.dev/docs/native-components.md) - [Signed packages](https://nsh.dev/docs/signed-packages.md) - [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md) Source: https://nsh.dev/docs/embedding-ios --- # Android NSH apps run on Android with ordinary Android views and `StaticLayout` text, from Android 9 (API 28), on `arm64-v8a` and `x86_64`. An existing app embeds modules in an `NshView` and loads signed packages it downloads, under limits and capabilities it sets: that is the main use. Running and packaging whole apps work too, as on the other platforms. On Android, `Platform.OS` is `"android"` and the `android={{…}}` props apply. ## Requirements - **The Android SDK** (with `platforms;android-35`, build tools and `platform-tools`) and **a JDK**, on macOS, Windows or Linux. See [Installation](https://nsh.dev/docs/installation.md#android). - An emulator, or a device connected over `adb`. ## Running an app ```sh nsh run my-app --target android ``` Start an emulator or connect a device first (`adb devices` lists them; NSH uses the first one). `nsh` installs a development host APK (rebuilt only when the runtime changes) and forwards the dev server's port with `adb reverse`, so it works over USB without a shared network; the app updates as you save, with logs in the terminal. ## Packaging ```sh nsh package my-app --target android --run # out/.apk, installed and launched ``` The APK is built with the SDK's build tools, without Gradle, and signed with the debug key unless you pass a release keystore. Signing for release and icons: [Packaging](https://nsh.dev/docs/packaging.md#android). ## Embedding in an Android app An existing Android app can show NSH apps inside its own UI. The runtime ships as an Android library, `nsh.aar`: you create a `com.longbridge.nsh.NshView` (a `FrameLayout`), add it to any layout and load an app into it. The NSH app renders with ordinary Android views next to your own. Each `NshView` is one independent engine with its own JS VM; there are no global singletons, so several can run at once. ![One host app with two nsh views (NshAppView on iOS, NshView on Android): each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map.](https://nsh.dev/diagrams/host-app-light.svg) ## Get the library The distribution contains both variants: ```text dist/nsh/embed/android/nsh.aar release: bundles and signed packages dist/nsh/embed/android/nsh-dev.aar also connects to a `nsh dev` server ``` The AAR holds the Java runtime and `libnsh.so` for `arm64-v8a` and `x86_64` (64-bit only). It declares the `INTERNET` permission and needs **API 28** (Android 9) or newer. Add it to the app module, for example from a `libs` folder: ```kotlin // app/build.gradle.kts dependencies { debugImplementation(files("libs/nsh-dev.aar")) releaseImplementation(files("libs/nsh.aar")) } ``` ## Ship a bundle with the app Build the NSH app into the app's assets: ```sh nsh build App.tsx --release --out app/src/main/assets/nsh ``` This writes `bundle.nshc` (bytecode), `bundle.js` (source), `bundle.js.map`, `capabilities.json` (when `nsh.json` declares [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md)) and `assets/` (the images and fonts the app imports). ## Show the app ```java import android.app.Activity; import android.os.Bundle; import android.util.Log; import com.longbridge.nsh.NshView; public final class MainActivity extends Activity { private NshView nsh; @Override protected void onCreate(Bundle state) { super.onCreate(state); nsh = new NshView(this); nsh.setErrorListener(message -> Log.e("nsh", message)); setContentView(nsh); nsh.loadAssetBundle("nsh"); // runs assets/nsh/bundle.nshc } @Override protected void onDestroy() { nsh.destroy(); super.onDestroy(); } } ``` In Kotlin: ```kotlin class MainActivity : Activity() { private lateinit var nsh: NshView override fun onCreate(state: Bundle?) { super.onCreate(state) nsh = NshView(this) nsh.setErrorListener { message -> Log.e("nsh", message) } setContentView(nsh) nsh.loadAssetBundle("nsh") } override fun onDestroy() { nsh.destroy() super.onDestroy() } } ``` `NshView` can also sit inside any layout, below your own toolbar for example: ```java LinearLayout column = new LinearLayout(this); column.setOrientation(LinearLayout.VERTICAL); column.addView(toolbar); column.addView(nsh, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1)); setContentView(column); ``` The view's size is the app's window: NSH lays the app out at the view's size and again whenever it changes. Loading again replaces the running app. The complete sample is `examples/embed-android/src/com/longbridge/nsh/embeddemo/MainActivity.java`. ## `NshView` | Method | Description | |---|---| | `NshView(Context context)`, `NshView(Context context, boolean trace)` | Creates an empty view. `trace` logs the time of every commit. | | `setErrorListener(ErrorListener listener)` | Receives fatal app errors (load failures, uncaught JS exceptions, a VM stopped by a limit) on the UI thread. They are always logged. | | `setLimits(int memoryMb, int stackKb, int loadMs, int callMs, int maxNodes)` | VM limits of the apps loaded next; 0 keeps a default. | | `setGrantedCapabilities(String json)` | The most the apps loaded next may do, as JSON in the form of `nsh.json`'s `capabilities`; `null` grants what each app declares. Returns `false` (and is ignored) for an invalid grant. | | `loadAssetBundle(String dir)` | Runs `dir/bundle.nshc` from the APK's assets; app assets come from `dir/assets/`. Returns `false` if it cannot be read (also reported to the error listener). | | `loadBundleFile(File file)` | Runs a `bundle.js` file; assets come from `assets/` next to it. Refuses `.nshc` files. | | `runPackage(NshPackage pkg)` | Runs a verified signed package, replacing the running app. A package runs once. | | `registerComponent(String name, NshComponent.Factory factory)` | Registers a [native component](https://nsh.dev/docs/native-components.md). | | `connectDevServer(String url)` | Development: loads from a `nsh dev` server and reloads on every change. Returns `false` with the release AAR. | | `handleBack()` | Lets the app handle a back press (closes modals, pops screens, runs `BackHandler` listeners); returns whether it did. | | `destroy()` | Stops the app. The view can no longer be used. | > [!IMPORTANT] > Compiled bytecode (`.nshc`) only loads from the APK's own assets or from a signed package. Files on storage can be replaced, > and QuickJS does not validate bytecode, so `loadBundleFile` takes JS source only. Code the app downloads should come > as a [signed package](https://nsh.dev/docs/signed-packages.md). `setLimits` and `setGrantedCapabilities` apply from the next load: call them before `loadAssetBundle` and friends. ## Limits ```java // 128 MB heap, default stack, 10 s to load, 1 s per call, 50 000 nodes nsh.setLimits(128, 0, 10_000, 1_000, 50_000); ``` The defaults are a 256 MB heap, a 4096 KB stack, 10 s to load, 2 s per call and 200 000 nodes. What each limit does when it is hit: [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). ## Capabilities ```java nsh.setGrantedCapabilities( "{\"network\": {\"hosts\": [\"api.example.com\"]}, \"webview\": {\"hosts\": [\"help.example.com\"]}}"); nsh.loadAssetBundle("nsh"); ``` A request must be allowed both by what the app declares in its `nsh.json` and by the grant. See [Sandbox and capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). ## Signed packages Verify downloaded packages off the UI thread, then run them on it: ```java NshTrust trust = new NshTrust(new File(getFilesDir(), "nsh")); trust.addRoot(rootPublicKey); // the contents of root.pub from `nsh keygen` executor.execute(() -> { try { NshPackage pkg = trust.openPackage(downloadedFile); runOnUiThread(() -> nsh.runPackage(pkg)); } catch (NshPackage.RefusedException e) { Log.w("nsh", "refused: " + e.getMessage()); // the running app stays } }); ``` | API | Description | |---|---| | `new NshTrust(File stateDir)` | Opens or creates the trust store. Its state (newest revocation list, highest build accepted per app) persists there and packages' assets are extracted under it. Trusts no key until `addRoot`. Usable from any thread; `close()` when done. | | `boolean addRoot(String publicKey)` | Trusts a root public key (base64, the contents of a `.pub` file). `false` for an invalid key. | | `boolean updateRevocations(String json)` | Applies a revocation list from `nsh revoke`, signed by a trusted root and newer than the store's. | | `NshPackage openPackage(File file) throws NshPackage.RefusedException` | Verifies, extracts the assets and accepts the package (older builds of the app are refused from then on). Slow for large packages: call it off the UI thread. | | `NshPackage` | `appId`, `appName`, `version`, `build`; `close()` if it will not run. | How keys, certificates and revocation work: [Signed packages](https://nsh.dev/docs/signed-packages.md). ## Development With `nsh-dev.aar`: ```java if (BuildConfig.DEBUG) { nsh.connectDevServer("ws://127.0.0.1:8790/ws"); } else { nsh.loadAssetBundle("nsh"); } ``` Run `nsh dev App.tsx` on your computer. For an emulator or a USB device, forward the port so the device's own `127.0.0.1` reaches the server: ```sh adb reverse tcp:8790 tcp:8790 ``` Otherwise use the LAN address `nsh dev` prints. Assets come from the dev server over plain HTTP, so `nsh-dev.aar` declares `android:usesCleartextTraffic="true"`, which the manifest merger applies to the app (a network security config of the app may override it). The release `nsh.aar` does not. ## Activity setup - **Back button.** On Android 13 and newer, `NshView` registers its own back callback while the app wants back presses (an open modal, a pushed screen, a `BackHandler` listener). On Android 12 and older, forward the press: ```java @Override public void onBackPressed() { if (!nsh.handleBack()) super.onBackPressed(); } ``` For predictive back on Android 13+, set `android:enableOnBackInvokedCallback="true"` on the ``, as packaged NSH apps do. - **Keyboard.** `NshView` scrolls focused inputs clear of the keyboard itself. Leave `android:windowSoftInputMode` unset or use `adjustResize`; with `adjustPan` the system moves the whole window instead. - **Safe area.** The view reports as insets only the parts of the system bars and the keyboard that overlap it, so an `NshView` below a toolbar gets no top inset. For edge-to-edge content, draw behind the system bars and let the app use `SafeAreaView` / `useSafeAreaInsets()`. - **Configuration changes.** An activity recreated on rotation or a theme change destroys the view, and the app starts again from scratch. To keep it running, declare the changes the activity handles itself; packaged NSH apps use: ```xml ``` `NshView` follows the new size, color scheme, font scale and layout direction itself. - **Lifecycle.** Call `destroy()` when the hosting activity or fragment goes away. ## See also - [Embedding in iOS apps](https://nsh.dev/docs/embedding-ios.md) - [Native components](https://nsh.dev/docs/native-components.md) - [Signed packages](https://nsh.dev/docs/signed-packages.md) - [Packaging](https://nsh.dev/docs/packaging.md): a standalone APK without writing any Java Source: https://nsh.dev/docs/embedding-android --- # macOS NSH apps also run as native Mac apps. The macOS backend renders them with AppKit views and Core Animation and lays out text with CoreText; the core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same as on iOS and Android, so one bundle runs everywhere. On macOS, `Platform.OS` is `"macos"` and the `desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from [`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md). The macOS backend behaves like a desktop app: mouse, trackpad and keyboard input, resizable windows, centered sheets and a menu bar. ## Requirements - **macOS 14** or later, where the app runs. - Xcode's command line tools (`xcode-select --install`), to build NSH's macOS runtime from a checkout and to package apps (`codesign`, `sips`). The distribution made with `cargo xtask dist` on a Mac ships the runtime in `runtime/macos/`. ## Running an app ```sh nsh run my-app --target macos ``` The app opens in an AppKit window (1200×800 by default), with hot reload, logs in the terminal and the error overlay as on the other targets; Cmd+R reloads the latest build. The menu bar and the window show the app's name (nsh.json `"name"`, else its directory's). In a checkout, `nsh run` builds the development host (`nsh-macos-host`) first. Stopping `nsh run` closes the window, and closing the window ends `nsh run`. The host also runs a built bundle on its own: ```sh nsh build my-app --release -o build dist/nsh/runtime/macos/nsh-macos-host --bundle build/bundle.nshc --size 1200x800 # in a checkout: target/debug/nsh-macos-host ``` `--size` is the window's initial size; the window can be resized. ### Performance monitor `--fps` (`nsh run my-app --target macos --fps`, the host's `--fps`, a packaged app's `open -a MyApp --args --fps`, or `AppOptions { fps: true, .. }` / `AppView::set_fps_monitor(true)` when embedding) shows a panel at the top right of the app that updates twice a second: | Row | Measures | |---|---| | `FPS` | Frames shown over the last second: display refreshes in which the app changed (an idle app reads low; that is not slowness). `max` is what the main thread could keep up with, then the display's rate (120 Hz on ProMotion screens). | | `TICK` | The gaps between display refreshes as they arrived (mean, p95) and `gap`, the p95 gap between shown frames: one refresh (8.3 ms at 120 Hz) when nothing is late. | | `MAIN` | Main-thread time per frame (applying the engine's changes, drawing text and canvases, animations, input), its 95th percentile, and `drop`: the share of frames that took longer than one refresh. | | `JS` / `CORE` | Per commit: your code (handlers, timers, list rows) and NSH's core (layout, text, diffing); commits per second, native view changes per commit. | | `HOP` | How long changes wait between the engine thread and the main thread; how busy the engine thread is. | | `CPU` / `GPU` / `MEM` | The process: CPU (100% is one core), its GPU share, its memory footprint (Activity Monitor's figure). | The bars below are the last 120 frames' main-thread time against one refresh (the line): green within half of it, amber within it, red over. With `--trace` too, the host also logs the readings once a second (milliseconds as mean/p50/p95/max). Measure release builds; the monitor costs nothing while it is off. ## Packaging ```sh nsh package my-app --target macos [--run] ``` Writes an app bundle: ```text out/.app/Contents/ Info.plist MacOS/ the executable (nsh's release runner) Resources/nsh/bundle.nshc your app, compiled Resources/nsh/assets/… the assets it imports Resources/nsh/app.json the window title and the app's appearance Resources/AppIcon.icns the icon, when nsh.json names one (converted with sips) ``` The `.app` is signed ad hoc, so it runs on the Mac that built it. To distribute it, sign it with a Developer ID and notarize it. `--run` opens the app after packaging it. ## Embedding in a Mac app The `nsh-platform-macos` crate (from the NSH repository) gives a Rust Mac app `AppView`: one NSH engine as an `NSView`. Each `AppView` is independent; several can run at once. ```rust use std::rc::Rc; use nsh_platform_macos::{ objc2::MainThreadMarker, objc2_app_kit::NSWindow, AppOptions, AppView, AssetBase, Bundle, }; fn show(window: &NSWindow) -> Rc { let mtm = MainThreadMarker::new().expect("main thread"); let view = AppView::new(mtm, AppOptions { asset_base: Some(AssetBase::Directory("app/assets".into())), ..AppOptions::default() }); // register native components here, before loading let code = std::fs::read_to_string("app/bundle.js").expect("bundle"); view.load(Bundle::source(code).into()); window.setContentView(Some(&view.view())); // Keep the view while the window shows it; `stop` it when the window closes. view } ``` `AppView::load` replaces whatever ran; `stop` ends the app. `asset_base` tells it where `asset:/` URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with `unsafe { Bundle::bytecode(bytes) }`: bytecode is trusted input, so only load it from your own app's files. For an app that is only an NSH app, `run_app(source, options)` opens one `AppView` in a window with a standard menu (About, Hide, Quit, Edit, Minimize, Zoom, Enter Full Screen, Close), which the app can replace with `Menu.setApplicationMenu` ([`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md)), and runs until the window closes. Native components are registered with `AppView::register_component`; see [Native components](https://nsh.dev/docs/native-components.md#registering-on-macos-rust). ## What differs on macOS The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is `"desktop"`, `Platform.Version` is the macOS version, and dark mode follows the system's appearance live. See [Device and environment](https://nsh.dev/docs/device-and-environment.md). Input is a desktop's: the mouse, the trackpad (scrolling with momentum, pinch and rotation) and the keyboard. `selectable` text is selected by dragging, double- and triple-clicking, and copied with Cmd+C. Escape, Option+Left, Cmd+[ and the mouse's back button go back in a stack or ask a modal to close (Escape in a focused field or web page only leaves it; a page gets these keys first, and keeps those it uses); page and form sheets are cards centered in the window over a dimmed backdrop. A click anywhere in an `Input`, its padding too, puts the caret at the nearest point. `Alert` and `ActionSheet` show an `NSAlert` sheet on the window: up to two alert buttons in a row (the cancel button leftmost, the last regular one the default for Return), more buttons and an action sheet's options stacked in their order with the cancel button last and no default; Escape presses the cancel button. ### Keyboard | Keys | Do | |---|---| | Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) | | Shift+Return, Option+Return, Ctrl+Return | A new line in a multiline input whose return submits | | Tab, Shift+Tab (Ctrl too) | The next / previous field in screen order, within the newest modal; Option+Tab types a tab in a multiline input | | Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close | | Option+Left, Cmd+[, mouse back button | Go back (not while a field has the focus: there Option+Left moves the caret) | | Option / Cmd + arrows, with Shift; Option+Delete, Cmd+Delete; Ctrl+A / E / K | Move, select and delete by word and line in a field, as in every Mac app | | Cmd+Z, Shift+Cmd+Z, Cmd+X / C / V / A | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Cmd+A / Cmd+C select and copy `selectable` text | | Arrows; Page Up / Down, Space, Shift+Space, Option+Up / Down; Home / End, Cmd+Up / Down | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) | | Cmd+W, Cmd+M, Ctrl+Cmd+F, Cmd+H, Option+Cmd+H, Cmd+Q | Close, minimize, full screen, hide, hide others, quit (`nsh package` and `nsh run` apps' menus) | | Cmd+R | Reloads the latest build (`nsh run`) | The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) come before all of these, except Quit, Hide, Minimize and Full Screen, which cannot be bound. Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Cmd+C is the key that types C on a US layout). A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`. Not supported, or different: - **Zooming scroll views** (`zoomTo`, the zoom props). - **`mixBlendMode`**, and `brightness()` above 1. - **Inputs**: content types and autofill, keyboard types, autocapitalization, letter spacing; input accessory views are never shown (there is no software keyboard) and `keyboardShouldPersistTaps` is ignored. - **`WebView`'s `scrollEnabled={false}`** keeps the mouse wheel and the trackpad from scrolling the page: they scroll the app's scroll views around it, as on iOS. The page still scrolls with its keyboard (Space, the arrows) and its own scripts, and trackpad swipes between its pages (`allowsBackForwardNavigationGestures`) stop as well. - **Accessibility**: custom `accessibilityActions`. - **The status bar** and **haptics** (no desktop equivalent). ## See also - [CLI](https://nsh.dev/docs/cli.md): `nsh run --target macos`, `nsh package --target macos` - [Packaging](https://nsh.dev/docs/packaging.md) - [Platform-specific code](https://nsh.dev/docs/platform-specific.md) Source: https://nsh.dev/docs/macos --- # Windows NSH apps also run on Windows. The Windows backend draws them in Win32 windows with Direct2D and lays out text with DirectWrite; the core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same as on iOS and Android, so one bundle runs everywhere. On Windows, `Platform.OS` is `"windows"` and the `desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from [`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md). ## Requirements - **Windows 10 1809** or later, or Windows 11. - The **WebView2 Runtime** for web views. It comes with Windows 11 and with Microsoft Edge on Windows 10; without it a `WebView` shows a placeholder. - **Media Foundation codecs** for the video formats your app plays (videos play through Windows' own media stack). - To build NSH's Windows runtime (from a checkout, or `cargo xtask dist` on Windows): Visual Studio Build Tools with the C++ workload, the Windows SDK and LLVM (for QuickJS' bindings). ## Running an app On Windows, `nsh run` opens the app in a window (1200×800 by default), with hot reload, logs in the terminal and the error overlay as on the other targets: ```sh nsh run my-app # --target windows is the default on Windows ``` In a checkout, `nsh run` builds the development host (`nsh-windows-host.exe`) first; the distribution made on Windows ships it in `runtime\windows\`. Ctrl-C closes the window, and closing the window ends `nsh run`; the host never outlives `nsh`, however it ends. The host also runs a built bundle on its own: ```sh nsh build my-app --release -o build dist\nsh\runtime\windows\nsh-windows-host --bundle build\bundle.nshc --size 390x844 # in a checkout: target\debug\nsh-windows-host ``` `--size` is the window's size in points (`x`). ## Packaging ```sh nsh package my-app --target windows [--run] ``` Writes a directory and the same as an archive: ```text out\\ .exe the executable (nsh's release runner), with the app's icon nsh\bundle.nshc your app, compiled nsh\assets\… the assets it imports nsh\app.json the window title, the app id, the version and the appearance out\.zip ``` The executable finds `nsh\` next to itself; copy the whole directory (or unpack the `.zip`) wherever the app should live: nothing is installed. The icon from `nsh.json` is embedded in the executable (Explorer and the taskbar show it) without any extra tools; packaging on another system keeps the default icon. The app id groups the app's windows in the taskbar. `--run` launches the app after packaging it. The executable is not signed: sign it with your own certificate (`signtool`) to distribute it. ## Embedding in a Win32 app The `nsh-platform-windows` crate (from the NSH repository) gives a Rust Win32 app `AppView`: one NSH engine in a child window of a window of yours. Each `AppView` is independent; several can run at once. ```rust use std::rc::Rc; use nsh_platform_windows::{ windows::{self, Win32::{Foundation::HWND, System::Ole::OleInitialize, UI::WindowsAndMessaging::MoveWindow}}, AppOptions, AppView, AssetBase, Bundle, }; /// Called on the thread that runs your message loop, with your window. fn embed(parent: HWND) -> windows::core::Result> { // Once per thread: COM and OLE (the clipboard, image decoding). unsafe { OleInitialize(None)? }; let view = AppView::new( Some(parent), AppOptions { asset_base: Some(AssetBase::Directory("app/assets".into())), app_id: "com.example.host".into(), ..AppOptions::default() }, )?; // register native components here, before loading let code = std::fs::read_to_string("app/bundle.js").expect("bundle"); view.load(Bundle::source(code).into()); Ok(view) } // On WM_SIZE of `parent`: // unsafe { MoveWindow(view.hwnd(), 0, 0, width, height, true) }; ``` The view is a child window: your message loop runs it, and you place it with `MoveWindow` on its `hwnd()` when the parent resizes. `AppView::load` replaces whatever ran; `stop` (or destroying the window) ends the app. `asset_base` tells it where `asset:/` URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with `unsafe { Bundle::bytecode(bytes) }` (or `Bundle::read_trusted(path)`, unsafe too): QuickJS bytecode is trusted input, so only load it from your own app's files. A standalone window is `nsh_platform_windows::run_app(source, options)`. Native components are registered with `AppView::register_component`; see [Native components](https://nsh.dev/docs/native-components.md#registering-on-windows-rust). ## What differs on Windows The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is `"desktop"` and `Platform.Version` is `"10.0"` (on Windows 11 too: Windows kept the version number). Dark mode follows Settings > Personalization > Colors live, `fontScale()` the Text size setting, reduce motion the animation effects setting and the locale the user's regional format. See [Device and environment](https://nsh.dev/docs/device-and-environment.md). ### Keyboard | Keys | Do | |---|---| | Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) | | Shift+Return, Alt+Return | A new line in a multiline input whose return submits (Ctrl+Return submits) | | Tab, Shift+Tab | The next / previous text field, within the newest modal; the field's text is selected, as in Windows' dialogs | | Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close | | Alt+Left, Browser Back key, mouse back button | Go back (also from a field) | | Ctrl+arrows, Home / End, Ctrl+Home / End, with Shift; Ctrl+Backspace / Delete | Move, select and delete by word and line in a field, as in every Windows app | | Ctrl+Z, Ctrl+Y, Ctrl+X / C / V / A; Shift+Delete, Ctrl+Insert, Shift+Insert | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Ctrl+A / Ctrl+C select and copy `selectable` text | | Arrows; Page Up / Down, Space, Shift+Space; Home / End, Ctrl+Home / End | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) | | F11, Alt+F4 | Full screen, close the window (`nsh package` and `nsh run` apps) | | Ctrl+R, F5 | Reload the latest build (`nsh run`) | Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Ctrl+C is the key that types C on a US layout). A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`; pasted text is not reported. The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) come before a focused field's keys, navigation and scrolling; Alt+F4, F11 and Ctrl+Q cannot be bound. Different, or not supported: - **`WebView`**: a page inside a view with a 3D transform (perspective) is not shown, and a `backdropFilter` over a page does not blur it. `source.baseUrl` takes `http(s)` URLs. A native component that brings a window of its own stays above everything NSH draws. - **Video**: the formats the installed codecs decode; no poster; the control bar always shows with `controls`. - **Zooming scroll views** (`zoomTo`, the zoom props); the scroll indicator cannot be dragged. - **`mixBlendMode` and `backdropFilter`** are drawn, but not inside a view that has a `filter` or a 3D transform; `borderCurve: "continuous"` draws circular corners. - **Text**: head and middle ellipses on a single line only (more lines get a tail ellipsis); color emoji from COLR fonts only; small caps only where the font has them. - **Text inputs**: keyboard types and autofill do not apply (there is no software keyboard); Tab moves between text fields. A right click opens the edit menu (Undo, Cut, Copy, Paste, Delete, Select All), as on selectable text (Copy, Select All). - **Accessibility** (UI Automation): headers' titles and buttons, dialogs and toasts are not exposed; headings report level 1. - **Input accessory views** and the **status bar** (no desktop equivalent). ## See also - [CLI](https://nsh.dev/docs/cli.md): `nsh run --target windows`, `nsh package --target windows` - [Packaging](https://nsh.dev/docs/packaging.md) - [Platform-specific code](https://nsh.dev/docs/platform-specific.md) Source: https://nsh.dev/docs/windows --- # Linux NSH apps also run on Linux. The Linux backend renders them with GTK 4 widgets and lays out text with Pango; the core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same as on iOS and Android, so one bundle runs everywhere. On Linux, `Platform.OS` is `"linux"` and the `desktop={{…}}` props apply. The window's title, a title bar of the app's own, the menu bar and context menus come from [`nsh/desktop`](https://nsh.dev/modules/nsh-desktop.md). ## Requirements - **GTK 4.14** or later. To build NSH's Linux runtime (from a checkout, or `cargo xtask dist` on Linux) you need their development packages and clang: `libgtk-4-dev clang` on Debian and Ubuntu (24.04 or later), `gtk4 clang` on Arch, `gtk4-devel clang` on Fedora. - **WebKitGTK 6.0** (2.40 or later) for web views: `libwebkitgtk-6.0-dev` on Debian and Ubuntu, `webkitgtk-6.0` on Arch, `webkitgtk6.0-devel` on Fedora to build the runtime. The development host and the release runner use it by default, so packaged apps need WebKitGTK 6.0 where they run (`libwebkitgtk-6.0-4` on Debian and Ubuntu; GNOME desktops have it). `cargo xtask dist` on a machine without its development files builds a runtime whose web views are placeholders, and whose apps need only GTK. - **GStreamer** plugins for the video formats your app plays (GTK plays video through GStreamer). ## Running an app On Linux, `nsh run` opens the app in a GTK window (390×844 by default), with hot reload, logs in the terminal and the error overlay as on the other targets: ```sh nsh run my-app # --target linux is the default on Linux ``` In a checkout, `nsh run` builds the development host (`nsh-linux-host`) first; the distribution made on Linux ships it in `runtime/linux/`. Ctrl-C closes the window, and closing the window ends `nsh run`. The host also runs a built bundle on its own: ```sh nsh build my-app --release -o build dist/nsh/runtime/linux/nsh-linux-host --bundle build/bundle.nshc --size 1024x768 # in a checkout: target/debug/nsh-linux-host ``` `--size` is the window's default size; tiling window managers may give the window another one. ## Packaging ```sh nsh package my-app --target linux [--run] ``` Writes a directory and the same as an archive: ```text out// the executable (nsh's release runner) nsh/bundle.nshc your app, compiled nsh/assets/… the assets it imports nsh/app.json the window title and the application id .desktop a desktop entry (StartupWMClass is the id) .png the icon, when nsh.json names one out/.tar.gz ``` The executable finds `nsh/` next to itself (a symbolic link to it, in `~/.local/bin` say, works too). GTK 4, Pango and WebKitGTK 6.0 must be installed where the app runs. To show the app in a desktop's launcher, put the executable (or a link) on the `PATH`, the `.desktop` file in `~/.local/share/applications` and the icon in an icon theme directory. `--run` launches the app after packaging it. ## Embedding in a GTK app The `nsh-platform-linux` crate (from the NSH repository) gives a Rust GTK app `AppView`: one NSH engine as a GTK widget. Each `AppView` is independent; several can run at once. ```rust use std::rc::Rc; use nsh_platform_linux::{gtk4 as gtk, AppOptions, AppView, AssetBase, Bundle}; use gtk::prelude::*; fn main() -> gtk::glib::ExitCode { let app = gtk::Application::builder().application_id("com.example.host").build(); app.connect_activate(|app| { let window = gtk::ApplicationWindow::builder().application(app).default_width(390).default_height(844).build(); let view = AppView::new(AppOptions { asset_base: Some(AssetBase::Directory("app/assets".into())), ..AppOptions::default() }); // register native components here, before loading let code = std::fs::read_to_string("app/bundle.js").expect("bundle"); view.load(Bundle::source(code).into()); window.set_child(Some(&view.widget())); // The window keeps the view; closing it stops the app. let keep = std::cell::RefCell::new(Some(Rc::clone(&view))); window.connect_destroy(move |_| if let Some(view) = keep.take() { view.stop() }); window.present(); }); app.run() } ``` `AppView::load` replaces whatever ran; `stop` ends the app. `asset_base` tells it where `asset:/` URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with `unsafe { Bundle::bytecode(bytes) }`: QuickJS bytecode is trusted input, so only load it from your own app's files. Native components are registered with `AppView::register_component`; see [Native components](https://nsh.dev/docs/native-components.md#registering-on-linux-rust). ## What differs on Linux The environment is a desktop's: the safe area and the keyboard height are 0, `Platform.constants.interfaceIdiom` is `"desktop"`, `Platform.Version` is the kernel's major and minor version, and dark mode follows the desktop's setting (through the XDG settings portal) live. See [Device and environment](https://nsh.dev/docs/device-and-environment.md). ### Keyboard | Keys | Do | |---|---| | Return | In an `Input`, what `submitBehavior` says: submit (`onSubmitEditing`), submit and leave the field, or a new line (multiline inputs) | | Shift+Return, Alt+Return | A new line in a multiline input whose return submits (Ctrl+Return submits) | | Tab, Shift+Tab | The next / previous field in screen order, within the newest modal | | Escape | Leaves the focused field; else clears a text selection, then goes back or asks the newest modal to close | | Alt+Left, Back key, mouse back button | Go back (also from a field) | | Ctrl+arrows, Home / End, with Shift; Ctrl+Backspace / Delete | Move, select and delete by word and line in a field, as in every GTK app | | Ctrl+Z, Ctrl+Shift+Z, Ctrl+X / C / V / A | Undo, redo, cut, copy, paste, select all (a secure field refuses cut and copy); Ctrl+A / Ctrl+C select and copy `selectable` text | | Arrows; Page Up / Down, Space, Shift+Space; Home / End | Scroll the scroll view around the focused view or under the mouse by a line, a page, to the start / end (not while a field has the focus) | | Ctrl+W, F11, Ctrl+Q | Close the window, full screen, quit (`nsh package` and `nsh run` apps) | | Ctrl+R | Reloads the latest build (`nsh run`) | The app's own shortcuts ([`nsh/keymap`](https://nsh.dev/modules/nsh-keymap.md)) are a window-wide shortcut controller; Ctrl+Q and F11 cannot be bound. Shortcuts work with any keyboard layout (on a Cyrillic or Greek one, Ctrl+C is the key that types C on a US layout). A return that commits an input method's composition (Chinese, Japanese, ...) never submits. `onKeyPress` reports what React Native reports: the typed text, `"Enter"`, `"Backspace"`, and `"Escape"`; pasted text is not reported. Web views are WebKitGTK pages, drawn inside NSH's views: sheets, dialogs, toasts and views after them draw over a page, and parents clip it and fade it. Their website data (cookies, storage, the cache) is kept per app, under `~/.local/share//webkit` and `~/.cache//webkit`. The `webview` capability also applies to the pages of frames: an `iframe` of a host your app was not granted stays empty. Escape in a page leaves it, as in a text field (unless the page uses it); Alt+Left and the mouse's back button go back in the page while it has a history. Not supported: - **Video `rate`**: GTK plays at normal speed only. `onEnd` only comes for videos that do not loop. - **Zooming scroll views** (`zoomTo`, the zoom props). - **`mixBlendMode` and `backdropFilter`** (the background color still draws); `borderCurve: "continuous"` draws circular corners. - **Input accessory views** and the **status bar** (no desktop equivalent). - **Accessibility**: `accessible` grouping, custom `accessibilityActions`; live regions are announced instead. ## See also - [CLI](https://nsh.dev/docs/cli.md): `nsh run --target linux`, `nsh package --target linux` - [Packaging](https://nsh.dev/docs/packaging.md) - [Platform-specific code](https://nsh.dev/docs/platform-specific.md) Source: https://nsh.dev/docs/linux --- # GPUI The GPUI backend draws NSH apps with [GPUI](https://gpui-kit.com) (GPUI Fast), the GPU-accelerated UI framework that GPUI Kit is built on, on macOS, Windows and Linux alike. Its main use is embedding: an `AppView` is a GPUI view, so a GPUI or GPUI Kit app shows NSH modules anywhere in its own element tree. The core, the reactive graph, the renderer, Taffy layout and the `nsh/ui/mobile` components are the same as on every other platform, so one bundle runs everywhere. `Platform.OS` is the OS it runs on (`"macos"`, `"windows"` or `"linux"`) and the `desktop={{…}}` props apply. Every element and module of the [macOS](https://nsh.dev/docs/macos.md) (AppKit), [Windows](https://nsh.dev/docs/windows.md) (Win32) and [Linux](https://nsh.dev/docs/linux.md) (GTK) backends is there; where GPUI draws something its own way, [Differences](#differences) says how. ## How it works - **One element per surface.** A surface is one GPUI element that fills the space its parent gives it and paints the whole view tree with GPUI's primitives: quads, shadows, paths, glyphs and images. NSH keeps one retained view per node, as on Windows, and redraws only when a commit changed something. - **Text is shaped on the engine thread** with GPUI's text system (the app's own fonts), broken into lines by NSH; the main thread only paints the glyphs. - **One hop per commit.** The engine runs on its own thread; each commit is one closure sent to GPUI's main thread, which notifies the view. - **Input** comes from GPUI's mouse, scroll, key and input method events; hit testing, presses, the responder system, scrolling and text editing are the same logic as the Windows backend's. ## Requirements - macOS, Windows or Linux, with a GPU GPUI can draw with (Metal on macOS, DirectX on Windows, Vulkan on Linux). - On Linux: a Vulkan driver, Wayland or X11 with `libxkbcommon`, GTK 3 and WebKitGTK 4.1 (web views) and GStreamer 1.20 or later with its base and good plug-ins (videos). Web views need X11: on a Wayland desktop run the app under Xwayland (`WAYLAND_DISPLAY=` empty), else they show a placeholder. Building the runtime from a checkout also needs the development packages; on Debian and Ubuntu: ```sh sudo apt-get install clang libclang-dev pkg-config libfontconfig-dev libfreetype-dev libwayland-dev libx11-xcb-dev \ libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libzstd-dev libvulkan1 libgtk-3-dev libwebkit2gtk-4.1-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good ``` ## Running an app ```sh nsh run my-app --target gpui ``` Opens the app in a GPUI window (1200×800 by default), with hot reload, logs in the terminal and the error overlay as on the other targets. The development host (`nsh-gpui-host`) runs in place on every OS, with no app bundle around it. In a checkout, `nsh run` builds it first; the distribution ships it in `runtime/gpui/`. The host also runs a built bundle on its own: ```sh nsh build my-app --release -o build dist/nsh/runtime/gpui/nsh-gpui-host --bundle build/bundle.nshc --size 390x844 # in a checkout: target/debug/nsh-gpui-host ``` `--size` is the window's size in points (`x`). ## Packaging ```sh nsh package my-app --target gpui [--run] ``` The package is laid out as the native backend's of the machine it is made on, with GPUI's release runner (`nsh-gpui-app`) as the executable: | Packaged on | Output | |---|---| | macOS | an ad hoc signed `.app`, as [`--target macos`](https://nsh.dev/docs/macos.md#packaging) makes | | Windows | a directory `\` with `.exe` and `nsh\`, and `.zip`, as [`--target windows`](https://nsh.dev/docs/windows.md#packaging) makes | | Linux | a directory `/` with the executable, `nsh/` and a `.desktop` file, and `.tar.gz`, as [`--target linux`](https://nsh.dev/docs/linux.md#packaging) makes | As with the native backends, the runner is built for the machine it is built on: package a Mac app on a Mac, a Windows app on Windows and a Linux app on Linux. `--run` launches the app after packaging it. A Linux package links WebKitGTK 4.1 (web views) and GStreamer (videos) dynamically, so the machines it runs on need them installed, with GTK 3 and GStreamer's base and good plug-ins (Debian / Ubuntu: `libwebkit2gtk-4.1-0`, `libgstreamer1.0-0`, `libgstreamer-plugins-base1.0-0`, `gstreamer1.0-plugins-good`), besides a Vulkan driver and `libxkbcommon`. ## Embedding in a GPUI app The `nsh-platform-gpui` crate (from the NSH repository) gives a Rust GPUI app `AppView`: one NSH engine as a GPUI view (`Entity`), which fills the space its parent element gives it. Each `AppView` is independent; several can run at once. ```rust use gpui::{AppContext, Application, WindowOptions}; use nsh_platform_gpui::{App, AppOptions, AppView, Bundle}; gpui_platform::application().run(|cx| { cx.open_window(WindowOptions::default(), |window, cx| { cx.new(|cx| { let view = AppView::new(AppOptions::default(), window, cx); let code = std::fs::read_to_string("app/bundle.js").expect("bundle"); view.load(App::Js(Bundle::source(code))); view }) }) .unwrap(); }); ``` Put the entity in your element tree like any other view (a GPUI Kit `Root`, a panel, a dock). `AppView::load` replaces whatever ran; `stop` (or dropping the view) ends the app. `AppOptions::asset_base` tells it where `asset:/` URIs resolve (`AssetBase::Directory` or `AssetBase::Http`). Bytecode bundles (`bundle.nshc`) load with `unsafe { Bundle::bytecode(bytes) }` (or `Bundle::read_trusted(path)`, unsafe too): QuickJS bytecode is trusted input, so only load it from your own app's files. - `open_app_window(source, options, cx)` opens a window with one `AppView` in a running GPUI app. - `run_app(source, options)` starts GPUI and runs one app in a window until it closes (what the release runner does). - `AppView::register_component(name, factory)` registers a native component of yours before loading ([Native components](https://nsh.dev/docs/native-components.md)). - `set_ui_sets(sets)` replaces the default `nsh/ui/mobile` set, for apps with Rust components of their own. ## Differences What draws or behaves differently from the native desktop backends: - **Transforms**: rotated, skewed and projected views turn their backgrounds, borders and shadows, but text stays upright, and images, videos and canvases draw at their bounds. - **Opacity** applies to each color rather than to a group, so overlapping children of a translucent view show through each other; an image's opacity (a fade-in, a translucent view) changes in sixteenths. - **Rounded clips** (`overflow: "hidden"` with a radius) round the backgrounds and images of the children, but text, borders, videos and native components are cut by the rectangle. - **Not drawn**: `filter: blur()`, `backdropFilter` and `mixBlendMode`; shadows show through translucent fills; `borderCurve: "continuous"` draws circular corners. - **Canvas text** is approximated: upright under rotations, made smaller rather than narrower for `maxWidth`, gradients paint it in one color, `strokeText` fills, its shadows are not blurred, and composite operations and `clearRect` do not apply to it. - **Scroll views** do not zoom (`zoomTo` and the zoom props are ignored). - **Video** plays on Linux (GStreamer); on macOS and Windows a video shows a placeholder and reports `onError`. - **Web views** are native views above everything NSH draws at their place. On Linux they need X11 (see [Requirements](#requirements)); where pages stay black under Xwayland ("Failed to create GBM buffer" in the log), run the app with `WEBKIT_DISABLE_DMABUF_RENDERER=1`. On macOS and Windows clipping ancestors do not cut them and failed loads report no `onError`. `source.baseUrl`, `scrollEnabled` and `allowsBackForwardNavigationGestures` are not supported. - **Accessibility** goes through AccessKit: screen readers can activate, focus, set values and scroll elements into view everywhere; custom actions and the context-menu action are not passed on yet. For comparison, each native backend's own list: [macOS](https://nsh.dev/docs/macos.md#what-differs-on-macos), [Windows](https://nsh.dev/docs/windows.md#what-differs-on-windows), [Linux](https://nsh.dev/docs/linux.md#what-differs-on-linux). ## See also - [CLI](https://nsh.dev/docs/cli.md): `nsh run --target gpui`, `nsh package --target gpui` - [Packaging](https://nsh.dev/docs/packaging.md) - [Platform overview](https://nsh.dev/docs/platforms.md) Source: https://nsh.dev/docs/gpui --- # Web (WebAssembly) The NSH runtime also compiles to WebAssembly. The same engine runs in a browser page and renders to the DOM instead of UIKit or Android views: the core, the reactive graph, the renderer, Taffy layout, the `nsh/ui/mobile` components and QuickJS itself. `nsh package --target web` publishes an app as a static site for any web server or CDN ([Publish an app](#publish-an-app)), `nsh run --target web` runs it in the browser while you edit it ([Develop in the browser](#develop-in-the-browser)), and every live demo on this site is an app bundle from `nsh build` running on this runtime. ## What runs where On every native platform the engine has its own thread and hands one `MountBatch` per commit to the main thread. A browser page has one thread, so the web build drives the same engine step by step from the page's event loop (`nsh_engine::LocalEngine`): ```text pointer / keyboard / scroll events ─┐ setTimeout (the engine's deadline) ─┼─▶ nsh.wasm: engine turn (JS, core, layout) ─▶ one JSON batch ─▶ nsh.js: DOM requestAnimationFrame ──────────────┘ ▲ native animation outputs ───┘ ``` - **Layout is the core's.** Every view is an absolutely positioned element at the frame Taffy computed, so flexbox, grid, `aspectRatio` and percentages behave exactly as on devices. The browser's own layout is not involved. - **Styles are turned into CSS in Rust.** Background, borders, radii, shadows (`shadow*` and `boxShadow`), gradients, filters, `backdropFilter`, transforms, `transformOrigin`, `outline`, `mixBlendMode`, transitions and the built-in keyframe animations become CSS properties. Tailwind classes, `active:`, `hover:` and `focus:` states and `dark:` all resolve natively, as they do on devices. - **Text is laid out in Rust** with font measurements from the browser (`CanvasRenderingContext2D.measureText`) and drawn run by run at the positions the layout chose, so what was measured is what is drawn. Lines break after spaces, around CJK characters and at `\n`; `numberOfLines` truncates with an ellipsis. - **Native animations run in Rust** on `requestAnimationFrame`: `Animated` timing, spring and decay, `interpolate`, native values bound to `opacity`, `transform` and `backgroundColor`, drags bound with `pan`, scroll-linked values and canvas arguments. JS is not involved per frame, as on devices. `LayoutAnimation`, style transitions, the `animate-*` keyframes and screen transitions move as on the desktop backends, and `prefers-reduced-motion` turns them off (screens fade). - **Input:** presses with `activeOpacity` and long presses, the responder system (`onTouch*`, `PanResponder`), native drags and two-finger pinch and rotation, scroll views (the browser's scrolling), pull to refresh, zooming scroll views (pinch, ctrl+wheel and touchpad pinches, `zoomTo`), `` with controlled values, focus and submit, `Canvas`, `Stack` and `Screen` with a header, `Modal`. - **System UI is drawn in the page:** `Alert` and `ActionSheet` look like the desktop backends' dialogs on desktop browsers and like iOS's on phones (with the `desktop` idiom, see below), and `Toast.show` shows a toast at the bottom. Custom fonts are measured again once the browser has loaded them, and an app waits for the fonts it imports before its first frame shows. - **Modals** are presented as on the platform the browser is like: `pageSheet` and `formSheet` (`desktop={{ presentationStyle }}` or `web={{ presentationStyle }}`) are cards centered over the dimmed page in desktop and tablet browsers, the sizes the desktop backends use, closed by Escape, the browser's back button or a click outside (each asks through `onRequestClose`); in phone browsers they rise from the bottom to just below the top, as iPhone sheets do. `fullScreen` and transparent modals cover the app; `animationType`, `onShow` and `onDismiss` work as on devices. ## Keyboard, mouse and touch Published apps behave as desktop apps in desktop browsers and as phone apps in phone browsers: - **Wheels and touchpads** scroll the innermost scroll view along their axis that can still move (a vertical wheel scrolls a horizontal list when nothing vertical can), and stay on it until the scroll ends; wheel notches glide, paging and snapping views move one page per scroll. Ctrl + wheel and touchpad pinches pinch views with native pinch values instead of zooming the page. Dragging with the mouse does not scroll, as on the desktop platforms: mouse drags go to pans, the responder system and text selection. Scroll views report `onScrollBeginDrag` / `onScrollEndDrag`, `onMomentumScrollBegin` / `End` and `onScrollEnd` as on the desktop platforms. - **Keyboard**: Tab and Shift+Tab move between pressables and inputs (a focus ring and `focus:` styles show the keyboard focus); Return presses the focused pressable, Space presses it when released; arrows, Page Up / Page Down, Space, Home and End scroll the scroll view around the focus or under the mouse; Escape leaves a field, closes the dialog, then goes back. `nsh/keymap` shortcuts work in the browser, matched on the spot (`mod` is Command in Safari and Chrome on a Mac, Control elsewhere). - **Going back**: Escape, Alt+Left (Cmd+[ on a Mac) and the mouse's back button pop the innermost stack, or ask the newest modal to close, as the Android back button does; `preventBack` and `onBackPress` work as there. When the app is the whole page, every pushed screen and presented modal is a browser history entry, so the browser's back button goes back in the app. - **Touch**: only scroll views pan; the page itself never scrolls, zooms or pulls to refresh under the app, and taps have no delay. In an iframe, swipes the app does not take scroll the embedding page. Drags on views with native pans, pinches and the responder system are the app's. - **Accessibility**: `accessibilityRole`, `accessibilityLabel`, `accessibilityHint`, `accessibilityState`, `accessibilityValue`, `accessibilityLiveRegion` and `accessibilityElementsHidden` become ARIA; `AccessibilityInfo.announceForAccessibility` speaks through a live region and `setAccessibilityFocus` moves the focus. Views under a modal or alert are inert. - **Selection and menus**: in desktop browsers text is selectable by default, as on a web page, except inside interactive elements (views with press or touch handlers: buttons, pressable rows, tabs) and text with `selectable={false}` or `select-none`; dragging the mouse over it selects it, Ctrl/Cmd+C copies it line by line and Ctrl/Cmd+A selects all of the text the selection is in. In phone and tablet browsers only `selectable` text is selectable, as in native apps. The browser's context menu appears only in selectable text and inputs. ## Publish an app ```sh nsh package App.tsx --target web # out//: a static site nsh package App.tsx --target web --run # and serve it on this machine, opened in the browser ``` `nsh package --target web` writes `out//` (`--out` for another directory), laid out like a frontend build: one page and files named after their content. | File | What it is | |---|---| | `index.html` | The app's page: its name as the title, the icons, the colors of its `appearance`, a loading indicator, and a message when the app cannot load or start | | `manifest.webmanifest` | The name, id, icons and colors browsers install the app with | | `nsh..js` | The DOM runtime, minified (79 KB, 22.5 KB with brotli) | | `nsh..wasm` | The engine, the core, the mobile components and QuickJS without its compiler (about 3.5 MB, 1.2 MB gzipped, 0.93 MB with brotli) | | `app..nshc` | The app, built as `nsh build --release --split` builds it, compiled to QuickJS bytecode: nothing is compiled in the browser, so it starts sooner | | `harden..nshc` | The [sandbox's](https://nsh.dev/docs/sandbox-and-capabilities.md) hardening, compiled likewise (1.4 KB) | | `chunks/..nshc` | What `import()` loads, compiled likewise, downloaded the first time it is imported ([split bundles](#split-bundles)) | | `assets/..` | Every asset the app imports, loaded from its URL when it is used (fonts before the code that registers them runs, as text is measured with them: before the app starts, or with the chunk that imports them) | | `icons/..png` | The icon of `nsh.json` at 32 (favicon), 180 (`apple-touch-icon`), 192 and 512 pixels | `` is the start of the SHA-256 of the file's content: a file's name changes exactly when its content does, so a new release changes only `index.html` and the files that really changed. Everything uses relative URLs, so the site works from any directory of any host (`https://example.com/apps/counter/`). ### Deploy it Upload the directory to any static host: an object store behind a CDN, Cloudflare Pages, Netlify, GitHub Pages, nginx. Two rules make it load fast and update reliably: | Files | `Cache-Control` | |---|---| | `index.html`, `manifest.webmanifest` | `no-cache` (revalidated on every load, so a release shows at once) | | everything else | `public, max-age=31536000, immutable` | Serve `.wasm` as `application/wasm` (browsers then compile it while it downloads) and let the host or CDN compress text and WebAssembly (gzip or brotli). `nsh package --target web --run` serves the site this way on `http://127.0.0.1:4173/`. An app published for the web runs like a packaged native app: as a release, [hardened](https://nsh.dev/docs/sandbox-and-capabilities.md) (no `eval`), with the capabilities its `nsh.json` declares. When they restrict `network`, the page also carries a Content-Security-Policy `connect-src` with the declared hosts (and the page's own origin), so the browser itself refuses requests and redirect hops to any other host. An error that keeps it from starting is shown on the page; later errors go to the browser's console. ### Embed it with an iframe There is no embedding API on the web: embed the published app with an ` ``` ## Develop in the browser ```sh nsh run App.tsx --target web ``` starts the dev server and opens its page, `http://127.0.0.1:8790/`, in the browser. The page is a development host like the others: it runs each build of the app, applies edits in place (hot updates), shows build errors and runtime errors over the app, and sends the app's `console` output to the terminal. Phones on the same network open the address `nsh run` prints (`browsers on this network: http://192.168.…:8790/`). `nsh dev` serves the same page once the web runtime is there (in the distribution, always). ## Build the website's player ```sh cargo xtask web # nsh.wasm, nsh.js, the player and website/demos/*.tsx cargo xtask web --out target/play # somewhere else ``` The output directory holds: | File | What it is | |---|---| | `gallery/nsh..wasm` | The runtime of published apps (about 3.5 MB, 1.2 MB gzipped, 0.93 MB with brotli; built with the `web` profile), shared by the player and the gallery | | `nsh-playground.wasm` | The same plus NSH's TSX compiler, for the [playground](#the-playground) (about 8 MB, 2.4 MB gzipped) | | `gallery/nsh..js` | The DOM runtime (minified): loads the wasm, applies batches, turns input into events | | `index.html` | A player page: runs `demos/.nshc` for `#` | | `demos/.nshc` | Each demo compiled by `nsh build --release` | | `gallery/` | The gallery app published with `nsh package --target web`, shown on the [Gallery](/gallery) page | Serve the directory with any static server (it needs the `application/wasm` MIME type for streaming compilation) and open `index.html#counter`. ## The playground The [Playground](/playground) edits and runs an app in the browser: the page compiles the TSX with NSH's own JSX compiler (built into `nsh-playground.wasm`, without rolldown) and runs it on every pause in typing. Every demo on this site has an **Edit in Playground** link, and **Share** puts the code in the link itself, so nothing is stored on a server. A playground app is one module: it imports NSH's modules (`nsh`, `nsh/*`, `nsh/ui/mobile`) but no other files or packages. `console.log` output and errors appear below the phone. The compiler is also a Rust function, `nsh_bundler::compile_module(name, source, ui_sets)`, available without the crate's `bundle` feature. ## Embed it in a page `nsh.js` is an ES module. `createPlayer` loads the runtime into a container; `run` starts a bundle that `nsh build` writes: `bundle.nshc`, its bytecode, as bytes, or `bundle.js` as a string. The runtime `nsh package --target web` ships (`nsh.wasm`) has no JavaScript compiler and runs bytecode only; the development runtime (`nsh-dev.wasm` in the distribution) runs both: ```js import { createPlayer } from "./nsh.js"; const player = await createPlayer(document.querySelector("#phone"), { wasm: "./nsh.wasm", assetBase: "./assets/", // where `asset:/…` URIs (image imports) are served from // dark: true, // force an appearance; otherwise it follows the system // idiom: "phone", // force "phone", "pad" or "desktop"; otherwise the pointer and the size decide // history: false, // screens and modals as browser history entries; by default only when the app is the page, not in an iframe }); player.run(await (await fetch("./bundle.nshc")).arrayBuffer()); ``` `assets` (a map from `asset:/` paths to URLs) replaces `assetBase`, and `readAsset(uri)` returns the bytes of an asset the engine needs at once (a font) or `null`; `player.run(code, { release: true, capabilities })` runs a bundle as a release under the capabilities it declares (the default is development settings); with bytecode, a release also takes the hardening's bytecode (`harden`, the bytes of `harden..nshc`). The page `nsh package --target web` writes uses all of them. `run(code, { chunks, chunkBase })` also takes where the chunks of a [split bundle](#split-bundles) are. With `nsh-playground.wasm`, `player.runSource(tsx)` compiles TSX first and returns the compile error, or `null`; `player.compile(tsx)` returns `{ code }` or `{ error }`. The container's size is the app's window (`windowSize()`); resizing the container resizes the app. `player.setDark(true)` switches the appearance at run time, which apps observe through `colorScheme()` and `dark:` classes. ## The browser as a platform `Platform.OS` is `"web"`. What kind of device the browser is on decides how the app behaves, as `Platform.constants.interfaceIdiom`: - `"desktop"`: a mouse (a fine pointer that hovers) and a container at least 600 points wide. `Platform.isDesktop` is `true`, `Platform.select` picks `desktop`, `desktop={{…}}` props apply and headers are as tall as on the desktop backends. - `"pad"`: a touch screen whose short side is at least 600 points; `"phone"` otherwise. These are mobile: `Platform.select` picks `mobile`. The idiom is decided when the app starts and stays the same while it runs, as on every platform; a narrow desktop window is still a desktop, laid out for its `windowSize()`. `web={{…}}` props, `Platform.select({ web })` and `web:` classes apply in browsers only; `ios={{…}}`, `native` and `ios:` do not apply there. The page reports the rest of the environment, and keeps it current: | Value | From the browser | |---|---| | Locale (`Intl`'s default) | `navigator.languages[0]` | | `I18nManager.isRTL` | `` when set, else the locale's script | | `reduceMotion()` | `prefers-reduced-motion` | | `fontScale()` | the root element's font size / 16 px (the browser's text size setting) | | `PixelRatio.get()` | `devicePixelRatio` (it changes with zoom and screens) | | `colorScheme()` | `prefers-color-scheme` | | `safeArea()` | `env(safe-area-inset-*)` where it overlaps the container (`viewport-fit=cover` pages on phones) | | `keyboard()` | how much of the container the on-screen keyboard covers (`visualViewport`) | `Platform.Version` is `"0.0"`, and `screenReaderEnabled()` stays `false` (browsers do not tell). [`StatusBar`](https://nsh.dev/components/status-bar.md)'s `web={{ themeColor }}` sets the page's ``, the color browsers give their bars around the page. ## Networking `fetch` (with streams, `AbortController` and `FormData`) and `` URLs go through the browser's own network stack, after NSH has checked the app's [capabilities](https://nsh.dev/docs/sandbox-and-capabilities.md). Since the requests are the browser's, the browser's rules apply: - **Relative URLs** resolve against the page's URL, as in the browser: a published app reaches its own API with `fetch("/api/items")`, and `` loads from its own server. (Native platforms have no page, so they need absolute URLs.) - **CORS.** A request to another origin than the page's only succeeds if that server allows the page's origin (`Access-Control-Allow-Origin`); otherwise `fetch` rejects with a network error. Only the response headers the server exposes are visible. - **Cookies** (and HTTP authentication) are sent to the page's own origin only (`credentials: "same-origin"`), and headers browsers forbid (`Cookie`, `User-Agent`, `Host`, ...) are dropped. - **Redirects** are followed by the browser. A redirected request that must not follow it fails: `redirect: "manual"` and `"error"`, and requests of an app whose capabilities restrict `network` (NSH checks every hop, and browsers do not reveal a redirect before following it). - **Request bodies** that are a `ReadableStream` are collected first and sent in one piece; response bodies stream as they arrive. ## Split bundles A web app does not need to download all of its code before the first screen shows. `nsh package --target web` publishes apps split, with each chunk as `chunks/..nshc` and the page mapping chunks to those files (a bundle run as bytecode loads its chunks as bytecode: `Screen.nshc` next to `Screen.js`). To split a bundle yourself, `nsh build --split` puts the modules that `import()` loads into chunks of their own, next to `bundle.js`: ```sh nsh build --release --split -o site/app # bundle.js the app up to its first screen # Screen.js import("./Screen") and what only it uses # detail.js what two chunks share, when they do (made by the bundler) ``` The first `import("./Screen")` downloads `Screen.js` and the chunks it imports, all at once, then evaluates them; later imports of it resolve at once. The page says where the chunks are: by default next to the page (`chunkBase`), or one URL per chunk, for files renamed with a content digest so a CDN can cache them forever: ```js player.run(code, { chunks: { "Screen.js": "/app.3f2a9c.js", "detail.js": "/app.81bb07.js" }, }); ``` A chunk that cannot be downloaded rejects the `import()` with a `TypeError` naming it; the next `import()` tries again. Chunk names are fixed file names from the bundle: the app cannot make the page download anything else. The [gallery](/gallery) loads each demo this way: `lazy` (in `examples/gallery/ui.tsx`) makes a component that starts the import the first time it renders, shows a placeholder until it resolves, then renders what it loaded. Its first load is the index and the demo shown first; a font a demo imports comes with that demo's chunk. Native builds hold every module, so there the import resolves at once: ```tsx const Stocks = lazy(() => import("./demos/Stocks").then((m) => m.Stocks)); ``` ## Limits The web runtime is for demos and documentation. It does not have: - **Web views of other sites**: `WebView` is an `