update HMR to support kjol
This commit is contained in:
@@ -28,16 +28,19 @@ 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
|
||||
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"),
|
||||
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))
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
@@ -71,17 +70,16 @@ func (d *devServer) serveVendor(w http.ResponseWriter, r *http.Request) {
|
||||
// re-externalizing the OTHER vendored specifiers so they resolve (once) through
|
||||
// the import map. Mirrors the prod vendor resolution (NodePaths + development
|
||||
// condition) so the dev copy matches the shipped one.
|
||||
func (d *devServer) bundleVendor(relFile string) ([]byte, error) {
|
||||
entry := filepath.Join(d.vendor, filepath.FromSlash(relFile))
|
||||
func (d *devServer) bundleVendor(absFile string) ([]byte, error) {
|
||||
result := esbuild.Build(esbuild.BuildOptions{
|
||||
EntryPoints: []string{entry},
|
||||
EntryPoints: []string{absFile},
|
||||
Bundle: true,
|
||||
Write: false,
|
||||
Format: esbuild.FormatESModule,
|
||||
Target: esbuild.ES2022,
|
||||
Platform: esbuild.PlatformBrowser,
|
||||
Conditions: []string{"development"},
|
||||
NodePaths: []string{d.vendor},
|
||||
NodePaths: d.vendorDirs,
|
||||
Sourcemap: esbuild.SourceMapInline,
|
||||
LogLevel: esbuild.LogLevelSilent,
|
||||
Plugins: []esbuild.Plugin{d.vendorSharedExternalPlugin()},
|
||||
|
||||
@@ -26,6 +26,9 @@ 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 web kit + styles
|
||||
roots = append(roots, d.kitRoot)
|
||||
}
|
||||
mtimes := map[string]time.Time{}
|
||||
d.scan(roots, mtimes, nil) // seed: record current state, emit nothing
|
||||
|
||||
@@ -117,7 +120,7 @@ func (d *devServer) handleChanges(changed []string) {
|
||||
|
||||
// hmrJS turns one changed source module into a hot update or a full reload.
|
||||
func (d *devServer) hmrJS(abs string) {
|
||||
rel := filepath.ToSlash(mustRel(d.srcRoot, abs))
|
||||
rel := d.moduleID(abs)
|
||||
boundaries, version, reload := d.graph.invalidate(abs)
|
||||
if reload {
|
||||
fmt.Printf("[hmr] full reload (%s)\n", rel)
|
||||
@@ -131,7 +134,7 @@ func (d *devServer) hmrJS(abs string) {
|
||||
updates := make([]hmrUpdate, 0, len(boundaries))
|
||||
for _, b := range boundaries {
|
||||
updates = append(updates, hmrUpdate{
|
||||
Path: srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, b)),
|
||||
Path: d.moduleURLBase(b),
|
||||
Timestamp: version,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user