Skip to content

Vend bundled Mermaid to standalone artifacts via /api/vendor/ (offline diagrams) #27

Description

@ashwin-pc

Problem

Standalone HTML artifacts served from .pi/web/artifacts/ (via serveArtifact) cannot use the Mermaid library that pi-web already bundles. The bundled copy in node_modules/mermaid is only wired into the chat markdown renderer (src/markdown/render.ts, import("mermaid") via Vite) and is not exposed at any stable URL.

As a result, a self-contained explainer/design artifact that wants Mermaid diagrams must load it from a public CDN:

import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";

That breaks in offline / air-gapped / locked-down-network environments, and pins the artifact to an external CDN's availability and version.

Proposal

Add a tiny read-only vendor route that streams the already-installed Mermaid ESM bundle from node_modules, so artifacts can load it locally:

GET /api/vendor/mermaid.esm.min.mjs  ->  node_modules/mermaid/dist/mermaid.esm.min.mjs

Artifacts then import from the same origin (no CDN dependency):

import mermaid from "/api/vendor/mermaid.esm.min.mjs";

Sketch

server.ts already has the streaming primitives (serveStatic streams from staticDir; contentTypes maps extensions). A minimal handler:

// Allowlist of vendored browser libs we are willing to serve from node_modules.
const VENDOR_FILES: Record<string, string> = {
  "mermaid.esm.min.mjs": "mermaid/dist/mermaid.esm.min.mjs",
  // mermaid ships sub-chunks it imports lazily; may need to serve the dir.
};

function serveVendor(req: IncomingMessage, res: ServerResponse) {
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
  const name = decodeURIComponent(url.pathname.slice("/api/vendor/".length));
  const rel = VENDOR_FILES[name];
  if (!rel) return sendJson(res, 404, { ok: false, error: "Unknown vendor file" });
  const file = require.resolve(rel, { paths: [process.cwd()] }); // or resolve against pkg root
  res.writeHead(200, {
    "content-type": contentTypes[extname(file)] || "application/javascript",
    "cache-control": "public, max-age=86400, immutable",
  });
  createReadStream(file).pipe(res);
}

// in the request router:
if (method === "GET" && url.pathname.startsWith("/api/vendor/")) return serveVendor(req, res);

Caveats / things to verify

  • Lazy chunks: Mermaid 11's ESM bundle dynamically imports diagram-specific chunks (e.g. flowDiagram-*.mjs) relative to its own URL. Serving a single file may not be enough — the route likely needs to serve the whole mermaid/dist/ directory (path-safe, allowlisted to that dir) so relative chunk imports resolve. Worth confirming with a flowchart + sequence + state diagram.
  • ESM MIME: must be served as text/javascript / application/javascript for the browser to accept the import.
  • Auth: this is a static asset import from <script type="module">; it can't send the bearer header. Either keep /api/vendor/* unauthenticated (it's read-only public library code) or document that.
  • Security: strictly allowlist filenames/dir and reject ../absolute paths, mirroring safeArtifactName in serveArtifact.

Why

Enables fully self-contained, offline-capable HTML artifacts (explainers, design docs) with first-class diagrams, without depending on an external CDN. This pairs with a "visual-explainer" skill that generates diagram-first HTML artifacts instead of verbose Markdown.

Acceptance

  • GET /api/vendor/mermaid.esm.min.mjs returns the bundle with a JS MIME type.
  • An artifact at .pi/web/artifacts/foo.html that imports from /api/vendor/... renders flowchart, sequence, and state diagrams with no network access.
  • Path traversal / unknown files are rejected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions