Add js web stuff to landing page + documentation
This commit is contained in:
138
go/jsbundler/genroutes.go
Normal file
138
go/jsbundler/genroutes.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user