oh-my-dsh

plugin-heavy-dep

Use when adding a heavyweight browser dependency (diagram/chart renderers like mermaid, code editors, big wasm-adjacent libs) to a lightweight DSH Web plugin that must stay small, when wiring a lazy-loaded chunk through a host route, when the lazy import intermittently fails or falls back, or when rendering untrusted markup (SVG/HTML) produced by such a dependency.

oh-my-dsh 66 23 Updated 5d ago

Resources

1
GitHub

Install

npx skillscat add oh-my-dsh/dsh-plugin-upgrade-skill/plugin-heavy-dep

Install via the SkillsCat registry.

SKILL.md

Add Heavy Dependencies to a Lightweight DSH Web Plugin

A lightweight Web plugin (small client bundle, no build farm) can still ship a
multi-megabyte renderer — if the heavy code never loads until it is needed and
degrades gracefully when it cannot load. This skill is the integration
checklist; every item below was earned from a real mermaid integration.

1. Decide lazy vs inline

If the dependency would multiply the client bundle several-fold and only one
feature needs it (a fence renderer, an editor opened on demand), do NOT inline
it into the client bundle. Split it into a separate chunk file the client
imports dynamically only when the feature actually renders.

2. Bundle the chunk as ONE file

Bundle the dependency into a single self-contained ESM file with
code-splitting disabled. A general-purpose bundler left on default settings
splits the library's internal dynamic imports into sibling chunk files with
content-hashed names — the browser then resolves them as relative imports
against the chunk's URL, and every sibling must also be served, named exactly,
and MIME-correct. One file, one import, no relative-resolution class of bugs.

3. Serve it from a host route scoped to your own lib

Register a prefix route on webServer that serves files from the plugin's
own lib directory only:

  • Resolve the lib directory from the host bundle itself (import.meta.url),
    never from process.cwd().
  • Restrict to a whitelist of extensions (your chunk is .js/.mjs — nothing
    else should ever be served).
  • Containment guard: verify the requested path stays inside the lib dir. Two
    hard-won rules:
    • Compare with path.relative, not startsWith: a prefix compare is
      wrong the moment the filesystem normalizes differently than your base
      string.
    • Windows drive letters change case: realpathSync may return e:\…
      where your base says E:\…; a case-sensitive compare then misjudges a
      perfectly contained path as an escape and answers 403. path.relative
      (plus an isAbsolute check on the result for cross-drive) is robust.
  • Serve with a JavaScript MIME (application/javascript) — a wrong MIME makes
    the browser reject the dynamic import.

Remember: the host route only exists after a dsh restart; a hard refresh
alone does not register new host code. A 404 on a freshly added route almost
always means "not restarted yet" or "the installed copy predates the route".

4. Import lazily, cache verdicts, fall back

  • import() the chunk URL on first render of the feature; cache the
    successful module
    so repeated fences do not re-import.
  • Do not cache failures the same way — or a transient failure sticks for
    the page lifetime; let the next attempt retry, but throttle (a failing
    import in a loop is its own console spam).
  • Always render a fallback (e.g. the original code block) when the import
    or the render throws; reading must never break because a diagram could not
    load. Tag the fallback with a state attribute and log the failure reason to
    the console — "it fell back" without a reason is undebuggable.

5. Untrusted markup: sanitize before innerHTML

A renderer fed untrusted text (markdown, model output) emits markup you must
treat as hostile before dangerouslySetInnerHTML:

  • Configure the renderer to its strict mode and to emit real SVG text
    rather than HTML labels (HTML labels ride inside <foreignObject> — the
    one channel that carries raw HTML inside an SVG).
  • Suppress the library's global error side effects (some renderers dump a
    giant error SVG into document.body before rejecting).
  • Then re-sanitize the emitted SVG yourself with a zero-dependency
    whitelist pass: parse as XML (image/svg+xml; a parse failure rejects the
    whole string), accept only an <svg> root, strip foreignObject/script
    and foreign-HTML elements case-insensitively, strip on*/@* attributes,
    and strip all href/xlink:href (static diagrams gain nothing from
    links; a hostile href can navigate the GUI). Defense in depth, not
    defense instead.

6. Interaction ownership under a modal

A fullscreen zoom/pan modal over the plugin's panels must own ALL wheel
events while open — including Ctrl+wheel, if the underlying pane already
binds Ctrl+wheel (font sizing). Guard the pane-level handler with a modal
presence check; otherwise both behaviors fire on one gesture. Modal zoom:
wheel anchored at the cursor, drag to pan, keyboard shortcuts, Esc/overlay
click to close.

7. Ship it

  • Commit the built chunk to the package (the plugin ships lib/); declare it
    in files so published payloads stay closed.
  • Document the restart requirement (host route) vs refresh-only (client) in
    the release notes — users hit 404s otherwise.
  • A regression test per pitfall: the fallback path (chunk import fails →
    original rendering), the sanitizer (strips the hostile channels), and — for
    the host route — a containment test that a case-differing but contained path
    is served, not refused.