Repository navigation
Registry publish drops a module's static web assets: every landed view pack has no wwwroot (EntityViews CSS 404 in prod; blocks the Views/Graph flip) #2221
Description
The defect
PluginBundleEndpoints.MapPublish — the registry's module-publish endpoint — never passes a bundle's static web assets to the shelf. It calls:
ModulePublish.Validate(plugin, manifest, files, version, packagePath) // no staticAssets
ShelveModule(accepted.Module, accepted.Files, …, minMeshVersion: …) // no staticAssetsEvery other part of the lane was already built for them: Validate takes a staticAssets parameter, Accepted carries StaticAssets, ShelveModule takes staticAssets, LandCore materializes them into the generation directory, and the CONSUMER path (PluginBundleClient.LandFromBundle) passes them correctly. Only these two arguments were missing — so every module ever published to the registry landed with assemblies and no wwwroot, and no consumer could receive assets no matter how complete the packed bundle was (a consumer never reads the producer's archive; it fetches what the shelf holds).
Evidence
Production, memex-cloud, landed generation directories (/data/modules/<name>@<gen>):
| module | files | wwwroot |
|---|---|---|
| MeshWeaver.Blazor.Views@3badbda5 | 11 | 0 |
| MeshWeaver.Blazor.Graph@9877c683 | 3 | 0 |
| MeshWeaver.Blazor.EntityViews@12497147 | 8 | 0 |
| MeshWeaver.Blazor.Radzen@dc9459b4 | 3 | 0 |
| MeshWeaver.Blazor.GoogleMaps@26755191 | 2 | 0 |
| MeshWeaver.Blazor.Analysis@59b6417d | 2 | 0 |
The image copy of the same pack carries 58 files including wwwroot/{Components/, _content/, MeshWeaver.Blazor.Views.styles.css, MeshWeaver.Blazor.Views.modules.json}.
The producer side is healthy — I ran the workflow's exact sequence locally on MeshWeaver.Blazor.Views (dotnet build → dotnet publish --no-build → module-pack --deps-closure): the publish output carries wwwroot/ (8 entries, incl. the scoped-CSS bundle and JS initializers) and the packed bundle's manifest declares 254 staticAssets with the tree under meshweaver/moduleassets/. So neither the RCL publish nor the packer is at fault.
Shipped user impact
https://memex.meshweaver.cloud/_content/MeshWeaver.Blazor.EntityViews/MeshWeaver.Blazor.EntityViews.styles.css → 404. EntityViews has been store-only since #2188 retired its image copy, so NumberFieldView.razor.css has not been served since that merge (the rendered page does not even reference the stylesheet). Small blast radius — one component's scoped styles — but live, and it is the same defect that would take down the whole portal's styling one step later:
This blocks the Views/Graph source flip (#2169 Phase B2). MeshWeaver.Blazor.Views.styles.css and MeshWeaver.Blazor.Graph.styles.css return 200 today only because their image copies still exist. Retiring them — the entire point of the flip — would 404 both of the portal's main stylesheets and break collocated JS (CollaborativeMarkdownView.razor.js, DocumentSourceView.razor.js), rendering the portal unstyled. The flip is on hold until this ships and the packs are republished.
Fix
The two missing arguments, through the seam the design already provides (BundleReader.ReadModuleAssets(bytes) → Validate → accepted.StaticAssets → ShelveModule), plus PluginBundlePublishAssetsTest: publishes a real bundle through the endpoint and asserts the asset exists as a file under the landed generation, with an entry-assembly control and a declares-no-assets case. A test is needed precisely because nothing else fails when assets are dropped — the publish returns 200, the entry is recorded, the module loads, pages render, just unstyled.
🚨 Republish is required after the fix
The fix does not retro-fill the shelf: every already-published pack holds asset-less bytes and must be republished to gain its wwwroot. Order for the flip: merge → republish DefaultViews/GraphViews (and the other Blazor packs) → verify the landed generation carries wwwroot → verify the stylesheet URLs 200 while the image copy is still present → only then retire the image copy.
Related: #2169 (extraction program), #2188 (retired EntityViews' image copy — where the 404 starts), #2193 §D (bake/land per consumer identity: "a landed pack must carry its static web assets" is now assertable), #2184 (moved Views/Graph onto the modules lane).
Activity
rbuergi commentedon Aug 25, 2026
Fix implemented and verified locally (branch
fix/registry-shelf-drops-static-assets):MapPublishnow readsBundleReader.ReadModuleAssets(bytes)intoModulePublish.Validateand passesaccepted.StaticAssetstoShelveModule— the seam the design already provided, so shelf and adopt place byte-identical content.PluginBundlePublishAssetsTestpublishes a REAL bundle (built with the productionNuGetPackageWriter) through the endpoint against aModuleLandingServicerooted in a temp dir, and asserts two declared assets materialize as files with byte-exact content — one at the pack root (<pack>.styles.css) and one NESTED (Components/TestView.razor.js), so a partial drop fails too and the relative path that_content/<pack>/…URLs depend on is pinned. Plus an entry-assembly control (the asset assertion must be evidence about assets, not about an all-or-nothing writer) and a declares-no-assets case (the shelf must never fabricate a wwwroot).-warnaserror: 0 warnings, 0 errors. Tests 3/3 green.Verification note: the producer side was re-proven from scratch rather than assumed — running the workflow's exact sequence locally (
dotnet build→dotnet publish --no-build→module-pack --deps-closure) on MeshWeaver.Blazor.Views yields a publishwwwroot/with the scoped-CSS bundle and JS initializers, and a bundle manifest declaring 254staticAssets. So the only defect was at the warehouse door.rbuergi commentedon Aug 25, 2026
Fixed by #2224 (merged, 097e879) — both seams, after the review found the second one:
MapPublishreads the bundle's assets inside the guarded parse and passes them throughValidate→accepted.StaticAssets→ShelveModule.ModuleBundleSource.Collectreturned top-level.dll/.pdbonly andBuildResultemitted nomodule.staticAssets— so the registry would have held a complete pack while handing every consumer an assemblies-only bundle. The registry's own copy would look right while every downstream portal stayed unstyled.Collectnow returns(Files, Assets, DeclineReason)with a recursivewwwrootwalk keyed by module-relative path, andBuildResultwrites the entries and declares them.Tests:
PluginBundlePublishAssetsTest(publish → landed file on disk, two assets incl. a nested one, byte-exact, + entry-assembly control + declares-nothing case) andModuleBundleSourceTest(assets served with exact relative paths, never leaking into the assembly closure, none when the pack has nowwwroot). Publish-side negative control verified: reverting theShelveModuleargument turns the asset test red.🚨 This issue is not done when the code merges. Remaining, and I'm driving it: an image carrying #2224 must be running on the registry (memex-cloud), then every Blazor pack must be republished — the fix does not retro-fill shelf bytes. The verification that closes this issue is
MeshWeaver.Blazor.EntityViews.styles.cssgoing 404 → 200 with the right bytes, since EntityViews has no image copy to mask the result.test(#3244): pin that the sealed build's static web assets ride with it
review(#2384): cover .cshtml.js, align the kind numbering, and say th…