Add js web stuff to landing page + documentation
This commit is contained in:
161
go/jsbundler/hmr_watch.go
Normal file
161
go/jsbundler/hmr_watch.go
Normal file
@@ -0,0 +1,161 @@
|
||||
//go:build dev
|
||||
|
||||
package jsbundler
|
||||
|
||||
// The dev watcher: a lightweight mtime poll over frontend/src and frontend/css
|
||||
// (no external dependency). A source-module change is turned into an HMR update
|
||||
// or a full reload via the module graph; a CSS/Tailwind change triggers a
|
||||
// (coalesced) stylesheet rebuild and hot-swap. The public-route manifest is
|
||||
// regenerated on edit, then the page reloads.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// pollInterval is the mtime scan cadence. Kept tight so save→update latency isn't
|
||||
// dominated by detection lag: the WalkDir+Stat over frontend/src+css is sub-ms, so
|
||||
// scanning this often is cheap. (For zero detection lag, swap the poll for native
|
||||
// FS events — deliberately avoided to keep the watcher dependency-free.)
|
||||
const pollInterval = 50 * time.Millisecond
|
||||
|
||||
func (d *devServer) watch() {
|
||||
go d.cssLoop()
|
||||
|
||||
roots := []string{d.srcRoot, filepath.Join(d.frontend, "css")}
|
||||
if webDir != "" { // cross-tree: also watch the shared kjol JS kit + styles
|
||||
roots = append(roots, d.kitRoot)
|
||||
}
|
||||
mtimes := map[string]time.Time{}
|
||||
d.scan(roots, mtimes, nil) // seed: record current state, emit nothing
|
||||
|
||||
for {
|
||||
time.Sleep(pollInterval)
|
||||
var changed []string
|
||||
d.scan(roots, mtimes, func(p string) { changed = append(changed, p) })
|
||||
if len(changed) > 0 {
|
||||
d.handleChanges(changed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scan walks roots, calling onchange for every file whose mtime advanced since
|
||||
// the last scan (or is new). mtimes is updated in place.
|
||||
func (d *devServer) scan(roots []string, mtimes map[string]time.Time, onchange func(string)) {
|
||||
for _, root := range roots {
|
||||
filepath.WalkDir(root, func(p string, entry os.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
switch filepath.Ext(p) {
|
||||
case ".ts", ".tsx", ".js", ".jsx", ".css":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
mt := info.ModTime()
|
||||
if prev, ok := mtimes[p]; !ok || mt.After(prev) {
|
||||
mtimes[p] = mt
|
||||
// onchange is nil on the seed pass, so nothing is reported until the
|
||||
// baseline is recorded; afterwards every new/changed file is reported.
|
||||
if onchange != nil {
|
||||
onchange(p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *devServer) handleChanges(changed []string) {
|
||||
pagesManifestAbs := filepath.Join(d.frontend, filepath.FromSlash(pagesManifest))
|
||||
cssDirty := false
|
||||
srcDirty := false
|
||||
|
||||
for _, p := range changed {
|
||||
base := filepath.Base(p)
|
||||
// Generated files are written by the tools below; ignore to avoid loops.
|
||||
if strings.HasSuffix(base, ".gen.ts") || strings.HasSuffix(base, ".gen.go") {
|
||||
continue
|
||||
}
|
||||
|
||||
if p == pagesManifestAbs {
|
||||
// The public-page manifest drives generated route tables — regenerate,
|
||||
// then reload (these pages are live-reload scope, not component HMR).
|
||||
if err := generatePublicRoutes(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[hmr] regenerating public routes: %v\n", err)
|
||||
}
|
||||
d.hub.broadcastJSON(hmrMessage{Type: "full-reload"})
|
||||
return
|
||||
}
|
||||
|
||||
switch filepath.Ext(p) {
|
||||
case ".css":
|
||||
cssDirty = true
|
||||
case ".ts", ".tsx", ".js", ".jsx":
|
||||
d.hmrJS(p)
|
||||
cssDirty = true // a class may have been added/removed
|
||||
srcDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
if cssDirty {
|
||||
select {
|
||||
case d.cssTrigger <- struct{}{}:
|
||||
default: // a rebuild is already queued
|
||||
}
|
||||
}
|
||||
if srcDirty {
|
||||
// Proactively re-render the public pages' SSR so the no-JS ("static")
|
||||
// version served on the next reload is already current, not rendered lazily.
|
||||
RewarmDevPublicPages()
|
||||
}
|
||||
}
|
||||
|
||||
// hmrJS turns one changed source module into a hot update or a full reload.
|
||||
func (d *devServer) hmrJS(abs string) {
|
||||
rel := d.moduleID(abs)
|
||||
boundaries, version, reload := d.graph.invalidate(abs)
|
||||
if reload {
|
||||
fmt.Printf("[hmr] full reload (%s)\n", rel)
|
||||
d.hub.broadcastJSON(hmrMessage{Type: "full-reload"})
|
||||
return
|
||||
}
|
||||
if len(boundaries) == 0 {
|
||||
return // not on the current page's graph — nothing to do
|
||||
}
|
||||
|
||||
updates := make([]hmrUpdate, 0, len(boundaries))
|
||||
for _, b := range boundaries {
|
||||
updates = append(updates, hmrUpdate{
|
||||
Path: d.moduleURLBase(b),
|
||||
Timestamp: version,
|
||||
})
|
||||
}
|
||||
fmt.Printf("[hmr] update %s -> %d boundary(ies)\n", rel, len(boundaries))
|
||||
d.hub.broadcastJSON(hmrMessage{Type: "update", Updates: updates})
|
||||
}
|
||||
|
||||
// cssLoop serializes and coalesces CSS rebuilds, hot-swapping the stylesheet when
|
||||
// each completes.
|
||||
func (d *devServer) cssLoop() {
|
||||
for range d.cssTrigger {
|
||||
if _, err := bundleSPACSS(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[hmr] CSS rebuild failed: %v\n", err)
|
||||
} else {
|
||||
d.hub.broadcastJSON(hmrMessage{Type: "css-update", Path: "/bundle.min.css"})
|
||||
}
|
||||
// Public pages hot-reload too, off their own stylesheet.
|
||||
if _, err := bundlePublicCSS(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[hmr] public CSS rebuild failed: %v\n", err)
|
||||
} else {
|
||||
d.hub.broadcastJSON(hmrMessage{Type: "css-update", Path: "/public.bundle.min.css"})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user