Add js web stuff to landing page + documentation
This commit is contained in:
301
go/jsbundler/compile_solid_render_test.go
Normal file
301
go/jsbundler/compile_solid_render_test.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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 <root>/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 = () => <div class="x">hi</div>;`},
|
||||
{"dyn-text", `export const A = () => { const c = () => 42; return <div>{c()}</div>; };`},
|
||||
{"nested", `export const A = () => { const x = () => "X"; return <div><span>a</span><b>{x()}</b></div>; };`},
|
||||
{"mixed-children", `export const A = () => { const x = () => "X"; const y = () => "Y"; return <div>before {x()} after {y()}</div>; };`},
|
||||
{"dyn-attr", `export const A = () => { const id = () => "foo"; return <div class="s" id={id()}>hi</div>; };`},
|
||||
{"list", `export const A = () => { const items = ["a", "b", "c"]; return <ul>{items.map((i) => <li>{i}</li>)}</ul>; };`},
|
||||
{"deep-static", `export const A = () => <section><header><h1>Title</h1></header><p>body text</p></section>;`},
|
||||
{"multi-attr", `export const A = () => <input type="text" name="q" disabled />;`},
|
||||
|
||||
// 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 (
|
||||
<button>
|
||||
Clicked {c()} times
|
||||
</button>
|
||||
); };`},
|
||||
{"ml-text-before-el", `export const A = () => (
|
||||
<p>
|
||||
selected: <span>day</span>
|
||||
</p>
|
||||
);`},
|
||||
{"ml-wrapped-prose", `export const A = () => (
|
||||
<p>
|
||||
one two
|
||||
three four
|
||||
</p>
|
||||
);`},
|
||||
{"ml-expr-both-sides", `export const A = () => { const x = () => "X"; const y = () => "Y"; return (
|
||||
<div>
|
||||
a {x()} b {y()} c
|
||||
</div>
|
||||
); };`},
|
||||
}
|
||||
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) => <div class="box">{props.children}</div>; return <Box>hi</Box>; };`},
|
||||
{"component-dyn-prop", `export const A = () => { const Lbl = (props) => <span>{props.text}</span>; const t = () => "yo"; return <Lbl text={t()} />; };`},
|
||||
{"nested-components", `export const A = () => { const Row = (props) => <li>{props.children}</li>; return <ul><Row>one</Row><Row>two</Row></ul>; };`},
|
||||
{"show-true", `import { Show } from "solid-js"; export const A = () => <div><Show when={true} fallback={<p>no</p>}>yes</Show></div>;`},
|
||||
{"show-false", `import { Show } from "solid-js"; export const A = () => <div><Show when={false} fallback={<p>no</p>}>yes</Show></div>;`},
|
||||
{"for", `import { For } from "solid-js"; export const A = () => <ul><For each={[1, 2, 3]}>{(n) => <li>{n}</li>}</For></ul>;`},
|
||||
{"fragment", `export const A = () => { const a = () => "A"; const b = () => "B"; return <div>{a()}{b()}</div>; };`},
|
||||
}
|
||||
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 <div {...p} class="x">hi</div>; };`},
|
||||
{"spread-override", `export const A = () => { const p = { class: "from-p" }; return <div {...p} class="from-attr">hi</div>; };`},
|
||||
{"ref", `export const A = () => { let r; return <div ref={r}>hi</div>; };`},
|
||||
}
|
||||
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 <div>{c()}</div>; }
|
||||
export const Banner = () => <span>hi</span>;
|
||||
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 <Ctx.Provider value="ok">{props.children}</Ctx.Provider>; }
|
||||
function Consumer() { const v = useContext(Ctx); if (!v) throw new Error("no ctx"); return <span>{v}</span>; }
|
||||
export const A = () => <Provider><Consumer /></Provider>;`
|
||||
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 <div style={s()}>x</div>; };`, "10px", "[object Object]"},
|
||||
{"innerHTML", "export const A = () => { const h = () => '<path d=\"M1 2\"/>'; return <svg innerHTML={h()}></svg>; };", "<path", "innerHTML="},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
out, err := compileSolidGo(c.src, c.name+".tsx", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
html, err := renderComponent(t, out)
|
||||
if err != nil {
|
||||
t.Fatalf("render: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(html, c.want) {
|
||||
t.Errorf("output %q missing %q\ncompiled:\n%s", html, c.want, out)
|
||||
}
|
||||
if strings.Contains(html, c.absent) {
|
||||
t.Errorf("output %q still contains broken %q", html, c.absent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user