update HMR to support kjol

This commit is contained in:
2026-07-09 12:49:17 -04:00
parent c8fe422b4e
commit a0c5f73b64
3 changed files with 135 additions and 29 deletions

View File

@@ -28,17 +28,20 @@ import (
// devURLPrefix roots the served source tree: GET /@src/<rel> serves the
// transformed module frontend/src/<rel>.
const (
srcURLPrefix = "/@src/" // transformed source modules
srcURLPrefix = "/@src/" // transformed app source modules
kitURLPrefix = "/@kit/" // transformed shared-kit modules (kjol web tree)
assetURLPrefix = "/@url/" // `?url` shim: a module whose default export is the file URL
fsURLPrefix = "/@fs/" // raw file passthrough (the URL the shim points at)
)
type devServer struct {
hub *hub
frontend string // abs frontend/
srcRoot string // abs frontend/src
vendor string // abs frontend/vendor
output string // abs wwwroot/
hub *hub
frontend string // abs app frontend/
srcRoot string // abs app frontend/src
kitRoot string // abs kjol web (@ui/@kjol root); == frontend in single-tree
genRoot string // abs app generated dir (@appgen)
vendorDirs []string // abs vendor roots (kjol runtime first, then app vendor)
output string // abs wwwroot/
graph *moduleGraph
@@ -55,7 +58,8 @@ type devServer struct {
// and returns the import map (JSON object body) the SPA shell must inline so bare
// specifiers resolve to the single vendored copies. Called only from cmd/server's
// `dev`-tagged shim.
func StartDevHMR(mux *http.ServeMux) (importMap string, err error) {
func StartDevHMR(mux *http.ServeMux, cfg Config) (importMap string, err error) {
Configure(cfg)
d, err := newDevServer()
if err != nil {
return "", err
@@ -83,18 +87,26 @@ func newDevServer() (*devServer, error) {
// the containment/URL math breaks on symlinked trees (e.g. macOS /var→/private/var).
frontendAbs = evalSymlinks(frontendAbs)
outputAbs = evalSymlinks(outputAbs)
absSym := func(p string) string { a, _ := filepath.Abs(p); return evalSymlinks(a) }
var vdirs []string
for _, v := range vendorDirs() {
vdirs = append(vdirs, absSym(v))
}
d := &devServer{
hub: newHub(),
frontend: frontendAbs,
srcRoot: filepath.Join(frontendAbs, "src"),
vendor: filepath.Join(frontendAbs, "vendor"),
output: outputAbs,
hub: newHub(),
frontend: frontendAbs,
srcRoot: filepath.Join(frontendAbs, "src"),
kitRoot: absSym(webRoot()),
genRoot: absSym(genTSDir),
vendorDirs: vdirs,
output: outputAbs,
graph: newModuleGraph(),
vendorCache: map[string][]byte{},
cssTrigger: make(chan struct{}, 1),
}
eps, err := loadVendorManifest(d.vendor)
eps, err := loadVendorManifest(d.vendorDirs)
if err != nil {
return nil, fmt.Errorf("loading vendor manifest: %w", err)
}
@@ -105,6 +117,7 @@ func newDevServer() (*devServer, error) {
func (d *devServer) register(mux *http.ServeMux) {
mux.HandleFunc(srcURLPrefix, d.serveModule)
mux.HandleFunc(kitURLPrefix, d.serveKit)
mux.HandleFunc(vendorURLPrefix, d.serveVendor)
mux.HandleFunc(assetURLPrefix, d.serveAssetURL)
mux.HandleFunc(fsURLPrefix, d.serveFS)
@@ -139,6 +152,72 @@ func (d *devServer) serveModule(w http.ResponseWriter, r *http.Request) {
w.Write(code)
}
// serveKit transforms and serves one shared-kit module (kjol web tree) as ESM.
func (d *devServer) serveKit(w http.ResponseWriter, r *http.Request) {
rel := strings.TrimPrefix(r.URL.Path, kitURLPrefix)
abs := filepath.Join(d.kitRoot, filepath.FromSlash(rel))
if !within(d.kitRoot, abs) {
http.NotFound(w, r)
return
}
if _, statErr := os.Stat(abs); statErr != nil {
http.NotFound(w, r)
return
}
code, err := d.transformModule(abs)
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
if err != nil {
fmt.Fprintf(os.Stderr, "HMR transform %s: %v\n", rel, err)
w.Write([]byte(errorModule(rel, err)))
return
}
w.Write(code)
}
// moduleURLBase maps an absolute module file to its dev URL (no version query):
// app sources → /@src/, shared-kit modules (kjol web) → /@kit/. "" if outside both.
func (d *devServer) moduleURLBase(abs string) string {
if within(d.srcRoot, abs) {
return srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, abs))
}
if within(d.kitRoot, abs) {
return kitURLPrefix + filepath.ToSlash(mustRel(d.kitRoot, abs))
}
return ""
}
// moduleID is a stable identifier for solid-refresh's HMR registry (survives
// recompiles), tree-aware so app and kit modules never collide.
func (d *devServer) moduleID(abs string) string {
if within(d.srcRoot, abs) {
return filepath.ToSlash(mustRel(d.srcRoot, abs))
}
if within(d.kitRoot, abs) {
return "@kit/" + filepath.ToSlash(mustRel(d.kitRoot, abs))
}
return filepath.ToSlash(abs)
}
// resolveAliasSpec resolves an @ui/@kjol/@appgen import to an absolute file, or ""
// if spec is not one of those aliases (e.g. a vendored @scope/pkg like @solidjs/router).
func (d *devServer) resolveAliasSpec(spec string) string {
roots := aliasRoots()
slash := strings.IndexByte(spec, '/')
if slash < 0 {
return ""
}
prefix, rest := spec[:slash], spec[slash+1:]
root, ok := roots[prefix]
if !ok {
return ""
}
if p, ok := resolveAlias(root, rest); ok {
return evalSymlinks(p)
}
return ""
}
// transformModule runs esbuild over a single entry file with all imports marked
// external (so only this file is transformed) and rewritten to dev URLs, then
// prepends the import.meta.hot bootstrap. .tsx/.jsx are Solid+refresh compiled
@@ -174,7 +253,7 @@ func (d *devServer) transformModule(abs string) ([]byte, error) {
return nil, fmt.Errorf("no output")
}
selfURL := srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, abs))
selfURL := d.moduleURLBase(abs)
prelude := "import { createHotContext as __createHotContext } from \"/@hmr/client\";\n" +
"import.meta.hot = __createHotContext(" + jsString(selfURL) + ");\n"
return append([]byte(prelude), result.OutputFiles[0].Contents...), nil
@@ -192,7 +271,7 @@ func (d *devServer) solidRefreshLoadPlugin() esbuild.Plugin {
if err != nil {
return esbuild.OnLoadResult{}, err
}
id := filepath.ToSlash(mustRel(d.srcRoot, a.Path))
id := d.moduleID(a.Path)
out, err := CompileDev(string(data), id)
if err != nil {
return esbuild.OnLoadResult{}, err
@@ -233,22 +312,43 @@ func (d *devServer) externalRewritePlugin() esbuild.Plugin {
}
}
// @ui/@kjol/@appgen → resolve into the kjol web tree (or the app's
// generated dir) and rewrite to the /@src/ or /@kit/ URL that serves it.
if strings.HasPrefix(spec, "@") {
if resolved := d.resolveAliasSpec(spec); resolved != "" {
if url := d.moduleURLBase(resolved); url != "" {
if a.Importer != "" {
d.graph.recordEdge(a.Importer, resolved)
}
if v := d.graph.versionOf(resolved); v > 0 {
url += fmt.Sprintf("?t=%d", v)
}
return esbuild.OnResolveResult{Path: url, External: true}, nil
}
}
// Not an @ui/@kjol/@appgen alias (e.g. @solidjs/router): fall through
// to the bare-specifier / import-map path below.
}
// Bare specifier → import map (single vendored copy).
if !strings.HasPrefix(spec, ".") && !filepath.IsAbs(spec) {
return esbuild.OnResolveResult{Path: spec, External: true}, nil
}
// Relative/absolute → resolve to the real source file and rewrite to
// its /@src/ URL, recording the importer→import edge for HMR.
// Relative/absolute → resolve to the real source file (app OR kit tree)
// and rewrite to its /@src/ or /@kit/ URL, recording the edge for HMR.
resolved := resolveSource(a.ResolveDir, spec)
if resolved == "" || !within(d.srcRoot, resolved) {
// Unknown relative import: leave it and let the browser 404 loudly.
url := ""
if resolved != "" {
url = d.moduleURLBase(resolved)
}
if url == "" {
// Unknown import: leave it and let the browser 404 loudly.
return esbuild.OnResolveResult{Path: spec, External: true}, nil
}
if a.Importer != "" {
d.graph.recordEdge(a.Importer, resolved)
}
url := srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, resolved))
if v := d.graph.versionOf(resolved); v > 0 {
url += fmt.Sprintf("?t=%d", v)
}
@@ -268,9 +368,14 @@ func (d *devServer) resolveAsset(resolveDir, spec string) string {
if strings.HasPrefix(spec, ".") || filepath.IsAbs(spec) {
abs = filepath.Join(resolveDir, spec)
} else {
abs = filepath.Join(d.vendor, filepath.FromSlash(spec)) // vendored subpath
for _, vd := range d.vendorDirs { // vendored subpath
if cand := filepath.Join(vd, filepath.FromSlash(spec)); fileExists(cand) {
abs = cand
break
}
}
}
if !within(d.frontend, abs) || !fileExists(abs) {
if abs == "" || !within(d.frontend, abs) || !fileExists(abs) {
return ""
}
return assetURLPrefix + filepath.ToSlash(mustRel(d.frontend, abs))