From f262f4560dc24f2dfc60e9be7eddea15cdd1cc4d Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 23 Dec 2020 16:49:02 +0100 Subject: [PATCH] Remove JSX from output by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, we required an extra build step to produce runnable code. This change makes the output of MDX immediately runnable. This drops the final requirement on Babel (Or Bublé). Dropping Babel leads to a size and performance win for the runtime (and for any use case that doesn’t otherwise require Babel, such as running in Node). Of course, if people want to use the latest JavaScript features, they can still use Babel, but it’s not *required*. Finally, if JSX is preferred (for example, Vue treats JSX radically different from other hyperscript interfaces and has its own JSX builders), `keepJsx` can be set to `true`. In short, the size breakdown for the runtime is: * `@mdx-js/runtime@1.6.22` (last stable tag): 356.4kb * `@mdx-js/runtime@2.0.0-next.8` (last next tag): 362.9kb * Previous commit (on an unmaintained Bublé fork): 165kb * This commit: 120kb (26% / 66% / 69% smaller) Core only adds ±1kb to its bundle size, because `estree-util-build-jsx` reuses dependencies that we already use, too. Related to GH-1041. Related to GH-1044. Related to GH-1152. --- packages/loader/readme.md | 6 +++--- packages/loader/test/index.test.js | 24 +++------------------ packages/mdx/estree-to-js.js | 2 +- packages/mdx/index.js | 12 ++++------- packages/mdx/mdx-hast-to-jsx.js | 13 ++++++++++++ packages/mdx/package.json | 1 + packages/mdx/test/index.test.js | 25 +++++++++++++--------- packages/preact/test/test.js | 1 - packages/react/test/test.js | 1 - packages/runtime/package.json | 3 +-- packages/runtime/src/index.js | 7 ++---- packages/vue-loader/index.js | 3 ++- packages/vue/test/test.js | 6 +++++- yarn.lock | 34 +++++++++++------------------- 14 files changed, 62 insertions(+), 76 deletions(-) diff --git a/packages/loader/readme.md b/packages/loader/readme.md index 4eaec6f4e..8b1542450 100644 --- a/packages/loader/readme.md +++ b/packages/loader/readme.md @@ -31,14 +31,14 @@ module: { // … { test: /\.mdx$/, - use: ['babel-loader', '@mdx-js/loader'] + use: ['@mdx-js/loader'] } ] } ``` -You’ll probably want to configure Babel to use `@babel/preset-react` or so, but -that’s not required. +You might want to add `babel-loader` there too if you have modern JS features +that you want to compile down. All options given to `mdx-js/loader`, except for `renderer` (see below), are passed to MDX itself: diff --git a/packages/loader/test/index.test.js b/packages/loader/test/index.test.js index e073eb0a7..fa70f0393 100644 --- a/packages/loader/test/index.test.js +++ b/packages/loader/test/index.test.js @@ -20,20 +20,7 @@ const transform = (filePath, options) => { rules: [ { test: /\.mdx$/, - use: [ - { - loader: 'babel-loader', - options: { - configFile: false, - plugins: [ - '@babel/plugin-transform-runtime', - '@babel/plugin-syntax-jsx', - '@babel/plugin-transform-react-jsx' - ] - } - }, - {loader: path.resolve(__dirname, '..'), options} - ] + use: [{loader: path.resolve(__dirname, '..'), options}] } ] } @@ -64,13 +51,8 @@ const run = value => { // 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 React from 'react'/, '') + .replace(/import \{mdx} from '@mdx-js\/react'/, '') .replace(/export default/, 'return') // eslint-disable-next-line no-new-func diff --git a/packages/mdx/estree-to-js.js b/packages/mdx/estree-to-js.js index f05a5c9c9..9b86628b5 100644 --- a/packages/mdx/estree-to-js.js +++ b/packages/mdx/estree-to-js.js @@ -20,7 +20,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"` 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..6a56169e2 100644 --- a/packages/mdx/mdx-hast-to-jsx.js +++ b/packages/mdx/mdx-hast-to-jsx.js @@ -1,5 +1,6 @@ const toEstree = require('hast-util-to-estree') const walk = require('estree-walker').walk +const buildJsx = require('estree-util-build-jsx') const periscopic = require('periscopic') const estreeToJs = require('./estree-to-js') @@ -7,6 +8,7 @@ function serializeEstree(estree, options) { const { // Default options skipExport = false, + keepJsx = false, wrapExport } = options @@ -150,6 +152,13 @@ function serializeEstree(estree, options) { exports.push({type: 'ExportDefaultDeclaration', declaration: declaration}) } + // Add JSX pragma comments. + estree.comments.unshift( + {type: 'Block', value: '@jsxRuntime classic'}, + {type: 'Block', value: '@jsx mdx'}, + {type: 'Block', value: '@jsxFrag mdx.Fragment'} + ) + estree.body = [ ...createMakeShortcodeHelper( magicShortcodes, @@ -159,6 +168,10 @@ function serializeEstree(estree, options) { ...exports ] + if (!keepJsx) { + buildJsx(estree) + } + return estreeToJs(estree) } diff --git a/packages/mdx/package.json b/packages/mdx/package.json index c25bb84fc..e1c0d1984 100644 --- a/packages/mdx/package.json +++ b/packages/mdx/package.json @@ -47,6 +47,7 @@ "astring": "^1.4.0", "detab": "^2.0.0", "estree-walker": "^2.0.0", + "estree-util-build-jsx": "^1.0.0", "hast-util-to-estree": "^1.1.0", "mdast-util-to-hast": "^10.1.0", "periscopic": "^2.0.0", diff --git a/packages/mdx/test/index.test.js b/packages/mdx/test/index.test.js index 6ad30e603..84c361a82 100644 --- a/packages/mdx/test/index.test.js +++ b/packages/mdx/test/index.test.js @@ -19,7 +19,6 @@ const run = async (value, options = {}) => { const {code} = await babel.transformAsync(doc, { configFile: false, plugins: [ - '@babel/plugin-transform-react-jsx', path.resolve(__dirname, '../../babel-plugin-remove-export-keywords') ] }) @@ -30,13 +29,19 @@ const run = async (value, options = {}) => { } describe('@mdx-js/mdx', () => { - it('should generate JSX', async () => { + it('should generate JS (by default)', async () => { const result = await mdx('Hello World') + expect(result).toMatch(/mdx\("p", null, "Hello World"\)/) + }) + + it('should generate JSX (`keepJsx: true`)', async () => { + const result = await mdx('Hello World', {keepJsx: true}) + expect(result).toMatch(/

\{"Hello World"\}<\/p>/) }) - it('should generate runnable JSX', async () => { + it('should generate runnable JS', async () => { const Content = await run('Hello World') expect(renderToStaticMarkup()).toEqual( @@ -396,7 +401,7 @@ describe('@mdx-js/mdx', () => { rehypePlugins: [plugin] }) - expect(result).toMatch(/export const A = \(\) => !<\/b>/) + expect(result).toMatch(/export const A = \(\) => mdx\("b", null, "!"\)/) }) it('should crash on incorrect exports', async () => { @@ -711,24 +716,24 @@ describe('@mdx-js/mdx', () => { describe('default', () => { it('should be async', async () => { - expect(mdx('x')).resolves.toMatch(/

{"x"}<\/p>/) + expect(mdx('x')).resolves.toMatch(/mdx\("p", null, "x"\)/) }) it('should support `remarkPlugins`', async () => { expect(mdx('$x$', {remarkPlugins: [math]})).resolves.toMatch( - /className="math math-inline"/ + /className: "math math-inline"/ ) }) }) describe('sync', () => { it('should be sync', () => { - expect(mdx.sync('x')).toMatch(/

{"x"}<\/p>/) + expect(mdx.sync('x')).toMatch(/mdx\("p", null, "x"\)/) }) it('should support `remarkPlugins`', () => { expect(mdx.sync('$x$', {remarkPlugins: [math]})).toMatch( - /className="math math-inline"/ + /className: "math math-inline"/ ) }) }) @@ -772,7 +777,7 @@ describe('createMdxAstCompiler', () => { describe('createCompiler', () => { it('should create a unified processor', () => { expect(String(mdx.createCompiler().processSync('x'))).toMatch( - /

{"x"}<\/p>/ + /mdx\("p", null, "x"\)/ ) }) }) @@ -817,6 +822,6 @@ describe('mdx-hast-to-jsx', () => { const doc = unified().use(toJsx).stringify(tree) expect(doc).toMatch(/export default MDXContent/) - expect(doc).toMatch(/\{"a"}<\/x>/) + expect(doc).toMatch(/mdx\("x", null, "a"\)/) }) }) diff --git a/packages/preact/test/test.js b/packages/preact/test/test.js index dd715e064..f80d9f0e8 100644 --- a/packages/preact/test/test.js +++ b/packages/preact/test/test.js @@ -15,7 +15,6 @@ const run = async value => { const {code} = await babelTransform(doc, { configFile: false, plugins: [ - '@babel/plugin-transform-react-jsx', path.resolve(__dirname, '../../babel-plugin-remove-export-keywords') ] }) diff --git a/packages/react/test/test.js b/packages/react/test/test.js index 3bf34409d..6901bf585 100644 --- a/packages/react/test/test.js +++ b/packages/react/test/test.js @@ -15,7 +15,6 @@ const run = async value => { const {code} = await babelTransform(doc, { configFile: false, plugins: [ - '@babel/plugin-transform-react-jsx', path.resolve(__dirname, '../../babel-plugin-remove-export-keywords') ] }) diff --git a/packages/runtime/package.json b/packages/runtime/package.json index cd8a6defd..43a8fc9a5 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -53,8 +53,7 @@ }, "dependencies": { "@mdx-js/mdx": "^2.0.0-next.8", - "@mdx-js/react": "^2.0.0-next.8", - "buble-jsx-only": "^0.19.8" + "@mdx-js/react": "^2.0.0-next.8" }, "devDependencies": { "microbundle": "^0.12.0", diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js index 5a0967174..aab92f4c6 100644 --- a/packages/runtime/src/index.js +++ b/packages/runtime/src/index.js @@ -1,5 +1,4 @@ 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' @@ -25,7 +24,7 @@ export default ({ ...scope } - const jsx = mdx + const js = mdx .sync(children, { remarkPlugins, rehypePlugins, @@ -33,13 +32,11 @@ export default ({ }) .trim() - const code = transform(jsx, {objectAssign: 'Object.assign'}).code - const keys = Object.keys(fullScope) 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('React', ...keys, `${js}\n\n${suffix}`) return fn(React, ...values) } diff --git a/packages/vue-loader/index.js b/packages/vue-loader/index.js index becb39b09..397e02472 100644 --- a/packages/vue-loader/index.js +++ b/packages/vue-loader/index.js @@ -38,7 +38,8 @@ async function mdxLoader(content) { result = await mdx(content, { ...options, skipExport: true, - mdxFragment: false + mdxFragment: false, + keepJsx: true }) } catch (err) { return callback(err) diff --git a/packages/vue/test/test.js b/packages/vue/test/test.js index ac9894122..4f5981fb3 100644 --- a/packages/vue/test/test.js +++ b/packages/vue/test/test.js @@ -7,7 +7,11 @@ import {MDXProvider, mdx} 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, + keepJsx: true + }) // …and that into serialized JS. const {code} = await babelTransform(doc, { diff --git a/yarn.lock b/yarn.lock index 87d77cb45..45b420b85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4985,11 +4985,6 @@ accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^4.1.0, acorn-globals@^4.3.0: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" @@ -5006,7 +5001,7 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.0.0, acorn-jsx@^5.0.1, acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: +acorn-jsx@^5.0.0, acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -5026,7 +5021,7 @@ acorn@^5.5.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.1, acorn@^6.0.4, acorn@^6.0.7, acorn@^6.1.1, acorn@^6.2.1, acorn@^6.4.1: +acorn@^6.0.1, acorn@^6.0.4, acorn@^6.0.7, acorn@^6.2.1, acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== @@ -6590,19 +6585,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buble-jsx-only@^0.19.8: - version "0.19.8" - resolved "https://registry.yarnpkg.com/buble-jsx-only/-/buble-jsx-only-0.19.8.tgz#6e3524aa0f1c523de32496ac9aceb9cc2b493867" - integrity sha512-7AW19pf7PrKFnGTEDzs6u9+JZqQwM1VnLS19OlqYDhXomtFFknnoQJAPHeg84RMFWAvOhYrG7harizJNwUKJsA== - dependencies: - acorn "^6.1.1" - acorn-dynamic-import "^4.0.0" - acorn-jsx "^5.0.1" - chalk "^2.4.2" - magic-string "^0.25.3" - minimist "^1.2.0" - regexpu-core "^4.5.4" - buble@0.19.6: version "0.19.6" resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3" @@ -10515,6 +10497,14 @@ estree-util-attach-comments@^1.0.0: resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-1.0.0.tgz#51d280e458ce85dec0b813bd96d2ce98eae8a3f2" integrity sha512-sL7dTwFGqzelPlB56lRZY1CC/yDxCe365WQpxNd49ispL40Yv8Tv4SmteGbvZeFwShOOVKfMlo4jrVvwoaMosA== +estree-util-build-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-1.0.0.tgz#80df2b0d8fbdfa7e2e7b16c02d007062af2f0ed0" + integrity sha512-OVzOP9kjOBO7xiN+A7mMjfJQyIxf+prnohyg1afd3sVHW1GTOY55SfyeKvPO+C0Ej7crP1NG/gFMmowxcKy6kw== + dependencies: + estree-util-is-identifier-name "^1.0.0" + estree-walker "^2.0.0" + estree-util-is-identifier-name@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.0.0.tgz#3f9b8ae3e9d661858752ce73450f37dca160f029" @@ -16615,7 +16605,7 @@ magic-string@^0.22.4: dependencies: vlq "^0.2.2" -magic-string@^0.25.1, magic-string@^0.25.2, magic-string@^0.25.3: +magic-string@^0.25.1, magic-string@^0.25.2: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== @@ -21821,7 +21811,7 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.2.0, regexpu-core@^4.5.4, regexpu-core@^4.7.1: +regexpu-core@^4.2.0, regexpu-core@^4.7.1: version "4.7.1" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==