solid icon indicating copy to clipboard operation
solid copied to clipboard

Use SolidJS with ESBuild

Open Mqxx opened this issue 2 months ago • 2 comments

Hey,

as stated by the documentation, SolidJS uses Babel in combination with a preset to transpile the JSX/TSX. I am currently trying to transpile my SolidJS app using ESBuild in combination with a Deno setup and ran into a few problems after realizing that SolidJS needs the Babel preset. I am now using the the esbuild-plugin-solid and that works well.

But this seems like a "workaround", because the plugin does not utilize the speed of ESBuild because it just calls Babel transpiler under the hood. I was wondering what features SolidJS requires, that ESBuild currently does not offer. ryansolid mentioned that SolidJS needs "AST level plugin manipulation". Is this just for performance so that SolidJS can be optimized or is this needed for something else? And would it be possible to transpile SolidJS without the need of Babel and the preset?

Currently ESBuild offers a "minimalistic" plugin API. ESBuild does not create an AST, so "AST level plugin manipulation" would not be possible without an other parser on top (That's what the plugin currently does). But the question is if we even need "AST level plugin manipulation" in the first place.

Maybe someone can give me a hint on why is is so "complicated and complex" to transpile SolidJS JSX/TSX.

Mqxx avatar Nov 03 '25 09:11 Mqxx

Maybe someone can give me a hint on why is is so "complicated and complex" to transpile SolidJS JSX/TSX.

Because we use AST manipulation to turn JSX into multiple other statements that depends on context a lot. I have documented the process here, but it is unfortunately not being merged: https://github.com/atk/solid-docs-next/blob/feat/how-it-works/src/routes/advanced-concepts/jsx-transpilation.mdx

import { createSignal } from "solid-js";

const Test = () => {
  const [counter, setCounter] = createSignal(0);
  return <button onClick={() => setCounter(c => c + 1)}>{counter()}</div>
};

is turned into

import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<button>`);
import { createSignal } from "solid-js";
const Test = () => {
  const [counter, setCounter] = createSignal(0);
  return (() => {
    var _el$ = _tmpl$();
    _el$.$$click = () => setCounter(c => c + 1);
    _$insert(_el$, counter);
    return _el$;
  })();
};
_$delegateEvents(["click"]);

This means there is not a 1:1 relation from an input to an output. For example, the transpilation checks that counter is a function and directly puts it in the _$insert call, which will bind all the reactivity so counter will actually update.

atk avatar Nov 03 '25 10:11 atk

This means there is not a 1:1 relation from an input to an output. For example, the transpilation checks that counter is a function and directly puts it in the _$insert call, which will bind all the reactivity so counter will actually update.

Ahh okay so it looks like this was the problem when i was using ESBuild directly without the plugin.

This is what ESBuild produces directly. From this:

import { render } from '@solid-js/web';
import { createSignal } from "@solid-js";

const Test = () => {
  const [counter, setCounter] = createSignal(0);
  return <button type='button' onClick={() => setCounter(c => c + 1)}>{counter()}</button>
};
render(
  () => <Test></Test>,
  globalThis.document.body
);

To this:

// ... a bunch of minification

var h = createHyperScript({
  spread,
  assign,
  insert,
  createComponent,
  dynamicProperty,
  SVGElements
});

// ../../../../AppData/Local/deno/npm/registry.npmjs.org/solid-js/1.9.10/h/jsx-runtime/dist/jsx.js
function jsx(type, props) {
  return h(type, props);
}

// src/client/index.tsx
var Test = () => {
  const [counter, setCounter] = createSignal(0);
  return /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setCounter((c) => c + 1), children: counter() });
};
render(() => /* @__PURE__ */ jsx(Test, {}), globalThis.document.body);
//# sourceMappingURL=index.js.map

And yes when using the plugin I get this:

// ... a bunch of minification

// src/client/index.tsx
var _tmpl$ = /* @__PURE__ */ template(`<button type=button>`);
var Test = () => {
  const [counter, setCounter] = createSignal(0);
  return (() => {
    var _el$ = _tmpl$();
    _el$.$$click = () => setCounter((c) => c + 1);
    insert(_el$, counter);
    return _el$;
  })();
};
render(() => createComponent(Test, {}), globalThis.document.body);
delegateEvents(["click"]);
//# sourceMappingURL=index.js.map

Mqxx avatar Nov 03 '25 10:11 Mqxx