Ctrl K
ComunidadGuías…

engine/hmr.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

// Hot Module Replacement (HMR) update endpoint.
// React-specific logic lives in react.go.

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/evanw/esbuild/pkg/api"
	"github.com/sirupsen/logrus"
	"github.com/xyproto/algernon/utils"
)

// hmrUpdatePrefix is the URL prefix for the HMR update endpoint
const hmrUpdatePrefix = "/@algernon/hmr/"

// hmrExtensions are the file extensions that the HMR endpoint may serve
var hmrExtensions = []string{".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"}

// servableByHMR reports whether the given file can be served by the HMR endpoint
func servableByHMR(filename string) bool {
	ext := strings.ToLower(filepath.Ext(filename))
	return slices.Contains(hmrExtensions, ext)
}

// HMRUpdateHandler serves a freshly compiled (never cached) version of a
// JSX or JS file so the browser can hot-swap it without reloading the page.
func (ac *Config) HMRUpdateHandler(w http.ResponseWriter, req *http.Request) {
	relPath := strings.TrimPrefix(req.URL.Path, hmrUpdatePrefix)
	if relPath == "" {
		http.Error(w, "missing path", http.StatusBadRequest)
		return
	}
	relPath = filepath.FromSlash(relPath)
	if filepath.IsAbs(relPath) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	// Canonicalize the server root.
	serverRoot, err := filepath.Abs(ac.serverDirOrFilename)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if resolved, err := filepath.EvalSymlinks(serverRoot); err == nil {
		serverRoot = resolved
	}

	// Containment check via filepath.Rel after normalization.
	absPath := filepath.Clean(filepath.Join(serverRoot, relPath))
	if !utils.WithinDir(serverRoot, absPath) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	// Re-check after resolving symlinks inside the root.
	if resolved, err := filepath.EvalSymlinks(absPath); err == nil {
		if !utils.WithinDir(serverRoot, resolved) {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		absPath = resolved
	}

	// Only script files can be hot-swapped. Serving anything else, like .env
	// or .lua, would just be a way to read files.
	if !servableByHMR(absPath) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	src, err := os.ReadFile(absPath)
	if err != nil {
		logrus.Errorf("hmr: cannot read %s: %s", absPath, err)
		http.Error(w, "not found", http.StatusNotFound)
		return
	}

	var data []byte
	if needsBundling(src) {
		data, err = bundleUncached(absPath, src, ac.jsxOptions, ac.autoRefresh)
		if err != nil {
			logrus.Errorf("hmr: compile error for %s: %s", absPath, err)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	} else {
		srcStr := string(src)
		if ac.autoRefresh {
			srcStr = injectRefreshRegistrations(srcStr, filepath.Base(absPath))
		}
		opts := ac.jsxOptions
		opts.Loader = loaderForFile(absPath)
		result := api.Transform(srcStr, opts)
		if len(result.Errors) > 0 {
			msgs := make([]string, len(result.Errors))
			for i, e := range result.Errors {
				msgs[i] = e.Text
			}
			http.Error(w, strings.Join(msgs, "\n"), http.StatusInternalServerError)
			return
		}
		data = result.Code
	}

	w.Header().Set("Content-Type", "text/javascript;charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.Write(data)
}

// bundleUncached runs esbuild on the given source without consulting the cache
func bundleUncached(filename string, srcData []byte, jsxOpts api.TransformOptions, withRefresh bool) ([]byte, error) {
	dir := filepath.Dir(filename)

	loader := loaderForFile(filename)

	contents := string(srcData)
	if withRefresh {
		contents = injectRefreshRegistrations(contents, filepath.Base(filename))
	}

	opts := api.BuildOptions{
		Bundle:            true,
		Platform:          api.PlatformBrowser,
		Format:            api.FormatIIFE,
		MinifyWhitespace:  jsxOpts.MinifyWhitespace,
		MinifyIdentifiers: jsxOpts.MinifyIdentifiers,
		MinifySyntax:      jsxOpts.MinifySyntax,
		Charset:           jsxOpts.Charset,
		Write:             false,
		AbsWorkingDir:     dir,
		LogLevel:          api.LogLevelSilent,
		Stdin: &api.StdinOptions{
			Contents:   contents,
			ResolveDir: dir,
			Sourcefile: filename,
			Loader:     loader,
		},
	}
	if withRefresh {
		opts.Plugins = []api.Plugin{reactRefreshPlugin()}
	}

	result := api.Build(opts)
	if len(result.Errors) > 0 {
		msgs := make([]string, len(result.Errors))
		for i, e := range result.Errors {
			msgs[i] = e.Text
		}
		return nil, fmt.Errorf("bundle %s: %s", filepath.Base(filename), strings.Join(msgs, "; "))
	}
	if len(result.OutputFiles) == 0 {
		return nil, fmt.Errorf("bundle %s: no output produced", filepath.Base(filename))
	}
	return result.OutputFiles[0].Contents, nil
}