Construa aplicações reativas direto no HTML.
Sem passo de build obrigatório · Sem dependências em tempo de execução · Sem Virtual DOM · Sem configuração
JavaScript feels like magic.
Instalação · Início rápido · Documentação · Exemplos · English
Salve este arquivo. Abra no navegador. Funciona.
<script src="voodoo.min.js" defer></script>
<div v-data="{ contador: 0 }">
<button @click="contador--">-</button>
<strong>{ contador }</strong>
<button @click="contador++">+</button>
</div>
Sem bundler, sem npm install, sem arquivo de configuração, sem JSX. Só HTML que pensa.
Estado reativo, uma requisição HTTP ao vivo, um formulário validado e uma notificação — ainda em uma tag de script, ainda sem passo de build:
<div v-data="{ }">
<!-- Uma requisição com estado próprio de carregamento, erro e dados, declarada no HTML -->
<div v-resource="usuarios: /api/usuarios">
<p v-if="usuarios.loading">Carregando…</p>
<p v-else-if="usuarios.error">{ usuarios.error.message }</p>
<ul v-else>
<li v-for="u in usuarios.data" :key="u.id">{ u.nome }</li>
</ul>
<button @click="usuarios.reload()">Atualizar</button>
</div>
<!-- Um formulário que valida, envia por AJAX e devolve o resultado -->
<form v-submit="/api/usuarios" v-method="POST" v-validate
v-toast-success="Usuário criado" v-reset-success>
<input name="nome" v-required>
<input name="email" type="email" v-required v-email>
<button type="submit" :disabled="$form.loading">
{ $form.loading ? 'Salvando…' : 'Salvar' }
</button>
</form>
</div>
Essa é a aplicação inteira. Não existe um app.js ao lado fazendo a ligação.
React e Vue começam pelo JavaScript: você descreve a interface em uma linguagem de componentes, e o HTML é o que o framework produz no final.
A Voodoo começa pelo HTML. A página que você já tem é a aplicação. Você acrescenta atributos a ela, cada atributo é ligado ao estado reativo, e quando esse estado muda apenas os nós do DOM que dependem dele são atualizados — nada mais é tocado, e não existe Virtual DOM no meio do caminho.
Quando o HTML não basta, a API V está ali: V.reactive, V.component, V.http, V.store,
V.router e uma coleção de DOM encadeável através de V('#seletor'). Se preferir, dá para
escrever a aplicação inteira em JavaScript com V.createApp().mount('#app'). É o mesmo runtime, e
os dois modos convivem livremente na mesma página.
Isto não é um "matador de React". É um ponto de partida diferente para um tipo de projeto diferente: painéis administrativos gerados no servidor, sites de conteúdo, protótipos, páginas legadas e qualquer coisa em que montar um pipeline de build custa mais do que o problema que você está resolvendo.
reactive / ref / computed / effect sobre Proxy. Uma escrita
reexecuta apenas os efeitos que de fato leram aquele valor.<script> já é a instalação completa. O build existe
quando você quiser, nunca por obrigação.eval, sem new Function — por isso a Voodoo roda
sob uma Content Security Policy sem unsafe-eval.Modo 1 — HTML. Estado declarado onde ele é usado:
<div v-data="{ contador: 0 }">
<button @click="contador++">Cliques: { contador }</button>
</div>
Modo 2 — JavaScript. O mesmo motor de reatividade, guiado pelo seu código:
const estado = V.reactive({ contador: 0 })
V.effect(() => { document.title = `Cliques: ${estado.contador}` })
estado.contador++
Cada linha abaixo faz parte do runtime publicado.
| Pilar | O que você tem |
|---|---|
| Reatividade | reactive, ref, computed, effect, watch, watchEffect, nextTick, effectScope, flushSync |
| Expressões | Lexer + parser Pratt + interpretador de AST próprios. Sem eval, compatível com CSP |
| Directives | Texto, condicionais, listas, ligação, classes, estilos, eventos, refs, transições e mais |
| Componentes | Props, estado, computados, métodos, watchers, template, estilo próprio, slots nomeados, provide/inject, ciclo de vida |
| Modo aplicação | V.createApp({…}).mount('#app') com use, provide, componentes locais e unmount |
| DOM | Coleção encadeável via V('#seletor'), mais utilitários de transição |
| HTTP | Interceptors, timeout, retry exponencial, cache, CSRF, progresso de upload, SSE, stream NDJSON, fila offline |
| Requisições declarativas | v-get/v-post/v-put/v-patch/v-delete, v-resource, v-load, v-load-visible, v-search, polling |
| Formulários | Envio por AJAX, serialização, upload, dropzone, autosave, aviso ao sair, estado reativo $form |
| Validação e máscaras | Conjunto completo de regras, regras assíncronas, regras e mensagens próprias, máscaras de entrada |
| Stores | V.store(nome, def, { persist }) e a mágica $store, com persistência opcional no localStorage |
| Armazenamento | storage, session, cookie, cache, url, theme |
| Interface | Toast, modal, alert, confirm, prompt, dialog, paleta de comandos, mais directives de abas, dropdown, tooltip, gaveta, popover e acordeão |
| Arrastar e soltar | v-draggable, v-droppable, v-sortable, grupos, suporte a teclado |
| Roteador | Modos history e hash, params, query, guards, restauração de rolagem, cache de view, v-link, v-router-view, rotas dinâmicas |
| i18n | Mensagens por idioma, V.t, v-t, pluralização, troca de idioma em tempo de execução |
| Animação | Física de mola, stagger, inView, progresso de rolagem, presets |
| Gráficos | Desenhados em SVG puro, sem dependência de biblioteca de gráficos |
| Devtools | Inspetor de reatividade V.xray e um barramento de eventos |
| CLI | @voodoo/cli: init, build --modules=…, add, info |
Sobre o npm. O pacote
voodoojsainda não foi publicado no registro do npm. As instruções de npm e CDN abaixo estão escritas para o momento em que ele for. Até lá, use o download direto ou um build local — os dois funcionam hoje.
Download direto, ou build local — os dois funcionam agora
curl -O https://github.com/kwy404/Voodoo.js/raw/main/packages/voodoojs/dist/voodoo.min.js
# ou
git clone https://github.com/kwy404/Voodoo.js.git
cd Voodoo.js && npm install && npm run build # os bundles saem em packages/voodoojs/dist/
<script src="voodoo.min.js" defer></script>
CDN direto do repositório (funciona agora)
<script src="https://cdn.jsdelivr.net/gh/kwy404/Voodoo.js@main/packages/voodoojs/dist/voodoo.min.js" defer></script>
CDN, npm e ES Modules (quando publicado)
Os caminhos de CDN apoiados no npm abaixo ainda não resolvem, porque o pacote não está no registro.
<script src="https://cdn.jsdelivr.net/npm/voodoojs/dist/voodoo.min.js" defer></script>
<script src="https://unpkg.com/voodoojs/dist/voodoo.min.js" defer></script>
// npm install voodoojs (planejado, ainda não está no registro)
import V from 'voodoojs'
V.start()
// ou importe só o que precisa — pontos de entrada separados, em ESM e CJS, com tipos
import { reactive, computed } from 'voodoojs/reactivity'
import { http } from 'voodoojs/http'
import { debounce } from 'voodoojs/utils'
CLI
npx voodoo init minha-pagina # cria um projeto pronto para abrir
npx voodoo build --modules=core,directives,http # bundle sob medida, só com o que você usa
npx voodoo add card # copia um componente para o seu projeto
npx voodoo info # lista os módulos e o tamanho de cada um
Qual bundle escolher?
| Arquivo | O que vem dentro |
|---|---|
voodoo.core.min.js | Build mínimo: reatividade, expressões, directives, componentes, DOM, requisições |
voodoo.min.js | Build essencial — o padrão. Soma formulários, validação, máscaras, interface, arrastar e soltar |
voodoo.full.min.js | Tudo: gráficos, animação, roteador, i18n, devtools, componentes prontos |
Os tamanhos são dinâmicos — veja o badge acima, ou rode npm run size / npx voodoo info.
Estado, eventos e ligação de dois sentidos. A interpolação usa chave simples:
{ expressão }. {{ expressão }} também é aceita, para quem vem do Vue.
<div v-data="{ nome: 'Mundo', email: '' }">
<p>Olá, { nome }!</p>
<button @click="nome = 'Voodoo'">Trocar</button>
<input v-model="email" type="email">
<p v-show="email">Você digitou: { email }</p>
</div>
Condicionais, listas, classes e estilos.
<div v-data="{ status: 'pronto', itens: ['um', 'dois'], largura: 60 }">
<p v-if="status === 'carregando'">Carregando…</p>
<p v-else-if="status === 'erro'">Alguma coisa deu errado.</p>
<p v-else>Pronto.</p>
<li v-for="(item, i) in itens" :key="i">{ i + 1 }. { item }</li>
<span :class="{ 'ativo': status === 'pronto' }" :style="{ width: largura + '%' }"></span>
</div>
HTTP sem escrever fetch, e um componente.
<button v-get="/api/estatisticas" v-target="#painel" v-swap="innerHTML">Carregar</button>
<div id="painel"></div>
<script>
V.component('cartao-usuario', {
props: { nome: { type: 'string', default: 'Anônimo' } },
template: `<div class="cartao"><strong v-text="nome"></strong></div>`
})
</script>
<cartao-usuario nome="Ada"></cartao-usuario>
Um componente é um escopo com estado, métodos, computados, watchers, props, slots e ciclo de vida, montado sobre um elemento. Sem passo de compilação, sem formato de arquivo único.
V.component('contador', {
props: {
inicio: { type: 'number', default: 0 },
rotulo: { type: 'string', required: true }
},
state(props) { return { valor: props.inicio } },
computed: { dobro() { return this.valor * 2 } },
methods: {
somar() {
this.valor++
this.emit('mudou', this.valor) // um CustomEvent de verdade, que sobe pela árvore
}
},
watch: { valor(v) { console.log('agora', v) } },
template: `
<button @click="somar()">{ rotulo }: { valor }</button>
<small>dobro: { dobro }</small>
<slot name="rodape"></slot>
`,
style: `.contador { font-weight: 600 }`,
mounted() { /* o elemento já está no DOM */ }
})
Três formas equivalentes de usar, e ouvir o que ele emite é um listener comum:
<div v-component="contador" rotulo="Cliques"></div>
<contador rotulo="Cliques" :inicio="10" @mudou="console.log($event.detail)"></contador>
<Contador rotulo="Cliques"></Contador>
Atributos estáticos viram props de texto, convertidas para o type declarado. Atributos escritos
com : são ligações reativas avaliadas no escopo do pai, e o conteúdo dos slots também; slots
nomeados casam por slot="nome". Dentro da instância você ainda tem $el, $props, $refs,
$parent, $name, $emit, $watch e $nextTick, mais provide / inject para injeção de
dependência.
Modo aplicação — se preferir descrever tudo em JavaScript:
V.createApp({
data: () => ({ n: 0 }),
computed: { dobro() { return this.n * 2 } },
methods: { somar() { this.n++ } },
template: `<button @click="somar()">Cliques: { n }</button><p>Dobro: { dobro }</p>`
}).mount('#app')
O mount aceita um alvo que ainda não existe — ele espera, então não há corrida com o carregamento
da página. O unmount devolve o HTML original do container em vez de deixá-lo vazio.
Declarativo. Ligue uma requisição a um elemento e diga para onde vai a resposta:
<button v-get="/api/relatorio" v-target="#saida" v-swap="innerHTML">Carregar</button>
<button v-delete="'/api/usuarios/' + usuario.id"
v-confirm="Excluir este usuário?"
v-toast-success="Usuário excluído">Excluir</button>
<div v-get="/api/feed" v-trigger="visible" v-poll="30s"></div>
<input v-search="/api/busca" v-param="q" v-debounce="300ms" v-target="#resultados">
A URL pode ser um literal (/api/usuarios) ou uma expressão ('/api/usuarios/' + id). Os
atributos de apoio incluem v-target, v-swap, v-trigger, v-poll, v-body, v-params,
v-headers, v-cache, v-retry, v-timeout, v-json-path, v-template, v-offline-queue,
v-redirect, v-scroll-to, v-toast-success, v-toast-error, v-on-success, v-on-error e
v-on-complete.
O v-resource é a versão que entrega o estado da requisição como dado reativo em vez de trocar
HTML. Ele expõe data, loading, error, loaded, reload() e set() — veja a demonstração no
começo deste arquivo.
Programático.
const usuarios = await V.http.get('/api/usuarios')
const criado = await V.http.post('/api/usuarios', { nome: 'Ada' })
// Resposta completa, com status e cabeçalhos
const res = await V.http.request({ url: '/api/usuarios', retry: 2, timeout: 5000, cache: 60000 })
// Upload com progresso real
await V.http.upload('/api/arquivos', formData, { onProgress: (pct) => console.log(pct + '%') })
// Server-Sent Events e stream NDJSON
V.http.sse('/api/eventos', { message: (dados) => console.log(dados) })
await V.http.stream('/api/tokens', (linha) => console.log(linha))
Padrões, interceptors, URL base, cabeçalho de CSRF e cache ficam em V.http.defaults e
V.http.interceptors.
Enviar, validar, mostrar o carregamento e reportar o resultado — tudo declarado no próprio formulário:
<form v-submit="/api/contato" v-method="POST" v-validate
v-toast-success="Mensagem enviada" v-toast-error="Não foi possível enviar" v-reset-success>
<input name="nome" v-required>
<input name="email" type="email" v-required v-email>
<input name="telefone" v-mask="phone">
<textarea name="mensagem" v-minlength="20"></textarea>
<p v-if="$form.errors.email">{ $form.errors.email }</p>
<button type="submit" :disabled="$form.loading">
{ $form.loading ? 'Enviando…' : 'Enviar' }
</button>
</form>
$form é reativo e carrega loading, saving, success, errors, message, data, status,
dirty e progress. O conjunto de regras cobre o esperado — required, email, url, number,
min, max, minlength, maxlength, between, match, regex, date, same, different,
in, strongpassword, cpf, cnpj, cep e mais — além de regras assíncronas e das suas
próprias, via V.validator().
A Voodoo cresce além de uma página sem mudar o modelo.
V.store('carrinho', { itens: [] }, { persist: true }), lido em qualquer lugar como
$store.carrinho.V.router({ mode: 'history', routes: { '/usuarios/:id': { component: 'pagina-usuario' } } }),
com guards, params, comportamento de rolagem, v-link e v-router-view.V.use(plugin) ou app.use(plugin) para registrar directives, componentes e
serviços.v-load-visible e rotas com view buscam o HTML só quando ele é
necessário.V.i18n({ locale: 'pt-BR', messages }), depois v-t na marcação e V.t() no código.Cada um deles tem o seu guia em docs/.
O build completo traz o xray, um inspetor visual de reatividade: ele mostra a árvore de escopos,
o estado ao vivo, quais efeitos estão rodando, e os registros de eventos e de rede.
V.xray() // alterna o painel
V.xray(true) // abre
V.xray(false) // fecha
V.enableXrayShortcut() // instala apenas o atalho, sem abrir o painel
A primeira chamada também instala o atalho Ctrl+Shift+X, então o inspetor fica a uma tecla de
distância durante o desenvolvimento, sem custo nenhum enquanto ninguém aperta.
atributos HTML ─▶ Walker + MutationObserver acha os atributos v-*, monta os escopos
│
expressões ─▶ Lexer → parser Pratt → interpretador de AST sem eval, sem new Function
│
Reatividade: alvos em Proxy + efeitos leituras rastreadas, escritas na fila
│
Directives atualizam os nós reais do DOM sem Virtual DOM, sem diff
Por cima: componentes · stores · roteador · HTTP · formulários · interface · i18n · animação · gráficos
A versão longa, com as fronteiras de cada módulo e o modelo de escopos, está em
ARCHITECTURE.md.
Os benchmarks são reproduzíveis: versões de dependência fixadas, builds de produção, metodologia
publicada e ambiente registrado. O relatório completo — incluindo os casos em que a Voodoo perde
— está em benchmarks/README.md, com a metodologia, o ambiente e como
reproduzir cada medição.
O JavaScript puro é o teto aqui, e a Voodoo cobra um custo real pela produtividade que entrega. A função destes números é mostrar o tamanho desse custo, não fingir que ele é zero.
Tempo de execução. (carga de 1.000 linhas; ambiente, número de amostras e estatística estão registrados no relatório; medianas).
| Criar 1k linhas | Atualizar 1k linhas | Trocar linhas | Selecionar linha | Limpar linhas | Pico de memória | |
|---|---|---|---|---|---|---|
| JavaScript puro | ||||||
| Voodoo.js | ||||||
| Alpine.js | ||||||
| Vue | ||||||
| React | ||||||
| Preact | ||||||
| Svelte | ||||||
| Solid |
Tamanho dos bundles, medido sobre os builds versionados em :
| Build | Minificado | Gzip | Brotli |
|---|---|---|---|
voodoo.core.min.js | |||
voodoo.min.js | |||
voodoo.full.min.js |
Rode você mesmo: o arcabouço de medição e as versões exatas testadas estão em
benchmarks/.
Uma comparação honesta. Toda ferramenta aqui é boa naquilo para que foi desenhada.
| Voodoo.js | Alpine.js | HTMX | Vue 3 | React | jQuery | |
|---|---|---|---|---|---|---|
| Ponto de partida | HTML | HTML | HTML | JavaScript | JavaScript | JavaScript |
| Roda por uma tag de CDN | nativo | nativo | nativo | nativo | nativo | nativo |
| Passo de build | possível | possível | possível | recomendado | recomendado | possível |
| Renderização | DOM direto | DOM direto | HTML do servidor | Virtual DOM | Virtual DOM | manual |
| Estado reativo | nativo | nativo | — | nativo | nativo | — |
| Componentes | nativo | via pacote do ecossistema | — | nativo | nativo | — |
| Cliente HTTP | nativo | via pacote do ecossistema | nativo | via pacote do ecossistema | via pacote do ecossistema | nativo |
| Formulários + validação | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Roteador | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Interface (toast, modal, abas) | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Gráficos / i18n | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Renderização no servidor | — | — | nativo (quem renderiza é o servidor) | nativo | nativo | — |
| Tamanho do ecossistema | jovem | crescendo | crescendo | grande | muito grande | muito grande |
A diferença real é filosófica. O HTMX diz que o HTML é do servidor e o navegador apenas o
encaixa. O Alpine dá ao HTML uma pitada de estado reativo e para por aí, de propósito. Vue e
React pedem que você descreva a interface em JavaScript e geram o HTML. A Voodoo mantém o
HTML como fonte da verdade e entrega a caixa de ferramentas completa — assim você raramente
precisa sair dele, e a API V espera pelos momentos em que precisar.
Encaixa bem em: painéis administrativos gerados no servidor (Laravel, Rails, Django, Spring, PHP puro); sites de conteúdo e landing pages que precisam de comportamento sem um pipeline de front-end; protótipos, em que abrir um arquivo vale mais do que qualquer arquitetura; times pequenos que não querem manter um build só para mostrar uma tabela; páginas legadas, em que a Voodoo convive com o código existente porque nunca toma conta do documento; e aplicações de página única completas, com componentes, stores e roteador.
Ditas com clareza, para que nada surpreenda depois:
v-for reaproveita elementos por chave, mas listas muito
grandes ainda renderizam todas as linhas.A documentação existe em dois idiomas: português em docs/ (completa) e inglês em
docs/en/.
| Onde | O quê |
|---|---|
docs/ | Índice do guia completo e da referência |
docs/introducao.md | O que é, para quem serve, quando não usar |
docs/instalacao.md | Bundles, CDN, npm, configuração pela tag script |
docs/inicio-rapido.md | Do arquivo vazio ao primeiro app |
docs/directives.md | Referência completa de directives |
docs/api.md | Referência completa da API V |
site/ | Código do site de documentação |
Cada exemplo é um único arquivo HTML que você abre direto no navegador. Comece por
examples/index.html.
| Exemplo | O que mostra |
|---|---|
| Tarefas | Estado, listas, edição no lugar, filtros, reordenação |
| CRUD | Um componente, formulários, validação, máscaras, toasts, atualização otimista |
| Painel | Gráficos, computados reativos, atualização periódica |
| Kanban | Arrastar e soltar entre colunas, estado persistido |
| Chat | Atualização ao vivo, rolagem, composição de mensagens |
| E-commerce | Catálogo, filtros, store de carrinho, fluxo de checkout |
| Pokédex | Consumo de API real, busca, paginação, carregamento sob demanda |
| Pacote | Para que serve |
|---|---|
voodoojs | O framework: runtime, directives, componentes, HTTP, formulários, interface, roteador, i18n |
@voodoo/cli | Criação de projetos, builds sob medida, cópia de componentes, informação de módulos |
O código inteiro é TypeScript, e todo ponto de entrada publica declarações .d.ts.
import V, { reactive, computed, type HttpResponse } from 'voodoojs'
const estado = reactive({ contador: 0 })
const dobro = computed(() => estado.contador * 2)
O foco de curto prazo é a documentação em inglês, mais exemplos, uma superfície maior de plugins e
a publicação no npm. O plano acompanhado fica em ROADMAP.md; o que já foi entregue
está registrado no CHANGELOG.md.
git clone https://github.com/kwy404/Voodoo.js.git
cd Voodoo.js && npm install
Um monorepo com npm workspaces: packages/voodoojs é o framework,
packages/cli é a CLI, mais docs/, site/ e
examples/.
| Comando | O que faz |
|---|---|
npm test | Roda a suíte inteira uma vez (vitest + jsdom) |
npm run test:watch | A mesma suíte, reexecutando enquanto você edita |
npm run coverage | Rodada de testes com relatório de cobertura |
npm run typecheck | tsc --noEmit sobre o pacote do framework |
npm run build | Gera todos os bundles do voodoojs e do @voodoo/cli |
npm run size | Informa o tamanho de cada bundle gerado |
npm run serve | Servidor estático local para os exemplos e o site |
npm run format | Prettier sobre o repositório |
Novos scripts entram com o tempo — o package.json da raiz é a lista definitiva.
O ciclo. Crie um branch a partir da main, faça a mudança, rode npm test e
npm run typecheck antes de abrir o PR, e escreva as mensagens de commit no estilo
Conventional Commits (fix:, feat:, docs:). O CI
(.github/workflows/ci.yml) roda typecheck, testes, build e a
checagem de tamanho dos bundles no Node 20 e no 22.
Onde mexer. Bug no runtime → src/runtime/. Reatividade → src/reactivity/. Expressões →
src/parser/. Directive nova → src/directives/. Componente de interface → src/ui/.
Documentação → docs/ e site/docs/.
As directives internas são registradas com defineDirective(nome, setup, { priority, terminal }),
de src/runtime/registry.ts, no arquivo correspondente dentro de src/directives/. A função
setup recebe um DirectiveContext com el, scope, expression, arg, modifiers,
evaluate(), effect(), cleanup() e walk(). O priority define a ordem de execução (maior
roda primeiro, veja a tabela PRIORITY); terminal: true impede o walker de descer nos filhos,
como fazem o v-for e o v-if.
import { defineDirective } from '../runtime/registry';
// <p v-shout="mensagem"> → mostra o valor em caixa alta
defineDirective('shout', ({ el, effect, evaluate, cleanup }) => {
effect(() => {
el.textContent = String(evaluate() ?? '').toUpperCase();
});
// Sempre libere o que você prendeu: listeners, timers, observers.
const aoClicar = () => el.classList.toggle('alto');
el.addEventListener('click', aoClicar);
cleanup(() => el.removeEventListener('click', aoClicar));
});
Essa é a API interna. A pública é V.directive(nome, hooks), que embrulha o mesmo mecanismo em
hooks de ciclo de vida no estilo do Vue (created, mounted, updated, unmounted) e entrega a
cada hook um binding com o value já avaliado, oldValue, arg e modifiers. Use
V.directive no código da aplicação; use defineDirective dentro do framework.
Os testes ficam em packages/voodoojs/test/ como *.test.ts, e rodam
em vitest com jsdom. Um teste de directive monta o HTML, percorre com um escopo e verifica o DOM:
import { describe, it, expect } from 'vitest';
import { reactive, nextTick } from '../src/reactivity';
import { Scope } from '../src/runtime/scope';
import { walk } from '../src/runtime/walker';
import '../src/core';
describe('v-shout', () => {
it('mostra o valor em caixa alta', async () => {
const dados = reactive({ mensagem: 'ola' });
const raiz = document.createElement('div');
raiz.innerHTML = '<p v-shout="mensagem"></p>';
document.body.appendChild(raiz);
walk(raiz, new Scope(dados));
expect(raiz.textContent).toBe('OLA');
dados.mensagem = 'tchau';
await nextTick();
await nextTick();
expect(raiz.textContent).toBe('TCHAU');
});
});
A regra de ouro: toda correção de bug entra junto com um teste de regressão. Se quebrou uma vez pode quebrar de novo, e o teste é o que impede.
O detalhe completo está no CONTRIBUTING.md, e todo mundo deve seguir o
CODE_OF_CONDUCT.md.
MIT © contribuidores da Voodoo.js.
Prefer to read in English? → README.en.md
JavaScript feels like magic.
21 commits
Hacker News (2)
TypeScript
46.3%
HTML
23.9%
JavaScript
23.0%
CSS
6.8%
Construa aplicações reativas direto no HTML.
Sem passo de build obrigatório · Sem dependências em tempo de execução · Sem Virtual DOM · Sem configuração
JavaScript feels like magic.
Instalação · Início rápido · Documentação · Exemplos · English
Salve este arquivo. Abra no navegador. Funciona.
<script src="voodoo.min.js" defer></script>
<div v-data="{ contador: 0 }">
<button @click="contador--">-</button>
<strong>{ contador }</strong>
<button @click="contador++">+</button>
</div>
Sem bundler, sem npm install, sem arquivo de configuração, sem JSX. Só HTML que pensa.
Estado reativo, uma requisição HTTP ao vivo, um formulário validado e uma notificação — ainda em uma tag de script, ainda sem passo de build:
<div v-data="{ }">
<!-- Uma requisição com estado próprio de carregamento, erro e dados, declarada no HTML -->
<div v-resource="usuarios: /api/usuarios">
<p v-if="usuarios.loading">Carregando…</p>
<p v-else-if="usuarios.error">{ usuarios.error.message }</p>
<ul v-else>
<li v-for="u in usuarios.data" :key="u.id">{ u.nome }</li>
</ul>
<button @click="usuarios.reload()">Atualizar</button>
</div>
<!-- Um formulário que valida, envia por AJAX e devolve o resultado -->
<form v-submit="/api/usuarios" v-method="POST" v-validate
v-toast-success="Usuário criado" v-reset-success>
<input name="nome" v-required>
<input name="email" type="email" v-required v-email>
<button type="submit" :disabled="$form.loading">
{ $form.loading ? 'Salvando…' : 'Salvar' }
</button>
</form>
</div>
Essa é a aplicação inteira. Não existe um app.js ao lado fazendo a ligação.
React e Vue começam pelo JavaScript: você descreve a interface em uma linguagem de componentes, e o HTML é o que o framework produz no final.
A Voodoo começa pelo HTML. A página que você já tem é a aplicação. Você acrescenta atributos a ela, cada atributo é ligado ao estado reativo, e quando esse estado muda apenas os nós do DOM que dependem dele são atualizados — nada mais é tocado, e não existe Virtual DOM no meio do caminho.
Quando o HTML não basta, a API V está ali: V.reactive, V.component, V.http, V.store,
V.router e uma coleção de DOM encadeável através de V('#seletor'). Se preferir, dá para
escrever a aplicação inteira em JavaScript com V.createApp().mount('#app'). É o mesmo runtime, e
os dois modos convivem livremente na mesma página.
Isto não é um "matador de React". É um ponto de partida diferente para um tipo de projeto diferente: painéis administrativos gerados no servidor, sites de conteúdo, protótipos, páginas legadas e qualquer coisa em que montar um pipeline de build custa mais do que o problema que você está resolvendo.
reactive / ref / computed / effect sobre Proxy. Uma escrita
reexecuta apenas os efeitos que de fato leram aquele valor.<script> já é a instalação completa. O build existe
quando você quiser, nunca por obrigação.eval, sem new Function — por isso a Voodoo roda
sob uma Content Security Policy sem unsafe-eval.Modo 1 — HTML. Estado declarado onde ele é usado:
<div v-data="{ contador: 0 }">
<button @click="contador++">Cliques: { contador }</button>
</div>
Modo 2 — JavaScript. O mesmo motor de reatividade, guiado pelo seu código:
const estado = V.reactive({ contador: 0 })
V.effect(() => { document.title = `Cliques: ${estado.contador}` })
estado.contador++
Cada linha abaixo faz parte do runtime publicado.
| Pilar | O que você tem |
|---|---|
| Reatividade | reactive, ref, computed, effect, watch, watchEffect, nextTick, effectScope, flushSync |
| Expressões | Lexer + parser Pratt + interpretador de AST próprios. Sem eval, compatível com CSP |
| Directives | Texto, condicionais, listas, ligação, classes, estilos, eventos, refs, transições e mais |
| Componentes | Props, estado, computados, métodos, watchers, template, estilo próprio, slots nomeados, provide/inject, ciclo de vida |
| Modo aplicação | V.createApp({…}).mount('#app') com use, provide, componentes locais e unmount |
| DOM | Coleção encadeável via V('#seletor'), mais utilitários de transição |
| HTTP | Interceptors, timeout, retry exponencial, cache, CSRF, progresso de upload, SSE, stream NDJSON, fila offline |
| Requisições declarativas | v-get/v-post/v-put/v-patch/v-delete, v-resource, v-load, v-load-visible, v-search, polling |
| Formulários | Envio por AJAX, serialização, upload, dropzone, autosave, aviso ao sair, estado reativo $form |
| Validação e máscaras | Conjunto completo de regras, regras assíncronas, regras e mensagens próprias, máscaras de entrada |
| Stores | V.store(nome, def, { persist }) e a mágica $store, com persistência opcional no localStorage |
| Armazenamento | storage, session, cookie, cache, url, theme |
| Interface | Toast, modal, alert, confirm, prompt, dialog, paleta de comandos, mais directives de abas, dropdown, tooltip, gaveta, popover e acordeão |
| Arrastar e soltar | v-draggable, v-droppable, v-sortable, grupos, suporte a teclado |
| Roteador | Modos history e hash, params, query, guards, restauração de rolagem, cache de view, v-link, v-router-view, rotas dinâmicas |
| i18n | Mensagens por idioma, V.t, v-t, pluralização, troca de idioma em tempo de execução |
| Animação | Física de mola, stagger, inView, progresso de rolagem, presets |
| Gráficos | Desenhados em SVG puro, sem dependência de biblioteca de gráficos |
| Devtools | Inspetor de reatividade V.xray e um barramento de eventos |
| CLI | @voodoo/cli: init, build --modules=…, add, info |
Sobre o npm. O pacote
voodoojsainda não foi publicado no registro do npm. As instruções de npm e CDN abaixo estão escritas para o momento em que ele for. Até lá, use o download direto ou um build local — os dois funcionam hoje.
Download direto, ou build local — os dois funcionam agora
curl -O https://github.com/kwy404/Voodoo.js/raw/main/packages/voodoojs/dist/voodoo.min.js
# ou
git clone https://github.com/kwy404/Voodoo.js.git
cd Voodoo.js && npm install && npm run build # os bundles saem em packages/voodoojs/dist/
<script src="voodoo.min.js" defer></script>
CDN direto do repositório (funciona agora)
<script src="https://cdn.jsdelivr.net/gh/kwy404/Voodoo.js@main/packages/voodoojs/dist/voodoo.min.js" defer></script>
CDN, npm e ES Modules (quando publicado)
Os caminhos de CDN apoiados no npm abaixo ainda não resolvem, porque o pacote não está no registro.
<script src="https://cdn.jsdelivr.net/npm/voodoojs/dist/voodoo.min.js" defer></script>
<script src="https://unpkg.com/voodoojs/dist/voodoo.min.js" defer></script>
// npm install voodoojs (planejado, ainda não está no registro)
import V from 'voodoojs'
V.start()
// ou importe só o que precisa — pontos de entrada separados, em ESM e CJS, com tipos
import { reactive, computed } from 'voodoojs/reactivity'
import { http } from 'voodoojs/http'
import { debounce } from 'voodoojs/utils'
CLI
npx voodoo init minha-pagina # cria um projeto pronto para abrir
npx voodoo build --modules=core,directives,http # bundle sob medida, só com o que você usa
npx voodoo add card # copia um componente para o seu projeto
npx voodoo info # lista os módulos e o tamanho de cada um
Qual bundle escolher?
| Arquivo | O que vem dentro |
|---|---|
voodoo.core.min.js | Build mínimo: reatividade, expressões, directives, componentes, DOM, requisições |
voodoo.min.js | Build essencial — o padrão. Soma formulários, validação, máscaras, interface, arrastar e soltar |
voodoo.full.min.js | Tudo: gráficos, animação, roteador, i18n, devtools, componentes prontos |
Os tamanhos são dinâmicos — veja o badge acima, ou rode npm run size / npx voodoo info.
Estado, eventos e ligação de dois sentidos. A interpolação usa chave simples:
{ expressão }. {{ expressão }} também é aceita, para quem vem do Vue.
<div v-data="{ nome: 'Mundo', email: '' }">
<p>Olá, { nome }!</p>
<button @click="nome = 'Voodoo'">Trocar</button>
<input v-model="email" type="email">
<p v-show="email">Você digitou: { email }</p>
</div>
Condicionais, listas, classes e estilos.
<div v-data="{ status: 'pronto', itens: ['um', 'dois'], largura: 60 }">
<p v-if="status === 'carregando'">Carregando…</p>
<p v-else-if="status === 'erro'">Alguma coisa deu errado.</p>
<p v-else>Pronto.</p>
<li v-for="(item, i) in itens" :key="i">{ i + 1 }. { item }</li>
<span :class="{ 'ativo': status === 'pronto' }" :style="{ width: largura + '%' }"></span>
</div>
HTTP sem escrever fetch, e um componente.
<button v-get="/api/estatisticas" v-target="#painel" v-swap="innerHTML">Carregar</button>
<div id="painel"></div>
<script>
V.component('cartao-usuario', {
props: { nome: { type: 'string', default: 'Anônimo' } },
template: `<div class="cartao"><strong v-text="nome"></strong></div>`
})
</script>
<cartao-usuario nome="Ada"></cartao-usuario>
Um componente é um escopo com estado, métodos, computados, watchers, props, slots e ciclo de vida, montado sobre um elemento. Sem passo de compilação, sem formato de arquivo único.
V.component('contador', {
props: {
inicio: { type: 'number', default: 0 },
rotulo: { type: 'string', required: true }
},
state(props) { return { valor: props.inicio } },
computed: { dobro() { return this.valor * 2 } },
methods: {
somar() {
this.valor++
this.emit('mudou', this.valor) // um CustomEvent de verdade, que sobe pela árvore
}
},
watch: { valor(v) { console.log('agora', v) } },
template: `
<button @click="somar()">{ rotulo }: { valor }</button>
<small>dobro: { dobro }</small>
<slot name="rodape"></slot>
`,
style: `.contador { font-weight: 600 }`,
mounted() { /* o elemento já está no DOM */ }
})
Três formas equivalentes de usar, e ouvir o que ele emite é um listener comum:
<div v-component="contador" rotulo="Cliques"></div>
<contador rotulo="Cliques" :inicio="10" @mudou="console.log($event.detail)"></contador>
<Contador rotulo="Cliques"></Contador>
Atributos estáticos viram props de texto, convertidas para o type declarado. Atributos escritos
com : são ligações reativas avaliadas no escopo do pai, e o conteúdo dos slots também; slots
nomeados casam por slot="nome". Dentro da instância você ainda tem $el, $props, $refs,
$parent, $name, $emit, $watch e $nextTick, mais provide / inject para injeção de
dependência.
Modo aplicação — se preferir descrever tudo em JavaScript:
V.createApp({
data: () => ({ n: 0 }),
computed: { dobro() { return this.n * 2 } },
methods: { somar() { this.n++ } },
template: `<button @click="somar()">Cliques: { n }</button><p>Dobro: { dobro }</p>`
}).mount('#app')
O mount aceita um alvo que ainda não existe — ele espera, então não há corrida com o carregamento
da página. O unmount devolve o HTML original do container em vez de deixá-lo vazio.
Declarativo. Ligue uma requisição a um elemento e diga para onde vai a resposta:
<button v-get="/api/relatorio" v-target="#saida" v-swap="innerHTML">Carregar</button>
<button v-delete="'/api/usuarios/' + usuario.id"
v-confirm="Excluir este usuário?"
v-toast-success="Usuário excluído">Excluir</button>
<div v-get="/api/feed" v-trigger="visible" v-poll="30s"></div>
<input v-search="/api/busca" v-param="q" v-debounce="300ms" v-target="#resultados">
A URL pode ser um literal (/api/usuarios) ou uma expressão ('/api/usuarios/' + id). Os
atributos de apoio incluem v-target, v-swap, v-trigger, v-poll, v-body, v-params,
v-headers, v-cache, v-retry, v-timeout, v-json-path, v-template, v-offline-queue,
v-redirect, v-scroll-to, v-toast-success, v-toast-error, v-on-success, v-on-error e
v-on-complete.
O v-resource é a versão que entrega o estado da requisição como dado reativo em vez de trocar
HTML. Ele expõe data, loading, error, loaded, reload() e set() — veja a demonstração no
começo deste arquivo.
Programático.
const usuarios = await V.http.get('/api/usuarios')
const criado = await V.http.post('/api/usuarios', { nome: 'Ada' })
// Resposta completa, com status e cabeçalhos
const res = await V.http.request({ url: '/api/usuarios', retry: 2, timeout: 5000, cache: 60000 })
// Upload com progresso real
await V.http.upload('/api/arquivos', formData, { onProgress: (pct) => console.log(pct + '%') })
// Server-Sent Events e stream NDJSON
V.http.sse('/api/eventos', { message: (dados) => console.log(dados) })
await V.http.stream('/api/tokens', (linha) => console.log(linha))
Padrões, interceptors, URL base, cabeçalho de CSRF e cache ficam em V.http.defaults e
V.http.interceptors.
Enviar, validar, mostrar o carregamento e reportar o resultado — tudo declarado no próprio formulário:
<form v-submit="/api/contato" v-method="POST" v-validate
v-toast-success="Mensagem enviada" v-toast-error="Não foi possível enviar" v-reset-success>
<input name="nome" v-required>
<input name="email" type="email" v-required v-email>
<input name="telefone" v-mask="phone">
<textarea name="mensagem" v-minlength="20"></textarea>
<p v-if="$form.errors.email">{ $form.errors.email }</p>
<button type="submit" :disabled="$form.loading">
{ $form.loading ? 'Enviando…' : 'Enviar' }
</button>
</form>
$form é reativo e carrega loading, saving, success, errors, message, data, status,
dirty e progress. O conjunto de regras cobre o esperado — required, email, url, number,
min, max, minlength, maxlength, between, match, regex, date, same, different,
in, strongpassword, cpf, cnpj, cep e mais — além de regras assíncronas e das suas
próprias, via V.validator().
A Voodoo cresce além de uma página sem mudar o modelo.
V.store('carrinho', { itens: [] }, { persist: true }), lido em qualquer lugar como
$store.carrinho.V.router({ mode: 'history', routes: { '/usuarios/:id': { component: 'pagina-usuario' } } }),
com guards, params, comportamento de rolagem, v-link e v-router-view.V.use(plugin) ou app.use(plugin) para registrar directives, componentes e
serviços.v-load-visible e rotas com view buscam o HTML só quando ele é
necessário.V.i18n({ locale: 'pt-BR', messages }), depois v-t na marcação e V.t() no código.Cada um deles tem o seu guia em docs/.
O build completo traz o xray, um inspetor visual de reatividade: ele mostra a árvore de escopos,
o estado ao vivo, quais efeitos estão rodando, e os registros de eventos e de rede.
V.xray() // alterna o painel
V.xray(true) // abre
V.xray(false) // fecha
V.enableXrayShortcut() // instala apenas o atalho, sem abrir o painel
A primeira chamada também instala o atalho Ctrl+Shift+X, então o inspetor fica a uma tecla de
distância durante o desenvolvimento, sem custo nenhum enquanto ninguém aperta.
atributos HTML ─▶ Walker + MutationObserver acha os atributos v-*, monta os escopos
│
expressões ─▶ Lexer → parser Pratt → interpretador de AST sem eval, sem new Function
│
Reatividade: alvos em Proxy + efeitos leituras rastreadas, escritas na fila
│
Directives atualizam os nós reais do DOM sem Virtual DOM, sem diff
Por cima: componentes · stores · roteador · HTTP · formulários · interface · i18n · animação · gráficos
A versão longa, com as fronteiras de cada módulo e o modelo de escopos, está em
ARCHITECTURE.md.
Os benchmarks são reproduzíveis: versões de dependência fixadas, builds de produção, metodologia
publicada e ambiente registrado. O relatório completo — incluindo os casos em que a Voodoo perde
— está em benchmarks/README.md, com a metodologia, o ambiente e como
reproduzir cada medição.
O JavaScript puro é o teto aqui, e a Voodoo cobra um custo real pela produtividade que entrega. A função destes números é mostrar o tamanho desse custo, não fingir que ele é zero.
Tempo de execução. (carga de 1.000 linhas; ambiente, número de amostras e estatística estão registrados no relatório; medianas).
| Criar 1k linhas | Atualizar 1k linhas | Trocar linhas | Selecionar linha | Limpar linhas | Pico de memória | |
|---|---|---|---|---|---|---|
| JavaScript puro | ||||||
| Voodoo.js | ||||||
| Alpine.js | ||||||
| Vue | ||||||
| React | ||||||
| Preact | ||||||
| Svelte | ||||||
| Solid |
Tamanho dos bundles, medido sobre os builds versionados em :
| Build | Minificado | Gzip | Brotli |
|---|---|---|---|
voodoo.core.min.js | |||
voodoo.min.js | |||
voodoo.full.min.js |
Rode você mesmo: o arcabouço de medição e as versões exatas testadas estão em
benchmarks/.
Uma comparação honesta. Toda ferramenta aqui é boa naquilo para que foi desenhada.
| Voodoo.js | Alpine.js | HTMX | Vue 3 | React | jQuery | |
|---|---|---|---|---|---|---|
| Ponto de partida | HTML | HTML | HTML | JavaScript | JavaScript | JavaScript |
| Roda por uma tag de CDN | nativo | nativo | nativo | nativo | nativo | nativo |
| Passo de build | possível | possível | possível | recomendado | recomendado | possível |
| Renderização | DOM direto | DOM direto | HTML do servidor | Virtual DOM | Virtual DOM | manual |
| Estado reativo | nativo | nativo | — | nativo | nativo | — |
| Componentes | nativo | via pacote do ecossistema | — | nativo | nativo | — |
| Cliente HTTP | nativo | via pacote do ecossistema | nativo | via pacote do ecossistema | via pacote do ecossistema | nativo |
| Formulários + validação | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Roteador | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Interface (toast, modal, abas) | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Gráficos / i18n | nativo | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema | via pacote do ecossistema |
| Renderização no servidor | — | — | nativo (quem renderiza é o servidor) | nativo | nativo | — |
| Tamanho do ecossistema | jovem | crescendo | crescendo | grande | muito grande | muito grande |
A diferença real é filosófica. O HTMX diz que o HTML é do servidor e o navegador apenas o
encaixa. O Alpine dá ao HTML uma pitada de estado reativo e para por aí, de propósito. Vue e
React pedem que você descreva a interface em JavaScript e geram o HTML. A Voodoo mantém o
HTML como fonte da verdade e entrega a caixa de ferramentas completa — assim você raramente
precisa sair dele, e a API V espera pelos momentos em que precisar.
Encaixa bem em: painéis administrativos gerados no servidor (Laravel, Rails, Django, Spring, PHP puro); sites de conteúdo e landing pages que precisam de comportamento sem um pipeline de front-end; protótipos, em que abrir um arquivo vale mais do que qualquer arquitetura; times pequenos que não querem manter um build só para mostrar uma tabela; páginas legadas, em que a Voodoo convive com o código existente porque nunca toma conta do documento; e aplicações de página única completas, com componentes, stores e roteador.
Ditas com clareza, para que nada surpreenda depois:
v-for reaproveita elementos por chave, mas listas muito
grandes ainda renderizam todas as linhas.A documentação existe em dois idiomas: português em docs/ (completa) e inglês em
docs/en/.
| Onde | O quê |
|---|---|
docs/ | Índice do guia completo e da referência |
docs/introducao.md | O que é, para quem serve, quando não usar |
docs/instalacao.md | Bundles, CDN, npm, configuração pela tag script |
docs/inicio-rapido.md | Do arquivo vazio ao primeiro app |
docs/directives.md | Referência completa de directives |
docs/api.md | Referência completa da API V |
site/ | Código do site de documentação |
Cada exemplo é um único arquivo HTML que você abre direto no navegador. Comece por
examples/index.html.
| Exemplo | O que mostra |
|---|---|
| Tarefas | Estado, listas, edição no lugar, filtros, reordenação |
| CRUD | Um componente, formulários, validação, máscaras, toasts, atualização otimista |
| Painel | Gráficos, computados reativos, atualização periódica |
| Kanban | Arrastar e soltar entre colunas, estado persistido |
| Chat | Atualização ao vivo, rolagem, composição de mensagens |
| E-commerce | Catálogo, filtros, store de carrinho, fluxo de checkout |
| Pokédex | Consumo de API real, busca, paginação, carregamento sob demanda |
| Pacote | Para que serve |
|---|---|
voodoojs | O framework: runtime, directives, componentes, HTTP, formulários, interface, roteador, i18n |
@voodoo/cli | Criação de projetos, builds sob medida, cópia de componentes, informação de módulos |
O código inteiro é TypeScript, e todo ponto de entrada publica declarações .d.ts.
import V, { reactive, computed, type HttpResponse } from 'voodoojs'
const estado = reactive({ contador: 0 })
const dobro = computed(() => estado.contador * 2)
O foco de curto prazo é a documentação em inglês, mais exemplos, uma superfície maior de plugins e
a publicação no npm. O plano acompanhado fica em ROADMAP.md; o que já foi entregue
está registrado no CHANGELOG.md.
git clone https://github.com/kwy404/Voodoo.js.git
cd Voodoo.js && npm install
Um monorepo com npm workspaces: packages/voodoojs é o framework,
packages/cli é a CLI, mais docs/, site/ e
examples/.
| Comando | O que faz |
|---|---|
npm test | Roda a suíte inteira uma vez (vitest + jsdom) |
npm run test:watch | A mesma suíte, reexecutando enquanto você edita |
npm run coverage | Rodada de testes com relatório de cobertura |
npm run typecheck | tsc --noEmit sobre o pacote do framework |
npm run build | Gera todos os bundles do voodoojs e do @voodoo/cli |
npm run size | Informa o tamanho de cada bundle gerado |
npm run serve | Servidor estático local para os exemplos e o site |
npm run format | Prettier sobre o repositório |
Novos scripts entram com o tempo — o package.json da raiz é a lista definitiva.
O ciclo. Crie um branch a partir da main, faça a mudança, rode npm test e
npm run typecheck antes de abrir o PR, e escreva as mensagens de commit no estilo
Conventional Commits (fix:, feat:, docs:). O CI
(.github/workflows/ci.yml) roda typecheck, testes, build e a
checagem de tamanho dos bundles no Node 20 e no 22.
Onde mexer. Bug no runtime → src/runtime/. Reatividade → src/reactivity/. Expressões →
src/parser/. Directive nova → src/directives/. Componente de interface → src/ui/.
Documentação → docs/ e site/docs/.
As directives internas são registradas com defineDirective(nome, setup, { priority, terminal }),
de src/runtime/registry.ts, no arquivo correspondente dentro de src/directives/. A função
setup recebe um DirectiveContext com el, scope, expression, arg, modifiers,
evaluate(), effect(), cleanup() e walk(). O priority define a ordem de execução (maior
roda primeiro, veja a tabela PRIORITY); terminal: true impede o walker de descer nos filhos,
como fazem o v-for e o v-if.
import { defineDirective } from '../runtime/registry';
// <p v-shout="mensagem"> → mostra o valor em caixa alta
defineDirective('shout', ({ el, effect, evaluate, cleanup }) => {
effect(() => {
el.textContent = String(evaluate() ?? '').toUpperCase();
});
// Sempre libere o que você prendeu: listeners, timers, observers.
const aoClicar = () => el.classList.toggle('alto');
el.addEventListener('click', aoClicar);
cleanup(() => el.removeEventListener('click', aoClicar));
});
Essa é a API interna. A pública é V.directive(nome, hooks), que embrulha o mesmo mecanismo em
hooks de ciclo de vida no estilo do Vue (created, mounted, updated, unmounted) e entrega a
cada hook um binding com o value já avaliado, oldValue, arg e modifiers. Use
V.directive no código da aplicação; use defineDirective dentro do framework.
Os testes ficam em packages/voodoojs/test/ como *.test.ts, e rodam
em vitest com jsdom. Um teste de directive monta o HTML, percorre com um escopo e verifica o DOM:
import { describe, it, expect } from 'vitest';
import { reactive, nextTick } from '../src/reactivity';
import { Scope } from '../src/runtime/scope';
import { walk } from '../src/runtime/walker';
import '../src/core';
describe('v-shout', () => {
it('mostra o valor em caixa alta', async () => {
const dados = reactive({ mensagem: 'ola' });
const raiz = document.createElement('div');
raiz.innerHTML = '<p v-shout="mensagem"></p>';
document.body.appendChild(raiz);
walk(raiz, new Scope(dados));
expect(raiz.textContent).toBe('OLA');
dados.mensagem = 'tchau';
await nextTick();
await nextTick();
expect(raiz.textContent).toBe('TCHAU');
});
});
A regra de ouro: toda correção de bug entra junto com um teste de regressão. Se quebrou uma vez pode quebrar de novo, e o teste é o que impede.
O detalhe completo está no CONTRIBUTING.md, e todo mundo deve seguir o
CODE_OF_CONDUCT.md.
MIT © contribuidores da Voodoo.js.
Prefer to read in English? → README.en.md
JavaScript feels like magic.
Hacker News (2)
21 commits
TypeScript
46.3%
HTML
23.9%
JavaScript
23.0%
CSS
6.8%