Appearance
The generative-dom Umbrella Package
generative-dom is a single-install distribution: the core engine, every first-party plugin, and every framework adapter behind one npm package and a namespaced subpath export map. It exists so a project carries one dependency line, one version, and one import root — and trusts the bundle's tree-shaking instead of curating a per-plugin install list.
This page documents the umbrella's contents, the test for picking it over the granular @generative-dom/* packages, and the four built-in presets (minimal, commonmark, github, full).
When to Choose the Umbrella
The crisp test: does anything in your project need to disagree with the umbrella's release cadence? If the answer is no, use generative-dom. The umbrella is the right default for an application — a chat UI, a docs site, a playground, a CLI consumer. The granular packages exist for the cases where lockstep versioning is the wrong answer:
- You publish a downstream library that has its own consumers. Re-exporting
generative-domwould force them to inherit your version pin. - You need to pin one plugin to a specific patch — for a regression hold, a security fix, or a fork — without dragging the rest along.
- You run on a bundler that mishandles subpath exports (older webpack 4, some legacy Rollup configs) and you would rather depend on flat package names.
- You require a plugin the umbrella does not re-export — currently
@generative-dom/plugin-companionand@generative-dom/plugin-rich-table. Mix granular for those; see Mixing umbrella and granular below.
Runtime behavior is identical either way. The umbrella inlines workspace @generative-dom/* packages at build time via Vite library mode; after a competent bundler's tree-shaking, the wire sizes are within minifier noise of the granular install.
Install
bash
pnpm add generative-domThe umbrella declares no required runtime dependencies. React, Vue, Svelte, Angular, Lit, and Astro are listed as optional peers -- they are pulled only when you import the matching subpath, and your package manager will not fail if you do not install them.
Import Surface
The umbrella exposes 27 subpath exports across five groups:
| Subpath | What it gives you |
|---|---|
generative-dom | GenerativeDom, createGenerativeDom, all public types |
generative-dom/plugins | Barrel re-export of every plugin factory |
generative-dom/plugins/<name> | One plugin per subpath — 15 in total: markdown-base, markdown-inline, markdown-heading, markdown-code, markdown-quote, markdown-list, markdown-table, markdown-link, highlight, custom-elements, events, interactive, cursor, code-actions, external-renderer |
generative-dom/presets/<name> | The four presets covered below: minimal, commonmark, github, full |
generative-dom/<framework> | Framework adapter — react, vue, svelte, angular, lit, astro (6 in total) |
Prefer subpath imports over the barrel. import { highlight } from 'generative-dom/plugins' resolves the barrel module first, which means tree-shaking has to prove every other plugin export is unused. Modern bundlers usually get this right with "sideEffects": false (which the umbrella sets), but import { highlight } from 'generative-dom/plugins/highlight' skips the question entirely. For a ~14 KB gzipped plugin like highlight, the deterministic path is worth the longer import string.
The Default Factory
createGenerativeDom is a thin convenience that pre-configures the CommonMark preset and appends any caller-supplied plugins after it:
ts
import { createGenerativeDom } from 'generative-dom';
const md = createGenerativeDom({
container: document.getElementById('out')!,
});
md.push('# Hello **world**');
md.flush();The factory exists so a "just give me a working markdown renderer" snippet fits in three lines. If you need anything other than CommonMark output (e.g. syntax highlighting, an LLM cursor, tool-call custom elements) skip the factory and instantiate GenerativeDom directly with an explicit plugin list.
The Four Built-in Presets
Each preset is exported from its own subpath so importing one preset does not pull the others into your bundle. Every preset is a zero-argument function returning GenerativeDomPlugin[] -- you can spread it, filter it, or pass it straight to the constructor.
presets/minimal — Just Paragraphs
ts
import { GenerativeDom } from 'generative-dom';
import { minimal } from 'generative-dom/presets/minimal';
const md = new GenerativeDom({ plugins: minimal() });Contains: markdownBase only.
The minimal preset registers nothing but the catch-all paragraph plugin. Use it when you want a guaranteed-stable baseline plus a custom plugin set you are still designing -- for example, an experimental DSL where you control every block matcher and only need the engine's paragraph fallback for unmatched lines.
In practice most teams reach for commonmark instead. minimal is the one to pick when even ATX headings would be too much.
presets/commonmark — The Eight CommonMark Plugins
ts
import { GenerativeDom } from 'generative-dom';
import { commonmark } from 'generative-dom/presets/commonmark';
const md = new GenerativeDom({ plugins: commonmark() });Contains: markdownBase, markdownInline, markdownHeading, markdownCode, markdownQuote, markdownList, markdownLink, markdownTable.
Full CommonMark surface plus GFM-style pipe tables. This is what createGenerativeDom uses by default and what most static-content sites should start with: blog posts, docs pages, READMEs, and anything else where the input is hand-authored Markdown rather than streamed model output.
commonmark() is also the preset to spread when you need CommonMark plus something specific:
ts
import { commonmark } from 'generative-dom/presets/commonmark';
import { highlight } from 'generative-dom/plugins/highlight';
const md = new GenerativeDom({
plugins: [...commonmark(), highlight()],
});That snippet is exactly what the next preset gives you in one import.
presets/github — CommonMark + Syntax Highlighting
ts
import { GenerativeDom } from 'generative-dom';
import { github } from 'generative-dom/presets/github';
const md = new GenerativeDom({ plugins: github() });Contains: everything from commonmark() plus highlight().
Named for the rendering style, not for any GitHub-specific syntax: CommonMark plus syntax-highlighted fenced code blocks. The bundled highlighter ships 10 built-in languages — JavaScript, TypeScript, HTML, CSS, JSON, C, C++, Bash, Julia, Zig — and exposes a LangDef API for declaring more without rebuilding the package.
This is the smallest preset that includes highlight. If you are spreading commonmark() and appending highlight() by hand, switch to github() — it is the same plugin list with one fewer line of setup.
Extended GFM features beyond pipe tables and code highlighting are not included today: task lists, footnotes, strikethrough autolinks, and the GFM www. shorthand all wait on their own plugins. (Standard CommonMark autolinks — <https://example.com> — work; they are part of markdownLink.) If you depend on a specific GFM extension, watch the plugin index or vendor your own.
presets/full — Every Zero-Config Plugin
ts
import { GenerativeDom } from 'generative-dom';
import { full } from 'generative-dom/presets/full';
const md = new GenerativeDom({ plugins: full() });Contains: everything from github() plus customElements, events, interactive, cursor, codeActions.
full is the kitchen-sink preset for streamed model output — chat UIs, copilots, anything where the source is an LLM. It adds:
customElements— rendermd-clock,md-plot,md-counter, and other Web Components emitted in the stream.events— invisible event-emitter elements that fire DOM events without rendering anything visible (handy for tool-call hooks).interactive— stateful interactive elements that survive re-renders.cursor— the blinking caret on the in-flight token.codeActions— a language label and a copy button on every fenced code block.
full deliberately excludes two plugins:
external-renderer— requires caller-supplied adapters (e.g. mermaid, KaTeX), so it cannot be zero-config. Add it explicitly when you need it.companion— a niche plugin for projects using thecustom-elements-collectionecosystem (ce-*andlesson-*tags). Add it explicitly if you need that specific tag namespace.
To extend full with one of those:
ts
import { full } from 'generative-dom/presets/full';
import { externalRenderer } from 'generative-dom/plugins/external-renderer';
const md = new GenerativeDom({
plugins: [...full(), externalRenderer({ mermaid: renderMermaid })],
});Choosing a Preset
| If you are rendering... | Start with |
|---|---|
| Hand-authored Markdown (blog posts, docs) | commonmark |
| Hand-authored Markdown with code samples | github |
| Streamed LLM output (chat, copilots) | full |
| Your own DSL with custom matchers | minimal |
You can always start narrow and add plugins as needs surface. The presets are plain GenerativeDomPlugin[], so dropping or replacing one is a one-line change:
ts
import { full } from 'generative-dom/presets/full';
import { highlight } from 'generative-dom/plugins/highlight';
// Replace the bundled highlighter with a configured instance
const plugins = [
...full().filter((p) => p.name !== 'highlight'),
highlight({ include: ['typescript', 'json', 'bash'] }),
];Custom Plugin Sets Without a Preset
Sometimes you know exactly which plugins you want and do not want a preset to make the decision. Import them individually from generative-dom/plugins/*:
ts
import { GenerativeDom } from 'generative-dom';
import { markdownHeading } from 'generative-dom/plugins/markdown-heading';
import { markdownTable } from 'generative-dom/plugins/markdown-table';
import { highlight } from 'generative-dom/plugins/highlight';
const md = new GenerativeDom({
plugins: [markdownHeading(), markdownTable(), highlight()],
});Order is plugin priority, not import order — the engine sorts plugins by their internal priority field at registration time. The list above renders identically to the same plugins listed in any other order. The Plugin Overview explains the priority ranges if you need to reason about precedence (e.g. why highlight at priority 95 wins over markdown-code at 100 for fenced blocks).
Framework Adapters
Each adapter lives behind its own subpath and depends on the matching framework as an optional peer. Install only the peer you actually use.
tsx
// React
import { GenerativeDomRenderer } from 'generative-dom/react';ts
// Vue
import { GenerativeDomRenderer } from 'generative-dom/vue';ts
// Svelte
import { createGenerativeDom, generativeDomAction } from 'generative-dom/svelte';ts
// Angular (standalone component)
import { GenerativeDomRendererComponent } from 'generative-dom/angular';ts
// Lit — side-effect import registers <generative-dom-renderer>
import 'generative-dom/lit';ts
// Astro
import { defineGenerativeDomIntegration } from 'generative-dom/astro';The adapters wrap the same GenerativeDom class shipped from generative-dom, so anything you can pass to the constructor (presets, custom plugin sets, options like debounceMs) also works through the adapter's props or arguments. Framework-specific docs live under each adapter package's README.
Bundle Footprint
Order-of-magnitude gzip sizes for the published builds, ES2022 target, no application minifier applied:
| Preset | Plugins | Approx. gzip |
|---|---|---|
minimal | 1 | ~28 KB (engine + base) |
commonmark | 8 | ~55 KB (engine + 8 markdown plugins) |
github | 9 | ~70 KB (commonmark + highlight) |
full | 14 | ~80 KB (github + 5 LLM-oriented plugins) |
@generative-dom/core (~25 KB gz) is the dominant line item across every preset; plugin-highlight (~14 KB gz, with all 10 languages) is the second. Past those two, individual plugins are 1–7 KB gzipped each. The delta from commonmark to github is essentially the highlighter; the delta from github to full is the cursor, interactive, custom-elements, events, and code-actions layer summed.
These figures are upper bounds. A production bundler (Vite, Rollup, esbuild with --minify) will run terser-equivalent minification on top of the published code and typically claw back 20–35%. Measure your own bundle before optimising — the absolute number depends on what else lives in your application chunk.
If highlight is the dominant line item and you do not need every language, scope it explicitly:
ts
import { commonmark } from 'generative-dom/presets/commonmark';
import { highlight } from 'generative-dom/plugins/highlight';
const md = new GenerativeDom({
plugins: [
...commonmark(),
highlight({ include: ['typescript', 'json', 'bash'] }),
],
});The include option scopes the matchBlock allowlist; it does not currently strip the unused LangDef modules from the bundle (that requires custom build wiring against the granular @generative-dom/plugin-highlight source). What you save is parser-loop work on streamed input, not bytes on the wire.
Mixing Umbrella and Granular
The granular @generative-dom/* packages stay published independently. You can install them alongside the umbrella, but you should know what happens when you do.
The umbrella is built with Vite library mode and inlines every workspace @generative-dom/* source it depends on into its own dist/. From npm's perspective the bundled copies are just bytes inside the generative-dom package — they are not deduplicated against a separately installed @generative-dom/plugin-highlight, because they are not the same package. The same module identity rule applies to the @generative-dom/core exports: the GenerativeDom class re-exported from generative-dom is not === identical to the one re-exported from a separately installed @generative-dom/core if both end up in the graph.
The practical consequences:
- Double bytes. A separately installed plugin lives next to its umbrella-bundled twin. For
plugin-highlightthat is ~14 KB gz of duplicate code on the wire. Your bundler will not warn about it. - Split state. Plugin singletons, registries, or module-level caches live in whichever copy you imported. Two
cursor()instances from two different module identities do not share a singleton; mixing them inside oneGenerativeDominstance is undefined territory. - Type drift. Two copies of
GenerativeDomPluginfrom different module identities are structurally compatible but not nominally equal — TypeScript will accept assignments, but ainstanceofcheck will not.
The legitimate reasons to mix anyway:
- Plugin not exposed by the umbrella.
@generative-dom/plugin-companion(thece-*/lesson-*namespace for thecustom-elements-collectionecosystem) and@generative-dom/plugin-rich-table(DOM-driven extended tables) are not in the umbrella's subpath map. Install them granularly when you need them. - Patch-pinning one plugin. You need
@generative-dom/plugin-highlight@1.4.2while the umbrella has shipped1.5.x. Install the granular package, import it from@generative-dom/plugin-highlight, and ensure no other code path importsgenerative-dom/plugins/highlight(which would resolve to the umbrella-bundled version and reintroduce duplication).
Rule of thumb: import any given plugin from exactly one of generative-dom/plugins/<name> or @generative-dom/plugin-<name> across your whole codebase. Grep your imports during review.
Next
- Quick Start — three-line example to first render.
- Core Concepts — the engine model and what plugins actually do.
- Plugin Overview — every plugin, its priority, and what it matches.