package jsbundler
// Public-route code generation. The single source of truth is the TypeScript
// manifest frontend/src/pages/public/pages.ts. This reads it (via esbuild +
// goja, so it's real evaluation, not fragile text parsing) and generates:
//
// - internal/handlers/public_pages.gen.go Go registry: routes +
+
// the page's pre-rendered body (baked here, see genssr.go: writeGoRegistry)
// - frontend/src/pages/public/routes.gen.ts client router maps: route → body
// component (publicRoutes) and route → (publicTitles)
//
// Generated files are committed like the other build artifacts; regenerate by
// running the bundler.
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/dop251/goja"
esbuild "github.com/evanw/esbuild/pkg/api"
)
const pagesManifest = "src/pages/public/pages.ts" // relative to frontendDir
type pageDef struct {
Path string `json:"path"`
Module string `json:"module"` // relative to frontend/src
Component string `json:"component"`
Title string `json:"title"`
Dynamic bool `json:"dynamic"` // ISR page: also bake its render JS for request-time data rendering
}
func generatePublicRoutes() error {
defs, err := loadPageDefs()
if err != nil {
return fmt.Errorf("loading page manifest: %w", err)
}
if err := writeGoRegistry(defs); err != nil {
return fmt.Errorf("writing Go registry: %w", err)
}
if err := writeClientRoutes(defs); err != nil {
return fmt.Errorf("writing client routes: %w", err)
}
fmt.Printf(" Public routes: %d page(s) generated\n", len(defs))
return nil
}
// loadPageDefs bundles and evaluates the TS manifest to get the page list.
func loadPageDefs() ([]pageDef, error) {
absCwd, err := filepath.Abs(".")
if err != nil {
return nil, err
}
pagesAbs := filepath.ToSlash(filepath.Join(absCwd, frontendDir, pagesManifest))
entry := fmt.Sprintf("import { publicPages } from %q;\nglobalThis.__PAGES__ = JSON.stringify(publicPages);", pagesAbs)
res := esbuild.Build(esbuild.BuildOptions{
Stdin: &esbuild.StdinOptions{
Contents: entry,
ResolveDir: absCwd,
Sourcefile: "pages-manifest.js",
Loader: esbuild.LoaderTS,
},
Bundle: true,
Format: esbuild.FormatIIFE,
Target: esbuild.ES2017,
Platform: esbuild.PlatformNeutral,
LogLevel: esbuild.LogLevelSilent,
Write: false,
})
if len(res.Errors) > 0 {
msgs := esbuild.FormatMessages(res.Errors, esbuild.FormatMessagesOptions{})
return nil, fmt.Errorf("esbuild: %s", strings.Join(msgs, "\n"))
}
vm := goja.New()
if _, err := vm.RunString(string(res.OutputFiles[0].Contents)); err != nil {
return nil, err
}
raw := vm.Get("__PAGES__")
if raw == nil {
return nil, fmt.Errorf("manifest did not export publicPages")
}
var defs []pageDef
if err := json.Unmarshal([]byte(raw.String()), &defs); err != nil {
return nil, err
}
return defs, nil
}
// goRoute maps a URL pathname to a Go ServeMux pattern. Root needs "/{$}" so
// it matches exactly instead of as a catch-all subtree.
func goRoute(pathname string) string {
if pathname == "/" {
return "/{$}"
}
return pathname
}
func writeClientRoutes(defs []pageDef) error {
// Dedupe imports by component name (a component may back several routes).
seen := map[string]bool{}
var imports, entries, titles strings.Builder
for _, d := range defs {
if !seen[d.Component] {
seen[d.Component] = true
rel, err := filepath.Rel("pages/public", filepath.FromSlash(d.Module))
if err != nil {
return err
}
imp := "./" + filepath.ToSlash(rel)
fmt.Fprintf(&imports, "import { %s } from %q;\n", d.Component, imp)
}
fmt.Fprintf(&entries, " %q: %s,\n", d.Path, d.Component)
fmt.Fprintf(&titles, " %q: %q,\n", d.Path, d.Title)
}
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\n")
b.WriteString("import { JSXElement } from \"solid-js\";\n")
b.WriteString(imports.String())
b.WriteString("\n// Body component for each public route, keyed by URL pathname. The client\n")
b.WriteString("// router (public.ts) renders these when navigating without a full reload.\n")
b.WriteString("export const publicRoutes: Record JSXElement> = {\n")
b.WriteString(entries.String())
b.WriteString("};\n")
b.WriteString("\n// for each public route, applied by the client router on navigation\n")
b.WriteString("// (the first load gets its title from the server-rendered shell).\n")
b.WriteString("export const publicTitles: Record = {\n")
b.WriteString(titles.String())
b.WriteString("};\n")
return os.WriteFile(filepath.Join(frontendDir, "src", "pages", "public", "routes.gen.ts"), []byte(b.String()), 0644)
}