wooorm / xdm

Just a *really* good MDX compiler. No runtime. With esbuild, Rollup, and webpack plugins
http://wooorm.com/xdm/
MIT License
593 stars 18 forks source link

Question: are there plans to start releasing recma libraries? #97

Closed ElMassimo closed 2 years ago

ElMassimo commented 2 years ago

Hi, I'd like to know if you have plans to start releasing plugins for the new recma ecosystem.

That would make it possible to customize MDX compilation in the same way that remark and rehype plugins allow to customize Markdown and HTML processing.

For example, I have plans to create a recma plugin that users can add to convert JSX to Vue render functions, skipping the need for a babel transform. Right now the only way to use that while leveraging all the plugins in xdm, is to fork.

wooorm commented 2 years ago

You can already create and pass recma plugins, no change needed here?

As for whether to create a whole gh org and parser/compiler and such, I'm open to it, but was waiting for what the people will do first!

ElMassimo commented 2 years ago

I meant if you would publish the recma plugins that are inside this repo so that they can be used independently in a granular way, just like micromark extensions.

For example, in îles it's beneficial to skip the jsx rewrite to allow providing components, because that enables Vue to detect which components need resolution.

Right now there's no way to skip the default plugins in xdm?

wooorm commented 2 years ago

Some of the plugins are very tightly coupled to what's going on to compile md(x) to js(x), so uhm, instead of publishing them all:

ElMassimo commented 2 years ago

can you pass jsx: true to keep the jsx and then compile it away the way you want to?

Yes, that's the current approach.

what if it's possible to turn this plugin off instead?

It would be enough to avoid the need to fork 😃

It might be necessary to also remove the layout component, since that identifier wouldn't be defined when the components transform is not run?

Would you accept a PR to implement these changes, and what name do you think the setting should have?

wooorm commented 2 years ago

compile it away the way you want to

I meant to let xdm still create jsx elements according to how it works in babel/esbuild/swc, then leaving those with jsx: true, and using a new recma plugin to do things in a way you want to.

I'm not quite sure what you'd want to do differently in your plugin, and so I'm not sure whether it's a good idea in which place?

ElMassimo commented 2 years ago

I am leveraging that capability with jsx: true, and then running https://github.com/vuejs/babel-plugin-transform-vue-jsx on the resulting JSX.

By not adding this component and layout layer (which is custom in xdm), all JSX tooling is still able to process it correctly (output is valid JSX), with the additional benefit of Vue being able to detect which components should be resolved (could be globally registered components).

Seems reasonable to make those extensions optional, especially since the resulting code is leaner, and allows other recma plugins to modify it in different ways (which is not my goal, just saying).

wooorm commented 2 years ago

with the additional benefit of Vue being able to detect which components should be resolved (could be globally registered components).

What prevents the current setup from allowing this auto-detection? I’ve used Vue a little bit, and know that the current setup, with jsx: true and babel-plugin-transform-vue-jsx support Vue nicely. But I don’t yet get how recmaDocument is interfering?

especially since the resulting code is leaner

recmaDocument is providing some important document features of MDX (such as passing components and the layout). I don’t yet see a good reason for making a different version of MDX that has less features. I’m assuming users would expect those features to exist.

wooorm commented 2 years ago

What I don’t get is: you can already turn vuejs/babel-plugin-transform-vue-jsx into a recma plugin, right? Why is that not enough?

ElMassimo commented 2 years ago

The components layer added by recma-jsx-rewrite prevents the vue-jsx babel plugin from detecting unresolved components, as recma-jsx-rewrite defines local variables for all components, which vue-jsx assumes are component definitions (it has no way to know they might be undefined).

Without recma-jsx-rewrite, vue-jsx can correctly detect and transform unresolved components to resolveComponent calls.

This is why being able to skip the recma-jsx-rewrite plugin would be great for certain use cases (as well as skipping the layout in the recma-document plugin whose output is not valid when skipping recma-jsx-rewrite).

I understand that this means removing "features" from xdm. This is why I started with the question of whether you would publish some of these plugins (especially remarkWrapAndUnravel and buildJsx) so that custom pipelines can be created without affecting the vision of xdm.

wooorm commented 2 years ago

The components layer added by recma-jsx-rewrite prevents the vue-jsx babel plugin from detecting unresolved components, as recma-jsx-rewrite defines local variables for all components, which vue-jsx assumes are component definitions (it has no way to know they might be undefined).

Can you expand on how this mechanism in Vue works?

This is why being able to skip the recma-jsx-rewrite plugin would be great for certain use cases (as well as skipping the layout in the recma-document plugin whose output is not valid when skipping recma-jsx-rewrite).

If a plugin X does thing Y, and you don’t want Y, that does not mean that plugin X should be removed. Perhaps there is a way thing Y can be changed to support both use cases. Especially as you later say that plugin Z doesn’t work without X. If we continue with that strain of thought we might just end up with nothing 😅 How could both mechanisms be supported? What is breaking the Vue mechanism specifically? What output would work for you?

This is why I started with the question of whether you would publish some of these plugins

You can use these plugins without forking. You can import somePlugin from 'xdm/lib/plugin/*.js' fine?


Is recma-vue-jsx-build in your fork a direct replacement of https://github.com/syntax-tree/estree-util-build-jsx, and a fork of vuejs/babel-plugin-transform-vue-jsx? If so, that would also be useful as a separate recma plugin, and to suggest for Vue users here in xdm and in mdx!?

ElMassimo commented 2 years ago

Can you expand on how this mechanism in Vue works?

Tags that have a name matching a local identifier, such as those in variable or import declarations, are compiled as createVNode(identifier, props, slots).

Known HTML tags are compiled as a simple call createVNode("a", props, slots).

The rest will be compiled as createVNode(resolveComponent("tagName"), props, slots), which allows resolving globally registered components.

Currently, once the recmaJsxRewrite plugin runs, it will define local identifiers for all used tags, which prevents using the algorithm of checking for local declarations (as used in the vue-jsx babel plugin, and my recma-build-vue-jsx plugin).

plugin Z doesn’t work without X

That's the coupling that you mentioned earlier as a reason not to release them independently, and I agree.

You can import somePlugin from 'xdm/lib/plugin`

Wasn't sure if the plugins were considered internal, which means breaking changes could happen in patch releases.

that would also be useful as a separate recma plugin

Definitely! It's a fork of estree-util-build-jsx. Finished a working draft yesterday, will release it once it has tests.


Had an idea, what about flagging unresolved tag names by adding a custom flag to the nodes at a stage prior to the jsx-rewrite? (just like it's currently flagging "explicit jsx")

That would allow any Compiler plugin later in the pipeline (such as build-vue-jsx) to detect these and render resolveComponent calls, or the explicit checks you added recently.

wooorm commented 2 years ago

Is there a version of resolveComponent(tagName) that resolves all know components? Because this 3rd mechanism is very close to what we already have with context based providers: https://github.com/wooorm/xdm#optionsproviderimportsource (open details for diff).

What if @mdx-js/vue used resolveComponent? https://github.com/mdx-js/mdx/blob/7ff979c8dc2d6f75a6190c84eaffc802e294b0d2/packages/vue/lib/index.js.

One feature of MDX that folks like a lot is being able to pass p: MyParagraph, the Vue algorithm you describe does not have that. It might be nice if we find a way that does allow this feature to remain.


There was some discussion somewhere about making the behavior of which component names can be passed configurable.

Both have some downsides though. Perhaps an option to change this behavior could also support the behavior you want.


Wasn't sure if the plugins were considered internal, which means breaking changes could happen in patch releases.

That can indeed occur, but you can step around that by using ~ / pinning it. Less maintenance that a whole fork IMO

ElMassimo commented 2 years ago

Is there a version of resolveComponent(tagName) that resolves all know components?

Not in the public API.

One feature of MDX that folks like a lot is being able to pass p: MyParagraph

If the component destructuring property nodes are flagged explicitly (or the names of unresolved components are shared across plugins), then a recma plugin could rewrite:

const {
  h1 = 'h1',
  NoteIcon,
} = props.component

to

const {
  h1 = 'h1',
  NoteIcon = resolveComponent("NoteIcon"),
} = props.component

which combines the best of both worlds.

Not as clean as the provider option, and requires flagging those nodes (should be possible given that the identifiers are being tracked to add _missingReference calls), but it would enable Vue users to get the best out of xdm.

wooorm commented 2 years ago

Not in the public API.

Where is that code even? 🤔 Do you happen to know?

NoteIcon = resolveComponent("NoteIcon"),

How would that work with members? <MyObject.MyComponent />. And because functions in JS are also objects, people might also use <MyObject /> on its own That’s why _missingMdxReference is after it.


_missingMdxReference is a good idea! Currently we have:

function _createMdxContent() {
  const {C} = props.components || ({});
  if (!C) _missingMdxReference("C", false);
  return <C />;
}

function _missingMdxReference(id, isComponent) {
  throw new Error("Expected " + (isComponent ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it.");
}

What if we, by default, turned it into:

 function _createMdxContent() {
   const {C} = props.components || ({});
-  if (!C) _missingMdxReference("C", false);
+  if (!C) C = _missingMdxReference("C", false);
   return <C />;
 }

And then, somehow, add useMDXComponent support (the providers currently have useMDXComponents, note the plural). When this is supported the output somehow:

import {useMDXComponent as _useMDXComponent} from 'some-place'

// …

function _missingMdxReference(id, component) {
  const component = useMDXComponent(id)
  if (!component) throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it.");
  return component
}

These questions remain open:

ElMassimo commented 2 years ago

Where is that code even?

Here inside resolveComponent. Technically @mdx/vue could "provide" getCurrentInstance().app context.components, though it wouldn't cover name-casing differences.

What if we, by default, turned it into:

That would be great!

Adding it to every provider and backporting it is some work

What about toggling the addition of useMDXComponent with a setting, similar to the pragma and provider ones?

How to handle objects?

Vue doesn't allow object namespaces, so useMDXComponent would only be added if the component is true. Failing for missing namespaces or objects seems reasonable.


Thanks for your patience going through this use case! Sorry for not providing more details about component resolution upfront 😅

If it sounds good, I'll do a spike by creating a recma plugin to:

  1. Replace const with let in the components definition
  2. Assign the result from missingReference
  3. Inject a resolveComponent call when the component is true
  4. Test the result with recma-build-vue-jsx

If it works as nicely as it seems in theory, I'll be happy to contribute 1 and 2 back to XDM, and we can discuss whether to add a setting to enable 3 as well (with an agnostic name such as useMDXComponent).

4 doesn't need changes to XDM, since adding the plugin to the pipeline should replace any previously assigned Compiler.

wooorm commented 2 years ago

What about toggling the addition of useMDXComponent with a setting, similar to the pragma and provider ones?

Yeah, but what would the option be called? Is it something where current providers with useMDXComponents are “old” / “legacy”, and useMDXComponent is the modern version? Or are we getting different providers: options.providerIsVue / options.providerIsSingular (?) switches to useMDXComponent but the default is useMDXComponents? Downside of a toggle is, it would work for fine for you as you know so much about xdm/mdx, but there are also many users that don’t read readmes and will be stumped by why things don’t work.

Vue doesn't allow object namespaces, so useMDXComponent would only be added if the component is true. Failing for missing namespaces or objects seems reasonable.

Oh so you mean that both id (the component name) and component (whether it’s a component or an object) are passed? Smart. But, that’s just for Vue. Everyone else does support them and has this unsolved problem.

Thanks for your patience going through this use case! Sorry for not providing more details about component resolution upfront 😅

<3

If it sounds good, I'll do a spike by creating a recma plugin to:

Sounds perfect! I’ll close this, as the original question is somewhat answered, and we figured out a good way to add something else if needed in the future. We can keep on discussing here, or when you’re ready, we can discuss on a (WIP) PR?

ElMassimo commented 2 years ago

Here's a prototype of the idea above.

Since the recma-build-jsx can not be removed from the chain, I decided to drop recma-vue-build-jsx and provide a custom jsx runtime that uses createVNode.

The missingReference function is replaced with a strict version of resolveComponent.

It seems to be working nicely:

I'll be making a PR to replace const with let in component definitions, which should have no practical downsides.

Would also love it if we could flag the estree nodes relative to the assertion of missing components so that my implementation can be more resilient to changes (though it should be easy to update as xdm evolves).


Thanks again for your help thinking through this! The recent addition of missingReference enabled this solution which is cleaner and should be easier to maintain :smiley: