eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsCompilation
eQuantic.UI Build Flow
Edit this page
17 min read
This document describes the eQuantic.UI build flow, demonstrating how the framework maintains zero external dependencies for the consumer.
Visual Flow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │
│ └─ arguments, one a line, in │
│ obj/…/equantic.eqc.rsp │
│ └─ dotnet eqc.dll │
│ @obj/…/equantic.eqc.rsp │
│ │
│ 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.
A class library shared by a solution's projects
Since 0.2.0-preview.62
An app that outgrows one project shares code through a class library: its brand's tokens, a record, a component two apps place. The app references the library with a ProjectReference and writes nothing else about it.
1
2
3
4
5
<Project Sdk="eQuantic.UI.Sdk">
<ItemGroup>
<ProjectReference Include="../Shop.Common/Shop.Common.csproj" />
</ItemGroup>
</Project>
What a page USES of the library reaches the browser, as the app's own types do:
The page
The bundle holds
reads Palette.Cobalt, a static member
a module Palette
runs new Cursor(3), a record
a module Cursor, equal by value there too
places new Wordmark(), a component
a module Wordmark, rendered on the server and rebuilt at hydration
is answered a Receipt by a Server Action
the record's own twin, which reads a long, a date and a Guid as what they are
A type those types name crosses with them, in whichever project it is declared. A record of a Domain project that the library references and the app does not is a module too.
Only what the browser code reaches is compiled. The build writes the app's own types and follows what they import, until no import is unmet. A class of the library that no page names is never compiled for the browser, so a Domain or a Persistence project costs what the pages use of it and reports nothing, whatever else it holds. A Server Action's body does not cross, and neither does a method marked [ServerOnly], so both name the library's server code freely: a Server Action that maps the Domain's Order into the app's own Order reaches the app's and nothing of the Domain. A name the build writes nothing for reaches nothing either: one inside nameof, one in an attribute, and a constant, which is written as its value.
A reached type is held to the rules the app's own types are held to:
•
What a browser cannot run is refused at the library's OWN file, with the diagnostic the same code raises in the app. A class that reads a file is EQ2004.
•
A type or a member marked [ServerOnly] is refused where the page names it (EQ2010).
•
A module is named for its type, so two REACHED types of one name are refused, naming both files (EQ1005). A type nothing reaches collides with nothing.
One case is stricter than it was. A record of a referenced project that a Server Action answers used to cross as data alone, whatever else it held, because it had no twin at all. Its twin is the whole record now, as an app's own record's is, so a method of it that a browser cannot run fails the build at that method, though no page calls it. Until one member can be left out of a twin (#826), keep that method out of the type the page reads: an extension method in a class of its own is called the same way.
Each project keeps its own C# scope: a library's global using directives and its internal types are its own, and the app's are the app's. An internal type the library opens to the app with InternalsVisibleTo crosses as a public one does. That declaration names the app's ASSEMBLY, and the build knows the app by the name its project evaluates to, so an app whose project file sets <AssemblyName> apart from its own name is the friend the library named.
Its strings
A component of the library reads its strings from the library's own .resx. The app's catalogs hold the keys it read, in every culture the library has a .resx for, and no other key of that catalogue. The app and the library may each have a Strings with a Title: each read shows its own project's text.
Writing the library
The library is an ordinary .NET project. One that holds components references the component library, at the version the app's SDK has, and brings the factories into scope:
1
2
3
4
5
6
<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>
A project that holds no component, a Domain, references nothing of eQuantic.UI.
What is not there yet
•
A component of the library has no factory, so a page writes new Wordmark() (#818).
•
A [Page] or a [ServerAction] declared in the library (#819).
•
What a source generator writes in the library does not reach the build (#820).
•
A type of a PACKAGE, which the build has no source for, is still a name nothing defines when a page reads a member of it (#822).
samples/SharedLibrary is this section as three projects: a web app, the library it references and the project that library references.
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:
1
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:
1
2
3
4
5
6
7
8
9
10
<!-- 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>
1
2
3
4
5
<!-- Directory.Build.props -->
<Project>
<Import Project="$(MSBuildThisFileDirectory)EQuanticDev.props" Condition="'$(_EqDevPropsImported)' != 'true'" />
...
</Project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 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>
1
2
3
4
5
6
7
8
9
<!-- 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:
1
2
3
dotnet msbuild src/App/App.csproj -getProperty:IsEQuanticDevMode # true
dotnet msbuild src/Lib/Lib.csproj -t:ResolveReferences -getItem:ReferencePath | grep Primitives
# → .../equantic-ui/src/eQuantic.UI.Primitives/bin/... 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:
1
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):
1
dotnet msbuild -t:ClearEQuanticCache
1
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):
1
2
3
SDK Package
❌
├─ Embedded runtime.js (copied from Server)
└─ Embedded *.cs files (copied from Components)
Problems: tight coupling, version conflicts, artifact duplication
After (Correct):
1
2
3
4
5
6
7
8
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- eQuantic.UI.Server.csproj -->
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CollectServedRuntime</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<ItemGroup>
<!-- 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- Sdk/Sdk.targets -->
<Target Name="CopyEQuanticRuntime" AfterTargets="CompileEQuanticUI"
Condition="'$(EnableEQuanticUICompilation)' == 'true'">
<PropertyGroup>
<!-- 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:
1
2
3
4
5
<!-- eQuantic.UI.Components.csproj -->
<ItemGroup>
<Content Include="**\*.cs" Exclude="obj\**;bin\**" PackagePath="tools\source\" />
<Content Include="SdkResources*.resx" PackagePath="tools\source\" />
</ItemGroup>
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- Sdk/Sdk.targets -->
<Target Name="CompileEQuanticUI" BeforeTargets="Build"
DependsOnTargets="ResolveReferences;FindReferenceAssembliesForReferences;GenerateGlobalUsings">
<PropertyGroup>
<!-- Resolve 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)')" />
<!-- What eqc is handed is a file, one argument a line, among them the exact assemblies csc
compiles against (refs), so eqc's semantic model matches the real build -->
<WriteLinesToFile File="$(_EqcArgumentsPath)" Lines="@(_EqcArgument->'%(Value)')" Overwrite="true" />
<!-- Two arguments: the compiler, and that file by its full path -->
<RunProgram Program="dotnet" Arguments="@(_EqcStartArgument)" />
</Target>
In Sdk.targets this is three targets, chained: what eqc reads (its source roots and the lists of the references, the icon packs and the referenced projects), the response file, and eqc itself. eqc is started with RunProgram, the task the SDK starts every program with: the compiler and the response file by its full path, as a list of arguments, with no shell between the build and the program. MSBuild's own Exec writes its command into a script a shell reads first, and it read an assembly name Cash$Flow as Cash. So a name or a folder holding a $, a backtick or a quote, single or double, reaches eqc as it is, and bun after it, which eqc hands each path as an argument of its own (Since 0.2.0-preview.62).
The same holds for every other program the two SDKs start: bun's chmod and bun add, eqicon, git in a source tree, and on macOS codesign, ditto, notarytool, stapler and hdiutil. An argument is an item whose Value metadata holds the text, after its Option metadata when it names one, so MSBuild neither splits it at a semicolon nor expands a wildcard in it. With the NuGet package folder under a folder named nuget $b, a build used to stop at chmod +x on bun's own path. The tasks ship inside the SDK packages, under tools/netstandard2.0/, and a program that fails without an error of its own is EQ4004, naming the arguments it was started with.
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 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.
bun computes that hash from what the chunk holds and from each of its modules' paths, taken from the folder bun runs in. So eqc runs bun in the project's folder, wherever eqc itself runs, and a chunk keeps its name from one build to the next as long as what it holds stays the same.