package jsbundler import ( "os" "path/filepath" "strings" "testing" ) // renderComponent bundles a compiled component module (which must `export const A`) // with a render harness, runs it in the goja SSR engine, and returns the HTML. func renderComponent(t *testing.T, compiledJS string) (string, error) { t.Helper() entry := compiledJS + "\n" + `import { render as _$r, createComponent as _$cc } from "solid-js/web"; globalThis.__render = function () { var root = document.createElement("div"); var dispose = _$r(function () { return _$cc(A, {}); }, root); var out = globalThis.__serialize(root); dispose(); return out; };` bundle, err := BundleEntry(entry, ".") if err != nil { return "", err } eng, err := New() if err != nil { return "", err } if err := eng.LoadBundle(bundle); err != nil { return "", err } return eng.Render() } // assertRenderEquivalent compiles src with babel AND the Go compiler, renders // both, and requires identical HTML. This is the compiler's correctness oracle. func assertRenderEquivalent(t *testing.T, name, src string) { t.Helper() root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } // Point the bundler at kjol's own vendored Solid before chdir'ing — the path is // relative to the package dir, and Configure stores it for vendorDirs(). // // Without this the oracle silently never ran: webDir defaults to "", so // vendorDirs() resolved to /frontend/vendor — a directory kjol does not // have and never had (it is a framework; there is no app frontend here). Every // render then failed to resolve solid-js/web. It only looked green if a test // that calls Configure happened to run first, and none does: the sole callers // are in crosstree_test.go, which sorts after this file. webAbs, err := filepath.Abs(filepath.Join("..", "jsruntime")) if err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(webAbs, "runtime", "vendor.json")); err != nil { t.Skipf("kjol vendored solid runtime not present (%v)", err) } Configure(Config{WebDir: webAbs}) t.Chdir(root) babelJS, err := Compile(src, name+".tsx") if err != nil { t.Fatalf("[%s] babel compile: %v", name, err) } goJS, err := compileSolidGo(src, name+".tsx", false) if err != nil { t.Fatalf("[%s] go compile: %v\n--- go output ---\n%s", name, err, goJS) } babelHTML, err := renderComponent(t, babelJS) if err != nil { t.Fatalf("[%s] render babel: %v", name, err) } goHTML, err := renderComponent(t, goJS) if err != nil { t.Fatalf("[%s] render go: %v\n--- go output ---\n%s", name, err, goJS) } if babelHTML != goHTML { t.Errorf("[%s] render mismatch:\n babel: %q\n go: %q\n--- go compiled ---\n%s", name, babelHTML, goHTML, goJS) } } func TestGoCompilerRenderCore(t *testing.T) { cases := []struct{ name, src string }{ {"static", `export const A = () =>
hi
;`}, {"dyn-text", `export const A = () => { const c = () => 42; return
{c()}
; };`}, {"nested", `export const A = () => { const x = () => "X"; return
a{x()}
; };`}, {"mixed-children", `export const A = () => { const x = () => "X"; const y = () => "Y"; return
before {x()} after {y()}
; };`}, {"dyn-attr", `export const A = () => { const id = () => "foo"; return
hi
; };`}, {"list", `export const A = () => { const items = ["a", "b", "c"]; return ; };`}, {"deep-static", `export const A = () =>

Title

body text

;`}, {"multi-attr", `export const A = () => ;`}, // Multi-line children. Every case above is written on ONE line, which is why // they all passed while the compiler was eating the spaces around an expression // the moment the JSX was indented — i.e. in essentially all real code. These // pin JSX's asymmetric whitespace rule: indentation goes, the space between the // words stays. {"ml-text-around-expr", `export const A = () => { const c = () => 0; return ( ); };`}, {"ml-text-before-el", `export const A = () => (

selected: day

);`}, {"ml-wrapped-prose", `export const A = () => (

one two three four

);`}, {"ml-expr-both-sides", `export const A = () => { const x = () => "X"; const y = () => "Y"; return (
a {x()} b {y()} c
); };`}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { assertRenderEquivalent(t, c.name, c.src) }) } } func TestGoCompilerRenderComponents(t *testing.T) { cases := []struct{ name, src string }{ {"component-children", `export const A = () => { const Box = (props) =>
{props.children}
; return hi; };`}, {"component-dyn-prop", `export const A = () => { const Lbl = (props) => {props.text}; const t = () => "yo"; return ; };`}, {"nested-components", `export const A = () => { const Row = (props) =>
  • {props.children}
  • ; return ; };`}, {"show-true", `import { Show } from "solid-js"; export const A = () =>
    no

    }>yes
    ;`}, {"show-false", `import { Show } from "solid-js"; export const A = () =>
    no

    }>yes
    ;`}, {"for", `import { For } from "solid-js"; export const A = () => ;`}, {"fragment", `export const A = () => { const a = () => "A"; const b = () => "B"; return
    {a()}{b()}
    ; };`}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { assertRenderEquivalent(t, c.name, c.src) }) } } func TestGoCompilerRenderSpreadRef(t *testing.T) { cases := []struct{ name, src string }{ {"spread", `export const A = () => { const p = { id: "pid", title: "t" }; return
    hi
    ; };`}, {"spread-override", `export const A = () => { const p = { class: "from-p" }; return
    hi
    ; };`}, {"ref", `export const A = () => { let r; return
    hi
    ; };`}, {"component-spread", `export const A = () => { const Box = (props) =>
    {props.children}
    ; const p = { id: "pid", title: "t" }; return hi; };`}, {"component-spread-override", `export const A = () => { const Box = (props) =>
    x
    ; const p = { class: "from-p" }; return ; };`}, {"component-spread-before", `export const A = () => { const Box = (props) =>
    x
    ; const p = { class: "from-p" }; return ; };`}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { assertRenderEquivalent(t, c.name, c.src) }) } } // Compile every real .tsx with the Go compiler and assert it produces parseable // output. This surfaces constructs the codebase uses that the codegen doesn't // handle yet (the failures ARE the milestone-3/4 gap list). func TestGoCompilerCompilesRealFiles(t *testing.T) { files := walkRepoTSX(t) var failed, ok int for _, f := range files { data, err := os.ReadFile(f) if err != nil { continue } out, err := compileSolidGo(string(data), f, false) if err != nil { failed++ t.Logf("COMPILE-ERR %s: %v", f, err) continue } if err := validateJS(out); err != nil { failed++ t.Logf("PARSE-ERR %s: %v", f, err) continue } ok++ } t.Logf("Go compiler: %d/%d real .tsx produced parseable output (%d failed)", ok, len(files), failed) } // M5: dev mode wraps components with solid-refresh. Verify the shape and that // every real file still produces parseable output with instrumentation on. func TestGoCompilerRefreshInstrumentation(t *testing.T) { src := `export function Counter() { const c = () => 1; return
    {c()}
    ; } export const Banner = () => hi; const NOT_A_COMPONENT = 42; function helper() { return 5; }` out, err := compileSolidGo(src, "Refresh.tsx", true) if err != nil { t.Fatalf("dev compile: %v", err) } if err := validateJS(out); err != nil { t.Fatalf("dev output does not parse: %v\n%s", err, out) } for _, want := range []string{ `from "solid-refresh"`, "const _REGISTRY = _$$registry();", `_$$component(_REGISTRY, "Counter", function Counter`, `_$$component(_REGISTRY, "Banner",`, `if (import.meta.hot) { _$$refresh("esm", import.meta.hot, _REGISTRY); }`, } { if !strings.Contains(out, want) { t.Errorf("dev output missing %q\n--- output ---\n%s", want, out) } } // Non-components must NOT be wrapped. if strings.Contains(out, `"NOT_A_COMPONENT"`) || strings.Contains(out, `"helper"`) { t.Errorf("non-component was wrapped:\n%s", out) } // Prod mode must have no refresh instrumentation. prod, _ := compileSolidGo(src, "Refresh.tsx", false) if strings.Contains(prod, "solid-refresh") { t.Errorf("prod output leaked refresh instrumentation:\n%s", prod) } } func TestGoCompilerDevCompilesRealFiles(t *testing.T) { files := walkRepoTSX(t) var failed, ok int for _, f := range files { data, err := os.ReadFile(f) if err != nil { continue } out, err := compileSolidGo(string(data), f, true) if err != nil { failed++ t.Logf("DEV-COMPILE-ERR %s: %v", f, err) continue } if err := validateJS(out); err != nil { failed++ t.Logf("DEV-PARSE-ERR %s: %v", f, err) continue } ok++ } t.Logf("Go compiler (dev): %d/%d real .tsx parseable with refresh (%d failed)", ok, len(files), failed) } // Regression: a context provider's JSX children must evaluate lazily (inside the // provider), or a consumer reads the context before it's set. Eager `children:` // makes Consumer throw "no ctx"; lazy `get children()` renders correctly. func TestGoCompilerContextChildren(t *testing.T) { src := `import { createContext, useContext } from "solid-js"; const Ctx = createContext(); function Provider(props) { return {props.children}; } function Consumer() { const v = useContext(Ctx); if (!v) throw new Error("no ctx"); return {v}; } export const A = () => ;` root, _ := filepath.Abs("../..") t.Chdir(root) goJS, err := compileSolidGo(src, "Ctx.tsx", false) if err != nil { t.Fatalf("compile: %v", err) } html, err := renderComponent(t, goJS) if err != nil { t.Fatalf("render (context leaked?): %v\n--- compiled ---\n%s", err, goJS) } if !strings.Contains(html, "ok") { t.Errorf("expected context value in output, got: %q", html) } } // Regression: object `style` must go through _$style (setProperty per key), not // setAttribute (which stringifies to "[object Object]" and breaks positioning); // innerHTML must be a property assignment (setAttribute is a no-op → empty icons). func TestGoCompilerStyleAndInnerHTML(t *testing.T) { root, _ := filepath.Abs("../..") t.Chdir(root) cases := []struct{ name, src, want, absent string }{ {"object-style", `export const A = () => { const s = () => ({ top: "10px", left: "20px" }); return
    x
    ; };`, "10px", "[object Object]"}, {"innerHTML", "export const A = () => { const h = () => ''; return ; };", " { const no = () => false; const yes = () => true; return
    ; };` out, err := compileSolidGo(src, "bools.tsx", false) if err != nil { t.Fatal(err) } html, err := renderComponent(t, out) if err != nil { t.Fatalf("render: %v\n%s", err, out) } for _, broken := range []string{`checked="false"`, `disabled="false"`} { if strings.Contains(html, broken) { t.Errorf("false-valued boolean attr still present (reads as true in browser): %q in %s", broken, html) } } for _, present := range []string{`checked=""`, `required=""`} { if !strings.Contains(html, present) { t.Errorf("true-valued boolean attr missing: expected %q in %s", present, html) } } } // Regression: a comment-only JSX expression child ({/* note */}) must be // dropped like whitespace, not compiled as a real expression. childrenProp // (component children) already filtered these via renderedChild, but a bare // root fragment with 2+ children went through genFragment's own filter, which // only skipped blank text — a comment-only child slipped through and compiled // to `_$memo(() => /* note */)`, a syntax error that breaks the whole module. func TestGoCompilerFragmentCommentChild(t *testing.T) { root, _ := filepath.Abs("../..") t.Chdir(root) src := `export const A = () => { return <> {/* a comment */}
    one
    two
    ; };` out, err := compileSolidGo(src, "frag-comment.tsx", false) if err != nil { t.Fatal(err) } if err := validateJS(out); err != nil { t.Fatalf("compiled output doesn't parse: %v\n--- output ---\n%s", err, out) } html, err := renderComponent(t, out) if err != nil { t.Fatalf("render: %v\n%s", err, out) } if !strings.Contains(html, "one") || !strings.Contains(html, "two") { t.Errorf("expected both siblings in output, got: %q", html) } }