kjelsrud.dev/node_modules/@mdx-js/mdx/lib/plugin/recma-document.js

605 lines
20 KiB
JavaScript
Raw Normal View History

2023-07-19 21:31:30 +02:00
/**
* @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}]
}
}
]
}
}