update wasmgen
This commit is contained in:
214
go/cmd/wasmgen/main.go
Normal file
214
go/cmd/wasmgen/main.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// Command wasmgen preprocesses component directives into glue code so that
|
||||
// server-component calls look identical to client-component calls, and routes
|
||||
// can opt into SSR with a tag.
|
||||
//
|
||||
// Directives (as doc comments on functions in the target package):
|
||||
//
|
||||
// //gowasm:page <path> [static] [layout=<name>]
|
||||
// a page factory `func(Deps) func() *vdom.VNode`.
|
||||
// `static` => the server SSRs that route;
|
||||
// `layout=` => wrap it in a named //gowasm:layout.
|
||||
// //gowasm:layout <name> a layout `func(Deps, *vdom.VNode) *vdom.VNode`.
|
||||
// //gowasm:server a server component `func() func() *vdom.VNode`.
|
||||
//
|
||||
// It emits three files in the package:
|
||||
// - routes.gen.go (neutral) Routes(Deps) + StaticPaths + RouteLayout/LayoutFor
|
||||
// - server.gen.go (native) init() registering each server component
|
||||
// - client.gen.go (wasm) a client stub per server component that
|
||||
// mounts it over /rsc
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type page struct {
|
||||
Func string
|
||||
Path string
|
||||
Static bool
|
||||
Layout string // name of the //gowasm:layout wrapper (empty => default)
|
||||
}
|
||||
|
||||
type layoutDecl struct {
|
||||
Name string
|
||||
Func string
|
||||
}
|
||||
|
||||
type genData struct {
|
||||
Pages []page
|
||||
Servers []string
|
||||
Layouts []layoutDecl
|
||||
DefaultLayout string // func name used when a page declares no layout
|
||||
}
|
||||
|
||||
func main() {
|
||||
dir := "app"
|
||||
if len(os.Args) > 1 {
|
||||
dir = os.Args[1]
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool {
|
||||
return !strings.HasSuffix(fi.Name(), ".gen.go")
|
||||
}, parser.ParseComments)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "wasmgen: parse:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var data genData
|
||||
for _, pkg := range pkgs {
|
||||
for _, file := range pkg.Files {
|
||||
for _, decl := range file.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Doc == nil {
|
||||
continue
|
||||
}
|
||||
for _, c := range fn.Doc.List {
|
||||
line := strings.TrimSpace(strings.TrimPrefix(c.Text, "//"))
|
||||
switch {
|
||||
case strings.HasPrefix(line, "gowasm:page "):
|
||||
fields := strings.Fields(line[len("gowasm:page "):])
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
p := page{Func: fn.Name.Name, Path: fields[0]}
|
||||
for _, f := range fields[1:] {
|
||||
switch {
|
||||
case f == "static":
|
||||
p.Static = true
|
||||
case strings.HasPrefix(f, "layout="):
|
||||
p.Layout = strings.TrimPrefix(f, "layout=")
|
||||
}
|
||||
}
|
||||
data.Pages = append(data.Pages, p)
|
||||
case strings.HasPrefix(line, "gowasm:layout "):
|
||||
fields := strings.Fields(line[len("gowasm:layout "):])
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
data.Layouts = append(data.Layouts, layoutDecl{Name: fields[0], Func: fn.Name.Name})
|
||||
case line == "gowasm:server":
|
||||
data.Servers = append(data.Servers, fn.Name.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(data.Pages, func(i, j int) bool { return data.Pages[i].Path < data.Pages[j].Path })
|
||||
sort.Strings(data.Servers)
|
||||
sort.Slice(data.Layouts, func(i, j int) bool { return data.Layouts[i].Name < data.Layouts[j].Name })
|
||||
|
||||
// Default layout: prefer one literally named "app", else the first declared.
|
||||
var defaultName string
|
||||
if len(data.Layouts) > 0 {
|
||||
data.DefaultLayout, defaultName = data.Layouts[0].Func, data.Layouts[0].Name
|
||||
for _, l := range data.Layouts {
|
||||
if l.Name == "app" {
|
||||
data.DefaultLayout, defaultName = l.Func, l.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range data.Pages {
|
||||
if data.Pages[i].Layout == "" {
|
||||
data.Pages[i].Layout = defaultName
|
||||
}
|
||||
}
|
||||
|
||||
write(filepath.Join(dir, "routes.gen.go"), routesTmpl, data)
|
||||
write(filepath.Join(dir, "server.gen.go"), serverTmpl, data)
|
||||
write(filepath.Join(dir, "client.gen.go"), clientTmpl, data)
|
||||
|
||||
fmt.Printf("wasmgen: %d page(s), %d server component(s)\n", len(data.Pages), len(data.Servers))
|
||||
}
|
||||
|
||||
func write(path, tmpl string, data genData) {
|
||||
var b strings.Builder
|
||||
if err := template.Must(template.New("g").Parse(tmpl)).Execute(&b, data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
src, err := format.Source([]byte(b.String()))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "wasmgen: format %s: %v\n---\n%s\n", path, err, b.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
// Only write when changed, so re-running codegen doesn't churn mtimes (which
|
||||
// would make the dev server's file watcher loop).
|
||||
if existing, err := os.ReadFile(path); err == nil && string(existing) == string(src) {
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(path, src, 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const routesTmpl = `// Code generated by wasmgen. DO NOT EDIT.
|
||||
package app
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
// Routes maps each //gowasm:page path to its instantiated render function.
|
||||
func Routes(d Deps) map[string]func() *vdom.VNode {
|
||||
return map[string]func() *vdom.VNode{
|
||||
{{range .Pages}} "{{.Path}}": {{.Func}}(d),
|
||||
{{end}} }
|
||||
}
|
||||
|
||||
// StaticPaths are the routes the server pre-renders (SSR); others render client-side.
|
||||
var StaticPaths = map[string]bool{
|
||||
{{range .Pages}}{{if .Static}} "{{.Path}}": true,
|
||||
{{end}}{{end}}}
|
||||
|
||||
// RouteLayout maps each route to the name of the layout that wraps it.
|
||||
var RouteLayout = map[string]string{
|
||||
{{range .Pages}} "{{.Path}}": "{{.Layout}}",
|
||||
{{end}}}
|
||||
|
||||
// LayoutFor wraps a page's content in the layout declared for its route.
|
||||
func LayoutFor(d Deps, path string, content *vdom.VNode) *vdom.VNode {
|
||||
{{if .Layouts}} switch RouteLayout[path] {
|
||||
{{range .Layouts}} case "{{.Name}}":
|
||||
return {{.Func}}(d, content)
|
||||
{{end}} }
|
||||
return {{.DefaultLayout}}(d, content)
|
||||
{{else}} return content
|
||||
{{end}}}
|
||||
`
|
||||
|
||||
const serverTmpl = `// Code generated by wasmgen. DO NOT EDIT.
|
||||
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package app
|
||||
|
||||
import "kjol/rsc"
|
||||
|
||||
func init() {
|
||||
{{range .Servers}} rsc.Register("{{.}}", {{.}})
|
||||
{{end}}}
|
||||
`
|
||||
|
||||
const clientTmpl = `// Code generated by wasmgen. DO NOT EDIT.
|
||||
|
||||
//go:build js && wasm
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"kjol/rsc"
|
||||
"kjol/vdom"
|
||||
)
|
||||
|
||||
{{range .Servers}}// {{.}} is a generated client stub for the server component of the same name.
|
||||
func {{.}}() func() *vdom.VNode { return rsc.Mount("{{.}}") }
|
||||
{{end}}`
|
||||
Reference in New Issue
Block a user