BuilderIO / mitosis

Write components once, run everywhere. Compiles to React, Vue, Qwik, Solid, Angular, Svelte, and more.
https://mitosis.builder.io
MIT License
11.84k stars 513 forks source link

Elm + Webcomponents output #808

Open gampleman opened 1 year ago

gampleman commented 1 year ago

Hi this looks like a super useful project!

I would be interested in working on an output target aimed at integrating in Elm. This is somewhat challenging as Elm has a fairly radically different model than most of the existing targets for mitosis.

The ideal outcome is that most components could be compiled to pure Elm. To do this, there needs to be a reasonably good transpiration story from Typescript to Elm. I've started prototyping this and it seems like it could work for a decent amount of relatively simple components.

The fallback would then be to generate 2 files: a web component using the existing web component output + an Elm "interface" file that would reference and wrap that component. I would foresee this happening in 3 scenarios:

  1. Basically whenever ref is used, as Elm has no such concept.
  2. When the JS/TS code uses concepts that aren't well suited to compiling into Elm:
    • heavy mutation
    • DOM APIs
    • importing random library code
  3. When the typescript type information isn't good enough to allow sufficient analysis to transpile into Elm. This could alternatively throw an error. I suspect this wouldn't happen if compiling on the strictest TS settings (like forbidding any), but not sure how that's setup in Mitosis projects...

So, are there any docs for contributing code emitters? Also, looking at the JSON output, the code seems to get transformed into an imperative form. Is it possible to emit full on typescript AST with type information included?

samijaber commented 1 year ago

Would love more folks contributing generators! :)

So, are there any docs for contributing code emitters?

Yes! https://github.com/BuilderIO/mitosis/blob/main/developer/generators.md

Also, @raymondmuller worked on our Vue Composition API generator, and @sbrow is working on an Alpine generator here https://github.com/BuilderIO/mitosis/pull/846. Feel free to reach out in the discord to me/them/anyone with questions

I'm not familiar enough with Elm and/or Elm+webcomponents to know about the challenges you outlined. What I can note:

1- Mitosis currently only does a 1:1 mapping of Mitosis component file to output file. So it's going to require a non-trivial refactor across all generators to allow generators to output many files for each Mitosis component file.

2- We have been improving our preservation of types in the code, but we do not currently store a full TS AST. The premise of Mitosis is to parse a given syntax into the Mitosis Component JSON schema, acting as an intermediate representation between parsers and generators. So I don't know if we'll ever store a full AST like that. However, if you look at the JSON output of our fiddle, you'll see that we store types/interfaces, and preserve (most) types that are part of code blocks.

It does however sound like Elm would need the entire full types graph to be able to do its work? Not sure how far you can get with what Mitosis does at the moment

gampleman commented 1 year ago

Mitosis currently only does a 1:1 mapping of Mitosis component file to output file. So it's going to require a non-trivial refactor across all generators to allow generators to output many files for each Mitosis component file.

I suspect this may well be useful for other targets as well. For instance I could imagine it being useful when not using CSS-in-JS for something like React, where you would like to output JS components + CSS modules, or some such.

It does however sound like Elm would need the entire full types graph to be able to do its work? Not sure how far you can get with what Mitosis does at the moment

It depends on how nice one would want the output to be. For something that most Elm folks would be happy with, I think we're definitely into some Sufficiently Smart Compiler territory. I suppose that perhaps what would be possible is to transform the JSON output into some form of Typescript representation, than use the TS compiler to get type info from that. So perhaps that might not be a blocker (although probably not great for performance... not sure how important that is in this context).

samijaber commented 1 year ago

I suspect this may well be useful for other targets as well. For instance I could imagine it being useful when not using CSS-in-JS for something like React, where you would like to output JS components + CSS modules, or some such.

Yeah, agreed! I can see there being other benefits to making this change. Outputting CSS Modules is a perfect example.

For something that most Elm folks would be happy with, I think we're definitely into some Sufficiently Smart Compiler territory

Sufficiently Smart sounds good enough for me for a v1. 😄

If you're interested in making the change to our infra such that it supports 1:many generators, I'll write up a guide here shortly on how to make this migration. If not, anyone else is welcome to grab it!

samijaber commented 1 year ago

Updating generators to output multiple files

1- Start by updating the types used by all generators: https://github.com/BuilderIO/mitosis/blob/084de1af31f1181772fd73dcbfd20382e1508d47/packages/core/src/types/transpiler.ts#L9-L16

to:

type GeneratorOutput<R = string> = {
  // content of output. Currently either a component string or a builder component JSON.
  content: R;
  // in the future, we will add more types like 'styles' for CSS Modules, etc.
  type: 'component';
}

 export type Transpiler<R = string> = (args: TranspilerArgs) => GeneratorOutput<R>[]; 

 /** 
  * This type guarantees that all code generators receive the same base options 
  */ 
 export type TranspilerGenerator<X extends BaseTranspilerOptions, Y = string> = ( 
   args?: X, 
 ) => Transpiler<Y>; 

2- At that point, Typescript will point all the places containing errors. Easiest thing to do is to change the return statements in generators from return src to return [{ content: src, type: 'component' }]

3- In the CLI, change this to grab the first element of the array: https://github.com/BuilderIO/mitosis/blob/084de1af31f1181772fd73dcbfd20382e1508d47/packages/cli/src/build/build.ts#L310

transpiled = overrideFile ?? generator(options.options[target])({ path, component })[0].content; 

This is good enough for a first pass. When we actually start generating multiple files, and adding different types of content/type, we will need to improve our CLI to adequately process those. But this can come later

sbrow commented 1 year ago

If you're interested in making the change to our infra such that it supports 1:many generators, I'll write up a guide here shortly on how to make this migration. If not, anyone else is welcome to grab it!

Awesome guide! I opened an issue on this very subject #818. Might take a whack at it if I have the time.

sbrow commented 1 year ago

For anyone coming to this issue from Google, I'd like say that I don't think converting Mitosis components into Elm is an idea you should pursue.

There are two reasons for this:

  1. The power of Elm lies in the strict type checking and lack of runtime errors provided by the compiler.
  2. Elm works when each page is one component, with few or no sub-components. To quote the designers of elm: "Actively trying to make components is a recipe for disaster in Elm"

Elm is extremely powerful in that it lets you refactor without fear. However that only works when you are writing your code in elm, not when you write Javascript that gets converted into Mitosis JSON and then converted into Elm and then back into Javascript. You're going to give yourself way too many headaches trying to get the type system, (or systems, if you're using Typescript), to work together.

Writing good Elm components requires a significant change of mindset, one that can't easily be done automatically by software.

I think a more useful approach would be to write your components in Elm, and then make a tool like Sveltosis to convert them into Mitosis components, which can then be re-used with whatever framework you like. This would (theoretically) give you all the safety and power of Elm in any framework supported by Mitosis – a powerful thought indeed.

If you do need to use legacy (non-Elm) components in an Elm app, I recommend wrapping them in custom-elements, or just re-writing them by hand in Elm directly.

gampleman commented 1 year ago

@sbrow I think I should explain some of the motivation here.

I work on a an Elm team in a large corporation that is mostly React based. We have significant pressure to maintain a consistent look and feel with the rest of the company. So far we have been doing this by hand building our own UI kit that tries to mimic the look and feel of the rest of the company, but there are several factors making this approach difficult:

  1. Constant company-wide design changes. This is less of a problem for the rest of the company, since there is a dedicated team maintaining the UI kit, so for most other teams this is a matter of updating a dependency and potentially fixing some minor breakage. For us it could mean spending weeks rewriting our UI kit code.
  2. As our products mature, there is more attention being paid to details, like actually consistent padding, etc. So while before having roughly the right colours and typography was enough to make things look passingly similar, now issues get raised for even relatively minor discrepancies.
  3. Our team has shrunk a bit and we have other priorities than just finessing UIs.

As such, an ideal solution for us is to have the dedicated UI team describe their components in something like mitosis, then let everyone generate the code they need.

Now to address some specific points:

If you do need to use legacy (non-Elm) components in an Elm app, I recommend wrapping them in custom-elements

This isn't great when they are written in fairly heavy React style. You are now shipping React+Emotion or whatever on top of all your Elm code. React is a pretty huge dependency, but perhaps acceptable if you are using it to ship a large and complex app. If all you need it is to render a few UI components, then ugh.

The power of Elm lies in the strict type checking and lack of runtime errors provided by the compiler.

Most of our front-end code is business logic anyway. Even if styling and UI presentation is a bit less nice, this won't significantly impact the benefits we derive.

Writing good Elm components requires a significant change of mindset, one that can't easily be done automatically by software.

I suspect that there would be some learning curve to make Mitosis components that feel great in both Elm and (say) React. I wouldn't anticipate that just any code would product great Elm APIs, but I don't really see why for instance something like React functional components sans-hooks wouldn't produce perfectly idiomatic Elm view functions.

sbrow commented 1 year ago

@gampleman It sounds like you know what you're doing– I didn't mean to dissuade you or attack your practices. I just wanted the Mitosis team to be aware before they committed to having Elm as a target, that it might not be possible to do a full 1:1 component mapping from Mitosis to Elm. :)

If Elm were to become a target for Mitosis, I think it'd be worth having an asterisk to warn people that unlike most of the other targets, you can't just copy and paste the fiddle output and expect a seamless result.