1
1
mirror of https://github.com/mdx-js/mdx.git synced 2024-10-03 19:07:42 +03:00

Refactor code-style

This commit is contained in:
Titus Wormer 2023-10-18 14:38:06 +02:00
parent 89097e4c31
commit a94f28521d
No known key found for this signature in database
GPG Key ID: E6E581152ED04E2E
125 changed files with 5722 additions and 4618 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ node_modules/
*.d.cts
*.d.ts
/public/
!/packages/mdx/lib/types.d.ts
!/website/types.d.ts

View File

@ -1,4 +1,5 @@
import {Note} from './_component/note.jsx'
export {Home as default} from './_component/home.jsx'
export const navExclude = true

View File

@ -2,31 +2,43 @@
/**
* @typedef {import('@wooorm/starry-night').Grammar} Grammar
* @typedef {import('estree').Node} EstreeNode
* @typedef {import('estree').Program} Program
* @typedef {import('hast').Nodes} HastNodes
* @typedef {import('hast').Root} HastRoot
* @typedef {import('mdast').Nodes} MdastNodes
* @typedef {import('mdast').Root} MdastRoot
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttribute} MdxJsxAttribute
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttributeValueExpression} MdxJsxAttributeValueExpression
* @typedef {import('mdast-util-mdx-jsx').MdxJsxExpressionAttribute} MdxJsxExpressionAttribute
* @typedef {import('mdx/types.js').MDXModule} MDXModule
* @typedef {import('react-error-boundary').FallbackProps} FallbackProps
* @typedef {import('unified').PluggableList} PluggableList
* @typedef {import('estree').Program} Program
* @typedef {import('estree').Node} EstreeNode
* @typedef {import('hast').Root} HastRoot
* @typedef {import('hast').Nodes} HastNodes
* @typedef {import('mdast').Root} MdastRoot
* @typedef {import('mdast').Nodes} MdastNodes
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttribute} MdxJsxAttribute
* @typedef {import('mdast-util-mdx-jsx').MdxJsxExpressionAttribute} MdxJsxExpressionAttribute
* @typedef {import('mdast-util-mdx-jsx').MdxJsxAttributeValueExpression} MdxJsxAttributeValueExpression
* @typedef {import('unist').Node} UnistNode
*/
/**
* @typedef EvalOk
* @property {true} ok
* @property {JSX.Element} value
* @typedef DisplayProps
* Props.
* @property {Error} error
* Error.
*
* @typedef EvalNok
* Not OK.
* @property {false} ok
* Whether OK.
* @property {Error} value
* Error.
*
* @typedef EvalOk
* OK.
* @property {true} ok
* Whether OK.
* @property {JSX.Element} value
* Result.
*
* @typedef {EvalNok | EvalOk} EvalResult
* Result.
*/
import {compile, nodeTypes, run} from '@mdx-js/mdx'
@ -42,7 +54,7 @@ import textMd from '@wooorm/starry-night/text.md'
import {visit as visitEstree} from 'estree-util-visit'
import {toJsxRuntime} from 'hast-util-to-jsx-runtime'
import {useEffect, useState} from 'react'
// @ts-expect-error: untyped.
// @ts-expect-error: the automatic react runtime is untyped.
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
import ReactDom from 'react-dom/client'
import {ErrorBoundary} from 'react-error-boundary'
@ -61,7 +73,7 @@ const sample = `# Hello, world!
Below is an example of markdown in JSX.
<div style={{padding: '1rem', backgroundColor: 'violet'}}>
<div style={{backgroundColor: 'violet', padding: '1rem'}}>
Try and change the background color to \`tomato\`.
</div>`
@ -92,6 +104,9 @@ if (body && window.location.pathname === '/playground/') {
/**
* @param {Element} main
* DOM element.
* @returns {undefined}
* Nothing.
*/
function init(main) {
const root = ReactDom.createRoot(main)
@ -99,6 +114,7 @@ function init(main) {
createStarryNight(grammars).then(
/**
* @returns {undefined}
* Nothing.
*/
function (x) {
starryNight = x
@ -116,6 +132,7 @@ function init(main) {
function Playground() {
const [directive, setDirective] = useState(false)
const [evalResult, setEvalResult] = useState(
// Cast to more easily use actual value.
/** @type {unknown} */ (undefined)
)
const [development, setDevelopment] = useState(false)
@ -139,6 +156,9 @@ function Playground() {
},
/**
* @param {Error} error
* Error.
* @returns {undefined}
* Nothing.
*/
function (error) {
setEvalResult({ok: false, value: error})
@ -225,6 +245,9 @@ function Playground() {
function captureMdast() {
/**
* @param {MdastRoot} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
const clone = structuredClone(tree)
@ -236,6 +259,9 @@ function Playground() {
function captureHast() {
/**
* @param {HastRoot} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
const clone = structuredClone(tree)
@ -247,6 +273,9 @@ function Playground() {
function captureEsast() {
/**
* @param {Program} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
const clone = structuredClone(tree)
@ -273,6 +302,7 @@ function Playground() {
)
const scope = formatMarkdown ? 'text.md' : 'source.mdx'
// Cast to actual value.
const compiledResult = /** @type {EvalResult | undefined} */ (evalResult)
/** @type {JSX.Element | undefined} */
let display
@ -532,13 +562,14 @@ function Playground() {
/**
*
* @param {FallbackProps} props
* @param {Readonly<FallbackProps>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
function ErrorFallback(props) {
/** @type {Error} */
// type-coverage:ignore-next-line
const error = props.error
const error = /** @type {Error} */ (props.error)
return (
<div role="alert">
<p>Something went wrong:</p>
@ -551,9 +582,10 @@ function ErrorFallback(props) {
}
/**
*
* @param {{error: Error}} props
* @param {DisplayProps} props
* Props.
* @returns {JSX.Element}
* Element.
*/
function DisplayError(props) {
return (
@ -565,6 +597,9 @@ function DisplayError(props) {
/**
* @param {HastRoot | MdastRoot} node
* mdast or hast root.
* @returns {undefined}
* Nothing.
*/
function cleanUnistTree(node) {
removePosition(node, {force: true})
@ -572,7 +607,10 @@ function cleanUnistTree(node) {
}
/**
* @param {HastNodes | MdastNodes | MdxJsxAttribute | MdxJsxExpressionAttribute | MdxJsxAttributeValueExpression} node
* @param {HastNodes | MdastNodes | MdxJsxAttribute | MdxJsxAttributeValueExpression | MdxJsxExpressionAttribute} node
* Node.
* @returns {undefined}
* Nothing.
*/
function cleanUnistNode(node) {
if (
@ -601,12 +639,15 @@ function cleanUnistNode(node) {
/**
* @param {EstreeNode} node
* estree node.
* @returns {undefined}
* Nothing.
*/
function removeFromEstree(node) {
delete node.loc
// @ts-expect-error: acorn.
// @ts-expect-error: this field is added by acorn.
delete node.start
// @ts-expect-error: acorn.
// @ts-expect-error: this field is added by acorn.
delete node.end
delete node.range
}

View File

@ -62,6 +62,9 @@ for (const copy of copies) {
/**
* @this {HTMLButtonElement}
* Button element.
* @returns {undefined}
* Nothing.
*/
function onclick() {
assert(copyIcon)

View File

@ -4,19 +4,25 @@
/**
* @typedef EntryProps
* @property {Item} item
* Props for `BlogEntry`.
* @property {Readonly<Item>} item
* Item.
*
* @typedef GroupProps
* Props for `BlogGroup`.
* @property {string | undefined} [className]
* @property {Array<Item>} items
* Class name.
* @property {ReadonlyArray<Item>} items
* Items.
* @property {string | undefined} [sort]
* Fields to sort on.
*/
import React from 'react'
// @ts-expect-error: untyped.
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
import {apStyleTitleCase} from 'ap-style-title-case'
import {toJsxRuntime} from 'hast-util-to-jsx-runtime'
import React from 'react'
// @ts-expect-error: the automatic react runtime is untyped.
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
import {sortItems} from './sort.js'
const runtime = {Fragment, jsx, jsxs}
@ -24,14 +30,14 @@ const runtime = {Fragment, jsx, jsxs}
const dateTimeFormat = new Intl.DateTimeFormat('en', {dateStyle: 'long'})
/**
* @param {EntryProps} props
* @param {Readonly<EntryProps>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function BlogEntry(props) {
const {item} = props
const {name, data = {}} = item
const {data, name} = item
const {matter = {}, meta = {}} = data
const title = matter.title || meta.title
const defaultTitle = apStyleTitleCase(
@ -44,7 +50,9 @@ export function BlogEntry(props) {
? meta.readingTime
: [meta.readingTime, meta.readingTime]
: []
).map((d) => Math.ceil(d))
).map(function (d) {
return Math.ceil(d)
})
/** @type {string | undefined} */
let timeLabel
@ -64,7 +72,7 @@ export function BlogEntry(props) {
toJsxRuntime(meta.descriptionHast, runtime)
) : description ? (
<p>{description}</p>
) : null}
) : undefined}
<span>
<a href={name}>Continue reading »</a>
</span>
@ -98,20 +106,20 @@ export function BlogEntry(props) {
}
/**
* @param {GroupProps} props
* @param {Readonly<GroupProps>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function BlogGroup(props) {
const {items, className, sort = 'navSortSelf,meta.title', ...rest} = props
const {className, items, sort = 'navSortSelf,meta.title', ...rest} = props
const sorted = sortItems(items, sort)
return (
<>
{sorted.map((d) => (
<BlogEntry key={d.name} {...rest} item={d} />
))}
{sorted.map(function (d) {
return <BlogEntry key={d.name} {...rest} item={d} />
})}
</>
)
}

View File

@ -6,8 +6,8 @@ export function FootSite() {
<footer className="foot-site">
<div className="content">
<div
style={{display: 'flex', justifyContent: 'space-between'}}
className="block"
style={{display: 'flex', justifyContent: 'space-between'}}
>
<div>
<small>

View File

@ -1,39 +1,45 @@
/**
* @typedef {import('react').ReactNode} ReactNode
* @typedef {Exclude<import('vfile').Data['meta'], undefined>} Meta
* @typedef {import('vfile').Data['meta']} DataMeta
* @typedef {import('./sort.js').Item} Item
*/
/**
* @typedef {Exclude<DataMeta, undefined>} Meta
*
* @typedef Props
* Props.
* @property {string} name
* Name.
* @property {ReactNode} children
* Children.
* @property {Item} navTree
* Navigation tree.
* @property {Meta} meta
* Meta.
*/
import React from 'react'
import {NavSite, NavSiteSkip} from './nav-site.jsx'
import {FootSite} from './foot-site.jsx'
import {NavSite, NavSiteSkip} from './nav-site.jsx'
/**
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function Home(props) {
const {name, meta, navTree, children} = props
/** @type {unknown} */
// @ts-expect-error: to do: type.
const schema = meta.schemaOrg
const {children, meta, name, navTree} = props
return (
<div className="page home">
<NavSiteSkip />
<main>
{schema ? (
<script type="application/ld+json">{JSON.stringify(schema)}</script>
{meta.schemaOrg ? (
<script type="application/ld+json">
{JSON.stringify(meta.schemaOrg)}
</script>
) : undefined}
<article>
<div className="content body">{children}</div>

View File

@ -1,32 +1,38 @@
/**
* @typedef {import('vfile').Data['meta']} DataMeta
* @typedef {import('./sort.js').Item} Item
* @typedef {import('../../website/generate.js').Author} Author
*/
/**
* @typedef Props
* Props.
* @property {string} name
* @property {URL} ghUrl
* @property {DataMeta | undefined} [meta]
* @property {Item} navTree
* Name.
* @property {Readonly<URL>} ghUrl
* GitHub URL.
* @property {Readonly<DataMeta> | undefined} [meta]
* Meta.
* @property {Readonly<Item>} navTree
* Navigation tree.
* @property {JSX.Element} children
* Children.
*/
import React from 'react'
import {NavSite, NavSiteSkip} from './nav-site.jsx'
import {FootSite} from './foot-site.jsx'
import {NavSite, NavSiteSkip} from './nav-site.jsx'
import {sortItems} from './sort.js'
const dateTimeFormat = new Intl.DateTimeFormat('en', {dateStyle: 'long'})
/**
*
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function Layout(props) {
const {name, navTree, ghUrl} = props
const {ghUrl, name, navTree} = props
const [self, parent] = findSelfAndParent(navTree) || []
const navSortItems = parent ? parent.data.navSortItems : undefined
const siblings = parent
@ -39,8 +45,6 @@ export function Layout(props) {
const index = self ? siblings.indexOf(self) : -1
const previous = index === -1 ? undefined : siblings[index - 1]
const next = index === -1 ? undefined : siblings[index + 1]
/** @type {Array<Author>} */
// @ts-expect-error: to do: augment types.
const metaAuthors = meta.authors || []
const metaTime = (
self
@ -50,7 +54,9 @@ export function Layout(props) {
? meta.readingTime
: [meta.readingTime, meta.readingTime]
: []
).map((d) => (d > 15 ? Math.round(d / 5) * 5 : Math.ceil(d)))
).map(function (d) {
return d > 15 ? Math.round(d / 5) * 5 : Math.ceil(d)
})
/** @type {string | undefined} */
let timeLabel
@ -130,7 +136,7 @@ export function Layout(props) {
const readingTime = timeLabel ? <>{timeLabel} read</> : undefined
const creditsList = metaAuthors.map((d, i) => {
const creditsList = metaAuthors.map(function (d, i) {
const href = d.github
? 'https://github.com/' + d.github
: d.twitter
@ -207,8 +213,11 @@ export function Layout(props) {
/**
* @param {Item} item
* Item.
* @param {Item | undefined} [parent]
* Parent.
* @returns {[self: Item, parent: Item | undefined] | undefined}
* Self and parent.
*/
function findSelfAndParent(item, parent) {
if (item.name === name) return [item, parent]
@ -225,7 +234,9 @@ export function Layout(props) {
/**
* @param {Item} d
* Item.
* @returns {string | undefined}
* Title.
*/
function entryToTitle(d) {
return d.data.matter?.title || d.data.meta?.title || undefined
@ -233,7 +244,9 @@ function entryToTitle(d) {
/**
* @param {Item} d
* Item.
* @returns {[number, number] | [number] | []}
* Reading time.
*/
function accumulateReadingTime(d) {
const time = (d.data.meta || {}).readingTime

View File

@ -4,17 +4,20 @@
/**
* @typedef Props
* Props.
* @property {string} name
* @property {Item} navTree
* Name.
* @property {Readonly<Item>} navTree
* Navigation tree.
*/
import React from 'react'
import {config} from '../_config.js'
import {NavGroup} from './nav.jsx'
import {Mdx} from './icon/mdx.jsx'
import {GitHub} from './icon/github.jsx'
import {Twitter} from './icon/twitter.jsx'
import {Mdx} from './icon/mdx.jsx'
import {OpenCollective} from './icon/open-collective.jsx'
import {Twitter} from './icon/twitter.jsx'
import {NavGroup} from './nav.jsx'
export function NavSiteSkip() {
return (
@ -29,8 +32,10 @@ export function NavSiteSkip() {
}
/**
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function NavSite(props) {
const {name, navTree} = props

View File

@ -9,23 +9,37 @@
/**
* @typedef ItemProps
* @property {boolean | undefined} [includeDescription]
* @property {boolean | undefined} [includePublished]
* @property {Item} item
* Props for `NavItem`.
* @property {boolean | undefined} [includeDescription=false]
* Whether to include the description (default: `false`).
* @property {boolean | undefined} [includePublished=false]
* Whether to include the published date (default: `false`).
* @property {Readonly<Item>} item
* Item.
* @property {string | undefined} [name]
* Name.
*
* @typedef GroupProps
* @typedef GroupOnlyProps
* Props for `NavGroup`;
* Other fields are passed to `NavItem`.
* @property {string | undefined} [className]
* @property {Array<Item>} items
* Class name.
* @property {ReadonlyArray<Item>} items
* Items.
* @property {string | undefined} [sort]
* Fields to sort on.
* @property {string | undefined} [name]
* Name.
*
* @typedef {Omit<ItemProps, 'item'> & GroupOnlyProps} GroupProps
* Props for `NavGroup`.
*/
import React from 'react'
// @ts-expect-error: untyped.
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
import {apStyleTitleCase} from 'ap-style-title-case'
import {toJsxRuntime} from 'hast-util-to-jsx-runtime'
import React from 'react'
// @ts-expect-error: the automatic react runtime is untyped.
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
import {sortItems} from './sort.js'
const runtime = {Fragment, jsx, jsxs}
@ -33,28 +47,32 @@ const runtime = {Fragment, jsx, jsxs}
const dateTimeFormat = new Intl.DateTimeFormat('en', {dateStyle: 'long'})
/**
* @param {GroupProps} props
* @param {Readonly<GroupProps>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function NavGroup(props) {
const {items, className, sort = 'navSortSelf,meta.title', ...rest} = props
const {className, items, sort = 'navSortSelf,meta.title', ...rest} = props
return (
<ol {...{className}}>
{sortItems(items, sort).map((d) => (
<NavItem key={d.name} {...rest} item={d} />
))}
{sortItems(items, sort).map(function (d) {
return <NavItem key={d.name} {...rest} item={d} />
})}
</ol>
)
}
/**
* @param {ItemProps} props
* @param {Readonly<ItemProps>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function NavItem(props) {
const {item, name: activeName, includeDescription, includePublished} = props
const {name, children, data = {}} = item
const {includeDescription, includePublished, item, name: activeName} = props
const {children, data = {}, name} = item
const {matter = {}, meta = {}, navExcludeGroup, navSortItems} = data
const title = matter.title || meta.title
const defaultTitle = apStyleTitleCase(
@ -67,6 +85,7 @@ export function NavItem(props) {
if (includeDescription) {
if (meta.descriptionHast) {
// Cast because we dont expect doctypes.
const children = /** @type {Array<ElementContent>} */ (
meta.descriptionHast.children
)
@ -81,7 +100,7 @@ export function NavItem(props) {
runtime
)
} else {
description = matter.description || meta.description || null
description = matter.description || meta.description || undefined
if (description) {
description = (
@ -110,15 +129,15 @@ export function NavItem(props) {
) : (
defaultTitle
)}
{published ? ' — ' + published : null}
{description || null}
{published ? ' — ' + published : undefined}
{description || undefined}
{!navExcludeGroup && children.length > 0 ? (
<NavGroup
items={children}
sort={typeof navSortItems === 'string' ? navSortItems : undefined}
name={activeName}
/>
) : null}
) : undefined}
</li>
)
}

View File

@ -3,11 +3,15 @@
*/
/**
* @typedef {'info' | 'legacy' | 'important'} NoteType
* @typedef {'important' | 'info' | 'legacy'} NoteType
* Type.
*
* @typedef Props
* Props for `Note`.
* @property {NoteType} type
* @property {ReactNode} children
* Kind.
* @property {Readonly<ReactNode>} children
* Children.
*/
import React from 'react'
@ -16,7 +20,7 @@ import React from 'react'
const known = new Set(['info', 'legacy', 'important'])
/**
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.

View File

@ -1,7 +1,10 @@
/**
* @typedef Props
* Props.
* @property {string} color
* Color.
* @property {number} year
* Year.
*/
import React from 'react'
@ -9,7 +12,7 @@ import React from 'react'
const data = [6, 5, 2, 4.5, 1.5, 2.5, 2, 2.5, 1.5, 2.5, 3.5, 7]
/**
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
@ -17,17 +20,18 @@ const data = [6, 5, 2, 4.5, 1.5, 2.5, 2, 2.5, 1.5, 2.5, 3.5, 7]
export function Chart(props) {
return (
<div className="snowfall">
{data.map((d, i) => (
<div
/* eslint-disable-next-line react/no-array-index-key */
key={i}
className="snowfall-bar"
style={{
height: 'calc(' + d + ' * 0.5 * (1em + 1ex))',
backgroundColor: props.color
}}
/>
))}
{data.map(function (d) {
return (
<div
key={d}
className="snowfall-bar"
style={{
backgroundColor: props.color,
height: 'calc(' + d + ' * 0.5 * (1em + 1ex))'
}}
/>
)
})}
</div>
)
}

View File

@ -4,9 +4,13 @@
/**
* @typedef Item
* Item.
* @property {string} name
* @property {Data} data
* Name.
* @property {Readonly<Data>} data
* Data.
* @property {Array<Item>} children
* Children.
*/
import dlv from 'dlv'
@ -14,13 +18,16 @@ import dlv from 'dlv'
const collator = new Intl.Collator('en').compare
/**
* @param {Array<Item>} items
* @param {ReadonlyArray<Item>} items
* Items.
* @param {string | undefined} [sortString]
* @returns {Array<Item>}
* Fields to sort on (default: `'navSortSelf,meta.title'`).
* @returns {ReadonlyArray<Item>}
* Items.
*/
export function sortItems(items, sortString = 'navSortSelf,meta.title') {
/** @type {Array<[string, 'asc' | 'desc']>} */
const fields = sortString.split(',').map((d) => {
/** @type {ReadonlyArray<[string, 'asc' | 'desc']>} */
const fields = sortString.split(',').map(function (d) {
const [field, order = 'asc'] = d.split(':')
if (order !== 'asc' && order !== 'desc') {
@ -30,7 +37,7 @@ export function sortItems(items, sortString = 'navSortSelf,meta.title') {
return [field, order]
})
return [...items].sort((left, right) => {
return [...items].sort(function (left, right) {
let index = -1
while (++index < fields.length) {
@ -40,6 +47,7 @@ export function sortItems(items, sortString = 'navSortSelf,meta.title') {
/** @type {unknown} */
let b = dlv(right.data, field)
// Dates.
if (a && typeof a === 'object' && 'valueOf' in a) a = a.valueOf()
if (b && typeof b === 'object' && 'valueOf' in b) b = b.valueOf()

View File

@ -3,19 +3,19 @@ const git = new URL('../', import.meta.url)
const gh = new URL('https://github.com/mdx-js/mdx/')
export const config = {
input: new URL('docs/', git),
output: new URL('public/', git),
git,
author: 'MDX contributors',
color: '#010409',
gh,
ghBlob: new URL('blob/main/', gh),
ghTree: new URL('tree/main/', gh),
site,
twitter: new URL('https://twitter.com/mdx_js'),
git,
input: new URL('docs/', git),
oc: new URL('https://opencollective.com/unified'),
color: '#010409',
title: 'MDX',
output: new URL('public/', git),
site,
tags: ['mdx', 'markdown', 'jsx', 'oss', 'react'],
author: 'MDX contributors'
title: 'MDX',
twitter: new URL('https://twitter.com/mdx_js')
}
/** @type {Record<string, string>} */

View File

@ -1,10 +1,11 @@
import {Note} from '../_component/note.jsx'
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2020-07-31'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2020-07-31')
}
<Note type="legacy">

View File

@ -1,10 +1,11 @@
import {Note} from '../_component/note.jsx'
export const info = {
author: [
{name: 'Chris Biscardi', github: 'christopherbiscardi', twitter: 'chrisbiscardi'}
{github: 'christopherbiscardi', name: 'Chris Biscardi', twitter: 'chrisbiscardi'}
],
published: new Date('2019-03-11'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2019-03-11')
}
<Note type="legacy">

View File

@ -1,22 +1,31 @@
import assert from 'node:assert/strict'
import {BlogGroup} from '../_component/blog.jsx'
export const navExcludeGroup = true
export const navSortSelf = 7
export const navSortItems = 'navSortSelf,meta.published:desc'
export const info = {
author: [{name: 'MDX Contributors'}],
published: new Date('2021-11-01'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-11-01')
}
export const navExcludeGroup = true
export const navSortItems = 'navSortSelf,meta.published:desc'
export const navSortSelf = 7
# Blog
The latest news about MDX.
{
(() => {
const category = props.navTree.children.find(
item => item.name === '/blog/'
)
(function () {
/**
* @typedef {import('../_component/sort.js').Item} Item
*/
/** @type {Item} */
const navTree = props.navTree
const category = navTree.children.find(function (item) {
return item.name === '/blog/'
})
assert(category)
return (
<nav>

View File

@ -1,10 +1,11 @@
import {Note} from '../_component/note.jsx'
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2019-05-14'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2019-05-14')
}
<Note type="legacy">
@ -30,9 +31,9 @@ you can add in components with the `MDXProvider`:
```tsx path="src/App.js"
import React from 'react'
import {MDXProvider} from '@mdx-js/react'
import {YouTube, Twitter, TomatoBox} from './ui'
import {TomatoBox, Twitter, YouTube} from './ui'
const shortcodes = {YouTube, Twitter, TomatoBox}
const shortcodes = {TomatoBox, Twitter, YouTube}
export default ({children}) => (
<MDXProvider components={shortcodes}>{children}</MDXProvider>

View File

@ -1,10 +1,11 @@
import {Note} from '../_component/note.jsx'
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2019-04-11'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2019-04-11')
}
<Note type="legacy">

View File

@ -1,10 +1,10 @@
import {Note} from '../_component/note.jsx'
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2022-02-01'),
modified: new Date('2022-02-01')
modified: new Date('2022-02-01'),
published: new Date('2022-02-01')
}
<Note type="info">

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 4
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-10-06')
}
export const navSortSelf = 4
# About

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 2
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2018-11-04'),
modified: new Date('2022-01-25')
modified: new Date('2022-01-25'),
published: new Date('2018-11-04')
}
export const navSortSelf = 2
# Contribute

View File

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import {NavGroup} from '../_component/nav.jsx'
export const navSortSelf = 6
export const info = {
author: [{name: 'MDX Contributors'}],
published: new Date('2021-11-01'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-11-01')
}
export const navSortSelf = 6
# Community
@ -12,10 +14,17 @@ These pages explain how to contribute, get help, sponsor us, share your work,
and some background information.
{
(() => {
const category = props.navTree.children.find(
item => item.name === '/community/'
)
(function () {
/**
* @typedef {import('../_component/sort.js').Item} Item
*/
/** @type {Item} */
const navTree = props.navTree
const category = navTree.children.find(function (item) {
return item.name === '/community/'
})
assert(category)
return (
<nav>

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 5
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2018-08-11'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2018-08-11')
}
export const navSortSelf = 5
# Projects

View File

@ -1,11 +1,11 @@
export const navSortSelf = 3
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
modified: new Date('2022-02-01'),
published: new Date('2021-10-06'),
modified: new Date('2022-02-01')
}
export const navSortSelf = 3
# Sponsor

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 1
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2019-07-03'),
modified: new Date('2022-02-01')
modified: new Date('2022-02-01'),
published: new Date('2019-07-03')
}
export const navSortSelf = 1
# Support

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 4
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2023-01-19')
modified: new Date('2023-01-19'),
published: new Date('2021-10-06')
}
export const navSortSelf = 4
# Extending MDX
@ -129,13 +130,13 @@ await compile(file, {remarkPlugins: [remarkGfm, remarkFrontmatter]})
await compile(file, {remarkPlugins: [[remarkGfm, {singleTilde: false}], remarkFrontmatter]})
// remark and rehype plugins:
await compile(file, {remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex]})
await compile(file, {rehypePlugins: [rehypeKatex], remarkPlugins: [remarkMath]})
// remark and rehype plugins, last w/ options:
await compile(file, {
remarkPlugins: [remarkMath],
// A plugin with options:
rehypePlugins: [[rehypeKatex, {throwOnError: true, strict: true}]]
rehypePlugins: [[rehypeKatex, {strict: true, throwOnError: true}]],
remarkPlugins: [remarkMath]
})
```

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 2
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-05'),
modified: new Date('2022-12-14')
modified: new Date('2022-12-14'),
published: new Date('2021-10-05')
}
export const navSortSelf = 2
# Getting started
@ -224,8 +225,8 @@ long.
await esbuild.build({
entryPoints: ['index.mdx'],
outfile: 'output.js',
format: 'esm',
outfile: 'output.js',
plugins: [mdx({/* jsxImportSource: …, otherOptions… */})]
})
```
@ -464,10 +465,7 @@ if youre using that, for more info.
// A Babel parser that parses MDX files with `@mdx-js/mdx` and passes any
// other things through to the normal Babel parser.
function babelParserWithMdx(value, options) {
if (
options.sourceFileName &&
/\.mdx?$/.test(path.extname(options.sourceFileName))
) {
if (options.sourceFileName && /\.mdx?$/.test(options.sourceFileName)) {
// Babel does not support async parsers, unfortunately.
return compileSync(
{value, path: options.sourceFileName},
@ -484,7 +482,7 @@ if youre using that, for more info.
// This plugin defines `'estree-to-babel'` as the compiler, which means that
// the resulting Babel tree is given back by `compileSync`.
function recmaBabel() {
Object.assign(this, {Compiler: estreeToBabel})
this.compiler = estreeToBabel
}
```
@ -579,8 +577,8 @@ integration docs](https://docs.astro.build/guides/integrations-guide/mdx/).
webpack: {
configure(webpackConfig) {
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
test: /\.mdx?$/,
loader: require.resolve('@mdx-js/loader')
loader: require.resolve('@mdx-js/loader'),
test: /\.mdx?$/
})
return webpackConfig
}
@ -673,9 +671,7 @@ well.
import {MDXProvider} from '@mdx-js/react'
import {Header} from '../components/Header.js'
const components = {
h1: Header
}
const components = {h1: Header}
export default function App({Component, pageProps}) {
return (
@ -743,7 +739,7 @@ info.
```tsx path="example.js"
import React from 'react'
import {render, Text} from 'ink'
import {Text, render} from 'ink'
import Content from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
const components = {
@ -818,7 +814,7 @@ which you might be using, for more info.
```tsx path="example.js"
import {base} from '@theme-ui/preset-base'
import {components, ThemeProvider} from 'theme-ui'
import {ThemeProvider, components} from 'theme-ui'
import Post from './post.mdx' // Assumes an integration is used to compile MDX -> JS.
<ThemeProvider theme={base}>

View File

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import {NavGroup} from '../_component/nav.jsx'
export const navSortSelf = 1
export const info = {
author: [{name: 'MDX Contributors'}],
published: new Date('2021-11-01'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-11-01')
}
export const navSortSelf = 1
# Docs
@ -14,10 +16,17 @@ to extend them.
Reading through these should give you a good understanding of MDX.
{
(() => {
const category = props.navTree.children.find(
item => item.name === '/docs/'
)
(function () {
/**
* @typedef {import('../_component/sort.js').Item} Item
*/
/** @type {Item} */
const navTree = props.navTree
const category = navTree.children.find(function (item) {
return item.name === '/docs/'
})
assert(category)
return (
<nav>

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 5
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-18'),
modified: new Date('2022-02-01')
modified: new Date('2022-02-01'),
published: new Date('2021-10-18')
}
export const navSortSelf = 5
{/* lint disable maximum-heading-length */}
@ -296,14 +297,14 @@ It occurs when there are multiple values spread into a JSX tag.
An example is:
```txt chrome=no
<div {...values, ...other} />
<div {...a, ...b} />
```
The reason for this error is that JSX only allows spreading a single value at a
time:
```mdx chrome=no
<div {...values} {...other} />
<div {...a} {...b} />
```
### ``Unexpected `$type` in code: only spread elements are supported``
@ -322,7 +323,7 @@ An example is:
The reason for this error is that JSX only allows spreading values:
```mdx chrome=no
<div {...values} />
<div {...a} />
```
### `Unexpected end of file $at, expected $expect`

View File

@ -1,12 +1,11 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 3
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-09-30'),
modified: new Date('2022-06-17')
modified: new Date('2022-06-17'),
published: new Date('2021-09-30')
}
export const navSortSelf = 3
# Using MDX
@ -31,7 +30,9 @@ An integration compiles MDX syntax to JavaScript.
Say we have an MDX document, `example.mdx`:
```mdx path="input.mdx"
export const Thing = () => <>World</>
export function Thing() {
return <>World</>
}
# Hello <Thing />
```
@ -42,7 +43,9 @@ The below might help to form a mental model:
```tsx path="output-outline.jsx"
/* @jsxRuntime automatic @jsxImportSource react */
export const Thing = () => <>World</>
export function Thing() {
return <>World</>
}
export default function MDXContent() {
return <h1>Hello <Thing /></h1>
@ -62,7 +65,9 @@ The *actual* output is:
/* @jsxRuntime automatic @jsxImportSource react */
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
export const Thing = () => _jsx(_Fragment, {children: 'World'})
export function Thing() {
return _jsx(_Fragment, {children: 'World'})
}
function _createMdxContent(props) {
const _components = {
@ -124,7 +129,9 @@ All other values are also exported.
Take this example:
```mdx path="example.mdx"
export const Thing = () => <>World</>
export function Thing() {
return <>World</>
}
# Hello <Thing />
```
@ -198,7 +205,13 @@ It can be imported from JavaScript and passed components like so:
```tsx path="example.jsx"
import Example from './example.mdx' // Assumes an integration is used to compile MDX -> JS.
<Example components={{Planet: () => <span style={{color: 'tomato'}}>Pluto</span>}} />
<Example
components={{
Planet() {
return <span style={{color: 'tomato'}}>Pluto</span>
}
}}
/>
```
You dont have to pass components.
@ -237,13 +250,23 @@ Here are some other examples of passing components:
// Map `h1` (`# heading`) to use `h2`s.
h1: 'h2',
// Rewrite `em`s (`*like so*`) to `i` with a goldenrod foreground color.
em: (props) => <i style={{color: 'goldenrod'}} {...props} />,
em(props) {
return <i style={{color: 'goldenrod'}} {...props} />
},
// Pass a layout (using the special `'wrapper'` key).
wrapper: ({components, ...rest}) => <main {...rest} />,
wrapper({components, ...rest}) {
return <main {...rest} />
},
// Pass a component.
Planet: () => 'Neptune',
Planet() {
return 'Neptune'
},
// This nested component can be used as `<theme.text>hi</theme.text>`
theme: {text: (props) => <span style={{color: 'grey'}} {...props} />}
theme: {
text(props) {
return <span style={{color: 'grey'}} {...props} />
}
}
}}
/>
```
@ -421,7 +444,13 @@ In this example the current context components are discarded:
```tsx
<>
<MDXProvider components={{h1: Component1, h2: Component2}}>
<MDXProvider components={() => ({h2: Component3, h3: Component4})}>
<MDXProvider
components={
function () {
return {h2: Component3, h3: Component4}
}
}
>
<Content />
</MDXProvider>
</MDXProvider>

View File

@ -1,13 +1,14 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 1
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'},
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'},
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2018-08-11'),
modified: new Date('2023-01-06')
modified: new Date('2023-01-06'),
published: new Date('2018-08-11')
}
export const navSortSelf = 1
# What is MDX?

View File

@ -1,11 +1,11 @@
export const navSortSelf = 5
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2022-06-17')
modified: new Date('2022-06-17'),
published: new Date('2021-10-06')
}
export const navSortSelf = 5
# Embed

View File

@ -1,11 +1,11 @@
export const navSortSelf = 2
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2022-12-14')
modified: new Date('2022-12-14'),
published: new Date('2021-10-06')
}
export const navSortSelf = 2
# Frontmatter

View File

@ -1,11 +1,11 @@
export const navSortSelf = 1
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2022-12-14')
modified: new Date('2022-12-14'),
published: new Date('2021-10-06')
}
export const navSortSelf = 1
# GitHub flavored markdown (GFM)

View File

@ -1,21 +1,29 @@
import assert from 'node:assert/strict'
import {NavGroup} from '../_component/nav.jsx'
export const navSortSelf = 2
export const info = {
author: [{name: 'MDX Contributors'}],
published: new Date('2021-11-01'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-11-01')
}
export const navSortSelf = 2
# Guides
These guides explain how to accomplish several common use cases and patterns
around MDX.
{
(() => {
const category = props.navTree.children.find(
item => item.name === '/guides/'
)
(function () {
/**
* @typedef {import('../_component/sort.js').Item} Item
*/
/** @type {Item} */
const navTree = props.navTree
const category = navTree.children.find(function (item) {
return item.name === '/guides/'
})
assert(category)
return (
<nav>

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 3
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2023-10-09')
modified: new Date('2023-10-09'),
published: new Date('2021-10-06')
}
export const navSortSelf = 3
# Math
@ -39,8 +40,8 @@ import remarkMath from 'remark-math'
console.log(
String(
await compile(await fs.readFile('example.mdx'), {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex]
rehypePlugins: [rehypeKatex],
remarkPlugins: [remarkMath]
})
)
)

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 6
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-11-13'),
modified: new Date('2021-11-14')
modified: new Date('2021-11-14'),
published: new Date('2021-11-13')
}
export const navSortSelf = 6
# MDX on demand
@ -29,11 +30,11 @@ On the server:
import {compile} from '@mdx-js/mdx'
const code = String(await compile('# hi', {
outputFormat: 'function-body',
development: false
// ^-- Generate code for production.
// `false` if you use `/jsx-runtime` on client, `true` if you use
// `/jsx-dev-runtime`.
outputFormat: 'function-body',
/* …otherOptions */
}))
// To do: send `code` to the client somehow.
@ -71,7 +72,7 @@ Some frameworks let you write the server and client code in one file, such as
Next.
```tsx path="pages/hello.js"
import {useState, useEffect, Fragment} from 'react'
import {Fragment, useEffect, useState} from 'react'
import * as runtime from 'react/jsx-runtime' // Production.
// import * as runtime from 'react/jsx-dev-runtime' // Development.
import {compile, run} from '@mdx-js/mdx'
@ -80,8 +81,8 @@ export default function Page({code}) {
const [mdxModule, setMdxModule] = useState()
const Content = mdxModule ? mdxModule.default : Fragment
useEffect(() => {
;(async () => {
useEffect(function () {
;(async function () {
setMdxModule(await run(code, runtime))
})()
}, [code])
@ -92,11 +93,11 @@ export default function Page({code}) {
export async function getStaticProps() {
const code = String(
await compile('# hi', {
outputFormat: 'function-body',
development: false,
// ^-- Generate code for production.
// `false` if you use `/jsx-runtime` on client, `true` if you use
// `/jsx-dev-runtime`.
outputFormat: 'function-body',
/* …otherOptions */
})
)

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navSortSelf = 4
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-10-06'),
modified: new Date('2022-08-27')
modified: new Date('2022-08-27'),
published: new Date('2021-10-06')
}
export const navSortSelf = 4
# Syntax highlighting
@ -105,11 +106,11 @@ function code({className, ...props}) {
<div
className="language-js"
style={{
background: '#F0F0F0',
color: '#444',
display: 'block',
overflowX: 'auto',
padding: '0.5em',
background: '#F0F0F0',
color: '#444'
padding: '0.5em'
}}
>
<code style={{whiteSpace: 'pre'}}>
@ -155,10 +156,13 @@ For example, its possible to pass that string as a prop with a rehype plugin:
```tsx path="rehype-meta-as-attributes.js"
import {visit} from 'unist-util-visit'
/** @type {import('unified').Plugin<[], import('hast').Root>} */
function rehypeMetaAsAttributes() {
return (tree) => {
visit(tree, 'element', (node) => {
/**
* @param {import('hast').Root} tree
* Tree.
*/
return function (tree) {
visit(tree, 'element', function (node) {
if (node.tagName === 'code' && node.data && node.data.meta) {
node.properties.meta = node.data.meta
}
@ -195,7 +199,7 @@ That can be achieved with the same rehype plugin as above with a different
const re = /\b([-\w]+)(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g
// …
visit(tree, 'element', (node) => {
visit(tree, 'element', function (node) {
let match
if (node.tagName === 'code' && node.data && node.data.meta) {

View File

@ -1,12 +1,12 @@
import {Note} from './_component/note.jsx'
import {Chart} from './_component/snowfall.jsx'
export {Home as default} from './_component/home.jsx'
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2017-12-23'),
modified: new Date('2022-02-01'),
published: new Date('2017-12-23'),
schemaOrg: {
"@context": "https://schema.org",
"@type": "SoftwareApplication",

View File

@ -1,12 +1,13 @@
import {Note} from '../_component/note.jsx'
export const navExclude = true
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'}
],
published: new Date('2019-04-04'),
modified: new Date('2021-10-17')
modified: new Date('2021-10-17'),
published: new Date('2019-04-04')
}
export const navExclude = true
<Note type="legacy">
**Note**: This is an old migration guide.

View File

@ -1,12 +1,11 @@
import {Note} from '../_component/note.jsx'
export const navExclude = true
export const info = {
author: [
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2022-02-01'),
modified: new Date('2022-02-01')
modified: new Date('2022-02-01'),
published: new Date('2022-02-01')
}
export const navExclude = true
# Migrating from v1 to v2
@ -58,17 +57,13 @@ that works with our previous `@mdx-js/loader` (`1.6.22`):
<summary>Expand example of a `webpack.config.js` in ESM</summary>
```tsx path="webpack.config.js"
import {URL, fileURLToPath} from 'node:url'
import {fileURLToPath} from 'node:url'
import webpack from 'webpack'
const config = {
mode: 'none',
context: fileURLToPath(new URL('src/', import.meta.url)),
entry: ['./index.js'],
output: {
path: fileURLToPath(new URL('dest/', import.meta.url)),
filename: 'bundle.js'
},
mode: 'none',
module: {
rules: [
{
@ -86,6 +81,10 @@ that works with our previous `@mdx-js/loader` (`1.6.22`):
]
}
]
},
output: {
filename: 'bundle.js',
path: fileURLToPath(new URL('dest/', import.meta.url))
}
};
@ -369,11 +368,11 @@ You can update your code as follows:
const components = {/* … */}
const value = '# hi'
export default () => (
<MDX components={components}>
export default function () {
return <MDX components={components}>
{value}
</MDX>
)
}
```
</div>
@ -389,9 +388,9 @@ You can update your code as follows:
const value = '# hi'
const {default: Content} = await evaluate(value, {...provider, ...runtime, development: false})
export default () => (
<Content components={components} />
)
export default function () {
return <Content components={components} />
}
```
</div>
</div>
@ -589,7 +588,7 @@ You can more easily embed components in MDX because blank lines are allowed:
{/* Note: `language` because theme in VS Code is broken. */}
```tsx language="mdx" chrome=no
export const Button = (props) => {
export function Button(props) {
const style = {color: 'red'}
return <button style={style} {...props} />

View File

@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import {NavGroup} from '../_component/nav.jsx'
export const navSortSelf = 3
export const info = {
author: [{name: 'MDX Contributors'}],
published: new Date('2021-11-01'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2021-11-01')
}
export const navSortSelf = 3
# Packages
@ -14,10 +16,17 @@ the remark plugin to support the MDX syntax; and several integrations with
bundlers and frontend frameworks.
{
(() => {
const category = props.navTree.children.find(
item => item.name === '/packages/'
)
(function () {
/**
* @typedef {import('../_component/sort.js').Item} Item
*/
/** @type {Item} */
const navTree = props.navTree
const category = navTree.children.find(function (item) {
return item.name === '/packages/'
})
assert(category)
return (
<nav>

View File

@ -1,12 +1,12 @@
export const navSortSelf = 5
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'},
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'},
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2021-09-13'),
modified: new Date('2023-09-29')
modified: new Date('2023-09-29'),
published: new Date('2021-09-13')
}
export const navSortSelf = 5
# Playground

View File

@ -1,12 +1,12 @@
export const navSortSelf = 4
export const info = {
author: [
{name: 'John Otander', github: 'johno', twitter: '4lpine'},
{name: 'Titus Wormer', github: 'wooorm', twitter: 'wooorm'}
{github: 'johno', name: 'John Otander', twitter: '4lpine'},
{github: 'wooorm', name: 'Titus Wormer', twitter: 'wooorm'}
],
published: new Date('2020-03-11'),
modified: new Date('2021-11-01')
modified: new Date('2021-11-01'),
published: new Date('2020-03-11')
}
export const navSortSelf = 4
# Components

1255
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,6 @@
"hast-util-to-jsx-runtime": "^2.0.0",
"hast-util-to-text": "^4.0.0",
"hastscript": "^8.0.0",
"nanoid": "^4.0.0",
"p-all": "^5.0.0",
"periscopic": "^3.0.0",
"postcss": "^8.0.0",
@ -98,7 +97,6 @@
"remark-strip-badges": "^7.0.0",
"remark-toc": "^9.0.0",
"rollup": "^4.0.0",
"source-map-support": "^0.5.0",
"type-coverage": "^2.0.0",
"typescript": "^5.0.0",
"unified": "^11.0.3",
@ -193,14 +191,24 @@
"rules": {
"react/react-in-jsx-scope": "off"
}
},
{
"files": [
"**/*.ts"
],
"rules": {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-definitions": "off"
}
}
],
"prettier": true,
"rules": {
"complexity": "off",
"n/file-extension-in-import": "off",
"react/jsx-no-bind": "off",
"react/prop-types": "off",
"complexity": "off"
"react/prop-types": "off"
}
}
}

View File

@ -1,53 +1,83 @@
/**
* @typedef {import('esbuild').Plugin} Plugin
* @typedef {import('esbuild').PluginBuild} PluginBuild
* @typedef {import('@mdx-js/mdx/lib/core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('esbuild').Message} Message
* @typedef {import('esbuild').OnLoadArgs} OnLoadArgs
* @typedef {import('esbuild').OnLoadResult} OnLoadResult
* @typedef {import('esbuild').OnResolveArgs} OnResolveArgs
* @typedef {import('esbuild').Message} Message
* @typedef {import('unist').Position} Position
* @typedef {import('vfile').VFileValue} VFileValue
* @typedef {import('esbuild').Plugin} Plugin
* @typedef {import('esbuild').PluginBuild} PluginBuild
* @typedef {import('vfile').Value} Value
* @typedef {import('vfile-message').VFileMessage} VFileMessage
* @typedef {import('@mdx-js/mdx/lib/core.js').ProcessorOptions} ProcessorOptions
*/
/**
* @typedef {ProcessorOptions & {allowDangerousRemoteMdx?: boolean | null | undefined}} Options
* @typedef EsbuildOptions
* Extra options.
* @property {boolean | null | undefined} [allowDangerousRemoteMdx=false]
* Allow remote MDX (default: `false`).
*
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
* Data passed to `onload`.
*
* @typedef LoadDataFields
* Extra fields given in `data` to `onload`.
* @property {PluginData | null | undefined} [pluginData]
* Plugin data.
*
* @typedef {EsbuildOptions & ProcessorOptions} Options
* Configuration.
*
* @typedef PluginData
* Extra data passed.
* @property {Buffer | string | null | undefined} [contents]
* File contents.
*
* @typedef State
* Info passed around.
* @property {string} doc
* File value.
* @property {string} name
* Plugin name.
* @property {string} path
* File path.
*/
import assert from 'node:assert'
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import fetch from 'node-fetch'
import {VFile} from 'vfile'
import {createFormatAwareProcessors} from '@mdx-js/mdx/lib/util/create-format-aware-processors.js'
import {extnamesToRegex} from '@mdx-js/mdx/lib/util/extnames-to-regex.js'
import {fetch} from 'undici'
import {VFile} from 'vfile'
const eol = /\r\n|\r|\n|\u2028|\u2029/g
/** @type Map<string, string> */
/** @type {Map<string, string>} */
const cache = new Map()
const name = '@mdx-js/esbuild'
const p = process
const remoteNamespace = name + '-remote'
/**
* Compile MDX w/ esbuild.
*
* @param {Options | null | undefined} [options]
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @return {Plugin}
* Plugin.
*/
export function esbuild(options) {
const {allowDangerousRemoteMdx, ...rest} = options || {}
const name = '@mdx-js/esbuild'
const remoteNamespace = name + '-remote'
const {extnames, process} = createFormatAwareProcessors(rest)
return {name, setup}
/**
* @param {PluginBuild} build
* Build.
* @returns {undefined}
* Nothing.
*/
function setup(build) {
const filter = extnamesToRegex(extnames)
@ -74,9 +104,9 @@ export function esbuild(options) {
build.onLoad({filter: /.*/, namespace: remoteNamespace}, onloadremote)
build.onLoad({filter}, onload)
/** @param {OnResolveArgs} args */
/** @param {OnResolveArgs} args */
function resolveRemoteInLocal(args) {
return {path: args.path, namespace: remoteNamespace}
return {namespace: remoteNamespace, path: args.path}
}
// Intercept all import paths inside downloaded files and resolve them against
@ -84,17 +114,19 @@ export function esbuild(options) {
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
/** @param {OnResolveArgs} args */
/** @param {OnResolveArgs} args */
function resolveInRemote(args) {
return {
path: String(new URL(args.path, args.importer)),
namespace: remoteNamespace
namespace: remoteNamespace,
path: String(new URL(args.path, args.importer))
}
}
/**
* @param {OnLoadArgs} data
* Data.
* @returns {Promise<OnLoadResult>}
* Result.
*/
async function onloadremote(data) {
const href = data.path
@ -112,23 +144,29 @@ export function esbuild(options) {
cache.set(href, contents)
}
return filter.test(href)
? onload({
suffix: '',
// Clean search and hash from URL.
path: Object.assign(new URL(href), {search: '', hash: ''}).href,
namespace: 'file',
pluginData: {contents}
})
: {contents, loader: 'js', resolveDir: p.cwd()}
if (filter.test(href)) {
// Clean search and hash from URL.
const url = new URL(href)
url.hash = ''
url.search = ''
return onload({
namespace: 'file',
path: url.href,
pluginData: {contents},
suffix: ''
})
}
return {contents, loader: 'js', resolveDir: p.cwd()}
}
/**
* @param {Omit<OnLoadArgs, 'pluginData'> & {pluginData?: {contents?: Buffer | string | null | undefined}}} data
* @param {LoadData} data
* Data.
* @returns {Promise<OnLoadResult>}
* Result.
*/
async function onload(data) {
/** @type {string} */
const doc = String(
data.pluginData &&
data.pluginData.contents !== null &&
@ -139,8 +177,8 @@ export function esbuild(options) {
/** @type {State} */
const state = {doc, name, path: data.path}
let file = new VFile({value: doc, path: data.path})
/** @type {VFileValue | undefined} */
let file = new VFile({path: data.path, value: doc})
/** @type {Value | undefined} */
let value
/** @type {Array<Error | VFileMessage>} */
let messages = []
@ -170,26 +208,22 @@ export function esbuild(options) {
return {
contents: value || '',
errors,
warnings,
resolveDir: http.test(file.path)
? p.cwd()
: path.resolve(file.cwd, file.dirname)
: path.resolve(file.cwd, file.dirname),
warnings
}
}
}
}
/**
* @typedef State
* @property {string} doc
* @property {string} name
* @property {string} path
*/
/**
* @param {State} state
* @param {Error | VFileMessage} message
* @param {Readonly<State>} state
* Info passed around.
* @param {Readonly<Error | VFileMessage>} message
* VFile message or error.
* @returns {Message}
* ESBuild message.
*/
function vfileMessageToEsbuild(state, message) {
const place = 'place' in message ? message.place : undefined
@ -217,25 +251,24 @@ function vfileMessageToEsbuild(state, message) {
const lineEnd = match ? match.index : state.doc.length
return {
pluginName: state.name,
detail: message,
id: '',
text: String(
'reason' in message
? message.reason
: /* Extra fallback to make sure weird values are definitely strings */
/* c8 ignore next */
message.stack || message
),
notes: [],
location: {
namespace: 'file',
suggestion: '',
file: state.path,
line,
column,
file: state.path,
length: Math.min(length, lineEnd),
lineText: state.doc.slice(lineStart, lineEnd)
line,
lineText: state.doc.slice(lineStart, lineEnd),
namespace: 'file',
suggestion: ''
},
detail: message
notes: [],
pluginName: state.name,
text: String(
('reason' in message ? message.reason : undefined) ||
/* c8 ignore next 2 - errors should have stacks */
message.stack ||
message
)
}
}

View File

@ -40,13 +40,12 @@
"dependencies": {
"@mdx-js/mdx": "^2.0.0",
"@types/unist": "^3.0.0",
"node-fetch": "^3.0.0",
"undici": "^5.0.0",
"vfile": "^6.0.0"
},
"peerDependencies": {
"esbuild": ">=0.11.0"
},
"devDependencies": {},
"scripts": {
"test": "npm run test-coverage",
"test-api": "node --conditions development test/index.js",

View File

@ -66,8 +66,8 @@ import mdx from '@mdx-js/esbuild'
await esbuild.build({
entryPoints: ['index.mdx'],
outfile: 'output.js',
format: 'esm',
outfile: 'output.js',
plugins: [mdx({/* Options… */})]
})
```
@ -126,8 +126,8 @@ import mdx from '@mdx-js/esbuild'
await esbuild.build({
entryPoints: ['index.mdx'],
outfile: 'output.js',
format: 'esm',
outfile: 'output.js',
plugins: [mdx({allowDangerousRemoteMdx: true, /* Other options… */})]
})
```

View File

@ -1,14 +1,20 @@
import React from 'react'
/**
* @param {Record<string, unknown>} props
* @param {JSX.IntrinsicElements['span']} props
* Props.
* @returns
* `span` element.
*/
export function Pill(props) {
return React.createElement('span', {...props, style: {color: 'red'}})
}
/**
* @param {Record<string, unknown>} props
* @param {JSX.IntrinsicElements['div']} props
* Props.
* @returns
* `div` element.
*/
export function Layout(props) {
return React.createElement('div', {...props, style: {color: 'red'}})

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,16 @@
* @todo once webpack supports ESM loaders, remove this wrapper.
*
* @this {LoaderContext}
* Context.
* @param {string} code
* Code.
* @returns {undefined}
* Nothing.
*/
module.exports = function (code) {
const callback = this.async()
// Note that `import()` caches, so this should be fast enough.
import('./lib/index.js').then((module) =>
module.loader.call(this, code, callback)
)
import('./lib/index.js').then((module) => {
return module.loader.call(this, code, callback)
})
}

View File

@ -1,3 +0,0 @@
declare function _exports(this: LoaderContext, code: string): void;
export = _exports;
export type LoaderContext = import('webpack').LoaderContext<unknown>;

View File

@ -1,31 +1,31 @@
/**
* @typedef {import('@mdx-js/mdx').CompileOptions} CompileOptions
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('vfile').VFile} VFile
* @typedef {import('vfile-message').VFileMessage} VFileMessage
* @typedef {import('webpack').LoaderContext<unknown>} LoaderContext
* @typedef {import('webpack').Compiler} WebpackCompiler
* @typedef {import('webpack').LoaderContext<unknown>} LoaderContext
*/
/**
* @typedef {Pick<CompileOptions, 'SourceMapGenerator'>} Defaults
* Defaults.
* @typedef {Omit<CompileOptions, 'SourceMapGenerator'>} Options
* Configuration.
*
* @callback Process
* Process.
* @param {VFileCompatible} vfileCompatible
* @param {Compatible} vfileCompatible
* Input.
* @returns {Promise<VFile>}
* File.
*/
import {Buffer} from 'node:buffer'
import {createHash} from 'node:crypto'
import path from 'node:path'
import {SourceMapGenerator} from 'source-map'
import {createFormatAwareProcessors} from '@mdx-js/mdx/lib/util/create-format-aware-processors.js'
const own = {}.hasOwnProperty
import {SourceMapGenerator} from 'source-map'
// Note: the cache is heavily inspired by:
// <https://github.com/TypeStrong/ts-loader/blob/5c030bf/src/instance-cache.ts>
@ -39,8 +39,13 @@ const cache = new WeakMap()
* be CommonJS.
*
* @this {LoaderContext}
* Context.
* @param {string} value
* Value.
* @param {LoaderContext['callback']} callback
* Callback.
* @returns {undefined}
* Nothing.
*/
export function loader(value, callback) {
/** @type {Defaults} */
@ -51,16 +56,16 @@ export function loader(value, callback) {
}
const config = {...defaults, ...options}
const hash = getOptionsHash(options)
// Some loaders set `undefined` (see `TypeStrong/ts-loader`).
/* c8 ignore next */
/* c8 ignore next -- some loaders set `undefined` (see `TypeStrong/ts-loader`). */
const compiler = this._compiler || marker
/* Removed option. */
/* c8 ignore next 5 */
if ('renderer' in config) {
throw new Error(
'`options.renderer` is no longer supported. Please see <https://mdxjs.com/migrating/v2/> for more information'
callback(
new Error(
'`options.renderer` is no longer supported. Please see <https://mdxjs.com/migrating/v2/> for more information'
)
)
return
}
let map = cache.get(compiler)
@ -77,14 +82,27 @@ export function loader(value, callback) {
map.set(hash, process)
}
process({value, path: this.resourcePath}).then(
(file) => {
// @ts-expect-error: `webpack` is not compiled with `exactOptionalPropertyTypes`,
// so it does not allow `file.map` to be `undefined` here.
callback(null, file.value, file.map)
const context = this.context
const filePath = this.resourcePath
process({value, path: filePath}).then(
function (file) {
callback(
undefined,
Buffer.from(file.value),
// @ts-expect-error: `webpack` is not compiled with `exactOptionalPropertyTypes`,
// so it does not allow `sourceRoot` in `file.map` to be `undefined` here.
file.map || undefined
)
},
(/** @type VFileMessage */ error) => {
const fpath = path.relative(this.context, this.resourcePath)
/**
* @param {VFileMessage} error
* Error.
* @returns {undefined}
* Nothing.
*/
function (error) {
const fpath = path.relative(context, filePath)
error.message = `${fpath}:${error.name}: ${error.message}`
callback(error)
}
@ -92,7 +110,10 @@ export function loader(value, callback) {
}
/**
* @param {Options} options
* @param {Readonly<Options>} options
* Configuration.
* @returns {string}
* Hash.
*/
function getOptionsHash(options) {
const hash = createHash('sha256')
@ -100,7 +121,7 @@ function getOptionsHash(options) {
let key
for (key in options) {
if (own.call(options, key)) {
if (Object.hasOwn(options, key)) {
const value = options[key]
if (value !== undefined) {

View File

@ -1,163 +1,228 @@
/**
* @typedef {import('mdx/types.js').MDXContent} MDXContent
* @typedef {import('preact').FunctionComponent<unknown>} PreactComponent
*/
import assert from 'node:assert/strict'
import {promises as fs} from 'node:fs'
import fs from 'node:fs/promises'
import {test} from 'node:test'
import {promisify} from 'node:util'
import {fileURLToPath} from 'node:url'
import webpack from 'webpack'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
import {h} from 'preact'
import {render} from 'preact-render-to-string'
import webpackCallback from 'webpack'
test('@mdx-js/loader', async () => {
// Setup.
const base = new URL('.', import.meta.url)
const webpack = await promisify(webpackCallback)
await fs.writeFile(new URL('webpack.mdx', base), '# Hello, {<Message />')
// Errors.
const failedResult = await promisify(webpack)({
// @ts-expect-error To do: webpack types miss support for `context`.
context: fileURLToPath(base),
entry: './webpack.mdx',
mode: 'none',
module: {
rules: [
{
test: /\.mdx$/,
use: [fileURLToPath(new URL('../index.cjs', import.meta.url))]
}
]
},
output: {
path: fileURLToPath(base),
filename: 'react.cjs',
libraryTarget: 'commonjs'
}
test('@mdx-js/loader', async function (t) {
await t.test('should expose the public api', async function () {
assert.deepEqual(Object.keys(await import('@mdx-js/loader')).sort(), [
'default'
])
})
const error = failedResult?.toJson()?.errors?.[0]
await t.test('should work', async function () {
const folderUrl = new URL('./', import.meta.url)
const mdxUrl = new URL('webpack.mdx', import.meta.url)
const jsUrl = new URL('webpack.cjs', import.meta.url)
assert.ok(error)
assert.equal(
error.message,
`Module build failed (from ../index.cjs):
await fs.writeFile(
mdxUrl,
'export function Message() { return <>World!</> }\n\n# Hello, <Message />'
)
const result = await webpack({
// @ts-expect-error: webpack types do not include `context`, which does work.
context: fileURLToPath(folderUrl),
entry: './webpack.mdx',
mode: 'none',
module: {rules: [{test: /\.mdx$/, use: ['@mdx-js/loader']}]},
output: {
filename: 'webpack.cjs',
libraryTarget: 'commonjs',
path: fileURLToPath(folderUrl)
}
})
assert(result)
assert.ok(!result.hasErrors())
// One for ESM loading CJS, one for webpack.
const mod = /** @type {{default: {default: MDXContent}}} */ (
await import(jsUrl.href + '#' + Math.random())
)
const Content = mod.default.default
assert.equal(
renderToStaticMarkup(React.createElement(Content)),
'<h1>Hello, World!</h1>'
)
const output = String(await fs.readFile(jsUrl))
assert.doesNotMatch(
output,
/react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_\d+__\.jsxDEV/
)
assert.doesNotMatch(output, /\/\/# sourceMappingURL/)
await fs.rm(mdxUrl)
await fs.rm(jsUrl)
})
await t.test(
'should support source maps and development mode',
async function () {
const folderUrl = new URL('./', import.meta.url)
const mdxUrl = new URL('webpack.mdx', import.meta.url)
const jsUrl = new URL('webpack.cjs', import.meta.url)
await fs.writeFile(
mdxUrl,
'export function Message() { return <>World!</> }\n\n# Hello, <Message />'
)
const result = await webpack({
// @ts-expect-error: webpack types do not include `context`, which does work.
context: fileURLToPath(folderUrl),
devtool: 'inline-source-map',
entry: './webpack.mdx',
mode: 'development',
module: {rules: [{test: /\.mdx$/, use: ['@mdx-js/loader']}]},
output: {
filename: 'webpack.cjs',
libraryTarget: 'commonjs',
path: fileURLToPath(folderUrl)
}
})
assert(result)
assert.ok(!result.hasErrors())
const output = String(await fs.readFile(jsUrl))
assert.match(
output,
/react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_\d+__\.jsxDEV/
)
assert.match(output, /\/\/# sourceMappingURL/)
await fs.rm(mdxUrl)
await fs.rm(jsUrl)
}
)
await t.test('should emit an error', async function () {
const folderUrl = new URL('./', import.meta.url)
const mdxUrl = new URL('webpack.mdx', import.meta.url)
await fs.writeFile(mdxUrl, '# Hello, {<Message />')
const result = await webpack({
// @ts-expect-error: webpack types do not include `context`, which does work.
context: fileURLToPath(folderUrl),
entry: './webpack.mdx',
mode: 'none',
module: {rules: [{test: /\.mdx$/, use: ['@mdx-js/loader']}]},
output: {
filename: 'webpack.cjs',
libraryTarget: 'commonjs',
path: fileURLToPath(folderUrl)
}
})
assert(result)
const errors = result.toJson().errors || []
const error = errors[0]
assert.equal(
error.message,
`Module build failed (from ../index.cjs):
webpack.mdx:1:22: Unexpected end of file in expression, expected a corresponding closing brace for \`{\``,
'received expected error message'
)
'received expected error message'
)
await fs.writeFile(
new URL('webpack.mdx', base),
'export const Message = () => <>World!</>\n\n# Hello, <Message />'
)
// React.
const reactBuild = await promisify(webpack)({
// @ts-expect-error To do: webpack types miss support for `context`.
context: fileURLToPath(base),
entry: './webpack.mdx',
mode: 'none',
module: {
rules: [
{
test: /\.mdx$/,
use: [fileURLToPath(new URL('../index.cjs', import.meta.url))]
}
]
},
output: {
path: fileURLToPath(base),
filename: 'react.cjs',
libraryTarget: 'commonjs'
}
await fs.rm(mdxUrl)
await fs.rm(new URL('webpack.cjs', folderUrl))
})
assert.ok(!reactBuild?.hasErrors())
await t.test(
'should support source maps and development mode',
async function () {
const folderUrl = new URL('./', import.meta.url)
const mdxUrl = new URL('webpack.mdx', import.meta.url)
const jsUrl = new URL('webpack.cjs', import.meta.url)
// One for ESM loading CJS, one for webpack.
const modReact = /** @type {{default: {default: MDXContent}}} */ (
// @ts-ignore file is dynamically generated
await import('./react.cjs')
)
await fs.writeFile(
mdxUrl,
'export function Message() { return <>World!</> }\n\n# Hello, <Message />'
)
assert.equal(
renderToStaticMarkup(React.createElement(modReact.default.default)),
'<h1>Hello, World!</h1>',
'should compile (react)'
)
const reactOutput = await fs.readFile(new URL('react.cjs', base), 'utf8')
assert.doesNotMatch(
reactOutput,
/react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_\d+__\.jsxDEV/,
'should infer the development option from webpacks production mode'
)
await fs.unlink(new URL('react.cjs', base))
// Preact and source maps
const preactBuild = await promisify(webpack)({
// @ts-expect-error To do: webpack types miss support for `context`.
context: fileURLToPath(base),
entry: './webpack.mdx',
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.mdx$/,
use: [
{
loader: fileURLToPath(new URL('../index.cjs', import.meta.url)),
options: {jsxImportSource: 'preact'}
}
]
const result = await webpack({
// @ts-expect-error: webpack types do not include `context`, which does work.
context: fileURLToPath(folderUrl),
devtool: 'inline-source-map',
entry: './webpack.mdx',
mode: 'development',
module: {rules: [{test: /\.mdx$/, use: ['@mdx-js/loader']}]},
output: {
filename: 'webpack.cjs',
libraryTarget: 'commonjs',
path: fileURLToPath(folderUrl)
}
]
},
output: {
path: fileURLToPath(base),
filename: 'preact.cjs',
libraryTarget: 'commonjs'
})
assert(result)
assert.ok(!result.hasErrors())
const output = String(await fs.readFile(jsUrl))
assert.match(
output,
/react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_\d+__\.jsxDEV/
)
assert.match(output, /\/\/# sourceMappingURL/)
await fs.rm(mdxUrl)
await fs.rm(jsUrl)
}
)
await t.test('should throw for `renderer`', async function () {
const folderUrl = new URL('./', import.meta.url)
const mdxUrl = new URL('webpack.mdx', import.meta.url)
await fs.writeFile(mdxUrl, '\ta')
const result = await webpack({
// @ts-expect-error: webpack types do not include `context`, which does work.
context: fileURLToPath(folderUrl),
entry: './webpack.mdx',
mode: 'none',
module: {
rules: [
{
test: /\.mdx$/,
use: [{loader: '@mdx-js/loader', options: {renderer: '?'}}]
}
]
},
output: {
filename: 'webpack.cjs',
libraryTarget: 'commonjs',
path: fileURLToPath(folderUrl)
}
})
assert(result)
const errors = result.toJson().errors || []
const error = errors[0]
assert.match(error.message, /`options.renderer` is no longer supported/)
await fs.rm(mdxUrl)
await fs.rm(new URL('webpack.cjs', folderUrl))
})
assert.ok(!preactBuild?.hasErrors())
// One for ESM loading CJS, one for webpack.
const modPreact = /** @type {{default: {default: PreactComponent}}} */ (
// @ts-ignore file is dynamically generated.
await import('./preact.cjs')
)
assert.equal(
// To do: fix?
// @ts-expect-error: preact + react conflict.
render(h(modPreact.default.default, {})),
'<h1>Hello, World!</h1>',
'should compile (preact)'
)
const preactOutput = await fs.readFile(new URL('preact.cjs', base), 'utf8')
assert.match(
preactOutput,
/preact_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_\d+__\.jsxDEV/,
'should infer the development option from webpacks development mode'
)
assert.match(
preactOutput,
/\/\/# sourceMappingURL/,
'should add a source map if requested'
)
await fs.unlink(new URL('preact.cjs', base))
// Clean.
await fs.unlink(new URL('webpack.mdx', base))
})

View File

@ -1,11 +1,11 @@
/**
* @typedef {import('./lib/core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('./lib/compile.js').CompileOptions} CompileOptions
* @typedef {import('./lib/core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('./lib/evaluate.js').EvaluateOptions} EvaluateOptions
*/
export {createProcessor} from './lib/core.js'
export {compile, compileSync} from './lib/compile.js'
export {createProcessor} from './lib/core.js'
export {evaluate, evaluateSync} from './lib/evaluate.js'
export {run, runSync} from './lib/run.js'
export {nodeTypes} from './lib/node-types.js'
export {run, runSync} from './lib/run.js'

View File

@ -1,8 +1,8 @@
/**
* @typedef {import('vfile').VFile} VFile
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('./core.js').PluginOptions} PluginOptions
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('./core.js').BaseProcessorOptions} BaseProcessorOptions
* @typedef {import('./core.js').PluginOptions} PluginOptions
*/
/**
@ -11,24 +11,24 @@
*
* @typedef ExtraOptions
* Extra configuration.
* @property {'detect' | 'mdx' | 'md' | null | undefined} [format='detect']
* Format of `file`.
* @property {'detect' | 'md' | 'mdx' | null | undefined} [format='detect']
* Format of `file` (default: `'detect'`).
*
* @typedef {CoreProcessorOptions & PluginOptions & ExtraOptions} CompileOptions
* @typedef {CoreProcessorOptions & ExtraOptions & PluginOptions} CompileOptions
* Configuration.
*/
import {createProcessor} from './core.js'
import {resolveFileAndOptions} from './util/resolve-file-and-options.js'
import {createProcessor} from './core.js'
/**
* Compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible
* @param {Readonly<Compatible>} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {CompileOptions | null | undefined} [compileOptions]
* Compile configuration.
* @param {Readonly<CompileOptions> | null | undefined} [compileOptions]
* Compile configuration (optional).
* @return {Promise<VFile>}
* File.
*/
@ -40,11 +40,11 @@ export function compile(vfileCompatible, compileOptions) {
/**
* Synchronously compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible
* @param {Readonly<Compatible>} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {CompileOptions | null | undefined} [compileOptions]
* Compile configuration.
* @param {Readonly<CompileOptions> | null | undefined} [compileOptions]
* Compile configuration (optional).
* @return {VFile}
* File.
*/

View File

@ -1,59 +1,62 @@
/**
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('mdast').Root} Root
* @typedef {import('remark-rehype').Options} RemarkRehypeOptions
* @typedef {import('unified').PluggableList} PluggableList
* @typedef {import('unified').Processor} Processor
* @typedef {import('unified').Processor<Root, Program, Program, Program, string>} Processor
* @typedef {import('./plugin/recma-document.js').Options} RecmaDocumentOptions
* @typedef {import('./plugin/recma-jsx-rewrite.js').Options} RecmaJsxRewriteOptions
* @typedef {import('./plugin/recma-stringify.js').Options} RecmaStringifyOptions
* @typedef {import('./plugin/rehype-recma.js').Options} RehypeRecmaOptions
* @typedef {import('./plugin/recma-document.js').RecmaDocumentOptions} RecmaDocumentOptions
* @typedef {import('./plugin/recma-stringify.js').RecmaStringifyOptions} RecmaStringifyOptions
* @typedef {import('./plugin/recma-jsx-rewrite.js').RecmaJsxRewriteOptions} RecmaJsxRewriteOptions
*/
/**
* @typedef BaseProcessorOptions
* Base configuration.
* @property {boolean | null | undefined} [jsx=false]
* Whether to keep JSX.
* @property {'mdx' | 'md' | null | undefined} [format='mdx']
* Format of the files to be processed.
* Whether to keep JSX (default: `false`).
* @property {'md' | 'mdx' | null | undefined} [format='mdx']
* Format of the files to be processed (default: `'mdx'`).
* @property {'function-body' | 'program'} [outputFormat='program']
* Whether to compile to a whole program or a function body..
* @property {Array<string> | null | undefined} [mdExtensions]
* Extensions (with `.`) for markdown.
* @property {Array<string> | null | undefined} [mdxExtensions]
* Extensions (with `.`) for MDX.
* Whether to compile to a whole program or a function body (default:
* `'program'`).
* @property {ReadonlyArray<string> | null | undefined} [mdExtensions]
* Extensions (with `.`) for markdown (default: `['.md', '.markdown', …]`).
* @property {ReadonlyArray<string> | null | undefined} [mdxExtensions]
* Extensions (with `.`) for MDX (default: `['.mdx']`).
* @property {PluggableList | null | undefined} [recmaPlugins]
* List of recma (esast, JavaScript) plugins.
* List of recma (esast, JavaScript) plugins (optional).
* @property {PluggableList | null | undefined} [remarkPlugins]
* List of remark (mdast, markdown) plugins.
* List of remark (mdast, markdown) plugins (optional).
* @property {PluggableList | null | undefined} [rehypePlugins]
* List of rehype (hast, HTML) plugins.
* @property {RemarkRehypeOptions | null | undefined} [remarkRehypeOptions]
* Options to pass through to `remark-rehype`.
* List of rehype (hast, HTML) plugins (optional).
* @property {Readonly<RemarkRehypeOptions> | null | undefined} [remarkRehypeOptions]
* Options to pass through to `remark-rehype` (optional).
*
* @typedef {Omit<RehypeRecmaOptions & RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
* @typedef {Omit<RecmaDocumentOptions & RecmaJsxRewriteOptions & RecmaStringifyOptions & RehypeRecmaOptions, 'outputFormat'>} PluginOptions
* Configuration for internal plugins.
*
* @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
* Configuration for processor.
*/
import {unified} from 'unified'
import remarkMdx from 'remark-mdx'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {recmaJsxBuild} from './plugin/recma-jsx-build.js'
import {unified} from 'unified'
import {recmaDocument} from './plugin/recma-document.js'
import {recmaJsxBuild} from './plugin/recma-jsx-build.js'
import {recmaJsxRewrite} from './plugin/recma-jsx-rewrite.js'
import {recmaStringify} from './plugin/recma-stringify.js'
import {rehypeRecma} from './plugin/rehype-recma.js'
import {rehypeRemoveRaw} from './plugin/rehype-remove-raw.js'
import {remarkMarkAndUnravel} from './plugin/remark-mark-and-unravel.js'
import {nodeTypes} from './node-types.js'
import {development as defaultDevelopment} from './condition.js'
import {nodeTypes} from './node-types.js'
const removedOptions = [
'filepath',
'compilers',
'filepath',
'hastPlugins',
'mdPlugins',
'skipExport',
@ -67,8 +70,8 @@ const removedOptions = [
* 2. Transform through remark (mdast), rehype (hast), and recma (esast)
* 3. Serialize as JavaScript
*
* @param {ProcessorOptions | null | undefined} [options]
* Configuration.
* @param {Readonly<ProcessorOptions> | null | undefined} [options]
* Configuration (optional).
* @return {Processor}
* Processor.
*/
@ -105,8 +108,8 @@ export function createProcessor(options) {
}
}
// @ts-expect-error runtime exception for disallowed field here, which is
// allowed in `compile`.
// @ts-expect-error: throw an error for a runtime value which is not allowed
// by the types.
if (format === 'detect') {
throw new Error(
"Incorrect `format: 'detect'`: `createProcessor` can support either `md` or `mdx`; it does not support detecting the format"
@ -120,14 +123,12 @@ export function createProcessor(options) {
}
const extraNodeTypes = remarkRehypeOptions
? /* c8 ignore next */
remarkRehypeOptions.passThrough || []
? remarkRehypeOptions.passThrough || []
: []
pipeline
.use(remarkMarkAndUnravel)
.use(remarkPlugins || [])
// @ts-expect-error: to do: fix types of `passThrough`.
.use(remarkRehype, {
...remarkRehypeOptions,
allowDangerousHtml: true,
@ -154,6 +155,6 @@ export function createProcessor(options) {
pipeline.use(recmaStringify, {SourceMapGenerator}).use(recmaPlugins || [])
// @ts-expect-error: to do: fix types.
// @ts-expect-error: we added plugins with if-checks, which TS doesnt get.
return pipeline
}

View File

@ -1,22 +1,22 @@
/**
* @typedef {import('mdx/types.js').MDXModule} ExportMap
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('mdx/types.js').MDXModule} MDXModule
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('./util/resolve-evaluate-options.js').EvaluateOptions} EvaluateOptions
*/
import {resolveEvaluateOptions} from './util/resolve-evaluate-options.js'
import {compile, compileSync} from './compile.js'
import {run, runSync} from './run.js'
import {resolveEvaluateOptions} from './util/resolve-evaluate-options.js'
/**
* Evaluate MDX.
*
* @param {VFileCompatible} vfileCompatible
* @param {Readonly<Compatible>} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {EvaluateOptions} evaluateOptions
* @param {Readonly<EvaluateOptions>} evaluateOptions
* Configuration for evaluation.
* @return {Promise<ExportMap>}
* @return {Promise<MDXModule>}
* Export map.
*/
export async function evaluate(vfileCompatible, evaluateOptions) {
@ -27,12 +27,12 @@ export async function evaluate(vfileCompatible, evaluateOptions) {
/**
* Synchronously evaluate MDX.
*
* @param {VFileCompatible} vfileCompatible
* @param {Readonly<Compatible>} vfileCompatible
* MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be
* given to `vfile`).
* @param {EvaluateOptions} evaluateOptions
* @param {Readonly<EvaluateOptions>} evaluateOptions
* Configuration for evaluation.
* @return {ExportMap}
* @return {MDXModule}
* Export map.
*/
export function evaluateSync(vfileCompatible, evaluateOptions) {

View File

@ -2,10 +2,10 @@
* List of node types made by `mdast-util-mdx`, which have to be passed
* through untouched from the mdast tree to the hast tree.
*/
export const nodeTypes = [
export const nodeTypes = /** @type {const} */ ([
'mdxFlowExpression',
'mdxJsxFlowElement',
'mdxJsxTextElement',
'mdxTextExpression',
'mdxjsEsm'
]
])

View File

@ -10,8 +10,9 @@
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportExpression} ImportExpression
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').Literal} Literal
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').JSXFragment} JSXFragment
* @typedef {import('estree-jsx').Literal} Literal
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Program} Program
@ -20,50 +21,58 @@
* @typedef {import('estree-jsx').SpreadElement} SpreadElement
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
* @typedef {import('vfile').VFile} VFile
*/
/**
* @typedef RecmaDocumentOptions
* @typedef Options
* Configuration for internal plugin `recma-document`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to use either `import` and `export` statements to get the runtime
* (and optionally provider) and export the content, or get values from
* `arguments` and return things.
* `arguments` and return things (default: `'program'`).
* @property {boolean | null | undefined} [useDynamicImport=false]
* Whether to keep `import` (and `export … from`) statements or compile them
* to dynamic `import()` instead.
* to dynamic `import()` instead (default: `false`).
* @property {string | null | undefined} [baseUrl]
* Resolve `import`s (and `export … from`, and `import.meta.url`) relative to
* this URL.
* this URL (optional).
* @property {string | null | undefined} [pragma='React.createElement']
* Pragma for JSX (used in classic runtime).
* Pragma for JSX (used in classic runtime) (default:
* `'React.createElement'`).
* @property {string | null | undefined} [pragmaFrag='React.Fragment']
* Pragma for JSX fragments (used in classic runtime).
* Pragma for JSX fragments (used in classic runtime) (default:
* `'React.Fragment'`).
* @property {string | null | undefined} [pragmaImportSource='react']
* Where to import the identifier of `pragma` from (used in classic runtime).
* Where to import the identifier of `pragma` from (used in classic runtime)
* (default: `'react'`).
* @property {string | null | undefined} [jsxImportSource='react']
* Place to import automatic JSX runtimes from (used in automatic runtime).
* Place to import automatic JSX runtimes from (used in automatic runtime)
* (default: `'react'`).
* @property {'automatic' | 'classic' | null | undefined} [jsxRuntime='automatic']
* JSX runtime to use.
* JSX runtime to use (default: `'automatic'`).
*/
import {analyze} from 'periscopic'
import {stringifyPosition} from 'unist-util-stringify-position'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {ok as assert} from 'devlop'
import {walk} from 'estree-walker'
import {analyze} from 'periscopic'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {stringifyPosition} from 'unist-util-stringify-position'
import {create} from '../util/estree-util-create.js'
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
import {isDeclaration} from '../util/estree-util-is-declaration.js'
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
/**
* A plugin to wrap the estree in `MDXContent`.
* Wrap the estree in `MDXContent`.
*
* @type {import('unified').Plugin<[RecmaDocumentOptions | null | undefined] | [], Program>}
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export function recmaDocument(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
/* c8 ignore next -- always given in `@mdx-js/mdx` */
const options_ = options || {}
const baseUrl = options_.baseUrl || undefined
const useDynamicImport = options_.useDynamicImport || undefined
@ -76,7 +85,15 @@ export function recmaDocument(options) {
const jsxImportSource = options_.jsxImportSource || 'react'
const jsxRuntime = options_.jsxRuntime || 'automatic'
return (tree, file) => {
/**
* @param {Program} tree
* Tree.
* @param {VFile} file
* File.
* @returns {undefined}
* Nothing.
*/
return function (tree, file) {
/** @type {Array<[string, string] | string>} */
const exportedIdentifiers = []
/** @type {Array<Directive | ModuleDeclaration | Statement>} */
@ -91,10 +108,6 @@ export function recmaDocument(options) {
/** @type {Node} */
let child
// Patch missing comments, which types say could occur.
/* c8 ignore next */
if (!tree.comments) tree.comments = []
if (jsxRuntime) {
pragmas.push('@jsxRuntime ' + jsxRuntime)
}
@ -111,6 +124,9 @@ export function recmaDocument(options) {
pragmas.push('@jsxFrag ' + pragmaFrag)
}
/* c8 ignore next -- comments can be missing in the types, we always have it. */
if (!tree.comments) tree.comments = []
if (pragmas.length > 0) {
tree.comments.unshift({type: 'Block', value: pragmas.join(' ')})
}
@ -172,10 +188,11 @@ export function recmaDocument(options) {
// export {a, b as c} from 'd'
// ```
else if (child.type === 'ExportNamedDeclaration' && child.source) {
// Cast because always simple.
const source = /** @type {SimpleLiteral} */ (child.source)
// Remove `default` or `as default`, but not `default as`, specifier.
child.specifiers = child.specifiers.filter((specifier) => {
child.specifiers = child.specifiers.filter(function (specifier) {
if (specifier.exported.name === 'default') {
if (layout) {
file.fail(
@ -247,18 +264,16 @@ export function recmaDocument(options) {
handleEsm(child)
} else if (
child.type === 'ExpressionStatement' &&
// @ts-expect-error types are wrong: `JSXFragment` is an `Expression`.
(child.expression.type === 'JSXFragment' ||
child.expression.type === 'JSXElement')
(child.expression.type === 'JSXElement' ||
// @ts-expect-error: `estree-jsx` does not register `JSXFragment` as an expression.
child.expression.type === 'JSXFragment')
) {
content = true
replacement.push(...createMdxContent(child.expression, Boolean(layout)))
// The following catch-all branch is because plugins mightve added
// other things.
} else {
// This catch-all branch is because plugins might add other things.
// Normally, we only have import/export/jsx, but just add whatevers
// there.
/* c8 ignore next 3 */
} else {
replacement.push(child)
}
}
@ -279,15 +294,23 @@ export function recmaDocument(options) {
...Array.from({length: exportAllCount}).map(
/**
* @param {undefined} _
* Nothing.
* @param {number} index
* Index.
* @returns {SpreadElement}
* Node.
*/
(_, index) => ({
type: 'SpreadElement',
argument: {type: 'Identifier', name: '_exportAll' + (index + 1)}
})
function (_, index) {
return {
type: 'SpreadElement',
argument: {
type: 'Identifier',
name: '_exportAll' + (index + 1)
}
}
}
),
...exportedIdentifiers.map((d) => {
...exportedIdentifiers.map(function (d) {
/** @type {Property} */
const prop = {
type: 'Property',
@ -341,7 +364,9 @@ export function recmaDocument(options) {
/**
* @param {ExportAllDeclaration | ExportNamedDeclaration} node
* @returns {void}
* Export node.
* @returns {undefined}
* Nothing.
*/
function handleExport(node) {
if (node.type === 'ExportNamedDeclaration') {
@ -370,7 +395,9 @@ export function recmaDocument(options) {
/**
* @param {ExportAllDeclaration | ExportNamedDeclaration | ImportDeclaration} node
* @returns {void}
* Export or import node.
* @returns {undefined}
* Nothing.
*/
function handleEsm(node) {
// Rewrite the source of the `import` / `export … from`.
@ -420,11 +447,8 @@ export function recmaDocument(options) {
)
}
// Just for types.
/* c8 ignore next 3 */
if (!node.source) {
throw new Error('Expected `node.source` to be defined')
}
// We always have a source, but types say they can be missing.
assert(node.source, 'expected `node.source` to be defined')
// ```
// import 'a'
@ -471,14 +495,16 @@ export function recmaDocument(options) {
} else {
/** @type {Array<VariableDeclarator>} */
const declarators = node.specifiers
.filter(
(specifier) => specifier.local.name !== specifier.exported.name
)
.map((specifier) => ({
type: 'VariableDeclarator',
id: specifier.exported,
init: specifier.local
}))
.filter(function (specifier) {
return specifier.local.name !== specifier.exported.name
})
.map(function (specifier) {
return {
type: 'VariableDeclarator',
id: specifier.exported,
init: specifier.local
}
})
if (declarators.length > 0) {
replace = {
@ -499,9 +525,12 @@ export function recmaDocument(options) {
}
/**
* @param {Expression | undefined} [content]
* @param {boolean | undefined} [hasInternalLayout]
* @param {Readonly<Expression> | undefined} [content]
* Content.
* @param {boolean | undefined} [hasInternalLayout=false]
* Whether theres an internal layout (default: `false`).
* @returns {Array<FunctionDeclaration>}
* Functions.
*/
function createMdxContent(content, hasInternalLayout) {
/** @type {JSXElement} */
@ -558,19 +587,18 @@ export function recmaDocument(options) {
}
}
let argument = content || {type: 'Literal', value: null}
let argument =
// Cast because TS otherwise does not think `JSXFragment`s are expressions.
/** @type {Readonly<Expression> | Readonly<JSXFragment>} */ (
content || {type: 'Identifier', name: 'undefined'}
)
// Unwrap a fragment of a single element.
if (
argument &&
// @ts-expect-error: fine.
argument.type === 'JSXFragment' &&
// @ts-expect-error: fine.
argument.children.length === 1 &&
// @ts-expect-error: fine.
argument.children[0].type === 'JSXElement'
) {
// @ts-expect-error: fine.
argument = argument.children[0]
}
@ -581,7 +609,14 @@ export function recmaDocument(options) {
params: [{type: 'Identifier', name: 'props'}],
body: {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument}]
body: [
{
type: 'ReturnStatement',
// Cast because TS doesnt think `JSXFragment` is an expression.
// eslint-disable-next-line object-shorthand
argument: /** @type {Expression} */ (argument)
}
]
}
},
{

View File

@ -1,6 +1,7 @@
/**
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-util-build-jsx').Options} BuildJsxOptions
* @typedef {import('vfile').VFile} VFile
*/
/**
@ -8,9 +9,10 @@
* Configuration for internal plugin `recma-jsx-build`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to keep the import of the automatic runtime or get it from
* `arguments[0]` instead.
* `arguments[0]` instead (default: `'program'`).
*
* @typedef {BuildJsxOptions & ExtraOptions} RecmaJsxBuildOptions
* @typedef {BuildJsxOptions & ExtraOptions} Options
* Options.
*/
import {buildJsx} from 'estree-util-build-jsx'
@ -21,14 +23,24 @@ import {toIdOrMemberExpression} from '../util/estree-util-to-id-or-member-expres
* A plugin to build JSX into function calls.
* `estree-util-build-jsx` does all the work for us!
*
* @type {import('unified').Plugin<[RecmaJsxBuildOptions | null | undefined] | [], Program>}
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export function recmaJsxBuild(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
/* c8 ignore next -- always given in `@mdx-js/mdx` */
const {development, outputFormat} = options || {}
return (tree, file) => {
/**
* @param {Program} tree
* Tree.
* @param {VFile} file
* File.
* @returns {undefined}
* Nothing.
*/
return function (tree, file) {
buildJsx(tree, {development, filePath: file.history[0]})
// When compiling to a function body, replace the import that was just

View File

@ -13,11 +13,13 @@
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*
* @typedef {import('periscopic').Scope & {node: Node}} Scope
* @typedef {import('periscopic').Scope} PeriscopicScope
*
* @typedef {import('vfile').VFile} VFile
*/
/**
* @typedef RecmaJsxRewriteOptions
* @typedef Options
* Configuration for internal plugin `recma-jsx-rewrite`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to use an import statement or `arguments[0]` to get the provider.
@ -32,28 +34,36 @@
* The default can be set to `true` in Node.js through environment variables:
* set `NODE_ENV=development`.
*
* @typedef {PeriscopicScope & {node: Node}} Scope
* Scope (with a `node`).
*
* @typedef StackEntry
* @property {Array<string>} objects
* Entry.
* @property {Array<string>} components
* @property {Array<string>} tags
* @property {Record<string, {node: JSXElement, component: boolean}>} references
* Used components.
* @property {Map<string, string>} idToInvalidComponentName
* @property {EstreeFunction} node
* Map of JSX identifiers which cannot be used as JS identifiers, to valid JS identifiers.
* @property {Readonly<EstreeFunction>} node
* Function.
* @property {Array<string>} objects
* Identifiers of used objects (such as `x` in `x.y`).
* @property {Record<string, {node: Readonly<JSXElement>, component: boolean}>} references
* Map of JSX identifiers for components and objects, to where they were first used.
* @property {Array<string>} tags
* Tag names.
*/
import {stringifyPosition} from 'unist-util-stringify-position'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
import {walk} from 'estree-walker'
import {analyze} from 'periscopic'
import {stringifyPosition} from 'unist-util-stringify-position'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
import {toBinaryAddition} from '../util/estree-util-to-binary-addition.js'
import {
toIdOrMemberExpression,
toJsxIdOrMemberExpression
} from '../util/estree-util-to-id-or-member-expression.js'
import {toBinaryAddition} from '../util/estree-util-to-binary-addition.js'
const own = {}.hasOwnProperty
/**
* A plugin that rewrites JSX in functions to accept components as
@ -62,14 +72,24 @@ const own = {}.hasOwnProperty
* It also makes sure that any undefined components are defined: either from
* received components or as a function that throws an error.
*
* @type {import('unified').Plugin<[RecmaJsxRewriteOptions | null | undefined] | [], Program>}
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export function recmaJsxRewrite(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
const {development, providerImportSource, outputFormat} = options || {}
/* c8 ignore next -- always given in `@mdx-js/mdx` */
const {development, outputFormat, providerImportSource} = options || {}
return (tree, file) => {
/**
* @param {Program} tree
* Tree.
* @param {VFile} file
* File.
* @returns {undefined}
* Nothing.
*/
return function (tree, file) {
// Find everything thats defined in the top-level scope.
const scopeInfo = analyze(tree)
/** @type {Array<StackEntry>} */
@ -81,6 +101,7 @@ export function recmaJsxRewrite(options) {
walk(tree, {
enter(node) {
// Cast because we match `node`.
const newScope = /** @type {Scope | undefined} */ (
scopeInfo.map.get(node)
)
@ -91,12 +112,12 @@ export function recmaJsxRewrite(options) {
node.type === 'ArrowFunctionExpression'
) {
fnStack.push({
objects: [],
components: [],
tags: [],
references: {},
idToInvalidComponentName: new Map(),
node
node,
objects: [],
references: {},
tags: []
})
// MDXContent only ever contains MDXLayout
@ -143,7 +164,8 @@ export function recmaJsxRewrite(options) {
const isInScope = inScope(currentScope, id)
if (!own.call(fnScope.references, fullId)) {
if (!Object.hasOwn(fnScope.references, fullId)) {
// Cast because we match `node`.
const parentScope = /** @type {Scope | undefined} */ (
currentScope.parent
)
@ -155,7 +177,7 @@ export function recmaJsxRewrite(options) {
parentScope.node.type === 'FunctionDeclaration' &&
isNamedFunction(parentScope.node, '_createMdxContent'))
) {
fnScope.references[fullId] = {node, component: true}
fnScope.references[fullId] = {component: true, node}
}
}
@ -177,18 +199,18 @@ export function recmaJsxRewrite(options) {
if (!inScope(currentScope, id)) {
// No need to add an error for an undefined layout — we use an
// `if` later.
if (id !== 'MDXLayout' && !own.call(fnScope.references, id)) {
fnScope.references[id] = {node, component: true}
if (
id !== 'MDXLayout' &&
!Object.hasOwn(fnScope.references, id)
) {
fnScope.references[id] = {component: true, node}
}
if (!fnScope.components.includes(id)) {
fnScope.components.push(id)
}
}
}
// @ts-expect-error Allow fields passed through from mdast through hast to
// esast.
else if (node.data && node.data._mdxExplicitJsx) {
} else if (node.data && node.data._mdxExplicitJsx) {
// Do not turn explicit JSX into components from `_components`.
// As in, a given `h1` component is used for `# heading` (next case),
// but not for `<h1>heading</h1>`.
@ -199,7 +221,7 @@ export function recmaJsxRewrite(options) {
fnScope.tags.push(id)
}
/** @type {Array<string | number>} */
/** @type {Array<number | string>} */
let jsxIdExpression = ['_components', id]
if (isIdentifierName(id) === false) {
let invalidComponentName =
@ -233,8 +255,8 @@ export function recmaJsxRewrite(options) {
const declarations = []
if (currentScope && currentScope.node === node) {
// @ts-expect-error: `node`s were patched when entering.
currentScope = currentScope.parent
// Cast to patch our `node`.
currentScope = /** @type {Scope} */ (currentScope.parent)
}
if (
@ -333,18 +355,20 @@ export function recmaJsxRewrite(options) {
if (actual.length > 0) {
componentsPattern = {
type: 'ObjectPattern',
properties: actual.map((name) => ({
type: 'Property',
kind: 'init',
key: {
type: 'Identifier',
name: name === 'MDXLayout' ? 'wrapper' : name
},
value: {type: 'Identifier', name},
method: false,
shorthand: name !== 'MDXLayout',
computed: false
}))
properties: actual.map(function (name) {
return {
type: 'Property',
kind: 'init',
key: {
type: 'Identifier',
name: name === 'MDXLayout' ? 'wrapper' : name
},
value: {type: 'Identifier', name},
method: false,
shorthand: name !== 'MDXLayout',
computed: false
}
})
}
}
@ -360,7 +384,9 @@ export function recmaJsxRewrite(options) {
if (isNamedFunction(scope.node, '_createMdxContent')) {
for (const [id, componentName] of [
...scope.idToInvalidComponentName
].sort(([a], [b]) => a.localeCompare(b))) {
].sort(function ([a], [b]) {
return a.localeCompare(b)
})) {
// For JSX IDs that cant be represented as JavaScript IDs (as in,
// those with dashes, such as `custom-element`), generate a
// separate variable that is a valid JS ID (such as `_component0`),
@ -405,15 +431,15 @@ export function recmaJsxRewrite(options) {
// Add partials (so for `x.y.z` itd generate `x` and `x.y` too).
for (key in scope.references) {
if (own.call(scope.references, key)) {
if (Object.hasOwn(scope.references, key)) {
const parts = key.split('.')
let index = 0
while (++index < parts.length) {
const partial = parts.slice(0, index).join('.')
if (!own.call(scope.references, partial)) {
if (!Object.hasOwn(scope.references, partial)) {
scope.references[partial] = {
node: scope.references[key].node,
component: false
component: false,
node: scope.references[key].node
}
}
}
@ -455,7 +481,7 @@ export function recmaJsxRewrite(options) {
optional: false
}
},
alternate: null
alternate: undefined
})
}
@ -553,8 +579,11 @@ export function recmaJsxRewrite(options) {
/**
* @param {string} providerImportSource
* @param {RecmaJsxRewriteOptions['outputFormat']} outputFormat
* @returns {Statement | ModuleDeclaration}
* Provider source.
* @param {Options['outputFormat']} outputFormat
* Format.
* @returns {ModuleDeclaration | Statement}
* Node.
*/
function createImportProvider(providerImportSource, outputFormat) {
/** @type {Array<ImportSpecifier>} */
@ -583,18 +612,24 @@ function createImportProvider(providerImportSource, outputFormat) {
}
/**
* @param {EstreeFunction} node
* @param {Readonly<EstreeFunction>} node
* Node.
* @param {string} name
* Name.
* @returns {boolean}
* Whether `node` is a named function with `name`.
*/
function isNamedFunction(node, name) {
return Boolean(node && 'id' in node && node.id && node.id.name === name)
}
/**
* @param {Scope} scope
* @param {Readonly<Scope>} scope
* Scope.
* @param {string} id
* Identifier.
* @returns {boolean}
* Whether `id` is in `scope`.
*/
function inScope(scope, id) {
/** @type {Scope | undefined} */
@ -605,8 +640,10 @@ function inScope(scope, id) {
return true
}
// @ts-expect-error: `node`s have been added when entering.
currentScope = currentScope.parent
// Cast to patch our `node`.
currentScope = /** @type {Scope | undefined} */ (
currentScope.parent || undefined
)
}
return false

View File

@ -1,37 +1,48 @@
/**
* @typedef {import('estree-jsx').Program} Program
* @typedef {typeof import('source-map').SourceMapGenerator} SourceMapGenerator
*
* @typedef RecmaStringifyOptions
* @typedef {import('unified').Processor<undefined, undefined, undefined, Program, string>} Processor
* @typedef {import('vfile').VFile} VFile
*/
/**
* @typedef Options
* Configuration for internal plugin `recma-stringify`.
* @property {SourceMapGenerator | null | undefined} [SourceMapGenerator]
* Generate a source map by passing a `SourceMapGenerator` from `source-map`
* in.
* in (optional).
*/
import {toJs, jsx} from 'estree-util-to-js'
import {jsx, toJs} from 'estree-util-to-js'
/**
* A plugin that adds an esast compiler: a small wrapper around `astring` to add
* support for serializing JSX.
* Serialize an esast (estree) program to JavaScript.
*
* @this {import('unified').Processor}
* @type {import('unified').Plugin<[RecmaStringifyOptions | null | undefined] | [], Program, string>}
* @type {import('unified').Plugin<[Options | null | undefined] | [], Program, string>}
* Plugin.
*/
export function recmaStringify(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
// @ts-expect-error: TS is wrong about `this`.
// eslint-disable-next-line unicorn/no-this-assignment
const self = /** @type {Processor} */ (this)
/* c8 ignore next -- always given in `@mdx-js/mdx` */
const {SourceMapGenerator} = options || {}
// @ts-expect-error: to do: improve types.
this.compiler = compiler
self.compiler = compiler
/** @type {import('unified').Compiler<Program, string>} */
/**
* @param {Program} tree
* Tree.
* @param {VFile} file
* File.
* @returns {string}
* JavaScript.
*/
function compiler(tree, file) {
const result = SourceMapGenerator
? toJs(tree, {
filePath: file.path || 'unknown.mdx',
SourceMapGenerator,
filePath: file.path || 'unknown.mdx',
handlers: jsx
})
: toJs(tree, {handlers: jsx})

View File

@ -10,24 +10,24 @@
* HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
*
* @typedef Options
* Configuration for internal plugin `rehype-recma`.
* @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
* Specify casing to use for attribute names (default: `'react'`).
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
* @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
* Specify casing to use for property names in `style` objects (default: `'dom'`).
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
*
* @typedef {'css' | 'dom'} StylePropertyNameCase
* Casing to use for property names in `style` objects.
*
* CSS casing is for example `background-color` and `-webkit-line-clamp`.
* DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
*
* @typedef Options
* Configuration for internal plugin `rehype-recma`.
* @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
* Specify casing to use for attribute names.
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
* @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
* Specify casing to use for property names in `style` objects.
*
* This casing is used for hast elements, not for embedded MDX JSX nodes
* (components that someone authored manually).
*/
import {toEstree} from 'hast-util-to-estree'
@ -36,8 +36,19 @@ import {toEstree} from 'hast-util-to-estree'
* A plugin to transform an HTML (hast) tree to a JS (estree).
* `hast-util-to-estree` does all the work for us!
*
* @type {import('unified').Plugin<[Options | null | undefined] | [], Root, Program>}
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export function rehypeRecma(options) {
return (tree) => toEstree(tree, options)
/**
* @param {Root} tree
* Tree (hast).
* @returns {Program}
* Program (esast).
*/
return function (tree) {
return toEstree(tree, options)
}
}

View File

@ -1,6 +1,5 @@
/**
* @typedef {import('hast').Root} Root
* @typedef {import('mdast-util-to-hast')} DoNotRemoveUsedToAddRawToNodeType
*/
import {visit} from 'unist-util-visit'
@ -11,11 +10,18 @@ import {visit} from 'unist-util-visit'
* This is needed if the format is `md` and `rehype-raw` was not used to parse
* dangerous HTML into nodes.
*
* @type {import('unified').Plugin<[], Root>}
* @returns
* Transform.
*/
export function rehypeRemoveRaw() {
return (tree) => {
visit(tree, 'raw', (_, index, parent) => {
/**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
visit(tree, 'raw', function (_, index, parent) {
if (parent && typeof index === 'number') {
parent.children.splice(index, 1)
return index

View File

@ -1,8 +1,6 @@
/**
* @typedef {import('mdast').Content} Content
* @typedef {import('mdast').Root} Root
*
* @typedef {import('remark-mdx')} DoNotTouchAsThisImportItIncludesMdxInTree
* @typedef {import('mdast').RootContent} RootContent
*/
import {visit} from 'unist-util-visit'
@ -14,11 +12,18 @@ import {visit} from 'unist-util-visit'
* It also marks JSX as being explicitly JSX, so when a user passes a `h1`
* component, it is used for `# heading` but not for `<h1>heading</h1>`.
*
* @type {import('unified').Plugin<[], Root>}
* @returns
* Transform.
*/
export function remarkMarkAndUnravel() {
return (tree) => {
visit(tree, (node, index, parent) => {
/**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
visit(tree, function (node, index, parent) {
let offset = -1
let all = true
let oneOrMore = false
@ -36,6 +41,7 @@ export function remarkMarkAndUnravel() {
oneOrMore = true
} else if (
child.type === 'text' &&
// To do: use `collapse-whitespace`?
/^[\t\r\n ]+$/.test(String(child.value))
) {
// Empty.
@ -48,19 +54,19 @@ export function remarkMarkAndUnravel() {
if (all && oneOrMore) {
offset = -1
/** @type {Array<Content>} */
/** @type {Array<RootContent>} */
const newChildren = []
while (++offset < children.length) {
const child = children[offset]
if (child.type === 'mdxJsxTextElement') {
// @ts-expect-error: content model is fine.
// @ts-expect-error: mutate because it is faster; content model is fine.
child.type = 'mdxJsxFlowElement'
}
if (child.type === 'mdxTextExpression') {
// @ts-expect-error: content model is fine.
// @ts-expect-error: mutate because it is faster; content model is fine.
child.type = 'mdxFlowExpression'
}
@ -84,7 +90,6 @@ export function remarkMarkAndUnravel() {
node.type === 'mdxJsxTextElement'
) {
const data = node.data || (node.data = {})
// @ts-expect-error: to do: type.
data._mdxExplicitJsx = true
}
})

46
packages/mdx/lib/types.d.ts vendored Normal file
View File

@ -0,0 +1,46 @@
import type {Data as UnistData} from 'unist'
interface EsastData extends UnistData {
/**
* Whether a node was authored as explicit JSX (`<h1>`) or as implicitly
* turned into JSX (`# hi`).
*
* Registered by `@mdx-js/mdx/lib/types.d.ts`.
*/
_mdxExplicitJsx?: boolean | null | undefined
}
// Register data on `estree`.
declare module 'estree' {
interface BaseNode {
/**
* Extra unist data passed through from mdast through hast to esast.
*
* Registered by `@mdx-js/mdx/lib/types.d.ts`.
*/
data?: EsastData | undefined
}
}
// Register data on `mdast`.
declare module 'mdast-util-mdx-jsx' {
interface MdxJsxFlowElementData {
/**
* Whether a node was authored as explicit JSX (`<h1>`) or as implicitly
* turned into JSX (`# hi`).
*
* Registered by `@mdx-js/mdx/lib/types.d.ts`.
*/
_mdxExplicitJsx?: boolean | null | undefined
}
interface MdxJsxTextElementData {
/**
* Whether a node was authored as explicit JSX (`<h1>`) or as implicitly
* turned into JSX (`# hi`).
*
* Registered by `@mdx-js/mdx/lib/types.d.ts`.
*/
_mdxExplicitJsx?: boolean | null | undefined
}
}

View File

@ -1,7 +1,9 @@
/**
* @typedef {import('unified').Processor} Processor
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('mdast').Root} Root
* @typedef {import('unified').Processor<Root, Program, Program, Program, string>} Processor
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('vfile').VFile} VFile
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('../compile.js').CompileOptions} CompileOptions
*/
@ -12,9 +14,9 @@ import {resolveFileAndOptions} from './resolve-file-and-options.js'
/**
* Create smart processors to handle different formats.
*
* @param {CompileOptions | null | undefined} [compileOptions]
* configuration.
* @return {{extnames: Array<string>, process: process, processSync: processSync}}
* @param {Readonly<CompileOptions> | null | undefined} [compileOptions]
* Configuration (optional).
* @return {{extnames: ReadonlyArray<string>, process: process, processSync: processSync}}
* Smart processor.
*/
export function createFormatAwareProcessors(compileOptions) {
@ -32,7 +34,7 @@ export function createFormatAwareProcessors(compileOptions) {
? mdExtensions
: compileOptions_.format === 'mdx'
? mdxExtensions
: mdExtensions.concat(mdxExtensions),
: [...mdExtensions, ...mdxExtensions],
process,
processSync
}
@ -40,7 +42,7 @@ export function createFormatAwareProcessors(compileOptions) {
/**
* Smart processor.
*
* @param {VFileCompatible} vfileCompatible
* @param {Compatible} vfileCompatible
* MDX or markdown document.
* @return {Promise<VFile>}
* File.
@ -53,14 +55,11 @@ export function createFormatAwareProcessors(compileOptions) {
/**
* Sync smart processor.
*
* @param {VFileCompatible} vfileCompatible
* @param {Compatible} vfileCompatible
* MDX or markdown document.
* @return {VFile}
* File.
*/
// C8 does not cover `.cjs` files (this is only used for the require hook,
// which has to be CJS).
/* c8 ignore next 4 */
function processSync(vfileCompatible) {
const {file, processor} = split(vfileCompatible)
return processor.processSync(file)
@ -72,7 +71,7 @@ export function createFormatAwareProcessors(compileOptions) {
* This caches processors (one for markdown and one for MDX) so that they do
* not have to be reconstructed for each file.
*
* @param {VFileCompatible} vfileCompatible
* @param {Compatible} vfileCompatible
* MDX or markdown document.
* @return {{file: VFile, processor: Processor}}
* File and corresponding processor.

View File

@ -2,12 +2,15 @@
* @typedef {import('estree-jsx').Node} Node
*/
// Fix to show references to above types in VS Code.
''
/**
* @param {Node} from
* @param {Readonly<Node>} from
* Node to take from.
* @param {Node} to
* Node to add to.
* @returns {void}
* @returns {undefined}
* Nothing.
*/
export function create(from, to) {

View File

@ -3,6 +3,8 @@
* @typedef {import('estree-jsx').Expression} Expression
*/
import {ok as assert} from 'devlop'
/**
* Turn a declaration into an expression.
*
@ -10,7 +12,7 @@
* because currently were using this utility for export default declarations,
* which cant contain variable declarations.
*
* @param {Declaration} declaration
* @param {Readonly<Declaration>} declaration
* Declaration.
* @returns {Expression}
* Expression.
@ -20,13 +22,8 @@ export function declarationToExpression(declaration) {
return {...declaration, type: 'FunctionExpression'}
}
if (declaration.type === 'ClassDeclaration') {
return {...declaration, type: 'ClassExpression'}
/* Internal utility so the next shouldnt happen or a maintainer is making a
* mistake. */
/* c8 ignore next 4 */
}
// Probably `VariableDeclaration`.
throw new Error('Cannot turn `' + declaration.type + '` into an expression')
// This is currently an internal utility so the next shouldnt happen or a
// maintainer is making a mistake.
assert(declaration.type === 'ClassDeclaration', 'unexpected node type')
return {...declaration, type: 'ClassExpression'}
}

View File

@ -1,12 +1,15 @@
/**
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Declaration} Declaration
* @typedef {import('estree-jsx').Node} Node
*/
// Fix to show references to above types in VS Code.
''
/**
* Check if `node` is a declaration.
*
* @param {Node} node
* @param {Readonly<Node>} node
* Node to check.
* @returns {node is Declaration}
* Whether `node` is a declaration.

View File

@ -12,15 +12,18 @@
import {create} from './estree-util-create.js'
/**
* @param {Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier>} specifiers
* @param {Expression} init
* @param {ReadonlyArray<Readonly<ExportSpecifier> | Readonly<ImportDefaultSpecifier> | Readonly<ImportNamespaceSpecifier> | Readonly<ImportSpecifier>>} specifiers
* Specifiers.
* @param {Readonly<Expression>} init
* Initializer.
* @returns {Array<VariableDeclarator>}
* Declarations.
*/
export function specifiersToDeclarations(specifiers, init) {
let index = -1
/** @type {Array<VariableDeclarator>} */
const declarations = []
/** @type {Array<ImportSpecifier | ImportDefaultSpecifier | ExportSpecifier>} */
/** @type {Array<ExportSpecifier | ImportDefaultSpecifier | ImportSpecifier>} */
const otherSpecifiers = []
// Can only be one according to JS syntax.
/** @type {ImportNamespaceSpecifier | undefined} */
@ -51,7 +54,7 @@ export function specifiersToDeclarations(specifiers, init) {
type: 'VariableDeclarator',
id: {
type: 'ObjectPattern',
properties: otherSpecifiers.map((specifier) => {
properties: otherSpecifiers.map(function (specifier) {
/** @type {Identifier} */
let key =
specifier.type === 'ImportSpecifier'

View File

@ -2,8 +2,13 @@
* @typedef {import('estree-jsx').Expression} Expression
*/
import {ok as assert} from 'devlop'
/**
* @param {Array<Expression>} expressions
* @param {ReadonlyArray<Expression>} expressions
* Expressions.
* @returns {Expression}
* Addition.
*/
export function toBinaryAddition(expressions) {
let index = -1
@ -15,9 +20,6 @@ export function toBinaryAddition(expressions) {
left = left ? {type: 'BinaryExpression', left, operator: '+', right} : right
}
// Just for types.
/* c8 ignore next */
if (!left) throw new Error('Expected non-empty `expressions` to be passed')
assert(left, 'expected non-empty `expressions` to be passed')
return left
}

View File

@ -6,103 +6,66 @@
* @typedef {import('estree-jsx').MemberExpression} MemberExpression
*/
import {
start as esStart,
cont as esCont,
name as isIdentifierName
} from 'estree-util-is-identifier-name'
export const toIdOrMemberExpression = toIdOrMemberExpressionFactory(
'Identifier',
'MemberExpression',
isIdentifierName
)
export const toJsxIdOrMemberExpression =
// @ts-expect-error: fine
/** @type {(ids: Array<string | number>) => JSXIdentifier | JSXMemberExpression)} */
(
toIdOrMemberExpressionFactory(
'JSXIdentifier',
'JSXMemberExpression',
isJsxIdentifierName
)
)
import {ok as assert} from 'devlop'
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
/**
* @param {string} idType
* @param {string} memberType
* @param {(value: string) => boolean} isIdentifier
* @param {ReadonlyArray<number | string>} ids
* Identifiers (example: `['list', 0]).
* @returns {Identifier | MemberExpression}
* Identifier or member expression.
*/
function toIdOrMemberExpressionFactory(idType, memberType, isIdentifier) {
return toIdOrMemberExpression
/**
* @param {Array<string | number>} ids
* @returns {Identifier | MemberExpression}
*/
function toIdOrMemberExpression(ids) {
let index = -1
/** @type {Identifier | Literal | MemberExpression | undefined} */
let object
while (++index < ids.length) {
const name = ids[index]
const valid = typeof name === 'string' && isIdentifier(name)
// A value of `asd.123` could be turned into `asd['123']` in the JS form,
// but JSX does not have a form for it, so throw.
/* c8 ignore next 3 */
if (idType === 'JSXIdentifier' && !valid) {
throw new Error('Cannot turn `' + name + '` into a JSX identifier')
}
/** @type {Identifier | Literal} */
// @ts-expect-error: JSX is fine.
const id = valid ? {type: idType, name} : {type: 'Literal', value: name}
// @ts-expect-error: JSX is fine.
object = object
? {
type: memberType,
object,
property: id,
computed: id.type === 'Literal',
optional: false
}
: id
}
// Just for types.
/* c8 ignore next 3 */
if (!object) throw new Error('Expected non-empty `ids` to be passed')
if (object.type === 'Literal')
throw new Error('Expected identifier as left-most value')
return object
}
}
/**
* Checks if the given string is a valid JSX identifier name.
* @param {string} name
*/
function isJsxIdentifierName(name) {
export function toIdOrMemberExpression(ids) {
let index = -1
/** @type {Identifier | Literal | MemberExpression | undefined} */
let object
while (++index < name.length) {
// We currently receive valid input, but this catches bugs and is needed
// when externalized.
/* c8 ignore next */
if (!(index ? jsxCont : esStart)(name.charCodeAt(index))) return false
while (++index < ids.length) {
const name = ids[index]
/** @type {Identifier | Literal} */
const id =
typeof name === 'string' && isIdentifierName(name)
? {type: 'Identifier', name}
: {type: 'Literal', value: name}
object = object
? {
type: 'MemberExpression',
object,
property: id,
computed: id.type === 'Literal',
optional: false
}
: id
}
// `false` if `name` is empty.
return index > 0
assert(object, 'expected non-empty `ids` to be passed')
assert(object.type !== 'Literal', 'expected identifier as left-most value')
return object
}
/**
* Checks if the given character code can continue a JSX identifier.
* @param {number} code
* @param {ReadonlyArray<number | string>} ids
* Identifiers (example: `['list', 0]).
* @returns {JSXIdentifier | JSXMemberExpression}
* Identifier or member expression.
*/
function jsxCont(code) {
return code === 45 /* `-` */ || esCont(code)
export function toJsxIdOrMemberExpression(ids) {
let index = -1
/** @type {JSXIdentifier | JSXMemberExpression | undefined} */
let object
while (++index < ids.length) {
const name = ids[index]
assert(
typeof name === 'string' && isIdentifierName(name, {jsx: true}),
'expected valid jsx identifier, not `' + name + '`'
)
/** @type {JSXIdentifier} */
const id = {type: 'JSXIdentifier', name}
object = object ? {type: 'JSXMemberExpression', object, property: id} : id
}
assert(object, 'expected non-empty `ids` to be passed')
return object
}

View File

@ -1,13 +1,19 @@
/**
* Utility to turn a list of extnames (*with* dots) into an expression.
* Turn a list of extnames (*with* dots) into an expression.
*
* @param {Array<string>} extnames
* @param {ReadonlyArray<string>} extnames
* List of extnames.
* @returns {RegExp}
* Regex matching them.
*/
export function extnamesToRegex(extnames) {
return new RegExp(
'\\.(' + extnames.map((d) => d.slice(1)).join('|') + ')([?#]|$)'
'\\.(' +
extnames
.map(function (d) {
return d.slice(1)
})
.join('|') +
')([?#]|$)'
)
}

View File

@ -1,5 +1,6 @@
import markdownExtensions from 'markdown-extensions'
export const md = markdownExtensions.map(function (d) {
return '.' + d
})
export const mdx = ['.mdx']
/** @type {Array<string>} */
export const md = markdownExtensions.map((d) => '.' + d)

View File

@ -10,7 +10,7 @@
* @typedef {EvaluateProcessorOptions & RunnerOptions} EvaluateOptions
* Configuration for evaluation.
*
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource' | 'outputFormat'> } EvaluateProcessorOptions
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'outputFormat' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource'> } EvaluateProcessorOptions
* Compile configuration without JSX options for evaluation.
*
* @typedef {unknown} Fragment
@ -114,14 +114,19 @@
* Primitive property value and `Style` map.
*/
// Fix to show references to above types in VS Code.
''
/**
* Split compiletime options from runtime options.
*
* @param {EvaluateOptions | null | undefined} options
* @param {Readonly<EvaluateOptions> | null | undefined} options
* Configuration.
* @returns {{compiletime: ProcessorOptions, runtime: RunnerOptions}}
* Split options.
*/
export function resolveEvaluateOptions(options) {
const {development, Fragment, jsx, jsxs, jsxDEV, useMDXComponents, ...rest} =
const {Fragment, development, jsx, jsxDEV, jsxs, useMDXComponents, ...rest} =
options || {}
if (!Fragment) throw new Error('Expected `Fragment` given to `evaluate`')
@ -139,6 +144,6 @@ export function resolveEvaluateOptions(options) {
outputFormat: 'function-body',
providerImportSource: useMDXComponents ? '#' : undefined
},
runtime: {Fragment, jsx, jsxs, jsxDEV, useMDXComponents}
runtime: {Fragment, jsx, jsxDEV, jsxs, useMDXComponents}
}
}

View File

@ -1,7 +1,7 @@
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('vfile').Compatible} Compatible
* @typedef {import('../compile.js').CompileOptions} CompileOptions
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
*/
import {VFile} from 'vfile'
@ -11,9 +11,12 @@ import {md} from './extnames.js'
* Create a file and options from a given `vfileCompatible` and options that
* might contain `format: 'detect'`.
*
* @param {VFileCompatible} vfileCompatible
* @param {CompileOptions | null | undefined} [options]
* @param {Readonly<Compatible>} vfileCompatible
* File.
* @param {Readonly<CompileOptions> | null | undefined} [options]
* Configuration (optional).
* @returns {{file: VFile, options: ProcessorOptions}}
* File and options.
*/
export function resolveFileAndOptions(vfileCompatible, options) {
const file = looksLikeAVFile(vfileCompatible)
@ -35,8 +38,10 @@ export function resolveFileAndOptions(vfileCompatible, options) {
}
/**
* @param {VFileCompatible | null | undefined} [value]
* @param {Readonly<Compatible> | null | undefined} [value]
* Thing.
* @returns {value is VFile}
* Check.
*/
function looksLikeAVFile(value) {
return Boolean(

View File

@ -49,6 +49,7 @@
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/mdx": "^2.0.0",
"devlop": "^1.0.0",
"estree-util-build-jsx": "^3.0.0",
"estree-util-is-identifier-name": "^3.0.0",
"estree-util-to-js": "^2.0.0",
@ -69,10 +70,22 @@
"devDependencies": {},
"scripts": {
"test": "npm run test-coverage",
"test-api": "node --conditions development test/index.js",
"test-api": "node --conditions development --enable-source-maps test/index.js",
"test-coverage": "c8 --100 --reporter lcov npm run test-api"
},
"xo": {
"overrides": [
{
"files": [
"**/*.ts"
],
"rules": {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-type-definitions": "off"
}
}
],
"prettier": true,
"rules": {
"complexity": "off",

View File

@ -67,7 +67,9 @@ yarn add @mdx-js/mdx
Say we have an MDX document, `example.mdx`:
```mdx
export const Thing = () => <>World!</>
export function Thing() {
return <>World!</>
}
# Hello, <Thing />
```
@ -89,7 +91,9 @@ Yields roughly:
/* @jsxRuntime automatic @jsxImportSource react */
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
export const Thing = () => _jsx(_Fragment, {children: 'World'})
export function Thing() {
return _jsx(_Fragment, {children: 'World'})
}
function _createMdxContent(props) {
const _components = {
@ -177,12 +181,12 @@ List of [rehype plugins][rehype-plugins], presets, and pairs.
import rehypeKatex from 'rehype-katex' // Render math with KaTeX.
import remarkMath from 'remark-math' // Support math like `$so$`.
await compile(file, {remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex]})
await compile(file, {rehypePlugins: [rehypeKatex], remarkPlugins: [remarkMath]})
await compile(file, {
remarkPlugins: [remarkMath],
// A plugin with options:
rehypePlugins: [[rehypeKatex, {throwOnError: true, strict: true}]]
rehypePlugins: [[rehypeKatex, {strict: true, throwOnError: true}]],
remarkPlugins: [remarkMath]
})
```
@ -241,11 +245,11 @@ So pass a full vfile (with `path`) or an object with a path.
```tsx
compile({value: '…'}) // Seen as MDX
compile({value: '…'}, {format: 'md'}) // Seen as markdown
compile({value: '…', path: 'readme.md'}) // Seen as markdown
compile({path: 'readme.md', value: '…'}) // Seen as markdown
// Please do not use `.md` for MDX as other tools wont know how to handle it.
compile({value: '…', path: 'readme.md'}, {format: 'mdx'}) // Seen as MDX
compile({value: '…', path: 'readme.md'}, {mdExtensions: []}) // Seen as MDX
compile({path: 'readme.md', value: '…'}, {format: 'mdx'}) // Seen as MDX
compile({path: 'readme.md', value: '…'}, {mdExtensions: []}) // Seen as MDX
```
</details>
@ -500,11 +504,11 @@ console.log(file.map)
```tsx
{
version: 3,
sources: ['example.mdx'],
names: ['Thing'],
file: 'example.mdx',
mappings: ';;aAAaA,QAAQ;YAAQ;;;;;;;;iBAE3B',
file: 'example.mdx'
names: ['Thing'],
sources: ['example.mdx'],
version: 3
}
```
@ -533,7 +537,9 @@ compile(file, {providerImportSource: '@mdx-js/react'})
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
+import {useMDXComponents as _provideComponents} from '@mdx-js/react'
export const Thing = () => _jsx(_Fragment, {children: 'World!'})
export function Thing() {
return _jsx(_Fragment, {children: 'World'})
}
function _createMdxContent(props) {
const _components = {
@ -579,8 +585,10 @@ compile(file, {jsx: true})
/* @jsxRuntime automatic @jsxImportSource react */
-import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
-export const Thing = () => _jsx(_Fragment, {children: 'World!'})
+export const Thing = () => <>World!</>
export function Thing() {
- return _jsx(_Fragment, {children: 'World'})
+ return <>World!</>
}
function _createMdxContent(props) {
const _components = {
@ -626,8 +634,10 @@ compile(file, {jsxRuntime: 'classic'})
+/* @jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment */
+import React from 'react'
-export const Thing = () => _jsx(_Fragment, {children: 'World!'})
+export const Thing = () => React.createElement(React.Fragment, null, 'World!')
export function Thing() {
- return _jsx(_Fragment, {children: 'World'})
+ return React.createElement(React.Fragment, null, 'World!')
}
```
@ -690,8 +700,10 @@ compile(file, {
+/* @jsxRuntime classic @jsx preact.createElement @jsxFrag preact.Fragment */
+import preact from 'preact/compat'
-export const Thing = () => React.createElement(React.Fragment, null, 'World!')
+export const Thing = () => preact.createElement(preact.Fragment, null, 'World!')
export function Thing() {
- return React.createElement(React.Fragment, null, 'World!')
+ return preact.createElement(preact.Fragment, null, 'World!')
}
```
@ -812,9 +824,9 @@ They come from an automatic JSX runtime that you must import yourself.
import * as runtime from 'react/jsx-runtime'
const {default: Content} = await evaluate('# hi', {
...runtime,
development: false,
...otherOptions,
development: false
...runtime
})
```
@ -832,10 +844,10 @@ import * as provider from '@mdx-js/react'
import * as runtime from 'react/jsx-runtime'
const {default: Content} = await evaluate('# hi', {
...provider,
...runtime,
development: false,
...otherOptions,
development: false
...provider,
...runtime
})
```
@ -919,8 +931,8 @@ On the server:
import {compile} from '@mdx-js/mdx'
const code = String(await compile('# hi', {
outputFormat: 'function-body',
development: false
development: false,
outputFormat: 'function-body'
}))
// To do: send `code` to the client somehow.
```

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,20 @@
import React from 'react'
/**
* @param {Record<string, unknown>} props
* @param {Readonly<JSX.IntrinsicElements['span']>} props
* Props
* @returns
* `span` element.
*/
export function Pill(props) {
return React.createElement('span', {...props, style: {color: 'red'}})
}
/**
* @param {Record<string, unknown>} props
* @param {Readonly<JSX.IntrinsicElements['div']>} props
* Props
* @returns
* `div` element.
*/
export function Layout(props) {
return React.createElement('div', {...props, style: {color: 'red'}})

View File

@ -1,7 +1,19 @@
/**
* Number.
*/
export const number = 3.14
/**
* Object.
*/
export const object = {a: 1, b: 2}
/**
* Array.
*/
export const array = [1, 2]
/**
* Number.
*/
export default 2 * number

View File

@ -0,0 +1,42 @@
/**
* @typedef {import('mdx/types.js').MDXContent} MDXContent
* @typedef {import('mdx/types.js').MDXModule} MDXModule
* @typedef {import('vfile').Compatible} Compatible
*/
import fs from 'node:fs/promises'
/**
* @param {Readonly<Compatible>} input
* MDX document.
* @return {Promise<MDXContent>}
* MDX content.
*/
export async function run(input) {
const mod = await runWhole(input)
return mod.default
}
/**
*
* @param {Readonly<Compatible>} input
* MDX document.
* @return {Promise<MDXModule>}
* MDX module.
*/
export async function runWhole(input) {
const fileName = 'fixture-' + Math.random() + '.js'
const fileUrl = new URL(fileName, import.meta.url)
const doc = String(input)
await fs.writeFile(fileUrl, doc)
try {
/** @type {MDXModule} */
return await import(fileUrl.href)
} finally {
// This is not a bug: the `finally` runs after the whole `try` block, but
// before the `return`.
await fs.rm(fileUrl)
}
}

17
packages/mdx/test/core.js Normal file
View File

@ -0,0 +1,17 @@
import assert from 'node:assert/strict'
import {test} from 'node:test'
test('@mdx-js/mdx: core', async function (t) {
await t.test('should expose the public api', async function () {
assert.deepEqual(Object.keys(await import('@mdx-js/mdx')).sort(), [
'compile',
'compileSync',
'createProcessor',
'evaluate',
'evaluateSync',
'nodeTypes',
'run',
'runSync'
])
})
})

View File

@ -5,72 +5,91 @@
import assert from 'node:assert/strict'
import {test} from 'node:test'
import {evaluate, evaluateSync, compile} from '@mdx-js/mdx'
import * as provider from '@mdx-js/react'
import {renderToStaticMarkup} from 'react-dom/server'
import * as runtime_ from 'react/jsx-runtime'
import * as devRuntime_ from 'react/jsx-dev-runtime'
import React from 'react'
import * as provider from '../../react/index.js'
import {evaluate, evaluateSync, compile} from '../index.js'
/** @type {RuntimeProduction} */
// @ts-expect-error: types are wrong.
// @ts-expect-error: the automatic react runtime is untyped.
const runtime = runtime_
/** @type {RuntimeDevelopment} */
// @ts-expect-error: types are wrong.
// @ts-expect-error: the automatic dev react runtime is untyped.
const devRuntime = devRuntime_
test('evaluate', async (t) => {
await t.test('should throw on missing `Fragment`', async () => {
assert.throws(() => {
// @ts-expect-error: missing required arguments
evaluateSync('a')
}, /Expected `Fragment` given to `evaluate`/)
test('@mdx-js/mdx: evaluate', async function (t) {
await t.test('should throw on missing `Fragment`', async function () {
try {
// @ts-expect-error: check how the runtime handles missing options.
await evaluate('a')
assert.fail()
} catch (error) {
assert.match(String(error), /Expected `Fragment` given to `evaluate`/)
}
})
await t.test('should throw on missing `jsx`', async () => {
assert.throws(() => {
evaluateSync('a', {Fragment: runtime.Fragment})
}, /Expected `jsx` given to `evaluate`/)
await t.test('should throw on missing `jsx`', async function () {
try {
await evaluate('a', {Fragment: runtime.Fragment})
assert.fail()
} catch (error) {
assert.match(String(error), /Expected `jsx` given to `evaluate`/)
}
})
await t.test('should throw on missing `jsxs`', async () => {
assert.throws(() => {
evaluateSync('a', {Fragment: runtime.Fragment, jsx: runtime.jsx})
}, /Expected `jsxs` given to `evaluate`/)
await t.test('should throw on missing `jsxs`', async function () {
try {
await evaluate('a', {Fragment: runtime.Fragment, jsx: runtime.jsx})
assert.fail()
} catch (error) {
assert.match(String(error), /Expected `jsxs` given to `evaluate`/)
}
})
await t.test('should throw on missing `jsxDEV` in dev mode', async () => {
assert.throws(() => {
evaluateSync('a', {Fragment: runtime.Fragment, development: true})
}, /Expected `jsxDEV` given to `evaluate`/)
})
await t.test(
'should throw on missing `jsxDEV` in dev mode',
async function () {
try {
await evaluate('a', {Fragment: runtime.Fragment, development: true})
assert.fail()
} catch (error) {
assert.match(String(error), /Expected `jsxDEV` given to `evaluate`/)
}
}
)
await t.test('should evaluate', async () => {
await t.test('should evaluate', async function () {
const mod = await evaluate('# hi!', runtime)
assert.equal(
renderToStaticMarkup(React.createElement(mod.default)),
'<h1>hi!</h1>'
)
})
await t.test('should evaluate (sync)', async () => {
await t.test('should evaluate (sync)', async function () {
const mod = evaluateSync('# hi!', runtime)
assert.equal(
renderToStaticMarkup(React.createElement(mod.default)),
'<h1>hi!</h1>'
)
})
await t.test('should evaluate (sync)', async () => {
await t.test('should evaluate (dev)', async function () {
const mod = await evaluate('# hi dev!', {development: true, ...devRuntime})
assert.equal(
renderToStaticMarkup(React.createElement(mod.default)),
'<h1>hi dev!</h1>'
)
})
await t.test('should evaluate (sync)', async () => {
const mod = evaluateSync('# hi dev!', {development: true, ...devRuntime})
await t.test('should evaluate (async, dev)', async function () {
const mod = await evaluate('# hi dev!', {development: true, ...devRuntime})
assert.equal(
renderToStaticMarkup(React.createElement(mod.default)),
'<h1>hi dev!</h1>'
@ -79,7 +98,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `import` of a relative url w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'import {number} from "./context/data.js"\n\n{number}',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
@ -94,7 +113,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `import` of a full url w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'import {number} from "' +
new URL('context/data.js', import.meta.url) +
@ -111,7 +130,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `import` w/o specifiers w/ `useDynamicImport`',
async () => {
async function () {
assert.match(
String(
await compile('import "a"', {
@ -126,7 +145,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `import` w/ 0 specifiers w/ `useDynamicImport`',
async () => {
async function () {
assert.match(
String(
await compile('import {} from "a"', {
@ -141,7 +160,7 @@ test('evaluate', async (t) => {
await t.test(
'should support a namespace import w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'import * as x from "./context/components.js"\n\n<x.Pill>Hi!</x.Pill>',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
@ -156,7 +175,7 @@ test('evaluate', async (t) => {
await t.test(
'should support a namespace import and a bare specifier w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'import Div, * as x from "./context/components.js"\n\n<x.Pill>a</x.Pill> and <Div>b</Div>',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
@ -169,7 +188,7 @@ test('evaluate', async (t) => {
}
)
await t.test('should support an `export`', async () => {
await t.test('should support an `export`', async function () {
const mod = await evaluate('export const a = 1\n\n{a}', runtime)
assert.equal(renderToStaticMarkup(React.createElement(mod.default)), '1')
@ -177,7 +196,7 @@ test('evaluate', async (t) => {
assert.equal(mod.a, 1)
})
await t.test('should support an `export function`', async () => {
await t.test('should support an `export function`', async function () {
const mod = await evaluate(
'export function a() { return 1 }\n\n{a()}',
runtime
@ -190,7 +209,7 @@ test('evaluate', async (t) => {
assert.equal(mod.a(), 1)
})
await t.test('should support an `export class`', async () => {
await t.test('should support an `export class`', async function () {
const mod = await evaluate(
'export class A { constructor() { this.b = 1 } }\n\n{new A().b}',
runtime
@ -203,7 +222,7 @@ test('evaluate', async (t) => {
assert.equal(a.b, 1)
})
await t.test('should support an `export as`', async () => {
await t.test('should support an `export as`', async function () {
const mod = await evaluate(
'export const a = 1\nexport {a as b}\n\n{a}',
runtime
@ -215,7 +234,7 @@ test('evaluate', async (t) => {
assert.equal(mod.b, 1, 'should support an `export as` (3)')
})
await t.test('should support an `export default`', async () => {
await t.test('should support an `export default`', async function () {
const mod = await evaluate(
'export default function Layout({components, ...props}) { return <section {...props} /> }\n\na',
runtime
@ -227,15 +246,21 @@ test('evaluate', async (t) => {
)
})
await t.test('should throw on an export from', () => {
assert.throws(() => {
evaluateSync('export {a} from "b"', runtime)
}, /Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/)
await t.test('should throw on an export from', async function () {
try {
await evaluate('export {a} from "b"', runtime)
assert.fail()
} catch (error) {
assert.match(
String(error),
/Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/
)
}
})
await t.test(
'should support an `export from` w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate('export {number} from "./context/data.js"', {
baseUrl: import.meta.url,
useDynamicImport: true,
@ -246,18 +271,21 @@ test('evaluate', async (t) => {
}
)
await t.test('should support an `export` w/ `useDynamicImport`', async () => {
const mod = await evaluate(
'import {number} from "./context/data.js"\nexport {number}',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
)
await t.test(
'should support an `export` w/ `useDynamicImport`',
async function () {
const mod = await evaluate(
'import {number} from "./context/data.js"\nexport {number}',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
)
assert.equal(mod.number, 3.14)
})
assert.equal(mod.number, 3.14)
}
)
await t.test(
'should support an `export as from` w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'export {number as data} from "./context/data.js"',
{
@ -273,7 +301,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `export default as from` w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'export {default as data} from "./context/data.js"',
{
@ -289,7 +317,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `export all from` w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate('export * from "./context/data.js"', {
baseUrl: import.meta.url,
useDynamicImport: true,
@ -305,7 +333,7 @@ test('evaluate', async (t) => {
await t.test(
'should support an `export all from`, but prefer explicit exports, w/ `useDynamicImport`',
async () => {
async function () {
const mod = await evaluate(
'export {default as number} from "./context/data.js"\nexport * from "./context/data.js"',
{baseUrl: import.meta.url, useDynamicImport: true, ...runtime}
@ -313,10 +341,7 @@ test('evaluate', async (t) => {
// Im not sure if this makes sense, but it is how Node works.
assert.deepEqual(
{
...mod,
default: undefined
},
{...mod, default: undefined},
{array: [1, 2], default: undefined, number: 6.28, object: {a: 1, b: 2}}
)
}
@ -324,7 +349,7 @@ test('evaluate', async (t) => {
await t.test(
'should support rewriting `import.meta.url` w/ `baseUrl`',
async () => {
async function () {
const mod = await evaluate(
'export const x = new URL("example.png", import.meta.url).href',
{baseUrl: 'https://example.com', ...runtime}
@ -334,25 +359,43 @@ test('evaluate', async (t) => {
}
)
await t.test('should throw on an export all from', () => {
assert.throws(() => {
evaluateSync('export * from "a"', runtime)
}, /Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/)
await t.test('should throw on an export all from', async function () {
try {
await evaluate('export * from "a"', runtime)
assert.fail()
} catch (error) {
assert.match(
String(error),
/Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/
)
}
})
await t.test('should throw on an import', () => {
assert.throws(() => {
evaluateSync('import {a} from "b"', runtime)
}, /Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/)
await t.test('should throw on an import', async function () {
try {
await evaluate('import {a} from "b"', runtime)
assert.fail()
} catch (error) {
assert.match(
String(error),
/Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/
)
}
})
await t.test('should throw on an import default', () => {
assert.throws(() => {
evaluateSync('import a from "b"', runtime)
}, /Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/)
await t.test('should throw on an import default', async function () {
try {
await evaluate('import a from "b"', runtime)
assert.fail()
} catch (error) {
assert.match(
String(error),
/Cannot use `import` or `export … from` in `evaluate` \(outputting a function body\) by default/
)
}
})
await t.test('should support a given components', async () => {
await t.test('should support a given components', async function () {
const mod = await evaluate('<X/>', runtime)
assert.equal(
@ -369,24 +412,27 @@ test('evaluate', async (t) => {
)
})
await t.test('should support a provider w/ `useMDXComponents`', async () => {
const mod = await evaluate('<X/>', {...runtime, ...provider})
await t.test(
'should support a provider w/ `useMDXComponents`',
async function () {
const mod = await evaluate('<X/>', {...runtime, ...provider})
assert.equal(
renderToStaticMarkup(
React.createElement(
provider.MDXProvider,
{
components: {
X() {
return React.createElement('span', {}, '!')
assert.equal(
renderToStaticMarkup(
React.createElement(
provider.MDXProvider,
{
components: {
X() {
return React.createElement('span', {}, '!')
}
}
}
},
React.createElement(mod.default)
)
),
'<span>!</span>'
)
})
},
React.createElement(mod.default)
)
),
'<span>!</span>'
)
}
)
})

View File

@ -1,4 +1,5 @@
// eslint-disable-next-line import/no-unassigned-import
/* eslint-disable import/no-unassigned-import */
import './compile.js'
// eslint-disable-next-line import/no-unassigned-import
import './core.js'
import './evaluate.js'
import './syntax.js'

904
packages/mdx/test/syntax.js Normal file
View File

@ -0,0 +1,904 @@
// Register directive nodes in mdast:
/// <reference types="mdast-util-directive" />
/**
* @typedef {import('mdast').Root} MdastRoot
*/
import assert from 'node:assert/strict'
import {test} from 'node:test'
import {compile} from '@mdx-js/mdx'
import {h} from 'hastscript'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
import rehypeKatex from 'rehype-katex'
import remarkDirective from 'remark-directive'
import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import {visit} from 'unist-util-visit'
import {run, runWhole} from './context/run.js'
test('@mdx-js/mdx: syntax: markdown (CommonMark)', async function (t) {
await t.test(
'should support links (resource) (`[]()` -> `a`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('[a](b)')))
),
'<p><a href="b">a</a></p>'
)
}
)
await t.test(
'should support links (reference) (`[][]` -> `a`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('[a]: b\n[a]')))
),
'<p><a href="b">a</a></p>'
)
}
)
await t.test(
'should *not* support links (autolink) (`<http://a>` -> error)',
async function () {
try {
await compile('<http://a>')
assert.fail()
} catch (error) {
assert.match(
String(error),
/note: to create a link in MDX, use `\[text]\(url\)/
)
}
}
)
await t.test(
'should support block quotes (`>` -> `blockquote`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('> a')))
),
'<blockquote>\n<p>a</p>\n</blockquote>'
)
}
)
await t.test(
'should support characters (escape) (`\\` -> ``)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('\\*a*')))
),
'<p>*a*</p>'
)
}
)
await t.test(
'should support character (reference) (`&lt;` -> `<`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('&lt;')))
),
'<p>&lt;</p>'
)
}
)
await t.test(
'should support code (fenced) (` ``` ` -> `pre code`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('```\na')))
),
'<pre><code>a\n</code></pre>'
)
}
)
await t.test(
'should *not* support code (indented) (`\\ta` -> `p`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile(' a')))
),
'<p>a</p>'
)
}
)
await t.test(
'should support code (text) (`` `a` `` -> `code`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('`a`')))
),
'<p><code>a</code></p>'
)
}
)
await t.test('should support emphasis (`*` -> `em`)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('*a*')))
),
'<p><em>a</em></p>'
)
})
await t.test(
'should support hard break (escape) (`\\\\\\n` -> `<br>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a\\\nb')))
),
'<p>a<br/>\nb</p>'
)
}
)
await t.test(
'should support hard break (whitespace) (`\\\\\\n` -> `<br>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a \nb')))
),
'<p>a<br/>\nb</p>'
)
}
)
await t.test(
'should support headings (atx) (`#` -> `<h1>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('#')))
),
'<h1></h1>'
)
}
)
await t.test(
'should support headings (setext) (`=` -> `<h1>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a\n=')))
),
'<h1>a</h1>'
)
}
)
await t.test(
'should support list (ordered) (`1.` -> `<ol><li>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('1.')))
),
'<ol>\n<li></li>\n</ol>'
)
}
)
await t.test(
'should support list (unordered) (`*` -> `<ul><li>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('*')))
),
'<ul>\n<li></li>\n</ul>'
)
}
)
await t.test('should support strong (`**` -> `strong`)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('**a**')))
),
'<p><strong>a</strong></p>'
)
})
await t.test(
'should support thematic break (`***` -> `<hr>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('***')))
),
'<hr/>'
)
}
)
})
test('@mdx-js/mdx: syntax: markdown (GFM, `remark-gfm`)', async function (t) {
await t.test(
'should support links (autolink literal) (`http://a` -> `a`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(await compile('http://a', {remarkPlugins: [remarkGfm]}))
)
),
'<p><a href="http://a">http://a</a></p>'
)
}
)
await t.test(
'should support footnotes (`[^a]` -> `<sup><a…>`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('[^a]\n[^a]: b', {remarkPlugins: [remarkGfm]})
)
)
),
`<p><sup><a href="#user-content-fn-a" id="user-content-fnref-a" data-footnote-ref="true" aria-describedby="footnote-label">1</a></sup></p>
<section data-footnotes="true" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-a">
<p>b <a href="#user-content-fnref-a" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref"></a></p>
</li>
</ol>
</section>`
)
}
)
await t.test(
'should support tables (`| a |` -> `<table>...`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('| a |\n| - |', {remarkPlugins: [remarkGfm]})
)
)
),
'<table><thead><tr><th>a</th></tr></thead></table>'
)
}
)
await t.test(
'should support task lists (`* [x]` -> `input`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('* [x] a\n* [ ] b', {remarkPlugins: [remarkGfm]})
)
)
),
'<ul class="contains-task-list">\n<li class="task-list-item"><input type="checkbox" disabled="" checked=""/> a</li>\n<li class="task-list-item"><input type="checkbox" disabled=""/> b</li>\n</ul>'
)
}
)
await t.test(
'should support strikethrough (`~` -> `del`)',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(await compile('~a~', {remarkPlugins: [remarkGfm]}))
)
),
'<p><del>a</del></p>'
)
}
)
})
test('@mdx-js/mdx: syntax: markdown (GFM footnotes, `remark-gfm`, `remark-rehype` options)', async function (t) {
await t.test('should support `remark-rehype` options', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('Text[^1]\n\n[^1]: Note.', {
remarkPlugins: [remarkGfm],
remarkRehypeOptions: {
footnoteLabel: 'Notes',
footnoteBackLabel: 'Back'
}
})
)
)
),
`<p>Text<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="true" aria-describedby="footnote-label">1</a></sup></p>
<section data-footnotes="true" class="footnotes"><h2 class="sr-only" id="footnote-label">Notes</h2>
<ol>
<li id="user-content-fn-1">
<p>Note. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back" class="data-footnote-backref"></a></p>
</li>
</ol>
</section>`,
'should pass options to remark-rehype'
)
})
})
test('@mdx-js/mdx: syntax: markdown (frontmatter, `remark-frontmatter`)', async function (t) {
await t.test('should support frontmatter (YAML)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('---\na: b\n---\nc', {
remarkPlugins: [remarkFrontmatter]
})
)
)
),
'<p>c</p>'
)
})
await t.test('should support frontmatter (TOML)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('+++\na: b\n+++\nc', {
remarkPlugins: [[remarkFrontmatter, 'toml']]
})
)
)
),
'<p>c</p>'
)
})
})
test('@mdx-js/mdx: syntax: markdown (math, `remark-math`, `rehype-katex`)', async function (t) {
await t.test('should support math', async function () {
assert.match(
renderToStaticMarkup(
React.createElement(
await run(
await compile('$C_L$', {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex]
})
)
)
),
/<math/,
'should support math (LaTeX)'
)
})
})
test('@mdx-js/mdx: syntax: markdown (directive, `remark-directive`)', async function (t) {
await t.test('should support directives', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile(':span[*text*]{.red}', {
remarkPlugins: [remarkDirective, plugin]
})
)
)
),
'<p><span class="red"><em>text</em></span></p>'
)
})
function plugin() {
/**
* @param {MdastRoot} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
visit(tree, function (node) {
if (
node.type === 'containerDirective' ||
node.type === 'leafDirective' ||
node.type === 'textDirective'
) {
const element = h(node.name, node.attributes || {})
node.data = {
hName: element.tagName,
hProperties: element.properties
}
}
})
}
}
})
test('@mdx-js/mdx: syntax: MDX (JSX)', async function (t) {
await t.test('should support JSX (text)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a <s>b</s>')))
),
'<p>a <s>b</s></p>'
)
})
await t.test('should support JSX (flow)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<div>\n b\n</div>')))
),
'<div><p>b</p></div>'
)
})
await t.test('should unravel JSX (text) as an only child', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<h1>b</h1>')))
),
'<h1>b</h1>'
)
})
await t.test('should unravel JSX (text) as only children', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<a>b</a><b>c</b>')))
),
'<a>b</a>\n<b>c</b>'
)
})
await t.test(
'should unravel JSX (text) and whitespace as only children',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<a>b</a>\t<b>c</b>')))
),
'<a>b</a>\n<b>c</b>'
)
}
)
await t.test(
'should unravel expression (text) as an only child',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('{1}')))
),
'1'
)
}
)
await t.test(
'should unravel expression (text) as only children',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('{1}{2}')))
),
'1\n2'
)
}
)
await t.test(
'should unravel expression (text) and whitespace as only children',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('{1}\n{2}')))
),
'1\n2'
)
}
)
await t.test('should support JSX (text, fragment)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a <>b</>')))
),
'<p>a b</p>'
)
})
await t.test('should support JSX (flow, fragment)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<>\n b\n</>')))
),
'<p>b</p>'
)
})
await t.test('should support JSX (namespace)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('a <x:y>b</x:y>')))
),
'<p>a <x:y>b</x:y></p>'
)
})
await t.test('should support expressions in MDX (text)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(await compile('export const a = 1\n\na {a}'))
)
),
'<p>a 1</p>'
)
})
await t.test('should support expressions in MDX (flow)', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('{\n 1 + 1\n}')))
),
'2'
)
})
await t.test('should support empty expressions in MDX', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('{/*!*/}')))
),
''
)
})
await t.test('should support JSX attribute names', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(await compile('<x a="1" b:c="1" hidden />'))
)
),
'<x a="1" b:c="1" hidden=""></x>'
)
})
await t.test('should support JSX attribute values', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(await compile('<x y="1" z=\'w\' style={{color: "red"}} />'))
)
),
'<x y="1" z="w" style="color:red"></x>'
)
})
await t.test('should support JSX spread attributes', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(await run(await compile('<x {...{a: 1}} />')))
),
'<x a="1"></x>'
)
})
await t.test('should support JSX in expressions', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('{<i>the sum of one and one is: {1 + 1}</i>}')
)
)
),
'<i>the sum of one and one is: 2</i>'
)
})
await t.test('should not include whitespace in tables', async function () {
// Important: there should not be whitespace in the `tr`.
// This is normally not present, but unraveling makes this a bit more complex.
// See: <https://github.com/mdx-js/mdx/issues/2000>.
assert.equal(
String(
await compile(`<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
</table>`)
),
[
'/*@jsxRuntime automatic @jsxImportSource react*/',
'import {jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";',
'function _createMdxContent(props) {',
' return _jsx("table", {',
' children: _jsx("thead", {',
' children: _jsxs("tr", {',
' children: [_jsx("th", {',
' children: "a"',
' }), _jsx("th", {',
' children: "b"',
' })]',
' })',
' })',
' });',
'}',
'function MDXContent(props = {}) {',
' const {wrapper: MDXLayout} = props.components || ({});',
' return MDXLayout ? _jsx(MDXLayout, {',
' ...props,',
' children: _jsx(_createMdxContent, {',
' ...props',
' })',
' }) : _createMdxContent(props);',
'}',
'export default MDXContent;',
''
].join('\n')
)
})
})
test('@mdx-js/mdx: syntax: MDX (ESM)', async function (t) {
await t.test('should support importing components w/ ESM', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile(
'import {Pill} from "./components.js"\n\n<Pill>!</Pill>'
)
)
)
),
'<span style="color:red">!</span>'
)
})
await t.test('should support importing data w/ ESM', async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('import {number} from "./data.js"\n\n{number}')
)
)
),
'3.14'
)
})
await t.test('should support exporting w/ ESM', async function () {
const mod = await runWhole(await compile('export const number = Math.PI'))
assert.equal(mod.number, Math.PI)
})
await t.test(
'should support exporting an identifier w/o a value',
async function () {
assert.ok('a' in (await runWhole(await compile('export var a'))))
}
)
await t.test('should support exporting an object pattern', async function () {
const mod = await runWhole(
await compile('import {object} from "./data.js"\nexport var {a} = object')
)
assert.equal(mod.a, 1)
})
await t.test(
'should support exporting a rest element in an object pattern',
async function () {
const mod = await runWhole(
await compile(
'import {object} from "./data.js"\nexport var {a, ...rest} = object'
)
)
assert.deepEqual(mod.rest, {b: 2})
}
)
await t.test(
'should support exporting an assignment pattern in an object pattern',
async function () {
const mod = await runWhole(
await compile(
'import {object} from "./data.js"\nexport var {c = 3} = object'
)
)
assert.equal(mod.c, 3)
}
)
await t.test('should support exporting an array pattern', async function () {
const mod = await runWhole(
await compile('import {array} from "./data.js"\nexport var [a] = array')
)
assert.equal(mod.a, 1)
})
await t.test('should support `export as` w/ ESM', async function () {
const mod = await runWhole(
await compile('export const number = Math.PI\nexport {number as pi}')
)
assert.equal(mod.pi, Math.PI)
})
await t.test(
'should support default export to define a layout',
async function () {
const Content = await run(
await compile(
'export default function Layout(props) { return <div {...props} /> }\n\na'
)
)
assert.equal(
renderToStaticMarkup(React.createElement(Content)),
'<div><p>a</p></div>'
)
}
)
await t.test(
'should support default export from a source',
async function () {
const Content = await run(
await compile('export {Layout as default} from "./components.js"\n\na')
)
assert.equal(
renderToStaticMarkup(React.createElement(Content)),
'<div style="color:red"><p>a</p></div>'
)
}
)
await t.test(
'should support rexporting something as a default export from a source',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('export {default} from "./components.js"\n\na')
)
)
),
'<div style="color:red"><p>a</p></div>'
)
}
)
await t.test(
'should support rexporting the default export from a source',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('export {default} from "./components.js"\n\na')
)
)
),
'<div style="color:red"><p>a</p></div>'
)
}
)
await t.test(
'should support rexporting the default export from a source',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile('export {default} from "./components.js"\n\na')
)
)
),
'<div style="color:red"><p>a</p></div>'
)
}
)
await t.test(
'should support rexporting the default export, and other things, from a source',
async function () {
assert.equal(
renderToStaticMarkup(
React.createElement(
await run(
await compile(
'export {default, Pill} from "./components.js"\n\na'
)
)
)
),
'<div style="color:red"><p>a</p></div>'
)
}
)
await t.test('should support the jsx dev runtime', async function () {
assert.equal(
String(
await compile(
{value: '<X />', path: 'path/to/file.js'},
{development: true}
)
),
[
'/*@jsxRuntime automatic @jsxImportSource react*/',
'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";',
'function _createMdxContent(props) {',
' const {X} = props.components || ({});',
' if (!X) _missingMdxReference("X", true, "1:1-1:6");',
' return _jsxDEV(X, {}, undefined, false, {',
' fileName: "path/to/file.js",',
' lineNumber: 1,',
' columnNumber: 1',
' }, this);',
'}',
'function MDXContent(props = {}) {',
' const {wrapper: MDXLayout} = props.components || ({});',
' return MDXLayout ? _jsxDEV(MDXLayout, {',
' ...props,',
' children: _jsxDEV(_createMdxContent, {',
' ...props',
' }, undefined, false, {',
' fileName: "path/to/file.js"',
' }, this)',
' }, undefined, false, {',
' fileName: "path/to/file.js"',
' }, this) : _createMdxContent(props);',
'}',
'export default MDXContent;',
'function _missingMdxReference(id, component, place) {',
' throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it." + (place ? "\\nIts referenced in your code at `" + place + "` in `path/to/file.js`" : ""));',
'}',
''
].join('\n')
)
})
})

View File

@ -4,7 +4,7 @@
import {createLoader} from './lib/index.js'
const {load, getFormat, transformSource} = createLoader()
const {getFormat, load, transformSource} = createLoader()
export {load, getFormat, transformSource}
export {getFormat, load, transformSource}
export {createLoader} from './lib/index.js'

View File

@ -1,11 +1,13 @@
/**
* @typedef {import('@mdx-js/mdx/lib/compile.js').CompileOptions} CompileOptions
*
*/
/**
* @typedef LoaderOptions
* Extra configuration.
* @property {boolean | null | undefined} [fixRuntimeWithoutExportMap=true]
* Several JSX runtimes, notably React below 18 and Emotion below 11.10.0,
* dont yet have a proper export map set up.
* dont yet have a proper export map set up (default: `true`).
* Export maps are needed to map `xxx/jsx-runtime` to an actual file in ESM.
* This option fixes React et al by turning those into `xxx/jsx-runtime.js`.
*
@ -14,19 +16,22 @@
*/
import fs from 'node:fs/promises'
import path from 'node:path'
import {URL, fileURLToPath} from 'node:url'
import {VFile} from 'vfile'
import {createFormatAwareProcessors} from '@mdx-js/mdx/lib/util/create-format-aware-processors.js'
import {extnamesToRegex} from '@mdx-js/mdx/lib/util/extnames-to-regex.js'
import {VFile} from 'vfile'
/**
* Create smart processors to handle different formats.
*
* @param {Options | null | undefined} [options]
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Loader.
*/
export function createLoader(options) {
const options_ = options || {}
const {extnames, process} = createFormatAwareProcessors(options_)
const regex = extnamesToRegex(extnames)
let fixRuntimeWithoutExportMap = options_.fixRuntimeWithoutExportMap
if (
@ -38,63 +43,86 @@ export function createLoader(options) {
return {load, getFormat, transformSource}
/* c8 ignore start */
// Node version 17.
/**
* @param {string} url
* @param {string} href
* URL.
* @param {unknown} context
* Context.
* @param {Function} defaultLoad
* Default `load` function.
* @returns
* Result.
*/
async function load(url, context, defaultLoad) {
if (!extnames.includes(path.extname(url))) {
return defaultLoad(url, context, defaultLoad)
async function load(href, context, defaultLoad) {
const url = new URL(href)
if (url.protocol === 'file:' && regex.test(url.pathname)) {
const value = await fs.readFile(url)
const file = await process(new VFile({value, path: url}))
let source = String(file)
/* c8 ignore next 3 -- to do: remove. */
if (fixRuntimeWithoutExportMap) {
source = String(file).replace(/\/jsx-runtime(?=["'])/, '$&.js')
}
return {format: 'module', shortCircuit: true, source}
}
const value = await fs.readFile(fileURLToPath(new URL(url)))
const file = await process(new VFile({value, path: new URL(url)}))
let source = String(file)
if (fixRuntimeWithoutExportMap) {
source = String(file).replace(/\/jsx-runtime(?=["'])/, '$&.js')
}
return {format: 'module', source, shortCircuit: true}
return defaultLoad(href, context, defaultLoad)
}
// To do: remove.
/* c8 ignore start */
// Pre version 17.
/**
* @param {string} url
* @param {string} href
* URL.
* @param {unknown} context
* Context.
* @param {Function} defaultGetFormat
* Default `getFormat` function.
* @deprecated
* This is an obsolete legacy function that no longer works in Node 17.
* @returns
* Result.
*/
function getFormat(url, context, defaultGetFormat) {
return extnames.includes(path.extname(url))
function getFormat(href, context, defaultGetFormat) {
const url = new URL(href)
return url.protocol === 'file:' && regex.test(url.pathname)
? {format: 'module'}
: defaultGetFormat(url, context, defaultGetFormat)
: defaultGetFormat(href, context, defaultGetFormat)
}
/**
* @param {string} value
* Code.
* @param {{url: string, [x: string]: unknown}} context
* Context.
* @param {Function} defaultTransformSource
* Default `transformSource` function.
* @deprecated
* This is an obsolete legacy function that no longer works in Node 17.
* @returns
* Result.
*/
async function transformSource(value, context, defaultTransformSource) {
if (!extnames.includes(path.extname(context.url))) {
return defaultTransformSource(value, context, defaultTransformSource)
const url = new URL(context.url)
if (url.protocol === 'file:' && regex.test(url.pathname)) {
const file = await process(new VFile({path: new URL(context.url), value}))
let source = String(file)
if (fixRuntimeWithoutExportMap) {
source = String(file).replace(/\/jsx-runtime(?=["'])/, '$&.js')
}
return {source}
}
const file = await process(new VFile({value, path: new URL(context.url)}))
let source = String(file)
if (fixRuntimeWithoutExportMap) {
source = String(file).replace(/\/jsx-runtime(?=["'])/, '$&.js')
}
return {source}
return defaultTransformSource(value, context, defaultTransformSource)
}
/* c8 ignore end */
}

View File

@ -66,7 +66,9 @@ yarn add @mdx-js/node-loader
Say we have an MDX document, `example.mdx`:
```mdx
export const Thing = () => <>World!</>
export function Thing() {
return <>World!</>
}
# Hello, <Thing />
```
@ -132,9 +134,9 @@ This option fixes React et al by turning those into `xxx/jsx-runtime.js`.
import {createLoader} from '@mdx-js/node-loader'
// Load is for Node 17+, the rest for 12-16.
const {load, getFormat, transformSource} = createLoader(/* Options… */)
const {getFormat, load, transformSource} = createLoader(/* Options… */)
export {load, getFormat, transformSource}
export {getFormat, load, transformSource}
```
This example can then be used with `node --experimental-loader=./my-loader.js`.

View File

@ -1,42 +1,57 @@
/**
* @typedef {import('mdx/types.js').MDXContent} MDXContent
* @typedef {import('mdx/types.js').MDXModule} MDXModule
*/
import assert from 'node:assert/strict'
import {promises as fs} from 'node:fs'
import fs from 'node:fs/promises'
import {test} from 'node:test'
import React from 'react'
import {renderToStaticMarkup} from 'react-dom/server'
test('@mdx-js/node-loader', async () => {
await fs.writeFile(
new URL('esm-loader.mdx', import.meta.url),
'export const Message = () => <>World!</>\n\n# Hello, <Message />'
)
test('@mdx-js/node-loader', async function (t) {
await t.test('should expose the public api', async function () {
assert.deepEqual(Object.keys(await import('@mdx-js/node-loader')).sort(), [
'createLoader',
'getFormat',
'load',
'transformSource'
])
})
/** @type {MDXContent} */
let Content
await t.test('should work', async function () {
const mdxUrl = new URL('node-loader.mdx', import.meta.url)
try {
const mod = await import('./esm-loader.mdx')
Content = mod.default
} catch (error) {
const exception = /** @type {NodeJS.ErrnoException} */ (error)
if (exception.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
await fs.unlink(new URL('esm-loader.mdx', import.meta.url))
throw new Error(
'Please run Node with `--experimental-loader=./esm-loader.js` to test the ESM loader'
)
await fs.writeFile(
mdxUrl,
'export function Message() { return <>World!</> }\n\n# Hello, <Message />'
)
/** @type {MDXModule} */
let mod
try {
mod = await import(mdxUrl.href)
} catch (error) {
const exception = /** @type {NodeJS.ErrnoException} */ (error)
if (exception.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
await fs.rm(mdxUrl)
throw new Error(
'Please run Node with `--loader=./test/react-18-node-loader.js` to test the ESM loader'
)
}
throw error
}
throw error
}
const Content = mod.default
assert.equal(
renderToStaticMarkup(React.createElement(Content)),
'<h1>Hello, World!</h1>',
'should compile'
)
assert.equal(
renderToStaticMarkup(React.createElement(Content)),
'<h1>Hello, World!</h1>'
)
await fs.unlink(new URL('esm-loader.mdx', import.meta.url))
await fs.rm(mdxUrl)
})
})

View File

@ -1,4 +1,4 @@
import {createLoader} from '../index.js'
import {createLoader} from '@mdx-js/node-loader'
// To do: break to not fix by default, remove this file.
const {load} = createLoader({fixRuntimeWithoutExportMap: false})

View File

@ -1,29 +1,33 @@
/**
* @typedef {import('preact').ComponentChildren} ComponentChildren
* @typedef {import('mdx/types.js').MDXComponents} Components
*
* @typedef Props
* Configuration.
* @property {Components | MergeComponents | null | undefined} [components]
* Mapping of names for JSX components to Preact components.
* @property {boolean | null | undefined} [disableParentContext=false]
* Turn off outer component context.
* @property {ComponentChildren | null | undefined} [children]
* Children.
*
* @typedef {import('preact').ComponentChildren} ComponentChildren
* @typedef {import('preact').Context<Components>} Context
*/
/**
* @callback MergeComponents
* Custom merge function.
* @param {Components} currentComponents
* @param {Readonly<Components>} currentComponents
* Current components from the context.
* @returns {Components}
* Merged components.
*
* @typedef Props
* Configuration.
* @property {Readonly<Components> | MergeComponents | null | undefined} [components]
* Mapping of names for JSX components to Preact components (optional).
* @property {boolean | null | undefined} [disableParentContext=false]
* Turn off outer component context (default: `false`).
* @property {ComponentChildren | null | undefined} [children]
* Children (optional).
*/
import {createContext, h} from 'preact'
import {useContext} from 'preact/hooks'
/**
* @type {import('preact').Context<Components>}
* @type {Context}
* Context.
* @deprecated
* This export is marked as a legacy feature.
* That means its no longer recommended for use as it might be removed
@ -36,19 +40,24 @@ export const MDXContext = createContext({})
/**
* @param {import('preact').ComponentType<any>} Component
* Component.
* @deprecated
* This export is marked as a legacy feature.
* That means its no longer recommended for use as it might be removed
* in a future major release.
*
* Please use `useMDXComponents` to get context based components instead.
* @returns
* Bound component.
*/
export function withMDXComponents(Component) {
return boundMDXComponent
/**
* @param {Record<string, unknown> & {components?: Components | null | undefined}} props
* Props.
* @returns {JSX.Element}
* Element.
*/
function boundMDXComponent(props) {
const allComponents = useMDXComponents(props.components)
@ -59,9 +68,9 @@ export function withMDXComponents(Component) {
/**
* Get current components from the MDX Context.
*
* @param {Components | MergeComponents | null | undefined} [components]
* @param {Readonly<Components> | MergeComponents | null | undefined} [components]
* Additional components to use or a function that takes the current
* components and filters/merges/changes them.
* components and filters/merges/changes them (optional).
* @returns {Components}
* Current components.
*/
@ -76,17 +85,19 @@ export function useMDXComponents(components) {
return {...contextComponents, ...components}
}
/** @type {Components} */
/** @type {Readonly<Components>} */
const emptyObject = {}
/**
* Provider for MDX context
*
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function MDXProvider({components, children, disableParentContext}) {
/** @type {Components} */
export function MDXProvider({children, components, disableParentContext}) {
/** @type {Readonly<Components>} */
let allComponents
if (disableParentContext) {
@ -100,7 +111,7 @@ export function MDXProvider({components, children, disableParentContext}) {
return h(
MDXContext.Provider,
{value: allComponents, children: undefined},
{children: undefined, value: allComponents},
children
)
}

View File

@ -60,6 +60,7 @@
"**/*.jsx"
],
"rules": {
"react/jsx-no-bind": "off",
"react/react-in-jsx-scope": "off"
}
}

View File

@ -107,7 +107,7 @@ Configuration (`Object`, optional).
###### `props.components`
Mapping of names for JSX components to Preact components
(`Record<string, string|Component|Components>`, optional).
(`Record<string, string | Component | Components>`, optional).
###### `props.disableParentContext`

View File

@ -6,210 +6,247 @@
import assert from 'node:assert/strict'
import {test} from 'node:test'
import {evaluate} from '@mdx-js/mdx'
import {MDXProvider, useMDXComponents, withMDXComponents} from '@mdx-js/preact'
import * as runtime_ from 'preact/jsx-runtime'
import {render} from 'preact-render-to-string'
import {evaluate} from '@mdx-js/mdx'
import {MDXProvider, useMDXComponents, withMDXComponents} from '../index.js'
const runtime = /** @type {RuntimeProduction} */ (runtime_)
test('should support `components` with `MDXProvider`', async () => {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
test('@mdx-js/preact', async function (t) {
await t.test('should expose the public api', async function () {
assert.deepEqual(Object.keys(await import('@mdx-js/preact')).sort(), [
'MDXContext',
'MDXProvider',
'useMDXComponents',
'withMDXComponents'
])
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<Content />
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>'
await t.test(
'should support `components` with `MDXProvider`',
async function () {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<Content />
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>'
)
}
)
})
test('should support `wrapper` in `components`', async () => {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
})
await t.test('should support `wrapper` in `components`', async function () {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
wrapper(/** @type {Record<string, unknown>} */ props) {
return <div id="layout" {...props} />
}
}}
>
<Content />
</MDXProvider>
),
'<div id="layout"><h1>hi</h1></div>'
)
})
test('should combine components in nested `MDXProvider`s', async () => {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
},
h2(props) {
return <h2 style={{color: 'rebeccapurple'}} {...props} />
}
}}
>
assert.equal(
render(
<MDXProvider
components={{
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
/**
* @param {JSX.IntrinsicElements['div']} props
* Props.
* @returns
* Element.
*/
wrapper(props) {
return <div id="layout" {...props} />
}
}}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
})
test('should support components as a function', async () => {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
),
'<div id="layout"><h1>hi</h1></div>'
)
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
},
h2(props) {
return <h2 style={{color: 'rebeccapurple'}} {...props} />
}
}}
>
await t.test(
'should combine components in nested `MDXProvider`s',
async function () {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
},
h2(props) {
return <h2 style={{color: 'rebeccapurple'}} {...props} />
}
}}
>
<MDXProvider
components={{
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
}}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
}
)
await t.test('should support components as a function', async function () {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={() => ({
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
})}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
})
test('should support a `disableParentContext` prop (sandbox)', async () => {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<MDXProvider disableParentContext>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>'
)
})
test('should support a `disableParentContext` *and* `components as a function', async () => {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<MDXProvider
disableParentContext
components={() => ({
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
})}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
})
test('should support `withComponents`', async () => {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
// Unknown props.
// type-coverage:ignore-next-line
const With = withMDXComponents((props) => props.children)
// Bug: this should use the `h2` component too, logically?
// As `withMDXComponents` is deprecated, and it would probably be a breaking
// change, we can just remove it later.
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<With
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
},
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
return <h2 style={{color: 'rebeccapurple'}} {...props} />
}
}}
>
<Content />
</With>
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>\n<h2>hello</h2>'
<MDXProvider
components={function () {
return {
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
}
}}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
})
await t.test(
'should support a `disableParentContext` prop (sandbox)',
async function () {
const {default: Content} = await evaluate('# hi', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<MDXProvider disableParentContext>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>'
)
}
)
await t.test(
'should support a `disableParentContext` *and* `components` as a function',
async function () {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<MDXProvider
disableParentContext
components={function () {
return {
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
}
}}
>
<Content />
</MDXProvider>
</MDXProvider>
),
'<h1>hi</h1>\n<h2 style="color:papayawhip;">hello</h2>'
)
}
)
await t.test('should support `withComponents`', async function () {
const {default: Content} = await evaluate('# hi\n## hello', {
...runtime,
useMDXComponents
})
// Unknown props.
// type-coverage:ignore-next-line
const With = withMDXComponents(function (props) {
// Unknown props.
// type-coverage:ignore-next-line
return props.children
})
// Bug: this should use the `h2` component too, logically?
// As `withMDXComponents` is deprecated, and it would probably be a breaking
// change, we can just remove it later.
assert.equal(
render(
<MDXProvider
components={{
h1(props) {
return <h1 style={{color: 'tomato'}} {...props} />
}
}}
>
<With
components={{
h2(props) {
return <h2 style={{color: 'papayawhip'}} {...props} />
}
}}
>
<Content />
</With>
</MDXProvider>
),
'<h1 style="color:tomato;">hi</h1>\n<h2>hello</h2>'
)
})
})

View File

@ -1,28 +1,31 @@
/**
* @typedef {import('react').ReactNode} ReactNode
* @typedef {import('mdx/types.js').MDXComponents} Components
*
* @typedef Props
* Configuration.
* @property {Components | MergeComponents | null | undefined} [components]
* Mapping of names for JSX components to React components.
* @property {boolean | null | undefined} [disableParentContext=false]
* Turn off outer component context.
* @property {ReactNode | null | undefined} [children]
* Children.
*
* @typedef {import('react').Context<Components>} Context
* @typedef {import('react').ReactNode} ReactNode
*/
/**
* @callback MergeComponents
* Custom merge function.
* @param {Components} currentComponents
* @param {Readonly<Components>} currentComponents
* Current components from the context.
* @returns {Components}
* Merged components.
*
* @typedef Props
* Configuration.
* @property {Readonly<Components> | MergeComponents | null | undefined} [components]
* Mapping of names for JSX components to React components (optional).
* @property {boolean | null | undefined} [disableParentContext=false]
* Turn off outer component context (default: `false`).
* @property {ReactNode | null | undefined} [children]
* Children (optional).
*/
import React from 'react'
/**
* @type {import('react').Context<Components>}
* @type {Context}
* @deprecated
* This export is marked as a legacy feature.
* That means its no longer recommended for use as it might be removed
@ -35,6 +38,7 @@ export const MDXContext = React.createContext({})
/**
* @param {import('react').ComponentType<any>} Component
* Component.
* @deprecated
* This export is marked as a legacy feature.
* That means its no longer recommended for use as it might be removed
@ -47,7 +51,9 @@ export function withMDXComponents(Component) {
/**
* @param {Record<string, unknown> & {components?: Components | null | undefined}} props
* Props.
* @returns {JSX.Element}
* Element.
*/
function boundMDXComponent(props) {
const allComponents = useMDXComponents(props.components)
@ -58,9 +64,9 @@ export function withMDXComponents(Component) {
/**
* Get current components from the MDX Context.
*
* @param {Components | MergeComponents | null | undefined} [components]
* @param {Readonly<Components> | MergeComponents | null | undefined} [components]
* Additional components to use or a function that takes the current
* components and filters/merges/changes them.
* components and filters/merges/changes them (optional).
* @returns {Components}
* Current components.
*/
@ -68,27 +74,32 @@ export function useMDXComponents(components) {
const contextComponents = React.useContext(MDXContext)
// Memoize to avoid unnecessary top-level context changes
return React.useMemo(() => {
// Custom merge via a function prop
if (typeof components === 'function') {
return components(contextComponents)
}
return React.useMemo(
function () {
// Custom merge via a function prop
if (typeof components === 'function') {
return components(contextComponents)
}
return {...contextComponents, ...components}
}, [contextComponents, components])
return {...contextComponents, ...components}
},
[contextComponents, components]
)
}
/** @type {Components} */
/** @type {Readonly<Components>} */
const emptyObject = {}
/**
* Provider for MDX context
*
* @param {Props} props
* @param {Readonly<Props>} props
* Props.
* @returns {JSX.Element}
* Element.
*/
export function MDXProvider({components, children, disableParentContext}) {
/** @type {Components} */
export function MDXProvider({children, components, disableParentContext}) {
/** @type {Readonly<Components>} */
let allComponents
if (disableParentContext) {

Some files were not shown because too many files have changed in this diff Show More