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

Fixes #990 #1014

Closed
wants to merge 1 commit into from
Closed

Fixes #990 #1014

wants to merge 1 commit into from

Conversation

adammockor
Copy link

Replace /* @jsx mdx / with / @jsxFrag React.Fragment */.

This fixes problem for me (custom cra + mdx). Could it break something?

Replace /* @jsx mdx */ with /* @jsxFrag React.Fragment */
@vercel
Copy link

vercel bot commented Apr 13, 2020

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

🔍 Inspect: https://zeit.co/mdx/mdx/206fbqv5r
✅ Preview: https://mdx-git-fork-adammockor-patch-1.mdx.now.sh

@vercel vercel bot temporarily deployed to Preview April 13, 2020 16:13 Inactive
@wooorm
Copy link
Member

wooorm commented Nov 11, 2020

thanks for your patience, @adammockor!

I think this will break things, yes, because jsx and jsxFrag are two separate inline babel options in the classic runtime: https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#example

@wooorm
Copy link
Member

wooorm commented Nov 13, 2020

Would you be interested in continuing work on this? E.g., adding some tests to make sure it works, and also for Vue? For my earlier comment, adding both /** @jsx mdx */ and / @jsxFrag mdx.Fragment */, and actually addingmdx.Fragment, might be the way to go?

@eric-burel
Copy link

eric-burel commented Nov 20, 2020

I am not sure I get "automatic" and "classic" runtime stuffs yet, but it seems that the "automatic" runtime expects you not to set pragma and pragmaFrag at all, as it triggers SyntaxError: /code/vulcan-next-starter/README.md: pragma and pragmaFrag cannot be set when runtime is automatic..

@karlhorky
Copy link
Contributor

karlhorky commented Nov 20, 2020

Edit: Looking at the comment from Andarist below, it looks like the mdx packages will need to be changed, in order to support the new runtime.


Yeah, if both plugins / presets support the automatic runtime, then you do not need to use the pragma comments:

@karlhorky
Copy link
Contributor

karlhorky commented Nov 20, 2020

Maybe if you need to continue using the comments on a file-by-file basis (instead of configuring it for the whole project like in my example), the @jsxImportSource comment would help (this seems to be the equivalent of preset-react.importSource):

https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#customizing-the-automatic-runtime-import

@@ -124,7 +124,7 @@ async function compile(mdx, options = {}) {

const {contents} = await compiler.process(fileOpts)

return `/* @jsx mdx */
return `/* @jsxFrag React.Fragment */
Copy link
Contributor

@karlhorky karlhorky Nov 20, 2020

Choose a reason for hiding this comment

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

Edit: This is wrong, looking at the comment from Andarist below. It looks like the mdx packages will need to be changed.

Maybe something like this?

Suggested change
return `/* @jsxFrag React.Fragment */
return `/** @jsxImportSource mdx */

Copy link

@sgmheyhey sgmheyhey left a comment

Choose a reason for hiding this comment

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

113   return `/* @jsxRuntime classic */
114 /* @jsxFrag React.Fragment */
115 /* @jsx mdx */
116 ${contents}`
117 }
129   return `/* @jsxRuntime classic */
130 /* @jsxImportSource mdx */
131 /* @jsx mdx */
132 ${contents}`
133 }

it's also not working in ubuntu 20.4

@karlhorky
Copy link
Contributor

@sgmheyhey these three lines are incompatible:

/* @jsxRuntime classic */
/* @jsxImportSource mdx */
/* @jsx mdx */

As far as I understand, you want EITHER:

  1. If you're using the automatic runtime:
/* @jsxImportSource mdx */
  1. If you're using the classic runtime:
/* @jsxRuntime classic */
/* @jsx mdx */

...but then again, I am not an expert here. @Andarist has been super helpful in the past elsewhere (with the integration of Emotion and Next.js).

@Andarist
Copy link

these three lines are incompatible

Yes, they are incompatible.

@jsxImportSource mdx

This means that there is an mdx package that:

  • has jsx & jsxs exports that can be imported from mdx/jsx-runtime
  • has jsxDEV export that can be imported from mdx/jsx-dev-runtime
  • has createElement export that can be imported from mdx

From what I understand none of this is true - especially given that mdx is not even a package name.

@jsxRuntime classic + @jsx mdx

This is most likely the most correct answer here for now. We'll probably be lifting the @jsxRuntime requirement in Babel in the following weeks but it might take some time before this will finally land in tools like CRA etc. I hope that in the future @jsx mdx will be enough to reconfigure the transform to use its "classic" variant.

@sgmheyhey

This comment has been minimized.

@karlhorky
Copy link
Contributor

@Andarist Ah ok thanks!

Alright, so it sounds like according to your comment above, the classic runtime is the stopgap method, until MDX supports the React 17 automatic runtime.

From what I understand none of this is true - especially given that mdx is not even a package name

Then what needs to happen for compatibility with the new automatic runtime is that the MDX package be reworked to provide these exports, as in your comment above:

  • jsx
  • jsxs
  • jsxDEV
  • createElement

I'm guessing these changes are probably also similar to the changes in emotion-js/emotion#1970

cc @johno @wooorm

I've edited my comments above.

@wooorm
Copy link
Member

wooorm commented Nov 24, 2020

until MDX supports the React 17 automatic runtime.

I’m working on trying to see if ditching JSX completely is feasible, which would compile MDX to React.createElement (or relevant preact/vue/etc) calls instead.

@wooorm
Copy link
Member

wooorm commented Dec 18, 2020

OK, so to solve this, I think we should add, to these two lines

const pragma = `/* @jsxRuntime classic */
/* @jsx mdx */`
.

This code: /* @jsxFrag mdx.Fragment */.

Then, in packages/react, mdx.Fragment should be exposed and set to React.Fragment

For packages/vue, well, vue 2 doesn‘t have fragments, so that’s complex. Maybe exposing mdx.Fragment there as "div" might work? But that’ll break inline stuff... Or set it to null, and then in Vue’s createElement, handle the first argument (type) set to null by returning the array of children (if that’s supported in Vue) 🤷‍♂️

@Andarist
Copy link

Note - I will hopefully file a PR to Babel this week to somewhat make @jsxRuntime pragma obsolete.

@wooorm
Copy link
Member

wooorm commented Dec 19, 2020

@Andarist Oh, that sounds interesting! I’d love to read that PR if you’ve posted it.

@Andarist
Copy link

Sure, we'll try to remember to cc you there - just need to find time for a rebase and adding few tests.

wooorm added a commit that referenced this pull request Dec 19, 2020
This adds a `/* @jsxFrag mdx.Fragment */` next to the existing
`/* @jsx mdx */` pragma.
From MDX runtimes, this exports as `mdx.Fragment` either `React.Fragment`
or `Preact.Fragment`.

Vue 2 does not support fragments, but as JSX and hence MDX is already
specific to React or Vue, well: folks shouldn’t use fragments in MDX
files targeting Vue.

As we have fragments, we can also use that to pass children through
missing components: `<>{props.children}</>`.
This fixes runtimes where HTML is not available, such as React Native.
But, as Vue doesn’t like that, there’s a hidden flag to still use
the original behavior: `<div {...props} />`.
Still, there remains a difference in frameworks: Vue does not put
`children` in `props`, so `{...props}` has never passed children along
in Vue.

Closes GH-972.
Closes GH-990.
Closes GH-1014.
@karlhorky
Copy link
Contributor

OK, so to solve this, I think we should add, to these two lines ... @jsxRuntime classic

Note - I will hopefully file a PR to Babel this week to somewhat make @jsxRuntime pragma obsolete

I suppose any usage of the classic runtime is only a temporary workaround? Since there's downsides to it and the new JSX transform looks like the way to go, going forward?

@wooorm
Copy link
Member

wooorm commented Dec 19, 2020

This upgrade will not change the JSX syntax and is not required. The old JSX transform will keep working as usual, and there are no plans to remove the support for it.
https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html

wooorm added a commit that referenced this pull request Dec 20, 2020
This adds a `/* @jsxFrag mdx.Fragment */` next to the existing
`/* @jsx mdx */` pragma.
From MDX runtimes, this exports as `mdx.Fragment` either `React.Fragment`
or `Preact.Fragment`.

Vue 2 does not support fragments, but as JSX and hence MDX is already
specific to React or Vue, well: folks shouldn’t use fragments in MDX
files targeting Vue.

As we have fragments, we can also use that to pass children through
missing components: `<>{props.children}</>`.
This fixes runtimes where HTML is not available, such as React Native.
But, as Vue doesn’t like that, there’s a hidden flag to still use
the original behavior: `<div {...props} />`.
Still, there remains a difference in frameworks: Vue does not put
`children` in `props`, so `{...props}` has never passed children along
in Vue.

Closes GH-972.
Closes GH-990.
Closes GH-1014.
wooorm added a commit that referenced this pull request Dec 20, 2020
This adds a `/* @jsxFrag mdx.Fragment */` next to the existing
`/* @jsx mdx */` pragma.
From MDX runtimes, this exports as `mdx.Fragment` either `React.Fragment`
or `Preact.Fragment`.

Vue 2 does not support fragments, but as JSX and hence MDX is already
specific to React or Vue, well: folks shouldn’t use fragments in MDX
files targeting Vue.

As we have fragments, we can also use that to pass children through
missing components: `<>{props.children}</>`.
This fixes runtimes where HTML is not available, such as React Native.
But, as Vue doesn’t like that, there’s a hidden flag to still use
the original behavior: `<div {...props} />`.
Still, there remains a difference in frameworks: Vue does not put
`children` in `props`, so `{...props}` has never passed children along
in Vue.

Closes GH-972.
Closes GH-990.
Closes GH-1014.
@wooorm wooorm closed this in #1394 Dec 20, 2020
wooorm added a commit that referenced this pull request Dec 20, 2020
This adds a `/* @jsxFrag mdx.Fragment */` next to the existing
`/* @jsx mdx */` pragma.
From MDX runtimes, this exports as `mdx.Fragment` either `React.Fragment`
or `Preact.Fragment`.

Vue 2 does not support fragments, but as JSX and hence MDX is already
specific to React or Vue, well: folks shouldn’t use fragments in MDX
files targeting Vue.

As we have fragments, we can also use that to pass children through
missing components: `<>{props.children}</>`.
This fixes runtimes where HTML is not available, such as React Native.
But, as Vue doesn’t like that, there’s a hidden flag to still use
the original behavior: `<div {...props} />`.
Still, there remains a difference in frameworks: Vue does not put
`children` in `props`, so `{...props}` has never passed children along
in Vue.

Closes GH-972.
Closes GH-990.
Closes GH-1014.
Closes GH-1394.

Reviewed-by: Christian Murphy <[email protected]>
Reviewed-by: John Otander <[email protected]>
@okonet
Copy link

okonet commented Dec 28, 2020

Not sure if it's related to this one but I'm trying to use Theme UI with MDX and I'm getting:

importSource cannot be set when runtime is classic.

with the following next.config.js config:

{
  "presets": [
    [
      "next/babel",
      {
        "preset-react": {
          "throwIfNamespace": false,
          "runtime": "automatic",
          "importSource": "theme-ui"
        }
      }
    ]
  ],
  "plugins": ["inline-react-svg", "@emotion/babel-plugin"]
}

@wooorm
Copy link
Member

wooorm commented Dec 28, 2020

I don’t think the automatic runtime is supported (or even can be, MDX overwrites React.createElement).
Can you use classic as a runtime, and use theme-ui some other way?

@Andarist
Copy link

The problem is very similar to not being able to use 2 classic pragmas at once. I believe that MDX could use the automatic runtime just fine (it's just a different way of declaring the overwrite) but it still would not play OK with other custom automatic runtimes - as there is no built-in way to compose them.

@karlhorky
Copy link
Contributor

karlhorky commented Dec 28, 2020

Oh, so you mean that there is currently no way to specify multiple importSources like the following non-working code?

// NOT WORKING
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic",
        // DO NOT USE, DOES NOT WORK
        "importSource": ["theme-ui", "mdx"]
      }
    ]
  ]
}

Interesting drawback - is there anything being worked on to add this capability to compose runtimes? Or to do something like this, would a new wrapper library need to be created (eg. mdx-with-theme-ui)?

@wooorm
Copy link
Member

wooorm commented Dec 28, 2020

@Andarist “The problem is very similar to not being able to use 2 classic pragmas at once.”

The same can be said for two automatic runtimes too though, right? Or generally: two runtimes?

@karlhorky “is there anything being worked on to add this capability to compose runtimes?”

How would that work if both react and preact are used?

React/preact/vue/other hyperscripts are actual renderers (“h interfaces”), whereas emotion (through theme-ui) and @mdx-js/react and friends hijack that to change some props and do other things, so your proposed feature might work with that, but then I’d propose jsxTransformSources or so.

Emotion really needs to add props to all elements, but I’m personally of the opinion that MDX does not need a runtime/renderer/h part: it’s hijacking the renderer so that users can pass components in through providers, whereas I think that those could just as well be passed as a prop. This would then solve @okonet’s question. An alternative is indeed a wrapper like mdx-with-theme-ui, although that seems already provided by theme-ui?

@okonet
Copy link

okonet commented Dec 28, 2020

Hmm that's an interesting drawback of this design... I'm wondering how it's going affect tooling.

I agree it feels like Theme UI shouldn't need pragma for their sx prop since it's using their own components anyways.

I'm going to take a look at the package you mentioned but the problem there are so many things that start to depend on each other and debugging it is a nightmare.

@karlhorky
Copy link
Contributor

karlhorky commented Dec 28, 2020

How would that work if both react and preact are used? React/preact/vue/other hyperscripts are actual renderers

@wooorm Yeah, good question. I'm not as deep into this architecture as I should be to seriously propose anything, but FWIW I guess my idea was based on functional composition, where you can put two functions that have a similar input and output API and string them together (eg. compose(fn1, fn2)). Regarding your question about react and preact, there is no value to have both of them running, so I guess this would not be a use case for this particular API.

So I suppose I was probably thinking about something similar to what you were mentioning with jsxTransformSources - the ability to pass in composable transformer functions to either:

  1. add or modify a tree of existing JSX elements (after the runtime has run)
  2. (maybe) "plug in to" and replace a part of an existing runtime

@Andarist
Copy link

Or to do something like this, would a new wrapper library need to be created (eg. mdx-with-theme-ui)?

Probably this. And it seems that such a package already exists - as @wooorm has found out.

The same can be said for two automatic runtimes too though, right? Or generally: two runtimes?

Yes, that's exactly what I have meant - sorry if the intention was not clear.

wooorm added a commit that referenced this pull request Jan 2, 2021
This PR moves most of the runtime to the compile time.

This issue has nothing to do with `@mdx-js/runtime`. It’s about
`@mdx-js/mdx` being compile time, and moving most work there, from the
“runtimes” `@mdx-js/react`, `@mdx-js/preact`, `@mdx-js/vue`.

Most of the runtime is undocumented features that allow amazing things,
but those are in my opinion *too magical*, more powerful than needed,
complex to reason about, and again: undocumented.
These features are added by overwriting an actual renderer (such as
react, preact, or vue). Doing so makes it hard to combine MDX with for
example Emotion or theme-ui, to opt into a new JSX transform when React
introduces one, to support other hyperscripts, or to add features such
as members (`<Foo.Bar />`). Removing these runtime features does what
MDX says in the readme: “**🔥 Blazingly blazing fast: MDX has no
runtime […]**”

This does remove the ability to overwrite *anything* at runtime. This
brings back the project to what is documented: users can still
overwrite markdown things (e.g., blockquotes) to become components and
pass components in at runtime without importing them. And it does still
allow undocumented parent-child combos (`blockquote.p`).

* Remove runtime renderers (`createElement`s hijacking) from
  `@mdx-js/react`, `@mdx-js/preact`, `@mdx-js/vue`
* Add `jsxRuntime` option to switch to the modern automatic JSX runtime
* Add `jsxImportSource` option to switch to a modern non-React JSX
  runtime
* Add `pragma` option to define a classic JSX pragma
* Add `pragmaFrag` option to define a classic JSX fragment
* Add `mdxProviderImportSource` option to load an optional runtime
  provider
* Add tests for automatic React JSX runtime
* Add tests for `@mdx-js/mdx` combined with `emotion`
* Add support and test members as “tag names” of elements
* Add support and test qualified names (namespaces) as “tag names” of
  elements
* Add tests for parent-child combos
* Add tests to assert explicit (inline) components precede over
  provided/given components
* Add tests for `mdxFragment: false` (runtime renderers w/o fragment
  support)
* Fix and test double quotes in attribute values

This PR removes the runtime renderers and related things such as the
`mdxType` and `parentName` props while keeping the `MDXProvider` in
tact.

This improves runtime performance, because all that runs at runtime is
plain vanilla React/preact/vue code.

This reduces the surface of the MDX API while being identical to what
is documented and hence to user expectations (except perhaps to some
power users).

This also makes it easier to support other renderers without having to
maintain projects like `@mdx-js/react`, `@mdx-js/preact`, `@mdx-js/vue`:
anything that can be used as a JSX pragma (including the [automatic
runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html))
is now supported.
A related benefit is that it’s easier to integrate with
[emotion](https://github.com/emotion-js/emotion/blob/master/packages/react/src/jsx.js#L7)
(including through `theme-ui`) and similar projects which also
overwrite the renderer: as it’s not possible to have two runtimes, they
were hard to combine; because with this PR MDX is no longer a renderer,
there’s no conflict anymore.

This is done by the compile time (`@mdx-js/mdx`) knowing about an
(**optional**) runtime for an `MDXProvider` (such as `@mdx-js/react`,
`@mdx-js/preact`). Importantly, it’s not required for other
hyperscript interfaces to have a provider: `MDXContent` exported from
a compiled MDX file *also* accepts components (it already did), and Vue
comes with component passing out of the box.

In short, the runtime looked like this:

```js
function mdx(thing, props, ...children) {
  const overwrites = getOverwritesSomeWay()
  return React.createElement(overwrites[props.mdxType] || thing, props, ...children)
}
```

And we had a compile time, which added that `mdxType` prop. So:

```mdx
<Youtube />
```

Became:

```js
const Youtube = () => throw new Error('Youtube is not loaded!')

<Youtube mdxType="Youtube" />
```

Which in plain JS looks like:

```js
const Youtube = () => throw new Error('Youtube is not loaded!')

React.createElement(Youtube, {mdxType: 'Youtube'})
```

Instead, this now compiles to:

```js
const {Youtube} = Object.assign({Youtube: () => throw new Error('Youtube is not loaded!')}, getOverwritesSomeWay())

React.createElement(Youtube)
```

The previous example shows what is sometimes called a “shortcode”: a
way to inject components as identifiers into the MDX file, which was
introduced in [MDX 1](https://mdxjs.com/blog/shortcodes)

A different use case for the runtime was overwriting “defaults”. This
is documented on the website as the “[Table of
components](https://mdxjs.com/table-of-components)”. This MDX:

```mdx
Hello, *world*!
```

Became:

```js
<p mdxType="p">Hello, <em mdxType="em">world</em>!</p>
```

This now compiles to:

```js
const overwrites = Object.assign({p: 'p', em: 'em'}, getOverwritesSomeWay())

<overwrites.p>Hello, <overwrites.em>world</overwrites.em>!</overwrites.p>
```

This MDX:

```mdx
export const Video = () => <Vimeo />

<Video />
```

Used like so:

```jsx
<MDXProvider components={{Video: () => <Youtube />}}>
  <Content />
</MDXProvider>
```

Would result in a `Youtube` component being rendered. It no longer
does. I see the previous behavior as a bug and hence this as a fix.

A subset of the above point is that:

```mdx
export default props => <main {...props} />

x
```

Used like so:

```jsx
<MDXProvider components={{wrapper: props => <article {...props} />}}>
  <Content />
</MDXProvider>
```

Would result in an `article` instead of the explicit `main`. It no
longer does. I see the previous behavior as a bug and hence this as a
fix.

(#821)

```mdx

<h2>World</h2>
```

Used like so:

```jsx
<MDXProvider components={{h2: () => <SomethingElse />}}>
  <Content />
</MDXProvider>
```

Would result in a `SomethingElse` for both. This PR **does not** change
that. But it could more easily be changed if we want to, because at
compile time we know whether something was a tag or not.

An undocumented feature of the current MDX runtime renderer is that
it’s possible to overwrite anything:

```mdx
<span />
```

Used like so:

```jsx
<MDXProvider components={{span: props => <b>{props.children}</b>}}>
  <Content />
</MDXProvider>
```

Would overwrite to become bold, even though it’s not documented
anywhere. This PR changes that: only allowed markdown “tag names” can
be changed (`p`, `li`, ...). **This list could be expanded.**

Another undocumented feature is that parent–child combos can be
overwritten. A `li` in an `ol` can be treated differently from one in
an `ul` by passing `'ol.li': () => <SomethingElse />`.

This PR no longer lets users “nest” arbitrary parent–child combos
except for `ol.li`, `ul.li`, and `blockquote.p`. **This list could
be expanded.**

It was not possible to use members (`<foo.bar />`, `<Foo.bar.baz />`,
<#953>) and supporting it previously
would be complex. This PR adds support for them.

Previously, `mdxType` and `parentName` attributes were added to all
elements. And a `components` prop was accepted on **all** elements to
change the provider. These are no longer passed and no longer accepted.
Lastly, `components`, `props` were in scope for all JSX tags defined in
the “markdown” section (not the import/exports) of each document.

This adds identifiers to the scope prefixed with double underscores:
`__provideComponents`, `__components`, and `__props`.

A single 1mb MDX file, about 20k lines and 135k words (basically 3
books). Heavy on the “markdown”, few tags, no import/exports.
322kb gzipped.

* v1: 2895.122856
* 2.0.0-next.8: 3187.4684129999996
* main: 4058.917152000001
* this pr: 4066.642403

* v1: raw: 1.5mb, gzip: 348kb
* 2.0.0-next.8: raw: 1.4mb, gzip: 347kb
* main: raw: 1.3mb, gzip: 342kb
* this pr: raw: 1.8mb, gzip: 353kb
* this pr, automatic runtime: raw: 1.7mb, gzip: 355kb

* v1: 321.761208
* 2.0.0-next.8: 321.79749599999997
* main: 162.412757
* this pr: 107.28038599999996
* this pr, automatic runtime: 123.73588899999999

This PR is much faster on giant markdown-esque documents during runtime.
The win over the current `main` branch is 34%, the win over the last
beta and v1 is 66%.

For output size, the raw value increases with this PR, which is because
the output is now `/*#__PURE__*/React.createElement(__components.span…)`
or `/*#__PURE__*/_jsx(__components.span…)`, instead of `mdx("span",
{mdxType: "span"…})`. The change is more repetition, as can be seen by
the roughly same gzip sizes.

That the build time of `main` and this PR is slower than v1 and the
last beta does surprise me a lot. I benchmarked earlier with 1000 small
simple MDX files, totalling 1mb, [where the results were the
inverse](#1399 (comment)). So
it looks like we have a problem with giant files. Still, this PR has no
effect on build time performance, because the results are the same as
currently on `main`.

This PR makes MDX faster, adds support for the modern automatic JSX
runtime, and makes it easier to combine with Emotion and similar
projects.

---

Some of what this PR does has been discussed over the years:

Related-to: GH-166.
Related-to: GH-197.
Related-to: GH-466 (very similar).
Related-to: GH-714.
Related-to: GH-938.
Related-to: GH-1327.

This PR solves some of the items outlined in these issues:

Related-to: GH-1152.
Related-to: #1014 (comment).

This PR solves:

Closes GH-591.
Closes GH-638.
Closes GH-785.
Closes GH-953.
Closes GH-1084.
Closes GH-1385.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

7 participants