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

Update @mdx-js/mdx (#1669)

* Import xdm’s core into `@mdx-js/mdx`
* Tons of fixes and updates and a couiple of changes that will be
  tested and documented later
* Use ESM
* Add JSDoc based types
This commit is contained in:
Titus 2021-09-22 12:02:09 +02:00 committed by GitHub
parent 22c40f78b3
commit 57e637cf55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 4131 additions and 3624 deletions

View File

@ -22,14 +22,15 @@ extends:
- xo
- prettier
rules:
react/prop-types: off
react/react-in-jsx-scope: off
settings:
react:
version: detect
rules:
react/prop-types: off
react/react-in-jsx-scope: off
prefer-destructuring: off
overrides:
- files:
- '**/test/**/*.js'

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ coverage/
/public
/packages/loader/lib/**/*.d.ts
/packages/loader/test/**/*.d.ts
/packages/mdx/**/*.d.ts
/packages/preact/lib/**/*.d.ts
/packages/preact/test/**/*.d.ts
/packages/preact/index.d.ts

4492
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@
"lint": "eslint --ext .jsx --report-unused-disable-directives --cache .",
"publish-ci": "# lerna publish -y --canary --preid ci --pre-dist-tag ci",
"publish-next": "# lerna publish --force-publish=\"*\" --pre-dist-tag next --preid next",
"build": "npm run build --workspaces -w packages/loader -w packages/react -w packages/preact --if-present",
"build": "npm run build --workspaces -w packages/loader -w packages/mdx -w packages/preact -w packages/react -w packages/remark-mdx -w packages/vue --if-present",
"test-api": "npm run test-api --workspaces --if-present",
"test-coverage": "npm run test-coverage --workspaces --if-present",
"test-types": "npm run test-types --workspaces --if-present",

View File

@ -1,12 +0,0 @@
{
"presets": [
[
"@babel/env",
{
"corejs": 3,
"useBuiltIns": "usage"
}
],
"@babel/react"
]
}

View File

@ -1,176 +0,0 @@
const astring = require('astring')
module.exports = estreeToJs
const customGenerator = {
...astring.baseGenerator,
JSXAttribute,
JSXClosingElement,
JSXClosingFragment,
JSXElement,
JSXEmptyExpression,
JSXExpressionContainer,
JSXFragment,
JSXIdentifier,
JSXMemberExpression,
JSXNamespacedName,
JSXOpeningElement,
JSXOpeningFragment,
JSXSpreadAttribute,
JSXText
}
function estreeToJs(estree) {
return astring.generate(estree, {generator: customGenerator})
}
// `attr="something"`
function JSXAttribute(node, state) {
state.write(' ')
this[node.name.type](node.name, state)
if (node.value !== undefined && node.value !== null) {
state.write('=')
// Encode double quotes in attribute values.
if (node.value.type === 'Literal') {
state.write(
'"' + encodeJsx(String(node.value.value)).replace(/"/g, '"') + '"',
node
)
} else {
this[node.value.type](node.value, state)
}
}
}
// `</div>`
function JSXClosingElement(node, state) {
this[node.name.type](node.name, state)
}
// `</>`
function JSXClosingFragment(node, state) {
state.write('</>')
}
// `<div></div>`
function JSXElement(node, state) {
state.write('<')
this[node.openingElement.type](node.openingElement, state)
if (node.closingElement) {
state.write('>')
let index = -1
while (++index < node.children.length) {
this[node.children[index].type](node.children[index], state)
}
state.write('</')
this[node.closingElement.type](node.closingElement, state)
state.write('>')
} else {
state.write(' />')
}
}
// `<></>`
function JSXFragment(node, state) {
this[node.openingFragment.type](node.openingElement, state)
let index = -1
while (++index < node.children.length) {
this[node.children[index].type](node.children[index], state)
}
// Incorrect tree.
/* c8 ignore next 3 */
if (!node.closingFragment) {
throw new Error('Cannot handle fragment w/o closing tag')
}
this[node.closingFragment.type](node.closingElement, state)
}
// `{}`
function JSXEmptyExpression() {}
// `{expression}`
function JSXExpressionContainer(node, state) {
state.write('{')
this[node.expression.type](node.expression, state)
state.write('}')
}
// `<div>`
function JSXOpeningElement(node, state) {
let index = -1
this[node.name.type](node.name, state)
while (++index < node.attributes.length) {
this[node.attributes[index].type](node.attributes[index], state)
}
}
// `<>`
function JSXOpeningFragment(node, state) {
state.write('<>')
}
// `div`
function JSXIdentifier(node, state) {
state.write(node.name)
}
// `member.expression`
function JSXMemberExpression(node, state) {
this[node.object.type](node.object, state)
state.write('.')
this[node.property.type](node.property, state)
}
// `ns:attr="something"`
// MDX (and most JSX things) dont support them.
// But keep it here just in case we might in the future.
/* c8 ignore next 5 */
function JSXNamespacedName(node, state) {
this[node.namespace.type](node.namespace, state)
state.write(':')
this[node.name.type](node.name, state)
}
// `{...argument}`
function JSXSpreadAttribute(node, state) {
state.write(' {')
/* eslint-disable-next-line new-cap */
this.SpreadElement(node, state)
state.write('}')
}
// `!`
function JSXText(node, state) {
// `raw` is currently always be set, but could be
// missing if something injects a `JSXText` into the tree.
// Preferring `raw` over `value` means character references are kept as-is.
/* c8 ignore next */
const value = node.raw || node.value
state.write(value)
}
/**
* Make sure that character references dont pop up.
* For example, the text `&copy;` should stay that way, and not turn into `©`.
* We could encode all `&` (easy but verbose) or look for actual valid
* references (complex but cleanest output).
* Looking for the 2nd character gives us a middle ground.
* The `#` is for (decimal and hexadecimal) numeric references, the letters
* are for the named references.
*
* @param {string} value
* @returns {string}
*/
function encodeJsx(value) {
return value.replace(/&(?=[#a-z])/gi, '&amp;')
}

View File

@ -1,55 +1,4 @@
const unified = require('unified')
const remarkParse = require('remark-parse')
const remarkMdx = require('remark-mdx')
const squeeze = require('remark-squeeze-paragraphs')
const minifyWhitespace = require('rehype-minify-whitespace')
const mdxAstToMdxHast = require('./mdx-ast-to-mdx-hast')
const mdxHastToJsx = require('./mdx-hast-to-jsx')
const pragma = `/* @jsxRuntime classic */
/* @jsx mdx */
/* @jsxFrag mdx.Fragment */`
function createMdxAstCompiler(options = {}) {
return unified()
.use(remarkParse)
.use(remarkMdx)
.use(squeeze)
.use(options.remarkPlugins)
.use(mdxAstToMdxHast)
}
function createCompiler(options = {}) {
return createMdxAstCompiler(options)
.use(options.rehypePlugins)
.use(minifyWhitespace, {newlines: true})
.use(mdxHastToJsx, options)
}
function createConfig(mdx, options) {
const config = {contents: mdx}
if (options.filepath) {
config.path = options.filepath
}
return config
}
function sync(mdx, options = {}) {
const file = createCompiler(options).processSync(createConfig(mdx, options))
return pragma + '\n' + String(file)
}
async function compile(mdx, options = {}) {
const file = await createCompiler(options).process(createConfig(mdx, options))
// V8 bug on Node 12.
/* c8 ignore next */
return pragma + '\n' + String(file)
}
module.exports = compile
compile.default = compile
compile.sync = sync
compile.createMdxAstCompiler = createMdxAstCompiler
compile.createCompiler = createCompiler
export {createProcessor} from './lib/core.js'
export {compile, compileSync} from './lib/compile.js'
export {evaluate, evaluateSync} from './lib/evaluate.js'
export {nodeTypes} from './lib/node-types.js'

View File

@ -0,0 +1,39 @@
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('vfile').VFile} VFile
* @typedef {import('./core.js').PluginOptions} PluginOptions
* @typedef {import('./core.js').BaseProcessorOptions} BaseProcessorOptions
* @typedef {Omit<BaseProcessorOptions, 'format'>} CoreProcessorOptions
*
* @typedef ExtraOptions
* @property {'detect'|'mdx'|'md'} [format='detect'] Format of `file`
*
* @typedef {CoreProcessorOptions & PluginOptions & ExtraOptions} CompileOptions
*/
import {createProcessor} from './core.js'
import {resolveFileAndOptions} from './util/resolve-file-and-options.js'
/**
* Compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
* @param {CompileOptions} [compileOptions]
* @return {Promise<VFile>}
*/
export function compile(vfileCompatible, compileOptions) {
const {file, options} = resolveFileAndOptions(vfileCompatible, compileOptions)
return createProcessor(options).process(file)
}
/**
* Synchronously compile MDX to JS.
*
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
* @param {CompileOptions} [compileOptions]
* @return {VFile}
*/
export function compileSync(vfileCompatible, compileOptions) {
const {file, options} = resolveFileAndOptions(vfileCompatible, compileOptions)
return createProcessor(options).processSync(file)
}

93
packages/mdx/lib/core.js Normal file
View File

@ -0,0 +1,93 @@
/**
* @typedef {import('unified').Processor} Processor
* @typedef {import('unified').PluggableList} PluggableList
* @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
* @property {boolean} [jsx=false] Whether to keep JSX
* @property {'mdx'|'md'} [format='mdx'] Format of the files to be processed
* @property {'program'|'function-body'} [outputFormat='program'] Whether to compile to a whole program or a function body.
* @property {string[]} [mdExtensions] Extensions (with `.`) for markdown
* @property {string[]} [mdxExtensions] Extensions (with `.`) for MDX
* @property {PluggableList} [recmaPlugins] List of recma (esast, JavaScript) plugins
* @property {PluggableList} [remarkPlugins] List of remark (mdast, markdown) plugins
* @property {PluggableList} [rehypePlugins] List of rehype (hast, HTML) plugins
*
* @typedef {Omit<RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
* @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
*/
import {unified} from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {recmaJsxBuild} from './plugin/recma-jsx-build.js'
import {recmaDocument} from './plugin/recma-document.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 {remarkMdx} from './plugin/remark-mdx.js'
import {nodeTypes} from './node-types.js'
/**
* Pipeline to:
*
* 1. Parse MDX (serialized markdown with embedded JSX, ESM, and expressions)
* 2. Transform through remark (mdast), rehype (hast), and recma (esast)
* 3. Serialize as JavaScript
*
* @param {ProcessorOptions} [options]
* @return {Processor}
*/
export function createProcessor(options = {}) {
const {
jsx,
format,
outputFormat,
providerImportSource,
recmaPlugins,
rehypePlugins,
remarkPlugins,
SourceMapGenerator,
...rest
} = options
// @ts-expect-error runtime.
if (format === 'detect') {
throw new Error(
"Incorrect `format: 'detect'`: `createProcessor` can support either `md` or `mdx`; it does not support detecting the format"
)
}
const pipeline = unified().use(remarkParse)
if (format !== 'md') {
pipeline.use(remarkMdx)
}
pipeline
.use(remarkMarkAndUnravel)
.use(remarkPlugins || [])
.use(remarkRehype, {allowDangerousHtml: true, passThrough: nodeTypes})
.use(rehypePlugins || [])
if (format === 'md') {
pipeline.use(rehypeRemoveRaw)
}
pipeline
.use(rehypeRecma)
.use(recmaDocument, {...rest, outputFormat})
.use(recmaJsxRewrite, {providerImportSource, outputFormat})
if (!jsx) {
pipeline.use(recmaJsxBuild, {outputFormat})
}
pipeline.use(recmaStringify, {SourceMapGenerator}).use(recmaPlugins || [])
return pipeline
}

View File

@ -0,0 +1,39 @@
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('./util/resolve-evaluate-options.js').EvaluateOptions} EvaluateOptions
*
* @typedef {{[name: string]: any}} ComponentMap
* @typedef {{[props: string]: any, components?: ComponentMap}} MDXContentProps
* @typedef {(props: MDXContentProps) => any} MDXContent
* @typedef {{[exports: string]: unknown, default: MDXContent}} ExportMap
*/
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 MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
* @param {EvaluateOptions} evaluateOptions
* @return {Promise<ExportMap>}
*/
export async function evaluate(vfileCompatible, evaluateOptions) {
const {compiletime, runtime} = resolveEvaluateOptions(evaluateOptions)
// V8 on Erbium.
/* c8 ignore next 2 */
return run(await compile(vfileCompatible, compiletime), runtime)
}
/**
* Synchronously evaluate MDX.
*
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
* @param {EvaluateOptions} evaluateOptions
* @return {ExportMap}
*/
export function evaluateSync(vfileCompatible, evaluateOptions) {
const {compiletime, runtime} = resolveEvaluateOptions(evaluateOptions)
return runSync(compileSync(vfileCompatible, compiletime), runtime)
}

View File

@ -0,0 +1,9 @@
// 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 = [
'mdxFlowExpression',
'mdxJsxFlowElement',
'mdxJsxTextElement',
'mdxTextExpression',
'mdxjsEsm'
]

View File

@ -0,0 +1,523 @@
/**
* @typedef {import('estree-jsx').Directive} Directive
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').SimpleLiteral} SimpleLiteral
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
* @typedef {import('estree-jsx').SpreadElement} SpreadElement
* @typedef {import('estree-jsx').Property} Property
*
* @typedef RecmaDocumentOptions
* @property {'program'|'function-body'} [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
* @property {boolean} [useDynamicImport=false] Whether to keep `import` (and `export … from`) statements or compile them to dynamic `import()` instead
* @property {string} [baseUrl] Resolve relative `import` (and `export … from`) relative to this URL
* @property {string} [pragma='React.createElement'] Pragma for JSX (used in classic runtime)
* @property {string} [pragmaFrag='React.Fragment'] Pragma for JSX fragments (used in classic runtime)
* @property {string} [pragmaImportSource='react'] Where to import the identifier of `pragma` from (used in classic runtime)
* @property {string} [jsxImportSource='react'] Place to import automatic JSX runtimes from (used in automatic runtime)
* @property {'automatic'|'classic'} [jsxRuntime='automatic'] JSX runtime to use
*/
import {URL} from 'url'
import {analyze} from 'periscopic'
import {stringifyPosition} from 'unist-util-stringify-position'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {create} from '../util/estree-util-create.js'
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
import {isDeclaration} from '../util/estree-util-is-declaration.js'
/**
* A plugin to wrap the estree in `MDXContent`.
*
* @type {import('unified').Plugin<[RecmaDocumentOptions]|[], Program>}
*/
export function recmaDocument(options = {}) {
const {
baseUrl,
useDynamicImport,
outputFormat = 'program',
pragma = 'React.createElement',
pragmaFrag = 'React.Fragment',
pragmaImportSource = 'react',
jsxImportSource = 'react',
jsxRuntime = 'automatic'
} = options
// eslint-disable-next-line complexity
return (tree, file) => {
/** @type {Array.<string|[string, string]>} */
const exportedIdentifiers = []
/** @type {Array.<Directive|Statement|ModuleDeclaration>} */
const replacement = []
/** @type {Array.<string>} */
const pragmas = []
let exportAllCount = 0
/** @type {ExportDefaultDeclaration|ExportSpecifier|undefined} */
let layout
/** @type {boolean|undefined} */
let content
/** @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)
}
if (jsxRuntime === 'automatic' && jsxImportSource) {
pragmas.push('@jsxImportSource ' + jsxImportSource)
}
if (jsxRuntime === 'classic' && pragma) {
pragmas.push('@jsx ' + pragma)
}
if (jsxRuntime === 'classic' && pragmaFrag) {
pragmas.push('@jsxFrag ' + pragmaFrag)
}
if (pragmas.length > 0) {
tree.comments.unshift({type: 'Block', value: pragmas.join(' ')})
}
if (jsxRuntime === 'classic' && pragmaImportSource) {
if (!pragma) {
throw new Error(
'Missing `pragma` in classic runtime with `pragmaImportSource`'
)
}
handleEsm({
type: 'ImportDeclaration',
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: {type: 'Identifier', name: pragma.split('.')[0]}
}
],
source: {type: 'Literal', value: pragmaImportSource}
})
}
// Find the `export default`, the JSX expression, and leave the rest
// (import/exports) as they are.
for (child of tree.body) {
// ```js
// export default props => <>{props.children}</>
// ```
//
// Treat it as an inline layout declaration.
if (child.type === 'ExportDefaultDeclaration') {
if (layout) {
file.fail(
'Cannot specify multiple layouts (previous: ' +
stringifyPosition(positionFromEstree(layout)) +
')',
positionFromEstree(child),
'recma-document:duplicate-layout'
)
}
layout = child
replacement.push({
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: {type: 'Identifier', name: 'MDXLayout'},
init: isDeclaration(child.declaration)
? declarationToExpression(child.declaration)
: child.declaration
}
]
})
}
// ```js
// export {a, b as c} from 'd'
// ```
else if (child.type === 'ExportNamedDeclaration' && child.source) {
/** @type {SimpleLiteral} */
// @ts-expect-error `ExportNamedDeclaration.source` can only be a string literal.
const source = child.source
// Remove `default` or `as default`, but not `default as`, specifier.
child.specifiers = child.specifiers.filter((specifier) => {
if (specifier.exported.name === 'default') {
if (layout) {
file.fail(
'Cannot specify multiple layouts (previous: ' +
stringifyPosition(positionFromEstree(layout)) +
')',
positionFromEstree(child),
'recma-document:duplicate-layout'
)
}
layout = specifier
// Make it just an import: `import MDXLayout from '…'`.
handleEsm(
create(specifier, {
type: 'ImportDeclaration',
specifiers: [
// Default as default / something else as default.
specifier.local.name === 'default'
? {
type: 'ImportDefaultSpecifier',
local: {type: 'Identifier', name: 'MDXLayout'}
}
: create(specifier.local, {
type: 'ImportSpecifier',
imported: specifier.local,
local: {type: 'Identifier', name: 'MDXLayout'}
})
],
source: create(source, {type: 'Literal', value: source.value})
})
)
return false
}
return true
})
// If there are other things imported, keep it.
if (child.specifiers.length > 0) {
handleExport(child)
}
}
// ```js
// export {a, b as c}
// export * from 'a'
// ```
else if (
child.type === 'ExportNamedDeclaration' ||
child.type === 'ExportAllDeclaration'
) {
handleExport(child)
} else if (child.type === 'ImportDeclaration') {
handleEsm(child)
} else if (
child.type === 'ExpressionStatement' &&
// @ts-expect-error types are wrong: `JSXElement`/`JSXFragment` are
// `Expression`s.
(child.expression.type === 'JSXFragment' ||
// @ts-expect-error "
child.expression.type === 'JSXElement')
) {
content = true
replacement.push(createMdxContent(child.expression))
// The following catch-all branch is because plugins mightve added
// other things.
// Normally, we only have import/export/jsx, but just add whatevers
// there.
/* c8 ignore next 3 */
} else {
replacement.push(child)
}
}
// If there was no JSX content at all, add an empty function.
if (!content) {
replacement.push(createMdxContent())
}
exportedIdentifiers.push(['MDXContent', 'default'])
if (outputFormat === 'function-body') {
replacement.push({
type: 'ReturnStatement',
argument: {
type: 'ObjectExpression',
properties: [
...Array.from({length: exportAllCount}).map(
/**
* @param {undefined} _
* @param {number} index
* @returns {SpreadElement}
*/
(_, index) => ({
type: 'SpreadElement',
argument: {type: 'Identifier', name: '_exportAll' + (index + 1)}
})
),
...exportedIdentifiers.map((d) => {
/** @type {Property} */
const prop = {
type: 'Property',
kind: 'init',
method: false,
computed: false,
shorthand: typeof d === 'string',
key: {
type: 'Identifier',
name: typeof d === 'string' ? d : d[1]
},
value: {
type: 'Identifier',
name: typeof d === 'string' ? d : d[0]
}
}
return prop
})
]
}
})
} else {
replacement.push({
type: 'ExportDefaultDeclaration',
declaration: {type: 'Identifier', name: 'MDXContent'}
})
}
tree.body = replacement
/**
* @param {ExportNamedDeclaration|ExportAllDeclaration} node
* @returns {void}
*/
function handleExport(node) {
if (node.type === 'ExportNamedDeclaration') {
// ```js
// export function a() {}
// export class A {}
// export var a = 1
// ```
if (node.declaration) {
exportedIdentifiers.push(
...analyze(node.declaration).scope.declarations.keys()
)
}
// ```js
// export {a, b as c}
// export {a, b as c} from 'd'
// ```
for (child of node.specifiers) {
exportedIdentifiers.push(child.exported.name)
}
}
handleEsm(node)
}
/**
* @param {ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration} node
* @returns {void}
*/
function handleEsm(node) {
// Rewrite the source of the `import` / `export … from`.
// See: <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
if (baseUrl && node.source) {
let value = String(node.source.value)
try {
// A full valid URL.
value = String(new URL(value))
} catch {
// Relative: `/example.js`, `./example.js`, and `../example.js`.
if (/^\.{0,2}\//.test(value)) {
value = String(new URL(value, baseUrl))
}
// Otherwise, its a bare specifiers.
// For example `some-package`, `@some-package`, and
// `some-package/path`.
// These are supported in Node and browsers plan to support them
// with import maps (<https://github.com/WICG/import-maps>).
}
node.source = create(node.source, {type: 'Literal', value})
}
/** @type {Statement|ModuleDeclaration|undefined} */
let replace
/** @type {Expression} */
let init
if (outputFormat === 'function-body') {
if (
// Always have a source:
node.type === 'ImportDeclaration' ||
node.type === 'ExportAllDeclaration' ||
// Source optional:
(node.type === 'ExportNamedDeclaration' && node.source)
) {
if (!useDynamicImport) {
file.fail(
'Cannot use `import` or `export … from` in `evaluate` (outputting a function body) by default: please set `useDynamicImport: true` (and probably specify a `baseUrl`)',
positionFromEstree(node),
'recma-document:invalid-esm-statement'
)
}
// Just for types.
/* c8 ignore next 3 */
if (!node.source) {
throw new Error('Expected `node.source` to be defined')
}
// ```
// import 'a'
// //=> await import('a')
// import a from 'b'
// //=> const {default: a} = await import('b')
// export {a, b as c} from 'd'
// //=> const {a, c: b} = await import('d')
// export * from 'a'
// //=> const _exportAll0 = await import('a')
// ```
init = {
type: 'AwaitExpression',
argument: create(node, {
type: 'ImportExpression',
source: node.source
})
}
if (
(node.type === 'ImportDeclaration' ||
node.type === 'ExportNamedDeclaration') &&
node.specifiers.length === 0
) {
replace = {type: 'ExpressionStatement', expression: init}
} else {
replace = {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id:
node.type === 'ImportDeclaration' ||
node.type === 'ExportNamedDeclaration'
? specifiersToObjectPattern(node.specifiers)
: {
type: 'Identifier',
name: '_exportAll' + ++exportAllCount
},
init
}
]
}
}
} else if (node.declaration) {
replace = node.declaration
} 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
}))
if (declarators.length > 0) {
replace = {
type: 'VariableDeclaration',
kind: 'const',
declarations: declarators
}
}
}
} else {
replace = node
}
if (replace) {
replacement.push(replace)
}
}
}
/**
* @param {Expression} [content]
* @returns {FunctionDeclaration}
*/
function createMdxContent(content) {
/** @type {JSXElement} */
const element = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {type: 'JSXIdentifier', name: 'MDXLayout'},
attributes: [
{
type: 'JSXSpreadAttribute',
argument: {type: 'Identifier', name: 'props'}
}
],
selfClosing: false
},
closingElement: {
type: 'JSXClosingElement',
name: {type: 'JSXIdentifier', name: 'MDXLayout'}
},
children: [
{
type: 'JSXExpressionContainer',
expression: {type: 'Identifier', name: '_content'}
}
]
}
/** @type {Expression} */
// @ts-expect-error types are wrong: `JSXElement` is an `Expression`.
const consequent = element
return {
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: 'MDXContent'},
params: [
{
type: 'AssignmentPattern',
left: {type: 'Identifier', name: 'props'},
right: {type: 'ObjectExpression', properties: []}
}
],
body: {
type: 'BlockStatement',
body: [
{
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: {type: 'Identifier', name: '_content'},
init: content || {type: 'Literal', value: null}
}
]
},
{
type: 'ReturnStatement',
argument: {
type: 'ConditionalExpression',
test: {type: 'Identifier', name: 'MDXLayout'},
consequent,
alternate: {type: 'Identifier', name: '_content'}
}
}
]
}
}
}
}

View File

@ -0,0 +1,52 @@
/**
* @typedef {import('estree-jsx').Program} Program
*
* @typedef RecmaJsxBuildOptions
* @property {'program'|'function-body'} [outputFormat='program'] Whether to keep the import of the automatic runtime or get it from `arguments[0]` instead
*/
import {buildJsx} from 'estree-util-build-jsx'
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
/**
* A plugin to build JSX into function calls.
* `estree-util-build-jsx` does all the work for us!
*
* @type {import('unified').Plugin<[RecmaJsxBuildOptions]|[], Program>}
*/
export function recmaJsxBuild(options = {}) {
const {outputFormat} = options
return (tree) => {
buildJsx(tree)
// When compiling to a function body, replace the import that was just
// generated, and get `jsx`, `jsxs`, and `Fragment` from `arguments[0]`
// instead.
if (
outputFormat === 'function-body' &&
tree.body[0] &&
tree.body[0].type === 'ImportDeclaration' &&
typeof tree.body[0].source.value === 'string' &&
/\/jsx-runtime$/.test(tree.body[0].source.value)
) {
tree.body[0] = {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: specifiersToObjectPattern(tree.body[0].specifiers),
init: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'arguments'},
property: {type: 'Literal', value: 0},
computed: true,
optional: false
}
}
]
}
}
}
}

View File

@ -0,0 +1,365 @@
/**
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').Function} ESFunction
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
* @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').Property} Property
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*
* @typedef {import('estree-walker').SyncHandler} WalkHandler
*
* @typedef {import('periscopic').Scope & {node: Node}} Scope
*
* @typedef RecmaJsxRewriteOptions
* @property {'program'|'function-body'} [outputFormat='program'] Whether to use an import statement or `arguments[0]` to get the provider
* @property {string} [providerImportSource] Place to import a provider from
*
* @typedef StackEntry
* @property {Array.<string>} objects
* @property {Array.<string>} components
* @property {Array.<string>} tags
* @property {ESFunction} node
*/
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
import {walk} from 'estree-walker'
import {analyze} from 'periscopic'
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
/**
* A plugin that rewrites JSX in functions to accept components as
* `props.components` (when the function is called `MDXContent`), or from
* a provider (if there is one).
* 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]|[], Program>}
*/
export function recmaJsxRewrite(options = {}) {
const {providerImportSource, outputFormat} = options
return (tree) => {
// Find everything thats defined in the top-level scope.
const scopeInfo = analyze(tree)
/** @type {Array.<StackEntry>} */
const fnStack = []
/** @type {boolean|undefined} */
let importProvider
/** @type {Scope|null} */
let currentScope
walk(tree, {
// eslint-disable-next-line complexity
enter(_node) {
const node = /** @type {Node} */ (_node)
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
fnStack.push({objects: [], components: [], tags: [], node})
}
const fnScope = fnStack[0]
if (
!fnScope ||
(!isMdxContent(fnScope.node) && !providerImportSource)
) {
return
}
const newScope = /** @type {Scope|undefined} */ (
// @ts-expect-error: periscopic doesnt support JSX.
scopeInfo.map.get(node)
)
if (newScope) {
newScope.node = node
currentScope = newScope
}
if (currentScope && node.type === 'JSXElement') {
let name = node.openingElement.name
// `<x.y>`, `<Foo.Bar>`, `<x.y.z>`.
if (name.type === 'JSXMemberExpression') {
// Find the left-most identifier.
while (name.type === 'JSXMemberExpression') name = name.object
const id = name.name
if (!fnScope.objects.includes(id) && !inScope(currentScope, id)) {
fnScope.objects.push(id)
}
}
// `<xml:thing>`.
else if (name.type === 'JSXNamespacedName') {
// Ignore namespaces.
}
// If the name is a valid ES identifier, and it doesnt start with a
// lowercase letter, its a component.
// For example, `$foo`, `_bar`, `Baz` are all component names.
// But `foo` and `b-ar` are tag names.
else if (isIdentifierName(name.name) && !/^[a-z]/.test(name.name)) {
const id = name.name
if (
!fnScope.components.includes(id) &&
!inScope(currentScope, id)
) {
fnScope.components.push(id)
}
}
// @ts-expect-error Allow fields passed through from mdast through hast to
// esast.
else if (node.data && node.data._xdmExplicitJsx) {
// 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>`.
} else {
const id = name.name
if (!fnScope.tags.includes(id)) {
fnScope.tags.push(id)
}
node.openingElement.name = {
type: 'JSXMemberExpression',
object: {type: 'JSXIdentifier', name: '_components'},
property: name
}
if (node.closingElement) {
node.closingElement.name = {
type: 'JSXMemberExpression',
object: {type: 'JSXIdentifier', name: '_components'},
property: {type: 'JSXIdentifier', name: id}
}
}
}
}
},
leave(node) {
/** @type {Array.<Property>} */
const defaults = []
/** @type {Array.<string>} */
const actual = []
/** @type {Array.<Expression>} */
const parameters = []
/** @type {Array.<VariableDeclarator>} */
const declarations = []
if (currentScope && currentScope.node === node) {
// @ts-expect-error: `node`s were patched when entering.
currentScope = currentScope.parent
}
if (
node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
const fn = /** @type {ESFunction} */ (node)
const scope = fnStack[fnStack.length - 1]
/** @type {string} */
let name
for (name of scope.tags) {
defaults.push({
type: 'Property',
kind: 'init',
key: {type: 'Identifier', name},
value: {type: 'Literal', value: name},
method: false,
shorthand: false,
computed: false
})
}
actual.push(...scope.components)
for (name of scope.objects) {
// In some cases, a component is used directly (`<X>`) but its also
// used as an object (`<X.Y>`).
if (!actual.includes(name)) {
actual.push(name)
}
}
if (defaults.length > 0 || actual.length > 0) {
parameters.push({type: 'ObjectExpression', properties: defaults})
if (providerImportSource) {
importProvider = true
parameters.push({
type: 'CallExpression',
callee: {type: 'Identifier', name: '_provideComponents'},
arguments: [],
optional: false
})
}
// Accept `components` as a prop if this is the `MDXContent` function.
if (isMdxContent(scope.node)) {
parameters.push({
type: 'MemberExpression',
object: {type: 'Identifier', name: 'props'},
property: {type: 'Identifier', name: 'components'},
computed: false,
optional: false
})
}
declarations.push({
type: 'VariableDeclarator',
id: {type: 'Identifier', name: '_components'},
init: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'Object'},
property: {type: 'Identifier', name: 'assign'},
computed: false,
optional: false
},
arguments: parameters,
optional: false
}
})
// Add components to scope.
// For `['MyComponent', 'MDXLayout']` this generates:
// ```js
// const {MyComponent, wrapper: MDXLayout} = _components
// ```
// Note that MDXLayout is special as its taken from
// `_components.wrapper`.
if (actual.length > 0) {
declarations.push({
type: 'VariableDeclarator',
id: {
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
}))
},
init: {type: 'Identifier', name: '_components'}
})
}
// Arrow functions with an implied return:
if (fn.body.type !== 'BlockStatement') {
fn.body = {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument: fn.body}]
}
}
fn.body.body.unshift({
type: 'VariableDeclaration',
kind: 'const',
declarations
})
}
fnStack.pop()
}
}
})
// If a provider is used (and can be used), import it.
if (importProvider && providerImportSource) {
tree.body.unshift(
createImportProvider(providerImportSource, outputFormat)
)
}
}
}
/**
* @param {string} providerImportSource
* @param {RecmaJsxRewriteOptions['outputFormat']} outputFormat
* @returns {Statement|ModuleDeclaration}
*/
function createImportProvider(providerImportSource, outputFormat) {
/** @type {Array<ImportSpecifier>} */
const specifiers = [
{
type: 'ImportSpecifier',
imported: {type: 'Identifier', name: 'useMDXComponents'},
local: {type: 'Identifier', name: '_provideComponents'}
}
]
return outputFormat === 'function-body'
? {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: specifiersToObjectPattern(specifiers),
init: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'arguments'},
property: {type: 'Literal', value: 0},
computed: true,
optional: false
}
}
]
}
: {
type: 'ImportDeclaration',
specifiers,
source: {type: 'Literal', value: providerImportSource}
}
}
/**
* @param {ESFunction} [node]
* @returns {boolean}
*/
function isMdxContent(node) {
return Boolean(
node && 'id' in node && node.id && node.id.name === 'MDXContent'
)
}
/**
* @param {Scope} scope
* @param {string} id
*/
function inScope(scope, id) {
/** @type {Scope|null} */
let currentScope = scope
while (currentScope) {
if (currentScope.declarations.has(id)) {
return true
}
// @ts-expect-error: `node`s have been added when entering.
currentScope = currentScope.parent
}
return false
}

View File

@ -0,0 +1,340 @@
/**
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').JSXAttribute} JSXAttribute
* @typedef {import('estree-jsx').JSXClosingElement} JSXClosingElement
* @typedef {import('estree-jsx').JSXClosingFragment} JSXClosingFragment
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').JSXEmptyExpression} JSXEmptyExpression
* @typedef {import('estree-jsx').JSXExpressionContainer} JSXExpressionContainer
* @typedef {import('estree-jsx').JSXFragment} JSXFragment
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
* @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
* @typedef {import('estree-jsx').JSXOpeningElement} JSXOpeningElement
* @typedef {import('estree-jsx').JSXOpeningFragment} JSXOpeningFragment
* @typedef {import('estree-jsx').JSXSpreadAttribute} JSXSpreadAttribute
* @typedef {import('estree-jsx').JSXText} JSXText
* @typedef {import('vfile').VFile} VFile
* @typedef {typeof import('source-map').SourceMapGenerator} SourceMapGenerator
*
* @typedef {Omit<import('astring').State, 'write'> & {write: ((code: string, node?: Node) => void)}} State
*
* @typedef {{[K in Node['type']]: (node: Node, state: State) => void}} Generator
*
* @typedef RecmaStringifyOptions
* @property {SourceMapGenerator} [SourceMapGenerator] Generate a source map by passing a `SourceMapGenerator` from `source-map` in
*/
// @ts-expect-error baseGenerator is not yet exported by astring typings
import {baseGenerator, generate} from 'astring'
/**
* A plugin that adds an esast compiler: a small wrapper around `astring` to add
* support for serializing JSX.
*
* @type {import('unified').Plugin<[RecmaStringifyOptions]|[], Program, string>}
*/
export function recmaStringify(options = {}) {
const {SourceMapGenerator} = options
Object.assign(this, {Compiler: compiler})
/** @type {import('unified').CompilerFunction<Program, string>} */
function compiler(tree, file) {
/** @type {InstanceType<SourceMapGenerator>|undefined} */
let sourceMap
if (SourceMapGenerator) {
sourceMap = new SourceMapGenerator({file: file.path || 'unknown.mdx'})
}
const result = generate(tree, {
generator: { ...baseGenerator, JSXAttribute,
JSXClosingElement,
JSXClosingFragment,
JSXElement,
JSXEmptyExpression,
JSXExpressionContainer,
JSXFragment,
JSXIdentifier,
JSXMemberExpression,
JSXNamespacedName,
JSXOpeningElement,
JSXOpeningFragment,
JSXSpreadAttribute,
JSXText},
comments: true,
sourceMap
})
if (sourceMap) {
file.map = sourceMap.toJSON()
}
return result
}
}
/**
* `attr`
* `attr="something"`
* `attr={1}`
*
* @this {Generator}
* @param {JSXAttribute} node
* @param {State} state
* @returns {void}
*/
function JSXAttribute(node, state) {
this[node.name.type](node.name, state)
if (node.value !== undefined && node.value !== null) {
state.write('=')
// Encode double quotes in attribute values.
if (node.value.type === 'Literal') {
state.write(
'"' + encodeJsx(String(node.value.value)).replace(/"/g, '&quot;') + '"',
node
)
} else {
this[node.value.type](node.value, state)
}
}
}
/**
* `</div>`
*
* @this {Generator}
* @param {JSXClosingElement} node
* @param {State} state
* @returns {void}
*/
function JSXClosingElement(node, state) {
state.write('</')
this[node.name.type](node.name, state)
state.write('>')
}
/**
* `</>`
*
* @this {Generator}
* @param {JSXClosingFragment} node
* @param {State} state
* @returns {void}
*/
function JSXClosingFragment(node, state) {
state.write('</>', node)
}
/**
* `<div />`
* `<div></div>`
*
* @this {Generator}
* @param {JSXElement} node
* @param {State} state
* @returns {void}
*/
function JSXElement(node, state) {
let index = -1
this[node.openingElement.type](node.openingElement, state)
if (node.children) {
while (++index < node.children.length) {
const child = node.children[index]
// Supported in types but not by Acorn.
/* c8 ignore next 3 */
if (child.type === 'JSXSpreadChild') {
throw new Error('JSX spread children are not supported')
}
this[child.type](child, state)
}
}
if (node.closingElement) {
this[node.closingElement.type](node.closingElement, state)
}
}
/**
* `{}` (always in a `JSXExpressionContainer`, which does the curlies)
*
* @this {Generator}
* @returns {void}
*/
function JSXEmptyExpression() {}
/**
* `{expression}`
*
* @this {Generator}
* @param {JSXExpressionContainer} node
* @param {State} state
* @returns {void}
*/
function JSXExpressionContainer(node, state) {
state.write('{')
this[node.expression.type](node.expression, state)
state.write('}')
}
/**
* `<></>`
*
* @this {Generator}
* @param {JSXFragment} node
* @param {State} state
* @returns {void}
*/
function JSXFragment(node, state) {
let index = -1
this[node.openingFragment.type](node.openingFragment, state)
if (node.children) {
while (++index < node.children.length) {
const child = node.children[index]
// Supported in types but not by Acorn.
/* c8 ignore next 3 */
if (child.type === 'JSXSpreadChild') {
throw new Error('JSX spread children are not supported')
}
this[child.type](child, state)
}
}
this[node.closingFragment.type](node.closingFragment, state)
}
/**
* `div`
*
* @this {Generator}
* @param {JSXIdentifier} node
* @param {State} state
* @returns {void}
*/
function JSXIdentifier(node, state) {
state.write(node.name, node)
}
/**
* `member.expression`
*
* @this {Generator}
* @param {JSXMemberExpression} node
* @param {State} state
* @returns {void}
*/
function JSXMemberExpression(node, state) {
this[node.object.type](node.object, state)
state.write('.')
this[node.property.type](node.property, state)
}
/**
* `ns:name`
*
* @this {Generator}
* @param {JSXNamespacedName} node
* @param {State} state
* @returns {void}
*/
function JSXNamespacedName(node, state) {
this[node.namespace.type](node.namespace, state)
state.write(':')
this[node.name.type](node.name, state)
}
/**
* `<div>`
*
* @this {Generator}
* @param {JSXOpeningElement} node
* @param {State} state
* @returns {void}
*/
function JSXOpeningElement(node, state) {
let index = -1
state.write('<')
this[node.name.type](node.name, state)
if (node.attributes) {
while (++index < node.attributes.length) {
state.write(' ')
this[node.attributes[index].type](node.attributes[index], state)
}
}
state.write(node.selfClosing ? ' />' : '>')
}
/**
* `<>`
*
* @this {Generator}
* @param {JSXOpeningFragment} node
* @param {State} state
* @returns {void}
*/
function JSXOpeningFragment(node, state) {
state.write('<>', node)
}
/**
* `{...argument}`
*
* @this {Generator}
* @param {JSXSpreadAttribute} node
* @param {State} state
* @returns {void}
*/
function JSXSpreadAttribute(node, state) {
state.write('{')
// eslint-disable-next-line new-cap
this.SpreadElement(node, state)
state.write('}')
}
/**
* `!`
*
* @this {Generator}
* @param {JSXText} node
* @param {State} state
* @returns {void}
*/
function JSXText(node, state) {
state.write(
encodeJsx(node.value).replace(/<|{/g, ($0) =>
$0 === '<' ? '&lt;' : '&#123;'
),
node
)
}
/**
* Make sure that character references dont pop up.
* For example, the text `&copy;` should stay that way, and not turn into `©`.
* We could encode all `&` (easy but verbose) or look for actual valid
* references (complex but cleanest output).
* Looking for the 2nd character gives us a middle ground.
* The `#` is for (decimal and hexadecimal) numeric references, the letters
* are for the named references.
*
* @param {string} value
* @returns {string}
*/
function encodeJsx(value) {
return value.replace(/&(?=[#a-z])/gi, '&amp;')
}

View File

@ -0,0 +1,16 @@
/**
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('hast').Root} Root
*/
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<void[], Root, Program>}
*/
export function rehypeRecma() {
return (tree) => toEstree(tree)
}

View File

@ -0,0 +1,23 @@
/**
* @typedef {import('hast').Root} Root
*/
import {visit} from 'unist-util-visit'
/**
* A tiny plugin that removes raw HTML.
* This is needed if the format is `md` and `rehype-raw` was not used to parse
* dangerous HTML into nodes.
*
* @type {import('unified').Plugin<void[], Root>}
*/
export function rehypeRemoveRaw() {
return (tree) => {
visit(tree, 'raw', (_, index, parent) => {
if (parent && typeof index === 'number') {
parent.children.splice(index, 1)
return index
}
})
}
}

View File

@ -0,0 +1,82 @@
/**
* @typedef {import('mdast').Root} Root
* @typedef {import('mdast').Content} Content
* @typedef {Root|Content} Node
* @typedef {Extract<Node, import('unist').Parent>} Parent
*
* @typedef {import('./remark-mdx.js')} DoNotTouchAsThisImportIncludesMdxInTree
*/
import {visit} from 'unist-util-visit'
/**
* A tiny plugin that unravels `<p><h1>x</h1></p>` but also
* `<p><Component /></p>` (so it has no knowledge of HTML).
* 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<void[], Root>}
*/
export function remarkMarkAndUnravel() {
return (tree) => {
visit(tree, (node, index, parent_) => {
const parent = /** @type {Parent} */ (parent_)
let offset = -1
let all = true
/** @type {boolean|undefined} */
let oneOrMore
if (parent && typeof index === 'number' && node.type === 'paragraph') {
const children = node.children
while (++offset < children.length) {
const child = children[offset]
if (
child.type === 'mdxJsxTextElement' ||
child.type === 'mdxTextExpression'
) {
oneOrMore = true
} else if (
child.type === 'text' &&
/^[\t\r\n ]+$/.test(String(child.value))
) {
// Empty.
} else {
all = false
break
}
}
if (all && oneOrMore) {
offset = -1
while (++offset < children.length) {
const child = children[offset]
if (child.type === 'mdxJsxTextElement') {
// @ts-expect-error: content model is fine.
child.type = 'mdxJsxFlowElement'
}
if (child.type === 'mdxTextExpression') {
// @ts-expect-error: content model is fine.
child.type = 'mdxFlowExpression'
}
}
parent.children.splice(index, 1, ...children)
return index
}
}
if (
node.type === 'mdxJsxFlowElement' ||
node.type === 'mdxJsxTextElement'
) {
const data = node.data || (node.data = {})
data._xdmExplicitJsx = true
}
})
}
}

View File

@ -0,0 +1,36 @@
/**
* @typedef {import('mdast').Root} Root
* @typedef {import('micromark-extension-mdxjs').Options} Options
*
* @typedef {import('mdast-util-mdx')} DoNotTouchAsThisImportIncludesMdxInTree
*/
import {mdxjs} from 'micromark-extension-mdxjs'
import {mdxFromMarkdown, mdxToMarkdown} from 'mdast-util-mdx'
/**
* Add the micromark and mdast extensions for MDX.js (JS aware MDX).
*
* @type {import('unified').Plugin<[Options?]|[], Root>}
*/
export function remarkMdx(options = {}) {
const data = this.data()
add('micromarkExtensions', mdxjs(options))
add('fromMarkdownExtensions', mdxFromMarkdown)
add('toMarkdownExtensions', mdxToMarkdown)
/**
* @param {string} field
* @param {unknown} value
*/
function add(field, value) {
const list = /** @type {unknown[]} */ (
// Other extensions
/* c8 ignore next 2 */
data[field] ? data[field] : (data[field] = [])
)
list.push(value)
}
}

31
packages/mdx/lib/run.js Normal file
View File

@ -0,0 +1,31 @@
/**
* @typedef {import('vfile').VFile} VFile
*/
/** @type {new (code: string, ...args: unknown[]) => Function} **/
const AsyncFunction = Object.getPrototypeOf(run).constructor
/**
* Asynchronously run code.
*
* @param {VFile} file JS document to run
* @param {unknown} options
* @return {Promise<*>}
*/
export async function run(file, options) {
// V8 on Erbium.
/* c8 ignore next 2 */
return new AsyncFunction(String(file))(options)
}
/**
* Synchronously run code.
*
* @param {VFile} file JS document to run
* @param {unknown} options
* @return {*}
*/
export function runSync(file, options) {
// eslint-disable-next-line no-new-func
return new Function(String(file))(options)
}

View File

@ -0,0 +1,82 @@
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('vfile').VFile} VFile
* @typedef {import('unified').Processor} Processor
* @typedef {import('../compile.js').CompileOptions} CompileOptions
*/
import {createProcessor} from '../core.js'
import {md, mdx} from './extnames.js'
import {resolveFileAndOptions} from './resolve-file-and-options.js'
/**
* Create smart processors to handle different formats.
*
* @param {CompileOptions} [compileOptions]
* @return {{extnames: string[], process: process, processSync: processSync}}
*/
export function createFormatAwareProcessors(compileOptions = {}) {
const mdExtensions = compileOptions.mdExtensions || md
const mdxExtensions = compileOptions.mdxExtensions || mdx
/** @type {Processor} */
let cachedMarkdown
/** @type {Processor} */
let cachedMdx
return {
extnames:
compileOptions.format === 'md'
? mdExtensions
: compileOptions.format === 'mdx'
? mdxExtensions
: mdExtensions.concat(mdxExtensions),
process,
processSync
}
/**
* Smart processor.
*
* @param {VFileCompatible} vfileCompatible MDX or markdown document
* @return {Promise<VFile>}
*/
function process(vfileCompatible) {
const {file, processor} = split(vfileCompatible)
return processor.process(file)
}
/**
* Sync smart processor.
*
* @param {VFileCompatible} vfileCompatible MDX or markdown document
* @return {VFile}
*/
// 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)
}
/**
* Make a full vfile from whats given, and figure out which processor
* should be used for it.
* 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 MDX or markdown document
* @return {{file: VFile, processor: Processor}}
*/
function split(vfileCompatible) {
const {file, options} = resolveFileAndOptions(
vfileCompatible,
compileOptions
)
const processor =
options.format === 'md'
? cachedMarkdown || (cachedMarkdown = createProcessor(options))
: cachedMdx || (cachedMdx = createProcessor(options))
return {file, processor}
}
}

View File

@ -0,0 +1,27 @@
/**
* @typedef {import('estree-jsx').Node} Node
*/
/**
* @template {Node} N
* @param {Node} template
* @param {N} node
* @returns {N}
*/
export function create(template, node) {
/** @type {Array<keyof template>} */
// @ts-expect-error: `start`, `end`, `comments` are custom Acorn fields.
const fields = ['start', 'end', 'loc', 'range', 'comments']
let index = -1
while (++index < fields.length) {
const field = fields[index]
if (field in template) {
// @ts-expect-error: assume theyre settable.
node[field] = template[field]
}
}
return node
}

View File

@ -0,0 +1,27 @@
/**
* @typedef {import('estree-jsx').Declaration} Declaration
* @typedef {import('estree-jsx').Expression} Expression
*/
/**
* Turn a declaration into an expression.
* Doesnt work for variable declarations, but thats fine for our use case
* because currently were using this utility for export default declarations,
* which cant contain variable declarations.
*
* @param {Declaration} declaration
* @returns {Expression}
*/
export function declarationToExpression(declaration) {
if (declaration.type === 'FunctionDeclaration') {
return {...declaration, type: 'FunctionExpression'}
}
if (declaration.type === 'ClassDeclaration') {
return {...declaration, type: 'ClassExpression'}
/* c8 ignore next 4 */
}
// Probably `VariableDeclaration`.
throw new Error('Cannot turn `' + declaration.type + '` into an expression')
}

View File

@ -0,0 +1,18 @@
/**
* @typedef {import('estree-jsx').Declaration} Declaration
*/
/**
* @param {unknown} node
* @returns {node is Declaration}
*/
export function isDeclaration(node) {
/** @type {string} */
// @ts-expect-error Hush typescript, looks like `type` is available.
const type = node && typeof node === 'object' && node.type
return Boolean(
type === 'FunctionDeclaration' ||
type === 'ClassDeclaration' ||
type === 'VariableDeclaration'
)
}

View File

@ -0,0 +1,46 @@
/**
* @typedef {import('estree-jsx').Identifier} Identifier
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportNamespaceSpecifier} ImportNamespaceSpecifier
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').ObjectPattern} ObjectPattern
*/
import {create} from './estree-util-create.js'
/**
* @param {Array<ImportSpecifier|ImportDefaultSpecifier|ImportNamespaceSpecifier|ExportSpecifier>} specifiers
* @returns {ObjectPattern}
*/
export function specifiersToObjectPattern(specifiers) {
return {
type: 'ObjectPattern',
properties: specifiers.map((specifier) => {
/** @type {Identifier} */
let key =
specifier.type === 'ImportSpecifier'
? specifier.imported
: specifier.type === 'ExportSpecifier'
? specifier.exported
: {type: 'Identifier', name: 'default'}
let value = specifier.local
// Switch them around if were exporting.
if (specifier.type === 'ExportSpecifier') {
value = key
key = specifier.local
}
return create(specifier, {
type: 'Property',
kind: 'init',
shorthand: key.name === value.name,
method: false,
computed: false,
key,
value
})
})
}
}

View File

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

View File

@ -0,0 +1,6 @@
// @ts-expect-error: untyped.
import markdownExtensions from 'markdown-extensions'
export const mdx = ['.mdx']
/** @type {string[]} */
export const md = markdownExtensions.map((/** @type {string} */ d) => '.' + d)

View File

@ -0,0 +1,36 @@
/**
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
*
* @typedef RunnerOptions
* @property {*} Fragment Symbol to use for fragments
* @property {*} jsx Function to generate an element with static children
* @property {*} jsxs Function to generate an element with dynamic children
* @property {*} [useMDXComponents] Function to get `MDXComponents` from context
*
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource' | 'outputFormat'> } EvaluateProcessorOptions
*
* @typedef {EvaluateProcessorOptions & RunnerOptions} EvaluateOptions
*/
/**
* Split compiletime options from runtime options.
*
* @param {EvaluateOptions} options
* @returns {{compiletime: ProcessorOptions, runtime: RunnerOptions}}
*/
export function resolveEvaluateOptions(options) {
const {Fragment, jsx, jsxs, useMDXComponents, ...rest} = options || {}
if (!Fragment) throw new Error('Expected `Fragment` given to `evaluate`')
if (!jsx) throw new Error('Expected `jsx` given to `evaluate`')
if (!jsxs) throw new Error('Expected `jsxs` given to `evaluate`')
return {
compiletime: {
...rest,
outputFormat: 'function-body',
providerImportSource: useMDXComponents ? '#' : undefined
},
runtime: {Fragment, jsx, jsxs, useMDXComponents}
}
}

View File

@ -0,0 +1,48 @@
/**
* @typedef {import('vfile').VFileCompatible} VFileCompatible
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
* @typedef {import('../compile.js').CompileOptions} CompileOptions
*/
import {VFile} from 'vfile'
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} [options]
* @returns {{file: VFile, options: ProcessorOptions}}
*/
export function resolveFileAndOptions(vfileCompatible, options) {
const file = looksLikeAVFile(vfileCompatible)
? vfileCompatible
: new VFile(vfileCompatible)
const {format, ...rest} = options || {}
return {
file,
options: {
format:
format === 'md' || format === 'mdx'
? format
: file.extname && (rest.mdExtensions || md).includes(file.extname)
? 'md'
: 'mdx',
...rest
}
}
}
/**
* @param {VFileCompatible} [value]
* @returns {value is VFile}
*/
function looksLikeAVFile(value) {
return Boolean(
value &&
typeof value === 'object' &&
'message' in value &&
'messages' in value
)
}

View File

@ -1,67 +0,0 @@
const toHast = require('mdast-util-to-hast')
const detab = require('detab')
const u = require('unist-builder')
function mdxAstToMdxHast() {
return tree =>
toHast(tree, {
passThrough: [
'mdxFlowExpression',
'mdxJsxFlowElement',
'mdxJsxTextElement',
'mdxTextExpression',
'mdxjsEsm'
],
handlers: {
// Use a custom `inlineCode` element for inline code.
inlineCode(h, node) {
return h(node, 'inlineCode', [{type: 'text', value: node.value}])
},
// Add a custom `metastring` attribute to `code` elements,
// and support it also as a key/value attribute setter.
code(h, node) {
const value = node.value ? detab(node.value + '\n') : ''
const {lang} = node
const props = {}
if (lang) {
props.className = ['language-' + lang]
}
props.metastring = node.meta
// To do: this handling seems not what users expect:
// <https://github.com/mdx-js/mdx/issues/702>.
const meta =
node.meta &&
node.meta.split(' ').reduce((acc, cur) => {
if (cur.split('=').length > 1) {
const t = cur.split('=')
acc[t[0]] = t[1]
return acc
}
acc[cur] = true
return acc
}, {})
if (meta) {
Object.keys(meta).forEach(key => {
const isClassKey = key === 'class' || key === 'className'
if (props.className && isClassKey) {
props.className.push(meta[key])
} else {
props[key] = meta[key]
}
})
}
return h(node.position, 'pre', [
h(node, 'code', props, [u('text', value)])
])
}
}
})
}
module.exports = mdxAstToMdxHast

View File

@ -1,397 +0,0 @@
const toEstree = require('hast-util-to-estree')
const {walk} = require('estree-walker')
const periscopic = require('periscopic')
const estreeToJs = require('./estree-to-js')
function serializeEstree(estree, options) {
const {
// Default options
skipExport = false,
wrapExport
} = options
let layout
let children = []
let mdxLayoutDefault
// Find the `export default`, the JSX expression, and leave the rest
// (import/exports) as they are.
estree.body = estree.body.filter(child => {
// ```js
// export default a = 1
// ```
if (child.type === 'ExportDefaultDeclaration') {
layout = child.declaration
return false
}
// ```js
// export {default} from "a"
// export {default as a} from "b"
// export {default as a, b} from "c"
// export {a as default} from "b"
// export {a as default, b} from "c"
// ```
if (child.type === 'ExportNamedDeclaration' && child.source) {
// Remove `default` or `as default`, but not `default as`, specifier.
child.specifiers = child.specifiers.filter(specifier => {
if (specifier.exported.name === 'default') {
mdxLayoutDefault = {local: specifier.local, source: child.source}
return false
}
return true
})
// Keep the export if there are other specifiers, drop it if there was
// just a default.
return child.specifiers.length > 0
}
if (
child.type === 'ExpressionStatement' &&
(child.expression.type === 'JSXFragment' ||
child.expression.type === 'JSXElement')
) {
children =
child.expression.type === 'JSXFragment'
? child.expression.children
: [child.expression]
return false
}
return true
})
// Find everything thats defined in the top-level scope.
// Do this here because `estree` currently only includes import/exports
// and we dont have to walk all the JSX to figure out the top scope.
const inTopScope = [
'MDXLayout',
'MDXContent',
...periscopic.analyze(estree).scope.declarations.keys()
]
estree.body = [
...estree.body,
...createMdxLayout(layout, mdxLayoutDefault),
...createMdxContent(children)
]
// Add `mdxType`, `parentName` props to JSX elements.
const magicShortcodes = []
const stack = []
walk(estree, {
enter(node) {
if (
node.type === 'JSXElement' &&
// To do: support members (`<x.y>`).
node.openingElement.name.type === 'JSXIdentifier'
) {
const {name} = node.openingElement.name
if (stack.length > 1) {
const parentName = stack[stack.length - 1]
node.openingElement.attributes.push({
type: 'JSXAttribute',
name: {type: 'JSXIdentifier', name: 'parentName'},
value: {
type: 'Literal',
value: parentName,
raw: JSON.stringify(parentName)
}
})
}
const head = name.charAt(0)
// A component.
if (head === head.toUpperCase() && name !== 'MDXLayout') {
node.openingElement.attributes.push({
type: 'JSXAttribute',
name: {type: 'JSXIdentifier', name: 'mdxType'},
value: {type: 'Literal', value: name, raw: JSON.stringify(name)}
})
if (!inTopScope.includes(name) && !magicShortcodes.includes(name)) {
magicShortcodes.push(name)
}
}
stack.push(name)
}
},
leave(node) {
if (
node.type === 'JSXElement' &&
// To do: support members (`<x.y>`).
node.openingElement.name.type === 'JSXIdentifier'
) {
stack.pop()
}
}
})
const exports = []
if (!skipExport) {
let declaration = {type: 'Identifier', name: 'MDXContent'}
if (wrapExport) {
declaration = {
type: 'CallExpression',
callee: {type: 'Identifier', name: wrapExport},
arguments: [declaration]
}
}
exports.push({type: 'ExportDefaultDeclaration', declaration})
}
estree.body = [
...createMakeShortcodeHelper(
magicShortcodes,
options.mdxFragment === false
),
...estree.body,
...exports
]
return estreeToJs(estree)
}
function compile(options = {}) {
function compiler(tree, file) {
return serializeEstree(toEstree(tree), {filename: file.path, ...options})
}
this.Compiler = compiler
}
module.exports = compile
compile.default = compile
function createMdxContent(children) {
return [
{
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: 'MDXContent'},
expression: false,
generator: false,
async: false,
params: [
{
type: 'ObjectPattern',
properties: [
{
type: 'Property',
method: false,
shorthand: true,
computed: false,
key: {type: 'Identifier', name: 'components'},
kind: 'init',
value: {type: 'Identifier', name: 'components'}
},
{type: 'RestElement', argument: {type: 'Identifier', name: 'props'}}
]
}
],
body: {
type: 'BlockStatement',
body: [
{
type: 'ReturnStatement',
argument: {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
attributes: [
{
type: 'JSXAttribute',
name: {type: 'JSXIdentifier', name: 'components'},
value: {
type: 'JSXExpressionContainer',
expression: {type: 'Identifier', name: 'components'}
}
},
{
type: 'JSXSpreadAttribute',
argument: {type: 'Identifier', name: 'props'}
}
],
name: {type: 'JSXIdentifier', name: 'MDXLayout'},
selfClosing: false
},
closingElement: {
type: 'JSXClosingElement',
name: {type: 'JSXIdentifier', name: 'MDXLayout'}
},
children
}
}
]
}
},
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'MDXContent'},
property: {type: 'Identifier', name: 'isMDXComponent'},
computed: false,
optional: false
},
right: {type: 'Literal', value: true, raw: 'true'}
}
}
]
}
function createMdxLayout(declaration, mdxLayoutDefault) {
const id = {type: 'Identifier', name: 'MDXLayout'}
const init = {type: 'Literal', value: 'wrapper', raw: '"wrapper"'}
return [
mdxLayoutDefault
? {
type: 'ImportDeclaration',
specifiers: [
mdxLayoutDefault.local.name === 'default'
? {type: 'ImportDefaultSpecifier', local: id}
: {
type: 'ImportSpecifier',
imported: mdxLayoutDefault.local,
local: id
}
],
source: {
type: 'Literal',
value: mdxLayoutDefault.source.value,
raw: mdxLayoutDefault.source.raw
}
}
: {
type: 'VariableDeclaration',
declarations: [
{type: 'VariableDeclarator', id, init: declaration || init}
],
kind: 'const'
}
]
}
function createMakeShortcodeHelper(names, useElement) {
const func = {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {type: 'Identifier', name: 'makeShortcode'},
init: {
type: 'ArrowFunctionExpression',
id: null,
expression: true,
generator: false,
async: false,
params: [{type: 'Identifier', name: 'name'}],
body: {
type: 'ArrowFunctionExpression',
id: null,
expression: false,
generator: false,
async: false,
params: [{type: 'Identifier', name: 'props'}],
body: {
type: 'BlockStatement',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'console'},
property: {type: 'Identifier', name: 'warn'},
computed: false,
optional: false
},
arguments: [
{
type: 'Literal',
value:
'Component `%s` was not imported, exported, or provided by MDXProvider as global scope'
},
{type: 'Identifier', name: 'name'}
]
}
},
{
type: 'ReturnStatement',
// Vue.
/* c8 ignore next 16 */
argument: useElement
? {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
attributes: [
{
type: 'JSXSpreadAttribute',
argument: {type: 'Identifier', name: 'props'}
}
],
name: {type: 'JSXIdentifier', name: 'div'},
selfClosing: true
},
closingElement: null,
children: []
}
: {
type: 'JSXFragment',
openingFragment: {type: 'JSXOpeningFragment'},
closingFragment: {type: 'JSXClosingFragment'},
children: [
{
type: 'JSXExpressionContainer',
expression: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'props'},
property: {type: 'Identifier', name: 'children'},
computed: false
}
}
]
}
}
]
}
}
}
}
],
kind: 'const'
}
const shortcodes = names.map(name => ({
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {type: 'Identifier', name: String(name)},
init: {
type: 'CallExpression',
callee: {type: 'Identifier', name: 'makeShortcode'},
arguments: [{type: 'Literal', value: String(name)}]
}
}
],
kind: 'const'
}))
return shortcodes.length > 0 ? [func, ...shortcodes] : []
}

View File

@ -1,13 +1,22 @@
{
"name": "@mdx-js/mdx",
"version": "2.0.0-next.9",
"description": "Parse MDX and transpile to JSX",
"#": "to do: this prevents for a second that siblings use this",
"version": "2.0.0-next.999",
"description": "Compile MDX",
"license": "MIT",
"keywords": [
"mdx",
"markdown",
"jsx",
"remark",
"mdxast"
],
"homepage": "https://mdxjs.com",
"repository": {
"type": "git",
"url": "https://github.com/mdx-js/mdx",
"directory": "packages/mdx"
},
"homepage": "https://mdxjs.com",
"bugs": "https://github.com/mdx-js/mdx/issues",
"funding": {
"type": "opencollective",
@ -22,53 +31,61 @@
"JounQin <admin@1stg.me> (https://www.1stg.me)",
"Christian Murphy <christian.murphy.42@gmail.com>"
],
"license": "MIT",
"types": "types/index.d.ts",
"type": "module",
"sideEffects": false,
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"util.js",
"estree-to-js.js",
"mdx-ast-to-mdx-hast.js",
"mdx-hast-to-jsx.js",
"types/index.d.ts"
"lib/",
"index.d.ts",
"index.js"
],
"keywords": [
"mdx",
"markdown",
"react",
"jsx",
"remark",
"mdxast"
],
"scripts": {
"test-api": "uvu -r esbuild-register test \"\\.jsx?$\"",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
"test-types": "dtslint types",
"test": "npm run test-coverage && npm run test-types"
},
"dependencies": {
"@mdx-js/util": "2.0.0-next.1",
"astring": "^1.0.0",
"detab": "^2.0.0",
"estree-walker": "^2.0.0",
"hast-util-to-estree": "^1.0.0",
"mdast-util-to-hast": "^10.1.0",
"periscopic": "^2.0.0",
"rehype-minify-whitespace": "^4.0.0",
"remark-mdx": "2.0.0-next.9",
"remark-parse": "^9.0.0",
"remark-squeeze-paragraphs": "^4.0.0",
"unified": "^9.0.0",
"unist-builder": "^2.0.0"
"@types/estree-jsx": "^0.0.1",
"astring": "^1.6.0",
"estree-util-build-jsx": "^2.0.0",
"estree-util-is-identifier-name": "^2.0.0",
"estree-walker": "^3.0.0",
"hast-util-to-estree": "^2.0.0",
"markdown-extensions": "^1.0.0",
"mdast-util-mdx": "^1.0.0",
"micromark-extension-mdxjs": "^1.0.0",
"periscopic": "^3.0.0",
"remark-parse": "^10.0.0",
"remark-rehype": "^9.0.0",
"source-map": "^0.7.0",
"unified": "^10.0.0",
"unist-util-position-from-estree": "^1.0.0",
"unist-util-stringify-position": "^3.0.0",
"unist-util-visit": "^4.0.0",
"vfile": "^5.0.0"
},
"devDependencies": {
"@mdx-js/react": "2.0.0-next.9",
"@mdx-js/react": "2.0.0-next.8",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"rehype-katex": "^4.0.0",
"remark-footnotes": "^3.0.0",
"remark-gfm": "^1.0.0",
"remark-math": "^4.0.0"
"rehype-katex": "^6.0.0",
"remark-footnotes": "^4.0.0",
"remark-gfm": "^2.0.0",
"remark-math": "^5.0.0",
"unist-util-remove-position": "^4.0.0"
},
"scripts": {
"prepack": "npm run build",
"build": "rimraf \"lib/**/*.d.ts\" \"test/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage",
"test-api": "node --no-warnings --experimental-loader=../../script/jsx-loader.js ../../node_modules/uvu/bin.js test \"\\.jsx?$\"",
"test-coverage": "node --no-warnings --experimental-loader=../../script/jsx-loader.js ../../node_modules/uvu/bin.js test \"\\.jsx?$\"",
"#": "to do: use this if xdm tests are ported over: `c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api`",
"test": "npm run build && npm run test-coverage"
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true,
"ignoreFiles": [
"lib/util/resolve-evaluate-options.{d.ts,js}"
]
},
"gitHead": "bf7deab69996449cb99c2217dff75e65855eb2c1"
}

View File

@ -1,46 +1,34 @@
import {test} from 'uvu'
import assert from 'uvu/assert'
import path from 'path'
import babel from '@babel/core'
import unified from 'unified'
import * as assert from 'uvu/assert'
import {removePosition} from 'unist-util-remove-position'
import React from 'react'
import {
renderToStaticMarkup
} from 'react-dom/server'
import {mdx as mdxReact, MDXProvider} from '@mdx-js/react'
import mdx from '..'
import toMdxHast from '../mdx-ast-to-mdx-hast'
import toJsx from '../mdx-hast-to-jsx'
import * as runtime from 'react/jsx-runtime.js'
import {renderToStaticMarkup} from 'react-dom/server.js'
// `eslint-plugin-import` is wrong.
/* eslint-disable-next-line import/default */
import mdxReact from '@mdx-js/react'
import footnotes from 'remark-footnotes'
import gfm from 'remark-gfm'
import math from 'remark-math'
import katex from 'rehype-katex'
import {compile, compileSync, evaluate, createProcessor} from '../index.js'
const run = async (value, options = {}) => {
const doc = await mdx(value, {...options, skipExport: true})
// and that into serialized JS.
const {code} = await babel.transformAsync(doc, {
configFile: false,
plugins: [
'@babel/plugin-transform-react-jsx',
path.resolve(__dirname, '../../babel-plugin-remove-export-keywords')
]
})
// and finally run it, returning the component.
// eslint-disable-next-line no-new-func
return new Function('mdx', `${code}; return MDXContent`)(mdxReact)
}
const {MDXProvider, useMDXComponents} = mdxReact
test('should generate JSX', async () => {
const result = await mdx('Hello World')
const result = await compile('Hello World', {jsx: true})
assert.match(result, /<p>\{"Hello World"\}<\/p>/)
assert.match(result, /<_components.p>\{"Hello World"\}<\/_components.p>/)
})
test('should generate runnable JSX', async () => {
const Content = await run('Hello World')
test('should compile JSX to function calls', async () => {
const result = await compile('Hello World')
assert.match(result, /_jsx\(_components\.p, {\s+children: "Hello World"\s+}\)/)
})
test('should generate runnable JS', async () => {
const {default: Content} = await evaluate('Hello World', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -51,7 +39,7 @@ test('should generate runnable JSX', async () => {
test('should support `&`, `<`, and `>` in text', async () => {
// Note: we dont allow `<` in MDX files, but the character reference will
// get decoded and is present in the AST as `<`.
const Content = await run('a &lt; b > c & d')
const {default: Content} = await evaluate('a &lt; b > c & d', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -60,7 +48,7 @@ test('should support `&`, `<`, and `>` in text', async () => {
})
test('should generate JSX-compliant strings', async () => {
const Content = await run('!["so" cute](cats.com/cat.jpeg)')
const {default: Content} = await evaluate('!["so" cute](cats.com/cat.jpeg)', runtime)
// Note: Escaped quotes (\") isn't valid for JSX string syntax. So we're
// making sure that quotes aren't escaped here (prettier doesn't like us
@ -77,7 +65,7 @@ test('should generate JSX-compliant strings', async () => {
})
test('should support `remarkPlugins` (math)', async () => {
const Content = await run('$x$', {remarkPlugins: [math]})
const {default: Content} = await evaluate('$x$', {remarkPlugins: [math], ...runtime})
assert.equal(
renderToStaticMarkup(<Content />),
@ -90,7 +78,7 @@ test('should support `remarkPlugins` (math)', async () => {
})
test('should support `remarkPlugins` (footnotes)', async () => {
const Content = await run('x [^y]\n\n[^y]: z', {remarkPlugins: [footnotes]})
const {default: Content} = await evaluate('x [^y]\n\n[^y]: z', {remarkPlugins: [footnotes], ...runtime})
assert.equal(
renderToStaticMarkup(<Content />),
@ -98,32 +86,28 @@ test('should support `remarkPlugins` (footnotes)', async () => {
<>
<p>
x{' '}
<sup id="fnref-y">
<a href="#fn-y" className="footnote-ref">
y
</a>
</sup>
</p>
<div className="footnotes">
<hr />
<ol>
<li id="fn-y">
z
<a href="#fnref-y" className="footnote-backref">
</a>
</li>
</ol>
</div>
<a href="#fn1" className="footnote-ref" id="fnref1" role="doc-noteref">
<sup>
1
</sup>
</a>
</p>{'\n'}
<section className="footnotes" role="doc-endnotes">{'\n'}
<hr />{'\n'}
<ol>{'\n'}
<li id="fn1" role="doc-endnote">z<a href="#fnref1" className="footnote-back" role="doc-backlink"></a></li>{'\n'}
</ol>{'\n'}
</section>
</>
)
)
})
test('should support `rehypePlugins`', async () => {
const Content = await run('$x$', {
const {default: Content} = await evaluate('$x$', {
remarkPlugins: [math],
rehypePlugins: [katex]
rehypePlugins: [katex],
...runtime
})
assert.equal(
@ -163,7 +147,7 @@ test('should support async plugins', async () => {
tree.children[0].children[0].value = 'y'
}
const Content = await run('x', {remarkPlugins: [plugin]})
const {default: Content} = await evaluate('x', {remarkPlugins: [plugin], ...runtime})
assert.equal(
renderToStaticMarkup(<Content />),
@ -171,38 +155,30 @@ test('should support async plugins', async () => {
)
})
test('should support `filepath` to set the vfiles path', async () => {
test('should support a `VFileCompatible` to set the vfiles path', async () => {
const plugin = () => (_, file) => {
assert.equal(file.path, 'y')
}
await run('x', {filepath: 'y', remarkPlugins: [plugin]})
await evaluate({value: 'x', path: 'y'}, {remarkPlugins: [plugin], ...runtime})
})
test('should use an `inlineCode` “element” in mdxhast', async () => {
let called = false
const {default: Content} = await evaluate('`x`', runtime)
const plugin = () => tree => {
assert.equal(tree.children[0].children[0], {
type: 'element',
tagName: 'inlineCode',
properties: {},
children: [{type: 'text', value: 'x'}],
position: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 4, offset: 3}
}
})
called = true
}
await run('`x`', {rehypePlugins: [plugin]})
assert.ok(called)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<p>
<code>x</code>
</p>
)
)
})
test('should support `pre`/`code` from empty fenced code in mdxhast', async () => {
const Content = await run('```\n```')
test('should support `pre`/`code` from empty fenced code', async () => {
const {default: Content} = await evaluate('```\n```', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -214,8 +190,8 @@ test('should support `pre`/`code` from empty fenced code in mdxhast', async () =
)
})
test('should support `pre`/`code` from fenced code in mdxhast', async () => {
const Content = await run('```\nx\n```')
test('should support `pre`/`code` from fenced code', async () => {
const {default: Content} = await evaluate('```\nx\n```', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -228,7 +204,7 @@ test('should support `pre`/`code` from fenced code in mdxhast', async () => {
})
test('should support `pre`/`code` from fenced code w/ lang in mdxhast', async () => {
const Content = await run('```js\nx\n```')
const {default: Content} = await evaluate('```js\nx\n```', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -240,47 +216,24 @@ test('should support `pre`/`code` from fenced code w/ lang in mdxhast', async ()
)
})
test('should support attributes from fenced code meta string in mdxhast', async () => {
const Content = await run('```js id class=y title=z\nx\n```')
const {error} = console
console.error = () => { /* Empty */ }
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<pre>
<code
className="language-js y"
metastring="id class=y title=z"
title="z"
>
x{'\n'}
</code>
</pre>
)
)
console.error = error
})
test('should support a block quote in markdown', async () => {
const Content = await run('> x\n> `y`')
const {default: Content} = await evaluate('> x\n> `y`', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<blockquote>
<blockquote>{'\n'}
<p>
x{'\n'}
<code>y</code>
</p>
</p>{'\n'}
</blockquote>
)
)
})
test('should support html/jsx inside code in markdown', async () => {
const Content = await run('`<x>`')
const {default: Content} = await evaluate('`<x>`', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -293,9 +246,7 @@ test('should support html/jsx inside code in markdown', async () => {
})
test('should support tables in markdown', async () => {
const Content = await run('| A | B |\n| :- | -: |\n| a | b |', {
remarkPlugins: [gfm]
})
const {default: Content} = await evaluate('| A | B |\n| :- | -: |\n| a | b |', {remarkPlugins: [gfm], ...runtime})
assert.equal(
renderToStaticMarkup(<Content />),
@ -319,7 +270,7 @@ test('should support tables in markdown', async () => {
})
test('should support line endings in paragraphs', async () => {
const Content = await run('x\ny')
const {default: Content} = await evaluate('x\ny', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -328,7 +279,7 @@ test('should support line endings in paragraphs', async () => {
})
test('should support line endings between nodes paragraphs', async () => {
const Content = await run('*x*\n[y]()')
const {default: Content} = await evaluate('*x*\n[y]()', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -343,21 +294,21 @@ test('should support line endings between nodes paragraphs', async () => {
})
test('should support an empty document', async () => {
const Content = await run('')
const {default: Content} = await evaluate('', runtime)
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<></>))
})
test('should support an ignored node instead of a `root`', async () => {
const plugin = () => () => ({type: 'doctype', name: 'html'})
const Content = await run('', {rehypePlugins: [plugin]})
const {default: Content} = await evaluate('', {rehypePlugins: [plugin], ...runtime})
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<></>))
})
test('should support an element instead of a `root`', async () => {
const plugin = () => () => ({type: 'element', tagName: 'x', children: []})
const Content = await run('', {rehypePlugins: [plugin]})
const {default: Content} = await evaluate('', {rehypePlugins: [plugin], ...runtime})
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<x />))
})
@ -383,30 +334,33 @@ test('should support imports', async () => {
called = true
}
const result = await mdx('import X from "y"', {rehypePlugins: [plugin]})
const result = await compile('import X from "y"', {rehypePlugins: [plugin]})
assert.match(result, /import X from "y"/)
assert.ok(called)
})
test('should crash on incorrect imports', async () => {
assert.throws(() => {
mdx.sync('import a')
}, /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
try {
await compile('import a')
assert.unreachable('should not compile')
} catch (error) {
assert.match(String(error), /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
}
})
test('should support import as a word when its not the top level', async () => {
const Content = await run('> import a\n\n- import b')
const {default: Content} = await evaluate('> import a\n\n- import b', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<>
<blockquote>
<p>import a</p>
</blockquote>
<ul>
<li>import b</li>
<blockquote>{'\n'}
<p>import a</p>{'\n'}
</blockquote>{'\n'}
<ul>{'\n'}
<li>import b</li>{'\n'}
</ul>
</>
)
@ -433,8 +387,9 @@ test('should support exports', async () => {
called = true
}
const result = await mdx('export const A = () => <b>!</b>', {
rehypePlugins: [plugin]
const result = await compile('export const A = () => <b>!</b>', {
rehypePlugins: [plugin],
jsx: true
})
assert.match(result, /export const A = \(\) => <b>!<\/b>/)
@ -442,23 +397,26 @@ test('should support exports', async () => {
})
test('should crash on incorrect exports', async () => {
assert.throws(() => {
mdx.sync('export a')
}, /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
try {
await compile('export a')
assert.unreachable('should not compile')
} catch (error) {
assert.match(String(error), /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
}
})
test('should support export as a word when its not the top level', async () => {
const Content = await run('> export a\n\n- export b')
const {default: Content} = await evaluate('> export a\n\n- export b', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<>
<blockquote>
<p>export a</p>
</blockquote>
<ul>
<li>export b</li>
<blockquote>{'\n'}
<p>export a</p>{'\n'}
</blockquote>{'\n'}
<ul>{'\n'}
<li>export b</li>{'\n'}
</ul>
</>
)
@ -466,7 +424,7 @@ test('should support export as a word when its not the top level', async () =
})
test('should support JSX (flow, block)', async () => {
const Content = await run('<main><span /></main>')
const {default: Content} = await evaluate('<main><span /></main>', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -479,7 +437,7 @@ test('should support JSX (flow, block)', async () => {
})
test('should support JSX (text, inline)', async () => {
const Content = await run('x <span /> y')
const {default: Content} = await evaluate('x <span /> y', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -492,13 +450,13 @@ test('should support JSX (text, inline)', async () => {
})
test('should support JSX expressions (flow, block)', async () => {
const Content = await run('{1 + 1}')
const {default: Content} = await evaluate('{1 + 1}', runtime)
assert.equal(renderToStaticMarkup(<Content />), '2')
})
test('should support JSX expressions (text, inline)', async () => {
const Content = await run('x {1 + 1} y')
const {default: Content} = await evaluate('x {1 + 1} y', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -507,7 +465,7 @@ test('should support JSX expressions (text, inline)', async () => {
})
test('should support a default export for a layout', async () => {
const Content = await run('export default props => <main {...props} />\n\nx')
const {default: Content} = await evaluate('export default props => <main {...props} />\n\nx', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -520,36 +478,34 @@ test('should support a default export for a layout', async () => {
})
test('should support a default export from an import', async () => {
let result = await mdx('import a from "b"\nexport default a')
let result = await compile('import a from "b"\nexport default a')
assert.match(result, /import a from "b"/)
assert.match(result, /const MDXLayout = a/)
result = await mdx('export {default} from "a"')
result = await compile('export {default} from "a"')
assert.match(result, /import MDXLayout from "a"/)
// These are not export defaults: they imports default but export as
// something else.
result = await mdx('export {default as a} from "b"')
result = await compile('export {default as a} from "b"')
assert.match(result, /export {default as a} from "b"/)
assert.match(result, /const MDXLayout = "wrapper"/)
result = await mdx('export {default as a, b} from "c"')
assert.match(result, /{wrapper: MDXLayout}/)
result = await compile('export {default as a, b} from "c"')
assert.match(result, /export {default as a, b} from "c"/)
assert.match(result, /const MDXLayout = "wrapper"/)
assert.match(result, /{wrapper: MDXLayout}/)
// These are export defaults.
result = await mdx('export {a as default} from "b"')
result = await compile('export {a as default} from "b"')
assert.match(result, /import {a as MDXLayout} from "b"/)
assert.not.match(result, /const MDXLayout/)
result = await mdx('export {a as default, b} from "c"')
result = await compile('export {a as default, b} from "c"')
assert.match(result, /export {b} from "c"/)
assert.match(result, /import {a as MDXLayout} from "c"/)
assert.not.match(result, /const MDXLayout/)
})
test('should support semicolons in the default export', async () => {
const Content = await run(
'export default props => <section {...props} />;\n\nx'
)
const {default: Content} = await evaluate('export default props => <section {...props} />;\n\nx', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -562,9 +518,7 @@ test('should support semicolons in the default export', async () => {
})
test('should support a multiline default export', async () => {
const Content = await run(
'export default ({children}) => (\n <article>\n {children}\n </article>\n)\n\nx'
)
const {default: Content} = await evaluate('export default ({children}) => (\n <article>\n {children}\n </article>\n)\n\nx', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -577,13 +531,13 @@ test('should support a multiline default export', async () => {
})
test('should support using a non-default export in content', async () => {
const Content = await run('export var X = props => <b {...props} />\n\n<X />')
const {default: Content} = await evaluate('export var X = props => <b {...props} />\n\n<X />', runtime)
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<b />))
})
test('should support overwriting missing compile-time components at run-time', async () => {
const Content = await run('x <Y /> z')
const {default: Content} = await evaluate('x <Y /> z', {...runtime, useMDXComponents})
assert.equal(
renderToStaticMarkup(
@ -605,33 +559,30 @@ test('should support overwriting missing compile-time components at run-time', a
)
})
test('should not crash but issue a warning when an undefined component is used', async () => {
const Content = await run('w <X>y</X> z')
test('should throw when an undefined component is used', async () => {
const {default: Content} = await evaluate('w <X>y</X> z', runtime)
const calls = []
const {warn} = console
console.warn = (...parameters) => {
const {error} = console
console.error = (...parameters) => {
calls.push(parameters)
}
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(<p>w y z</p>)
)
try {
renderToStaticMarkup(<Content />)
assert.unreachable('should not compile')
} catch (error) {
assert.match(String(error), /Error: Element type is invalid/)
}
assert.equal(calls, [
[
'Component `%s` was not imported, exported, or provided by MDXProvider as global scope',
'X'
]
])
console.error = error
console.warn = warn
assert.equal(calls.length, 1)
assert.match(calls[0][0], /Warning: React.jsx: type is invalid/)
})
test('should support `.` in component names for members', async () => {
const Content = await run(
'export var x = {y: props => <b {...props} />}\n\n<x.y />'
)
const {default: Content} = await evaluate('export var x = {y: props => <b {...props} />}\n\n<x.y />', runtime)
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<b />))
})
@ -648,9 +599,12 @@ test('should crash on unknown nodes in mdxhast', async () => {
})
}
assert.throws(() => {
mdx.sync('x', {rehypePlugins: [plugin]})
}, /Cannot handle unknown node `unknown`/)
try {
await compile('x', {rehypePlugins: [plugin]})
assert.unreachable('should not compile')
} catch (error) {
assert.match(String(error), /Cannot handle unknown node `unknown`/)
}
})
test('should support `element` nodes w/o `properties` in mdxhast', async () => {
@ -662,7 +616,7 @@ test('should support `element` nodes w/o `properties` in mdxhast', async () => {
})
}
const Content = await run('x', {rehypePlugins: [plugin]})
const {default: Content} = await evaluate('x', {rehypePlugins: [plugin], ...runtime})
assert.equal(
renderToStaticMarkup(<Content />),
@ -675,35 +629,9 @@ test('should support `element` nodes w/o `properties` in mdxhast', async () => {
)
})
test('should support `skipExport` to not export anything', async () => {
const resultDefault = await mdx('x')
const resultTrue = await mdx('x', {skipExport: true})
const resultFalse = await mdx('x', {skipExport: false})
assert.equal(resultDefault, resultFalse)
assert.match(resultTrue, /\nfunction MDXContent/)
assert.match(resultFalse, /export default MDXContent/)
})
test('should support `wrapExport` to wrap the exported value', async () => {
const resultDefault = await mdx('x')
const resultValue = await mdx('x', {wrapExport: 'y'})
const resultNull = await mdx('x', {wrapExport: null})
assert.equal(resultDefault, resultNull)
assert.match(resultValue, /export default y\(MDXContent\)/)
assert.match(resultNull, /export default MDXContent/)
})
test('should expose an `isMDXComponent` field on the component', async () => {
const Content = await run('x')
assert.equal(Content.isMDXComponent, true)
})
test('should escape what could look like template literal placeholders in text', async () => {
/* eslint-disable no-template-curly-in-string */
const Content = await run('`${x}`')
const {default: Content} = await evaluate('`${x}`', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -717,7 +645,7 @@ test('should escape what could look like template literal placeholders in text',
})
test('should support a dollar in text', async () => {
const Content = await run('$')
const {default: Content} = await evaluate('$', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -726,7 +654,7 @@ test('should support a dollar in text', async () => {
})
test('should support an escaped dollar in text', async () => {
const Content = await run('\\$')
const {default: Content} = await evaluate('\\$', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
@ -735,129 +663,77 @@ test('should support an escaped dollar in text', async () => {
})
test('should support an empty expression in JSX', async () => {
const Content = await run('<x>{}</x>')
const {default: Content} = await evaluate('<x>{}</x>', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<p>
<x />
</p>
)
renderToStaticMarkup(<x />)
)
})
test('should support a more complex expression in JSX', async () => {
const Content = await run('<x>{(() => 1 + 2)(1)}</x>')
const {default: Content} = await evaluate('<x>{(() => 1 + 2)(1)}</x>', runtime)
assert.equal(
renderToStaticMarkup(<Content />),
renderToStaticMarkup(
<p>
<x>3</x>
</p>
)
renderToStaticMarkup(<x>3</x>)
)
})
test('default: should be async', async () => {
assert.match(await mdx('x'), /<p>{"x"}<\/p>/)
assert.match(await compile('x', {jsx: true}), /<_components\.p>{"x"}<\/_components\.p>/)
})
test('default: should support `remarkPlugins`', async () => {
assert.match(
await mdx('$x$', {remarkPlugins: [math]}),
await compile('$x$', {jsx: true, remarkPlugins: [math]}),
/className="math math-inline"/
)
})
test('sync: should be sync', () => {
assert.match(mdx.sync('x'), /<p>{"x"}<\/p>/)
assert.match(compileSync('x', {jsx: true}), /<_components\.p>{"x"}<\/_components\.p>/)
})
test('sync: should support `remarkPlugins`', () => {
assert.match(
mdx.sync('$x$', {remarkPlugins: [math]}),
/className="math math-inline"/
)
assert.match(compileSync('$x$', {remarkPlugins: [math], jsx: true}), /className="math math-inline"/)
})
test('createMdxAstCompiler: should create a unified processor', () => {
const result = mdx.createMdxAstCompiler()
const tree = result.runSync(result.parse('x'))
test('should create a unified processor', async () => {
const remarkPlugin = () => (tree) => {
const clone = removePosition(JSON.parse(JSON.stringify(tree)), true)
assert.equal(tree, {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
properties: {},
children: [
{
type: 'text',
value: 'x',
position: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 2, offset: 1}
}
}
],
position: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 2, offset: 1}
assert.equal(clone, {
type: 'root',
children: [
{ type: 'paragraph', children: [ { type: 'text', value: 'x' } ] }
]
})
}
const rehypePlugin = () => (tree) => {
const clone = removePosition(JSON.parse(JSON.stringify(tree)), true)
assert.equal(clone, {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
properties: {},
children: [ { type: 'text', value: 'x' } ]
}
}
],
position: {
start: {line: 1, column: 1, offset: 0},
end: {line: 1, column: 2, offset: 1}
}
})
})
test('createCompiler: should create a unified processor', () => {
assert.match(String(mdx.createCompiler().processSync('x')), /<p>{"x"}<\/p>/)
})
test('mdx-ast-to-mdx-hast: should be a unified plugin transforming the tree', () => {
const mdxast = {
type: 'root',
children: [
{
type: 'paragraph',
children: [{type: 'mdxTextExpression', value: '1 + 1'}]
}
]
]
})
}
const mdxhast = unified().use(toMdxHast).runSync(mdxast)
assert.equal(mdxhast, {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
properties: {},
children: [{type: 'mdxTextExpression', value: '1 + 1'}]
}
]
const processor = createProcessor({
remarkPlugins: [remarkPlugin],
rehypePlugins: [rehypePlugin],
jsx: true
})
})
test('mdx-hast-to-jsx: should be a unified plugin defining a compiler', () => {
const tree = {
type: 'root',
children: [
{type: 'element', tagName: 'x', children: [{type: 'text', value: 'a'}]}
]
}
const doc = unified().use(toJsx).stringify(tree)
assert.match(doc, /export default MDXContent/)
assert.match(doc, /<x>\{"a"}<\/x>/)
assert.match(await processor.process('x'), /<_components\.p>{"x"}<\/_components\.p>/)
})
test.run()

View File

@ -0,0 +1,16 @@
{
"include": ["lib/**/*.js", "test/**/*.js", "*.js"],
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"strict": true
}
}

View File

@ -1,2 +0,0 @@
console.warn('@mdx-js/util is deprecated: please update the code using it')
module.exports = require('@mdx-js/util')