Skip to content

Commit

Permalink
feat: support MDX v2 (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryce Kalow committed Feb 15, 2022
1 parent d2ad91e commit f08d885
Show file tree
Hide file tree
Showing 23 changed files with 20,602 additions and 6,862 deletions.
24 changes: 0 additions & 24 deletions .circleci/config.yml

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: 'Test'
on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
test:
name: 'Run Tests 🧪'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '16.x'
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install and build
run: |
npm ci
npm run build
- name: Run Jest
run: npm run test
27 changes: 27 additions & 0 deletions .jest/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ReactDOMServer from 'react-dom/server'
import React from 'react'

import { MDXRemote, MDXRemoteProps } from '../src/index'
import { serialize } from '../src/serialize'
import { SerializeOptions } from '../src/types'

export async function renderStatic(
mdx: string,
{
components,
scope,
mdxOptions,
minifyOptions,
parseFrontmatter,
}: SerializeOptions & Pick<MDXRemoteProps, 'components'> = {}
): Promise<string> {
const mdxSource = await serialize(mdx, {
mdxOptions,
minifyOptions,
parseFrontmatter,
})

return ReactDOMServer.renderToStaticMarkup(
<MDXRemote {...mdxSource} components={components} scope={scope} />
)
}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
114 changes: 58 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,48 @@ While it may seem strange to see these two in the same file, this is one of the
### Additional Examples

<details>
<summary>Parsing Frontmatter</summary>

Markdown in general is often paired with frontmatter, and normally this means adding some extra custom processing to the way markdown is handled. To address this, `next-mdx-remote` comes with optional parsing of frontmatter, which can be enabled by passing `parseFrontmatter: true` to `serialize`.

Here's what that looks like:

```jsx
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ mdxSource }) {
return (
<div className="wrapper">
<h1>{mdxSource.frontmatter.title}</h1>
<MDXRemote {...mdxSource} components={components} />
</div>
)
}

export async function getStaticProps() {
// MDX text - can be from a local file, database, anywhere
const source = `---
title: Test
---
Some **mdx** text, with a component <Test name={title}/>
`

const mdxSource = await serialize(source, { parseFrontmatter: true })
return { props: { mdxSource } }
}
```

_[`vfile-matter`](https://github.com/vfile/vfile-matter) is used to parse the frontmatter._

</details>

<details>
<summary>Passing custom data to a component with `scope`</summary>

Expand Down Expand Up @@ -133,7 +175,7 @@ export async function getStaticProps() {
Custom components from <code>MDXProvider</code><a id="mdx-provider"></a>
</summary>

If you want to make components available to any `<MDXRemote />` being rendered in your application, you can use [`<MDXProvider />`](https://mdxjs.com/advanced/components#mdxprovider) from `@mdx-js/react`.
If you want to make components available to any `<MDXRemote />` being rendered in your application, you can use [`<MDXProvider />`](https://mdxjs.com/docs/using-mdx/#mdx-provider) from `@mdx-js/react`.

```jsx
// pages/_app.jsx
Expand Down Expand Up @@ -180,7 +222,7 @@ export async function getStaticProps() {
Component names with dot (e.g. <code>motion.div</code>)
</summary>

Component names that contain a dot (`.`), such as those from `framer-motion`, can be rendered as long as the top-level namespace is declared in the MDX scope:
Component names that contain a dot (`.`), such as those from `framer-motion`, can be rendered the same way as other custom components, just pass `motion` in your components object.

```js
import { motion } from 'framer-motion'
Expand All @@ -192,7 +234,7 @@ import { MDXRemote } from 'next-mdx-remote'
export default function TestPage({ source }) {
return (
<div className="wrapper">
<MDXRemote {...source} scope={{ motion }} />
<MDXRemote {...source} components={{ motion }} />
</div>
)
}
Expand Down Expand Up @@ -246,9 +288,9 @@ export async function getStaticProps() {

This library exposes a function and a component, `serialize` and `<MDXRemote />`. These two are purposefully isolated into their own files -- `serialize` is intended to be run **server-side**, so within `getStaticProps`, which runs on the server/at build time. `<MDXRemote />` on the other hand is intended to be run on the client side, in the browser.

- **`serialize(source: string, { mdxOptions?: object, scope?: object, target?: string | string[] })`**
- **`serialize(source: string, { mdxOptions?: object, scope?: object, parseFrontmatter?: boolean })`**

**`serialize`** consumes a string of MDX. It also can optionally be passed options which are [passed directly to MDX](https://mdxjs.com/advanced/plugins), and a scope object that can be included in the mdx scope. The function returns an object that is intended to be passed into `<MDXRemote />` directly.
**`serialize`** consumes a string of MDX. It can also optionally be passed options which are [passed directly to MDX](https://mdxjs.com/docs/extending-mdx/), and a scope object that can be included in the mdx scope. The function returns an object that is intended to be passed into `<MDXRemote />` directly.

```ts
serialize(
Expand All @@ -267,9 +309,8 @@ This library exposes a function and a component, `serialize` and `<MDXRemote />`
compilers: [],
filepath: '/some/file/path',
},
// Specify the target environment for the generated code. See esbuild docs:
// https://esbuild.github.io/api/#target
target: ['esnext'],
// Indicates whether or not to parse the frontmatter from the mdx source
parseFrontmatter: false,
}
)
```
Expand All @@ -284,51 +325,9 @@ This library exposes a function and a component, `serialize` and `<MDXRemote />`
<MDXRemote {...source} components={components} />
```

## Frontmatter & Custom Processing

Markdown in general is often paired with frontmatter, and normally this means adding some extra custom processing to the way markdown is handled. Luckily, this can be done entirely independently of `next-mdx-remote`, along with any extra custom processing necessary.

Let's walk through an example of how we could process frontmatter out of our MDX source:

```jsx
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'

import matter from 'gray-matter'

import Test from '../components/test'

const components = { Test }

export default function TestPage({ source, frontMatter }) {
return (
<div className="wrapper">
<h1>{frontMatter.title}</h1>
<MDXRemote {...source} components={components} />
</div>
)
}

export async function getStaticProps() {
// MDX text - can be from a local file, database, anywhere
const source = `---
title: Test
---
Some **mdx** text, with a component <Test name={title}/>
`

const { content, data } = matter(source)
const mdxSource = await serialize(content, { scope: data })
return { props: { source: mdxSource, frontMatter: data } }
}
```

Nice and easy - since we get the content as a string originally and have full control, we can run any extra custom processing needed before passing it into `serialize`, and easily append extra data to the return value from `getStaticProps` without issue.

### Replacing default components

Rendering will use [`MDXProvider`](https://mdxjs.com/getting-started#mdxprovider) under the hood. This means you can replace HTML tags by custom components. Those components are listed in MDXJS [Table of components](https://mdxjs.com/table-of-components).
Rendering will use [`MDXProvider`](https://mdxjs.com/docs/using-mdx/#mdx-provider) under the hood. This means you can replace HTML tags by custom components. Those components are listed in MDXJS [Table of components](https://mdxjs.com/table-of-components/).

An example use case is rendering the content with your preferred styling library.

Expand Down Expand Up @@ -368,7 +367,7 @@ If you really insist though, check out [our official nextjs example implementati

### Environment Targets

The code generated by `next-mdx-remote`, which is used to actually render the MDX, is transformed to support: `>= node 12, es2020`.
The code generated by `next-mdx-remote`, which is used to actually render the MDX targets browsers with module support. If you need to support older browsers, consider transpiling the `compiledSource` output from `serialize`.

### `import` / `export`

Expand Down Expand Up @@ -410,10 +409,13 @@ export default function ExamplePage({ mdxSource }: Props) {
)
}

export const getStaticProps: GetStaticProps<{mdxSource: MDXRemoteSerializeResult}> = async () => {
const mdxSource = await serialize('some *mdx* content: <ExampleComponent />')
return { props: { mdxSource } }
}
export const getStaticProps: GetStaticProps<{mdxSource: MDXRemoteSerializeResult}> =
async () => {
const mdxSource = await serialize(
'some *mdx* content: <ExampleComponent />'
)
return { props: { mdxSource } }
}
```

## Migrating from v2 to v3
Expand Down
20 changes: 19 additions & 1 deletion __tests__/fixtures/basic/mdx/test.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Bar from 'bar'

# Headline

<Test name={name} />
<Test name={frontmatter.name} />

<ContextConsumer />

Expand All @@ -16,3 +16,21 @@ Some **markdown** content
~> Alert

<Dynamic />

```shell-session
curl localhost
```

This is more text.

~> < client node IP >:9999

"Authorize \<GITHUB_USER\>"

(support for version \<230)

### Some version \<= 1.3.x

#### metric.name.\<operation>.\<mount>

< 8ms
3 changes: 3 additions & 0 deletions __tests__/fixtures/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
16 changes: 9 additions & 7 deletions __tests__/fixtures/basic/pages/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { createContext, useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
import { serialize } from '../../../../serialize'
Expand Down Expand Up @@ -34,7 +33,7 @@ const MDX_COMPONENTS = {
Dynamic: dynamic(() => import('../components/dynamic')),
}

export default function TestPage({ data, mdxSource }) {
export default function TestPage({ mdxSource }) {
const [providerOptions, setProviderOptions] = useState(PROVIDER)

useEffect(() => {
Expand All @@ -48,19 +47,22 @@ export default function TestPage({ data, mdxSource }) {

return (
<>
<h1>{data.title}</h1>
<h1>{mdxSource.frontmatter.title}</h1>
<TestContext.Provider {...providerOptions.props}>
<MDXRemote {...mdxSource} components={MDX_COMPONENTS} scope={data} />
<MDXRemote {...mdxSource} components={MDX_COMPONENTS} />
</TestContext.Provider>
</>
)
}

export async function getStaticProps() {
const fixturePath = path.join(process.cwd(), 'mdx/test.mdx')
const { data, content } = matter(fs.readFileSync(fixturePath, 'utf8'))
const mdxSource = await serialize(content, {
const source = await fs.promises.readFile(fixturePath, 'utf8')

const mdxSource = await serialize(source, {
mdxOptions: { remarkPlugins: [paragraphCustomAlerts] },
parseFrontmatter: true,
})
return { props: { mdxSource, data } }

return { props: { mdxSource } }
}

0 comments on commit f08d885

Please sign in to comment.