From 641eb917986d3835fe19c5ebaddb19e7b0e1d6f1 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Wed, 14 Dec 2022 09:07:57 +0100 Subject: [PATCH] Add support JSX development runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSX dev runtime exposes more debugging information to users. For example, React exposes positional information through React Devtools. Although GH-2035 started off as an issue to support the `__source` prop, I have refrained from actually supporting the `__source` prop, as it’s specific to React. The automatic dev runtime supports this information without patching props. Closes GH-2035. Closes GH-2045. Reviewed-by: Titus Wormer --- packages/mdx/lib/core.js | 2 +- packages/mdx/lib/plugin/recma-jsx-build.js | 11 ++-- .../mdx/lib/util/resolve-evaluate-options.js | 24 ++++++--- packages/mdx/test/compile.js | 50 +++++++++++++++++++ packages/mdx/test/evaluate.js | 33 +++++++++++- 5 files changed, 104 insertions(+), 16 deletions(-) diff --git a/packages/mdx/lib/core.js b/packages/mdx/lib/core.js index 96e34d709..1d3aec731 100644 --- a/packages/mdx/lib/core.js +++ b/packages/mdx/lib/core.js @@ -125,7 +125,7 @@ export function createProcessor(options = {}) { .use(recmaJsxRewrite, {development, providerImportSource, outputFormat}) if (!jsx) { - pipeline.use(recmaJsxBuild, {outputFormat}) + pipeline.use(recmaJsxBuild, {development, outputFormat}) } pipeline.use(recmaStringify, {SourceMapGenerator}).use(recmaPlugins || []) diff --git a/packages/mdx/lib/plugin/recma-jsx-build.js b/packages/mdx/lib/plugin/recma-jsx-build.js index e15e733ed..f7e14b021 100644 --- a/packages/mdx/lib/plugin/recma-jsx-build.js +++ b/packages/mdx/lib/plugin/recma-jsx-build.js @@ -1,5 +1,6 @@ /** * @typedef {import('estree-jsx').Program} Program + * @typedef {import('estree-util-build-jsx').BuildJsxOptions} BuildJsxOptions * * @typedef RecmaJsxBuildOptions * @property {'program'|'function-body'} [outputFormat='program'] @@ -15,13 +16,13 @@ import {toIdOrMemberExpression} from '../util/estree-util-to-id-or-member-expres * A plugin to build JSX into function calls. * `estree-util-build-jsx` does all the work for us! * - * @type {import('unified').Plugin<[RecmaJsxBuildOptions]|[], Program>} + * @type {import('unified').Plugin<[BuildJsxOptions & RecmaJsxBuildOptions?], Program>} */ export function recmaJsxBuild(options = {}) { - const {outputFormat} = options + const {development, outputFormat} = options - return (tree) => { - buildJsx(tree) + return (tree, file) => { + buildJsx(tree, {development, filePath: file.history[0]}) // When compiling to a function body, replace the import that was just // generated, and get `jsx`, `jsxs`, and `Fragment` from `arguments[0]` @@ -31,7 +32,7 @@ export function recmaJsxBuild(options = {}) { tree.body[0] && tree.body[0].type === 'ImportDeclaration' && typeof tree.body[0].source.value === 'string' && - /\/jsx-runtime$/.test(tree.body[0].source.value) + /\/jsx-(dev-)?runtime$/.test(tree.body[0].source.value) ) { tree.body[0] = { type: 'VariableDeclaration', diff --git a/packages/mdx/lib/util/resolve-evaluate-options.js b/packages/mdx/lib/util/resolve-evaluate-options.js index 45cfefdf1..de73f412e 100644 --- a/packages/mdx/lib/util/resolve-evaluate-options.js +++ b/packages/mdx/lib/util/resolve-evaluate-options.js @@ -4,10 +4,12 @@ * @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 {*} [jsx] + * Function to generate an element with static children in production mode. + * @property {*} [jsxs] + * Function to generate an element with dynamic children in production mode. + * @property {*} [jsxDEV] + * Function to generate an element in development mode. * @property {*} [useMDXComponents] * Function to get `MDXComponents` from context. * @@ -23,18 +25,24 @@ * @returns {{compiletime: ProcessorOptions, runtime: RunnerOptions}} */ export function resolveEvaluateOptions(options) { - const {Fragment, jsx, jsxs, useMDXComponents, ...rest} = options || {} + const {development, Fragment, jsx, jsxs, jsxDEV, 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`') + if (development) { + if (!jsxDEV) throw new Error('Expected `jsxDEV` given to `evaluate`') + } else { + if (!jsx) throw new Error('Expected `jsx` given to `evaluate`') + if (!jsxs) throw new Error('Expected `jsxs` given to `evaluate`') + } return { compiletime: { ...rest, + development, outputFormat: 'function-body', providerImportSource: useMDXComponents ? '#' : undefined }, - runtime: {Fragment, jsx, jsxs, useMDXComponents} + runtime: {Fragment, jsx, jsxs, jsxDEV, useMDXComponents} } } diff --git a/packages/mdx/test/compile.js b/packages/mdx/test/compile.js index ea6ae715f..a59f23d49 100644 --- a/packages/mdx/test/compile.js +++ b/packages/mdx/test/compile.js @@ -543,6 +543,22 @@ test('compile', async () => { ) console.log('\nnote: the preceding warning is expected!\n') + const developmentSourceNode = ( + await run( + compileSync( + {value: '
', path: 'path/to/file.js'}, + {development: true} + ).value + ) + )({}) + assert.equal( + // @ts-expect-error React attaches source information on this property, + // but it’s private and untyped. + developmentSourceNode._source, + {fileName: 'path/to/file.js', lineNumber: 1, columnNumber: 1}, + 'should expose source information in the automatic jsx dev runtime' + ) + try { renderToStaticMarkup( React.createElement(await run(compileSync('', {development: true}))) @@ -1503,6 +1519,40 @@ test('MDX (ESM)', async () => { '

a

', 'should support rexporting the default export, and other things, from a source' ) + + assert.equal( + compileSync({value: '', path: 'path/to/file.js'}, {development: true}) + .value, + [ + '/*@jsxRuntime automatic @jsxImportSource react*/', + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + 'function _createMdxContent(props) {', + ' const {X} = props.components || ({});', + ' if (!X) _missingMdxReference("X", true, "1:1-1:6");', + ' return _jsxDEV(X, {}, undefined, false, {', + ' fileName: "path/to/file.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + ' }, this);', + '}', + 'function MDXContent(props = {}) {', + ' const {wrapper: MDXLayout} = props.components || ({});', + ' return MDXLayout ? _jsxDEV(MDXLayout, Object.assign({}, props, {', + ' children: _jsxDEV(_createMdxContent, props, undefined, false, {', + ' fileName: "path/to/file.js"', + ' }, this)', + ' }), undefined, false, {', + ' fileName: "path/to/file.js"', + ' }, this) : _createMdxContent(props);', + '}', + 'export default MDXContent;', + 'function _missingMdxReference(id, component, place) {', + ' throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it." + (place ? "\\nIt’s referenced in your code at `" + place + "` in `path/to/file.js`" : ""));', + '}', + '' + ].join('\n'), + 'should support the jsx dev runtime' + ) }) test('source maps', async () => { diff --git a/packages/mdx/test/evaluate.js b/packages/mdx/test/evaluate.js index fa7fbe792..2e04151ff 100644 --- a/packages/mdx/test/evaluate.js +++ b/packages/mdx/test/evaluate.js @@ -6,6 +6,8 @@ import {renderToStaticMarkup as renderToStaticMarkup_} from '../../react/node_mo // @ts-expect-error: make sure a single react is used. import * as runtime_ from '../../react/node_modules/react/jsx-runtime.js' // @ts-expect-error: make sure a single react is used. +import * as devRuntime from '../../react/node_modules/react/jsx-dev-runtime.js' +// @ts-expect-error: make sure a single react is used. import React_ from '../../react/node_modules/react/index.js' import * as provider from '../../react/index.js' @@ -30,7 +32,6 @@ test('evaluate', async () => { assert.throws( () => { - // @ts-expect-error: missing required arguments evaluateSync('a', {Fragment: runtime.Fragment}) }, /Expected `jsx` given to `evaluate`/, @@ -39,13 +40,20 @@ test('evaluate', async () => { assert.throws( () => { - // @ts-expect-error: missing required arguments evaluateSync('a', {Fragment: runtime.Fragment, jsx: runtime.jsx}) }, /Expected `jsxs` given to `evaluate`/, 'should throw on missing `jsxs`' ) + assert.throws( + () => { + evaluateSync('a', {Fragment: runtime.Fragment, development: true}) + }, + /Expected `jsxDEV` given to `evaluate`/, + 'should throw on missing `jsxDEV` in dev mode' + ) + assert.equal( renderToStaticMarkup( React.createElement((await evaluate('# hi!', runtime)).default) @@ -62,6 +70,27 @@ test('evaluate', async () => { 'should evaluate (sync)' ) + assert.equal( + renderToStaticMarkup( + React.createElement( + (await evaluate('# hi dev!', {development: true, ...devRuntime})) + .default + ) + ), + '

hi dev!

', + 'should evaluate (sync)' + ) + + assert.equal( + renderToStaticMarkup( + React.createElement( + evaluateSync('# hi dev!', {development: true, ...devRuntime}).default + ) + ), + '

hi dev!

', + 'should evaluate (sync)' + ) + assert.equal( renderToStaticMarkup( React.createElement(