Skip to content

Commit f0fde3b

Browse files
committed
Improve generated code
* Add support for providers inside layouts * Remove unneeded fragment if it contains one element
1 parent 3a86a6c commit f0fde3b

4 files changed

Lines changed: 224 additions & 116 deletions

File tree

lib/plugin/recma-document.js

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -471,14 +471,36 @@ export function recmaDocument(options = {}) {
471471
},
472472
children: [
473473
{
474-
type: 'JSXExpressionContainer',
475-
expression: {type: 'Identifier', name: '_content'}
474+
type: 'JSXElement',
475+
openingElement: {
476+
type: 'JSXOpeningElement',
477+
name: {type: 'JSXIdentifier', name: '_createMdxContent'},
478+
attributes: [],
479+
selfClosing: true
480+
},
481+
closingElement: null,
482+
children: []
476483
}
477484
]
478485
}
479-
/** @type {Expression} */
480-
// @ts-expect-error types are wrong: `JSXElement` is an `Expression`.
481-
const consequent = element
486+
487+
// @ts-expect-error: JSXElements are expressions.
488+
const consequent = /** @type {Expression} */ (element)
489+
490+
let argument = content || {type: 'Literal', value: null}
491+
492+
if (
493+
argument &&
494+
// @ts-expect-error: fine.
495+
argument.type === 'JSXFragment' &&
496+
// @ts-expect-error: fine.
497+
argument.children.length === 1 &&
498+
// @ts-expect-error: fine.
499+
argument.children[0].type === 'JSXElement'
500+
) {
501+
// @ts-expect-error: fine.
502+
argument = argument.children[0]
503+
}
482504

483505
return {
484506
type: 'FunctionDeclaration',
@@ -493,24 +515,27 @@ export function recmaDocument(options = {}) {
493515
body: {
494516
type: 'BlockStatement',
495517
body: [
496-
{
497-
type: 'VariableDeclaration',
498-
kind: 'const',
499-
declarations: [
500-
{
501-
type: 'VariableDeclarator',
502-
id: {type: 'Identifier', name: '_content'},
503-
init: content || {type: 'Literal', value: null}
504-
}
505-
]
506-
},
507518
{
508519
type: 'ReturnStatement',
509520
argument: {
510521
type: 'ConditionalExpression',
511522
test: {type: 'Identifier', name: 'MDXLayout'},
512523
consequent,
513-
alternate: {type: 'Identifier', name: '_content'}
524+
alternate: {
525+
type: 'CallExpression',
526+
callee: {type: 'Identifier', name: '_createMdxContent'},
527+
arguments: [],
528+
optional: false
529+
}
530+
}
531+
},
532+
{
533+
type: 'FunctionDeclaration',
534+
id: {type: 'Identifier', name: '_createMdxContent'},
535+
params: [],
536+
body: {
537+
type: 'BlockStatement',
538+
body: [{type: 'ReturnStatement', argument}]
514539
}
515540
}
516541
]

lib/plugin/recma-jsx-rewrite.js

Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @typedef {import('estree-jsx').Property} Property
1313
* @typedef {import('estree-jsx').Statement} Statement
1414
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
15+
* @typedef {import('estree-jsx').ObjectPattern} ObjectPattern
1516
*
1617
* @typedef {import('estree-walker').SyncHandler} WalkHandler
1718
*
@@ -35,7 +36,7 @@ import {specifiersToDeclarations} from '../util/estree-util-specifiers-to-declar
3536

3637
/**
3738
* A plugin that rewrites JSX in functions to accept components as
38-
* `props.components` (when the function is called `MDXContent`), or from
39+
* `props.components` (when the function is called `_createMdxContent`), or from
3940
* a provider (if there is one).
4041
* It also makes sure that any undefined components are defined: either from
4142
* received components or as a function that throws an error.
@@ -67,15 +68,23 @@ export function recmaJsxRewrite(options = {}) {
6768
fnStack.push({objects: [], components: [], tags: [], node})
6869
}
6970

70-
const fnScope = fnStack[0]
71+
let fnScope = fnStack[0]
7172

7273
if (
7374
!fnScope ||
74-
(!isMdxContent(fnScope.node) && !providerImportSource)
75+
(!isNamedFunction(fnScope.node, 'MDXContent') &&
76+
!providerImportSource)
7577
) {
7678
return
7779
}
7880

81+
if (
82+
fnStack[1] &&
83+
isNamedFunction(fnStack[1].node, '_createMdxContent')
84+
) {
85+
fnScope = fnStack[1]
86+
}
87+
7988
const newScope = /** @type {Scope|undefined} */ (
8089
// @ts-expect-error: periscopic doesn’t support JSX.
8190
scopeInfo.map.get(node)
@@ -195,8 +204,6 @@ export function recmaJsxRewrite(options = {}) {
195204
}
196205

197206
if (defaults.length > 0 || actual.length > 0) {
198-
parameters.push({type: 'ObjectExpression', properties: defaults})
199-
200207
if (providerImportSource) {
201208
importProvider = true
202209
parameters.push({
@@ -207,8 +214,12 @@ export function recmaJsxRewrite(options = {}) {
207214
})
208215
}
209216

210-
// Accept `components` as a prop if this is the `MDXContent` function.
211-
if (isMdxContent(scope.node)) {
217+
// Accept `components` as a prop if this is the `MDXContent` or
218+
// `_createMdxContent` function.
219+
if (
220+
isNamedFunction(scope.node, 'MDXContent') ||
221+
isNamedFunction(scope.node, '_createMdxContent')
222+
) {
212223
parameters.push({
213224
type: 'MemberExpression',
214225
object: {type: 'Identifier', name: 'props'},
@@ -218,22 +229,42 @@ export function recmaJsxRewrite(options = {}) {
218229
})
219230
}
220231

221-
declarations.push({
222-
type: 'VariableDeclarator',
223-
id: {type: 'Identifier', name: '_components'},
224-
init: {
225-
type: 'CallExpression',
226-
callee: {
227-
type: 'MemberExpression',
228-
object: {type: 'Identifier', name: 'Object'},
229-
property: {type: 'Identifier', name: 'assign'},
230-
computed: false,
231-
optional: false
232-
},
233-
arguments: parameters,
234-
optional: false
235-
}
236-
})
232+
if (defaults.length > 0 || parameters.length > 1) {
233+
parameters.unshift({
234+
type: 'ObjectExpression',
235+
properties: defaults
236+
})
237+
}
238+
239+
// If we’re getting components from several sources, merge them.
240+
/** @type {Expression} */
241+
let componentsInit =
242+
parameters.length > 1
243+
? {
244+
type: 'CallExpression',
245+
callee: {
246+
type: 'MemberExpression',
247+
object: {type: 'Identifier', name: 'Object'},
248+
property: {type: 'Identifier', name: 'assign'},
249+
computed: false,
250+
optional: false
251+
},
252+
arguments: parameters,
253+
optional: false
254+
}
255+
: parameters[0].type === 'MemberExpression'
256+
? // If we’re only getting components from `props.components`,
257+
// make sure it’s defined.
258+
{
259+
type: 'LogicalExpression',
260+
operator: '||',
261+
left: parameters[0],
262+
right: {type: 'ObjectExpression', properties: []}
263+
}
264+
: parameters[0]
265+
266+
/** @type {ObjectPattern|undefined} */
267+
let componentsPattern
237268

238269
// Add components to scope.
239270
// For `['MyComponent', 'MDXLayout']` this generates:
@@ -243,24 +274,37 @@ export function recmaJsxRewrite(options = {}) {
243274
// Note that MDXLayout is special as it’s taken from
244275
// `_components.wrapper`.
245276
if (actual.length > 0) {
277+
componentsPattern = {
278+
type: 'ObjectPattern',
279+
properties: actual.map((name) => ({
280+
type: 'Property',
281+
kind: 'init',
282+
key: {
283+
type: 'Identifier',
284+
name: name === 'MDXLayout' ? 'wrapper' : name
285+
},
286+
value: {type: 'Identifier', name},
287+
method: false,
288+
shorthand: name !== 'MDXLayout',
289+
computed: false
290+
}))
291+
}
292+
}
293+
294+
if (scope.tags.length > 0) {
246295
declarations.push({
247296
type: 'VariableDeclarator',
248-
id: {
249-
type: 'ObjectPattern',
250-
properties: actual.map((name) => ({
251-
type: 'Property',
252-
kind: 'init',
253-
key: {
254-
type: 'Identifier',
255-
name: name === 'MDXLayout' ? 'wrapper' : name
256-
},
257-
value: {type: 'Identifier', name},
258-
method: false,
259-
shorthand: name !== 'MDXLayout',
260-
computed: false
261-
}))
262-
},
263-
init: {type: 'Identifier', name: '_components'}
297+
id: {type: 'Identifier', name: '_components'},
298+
init: componentsInit
299+
})
300+
componentsInit = {type: 'Identifier', name: '_components'}
301+
}
302+
303+
if (componentsPattern) {
304+
declarations.push({
305+
type: 'VariableDeclarator',
306+
id: componentsPattern,
307+
init: componentsInit
264308
})
265309
}
266310

@@ -328,13 +372,12 @@ function createImportProvider(providerImportSource, outputFormat) {
328372
}
329373

330374
/**
331-
* @param {ESFunction} [node]
375+
* @param {ESFunction} node
376+
* @param {string} name
332377
* @returns {boolean}
333378
*/
334-
function isMdxContent(node) {
335-
return Boolean(
336-
node && 'id' in node && node.id && node.id.name === 'MDXContent'
337-
)
379+
function isNamedFunction(node, name) {
380+
return Boolean(node && 'id' in node && node.id && node.id.name === name)
338381
}
339382

340383
/**

readme.md

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -170,16 +170,14 @@ import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runti
170170
export const Thing = () => _jsx(_Fragment, {children: 'World!'})
171171

172172
function MDXContent(props = {}) {
173-
const _components = Object.assign({h1: 'h1'}, props.components)
174-
const {wrapper: MDXLayout} = _components
175-
const _content = _jsx(_Fragment, {
176-
children: _jsxs(_components.h1, {
177-
children: ['Hello, ', _jsx(Thing, {})]
178-
})
179-
})
173+
const {wrapper: MDXLayout} = props.components || ({})
180174
return MDXLayout
181-
? _jsx(MDXLayout, Object.assign({}, props, {children: _content}))
182-
: _content
175+
? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
176+
: _createMdxContent()
177+
function _createMdxContent() {
178+
const _components = Object.assign({h1: 'h1'}, props.components)
179+
return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
180+
}
183181
}
184182

185183
export default MDXContent
@@ -188,10 +186,10 @@ export default MDXContent
188186
Some more observations:
189187

190188
* JSX is compiled away to function calls and an import of React†
191-
* The content component can be given `{components: {h1: MyComponent}}` to use
192-
something else for the heading
193189
* The content component can be given `{components: {wrapper: MyLayout}}` to
194190
wrap the whole content
191+
* The content component can be given `{components: {h1: MyComponent}}` to use
192+
something else for the heading
195193

196194
**xdm** is not coupled to React.
197195
You can also use it with [Preact](#preact), [Vue](#vue), [Emotion](#emotion),
@@ -543,18 +541,26 @@ compile(file, {providerImportSource: '@mdx-js/react'})
543541
…yields this difference:
544542

545543
```diff
546-
/* @jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment */
547-
import React from 'react'
544+
/* @jsxRuntime automatic @jsxImportSource react */
545+
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
548546
+import {useMDXComponents as _provideComponents} from '@mdx-js/react'
549547

550-
export const Thing = () => React.createElement(React.Fragment, null, 'World!')
548+
export const Thing = () => _jsx(_Fragment, {children: 'World!'})
551549

552550
function MDXContent(props = {}) {
553-
- const _components = Object.assign({h1: 'h1'}, props.components)
554-
+ const _components = Object.assign({h1: 'h1'}, _provideComponents(), props.components)
555-
const {wrapper: MDXLayout} = _components
556-
const _content = React.createElement(
557-
React.Fragment,
551+
- const {wrapper: MDXLayout} = props.components || ({})
552+
+ const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components)
553+
return MDXLayout
554+
? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
555+
: _createMdxContent()
556+
function _createMdxContent() {
557+
- const _components = Object.assign({h1: 'h1'}, props.components)
558+
+ const _components = Object.assign({h1: 'h1'}, _provideComponents(), props.components)
559+
return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
560+
}
561+
}
562+
563+
export default MDXContent
558564
```
559565

560566
</details>
@@ -577,26 +583,26 @@ compile(file, {jsx: true})
577583
…yields this difference:
578584

579585
```diff
580-
/* @jsxRuntime classic @jsx React.createElement @jsxFrag React.Fragment */
586+
/* @jsxRuntime automatic @jsxImportSource react */
581587
-import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime'
582-
-
583-
-export const Thing = () => React.createElement(React.Fragment, null, 'World!')
588+
589+
-export const Thing = () => _jsx(_Fragment, {children: 'World!'})
584590
+export const Thing = () => <>World!</>
585591

586592
function MDXContent(props = {}) {
587-
const _components = Object.assign({h1: 'h1'}, props.components)
588-
const {wrapper: MDXLayout} = _components
589-
- const _content = _jsx(_Fragment, {
590-
- children: _jsxs(_components.h1, {
591-
- children: ['Hello, ', _jsx(Thing, {})]
592-
- })
593-
- })
594-
+ const _content = (
595-
+ <>
596-
+ <_components.h1>{'Hello, '}<Thing /></_components.h1>
597-
+ </>
598-
+ )
599-
593+
const {wrapper: MDXLayout} = props.components || ({})
594+
return MDXLayout
595+
- ? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, {})}))
596+
+ ? <MDXLayout {...props}><_createMdxContent /></MDXLayout>
597+
: _createMdxContent()
598+
function _createMdxContent() {
599+
const _components = Object.assign({h1: 'h1'}, props.components)
600+
- return _jsxs(_components.h1, {children: ['Hello, ', _jsx(Thing, {})]})
601+
+ return <_components.h1>{"Hello, "}<Thing /></_components.h1>
602+
}
603+
}
604+
605+
export default MDXContent
600606
```
601607

602608
</details>

0 commit comments

Comments
 (0)