@basemark/cli
v0.1.3
Published
Build/render tooling for basemark — renders a markdown(+directives) file to one self-contained HTML file
Readme
@basemark/cli
Build-time tooling for Basemark. First command: render — resolves a markdown(+directives) file into one self-contained HTML file. Open it in any browser: no server, no build step, no framework runtime.
Usage
npx @basemark/cli render doc.md -o doc.html
# or install it once:
npm install -g @basemark/cli
basemark render doc.md -o doc.htmlRuns on plain Node — no Bun install required. dist/index.js is a self-contained bundle (every dependency, including @basemark/bio/common/charts, is built in); your markdown input is not embedded, it's read from disk each run.
What gets resolved
render builds a registry from @basemark/core + @basemark/common + @basemark/bio + @basemark/charts.
common's 14 components are fully wired — each custom-element class is declared inside itsregister*function behind atypeof HTMLElementguard, so importing the package under Node (no browser) is safe.bio's 14 components go one step further — their vendor libraries (3dmol,protvista-uniprot,locuszoom) crash on load outside a browser regardless of any guard, so they're dynamicallyimport()ed instead, making everyregister*functionasync.charts' 7 components (ECharts) follow the same guard ascommon—echartsimports cleanly under Node with no DOM, so no dynamicimport()is needed.chemis an empty stub.
Any resolution failure (unknown directive, bad prop, unclosed :::) renders a visible basemark-error banner — never a silent drop.
How a render happens
markdown → parseMarkdown() (no DOM) → hast tree
→ hast-util-to-html → body markup
→ wrapped with theme.css + the component runtime → one .html fileThe "component runtime" is separate: browser-side JS that calls every register*Components() so the already-resolved tags (<basemark-card>, etc.) upgrade into real elements the moment the file opens — no hydration, no re-parsing.
Runtime bundling: pre-built, split per domain, inlined per document
The runtime is bundled at CLI build time, not per-render — scripts/bundle-runtime.ts runs Bun.build() once per entry in src/runtime/ (base, common, bio, charts), writing each to src/generated/. render.ts pulls each in via a static text import, same as theme.css.
Why build-time, not per-render: a per-render Bun.build() call needs a real filesystem to bundle from — doesn't survive being bundled itself. A static import, unlike a runtime call, is visible to the bundler ahead of time and gets embedded as a string constant.
Why four bundles, not one: bio.runtime.js is ~5MB (3Dmol.js/protvista-uniprot/locuszoom); charts.runtime.js (ECharts) is ~1.2MB; common.runtime.js is ~30KB; base.runtime.js (just the error component, always inlined) is ~2KB. render.ts's usedDomains() checks which domains a document's resolved tags actually belong to, and only inlines the bundles it needs — a card-only doc stays small, a ::protvista{} doc picks up the 5MB bio bundle but nothing else.
Known gaps
- No structural linter — an unclosed
:::produces abasemark-errorbanner, not a build failure. No--strictmode. - No batch rendering — one file at a time, no directory-in/directory-out mode.
- No scaffolding or registry-validation commands.
- The
biobundle is one file for all 14 components — using juststructurestill pulls inlocuszoom/protvista-uniprot's weight. Per-component splitting isn't built. - Every output links the
OnestGoogle Font (matching the example apps) — the one deliberate network dependency in an otherwise self-contained file. Falls back to the system font stack if the request fails.
