Skip to content

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

Closed

Description

@rbuergi

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 staticAssets

Every 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

  1. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    Fix implemented and verified locally (branch fix/registry-shelf-drops-static-assets):

    • MapPublish now reads BundleReader.ReadModuleAssets(bytes) into ModulePublish.Validate and passes accepted.StaticAssets to ShelveModule — the seam the design already provided, so shelf and adopt place byte-identical content.
    • PluginBundlePublishAssetsTest publishes a REAL bundle (built with the production NuGetPackageWriter) through the endpoint against a ModuleLandingService rooted 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).
    • Release + -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 publish wwwroot/ with the scoped-CSS bundle and JS initializers, and a bundle manifest declaring 254 staticAssets. So the only defect was at the warehouse door.

  2. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    Fixed by #2224 (merged, 097e879) — both seams, after the review found the second one:

    1. Publish: MapPublish reads the bundle's assets inside the guarded parse and passes them through Validate → accepted.StaticAssets → ShelveModule.
    2. Serve (found by the Copilot review, and the more consequential half): ModuleBundleSource.Collect returned top-level .dll/.pdb only and BuildResult emitted no module.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. Collect now returns (Files, Assets, DeclineReason) with a recursive wwwroot walk keyed by module-relative path, and BuildResult writes the entries and declares them.
    3. A corrupt asset entry in an otherwise-valid bundle now returns the existing 400 rather than a 500.

    Tests: PluginBundlePublishAssetsTest (publish → landed file on disk, two assets incl. a nested one, byte-exact, + entry-assembly control + declares-nothing case) and ModuleBundleSourceTest (assets served with exact relative paths, never leaking into the assembly closure, none when the pack has no wwwroot). Publish-side negative control verified: reverting the ShelveModule argument 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.css going 404 → 200 with the right bytes, since EntityViews has no image copy to mask the result.

  3. added a commit that references this issue on Sep 4, 2026
  4. added a commit that references this issue on Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions