Documentación
Primeros pasos
Esta guía crea una app NSH, la ejecuta en las plataformas a las que apuntas y la distribuye. Las apps NSH se escriben en Rust o TSX, sobre el mismo motor: Rust compila tu app a código nativo sin una VM de JavaScript, y TSX se ejecuta en QuickJS con actualizaciones en caliente, como módulos que una app nativa puede cargar en tiempo de ejecución. La guía sigue primero una app en Rust y después la misma app en TSX. Supone que nsh está instalado y en tu PATH (Instalación); en un checkout, usa
./target/debug/nsh en su lugar.
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, hoverFill, ui } from "./palette";
// The app `nsh init` creates, with a reset button and a short history.
function App() {
const count = signal(0);
const history = signal<number[]>([]);
const change = (delta: number) => {
const next = count() + delta;
count.set(next);
history.update((h) => [next, ...h].slice(0, 4));
};
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, alignItems: "center", justifyContent: "center", gap: 16 }}>
<Text style={{ fontSize: 15, color: ui().muted }}>{desktop ? "You clicked" : "You tapped"}</Text>
<Text style={{ fontSize: 56, fontWeight: "700", color: count() < 0 ? ui().down : ui().text }}>{count()}</Text>
<View style={{ flexDirection: "row", gap: 12 }}>
<View onPress={() => change(-1)} role="button" {...button()}>
<Text style={buttonText()}>−1</Text>
</View>
<View onPress={() => change(1)} role="button" {...button("accent")}>
<Text style={buttonText("accent")}>{desktop ? "+1" : "Tap"}</Text>
</View>
</View>
<If when={history().length > 0}>
<Text style={{ fontSize: 13, color: ui().muted }}>Recent: {history().join(", ")}</Text>
</If>
<View onPress={() => (count.set(0), history.set([]))} hitSlop={8} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
<Text style={{ fontSize: 15, color: ui().accent }}>Reset</Text>
</View>
</View>
);
}
mount(App);#Crear una app
En una terminal, nsh init pregunta en qué lenguaje se escribe la app: rust, ts o mixed (TSX con un crate de Rust propio).
--template elige sin preguntar.
#En Rust
nsh init my-app --template rust
nsh init crea el directorio y escribe un proyecto de Cargo:
| Archivo | Qué es |
|---|---|
src/lib.rs |
La app: su componente raíz, el estado y los manejadores |
src/main.rs |
Monta la misma raíz en macOS, Windows o Linux para cargo run |
Cargo.toml |
El crate, que depende de nsh-core y nsh-base en la versión del CLI |
nsh.json |
El nombre, el id, la versión y el número de compilación de la app, su crate ("rust": ".") y su componente raíz ("root": "App") (Estructura del proyecto) |
App.test.tsx |
La prueba de la raíz, en TypeScript (nsh test), con tsconfig.json y .nsh/types para tu editor |
README.md |
Cómo ejecutar, probar y empaquetar la app, y cómo añadir más adelante una entrada TSX |
.gitignore |
Ignora .nsh/ y target/ |
El desarrollo en Rust necesita una toolchain de Rust (rustup); los comandos de nsh compilan los runners de la app
a partir de los crates de NSH en crates.io, en la versión del CLI.
#En TSX
nsh init my-app --template ts
Una app TSX es uno o más archivos .tsx con una entrada: lo que ejecuta nsh run y lo que una app host carga como módulo.
| Archivo | Qué es |
|---|---|
App.tsx |
El módulo de entrada: renderiza la app con mount |
App.test.tsx |
Su prueba (nsh test) |
nsh.json |
El nombre, el id, la versión y el número de compilación de la app (Estructura del proyecto) |
tsconfig.json |
Extiende .nsh/tsconfig.json, la configuración de TypeScript de NSH, que resuelve nsh a las declaraciones de .nsh/types |
.gitignore |
Ignora .nsh/ y target/ |
.nsh/types/ |
Declaraciones generadas de todos los módulos de NSH, para tu editor |
Los archivos que ya existen se conservan, así que puedes ejecutarlo en un directorio que ya tenga algunos de ellos; una app en Rust se crea en un directorio nuevo.
En ambos casos, el nombre visible es por defecto el nombre del directorio; pasa --name "My App" para elegir otro. El id se convierte en
com.example.<name>: cámbialo en nsh.json antes de distribuir. nsh init my-app --template mixed crea una app TSX
que además es un crate de Rust (Cargo.toml y src/lib.rs junto a App.tsx), cuyo componente, módulo y clase
importa App.tsx (Compilar con Rust).
#El componente raíz
Las dos plantillas producen la misma app, un contador:
// src/lib.rs
use nsh_base::theme;
use nsh_core::prelude::*;
/// The crate's UI set: what a TSX entry may import from `my-app/native`.
#[native_module("my-app/native")]
fn native(registry: &mut Registry) {
registry.register::<App>();
}
/// The application's root component.
#[component]
pub fn App() -> impl IntoElement {
let count = signal(0_u32);
view()
.flex_1()
.bg(move || theme().background)
.text_color(move || theme().foreground)
.child(
view()
.flex_1()
.p_safe_area()
.child(
view()
.flex_1()
.items_center()
.justify_center()
.gap_4()
.child(text(move || format!("Count: {}", count.get())).text_size(px(28.0)))
.child(
view()
.px_5()
.py_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(move || count.update(|value| *value += 1))
.child(
text("Tap")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground),
),
),
),
)
}// App.tsx
import { mount, signal, Text, View } from "nsh";
function App() {
const count = signal(0);
return (
<View class="flex-1 items-center justify-center gap-4 bg-background pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<Text class="text-3xl font-semibold text-foreground">{count()}</Text>
<View
onPress={() => count.update((c) => c + 1)}
role="button"
class="rounded-(--radius) bg-primary px-5 py-3 active:bg-primary/90"
>
<Text class="text-lg text-primary-foreground">Tap</Text>
</View>
</View>
);
}
mount(App);En Rust:
use nsh_core::prelude::*trae las primitivas (view(),text(),scroll(),input(),image(), …), las señales y los métodos de estilo tipados (.p_safe_area()añade a una vista el relleno del área segura);nsh_basetiene los componentes base sin estilo (button(), el teclado, los diálogos) ytheme(), los colores y radios con los que dibujan los componentes de la app (Tema).#[component] pub fn App()es un componente: una función que se ejecuta una vez y devuelve sus elementos. El"root": "App"densh.jsonlo nombra, ynsh runlo monta.signal(0_u32)crea un valor que cambia con el tiempo.text(move || format!(…, count.get()))enlaza el texto a la señal: cuandocountcambia, NSH vuelve a ejecutar este closure y actualiza este único texto, nada más. Los estilos reciben closures de la misma manera:.bg(move || theme().primary)sigue al tema hacia el modo oscuro..on_press(..)hace que la vista responda a las pulsaciones;.active(|s| s.opacity(0.6))es su estilo mientras está pulsada, aplicado de forma nativa. El botón es propio de la app: NSH proporciona las primitivas y las capacidades, y la app dibuja su UI con ellas..px_5(),.rounded(px(10.0)),.gap_4()son métodos de estilo tipados con los mismos nombres que las utilidades de Tailwind, en puntos (px). El layout es flexbox, y los hijos se apilan en columna por defecto (Layout).#[native_module("my-app/native")]nombra lo que una entrada TSX puede importar del crate, para cuando añadas pantallas TSX encima (Compilar con Rust).
En TSX:
import { mount, signal, Text, View } from "nsh":nshes el módulo central: señales,mount, las demás primitivas reactivas y los elementos host. Lo sirve el runtime nativo, no se incluye en el bundle.nsh/basetiene los componentes base, igual quensh_baseen Rust.function App()es un componente que se ejecuta una vez.{count()}lee la señal dentro de JSX: el compilador ve una llamada y hace que este texto siga a la señal.<View onPress>es una vista nativa que reacciona a las pulsaciones; las clasesactive:son su estilo mientras está pulsada.View,Text,Image,Scrolly los demás elementos host se importan densh(Inputdensh/base); soloIfyEachno necesitan importación.classrecibe clases de Tailwind;style={{ … }}recibe un objeto de propiedades de estilo en camelCase, con números en puntos.bg-background,bg-primary,text-primary-foregroundyrounded-(--radius)nombran los tokens del tema de la app (Tokens del tema): siguen el modo oscuro por sí solos, y"theme"ennsh.jsonlos vuelve a estilizar.mount(App)renderiza el componente en la superficie de la app.
#Desarrollarla
#Ejecutarla
nsh run compila la app, la lanza y la recompila mientras editas; los logs de la app aparecen en la terminal. Pásale el
directorio de la app: ejecuta la raíz de una app Rust, o el App.tsx de una app TSX (nsh run my-app/App.tsx nombra otra entrada).
Para una app Rust, nsh run compila el runner propio de la app para el destino, con tu crate y la raíz montada y sin
QuickJS, y lo inicia; un cambio en el código Rust lo compila de nuevo y reinicia la app. cargo run en el
directorio de la app inicia la misma raíz directamente en el backend de escritorio. Para una app TSX, nsh run inicia un servidor de desarrollo y un
host de desarrollo que carga la app desde él, y parchea la app en ejecución con cada guardado.
Sin --target, nsh run usa la máquina en la que estás: una ventana de AppKit en macOS, una ventana de Win32 en Windows y una
ventana de GTK en Linux. Elige otra plataforma con --target:
| Destino | Comando | Se ejecuta desde |
|---|---|---|
| Simulador de iOS | nsh run my-app --target ios |
macOS con Xcode |
| Android | nsh run my-app --target android |
macOS, Windows, Linux |
| macOS (AppKit) | nsh run my-app |
macOS (el predeterminado) |
| Windows | nsh run my-app |
Windows (el predeterminado) |
| Linux (GTK 4) | nsh run my-app |
Linux (el predeterminado) |
| Web | nsh run my-app --target web |
macOS, Windows, Linux |
| Mac Catalyst | nsh run my-app --target catalyst |
macOS |
#Simulador de iOS
nsh run my-app --target ios
nsh run my-app --target ios --device "iPhone 16 Pro"
nsh usa un simulador ya iniciado, o inicia el iPhone más reciente si no hay ninguno en ejecución, e instala en él la app (el
runner de una app Rust, o el host de desarrollo). --device elige un simulador por nombre o UDID.
#Android
nsh run my-app --target android
Inicia primero un emulador o conecta un dispositivo (adb devices los lista; NSH usa el primero). nsh instala la app
en él: el libnsh.so propio de una app Rust, o un APK de host de desarrollo (recompilado solo cuando cambia el runtime) cuyo puerto de servidor
de desarrollo se reenvía al dispositivo con adb reverse, de modo que funciona por USB sin una red compartida.
#macOS (AppKit)
nsh run my-app --target macos
La app se abre como una app de escritorio para Mac (AppKit, en una ventana redimensionable), con entrada de ratón, trackpad y teclado en lugar de toques. Requiere macOS 14 o posterior; consulta macOS.
#Windows (predeterminado en Windows)
nsh run my-app
La app se abre en una ventana de escritorio (1200×800 puntos), con entrada de ratón, touchpad, táctil y teclado. Requiere Windows 10 1809 o posterior; consulta Windows.
#Linux (predeterminado en Linux)
nsh run my-app
La app se abre en una ventana de GTK (390×844 puntos). Requiere GTK 4.14 o posterior; consulta Linux.
#Web
nsh run my-app --target web
nsh imprime la dirección de la página; ábrela en un navegador. Una app Rust se ejecuta como WebAssembly con tu crate, y la página
abierta carga por sí sola la nueva compilación después de un cambio; consulta Web.
#Mac Catalyst
nsh run my-app --target catalyst
La app se abre en una ventana del tamaño de un iPhone (390×844 puntos) en tu Mac. Es la forma más rápida de ver una app como la muestra un teléfono:
sin simulador que iniciar. Usa UIKit como iOS, así que lo que ves es lo que muestra un iPhone, salvo por el comportamiento
propio de Mac, como el desplazamiento con la rueda del ratón. Para la app de Mac en sí, usa --target macos.
#Editarla
Deja nsh run en ejecución y cambia la etiqueta del botón:
text("Add one")
.text_size(px(17.0))
.text_color(move || theme().primary_foreground)<Text style={{ color: "white", fontSize: 17 }}>Add one</Text>Guarda. Una app Rust se compila de nuevo y se reinicia con la nueva etiqueta; el contador vuelve a empezar. Una app TSX recibe una actualización en caliente:
el CLI recompila y envía un parche, y el host vuelve a ejecutar el módulo modificado y renderiza de nuevo solo los componentes
cuyo código cambió. Aquí es App, que posee la señal count, así que el contador también vuelve a empezar.
Las piezas de UI pasan a componentes propios, o a funciones simples que devuelven elementos:
/// The app's button: a view that answers presses.
fn tap_button(on_press: impl Fn() + 'static) -> impl IntoElement {
view()
.p_3()
.rounded(px(10.0))
.bg(move || theme().primary)
.active(|s| s.opacity(0.6))
.on_press(on_press)
.child(text("Tap").text_size(px(17.0)).text_color(move || theme().primary_foreground))
}
// In App: .child(tap_button(move || count.update(|value| *value += 1)))function TapButton(props: { onPress: () => void }) {
return (
<View onPress={props.onPress} activeStyle={{ opacity: 0.6 }} style={{ padding: 12, borderRadius: 10, backgroundColor: "#0969da" }}>
<Text style={{ color: "white", fontSize: 17 }}>Tap</Text>
</View>
);
}
// In App: <TapButton onPress={() => count.update((c) => c + 1)} />En TSX, el estado sobrevive cuando editas un componente que no lo posee: toca varias veces y luego cambia el
color de TapButton, y el botón se actualiza mientras el contador se mantiene. El reemplazo en caliente funciona por componente de nivel superior: una función con un
nombre en mayúscula inicial cuyo cuerpo contiene JSX. Un componente se renderiza de nuevo cuando cambia su propio código, o cuando cambia el
código del módulo fuera de los componentes (una constante que usa, una función auxiliar). Si una edición no puede aplicarse en el lugar (por ejemplo, porque
importa un módulo de NSH que la app en ejecución aún no había importado, o cambia las capacidades de nsh.json), el host recarga la
app completa.
#Errores
Un error de compilación de Rust se imprime en la terminal con la salida de cargo, y nsh run compila de nuevo con el siguiente guardado. Un
error de compilación de TSX se imprime en la terminal y se muestra en el dispositivo como una superposición roja con el archivo, la línea y la
columna; la app sigue ejecutando la última compilación correcta. La superposición desaparece con la siguiente compilación exitosa (tócala para
cerrarla antes). Una excepción lanzada en tiempo de ejecución se muestra de la misma manera, con su stack asociado de nuevo a tu código fuente .tsx.
#Alojarla en tu app
Una app nativa muestra NSH en una vista propia: NshAppView (una UIView) en iOS, NshView (una View de Android) en
Android, AppView en macOS, Windows, Linux y en apps GPUI. Colócala como cualquier otra vista, en cualquier pantalla y tantas
veces como necesites.
Una raíz Rust forma parte de la compilación de la app: la app enlaza tu crate y monta la raíz con AppSource::Rust en el
AppView de macOS, Windows, Linux, GPUI e iOS (Compilar con Rust). Una app TSX es
un módulo que carga la vista: durante el desarrollo desde el servidor de desarrollo, en un release desde un bundle compilado o un paquete firmado
descargado. Las secciones siguientes cargan módulos TSX.
#iOS
Enlaza NativeShellDev.xcframework en Debug y NativeShell.xcframework en Release (Incrustar en apps de iOS), y luego:
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: "nshc", subdirectory: "nsh")!)
#endif
nsh dev my-app sirve el módulo a la app y envía cada guardado, igual que lo hace nsh run con su propio host. Para la
compilación de release, nsh build my-app --release --out MyApp/nsh escribe el bundle en una carpeta que añades a los
recursos de la app.
#Android
Añade nsh-dev.aar para las compilaciones de depuración y nsh.aar para las de release (Incrustar en apps de Android), y luego:
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 --release --out app/src/main/assets/nsh`
}
#macOS, Windows y Linux
Las apps de escritorio escritas en Rust incrustan NSH con AppView, a partir de los crates nsh-platform-macos, nsh-platform-windows,
nsh-platform-linux y nsh-platform-gpui: un NSView, una ventana hija, un widget de GTK o una vista de GPUI que
monta una raíz Rust o carga un bundle compilado. Consulta macOS,
Windows, Linux y GPUI,
y Plataformas para ver todos los hosts lado a lado.
#Lo que puede hacer el módulo
En iOS y Android, el host decide antes de cargar lo que un módulo puede hacer: grantCapabilities fija los hosts de red
y otras capacidades a las que puede acceder, y los límites restringen su memoria, el tiempo de llamada y el número de nodos (Sandbox y
capacidades). Para dar a los módulos tus propias vistas nativas (un mapa, un gráfico, un componente
de sistema de diseño), regístralas como componentes nativos.
#Distribuir actualizaciones como paquetes firmados
Un módulo TSX que la app descarga después del lanzamiento llega como un paquete firmado. Crea una clave raíz una sola vez y guárdala sin conexión, certifica con ella una clave de release y publica:
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 --key keys/release.secret --cert keys/release.cert --build 2
La app confía en la clave pública de la raíz y carga lo que descarga; NSH verifica la firma, el certificado y la lista de revocación antes de ejecutar nada:
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 tiene el mismo flujo con NshTrust y NshPackage. El alojamiento, la entrega y la revocación en detalle: Paquetes
firmados.
#O distribúyela como una app independiente
nsh package genera una app instalable, en Rust o TSX, sin un proyecto de Xcode ni una compilación de Gradle propios:
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
nsh package my-app --target web
Una app en Rust se empaqueta con su propio runner, compilado a partir de tu crate y de los crates de NSH, con su raíz montada; una app en TSX, con el runtime precompilado. Opciones, firma e iconos: Packaging.
#Pruébala en el terminal
nsh test ejecuta una app en modo headless, sin dispositivo ni ventana. Dado el nsh.json de una app en Rust o el punto de entrada de una app en TSX, abre la app, la deja ejecutarse e imprime lo que monta; con --trace, imprime además las operaciones de vistas nativas y el tiempo de cada commit (Testing):
nsh test my-app/nsh.json --trace # a Rust app's root
nsh test my-app/App.tsx --trace # a TSX app's entry
Dado un directorio, ejecuta las pruebas de la app: el App.test.tsx de cada plantilla abre la app, pulsa su botón y comprueba lo que muestra (nsh test my-app). El de una app en Rust abre su raíz (open(".")); también se puede probar en cargo test con nsh_testing::Headless (Building with Rust).
#Próximos pasos
- Building with Rust: apps en Rust, su tema y TSX sobre un crate de Rust.
- JSX and components y Reactivity: componentes y signals, en Rust y TSX.
- Platforms: qué admite cada plataforma y su propia página (iOS, Android, macOS, Windows, Linux).
- Signed packages y Sandbox and capabilities: la distribución y lo que pueden hacer los módulos.
- Project structure: qué va en
nsh.json, los assets y los archivos.