Modo C — transpile (Python → JavaScript nativo) 🚀¶
O que você vai aprender
Como transformar o seu app.py Python num bundle estático de JavaScript
nativo — sem runtime Python no browser — e servi-lo por qualquer CDN, já
com PWA instalável e offline de fábrica.
Os Modos A (WASM) e B (servidor) mantêm o Python vivo em tempo de execução — no browser (Pyodide) ou no servidor. O Modo C faz diferente: um compilador transcreve a camada de app do seu Python tipado para JavaScript nativo. Zero runtime Python, hospedagem estática, first-paint e SEO ótimos. É a "história do TypeScript" para Python. 🚀
Modo de primeira classe
O Modo C é maduro e de primeira classe — cobre 100% dos widgets do
tempest_core, um subset amplo de Python tipado (veja
O subset suportado) e uma história completa de PWA
(instalável, offline, fila de mutações, WebPush ponta a ponta). É a escolha
recomendada para sites e PWAs públicos. Só um punhado de construtos avançados
fica de fora do subset — o compilador falha cedo, com arquivo:linha, quando
você encosta neles.
Por que existe¶
| Modo A (WASM) | Modo B (servidor) | Modo C (transpile) | |
|---|---|---|---|
| Runtime Python | browser (~6 MB Pyodide) | servidor vivo | nenhum |
| First paint / SEO | ruim | bom | ótimo |
| Hospedagem | estática | servidor + WS/cliente | estática |
| Custo de escala | zero servidor | stateful por cliente | zero servidor |
O segredo: o cliente JS (dom.js, style.js, events.js) já é nativo e
compartilhado pelos três modos. O Modo C não transpila Python inteiro — só a
camada de app; todo o renderizador continua sendo o mesmo JS.
Seu primeiro build¶
Pegue o app do contador (o mesmo que roda nos Modos A/B, sem mudar uma linha):
from dataclasses import dataclass
from tempest_core import App, Button, Column, Row, Style, Text, Widget
from tempest_core import Edge
@dataclass
class CounterState:
value: int = 0
def make_state() -> CounterState:
return CounterState()
def view(app: App[CounterState]) -> Widget:
def increment() -> None:
app.set_state(lambda s: setattr(s, "value", s.value + 1))
def decrement() -> None:
app.set_state(lambda s: setattr(s, "value", s.value - 1))
return Column(
style=Style(gap=8.0, padding=Edge.all(16)),
children=[
Text(content=f"Count: {app.state.value}", key="label"),
Row(
style=Style(gap=4.0),
children=[
Button(label="-", on_click=decrement, key="dec"),
Button(label="+", on_click=increment, key="inc"),
],
),
],
)
Gere o bundle estático:
Isso escreve um diretório dist/transpile/ totalmente estático — sem Python:
dist/transpile/
├── index.html # monta o app via mountApp
└── client/
├── tempestweb.js dom.js style.js events.js … # o cliente compartilhado
└── transpile/
├── app.gen.js # o seu app.py transcrito para JS nativo
├── runtime.js widgets.js diff.js
└── widget-styles.gen.js # estilos MD3 resolvidos do core
Sirva com qualquer host estático (ou localmente):
Durante o desenvolvimento, use o loop com livereload — edite o app.py e o
browser recarrega com o bundle recompilado:
O que aconteceu
Seu view() virou app.gen.js — JavaScript nativo. O runtime segura o
estado, roda view(), faz o diff em JS e aplica patches granulares ao
DOM. Nenhum Python é baixado ou executado no browser.
O que o compilador emite¶
O app.py acima vira, essencialmente:
import { State } from "./runtime.js";
import { Button, Column, Edge, Row, Style, Text } from "./widgets.js";
export class CounterState extends State {
constructor() {
super();
this.value = 0;
}
}
export function makeState() {
return new CounterState();
}
export function view(app) {
const increment = () => {
app.setState((s) => {
s.value = (s.value + 1);
});
};
// …
return Column({
style: Style({ gap: 8.0, padding: Edge.all(16) }),
children: [
Text({ content: `Count: ${app.state.value}`, key: "label" }),
// …
],
});
}
Convenções de nome
O compilador traduz a API para o JS idiomático: make_state → makeState,
set_state → setState, on_click → onClick, color_scheme →
colorScheme. setattr(s, "x", v) vira s.x = v; f-strings viram template
literals.
Estado com métodos¶
Você não precisa se limitar a lambdas setattr. Um @dataclass com métodos
transpila para uma classe JS — self vira this:
@dataclass
class Counter:
value: int = 0
def increment(self) -> None:
self.value += 1
def view(app: App[Counter]) -> Widget:
def inc() -> None:
app.set_state(lambda s: s.increment())
return Button(label="+", on_click=inc, key="inc")
Campos de formulário reativos¶
Input resolve o estilo Material 3 e conecta o on_change. O binding é de duas
vias: digitar dispara o handler, que atualiza o estado e re-renderiza.
from tempest_core import App, Column, Style, Text, Widget
from tempest_core import Edge
from tempest_core import Input
@dataclass
class FormState:
name: str = ""
def view(app: App[FormState]) -> Widget:
def on_name(event) -> None:
app.set_state(lambda s: setattr(s, "name", event.payload["value"]))
return Column(
style=Style(gap=12.0, padding=Edge.all(24)),
children=[
Text(content=f"Hello, {app.state.name or 'stranger'}!", key="greet"),
Input(value=app.state.name, placeholder="Your name", on_change=on_name, key="name"),
],
)
Digite no campo e a saudação atualiza ao vivo — sem servidor, sem Python. ✨
Capacidades nativas (requests, storage, cookies…)¶
O mesmo native tipado dos Modos A/B funciona no Modo C — chamadas async
transcritas para chamadas JS em processo à glue de browser compartilhada
(fetch, IndexedDB/localStorage, document.cookie). Zero Python, zero rede.
As três formas de import que o Python escreve chegam no mesmo lugar:
from tempestweb import native # o namespace
from tempestweb.native import storage, get_position # grupo e função
from tempestweb.native.geolocation import get_position # o grupo como módulo
Capacidade que o Modo C não tem, dita na hora do build
camera não tem fachada em processo — camera.capture precisa do Modo A
(Pyodide) ou B (servidor). Importá-la em Modo C é erro de compilação com
arquivo:linha dizendo isso, e não uma página que carrega e quebra ao
clicar. A lista viva do que a fachada serve é gerada do próprio
client/transpile/native.js.
from tempestweb import native
@dataclass
class DataState:
body: str = ""
def view(app: App[DataState]) -> Widget:
async def fetch_it() -> None:
res = await native.http.request("GET", "/api/items")
await native.storage.put("last", res.body)
await native.cookies.set("seen", "1")
app.set_state(lambda s: setattr(s, "body", res.body))
return Button(label="fetch", on_click=fetch_it, key="go")
Handlers async
Um handler pode ser async def e usar await. O re-render acontece quando o
set_state roda (depois do await), então a UI reflete o resultado assim que
a capacidade resolve. Capacidades disponíveis no Modo C: http, storage
(IndexedDB/localStorage), clipboard, geolocation, cookies, share,
audio, file, notifications (incl. WebPush subscribe/unsubscribe),
install (prompt de instalação PWA), offline (fila de mutações durável).
Instalar o PWA (native.install)
await native.install.state() informa {can_install, installed}; após um
gesto do usuário, await native.install.prompt() dispara o prompt nativo de
instalação e resolve com "accepted", "dismissed" ou "unavailable". O
controller já suprime o mini-infobar frio do browser, então você mostra um
botão "Instalar" no momento certo.
Push (native.notifications)
await native.notifications.push_state() informa {supported, permission}
sem disparar prompt — use pra decidir mostrar o botão. await
native.notifications.request_permission() pede permissão; await
native.notifications.subscribe(vapid_public_key) roda o fluxo WebPush do
browser e devolve o JSON da assinatura — você o envia ao seu backend
(via native.http ou enfileirado com native.offline). unsubscribe()
cancela. O framework não decide seu schema de endpoint nem o servidor de
push: só entrega a assinatura crua.
Fila offline (native.offline)
Escritas feitas offline sobrevivem: await native.offline.enqueue("POST",
url, body) grava uma mutação durável no IndexedDB (com chave de
idempotência) e o replay acontece em ordem FIFO quando a conexão volta —
via o evento online, via Background Sync (aba fechada) ou explicitamente
com await native.offline.replay(). Inspecione com native.offline.size()
e native.offline.pending(). O servidor deduplica pela chave de
idempotência, então um replay nunca aplica duas vezes.
Validadores de campo
from tempest_core.validators import validate_email, validate_cpf,
validate_cnpj, validate_phone roda client-side no Modo C, com o mesmo
algoritmo e as mesmas mensagens PT-BR do core (port fiel, travado por fixture).
Combina com Input + estado para forms validados sem servidor.
Navegação (rotas + URL)¶
O Modo C fala a mesma navegação dos Modos A/B: app.push(Route(...)),
app.pop(), app.replace(...), app.nav.top — sincronizados com a URL do
browser (deep-link + voltar/avançar) automaticamente.
from tempest_core import App, Button, Column, Route, Text, Widget
def view(app: App[MyState]) -> Widget:
def open_product() -> None:
app.push(Route(name="/products/42"))
route = app.nav.top
return Column(children=[
Text(content=f"rota: {route.name}", key="r"),
Button(label="abrir produto", on_click=open_product, key="p"),
Button(label="voltar", on_click=lambda e: app.pop(), key="b"),
])
URL ↔ stack
app.push/pop empurram/voltam a URL (pushState); um deep-link ou o
botão voltar do browser resetam a stack a partir do path
(routes_from_path) — idêntico aos Modos A/B. Path/query params: o name
da rota carrega o path completo (incl. ?query), como no core; leia os
segmentos por app.nav.stack. Um router com params tipados é evolução no
nível do core.
Localização (i18n)¶
translate / t + Locale do core funcionam no Modo C: busca a chave na tabela
{idioma: {chave: template}} pelo idioma do locale e interpola {name} — mesma
semântica e fallbacks do core (chave/idioma ausente → a própria chave).
from tempest_core import App, Locale, Text, Widget, t
MESSAGES = {
"pt": {"greet": "Olá, {name}!"},
"en": {"greet": "Hello, {name}!"},
}
def view(app: App[MyState]) -> Widget:
loc = Locale(language=app.state.lang)
return Text(content=t("greet", locale=loc, translations=MESSAGES, name="Ana"), key="g")
Troque app.state.lang num handler e a UI re-renderiza no novo idioma —
verificado no Playwright (PT → EN ao vivo). A tabela MESSAGES é uma constante
de módulo (agora suportada no subset).
Tema + responsividade¶
O Modo C expõe app.theme e app.media como nos Modos A/B. app.theme.is_dark()
resolve claro/escuro (DARK/LIGHT absolutos; SYSTEM segue o SO); app.media
carrega width/height/platform_dark_mode/orientation — sincronizado com o
browser (matchMedia + resize) → a UI re-renderiza responsivamente.
from tempest_core import App, Column, Text, Theme, ThemeMode, Widget
def view(app: App[MyState]) -> Widget:
dark = app.theme.is_dark(platform_dark_mode=app.media.platform_dark_mode)
wide = app.media.width >= 600.0
def toggle() -> None:
app.set_theme(Theme(mode=ThemeMode.LIGHT if dark else ThemeMode.DARK))
return Column(children=[
Text(content=("escuro" if dark else "claro"), key="s"),
Text(content=("largo" if wide else "estreito"), key="l"),
])
Responsividade adaptativa
Redimensione a janela ou mude o prefers-color-scheme do SO e o view
re-renderiza — verificado no Playwright (400px→estreito, 900px→largo; toggle
de tema claro↔escuro). Os breakpoints do core (Breakpoints: sm/md/lg/xl)
também estão disponíveis.
Animação (transições)¶
Anime declarativamente: dê ao Style de um widget um Transition e o browser
faz o tween quando um campo estilizado muda (largura, cor, opacidade) — sem
runtime Python, sem driver de frame.
from tempest_core import App, Container, Style, Widget
from tempest_core import Color, Curve, Transition
def view(app: App[MyState]) -> Widget:
w = 320.0 if app.state.big else 120.0
return Container(key="box", style=Style(
width=w,
background=Color(r=103, g=80, b=164, a=1.0),
transition=Transition(duration_ms=400, curve=Curve.EASE_IN_OUT),
))
Verificado
Trocar app.state.big num handler anima a largura de 120→320px em 400ms
(Playwright confirmou a transição CSS aplicada). Curvas: linear, ease,
ease-in, ease-out, ease-in-out, bounce, elastic.
Animação imperativa (AnimationController)¶
Para controle por frame, use AnimationController + Tween — o runtime dirige
os controllers num loop requestAnimationFrame, computando o valor a cada frame
e re-renderizando.
from tempest_core import AnimationController, Tween
from tempest_core import Curve
def make_state() -> S:
s = S()
s.anim = AnimationController(0.6, curve=Curve.EASE_OUT)
return s
def view(app: App[S]) -> Widget:
w = Tween(begin=100.0, end=340.0).at(app.state.anim.value)
def go() -> None:
app.state.anim.forward()
app.register_animation(app.state.anim)
return Container(key="box", style=Style(width=w))
forward()/reverse()/stop(), curvas eased e springs (Spring) — mesma
matemática do core. Verificado no Playwright: a largura anima 100→340 (ease-out)
e assenta. Isto fecha 100% da cobertura do tempest-core no Modo C.
O tour completo¶
Tudo acima — estado com métodos, navegação, i18n, tema + responsividade, um
formulário validado e uma animação imperativa — vive junto num único app de
referência, examples/transpile-tour:
tempestweb build --mode transpile --path examples/transpile-tour
tempestweb dev --mode transpile --path examples/transpile-tour # livereload
Um só view, todos os modos
O mesmo view() do tour roda inalterado nos Modos A e B. O build valida
isso renderizando pelo core real — uma API que só existisse no Modo C
quebraria o build, então o tour é prova viva de portabilidade.
PWA: instalável e offline¶
Comece uma PWA em um comando
Gera um projeto Modo C já configurado (mode = "transpile" + bloco [pwa])
com um contador e um botão Install — pronto pra tempestweb build
--mode transpile.
Você já tem um bundle 100% estático e sem Python — o alvo perfeito para uma
PWA. Por isso o build --mode transpile já emite a camada PWA inteira sozinho:
o usuário pode instalar seu app na tela inicial e, depois da primeira visita,
abri-lo offline. Sem passo extra, sem configurar nada. 🚀
Basta o build de sempre:
Além do bundle do app, o Modo C agora escreve a camada PWA junto:
dist/transpile/
├── index.html # linka o manifest, theme-color, apple-touch-icon
│ # e registra o service worker
├── manifest.webmanifest # metadados de instalação (nome, ícones, cores)
├── sw.js # service worker cache-first (app shell)
├── register.js # registra o sw.js no carregamento
├── icons/ # o conjunto de ícones (maskable + apple-touch)
└── client/ … # o cliente compartilhado + o seu app.gen.js
O sw.js pré-cacheia o bundle estático inteiro — index.html, o cliente
compartilhado, client/transpile/* (incl. o seu app.gen.js), a árvore nativa,
os ícones e o manifest. Depois da primeira carga, o app abre e roda sem rede.
Offline de verdade ✅
Isso não é offline "meia-boca": com o servidor HTTP desligado, recarregar a página ainda renderiza o tour e a navegação continua funcionando — verificado ao vivo no Playwright (servidor morto, reload, tour intacto). Como o Modo C é um bundle estático sem Python, não há nada que dependa do servidor depois do primeiro fetch.
Configurando o manifest com [pwa]¶
Os metadados de instalação vêm de uma seção opcional [pwa] no seu
tempestweb.toml. Todos os campos são opcionais — sem a seção, o build usa
padrões sensatos derivados do nome do projeto:
| Campo | Tipo | Padrão | O que faz |
|---|---|---|---|
enabled |
booleano | true |
O default que manifest e service_worker seguem. |
manifest |
booleano | enabled |
Emite manifest.webmanifest e o <link> dele. |
service_worker |
booleano | enabled |
Emite e registra o worker cache-first. |
name |
string | nome do projeto | Nome completo exibido na instalação/splash. |
short_name |
string | — | Nome curto para o ícone da tela inicial. |
description |
string | — | Descrição do app no prompt de instalação. |
theme_color |
string | "#111111" |
Cor do tema (barra do navegador + <meta name="theme-color">). |
background_color |
string | "#ffffff" |
Cor de fundo da splash de abertura. |
display |
string | "standalone" |
Modo de exibição: standalone, fullscreen ou minimal-ui. |
orientation |
string | — | Orientação preferida (ex.: portrait, landscape). |
lang |
string | "pt-BR" |
Idioma primário do app. |
categories |
lista de string | — | Categorias da app store (ex.: ["productivity"]). |
Valor de display válido
display aceita apenas "standalone", "fullscreen" ou "minimal-ui". Um
valor fora dessa lista é erro de build — falha cedo, no espírito do resto
do compilador do Modo C.
Automático no Modo C
Você não precisa escrever service worker, manifest nem código de registro à
mão: o build --mode transpile gera tudo. A seção [pwa] ajusta os
metadados de instalação — o comportamento offline vem de graça porque o bundle
é estático.
App que não quer PWA
enabled = false desliga o layer inteiro; manifest e service_worker
desligam uma metade só. Desligar o worker emite um worker de teardown no
lugar do de cache, para quem já registrou não ficar preso ao precache antigo
— o porquê está em PWA e offline.
Prompt de atualização
Quando você publica uma versão nova, o service worker antigo continua no ar até a aba fechar. O shell detecta o worker em espera e mostra um banner discreto "nova versão disponível → Atualizar"; ao confirmar, o worker novo assume e a página recarrega uma vez. Automático — nada a escrever no app.
O subset suportado¶
O Modo C aceita um subset tipado de Python — o suficiente para a camada de
app. Um construto fora dele vira erro de compilação claro (arquivo:linha),
no espírito do mypy --strict.
Dentro do subset hoje
- Expressões: aritmética (
+ - * / % ** //), comparação (== != < <= > >=, encadeadaa < b < c), booleanos (and/or), unários (not/-), ternário (a if c else b), comprehensions de lista e de dict (inclusive alvo em tuplafor k, v in …), literaislist/tuple/set/dict,in/not in, indexação e slices (x[a:b]), f-strings (formatos{x:.2f},{x:,},{x:,.2f},{x:.1%},{x:d}; conversões{x!s},{x!r}), lambdas de expressão. - Builtins:
len,str/int/float/bool,abs,round(x[, n]),min/max(variádico ou sobre um iterável),sum(it),range(...),enumerate(it),zip(a, b). - Métodos stdlib: string/list (
.upper/.lower/.strip/.startswith/.endswith/.append), views de dict (.items/.keys/.values→Object.entries/keys/values),sep.join(it). Métodos de objetos do runtime (app.replace,native.storage.get,ctrl.forward) passam intactos — use subscritod[k]no lugar dedict.get. - Statements:
if/elif/else,for … in(com alvo em tupla),while,break/continue,try/except/finally(umexceptpega tudo; vários fazem dispatch por nome da classe de exceção),with … as x(protocolo__enter__/__exit__),raise Exc("msg")/raise(re-raise dentro deexcept),assert cond[, msg], atribuição (inclusive unpackinga, b = pare encadeadaa = b = x),+=e afins,return. - Estruturas:
@dataclassde estado (campos + métodos), herança de dataclass (class B(A)→extends),make_state(),view()com closures de handler. - Componentes de layout:
HStack/VStack(aliases ergonômicos estilo SwiftUI) —gappor token ("md") ou px,align/justifydiretos. - Widgets: todos os ~64 widgets do
tempest_core— layout (Column,Row,Container,Stack,Wrap,ScrollView,SafeArea,Spacer), exibição (Text,Icon,Image,Svg,Spinner,Skeleton,ProgressBar), entrada (Button,Input,TextArea,Switch,Checkbox,Slider,RangeSlider,Dropdown,DatePicker, …), overlays (Dialog,BottomSheet,Popover,Toast,Tooltip), gestos (GestureDetector,Draggable,PanHandler, …) e mais. Os builders JS são gerados por introspecção do core (widgets.gen.js), com o estilo MD3 resolvido dos 14 widgets estilizados.
Eventos por widget
Cada handler é ligado ao evento DOM que o renderizador (dom.js) emite para
aquele widget: Button.on_click → clique; Input/Checkbox (controles
nativos) → input/change; um Switch (div) → clique. Handlers de widgets
cujo evento o cliente ainda não emite (ex.: on_scan, on_reorder) ficam
registrados mas inertes por ora.
O kwarg de widget é validado no build
O Modo C não tem Python em runtime: o builder gerado desestrutura o objeto
que recebe e ignora chave que não nomeia. Por isso o compilador confere
cada chamada de modelo do core contra os campos reais e falha com
arquivo:linha — Container(children=[...]) viraria uma caixa vazia aqui e
ValidationError nos Modos A/B. O slot de filho usa o nome do core:
child no Container/Draggable, children no Column/Row, fields no
Form.
Dark mode não alcança widget nem componente em Modo C
As tabelas de estilo geradas não têm eixo de modo, então o estilo resolvido que viaja inline é sempre o do tema claro — e inline ganha da folha base. Trocar o tema muda o que a folha pinta, não o que o widget carrega. #106 discute as saídas.
Componentes estruturais estão portados
Os 42 componentes estruturais do core rodam em Modo C:
- superfície e estrutura:
Surface,StyledContainer,Card,Scaffold,Grid,Sidebar,Drawer,Divider, mais os aliasesHStack/VStack; - barras e navegação:
AppBar,Header,Footer,NavBar,Breadcrumb,Burger,SegmentedControl; - conteúdo:
ListTile,Avatar,Chip,Tag,Rating,Stepper,SearchBar,RadioGroup; - divulgação e seleção de painel:
Accordion(o corpo só existe na árvore quandoopen, então fechar é remove, não esconder) eTabs(a aba ativa leva o sublinhado porSideBorder, sem campo de estilo novo); - feedback:
Banner,Alert,Badge,EmptyState,Stat,ProgressStepper; - campos brasileiros:
EmailInput,PasswordInput,PhoneInput,CPFInput,CNPJInputeAddressInput— cada um é o rótulo mudo, oInput/MaskedInputcom a máscara certa e a linha de erro, e oon_changerecebe a string nova, não o evento; - composição:
MetricCard,StatCard,ConfidenceBadge— mais a função puraconfidence_scheme, que é como a app escolhe o esquema do badge.
A composição de cada um foi reescrita em components.js e o resultado dos
resolvedores de estilo do core viaja em tabela gerada
(component-styles.gen.js), do mesmo jeito que widget-styles.gen.js faz
pelos widgets. Cada builder é fixado por uma matriz de props construída do
core real — 356 casos — então drift de composição ou de estilo falha no
teste. São 151 componentes, cada um com um par __dark (o eixo de modo
que a #106
trouxe: um port que esquece de repassar o tema para um filho falha na cor
daquele filho), mais trinta e quatro
pares __keyed: o mesmo componente construído
com key= explícita, porque o build sem chave esconde justamente a derivação
(Accordion() emite accordion-header derivando ou não, enquanto
Accordion(key="faq-3") só emite faq-3-header se o builder deriva de
verdade). O comparador também passou a olhar a chave de cada descendente —
antes só comparava forma e estilo, e por isso um port com chave literal
atravessou um release inteiro.
E os componentes do próprio tempestweb (tempestweb.components) também:
TextField, EmailField, PasswordField, os formulários prontos
LoginForm e SignupForm, e os apelidos PhoneField/CPFField/
CNPJField/AddressField sobre os campos do core. Eles derivam a chave de
cada filho da chave do componente, então dois na mesma tela não disputam o
nome do Input que emite o evento — e o Modo C carrega essa derivação.
examples/mode-c-components exercita o lote inteiro num app só.
O componente carrega as props da base — inclusive aqui
Todo widget declara semantics, focusable, focus_order, tag e attrs.
Nos Modos A e B quem as carrega para a raiz que o componente renderiza é o
build do core (tempest-core 0.17.0). Em Modo C um componente é uma
função, não um nó que alguém expande: prop que o builder não lê chegaria a
nó nenhum, e uma tela acessível no browser ficaria muda na build transpilada
de si mesma.
Cada builder de components.js passa a carregar, com a regra do core: o
render é dono do que ele tocou. Prop que a árvore construída já define em
qualquer nó fica intacta — é o que mantém um campo correto, porque ele põe o
nome acessível no <input> em que o leitor de tela para, e uma segunda cópia
no wrapper sem role anunciaria o mesmo controle duas vezes.
Nos três modos, esse Card sai como uma <section> que se anuncia "Totais".
Fixado por seis pares __named na matriz de paridade (os dois ramos da
regra) e por um sweep sobre todos os builders em
tests/client/component-carry.test.js.
As formas que o app real escreve
O subset aceita o que um app de verdade escreve, não só o mínimo do counter:
- import só de anotação:
from collections.abc import Callableefrom typing import Anypassam e não custam import nenhum no JS — mas usar o nome como valor é erro ('Any' is a type-only name). Alias de tipo em nível de módulo (Fetcher = Callable[[], None]) também é descartado. from tempestweb.components import …: é o import que o tutorial de componentes ensina, e roteia para os mesmos nomes servidos. Nome que o cliente não tem é recusado pelo nome, não pelo módulo.[a, *rest](o idioma de "nova lista sem mutar"), alvo destructurado (for i, (q, a) in enumerate(pairs)),is/is not(contraNonevira== null, que responde certo para campo nunca atribuído) ef"{n:02d}"(zero-pad de relógio e placar — com o sinal fora do preenchimento, como o Python faz).- dataclass do jeito real: campo sem default (fica
undefinedaté omake_statepreencher),@dataclass(frozen=True), efield(default_factory=…)com callable próprio. - conversão de container:
list(xs),tuple(xs),set(xs),dict(pairs). - módulos da stdlib que o browser tem:
re,json,math,base64easyncio, nas duas formas de import (import re/from math import ceil).Pattern.matchancora no início como no Python,re.subtroca todas, easyncio.sleep(0.4)espera 400 ms. Membro fora da tabela é recusado pelo nome (re.escape), e módulo fora da lista diz o que fazer no lugar. enumdo app:class Phase(StrEnum)vira objeto congelado, como os enums do core já viajam emvalues.gen.js.- generator expression (
any(x for x in xs)),any/all,dict.getcom default, e os predicados destr(c.isdigit()). {**old, k: v}, o idioma de "novo dict sem mutar" (irmão do[a, *rest]), exs[:] = [...], a substituição no lugar — que virasplice, não uma atribuição a uma cópia.- dict como dict:
dict(outro)copia edict(pares)constrói — a mesma chamada em Python, operações diferentes em JS, resolvidas em runtime. Ed.pop(chave, default)remove de verdade, em vez de cair nopopde array. - a verdade do Python, não a do JS:
"",0,NoneeFalseas duas linguagens concordam; container vazio não —[]e{}são falsy em Python e truthy em JS. Posição booleana (if,elif,while,not, o ternário) passa portruthy$, entãoif s.errors:responde o que o Python responderia. Comparação,not, literal booleano e nome que o módulo só liga a booleano ficam sem embrulho, para o teste continuar legível.len(d)conta as chaves e"k" in dlê chave, em vez de cair no.lengthe no.includesde array.
and/or em posição de valor ficam como estão
nome or "—" devolve um operando nas duas linguagens, não um
booleano, então || já é o comportamento certo. A diferença aparece só
quando o operando da esquerda é container vazio — [] or x devolve x
em Python e [] em JS. Nenhum exemplo do corpus escreve isso, e
embrulhar mudaria o valor de todo or para ganhar um caso que ninguém
usa.
- predicados de caso:
c.isupper()/c.islower()exigem ao menos um caractere com caixa, como o Python —"1".isupper()éFalse. As classes são ASCII, como as dos outros predicados. Form.validate(values)— o único método de widget que o Modo C porta. Ele cabe porque o insumo sobrevive:validatorsnunca atravessa fio em Modo C, então as funções vivas estão no nó quando a validação roda. Qualquer outro método continua recusado.f"{x:+.1f}": o+força o sinal no positivo, como o Python. O valor é formatado primeiro e o prefixo decidido do resultado, senão um negativo viraria+-3.0. Combina com,,%ed; com0Ndé recusado, porque o Python conta o sinal dentro da largura.if __name__ == "__main__":é pulado, não recusado: o bloco é guarda de script e nunca roda quando o arquivo é importado como módulo — que é exatamente como o Modo C o compila. Umelsenele ainda é recusado, porque esse roda.-
construtor de evento do core:
ThemeChangeEvent(mode=ThemeMode.DARK)e os outros 32 eventos são gerados paravalues.gen.js. A app constrói um quando simula um evento do host. -
capacidade nativa, nas três formas de import:
from tempestweb import native,from tempestweb.native import storageefrom tempestweb.native.geolocation import get_positioncaem todas no mesmo objeto de./native.js. Grupo que a fachada não carrega (camera) é recusado dizendo qual modo o tem, e membro desconhecido é recusado pelo nome (geolocation.triangulate). -
listas virtualizadas:
LazyColumn,LazyRoweLazyGridmaterializam a janela visível chamandoitem_builder(índice), com o item re-chaveado pelo índice absoluto — o que faz a janela deslizar virar remove/reorder/insert mínimo em vez de rebuild. O evento descrolldesliza a janela no runtime, como o servidor faz no Modo B, e ela sobrevive aoviewre-rodando.
Medido no corpus: 44 dos 57 exemplos transpilam (eram 14).
Componente sempre com key explícita
A chave default de um componente é o nome dele (card, alert, navbar),
então dois Card sob o mesmo pai respondem os dois por card e o patch
endereça o errado. Vale nos três modos; em Modo C você percebe igual.
Enums, objetos de valor e tokens do core estão servidos
TextAlign.CENTER, FontWeight.BOLD, KeyboardType.EMAIL,
Semantics(label=…), Border, Shadow, Gradient, ACCENT, ON_SURFACE,
HOVER_OPACITY — os 32 enums do core, os objetos de valor não-widget e os
tokens de design são gerados para values.gen.js a partir do core, na
forma do fio. Style/Color/Edge continuam onde sempre estiveram
(widget-support.js).
Ainda fora do subset — e agora falha no build
Método de widget do core (form.validate(values), e qualquer outro): o
cliente porta o builder de cada widget, não os métodos Python da classe.
Chamar um é erro de compilação com arquivo:linha, e não uma página que
carrega e morre na primeira renderização.
Os componentes dirigidos por dados de tempest_core.components
(DataTable, Table, BarChart/LineChart, DetectionOverlay,
ResultView, Calendar/Clock, os pickers de mídia e de formulário, e o
CollapsingAppBar, que depende do scroll): a forma da árvore deles depende
dos dados recebidos — uma linha de células por registro, uma barra por dado —
então não há composição fixa para portar. Percorrer uma lista plana de
rótulos não é isso: Tabs e Accordion são composição fixa e estão
servidos, como os estruturais acima
(#107
acompanha o que falta). Também
fora: comprehension com mais de um for, e format-specs de f-string além
dos suportados (ex.: alinhamento {x:>5}, sinal {x:+.2f}, hex/bin {x:x},
dinâmico {x:.{n}f}, conversão !a).
Usar um desses nomes agora é erro de compilação com arquivo:linha:
Antes o compilador emitia import { Card } from "./widgets.js" mesmo assim —
um import que o browser não resolve, então o módulo nunca era avaliado e a
página ficava em branco, sem nada no log do build. Importar um tipo só
para anotação (DragEvent, TextChangeHandler) continua livre: anotação é
descartada, o nome nunca é referenciado, nenhum import é emitido.
Recapitulando¶
- Modo C transcreve a camada de app Python para JS nativo — zero runtime Python, bundle estático, first-paint/SEO ótimos.
tempestweb build --mode transpilegera um diretório servível por qualquer CDN;run --mode transpileserve localmente.- O mesmo
view()dos Modos A/B roda aqui — estado, handlers,Button/Inputestilizados, binding reativo, navegação, i18n, tema, animação. - É um modo maduro e de primeira classe: 100% dos widgets do core, subset
amplo de Python tipado e PWA turnkey. Detalhes de design em
docs/modo-c-transpile.md.
Continue para PWA e offline para o fluxo completo de instalação e WebPush, ou volte ao Tutorial para revisar o counter nos outros modos. 🚀