139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
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 + <title> +
|
|
// 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 → <title> (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<string, () => JSXElement> = {\n")
|
|
b.WriteString(entries.String())
|
|
b.WriteString("};\n")
|
|
b.WriteString("\n// <title> 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<string, string> = {\n")
|
|
b.WriteString(titles.String())
|
|
b.WriteString("};\n")
|
|
|
|
return os.WriteFile(filepath.Join(frontendDir, "src", "pages", "public", "routes.gen.ts"), []byte(b.String()), 0644)
|
|
}
|