117 lines
4.0 KiB
Go
117 lines
4.0 KiB
Go
//go:build dev
|
|
|
|
package webbundler
|
|
|
|
// 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
|
|
})
|
|
},
|
|
}
|
|
}
|