rename packages, refactor out tailwind compiler,
This commit is contained in:
154
go/webbundler/genssr.go
Normal file
154
go/webbundler/genssr.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package webbundler
|
||||
|
||||
// Public-page SSR code generation. For each page in the manifest
|
||||
// (frontend/src/pages/public/pages.ts) this renders the solid-js/html component
|
||||
// to its data-free skeleton HTML — at build time, via the same in-package goja
|
||||
// engine (ssr.go/renderer.go) the server re-runs for ISR — and bakes it into the
|
||||
// Go registry:
|
||||
//
|
||||
// - internal/handlers/public_pages.gen.go routes + <title> + rendered body
|
||||
//
|
||||
// The server then just wraps each baked body in the document shell and serves
|
||||
// it (internal/handlers/public_ssr.go); the browser bundle takes over on load.
|
||||
//
|
||||
// The env badge is intentionally absent from the rendered markup: this SSR
|
||||
// render doesn't define esbuild's __ENV_TYPE__ (see ssr.go), so env.ts reads ""
|
||||
// and the badge renders nothing here. The client takeover bundle bakes the real
|
||||
// compile-time environment, so the badge appears after takeover.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// writeGoRegistry renders each page's SSR skeleton and writes the Go registry
|
||||
// (internal/handlers/public_pages.gen.go). Rendering reads frontend/src and
|
||||
// wwwroot/vendor relative to the project root, so the bundler must run from
|
||||
// there (it always does).
|
||||
//
|
||||
// Rendering is cached per page (tmp/ssr-cache.json) keyed by the page's entry
|
||||
// source + the content hashes of every file esbuild bundled into it, so a build
|
||||
// only re-renders pages whose sources actually changed. Cache misses render
|
||||
// concurrently (one goja runtime per worker). See ssrcache.go.
|
||||
func writeGoRegistry(defs []pageDef) error {
|
||||
start := time.Now()
|
||||
engine := EngineHash()
|
||||
prev := loadSSRCache()
|
||||
reuse := prev.Engine == engine // an engine change invalidates every page
|
||||
hasher := newFileHasher()
|
||||
next := ssrCache{Engine: engine, Pages: make(map[string]ssrCacheEntry, len(defs))}
|
||||
|
||||
bodies := make([]string, len(defs))
|
||||
renderJSs := make([]string, len(defs)) // bundled render entry, baked for dynamic (ISR) pages
|
||||
entries := make([]string, len(defs))
|
||||
cached := make([]bool, len(defs))
|
||||
|
||||
// First pass (cheap, serial): reuse unchanged pages, collect the rest.
|
||||
var misses []renderJob
|
||||
for i, d := range defs {
|
||||
module := filepath.ToSlash(path.Join("frontend", "src", d.Module))
|
||||
entries[i] = ssrEntrySolid(module, d.Component, d.Path)
|
||||
if reuse {
|
||||
if ce, ok := prev.Pages[d.Path]; ok && ce.EntryHash == pageEntryHash(entries[i], d.Dynamic) && inputsUnchanged(ce.Inputs, hasher) {
|
||||
bodies[i] = ce.HTML
|
||||
renderJSs[i] = ce.RenderJS
|
||||
cached[i] = true
|
||||
next.Pages[d.Path] = ce // carry the fingerprint forward
|
||||
continue
|
||||
}
|
||||
}
|
||||
misses = append(misses, renderJob{idx: i, path: d.Path, component: d.Component, entry: entries[i]})
|
||||
}
|
||||
|
||||
// Second pass (parallel): render the misses, then fingerprint their inputs.
|
||||
results, err := renderMisses(misses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range results {
|
||||
bodies[r.idx] = r.html
|
||||
// Bake the bundled render entry only for dynamic pages — the server
|
||||
// re-runs it with data at request time (ISR), so no esbuild or source
|
||||
// files are needed at runtime.
|
||||
js := ""
|
||||
if defs[r.idx].Dynamic {
|
||||
js = r.js
|
||||
}
|
||||
renderJSs[r.idx] = js
|
||||
next.Pages[r.path] = ssrCacheEntry{
|
||||
EntryHash: pageEntryHash(entries[r.idx], defs[r.idx].Dynamic),
|
||||
Inputs: hashInputs(r.inputs, hasher),
|
||||
HTML: r.html,
|
||||
RenderJS: js,
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the registry in manifest order.
|
||||
var b strings.Builder
|
||||
b.WriteString("// Code generated by cmd/bundle; DO NOT EDIT.\n")
|
||||
b.WriteString("// Source: frontend/src/pages/public/pages.ts\n")
|
||||
b.WriteString("//\n")
|
||||
b.WriteString("// Each html field is the page's data-free SSR skeleton, rendered from its\n")
|
||||
b.WriteString("// Solid component at bundle time. The browser bundle re-renders it on load.\n\n")
|
||||
b.WriteString("package handlers\n\n")
|
||||
b.WriteString("var publicPages = []publicPage{\n")
|
||||
for i, d := range defs {
|
||||
fmt.Fprintf(&b, "\t{route: %q, title: %q, module: %q, component: %q, html: %q, renderJS: %q},\n", goRoute(d.Path), d.Title, d.Module, d.Component, bodies[i], renderJSs[i])
|
||||
status := "rendered"
|
||||
if cached[i] {
|
||||
status = "cached"
|
||||
}
|
||||
fmt.Printf(" %-20s %s (%s, %d bytes)\n", d.Path, d.Component, status, len(bodies[i]))
|
||||
}
|
||||
b.WriteString("}\n")
|
||||
|
||||
if err := os.WriteFile(filepath.Join("internal", "handlers", "public_pages.gen.go"), []byte(b.String()), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saveSSRCache(next)
|
||||
fmt.Printf(" SSR: %d rendered, %d cached in %s\n", len(misses), len(defs)-len(misses), time.Since(start).Round(time.Millisecond))
|
||||
return nil
|
||||
}
|
||||
|
||||
// pageEntryHash keys the render cache. It folds in the page's `dynamic` flag so
|
||||
// toggling ISR on/off re-renders the page (a dynamic page also bakes its render
|
||||
// JS, which an entry-only hash wouldn't notice changed).
|
||||
func pageEntryHash(entry string, dynamic bool) string {
|
||||
if dynamic {
|
||||
return hashString(entry + "\x00dynamic")
|
||||
}
|
||||
return hashString(entry)
|
||||
}
|
||||
|
||||
// ssrEntrySolid builds the goja entry for one page: import its body component,
|
||||
// wrap it in PublicLayout (currentPath = the page's path), render into a detached
|
||||
// DOM-shim root with solid-js/web's render, and serialize. Mirrors the client
|
||||
// takeover (public.tsx), which wraps the same body in the same layout — so the
|
||||
// server markup and post-takeover markup match.
|
||||
//
|
||||
// The entry is written as plain JS using createComponent (what compiled Solid JSX
|
||||
// would emit) rather than JSX, so it needs no transform; the imported .tsx page +
|
||||
// layout ARE Solid-compiled by the Go-native compiler (Plugin).
|
||||
//
|
||||
// esbuild's __ENV_TYPE__ define is not applied to this SSR build, so env.ts
|
||||
// yields "" and the EnvBadge in PublicLayout renders nothing during SSR; the
|
||||
// client takeover bundle carries the baked value on load.
|
||||
func ssrEntrySolid(module, component, currentPath string) string {
|
||||
return fmt.Sprintf("import { render, createComponent } from \"solid-js/web\";\n"+
|
||||
"import { PublicLayout } from \"./frontend/src/pages/public/PublicLayout.tsx\";\n"+
|
||||
"import { %[1]s } from \"./%[2]s\";\n"+
|
||||
"globalThis.__render = function () {\n"+
|
||||
"\tconst root = document.createElement(\"div\");\n"+
|
||||
"\tconst dispose = render(function () {\n"+
|
||||
"\t\treturn createComponent(PublicLayout, { currentPath: %[3]q, get children() { return createComponent(%[1]s, {}); } });\n"+
|
||||
"\t}, root);\n"+
|
||||
"\tconst out = globalThis.__serialize(root);\n"+
|
||||
"\tdispose();\n"+
|
||||
"\treturn out;\n"+
|
||||
"};", component, module, currentPath)
|
||||
}
|
||||
Reference in New Issue
Block a user