kjelsrud.dev/node_modules/@mdx-js/mdx/lib/plugin/recma-document.js
2023-07-19 21:31:30 +02:00

604 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @typedef {import('estree-jsx').Directive} Directive
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
* @typedef {import('estree-jsx').Expression} Expression
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
* @typedef {import('estree-jsx').ImportExpression} ImportExpression
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
* @typedef {import('estree-jsx').Literal} Literal
* @typedef {import('estree-jsx').JSXElement} JSXElement
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
* @typedef {import('estree-jsx').Node} Node
* @typedef {import('estree-jsx').Program} Program
* @typedef {import('estree-jsx').Property} Property
* @typedef {import('estree-jsx').SimpleLiteral} SimpleLiteral
* @typedef {import('estree-jsx').SpreadElement} SpreadElement
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
*/
/**
* @typedef RecmaDocumentOptions
* Configuration for internal plugin `recma-document`.
* @property {'function-body' | 'program' | null | undefined} [outputFormat='program']
* Whether to use either `import` and `export` statements to get the runtime
* (and optionally provider) and export the content, or get values from
* `arguments` and return things.
* @property {boolean | null | undefined} [useDynamicImport=false]
* Whether to keep `import` (and `export … from`) statements or compile them
* to dynamic `import()` instead.
* @property {string | null | undefined} [baseUrl]
* Resolve `import`s (and `export … from`, and `import.meta.url`) relative to
* this URL.
* @property {string | null | undefined} [pragma='React.createElement']
* Pragma for JSX (used in classic runtime).
* @property {string | null | undefined} [pragmaFrag='React.Fragment']
* Pragma for JSX fragments (used in classic runtime).
* @property {string | null | undefined} [pragmaImportSource='react']
* Where to import the identifier of `pragma` from (used in classic runtime).
* @property {string | null | undefined} [jsxImportSource='react']
* Place to import automatic JSX runtimes from (used in automatic runtime).
* @property {'automatic' | 'classic' | null | undefined} [jsxRuntime='automatic']
* JSX runtime to use.
*/
import {analyze} from 'periscopic'
import {stringifyPosition} from 'unist-util-stringify-position'
import {positionFromEstree} from 'unist-util-position-from-estree'
import {walk} from 'estree-walker'
import {create} from '../util/estree-util-create.js'
import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declarations.js'
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
import {isDeclaration} from '../util/estree-util-is-declaration.js'
/**
* A plugin to wrap the estree in `MDXContent`.
*
* @type {import('unified').Plugin<[RecmaDocumentOptions | null | undefined] | [], Program>}
*/
export function recmaDocument(options) {
// Always given inside `@mdx-js/mdx`
/* c8 ignore next */
const options_ = options || {}
const baseUrl = options_.baseUrl || undefined
const useDynamicImport = options_.useDynamicImport || undefined
const outputFormat = options_.outputFormat || 'program'
const pragma =
options_.pragma === undefined ? 'React.createElement' : options_.pragma
const pragmaFrag =
options_.pragmaFrag === undefined ? 'React.Fragment' : options_.pragmaFrag
const pragmaImportSource = options_.pragmaImportSource || 'react'
const jsxImportSource = options_.jsxImportSource || 'react'
const jsxRuntime = options_.jsxRuntime || 'automatic'
return (tree, file) => {
/** @type {Array<[string, string] | string>} */
const exportedIdentifiers = []
/** @type {Array<Directive | ModuleDeclaration | Statement>} */
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) {
const source = /** @type {SimpleLiteral} */ (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 '…'`.
/** @type {Array<ImportDefaultSpecifier | ImportSpecifier>} */
const specifiers = []
// Default as default / something else as default.
if (specifier.local.name === 'default') {
specifiers.push({
type: 'ImportDefaultSpecifier',
local: {type: 'Identifier', name: 'MDXLayout'}
})
} else {
/** @type {ImportSpecifier} */
const importSpecifier = {
type: 'ImportSpecifier',
imported: specifier.local,
local: {type: 'Identifier', name: 'MDXLayout'}
}
create(specifier.local, importSpecifier)
specifiers.push(importSpecifier)
}
/** @type {Literal} */
const from = {type: 'Literal', value: source.value}
create(source, from)
/** @type {ImportDeclaration} */
const declaration = {
type: 'ImportDeclaration',
specifiers,
source: from
}
create(specifier, declaration)
handleEsm(declaration)
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: `JSXFragment` is an `Expression`.
(child.expression.type === 'JSXFragment' ||
child.expression.type === 'JSXElement')
) {
content = true
replacement.push(...createMdxContent(child.expression, Boolean(layout)))
// 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(undefined, Boolean(layout)))
}
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
if (baseUrl) {
walk(tree, {
enter(node) {
if (
node.type === 'MemberExpression' &&
'object' in node &&
node.object.type === 'MetaProperty' &&
node.property.type === 'Identifier' &&
node.object.meta.name === 'import' &&
node.object.property.name === 'meta' &&
node.property.name === 'url'
) {
/** @type {SimpleLiteral} */
const replacement = {type: 'Literal', value: baseUrl}
this.replace(replacement)
}
}
})
}
/**
* @param {ExportAllDeclaration | ExportNamedDeclaration} 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 {ExportAllDeclaration | ExportNamedDeclaration | ImportDeclaration} 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>).
}
/** @type {Literal} */
const literal = {type: 'Literal', value}
create(node.source, literal)
node.source = literal
}
/** @type {ModuleDeclaration | Statement | 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')
// ```
/** @type {ImportExpression} */
const argument = {type: 'ImportExpression', source: node.source}
create(node, argument)
init = {type: 'AwaitExpression', argument}
if (
(node.type === 'ImportDeclaration' ||
node.type === 'ExportNamedDeclaration') &&
node.specifiers.length === 0
) {
replace = {type: 'ExpressionStatement', expression: init}
} else {
replace = {
type: 'VariableDeclaration',
kind: 'const',
declarations:
node.type === 'ExportAllDeclaration'
? [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: '_exportAll' + ++exportAllCount
},
init
}
]
: specifiersToDeclarations(node.specifiers, 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 | undefined} [content]
* @param {boolean | undefined} [hasInternalLayout]
* @returns {Array<FunctionDeclaration>}
*/
function createMdxContent(content, hasInternalLayout) {
/** @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: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {type: 'JSXIdentifier', name: '_createMdxContent'},
attributes: [
{
type: 'JSXSpreadAttribute',
argument: {type: 'Identifier', name: 'props'}
}
],
selfClosing: true
},
closingElement: null,
children: []
}
]
}
let result = /** @type {Expression} */ (element)
if (!hasInternalLayout) {
result = {
type: 'ConditionalExpression',
test: {type: 'Identifier', name: 'MDXLayout'},
consequent: result,
alternate: {
type: 'CallExpression',
callee: {type: 'Identifier', name: '_createMdxContent'},
arguments: [{type: 'Identifier', name: 'props'}],
optional: false
}
}
}
let argument = content || {type: 'Literal', value: null}
// Unwrap a fragment of a single element.
if (
argument &&
// @ts-expect-error: fine.
argument.type === 'JSXFragment' &&
// @ts-expect-error: fine.
argument.children.length === 1 &&
// @ts-expect-error: fine.
argument.children[0].type === 'JSXElement'
) {
// @ts-expect-error: fine.
argument = argument.children[0]
}
return [
{
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: '_createMdxContent'},
params: [{type: 'Identifier', name: 'props'}],
body: {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument}]
}
},
{
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: 'MDXContent'},
params: [
{
type: 'AssignmentPattern',
left: {type: 'Identifier', name: 'props'},
right: {type: 'ObjectExpression', properties: []}
}
],
body: {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument: result}]
}
}
]
}
}