Documentação
Compatibilidade retroativa
Apps host baixam apps NSH como pacotes assinados e os executam como plugins, e um app que foi publicado talvez nunca mais seja compilado de novo: a equipe seguiu em frente, o código-fonte está preso em um processo de negócio e o app host atualizou o NSH no próprio ritmo. Por isso o Native Shell mantém uma promessa a todo app: um app construído para uma versão compatível roda em todas as versões posteriores, sem alterações. Um app que para de funcionar após uma atualização do NSH é um bug do NSH.
Esta página descreve a promessa, como ela é garantida e como mudar o NSH dentro dela.
#No que um app pode confiar
A promessa cobre tudo o que um app já construído usa:
| Parte | Por que importa |
|---|---|
Os exports de cada módulo (nsh, nsh/*, nsh/base, os módulos padrão) e seus membros |
Um bundle os importa pelo nome: se um nome sumir, o app não inicia. |
nsh:rt, as funções que o JSX compilado chama |
Cada bundle carrega a saída do compilador que o construiu. |
Os globais (timers, fetch, console, …) e seus membros |
Os apps os chamam pelo nome. |
| Props de elementos, eventos e seus payloads, chaves de estilo e seus valores | Os apps os passam pelo nome; uma prop renomeada é ignorada, e o app passa a se comportar de forma errada. |
| Utilitários e variantes do Tailwind | As classes são resolvidas em tempo de execução a partir das strings no bundle. |
| Comportamento: padrões, unidades, payloads, a ordem dos callbacks | Um app depende do que viu quando foi escrito. |
As declarações TypeScript (.nsh/types) |
O código-fonte de um app antigo continua passando na checagem de tipos quando a equipe corrige um bug. |
| O formato do pacote e suas assinaturas | Um pacote assinado para uma versão anterior continua sendo verificado. |
As regras que sustentam a promessa:
-
O que uma versão publicou permanece. Nenhum módulo, export, membro, prop, evento, chave de payload, chave ou valor de estilo, utilitário Tailwind, global ou função de
nsh:rté removido. -
Uma renomeação mantém o nome antigo como alias da mesma implementação, marcado com
@deprecatedapontando para o novo nome, para sempre. Mover um export para outro módulo também o mantém no módulo antigo. -
Os tipos só se ampliam. Uma união ganha alternativas, uma função ganha overloads ou parâmetros opcionais no final, um parâmetro se torna opcional; o que os apps passam (props, estilos, opções) ganha apenas membros opcionais. Os valores que um app lê crescem da mesma forma:
platform.osganha uma plataforma, um estado ou um motivo ganha um caso. Um caso que o app já viu mantém seu valor, e o app trata um valor que não conhece, com odefaultde umswitch:switch (platform.os) { case "ios": case "android": return <MobileLayout />; default: // The desktops, the web, and platforms NSH adds later. return <WideLayout />; } -
O comportamento é mantido. O novo comportamento é opt-in, por meio de uma nova prop, opção ou valor.
-
nsh:rtsó cresce, e cada função mantém sua assinatura e seu significado. -
Os formatos são lidos para sempre. Pacotes, certificados, listas de revogação e
app.jsonde versões anteriores são lidos pelas posteriores; um pacote informa o runtime mais antigo de que precisa (nsh publish --min-runtime). -
A segurança é a única exceção. Um recurso que não pode ser mantido seguro é fechado, listado com seu motivo e citado nas notas de versão.
A promessa começa na primeira versão registrada como compatível e vale para sempre, seja qual for o número de versão.
Entre versões, uma API que apenas main possui ainda pode mudar.
#Como a promessa é garantida
O que as versões congelaram fica em um repositório próprio, longbridge/nsh-compat, separado do código do NSH. Uma mudança no NSH é executada contra ele e não pode editá-lo: um pull request que renomeia uma API não pode também reescrever o registro dessa API.
nsh nsh-compat
├─ crates/ the code ├─ releases/v<version>/ one per compatible release, never changed
└─ tests/compat/ apps that test the │ ├─ runtime.txt every export, global and nsh:rt function
├─ cases/ API being written │ ├─ types/ the declarations apps build against
└─ examples/ │ ├─ tailwind.txt every Tailwind class and what it sets
│ └─ apps/ the test apps as that release built them
├─ closed.txt security closures, each with its reason
└─ src/ the checks
Todo pull request para o NSH executa as verificações de nsh-compat sobre o seu checkout (o workflow Compat, ou
cargo xtask compat localmente):
| Verificação | O que passa |
|---|---|
checkout_apps_run |
Os apps de tests/compat/, construídos por este checkout, fazem o que seus scripts dizem, e toda parte da API que um script afirma verificar é realmente verificada. |
nothing_released_is_removed |
Tudo o que cada versão registrada oferecia (exports, membros e seus tipos, declarações, classes Tailwind e o que elas definem) continua sendo oferecido, igual ou ampliado. |
released_apps_run |
Os apps que cada versão registrada construiu, como o compilador dela os construiu, iniciam neste runtime e fazem o que seus scripts dizem. |
A primeira verificação testa a API que está sendo escrita; as outras duas protegem todas as versões que foram registradas. A renomeação de uma API já publicada falha assim:
v0.2.0: runtime [nsh] Platform: removed
Keep it: a rename keeps the old name as a @deprecated alias of the new one, a type only widens.
nsh-compat é revisado pelos mantenedores do NSH, e seu próprio CI o executa contra o main do NSH.
#O fluxo de versão
- Desenvolver. Uma API chega em
maincom um caso emtests/compat/cujo script a verifica. Até que uma versão a registre, ela pode ser renomeada ou remodelada; o caso muda junto. - Registrar. Em uma versão,
cargo xtask compat record <version>gravareleases/v<version>/em um checkout densh-compat: a superfície que a versão oferece e todos os apps detests/compat/construídos pelo seu compilador, com seus scripts. Ele registra apenas uma API cujas partes são todas verificadas por um script. - Congelar. O registro é incorporado por meio de um pull request de
nsh-compat. A partir daí, toda mudança no NSH é cobrada contra ele, junto com todos os registros anteriores.
#Desenvolvendo dentro da promessa
#Adicionando uma API
Todo novo export, global, prop, evento ou chave de estilo vem com um caso:
- Escreva um módulo
tests/compat/cases/<domain>/<case>.tsxcujo export padrão seja um componente que use a API e mostre o resultado. Os domínios sãobase,core,events,layout,paint,props,moduleseweb, além deandroid,macosebrowserpara o que é decidido por uma plataforma ou por uma configuração densh.json. - Adicione-o ao
App.tsxdo domínio, que mostra um caso por vez. - Acrescente os passos ao
script.txtdo domínio:tap id:case-<case>, depois os passos que verificam o que a API faz, e um passocovers <item>para cada parte que ela adiciona. - Execute
cargo xtask compat(NSH_COMPAT_APP=<domain>executa um domínio).
# props-media-video
tap id:case-props-media-video
prop id:clip src https://example.com/clip.mp4
prop id:clip paused true
tap id:play
prop id:clip paused false
covers [props] src
covers [props] paused
Um passo covers é verificado: o app precisa usar o item e, no caso de uma prop, evento ou chave de estilo, removê-lo do
app construído deve fazer um passo do script falhar. Os itens são nomeados como [<module>] <export>, [globals] <name>,
[props] <name>, [events] <name>, [styles] <name>, os próprios de um componente como [props] <Component>.<name>, e as props
de uma plataforma como ios.<name>, android.<name>, desktop.<name>, web.<name>.
#Renomeando ou movendo uma API
Adicione o novo nome e mantenha o antigo como alias da mesma implementação, declarado como @deprecated com o novo
nome. Atualize o caso e seu script para o novo nome. Uma API que nenhuma versão registrou ainda pode simplesmente ser renomeada.
#Alterando um tipo
Amplie-o: mais alternativas em uma união, mais overloads, parâmetros opcionais no final, novos membros opcionais no que os apps passam. Uma união que os apps leem ganha membros da mesma forma, cada novo membro para um novo caso. Os nomes dos parâmetros podem mudar livremente; sua quantidade, opcionalidade e tipos permanecem.
#Alterando comportamento
Mantenha o comportamento antigo e adicione o novo por trás de uma nova prop, opção ou valor. Uma correção de bug que muda o que um app em funcionamento vê também é uma mudança de comportamento: coloque-a por trás de um opt-in ou garanta que nenhum app possa depender do resultado antigo. Dê ao comportamento um passo em um caso, para que ele permaneça como está.
#Alterando o compilador
Nada extra: os apps de cada versão permanecem como o compilador daquela versão os construiu, e os casos são construídos do zero pelo
seu. As funções que ele emite nos bundles (nsh:rt) só crescem.
#Fechando uma API por segurança
Um pull request de nsh-compat adiciona a entrada ao seu closed.txt com o motivo (<entry> -- <reason>, a entrada
como a verificação a reporta) e é integrado primeiro; depois disso, a mudança no NSH que remove a API passa. As notas de versão explicam
o porquê.
#Scripts
Um script.txt tem um passo por linha. Passos que agem: tap <selector>, type <selector> <text>,
key <selector> <key>, scroll <selector> <x> <y>, touch start|move|end|cancel, event <selector> <onEvent> [<key>=<value>...], wait <ms>, keyboard <height> <duration>, pixelratio <ratio>. Passos que verificam:
expect <text>, absent <text>, frame <selector> <x> <y> <width> <height>, origin, prop / noprop,
style / nostyle, listens, state, statusbar, log <text>. As primeiras linhas podem definir a tela: viewport <w> <h>,
platform <os> [desktop], fontscale <scale>. Um seletor é id:<testID>, label:<label>, text:<text> ou
span:<text>; props e estilos são escritos como os apps os escrevem (style id:card backgroundColor #3366ffff).
NSH_COMPAT_DUMP=1 imprime o que cada passo adiciona à tela e remove dela, para ajudar a escrever um script.
#Comandos
cargo xtask compat # the checks against this checkout
NSH_COMPAT_APP=props cargo xtask compat -v
cargo xtask compat record 0.2.0 # at a release: record it in nsh-compat, then open a pull request there
cargo xtask compat mantém um checkout de nsh-compat em target/nsh-compat/ e o atualiza a cada execução;
um checkout com alterações próprias é usado como está, de modo que uma mudança nas verificações possa ser testada contra o NSH antes do respectivo
pull request.