Skip to content

Fixing layout - #56

Merged
kieronlanning merged 6 commits into
mainfrom
fixing-layout
Oct 24, 2026
Merged

kieronlanning merged 6 commits into
mainfrom
fixing-layout

Conversation

@kieronlanning

Copy link
Copy Markdown
Contributor

No description provided.

Both diagrams are written out as images whenever the generated diagram changes, on by default.

The ER diagram exports SVG and PNG through d2, whatever renderer is configured: the interactive
renderer is a browser application with no headless path, and d2 is the only format that both renders
images itself and keeps column-level edges. Its .d2 source is written alongside the diagram for the
other renderers purely to be rendered.

The architecture diagram exports PNG, and optionally JPG, through likec4 export. There is no SVG
option because the CLI offers none - it renders by screenshotting each view in a headless browser.
The export container reuses the server's single bind mount and working directory so the relative
include paths and image aliases in likec4.config.json resolve to the same files.

Export is gated on the same comparison that guards the file write, so an unchanged model exports
nothing, and runs in the background so it never delays startup. Failures are best-effort: they are
reported and leave the generated diagram untouched.

Image export is disabled in TestAppHostFixture alongside FormatGeneratedFile, for the same reason
plus one more - the fixture deletes its output directory on teardown, which would race an export
still writing into it. ImageExportTests covers both exporters against the real containers instead.
Both diagrams now export into ./.aspirec4/images/ rather than each writing into its own
generated-output directory. The ER diagram's images were landing among the interactive
viewer's index.html, app.js and vendor/, which is the last place anyone would look.

The architecture diagram's files are prefixed "architecture-", because likec4 export names
each file after its view id, and a bare index.png says nothing in a shared folder - it could
collide with the ER diagram outright. They are exported to a staging directory and moved in,
since the CLI gives no say over the name. The ER diagram's images drop the ".gen" marker: it
earns its place on a generated source file in a directory a consumer also hand-edits, but
not on an image.

The directory is resolved from the AppHost directory rather than either diagram's
OutputDirectory, so the two cannot drift apart when only one is overridden. The ER diagram's
dashboard row gained an Images property with the resolved path and an Open Diagram Images
command, because the dashboard cannot navigate to a file:// path from its own origin.

Both container invocations now bind their mounts with --mount instead of -v. A Windows host
path already contains a colon, so "C:\...\erd:/data:ro" left the runtime guessing where the
drive letter ended and the mode began - and it guessed wrong, failing with "invalid volume
specification" under load. --mount parses key=value and cannot be ambiguous.

ImageExportTests is marked [NotInParallel]: every test there starts a container, and several
at once alongside the rest of the suite loaded the runtime enough to make it reject valid
invocations. Its assertions now carry what the exporter logged, so a failed export explains
itself instead of reporting a count of zero.
A property the model readers have no store type for was taken for a navigation, so it gained a
table named after the framework type. A `JsonDocument Payload` field produced a table called
"JsonDocument" that is in nobody's model, and because the metadata reader also walks a
navigation's own properties, one unmapped type dragged a tree of them in behind it: a model with
a JsonDocument and an XDocument field produced 11 tables instead of 1.

Two fixes, applied to both the analyser and the reflection-based metadata reader, which had the
same gap. The System.Text.Json document types now resolve to a `json` store type, so such a
field appears as the column it is. And a type whose namespace is rooted in System or Microsoft
is never treated as a navigation target, so the next unmapped framework type does not reintroduce
the same class of bug.

Asserted on the parsed tables rather than on the descriptor text, because the CLR type name
legitimately appears there on the column itself.
The d2 export is a different diagram, not a differently-styled one: d2 draws sql_table shapes
with its own theme and layout, while the viewer draws React Flow cards with context colours, key
badges, complex-type groups and crow's-foot markers. WithViewerFidelityExport() renders through
the viewer itself so the file matches its Save as SVG/PNG output exactly. Off by default: the
viewer only runs in a browser, so it pulls a 3.7 GB image and takes seconds rather than
milliseconds. Interactive only; every other renderer exports through d2 and says so.

The viewer gained a __aspirec4RenderImage hook, split out of its download path, so a headless
render can take the data URL directly instead of intercepting a browser download. The script
drives chromium over raw CDP using only Node's own APIs, because the Playwright image ships the
browsers but not the playwright package, and installing one would put a network fetch on a path
meant to be hermetic. Note that toSvg returns percent-encoded text while toPng returns base64,
so the decode keys off the declared encoding rather than the format.

Export is now also gated on what is on disk. The in-memory change check starts empty every time
the AppHost does, so a restart re-rendered images that were already correct - on the viewer path
that meant pulling and starting a browser to reproduce identical bytes. A hash stamp beside the
images records the model, the formats, the renderer and which renderer produced them, and is
checked before anything is written or run. It is only trusted while the files it describes exist,
so clearing the folder still re-exports, and only written once every requested format landed.

Publish exports nothing at all. An export is fire-and-forget and publish exits immediately after
generating its output, so it would be killed part-way through; publish is also expected to be
hermetic rather than reaching for a container runtime a build agent may not have. The rule is
asserted directly through ShouldExportImages, because driving it through a publish cannot tell
"skipped" apart from "had not finished yet" - the first attempt at that test passed with the
guard removed.
The images are the one piece of generated output worth keeping: a diagram that shows up in a
pull request is documentation. The generated directory stays ignored, with the images carved
out of it - note the trailing /* rather than /, because git will not descend into an excluded
directory and the re-include would otherwise be unreachable. The stamp beside them is a local
cache and stays ignored. The sample AppHost's own images are committed as the worked example.

The stamp now ignores the port in an endpoint URL. The architecture model embeds the URLs
Aspire allocated and those ports differ on every run, so hashing them verbatim invalidated the
stamp every single time. Only the port is replaced, so a host or scheme that genuinely moved is
still noticed.

That does not make the architecture diagram fully cached, and the documentation no longer
claims it does. Its run-state tags also settle during startup, and those genuinely change the
picture, so normalising them away would hide a real difference. Measured across restarts the
exported PNGs come out byte-for-byte identical, so git sees nothing - what is left is a
background render rather than a dirty tree. The ER diagram, which is derived from the EF model
alone, is fully cached and renders nothing at all on an unchanged schema.
@kieronlanning
kieronlanning enabled auto-merge (squash) October 24, 2026 16:39
@kieronlanning
kieronlanning merged commit ded6f8d into main Oct 24, 2026
1 check passed
@kieronlanning
kieronlanning deleted the fixing-layout branch October 24, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant