eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsCompilação
Fluxo de build do eQuantic.UI
Editar esta página
18 min de leitura
Este documento descreve o fluxo de build do eQuantic.UI, demonstrando como o framework mantém zero dependências externas para o consumidor.
Fluxo visual
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
┌─────────────────────────────────────────────────────────────────────────────┐
│ DESENVOLVIMENTO (árvore de código) │
└─────────────────────────────────────────────────────────────────────────────┘
src/eQuantic.UI.Runtime/src/**/*.ts src/eQuantic.UI.Sdk/Resources/boot.ts
│ │
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────┐
│ dotnet build │
│ (eQuantic.UI.Server) │
│ │
│ Target ResolveBunForServer: │
│ └─ o bun embarcado do projeto │
│ Runtime.{Os}{Arch} │
│ (extraído do .zip, +x) │
│ │
│ Target BundleRuntime, antes de │
│ todo build: │
│ └─ "$(_BunPath)" build boot.ts │
│ --outfile wwwroot/runtime.js │
└──────────────────────────────────┘
│
▼
wwwroot/runtime.js ──── o ÚNICO bundle: embarcado no Server.dll e servido em
│ /_equantic/runtime.js, e o arquivo que um build da árvore
│ copia ao lado dos módulos de um app
▼
┌──────────────────────────────────┐
│ dotnet pack │
│ (todo projeto empacotável) │
│ │
│ Server: CollectServedRuntime │
│ └─ wwwroot/runtime.js → │
│ tools/runtime/runtime.js │
│ Sdk: CollectBuildOutput │
│ └─ eqc + eqicon → │
│ tools/net10.0/ │
└──────────────────────────────────┘
│
▼
*.nupkg — bin/Release/ no CI, ou um feed local (veja abaixo); a versão vem do
Directory.Build.props (ou da tag de release), nunca de um arquivo na árvore
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONSUMIDOR (projeto cliente) │
└─────────────────────────────────────────────────────────────────────────────┘
MyApp.csproj
└─ <Project Sdk="eQuantic.UI.Sdk"> ← essa linha é o pipeline de build inteiro
global.json
└─ { "msbuild-sdks": { "eQuantic.UI.Sdk": "<version>" } } ← o ÚNICO lugar onde a versão vive
(sem PackageReference: o SDK adiciona os dele)
│
▼
┌──────────────────────────────────┐
│ dotnet restore │
│ │
│ O NuGet instala os pacotes: │
│ ├─ eQuantic.UI.Sdk │
│ ├─ eQuantic.UI.Primitives │
│ ├─ eQuantic.UI.Code │
│ ├─ eQuantic.UI.Components │
│ ├─ eQuantic.UI.Charts │
│ ├─ eQuantic.UI.Web │
│ ├─ eQuantic.UI.Server │ ◄── o runtime que ele serve, também
│ │ └─ tools/runtime/runtime.js │ como arquivo para o SDK copiar
│ ├─ eQuantic.UI.Generators │ (analisador)
│ └─ eQuantic.UI.Runtime.OsxArm64 │ ◄── o Bun vem embarcado aqui! UM pacote,
│ └─ tools/bun/bun-darwin.zip │ escolhido por sistema + arquitetura
└──────────────────────────────────┘
> **Qual Bun?** O `src/eQuantic.UI.Runtime/bun-toolchain.json` guarda a versão e um SHA-256 por
> plataforma, conferidos a cada execução dos testes. Os digests provam que os bytes comitados são os
> que o release publicou; a versão é verificada extraindo o binário do host e rodando ele, porque um
> manifesto que ninguém compara com aquilo que ele descreve acaba se afastando dele.
>
> Os seis pacotes de plataforma (`Osx64`, `OsxArm64`, `Win64`, `WinArm64`, `Linux64`, `LinuxArm64`)
> carregam o mesmo build; um Bun diferente por arquitetura é como se consegue um bundle que só falha
> na máquina de uma pessoa.
>
> Para atualizar: troque os arquivos `.zip`, rode os testes uma vez com `EQ_UPDATE_BUN_MANIFEST=1`, e
> leia o diff. Uma versão que mudou com digests que não mudaram é um erro, e o contrário também.
│
▼
┌────────────────────────────────────────┐
│ dotnet build │
│ │
│ wwwroot/_equantic/** não é Content: │
│ este build o escreve (passo 11) │
│ │
│ Na ordem em que os targets rodam: │
│ │
│ 1. EQuanticVectors │ (quando Assets/ existe)
│ └─ eqicon: *.svg → C#, antes que a │
│ compilação o leia │
│ │
│ 2. CoreCompile │
│ └─ o C# do app, target do .NET │
│ │
│ 3. ResolveBunZipPath │
│ └─ $(PkgeQuantic_UI_Runtime_ │
│ OsxArm64)/tools/bun/*.zip │
│ │
│ 4. EnsureBunExtracted │
│ ├─ Descompacta se preciso │
│ └─ chmod +x (Unix) │
│ │
│ 5. ResolveBunPath │
│ └─ Define $(BunPath) │
│ │
│ 6. InstallBunPackages │
│ ├─ <BunPackage> → bun add │
│ └─ Symlink node_modules │
│ │
│ 7. EQuanticRequireCompiler │
│ └─ eqc faltando → EQ4002 │
│ │
│ 8. CompileEQuanticUI │
│ └─ argumentos, um por linha, em │
│ obj/…/equantic.eqc.rsp │
│ └─ dotnet eqc.dll │
│ @obj/…/equantic.eqc.rsp │
│ │
│ 9. CopyEQuanticRuntime │ ◄── entrega do runtime.js
│ └─ o runtime.js servido, │
│ do pacote Server, │
│ para wwwroot/_equantic/ │
│ │
│ 10. EQuanticWebAppIcon │ (quando há um AppIcon declarado)
│ └─ eqicon → │
│ wwwroot/_equantic/icons │
│ │
│ 11. _EQuanticOutputAsStaticWebAssets │ ◄── o gancho dos static web assets
│ └─ wwwroot/_equantic/ como os │
│ passos 8 a 10 o escreveram, sem │
│ os mapas, definido como static │
│ web assets │
│ │
│ depois o pipeline de static web │
│ assets comprime e escreve os │
│ manifestos: os arquivos deste build │
└────────────────────────────────────────┘
│
▼
wwwroot/_equantic/
├─ runtime.js (o bundle do Server, byte a byte)
├─ *.js (componentes compilados, e os chunks que eles compartilham)
└─ *.js.map (só em Debug, por padrão)
│
▼
┌──────────────────────────────────┐
│ dotnet run │
│ │
│ O servidor serve: │
│ ├─ runtime.js (do Server.dll) │
│ └─ *.js (de wwwroot/_equantic) │
└──────────────────────────────────┘
│
▼
O browser carrega a aplicação
Origem do Bun por componente
Componente
Origem do Bun
Runtime (-t:TestRuntime)
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (árvore de código)
Server (build do pacote)
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (árvore de código)
SDK (consumidor)
$(PkgeQuantic_UI_Runtime_{Os}{Arch})/tools/bun/ (cache do NuGet)
SDK (árvore de código)
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ ao lado dos arquivos do SDK
Requisitos do consumidor
O consumidor só precisa de:
•
.NET SDK 10.0
•
dotnet restore + dotnet build
Nenhuma instalação de Node.js, npm ou Bun global é necessária.
Uma biblioteca de classes compartilhada pelos projetos de uma solução
Desde 0.2.0-preview.62
Um app que cresce além de um projeto compartilha código por uma biblioteca de classes: os tokens da marca, um record, um componente que dois apps usam. O app referencia a biblioteca com um ProjectReference e não escreve mais nada sobre ela.
1
2
3
4
5
<Project Sdk="eQuantic.UI.Sdk">
<ItemGroup>
<ProjectReference Include="../Shop.Common/Shop.Common.csproj" />
</ItemGroup>
</Project>
O que uma página USA da biblioteca chega ao browser, como os tipos do próprio app:
A página
O bundle contém
lê Palette.Cobalt, um membro estático
um módulo Palette
executa new Cursor(3), um record
um módulo Cursor, igual por valor lá também
usa new Wordmark(), um componente
um módulo Wordmark, renderizado no servidor e reconstruído na hidratação
recebe um Receipt como resposta de um Server Action
o twin do próprio record, que lê um long, uma data e um Guid como o que são
Um tipo que esses tipos citam atravessa com eles, seja qual for o projeto que o declara. Um record de um projeto Domain que a biblioteca referencia e o app não também é um módulo.
Só o que o código do browser alcança é compilado. O build escreve os tipos do próprio app e segue o que eles importam, até não sobrar import sem módulo. Uma classe da biblioteca que nenhuma página cita nunca é compilada para o browser, então um projeto Domain ou Persistence custa o que as páginas usam dele e não reporta nada, tenha o que tiver. O corpo de um Server Action não atravessa, e um método marcado [ServerOnly] também não, então os dois citam o código de servidor da biblioteca à vontade: um Server Action que mapeia o Order do Domain para o Order do próprio app alcança o do app e nada do Domain. Um nome para o qual o build não escreve nada também não alcança nada: dentro de um nameof, num atributo, e uma constante, que é escrita como o seu valor.
Um tipo alcançado segue as regras que os tipos do próprio app seguem:
•
O que um browser não consegue executar é recusado no PRÓPRIO arquivo da biblioteca, com o diagnóstico que o mesmo código gera no app. Uma classe que lê um arquivo é EQ2004.
•
Um tipo ou um membro marcado [ServerOnly] é recusado onde a página o cita (EQ2010).
•
Um módulo tem o nome do seu tipo, então dois tipos ALCANÇADOS com o mesmo nome são recusados, com os dois arquivos na mensagem (EQ1005). Um tipo que nada alcança não colide com nada.
Um caso ficou mais rígido do que era. Um record de um projeto referenciado que um Server Action responde atravessava só como dado, tivesse o que tivesse, porque não tinha twin nenhum. O twin dele agora é o record inteiro, como o de um record do próprio app, então um método dele que um browser não consegue executar falha o build naquele método, mesmo que nenhuma página o chame. Até que um membro possa ficar fora de um twin (#826), mantenha esse método fora do tipo que a página lê: um método de extensão numa classe própria é chamado do mesmo jeito.
Cada projeto mantém o seu próprio escopo de C#: as diretivas global using e os tipos internos de uma biblioteca são dela, e os do app são do app. Um tipo interno que a biblioteca abre para o app com InternalsVisibleTo atravessa como um público. Essa declaração cita o ASSEMBLY do app, e o build conhece o app pelo nome que o projeto dele avalia, então um app cujo arquivo de projeto define <AssemblyName> diferente do próprio nome é o amigo que a biblioteca citou.
As strings dela
Um componente da biblioteca lê as suas strings do .resx da própria biblioteca. Os catálogos do app trazem as chaves que ele leu, em cada cultura para a qual a biblioteca tem um .resx, e nenhuma outra chave daquele catálogo. O app e a biblioteca podem ter, cada um, um Strings com um Title: cada leitura mostra o texto do seu próprio projeto.
Escrever a biblioteca
A biblioteca é um projeto .NET comum. Uma que contém componentes referencia a biblioteca de componentes, na versão do SDK do app, e traz as factories para o escopo:
1
2
3
4
5
6
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="eQuantic.UI.Components" Version="(a versão do SDK)" />
<Using Include="eQuantic.UI.Components.UI" Static="true" />
</ItemGroup>
</Project>
Um projeto que não contém componente, um Domain, não referencia nada do eQuantic.UI.
O que ainda não existe
•
Um componente da biblioteca não tem factory, então a página escreve new Wordmark() (#818).
•
Uma [Page] ou um [ServerAction] declarados na biblioteca (#819).
•
O que um source generator escreve na biblioteca não chega ao build (#820).
•
Um tipo de um PACOTE, do qual o build não tem a fonte, continua sendo um nome que nada define quando uma página lê um membro dele (#822).
samples/SharedLibrary é esta seção em três projetos: um app web, a biblioteca que ele referencia e o projeto que essa biblioteca referencia.
Desenvolver o framework: sem pacote nenhum
Um build a partir da própria árvore do repositório — os samples, um scaffold em teste — nunca passa por um pacote. (O site JÁ NÃO é um destes: o equantic-web consome pacotes publicados via global.json, como qualquer outro consumidor. A secção depois da próxima é como um repo fora desta árvore consegue o loop sem pacote mesmo assim.) O Sdk.props vê a árvore de código ao lado dele e troca cada PackageReference por um ProjectReference; o Sdk.targets executa o eqc e o eqicon que o grafo acabou de construir, e copia o runtime.js de src/eQuantic.UI.Server/wwwroot/runtime.js, que o BundleRuntime do projeto Server escreve antes de todo build do Server. Não há feed local para encher, nem cache para limpar, nem passo de pack entre uma edição e o sample que a mostra:
1
dotnet build samples/DefaultUIDashboard
O mecanismo — IsEQuanticDevMode, as arestas das ferramentas, _EqSourceTree — está descrito em Arquitetura de pacotes. A seção abaixo é para a OUTRA pergunta: a mudança sobrevive ao caminho real do consumidor?
O mesmo loop a partir de OUTRO repositório
Uma app desenvolvida ao lado do SDK — um repo consumidor, não um sample — quer o mesmo que os samples têm: editar o framework, reconstruir a app, ver. Ela pode ter isso, e o mecanismo é o que os samples já usam, não algo novo. O _DevRootDir é relativo a onde o Sdk.props ESTÁ FISICAMENTE, portanto importar o SDK por caminho a partir de qualquer sítio faz a sonda cair dentro da árvore e liga o dev mode.
Quatro coisas aqui não se adivinham, e cada uma foi medida em vez de deduzida.
O dev mode alcança só o projeto que importa o SDK. Numa solução com um head e bibliotecas, trocar só o head dá a pior metade do negócio: a app CORRE contra a sua árvore de trabalho enquanto as bibliotecas ainda COMPILAM contra o pacote fixo. No momento em que você muda uma API — que é para isso que o loop existe — as bibliotecas compilam limpas e a app falha em runtime com um membro que não existe. Todo projeto que referencia eQuantic.UI tem de bifurcar.
A propriedade tem de estar definida ANTES do import do SDK, portanto o Directory.Build.props chega tarde demais para o head. O Directory.Build.props é importado pelos props do próprio Microsoft.NET.Sdk, que o SDK do eQuantic importa no topo — quando ele é lido, o <Import> condicional do head já foi avaliado e saltado. Isto falha EM SILÊNCIO: você fica com os pacotes publicados a acreditar que está na árvore, e descobre enquanto depura outra coisa.
Um ficheiro de props partilhado, importado pelo head E pelo Directory.Build.props, precisa de uma sentinela, senão o MSBuild levanta MSB4011 no segundo import.
O braço dos pacotes publicados precisa de global.json. Sem msbuild-sdks a fixar uma versão, o fallback morre com MSB4236: The SDK could not be found e uma mensagem de resolver que não menciona o pin em falta.
A forma inteira, quatro ficheiros:
1
2
3
4
5
6
7
8
9
10
<!-- EQuanticDev.props — a definição única, lida pelo head E pelo Directory.Build.props -->
<Project>
<PropertyGroup>
<_EqDevPropsImported>true</_EqDevPropsImported>
</PropertyGroup>
<PropertyGroup Condition="'$(EQuanticUIDevRoot)' == ''">
<_EqDevCandidate>$(MSBuildThisFileDirectory)../equantic-ui</_EqDevCandidate>
<EQuanticUIDevRoot Condition="Exists('$(_EqDevCandidate)/src/eQuantic.UI.Primitives/eQuantic.UI.Primitives.csproj')">$(_EqDevCandidate)</EQuanticUIDevRoot>
</PropertyGroup>
</Project>
1
2
3
4
5
<!-- Directory.Build.props -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)EQuanticDev.props" Condition="'$(_EqDevPropsImported)' != 'true'" />
...
</Project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- O HEAD: um <Project> pelado, porque o atributo Sdk não aceita Condition -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)../../EQuanticDev.props" />
<Import Project="$(EQuanticUIDevRoot)/src/eQuantic.UI.Sdk.Native/Sdk/Sdk.props" Condition="'$(EQuanticUIDevRoot)' != ''" />
<Import Project="Sdk.props" Sdk="eQuantic.UI.Sdk.Native" Condition="'$(EQuanticUIDevRoot)' == ''" />
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<Import Project="$(EQuanticUIDevRoot)/src/eQuantic.UI.Sdk.Native/Sdk/Sdk.targets" Condition="'$(EQuanticUIDevRoot)' != ''" />
<Import Project="Sdk.targets" Sdk="eQuantic.UI.Sdk.Native" Condition="'$(EQuanticUIDevRoot)' == ''" />
</Project>
1
2
3
4
5
6
7
8
9
<!-- Toda BIBLIOTECA que referencia eQuantic.UI bifurca do mesmo jeito -->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup Condition="'$(EQuanticUIDevRoot)' == ''">
<PackageReference Include="eQuantic.UI.Primitives" Version="$(EQuanticUIVersion)" />
</ItemGroup>
<ItemGroup Condition="'$(EQuanticUIDevRoot)' != ''">
<ProjectReference Include="$(EQuanticUIDevRoot)/src/eQuantic.UI.Primitives/eQuantic.UI.Primitives.csproj" />
</ItemGroup>
</Project>
Confira que pegou, em vez de assumir — a falha é silenciosa:
1
2
3
dotnet msbuild src/App/App.csproj -getProperty:IsEQuanticDevMode # true
dotnet msbuild src/Lib/Lib.csproj -t:ResolveReferences -getItem:ReferencePath | grep Primitives
# → .../equantic-ui/src/eQuantic.UI.Primitives/bin/... e NÃO ~/.nuget/packages/
O que custa: o primeiro build compila o framework inteiro a partir do código, e uma árvore partida passa a ser um build de app partido. A guarda Exists(...) é o que mantém a CI e um clone limpo nos pacotes publicados.
Acrescentar o seu próprio projeto à sua própria solução pode arrastar este repo para dentro dela. O dotnet sln add lê o csproj, vê os ProjectReference do dev mode e puxa o eQuantic.UI.Primitives, o Components e o pack de ícones para o SEU .sln. Inofensivo até alguém construir o braço de fallback, onde esses caminhos não existem e a solução deixa de abrir. Confira o .sln depois de acrescentar um projeto.
Consumir um SDK não lançado a partir de pacotes locais
Since 0.2.0-preview.45
Valide uma mudança do framework num app REAL — pacotes, global.json, restore, o caminho inteiro do consumidor — sem cortar versão nem correr pipeline. O primeiro consumidor do Trilho W provou a sua migração assim: frames byte-idênticos entre o build por pacotes e o build por source.
No checkout do framework:
1
dotnet pack -c Release -p:PackageVersion=1.0.0-dev -o artifacts/local-feed
1.0.0-dev nunca colide com uma versão publicada, os dois SDKs derivam a versão de cada pacote irmão da sua própria, e nem os samples nem os testes empacotam — o feed é só produto. No app:
•
global.json: { "msbuild-sdks": { "eQuantic.UI.Sdk.Native": "1.0.0-dev" } } (ou eQuantic.UI.Sdk)
•
nuget.config: <clear />, depois o feed local PRIMEIRO, nuget.org a seguir
•
o ficheiro de projeto: Sdk="eQuantic.UI.Sdk.Native", sem atributo de versão
Depois de cada repack, limpe a cópia em cache e restaure de novo — o NuGet nunca baixa outra vez uma versão que já tem. O ClearEQuanticCache vive no Directory.Build.targets do framework, então corra a partir desse checkout (ele alcança todo projeto pela solução); ele remove todo pacote equantic.* do cache do NuGet reativamente (uma lista mantida à mão já deixou passar todos os pacotes nativos):
1
dotnet msbuild -t:ClearEQuanticCache
1
dotnet restore --force
Prova headless para um app nativo, sem janela: --Photon:ScreenshotPath out.png.
Arquivos-chave
Arquivo
Responsabilidade
Sdk/Sdk.props
Adiciona os pacotes do framework — ou, ao lado de uma árvore de código, os PROJETOS do framework e as duas arestas das ferramentas (IsEQuanticDevMode)
Sdk/Sdk.targets
Encontra as ferramentas (_EqSourceTree: o build da árvore ou o tools/ do pacote), resolve o Bun, instala os itens <BunPackage>, compila os componentes (com os source maps que o EQuanticSourceMaps pede, e nenhum num publish), copia o runtime.js, escreve os vetores e o ícone do app, define a pasta de saída como static web assets; falha pelo nome quando uma ferramenta falta (EQ4001, EQ4002)
Runtime.csproj
O TypeScript do runtime e a prova dele: o -t:TestRuntime roda bun install --frozen-lockfile, tsc e vitest run pelo bun embarcado. Não empacota nada e não é empacotado (IsPackable=false)
Server.csproj
O único escritor do runtime: empacota boot.ts → wwwroot/runtime.js antes de todo build, embarca o arquivo (servido em /_equantic/runtime.js) e publica o mesmo arquivo como tools/runtime/runtime.js
Runtime.{Os}{Arch}.csproj
Empacota o executável compactado do Bun para um sistema e arquitetura
Sdk.csproj
Empacota Sdk/, e recolhe eqc e eqicon em tools/net10.0/ a partir dos projetos de ferramenta que referencia como arestas
Targets do MSBuild (ordem de execução)
No SDK (consumidor)
Na ordem em que rodam num dotnet build:
1.
EQuanticRequireIconTool / EQuanticVectors - quando Assets/ existe, antes da compilação C#: falha com EQ4001 se o eqicon.dll falta, senão escreve Assets/**/*.svg como Vectors.g.cs
2.
CoreCompile - a compilação C# do app, target do próprio SDK do .NET; tudo abaixo roda depois dele
3.
ResolveBunZipPath - encontra o .zip do Bun no cache do NuGet (ou ao lado do SDK numa árvore de código)
4.
EnsureBunExtracted - extrai o executável se preciso
5.
ResolveBunPath - define $(BunPath) para uso posterior
6.
InstallBunPackages - instala os itens <BunPackage> via bun add (veja BunPackage)
7.
EQuanticRequireCompiler - falha com EQ4002 quando o eqc.dll não está onde o EqcCliPath diz
8.
CompileEQuanticUI - transpila C# → TypeScript → JavaScript
9.
CopyEQuanticRuntime - copia o runtime.js servido, do pacote Server (ao lado de uma árvore de código, o wwwroot/runtime.js do projeto Server), para wwwroot/\_equantic/, e apaga um equantic.css que um SDK anterior tenha deixado ali
10.
EQuanticWebAppIcon - quando há um ícone de app declarado: escreve os tamanhos do ícone e o manifesto web
11.
_EQuanticOutputAsStaticWebAssets - define wwwroot/_equantic/ como static web assets a partir do que os passos 8 a 10 escreveram, sem os mapas
Os passos 3 a 10 rodam onde rodam por causa do último. O _EQuanticOutputAsStaticWebAssets se pendura em $(GenerateComputedBuildStaticWebAssetsDependsOn), o gancho do próprio pipeline de static web assets para os assets que um build gera, que roda depois do CoreCompile e antes de o pipeline resolver as entradas dele, e depende do compilador, da cópia do runtime e do ícone do app. O CompileEQuanticUI continua declarando BeforeTargets="Build", e nessa hora já rodou.
A pasta não é Content pela mesma razão. O pipeline registra o que o wwwroot/ contém enquanto o projeto é avaliado, e o build depois reescreve esta pasta: um arquivo registrado e depois apagado fazia falhar a compressão do publish seguinte. Foi assim com todo mapa que um build Release remove, e com todo chunk compartilhado que o bun renomeia quando muda um componente que duas páginas compartilham, na sequência mais comum que existe: editar, depois publicar. Definida a partir do disco depois que os escritores dela rodaram, a pasta chega à compressão, aos manifestos e ao publish como este build a escreveu, e só isso. Um publish com --no-build carrega o manifesto do build em vez disso, então leva os arquivos que o último build escreveu. Com EnableEQuanticUICompilation=false nenhuma das duas metades se aplica, e os arquivos da pasta continuam sendo Content comum.
No SDK (publish do consumidor)
1.
_EQuanticKeepSourceMapsOutOfPublish - depois do ComputeResolvedFilesToPublishList: tira do ResolvedFileToPublish todo .map em wwwroot/_equantic/, diga o EQuanticSourceMaps o que disser. Os mapas também não estão entre os web assets acima, então esta é a segunda linha de defesa
No Runtime (-t:TestRuntime)
1.
ResolveBunForRuntime - encontra o Bun embarcado na árvore de código
2.
TestRuntime - bun install --frozen-lockfile, depois tsc e vitest run. Nada é empacotado aqui
No Server (build do pacote)
1.
ResolveBunForServer - encontra o Bun embarcado na árvore de código
2.
BundleRuntime - compila boot.ts → wwwroot/runtime.js, antes de todo build
3.
CollectServedRuntime - na hora do pack, coloca em tools/runtime/runtime.js o wwwroot/runtime.js que o build escreveu, e nunca empacota de novo
Arquitetura de pacotes e autocontenção
O eQuantic.UI segue uma arquitetura de pacotes autocontidos, em que cada pacote gerencia os próprios artefatos. O SDK age como orquestrador, referenciando os outros pacotes pelas propriedades $(Pkg*) do NuGet.
Princípios da arquitetura
Antes (problemático):
1
2
3
Pacote SDK
❌
├─ runtime.js embarcado (copiado do Server)
└─ arquivos *.cs embarcados (copiados do Components)
Problemas: acoplamento forte, conflitos de versão, duplicação de artefatos
Depois (correto):
1
2
3
4
5
6
7
8
Pacote Server
✅
└─ tools/runtime/runtime.js (o runtime que ele serve, como arquivo)
Pacote Components
✅
└─ tools/source/*.cs (autocontido)
Pacote SDK
✅
└─ Referencia os outros pacotes via $(PkgeQuantic_UI_*)
Benefícios: desacoplamento, versionamento correto, sem duplicação
Entrega do runtime.js
1. Empacotamento (desenvolvimento)
Todo build do eQuantic.UI.Server escreve o bundle, e o compilador o embarca; na hora do pack um target recolhe o mesmo arquivo. Um <Content Condition="Exists(…)"> estático seria avaliado antes de o bundle existir num build limpo, e o target recolhe o que o build escreveu sem empacotar de novo, então um pack sem build não consegue escrever um segundo bundle depois do embarcado:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- eQuantic.UI.Server.csproj -->
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CollectServedRuntime</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<ItemGroup>
<!-- uma SAÍDA DE BUILD, nunca um arquivo comitado -->
<None Remove="wwwroot/runtime.js" />
<EmbeddedResource Include="wwwroot/runtime.js">
<LogicalName>eQuantic.UI.Server.runtime.js</LogicalName>
</EmbeddedResource>
</ItemGroup>
<Target Name="CollectServedRuntime">
<ItemGroup>
<TfmSpecificPackageFile Include="wwwroot/runtime.js" PackagePath="tools/runtime/runtime.js" />
</ItemGroup>
<Error Condition="!Exists('wwwroot/runtime.js')"
Text="eQuantic.UI.Server: wwwroot/runtime.js was not produced, and the package would ship no runtime." />
</Target>
<Target Name="BundleRuntime" BeforeTargets="BeforeBuild" DependsOnTargets="ResolveBunForServer">
<MakeDir Directories="wwwroot" />
<Exec Command="&quot;$(_BunPath)&quot; build ../eQuantic.UI.Sdk/Resources/boot.ts --outfile wwwroot/runtime.js --minify-syntax --minify-whitespace" />
</Target>
O pacote que serve o runtime é o mesmo que o entrega, então o que um app recebe do servidor e o que o build dele copia são um arquivo só.
2. Entrega (build do consumidor)
Durante o dotnet build, o target CopyEQuanticRuntime do SDK executa:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- Sdk/Sdk.targets -->
<Target Name="CopyEQuanticRuntime" AfterTargets="CompileEQuanticUI"
Condition="'$(EnableEQuanticUICompilation)' == 'true'">
<PropertyGroup>
<!-- O arquivo do pacote Server, pela propriedade do NuGet -->
<_RuntimeSourcePath Condition="'$(PkgeQuantic_UI_Server)' != ''">$(PkgeQuantic_UI_Server)/tools/runtime/runtime.js</_RuntimeSourcePath>
<!-- Ao lado de uma árvore de código: a saída de build do próprio projeto Server -->
<_RuntimeSourcePath
Condition="'$(_RuntimeSourcePath)' == '' And Exists('$(MSBuildThisFileDirectory)../../eQuantic.UI.Server/wwwroot/runtime.js')">$(MSBuildThisFileDirectory)../../eQuantic.UI.Server/wwwroot/runtime.js</_RuntimeSourcePath>
<_RuntimeDestPath>$(MSBuildProjectDirectory)/$(EQuanticOutputPath)runtime.js</_RuntimeDestPath>
</PropertyGroup>
<Error Text="eQuantic.UI: the served runtime was not found. It ships inside the eQuantic.UI.Server package (version $(EQuanticUIVersion)) at tools/runtime/runtime.js; try 'dotnet restore --force'."
Condition="'$(_RuntimeSourcePath)' == '' Or !Exists('$(_RuntimeSourcePath)')" />
<Copy SourceFiles="$(_RuntimeSourcePath)" DestinationFiles="$(_RuntimeDestPath)" SkipUnchangedFiles="true" />
<!-- a folha de estilos base que um SDK anterior copiava para cá, e que nenhuma página linkava -->
<Delete Files="$(MSBuildProjectDirectory)/$(EQuanticOutputPath)equantic.css" />
</Target>
Um app em execução nunca lê essa cópia: o Server responde /_equantic/runtime.js com o bundle que embarca, antes dos arquivos estáticos. A cópia é para as ferramentas que carregam os módulos de um app fora de um servidor (o preview do VS Code lê wwwroot/_equantic/runtime.js), e é por isso que ela é esse bundle, byte a byte.
Entrega das fontes do Components
1. Empacotamento (desenvolvimento)
Durante o dotnet pack do eQuantic.UI.Components:
1
2
3
4
5
<!-- eQuantic.UI.Components.csproj -->
<ItemGroup>
<Content Include="**\*.cs" Exclude="obj\**;bin\**" PackagePath="tools\source\" />
<Content Include="SdkResources*.resx" PackagePath="tools\source\" />
</ItemGroup>
O pacote Components embarca os próprios arquivos de código C# para a resolução de tipos do compilador.
2. Compilação (build do consumidor)
Durante o dotnet build, o target CompileEQuanticUI do SDK executa:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- Sdk/Sdk.targets -->
<Target Name="CompileEQuanticUI" BeforeTargets="Build"
DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences;GenerateGlobalUsings">
<PropertyGroup>
<!-- Resolve a partir do pacote Components pela propriedade do NuGet -->
<_StandardComponentsDir Condition="'$(PkgeQuantic_UI_Components)' != ''">$(PkgeQuantic_UI_Components)/tools/source</_StandardComponentsDir>
<!-- Alternativa pela árvore de código (só em desenvolvimento) -->
<_DevComponentsDir>$(MSBuildThisFileDirectory)../../eQuantic.UI.Components</_DevComponentsDir>
<_StandardComponentsDir
Condition="'$(_StandardComponentsDir)' == '' And Exists('$(_DevComponentsDir)')">$(_DevComponentsDir)</_StandardComponentsDir>
<_EqcSourceDirs>$(MSBuildProjectDirectory);$(_StandardComponentsDir)</_EqcSourceDirs>
</PropertyGroup>
<Error Text="eQuantic.UI: Standard components not found. Ensure the eQuantic.UI.Components package (version $(EQuanticUIVersion)) is installed."
Condition="'$(_StandardComponentsDir)' == '' Or !Exists('$(_StandardComponentsDir)')" />
<!-- O que o eqc recebe é um arquivo, um argumento por linha, entre eles os mesmos assemblies
contra os quais o csc compila (refs), para o modelo semântico do eqc bater com o build real -->
<WriteLinesToFile File="$(_EqcArgumentsPath)" Lines="@(_EqcArgument->'%(Value)')" Overwrite="true" />
<!-- Dois argumentos: o compilador, e aquele arquivo pelo caminho completo -->
<RunProgram Program="dotnet" Arguments="@(_EqcStartArgument)" />
</Target>
No Sdk.targets isso são três targets, encadeados: o que o eqc lê (as raízes de código e as listas das referências, dos pacotes de ícones e dos projetos referenciados), o arquivo de resposta, e o próprio eqc. O eqc é iniciado com a RunProgram, a task com que o SDK inicia todo programa: o compilador e o arquivo de resposta pelo caminho completo, como uma lista de argumentos, sem shell entre o build e o programa. O Exec do próprio MSBuild escreve o comando num script que um shell lê primeiro, e ele lia um nome de assembly Cash$Flow como Cash. Então um nome ou uma pasta com $, crase ou aspas, simples ou duplas, chega ao eqc como é, e ao bun depois dele, a quem o eqc entrega cada caminho como um argumento à parte (Desde 0.2.0-preview.62).
O mesmo vale para todo outro programa que os dois SDKs iniciam: o chmod e o bun add do bun, o eqicon, o git numa árvore de fontes e, no macOS, codesign, ditto, notarytool, stapler e hdiutil. Um argumento é um item cujo metadado Value guarda o texto, depois do metadado Option quando ele nomeia uma opção, então o MSBuild nem o divide num ponto e vírgula nem expande um curinga nele. Com a pasta de pacotes do NuGet sob uma pasta chamada nuget $b, um build parava no chmod +x do caminho do próprio bun. As tasks viajam dentro dos pacotes dos SDKs, em tools/netstandard2.0/, e um programa que falha sem um erro próprio é o EQ4004, que nomeia os argumentos com que foi iniciado.
O BeforeTargets="Build" é o mais tarde que o target pode rodar. Num build ele roda antes, logo depois do CoreCompile, porque o target que define a pasta de saída como static web assets depende dele (veja a ordem de execução acima).
Principais benefícios da arquitetura
•
Desacoplamento: o SDK não embarca artefatos de outros pacotes
•
Versionamento correto: o SDK lê os artefatos da versão que o NuGet resolveu — nunca carrega uma cópia que possa discordar do pacote instalado
•
Sem duplicação: cada artefato existe só no pacote de origem dele
•
Flexibilidade: os pacotes evoluem independentemente, sem acoplamento forte
•
Interface clara: o SDK referencia os pacotes por propriedades bem definidas do NuGet ($(Pkg*))
•
Alternativa em desenvolvimento: os caminhos da árvore de código funcionam para o desenvolvimento do framework
Estratégia de bundle único do runtime
O runtime é UM arquivo: o BundleRuntime do Server entrega ao bun um único --outfile, e uma página chega a ele como um único módulo, @equantic/runtime, que o import map do shell aponta para /_equantic/runtime.js.
Por que um bundle único?
•
Entrega simplificada: só um arquivo para copiar (runtime.js)
•
Sem gestão de chunks: nenhuma parte do runtime é separada num arquivo próprio que uma página teria de encontrar
•
Distribuição confiável: garante que todos os recursos do runtime (logger, overlay de erro) estejam incluídos
•
Tamanho: um orçamento, não um número citado aqui. O ServedRuntimeBudgetTests comprime em gzip os bytes que o Server embarca (CompressionLevel.SmallestSize) e os compara com o registro em tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json: crescer mais de 1% falha, e encolher mais de 5% também, o que trava um ganho. EQ_UPDATE_RUNTIME_BUDGET=1 reescreve o registro, e o commit que faz isso diz o que o moveu
Os módulos de página são o caso oposto. O eqc roda o bun com --splitting, então o que dois ou mais módulos compartilham é um chunk com o nome de um deles e um hash (NotFoundScreen-<hash>.js no sample do dashboard é o shell do console dele, não um segundo tipo com esse nome), e ele carrega uma vez, com a primeira página que precisa dele. Os tamanhos deles são reportados em vez de barrados, porque um módulo de página é o código do próprio app: o CI escreve o tamanho de cada um, cru e em gzip, no resumo do job de todo pull request.
O bun calcula esse hash a partir do que o chunk contém e do caminho de cada módulo dele, contado da pasta em que o bun roda. Por isso o eqc roda o bun na pasta do projeto, onde quer que o próprio eqc rode, e um chunk mantém o nome de um build para o outro enquanto o que ele contém não muda.