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

Support for Exotics #22

Closed
4 tasks done
shellscape opened this issue Feb 18, 2024 · 15 comments
Closed
4 tasks done

Support for Exotics #22

shellscape opened this issue Feb 18, 2024 · 15 comments
Labels
🙋 no/question This does not need any changes 👎 phase/no Post cannot or will not be acted on

Comments

@shellscape
Copy link

shellscape commented Feb 18, 2024

Initial checklist

Problem

This is arguably outside of the scope of the reason that hastscript exists, and I fully acknowledge that. However, I do believe it would be a value add.

I'm working on jsx-email and one of my goals to have cross-framework compatibility with the components it exports - effectively untethering from requiring react as a dependency. One of the bananas scenarios there is that users are running the components it exports in several different environments, one of which is Storybook. Since it has a few components that perform async operations, it needs to use <Suspense/>. Similar to Fragment in react, this is a symbol with children, with the added fallback prop.

I put together a little test script to see what would happen should I inject react-specific JSX into the generic JSX supported by hastscript:

/** @jsxImportSource hastscript */

import { h } from 'hastscript';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { Suspense } from 'react';
import * as reactRuntime from 'react/jsx-runtime';

const component = (
  <>
    <Suspense fallback={<div>waiting</div>}>
      <div class="foo" id="some-id"></div>
    </Suspense>
  </>
);

console.log(component);
console.log();
console.log(toJsxRuntime(component, reactRuntime as any));

I was pleasantly surprised to find that it handled the Suspense "component" there quite gracefully by simply discarding it:

{
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'div',
      properties: [Object],
      children: [Array]
    }
  ]
}

But that result (in this potentially erroneous context) discards the Suspense component altogether. Using hast-util-to-jsx-runtime to convert it to React predictably results in the missing Suspense component.

To that end, I'd love to see support for exotic components (which may just be symbols that have props) in hastscript. While probably not the intended use it would open up some new possibilities for this lib's use.

Solution

I'm not quite sure how this would be accomplished, and since I'm in an evaluation phase of possible broader solutions for my use-case, I haven't done a deep dive on the code here to suggest a path forward. I would love to get your initial thoughts on how this might be supported.

Alternatives

I've been unsuccessfully working on a generic renderer that can handle JSX formats of React, Preact, and SolidJS. The variances are significant and I haven't been able to accommodate them all - and that doesn't even go into the type incompatibilities between them. Coupled with the fact that I'd have to race to keep up with any changes in each framework, it seems like a losing path over time. I was excited to find this and hast-util-to-jsx-runtime because it opens a new path where I can write my components in a standard which can then be converted to the appropriate format for each.

@github-actions github-actions bot added 👋 phase/new Post is being triaged automatically 🤞 phase/open Post is being triaged manually and removed 👋 phase/new Post is being triaged automatically labels Feb 18, 2024
@ChristianMurphy
Copy link
Member

ChristianMurphy commented Feb 18, 2024

Welcome @shellscape! 👋

I'm working on jsx-email and one of my goals to have cross-framework compatibility with the components it exports - effectively untethering from requiring react as a dependency.

HTML is not the same as JSX.
JSX is JavaScript superset programming language, with embedded XML constructs.
HTML is markup language with some XML inspiration.
While the syntax may have some initial passing similarity, the fundamental differences in the intent and strictness make them incompatible for most meaningful/full-featured projects.

Have you looked at or considered MDX? https://mdxjs.com
It supports full JSX, including exotic component and expressions.
And can render into React, Vue, Svelte, and other JSX/hyperscript compatible languages.
It also includes esast/mdx specific node types to allow embedding JavaScript/JSX content in markdown/html documents. https://mdxjs.com/playground/ (use the dropdown to see the different intermediate representations).

One of the bananas scenarios there is that users are running the components it exports in several different environments, one of which is Storybook. Since it has a few components that perform async operations, it needs to use . Similar to Fragment in react, this is a symbol with children, with the added fallback prop.

This seems to contradict your initial requirement?
Only react supports asyncronous components, wouldn't this cause framework lock in?

I was excited to find this and hast-util-to-jsx-runtime because it opens a new path where I can write my components in a standard which can then be converted to the appropriate format for each.

If you want to stick to standards while still offering some framework flexibility.
Consider specifying custom elements (https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_a_custom_element) as an alternative to exotics, then in https://github.com/syntax-tree/hast-util-to-jsx-runtime specify a renderer/component for the custom element that translates it into that specific frameworks' JSX constructs.

@shellscape
Copy link
Author

Thanks for the reply

HTML is not the same as JSX

Very much aware of this, and have worked a lot with other projects in the unified universe, fully understand the standards approach.

This seems to contradict your initial requirement? Only react supports asyncronous components, wouldn't this cause framework lock in?

Preact and Solid both support async/Suspense and have docs on that. Those three are my targets.

specify a renderer/component for the custom element that translates it into that specific frameworks' JSX constructs.

I didn't see an entry point or any docs around this on the repo for the runtime, maybe missed it? Any chance you have an example you could point me at?

I'll make my next stop the mdx repo 👍

@ChristianMurphy
Copy link
Member

ChristianMurphy commented Feb 18, 2024

I didn't see an entry point or any docs around this on the repo for the runtime, maybe missed it?

I'm not sure I follow, hast-util-to-jsx-runtime includes no runtime.
One has to be passed in, the example in the readme show's react but each JSX runtime has an equivalent export that can be passed in https://github.com/syntax-tree/hast-util-to-jsx-runtime

Any chance you have an example you could point me at?

Here is an example of an email-fragment custom element being translated by hast-util-to-jsx-runtime into an exotic component.

import { h } from 'hastscript';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
import { renderToStaticMarkup } from 'react-dom/server';

const tree = h(
  'email-fragment',
  null,
  h('h1', null, 'Hello, world!'),
  h('p', null, 'example paragraph')
);
const doc = renderToStaticMarkup(
  toJsxRuntime(tree, {
    Fragment,
    jsx,
    jsxs,
    components: { 'email-fragment': Fragment },
  })
);

console.log(doc);

https://stackblitz.com/edit/stackblitz-starters-8nqziz

@ChristianMurphy
Copy link
Member

I didn't see an entry point or any docs around this on the repo for the runtime, maybe missed it?

Or if you mean MDX, see https://mdxjs.com/docs/getting-started/

@shellscape
Copy link
Author

I'd meant that I didn't see an example of enabling a custom element with the hast-util-to-jsx-runtime capabilities, since that's what it looked like you were eluding to. Explicitly, this portion:

specify a renderer/component for the custom element that translates it into that specific frameworks' JSX constructs.

@ChristianMurphy
Copy link
Member

ChristianMurphy commented Feb 18, 2024

I gotcha now, see the example I posted above.
It shows how an email-fragment custom element could be added, using the components option.

@shellscape
Copy link
Author

Thanks, I'll look into that and follow up here. Please feel free to close this for housekeeping if you'd like, but I'll definitely post my findings (and any solutions) for posterity.

@shellscape
Copy link
Author

OK this worked really well, here's my fork of the example: https://stackblitz.com/edit/stackblitz-starters-bcn2ye

This does have a type error with the use of uppercased component names, but that's expected due to the spec stating that element names must be lowercase. I think this can be worked around.

The components option wasn't immediately obvious that it was the path forward. I'd suggest that the description of that is a bit terse, and with this being the key to unblocking my immediate experimentation I think there's a lot of value there that could be better explained and even accompanied by an example or recipe in the docs.

Thanks again for sharing that info

This comment has been minimized.

@JounQin JounQin added 💪 phase/solved Post is done 🙋 no/question This does not need any changes labels Feb 19, 2024
@github-actions github-actions bot removed the 🤞 phase/open Post is being triaged manually label Feb 19, 2024
Copy link

Hi! Thanks for reaching out! Because we treat issues as our backlog, we close issues that are questions since they don’t represent a task to be completed.

See our support docs for how and where to ask questions.

Thanks,
— bb

@github-actions github-actions bot added 👎 phase/no Post cannot or will not be acted on and removed 💪 phase/solved Post is done labels Feb 19, 2024
@wooorm
Copy link
Member

wooorm commented Feb 19, 2024

In to-jsx-runtime? Or where?

@shellscape
Copy link
Author

@wooorm if that's a response to my comment about the docs, yes in to-jsx-runtime.

@ChristianMurphy
Copy link
Member

The components option wasn't immediately obvious that it was the path forward. I'd suggest that the description of that is a bit terse

I appreciate the insight, a PR with a more articulate description would be welcome!

and with this being the key to unblocking my immediate experimentation I think there's a lot of value there that could be better explained and even accompanied by an example or recipe in the docs.

Good idea!
The components pattern is leveraged across many JSX adjacent unified projects.

It could be good to have the recipe added on the unified learn/recipe page https://unifiedjs.com/learn/ and linked from each of the respective projects. (source for website: https://github.com/unifiedjs/unifiedjs.github.io/tree/main/doc/learn)

@wooorm
Copy link
Member

wooorm commented Feb 20, 2024

The examples here are currently per framework. A more recipe/guide style thing at unifiedjs.com could also definitely work. But I am not opposed to here either.

Though, we often get the “would’ve been nice to have an example”. One problem with that is that there are a bazillion ways to combine everything. And that there’s already a ton of info in the readmes. A little note or an example can also be overlooked.

So, if you think back to reading through the readmes just before posting this, and the root question you had back then, what do you think would’ve helped you find that components was the solution? Would it be an example or something else? If an example, also with the knowledge of what you know now, how would that example look?

@shellscape
Copy link
Author

shellscape commented Feb 20, 2024

I think you might be overthinking this one. You've got a single, nondescript line here "components to use":

Screenshot_20240220_073306_Brave

Quite literally anything in addition to that is value add. Anyone without intimate knowledge of how the package works is probably not going to be able to derive the usage as explained above in this issue, from that one line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙋 no/question This does not need any changes 👎 phase/no Post cannot or will not be acted on
Development

No branches or pull requests

4 participants