package jsbundler import ( "os" "path/filepath" "strings" "testing" "time" ) // entryHome renders the fixture page the way ssrEntrySolid does: plain JS // (createComponent, no JSX in the entry) importing the .tsx page, which the Go // Solid compiler compiles. The layout is omitted to target the body. const entryHome = ` import { render, createComponent } from "solid-js/web"; import { Home } from "./frontend/src/pages/public/Home.tsx"; globalThis.__render = function () { const root = document.createElement("div"); const dispose = render(function () { return createComponent(Home, {}); }, root); const out = globalThis.__serialize(root); dispose(); return out; }; ` // homePage is the fixture entryHome imports: the smallest page that exercises the // thing under test — serverData() read inside the reactive body, a skeleton when // it is null, the data when it is not. // // It used to be cdrateline's real frontend/src/pages/public/Home.tsx, reached by // chdir'ing to "../..". That worked when the bundler lived inside cdrateline and // "../.." was the app root. Since the extraction, "../.." is the kjol repo root — // which has no frontend/ and, by the first golden rule, never will: the framework // does not import application code, and a test that does is the same coupling // wearing a different hat. Both tests here have been failing ever since. // It is deliberately not just a
: it carries the specific things the Solid // compiler has to get right when it bakes a template and goja serializes it — an // inline SVG (attribute CASE must survive: viewBox, not viewbox), a static string // style containing a url() with slashes and a comma-free but parenthesised value, // and an HTML entity. const homePage = ` import { serverData } from "@kjol/ssr/serverData.ts"; interface Rate { term: string; low: string; high: string; avg: string } const TERMS = ["90 Day", "180 Day", "1 Year"]; export const Home = () => { const data = () => serverData<{ rates: Rate[] }>(); const rates = () => data()?.rates ?? []; return (

Rates & terms

{rates().length === 0 ? {TERMS.map((t) => )}
{t}
: {rates().map((r) => ( ))}
{r.term}{r.low}{r.high}{r.avg}
}
); }; ` // ssrFixtureRoot writes the fixture app tree to a temp dir, points the bundler at // it plus kjol's vendored Solid, and chdirs there — reproducing the server's setup // (cwd at the app root, relative "." project root) without needing a real app. func ssrFixtureRoot(t *testing.T) string { t.Helper() 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) } root := t.TempDir() pageDir := filepath.Join(root, "frontend", "src", "pages", "public") if err := os.MkdirAll(pageDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(pageDir, "Home.tsx"), []byte(homePage), 0o644); err != nil { t.Fatal(err) } Configure(Config{AppFrontend: filepath.Join(root, "frontend"), WebDir: webAbs}) t.Chdir(root) return root } // Proves the ISR data-injection path the server uses: a pre-bundled render entry // run in goja with server data injected (RenderBundleWithData) makes Home render // the injected rates (via serverData()) instead of the loading skeleton. func TestRenderHomeWithServerData(t *testing.T) { ssrFixtureRoot(t) data := `{"rates":[{"term":"90 Day","low":"1.111%","high":"2.222%","avg":"1.500%"}]}` js, err := BundleEntry(entryHome, ".") if err != nil { t.Fatalf("bundle: %v", err) } out, err := RenderBundleWithData(js, data) if err != nil { t.Fatalf("render with data: %v", err) } for _, want := range []string{"1.111%", "2.222%", "1.500%"} { if !strings.Contains(out, want) { t.Errorf("output missing injected rate %q", want) } } // With data present the table shows it, not the loading skeleton. if strings.Contains(out, "animate-pulse") { t.Errorf("skeleton rendered despite injected data") } } // Reproduces the server's setup (cwd at the app root, relative "." project root). // Guards the esbuild alias-resolution bug where a non-absolute root yields // bare-specifier alias targets that fail to resolve. func TestRenderWithRelativeRoot(t *testing.T) { ssrFixtureRoot(t) r := NewRenderer(".", entryHome, true) out, err := r.HTML() if err != nil { t.Fatalf("render with relative root: %v", err) } if !strings.Contains(out, `class="page-home"`) { t.Errorf("output missing page-home") } } // Confirms the cache: the first HTML() pays bundle+render, the second (no source // change) returns the cached string near-instantly. func TestRendererCaching(t *testing.T) { root := ssrFixtureRoot(t) r := NewRenderer(root, entryHome, false) // prod: warm HTML() returns the cached string directly t0 := time.Now() first, err := r.HTML() if err != nil { t.Fatalf("cold render: %v", err) } cold := time.Since(t0) t1 := time.Now() second, err := r.HTML() if err != nil { t.Fatalf("warm render: %v", err) } warm := time.Since(t1) t.Logf("cold=%v warm=%v", cold, warm) if first != second { t.Errorf("cached output differs from first render") } if warm > cold/4 { t.Errorf("cache hit too slow: cold=%v warm=%v", cold, warm) } } // No data → the page renders its loading skeleton, exercising compiled Solid // against inline SVGs (viewBox case), string style attributes, entities, and the // table. SVG attribute case and the style string must survive serialization. func TestRenderHomeSkeleton(t *testing.T) { root := ssrFixtureRoot(t) bundle, err := BundleEntry(entryHome, root) if err != nil { t.Fatalf("bundle: %v", err) } eng, err := New() if err != nil { t.Fatalf("engine: %v", err) } if err := eng.LoadBundle(bundle); err != nil { t.Fatalf("load bundle: %v", err) } out, err := eng.Render() if err != nil { t.Fatalf("render: %v", err) } t.Logf("HOME SKELETON (%d bytes):\n%s", len(out), out) for _, want := range []string{ `class="page-home"`, `viewBox="0 0 24 24"`, // SVG attribute case preserved `background-image: url(/images/hero-bg.jpg)`, // static string style baked into the template `animate-pulse`, // skeleton bars (no server data => skeleton branch) `90 Day`, // term labels shown in the skeleton } { if !strings.Contains(out, want) { t.Errorf("output missing %q", want) } } }