O WingedSwift nasceu com uma ideia simples: montar HTML com uma DSL Swift, type-safe, sem browser, sem DOM, sem runtime. A saída é texto. Isso não mudou na 2.0 e não vai mudar.

O que mudou foi tudo que estava em volta dessa ideia e envelheceu mal — uma variável global controlando como o HTML era formatado, uma página que você montava na mão e torcia para não esquecer o doctype, e containers que só aceitavam array de filhos. Este artigo é o resumo do que a 2.0 trouxe, sem você precisar ler o changelog inteiro.

Se você tem um projeto rodando em 1.x e quer o passo a passo, o texto é outro: Migrando do WingedSwift 1.3.3 para o 2.0. Se você nunca usou e quer começar do zero, vá direto para o tutorial com Tailwind CSS.

Índice

Document é o novo topo da página

Na 1.x a função html { } produzia só o elemento <html>. O <!DOCTYPE html> vinha do StaticSiteGenerator, e o lang você pendurava com setAttribute — quando lembrava. O resultado é que "página correta" era algo que você montava certo, não algo que vinha pronto.

A 2.0 tem um tipo para isso:

let page = Document(lang: "pt-BR") {
    Meta(charset: "UTF-8")
    Title(content: "Início")
} body: {
    H1(content: "Olá")
}

print(page.render())
// <!DOCTYPE html>
// <html lang="pt-BR">
//   <head>
//     <meta charset="UTF-8">
//     <title>Início</title>
//   </head>
//   …

Document é dono do doctype e do lang. Tem também a forma com head: e body: já construídos, que é a que eu uso quando as duas metades vêm de funções separadas:

Document(lang: "pt-BR", head: Head { ... }, body: Body { ... })

E se algo mais precisar ser dono do doctype, page.root() devolve só o elemento <html>, com o lang já aplicado.

Detalhe que pega quem vem da 1.x: Document.render() sem argumento sai pretty, enquanto HTMLTag.render() sem argumento sai compact. Faz sentido — documento normalmente é arquivo que alguém abre — mas é uma diferença que surpreende. Para o site que vai pro ar, passe .compact explicitamente.

RenderOptions no lugar do estado global

Esse é o meu favorito, porque era o pior pedaço da 1.x. Existia um HTMLTag.xhtmlSelfClosing estático: você mexia numa variável do processo inteiro para decidir se <img> saía com barra no fim. Dois trechos de código renderizando com regras diferentes ao mesmo tempo era impossível.

Agora é valor:

page.render(.compact)   // uma linha só — é o que você publica
page.render(.pretty)    // indentado, para ler
page.render(RenderOptions(pretty: true, indent: "    ", xhtmlSelfClosing: false))

RenderOptions é Sendable e Equatable. Como é parâmetro e não estado, duas tarefas podem renderizar com configurações diferentes ao mesmo tempo sem uma pisar na outra.

Uma exceção que continua valendo: pre, code e textarea sempre saem compactos, mesmo no .pretty. Indentação dentro deles é texto visível na página.

Result builder em todo container

Na 1.x, filhos iam num array: Div(children: [ ... ]). Funciona, mas array não aceita if nem for no meio, então qualquer conteúdo condicional virava uma variável montada antes.

Na 2.0 todo container tem a forma com closure:

Section {
    H2(content: "Recursos Principais")
        .addClass("text-4xl font-bold text-center mb-16")

    Div {
        // `map` dentro do builder: compila sem ambiguidade,
        // não precisa montar o array antes.
        features.sorted { $0.order < $1.order }.map { featureCard(feature: $0) }
    }
    .addClass("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8")

    if mostrarRodape {
        P(content: "Mais recursos em breve")
    }
}

E aqui vale o aviso que economiza discussão: E aqui vale o aviso que economiza discussão: **a forma com children: não foi descontinuada. não foi descontinuada.** As duas convivem no mesmo arquivo e produzem o mesmo HTML. Onde já existe um array em mãos, children: é mais direto; onde tem if, for ou map, o builder ganha de longe. Não é migração obrigatória, é escolha por trecho.

Swift 6.0 e Xcode 16 viraram requisito

A biblioteca compila em language mode 6, então o compilador precisa ser 6.0 ou mais novo. Isso é sobre o compilador, não sobre o seu manifesto: um pacote com swift-tools-version: 5.9 consegue depender do WingedSwift 2.0 numa toolchain 6.x sem problema.

O que veio junto: a árvore de tags O que veio junto: a árvore de tags **não é Sendable**, e agora isso é explícito. HTMLTag é uma classe mutável — monte e renderize a página dentro de uma task só, e passe a String resultante entre isolation boundaries. Attribute, RenderOptions, StaticSiteGenerator e RSSGenerator são Sendable.

A CLI winged

A 2.0 trouxe um executável junto com a biblioteca:

git clone https://github.com/micheltlutz/Winged-Swift.git
cd Winged-Swift && swift build -c release
# o binário fica em .build/release/winged

winged new MeuSite --tailwind
cd MeuSite
winged build          # gera em dist/
winged serve --watch  # http://localhost:8000, rebuild a cada alteração

São três subcomandos:

  • winged new <nome> cria o esqueleto do projeto. Aceita --tailwind, --title, --description, --lang e --path.
  • winged build compila e roda o projeto, gerando em dist/. Aceita --output, --path e --release.
  • winged serve sobe um servidor local. Aceita --port (8000), --output (dist), --watch e --no-build.

Um detalhe que vale saber antes de apontar a CLI para um projeto existente: o winged build descobre o executável lendo o Package.swift (primeiro produto executável, ou o primeiro target executável se não houver produto declarado) e depois roda esse binário passando o diretório de saída como primeiro argumento. Ou seja, ele espera um projeto que leia o output dir de CommandLine.arguments — que é o que o winged new gera. Um projeto que escreve num caminho fixo compila e roda, mas o site cai no caminho fixo e o CLI reporta zero páginas em dist/.

Para esses casos existe o --no-build, que pula o build e serve o que já está lá:

winged serve --output output --no-build

O que não mudou

Nada foi removido na 2.0. Código escrito na 1.x continua compilando e gerando o mesmo HTML.

Cinco membros ficaram marcados como deprecated e saem na 3.0:

  • HTMLTag.xhtmlSelfClosing (a global)
  • render(pretty:indentLevel:)
  • renderCompact()
  • renderPretty(indentLevel:)
  • HTMLTag(_:_:) com a closure de atributos sem rótulo, que era ambígua com o builder de filhos

Repare no que não está nessa lista: html { }, Div(children:) e generate(page:to:pretty:doctype:) seguem vivos e sem aviso de depreciação. Document e RenderOptions são o caminho novo, não um ultimato.

O resto continua igual: conteúdo e atributos escapados por padrão (Script e Style são a exceção, porque escapar corromperia JS e CSS), void elements sem tag de fechamento, Fragment para agrupar sem <div> extra, RawHTML para injetar markup pronto, e os helpers encadeáveis addClass, setId, dataAttribute, ariaAttribute.

Por onde seguir

Recursos