@octanejs/mdx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +58 -0
- package/src/compile.js +281 -0
- package/src/env.d.ts +16 -0
- package/src/index.ts +109 -0
- package/src/octane-compiler.d.ts +13 -0
- package/src/server.ts +65 -0
- package/src/vite.js +87 -0
- package/types/compile.d.ts +68 -0
- package/types/vite.d.ts +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @octanejs/mdx
|
|
2
|
+
|
|
3
|
+
[MDX](https://mdxjs.com) for the [octane](https://github.com/octanejs/octane)
|
|
4
|
+
UI framework — documentation stays in `.mdx`/`.md` and renders as **compiled octane
|
|
5
|
+
components**.
|
|
6
|
+
|
|
7
|
+
The split mirrors `docs/react-library-compat-plan.md` §2: **@mdx-js/mdx's
|
|
8
|
+
compiler is framework-agnostic and reused verbatim** — with `jsx: true` it
|
|
9
|
+
emits the compiled document as classic JSX *source*, which is exactly the
|
|
10
|
+
React-style `.tsx` dialect octane's own compiler handles. The pipeline is
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
.mdx / .md → @mdx-js/mdx (JSX/ESM source) → octane/compiler → compiled octane module
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
— compile, don't interpret: no MDX runtime, no `_jsx` shims, the document
|
|
17
|
+
becomes an ordinary octane component module (client codegen or SSR HTML-string
|
|
18
|
+
codegen). Only @mdx-js/react's ~50-line provider layer is ported here.
|
|
19
|
+
|
|
20
|
+
## Vite
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// vite.config.ts
|
|
24
|
+
import { defineConfig } from 'vite';
|
|
25
|
+
import { octane } from 'octane/compiler/vite'; // or @octanejs/vite-plugin
|
|
26
|
+
import { octaneMdx } from '@octanejs/mdx/vite';
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
plugins: [octaneMdx(), octane()],
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`octaneMdx()` claims `.mdx`/`.md` and produces final JS, so it composes with the
|
|
34
|
+
octane plugin (which claims `.tsrx`/`.tsx`/`.ts`/`.js`) without ordering
|
|
35
|
+
hazards. SSR target selection matches the octane plugin: per-module
|
|
36
|
+
auto-detection, `ssr: true|false` to force.
|
|
37
|
+
|
|
38
|
+
Options: `md: false` (leave `.md` alone), `providerImportSource: null` (disable
|
|
39
|
+
the provider wiring), `remarkPlugins` / `rehypePlugins` / `recmaPlugins`,
|
|
40
|
+
`format`, `mdxOptions` (escape hatch). The default remark set is
|
|
41
|
+
`defaultRemarkPlugins` = GFM + frontmatter + `export const frontmatter`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```mdx
|
|
46
|
+
---
|
|
47
|
+
title: Getting started
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
import Counter from './Counter.tsrx';
|
|
51
|
+
|
|
52
|
+
# {frontmatter.title}
|
|
53
|
+
|
|
54
|
+
Octane components just work: <Counter start={2} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsrx
|
|
58
|
+
import Doc, { frontmatter } from './getting-started.mdx';
|
|
59
|
+
import { MDXProvider } from '@octanejs/mdx';
|
|
60
|
+
|
|
61
|
+
export function Page() @{
|
|
62
|
+
<MDXProvider components={{ h1: FancyHeading, code: Snippet }}>
|
|
63
|
+
<Doc />
|
|
64
|
+
</MDXProvider>
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A mapping can also be passed per-document: `<Doc components={{ h1: FancyHeading }} />`.
|
|
69
|
+
Mapping values are octane components or replacement host tag names (`{ em: 'i' }`);
|
|
70
|
+
the special `wrapper` key is the document layout.
|
|
71
|
+
|
|
72
|
+
## API (vs @mdx-js/react)
|
|
73
|
+
|
|
74
|
+
- `MDXProvider({ components, disableParentContext, children })` — ported;
|
|
75
|
+
nested providers merge, function-form `components` receives the inherited
|
|
76
|
+
mapping.
|
|
77
|
+
- `useMDXComponents(components?)` — ported, with one deliberate divergence:
|
|
78
|
+
the `useMemo` referential-stability wrapper is dropped so the call is valid
|
|
79
|
+
in BOTH runtimes (octane's client `useMemo` needs a live client render scope;
|
|
80
|
+
SSR passes call this during `renderToString`). Same observable mapping.
|
|
81
|
+
- `@octanejs/mdx/compile` — `compileMdx` / `compileMdxSync` /
|
|
82
|
+
`defaultRemarkPlugins`, the plugin's pipeline as a library (used by the SSR
|
|
83
|
+
tests, usable for static-site tooling).
|
|
84
|
+
|
|
85
|
+
## SSR + hydration
|
|
86
|
+
|
|
87
|
+
A document compiled with `mode: 'server'` renders through `octane/server`'s
|
|
88
|
+
`renderToString`, and the resulting HTML **hydrates byte-for-byte** into the
|
|
89
|
+
client-compiled module via `hydrateRoot` (embedded `.tsrx` components adopt
|
|
90
|
+
their server DOM and stay interactive).
|
|
91
|
+
|
|
92
|
+
Both mapping routes work on the server:
|
|
93
|
+
|
|
94
|
+
- the `components` **prop** — `renderToString(Doc, { components })`;
|
|
95
|
+
- **`MDXProvider` from `@octanejs/mdx/server`** — the same provider layer
|
|
96
|
+
mirrored onto `octane/server` context (the client and server runtimes have
|
|
97
|
+
disjoint context stores, so each side ships its own provider; server-mode
|
|
98
|
+
documents read `useMDXComponents` from `@octanejs/mdx/server`
|
|
99
|
+
automatically). A document rendered under the server provider hydrates
|
|
100
|
+
byte-for-byte into the client `MDXProvider` with the same mapping.
|
|
101
|
+
|
|
102
|
+
## Syntax highlighting (Shiki)
|
|
103
|
+
|
|
104
|
+
Highlighting is a rehype concern, so it hooks in through `rehypePlugins` — no
|
|
105
|
+
integration code and nothing bundled (add `@shikijs/rehype` yourself):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { octaneMdx } from '@octanejs/mdx/vite';
|
|
109
|
+
import rehypeShiki from '@shikijs/rehype';
|
|
110
|
+
|
|
111
|
+
octaneMdx({
|
|
112
|
+
rehypePlugins: [[rehypeShiki, { theme: 'github-light' }]],
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Shiki's hast output serializes through the same pipeline as any other content:
|
|
117
|
+
highlighted tokens render on the client, serialize identically on the server,
|
|
118
|
+
and hydrate cleanly (see `tests/shiki.test.ts`). Note `@shikijs/rehype` is
|
|
119
|
+
async — it works in the vite plugin and `compileMdx`, not `compileMdxSync`.
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- Markdown-generated elements ride octane's value-position (`createElement`
|
|
124
|
+
descriptor) path — documents are static content, and embedded `.tsrx`
|
|
125
|
+
components keep their full compiled fast path.
|
|
126
|
+
- `.mdx` edits fast-refresh in dev: the pipeline wraps the document's default
|
|
127
|
+
export in octane's runtime `hmr()` and self-accepts, so live mounts re-render
|
|
128
|
+
the new body in place (no page reload; the vite plugin enables this in serve
|
|
129
|
+
mode automatically).
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/mdx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "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 (MDXProvider/useMDXComponents) ported onto octane context.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Dominic Gannaway",
|
|
9
|
+
"email": "dg@domgan.com"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
17
|
+
"directory": "packages/mdx"
|
|
18
|
+
},
|
|
19
|
+
"main": "src/index.ts",
|
|
20
|
+
"module": "src/index.ts",
|
|
21
|
+
"types": "src/index.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts",
|
|
29
|
+
"./server": "./src/server.ts",
|
|
30
|
+
"./compile": {
|
|
31
|
+
"types": "./types/compile.d.ts",
|
|
32
|
+
"default": "./src/compile.js"
|
|
33
|
+
},
|
|
34
|
+
"./vite": {
|
|
35
|
+
"types": "./types/vite.d.ts",
|
|
36
|
+
"default": "./src/vite.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@jridgewell/remapping": "^2.3.5",
|
|
41
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
42
|
+
"remark-frontmatter": "^5.0.0",
|
|
43
|
+
"remark-gfm": "^4.0.1",
|
|
44
|
+
"remark-mdx-frontmatter": "^5.2.0",
|
|
45
|
+
"source-map": "^0.7.6",
|
|
46
|
+
"octane": "0.1.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@jridgewell/trace-mapping": "^0.3.31",
|
|
50
|
+
"@shikijs/rehype": "^3.0.0",
|
|
51
|
+
"vite": "^8.0.16",
|
|
52
|
+
"vitest": "^4.1.9",
|
|
53
|
+
"@octanejs/testing-library": "0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"test": "vitest run"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/compile.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octanejs/mdx — the compile pipeline (`…/compile` entry).
|
|
3
|
+
*
|
|
4
|
+
* Strategy (docs/react-library-compat-plan.md §2): @mdx-js/mdx's compiler is
|
|
5
|
+
* framework-agnostic — with `jsx: true` it emits the compiled document as
|
|
6
|
+
* CLASSIC JSX SOURCE instead of framework runtime calls. That emitted program
|
|
7
|
+
* is exactly the React-style `.tsx` dialect octane's compiler already handles,
|
|
8
|
+
* so the pipeline is
|
|
9
|
+
*
|
|
10
|
+
* .mdx/.md → @mdx-js/mdx (JSX/ESM source) → octane/compiler → compiled octane
|
|
11
|
+
*
|
|
12
|
+
* i.e. an MDX document becomes a REAL compiled octane component module (client
|
|
13
|
+
* descriptor/template codegen, or server HTML-string codegen when
|
|
14
|
+
* `mode: 'server'`) — no MDX runtime, no interpretation. `providerImportSource`
|
|
15
|
+
* defaults to `@octanejs/mdx`, wiring `_provideComponents()` in the emitted
|
|
16
|
+
* code to this package's `useMDXComponents` (the octane port of
|
|
17
|
+
* @mdx-js/react's provider).
|
|
18
|
+
*
|
|
19
|
+
* The only adaptation between the two compilers is `recmaOctaneAdapter`, a tiny
|
|
20
|
+
* ESTree pass over MDX's output: MDX's no-layout branch CALLS
|
|
21
|
+
* `_createMdxContent(props)` directly, which bypasses octane's
|
|
22
|
+
* `(props, __s, __extra)` component ABI (the server body would run with
|
|
23
|
+
* `__s === undefined` and lean on scope-recovery). The pass rewrites the bare
|
|
24
|
+
* call to `<_createMdxContent {...props}/>` so both branches mount through the
|
|
25
|
+
* component machinery on client AND server. (The layout branch's
|
|
26
|
+
* `<_createMdxContent {...props}/>` tag needs no help: octane classifies
|
|
27
|
+
* `_`-starting identifier tags as component references, per JSX semantics.)
|
|
28
|
+
*
|
|
29
|
+
* The two former SERVER-mode fixups are gone — their octane gaps are fixed:
|
|
30
|
+
* `ssrComponent` renders a host-tag-STRING comp as a
|
|
31
|
+
* `<!--[--><tag>…</tag><!--]-->` block (the shape the client's componentSlot /
|
|
32
|
+
* de-opt host renderer adopts on hydration), and the server compiler
|
|
33
|
+
* value-lowers a returned fragment through `ssrChild([...])` exactly like the
|
|
34
|
+
* client's descriptor array — so `<_components.h1>` member tags and the
|
|
35
|
+
* document's fragment body take the SAME shape on both sides (hydration-safe).
|
|
36
|
+
*
|
|
37
|
+
* Authored in `.js` (like octane's `compiler/vite.js` and @octanejs/stylex's
|
|
38
|
+
* vite entry) so the `…/vite` plugin — which imports this module — loads when a
|
|
39
|
+
* consuming app's `vite.config.ts` pulls it in through Node's ESM loader.
|
|
40
|
+
*/
|
|
41
|
+
import remapping from '@jridgewell/remapping';
|
|
42
|
+
import { compile as mdxCompile, compileSync as mdxCompileSync } from '@mdx-js/mdx';
|
|
43
|
+
import { compile as octaneCompile } from 'octane/compiler';
|
|
44
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
45
|
+
import remarkGfm from 'remark-gfm';
|
|
46
|
+
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
|
|
47
|
+
import { SourceMapGenerator } from 'source-map';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} CompileMdxResult
|
|
51
|
+
* @property {string} code
|
|
52
|
+
* @property {unknown} map
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} CompileMdxOptions
|
|
57
|
+
* @property {'client' | 'server'} [mode] octane codegen target: `'client'` (DOM) or `'server'` (SSR HTML strings). Default `'client'`.
|
|
58
|
+
* @property {boolean} [hmr] octane compiler HMR wrapping (client only; the vite plugin wires this to serve mode).
|
|
59
|
+
* @property {boolean} [dev] octane compiler dev metadata (client only; same gate as `hmr`).
|
|
60
|
+
* @property {string | null} [providerImportSource]
|
|
61
|
+
* Module the emitted document reads the provider mapping from
|
|
62
|
+
* (`useMDXComponents`). Defaults per mode — `'@octanejs/mdx'` (client) /
|
|
63
|
+
* `'@octanejs/mdx/server'` (server), so each runtime reads ITS OWN context
|
|
64
|
+
* store (they are disjoint; see src/server.ts). Pass `null` to disable the
|
|
65
|
+
* provider wiring entirely (only `props.components` applies).
|
|
66
|
+
* @property {import('@mdx-js/mdx').CompileOptions['remarkPlugins']} [remarkPlugins] remark plugins. Defaults to `defaultRemarkPlugins` (GFM + frontmatter + frontmatter-export).
|
|
67
|
+
* @property {import('@mdx-js/mdx').CompileOptions['rehypePlugins']} [rehypePlugins]
|
|
68
|
+
* @property {import('@mdx-js/mdx').CompileOptions['recmaPlugins']} [recmaPlugins] Extra recma (ESTree) plugins, run before the octane adapter pass.
|
|
69
|
+
* @property {'mdx' | 'md' | 'detect'} [format] Source syntax: `'mdx'`, plain `'md'` (no JSX/ESM/expressions), or `'detect'` by file extension. Default `'detect'`.
|
|
70
|
+
* @property {Omit<import('@mdx-js/mdx').CompileOptions, 'jsx' | 'jsxRuntime' | 'jsxImportSource' | 'outputFormat' | 'providerImportSource' | 'remarkPlugins' | 'rehypePlugins' | 'recmaPlugins' | 'format' | 'SourceMapGenerator'>} [mdxOptions] Escape hatch: other @mdx-js/mdx options. The pipeline owns `jsx`/`outputFormat`/the options above.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The default remark plugin set: GitHub-flavored markdown, YAML/TOML
|
|
75
|
+
* frontmatter parsing, and the `export const frontmatter = {…}` export.
|
|
76
|
+
* Exported so a custom `remarkPlugins` list can extend rather than replace it.
|
|
77
|
+
*
|
|
78
|
+
* @type {import('@mdx-js/mdx').CompileOptions['remarkPlugins']}
|
|
79
|
+
*/
|
|
80
|
+
export const defaultRemarkPlugins = [remarkGfm, remarkFrontmatter, remarkMdxFrontmatter];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compile MDX/markdown source to a compiled octane module (async — supports async plugins).
|
|
84
|
+
*
|
|
85
|
+
* @param {string} source
|
|
86
|
+
* @param {string} id
|
|
87
|
+
* @param {CompileMdxOptions} [options]
|
|
88
|
+
* @returns {Promise<CompileMdxResult>}
|
|
89
|
+
*/
|
|
90
|
+
export async function compileMdx(source, id, options = {}) {
|
|
91
|
+
const out = await mdxCompile({ value: source, path: id }, buildMdxOptions(id, options));
|
|
92
|
+
return octaneStage(String(out.value), out.map, id, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Synchronous {@link compileMdx} (the default plugin set is fully sync).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} source
|
|
99
|
+
* @param {string} id
|
|
100
|
+
* @param {CompileMdxOptions} [options]
|
|
101
|
+
* @returns {CompileMdxResult}
|
|
102
|
+
*/
|
|
103
|
+
export function compileMdxSync(source, id, options = {}) {
|
|
104
|
+
const out = mdxCompileSync({ value: source, path: id }, buildMdxOptions(id, options));
|
|
105
|
+
return octaneStage(String(out.value), out.map, id, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} id
|
|
110
|
+
* @param {CompileMdxOptions} options
|
|
111
|
+
* @returns {import('@mdx-js/mdx').CompileOptions}
|
|
112
|
+
*/
|
|
113
|
+
function buildMdxOptions(id, options) {
|
|
114
|
+
const format =
|
|
115
|
+
options.format && options.format !== 'detect'
|
|
116
|
+
? options.format
|
|
117
|
+
: id.endsWith('.md')
|
|
118
|
+
? 'md'
|
|
119
|
+
: 'mdx';
|
|
120
|
+
const provider =
|
|
121
|
+
options.providerImportSource === undefined
|
|
122
|
+
? options.mode === 'server'
|
|
123
|
+
? '@octanejs/mdx/server'
|
|
124
|
+
: '@octanejs/mdx'
|
|
125
|
+
: options.providerImportSource;
|
|
126
|
+
return {
|
|
127
|
+
...options.mdxOptions,
|
|
128
|
+
format,
|
|
129
|
+
// The load-bearing switch: emit JSX SOURCE (no jsx-runtime calls), which
|
|
130
|
+
// octane's compiler lowers to its own codegen.
|
|
131
|
+
jsx: true,
|
|
132
|
+
// Map the intermediate JSX back to the .mdx source — stage one of the
|
|
133
|
+
// chained map octaneStage composes (stage two is octane's own map).
|
|
134
|
+
SourceMapGenerator,
|
|
135
|
+
...(provider === null ? {} : { providerImportSource: provider }),
|
|
136
|
+
remarkPlugins: options.remarkPlugins ?? defaultRemarkPlugins,
|
|
137
|
+
rehypePlugins: options.rehypePlugins,
|
|
138
|
+
recmaPlugins: [...(options.recmaPlugins ?? []), recmaOctaneAdapter],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} jsxSource
|
|
144
|
+
* @param {unknown} mdxMap
|
|
145
|
+
* @param {string} id
|
|
146
|
+
* @param {CompileMdxOptions} options
|
|
147
|
+
* @returns {CompileMdxResult}
|
|
148
|
+
*/
|
|
149
|
+
function octaneStage(jsxSource, mdxMap, id, options) {
|
|
150
|
+
const mode = options.mode ?? 'client';
|
|
151
|
+
const out = octaneCompile(jsxSource, id, {
|
|
152
|
+
mode,
|
|
153
|
+
hmr: mode === 'client' && !!options.hmr,
|
|
154
|
+
dev: mode === 'client' && !!options.dev,
|
|
155
|
+
});
|
|
156
|
+
// Two-stage sourcemap: octane's map targets the INTERMEDIATE JSX text;
|
|
157
|
+
// @mdx-js/mdx's map (via SourceMapGenerator) targets the original .mdx.
|
|
158
|
+
// Compose them (most-recent-first) so generated positions trace all the way
|
|
159
|
+
// back to the document. The non-empty guard is defensive: if a compile shape
|
|
160
|
+
// ever yields no overlapping segments, keep octane's intermediate map (its
|
|
161
|
+
// `sourcesContent` is the intermediate JSX — still steppable in devtools,
|
|
162
|
+
// unlike a blank map). The octane SERVER compile emits an empty-mappings map
|
|
163
|
+
// by design (SSR maps are a later octane refinement).
|
|
164
|
+
if (out.map && mdxMap) {
|
|
165
|
+
const chained = remapping([out.map, mdxMap], () => null);
|
|
166
|
+
if (String(chained.mappings).length > 0) out.map = chained;
|
|
167
|
+
}
|
|
168
|
+
// Fast refresh for documents: octane's compiler only auto-wraps EXPORTED
|
|
169
|
+
// `@{}`-form components in `hmr(...)` — `MDXContent` (a passthrough function
|
|
170
|
+
// returning a ternary of descriptors) isn't recognized as one, so the octane
|
|
171
|
+
// `hmr` flag alone leaves `.mdx` edits as full module invalidations. The
|
|
172
|
+
// PIPELINE knows the emitted shape, so it appends the exact registration the
|
|
173
|
+
// octane compiler emits for `.tsrx` exports: wrap the default export in the
|
|
174
|
+
// runtime `hmr()` (identity-stable across edits — parents keep their mounted
|
|
175
|
+
// wrapper) + a self-accepting `import.meta.hot` block that swaps the body
|
|
176
|
+
// and re-renders live blocks in place. Appending after the fact keeps the
|
|
177
|
+
// source map's earlier segments valid (ESM imports hoist).
|
|
178
|
+
if (mode === 'client' && options.hmr && /\bexport default function MDXContent\b/.test(out.code)) {
|
|
179
|
+
out.code +=
|
|
180
|
+
"\nimport { hmr as _$mdxHmr, HMR as _$mdxHMR } from 'octane';\n" +
|
|
181
|
+
'MDXContent = _$mdxHmr(MDXContent);\n' +
|
|
182
|
+
'if (import.meta.hot) {\n' +
|
|
183
|
+
' import.meta.hot.accept((module) => {\n' +
|
|
184
|
+
' module && MDXContent[_$mdxHMR].update(module.default);\n' +
|
|
185
|
+
' });\n' +
|
|
186
|
+
'}\n';
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
// recmaOctaneAdapter — the ESTree pass described in the module doc.
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const MDX_BODY_NAME = '_createMdxContent';
|
|
196
|
+
|
|
197
|
+
function recmaOctaneAdapter() {
|
|
198
|
+
/** @param {unknown} tree */
|
|
199
|
+
return (tree) => {
|
|
200
|
+
walkReplace(/** @type {EstreeNode} */ (tree), adaptNode);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @typedef {{ type: string, [key: string]: unknown }} EstreeNode */
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {unknown} value
|
|
208
|
+
* @returns {value is EstreeNode}
|
|
209
|
+
*/
|
|
210
|
+
function isNode(value) {
|
|
211
|
+
return value !== null && typeof value === 'object' && typeof value.type === 'string';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Visit `node` (post-decision, pre-recursion): return a replacement node to
|
|
216
|
+
* swap in (recursed into by the caller), or null to keep the node and recurse.
|
|
217
|
+
*
|
|
218
|
+
* @param {EstreeNode} node
|
|
219
|
+
* @returns {EstreeNode | null}
|
|
220
|
+
*/
|
|
221
|
+
function adaptNode(node) {
|
|
222
|
+
// `_createMdxContent(props)` → `<_createMdxContent {...props}/>`.
|
|
223
|
+
if (
|
|
224
|
+
node.type === 'CallExpression' &&
|
|
225
|
+
isNode(node.callee) &&
|
|
226
|
+
node.callee.type === 'Identifier' &&
|
|
227
|
+
node.callee.name === MDX_BODY_NAME &&
|
|
228
|
+
Array.isArray(node.arguments) &&
|
|
229
|
+
node.arguments.length <= 1 &&
|
|
230
|
+
(node.arguments.length === 0 || isNode(node.arguments[0]))
|
|
231
|
+
) {
|
|
232
|
+
return jsxSelfClosing(MDX_BODY_NAME, node.arguments[0] ?? null);
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Depth-first walk that can REPLACE child nodes in place (arrays and single
|
|
239
|
+
* node-valued keys). Skips location metadata keys.
|
|
240
|
+
*
|
|
241
|
+
* @param {EstreeNode} node
|
|
242
|
+
* @param {(n: EstreeNode) => EstreeNode | null} visit
|
|
243
|
+
*/
|
|
244
|
+
function walkReplace(node, visit) {
|
|
245
|
+
for (const key of Object.keys(node)) {
|
|
246
|
+
if (key === 'loc' || key === 'range' || key === 'position' || key === 'data') continue;
|
|
247
|
+
const value = node[key];
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
for (let i = 0; i < value.length; i++) {
|
|
250
|
+
const child = value[i];
|
|
251
|
+
if (!isNode(child)) continue;
|
|
252
|
+
const next = visit(child);
|
|
253
|
+
if (next !== null) value[i] = next;
|
|
254
|
+
else walkReplace(child, visit);
|
|
255
|
+
}
|
|
256
|
+
} else if (isNode(value)) {
|
|
257
|
+
const next = visit(value);
|
|
258
|
+
if (next !== null) node[key] = next;
|
|
259
|
+
else walkReplace(value, visit);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} name
|
|
266
|
+
* @param {EstreeNode | null} spreadArgument
|
|
267
|
+
* @returns {EstreeNode}
|
|
268
|
+
*/
|
|
269
|
+
function jsxSelfClosing(name, spreadArgument) {
|
|
270
|
+
return {
|
|
271
|
+
type: 'JSXElement',
|
|
272
|
+
openingElement: {
|
|
273
|
+
type: 'JSXOpeningElement',
|
|
274
|
+
name: { type: 'JSXIdentifier', name },
|
|
275
|
+
attributes: spreadArgument ? [{ type: 'JSXSpreadAttribute', argument: spreadArgument }] : [],
|
|
276
|
+
selfClosing: true,
|
|
277
|
+
},
|
|
278
|
+
closingElement: null,
|
|
279
|
+
children: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @types/mdx (pulled in via @mdx-js/mdx's public types) requires a GLOBAL
|
|
2
|
+
// `JSX` namespace, which octane deliberately does not declare in its runtime
|
|
3
|
+
// types (JSX typing lives in the compiler's volar layer, per-`.tsrx` file).
|
|
4
|
+
// Minimal ambient declarations so this package typechecks standalone — no
|
|
5
|
+
// octane code reads these.
|
|
6
|
+
declare namespace JSX {
|
|
7
|
+
type Element = unknown;
|
|
8
|
+
|
|
9
|
+
interface ElementClass {
|
|
10
|
+
[name: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface IntrinsicElements {
|
|
14
|
+
[name: string]: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octanejs/mdx — the runtime provider layer.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (docs/react-library-compat-plan.md §2): @mdx-js/mdx's core is
|
|
5
|
+
* framework-agnostic (the compile pipeline lives in `./compile` + `./vite`);
|
|
6
|
+
* only @mdx-js/react's thin React layer is ported here, onto octane context.
|
|
7
|
+
* `MDXProvider` / `useMDXComponents` are a port of @mdx-js/react/lib/index.js
|
|
8
|
+
* (v3): the same context, the same function-vs-object `components` merge, the
|
|
9
|
+
* same `disableParentContext` behavior.
|
|
10
|
+
*
|
|
11
|
+
* Compiled `.mdx` modules import `useMDXComponents` from here (the pipeline's
|
|
12
|
+
* default `providerImportSource`) and merge its result under `props.components`
|
|
13
|
+
* — so a components mapping can come from either the provider context or the
|
|
14
|
+
* `components` prop, exactly like MDX + React.
|
|
15
|
+
*
|
|
16
|
+
* ONE deliberate divergence from the React source: @mdx-js/react wraps the
|
|
17
|
+
* merge in `useMemo([contextComponents, components])` — a referential-stability
|
|
18
|
+
* optimization only. Octane's `useMemo` is the CLIENT runtime's (the `octane`
|
|
19
|
+
* entry has no server condition), and it requires a live render scope — a
|
|
20
|
+
* server-compiled document calls `useMDXComponents()` during `renderToString`,
|
|
21
|
+
* where the client scope is null and `useMemo` would crash. `useContext`, by
|
|
22
|
+
* contrast, is null-scope safe (it returns the context default). So the merge
|
|
23
|
+
* runs unmemoized: same observable mapping every render, valid in both
|
|
24
|
+
* runtimes (on the server the client context yields its default `{}`, and the
|
|
25
|
+
* `props.components` route still applies).
|
|
26
|
+
*
|
|
27
|
+
* SSR provider support lives in `@octanejs/mdx/server` — the same layer
|
|
28
|
+
* mirrored onto `octane/server` context (the two runtimes' context stores are
|
|
29
|
+
* disjoint; server-mode documents import `useMDXComponents` from there). Keep
|
|
30
|
+
* the merge semantics here and in src/server.ts in lockstep.
|
|
31
|
+
*/
|
|
32
|
+
import { createContext, createElement, useContext, type ComponentBody } from 'octane';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A components mapping: markdown element name (`h1`, `p`, `code`, …) or
|
|
36
|
+
* embedded-component name → the octane component (or replacement host tag
|
|
37
|
+
* name) that renders it. The special `wrapper` key is the document layout.
|
|
38
|
+
*/
|
|
39
|
+
export interface MDXComponents {
|
|
40
|
+
[name: string]: ComponentBody<any> | string | MDXComponents;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `components` as accepted by MDXProvider/useMDXComponents: a mapping, or a function of the inherited mapping (@mdx-js/react parity). */
|
|
44
|
+
export type MDXComponentsProp = MDXComponents | ((inherited: MDXComponents) => MDXComponents);
|
|
45
|
+
|
|
46
|
+
// Per @mdx-js/react lib/index.js: `const emptyComponents = {}` +
|
|
47
|
+
// `const MDXContext = React.createContext(emptyComponents)`.
|
|
48
|
+
const emptyComponents: MDXComponents = {};
|
|
49
|
+
const MDXContext = createContext<MDXComponents>(emptyComponents);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current components mapping: the provider context merged with (or
|
|
53
|
+
* mapped through) `components`. Port of @mdx-js/react's `useMDXComponents`
|
|
54
|
+
* (see the module doc for the deliberately-dropped `useMemo`).
|
|
55
|
+
*
|
|
56
|
+
* `use*`-named, so a compiled caller appends its call-site slot symbol as the
|
|
57
|
+
* trailing arg — tolerated and ignored (no hook state lives here; `useContext`
|
|
58
|
+
* is not slot-threaded in octane).
|
|
59
|
+
*/
|
|
60
|
+
export function useMDXComponents(
|
|
61
|
+
components?: MDXComponentsProp | null | symbol,
|
|
62
|
+
_slot?: symbol,
|
|
63
|
+
): MDXComponents {
|
|
64
|
+
// The compiler-injected slot is arg 0 for the common no-`components` call
|
|
65
|
+
// (`useMDXComponents()` compiles to `useMDXComponents(SLOT)`). A symbol is
|
|
66
|
+
// never a components value — normalize it away.
|
|
67
|
+
if (typeof components === 'symbol') components = undefined;
|
|
68
|
+
const contextComponents = useContext(MDXContext);
|
|
69
|
+
// Custom merge via a function (@mdx-js/react parity).
|
|
70
|
+
if (typeof components === 'function') return components(contextComponents);
|
|
71
|
+
return { ...contextComponents, ...components };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface MDXProviderProps {
|
|
75
|
+
/** MDX content (and anything else that reads the mapping) rendered under this provider. */
|
|
76
|
+
children?: unknown;
|
|
77
|
+
/** Mapping to merge over (or function of) the inherited mapping. */
|
|
78
|
+
components?: MDXComponentsProp | null;
|
|
79
|
+
/** Ignore any parent MDXProvider and use exactly `components` (@mdx-js/react parity). */
|
|
80
|
+
disableParentContext?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Provider of the MDX components context. Port of @mdx-js/react's
|
|
85
|
+
* `MDXProvider`: nested providers MERGE by default (`components` over the
|
|
86
|
+
* inherited mapping); `disableParentContext` starts from scratch.
|
|
87
|
+
*/
|
|
88
|
+
export function MDXProvider(props: MDXProviderProps): unknown {
|
|
89
|
+
let allComponents: MDXComponents;
|
|
90
|
+
if (props.disableParentContext) {
|
|
91
|
+
allComponents =
|
|
92
|
+
typeof props.components === 'function'
|
|
93
|
+
? props.components(emptyComponents)
|
|
94
|
+
: (props.components ?? emptyComponents);
|
|
95
|
+
} else {
|
|
96
|
+
allComponents = useMDXComponents(props.components);
|
|
97
|
+
}
|
|
98
|
+
return createElement(MDXContext.Provider as ComponentBody<any>, {
|
|
99
|
+
value: allComponents,
|
|
100
|
+
children: props.children,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The props every compiled MDX document component accepts. */
|
|
105
|
+
export interface MDXProps {
|
|
106
|
+
/** Per-render components mapping, merged over the provider context. */
|
|
107
|
+
components?: MDXComponents;
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// `octane/compiler` is authored in JSDoc'd JS with no shipped declarations —
|
|
2
|
+
// a minimal ambient surface for the one entry point this package consumes.
|
|
3
|
+
declare module 'octane/compiler' {
|
|
4
|
+
export function compile(
|
|
5
|
+
source: string,
|
|
6
|
+
id: string,
|
|
7
|
+
options?: {
|
|
8
|
+
mode?: 'client' | 'server';
|
|
9
|
+
hmr?: boolean;
|
|
10
|
+
dev?: boolean;
|
|
11
|
+
},
|
|
12
|
+
): { code: string; map: unknown };
|
|
13
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octanejs/mdx/server — the provider layer for SERVER renders.
|
|
3
|
+
*
|
|
4
|
+
* octane's client and server runtimes are separate modules with disjoint
|
|
5
|
+
* context stores, so the client `MDXProvider` (./index.ts, built on `octane`
|
|
6
|
+
* context) cannot thread a mapping through `renderToString` — its `useContext`
|
|
7
|
+
* runs null-scope on the server and yields the `{}` default. The server
|
|
8
|
+
* runtime has its own full context implementation (`scope.$$ctxValues`,
|
|
9
|
+
* top-down within one render pass), so this entry mirrors the provider layer
|
|
10
|
+
* onto `octane/server` context: same merge, same function-form `components`,
|
|
11
|
+
* same `disableParentContext` — see ./index.ts for the @mdx-js/react port
|
|
12
|
+
* notes. KEEP THE MERGE SEMANTICS IN LOCKSTEP with ./index.ts (both are the
|
|
13
|
+
* same ~40-line port; only the runtime import differs).
|
|
14
|
+
*
|
|
15
|
+
* The compile pipeline points a server-mode document's `providerImportSource`
|
|
16
|
+
* here (client mode keeps `@octanejs/mdx`), so `useMDXComponents()` inside a
|
|
17
|
+
* server-compiled document reads THIS context — and a document rendered under
|
|
18
|
+
* this `MDXProvider` serializes with the same mapping the client provider
|
|
19
|
+
* produces at hydration. Client bundles never import this module.
|
|
20
|
+
*/
|
|
21
|
+
import { createContext, createElement, useContext } from 'octane/server';
|
|
22
|
+
import type { MDXComponents, MDXComponentsProp, MDXProviderProps } from './index.js';
|
|
23
|
+
|
|
24
|
+
export type { MDXComponents, MDXComponentsProp, MDXProviderProps, MDXProps } from './index.js';
|
|
25
|
+
|
|
26
|
+
const emptyComponents: MDXComponents = {};
|
|
27
|
+
const MDXContext = createContext<MDXComponents>(emptyComponents);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Server `useMDXComponents`: the provider context merged with (or mapped
|
|
31
|
+
* through) `components`. Same contract as the client export (./index.ts).
|
|
32
|
+
*/
|
|
33
|
+
export function useMDXComponents(
|
|
34
|
+
components?: MDXComponentsProp | null | symbol,
|
|
35
|
+
_slot?: symbol,
|
|
36
|
+
): MDXComponents {
|
|
37
|
+
// Tolerate a compiler-injected trailing slot symbol, like the client export.
|
|
38
|
+
if (typeof components === 'symbol') components = undefined;
|
|
39
|
+
const contextComponents = useContext(MDXContext);
|
|
40
|
+
if (typeof components === 'function') return components(contextComponents);
|
|
41
|
+
return { ...contextComponents, ...components };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Server `MDXProvider`: provides the mapping for every document rendered
|
|
46
|
+
* below it in this `renderToString` pass. Mount shape mirrors the client
|
|
47
|
+
* provider exactly (one provider component frame + one context frame), so a
|
|
48
|
+
* document server-rendered under this provider hydrates byte-for-byte into
|
|
49
|
+
* the client `MDXProvider`.
|
|
50
|
+
*/
|
|
51
|
+
export function MDXProvider(props: MDXProviderProps): unknown {
|
|
52
|
+
let allComponents: MDXComponents;
|
|
53
|
+
if (props.disableParentContext) {
|
|
54
|
+
allComponents =
|
|
55
|
+
typeof props.components === 'function'
|
|
56
|
+
? props.components(emptyComponents)
|
|
57
|
+
: (props.components ?? emptyComponents);
|
|
58
|
+
} else {
|
|
59
|
+
allComponents = useMDXComponents(props.components);
|
|
60
|
+
}
|
|
61
|
+
return createElement(MDXContext.Provider as any, {
|
|
62
|
+
value: allComponents,
|
|
63
|
+
children: props.children,
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octanejs/mdx — the Vite plugin (`…/vite` entry).
|
|
3
|
+
*
|
|
4
|
+
* `octaneMdx()` transforms `.mdx` (and by default `.md`) modules through the
|
|
5
|
+
* full pipeline in `./compile` — @mdx-js/mdx (JSX source) → octane/compiler —
|
|
6
|
+
* and returns FINAL JS, so it composes with `@octanejs/vite-plugin` /
|
|
7
|
+
* `octane/compiler/vite` without ordering hazards: both are `enforce: 'pre'`,
|
|
8
|
+
* but the octane plugin only claims `.tsrx`/`.tsx`/`.ts`/`.js` ids and this one
|
|
9
|
+
* only claims `.mdx`/`.md`, so the two transforms never see the same module.
|
|
10
|
+
*
|
|
11
|
+
* SSR target selection mirrors `octane/compiler/vite` exactly: an explicit
|
|
12
|
+
* `ssr` option wins; otherwise Vite's transform-level SSR flag OR the
|
|
13
|
+
* environment consumer marks a server build (`mode: 'server'` codegen), so the
|
|
14
|
+
* SAME document renders to HTML strings on the server and hydratable DOM code
|
|
15
|
+
* on the client.
|
|
16
|
+
*
|
|
17
|
+
* Authored in `.js` (like octane's `compiler/vite.js` and @octanejs/stylex's
|
|
18
|
+
* vite entry) so the plugin loads when a consuming app's `vite.config.ts`
|
|
19
|
+
* pulls it in through Node's ESM loader — which resolves the on-disk file
|
|
20
|
+
* exactly as written and never applies TS-style `.js` → `.ts` mapping.
|
|
21
|
+
*/
|
|
22
|
+
import { compileMdx } from './compile.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Omit<import('./compile.js').CompileMdxOptions, 'mode' | 'hmr' | 'dev'> & {
|
|
26
|
+
* ssr?: boolean,
|
|
27
|
+
* md?: boolean,
|
|
28
|
+
* hmr?: boolean,
|
|
29
|
+
* }} OctaneMdxPluginOptions
|
|
30
|
+
*
|
|
31
|
+
* `ssr` forces the codegen target for EVERY module — `true` always server,
|
|
32
|
+
* `false` always client. Leave unset for per-module auto-detection (standard
|
|
33
|
+
* Vite SSR setups). Mirrors `octane/compiler/vite`'s `ssr` option.
|
|
34
|
+
* `md` also transforms `.md` modules (plain-markdown format). Default `true`.
|
|
35
|
+
* `hmr` is the octane HMR/dev metadata override; defaults to on in serve mode
|
|
36
|
+
* (client only).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Structural Vite plugin type — avoids a hard type dependency on vite (this
|
|
41
|
+
* package's published surface is source; see @octanejs/stylex's vite entry for
|
|
42
|
+
* the same choice).
|
|
43
|
+
*
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* name: string,
|
|
46
|
+
* enforce: 'pre',
|
|
47
|
+
* configResolved(config: { command: string }): void,
|
|
48
|
+
* transform(code: string, id: string, options?: { ssr?: boolean }): Promise<{ code: string, map: unknown } | null>,
|
|
49
|
+
* }} OctaneMdxPlugin
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {OctaneMdxPluginOptions} [options]
|
|
54
|
+
* @returns {OctaneMdxPlugin}
|
|
55
|
+
*/
|
|
56
|
+
export function octaneMdx(options = {}) {
|
|
57
|
+
const { ssr: forceSsr, md, hmr, ...compileOptions } = options;
|
|
58
|
+
let hmrEnabled = hmr;
|
|
59
|
+
const includeMd = md !== false;
|
|
60
|
+
return {
|
|
61
|
+
name: 'octane-mdx',
|
|
62
|
+
enforce: 'pre',
|
|
63
|
+
configResolved(config) {
|
|
64
|
+
if (hmrEnabled === undefined) hmrEnabled = config.command === 'serve';
|
|
65
|
+
},
|
|
66
|
+
async transform(code, id, transformOptions) {
|
|
67
|
+
const [file, query = ''] = id.split('?'); // Vite ids carry ?v=/?used/?raw/… suffixes
|
|
68
|
+
if (!(file.endsWith('.mdx') || (includeMd && file.endsWith('.md')))) return null;
|
|
69
|
+
// An ASSET-query import (`import text from './doc.md?raw'`, ?url, ?inline)
|
|
70
|
+
// is vite's territory: its asset plugin already LOADED the module as JS
|
|
71
|
+
// (`export default "…"`), so compiling here would mangle it — and the
|
|
72
|
+
// author explicitly asked for the file, not the document. Internal
|
|
73
|
+
// bookkeeping queries (?v=hash, ?used, ?import) still transform.
|
|
74
|
+
if (/(^|&)(raw|url|inline|worker|sharedworker)(=|&|$)/.test(query)) return null;
|
|
75
|
+
const ssr =
|
|
76
|
+
forceSsr !== undefined
|
|
77
|
+
? forceSsr
|
|
78
|
+
: transformOptions?.ssr === true || this.environment?.config?.consumer === 'server';
|
|
79
|
+
return compileMdx(code, file, {
|
|
80
|
+
...compileOptions,
|
|
81
|
+
mode: ssr ? 'server' : 'client',
|
|
82
|
+
hmr: !ssr && !!hmrEnabled,
|
|
83
|
+
dev: !ssr && !!hmrEnabled,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Hand-written declarations for src/compile.js (authored in `.js` so the vite
|
|
2
|
+
// entry's import chain loads under Node's native ESM loader — same convention
|
|
3
|
+
// as @octanejs/vite-plugin's types/). Keep in lockstep with the implementation.
|
|
4
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
5
|
+
|
|
6
|
+
export interface CompileMdxResult {
|
|
7
|
+
code: string;
|
|
8
|
+
map: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CompileMdxOptions {
|
|
12
|
+
/** octane codegen target: `'client'` (DOM) or `'server'` (SSR HTML strings). Default `'client'`. */
|
|
13
|
+
mode?: 'client' | 'server';
|
|
14
|
+
/** octane compiler HMR wrapping (client only; the vite plugin wires this to serve mode). */
|
|
15
|
+
hmr?: boolean;
|
|
16
|
+
/** octane compiler dev metadata (client only; same gate as `hmr`). */
|
|
17
|
+
dev?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Module the emitted document reads the provider mapping from
|
|
20
|
+
* (`useMDXComponents`). Defaults per mode — `'@octanejs/mdx'` (client) /
|
|
21
|
+
* `'@octanejs/mdx/server'` (server), so each runtime reads ITS OWN context
|
|
22
|
+
* store (they are disjoint; see src/server.ts). Pass `null` to disable the
|
|
23
|
+
* provider wiring entirely (only `props.components` applies).
|
|
24
|
+
*/
|
|
25
|
+
providerImportSource?: string | null;
|
|
26
|
+
/** remark plugins. Defaults to `defaultRemarkPlugins` (GFM + frontmatter + frontmatter-export). */
|
|
27
|
+
remarkPlugins?: CompileOptions['remarkPlugins'];
|
|
28
|
+
rehypePlugins?: CompileOptions['rehypePlugins'];
|
|
29
|
+
/** Extra recma (ESTree) plugins, run before the octane adapter pass. */
|
|
30
|
+
recmaPlugins?: CompileOptions['recmaPlugins'];
|
|
31
|
+
/** Source syntax: `'mdx'`, plain `'md'` (no JSX/ESM/expressions), or `'detect'` by file extension. Default `'detect'`. */
|
|
32
|
+
format?: 'mdx' | 'md' | 'detect';
|
|
33
|
+
/** Escape hatch: other @mdx-js/mdx options. The pipeline owns `jsx`/`outputFormat`/the options above. */
|
|
34
|
+
mdxOptions?: Omit<
|
|
35
|
+
CompileOptions,
|
|
36
|
+
| 'jsx'
|
|
37
|
+
| 'jsxRuntime'
|
|
38
|
+
| 'jsxImportSource'
|
|
39
|
+
| 'outputFormat'
|
|
40
|
+
| 'providerImportSource'
|
|
41
|
+
| 'remarkPlugins'
|
|
42
|
+
| 'rehypePlugins'
|
|
43
|
+
| 'recmaPlugins'
|
|
44
|
+
| 'format'
|
|
45
|
+
| 'SourceMapGenerator'
|
|
46
|
+
>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The default remark plugin set: GitHub-flavored markdown, YAML/TOML
|
|
51
|
+
* frontmatter parsing, and the `export const frontmatter = {…}` export.
|
|
52
|
+
* Exported so a custom `remarkPlugins` list can extend rather than replace it.
|
|
53
|
+
*/
|
|
54
|
+
export declare const defaultRemarkPlugins: CompileOptions['remarkPlugins'];
|
|
55
|
+
|
|
56
|
+
/** Compile MDX/markdown source to a compiled octane module (async — supports async plugins). */
|
|
57
|
+
export declare function compileMdx(
|
|
58
|
+
source: string,
|
|
59
|
+
id: string,
|
|
60
|
+
options?: CompileMdxOptions,
|
|
61
|
+
): Promise<CompileMdxResult>;
|
|
62
|
+
|
|
63
|
+
/** Synchronous {@link compileMdx} (the default plugin set is fully sync). */
|
|
64
|
+
export declare function compileMdxSync(
|
|
65
|
+
source: string,
|
|
66
|
+
id: string,
|
|
67
|
+
options?: CompileMdxOptions,
|
|
68
|
+
): CompileMdxResult;
|
package/types/vite.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Hand-written declarations for src/vite.js (authored in `.js` so it loads
|
|
2
|
+
// from a consumer's vite.config.ts under Node's native ESM loader — same
|
|
3
|
+
// convention as @octanejs/vite-plugin's types/). Keep in lockstep.
|
|
4
|
+
import type { CompileMdxOptions } from './compile.js';
|
|
5
|
+
|
|
6
|
+
export interface OctaneMdxPluginOptions extends Omit<CompileMdxOptions, 'mode' | 'hmr' | 'dev'> {
|
|
7
|
+
/**
|
|
8
|
+
* Force the codegen target for EVERY module — `true` always server, `false`
|
|
9
|
+
* always client. Leave unset for per-module auto-detection (standard Vite
|
|
10
|
+
* SSR setups). Mirrors `octane/compiler/vite`'s `ssr` option.
|
|
11
|
+
*/
|
|
12
|
+
ssr?: boolean;
|
|
13
|
+
/** Also transform `.md` modules (plain-markdown format). Default `true`. */
|
|
14
|
+
md?: boolean;
|
|
15
|
+
/** octane HMR/dev metadata override; defaults to on in serve mode (client only). */
|
|
16
|
+
hmr?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Structural Vite plugin type — avoids a hard type dependency on vite (see
|
|
21
|
+
* @octanejs/stylex's vite entry for the same choice).
|
|
22
|
+
*/
|
|
23
|
+
export interface OctaneMdxPlugin {
|
|
24
|
+
name: string;
|
|
25
|
+
enforce: 'pre';
|
|
26
|
+
configResolved(config: { command: string }): void;
|
|
27
|
+
transform(
|
|
28
|
+
this: unknown,
|
|
29
|
+
code: string,
|
|
30
|
+
id: string,
|
|
31
|
+
options?: { ssr?: boolean },
|
|
32
|
+
): Promise<{ code: string; map: unknown } | null>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare function octaneMdx(options?: OctaneMdxPluginOptions): OctaneMdxPlugin;
|