Add js web stuff to landing page + documentation
This commit is contained in:
116
go/jsbundler/hmr_vendor.go
Normal file
116
go/jsbundler/hmr_vendor.go
Normal file
@@ -0,0 +1,116 @@
|
||||
//go:build dev
|
||||
|
||||
package jsbundler
|
||||
|
||||
// Vendor handling for the dev server. Bare specifiers (solid-js, @solidjs/router,
|
||||
// solid-refresh, …) are served as single pre-bundled ESM files under /@vendor/,
|
||||
// wired up by an import map the SPA shell inlines. Because each specifier maps to
|
||||
// exactly one URL, and every vendor bundle re-externalizes the OTHER vendored
|
||||
// specifiers (rather than inlining them), solid-js stays a single runtime
|
||||
// instance across the whole app — the invariant the prod bundler also guards.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||
)
|
||||
|
||||
const vendorURLPrefix = "/@vendor/"
|
||||
|
||||
// buildImportMap returns the JSON `{"imports": {...}}` body the SPA shell inlines
|
||||
// in <script type="importmap">, mapping every vendored bare specifier to its
|
||||
// /@vendor/ URL.
|
||||
func (d *devServer) buildImportMap() string {
|
||||
imports := make(map[string]string, len(d.vendorEntrypoints))
|
||||
for spec := range d.vendorEntrypoints {
|
||||
imports[spec] = vendorURLPrefix + spec + ".js"
|
||||
}
|
||||
body, err := json.MarshalIndent(map[string]any{"imports": imports}, "", " ")
|
||||
if err != nil {
|
||||
return `{"imports":{}}`
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
// serveVendor serves a pre-bundled vendored package (lazily built + cached).
|
||||
func (d *devServer) serveVendor(w http.ResponseWriter, r *http.Request) {
|
||||
spec := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, vendorURLPrefix), ".js")
|
||||
relFile, ok := d.vendorEntrypoints[spec]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
d.vendorMu.Lock()
|
||||
bundled, cached := d.vendorCache[r.URL.Path]
|
||||
d.vendorMu.Unlock()
|
||||
|
||||
if !cached {
|
||||
b, err := d.bundleVendor(relFile)
|
||||
if err != nil {
|
||||
http.Error(w, "vendor bundle failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
d.vendorMu.Lock()
|
||||
d.vendorCache[r.URL.Path] = b
|
||||
d.vendorMu.Unlock()
|
||||
bundled = b
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
// Vendor rarely changes; let the browser cache within a session.
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
w.Write(bundled)
|
||||
}
|
||||
|
||||
// bundleVendor bundles one vendored package entrypoint into a single ESM file,
|
||||
// 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(absFile string) ([]byte, error) {
|
||||
result := esbuild.Build(esbuild.BuildOptions{
|
||||
EntryPoints: []string{absFile},
|
||||
Bundle: true,
|
||||
Write: false,
|
||||
Format: esbuild.FormatESModule,
|
||||
Target: esbuild.ES2022,
|
||||
Platform: esbuild.PlatformBrowser,
|
||||
Conditions: []string{"development"},
|
||||
NodePaths: d.vendorDirs,
|
||||
Sourcemap: esbuild.SourceMapInline,
|
||||
LogLevel: esbuild.LogLevelSilent,
|
||||
Plugins: []esbuild.Plugin{d.vendorSharedExternalPlugin()},
|
||||
})
|
||||
if len(result.Errors) > 0 {
|
||||
msgs := esbuild.FormatMessages(result.Errors, esbuild.FormatMessagesOptions{})
|
||||
return nil, fmt.Errorf("%s", strings.Join(msgs, "\n"))
|
||||
}
|
||||
if len(result.OutputFiles) == 0 {
|
||||
return nil, fmt.Errorf("no output")
|
||||
}
|
||||
return result.OutputFiles[0].Contents, nil
|
||||
}
|
||||
|
||||
// vendorSharedExternalPlugin marks a bare import external ONLY when it's an exact
|
||||
// vendored entrypoint (thus resolvable via the import map). That keeps shared
|
||||
// singletons — above all solid-js — as one instance across every vendor bundle,
|
||||
// while deep subpaths and non-vendored transitive deps get bundled in.
|
||||
func (d *devServer) vendorSharedExternalPlugin() esbuild.Plugin {
|
||||
return esbuild.Plugin{
|
||||
Name: "dev-vendor-shared-external",
|
||||
Setup: func(b esbuild.PluginBuild) {
|
||||
b.OnResolve(esbuild.OnResolveOptions{Filter: `^[^./]`}, func(a esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
|
||||
if a.Kind == esbuild.ResolveEntryPoint {
|
||||
return esbuild.OnResolveResult{}, nil
|
||||
}
|
||||
if _, ok := d.vendorEntrypoints[a.Path]; ok {
|
||||
return esbuild.OnResolveResult{Path: a.Path, External: true}, nil
|
||||
}
|
||||
return esbuild.OnResolveResult{}, nil // bundle via NodePaths
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user