Skip to content

BuildFlow fr

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

Flux de compilation d'eQuantic.UI

🌐 Cette page en : English · Português · Español · Français · Deutsch

Ce document décrit le flux de compilation d'eQuantic.UI, en montrant comment le framework maintient zéro dépendance externe pour le consommateur.

Flux visuel

┌─────────────────────────────────────────────────────────────────────────────┐
│                      DÉVELOPPEMENT (arbre de sources)                       │
└─────────────────────────────────────────────────────────────────────────────┘

  src/eQuantic.UI.Runtime/src/**/*.ts          src/eQuantic.UI.Sdk/Resources/boot.ts
         │                                              │
         └──────────────────┬───────────────────────────┘
                            ▼
  ┌──────────────────────────────────┐
  │  dotnet build                    │
  │  (eQuantic.UI.Server)            │
  │                                  │
  │  cible ResolveBunForServer :     │
  │  └─ le bun embarqué du projet    │
  │     Runtime.{Os}{Arch}           │
  │     (extrait du .zip, +x)        │
  │                                  │
  │  cible BundleRuntime, avant      │
  │  chaque compilation :            │
  │  └─ "$(_BunPath)" build boot.ts  │
  │     --outfile wwwroot/runtime.js │
  └──────────────────────────────────┘
         │
         ▼
  wwwroot/runtime.js  ──── le SEUL bundle : embarqué dans Server.dll et servi à
         │                 /_equantic/runtime.js, et le fichier qu'une compilation
         │                 depuis l'arbre copie à côté des modules d'une app
         ▼
  ┌──────────────────────────────────┐
  │  dotnet pack                     │
  │  (tout projet empaquetable)      │
  │                                  │
  │  Server: CollectServedRuntime    │
  │  └─ wwwroot/runtime.js →         │
  │     tools/runtime/runtime.js     │
  │  Sdk: CollectBuildOutput         │
  │  └─ eqc + eqicon →               │
  │     tools/net10.0/               │
  └──────────────────────────────────┘
         │
         ▼
  *.nupkg — bin/Release/ en CI, ou un dépôt local (voir plus bas). La version vient de
  Directory.Build.props (ou du tag de la release), jamais d'un fichier de l'arbre


┌─────────────────────────────────────────────────────────────────────────────┐
│                        CONSOMMATEUR (projet client)                         │
└─────────────────────────────────────────────────────────────────────────────┘

  MyApp.csproj
  └─ <Project Sdk="eQuantic.UI.Sdk">          ← cette ligne est tout le pipeline
  global.json
  └─ { "msbuild-sdks": { "eQuantic.UI.Sdk": "<version>" } }   ← le SEUL endroit de la version
  (aucun PackageReference : le SDK ajoute les siens)

         │
         ▼
  ┌──────────────────────────────────┐
  │  dotnet restore                  │
  │                                  │
  │  NuGet installe les paquets :    │
  │  ├─ 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
  └──────────────────────────────────┘

> **Quel Bun ?** Le `src/eQuantic.UI.Runtime/bun-toolchain.json` tient la version et un SHA-256 par
> plateforme, vérifiés à chaque passage des tests. Les empreintes prouvent que les octets commités
> sont ceux que la release a publiés. La version est vérifiée en extrayant le binaire de l'hôte et
> en le lançant, parce qu'un manifeste que personne ne compare à ce qu'il décrit s'en écarte.
>
> Les six paquets de plateforme (`Osx64`, `OsxArm64`, `Win64`, `WinArm64`, `Linux64`,
> `LinuxArm64`) portent la même compilation. Un Bun différent par architecture est la façon d'obtenir
> un bundle qui ne casse que sur la machine d'une seule personne.
>
> Pour le mettre à jour : remplacez les `.zip`, lancez les tests une fois avec
> `EQ_UPDATE_BUN_MANIFEST=1`, et lisez le diff. Une version qui a bougé avec des empreintes qui n'ont pas bougé est une erreur, et l'inverse aussi.

         │
         ▼
  ┌────────────────────────────────────────┐
  │  dotnet build                          │
  │                                        │
  │  wwwroot/_equantic/** pas en Content   │
  │  cette compilation l'écrit (étape 11)  │
  │                                        │
  │  Dans l'ordre d'exécution :            │
  │                                        │
  │  1. EQuanticVectors                    │  (when Assets/ exists)
  │     └─ eqicon : *.svg → C#, avant      │
  │       que le compilateur le lise       │
  │                                        │
  │  2. CoreCompile                        │
  │     └─ le C# de l'app, cible de .NET   │
  │                                        │
  │  3. ResolveBunZipPath                  │
  │     └─ $(PkgeQuantic_UI_Runtime_       │
  │        OsxArm64)/tools/bun/*.zip       │
  │                                        │
  │  4. EnsureBunExtracted                 │
  │        ├─ Décompresse au besoin        │
  │           └─ chmod +x (Unix)           │
  │                                        │
  │  5. ResolveBunPath                     │
  │         └─ Définit $(BunPath)          │
  │                                        │
  │  6. InstallBunPackages                 │
  │     ├─ <BunPackage> → bun add          │
  │          └─ Lie node_modules           │
  │                                        │
  │  7. EQuanticRequireCompiler            │
  │        └─ eqc manquant → EQ4002        │
  │                                        │
  │  8. CompileEQuanticUI                  │
  │    └─ arguments, un par ligne, dans    │
  │        obj/…/equantic.eqc.rsp          │
  │     └─ dotnet eqc.dll                  │
  │        @obj/…/equantic.eqc.rsp         │
  │                                        │
  │  9. CopyEQuanticRuntime                │  ◄── runtime.js deployment
  │        └─ le runtime.js servi,         │
  │           du paquet Server,            │
  │        vers wwwroot/_equantic/         │
  │                                        │
  │ 10. EQuanticWebAppIcon                 │  (when an AppIcon is declared)
  │     └─ eqicon →                        │
  │        wwwroot/_equantic/icons         │
  │                                        │
  │ 11. _EQuanticOutputAsStaticWebAssets   │  ◄── the static web assets hook
  │     └─ wwwroot/_equantic/ comme 8 à 10 │
  │     l'ont écrit, sans les cartes,      │
  │        défini en static web assets     │
  │                                        │
  │  puis le pipeline static web assets    │
  │  les compresse et écrit les            │
  │  manifestes : ceux de cette compilation│
  └────────────────────────────────────────┘
         │
         ▼
  wwwroot/_equantic/
  ├─ runtime.js (le bundle du Server, octet pour octet)
  ├─ *.js (composants compilés, et les chunks qu'ils partagent)
  └─ *.js.map (Debug seulement, par défaut)
         │
         ▼
  ┌──────────────────────────────────┐
  │  dotnet run                      │
  │                                  │
  │  Le Server sert :                │
  │  ├─ runtime.js (de Server.dll)   │
  │  └─ *.js (de wwwroot/_equantic)  │
  └──────────────────────────────────┘
         │
         ▼
  Le navigateur charge l'application

Origine de Bun par composant

Composant Origine de Bun
Runtime (-t:TestRuntime) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (arbre de sources)
Server (compilation du paquet) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (arbre de sources)
SDK (consommateur) $(PkgeQuantic_UI_Runtime_{Os}{Arch})/tools/bun/ (cache NuGet)
SDK (arbre de sources) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ à côté des fichiers du SDK lui-même

Prérequis du consommateur

Le consommateur n'a besoin que de :

  • Le SDK .NET 10.0
  • dotnet restore + dotnet build

Aucun Node.js, npm ni installation globale de Bun n'est requis.

Une bibliothèque de classes partagée par les projets d'une solution

Depuis 0.2.0-preview.62

Une app qui devient trop grande pour un seul projet partage du code par une bibliothèque de classes : les tokens de sa marque, un record, un composant que deux apps placent. L'app référence la bibliothèque avec un ProjectReference et n'écrit rien d'autre à son sujet.

<Project Sdk="eQuantic.UI.Sdk">
    <ItemGroup>
        <ProjectReference Include="../Shop.Common/Shop.Common.csproj" />
    </ItemGroup>
</Project>

Ce qu'une page UTILISE de la bibliothèque atteint le navigateur, comme le font les types propres de l'app :

La page Le bundle contient
lit Palette.Cobalt, un membre statique un module Palette
exécute new Cursor(3), un record un module Cursor, égal par valeur là aussi
place new Wordmark(), un composant un module Wordmark, rendu sur le serveur et reconstruit à l'hydratation
reçoit un Receipt d'une Server Action le jumeau propre du record, qui lit un long, une date et un Guid pour ce qu'ils sont

Un type que ces types nomment traverse avec eux, dans le projet où il est déclaré. Un record d'un projet Domain que la bibliothèque référence et que l'app ne référence pas est un module aussi.

Le code propre de la bibliothèque lit les projets que la bibliothèque référence comme le compilateur C# les lit, quoi qu'il en lise : une constante, ou un type qu'il n'utilise qu'à l'intérieur d'une méthode. Quel projet référence lequel est pris de ce que restore a écrit pour l'app, et les assemblages ne peuvent pas le dire : une constante est copiée dans le code qui la lit, donc une bibliothèque qui ne lit qu'une constante d'un projet ne nomme aucune référence vers ce projet (#887).

Seul ce que le code du navigateur atteint est compilé. La compilation écrit les types propres de l'app et suit ce qu'ils importent, jusqu'à ce qu'aucun import ne reste non satisfait. Une classe de la bibliothèque qu'aucune page ne nomme n'est jamais compilée pour le navigateur, donc un projet Domain ou Persistence coûte ce que les pages en utilisent et ne signale rien, quoi qu'il contienne d'autre. Le corps d'une Server Action ne traverse pas, ni une méthode marquée [ServerOnly], donc les deux nomment librement le code serveur de la bibliothèque : une Server Action qui mappe l'Order du Domain vers l'Order propre de l'app atteint celui de l'app et rien du Domain. Un nom pour lequel la compilation n'écrit rien n'atteint rien non plus : un dans nameof, un dans un attribut, et une constante, qui est écrite comme sa valeur.

Un type atteint est tenu aux règles auxquelles les types propres de l'app sont tenus :

  • Ce qu'un navigateur ne peut pas exécuter est refusé au fichier PROPRE de la bibliothèque, avec le diagnostic que le même code lève dans l'app. Une classe qui lit un fichier est EQ2004.
  • Un type ou un membre marqué [ServerOnly] est refusé là où la page le nomme (EQ2010).
  • Un module est nommé d'après son type, donc deux types ATTEINTS d'un même nom sont refusés, en nommant les deux fichiers (EQ1005). Un type que rien n'atteint n'entre en collision avec rien.

Un cas est plus strict qu'il ne l'était. Un record d'un projet référencé qu'une Server Action renvoie traversait autrefois comme des données seules, quoi qu'il contînt d'autre, parce qu'il n'avait aucun jumeau. Son jumeau est le record entier maintenant, comme l'est celui d'un record propre de l'app, donc une de ses méthodes qu'un navigateur ne peut pas exécuter casse la compilation à cette méthode, bien qu'aucune page ne l'appelle. Jusqu'à ce qu'un membre puisse être laissé hors d'un jumeau (#826), gardez cette méthode hors du type que la page lit : une méthode d'extension dans une classe à elle s'appelle de la même façon.

Chaque projet garde sa propre portée C# : les directives global using d'une bibliothèque et ses types internes sont les siens, et ceux de l'app sont ceux de l'app. Un type interne que la bibliothèque ouvre à l'app avec InternalsVisibleTo traverse comme le fait un type public. Cette déclaration nomme l'ASSEMBLAGE de l'app, et la compilation connaît l'app par le nom auquel son projet s'évalue, donc une app dont le fichier de projet pose un <AssemblyName> différent de son propre nom est l'ami que la bibliothèque a nommé.

Ses chaînes

Un composant de la bibliothèque lit ses chaînes depuis le .resx propre de la bibliothèque. Les catalogues de l'app contiennent les clés qu'il a lues, dans chaque culture pour laquelle la bibliothèque a un .resx, et aucune autre clé de ce catalogue. L'app et la bibliothèque peuvent avoir chacune un Strings avec un Title : chaque lecture montre le texte de son propre projet.

Écrire la bibliothèque

La bibliothèque est un projet .NET ordinaire. Une qui contient des composants référence la bibliothèque de composants, à la version qu'a le SDK de l'app, et amène les fabriques dans la portée :

<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 projet qui ne contient aucun composant, un Domain, ne référence rien d'eQuantic.UI.

Ce qui n'est pas encore là

  • Un composant de la bibliothèque n'a pas de fabrique, donc une page écrit new Wordmark() (#818).
  • Un [Page] ou un [ServerAction] déclaré dans la bibliothèque (#819).
  • Ce qu'un générateur de source écrit dans la bibliothèque n'atteint pas la compilation (#820).
  • Un type d'un PAQUET, pour lequel la compilation n'a pas de source, reste un nom que rien ne définit quand une page lit un de ses membres (#822).

samples/SharedLibrary est cette section en trois projets : une app web, la bibliothèque qu'elle référence et le projet que cette bibliothèque référence.

Développer le framework : aucun paquet du tout

Une compilation depuis l'arbre propre du dépôt, les samples, un scaffold en cours de test, ne passe jamais par un paquet. (Le site web n'est PLUS l'un de ceux-là : equantic-web consomme des paquets publiés par global.json, comme tout autre consommateur. La section d'après la suivante est comment un dépôt hors de cet arbre obtient quand même la boucle sans paquets.) Sdk.props voit l'arbre de sources à côté de lui et échange chaque PackageReference contre un ProjectReference ; Sdk.targets lance l'eqc et l'eqicon que le graphe vient de construire, et copie runtime.js depuis src/eQuantic.UI.Server/wwwroot/runtime.js, que le BundleRuntime du projet Server écrit avant chaque compilation du Server. Il n'y a aucun dépôt local à remplir, aucun cache à vider, et aucune étape de pack entre une modification et le sample qui la montre :

dotnet build samples/DefaultUIDashboard

Le mécanisme, IsEQuanticDevMode, les arêtes d'outils, _EqSourceTree, est décrit dans Architecture des paquets. La section ci-dessous est pour l'AUTRE question : la modification survit-elle au vrai chemin du consommateur ?

La même boucle depuis UN AUTRE dépôt

Une app développée aux côtés du SDK, un dépôt consommateur et non un sample, veut la même chose que ce que les samples obtiennent : modifier le framework, recompiler l'app, le voir. Elle peut l'avoir, et le mécanisme est celui que les samples utilisent déjà plutôt que quoi que ce soit de nouveau. _DevRootDir est relatif à l'endroit où Sdk.props SE TROUVE PHYSIQUEMENT, donc importer le SDK par chemin depuis n'importe où pose la sonde dans l'arbre et le mode développement s'allume.

Quatre choses là-dessus ne se devinent pas, et chacune a été mesurée plutôt que raisonnée.

Le mode développement n'atteint que le projet qui importe le SDK. Dans une solution d'une tête plus des bibliothèques, basculer la tête seule vous donne la pire moitié du marché : l'app TOURNE contre votre arbre de travail tandis que les bibliothèques COMPILENT encore contre le paquet épinglé, donc au moment où vous changez une API, la chose pour laquelle la boucle existe, les bibliothèques compilent proprement et l'app échoue à l'exécution sur un membre manquant. Chaque projet qui référence eQuantic.UI doit se dédoubler.

La propriété doit être posée avant l'import du SDK, donc Directory.Build.props est trop tard pour la tête. Directory.Build.props est importé par les props propres de Microsoft.NET.Sdk, que le SDK eQuantic importe en haut de lui-même : au moment où il est lu, l'<Import> conditionnel de la tête a déjà été évalué et sauté. Cela échoue EN SILENCE : vous obtenez des paquets publiés en croyant être sur l'arbre, et vous le découvrez en déboguant autre chose.

Un fichier de props partagé importé à la fois par la tête et par Directory.Build.props a besoin d'une sentinelle, sinon MSBuild lève MSB4011 pour le second import.

Le bras des paquets publiés a besoin de global.json. Sans que msbuild-sdks épingle une version, le repli échoue avec MSB4236: The SDK could not be found et un message du résolveur qui ne mentionne pas l'épinglage manquant.

La forme entière, quatre fichiers :

<!-- EQuanticDev.props — la seule définition, lue par la tête ET par 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 TÊTE : un <Project> nu, parce que l'attribut Sdk ne peut pas porter de 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>
<!-- Chaque BIBLIOTHÈQUE qui référence eQuantic.UI se dédouble de la même façon -->
<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>

Vérifiez que cela a pris, plutôt que de le supposer : l'échec est silencieux :

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/... et NON ~/.nuget/packages/

Ce que cela coûte : la première compilation compile le framework entier depuis la source, et un arbre cassé devient une compilation d'app cassée. La garde Exists(...) est ce qui maintient la CI et un clone propre sur des paquets publiés.

Ajouter votre propre projet à votre propre solution peut y entraîner ce dépôt. dotnet sln add lit le csproj, voit les ProjectReference du mode développement et tire eQuantic.UI.Primitives, Components et le pack d'icônes dans VOTRE .sln. Inoffensif jusqu'à ce que quelqu'un compile le bras de repli, où ces chemins n'existent pas et la solution cesse de se charger. Vérifiez le .sln après avoir ajouté un projet, ou ajoutez avec --in-root et gardez les références dehors.

Consommer un SDK non publié depuis des paquets locaux

Depuis 0.2.0-preview.45

Validez une modification du framework dans une VRAIE app, paquets, global.json, restore, tout le chemin du consommateur, sans couper une version ni lancer un pipeline. Le premier consommateur de la piste W a prouvé sa migration ainsi : des images identiques octet pour octet depuis la compilation du paquet et depuis la compilation de la source.

Dans le checkout du framework :

dotnet pack -c Release -p:PackageVersion=1.0.0-dev -o artifacts/local-feed

1.0.0-dev n'entre jamais en collision avec une version publiée, les deux SDK dérivent la version de chaque paquet frère de la leur, et ni les samples ni les tests ne s'empaquettent : le dépôt est tout produit. Dans l'app :

  • global.json : { "msbuild-sdks": { "eQuantic.UI.Sdk.Native": "1.0.0-dev" } } (ou eQuantic.UI.Sdk)
  • nuget.config : <clear />, puis le dépôt local D'ABORD, nuget.org ensuite
  • le fichier de projet : Sdk="eQuantic.UI.Sdk.Native", sans attribut de version

Après chaque repack, videz la copie en cache et restaurez de nouveau : NuGet ne retélécharge jamais une version qu'il a déjà. ClearEQuanticCache vit dans le Directory.Build.targets du framework, donc lancez-le depuis ce checkout (il atteint chaque projet par la solution) ; il retire chaque paquet equantic.* du cache NuGet de façon réactive (une liste tenue à la main a une fois manqué tous les paquets natifs) :

dotnet msbuild -t:ClearEQuanticCache
dotnet restore --force

Preuve sans affichage pour une app native, aucune fenêtre nécessaire : --Photon:ScreenshotPath out.png.

Fichiers clés

Fichier Responsabilité
Sdk/Sdk.props Ajoute les paquets du framework, ou, à côté d'un arbre de sources, les PROJETS du framework et les deux arêtes d'outils (IsEQuanticDevMode)
Sdk/Sdk.targets Trouve les outils (_EqSourceTree : la compilation de l'arbre ou le tools/ du paquet), résout Bun, installe les items <BunPackage>, compile les composants (avec les source maps qu'EQuanticSourceMaps demande, et aucune dans une publication), copie runtime.js, écrit les vecteurs et l'icône de l'app, définit son dossier de sortie en static web assets ; échoue par nom quand un outil manque (EQ4001, EQ4002)
Runtime.csproj Le TypeScript du runtime et sa preuve : -t:TestRuntime lance bun install --frozen-lockfile, tsc et vitest run par le bun embarqué. Il n'empaquette rien et n'est pas empaqueté (IsPackable=false)
Server.csproj Le seul écrivain du runtime : empaquette boot.ts → wwwroot/runtime.js avant chaque compilation, l'embarque (servi à /_equantic/runtime.js) et empaquette le même fichier comme tools/runtime/runtime.js
Runtime.{Os}{Arch}.csproj Empaquette l'exécutable Bun compressé pour un OS et une architecture
Sdk.csproj Empaquette Sdk/, et rassemble eqc et eqicon dans tools/net10.0/ depuis les projets d'outils qu'il référence comme arêtes

Cibles MSBuild (ordre d'exécution)

Dans le SDK (consommateur)

Dans l'ordre où elles tournent dans un dotnet build :

  1. EQuanticRequireIconTool / EQuanticVectors - Quand Assets/ existe, avant la compilation du C# : échoue avec EQ4001 si eqicon.dll manque, sinon écrit Assets/**/*.svg comme Vectors.g.cs
  2. CoreCompile - La compilation du C# de l'app, la cible propre du SDK .NET ; tout ce qui est en dessous tourne après elle
  3. ResolveBunZipPath - Trouve le .zip de Bun dans le cache NuGet (ou à côté du SDK dans un arbre de sources)
  4. EnsureBunExtracted - Extrait l'exécutable au besoin
  5. ResolveBunPath - Définit $(BunPath) pour la suite
  6. InstallBunPackages - Installe les items <BunPackage> par bun add (voir BunPackage)
  7. EQuanticRequireCompiler - Échoue avec EQ4002 quand eqc.dll n'est pas là où EqcCliPath le dit
  8. CompileEQuanticUI - Transpile C# → TypeScript → JavaScript
  9. CopyEQuanticRuntime - Copie le runtime.js servi depuis le paquet Server (à côté d'un arbre de sources, le wwwroot/runtime.js du projet Server) vers wwwroot/_equantic/, et supprime un equantic.css qu'un SDK antérieur y a laissé
  10. EQuanticWebAppIcon - Quand une icône d'app est déclarée : écrit les tailles de l'icône et le manifeste web
  11. _EQuanticOutputAsStaticWebAssets - Définit wwwroot/_equantic/ en static web assets d'après ce que 8 à 10 ont écrit, les cartes laissées dehors

Les étapes 3 à 10 tournent où elles tournent à cause de la dernière. _EQuanticOutputAsStaticWebAssets s'accroche à $(GenerateComputedBuildStaticWebAssetsDependsOn), le crochet propre du pipeline static web assets pour les actifs qu'une compilation génère, qui tourne après CoreCompile et avant que le pipeline ne résolve ses entrées, et elle dépend du compilateur, de la copie du runtime et de l'icône de l'app. CompileEQuanticUI déclare toujours BeforeTargets="Build", et à ce moment-là elle a déjà tourné.

Le dossier n'est pas Content pour la même raison. Le pipeline enregistre ce que wwwroot/ contient pendant que le projet s'évalue, et la compilation réécrit ensuite ce dossier : un fichier enregistré puis supprimé faisait échouer la compression de la publication suivante. C'était chaque carte qu'une compilation Release retire, et chaque chunk partagé que bun renomme quand un composant que deux pages partagent change, dans la séquence la plus ordinaire qui soit : modifier, puis publier. Défini depuis le disque une fois que ses écrivains ont tourné, le dossier atteint la compression, les manifestes et la publication tel que cette compilation l'a écrit, et cela seulement. Une publication --no-build charge le manifeste de la compilation à la place, donc elle porte les fichiers que la dernière compilation a écrits. Avec EnableEQuanticUICompilation=false aucune des deux moitiés ne s'applique, et les fichiers du dossier restent du Content ordinaire.

Dans le SDK (publication du consommateur)

  1. _EQuanticKeepSourceMapsOutOfPublish - Après ComputeResolvedFilesToPublishList : sort chaque .map sous wwwroot/_equantic/ de ResolvedFileToPublish, quoi que dise EQuanticSourceMaps. Les cartes ne sont pas non plus parmi les web assets ci-dessus, donc ceci est la seconde ligne

Dans Runtime (-t:TestRuntime)

  1. ResolveBunForRuntime - Trouve le Bun embarqué dans l'arbre de sources
  2. TestRuntime - bun install --frozen-lockfile, puis tsc et vitest run. Rien n'est empaqueté ici

Dans Server (compilation du paquet)

  1. ResolveBunForServer - Trouve le Bun embarqué dans l'arbre de sources
  2. BundleRuntime - Compile boot.ts → wwwroot/runtime.js, avant chaque compilation
  3. CollectServedRuntime - Au pack, met le wwwroot/runtime.js que la compilation a écrit sous tools/runtime/runtime.js, et n'empaquette plus jamais

Architecture des paquets et auto-suffisance

eQuantic.UI suit une architecture de paquets auto-suffisante où chaque paquet gère ses propres artefacts. Le SDK agit en orchestrateur, en référençant les autres paquets par les propriétés $(Pkg*) de NuGet.

Principes de l'architecture

Avant (problématique) :

SDK Package ❌
├─ Embedded runtime.js (copied from Server)
└─ Embedded *.cs files (copied from Components)

Problèmes : couplage fort, conflits de version, duplication d'artefacts

Après (correct) :

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_*)

Bénéfices : découplage, versionnage correct, aucune duplication

Déploiement de runtime.js

1. Empaquetage (développement)

Chaque compilation d'eQuantic.UI.Server écrit le bundle, et le compilateur l'embarque ; au pack une cible rassemble le même fichier. Un <Content Condition="Exists(…)"> statique serait évalué avant que le bundle n'existe dans une compilation propre, et la cible rassemble ce que la compilation a écrit sans empaqueter de nouveau, donc un pack sans compilation ne peut pas écrire un second bundle après l'embarquement :

<!-- eQuantic.UI.Server.csproj -->
<PropertyGroup>
  <TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CollectServedRuntime</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>

<ItemGroup>
  <!-- une SORTIE DE COMPILATION, jamais un fichier commité -->
  <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>

Le paquet qui sert le runtime est celui qui le livre, donc ce qu'une app reçoit et ce que sa compilation copie sont un seul fichier.

2. Déploiement (compilation du consommateur)

Pendant dotnet build, la cible CopyEQuanticRuntime du SDK s'exécute :

<!-- Sdk/Sdk.targets -->
<Target Name="CopyEQuanticRuntime" AfterTargets="CompileEQuanticUI"
        Condition="'$(EnableEQuanticUICompilation)' == 'true'">
  <PropertyGroup>
    <!-- Le fichier du paquet Server, par une propriété NuGet -->
    <_RuntimeSourcePath Condition="'$(PkgeQuantic_UI_Server)' != ''">$(PkgeQuantic_UI_Server)/tools/runtime/runtime.js</_RuntimeSourcePath>

    <!-- À côté d'un arbre de sources : la sortie de compilation du projet 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 feuille de styles de base qu'un SDK antérieur copiait ici, qu'aucune page ne liait -->
  <Delete Files="$(MSBuildProjectDirectory)/$(EQuanticOutputPath)equantic.css" />
</Target>

Une app en marche ne lit jamais cette copie : le Server répond à /_equantic/runtime.js avec le bundle qu'il embarque, avant les fichiers statiques. La copie est pour les outils qui chargent les modules d'une app hors d'un serveur (l'aperçu de VS Code lit wwwroot/_equantic/runtime.js), et c'est pourquoi c'est ce bundle, octet pour octet.

Déploiement de la source de Components

1. Empaquetage (développement)

Pendant le dotnet pack d'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>

Le paquet Components embarque ses propres fichiers C# pour la résolution de types du compilateur.

2. Compilation (compilation du consommateur)

Pendant dotnet build, la cible CompileEQuanticUI du SDK s'exécute :

<!-- Sdk/Sdk.targets -->
<Target Name="CompileEQuanticUI" BeforeTargets="Build"
        DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences;GenerateGlobalUsings">
  <PropertyGroup>
    <!-- Résout depuis le paquet Components par une propriété NuGet -->
    <_StandardComponentsDir Condition="'$(PkgeQuantic_UI_Components)' != ''">$(PkgeQuantic_UI_Components)/tools/source</_StandardComponentsDir>

    <!-- Repli vers l'arbre de sources (développement seulement) -->
    <_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)')" />

  <!-- Ce qu'on remet à eqc est un fichier, un argument par ligne, parmi eux les assemblages exacts
       contre lesquels csc compile (refs), pour que le modèle sémantique d'eqc corresponde à la
       vraie compilation -->
  <WriteLinesToFile File="$(_EqcArgumentsPath)" Lines="@(_EqcArgument->'%(Value)')" Overwrite="true" />
  <!-- Deux arguments : le compilateur, et ce fichier par son chemin complet -->
  <RunProgram Program="dotnet" Arguments="@(_EqcStartArgument)" />
</Target>

Dans Sdk.targets ce sont trois cibles, enchaînées : ce qu'eqc lit (ses racines de sources et les listes des références, les packs d'icônes et les projets référencés), le fichier de réponse, et eqc lui-même. eqc est démarré avec RunProgram, la tâche avec laquelle le SDK démarre chaque programme : le compilateur et le fichier de réponse par son chemin complet, comme une liste d'arguments, sans aucun shell entre la compilation et le programme. L'Exec propre de MSBuild écrit sa commande dans un script qu'un shell lit d'abord, et il a lu un nom d'assemblage Cash$Flow comme Cash. Donc un nom ou un dossier contenant un $, un accent grave ou un guillemet, simple ou double, atteint eqc tel quel, et bun après lui, auquel eqc remet chaque chemin comme un argument à lui (Depuis 0.2.0-preview.62).

Il en va de même pour chaque autre programme que les deux SDK démarrent : le chmod et le bun add de bun, eqicon, git dans un arbre de sources, et sur macOS codesign, ditto, notarytool, stapler et hdiutil. Un argument est un item dont la métadonnée Value tient le texte, après sa métadonnée Option quand elle en nomme une, donc MSBuild ne le coupe ni à un point-virgule ni n'étend un joker dedans. Avec le dossier des paquets NuGet sous un dossier nommé nuget $b, une compilation s'arrêtait au chmod +x sur le chemin propre de bun. Les tâches sont livrées dans les paquets du SDK, sous tools/netstandard2.0/, et un programme qui échoue sans erreur à lui est EQ4004, en nommant les arguments avec lesquels il a été démarré.

BeforeTargets="Build" est le plus tard que la cible puisse tourner. Dans une compilation elle tourne plus tôt, juste après CoreCompile, parce que la cible qui définit le dossier de sortie en static web assets dépend d'elle (voir l'ordre d'exécution ci-dessus).

Bénéfices architecturaux clés

  • Découplage : le SDK n'embarque pas d'artefacts d'autres paquets
  • Versionnage correct : le SDK lit les artefacts de la version que NuGet a résolue, il ne porte jamais une copie qui pourrait être en désaccord avec le paquet installé
  • Aucune duplication : chaque artefact n'existe que dans son paquet d'origine
  • Souplesse : les paquets évoluent indépendamment sans couplage fort
  • Interface claire : le SDK référence les paquets par des propriétés NuGet bien définies ($(Pkg*))
  • Repli de développement : les chemins de l'arbre de sources fonctionnent pour le développement du framework

Stratégie du bundle unique pour le runtime

Le runtime est UN fichier : le BundleRuntime du Server remet à bun un seul --outfile, et une page l'atteint comme un unique module nu, @equantic/runtime, que l'import map du shell pointe vers /_equantic/runtime.js.

Pourquoi un bundle unique ?

  • Déploiement simplifié : un seul fichier à copier (runtime.js)
  • Aucune gestion de chunks : aucune partie du runtime n'est coupée en un fichier à elle qu'une page devrait trouver
  • Distribution fiable : garantie que toutes les fonctions du runtime (logger, surcouche d'erreurs) sont incluses
  • Taille : un budget, pas un nombre cité ici. ServedRuntimeBudgetTests compresse en gzip les octets que le Server embarque (CompressionLevel.SmallestSize) et les compare au relevé dans tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json : une croissance au-delà de 1 % échoue, et de même un rétrécissement au-delà de 5 %, ce qui verrouille un gain. EQ_UPDATE_RUNTIME_BUDGET=1 écrit le relevé, et le commit qui le fait dit ce qui l'a déplacé

Les modules de page sont le cas inverse. eqc lance bun avec --splitting, donc ce que deux modules ou plus partagent est un chunk nommé d'après l'un d'eux avec un hachage (NotFoundScreen-<hash>.js dans le sample du dashboard est son shell de console, pas un second type de ce nom), et il se charge une fois, avec la première page qui en a besoin. Leurs tailles sont rapportées plutôt que bornées, puisque un module de page est le code propre de l'app : la CI écrit la taille de chacun, brute et gzippée, dans le résumé du job de chaque pull request.

bun calcule ce hachage d'après ce que le chunk contient et d'après le chemin de chacun de ses modules, pris du dossier où bun tourne. Donc eqc lance bun dans le dossier du projet, où qu'eqc lui-même tourne, et un chunk garde son nom d'une compilation à la suivante tant que ce qu'il contient reste le même.

Clone this wiki locally