Skip to content

Commit

Permalink
feat(jsx): base for supporting more runtimes (#236)
Browse files Browse the repository at this point in the history
Co-authored-by: Kent C. Dodds <[email protected]>
  • Loading branch information
muningis and kentcdodds authored Feb 7, 2025
1 parent 9e5c958 commit 70cb04f
Show file tree
Hide file tree
Showing 15 changed files with 496 additions and 24 deletions.
93 changes: 89 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ everything for you.
</summary>

[MDX](https://mdxjs.com/) enables you to combine terse markdown syntax for your
content with the power of React components. For content-heavy sites, writing the
content with the power of JSX components. For content-heavy sites, writing the
content with straight-up HTML can be annoyingly verbose. Often people solve this
using a WSYWIG editor, but too often those fall short in mapping the writer's
intent to HTML. Many people prefer using markdown to express their content
Expand All @@ -56,8 +56,8 @@ to insert an element that JavaScript targets (which is annoyingly indirect), or
you can use an `iframe` or something.

As previously stated, [MDX](https://mdxjs.com/) enables you to combine terse
markdown syntax for your content with the power of React components. So you can
import a React component and render it within the markdown itself. It's the best
markdown syntax for your content with the power of JSX components. So you can
import a JSX component and render it within the markdown itself. It's the best
of both worlds.

</details>
Expand Down Expand Up @@ -141,6 +141,16 @@ bundling. So it's best suited for SSR frameworks like Remix/Next.

</details>

<details>
<summary>
<strong>
"Can I use this other JSX libraries other than React?"
</strong>
</summary>

Yes! If JSX runtime you want to use is mentioned here - https://mdxjs.com/docs/getting-started/#jsx, it's guaranteed to work. Libraries, such as `hono` which has `react` compatible API also works. Check to [Other JSX runtimes](#other-jsx-runtimes) to get started.
</details>

<details>
<summary>
<strong>
Expand Down Expand Up @@ -770,9 +780,84 @@ export const MDXComponent: React.FC<{
### Known Issues
### Other JSX runtimes
JSX runtimes mentioned [here](https://mdxjs.com/docs/getting-started/#jsx) are guaranteed to be supported, however any JSX runtime should work without problem, as long as they export their own jsx runtime. For example, `hono` is not mentioned here, but as it has `react` compatible API, it can be used without any issues.
To do so, you will have to pass a configuration object and use JSX Component factory.
```tsx
const getMDX = (source) => {
return bundleMDX({
source,
jsxConfig: {
jsxLib: {
varName: 'HonoJSX',
package: 'hono/jsx',
},
jsxDom: {
varName: 'HonoDOM',
package: 'hono/jsx/dom',
},
jsxRuntime: {
varName: '_jsx_runtime',
package: 'hono/jsx/jsx-runtime',
},
}
});
}

// ...

import { getMDXComponent } from "mdx-bundler/client/jsx";

import * as HonoJSX from "hono/jsx";
import * as HonoDOM from "hono/jsx/dom";
import * as _jsx_runtime from "hono/jsx/jsx-runtime";
const jsxConfig = {
HonoJSX,
HonoDOM,
_jsx_runtime
};

export const MDXComponent: React.FC<{
code: string;
}> = ({ code }) => {
const Component = useMemo(
() => getMDXComponent(code, jsxConfig),
[code],
);

return (
<Component components={{ Text: ({ children }) => <p>{children}</p> }} />
);
};
```
To use it with others, adjust `jsxConfig` passed to bundler.
```ts
const jsxConfig = {
jsxLib: {
varName: 'HonoJSX',
package: 'hono/jsx',
},
jsxDom: {
varName: 'HonoDOM',
package: 'hono/jsx/dom',
},
jsxRuntime: {
varName: '_jsx_runtime',
package: 'hono/jsx/jsx-runtime',
},
}
```
and to `getMDXComponent`
```ts
const jsxConfig = { HonoJSX, HonoDOM, _jsx_runtime };
```
#### Cloudflare Workers
We'd _love_ for this to work in cloudflare workers. Unfortunately cloudflares
We'd _love_ for this to work in cloudflare workers. Unfortunately cloudflare workers
have two limitations that prevent `mdx-bundler` from working in that
environment:
Expand Down
2 changes: 1 addition & 1 deletion client/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require('../dist/client')
module.exports = require('../dist/client/index.js')
1 change: 1 addition & 0 deletions client/jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../dist/client/jsx')
6 changes: 5 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"type": "commonjs",
"main": "./index.js",
"types": "./index.d.ts"
"types": "./index.d.ts",
"exports": {
"react": "./react.js",
"jsx": "./jsx.js"
}
}
1 change: 1 addition & 0 deletions client/react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../dist/client/index.js')
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"esbuild": "0.*"
},
"devDependencies": {
"@testing-library/preact": "3.2.4",
"@testing-library/react": "^14.1.0",
"@testing-library/vue": "8.1.0",
"@types/jsdom": "^21.1.5",
"@types/mdx": "^2.0.10",
"@types/react": "^18.2.37",
Expand All @@ -63,15 +65,18 @@
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"esbuild": "^0.19.5",
"hono": "4.6.14",
"jsdom": "^22.1.0",
"kcd-scripts": "^14.0.1",
"left-pad": "^1.3.0",
"mdx-test-data": "^1.0.1",
"preact": "10.25.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-mdx-images": "^3.0.0",
"typescript": "^5.2.2",
"uvu": "^0.5.6"
"uvu": "^0.5.6",
"vue": "3.5.13"
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
Expand Down
83 changes: 83 additions & 0 deletions src/__tests__/hono.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import './setup-tests.js'
import { Hono } from "hono";
/* eslint-disable import/no-unresolved --
* imports paths are there in node_modules/hono/package.json
* but it doesn't get resolved
*/
import * as HonoJSX from "hono/jsx";
import * as HonoDOM from "hono/jsx/dom";
import * as _jsx_runtime from "hono/jsx/jsx-runtime";
/* eslint-enable import/no-unresolved */
import {suite} from 'uvu'
import * as assert from 'uvu/assert'
import {bundleMDX} from '../index.js'
import {getMDXComponent} from '../client/jsx.js'

const test = suite("hono");

const jsxBundlerConfig = {
jsxLib: {
varName: 'HonoJSX',
package: 'hono/jsx',
},
jsxDom: {
varName: 'HonoDOM',
package: 'hono/jsx/dom',
},
jsxRuntime: {
varName: '_jsx_runtime',
package: 'hono/jsx/jsx-runtime',
},
}
const jsxComponentConfig = { HonoJSX, HonoDOM, _jsx_runtime }

const mdxSource = `
---
title: Example Post
published: 2021-02-13
description: This is some meta-data
---
import Demo from './demo'
# This is the title
Here's a **neat** demo:
<Demo />
`.trim();

const demoTsx = `
export default function Demo() {
return <div>mdx-bundler with hono's runtime!</div>
}
`.trim();


test('smoke test for hono', async () => {

const result = await bundleMDX({
source: mdxSource,
jsxConfig: jsxBundlerConfig,
files: {
'./demo.tsx': demoTsx
}
});

/** @param {HonoJSX.DOMAttributes} props */
const SpanBold = ({ children }) => {
return HonoJSX.createElement('span', { className: "strong" }, children)
}

const app = new Hono()
.get("/", (c) => {
const Component = getMDXComponent(result.code, jsxComponentConfig);
return c.html(HonoJSX.jsx(Component, { components: { strong: SpanBold } }).toString());
});

const req = new Request("http://localhost/");
const res = await app.fetch(req);
assert.equal(await res.text(), `<h1>This is the title</h1>
<p>Here&#39;s a <span class="strong">neat</span> demo:</p>
<div>mdx-bundler with hono&#39;s runtime!</div>`);
})

test.run()
5 changes: 3 additions & 2 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import './setup-tests.js'
import path from 'path'
import {test} from 'uvu'
import {suite} from 'uvu'
import * as assert from 'uvu/assert'
import React from 'react'
import rtl from '@testing-library/react'
import leftPad from 'left-pad'
import remarkMdxImages from 'remark-mdx-images'
import {VFile} from 'vfile'
import {bundleMDX} from '../index.js'
import {getMDXComponent, getMDXExport} from '../client.js'
import {getMDXComponent, getMDXExport} from '../client/react.js'

const test = suite("react");
const {render} = rtl

test('smoke test', async () => {
Expand Down
88 changes: 88 additions & 0 deletions src/__tests__/preact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import './setup-tests.js'
import * as Preact from "preact";
import * as PreactDOM from "preact/compat";
import * as _jsx_runtime from 'preact/jsx-runtime';
import {suite} from 'uvu'
import * as assert from 'uvu/assert'
import { render } from '@testing-library/preact'
import {bundleMDX} from '../index.js'
import {getMDXComponent} from '../client/jsx.js'

const test = suite("preact");

const jsxBundlerConfig = {
jsxLib: {
varName: 'Preact',
package: 'preact',
},
jsxDom: {
varName: 'PreactDom',
package: 'preact/compat',
},
jsxRuntime: {
varName: '_jsx_runtime',
package: 'preact/jsx-runtime',
},
}
const jsxComponentConfig = { Preact, PreactDOM, _jsx_runtime }

const mdxSource = `
---
title: Example Post
published: 2021-02-13
description: This is some meta-data
---
import Demo from './demo'
# This is the title
Here's a **neat** demo:
<Demo />
`.trim();

const demoTsx = `
export default function Demo() {
return <div>mdx-bundler with Preact's runtime!</div>
}
`.trim();


test('smoke test for preact', async () => {

const result = await bundleMDX({
source: mdxSource,
jsxConfig: jsxBundlerConfig,
files: {
'./demo.tsx': demoTsx
}
});

/**
* @type {import('preact').FunctionComponent<{ components?: Record<string, any> }>}
*/
const Component = getMDXComponent(result.code, jsxComponentConfig)

/** @type {Preact.FunctionComponent<{ children:Preact.ComponentChildren }>} props */
const SpanBold = ({children}) => {
return Preact.createElement('span', { className: "strong" }, children)
}

assert.equal(result.frontmatter, {
title: 'Example Post',
published: new Date('2021-02-13'),
description: 'This is some meta-data',
})

const {container} = render(
Preact.h(Component, {components: {strong: SpanBold}})
)

assert.equal(
container.innerHTML,
`<h1>This is the title</h1>
<p>Here's a <span class="strong">neat</span> demo:</p>
<div>mdx-bundler with Preact's runtime!</div>`,
)
})

test.run()
Loading

0 comments on commit 70cb04f

Please sign in to comment.