diff --git a/packages/loader/index.js b/packages/loader/index.js index 383f3434f..18f8b3cde 100644 --- a/packages/loader/index.js +++ b/packages/loader/index.js @@ -1,10 +1,7 @@ const {getOptions} = require('loader-utils') const mdx = require('@mdx-js/mdx') -const DEFAULT_RENDERER = ` -import React from 'react' -import {mdx} from '@mdx-js/react' -` +const DEFAULT_RENDERER = 'import React from "react"' module.exports = async function (content) { const callback = this.async() diff --git a/packages/loader/test/index.test.js b/packages/loader/test/index.test.js index e073eb0a7..e67518d7b 100644 --- a/packages/loader/test/index.test.js +++ b/packages/loader/test/index.test.js @@ -5,9 +5,7 @@ const webpack = require('webpack') const MemoryFs = require('memory-fs') const React = require('react') const {renderToString} = require('react-dom/server') -const _extends = require('@babel/runtime/helpers/extends') -const _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties') -const {mdx} = require('../../react') +const {useMDXComponents} = require('../../react') const transform = (filePath, options) => { return new Promise((resolve, reject) => { @@ -57,30 +55,18 @@ const transform = (filePath, options) => { } const run = value => { - // Webpack 5 (replace everything in this function with): - // const val = 'return ' + value.replace(/__webpack_require__\(0\)/, 'return $&') - // - // // eslint-disable-next-line no-new-func - // return new Function(val)().default // Replace import/exports w/ parameters and return value. const val = value - .replace( - /import _objectWithoutProperties from "@babel\/runtime\/helpers\/objectWithoutProperties";/, - '' - ) - .replace(/import _extends from "@babel\/runtime\/helpers\/extends";/, '') - .replace(/import React from 'react';/, '') - .replace(/import \{ mdx } from '@mdx-js\/react';/, '') + .replace(/import "core-js\/.+";/g, '') + .replace(/import React from "react";/, '') + .replace(/import \{.+?\} from "@mdx-js\/react";/g, '') .replace(/export default/, 'return') // eslint-disable-next-line no-new-func - return new Function( - 'mdx', - 'React', - '_extends', - '_objectWithoutProperties', - val - )(mdx, React, _extends, _objectWithoutProperties) + return new Function('React', '__provideComponents', val)( + React, + useMDXComponents + ) } describe('@mdx-js/loader', () => { diff --git a/packages/mdx/.babelrc b/packages/mdx/.babelrc index a8aaa0476..1938ea380 100644 --- a/packages/mdx/.babelrc +++ b/packages/mdx/.babelrc @@ -7,6 +7,11 @@ "useBuiltIns": "usage" } ], - "@babel/react" + [ + "@babel/react", + { + "throwIfNamespace": false + } + ] ] } diff --git a/packages/mdx/estree-to-js.js b/packages/mdx/estree-to-js.js index f05a5c9c9..558f44126 100644 --- a/packages/mdx/estree-to-js.js +++ b/packages/mdx/estree-to-js.js @@ -1,4 +1,5 @@ const astring = require('astring') +const stringifyEntities = require('stringify-entities/light') module.exports = estreeToJs @@ -20,7 +21,7 @@ const customGenerator = Object.assign({}, astring.baseGenerator, { }) function estreeToJs(estree) { - return astring.generate(estree, {generator: customGenerator}) + return astring.generate(estree, {generator: customGenerator, comments: true}) } // `attr="something"` @@ -30,7 +31,16 @@ function JSXAttribute(node, state) { if (node.value != null) { state.write('=') - this[node.value.type](node.value, state) + + // Encode double quotes in attribute values. + if (node.value.type === 'Literal' && typeof node.value.value === 'string') { + state.write( + '"' + stringifyEntities(node.value.value, {subset: ['"']}) + '"', + node + ) + } else { + this[node.value.type](node.value, state) + } } } diff --git a/packages/mdx/index.js b/packages/mdx/index.js index b5ea9a823..433e10a91 100644 --- a/packages/mdx/index.js +++ b/packages/mdx/index.js @@ -6,10 +6,6 @@ const minifyWhitespace = require('rehype-minify-whitespace') const mdxAstToMdxHast = require('./mdx-ast-to-mdx-hast') const mdxHastToJsx = require('./mdx-hast-to-jsx') -const pragma = `/* @jsxRuntime classic */ -/* @jsx mdx */ -/* @jsxFrag mdx.Fragment */` - function createMdxAstCompiler(options = {}) { return unified() .use(remarkParse) @@ -37,13 +33,13 @@ function createConfig(mdx, options) { } function sync(mdx, options = {}) { - const file = createCompiler(options).processSync(createConfig(mdx, options)) - return pragma + '\n' + String(file) + return String(createCompiler(options).processSync(createConfig(mdx, options))) } async function compile(mdx, options = {}) { - const file = await createCompiler(options).process(createConfig(mdx, options)) - return pragma + '\n' + String(file) + return String( + await createCompiler(options).process(createConfig(mdx, options)) + ) } module.exports = compile diff --git a/packages/mdx/mdx-hast-to-jsx.js b/packages/mdx/mdx-hast-to-jsx.js index e739e258d..ed61b140e 100644 --- a/packages/mdx/mdx-hast-to-jsx.js +++ b/packages/mdx/mdx-hast-to-jsx.js @@ -1,30 +1,110 @@ const toEstree = require('hast-util-to-estree') +const isIdentifierName = require('estree-util-is-identifier-name').name const walk = require('estree-walker').walk const periscopic = require('periscopic') const estreeToJs = require('./estree-to-js') +const own = {}.hasOwnProperty + +const tags = [ + 'a', + 'blockquote', + 'code', + 'del', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'img', + 'inlineCode', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'ul' +] + +const parentChildComponents = { + li: ['ol', 'ul'], + p: ['blockquote'] +} + +const componentDefaults = { + inlineCode: 'code' +} + function serializeEstree(estree, options) { const { // Default options skipExport = false, - wrapExport + wrapExport, + mdxProviderImportSource = '@mdx-js/react', + + jsxImportSource, + jsxRuntime = jsxImportSource ? 'automatic' : 'classic', + pragma = 'React.createElement', + pragmaFrag = 'React.Fragment' } = options - let layout - let children = [] - let mdxLayoutDefault + const comments = [] + const doc = [] + let hasMdxLayout + let hasMdxContent + + if (jsxRuntime) { + comments.push({type: 'Block', value: '@jsxRuntime ' + jsxRuntime}) + } + + if (jsxRuntime === 'automatic' && jsxImportSource) { + comments.push({type: 'Block', value: '@jsxImportSource ' + jsxImportSource}) + } + + if (jsxRuntime === 'classic' && pragma) { + comments.push({type: 'Block', value: '@jsx ' + pragma}) + } + + if (jsxRuntime === 'classic' && pragmaFrag) { + comments.push({type: 'Block', value: '@jsxFrag ' + pragmaFrag}) + } + + // Add JSX pragma comments. + estree.comments.unshift(...comments) // Find the `export default`, the JSX expression, and leave the rest // (import/exports) as they are. - estree.body = estree.body.filter(child => { + estree.body.forEach(child => { // ```js - // export default a = 1 + // export default props => <>{props.children} // ``` - if (child.type === 'ExportDefaultDeclaration') { - layout = child.declaration - return false + // + // Treat it as an inline layout declaration. + if (!hasMdxLayout && child.type === 'ExportDefaultDeclaration') { + hasMdxLayout = true + doc.push({ + type: 'VariableDeclaration', + declarations: [ + { + type: 'VariableDeclarator', + id: {type: 'Identifier', name: 'MDXLayout'}, + init: child.declaration + } + ], + kind: 'const' + }) } - + // Look for default “reexports”. + // // ```js // export {default} from "a" // export {default as a} from "b" @@ -32,148 +112,453 @@ function serializeEstree(estree, options) { // export {a as default} from "b" // export {a as default, b} from "c" // ``` - if (child.type === 'ExportNamedDeclaration' && child.source) { + else if (child.type === 'ExportNamedDeclaration' && child.source) { // Remove `default` or `as default`, but not `default as`, specifier. child.specifiers = child.specifiers.filter(specifier => { - if (specifier.exported.name === 'default') { - mdxLayoutDefault = {local: specifier.local, source: child.source} + if (!hasMdxLayout && specifier.exported.name === 'default') { + hasMdxLayout = true + // Make it just an import: `import MDXLayout from "..."`. + doc.push({ + type: 'ImportDeclaration', + specifiers: [ + // Default as default / something else as default. + specifier.local.name === 'default' + ? { + type: 'ImportDefaultSpecifier', + local: {type: 'Identifier', name: 'MDXLayout'} + } + : { + type: 'ImportSpecifier', + imported: specifier.local, + local: {type: 'Identifier', name: 'MDXLayout'} + } + ], + source: {type: 'Literal', value: child.source.value} + }) + return false } return true }) - // Keep the export if there are other specifiers, drop it if there was - // just a default. - return child.specifiers.length > 0 - } - - if ( + // If there are other things imported, keep it. + if (child.specifiers.length) { + doc.push(child) + } + } else if ( child.type === 'ExpressionStatement' && (child.expression.type === 'JSXFragment' || child.expression.type === 'JSXElement') ) { - children = - child.expression.type === 'JSXFragment' - ? child.expression.children - : [child.expression] - return false + let expression = child.expression + + // Depending on the hast, we’ll almost always have a fragment. + // Use a `
` if fragments are not supported. + if (expression.type === 'JSXFragment' && options.mdxFragment === false) { + expression = { + type: 'JSXElement', + openingElement: { + type: 'JSXOpeningElement', + attributes: [], + name: {type: 'JSXIdentifier', name: 'div'} + }, + closingElement: { + type: 'JSXClosingElement', + name: {type: 'JSXIdentifier', name: 'div'} + }, + children: expression.children + } + } + + hasMdxContent = true + doc.push(...createMdxContent(expression)) + } else { + doc.push(child) + } + }) + + // If there was no JSX content at all, add an empty function. + if (!hasMdxContent) { + doc.push(...createMdxContent()) + } + + if (!skipExport) { + let declaration = {type: 'Identifier', name: 'MDXContent'} + + if (wrapExport) { + declaration = { + type: 'CallExpression', + callee: {type: 'Identifier', name: wrapExport}, + arguments: [declaration] + } } - return true + doc.push({type: 'ExportDefaultDeclaration', declaration: declaration}) + } + + estree.body = doc + + const info = rewriteIdentifiers(estree, { + ...options, + // Find everything that’s defined in the top-level scope. + inTopScope: [...periscopic.analyze(estree).scope.declarations.keys()] }) - // Find everything that’s defined in the top-level scope. - // Do this here because `estree` currently only includes import/exports - // and we don’t have to walk all the JSX to figure out the top scope. - const inTopScope = [ - 'MDXLayout', - 'MDXContent', - ...periscopic.analyze(estree).scope.declarations.keys() - ] + // If there are “shortcodes” (undefined components expected to be passed), + // add the helper. + if (info.useShortcodeHelper) { + estree.body.unshift( + createShortcodeFallbackHelper(options.mdxFragment === false) + ) + } - estree.body = [ - ...estree.body, - ...createMdxLayout(layout, mdxLayoutDefault), - ...createMdxContent(children) - ] + // Import the provider when needed. + if (info.useProvidedComponentsHelper) { + estree.body.unshift({ + type: 'ImportDeclaration', + specifiers: [ + { + type: 'ImportSpecifier', + imported: {type: 'Identifier', name: 'useMDXComponents'}, + local: {type: 'Identifier', name: '__provideComponents'} + } + ], + source: {type: 'Literal', value: mdxProviderImportSource} + }) + } + + return estreeToJs(estree) +} - // Add `mdxType`, `parentName` props to JSX elements. - const magicShortcodes = [] +function compile(options = {}) { + function compiler(tree) { + return serializeEstree(toEstree(tree), options) + } + + this.Compiler = compiler +} + +module.exports = compile +compile.default = compile + +function rewriteIdentifiers(estree, options) { const stack = [] + let useShortcodeHelper = false + let useProvidedComponentsHelper = false walk(estree, { + // eslint-disable-next-line complexity enter: function (node) { if ( - node.type === 'JSXElement' && - // To do: support members (``). - node.openingElement.name.type === 'JSXIdentifier' + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' ) { - const name = node.openingElement.name.name + stack.push({ + node, + objects: [], + components: [], + markdownComponents: [], + elements: [] + }) + } - if (stack.length > 1) { - const parentName = stack[stack.length - 1] + if (node.type === 'JSXElement' && stack.length) { + // Note: inject into the top-level function that contains JSX. + const scope = stack[0] + const nameNode = node.openingElement.name - node.openingElement.attributes.push({ - type: 'JSXAttribute', - name: {type: 'JSXIdentifier', name: 'parentName'}, - value: { - type: 'Literal', - value: parentName, - raw: JSON.stringify(parentName) - } - }) + // ``, ``, ``. + if (nameNode.type === 'JSXMemberExpression') { + let left = nameNode + + while (left.type === 'JSXMemberExpression') { + left = left.object + } + + // `left` is now always an identifier. + if (!scope.objects.includes(left.name)) { + scope.objects.push(left.name) + } } + // ``. + else if (nameNode.type === 'JSXNamespacedName') { + // Ignore namespaces. + } else { + const name = nameNode.name + // If the name is a valid ES identifier, and it doesn’t start with a + // lowecase letter, it’s a component. + // For example, `$foo`, `_bar`, `Baz` are all component names. + // But `foo` and `b-ar` are tag names. + if (isIdentifierName(name) && !/^[a-z]/.test(name)) { + if (!scope.components.includes(name)) { + scope.components.push(name) + } + } else if (tags.includes(name)) { + let selector = name + const parent = scope.elements[scope.elements.length - 1] + + if ( + own.call(parentChildComponents, name) && + parent && + parent.openingElement.name.type === 'JSXMemberExpression' && + parent.openingElement.name.object.type === 'JSXIdentifier' && + parent.openingElement.name.object.name === '__components' && + parentChildComponents[name].includes( + parent.openingElement.name.property.name + ) + ) { + selector = parent.openingElement.name.property.name + '.' + name + } - const head = name.charAt(0) + if (!scope.markdownComponents.includes(selector)) { + scope.markdownComponents.push(selector) + } - // A component. - if (head === head.toUpperCase() && name !== 'MDXLayout') { - node.openingElement.attributes.push({ - type: 'JSXAttribute', - name: {type: 'JSXIdentifier', name: 'mdxType'}, - value: {type: 'Literal', value: name, raw: JSON.stringify(name)} - }) + node.openingElement.name = { + type: 'JSXMemberExpression', + object: {type: 'JSXIdentifier', name: '__components'}, + property: { + type: 'JSXIdentifier', + name: selectorToIdentifierName(selector) + } + } - if (!inTopScope.includes(name) && !magicShortcodes.includes(name)) { - magicShortcodes.push(name) + if (node.closingElement) { + node.closingElement.name = { + type: 'JSXMemberExpression', + object: {type: 'JSXIdentifier', name: '__components'}, + property: { + type: 'JSXIdentifier', + name: selectorToIdentifierName(selector) + } + } + } } } - stack.push(name) + scope.elements.push(node) } }, leave: function (node) { if ( - node.type === 'JSXElement' && - // To do: support members (``). - node.openingElement.name.type === 'JSXIdentifier' + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' ) { - stack.pop() + const scope = stack.pop() + const info = injectComponents(scope.node, scope, options) + + if (info.useShortcodeHelper) useShortcodeHelper = true + if (info.useProvidedComponentsHelper) useProvidedComponentsHelper = true + } + + if (node.type === 'JSXElement' && stack.length) { + stack[stack.length - 1].elements.pop() } } }) - const exports = [] + return {useShortcodeHelper, useProvidedComponentsHelper} +} - if (!skipExport) { - let declaration = {type: 'Identifier', name: 'MDXContent'} +function injectComponents(func, state, options) { + const {inTopScope, mdxProviderImportSource = '@mdx-js/react'} = options + const markdownComponents = [ + ...new Set( + state.markdownComponents.map(member => member.split('.').pop()).sort() + ) + ] + const parentalMarkdownComponents = state.markdownComponents + .filter(member => member.indexOf('.') > -1) + .sort() + const components = state.components.filter(id => !inTopScope.includes(id)) + const requiredComponents = components.filter(id => id !== 'MDXLayout') + const objects = state.objects.filter(id => !inTopScope.includes(id)) + const isMdxContent = + func.type === 'FunctionDeclaration' && func.id.name === 'MDXContent' + const injectLayout = isMdxContent && !inTopScope.includes('MDXLayout') + const nodes = [] + let useShortcodeHelper = false + let useProvidedComponentsHelper = false + + // We need to define some variables, add them here: + let declarations = [] + + if ( + markdownComponents.length || + components.length || + objects.length || + injectLayout + ) { + useShortcodeHelper = requiredComponents.length > 0 + + const parameters = [ + { + type: 'ObjectExpression', + properties: [ + ...markdownComponents.map(member => ({ + type: 'Property', + key: {type: 'Identifier', name: member}, + value: { + type: 'Literal', + value: + member in componentDefaults ? componentDefaults[member] : member + }, + kind: 'init' + })), + ...requiredComponents.map(id => ({ + type: 'Property', + key: {type: 'Identifier', name: id}, + value: { + type: 'CallExpression', + callee: {type: 'Identifier', name: 'mdxShortcodeFallback'}, + arguments: [{type: 'Literal', value: id}] + }, + kind: 'init' + })) + ] + } + ] - if (wrapExport) { - declaration = { + // Accept provided components if there is an import source defined. + if (mdxProviderImportSource) { + useProvidedComponentsHelper = true + parameters.push({ type: 'CallExpression', - callee: {type: 'Identifier', name: wrapExport}, - arguments: [declaration] - } + callee: {type: 'Identifier', name: '__provideComponents'}, + arguments: [] + }) } - exports.push({type: 'ExportDefaultDeclaration', declaration: declaration}) - } + // Accept `components` as a prop if this is the `MDXContent` component. + if (isMdxContent) { + parameters.push({ + type: 'MemberExpression', + object: {type: 'Identifier', name: '__props'}, + property: {type: 'Identifier', name: 'components'} + }) + } - estree.body = [ - ...createMakeShortcodeHelper( - magicShortcodes, - options.mdxFragment === false - ), - ...estree.body, - ...exports - ] + declarations.push({ + type: 'VariableDeclarator', + id: {type: 'Identifier', name: '__components'}, + init: + parameters.length > 1 + ? { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: {type: 'Identifier', name: 'Object'}, + property: {type: 'Identifier', name: 'assign'} + }, + arguments: parameters + } + : parameters[0] + }) - return estreeToJs(estree) -} + // Add components to scope. + // For `['MyComponent', 'MDXLayout']` this generates: + // ```js + // const {MyComponent, wrapper: MDXLayout} = __components + // ``` + // Note that MDXLayout is special as it’s taken from `__components.wrapper`. + if (components.length || objects.length) { + declarations.push({ + type: 'VariableDeclarator', + id: { + type: 'ObjectPattern', + properties: [ + ...objects.map(id => ({ + type: 'Property', + shorthand: true, + key: {type: 'Identifier', name: id}, + value: {type: 'Identifier', name: id}, + kind: 'init' + })), + ...components.map(id => ({ + type: 'Property', + shorthand: id !== 'MDXLayout', + key: { + type: 'Identifier', + name: id === 'MDXLayout' ? 'wrapper' : id + }, + value: {type: 'Identifier', name: id}, + kind: 'init' + })) + ] + }, + init: {type: 'Identifier', name: '__components'} + }) + } -function compile(options = {}) { - function compiler(tree, file) { - return serializeEstree(toEstree(tree), {filename: file.path, ...options}) + // Flush the declared variables. + nodes.push({ + type: 'VariableDeclaration', + declarations: declarations, + kind: 'const' + }) + + if (parentalMarkdownComponents.length) { + // For `parent.child` combos we default to their child. + // + // For `['blockquote.p', 'ol.li']` this generates: + // ```js + // __components.blockquoteP = __components['blockquote.p'] || __components.p + // __components.olLi = __components['ol.li'] || __components.li + // ``` + parentalMarkdownComponents.forEach(name => { + nodes.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: {type: 'Identifier', name: '__components'}, + property: { + type: 'Identifier', + name: selectorToIdentifierName(name) + } + }, + right: { + type: 'LogicalExpression', + left: { + type: 'MemberExpression', + object: {type: 'Identifier', name: '__components'}, + property: {type: 'Literal', value: name}, + computed: true + }, + right: { + type: 'MemberExpression', + object: {type: 'Identifier', name: '__components'}, + property: {type: 'Identifier', name: name.split('.').pop()} + }, + operator: '||' + } + } + }) + }) + } } - this.Compiler = compiler -} + if (nodes.length) { + // Arrow functions with an implied return: + if (func.body.type !== 'BlockStatement') { + func.body = { + type: 'BlockStatement', + body: [{type: 'ReturnStatement', argument: func.body}] + } + } -module.exports = compile -compile.default = compile + func.body.body = [...nodes, ...func.body.body] + } + + return {useShortcodeHelper, useProvidedComponentsHelper} +} -function createMdxContent(children) { +function createMdxContent(content) { return [ { type: 'FunctionDeclaration', @@ -181,54 +566,51 @@ function createMdxContent(children) { expression: false, generator: false, async: false, - params: [ - { - type: 'ObjectPattern', - properties: [ - { - type: 'Property', - method: false, - shorthand: true, - computed: false, - key: {type: 'Identifier', name: 'components'}, - kind: 'init', - value: {type: 'Identifier', name: 'components'} - }, - {type: 'RestElement', argument: {type: 'Identifier', name: 'props'}} - ] - } - ], + params: [{type: 'Identifier', name: '__props'}], body: { type: 'BlockStatement', body: [ + { + type: 'VariableDeclaration', + declarations: [ + { + type: 'VariableDeclarator', + id: {type: 'Identifier', name: '__content'}, + init: content || {type: 'Literal', value: null} + } + ], + kind: 'const' + }, { type: 'ReturnStatement', argument: { - type: 'JSXElement', - openingElement: { - type: 'JSXOpeningElement', - attributes: [ - { - type: 'JSXAttribute', - name: {type: 'JSXIdentifier', name: 'components'}, - value: { - type: 'JSXExpressionContainer', - expression: {type: 'Identifier', name: 'components'} + type: 'ConditionalExpression', + test: {type: 'Identifier', name: 'MDXLayout'}, + consequent: { + type: 'JSXElement', + openingElement: { + type: 'JSXOpeningElement', + attributes: [ + { + type: 'JSXSpreadAttribute', + argument: {type: 'Identifier', name: '__props'} } - }, + ], + name: {type: 'JSXIdentifier', name: 'MDXLayout'}, + selfClosing: false + }, + closingElement: { + type: 'JSXClosingElement', + name: {type: 'JSXIdentifier', name: 'MDXLayout'} + }, + children: [ { - type: 'JSXSpreadAttribute', - argument: {type: 'Identifier', name: 'props'} + type: 'JSXExpressionContainer', + expression: {type: 'Identifier', name: '__content'} } - ], - name: {type: 'JSXIdentifier', name: 'MDXLayout'}, - selfClosing: false + ] }, - closingElement: { - type: 'JSXClosingElement', - name: {type: 'JSXIdentifier', name: 'MDXLayout'} - }, - children: children + alternate: {type: 'Identifier', name: '__content'} } } ] @@ -252,46 +634,13 @@ function createMdxContent(children) { ] } -function createMdxLayout(declaration, mdxLayoutDefault) { - const id = {type: 'Identifier', name: 'MDXLayout'} - const init = {type: 'Literal', value: 'wrapper', raw: '"wrapper"'} - - return [ - mdxLayoutDefault - ? { - type: 'ImportDeclaration', - specifiers: [ - mdxLayoutDefault.local.name === 'default' - ? {type: 'ImportDefaultSpecifier', local: id} - : { - type: 'ImportSpecifier', - imported: mdxLayoutDefault.local, - local: id - } - ], - source: { - type: 'Literal', - value: mdxLayoutDefault.source.value, - raw: mdxLayoutDefault.source.raw - } - } - : { - type: 'VariableDeclaration', - declarations: [ - {type: 'VariableDeclarator', id: id, init: declaration || init} - ], - kind: 'const' - } - ] -} - -function createMakeShortcodeHelper(names, useElement) { - const func = { +function createShortcodeFallbackHelper(useElement) { + return { type: 'VariableDeclaration', declarations: [ { type: 'VariableDeclarator', - id: {type: 'Identifier', name: 'makeShortcode'}, + id: {type: 'Identifier', name: 'mdxShortcodeFallback'}, init: { type: 'ArrowFunctionExpression', id: null, @@ -305,7 +654,7 @@ function createMakeShortcodeHelper(names, useElement) { expression: false, generator: false, async: false, - params: [{type: 'Identifier', name: 'props'}], + params: [{type: 'Identifier', name: '__props'}], body: { type: 'BlockStatement', body: [ @@ -340,7 +689,7 @@ function createMakeShortcodeHelper(names, useElement) { attributes: [ { type: 'JSXSpreadAttribute', - argument: {type: 'Identifier', name: 'props'} + argument: {type: 'Identifier', name: '__props'} } ], name: {type: 'JSXIdentifier', name: 'div'}, @@ -358,7 +707,7 @@ function createMakeShortcodeHelper(names, useElement) { type: 'JSXExpressionContainer', expression: { type: 'MemberExpression', - object: {type: 'Identifier', name: 'props'}, + object: {type: 'Identifier', name: '__props'}, property: {type: 'Identifier', name: 'children'}, computed: false } @@ -374,22 +723,12 @@ function createMakeShortcodeHelper(names, useElement) { ], kind: 'const' } +} - const shortcodes = names.map(name => ({ - type: 'VariableDeclaration', - declarations: [ - { - type: 'VariableDeclarator', - id: {type: 'Identifier', name: String(name)}, - init: { - type: 'CallExpression', - callee: {type: 'Identifier', name: 'makeShortcode'}, - arguments: [{type: 'Literal', value: String(name)}] - } - } - ], - kind: 'const' - })) +function selectorToIdentifierName(selector) { + return selector.replace(/\../g, replace) - return shortcodes.length > 0 ? [func, ...shortcodes] : [] + function replace($0) { + return $0.charAt(1).toUpperCase() + } } diff --git a/packages/mdx/package.json b/packages/mdx/package.json index c25bb84fc..182f3616c 100644 --- a/packages/mdx/package.json +++ b/packages/mdx/package.json @@ -46,6 +46,7 @@ "@mdx-js/util": "2.0.0-next.1", "astring": "^1.4.0", "detab": "^2.0.0", + "estree-util-is-identifier-name": "^1.0.0", "estree-walker": "^2.0.0", "hast-util-to-estree": "^1.1.0", "mdast-util-to-hast": "^10.1.0", @@ -54,10 +55,12 @@ "remark-mdx": "^2.0.0-next.8", "remark-parse": "^9.0.0", "remark-squeeze-paragraphs": "^4.0.0", + "stringify-entities": "^3.1.0", "unified": "^9.2.0", "unist-builder": "^2.0.0" }, "devDependencies": { + "@emotion/react": "^11.0.0", "remark-footnotes": "^3.0.0", "remark-gfm": "^1.0.0", "rehype-katex": "^4.0.0", diff --git a/packages/mdx/test/index.test.js b/packages/mdx/test/index.test.js index 6ad30e603..41708fe32 100644 --- a/packages/mdx/test/index.test.js +++ b/packages/mdx/test/index.test.js @@ -2,8 +2,10 @@ const path = require('path') const babel = require('@babel/core') const unified = require('unified') const React = require('react') +const reactRuntime = require('react/jsx-runtime') +const emotionRuntime = require('@emotion/react/jsx-runtime') const {renderToStaticMarkup} = require('react-dom/server') -const {mdx: mdxReact, MDXProvider} = require('../../react') +const {MDXProvider, useMDXComponents} = require('../../react') const mdx = require('..') const toMdxHast = require('../mdx-ast-to-mdx-hast') const toJsx = require('../mdx-hast-to-jsx') @@ -13,27 +15,36 @@ const math = require('remark-math') const katex = require('rehype-katex') const run = async (value, options = {}) => { - const doc = await mdx(value, {...options, skipExport: true}) + let doc = await mdx(value, {...options, skipExport: true}) + + // Remove the import. + doc = doc.replace(/import \{.+?\} from "@mdx-js\/react";/g, '') // …and that into serialized JS. const {code} = await babel.transformAsync(doc, { configFile: false, plugins: [ - '@babel/plugin-transform-react-jsx', + ['@babel/plugin-transform-react-jsx', {throwIfNamespace: false}], path.resolve(__dirname, '../../babel-plugin-remove-export-keywords') ] }) // …and finally run it, returning the component. // eslint-disable-next-line no-new-func - return new Function('mdx', `${code}; return MDXContent`)(mdxReact) + return new Function( + 'React', + '__provideComponents', + `${code}; return MDXContent` + )(React, useMDXComponents) } describe('@mdx-js/mdx', () => { it('should generate JSX', async () => { const result = await mdx('Hello World') - expect(result).toMatch(/

\{"Hello World"\}<\/p>/) + expect(result).toMatch( + /<__components\.p>\{"Hello World"\}<\/__components\.p>/ + ) }) it('should generate runnable JSX', async () => { @@ -488,10 +499,10 @@ describe('@mdx-js/mdx', () => { // something else. result = await mdx('export {default as a} from "b"') expect(result).toMatch(/export {default as a} from "b"/) - expect(result).toMatch(/const MDXLayout = "wrapper"/) + expect(result).toMatch(/wrapper: MDXLayout/) result = await mdx('export {default as a, b} from "c"') expect(result).toMatch(/export {default as a, b} from "c"/) - expect(result).toMatch(/const MDXLayout = "wrapper"/) + expect(result).toMatch(/wrapper: MDXLayout/) // These are export defaults. result = await mdx('export {a as default} from "b"') @@ -542,7 +553,7 @@ describe('@mdx-js/mdx', () => { }) it('should support overwriting missing compile-time components at run-time', async () => { - const Content = await run('x z') + const Content = await run('x z w.') expect( renderToStaticMarkup( @@ -557,12 +568,65 @@ describe('@mdx-js/mdx', () => { ).toEqual( renderToStaticMarkup(

- x z + x z{' '} + w. +

+ ) + ) + }) + + it('should not be able to overwrite markdown constructs with an export', async () => { + const Content = await run('export const em = () => !\n\n*?*') + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( +

+ ? +

+ ) + ) + }) + + it('should prefer provided components over inline exported components', async () => { + const Content = await run('export const Em = () => !\n\n?') + + expect( + renderToStaticMarkup( + . + }} + > + + + ) + ).toEqual( + renderToStaticMarkup( +

+ !

) ) }) + it('should overwrite components in inline exports with provided components', async () => { + const Content = await run( + 'export const Component = () => !\n\n' + ) + + expect( + renderToStaticMarkup( + . + }} + > + + + ) + ).toEqual(renderToStaticMarkup(.)) + }) + it('should not crash but issue a warning when an undefined component is used', async () => { const Content = await run('w y z') const warn = console.warn @@ -580,6 +644,30 @@ describe('@mdx-js/mdx', () => { console.warn = warn }) + it('should not use a fragment to wrap missing components w/ `mdxFragment: false`', async () => { + const Content = await run('a', {mdxFragment: false}) + + const warn = console.warn + console.warn = jest.fn() + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( +
+

+

a
+

+
+ ) + ) + + expect(console.warn).toHaveBeenCalledWith( + 'Component `%s` was not imported, exported, or provided by MDXProvider as global scope', + 'X' + ) + + console.warn = warn + }) + it('should support `.` in component names for members', async () => { const Content = await run( 'export var x = {y: props => }\n\n' @@ -590,6 +678,163 @@ describe('@mdx-js/mdx', () => { ) }) + it('should support given member components', async () => { + const Content = await run('*em*, ') + + expect( + renderToStaticMarkup( + {children}, + z: ({children}) => {children} + } + } + }} + /> + ) + ).toEqual( + renderToStaticMarkup( +

+ + em + + {', '} + +

+ ) + ) + }) + + it('should support provided member components', async () => { + const Content = await run('*em*') + + expect( + renderToStaticMarkup( + {children}}} + }} + > + + + ) + ).toEqual( + renderToStaticMarkup( +

+ + em + +

+ ) + ) + }) + + it('should crash on missing member components', async () => { + const Content = await run('') + + expect(() => { + renderToStaticMarkup() + }).toThrow(/Cannot read property 'y' of undefined/) + }) + + it('should support namespace tags', async () => { + const Content = await run('*em*') + + expect(renderToStaticMarkup()).toEqual( + /* eslint-disable react/jsx-no-undef */ + renderToStaticMarkup( +

+ + em + +

+ ) + /* eslint-enable react/jsx-no-undef */ + ) + }) + + it('should support given parent-child components', async () => { + const Content = await run('* a\n1. b\n> c') + + expect( + renderToStaticMarkup( + {children}, + 'blockquote.p': ({children}) =>
{children}
+ }} + /> + ) + ).toEqual( + renderToStaticMarkup( + <> +
    +
  • a
  • +
+
    + b +
+
+
c
+
+ + ) + ) + }) + + it('should support providing the child component for potential parent-child components', async () => { + const Content = await run('* a\n1. b') + + expect( + renderToStaticMarkup( + {children} + }} + /> + ) + ).toEqual( + renderToStaticMarkup( + <> +
    + a +
+
    + b +
+ + ) + ) + }) + + it('should support providing a child component and a parent-child component', async () => { + const Content = await run('* a\n1. b') + + expect( + renderToStaticMarkup( + {children}, + li: ({children}) => {children} + }} + /> + ) + ).toEqual( + renderToStaticMarkup( + <> +
    + a +
+
    + b +
+ + ) + ) + }) + it('should crash on unknown nodes in mdxhast', async () => { const plugin = () => tree => { // A leaf. @@ -654,6 +899,25 @@ describe('@mdx-js/mdx', () => { expect(Content.isMDXComponent).toEqual(true) }) + it('should handle quotes in attribute values', async () => { + const Content = await run( + '```x"y"z\ncode()\n```\n\nc' + ) + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( + <> +
+            code(){'\n'}
+          
+

+ c +

+ + ) + ) + }) + it('should escape what could look like template literal placeholders in text', async () => { /* eslint-disable no-template-curly-in-string */ const Content = await run('`${x}`') @@ -707,11 +971,148 @@ describe('@mdx-js/mdx', () => { ) ) }) + + it('should work w/o `mdxProviderImportSource`', async () => { + const doc = await mdx('\n*em*', {mdxProviderImportSource: null}) + + expect(doc).not.toMatch(/__provideComponents/) + }) + + it('should warn on missing components w/o `mdxProviderImportSource`', async () => { + const Content = await run('\n*em*', {mdxProviderImportSource: null}) + + const warn = console.warn + console.warn = jest.fn() + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( +

+ em +

+ ) + ) + + expect(console.warn).toHaveBeenCalledWith( + 'Component `%s` was not imported, exported, or provided by MDXProvider as global scope', + 'X' + ) + + console.warn = warn + }) + + it('should support inline components w/o `mdxProviderImportSource`', async () => { + const Content = await run( + 'export const X = props => <>{props.children}.\n\n\n', + {mdxProviderImportSource: null} + ) + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( + <> + + . + + ) + ) + }) + + it('should work w/o `jsxRuntime`', async () => { + const doc = await mdx('\n*em*', {jsxRuntime: null}) + + expect(doc).not.toMatch(/@jsxRuntime/) + }) + + it('should support the automatic runtime', async () => { + let doc = await mdx('*Emphasis*, **strong**, `code`.', { + skipExport: true, + jsxRuntime: 'automatic' + }) + + // Compile JSX away. + let {code} = await babel.transformAsync(doc, { + configFile: false, + plugins: ['@babel/plugin-transform-react-jsx'] + }) + + // Remove the imports. + code = code + .replace(/import \{.+?} from "react\/jsx-runtime";/g, '') + .replace(/import \{.+?} from "@mdx-js\/react";/g, '') + + // eslint-disable-next-line no-new-func + const Content = new Function( + '_Fragment', + '_jsxs', + '_jsx', + '__provideComponents', + `${code}; return MDXContent` + )( + reactRuntime.Fragment, + reactRuntime.jsxs, + reactRuntime.jsx, + useMDXComponents + ) + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( +

+ Emphasis + {', '} + strong + {', '} + code. +

+ ) + ) + }) + + it('should support a `jsxImportSource`', async () => { + let doc = await mdx('*Emphasis* & ', { + skipExport: true, + jsxImportSource: '@emotion/react' + }) + + // Compile JSX away. + let {code} = await babel.transformAsync(doc, { + configFile: false, + plugins: ['@babel/plugin-transform-react-jsx'] + }) + + code = code + .replace(/import \{.+?} from "@emotion\/react\/jsx-runtime";/g, '') + .replace(/import \{.+?} from "@mdx-js\/react";/g, '') + + // eslint-disable-next-line no-new-func + const Content = new Function( + '_Fragment', + '_jsxs', + '_jsx', + '__provideComponents', + `${code}; return MDXContent` + )( + emotionRuntime.Fragment, + emotionRuntime.jsxs, + emotionRuntime.jsx, + useMDXComponents + ) + + expect(renderToStaticMarkup()).toEqual( + renderToStaticMarkup( +

+ Emphasis + {' & '} + +

+ ) + ) + }) }) describe('default', () => { it('should be async', async () => { - expect(mdx('x')).resolves.toMatch(/

{"x"}<\/p>/) + expect(mdx('x')).resolves.toMatch( + /<__components\.p>\{"x"\}<\/__components\.p>/ + ) }) it('should support `remarkPlugins`', async () => { @@ -723,7 +1124,7 @@ describe('default', () => { describe('sync', () => { it('should be sync', () => { - expect(mdx.sync('x')).toMatch(/

{"x"}<\/p>/) + expect(mdx.sync('x')).toMatch(/<__components\.p>\{"x"\}<\/__components\.p>/) }) it('should support `remarkPlugins`', () => { @@ -772,7 +1173,7 @@ describe('createMdxAstCompiler', () => { describe('createCompiler', () => { it('should create a unified processor', () => { expect(String(mdx.createCompiler().processSync('x'))).toMatch( - /

{"x"}<\/p>/ + /<__components\.p>\{"x"\}<\/__components\.p>/ ) }) }) diff --git a/packages/parcel-plugin-mdx/src/MDXAsset.js b/packages/parcel-plugin-mdx/src/MDXAsset.js index d7e776f14..4d1c647f1 100644 --- a/packages/parcel-plugin-mdx/src/MDXAsset.js +++ b/packages/parcel-plugin-mdx/src/MDXAsset.js @@ -3,7 +3,6 @@ const {Asset} = require('parcel-bundler') const mdx = require('@mdx-js/mdx') const prefix = `import React from 'react' -import {mdx} from '@mdx-js/react' ` class MDXAsset extends Asset { diff --git a/packages/parcel-plugin-mdx/test/index.test.js b/packages/parcel-plugin-mdx/test/index.test.js index ef4eabb6a..80bd5833d 100644 --- a/packages/parcel-plugin-mdx/test/index.test.js +++ b/packages/parcel-plugin-mdx/test/index.test.js @@ -23,10 +23,10 @@ describe('MDXAsset', () => { const results = await asset.process() const result = results[0] - expect(result.value).toContain('

{"Test"}

') - expect(result.value).toContain( - '{"component"}' + '<__components.h1>{"Test"}' ) + + expect(result.value).toContain('{"component"}') }) }) diff --git a/packages/preact/src/create-element.js b/packages/preact/src/create-element.js deleted file mode 100644 index 905c05cc6..000000000 --- a/packages/preact/src/create-element.js +++ /dev/null @@ -1,79 +0,0 @@ -import {h, Fragment} from 'preact' -import {forwardRef} from 'preact/compat' - -import {useMDXComponents} from './context' - -const TYPE_PROP_NAME = 'mdxType' - -const DEFAULTS = { - inlineCode: 'code', - wrapper: ({children}) => h(Fragment, {}, children) -} - -const MDXCreateElement = forwardRef((props, ref) => { - const { - components: propComponents, - mdxType, - originalType, - parentName, - ...etc - } = props - - const components = useMDXComponents(propComponents) - const type = mdxType - const Component = - components[`${parentName}.${type}`] || - components[type] || - DEFAULTS[type] || - originalType - - /* istanbul ignore if - To do: what is this useful for? */ - if (propComponents) { - return h(Component, { - ref, - ...etc, - components: propComponents - }) - } - - return h(Component, {ref, ...etc}) -}) - -MDXCreateElement.displayName = 'MDXCreateElement' - -function mdx(type, props) { - const args = arguments - const mdxType = props && props.mdxType - - if (typeof type === 'string' || mdxType) { - const argsLength = args.length - - const createElementArgArray = new Array(argsLength) - createElementArgArray[0] = MDXCreateElement - - const newProps = {} - for (let key in props) { - /* istanbul ignore else - folks putting stuff in `prototype`. */ - if (hasOwnProperty.call(props, key)) { - newProps[key] = props[key] - } - } - - newProps.originalType = type - newProps[TYPE_PROP_NAME] = typeof type === 'string' ? type : mdxType - - createElementArgArray[1] = newProps - - for (let i = 2; i < argsLength; i++) { - createElementArgArray[i] = args[i] - } - - return h.apply(null, createElementArgArray) - } - - return h.apply(null, args) -} - -mdx.Fragment = Fragment - -export default mdx diff --git a/packages/preact/src/index.js b/packages/preact/src/index.js index 954f81f91..38be5b677 100644 --- a/packages/preact/src/index.js +++ b/packages/preact/src/index.js @@ -4,5 +4,3 @@ export { useMDXComponents, withMDXComponents } from './context' - -export {default as mdx} from './create-element' diff --git a/packages/preact/test/test.js b/packages/preact/test/test.js index dd715e064..b9127cf94 100644 --- a/packages/preact/test/test.js +++ b/packages/preact/test/test.js @@ -5,11 +5,17 @@ import {h, Fragment} from 'preact' import {render} from 'preact-render-to-string' import {transformAsync as babelTransform} from '@babel/core' import mdxTransform from '../../mdx' -import {MDXProvider, withMDXComponents, mdx} from '../src' +import {MDXProvider, useMDXComponents, withMDXComponents} from '../src' const run = async value => { // Turn the serialized MDX code into serialized JSX… - const doc = await mdxTransform(value, {skipExport: true}) + let doc = await mdxTransform(value, { + skipExport: true, + pragma: 'h', + pragmaFrag: 'Fragment' + }) + + doc = doc.replace(/import \{.+?\} from "@mdx-js\/react";/g, '') // …and that into serialized JS. const {code} = await babelTransform(doc, { @@ -22,7 +28,12 @@ const run = async value => { // …and finally run it, returning the component. // eslint-disable-next-line no-new-func - return new Function('mdx', `${code}; return MDXContent`)(mdx) + return new Function( + 'h', + 'Fragment', + '__provideComponents', + `${code}; return MDXContent` + )(h, Fragment, useMDXComponents) } describe('@mdx-js/preact', () => { @@ -70,15 +81,6 @@ describe('@mdx-js/preact', () => { expect(render()).toEqual('!') }) - - test('should not crash if weird values could come from JSX', async () => { - // As JSX is function calls, that function can also be used directly in - // MDX. Definitely not a great idea, but it’s an easy way to pass in funky - // values. - const Content = await run('{mdx(1)}') - - expect(render()).toEqual('<1>') - }) }) describe('MDXProvider', () => { diff --git a/packages/preact/types/index.d.ts b/packages/preact/types/index.d.ts index 475aed1a5..c168f3175 100644 --- a/packages/preact/types/index.d.ts +++ b/packages/preact/types/index.d.ts @@ -1,7 +1,6 @@ // TypeScript Version: 3.4 import { - h, Context, AnyComponent, FunctionComponent @@ -53,17 +52,11 @@ declare function withMDXComponents( child: AnyComponent ): ReactElement | null -/** - * Preact hyperscript function wrapped with handler for MDX content - */ -declare const mdx: typeof h - export { ComponentDictionary, ComponentsProp, MDXContext, MDXProvider, useMDXComponents, - withMDXComponents, - mdx + withMDXComponents } diff --git a/packages/react/src/create-element.js b/packages/react/src/create-element.js deleted file mode 100644 index ae5c4c48c..000000000 --- a/packages/react/src/create-element.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' - -import {useMDXComponents} from './context' - -const TYPE_PROP_NAME = 'mdxType' - -const DEFAULTS = { - inlineCode: 'code', - wrapper: ({children}) => React.createElement(React.Fragment, {}, children) -} - -const MDXCreateElement = React.forwardRef((props, ref) => { - const { - components: propComponents, - mdxType, - originalType, - parentName, - ...etc - } = props - - const components = useMDXComponents(propComponents) - const type = mdxType - const Component = - components[`${parentName}.${type}`] || - components[type] || - DEFAULTS[type] || - originalType - - /* istanbul ignore if - To do: what is this useful for? */ - if (propComponents) { - return React.createElement(Component, { - ref, - ...etc, - components: propComponents - }) - } - - return React.createElement(Component, {ref, ...etc}) -}) - -MDXCreateElement.displayName = 'MDXCreateElement' - -function mdx(type, props) { - const args = arguments - const mdxType = props && props.mdxType - - if (typeof type === 'string' || mdxType) { - const argsLength = args.length - - const createElementArgArray = new Array(argsLength) - createElementArgArray[0] = MDXCreateElement - - const newProps = {} - for (let key in props) { - /* istanbul ignore else - folks putting stuff in `prototype`. */ - if (hasOwnProperty.call(props, key)) { - newProps[key] = props[key] - } - } - - newProps.originalType = type - newProps[TYPE_PROP_NAME] = typeof type === 'string' ? type : mdxType - - createElementArgArray[1] = newProps - - for (let i = 2; i < argsLength; i++) { - createElementArgArray[i] = args[i] - } - - return React.createElement.apply(null, createElementArgArray) - } - - return React.createElement.apply(null, args) -} - -mdx.Fragment = React.Fragment - -export default mdx diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 954f81f91..38be5b677 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -4,5 +4,3 @@ export { useMDXComponents, withMDXComponents } from './context' - -export {default as mdx} from './create-element' diff --git a/packages/react/test/test.js b/packages/react/test/test.js index 3bf34409d..0f9aa980c 100644 --- a/packages/react/test/test.js +++ b/packages/react/test/test.js @@ -5,11 +5,13 @@ import React from 'react' import {renderToString} from 'react-dom/server' import {transformAsync as babelTransform} from '@babel/core' import mdxTransform from '../../mdx' -import {MDXProvider, withMDXComponents, mdx} from '../src' +import {MDXProvider, useMDXComponents, withMDXComponents} from '../src' const run = async value => { // Turn the serialized MDX code into serialized JSX… - const doc = await mdxTransform(value, {skipExport: true}) + let doc = await mdxTransform(value, {skipExport: true}) + + doc = doc.replace(/import \{.+?\} from "@mdx-js\/react";/g, '') // …and that into serialized JS. const {code} = await babelTransform(doc, { @@ -22,7 +24,11 @@ const run = async value => { // …and finally run it, returning the component. // eslint-disable-next-line no-new-func - return new Function('mdx', `${code}; return MDXContent`)(mdx) + return new Function( + 'React', + '__provideComponents', + `${code}; return MDXContent` + )(React, useMDXComponents) } describe('@mdx-js/react', () => { @@ -70,23 +76,6 @@ describe('@mdx-js/react', () => { expect(renderToString()).toEqual('!') }) - - test('should crash if weird values could come from JSX', async () => { - // As JSX is function calls, that function can also be used directly in - // MDX. Definitely not a great idea, but it’s an easy way to pass in funky - // values. - const Content = await run('{mdx(1)}') - const error = console.error - console.error = jest.fn() - - expect(() => { - renderToString() - }).toThrow('Element type is invalid') - - expect(console.error).toHaveBeenCalled() - - console.error = error - }) }) describe('MDXProvider', () => { diff --git a/packages/react/types/index.d.ts b/packages/react/types/index.d.ts index aa9a2da9c..88aefff69 100644 --- a/packages/react/types/index.d.ts +++ b/packages/react/types/index.d.ts @@ -5,8 +5,7 @@ import { Consumer, ComponentType, FunctionComponent, - ReactElement, - createElement + ReactElement } from 'react' /** @@ -61,17 +60,11 @@ declare function withMDXComponents( child: ComponentType ): ReactElement | null -/** - * React createElement function wrapped with handler for MDX content - */ -declare const mdx: typeof createElement - export { ComponentDictionary, ComponentsProp, MDXContext, MDXProvider, useMDXComponents, - withMDXComponents, - mdx + withMDXComponents } diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js index 5a0967174..22d15369a 100644 --- a/packages/runtime/src/index.js +++ b/packages/runtime/src/index.js @@ -1,7 +1,7 @@ import React from 'react' import {transform} from 'buble-jsx-only' import mdx from '@mdx-js/mdx' -import {MDXProvider, mdx as createElement} from '@mdx-js/react' +import {MDXProvider, useMDXComponents} from '@mdx-js/react' const suffix = `return React.createElement( MDXProvider, @@ -18,8 +18,9 @@ export default ({ ...props }) => { const fullScope = { - mdx: createElement, + React, MDXProvider, + __provideComponents: useMDXComponents, components, props, ...scope @@ -31,7 +32,7 @@ export default ({ rehypePlugins, skipExport: true }) - .trim() + .replace(/import \{.+?\} from "@mdx-js\/react";/g, '') const code = transform(jsx, {objectAssign: 'Object.assign'}).code @@ -39,7 +40,7 @@ export default ({ const values = Object.values(fullScope) // eslint-disable-next-line no-new-func - const fn = new Function('React', ...keys, `${code}\n\n${suffix}`) + const fn = new Function(...keys, `${code}\n\n${suffix}`) - return fn(React, ...values) + return fn(...values) } diff --git a/packages/vue-loader/index.js b/packages/vue-loader/index.js index becb39b09..45e2b02e8 100644 --- a/packages/vue-loader/index.js +++ b/packages/vue-loader/index.js @@ -1,10 +1,7 @@ const {getOptions} = require('loader-utils') const mdx = require('@mdx-js/mdx') -const prefix = `// Vue babel plugin doesn't support pragma replacement -import {mdx} from '@mdx-js/vue' -let h -` +const prefix = `let h` const suffix = `export default { name: 'Mdx', @@ -19,7 +16,7 @@ const suffix = `export default { } }, render(createElement) { - h = mdx.bind({createElement, components: this.components}) + h = createElement return MDXContent({components: this.components}) } } @@ -38,7 +35,12 @@ async function mdxLoader(content) { result = await mdx(content, { ...options, skipExport: true, - mdxFragment: false + mdxFragment: false, + mdxProviderImportSource: null, + // Don’t add the comments. + jsxRuntime: null, + pragma: null, + pragmaFrag: null }) } catch (err) { return callback(err) diff --git a/packages/vue-loader/test/test.js b/packages/vue-loader/test/test.js index eb97ba5d3..7aede7348 100644 --- a/packages/vue-loader/test/test.js +++ b/packages/vue-loader/test/test.js @@ -2,8 +2,6 @@ const path = require('path') const webpack = require('webpack') const MemoryFs = require('memory-fs') const {mount} = require('@vue/test-utils') -const vueMergeProps = require('babel-helper-vue-jsx-merge-props') -const {mdx} = require('../../vue') // See `loader`’s tests for how to upgrade these to webpack 5. const transform = (filePath, options) => { @@ -45,22 +43,18 @@ const transform = (filePath, options) => { const run = value => { // Replace import/exports w/ parameters and return value. - const val = value - .replace( - /import _mergeJSXProps from "babel-helper-vue-jsx-merge-props";/, - '' - ) - .replace(/import \{ mdx } from '@mdx-js\/vue';/, '') - .replace(/export default/, 'return') + const val = value.replace(/export default/, 'return') // eslint-disable-next-line no-new-func - return new Function('mdx', '_mergeJSXProps', val)(mdx, vueMergeProps) + return new Function(val)() } describe('@mdx-js/vue-loader', () => { test('should create runnable code', async () => { const file = await transform('./fixture.mdx') - expect(mount(run(file.source)).html()).toEqual('

Hello, world!

') + expect(mount(run(file.source)).html()).toEqual( + '
\n

Hello, world!

\n
' + ) }) test('should handle MDX throwing exceptions', async () => { @@ -77,7 +71,7 @@ describe('@mdx-js/vue-loader', () => { }) expect(mount(run(file.source)).html()).toEqual( - '

Hello, world!

' + '
\n

Hello, world!

\n
' ) }) }) diff --git a/packages/vue/src/create-element.js b/packages/vue/src/create-element.js deleted file mode 100644 index 7a7c68f55..000000000 --- a/packages/vue/src/create-element.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * MDX default components - */ -const defaults = { - inlineCode: 'code', - wrapper: { - name: 'MDXWrapper', - render: function (h) { - const children = this.$slots.default - return children.length === 1 ? children : h('div', {}, children) - } - } -} - -const own = {}.hasOwnProperty - -export default function createMdxElement(type, props, children) { - if (own.call(this.components, type)) { - type = this.components[type] - } else if (own.call(defaults, type)) { - type = defaults[type] - } - - if (!children) { - children = [] - } - - if (props && typeof props === 'object' && !Array.isArray(props)) { - // Empty. - } else { - children.unshift(props) - props = {} - } - - children = children.map(d => - typeof d === 'number' || typeof d === 'string' - ? this.createElement('d', {}, String(d)).children[0] - : d - ) - - if (props.attrs) { - // Vue places the special MDX props in `props.attrs`, move them back into - // `props`. - const {components, mdxType, parentName, ...attrs} = props.attrs - props = {...props, components, mdxType, parentName, attrs} - } - - // Just a render function. - if (typeof type === 'function') { - /* istanbul ignore next - V8 is really good at inferring names, but add a name anyway. */ - const name = type.displayName || type.name || 'mdxFunction' - - type = {name, render: type} - } - - // Vue component. - return this.createElement(type, props, children) -} diff --git a/packages/vue/src/index.js b/packages/vue/src/index.js index 22a61a23b..8e843ce13 100644 --- a/packages/vue/src/index.js +++ b/packages/vue/src/index.js @@ -1,2 +1 @@ -export {default as mdx} from './create-element' export {default as MDXProvider} from './mdx-provider' diff --git a/packages/vue/src/mdx-provider.js b/packages/vue/src/mdx-provider.js index be1d12041..9e637c2a3 100644 --- a/packages/vue/src/mdx-provider.js +++ b/packages/vue/src/mdx-provider.js @@ -4,8 +4,8 @@ const MDXProvider = { provide() { return {$mdxComponents: () => this.components} }, - render(h) { - return h('div', this.$slots.default) + render() { + return this.$slots.default } } diff --git a/packages/vue/test/test.js b/packages/vue/test/test.js index ac9894122..0541d95a2 100644 --- a/packages/vue/test/test.js +++ b/packages/vue/test/test.js @@ -1,13 +1,20 @@ import path from 'path' import {mount} from '@vue/test-utils' import mdxTransform from '../../mdx' -import vueMergeProps from 'babel-helper-vue-jsx-merge-props' import {transformAsync as babelTransform} from '@babel/core' -import {MDXProvider, mdx} from '../src' +import {MDXProvider} from '../src' const run = async value => { // Turn the serialized MDX code into serialized JSX… - const doc = await mdxTransform(value, {skipExport: true, mdxFragment: false}) + const doc = await mdxTransform(value, { + skipExport: true, + mdxFragment: false, + mdxProviderImportSource: null, + // Don’t add the comments. + jsxRuntime: null, + pragma: null, + pragmaFrag: null + }) // …and that into serialized JS. const {code} = await babelTransform(doc, { @@ -21,13 +28,8 @@ const run = async value => { // …and finally run it, returning the component. // eslint-disable-next-line no-new-func return new Function( - 'mdx', - '_mergeJSXProps', `let h; - ${code.replace( - /import _mergeJSXProps from "babel-helper-vue-jsx-merge-props";/, - '' - )}; + ${code}; return { name: 'Mdx', @@ -42,18 +44,18 @@ const run = async value => { } }, render(createElement) { - h = mdx.bind({createElement, components: this.components}) + h = createElement return MDXContent({components: this.components}) } }` - )(mdx, vueMergeProps) + )() } describe('@mdx-js/vue', () => { test('should evaluate MDX code', async () => { const Content = await run('# hi') - expect(mount(Content).html()).toEqual('

hi

') + expect(mount(Content).html()).toEqual('
\n

hi

\n
') }) test('should evaluate some more complex MDX code (text, inline)', async () => { @@ -62,7 +64,7 @@ describe('@mdx-js/vue', () => { ) expect(mount(Content).html()).toEqual( - '

a b c MDX

' + '
\n

a b c MDX

\n
' ) }) @@ -90,7 +92,7 @@ describe('@mdx-js/vue', () => { const warn = console.warn console.warn = jest.fn() - expect(mount(Content).html()).toEqual('
') + expect(mount(Content).html()).toEqual('
\n \n
') expect(console.warn).toHaveBeenCalledWith( 'Component `%s` was not imported, exported, or provided by MDXProvider as global scope', @@ -105,7 +107,7 @@ describe('@mdx-js/vue', () => { 'export const A = {render() { return ! }}\n\n' ) - expect(mount(Content).html()).toEqual('!') + expect(mount(Content).html()).toEqual('
!
') }) }) @@ -119,7 +121,7 @@ describe('MDXProvider', () => { }) test('should support `components`', async () => { - const Content = await run('*a* and c.') + const Content = await run('*a* and c and .') expect( mount(MDXProvider, { @@ -136,36 +138,19 @@ describe('MDXProvider', () => { this.$slots.default ) } + }, + Custom: { + name: 'Custom', + render: function (h) { + return h('br') + } } } } }).html() ).toEqual( - '
\n

a and c.

\n
' - ) - }) - - test('should support functional `components`', async () => { - const Content = await run('*a* and c.') - const error = console.error - console.error = jest.fn() // Ignore the warnings that Vue emits. - - expect( - mount(MDXProvider, { - slots: {default: [Content]}, - propsData: { - components: { - em: function (h) { - return h('i', {style: {fontWeight: 'bold'}}, this.$slots.default) - } - } - } - }).html() - ).toEqual( - '
\n

a and c.

\n
' + '
\n

a and c and
.

\n
' ) - - console.error = error }) test('should support the readme example', async () => { diff --git a/packages/vue/types/index.d.ts b/packages/vue/types/index.d.ts index 44f4d0361..66e5f0383 100644 --- a/packages/vue/types/index.d.ts +++ b/packages/vue/types/index.d.ts @@ -1,5 +1,7 @@ // TypeScript Version: 3.4 +// To do: remove this and type the `MDXProvider` component. + import {CreateElement} from 'vue' /** diff --git a/yarn.lock b/yarn.lock index 52f3adf99..4064eb4d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1521,6 +1521,17 @@ "@emotion/utils" "0.11.3" "@emotion/weak-memoize" "0.2.5" +"@emotion/cache@^11.1.3": + version "11.1.3" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.1.3.tgz#c7683a9484bcd38d5562f2b9947873cf66829afd" + integrity sha512-n4OWinUPJVaP6fXxWZD9OUeQ0lY7DvtmtSuqtRWT0Ofo/sBLCVSgb4/Oa0Q5eFxcwablRKjUXqXtNZVyEwCAuA== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.0.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "^4.0.3" + "@emotion/core@10.0.28": version "10.0.28" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.28.tgz#bb65af7262a234593a9e952c041d0f1c9b9bef3d" @@ -1559,7 +1570,7 @@ "@emotion/utils" "0.11.3" babel-plugin-emotion "^10.0.27" -"@emotion/hash@0.8.0": +"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== @@ -1576,11 +1587,24 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== -"@emotion/memoize@^0.7.1": +"@emotion/memoize@^0.7.1", "@emotion/memoize@^0.7.4": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== +"@emotion/react@^11.0.0": + version "11.1.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.1.4.tgz#ddee4247627ff7dd7d0c6ae52f1cfd6b420357d2" + integrity sha512-9gkhrW8UjV4IGRnEe4/aGPkUxoGS23aD9Vu6JCGfEDyBYL+nGkkRBoMFGAzCT9qFdyUvQp4UUtErbKWxq/JS4A== + dependencies: + "@babel/runtime" "^7.7.2" + "@emotion/cache" "^11.1.3" + "@emotion/serialize" "^1.0.0" + "@emotion/sheet" "^1.0.1" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + "@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": version "0.11.16" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" @@ -1592,11 +1616,27 @@ "@emotion/utils" "0.11.3" csstype "^2.5.7" +"@emotion/serialize@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.0.tgz#1a61f4f037cf39995c97fc80ebe99abc7b191ca9" + integrity sha512-zt1gm4rhdo5Sry8QpCOpopIUIKU+mUSpV9WNmFILUraatm5dttNEaYzUWWSboSMUE6PtN2j1cAsuvcugfdI3mw== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + "@emotion/sheet@0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== +"@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.1.tgz#245f54abb02dfd82326e28689f34c27aa9b2a698" + integrity sha512-GbIvVMe4U+Zc+929N1V7nW6YYJtidj31lidSmdYcWozwoBIObXBnaJkKNDjZrLm9Nc0BR+ZyHNaRZxqNZbof5g== + "@emotion/styled-base@^10.0.27": version "10.0.31" resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.31.tgz#940957ee0aa15c6974adc7d494ff19765a2f742a" @@ -1620,7 +1660,7 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== -"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.0": +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.0", "@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== @@ -1630,7 +1670,12 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== -"@emotion/weak-memoize@0.2.5": +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== @@ -13280,7 +13325,7 @@ hoist-non-react-statics@^2.5.5: resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -25094,6 +25139,11 @@ stylis@3.5.4, stylis@^3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" integrity sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q== +stylis@^4.0.3: + version "4.0.6" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.6.tgz#0d8b97b6bc4748bea46f68602b6df27641b3c548" + integrity sha512-1igcUEmYFBEO14uQHAJhCUelTR5jPztfdVKrYxRnDa5D5Dn3w0NxXupJNPr/VV/yRfZYEAco8sTIRZzH3sRYKg== + sudo-prompt@^8.2.0: version "8.2.5" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-8.2.5.tgz#cc5ef3769a134bb94b24a631cc09628d4d53603e"