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
- 1. Toolchain: Swift 6.0 ou mais novo
- 2. A página: de html para Document
- 3. Renderização: de estado global para RenderOptions
- 4. Filhos: de array para result builder
- 5. Adotando a CLI num projeto que já existe
- O prazo: a 3.0
- Recursos
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:
| Item | Situação na 2.0 |
|---|---|
| Compilador abaixo de Swift 6.0 | Bloqueia. É o único requisito duro. |
html { } | Segue vivo, sem aviso de depreciação. |
Div(children: [...]) e afins | Seguem vivos, sem aviso. Não é forma legada. |
generate(page:to:pretty:doctype:) | Segue vivo, sem aviso. |
HTMLTag.xhtmlSelfClosing | Deprecated. 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ótulo | Deprecated. 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.0O 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.compactexplicitamente 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 olangdeixarem 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.x | 2.0 |
|---|---|
render(pretty: true) | render(.pretty) |
render(pretty: false) | render(.compact) |
renderPretty() | render(.pretty) |
renderCompact() | render(.compact) |
HTMLTag.xhtmlSelfClosing = true | RenderOptions(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-buildO --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 deprecatedCada 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.


