Skip to content

BuildFlow es

Edgar Mesquita edited this page Oct 14, 2026 · 1 revision

Flujo de compilación de eQuantic.UI

🌐 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.

Flujo visual

┌─────────────────────────────────────────────────────────────────────────────┐
│                        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

Origen de Bun por componente

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

Requisitos del consumidor

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.

Una biblioteca de clases compartida por los proyectos de una solución

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ó.

Sus cadenas

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.

Escribir la biblioteca

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.

Lo que todavía no está

  • 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.

Desarrollar el framework: ningún paquete en absoluto

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/DefaultUIDashboard

El 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?

El mismo bucle desde OTRO repositorio

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.

Consumir un SDK no publicado desde paquetes locales

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-feed

1.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" } } (o eQuantic.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:ClearEQuanticCache
dotnet restore --force

Prueba sin cabeza para una app nativa, sin ventana: --Photon:ScreenshotPath out.png.

Archivos clave

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

Targets de MSBuild (orden de ejecución)

En el SDK (consumidor)

En el orden en que corren en un dotnet build:

  1. EQuanticRequireIconTool / EQuanticVectors - Cuando existe Assets/, antes del compilado de C#: falla con EQ4001 si falta eqicon.dll, si no escribe Assets/**/*.svg como Vectors.g.cs
  2. CoreCompile - El compilado de C# de la app, el target propio del SDK de .NET; todo lo de abajo corre después de él
  3. ResolveBunZipPath - Encuentra el .zip de Bun en la caché de NuGet (o al lado del SDK en un árbol de código)
  4. EnsureBunExtracted - Extrae el ejecutable si hace falta
  5. ResolveBunPath - Define $(BunPath) para su uso posterior
  6. InstallBunPackages - Instala los items <BunPackage> por bun add (ver BunPackage)
  7. EQuanticRequireCompiler - Falla con EQ4002 cuando eqc.dll no está donde dice EqcCliPath
  8. CompileEQuanticUI - Transpila C# → TypeScript → JavaScript
  9. CopyEQuanticRuntime - Copia el runtime.js servido desde el paquete Server (al lado de un árbol de código, el wwwroot/runtime.js del proyecto Server) a wwwroot/_equantic/, y borra un equantic.css que un SDK anterior dejó ahí
  10. EQuanticWebAppIcon - Cuando se declara un icono de app: escribe los tamaños del icono y el manifiesto web
  11. _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.

En el SDK (publicación del consumidor)

  1. _EQuanticKeepSourceMapsOutOfPublish - Después de ComputeResolvedFilesToPublishList: saca todo .map bajo wwwroot/_equantic/ de ResolvedFileToPublish, diga lo que diga EQuanticSourceMaps. Los mapas tampoco están entre los web assets de arriba, así que esta es la segunda línea

En Runtime (-t:TestRuntime)

  1. ResolveBunForRuntime - Encuentra el Bun incrustado en el árbol de código
  2. TestRuntime - bun install --frozen-lockfile, luego tsc y vitest run. Aquí no se empaqueta nada

En Server (compilación del paquete)

  1. ResolveBunForServer - Encuentra el Bun incrustado en el árbol de código
  2. BundleRuntime - Compila boot.ts → wwwroot/runtime.js, antes de cada compilación
  3. CollectServedRuntime - En el pack, pone el wwwroot/runtime.js que la compilación escribió bajo tools/runtime/runtime.js, y nunca vuelve a empaquetar

Arquitectura de paquetes y autocontención

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.

Principios de la arquitectura

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

Despliegue de runtime.js

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="&quot;$(_BunPath)&quot; 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.

Despliegue de la fuente de Components

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).

Beneficios arquitectónicos clave

  • 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

Estrategia de un solo bundle para el runtime

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í. ServedRuntimeBudgetTests comprime con gzip los bytes que el Server incrusta (CompressionLevel.SmallestSize) y los compara con el registro en tests/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=1 escribe 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.

Clone this wiki locally