Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Documentation

Premiers pas

Ce guide crée une application NSH, l’exécute sur les plateformes ciblées et la publie. Les applications NSH s’écrivent en Rust ou en TSX, sur le même moteur : Rust compile votre application en code natif sans machine virtuelle JavaScript, et TSX s’exécute sur QuickJS avec des mises à jour à chaud, sous forme de modules qu’une application native peut charger à l’exécution. Le guide suit d’abord une application Rust, puis la même application en TSX. Il suppose que nsh est installé et présent dans votre PATH (Installation) ; dans un checkout, utilisez ./target/debug/nsh à la place.

guide-getting-started.tsx
guide-getting-started.tsxLive
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);

#Créer une application

Dans un terminal, nsh init demande dans quel langage l’application est écrite : rust, ts ou mixed (TSX avec un crate Rust dédié). --template choisit sans demander.

#En Rust

nsh init my-app --template rust

nsh init crée le répertoire et écrit un projet Cargo :

FichierRôle
src/lib.rsL’application : son composant racine, son état et ses gestionnaires
src/main.rsMonte la même racine sur macOS, Windows ou Linux pour cargo run
Cargo.tomlLe crate, qui dépend de nsh-core et nsh-base à la version de la CLI
nsh.jsonLe nom, l’id, la version et le numéro de build de l’application, son crate ("rust": ".") et son composant racine ("root": "App") (Structure du projet)
App.test.tsxLe test de la racine, en TypeScript (nsh test), avec tsconfig.json et .nsh/types pour votre éditeur
README.mdComment lancer, tester et packager l’application, et comment ajouter plus tard une entrée TSX
.gitignoreIgnore .nsh/ et target/

Le développement Rust nécessite une toolchain Rust (rustup) ; les commandes nsh compilent les runners de l’application à partir des crates de NSH sur crates.io, à la version de la CLI.

#En TSX

nsh init my-app --template ts

Une application TSX est constituée d’un ou plusieurs fichiers .tsx avec une entrée : ce que nsh run exécute, et ce qu’une application hôte charge comme module.

FichierRôle
App.tsxLe module d’entrée : rend l’application avec mount
App.test.tsxSon test (nsh test)
nsh.jsonLe nom, l’id, la version et le numéro de build de l’application (Structure du projet)
tsconfig.jsonÉtend .nsh/tsconfig.json, les réglages TypeScript de NSH, qui résolvent nsh vers les déclarations de .nsh/types
.gitignoreIgnore .nsh/ et target/
.nsh/types/Déclarations générées de chaque module NSH, pour votre éditeur

Les fichiers déjà existants sont conservés, vous pouvez donc l’exécuter dans un répertoire qui en contient certains ; une application Rust est créée dans un nouveau répertoire.

Dans les deux cas, le nom affiché est par défaut celui du répertoire ; passez --name "My App" pour en choisir un autre. L’id devient com.example.<name> : modifiez-le dans nsh.json avant de publier. nsh init my-app --template mixed crée une application TSX qui est aussi un crate Rust (Cargo.toml et src/lib.rs à côté de App.tsx), dont le composant, le module et la classe App.tsx importe (Développer avec Rust).

#Le composant racine

Les deux modèles produisent la même application, un compteur :

// 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),
                                ),
                        ),
                ),
        )
}

En Rust :

  • use nsh_core::prelude::* apporte les primitives (view(), text(), scroll(), input(), image(), …), les signaux et les méthodes de style typées (.p_safe_area() ajoute à une vue un padding égal à la zone de sécurité) ; nsh_base contient les composants de base sans style (button(), le clavier, les boîtes de dialogue) et theme(), les couleurs et les rayons avec lesquels les composants de l’application dessinent (Thème).
  • #[component] pub fn App() est un composant : une fonction qui s’exécute une seule fois et renvoie ses éléments. Le "root": "App" de nsh.json le nomme, et nsh run le monte. signal(0_u32) crée une valeur qui change au fil du temps.
  • text(move || format!(…, count.get())) lie le texte au signal : quand count change, NSH réexécute cette closure et met à jour ce seul texte, rien d’autre. Les styles acceptent des closures de la même façon : .bg(move || theme().primary) suit le thème en mode sombre.
  • .on_press(..) fait répondre la vue aux appuis ; .active(|s| s.opacity(0.6)) est son style pendant l’appui, appliqué nativement. Le bouton appartient à l’application : NSH fournit les primitives et les capacités, et l’application dessine son UI à partir de celles-ci.
  • .px_5(), .rounded(px(10.0)), .gap_4() sont des méthodes de style typées nommées comme les utilitaires de Tailwind, en points (px). La mise en page repose sur flexbox, et les enfants s’empilent en colonne par défaut (Mise en page).
  • #[native_module("my-app/native")] nomme ce qu’une entrée TSX peut importer depuis le crate, pour le moment où vous ajoutez des écrans TSX par-dessus (Développer avec Rust).

En TSX :

  • import { mount, signal, Text, View } from "nsh" : nsh est le module cœur : signaux, mount, les autres primitives réactives et les éléments hôtes. Il est servi par le runtime natif, et non inclus dans le bundle. nsh/base contient les composants de base, comme nsh_base en Rust.
  • function App() est un composant qui s’exécute une seule fois. {count()} lit le signal dans le JSX : le compilateur voit un appel et fait suivre le signal à ce texte.
  • <View onPress> est une vue native qui réagit aux appuis ; les classes active: sont son style pendant l’appui. View, Text, Image, Scroll et les autres éléments hôtes sont importés depuis nsh (Input depuis nsh/base) ; seuls If et Each n’ont pas besoin d’import.
  • class accepte des classes Tailwind ; style={{ … }} accepte un objet de propriétés de style en camelCase, avec des nombres en points. bg-background, bg-primary, text-primary-foreground et rounded-(--radius) désignent les tokens du thème de l’application (Tokens de thème) : ils suivent le mode sombre d’eux-mêmes, et "theme" dans nsh.json les restyle.
  • mount(App) rend le composant dans la surface de l’application.

#Développer

#Lancer l’application

nsh run compile l’application, la lance et recompile pendant que vous éditez ; les logs de l’application apparaissent dans le terminal. Donnez-lui le répertoire de l’application : il exécute la racine d’une application Rust, ou le App.tsx d’une application TSX (nsh run my-app/App.tsx désigne une autre entrée).

Pour une application Rust, nsh run compile le runner propre à l’application pour la cible, avec votre crate et la racine montée et sans QuickJS, puis le démarre ; une modification du code Rust le recompile et redémarre l’application. cargo run dans le répertoire de l’application démarre la même racine directement sur le backend desktop. Pour une application TSX, nsh run démarre un serveur de dev et un hôte de développement qui charge l’application depuis celui-ci, et patche l’application en cours d’exécution à chaque sauvegarde.

Sans --target, nsh run utilise la machine sur laquelle vous êtes : une fenêtre AppKit sur macOS, une fenêtre Win32 sur Windows et une fenêtre GTK sur Linux. Choisissez une autre plateforme avec --target :

CibleCommandeS’exécute depuis
Simulateur iOSnsh run my-app --target iosmacOS avec Xcode
Androidnsh run my-app --target androidmacOS, Windows, Linux
macOS (AppKit)nsh run my-appmacOS (par défaut)
Windowsnsh run my-appWindows (par défaut)
Linux (GTK 4)nsh run my-appLinux (par défaut)
Webnsh run my-app --target webmacOS, Windows, Linux
Mac Catalystnsh run my-app --target catalystmacOS

#Simulateur iOS

nsh run my-app --target ios
nsh run my-app --target ios --device "iPhone 16 Pro"

nsh utilise un simulateur déjà démarré, ou démarre l’iPhone le plus récent si aucun n’est en cours, et y installe l’application (le runner d’une application Rust, ou l’hôte de développement). --device choisit un simulateur par nom ou par UDID.

#Android

nsh run my-app --target android

Démarrez d’abord un émulateur ou connectez un appareil (adb devices les liste ; NSH utilise le premier). nsh y installe l’application : le libnsh.so propre à une application Rust, ou un APK d’hôte de développement (recompilé uniquement quand le runtime change) dont le port du serveur de dev est redirigé vers l’appareil avec adb reverse, ce qui fonctionne par USB sans réseau partagé.

#macOS (AppKit)

nsh run my-app --target macos

L’application s’ouvre comme une application Mac de bureau (AppKit, dans une fenêtre redimensionnable), avec la souris, le trackpad et le clavier comme entrées à la place du toucher. Elle nécessite macOS 14 ou ultérieur ; voir macOS.

#Windows (par défaut sur Windows)

nsh run my-app

L’application s’ouvre dans une fenêtre de bureau (1200×800 points), avec la souris, le pavé tactile, le tactile et le clavier comme entrées. Elle nécessite Windows 10 1809 ou ultérieur ; voir Windows.

#Linux (par défaut sur Linux)

nsh run my-app

L’application s’ouvre dans une fenêtre GTK (390×844 points). Elle nécessite GTK 4.14 ou ultérieur ; voir Linux.

#Web

nsh run my-app --target web

nsh affiche l’adresse de la page ; ouvrez-la dans un navigateur. Une application Rust s’exécute en WebAssembly avec votre crate, et la page ouverte charge d’elle-même le nouveau build après une modification ; voir Web.

#Mac Catalyst

nsh run my-app --target catalyst

L’application s’ouvre dans une fenêtre de la taille d’un iPhone (390×844 points) sur votre Mac. C’est le moyen le plus rapide de voir une application comme l’affiche un téléphone : aucun simulateur à démarrer. Elle utilise UIKit comme iOS, donc ce que vous voyez est ce qu’affiche un iPhone, hormis les comportements propres au Mac comme le défilement à la molette. Pour l’application Mac elle-même, utilisez --target macos.

#La modifier

Laissez nsh run en cours d’exécution et changez le libellé du bouton :

text("Add one")
    .text_size(px(17.0))
    .text_color(move || theme().primary_foreground)

Enregistrez. Une application Rust est recompilée et redémarre avec le nouveau libellé ; le compteur repart de zéro. Une application TSX reçoit une mise à jour à chaud : la CLI recompile et envoie un patch, et l’hôte réexécute le module modifié et ne re-rend que les composants dont le code a changé. Ici, c’est App, qui possède le signal count, donc le compteur repart lui aussi de zéro.

Des morceaux d’UI peuvent passer dans des composants à part, ou dans de simples fonctions qui renvoient des éléments :

/// 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)))

En TSX, l’état survit quand vous modifiez un composant qui ne le possède pas : appuyez plusieurs fois, puis changez la couleur de TapButton, et le bouton se met à jour tandis que le compteur reste. Le remplacement à chaud fonctionne par composant de premier niveau : une fonction dont le nom commence par une majuscule et dont le corps contient du JSX. Un composant est re-rendu quand son propre code change, ou quand le code du module situé en dehors des composants change (une constante qu’il utilise, un helper). Si une modification ne peut pas être appliquée en place (par exemple, elle importe un module NSH que l’application en cours n’avait pas encore importé, ou elle change les capacités de nsh.json), l’hôte recharge toute l’application.

#Erreurs

Une erreur de compilation Rust est affichée dans le terminal avec la sortie de cargo, et nsh run recompile à la sauvegarde suivante. Une erreur de build TSX est affichée dans le terminal et montrée sur l’appareil sous forme d’overlay rouge indiquant le fichier, la ligne et la colonne ; l’application continue d’exécuter le dernier build valide. L’overlay disparaît au build réussi suivant (touchez-le pour le fermer plus tôt). Une exception levée à l’exécution est montrée de la même façon, avec sa pile ramenée à votre source .tsx.

#L’héberger dans votre application

Une application native affiche NSH dans une vue qui lui est propre : NshAppView (un UIView) sur iOS, NshView (un View Android) sur Android, AppView sur macOS, Windows, Linux et dans les applications GPUI. Placez-la comme n’importe quelle autre vue, sur n’importe quel écran, autant de fois que nécessaire.

Une racine Rust fait partie du build de l’application : l’application lie votre crate et monte la racine avec AppSource::Rust sur le AppView de macOS, Windows, Linux, GPUI et iOS (Développer avec Rust). Une application TSX est un module que la vue charge : pendant le développement depuis le serveur de dev, en release depuis un bundle compilé ou un package signé téléchargé. Les sections ci-dessous chargent des modules TSX.

#iOS

Liez NativeShellDev.xcframework en Debug et NativeShell.xcframework en Release (Intégration dans les applications iOS), puis :

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 sert le module à l’application et pousse chaque sauvegarde, comme le fait nsh run avec son propre hôte. Pour le build de release, nsh build my-app --release --out MyApp/nsh écrit le bundle dans un dossier que vous ajoutez aux ressources de l’application.

#Android

Ajoutez nsh-dev.aar pour les builds debug et nsh.aar pour les builds release (Intégration dans les applications Android), puis :

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 et Linux

Les applications de bureau écrites en Rust intègrent NSH avec AppView, depuis les crates nsh-platform-macos, nsh-platform-windows, nsh-platform-linux et nsh-platform-gpui : une NSView, une fenêtre enfant, un widget GTK ou une vue GPUI qui monte une racine Rust ou charge un bundle compilé. Voir macOS, Windows, Linux et GPUI, ainsi que Plateformes pour tous les hôtes côte à côte.

#Ce que le module peut faire

Sur iOS et Android, l’hôte décide avant le chargement de ce qu’un module peut faire : grantCapabilities définit les hôtes réseau et les autres capacités auxquels il peut accéder, et des limites plafonnent sa mémoire, son temps d’appel et son nombre de nœuds (Sandbox et capacités). Pour donner à vos modules vos propres vues natives (une carte, un graphique, un composant de design system), enregistrez-les comme composants natifs.

#Publier des mises à jour sous forme de packages signés

Un module TSX que l’application télécharge après la publication prend la forme d’un package signé. Créez une clé racine une fois et gardez-la hors ligne, certifiez une clé de release avec elle, puis publiez :

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

L’application fait confiance à la clé publique de la racine et charge ce qu’elle télécharge ; NSH vérifie la signature, le certificat et la liste de révocation avant d’exécuter quoi que ce soit :

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 suit le même flux avec NshTrust et NshPackage. Hébergement, distribution et révocation en détail : Packages signés.

#Ou le livrer comme application autonome

nsh package construit une application installable, en Rust ou en TSX, sans projet Xcode ni build Gradle à maintenir de votre côté :

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

Une application Rust est empaquetée avec son propre runner, construit à partir de votre crate et des crates de NSH, avec sa racine montée ; une application TSX l’est avec le runtime précompilé. Options, signature et icônes : Packaging.

#Essayer dans le terminal

nsh test exécute une application en mode headless, sans appareil ni fenêtre. À partir du nsh.json d’une application Rust ou du point d’entrée d’une application TSX, il ouvre l’application, la laisse s’exécuter et affiche ce qu’elle monte ; avec --trace, il affiche aussi les opérations de vues natives et la durée de chaque 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

À partir d’un répertoire, il exécute les tests de l’application : le App.test.tsx de chaque modèle ouvre l’application, appuie sur son bouton et vérifie ce qu’elle affiche (nsh test my-app). Celui d’une application Rust ouvre sa racine (open(".")) ; elle peut aussi être testée dans cargo test avec nsh_testing::Headless (Building with Rust).

#Étapes suivantes