Hosting the Studio assets
Writing a platform adapter covers the half of embedding that answers Studio's questions. This page covers the other half: putting the editor on a screen. They are independent — you can serve the assets and reuse a stock adapter, or write an adapter and let the desktop app serve it.
@jxsuite/studio
ships a tree whose parts already know how to find each other. The entry reaches its own chunks and workers, the bundle stylesheet reaches Monaco's icon font, and the chrome stylesheet reaches the vendored webfonts. Your job is to keep those relationships intact and tell Studio where the tree ended up.
The manifest
@jxsuite/studio/hosting/layout
says what ships. It is pure — no filesystem, no DOM — so it reads the same from a Cloudflare Worker build, a Vite plugin, a Deno host or a Nix derivation.
import { STUDIO_ASSETS } from "@jxsuite/studio/hosting/layout";
for (const asset of STUDIO_ASSETS) {
asset.path; // "dist/studio.js", "dist/chunks", "styles", …
asset.dir; // true when the directory ships wholesale, names intact
asset.required; // false means you may omit it and lose a feature, not the editor
asset.why; // what the reader loses without it
}
If your build cannot import TypeScript at all, the same data is emitted as
dist/manifest.json
.
Do not write your own list. Every host that has done so has shipped an incomplete one — including this project's own desktop app, which for months omitted
dist/codicon.ttf
and drew empty boxes wherever Monaco draws an icon. Nothing errored, because a missing font is not an error.
Two layouts, one rule
assetUrl(base, path)
maps a package path to the URL your host answers on.
import { assetUrl } from "@jxsuite/studio/hosting/layout";
const nested = { mode: "nested", prefix: "/studio-assets/" } as const;
assetUrl(nested, "dist/studio.js"); // "/studio-assets/dist/studio.js"
const flat = { mode: "flat", prefix: "/" } as const;
assetUrl(flat, "dist/studio.js"); // "/studio.js"
assetUrl(flat, "dist/chunks/a.js"); // "/chunks/a.js"
flat
strips exactly one leading
dist/
segment and nothing else. That is the whole rule, and it is what makes flattening safe: everything inside
dist/
addresses everything else inside
dist/
relatively, so removing one segment moves all of it together.
styles/
and
fonts/
are untouched in both modes, which is why the chrome stylesheet's
url("../fonts/…")
resolves either way.
Staging the files
If your host runs Bun or Node,
@jxsuite/studio/hosting
will copy the tree for you:
import { stageStudioAssets } from "@jxsuite/studio/hosting";
const { base } = await stageStudioAssets("./public/studio-assets", {
prefix: "/studio-assets/",
});
It returns the
base
it staged at — hand that straight to the document generator below, so the two cannot disagree about where the files went. It skips source maps by default (the chunk maps alone are about 24 MB), and it refuses rather than staging an incomplete tree, naming both the missing entry and what its absence costs.
This module is a convenience, and the only one that touches the filesystem. A host in another runtime reads the manifest and moves the bytes itself.
The documents
Studio needs two HTML documents. Generate them; do not copy and rewrite them.
import { studioShellHtml } from "@jxsuite/studio/hosting/document";
import { canvasDocument } from "@jxsuite/studio/hosting";
const editor = studioShellHtml({ base, boot: ["/my-platform-init.js"] });
const canvas = await canvasDocument({ base });
studioShellHtml
emits the editor document with every asset reference rebased and the chrome stylesheets linked in cascade order.
canvasDocument
reads the package's canvas document and rebases its one entry reference — that document stays hand-authored, because the
<style>
block in it establishes the sizing container the canvas measures against and has to apply before the first paint.
Serve the editor document for
every
path the editor lives at. If your editor URL contains the project —
/edit/:owner/:repo
— the document is served from a deep path, and any document-relative reference in it would resolve into the wrong directory. Generating it with an absolute
base
is what makes that a non-issue.
Where your adapter plugs in
boot
is the seam. The modules you name load, in order,
before
the Studio entry:
<script type="module" src="/my-platform-init.js"></script>
<script type="module" src="/studio-assets/dist/studio.js"></script>
Your boot module registers the platform, exactly as
Writing a platform adapter
describes — and it must do so
synchronously, before its first
await
. A module script with top-level
await
does not block a later
<script>
tag, and the Studio entry reads the global as it evaluates.
// my-platform-init.ts
import { registerPlatform } from "@jxsuite/studio/platform";
registerPlatform(createMyPlatform()); // first, before anything async
Two things hosts get wrong
Serving the package's own
index.html
as well as your generated one.
If your generated editor lives at
/edit/*
and the package's copy is also reachable under your asset prefix, that second document boots Studio with the
default
adapter — the dev-server one — which then fetches
/__studio/*
against your origin. Under a single-page-application fallback those fetches answer with your marketing page at HTTP 200, so nothing errors and nothing logs. Pass
exclude: ["document"]
to
stageStudioAssets
and generate both documents yourself.
Resolving
canvasUrl
asynchronously without saying so.
If your adapter learns its canvas URL after
activate()
, set
canvasUrlDeferred: true
on it. Studio then waits instead of mounting the bundle-relative default into your shell's origin.
Related
Writing a platform adapter — the interface your boot module registers
The backend protocol — what the stock adapters speak
Embedding overview — which layer to implement