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:
parent
89097e4c31
commit
a94f28521d
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ node_modules/
|
||||
*.d.cts
|
||||
*.d.ts
|
||||
/public/
|
||||
!/packages/mdx/lib/types.d.ts
|
||||
!/website/types.d.ts
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Note} from './_component/note.jsx'
|
||||
|
||||
export {Home as default} from './_component/home.jsx'
|
||||
export const navExclude = true
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -62,6 +62,9 @@ for (const copy of copies) {
|
||||
|
||||
/**
|
||||
* @this {HTMLButtonElement}
|
||||
* Button element.
|
||||
* @returns {undefined}
|
||||
* Nothing.
|
||||
*/
|
||||
function onclick() {
|
||||
assert(copyIcon)
|
||||
|
@ -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} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 don’t 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>
|
||||
)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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>} */
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -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 you’re 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 you’re 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}>
|
||||
|
@ -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>
|
||||
|
@ -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`
|
||||
|
@ -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 don’t 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>
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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]
|
||||
})
|
||||
)
|
||||
)
|
||||
|
@ -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 */
|
||||
})
|
||||
)
|
||||
|
@ -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, it’s 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) {
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
1255
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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… */})]
|
||||
})
|
||||
```
|
||||
|
@ -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
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
declare function _exports(this: LoaderContext, code: string): void;
|
||||
export = _exports;
|
||||
export type LoaderContext = import('webpack').LoaderContext<unknown>;
|
@ -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) {
|
||||
|
@ -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 webpack’s 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 webpack’s 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))
|
||||
})
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 doesn’t get.
|
||||
return pipeline
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
]
|
||||
])
|
||||
|
@ -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 might’ve 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 whatever’s
|
||||
// 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 there’s 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 doesn’t think `JSXFragment` is an expression.
|
||||
// eslint-disable-next-line object-shorthand
|
||||
argument: /** @type {Expression} */ (argument)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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 that’s 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 can’t 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` it’d 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
|
||||
|
@ -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})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
46
packages/mdx/lib/types.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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 we’re using this utility for export default declarations,
|
||||
* which can’t 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 shouldn’t 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 shouldn’t happen or a
|
||||
// maintainer is making a mistake.
|
||||
assert(declaration.type === 'ClassDeclaration', 'unexpected node type')
|
||||
return {...declaration, type: 'ClassExpression'}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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('|') +
|
||||
')([?#]|$)'
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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",
|
||||
|
@ -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 won’t 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
@ -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'}})
|
||||
|
@ -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
|
||||
|
42
packages/mdx/test/context/run.js
Normal file
42
packages/mdx/test/context/run.js
Normal 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
17
packages/mdx/test/core.js
Normal 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'
|
||||
])
|
||||
})
|
||||
})
|
@ -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) => {
|
||||
|
||||
// I’m 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>'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -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
904
packages/mdx/test/syntax.js
Normal 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) (`<` -> `<`)',
|
||||
async function () {
|
||||
assert.equal(
|
||||
renderToStaticMarkup(
|
||||
React.createElement(await run(await compile('<')))
|
||||
),
|
||||
'<p><</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 ? "\\nIt’s referenced in your code at `" + place + "` in `path/to/file.js`" : ""));',
|
||||
'}',
|
||||
''
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
})
|
@ -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'
|
||||
|
@ -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,
|
||||
* don’t yet have a proper export map set up.
|
||||
* don’t 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 */
|
||||
}
|
||||
|
@ -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`.
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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})
|
||||
|
@ -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 it’s 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 it’s 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
|
||||
)
|
||||
}
|
||||
|
@ -60,6 +60,7 @@
|
||||
"**/*.jsx"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-no-bind": "off",
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
}
|
||||
|
@ -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`
|
||||
|
||||
|
@ -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>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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 it’s 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 it’s 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
Loading…
Reference in New Issue
Block a user