Skip to content

BuildFlow

Edgar Mesquita edited this page Sep 23, 2026 · 19 revisions

eQuantic.UI Build Flow

🌐 This page in: English · Português

This document describes the eQuantic.UI build flow, demonstrating how the framework maintains zero external dependencies for the consumer.

Visual Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                           DEVELOPMENT (source tree)                         │
└─────────────────────────────────────────────────────────────────────────────┘

  src/eQuantic.UI.Runtime/src/**/*.ts          src/eQuantic.UI.Sdk/Resources/boot.ts
         │                                              │
         └──────────────────┬───────────────────────────┘
                            ▼
  ┌──────────────────────────────────┐
  │  dotnet build                    │
  │  (eQuantic.UI.Server)            │
  │                                  │
  │  ResolveBunForServer target:     │
  │  └─ the embedded bun from the    │
  │     Runtime.{Os}{Arch} project   │
  │     (extracted from .zip, +x)    │
  │                                  │
  │  BundleRuntime target, before    │
  │  every build:                    │
  │  └─ "$(_BunPath)" build boot.ts  │
  │     --outfile wwwroot/runtime.js │
  └──────────────────────────────────┘
         │
         ▼
  wwwroot/runtime.js  ──── the ONE bundle: embedded in Server.dll and served at
         │                 /_equantic/runtime.js, and the file a source-tree build
         │                 copies beside an app's modules
         ▼
  ┌──────────────────────────────────┐
  │  dotnet pack                     │
  │  (every packable project)        │
  │                                  │
  │  Server: CollectServedRuntime    │
  │  └─ wwwroot/runtime.js →         │
  │     tools/runtime/runtime.js     │
  │  Sdk: CollectBuildOutput         │
  │  └─ eqc + eqicon →               │
  │     tools/net10.0/               │
  └──────────────────────────────────┘
         │
         ▼
  *.nupkg — bin/Release/ in CI, or a local feed (see below); the version comes from
  Directory.Build.props (or the release tag), never from a file in the tree


┌─────────────────────────────────────────────────────────────────────────────┐
│                        CONSUMER (client project)                            │
└─────────────────────────────────────────────────────────────────────────────┘

  MyApp.csproj
  └─ <Project Sdk="eQuantic.UI.Sdk">          ← that line is the whole build pipeline
  global.json
  └─ { "msbuild-sdks": { "eQuantic.UI.Sdk": "<version>" } }   ← the ONE place the version lives
  (no PackageReference: the SDK adds its own)

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

> **Which Bun?** `src/eQuantic.UI.Runtime/bun-toolchain.json` holds the version and a SHA-256 per
> platform, checked on every test run. The digests prove the committed bytes are the ones the
> release published; the version is verified by extracting the host's binary and running it, because
> a manifest nobody compares against the thing it describes drifts from it.
>
> All six platform packages (`Osx64`, `OsxArm64`, `Win64`, `WinArm64`, `Linux64`, `LinuxArm64`) carry
> the same build; a different Bun per architecture is how you get a bundle that only fails on one
> person's machine.
>
> Updating it: replace the `.zip` files, run the tests once with `EQ_UPDATE_BUN_MANIFEST=1`, and read
> the diff. A version that moved with digests that did not is a mistake, and so is the reverse.

         │
         ▼
  ┌────────────────────────────────────────┐
  │  dotnet build                          │
  │                                        │
  │  wwwroot/_equantic/** is not Content:  │
  │  this build writes it (step 11)        │
  │                                        │
  │  In the order the targets run:         │
  │                                        │
  │  1. EQuanticVectors                    │  (when Assets/ exists)
  │     └─ eqicon: *.svg → C#, before the  │
  │        compile reads it                │
  │                                        │
  │  2. CoreCompile                        │
  │     └─ the app's C#, .NET's own target │
  │                                        │
  │  3. ResolveBunZipPath                  │
  │     └─ $(PkgeQuantic_UI_Runtime_       │
  │        OsxArm64)/tools/bun/*.zip       │
  │                                        │
  │  4. EnsureBunExtracted                 │
  │     ├─ Unzip if needed                 │
  │     └─ chmod +x (Unix)                 │
  │                                        │
  │  5. ResolveBunPath                     │
  │     └─ Defines $(BunPath)              │
  │                                        │
  │  6. InstallBunPackages                 │
  │     ├─ <BunPackage> → bun add          │
  │     └─ Symlink node_modules            │
  │                                        │
  │  7. EQuanticRequireCompiler            │
  │     └─ eqc missing → EQ4002            │
  │                                        │
  │  8. CompileEQuanticUI                  │
  │     └─ dotnet eqc.dll ... --refs       │
  │        --bun "$(BunPath)"              │
  │        --source-maps                   │
  │        $(EQuanticSourceMaps)           │
  │                                        │
  │  9. CopyEQuanticRuntime                │  ◄── runtime.js deployment
  │     └─ the served runtime.js,          │
  │        from the Server package,        │
  │        to wwwroot/_equantic/           │
  │                                        │
  │ 10. EQuanticWebAppIcon                 │  (when an AppIcon is declared)
  │     └─ eqicon →                        │
  │        wwwroot/_equantic/icons         │
  │                                        │
  │ 11. _EQuanticOutputAsStaticWebAssets   │  ◄── the static web assets hook
  │     └─ wwwroot/_equantic/ as 8 to 10   │
  │        wrote it, maps left out,        │
  │        defined as static web assets    │
  │                                        │
  │  then the static web assets pipeline   │
  │  compresses them and writes the        │
  │  manifests: this build's files         │
  └────────────────────────────────────────┘
         │
         ▼
  wwwroot/_equantic/
  ├─ runtime.js (the Server's bundle, byte for byte)
  ├─ *.js (compiled components, and the chunks they share)
  └─ *.js.map (Debug only, by default)
         │
         ▼
  ┌──────────────────────────────────┐
  │  dotnet run                      │
  │                                  │
  │  Server serves:                  │
  │  ├─ runtime.js (from Server.dll) │
  │  └─ *.js (from wwwroot/_equantic)│
  └──────────────────────────────────┘
         │
         ▼
  Browser loads application

Bun Source by Component

Component Bun Source
Runtime (-t:TestRuntime) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (source tree)
Server (package build) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ (source tree)
SDK (consumer) $(PkgeQuantic_UI_Runtime_{Os}{Arch})/tools/bun/ (NuGet cache)
SDK (source tree) eQuantic.UI.Runtime.{Os}{Arch}/tools/bun/ beside the SDK's own files

Consumer Requirements

The consumer only needs:

  • .NET SDK 10.0
  • dotnet restore + dotnet build

No Node.js, npm, or global Bun installation required.

Developing the framework: no packages at all

A build from the repository's own tree — the samples, a scaffold under test — never goes through a package. (The website is NOT one of these any more: equantic-web consumes released packages through global.json, like any other consumer. The section after next is how a repo outside this tree gets the no-package loop anyway.) Sdk.props sees the source tree beside it and swaps every PackageReference for a ProjectReference; Sdk.targets runs the eqc and eqicon the graph just built, and copies runtime.js from src/eQuantic.UI.Server/wwwroot/runtime.js, which the Server project's BundleRuntime writes before every build of the Server. There is no local feed to fill, no cache to clear, and no pack step between an edit and the sample that shows it:

dotnet build samples/DefaultUIDashboard

The mechanism — IsEQuanticDevMode, the tool edges, _EqSourceTree — is described in Package Architecture. The section below is for the OTHER question: does the change survive the real consumer path?

The same loop from ANOTHER repository

An app developed alongside the SDK — a consumer repo, not a sample — wants the same thing the samples get: edit the framework, rebuild the app, see it. It can have that, and the mechanism is the one the samples already use rather than anything new. _DevRootDir is relative to where Sdk.props PHYSICALLY SITS, so importing the SDK by path from anywhere lands the probe inside the tree and dev mode turns on.

Four things about this are not guessable, and each one was measured rather than reasoned about.

Dev mode reaches only the project that imports the SDK. In a solution of a head plus libraries, switching the head alone gives you the worst half of the deal: the app RUNS against your working tree while the libraries still COMPILE against the pinned package, so the moment you change an API — the thing the loop exists for — the libraries compile clean and the app fails at run time with a missing member. Every project that references eQuantic.UI has to fork.

The property has to be set before the SDK import, so Directory.Build.props is too late for the head. Directory.Build.props is imported by Microsoft.NET.Sdk's own props, which the eQuantic SDK imports at its top — by the time it is read, the head's conditional <Import> has already been evaluated and skipped. This fails SILENTLY: you get released packages while believing you are on the tree, and you find out while debugging something else.

A shared props file imported by both the head and Directory.Build.props needs a sentinel, or MSBuild raises MSB4011 for the second import.

The released-package arm needs global.json. Without msbuild-sdks pinning a version, the fallback fails with MSB4236: The SDK could not be found and a resolver message that does not mention the missing pin.

The whole shape, four files:

<!-- EQuanticDev.props — the one definition, read by the head AND by 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>
<!-- The HEAD: a bare <Project>, because the Sdk attribute cannot carry a 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>
<!-- Every LIBRARY that references eQuantic.UI forks the same way -->
<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>

Check it took, rather than assuming — the failure is silent:

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/... and NOT ~/.nuget/packages/

What it costs: the first build compiles the whole framework from source, and a broken tree becomes a broken app build. The Exists(...) guard is what keeps CI and a clean clone on released packages.

Adding your own project to your own solution can drag this repo into it. dotnet sln add reads the csproj, sees the dev-mode ProjectReferences and pulls eQuantic.UI.Primitives, Components and the icon pack into YOUR .sln. Harmless until someone builds the fallback arm, where those paths do not exist and the solution stops loading. Check the .sln after adding a project, or add with --in-root and keep the references out.

Consuming an unreleased SDK from local packages

Since 0.2.0-preview.45

Validate a framework change in a REAL app — packages, global.json, restore, the whole consumer path — without cutting a version or running a pipeline. The first Track W consumer proved its migration this way: byte-identical frames from the package build and from the source build.

In the framework checkout:

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

1.0.0-dev never collides with a published version, both SDKs derive the version of every sibling package from their own, and neither the samples nor the tests pack — the feed is all product. In the app:

  • global.json: { "msbuild-sdks": { "eQuantic.UI.Sdk.Native": "1.0.0-dev" } } (or eQuantic.UI.Sdk)
  • nuget.config: <clear />, then the local feed FIRST, nuget.org after
  • the project file: Sdk="eQuantic.UI.Sdk.Native", no version attribute

After every repack, clear the cached copy and restore again — NuGet never re-downloads a version it already has. ClearEQuanticCache lives in the framework's Directory.Build.targets, so run it from that checkout (it reaches every project through the solution); it removes every equantic.* package from the NuGet cache reactively (a hand-kept list once missed all the native packages):

dotnet msbuild -t:ClearEQuanticCache
dotnet restore --force

Headless proof for a native app, no window needed: --Photon:ScreenshotPath out.png.

Key Files

File Responsibility
Sdk/Sdk.props Adds the framework packages — or, beside a source tree, the framework PROJECTS and the two tool edges (IsEQuanticDevMode)
Sdk/Sdk.targets Finds the tools (_EqSourceTree: the tree's build or the package's tools/), resolves Bun, installs <BunPackage> items, compiles components (with the source maps EQuanticSourceMaps asks for, and none in a publish), copies runtime.js, writes vectors and the app icon, defines its output folder as static web assets; fails by name when a tool is missing (EQ4001, EQ4002)
Runtime.csproj The runtime's TypeScript and its proof: -t:TestRuntime runs bun install --frozen-lockfile, tsc and vitest run through the embedded bun. It bundles nothing and is not packed (IsPackable=false)
Server.csproj The runtime's one writer: bundles boot.ts → wwwroot/runtime.js before every build, embeds it (served at /_equantic/runtime.js) and packs the same file as tools/runtime/runtime.js
Runtime.{Os}{Arch}.csproj Packages the zipped Bun executable for one OS and architecture
Sdk.csproj Packs Sdk/, and collects eqc and eqicon into tools/net10.0/ from the tool projects it references as edges

MSBuild Targets (Execution Order)

In SDK (consumer)

In the order they run in a dotnet build:

  1. EQuanticRequireIconTool / EQuanticVectors - When Assets/ exists, before the C# compile: fails with EQ4001 if eqicon.dll is missing, otherwise writes Assets/**/*.svg as Vectors.g.cs
  2. CoreCompile - The app's C# compile, the .NET SDK's own target; everything below runs after it
  3. ResolveBunZipPath - Finds the Bun .zip in the NuGet cache (or beside the SDK in a source tree)
  4. EnsureBunExtracted - Extracts the executable if needed
  5. ResolveBunPath - Defines $(BunPath) for later use
  6. InstallBunPackages - Installs <BunPackage> items via bun add (see BunPackage)
  7. EQuanticRequireCompiler - Fails with EQ4002 when eqc.dll is not where EqcCliPath says
  8. CompileEQuanticUI - Transpiles C# → TypeScript → JavaScript
  9. CopyEQuanticRuntime - Copies the served runtime.js from the Server package (beside a source tree, the Server project's wwwroot/runtime.js) to wwwroot/_equantic/, and deletes an equantic.css an earlier SDK left there
  10. EQuanticWebAppIcon - When an app icon is declared: writes the icon sizes and the web manifest
  11. _EQuanticOutputAsStaticWebAssets - Defines wwwroot/_equantic/ as static web assets from what 8 to 10 wrote, the maps left out

Steps 3 to 10 run where they do because of the last one. _EQuanticOutputAsStaticWebAssets hangs on $(GenerateComputedBuildStaticWebAssetsDependsOn), the static web assets pipeline's own hook for assets a build generates, which runs after CoreCompile and before the pipeline resolves its inputs, and it depends on the compiler, the runtime copy and the app icon. CompileEQuanticUI still declares BeforeTargets="Build", and by then it has already run.

The folder is not Content for the same reason. The pipeline registers what wwwroot/ holds while the project evaluates, and the build then rewrites this folder: a file registered and then deleted failed the next publish's compression. That was every map a Release build removes, and every shared chunk bun renames when a component two pages share changes, in the most ordinary sequence there is: edit, then publish. Defined from disk once its writers have run, the folder reaches the compression, the manifests and the publish as this build wrote it, and only that. A --no-build publish loads the build's manifest instead, so it carries the files the last build wrote. With EnableEQuanticUICompilation=false neither half applies, and the folder's files stay ordinary Content.

In SDK (consumer publish)

  1. _EQuanticKeepSourceMapsOutOfPublish - After ComputeResolvedFilesToPublishList: takes every .map under wwwroot/_equantic/ out of ResolvedFileToPublish, whatever EQuanticSourceMaps says. The maps are not among the web assets above either, so this is the second line

In Runtime (-t:TestRuntime)

  1. ResolveBunForRuntime - Finds the embedded Bun in the source tree
  2. TestRuntime - bun install --frozen-lockfile, then tsc and vitest run. Nothing is bundled here

In Server (package build)

  1. ResolveBunForServer - Finds the embedded Bun in the source tree
  2. BundleRuntime - Compiles boot.ts → wwwroot/runtime.js, before every build
  3. CollectServedRuntime - At pack time, puts the wwwroot/runtime.js the build wrote under tools/runtime/runtime.js, and never bundles again

Package Architecture & Self-Containment

eQuantic.UI follows a self-contained package architecture where each package manages its own artifacts. The SDK acts as an orchestrator, referencing other packages via NuGet's $(Pkg*) properties.

Architecture Principles

Before (Problematic):

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

Problems: tight coupling, version conflicts, artifact duplication

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

Benefits: decoupling, correct versioning, no duplication

Runtime.js Deployment

1. Packaging (Development)

Every build of eQuantic.UI.Server writes the bundle, and the compiler embeds it; at pack time a target collects the same file. A static <Content Condition="Exists(…)"> would be evaluated before the bundle exists on a clean build, and the target collects what the build wrote without bundling again, so a pack with no build cannot write a second bundle after the embed:

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

<ItemGroup>
  <!-- a BUILD OUTPUT, never a committed file -->
  <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>

The package that serves the runtime is the one that ships it, so what an app is served and what its build copies are one file.

2. Deployment (Consumer Build)

During dotnet build, the SDK's CopyEQuanticRuntime target executes:

<!-- Sdk/Sdk.targets -->
<Target Name="CopyEQuanticRuntime" AfterTargets="CompileEQuanticUI"
        Condition="'$(EnableEQuanticUICompilation)' == 'true'">
  <PropertyGroup>
    <!-- The Server package's file, via NuGet property -->
    <_RuntimeSourcePath Condition="'$(PkgeQuantic_UI_Server)' != ''">$(PkgeQuantic_UI_Server)/tools/runtime/runtime.js</_RuntimeSourcePath>

    <!-- Beside a source tree: the Server project's own build output -->
    <_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" />
  <!-- the base stylesheet an earlier SDK copied here, which no page linked -->
  <Delete Files="$(MSBuildProjectDirectory)/$(EQuanticOutputPath)equantic.css" />
</Target>

A running app never reads this copy: the Server answers /_equantic/runtime.js with the bundle it embeds, ahead of the static files. The copy is for the tools that load an app's modules outside a server (the VS Code preview reads wwwroot/_equantic/runtime.js), which is why it is that bundle, byte for byte.

Components Source Deployment

1. Packaging (Development)

During dotnet pack of 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>

The Components package embeds its own C# source files for compiler type resolution.

2. Compilation (Consumer Build)

During dotnet build, the SDK's CompileEQuanticUI target executes:

<!-- Sdk/Sdk.targets -->
<Target Name="CompileEQuanticUI" BeforeTargets="Build"
        DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences;GenerateGlobalUsings">
  <PropertyGroup>
    <!-- Resolve from Components package via NuGet property -->
    <_StandardComponentsDir Condition="'$(PkgeQuantic_UI_Components)' != ''">$(PkgeQuantic_UI_Components)/tools/source</_StandardComponentsDir>

    <!-- Fallback to source tree (development only) -->
    <_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)')" />

  <!-- --refs: the exact assemblies csc compiles against, so eqc's semantic model matches the real build -->
  <Exec Command="dotnet $(EqcCliPath) &quot;$(_EqcSourceDirs)&quot; $(MSBuildProjectDirectory)/$(EQuanticOutputPath) --refs &quot;$(_EqcRefsFile)&quot; ..." />
</Target>

BeforeTargets="Build" is the latest the target can run. In a build it runs earlier, right after CoreCompile, because the target that defines the output folder as static web assets depends on it (see the execution order above).

Key Architectural Benefits

  • Decoupling: SDK doesn't embed artifacts from other packages
  • Correct Versioning: the SDK reads the artifacts of whatever version NuGet resolved — it never carries a copy that can disagree with the installed package
  • No Duplication: Each artifact exists only in its source package
  • Flexibility: Packages evolve independently without tight coupling
  • Clear Interface: SDK references packages via well-defined NuGet properties ($(Pkg*))
  • Development Fallback: Source tree paths work for framework development

Runtime Single Bundle Strategy

The runtime is ONE file: the Server's BundleRuntime hands bun a single --outfile, and a page reaches it as one bare module, @equantic/runtime, which the shell's import map points at /_equantic/runtime.js.

Why Single Bundle?

  • Simplified Deployment: Only one file to copy (runtime.js)
  • No Chunk Management: no part of the runtime is split into a file of its own that a page would have to find
  • Reliable Distribution: Guaranteed that all runtime features (logger, error overlay) are included
  • Size: a budget, not a number quoted here. ServedRuntimeBudgetTests gzips the bytes the Server embeds (CompressionLevel.SmallestSize) and compares them with the record in tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json: growth past 1% fails, and so does a shrink past 5%, which locks a win in. EQ_UPDATE_RUNTIME_BUDGET=1 writes the record, and the commit that does says what moved it

The page modules are the opposite case. eqc runs bun with --splitting, so what two or more modules share is a chunk named after one of them with a content hash (NotFoundScreen-<hash>.js in the dashboard sample is its console shell, not a second type of that name), and it loads once, with the first page that needs it. Their sizes are reported rather than gated, since a page module is the app's own code: CI writes each one's size, raw and gzipped, to the job summary of every pull request.

Clone this wiki locally