Este texto é para quem já tem um projeto rodando em WingedSwift 1.x e quer subir para a 2.0 sem descobrir surpresa no meio do caminho. Se você quer entender o que a 2.0 trouxe antes de mexer em código, comece pelo resumo das novidades. Se está começando um projeto do zero, pule direto para o tutorial com Tailwind CSS.

Começo pela boa notícia, porque ela muda o tom da migração inteira: nada foi removido na 2.0. Seu projeto 1.3.3 compila do jeito que está e gera o mesmo HTML. O que existe é um punhado de membros marcados como deprecated, que saem só na 3.0. A migração aqui é dívida técnica agendada, não incêndio.

Índice

O que quebra e o que não quebra

Só uma coisa é obrigatória para subir de versão, e ela nem está no seu código:

ItemSituação na 2.0
Compilador abaixo de Swift 6.0Bloqueia. É o único requisito duro.
html { }Segue vivo, sem aviso de depreciação.
Div(children: [...]) e afinsSeguem vivos, sem aviso. Não é forma legada.
generate(page:to:pretty:doctype:)Segue vivo, sem aviso.
HTMLTag.xhtmlSelfClosingDeprecated. Sai na 3.0.
render(pretty:indentLevel:)Deprecated. Sai na 3.0.
renderCompact() e renderPretty()Deprecated. Saem na 3.0.
HTMLTag(_:_:) com atributos sem rótuloDeprecated. Sai na 3.0.

Ou seja: bump da versão, swift build, e você tem uma lista de warnings para resolver no seu ritmo. Comece por aí:

// Package.swift
dependencies: [
    .package(url: "https://github.com/micheltlutz/Winged-Swift.git", from: "2.0.0")
]

1. Toolchain: Swift 6.0 ou mais novo

A biblioteca compila em language mode 6, então o compilador precisa ser 6.0+ (Xcode 16+). Confira com swift --version antes de qualquer outra coisa — é o único ponto que impede o build de acontecer.

Repare que isso é sobre o compilador, não sobre o seu manifesto. Um pacote com swift-tools-version: 5.9 depende do WingedSwift 2.0 sem problema, desde que a toolchain seja 6.x. Se for começar um projeto novo, aí sim vale nascer em 6.0:

// swift-tools-version: 6.0

O efeito colateral que aparece em projeto com concorrência: a árvore de tags O efeito colateral que aparece em projeto com concorrência: a árvore de tags **não é Sendable**. HTMLTag é classe mutável. Monte e renderize a página dentro de uma task só e passe a String resultante entre isolation boundaries — não a árvore. Attribute, RenderOptions, StaticSiteGenerator e RSSGenerator são Sendable e atravessam sem reclamação.

2. A página: de html para Document

Na 1.x a página era montada na mão. html { } devolve só o elemento <html>, então o doctype vinha do gerador e o lang era pendurado depois:

// 1.x
let page = html {
    Head(children: [
        Meta(charset: "UTF-8"),
        Title(content: "Início")
    ])
    Body(children: [
        H1(content: "Olá")
    ])
}
page.setAttribute(key: "lang", value: "pt-BR")

let generator = StaticSiteGenerator(outputDirectory: "./output")
try generator.generate(page: page, to: "index.html")

Na 2.0 o Document é dono do doctype e do idioma:

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

let generator = StaticSiteGenerator(outputDirectory: "./output")
try generator.generate(document: page, to: "index.html", options: .compact)

Quando as duas metades vêm de funções separadas, existe a forma com head: e body: já prontos — é a que uso nos meus geradores:

Document(lang: "pt-BR", head: montarHead(config), body: montarBody(config))

Duas armadilhas nessa troca:

  • Document.render() sem argumento sai pretty; HTMLTag.render() sem argumento sai compact. Se o seu 1.x publicava HTML minificado, passe .compact explicitamente ou o arquivo vai crescer.
  • O overload antigo generate(page:to:pretty:doctype:) continua existindo e não está deprecated. Se você não quiser mexer nisso agora, não precisa. O ganho de trocar é o doctype e o lang deixarem de ser responsabilidade sua.

E se alguma outra coisa no seu pipeline for dona do doctype, page.root() devolve só o <html>, com o lang já aplicado.

3. Renderização: de estado global para RenderOptions

Este é o ponto que eu recomendo migrar primeiro, mesmo sendo deprecated e não removido — porque é o único que pode produzir HTML diferente do esperado sem erro de compilação.

Na 1.x a formatação era decidida por uma variável estática do processo, mais três métodos:

// 1.x
HTMLTag.xhtmlSelfClosing = true      // muda o processo inteiro
let a = page.render(pretty: true)
let b = page.renderCompact()
let c = page.renderPretty()

Na 2.0 tudo isso é um valor passado por parâmetro:

// 2.0
let a = page.render(.pretty)
let b = page.render(.compact)
let c = page.render(RenderOptions(pretty: true, indent: "    ", xhtmlSelfClosing: true))

O mapeamento é direto:

1.x2.0
render(pretty: true)render(.pretty)
render(pretty: false)render(.compact)
renderPretty()render(.pretty)
renderCompact()render(.compact)
HTMLTag.xhtmlSelfClosing = trueRenderOptions(xhtmlSelfClosing: true) passado no render

RenderOptions é Sendable e Equatable, e como é valor, duas tarefas podem renderizar com configurações diferentes ao mesmo tempo. Era exatamente isso que a global impedia.

Uma regra que continua igual: pre, code e textarea saem sempre compactos, mesmo no .pretty. Indentação dentro deles é texto visível.

4. Filhos: de array para result builder

Este item é opcional e eu faço questão de repetir, porque é o que mais gera dúvida: a forma com children: não foi descontinuada, não vai sair na 3.0, e não é código legado. As duas formas convivem no mesmo arquivo e produzem o mesmo HTML.

// as duas formas, lado a lado, geram o mesmo markup
Div(children: [
    H2(content: "Recursos"),
    P(content: "Descrição")
])

Div {
    H2(content: "Recursos")
    P(content: "Descrição")
}

Onde o builder ganha de verdade é em conteúdo condicional ou derivado de coleção, que no array exigia montar tudo antes:

// 1.x — precisa da variável intermediária
let cards = features.sorted { $0.order < $1.order }.map { featureCard(feature: $0) }
Div(children: cards)

// 2.0 — `map`, `for` e `if` compilam dentro do builder
Div {
    features.sorted { $0.order < $1.order }.map { featureCard(feature: $0) }

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

Minha regra na prática: array quando já tenho a coleção pronta em mãos e o trecho é uma sequência fixa de irmãos com muito .addClass encadeado; builder quando tem if, for ou map. Migrar arquivo inteiro só para trocar a sintaxe não paga.

O único membro depreciado nessa área é outro: HTMLTag(_:_:) com a closure de atributos sem rótulo, que ficou ambígua com o builder de filhos. A correção é dar o nome ao parâmetro:

// 1.x, agora ambíguo
HTMLTag("a") { Attribute(key: "href", value: "/") }

// 2.0
HTMLTag("a", attributes: [Attribute(key: "href", value: "/")])

5. Adotando a CLI num projeto que já existe

A CLI winged é nova na 2.0 e não é obrigatória — dá para seguir com o seu build.sh para sempre. Mas se você quiser usá-la num projeto que já existe, tem um detalhe de layout que economiza uma tarde.

O winged build descobre o executável lendo o Package.swift (o primeiro produto executável, ou o primeiro target executável se não houver produto declarado) e então roda esse binário passando o diretório de saída como primeiro argumento. É o layout que o winged new gera.

Um projeto 1.x típico não faz isso: ele escreve num caminho fixo, tipo Config.outputDir. Nesse caso o winged build compila, roda, o site cai no caminho fixo — e a CLI reporta zero páginas em dist/, porque olhou no lugar errado. Não é erro, é desencontro.

São dois caminhos. O primeiro é ensinar seu gerador a aceitar o argumento:

let outputDir = CommandLine.arguments.dropFirst().first ?? Config.outputDir
let generator = StaticSiteGenerator(outputDirectory: outputDir)

O segundo, se você não quer mexer no gerador, é usar a CLI só como servidor e deixar o build com quem já fazia:

./build.sh
winged serve --output output --no-build

O --no-build é o que faz isso funcionar: pula o build da CLI e serve o que já está lá.

O prazo: a 3.0

Os membros depreciados carregam @available(*, deprecated, message:) e a política do projeto é mantê-los por uma versão maior. Traduzindo: eles funcionam em toda a linha 2.x e somem na 3.0.

Se você quiser resolver de uma vez, o caminho mais curto é compilar e deixar o compilador fazer a lista:

swift build 2>&1 | grep deprecated

Cada warning aponta o substituto na própria mensagem. Na maioria dos projetos isso é meia hora de trabalho — quase tudo cai no item 3 deste texto.

Recursos