update HMR to support kjol
This commit is contained in:
@@ -28,17 +28,20 @@ import (
|
|||||||
// devURLPrefix roots the served source tree: GET /@src/<rel> serves the
|
// devURLPrefix roots the served source tree: GET /@src/<rel> serves the
|
||||||
// transformed module frontend/src/<rel>.
|
// transformed module frontend/src/<rel>.
|
||||||
const (
|
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
|
assetURLPrefix = "/@url/" // `?url` shim: a module whose default export is the file URL
|
||||||
fsURLPrefix = "/@fs/" // raw file passthrough (the URL the shim points at)
|
fsURLPrefix = "/@fs/" // raw file passthrough (the URL the shim points at)
|
||||||
)
|
)
|
||||||
|
|
||||||
type devServer struct {
|
type devServer struct {
|
||||||
hub *hub
|
hub *hub
|
||||||
frontend string // abs frontend/
|
frontend string // abs app frontend/
|
||||||
srcRoot string // abs frontend/src
|
srcRoot string // abs app frontend/src
|
||||||
vendor string // abs frontend/vendor
|
kitRoot string // abs kjol web (@ui/@kjol root); == frontend in single-tree
|
||||||
output string // abs wwwroot/
|
genRoot string // abs app generated dir (@appgen)
|
||||||
|
vendorDirs []string // abs vendor roots (kjol runtime first, then app vendor)
|
||||||
|
output string // abs wwwroot/
|
||||||
|
|
||||||
graph *moduleGraph
|
graph *moduleGraph
|
||||||
|
|
||||||
@@ -55,7 +58,8 @@ type devServer struct {
|
|||||||
// and returns the import map (JSON object body) the SPA shell must inline so bare
|
// 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
|
// specifiers resolve to the single vendored copies. Called only from cmd/server's
|
||||||
// `dev`-tagged shim.
|
// `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()
|
d, err := newDevServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -83,18 +87,26 @@ func newDevServer() (*devServer, error) {
|
|||||||
// the containment/URL math breaks on symlinked trees (e.g. macOS /var→/private/var).
|
// the containment/URL math breaks on symlinked trees (e.g. macOS /var→/private/var).
|
||||||
frontendAbs = evalSymlinks(frontendAbs)
|
frontendAbs = evalSymlinks(frontendAbs)
|
||||||
outputAbs = evalSymlinks(outputAbs)
|
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{
|
d := &devServer{
|
||||||
hub: newHub(),
|
hub: newHub(),
|
||||||
frontend: frontendAbs,
|
frontend: frontendAbs,
|
||||||
srcRoot: filepath.Join(frontendAbs, "src"),
|
srcRoot: filepath.Join(frontendAbs, "src"),
|
||||||
vendor: filepath.Join(frontendAbs, "vendor"),
|
kitRoot: absSym(webRoot()),
|
||||||
output: outputAbs,
|
genRoot: absSym(genTSDir),
|
||||||
|
vendorDirs: vdirs,
|
||||||
|
output: outputAbs,
|
||||||
graph: newModuleGraph(),
|
graph: newModuleGraph(),
|
||||||
vendorCache: map[string][]byte{},
|
vendorCache: map[string][]byte{},
|
||||||
cssTrigger: make(chan struct{}, 1),
|
cssTrigger: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
eps, err := loadVendorManifest(d.vendor)
|
eps, err := loadVendorManifest(d.vendorDirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading vendor manifest: %w", err)
|
return nil, fmt.Errorf("loading vendor manifest: %w", err)
|
||||||
}
|
}
|
||||||
@@ -105,6 +117,7 @@ func newDevServer() (*devServer, error) {
|
|||||||
|
|
||||||
func (d *devServer) register(mux *http.ServeMux) {
|
func (d *devServer) register(mux *http.ServeMux) {
|
||||||
mux.HandleFunc(srcURLPrefix, d.serveModule)
|
mux.HandleFunc(srcURLPrefix, d.serveModule)
|
||||||
|
mux.HandleFunc(kitURLPrefix, d.serveKit)
|
||||||
mux.HandleFunc(vendorURLPrefix, d.serveVendor)
|
mux.HandleFunc(vendorURLPrefix, d.serveVendor)
|
||||||
mux.HandleFunc(assetURLPrefix, d.serveAssetURL)
|
mux.HandleFunc(assetURLPrefix, d.serveAssetURL)
|
||||||
mux.HandleFunc(fsURLPrefix, d.serveFS)
|
mux.HandleFunc(fsURLPrefix, d.serveFS)
|
||||||
@@ -139,6 +152,72 @@ func (d *devServer) serveModule(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(code)
|
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
|
// 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
|
// 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
|
// 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")
|
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" +
|
prelude := "import { createHotContext as __createHotContext } from \"/@hmr/client\";\n" +
|
||||||
"import.meta.hot = __createHotContext(" + jsString(selfURL) + ");\n"
|
"import.meta.hot = __createHotContext(" + jsString(selfURL) + ");\n"
|
||||||
return append([]byte(prelude), result.OutputFiles[0].Contents...), nil
|
return append([]byte(prelude), result.OutputFiles[0].Contents...), nil
|
||||||
@@ -192,7 +271,7 @@ func (d *devServer) solidRefreshLoadPlugin() esbuild.Plugin {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return esbuild.OnLoadResult{}, err
|
return esbuild.OnLoadResult{}, err
|
||||||
}
|
}
|
||||||
id := filepath.ToSlash(mustRel(d.srcRoot, a.Path))
|
id := d.moduleID(a.Path)
|
||||||
out, err := CompileDev(string(data), id)
|
out, err := CompileDev(string(data), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return esbuild.OnLoadResult{}, err
|
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).
|
// Bare specifier → import map (single vendored copy).
|
||||||
if !strings.HasPrefix(spec, ".") && !filepath.IsAbs(spec) {
|
if !strings.HasPrefix(spec, ".") && !filepath.IsAbs(spec) {
|
||||||
return esbuild.OnResolveResult{Path: spec, External: true}, nil
|
return esbuild.OnResolveResult{Path: spec, External: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relative/absolute → resolve to the real source file and rewrite to
|
// Relative/absolute → resolve to the real source file (app OR kit tree)
|
||||||
// its /@src/ URL, recording the importer→import edge for HMR.
|
// and rewrite to its /@src/ or /@kit/ URL, recording the edge for HMR.
|
||||||
resolved := resolveSource(a.ResolveDir, spec)
|
resolved := resolveSource(a.ResolveDir, spec)
|
||||||
if resolved == "" || !within(d.srcRoot, resolved) {
|
url := ""
|
||||||
// Unknown relative import: leave it and let the browser 404 loudly.
|
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
|
return esbuild.OnResolveResult{Path: spec, External: true}, nil
|
||||||
}
|
}
|
||||||
if a.Importer != "" {
|
if a.Importer != "" {
|
||||||
d.graph.recordEdge(a.Importer, resolved)
|
d.graph.recordEdge(a.Importer, resolved)
|
||||||
}
|
}
|
||||||
url := srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, resolved))
|
|
||||||
if v := d.graph.versionOf(resolved); v > 0 {
|
if v := d.graph.versionOf(resolved); v > 0 {
|
||||||
url += fmt.Sprintf("?t=%d", v)
|
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) {
|
if strings.HasPrefix(spec, ".") || filepath.IsAbs(spec) {
|
||||||
abs = filepath.Join(resolveDir, spec)
|
abs = filepath.Join(resolveDir, spec)
|
||||||
} else {
|
} 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 ""
|
||||||
}
|
}
|
||||||
return assetURLPrefix + filepath.ToSlash(mustRel(d.frontend, abs))
|
return assetURLPrefix + filepath.ToSlash(mustRel(d.frontend, abs))
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
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
|
// re-externalizing the OTHER vendored specifiers so they resolve (once) through
|
||||||
// the import map. Mirrors the prod vendor resolution (NodePaths + development
|
// the import map. Mirrors the prod vendor resolution (NodePaths + development
|
||||||
// condition) so the dev copy matches the shipped one.
|
// condition) so the dev copy matches the shipped one.
|
||||||
func (d *devServer) bundleVendor(relFile string) ([]byte, error) {
|
func (d *devServer) bundleVendor(absFile string) ([]byte, error) {
|
||||||
entry := filepath.Join(d.vendor, filepath.FromSlash(relFile))
|
|
||||||
result := esbuild.Build(esbuild.BuildOptions{
|
result := esbuild.Build(esbuild.BuildOptions{
|
||||||
EntryPoints: []string{entry},
|
EntryPoints: []string{absFile},
|
||||||
Bundle: true,
|
Bundle: true,
|
||||||
Write: false,
|
Write: false,
|
||||||
Format: esbuild.FormatESModule,
|
Format: esbuild.FormatESModule,
|
||||||
Target: esbuild.ES2022,
|
Target: esbuild.ES2022,
|
||||||
Platform: esbuild.PlatformBrowser,
|
Platform: esbuild.PlatformBrowser,
|
||||||
Conditions: []string{"development"},
|
Conditions: []string{"development"},
|
||||||
NodePaths: []string{d.vendor},
|
NodePaths: d.vendorDirs,
|
||||||
Sourcemap: esbuild.SourceMapInline,
|
Sourcemap: esbuild.SourceMapInline,
|
||||||
LogLevel: esbuild.LogLevelSilent,
|
LogLevel: esbuild.LogLevelSilent,
|
||||||
Plugins: []esbuild.Plugin{d.vendorSharedExternalPlugin()},
|
Plugins: []esbuild.Plugin{d.vendorSharedExternalPlugin()},
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ func (d *devServer) watch() {
|
|||||||
go d.cssLoop()
|
go d.cssLoop()
|
||||||
|
|
||||||
roots := []string{d.srcRoot, filepath.Join(d.frontend, "css")}
|
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{}
|
mtimes := map[string]time.Time{}
|
||||||
d.scan(roots, mtimes, nil) // seed: record current state, emit nothing
|
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.
|
// hmrJS turns one changed source module into a hot update or a full reload.
|
||||||
func (d *devServer) hmrJS(abs string) {
|
func (d *devServer) hmrJS(abs string) {
|
||||||
rel := filepath.ToSlash(mustRel(d.srcRoot, abs))
|
rel := d.moduleID(abs)
|
||||||
boundaries, version, reload := d.graph.invalidate(abs)
|
boundaries, version, reload := d.graph.invalidate(abs)
|
||||||
if reload {
|
if reload {
|
||||||
fmt.Printf("[hmr] full reload (%s)\n", rel)
|
fmt.Printf("[hmr] full reload (%s)\n", rel)
|
||||||
@@ -131,7 +134,7 @@ func (d *devServer) hmrJS(abs string) {
|
|||||||
updates := make([]hmrUpdate, 0, len(boundaries))
|
updates := make([]hmrUpdate, 0, len(boundaries))
|
||||||
for _, b := range boundaries {
|
for _, b := range boundaries {
|
||||||
updates = append(updates, hmrUpdate{
|
updates = append(updates, hmrUpdate{
|
||||||
Path: srcURLPrefix + filepath.ToSlash(mustRel(d.srcRoot, b)),
|
Path: d.moduleURLBase(b),
|
||||||
Timestamp: version,
|
Timestamp: version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user