Repository navigation
BuildFlow es
🌐 Esta página en: English · Português · Español · Français · Deutsch
Este documento describe el flujo de compilación de eQuantic.UI, demostrando cómo el framework mantiene cero dependencias externas para el consumidor.
┌─────────────────────────────────────────────────────────────────────────────┐
│ DESARROLLO (árbol de código) │
└─────────────────────────────────────────────────────────────────────────────┘
src/eQuantic.UI.Runtime/src/**/*.ts src/eQuantic.UI.Sdk/Resources/boot.ts
│ │
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────┐
│ dotnet build │
│ (eQuantic.UI.Server) │
│ │
│ target ResolveBunForServer: │
│ └─ el bun incrustado del │
│ proyecto Runtime.{Os}{Arch} │
│ (extraído del .zip, +x) │
│ │
│ target BundleRuntime, antes de │
│ cada compilación: │
│ └─ "$(_BunPath)" build boot.ts │
│ --outfile wwwroot/runtime.js │
└──────────────────────────────────┘
│
▼
wwwroot/runtime.js ──── el ÚNICO bundle: incrustado en Server.dll y servido en
│ /_equantic/runtime.js, y el archivo que una compilación
│ desde el árbol copia al lado de los módulos de una app
▼
┌──────────────────────────────────┐
│ dotnet pack │
│ (todo proyecto empaquetable) │
│ │
│ Server: CollectServedRuntime │
│ └─ wwwroot/runtime.js → │
│ tools/runtime/runtime.js │
│ Sdk: CollectBuildOutput │
│ └─ eqc + eqicon → │
│ tools/net10.0/ │
└──────────────────────────────────┘
│
▼
*.nupkg — bin/Release/ en CI, o un feed local (ver abajo). La versión viene de
Directory.Build.props (o del tag del release), nunca de un archivo del árbol
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONSUMIDOR (proyecto cliente) │
└─────────────────────────────────────────────────────────────────────────────┘
MyApp.csproj
└─ <Project Sdk="eQuantic.UI.Sdk"> ← esa línea es todo el pipeline
global.json
└─ { "msbuild-sdks": { "eQuantic.UI.Sdk": "<version>" } } ← el ÚNICO lugar de la versión
(sin PackageReference: el SDK agrega los suyos)
│
▼
┌──────────────────────────────────┐
│ dotnet restore │
│ │
│ NuGet instala los paquetes: │
│ ├─ eQuantic.UI.Sdk │
│ ├─ eQuantic.UI.Primitives │
│ ├─ eQuantic.UI.Code │
│ ├─ eQuantic.UI.Components │
│ ├─ eQuantic.UI.Charts │
│ ├─ eQuantic.UI.Web │
│ ├─ eQuantic.UI.Server │ ◄── the runtime it serves, also as
│ │ └─ tools/runtime/runtime.js │ a file for the SDK to copy
│ ├─ eQuantic.UI.Generators │ (analyzer)
│ └─ eQuantic.UI.Runtime.OsxArm64 │ ◄── Bun embedded here! ONE package,
│ └─ tools/bun/bun-darwin.zip │ chosen by OS + architecture
└──────────────────────────────────┘
> **¿Qué Bun?** El `src/eQuantic.UI.Runtime/bun-toolchain.json` guarda la versión y un SHA-256 por
> plataforma, comprobados en cada corrida de pruebas. Los digests prueban que los bytes comiteados
> son los que el release publicó. La versión se verifica extrayendo el binario del host y
> ejecutándolo, porque un manifiesto que nadie compara con lo que describe se aparta de ello.
>
> Los seis paquetes de plataforma (`Osx64`, `OsxArm64`, `Win64`, `WinArm64`, `Linux64`,
> `LinuxArm64`) llevan la misma compilación. Un Bun distinto por arquitectura es cómo se consigue
> un bundle que solo falla en la máquina de una persona.
>
> Para actualizarlo: reemplaza los `.zip`, corre las pruebas una vez con
> `EQ_UPDATE_BUN_MANIFEST=1`, y lee el diff. Una versión que se movió con digests que no, es un error, y al revés también.
│
▼
┌────────────────────────────────────────┐
│ dotnet build │
│ │
│ wwwroot/_equantic/** no es Content: │
│ esta compilación lo escribe (paso 11) │
│ │
│ En el orden en que corren: │
│ │
│ 1. EQuanticVectors │ (when Assets/ exists)
│ └─ eqicon: *.svg → C#, antes de │
│ que el compilador lo lea │
│ │
│ 2. CoreCompile │
│ └─ el C# de la app, target de .NET │
│ │
│ 3. ResolveBunZipPath │
│ └─ $(PkgeQuantic_UI_Runtime_ │
│ OsxArm64)/tools/bun/*.zip │
│ │
│ 4. EnsureBunExtracted │
│ ├─ Descomprime si hace falta │
│ └─ chmod +x (Unix) │
│ │
│ 5. ResolveBunPath │
│ └─ Define $(BunPath) │
│ │
│ 6. InstallBunPackages │
│ ├─ <BunPackage> → bun add │
│ └─ Enlaza node_modules │
│ │
│ 7. EQuanticRequireCompiler │
│ └─ falta eqc → EQ4002 │
│ │
│ 8. CompileEQuanticUI │
│ └─ argumentos, uno por línea, en │
│ obj/…/equantic.eqc.rsp │
│ └─ dotnet eqc.dll │
│ @obj/…/equantic.eqc.rsp │
│ │
│ 9. CopyEQuanticRuntime │ ◄── runtime.js deployment
│ └─ el runtime.js servido, │
│ del paquete Server, │
│ a wwwroot/_equantic/ │
│ │
│ 10. EQuanticWebAppIcon │ (when an AppIcon is declared)
│ └─ eqicon → │
│ wwwroot/_equantic/icons │
│ │
│ 11. _EQuanticOutputAsStaticWebAssets │ ◄── the static web assets hook
│ └─ wwwroot/_equantic/ como 8 a 10 │
│ lo escribieron, sin mapas, │
│ definido como static web assets │
│ │
│ luego el pipeline de static web assets│
│ los comprime y escribe los │
│ manifiestos: los de esta compilación │
└────────────────────────────────────────┘
│
▼
wwwroot/_equantic/
├─ runtime.js (el bundle del Server, byte a byte)
├─ *.js (componentes compilados, y los chunks que comparten)
└─ *.js.map (solo en Debug, por defecto)
│
▼
┌──────────────────────────────────┐
│ dotnet run │
│ │
│ El Server sirve: │
│ ├─ runtime.js (de Server.dll) │
│ └─ *.js (de wwwroot/_equantic) │
└──────────────────────────────────┘
│
▼
El navegador carga la aplicación
| Componente | Origen de Bun |
|---|---|
Runtime (-t:TestRuntime) |
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (árbol de código) |
| Server (compilación del paquete) |
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (árbol de código) |
| SDK (consumidor) |
$(PkgeQuantic_UI_Runtime_{Os}{Arch})/tools/bun/ (caché de NuGet) |
| SDK (árbol de código) |
eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ al lado de los archivos del propio SDK |
El consumidor solo necesita:
- El SDK de .NET 10.0
-
dotnet restore+dotnet build
No se requiere Node.js, npm ni una instalación global de Bun.
Desde 0.2.0-preview.62
Una app que se le queda grande a un proyecto comparte código por una biblioteca de clases: los tokens
de su marca, un record, un componente que dos apps colocan. La app referencia la biblioteca con un
ProjectReference y no escribe nada más sobre ella.
<Project Sdk="eQuantic.UI.Sdk">
<ItemGroup>
<ProjectReference Include="../Shop.Common/Shop.Common.csproj" />
</ItemGroup>
</Project>Lo que una página USA de la biblioteca llega al navegador, como llegan los tipos propios de la app:
| La página | El bundle contiene |
|---|---|
lee Palette.Cobalt, un miembro estático |
un módulo Palette
|
ejecuta new Cursor(3), un record |
un módulo Cursor, igual por valor allí también |
coloca new Wordmark(), un componente |
un módulo Wordmark, renderizado en el servidor y reconstruido en la hidratación |
recibe un Receipt de una Server Action |
el gemelo propio del record, que lee un long, una fecha y un Guid como lo que son |
Un tipo que esos tipos nombran cruza con ellos, en el proyecto en el que esté declarado. Un record de
un proyecto Domain que la biblioteca referencia y la app no, también es un módulo.
El código propio de la biblioteca lee los proyectos que la biblioteca referencia como los lee el compilador de C#, cualquier cosa que lea de ellos: una constante, o un tipo que usa solo dentro de un método. Qué proyecto referencia a cuál se toma de lo que restore escribió para la app, y los ensamblados no pueden decirlo: una constante se copia en el código que la lee, así que una biblioteca que lee solo una constante de un proyecto no nombra ninguna referencia a ese proyecto (#887).
Solo lo que el código del navegador alcanza se compila. La compilación escribe los tipos propios
de la app y sigue lo que ellos importan, hasta que ningún import queda sin cumplir. Una clase de la
biblioteca que ninguna página nombra nunca se compila para el navegador, así que un proyecto Domain o
Persistence cuesta lo que las páginas usan de él y no reporta nada, sea lo que sea que contenga
además. El cuerpo de una Server Action no cruza, ni tampoco un método marcado [ServerOnly], así que
los dos nombran libremente el código de servidor de la biblioteca: una Server Action que mapea el
Order del Domain al Order propio de la app alcanza el de la app y nada del Domain. Un nombre para
el que la compilación no escribe nada tampoco alcanza nada: uno dentro de nameof, uno en un
atributo, y una constante, que se escribe como su valor.
Un tipo alcanzado se sujeta a las reglas a las que se sujetan los tipos propios de la app:
- Lo que un navegador no puede ejecutar se rechaza en el archivo PROPIO de la biblioteca, con el
diagnóstico que el mismo código levanta en la app. Una clase que lee un archivo es
EQ2004. - Un tipo o un miembro marcado
[ServerOnly]se rechaza donde la página lo nombra (EQ2010). - Un módulo se nombra por su tipo, así que dos tipos ALCANZADOS de un mismo nombre se rechazan,
nombrando los dos archivos (
EQ1005). Un tipo que nada alcanza no choca con nada.
Un caso es más estricto de lo que era. Un record de un proyecto referenciado que una Server Action responde solía cruzar solo como datos, sea lo que sea que contuviera además, porque no tenía gemelo en absoluto. Su gemelo es el record entero ahora, como lo es el de un record propio de la app, así que un método suyo que un navegador no puede ejecutar rompe la compilación en ese método, aunque ninguna página lo llame. Hasta que un miembro pueda dejarse fuera de un gemelo (#826), mantén ese método fuera del tipo que la página lee: un método de extensión en una clase propia se llama igual.
Cada proyecto conserva su propio ámbito de C#: las directivas global using de una biblioteca y sus
tipos internos son suyos, y los de la app son de la app. Un tipo interno que la biblioteca abre a la
app con InternalsVisibleTo cruza como lo hace uno público. Esa declaración nombra el ENSAMBLADO de
la app, y la compilación conoce la app por el nombre al que su proyecto evalúa, así que una app cuyo
archivo de proyecto pone un <AssemblyName> distinto de su propio nombre es el amigo que la
biblioteca nombró.
Un componente de la biblioteca lee sus cadenas del .resx propio de la biblioteca. Los catálogos de
la app contienen las claves que leyó, en toda cultura para la que la biblioteca tiene un .resx, y
ninguna otra clave de ese catálogo. La app y la biblioteca pueden tener cada una un Strings con un
Title: cada lectura muestra el texto de su propio proyecto.
La biblioteca es un proyecto ordinario de .NET. Uno que contiene componentes referencia la biblioteca de componentes, en la versión que tiene el SDK de la app, y trae las fábricas al ámbito:
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="eQuantic.UI.Components" Version="(the SDK's version)" />
<Using Include="eQuantic.UI.Components.UI" Static="true" />
</ItemGroup>
</Project>Un proyecto que no contiene ningún componente, un Domain, no referencia nada de eQuantic.UI.
- Un componente de la biblioteca no tiene fábrica, así que una página escribe
new Wordmark()(#818). - Un
[Page]o un[ServerAction]declarado en la biblioteca (#819). - Lo que un generador de código fuente escribe en la biblioteca no llega a la compilación (#820).
- Un tipo de un PAQUETE, para el que la compilación no tiene fuente, sigue siendo un nombre que nada define cuando una página lee un miembro de él (#822).
samples/SharedLibrary es esta sección como tres proyectos: una app web, la biblioteca que referencia
y el proyecto que esa biblioteca referencia.
Una compilación desde el árbol propio del repositorio, los samples, un scaffold bajo prueba, nunca
pasa por un paquete. (El sitio web ya NO es uno de estos: equantic-web consume paquetes publicados
por global.json, como cualquier otro consumidor. La sección de después de la siguiente es cómo un
repo fuera de este árbol consigue el bucle sin paquetes de todos modos.) Sdk.props ve el árbol de
código al lado y cambia todo PackageReference por un ProjectReference; Sdk.targets ejecuta el
eqc y el eqicon que el grafo acaba de compilar, y copia runtime.js de
src/eQuantic.UI.Server/wwwroot/runtime.js, que el BundleRuntime del proyecto Server escribe antes
de cada compilación del Server. No hay feed local que llenar, ni caché que limpiar, ni paso de pack
entre una edición y el sample que la muestra:
dotnet build samples/DefaultUIDashboardEl mecanismo, IsEQuanticDevMode, los bordes de herramientas, _EqSourceTree, está descrito en
Arquitectura de paquetes. La sección de
abajo es para la OTRA pregunta: ¿sobrevive el cambio al camino real del consumidor?
Una app desarrollada junto al SDK, un repo consumidor, no un sample, quiere lo mismo que consiguen los
samples: editar el framework, recompilar la app, verlo. Puede tenerlo, y el mecanismo es el que los
samples ya usan y no algo nuevo. _DevRootDir es relativo a donde Sdk.props ESTÁ FÍSICAMENTE, así
que importar el SDK por ruta desde cualquier lugar aterriza la sonda dentro del árbol y el modo de
desarrollo se enciende.
Cuatro cosas de esto no son adivinables, y cada una se midió en vez de razonarse.
El modo de desarrollo alcanza solo al proyecto que importa el SDK. En una solución de una cabeza más bibliotecas, cambiar la cabeza sola te da la peor mitad del trato: la app CORRE contra tu árbol de trabajo mientras las bibliotecas todavía COMPILAN contra el paquete fijado, así que en el momento en que cambias una API, la cosa para la que el bucle existe, las bibliotecas compilan limpio y la app falla en tiempo de ejecución con un miembro que falta. Todo proyecto que referencia eQuantic.UI tiene que bifurcarse.
La propiedad tiene que fijarse antes del import del SDK, así que Directory.Build.props es
demasiado tarde para la cabeza. Directory.Build.props lo importan los props propios de
Microsoft.NET.Sdk, que el SDK de eQuantic importa en su parte de arriba: para cuando se lee, el
<Import> condicional de la cabeza ya se ha evaluado y saltado. Esto falla EN SILENCIO: obtienes
paquetes publicados creyendo que estás sobre el árbol, y lo descubres depurando otra cosa.
Un archivo de props compartido que importan tanto la cabeza como Directory.Build.props necesita un
centinela, o MSBuild levanta MSB4011 por el segundo import.
El brazo de paquetes publicados necesita global.json. Sin que msbuild-sdks fije una versión, el
camino de reserva falla con MSB4236: The SDK could not be found y un mensaje del resolvedor que no
menciona la fijación que falta.
La forma entera, cuatro archivos:
<!-- EQuanticDev.props — la única definición, leída por la cabeza Y por 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><!-- Directory.Build.props -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)EQuanticDev.props" Condition="'$(_EqDevPropsImported)' != 'true'" />
...
</Project><!-- La CABEZA: un <Project> desnudo, porque el atributo Sdk no puede llevar una 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><!-- Toda BIBLIOTECA que referencia eQuantic.UI se bifurca igual -->
<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>Comprueba que tomó, en vez de asumirlo: el fallo es silencioso:
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/... y NO ~/.nuget/packages/Lo que cuesta: la primera compilación compila el framework entero desde la fuente, y un árbol roto se
vuelve una compilación de app rota. La guarda Exists(...) es lo que mantiene a CI y a un clon limpio
sobre paquetes publicados.
Agregar tu propio proyecto a tu propia solución puede arrastrar este repo dentro de ella.
dotnet sln add lee el csproj, ve los ProjectReference del modo de desarrollo y arrastra
eQuantic.UI.Primitives, Components y el paquete de iconos a TU .sln. Inofensivo hasta que
alguien compila el brazo de reserva, donde esas rutas no existen y la solución deja de cargar.
Comprueba el .sln después de agregar un proyecto, o agrégalo con --in-root y deja las referencias
fuera.
Desde 0.2.0-preview.45
Valida un cambio del framework en una app REAL, paquetes, global.json, restore, el camino entero del
consumidor, sin cortar una versión ni ejecutar un pipeline. El primer consumidor de la pista W probó su
migración así: fotogramas idénticos byte a byte desde la compilación del paquete y desde la compilación
de la fuente.
En el checkout del framework:
dotnet pack -c Release -p:PackageVersion=1.0.0-dev -o artifacts/local-feed1.0.0-dev nunca choca con una versión publicada, los dos SDK derivan la versión de todo paquete
hermano de la suya propia, y ni los samples ni las pruebas se empaquetan: el feed es todo producto. En
la app:
-
global.json:{ "msbuild-sdks": { "eQuantic.UI.Sdk.Native": "1.0.0-dev" } }(oeQuantic.UI.Sdk) -
nuget.config:<clear />, luego el feed local PRIMERO, nuget.org después - el archivo de proyecto:
Sdk="eQuantic.UI.Sdk.Native", sin atributo de versión
Después de cada repack, limpia la copia en caché y restaura otra vez: NuGet nunca vuelve a descargar
una versión que ya tiene. ClearEQuanticCache vive en el Directory.Build.targets del framework, así
que ejecútalo desde ese checkout (alcanza todo proyecto por la solución); quita todo paquete
equantic.* de la caché de NuGet de forma reactiva (una lista mantenida a mano una vez se perdió todos
los paquetes nativos):
dotnet msbuild -t:ClearEQuanticCachedotnet restore --forcePrueba sin cabeza para una app nativa, sin ventana: --Photon:ScreenshotPath out.png.
| Archivo | Responsabilidad |
|---|---|
Sdk/Sdk.props |
Agrega los paquetes del framework, o, al lado de un árbol de código, los PROYECTOS del framework y los dos bordes de herramientas (IsEQuanticDevMode) |
Sdk/Sdk.targets |
Encuentra las herramientas (_EqSourceTree: la compilación del árbol o el tools/ del paquete), resuelve Bun, instala los items <BunPackage>, compila componentes (con los mapas de fuente que EQuanticSourceMaps pide, y ninguno en una publicación), copia runtime.js, escribe los vectores y el icono de la app, define su carpeta de salida como static web assets; falla por nombre cuando falta una herramienta (EQ4001, EQ4002) |
Runtime.csproj |
El TypeScript del runtime y su prueba: -t:TestRuntime ejecuta bun install --frozen-lockfile, tsc y vitest run por el bun incrustado. No empaqueta nada y no se empaqueta (IsPackable=false) |
Server.csproj |
El único escritor del runtime: empaqueta boot.ts → wwwroot/runtime.js antes de cada compilación, lo incrusta (servido en /_equantic/runtime.js) y empaqueta el mismo archivo como tools/runtime/runtime.js
|
Runtime.{Os}{Arch}.csproj |
Empaqueta el ejecutable de Bun comprimido para un SO y una arquitectura |
Sdk.csproj |
Empaqueta Sdk/, y reúne eqc y eqicon en tools/net10.0/ desde los proyectos de herramienta que referencia como bordes |
En el orden en que corren en un dotnet build:
-
EQuanticRequireIconTool / EQuanticVectors - Cuando existe
Assets/, antes del compilado de C#: falla conEQ4001si faltaeqicon.dll, si no escribeAssets/**/*.svgcomoVectors.g.cs - CoreCompile - El compilado de C# de la app, el target propio del SDK de .NET; todo lo de abajo corre después de él
- ResolveBunZipPath - Encuentra el .zip de Bun en la caché de NuGet (o al lado del SDK en un árbol de código)
- EnsureBunExtracted - Extrae el ejecutable si hace falta
-
ResolveBunPath - Define
$(BunPath)para su uso posterior -
InstallBunPackages - Instala los items
<BunPackage>porbun add(ver BunPackage) -
EQuanticRequireCompiler - Falla con
EQ4002cuandoeqc.dllno está donde diceEqcCliPath - CompileEQuanticUI - Transpila C# → TypeScript → JavaScript
-
CopyEQuanticRuntime - Copia el runtime.js servido desde el paquete Server (al lado de un árbol de código, el
wwwroot/runtime.jsdel proyecto Server) a wwwroot/_equantic/, y borra unequantic.cssque un SDK anterior dejó ahí - EQuanticWebAppIcon - Cuando se declara un icono de app: escribe los tamaños del icono y el manifiesto web
-
_EQuanticOutputAsStaticWebAssets - Define
wwwroot/_equantic/como static web assets desde lo que escribieron 8 a 10, sin los mapas
Los pasos 3 a 10 corren donde corren por el último. _EQuanticOutputAsStaticWebAssets se cuelga de
$(GenerateComputedBuildStaticWebAssetsDependsOn), el gancho propio del pipeline de static web assets
para los activos que una compilación genera, que corre después de CoreCompile y antes de que el
pipeline resuelva sus entradas, y depende del compilador, de la copia del runtime y del icono de la
app. CompileEQuanticUI todavía declara BeforeTargets="Build", y para entonces ya ha corrido.
La carpeta no es Content por la misma razón. El pipeline registra lo que wwwroot/ contiene
mientras el proyecto se evalúa, y la compilación entonces reescribe esta carpeta: un archivo
registrado y luego borrado rompía la compresión de la siguiente publicación. Eso era todo mapa que una
compilación Release quita, y todo chunk compartido que bun renombra cuando cambia un componente que
dos páginas comparten, en la secuencia más ordinaria que hay: editar, luego publicar. Definida desde el
disco una vez que sus escritores han corrido, la carpeta llega a la compresión, a los manifiestos y a
la publicación como esta compilación la escribió, y solo eso. Una publicación con --no-build carga
el manifiesto de la compilación en su lugar, así que lleva los archivos que la última compilación
escribió. Con EnableEQuanticUICompilation=false no aplica ninguna de las dos mitades, y los archivos
de la carpeta se quedan como Content ordinario.
-
_EQuanticKeepSourceMapsOutOfPublish - Después de
ComputeResolvedFilesToPublishList: saca todo.mapbajowwwroot/_equantic/deResolvedFileToPublish, diga lo que digaEQuanticSourceMaps. Los mapas tampoco están entre los web assets de arriba, así que esta es la segunda línea
- ResolveBunForRuntime - Encuentra el Bun incrustado en el árbol de código
-
TestRuntime -
bun install --frozen-lockfile, luegotscyvitest run. Aquí no se empaqueta nada
- ResolveBunForServer - Encuentra el Bun incrustado en el árbol de código
- BundleRuntime - Compila boot.ts → wwwroot/runtime.js, antes de cada compilación
- CollectServedRuntime - En el pack, pone el wwwroot/runtime.js que la compilación escribió bajo tools/runtime/runtime.js, y nunca vuelve a empaquetar
eQuantic.UI sigue una arquitectura de paquetes autocontenida en la que cada paquete gestiona sus propios artefactos. El SDK actúa como orquestador, referenciando otros paquetes por las propiedades $(Pkg*) de NuGet.
Antes (problemático):
SDK Package ❌
├─ Embedded runtime.js (copied from Server)
└─ Embedded *.cs files (copied from Components)
Problemas: acoplamiento fuerte, conflictos de versión, duplicación de artefactos
Después (correcto):
Server Package ✅
└─ tools/runtime/runtime.js (the runtime it serves, as a file)
Components Package ✅
└─ tools/source/*.cs (self-contained)
SDK Package ✅
└─ References other packages via $(PkgeQuantic_UI_*)
Beneficios: desacoplamiento, versionado correcto, sin duplicación
1. Empaquetado (desarrollo)
Toda compilación de eQuantic.UI.Server escribe el bundle, y el compilador lo incrusta; en el pack
un target reúne el mismo archivo. Un <Content Condition="Exists(…)"> estático se evaluaría antes de
que el bundle exista en una compilación limpia, y el target reúne lo que la compilación escribió sin
volver a empaquetar, así que un pack sin compilación no puede escribir un segundo bundle después del
incrustado:
<!-- eQuantic.UI.Server.csproj -->
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CollectServedRuntime</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<ItemGroup>
<!-- una SALIDA DE COMPILACIÓN, nunca un archivo comiteado -->
<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=""$(_BunPath)" build ../eQuantic.UI.Sdk/Resources/boot.ts --outfile wwwroot/runtime.js --minify-syntax --minify-whitespace" />
</Target>El paquete que sirve el runtime es el que lo entrega, así que lo que se le sirve a una app y lo que su compilación copia son un solo archivo.
2. Despliegue (compilación del consumidor)
Durante dotnet build, el target CopyEQuanticRuntime del SDK se ejecuta:
<!-- Sdk/Sdk.targets -->
<Target Name="CopyEQuanticRuntime" AfterTargets="CompileEQuanticUI"
Condition="'$(EnableEQuanticUICompilation)' == 'true'">
<PropertyGroup>
<!-- El archivo del paquete Server, por una propiedad de NuGet -->
<_RuntimeSourcePath Condition="'$(PkgeQuantic_UI_Server)' != ''">$(PkgeQuantic_UI_Server)/tools/runtime/runtime.js</_RuntimeSourcePath>
<!-- Al lado de un árbol de código: la salida de compilación del proyecto 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" />
<!-- la hoja de estilos base que un SDK anterior copiaba aquí, que ninguna página enlazaba -->
<Delete Files="$(MSBuildProjectDirectory)/$(EQuanticOutputPath)equantic.css" />
</Target>Una app en ejecución nunca lee esta copia: el Server responde /_equantic/runtime.js con el bundle que
incrusta, por delante de los archivos estáticos. La copia es para las herramientas que cargan los
módulos de una app fuera de un servidor (la vista previa de VS Code lee
wwwroot/_equantic/runtime.js), que es por lo que es ese bundle, byte a byte.
1. Empaquetado (desarrollo)
Durante el dotnet pack de eQuantic.UI.Components:
<!-- eQuantic.UI.Components.csproj -->
<ItemGroup>
<Content Include="**\*.cs" Exclude="obj\**;bin\**" PackagePath="tools\source\" />
<Content Include="SdkResources*.resx" PackagePath="tools\source\" />
</ItemGroup>El paquete Components incrusta sus propios archivos de C# para la resolución de tipos del compilador.
2. Compilación (compilación del consumidor)
Durante dotnet build, el target CompileEQuanticUI del SDK se ejecuta:
<!-- Sdk/Sdk.targets -->
<Target Name="CompileEQuanticUI" BeforeTargets="Build"
DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences;GenerateGlobalUsings">
<PropertyGroup>
<!-- Resuelve desde el paquete Components por una propiedad de NuGet -->
<_StandardComponentsDir Condition="'$(PkgeQuantic_UI_Components)' != ''">$(PkgeQuantic_UI_Components)/tools/source</_StandardComponentsDir>
<!-- Reserva al árbol de código (solo en desarrollo) -->
<_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)')" />
<!-- A eqc se le entrega un archivo, un argumento por línea, entre ellos los ensamblados exactos
contra los que csc compila (refs), así que el modelo semántico de eqc coincide con la
compilación real -->
<WriteLinesToFile File="$(_EqcArgumentsPath)" Lines="@(_EqcArgument->'%(Value)')" Overwrite="true" />
<!-- Dos argumentos: el compilador, y ese archivo por su ruta completa -->
<RunProgram Program="dotnet" Arguments="@(_EqcStartArgument)" />
</Target>En Sdk.targets esto son tres targets, encadenados: lo que eqc lee (sus raíces de fuente y las listas
de las referencias, los paquetes de iconos y los proyectos referenciados), el archivo de respuesta, y
eqc mismo. eqc se inicia con RunProgram, la tarea con la que el SDK inicia todo programa: el
compilador y el archivo de respuesta por su ruta completa, como una lista de argumentos, sin ningún
shell entre la compilación y el programa. El Exec propio de MSBuild escribe su comando en un script
que un shell lee primero, y leyó un nombre de ensamblado Cash$Flow como Cash. Así que un nombre o
una carpeta que contiene un $, una tilde invertida o una comilla, simple o doble, llega a eqc tal
cual, y a bun después de él, al que eqc entrega cada ruta como un argumento propio (Desde
0.2.0-preview.62).
Lo mismo vale para todo otro programa que los dos SDK inician: el chmod y el bun add de bun,
eqicon, git en un árbol de código, y en macOS codesign, ditto, notarytool, stapler y
hdiutil. Un argumento es un item cuyo metadato Value contiene el texto, después de su metadato
Option cuando nombra uno, así que MSBuild ni lo parte en un punto y coma ni expande un comodín en
él. Con la carpeta de paquetes de NuGet bajo una carpeta llamada nuget $b, una compilación solía
detenerse en el chmod +x sobre la ruta propia de bun. Las tareas se entregan dentro de los paquetes
del SDK, bajo tools/netstandard2.0/, y un programa que falla sin un error propio es EQ4004,
nombrando los argumentos con los que se inició.
BeforeTargets="Build" es lo más tarde que el target puede correr. En una compilación corre más
temprano, justo después de CoreCompile, porque el target que define la carpeta de salida como static
web assets depende de él (ver el orden de ejecución de arriba).
- Desacoplamiento: el SDK no incrusta artefactos de otros paquetes
- Versionado correcto: el SDK lee los artefactos de cualquier versión que NuGet resolvió, nunca lleva una copia que pueda discrepar del paquete instalado
- Sin duplicación: cada artefacto existe solo en su paquete de origen
- Flexibilidad: los paquetes evolucionan de forma independiente sin acoplamiento fuerte
-
Interfaz clara: el SDK referencia paquetes por propiedades de NuGet bien definidas (
$(Pkg*)) - Reserva de desarrollo: las rutas del árbol de código funcionan para el desarrollo del framework
El runtime es UN archivo: el BundleRuntime del Server entrega a bun un único --outfile, y una
página lo alcanza como un solo módulo desnudo, @equantic/runtime, que el import map del shell apunta
a /_equantic/runtime.js.
¿Por qué un solo bundle?
- Despliegue simplificado: solo un archivo que copiar (runtime.js)
- Sin gestión de chunks: ninguna parte del runtime se parte en un archivo propio que una página tendría que encontrar
- Distribución fiable: garantía de que todas las funciones del runtime (logger, superposición de errores) están incluidas
-
Tamaño: un presupuesto, no un número citado aquí.
ServedRuntimeBudgetTestscomprime con gzip los bytes que el Server incrusta (CompressionLevel.SmallestSize) y los compara con el registro entests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json: un crecimiento de más del 1 % falla, y también un encogimiento de más del 5 %, que fija una ganancia.EQ_UPDATE_RUNTIME_BUDGET=1escribe el registro, y el commit que lo hace dice qué lo movió
Los módulos de página son el caso opuesto. eqc ejecuta bun con --splitting, así que lo que dos o más
módulos comparten es un chunk nombrado por uno de ellos con un hash (NotFoundScreen-<hash>.js en el
sample del dashboard es su shell de consola, no un segundo tipo de ese nombre), y se carga una vez,
con la primera página que lo necesita. Sus tamaños se reportan en vez de acotarse, ya que un módulo de
página es el código propio de la app: CI escribe el tamaño de cada uno, crudo y comprimido con gzip, en
el resumen del job de todo pull request.
bun calcula ese hash de lo que el chunk contiene y de la ruta de cada uno de sus módulos, tomada de la carpeta en la que bun corre. Así que eqc ejecuta bun en la carpeta del proyecto, donde sea que eqc mismo corra, y un chunk conserva su nombre de una compilación a la siguiente mientras lo que contiene siga siendo lo mismo.
🏁 Empieza aquí
🏗️ Arquitectura
- Visión general de la arquitectura
- Componentes write-once
- Superficie declarativa
- Arquitectura de paquetes
- Componentes
- Estilos
- Localización
- Analytics y GTM
📱 Write-once
- Motor Photon
- Design System
- Capacidades
- Almacenamiento
- Formularios
- Editor de código
- Markdown
- Mermaid
- Renderizado de email
⚙️ Compilación
⚡ Runtime
🔌 Servidor
🎨 Ecosistema
🚀 Desarrollo