@octanejs/mdx
v0.1.31
Published
MDX for the octane renderer — compiles .mdx/.md through @mdx-js/mdx to JSX source and then through octane/compiler, so documents render as real compiled octane components (templates, not runtime descriptors); ships the @mdx-js/react provider layer (MDXPro
Readme
@octanejs/mdx
MDX for the octane
UI framework — documentation stays in .mdx/.md and renders as compiled octane
components.
The split mirrors docs/react-library-compat-plan.md §2: @mdx-js/mdx's
compiler is framework-agnostic and reused verbatim — with jsx: true it
emits the compiled document as classic JSX source, which is exactly the
React-style .tsx dialect octane's own compiler handles. The pipeline is
.mdx / .md → @mdx-js/mdx (JSX/ESM source) → octane/compiler → compiled octane module— compile, don't interpret: no MDX runtime, no _jsx shims, the document
becomes an ordinary octane component module (client codegen or SSR HTML-string
codegen). Only @mdx-js/react's ~50-line provider layer is ported here.
Vite
// vite.config.ts
import { defineConfig } from 'vite';
import { octane } from '@octanejs/vite-plugin';
import { octaneMdx } from '@octanejs/mdx/vite';
export default defineConfig({
plugins: [octaneMdx(), octane()],
});Pass profile: true to both plugins for a browser profiling build:
plugins: [octaneMdx({ profile: true }), octane({ profile: true })];Profiling metadata is never emitted by the MDX server transform.
octaneMdx() claims .mdx/.md and produces final JS, so it composes with the
octane plugin (which claims .tsrx/.tsx/.ts/.js) without ordering
hazards. SSR target selection matches the octane plugin: per-module
auto-detection, ssr: true|false to force.
Options: md: false (leave .md alone), providerImportSource: null (disable
the provider wiring), remarkPlugins / rehypePlugins / recmaPlugins,
format, mdxOptions (escape hatch). The default remark set is
defaultRemarkPlugins = GFM + frontmatter + export const frontmatter.
Usage
---
title: Getting started
---
import Counter from './Counter.tsrx';
# {frontmatter.title}
Octane components just work: <Counter start={2} />import Doc, { frontmatter } from './getting-started.mdx';
import { MDXProvider } from '@octanejs/mdx';
export function Page() @{
<MDXProvider components={{ h1: FancyHeading, code: Snippet }}>
<Doc />
</MDXProvider>
}A mapping can also be passed per-document: <Doc components={{ h1: FancyHeading }} />.
Mapping values are octane components or replacement host tag names ({ em: 'i' });
the special wrapper key is the document layout.
API (vs @mdx-js/react)
MDXProvider({ components, disableParentContext, children })— ported; nested providers merge, function-formcomponentsreceives the inherited mapping.useMDXComponents(components?)— ported, with one deliberate divergence: theuseMemoreferential-stability wrapper is dropped so the call is valid in BOTH runtimes (octane's clientuseMemoneeds a live client render scope; SSR passes call this duringrenderToString). Same observable mapping.@octanejs/mdx/compile—compileMdx/compileMdxSync/defaultRemarkPlugins, the plugin's pipeline as a library (used by the SSR tests, usable for static-site tooling). Results include nonfatal compilerdiagnostics; JSX warning ranges are mapped back to the authored.mdxdocument. The Vite plugin publishes the same warnings during transforms.
SSR + hydration
A document compiled with mode: 'server' renders through octane/server's
renderToString, and the resulting HTML hydrates byte-for-byte into the
client-compiled module via hydrateRoot (embedded .tsrx components adopt
their server DOM and stay interactive).
Both mapping routes work on the server:
- the
componentsprop —renderToString(Doc, { components }); MDXProviderfrom@octanejs/mdx/server— the same provider layer mirrored ontooctane/servercontext (the client and server runtimes have disjoint context stores, so each side ships its own provider; server-mode documents readuseMDXComponentsfrom@octanejs/mdx/serverautomatically). A document rendered under the server provider hydrates byte-for-byte into the clientMDXProviderwith the same mapping.
Syntax highlighting (Shiki)
Highlighting is a rehype concern, so it hooks in through rehypePlugins — no
integration code and nothing bundled (add @shikijs/rehype yourself):
import { octaneMdx } from '@octanejs/mdx/vite';
import rehypeShiki from '@shikijs/rehype';
octaneMdx({
rehypePlugins: [[rehypeShiki, { theme: 'github-light' }]],
});Shiki's hast output serializes through the same pipeline as any other content:
highlighted tokens render on the client, serialize identically on the server,
and hydrate cleanly (see tests/shiki.test.ts). Note @shikijs/rehype is
async — it works in the vite plugin and compileMdx, not compileMdxSync.
Notes
- Markdown-generated elements ride octane's value-position (
createElementdescriptor) path — documents are static content, and embedded.tsrxcomponents keep their full compiled fast path. .mdxedits fast-refresh in dev: the pipeline wraps the document's default export in octane's runtimehmr()and self-accepts, so live mounts re-render the new body in place (no page reload; the vite plugin enables this in serve mode automatically).
Status
Current scope, known divergences, and verification status are tracked in the
generated bindings status table, sourced from
this package's status.json.
