Skip to content

Pinned module generations break static-asset discovery — every store-landed view pack renders unstyled (#2509 × #1724) #2562

Closed

Description

@sierragolflima

Summary

Since #2509 pins a store-landed module generation into process-local storage before loading it,
MeshModuleStaticAssetExtensions.Discover no longer recognises the loaded assembly's directory —
so it mounts nothing, and every static asset the module ships 404s. The module itself loads and its
components render; only their CSS and JS are unreachable, so the portal comes up unstyled.

The two changes are individually correct. ModuleWwwrootPath decides whether to trust the
assembly's own directory by asking whether its grandparent folder is literally named modules,
and the pin root is not.

Impact

Any deployment that installs a module carrying static web assets. Since #2169 Phase B2,
DefaultViews (MeshWeaver.Blazor.Views) and GraphViews (MeshWeaver.Blazor.Graph) reach a
deployment only as registry bundles, so they are landed modules everywhere — which makes the
default control views the affected case, i.e. the whole portal chrome.

It fails silently and in the worst possible shape: the assemblies load, the views render, the log
says nothing, and the only symptom is that the page looks broken.

Root cause

src/MeshWeaver.Hosting.AspNetCore/MeshModuleStaticAssetExtensions.cs:

var parent = Path.GetFileName(Path.GetDirectoryName(assemblyDirectory));
return string.Equals(parent, "modules", StringComparison.OrdinalIgnoreCase)
    ? Path.Combine(assemblyDirectory!, "wwwroot")                       // correct branch
    : Path.Combine(baseDirectory, "modules", moduleName, "wwwroot");    // taken instead

Path shapes, before and after #2509:

assembly directory grandparent branch taken
before /data/modules/<Name>@<gen>/ modules assembly directory ✅
after /tmp/meshweaver-pinned-modules/<n>-<hash>/<Name>@<gen>/ <n>-<hash> baseDirectory fallback ❌

The fallback resolves to /app/modules/<Name>/wwwroot, which does not exist for a store-landed
module — the legacy fixed folder is not written at all any more (#1949). Directory.Exists fails
and the loop hits continue with no log line, so nothing mounts and nothing explains why.

Note the pinned copy does carry the assets: wwwroot is copied along with the DLLs and sits
directly beside the loaded assembly. The bytes are in the right place; only the path test rejects
them.

Evidence (measured on a portal with both packs landed)

[ModuleLoad] MeshWeaver.Blazor.Views ←
  /tmp/meshweaver-pinned-modules/<n>-<hash>/MeshWeaver.Blazor.Views@<gen>/MeshWeaver.Blazor.Views.dll
  (source=store)

pinned copy:            wwwroot present, 254 files, incl. MeshWeaver.Blazor.Views.styles.css (102,617 B)
fallback path:          /app/modules/MeshWeaver.Blazor.Views/wwwroot  →  does not exist

GET /_content/MeshWeaver.Blazor.Views/Components/DocumentSourceView.razor.js   → 404
GET /_content/MeshWeaver.Blazor.Views/MeshWeaver.Blazor.Views.modules.json     → 404
GET /_content/MeshWeaver.Blazor/standard-page-layout.css                       → 200   (host RCL, unaffected)

<App>.styles.css (host scoped-CSS aggregate)                                   → 200, but 387 bytes
stylesheets linked by the page                                                 → 3, none of them the module's

AddMeshModuleStaticAssets() and UseMeshModuleStaticAssets() are both wired, and
ModuleWwwrootPath is present in the running binary — the mount is active and simply discovers
nothing. MeshModuleStaticAssetExtensions emits zero log lines, which is the silent continue.

How to reproduce

Unit level — the tightest repro; ModuleWwwrootPath is internal and takes baseDirectory
explicitly so the mapping rules can be exercised directly:

ModuleWwwrootPath(
    assemblyLocation: "/tmp/meshweaver-pinned-modules/1-a457e6a1/Foo@0badc0de/Foo.dll",
    baseDirectory:    "/app",
    moduleName:       "Foo")

// actual:   /app/modules/Foo/wwwroot          ← does not exist; module is skipped
// expected: /tmp/meshweaver-pinned-modules/1-a457e6a1/Foo@0badc0de/wwwroot

End to end, on any portal carrying #2509:

  1. Install a package whose module ships static web assets — DefaultViews is the canonical case.
  2. Restart the portal so the module is landed and loaded.
  3. In the log, confirm the module loads from the pin root:
    [ModuleLoad] <Name> ← /tmp/meshweaver-pinned-modules/<n>-<hash>/<Name>@<gen>/<Name>.dll
  4. Request any asset the module ships: GET /_content/<Name>/<file> → 404, while the file is
    present under that same pinned directory's wwwroot/.
  5. Open the portal: controls render, but unstyled.

Reverting only the pin (loading from the shared modules/ tree) restores the assets, which
isolates the interaction.

Suggested fix

  1. Stop keying on the folder name. The invariant that test protects is "never mount the app's
    own web root through a module namespace"
    — so test that the assembly directory is not the
    app base directory. That is stricter about the actual hazard and blind to where the loader put
    the bytes, so it survives the next relocation too.
  2. Log the skip. A module with no resolvable wwwroot should say so at Debug, naming the path
    it tried. The silent continue is why this presents as "the portal looks broken" with nothing in
    the log — the same failure mode ModuleWwwrootPath's own remarks warn about.

A regression test should pin the pinned-path shape specifically, not just the two shapes that
existed when #1724 was written.

Related

Not verified

Whether deployed portals are affected. The reasoning says any instance running a build with #2509
and landed view packs would be, but this was measured on a local install only.

Activity

  1. added a commit that references this issue on Aug 28, 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