Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"type": "commonjs",
"main": "./index.js",
"react": "./react.js",
"jsx": "./jsx.js",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these? This looks like it actually should be in a export config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I don't know what was there... Fixed it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for breaking changes regression check, probably result.code snapshot test would be best shot.

Create snapshots with separate MR, merge them into main, and then rebase into this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mostly just thinking about the imports that people have already. I'm pretty sure that you have everything wired up properly, but I just wanted to double check.

"types": "./index.d.ts"
}
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')
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"esbuild": "0.*"
},
"devDependencies": {
"@testing-library/preact": "3.2.4",
"@testing-library/react": "^14.1.0",
"@types/jsdom": "^21.1.5",
"@types/mdx": "^2.0.10",
Expand All @@ -63,10 +64,12 @@
"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",
Expand Down
77 changes: 77 additions & 0 deletions src/__tests__/hono.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import './setup-tests.js'
import { Hono } from "hono";
import * as HonoJSX from "hono/jsx";
import * as HonoDOM from "hono/jsx/dom";
import * as _jsx_runtime from "hono/jsx/jsx-runtime";
import {test} from 'uvu'
import * as assert from 'uvu/assert'
import {bundleMDX} from '../index.js'
import {getMDXComponent} from '../client/jsx.js'



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
}
});

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()
2 changes: 1 addition & 1 deletion src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 {render} = rtl

Expand Down
85 changes: 85 additions & 0 deletions src/__tests__/preact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import './setup-tests.js'
import * as Preact from "preact";
import * as PreactDOM from "preact/compat";
import * as _jsx_runtime from 'preact/jsx-runtime';
import {test} 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 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
}
});

const Component = getMDXComponent(result.code, jsxComponentConfig)

/** @param {Preact.JSX.HTMLAttributes<HTMLSpanElement>} 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()
1 change: 1 addition & 0 deletions src/client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./react"
33 changes: 33 additions & 0 deletions src/client/jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @typedef {import('../types').MDXContentProps} MDXContentProps
*/

/**
*
* @param {string} code - The string of code you got from bundleMDX
* @param {Record<string, unknown>} jsxGlobals - JSX globals
* @param {Record<string, unknown>} [globals] - Any variables your MDX needs to have accessible when it runs
* @return {(props: MDXContentProps) => JSX.Element}
*/
function getMDXComponent(code, jsxGlobals, globals) {
const mdxExport = getMDXExport(code, jsxGlobals, globals)
return mdxExport.default
}

/**
* @template {{}} ExportedObject
* @template {{}} Frontmatter
* @type {import('../types').MDXJsxExportFunction<ExportedObject, Frontmatter>}
* @param {string} code - The string of code you got from bundleMDX
* @param {Record<string, unknown>} jsxGlobals - JSX globals
* @param {Record<string, unknown>} [globals] - Any variables your MDX needs to have accessible when it runs
*
*/
function getMDXExport(code, jsxGlobals, globals) {
const scope = {...jsxGlobals, ...globals}
// eslint-disable-next-line
const fn = new Function(...Object.keys(scope), code)
return fn(...Object.values(scope))
}

export {getMDXComponent, getMDXExport}
Loading
Loading