Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Primeiros passos

Este guia cria um app NSH, o executa nas plataformas que você escolher como alvo e o publica. Apps NSH são escritos em Rust ou TSX, sobre o mesmo engine: Rust compila seu app para código nativo, sem VM de JavaScript, e TSX roda no QuickJS com hot updates, como módulos que um app nativo pode carregar em tempo de execução. O guia segue primeiro um app em Rust e depois o mesmo app em TSX. Ele pressupõe que nsh esteja instalado e no seu PATH (Instalação); em um checkout, use ./target/debug/nsh no lugar.

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

#Criar um app

Em um terminal, nsh init pergunta em que o app é escrito: rust, ts ou mixed (TSX com um crate Rust próprio). --template escolhe sem perguntar.

#Em Rust

nsh init my-app --template rust

nsh init cria o diretório e escreve um projeto Cargo:

Arquivo O que é
src/lib.rs O app: seu componente raiz, estado e handlers
src/main.rs Monta a mesma raiz no macOS, Windows ou Linux para cargo run
Cargo.toml O crate, que depende de nsh-core e nsh-base na versão da CLI
nsh.json O nome, o id, a versão e o número de build do app, seu crate ("rust": ".") e o componente raiz ("root": "App") (Estrutura do projeto)
App.test.tsx O teste da raiz, em TypeScript (nsh test), com tsconfig.json e .nsh/types para o seu editor
README.md Como executar, testar e empacotar o app, e como adicionar uma entrada TSX depois
.gitignore Ignora .nsh/ e target/

O desenvolvimento em Rust exige uma toolchain Rust (rustup); os comandos nsh compilam os runners do app a partir dos crates do NSH no crates.io, na versão da CLI.

#Em TSX

nsh init my-app --template ts

Um app TSX é composto por um ou mais arquivos .tsx com uma entrada: o que nsh run executa e o que um app host carrega como módulo.

Arquivo O que é
App.tsx O módulo de entrada: renderiza o app com mount
App.test.tsx Seu teste (nsh test)
nsh.json O nome, o id, a versão e o número de build do app (Estrutura do projeto)
tsconfig.json Estende .nsh/tsconfig.json, as configurações de TypeScript do NSH, que resolvem nsh para as declarações em .nsh/types
.gitignore Ignora .nsh/ e target/
.nsh/types/ Declarações geradas de todos os módulos do NSH, para o seu editor

Arquivos que já existem são mantidos, então você pode executá-lo em um diretório que já tenha alguns deles; um app Rust é criado em um novo diretório.

Em ambos, o nome de exibição usa por padrão o nome do diretório; passe --name "My App" para escolher outro. O id vira com.example.<name>: altere-o em nsh.json antes de publicar. nsh init my-app --template mixed cria um app TSX que também é um crate Rust (Cargo.toml e src/lib.rs ao lado de App.tsx), cujo componente, módulo e classe App.tsx importa (Building with Rust).

#O componente raiz

Os dois templates geram o mesmo app, um 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),
                                ),
                        ),
                ),
        )
}

Em Rust:

  • use nsh_core::prelude::* traz as primitivas (view(), text(), scroll(), input(), image(), …), signals e os métodos de estilo tipados (.p_safe_area() aplica padding a uma view conforme a área segura); nsh_base traz os componentes base sem estilo (button(), o teclado, diálogos) e theme(), as cores e os raios que os componentes do app usam para desenhar (Tema).
  • #[component] pub fn App() é um componente: uma função que roda uma vez e retorna seus elementos. O "root": "App" de nsh.json dá nome a ele, e nsh run o monta. signal(0_u32) cria um valor que muda ao longo do tempo.
  • text(move || format!(…, count.get())) vincula o texto ao signal: quando count muda, o NSH executa esta closure novamente e atualiza apenas este texto, nada mais. Estilos recebem closures da mesma forma: .bg(move || theme().primary) acompanha o tema no modo escuro.
  • .on_press(..) faz a view responder a toques; .active(|s| s.opacity(0.6)) é o estilo dela enquanto pressionada, aplicado nativamente. O botão é do próprio app: o NSH fornece as primitivas e as capacidades, e o app desenha sua UI a partir delas.
  • .px_5(), .rounded(px(10.0)), .gap_4() são métodos de estilo tipados, nomeados como os utilitários do Tailwind, em pontos (px). O layout é flexbox, e os filhos são empilhados em coluna por padrão (Layout).
  • #[native_module("my-app/native")] nomeia o que uma entrada TSX pode importar do crate, para quando você adicionar telas TSX por cima (Building with Rust).

Em TSX:

  • import { mount, signal, Text, View } from "nsh": nsh é o módulo central: signals, mount, as outras primitivas reativas e os elementos host. Ele é servido pelo runtime nativo, não empacotado no bundle. nsh/base traz os componentes base, como nsh_base faz em Rust.
  • function App() é um componente que roda uma vez. {count()} lê o signal dentro do JSX: o compilador vê uma chamada e faz este texto acompanhar o signal.
  • <View onPress> é uma view nativa que reage a toques; as classes active: são o estilo dela enquanto pressionada. View, Text, Image, Scroll e os demais elementos host são importados de nsh (Input de nsh/base); apenas If e Each não precisam de import.
  • class recebe classes do Tailwind; style={{ … }} recebe um objeto com propriedades de estilo em camelCase, com números em pontos. bg-background, bg-primary, text-primary-foreground e rounded-(--radius) nomeiam os tokens do tema do app (Tokens de tema): eles acompanham o modo escuro por conta própria, e "theme" em nsh.json os reestiliza.
  • mount(App) renderiza o componente na superfície do app.

#Desenvolva

#Execute

nsh run compila o app, o inicia e recompila enquanto você edita; os logs do app aparecem no terminal. Passe a ele o diretório do app: ele executa a raiz de um app Rust, ou o App.tsx de um app TSX (nsh run my-app/App.tsx indica outra entrada).

Para um app Rust, nsh run compila o runner próprio do app para o alvo, com seu crate e a raiz montada e sem QuickJS, e o inicia; uma alteração no código Rust o compila de novo e reinicia o app. cargo run no diretório do app inicia a mesma raiz diretamente no backend desktop. Para um app TSX, nsh run inicia um servidor de desenvolvimento e um host de desenvolvimento que carrega o app a partir dele, e aplica patches no app em execução a cada salvamento.

Sem --target, nsh run usa a máquina em que você está: uma janela AppKit no macOS, uma janela Win32 no Windows e uma janela GTK no Linux. Escolha outra plataforma com --target:

Alvo Comando Executa a partir de
Simulador de iOS nsh run my-app --target ios macOS com Xcode
Android nsh run my-app --target android macOS, Windows, Linux
macOS (AppKit) nsh run my-app macOS (o padrão)
Windows nsh run my-app Windows (o padrão)
Linux (GTK 4) nsh run my-app Linux (o padrão)
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 um simulador já iniciado, ou inicia o iPhone mais recente se nenhum estiver em execução, e instala o app nele (o runner de um app Rust, ou o host de desenvolvimento). --device escolhe um simulador pelo nome ou pelo UDID.

#Android

nsh run my-app --target android

Inicie um emulador ou conecte um dispositivo primeiro (adb devices os lista; o NSH usa o primeiro). nsh instala o app nele: o libnsh.so próprio de um app Rust, ou um APK de host de desenvolvimento (recompilado apenas quando o runtime muda) cuja porta do servidor de desenvolvimento é encaminhada ao dispositivo com adb reverse, de modo que funciona por USB sem uma rede compartilhada.

#macOS (AppKit)

nsh run my-app --target macos

O app abre como um app de desktop para Mac (AppKit, em uma janela redimensionável), com entrada de mouse, trackpad e teclado em vez de toques. Exige macOS 14 ou posterior; veja macOS.

#Windows (padrão no Windows)

nsh run my-app

O app abre em uma janela de desktop (1200×800 pontos), com entrada de mouse, touchpad, toque e teclado. Exige Windows 10 1809 ou posterior; veja Windows.

#Linux (padrão no Linux)

nsh run my-app

O app abre em uma janela GTK (390×844 pontos). Exige GTK 4.14 ou posterior; veja Linux.

#Web

nsh run my-app --target web

nsh imprime o endereço da página; abra-o em um navegador. Um app Rust roda como WebAssembly com seu crate, e a página aberta carrega a nova build sozinha após uma alteração; veja Web.

#Mac Catalyst

nsh run my-app --target catalyst

O app abre em uma janela do tamanho de um iPhone (390×844 pontos) no seu Mac. Esta é a forma mais rápida de ver um app como um celular o mostra: sem simulador para iniciar. Ele usa UIKit como o iOS, então o que você vê é o que um iPhone mostra, exceto por comportamentos específicos do Mac, como a rolagem com a roda do mouse. Para o app Mac em si, use --target macos.

#Edite

Deixe nsh run em execução e altere o rótulo do botão:

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

Salve. Um app Rust é compilado de novo e reinicia com o novo rótulo; a contagem recomeça. Um app TSX recebe um hot update: a CLI recompila e envia um patch, e o host executa novamente o módulo alterado e renderiza de novo apenas os componentes cujo código mudou. Aqui isso é App, que é dono do signal count, então a contagem também recomeça.

Trechos de UI viram componentes próprios, ou funções simples que retornam 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)))

Em TSX, o estado sobrevive quando você edita um componente que não é dono dele: toque algumas vezes e depois altere a cor de TapButton , e o botão é atualizado enquanto a contagem permanece. A substituição a quente funciona por componente de nível superior: uma função com nome em maiúscula cujo corpo contém JSX. Um componente é renderizado de novo quando seu próprio código muda, ou quando o código do módulo fora dos componentes muda (uma constante que ele usa, um helper). Se uma edição não puder ser aplicada no lugar (por exemplo, ela importa um módulo do NSH que o app em execução ainda não importou, ou altera as capacidades de nsh.json), o host recarrega o app inteiro.

#Erros

Um erro de compilação em Rust é impresso no terminal com a saída do cargo, e nsh run compila de novo no próximo salvamento. Um erro de build em TSX é impresso no terminal e exibido no dispositivo como uma sobreposição vermelha com o arquivo, a linha e a coluna; o app continua executando a última build boa. A sobreposição desaparece na próxima build bem-sucedida (toque nela para dispensá-la antes). Uma exceção lançada em tempo de execução é exibida da mesma forma, com a stack mapeada de volta para o seu código-fonte .tsx.

#Hospede no seu app

Um app nativo exibe o NSH em uma view própria: NshAppView (uma UIView) no iOS, NshView (uma View do Android) no Android, AppView no macOS, Windows, Linux e em apps GPUI. Posicione-a como qualquer outra view, em qualquer tela, quantas vezes precisar.

Uma raiz Rust faz parte da build do app: o app linka seu crate e monta a raiz com AppSource::Rust no AppView de macOS, Windows, Linux, GPUI e iOS (Building with Rust). Um app TSX é um módulo que a view carrega: durante o desenvolvimento, a partir do servidor de desenvolvimento; em release, a partir de um bundle compilado ou de um pacote assinado baixado. As seções abaixo carregam módulos TSX.

#iOS

Linke NativeShellDev.xcframework em Debug e NativeShell.xcframework em Release (Incorporação em apps iOS), depois:

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 serve o módulo ao app e envia cada salvamento, como nsh run faz com seu próprio host. Para a build de release, nsh build my-app --release --out MyApp/nsh grava o bundle em uma pasta que você adiciona aos recursos do app.

#Android

Adicione nsh-dev.aar para builds de debug e nsh.aar para builds de release (Incorporação em apps Android), depois:

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

Apps desktop escritos em Rust incorporam o NSH com AppView, a partir dos crates nsh-platform-macos, nsh-platform-windows, nsh-platform-linux e nsh-platform-gpui: uma NSView, uma janela filha, um widget GTK ou uma view GPUI que monta uma raiz Rust ou carrega um bundle compilado. Veja macOS, Windows, Linux e GPUI, e Plataformas para todos os hosts lado a lado.

#O que o módulo pode fazer

No iOS e no Android, o host decide antes do carregamento o que um módulo pode fazer: grantCapabilities define os hosts de rede e outras capacidades que ele pode acessar, e limites restringem sua memória, tempo de chamada e quantidade de nós (Sandbox e capacidades). Para oferecer aos módulos suas próprias views nativas (um mapa, um gráfico, um componente de design system), registre-as como componentes nativos.

#Distribua atualizações como pacotes assinados

Um módulo TSX que o app baixa após o lançamento vem como um pacote assinado. Crie uma chave raiz uma única vez e mantenha-a offline, certifique uma chave de release com ela e publique:

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

O app confia na chave pública da raiz e carrega o que baixa; o NSH verifica a assinatura, o certificado e a lista de revogação antes de executar qualquer coisa:

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)

O Android tem o mesmo fluxo com NshTrust e NshPackage. Hospedagem, entrega e revogação em detalhe: Pacotes assinados.

#Ou distribua como um app standalone

nsh package gera um app instalável, em Rust ou TSX, sem nenhum projeto Xcode ou build Gradle próprio:

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

Um app Rust é empacotado com seu próprio runner, construído a partir do seu crate e dos crates do NSH, com a raiz montada; um app TSX, com o runtime pré-compilado. Opções, assinatura e ícones: Packaging.

#Experimente no terminal

nsh test executa um app em modo headless, sem dispositivo nem janela. Dado o nsh.json de um app Rust ou a entrada de um app TSX, ele abre o app, deixa-o rodar e imprime o que ele monta; com --trace, imprime também as operações de view nativas e o tempo 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 um diretório, ele executa os testes do app: o App.test.tsx de cada template abre o app, pressiona seu botão e verifica o que ele exibe (nsh test my-app). Um app Rust abre sua raiz (open(".")); ele também pode ser testado em cargo test com nsh_testing::Headless (Building with Rust).

#Próximos passos