tomtheisen / mutraction

Futuristic web apps with model mutation tracking
https://mutraction.dev/
MIT License
17 stars 0 forks source link

Vite plugin #8

Open naturalethic opened 8 months ago

naturalethic commented 8 months ago

I've been hacking getting this to work with vite, using vite-plugin-babel, but ideally this would have native vite plugin, for performance reasons.

naturalethic commented 8 months ago

To use vite with babel plugins, add this to vite.config.ts:

import babel from "vite-plugin-babel";

export default defineConfig({
    plugins: [
        babel({
            filter: /\.tsx?$/,
            loader: "tsx",
            babelConfig: {
                babelrc: false,
                configFile: false,
                presets: ["@babel/preset-typescript"],
                plugins: [
                    "@babel/plugin-syntax-jsx",
                    "mutraction-dom/compile-jsx",
                ],
            },
        }),
    ],
    ...
})
naturalethic commented 8 months ago

Running Bun native would be neat too.

tomtheisen commented 8 months ago

Do you have any experience setting this up? Are you hoping for HMR?

naturalethic commented 8 months ago

Using the plugins as above works fine, though I suspect in a larger application the performance might start to drag.

I'm somewhat familiar with Vite plugins, but not babel. I'll take a look at compile-jsx and see if I can grok it.

naturalethic commented 8 months ago

Looking at this a bit today. The correct approach, which would go most of the way to satisfying a typical Vite or Bun setup, would be to write a custom JSX factory lib, similar to how React, Preact, Solid, etc do it. This would require a bit of a re-architecture of your framework for element construction.

For example, if a child element has dynamic content under your framework, you'll wrap that with child(() => ... ), however using a jsx factory, something like {state.val} is evaluated at construction, so there is no chance to wrap it in child. To keep that pattern, this is where a plugin might be needed to wrap that child content before it gets passed off to the jsx factory.

At any rate, this will require a lot more thinking and a determination from you to put it on the roadmap if you think its worthwhile or important. For now I'll just stick with the babel plugin while I evaluate the framework more fully.

Thus far my component code in my candidate has about half the number of lines as the Solid project I'm porting it from.

tomtheisen commented 8 months ago

Thanks for doing this investigation legwork. It seems I need to acquaint myself with JSX factories.

You have gotten to the heart of why I wasn't able to use any existing JSX compilers/transformers that I could find. The values need to be evaluated during element construction, not before.

Maybe I can figure it out if I look at some existing implementations, like you've mentioned.

To decide whether to commit to implementing this, I'd like to know what you see as the main benefits. The obvious thing you get with vite is fast startup and HMR. That may be compelling enough on it's own, but is there more?

naturalethic commented 8 months ago

Ultimately, I suppose the reasoning would be the same as why you created this framework in the first place.

I wouldn't want you to commit unless it scratched an itch for you. Certainly don't do it for me! I just like this area of of the stack and trying out different frameworks. Impossible for me to say whether I'd be in a position to use it in a production setting for anything significant.

For general rationale, I think just to keep up with modern tooling, as babel seems to be on the way out. And here I believe bun will incrementally implement everything vite currently does and supercede it.

Other things I'd look at is structuring your modules to allow for tree shaking out parts that a consumer isn't using, to keep their built size smaller. Also for components it would be nice to be able to use jsx syntax for them instead of {component(...)} type syntax.

Do you use this framework in any production capacity? Does anyone else that you know of?

As for existing solutions for inspiration, I'd probably use Solid as an exemplar, and this project has pretty readable code.

tomtheisen commented 8 months ago

I'm pretty sure no one is using this in production, and I couldn't really recommend doing so at this point. It's kind of a tech demo, but I do intend to support it, and I have (and will continue to) put effort into the developer ergonomics. But it doesn't (yet) have a proven track record of stability.

The real reason I made this is that I was under threat of being forced to use react at work. I didn't build this to use instead, but rather to be able to speak from an informed position about front end data-driven UI frameworks. I also spent a fair amount of time reading react source code. I do not like react.

I built mutraction just to see what would happen if I built something the way I would have chosen. I half expected the concept to fall apart when applied to a full framework. When I found how well everything seemed to be working out, I just kept going.

Most of the use I get out of it now is building mock-up type stuff using the sandbox. But I'm planning to keep developing it just because it's fun.

I don't know any technical reason why mutraction would be inappropriate for production. It just has a short existence, and few users or maintainers.

Anyway, I'm going to put some vite stuff on my reading list, including your link. I'm intending to learn enough to be able to decide what to do. I think keeping it modern is a big enough reason to give it a serious attempt. My main concern at this point is figuring out how to get it to work with the sandbox, which currently uses a portable browser-targeted build of babel.

As for <Component /> syntax, it seemed to me that { Component() } was just generally as good or better in every metric. Some benefits I can imagine for the latter:

  1. Component calls can use arbitrary JS syntax, like { comps[idx]() } or { (a ?? b)(...c) }.
  2. Component functions can use arbitrary parameter definitions. You're not constrained to a single "props" object. I find props type definitions slightly unwieldy.
  3. Getting the typescript types working correctly seemed challenging. I'm probably missing something. In general, non-React uses of JSX in typescript seem to be second class considerations. That's better than practically impossible, like it used to be.

Maybe I could make it work both ways for the happy paths, and esoteric fancy calls could still use the { comp } notation.

Ok, I'll stop now. Sorry for the extended ramble.

naturalethic commented 8 months ago

I like the extended ramble.

Regarding <Component /> you make very good points. For what its worth I was able to get typescript types working with jsx components without a problem (in my own experimental framework .. uh work), but regardless, I like your reasons for keeping the function call.

I've put a 'watch' on this repo and look forward to seeing it develop further. Also happy to be available to bounce ideas off if you like.

J

tomtheisen commented 8 months ago

I've done some investigation. I'm not done, but I found some stuff.

  1. The solid vite plugin has a direct dependency on babel for doing its source transformation.
  2. pota also uses babel for source transformation also.
  3. Vite source transformation plugins are rollup plugins. The "official" rollup plugin for jsx transformation is jsx-transform, which is based on esprima. Esprima is super old and dead, and doesn't support modern syntax like private class fields.

rollup provides a parse() method but the transform plugin hook doesn't seem to have a way to accept an AST as a transform output, as far as I can tell. A number of other "fancy" rollup transform plugins internally use babel or an alternative.

There are a number of alternatives to babel.

  1. SWC
  2. esprima
  3. espree
  4. acorn
  5. esbuild

SWC and esbuild seem to require plugins to built in WASM. esprima seems to be abondoned. espree is built on top of acorn. I'm not interested in getting into WASM for now. So it would seem that acorn and its ilk are all that are remaining. It seems to work with recast for AST transforms, but there doesn't seem to be a practical benefit over babel.

So, as it stands, here's my plan.

  1. I will make a vite/rollup plugin, and vite will become the recommended project scaffolding system.
  2. The vite plugin will do source transformation via a babel dependency and plugin.
  3. I haven't decided about hot module reloading.
naturalethic commented 8 months ago

I've done some work with Rust and WASM in the past. Rust is fantastic for code generation and targeting WASM is trivial. For AST transformation the experience isn't going to be much different between Rust or TS, but you can target WASM easily, as mentioned.

naturalethic commented 8 months ago

https://oxc-project.github.io/

tomtheisen commented 8 months ago

https://oxc-project.github.io/

Very interesting. The "babel compatible" transformer seems to be not ready. Depending how stuff goes this weekend, I might be able to get into swc and rust.

tomtheisen commented 8 months ago

One problem I'm having vite is that its build output is unusable from a file:// url.

It's an (unstated) goal of mine to be able to distribute browser-based applications as bundles of static files, invoked locally from a file system. <script type="module"> seems incompatible with this, since it requires same origin. But all file: urls represent different origins.

In the trivial case, I had to make some changes to the vite build output to get it to run this way.

  1. Remove type and crossorigin attributes from the script tag.
  2. Remove crossorigin from the style <link>.
  3. Move the script tag from <head> to <body> so that document.body would be defined. This could be accomplished with a defer attribute instead.

I'm guessing more problems would surface in a scenario where chunking or splitting came into play.

At this point, I'm targeting the goals in this order.

  1. Use vite with existing babel plugin - need a way to get file://-compatible build output
  2. Hot module reloading - this is a bit mysterious to me at the moment; I have some learning to do first.
  3. Maybe - switch from babel to swc or similar.
naturalethic commented 8 months ago

If your target is Electron or other native wrapper, this shouldn't be a problem. Browsers are so locked down I've never bothered with the use case you're attempting.