Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove JSX from output by default #1413

Closed
wants to merge 2 commits into from
Closed

Remove JSX from output by default #1413

wants to merge 2 commits into from

Conversation

wooorm
Copy link
Member

@wooorm wooorm commented Dec 23, 2020

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.

@wooorm wooorm added 🦋 type/enhancement This is great to have 🙆 yes/confirmed This is confirmed and ready to be worked on 🏡 area/internal This affects the hidden internals 🏁 area/perf This affects performance 🧒 semver/minor This is backwards-compatible change 👶 area/size This affects (bundle) size labels Dec 23, 2020
@wooorm wooorm requested a review from johno December 23, 2020 16:10
@vercel
Copy link

vercel bot commented Dec 23, 2020

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/mdx/mdx/klodxnuc6
✅ Preview: Failed

[Deployment for 31c6ed3 failed]

@@ -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 */`
Copy link
Member Author

Choose a reason for hiding this comment

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

I also thought to put the comments in the estree instead

},
{loader: path.resolve(__dirname, '..'), options}
]
use: [{loader: path.resolve(__dirname, '..'), options}]
Copy link
Member Author

Choose a reason for hiding this comment

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

a lot of the tests can be much simpler too

Copy link
Member

@ChristianMurphy ChristianMurphy left a comment

Choose a reason for hiding this comment

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

generally looks good.


One caveat I will note, in the new setup mdx depends on estree-util-build-jsx, which in turn depends on astring (in the docs).
astring hasn't been actively maintained in the past year, and it only supports a subset of ESTree supported by acorn.
Meaning an example like:

<>{a?.b}</>

will parse as expected in acorn (without babel), but will crash an generating a js file (due to optional chaining not being supported in astring).

@wooorm
Copy link
Member Author

wooorm commented Dec 24, 2020

@ChristianMurphy build-jsx does not depend on astring: https://github.com/wooorm/estree-util-build-jsx/blob/6f78e5e1af6fa165c74c6ce6bd043c8297688a95/index.js#L5-L6. In fact, it uses a resilient way of walking the tree that can handle spec changes, whereas typically Babel, acorn, espree, or estraverse walk functions fail on those.

astring did land in the previous PR: #1399, where I described some considerations about it, essentially: I see your point. The nice thing about astring is that it’s easy to extend it (e.g., how I added JSX in core). Not sure if escodegen also supports that (as it doesn’t support JSX).

@ChristianMurphy
Copy link
Member

Apologies I was unclear in my comment, I updated to reflect that the documentation and MDX treat astring as an unlisted peer dependency, you are right it is not a dependency in the npm sense.

we might also consider writing our own serializer in the future

ESTree code gen is going to live under the unified umbrella? 😅
That's a big ol' chunk of code/work/maintenance.
This feels like an opportunity to reach out to other ESTree projects and/or Babel to build some consensus around a runtime amiable code generator. 😅

But I digress.
This PR looks fine.

@ChristopherBiscardi
Copy link
Member

Was this approved as a breaking change for v2? changing the compile target of MDX (and thus the expectation of all consumers, such as next/gatsby plugins as well as manual consumers) seems like it would have been discussed.

@ChristianMurphy
Copy link
Member

@ChristopherBiscardi see #1041 under "Performance Considerations" and #1152

@ChristopherBiscardi
Copy link
Member

@ChristianMurphy I don't see anything in those issues discussing making a breaking change to accomplish this in v2. Perhaps you can link me to a comment directly?

@wooorm
Copy link
Member Author

wooorm commented Dec 25, 2020

  1. The linked issues have v2 labels
  2. If you want to say something, say it
  3. You’re a maintainer here. A good time to discuss the aspects of this change was 6 months ago on the linked issue 🤷‍♂️

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.
@vercel vercel bot temporarily deployed to Preview December 28, 2020 13:56 Inactive
@vercel vercel bot temporarily deployed to Preview December 28, 2020 14:04 Inactive
Copy link
Member

@ChristopherBiscardi ChristopherBiscardi left a comment

Choose a reason for hiding this comment

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

If you want to say something, say it

To be clear, I meant exactly what I was asking in this thread and no more. We shouldn't be making arbitrary breaking changes in V2 without giving users the opportunity for a smooth upgrade.

You’re a maintainer here. A good time to discuss the aspects of this change was 6 months ago on the linked issue 🤷‍♂️

You're implying that this was spec'd out as a breaking change (in that we would be dropping support for the default behavior) to be included in v2, it was not. It being a breaking change is your choice and it is one I disagree with (as a maintainer, like you said).


In general, we should not be making non-critical breaking changes and dropping support for what worked before (like changing the default target format of MDX and removing the previous target) on a project that's getting 1.2MM downloads per week. End-users and integration owners should get time to upgrade incrementally and integration authors should not have to release "*-plugin-mdx-v2" packages or worry about which version of MDX they're running.

@wooorm
Copy link
Member Author

wooorm commented Jan 2, 2021

We shouldn't be making arbitrary breaking changes […]

This was tagged and milestoned as v2 over the summer by @johno when he was still at Gatsby, I’m assuming his reasoning was not arbitrary, nor do I see it as such: performance and making it easier to use MDX are good reasons.

[…] It being a breaking change is your choice and it is one I disagree with

I’m unclear what about this is breaking?

integration authors should not have to release "*-plugin-mdx-v2" packages or worry about which version of MDX they're running.

It sounds like you’re particularly worried about gatsby-plugin-mdx, as it matches that glob pattern: this change was suggested while at Gatsby, in a thread with other things suggested by Gatsby, Gatsby has an internal lessBabel option, so I’m assuming that project is not averse to having less Babel.
Are there other integrations you’re worried about?

@ChristianMurphy
Copy link
Member

We shouldn't be making arbitrary breaking changes in V2 without giving users the opportunity for a smooth upgrade.

JS is a subset of JSX, anything that ran before should still run, there's also a keepJsx option for the uncommon use case where an adopter needs JSX instead of JS.
I'm with @wooorm here, please expand on your concern with a more concrete example.
This comment sounds more like FUD.

In general, we should not be making non-critical breaking changes and dropping support for what worked before (like changing the default target format of MDX and removing the previous target) on a project that's getting 1.2MM downloads per week

There's a balance to this, at this scale Hyrum's law comes into play

With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.

Everything could be a breaking change.
Given that this can toggle between old (JSX) and new (JS) output.
I see this as low risk overall, especially in a major release.

@johno
Copy link
Member

johno commented Jan 2, 2021

I want to jump in here and make it clear that I don't think this should be the default behavior for v2, but I love having this as an option that's configurable.

To me, making this the default is sort of lumping in additional changes before a major drops which is poor strategy IMO. We want to make it compelling for folks to upgrade and this is primarily a hurdle. It's important to keep in mind that the majority of users are entering the ecosystem from frameworks like Next/Gatsby/Docusaurus/Storybook.

Incremental paths to adoption (of v2) is what we should be prioritizing. This fundamentally breaks any downstream package/framework for little benefit. The way I see it is that integrations can migrate to this now JS output over JSX without having to do the work upfront.

This was tagged and milestoned as v2 over the summer by @johno when he was still at Gatsby, I’m assuming his reasoning was not arbitrary, nor do I see it as such: performance and making it easier to use MDX are good reasons.

This is true, but I don't think it's what we should do by default at this point. As an option the Gatsby integration can toggle the option for users in an opaque fashion. Ultimately this is an implementation detail. Also, IIRC MDX output still requires a Babel pass in Gatsby, so the primary gain in perf is already seen by the work for v2 internals because MDX no longer has to use Babel itself.

I think it's important to also mention, MDX compilation has never been the bottleneck in Gatsby's integration.

Are there other integrations you’re worried about?

Storybook, Docusaurus, Docz, MDX Deck, Toast, and any other of the growing list of folks that include MDX by default (IIRC Remix fits that as well).

JS is a subset of JSX, anything that ran before should still run, there's also a keepJsx option for the uncommon use case where an adopter needs JSX instead of JS.

I respectfully disagree, anyone doing any type of post-processing is now hit with a new AST that's way clunkier to deal with. JSX is more abstracted, JS presumes implementation of the function call.

Given that this can toggle between old (JSX) and new (JS) output. I see this as low risk overall, especially in a major release.

Given the ease of the toggle, and it being less of an end user API and moreso for metaframeworks we should let them toggle it on when needed.


I appreciate the discussion here but I think we should be optimizing for fewer API changes and getting v2 shipped. This would involve continuing to keep JSX output as the default.

@wooorm
Copy link
Member Author

wooorm commented Jan 2, 2021

I want to jump in here and make it clear that I don't think this should be the default behavior for v2, but I love having this as an option that's configurable.

I’m OK with that!

We want to make it compelling for folks to upgrade and this is primarily a hurdle.

I don’t think compiling JSX away is a hurdle. I can’t see what it breaks and I’m missing proof that it does: take the tests, 100% coverage before and after without changes. I think it’s a nice feature. Again: an option, defaulting to off is fine to me for now; so is strategically not bundling too much; but I also see value in adding cool features, faster performance, and smaller bundle sizes, for folks that wouldn’t otherwise upgrade for the actual breaking syntax changes.

This fundamentally breaks any downstream package/framework for little benefit.

I think it’s important to note that MDX is dictating exactly how JSX is compiled to JS: there is no wiggle room. There are three projects that I know of that turn JSX into JS: Bublé, which is deprecated; React; and the project I created for this PR. They all work the same, and they‘re all a no-op if there is no JSX.

So: JSX -> JS calls here is not breaking anything. It used to be required to run Babel’s transform after MDX, it’s now done already and that Babel transform is a no-op.

Are there other integrations you’re worried about?

Storybook, Docusaurus, Docz, MDX Deck, Toast, and any other of the growing list of folks that include MDX by default (IIRC Remix fits that as well).

This is a list of projects using MDX, I’m struggling to understand how they depend on JSX instead of JS in the output (more info above). Specifically, I see no code in Gatsby, Docz, or Docusaurus that depends on a certain flavour of output

This is not to say that something might break: it might break someone’s build so we might as well put it in a major. But it shouldn’t, and I don’t see those project being hurt.

anyone doing any type of post-processing is now hit with a new AST that's way clunkier to deal with. JSX is more abstracted, JS presumes implementation of the function call.

This is true! I have not seen that though, so in my opinion those folks could turn that toggle on or off to get what they desire when they upgrade. Or we can add an estreePlugins / rescript options which works the same as remark/rehype plugins but on the estree ast.


Again, I’m fine with delaying this feature as we’re already jampacked, but would still suggest to add it as its been promised and also because I think folks can appreciate a release more if it’s not just breaking changes.

And: this PR becomes much less relevant due to GH-1425

Copy link
Member

@johno johno left a comment

Choose a reason for hiding this comment

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

I think this is good to go when:

  • We make keepJsx the default
  • Update loader and runtime to use the function call option

OliverDudgeon added a commit to InformaticsMatters/squonk2-data-manager-ui that referenced this pull request Jun 8, 2021
The index.tsx page is now index.mdx. This required a change to the babel config as the JSX pragma in use caused issues with the mdx loader but this issue is being worked on. mdx-js/mdx#1413

We seem to now require React to be explicitly imported
@wooorm
Copy link
Member Author

wooorm commented Oct 19, 2021

landed!

@wooorm wooorm closed this Oct 19, 2021
@wooorm wooorm deleted the vanilla-js branch October 20, 2021 07:52
@axe312ger
Copy link

Progress in here? AMAZING! Thank you @wooorm !!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏡 area/internal This affects the hidden internals 🏁 area/perf This affects performance 👶 area/size This affects (bundle) size 🧒 semver/minor This is backwards-compatible change 🦋 type/enhancement This is great to have 🙆 yes/confirmed This is confirmed and ready to be worked on
Development

Successfully merging this pull request may close these issues.

5 participants