Skip to content

Commit

Permalink
Improve generated code (#1810)
Browse files Browse the repository at this point in the history
* Add support for providers inside layouts
* Remove unneeded fragment if it contains one element

Backports: wooorm/xdm@f0fde3b.
  • Loading branch information
wooorm authored Nov 13, 2021
1 parent 2393084 commit e86e9e8
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 126 deletions.
20 changes: 9 additions & 11 deletions docs/docs/using-mdx.server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,14 @@ import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runti
export const Thing = () => _jsx(_Fragment, {children: 'World'})

function MDXContent(props = {}) {
const _components = Object.assign({h1: 'h1'}, props.components)
const {wrapper: MDXLayout} = _components
const _content = _jsx(_Fragment, {
children: _jsxs(_components.h1, {
children: ['Hello ', _jsx(Thing, {})]
})
})
const {wrapper: MDXLayout} = props.components || ({})
return MDXLayout
? _jsx(MDXLayout, Object.assign({}, props, {children: _content}))
: _content
? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
: _createMdxContent()
function _createMdxContent() {
const _components = Object.assign({h1: 'h1'}, props.components)
return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
}
}

export default MDXContent
Expand All @@ -94,10 +92,10 @@ export default MDXContent
Some more observations:

* JSX is compiled away to function calls and an import of React†
* The content component can be given `{components: {h1: MyComponent}}` to use
something else for the heading
* The content component can be given `{components: {wrapper: MyLayout}}` to
wrap all content
* The content component can be given `{components: {h1: MyComponent}}` to use
something else for the heading

† MDX is not coupled to React.
You can also use it with [Preact][],
Expand Down
59 changes: 42 additions & 17 deletions packages/mdx/lib/plugin/recma-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,14 +470,36 @@ export function recmaDocument(options = {}) {
},
children: [
{
type: 'JSXExpressionContainer',
expression: {type: 'Identifier', name: '_content'}
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {type: 'JSXIdentifier', name: '_createMdxContent'},
attributes: [],
selfClosing: true
},
closingElement: null,
children: []
}
]
}
/** @type {Expression} */
// @ts-expect-error types are wrong: `JSXElement` is an `Expression`.
const consequent = element

// @ts-expect-error: JSXElements are expressions.
const consequent = /** @type {Expression} */ (element)

let argument = content || {type: 'Literal', value: null}

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',
Expand All @@ -492,24 +514,27 @@ export function recmaDocument(options = {}) {
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'}
alternate: {
type: 'CallExpression',
callee: {type: 'Identifier', name: '_createMdxContent'},
arguments: [],
optional: false
}
}
},
{
type: 'FunctionDeclaration',
id: {type: 'Identifier', name: '_createMdxContent'},
params: [],
body: {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument}]
}
}
]
Expand Down
131 changes: 87 additions & 44 deletions packages/mdx/lib/plugin/recma-jsx-rewrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* @typedef {import('estree-jsx').Property} Property
* @typedef {import('estree-jsx').Statement} Statement
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
* @typedef {import('estree-jsx').ObjectPattern} ObjectPattern
*
* @typedef {import('estree-walker').SyncHandler} WalkHandler
*
Expand All @@ -35,7 +36,7 @@ import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declar

/**
* A plugin that rewrites JSX in functions to accept components as
* `props.components` (when the function is called `MDXContent`), or from
* `props.components` (when the function is called `_createMdxContent`), 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.
Expand Down Expand Up @@ -67,15 +68,23 @@ export function recmaJsxRewrite(options = {}) {
fnStack.push({objects: [], components: [], tags: [], node})
}

const fnScope = fnStack[0]
let fnScope = fnStack[0]

if (
!fnScope ||
(!isMdxContent(fnScope.node) && !providerImportSource)
(!isNamedFunction(fnScope.node, 'MDXContent') &&
!providerImportSource)
) {
return
}

if (
fnStack[1] &&
isNamedFunction(fnStack[1].node, '_createMdxContent')
) {
fnScope = fnStack[1]
}

const newScope = /** @type {Scope|undefined} */ (
// @ts-expect-error: periscopic doesn’t support JSX.
scopeInfo.map.get(node)
Expand Down Expand Up @@ -195,8 +204,6 @@ export function recmaJsxRewrite(options = {}) {
}

if (defaults.length > 0 || actual.length > 0) {
parameters.push({type: 'ObjectExpression', properties: defaults})

if (providerImportSource) {
importProvider = true
parameters.push({
Expand All @@ -207,8 +214,12 @@ export function recmaJsxRewrite(options = {}) {
})
}

// Accept `components` as a prop if this is the `MDXContent` function.
if (isMdxContent(scope.node)) {
// Accept `components` as a prop if this is the `MDXContent` or
// `_createMdxContent` function.
if (
isNamedFunction(scope.node, 'MDXContent') ||
isNamedFunction(scope.node, '_createMdxContent')
) {
parameters.push({
type: 'MemberExpression',
object: {type: 'Identifier', name: 'props'},
Expand All @@ -218,22 +229,42 @@ export function recmaJsxRewrite(options = {}) {
})
}

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
}
})
if (defaults.length > 0 || parameters.length > 1) {
parameters.unshift({
type: 'ObjectExpression',
properties: defaults
})
}

// If we’re getting components from several sources, merge them.
/** @type {Expression} */
let componentsInit =
parameters.length > 1
? {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {type: 'Identifier', name: 'Object'},
property: {type: 'Identifier', name: 'assign'},
computed: false,
optional: false
},
arguments: parameters,
optional: false
}
: parameters[0].type === 'MemberExpression'
? // If we’re only getting components from `props.components`,
// make sure it’s defined.
{
type: 'LogicalExpression',
operator: '||',
left: parameters[0],
right: {type: 'ObjectExpression', properties: []}
}
: parameters[0]

/** @type {ObjectPattern|undefined} */
let componentsPattern

// Add components to scope.
// For `['MyComponent', 'MDXLayout']` this generates:
Expand All @@ -243,24 +274,37 @@ export function recmaJsxRewrite(options = {}) {
// Note that MDXLayout is special as it’s taken from
// `_components.wrapper`.
if (actual.length > 0) {
componentsPattern = {
type: 'ObjectPattern',
properties: actual.map((name) => ({
type: 'Property',
kind: 'init',
key: {
type: 'Identifier',
name: name === 'MDXLayout' ? 'wrapper' : name
},
value: {type: 'Identifier', name},
method: false,
shorthand: name !== 'MDXLayout',
computed: false
}))
}
}

if (scope.tags.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'}
id: {type: 'Identifier', name: '_components'},
init: componentsInit
})
componentsInit = {type: 'Identifier', name: '_components'}
}

if (componentsPattern) {
declarations.push({
type: 'VariableDeclarator',
id: componentsPattern,
init: componentsInit
})
}

Expand Down Expand Up @@ -328,13 +372,12 @@ function createImportProvider(providerImportSource, outputFormat) {
}

/**
* @param {ESFunction} [node]
* @param {ESFunction} node
* @param {string} name
* @returns {boolean}
*/
function isMdxContent(node) {
return Boolean(
node && 'id' in node && node.id && node.id.name === 'MDXContent'
)
function isNamedFunction(node, name) {
return Boolean(node && 'id' in node && node.id && node.id.name === name)
}

/**
Expand Down
Loading

1 comment on commit e86e9e8

@vercel
Copy link

@vercel vercel bot commented on e86e9e8 Nov 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

Please sign in to comment.