Files
kjol/go/bundler/vendor_plugins.go

112 lines
4.3 KiB
Go

package bundler
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
esbuild "github.com/evanw/esbuild/pkg/api"
)
// VendorManifest mirrors frontend/vendor/vendor.json.
type VendorManifest struct {
Entrypoints map[string]string `json:"entrypoints"`
}
// loadVendorManifest reads frontend/vendor/vendor.json — the single place that
// declares the exact entrypoint file for every bundled vendored package.
func loadVendorManifest(vendorDir string) (map[string]string, error) {
data, err := os.ReadFile(filepath.Join(vendorDir, "vendor.json"))
if err != nil {
return nil, err
}
var m VendorManifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parsing vendor.json: %w", err)
}
return m.Entrypoints, nil
}
// vendorManifestPlugin resolves bare imports from the vendor manifest: a package
// listed in vendor.json resolves to its pinned entrypoint file, bypassing the
// package's own `exports` map (which mis-resolves — e.g. solid-js's bare core to
// its SSR build, where effects are no-ops). Subpath imports of a vendored package
// (e.g. pdfjs-dist/build/pdf.worker.min.mjs) resolve to the real file via
// NodePaths; anything not vendored is left external for the import map.
func vendorManifestPlugin(vendorDir string, entrypoints map[string]string) esbuild.Plugin {
abs := make(map[string]string, len(entrypoints))
for spec, rel := range entrypoints {
p, _ := filepath.Abs(filepath.Join(vendorDir, filepath.FromSlash(rel)))
abs[spec] = filepath.ToSlash(p)
}
return esbuild.Plugin{
Name: "vendor-manifest",
Setup: func(build esbuild.PluginBuild) {
// Bare specifiers only (relative/absolute/entry/?suffixed are skipped).
build.OnResolve(esbuild.OnResolveOptions{Filter: `^[^./]`}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
if args.Kind == esbuild.ResolveEntryPoint || filepath.IsAbs(args.Path) || strings.HasPrefix(args.Path, ".") || strings.ContainsRune(args.Path, '?') {
return esbuild.OnResolveResult{}, nil
}
if target, ok := abs[args.Path]; ok {
return esbuild.OnResolveResult{Path: target}, nil // pinned entrypoint
}
if vendorHasPackage(vendorDir, args.Path) {
return esbuild.OnResolveResult{}, nil // subpath of a vendored pkg -> NodePaths
}
return esbuild.OnResolveResult{External: true}, nil // not vendored -> import map
})
},
}
}
// vendorHasPackage reports whether a bare specifier maps to a package directory
// under vendorDir, honouring @scope/name.
func vendorHasPackage(vendorDir, spec string) bool {
parts := strings.Split(spec, "/")
pkg := parts[0]
if strings.HasPrefix(pkg, "@") && len(parts) > 1 {
pkg = pkg + "/" + parts[1]
}
info, err := os.Stat(filepath.Join(vendorDir, filepath.FromSlash(pkg)))
return err == nil && info.IsDir()
}
// assetURLPlugin implements a generic `import url from "<path>?url"`: the
// referenced file is emitted as a build asset (via esbuild's file loader,
// controlled by BuildOptions.AssetNames/PublicPath) and the import resolves to
// its served URL. Used for resources that must stay separate files rather than
// be inlined — e.g. the pdfjs Web Worker, which relies on import.meta.url and so
// can't run from a Blob. The asset path lives in the app code that needs it, not
// in the bundler.
func assetURLPlugin() esbuild.Plugin {
const ns = "asset-url"
return esbuild.Plugin{
Name: "asset-url",
Setup: func(build esbuild.PluginBuild) {
build.OnResolve(esbuild.OnResolveOptions{Filter: `\?url$`}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
real := strings.TrimSuffix(args.Path, "?url")
r := build.Resolve(real, esbuild.ResolveOptions{
ResolveDir: args.ResolveDir,
Importer: args.Importer,
Kind: args.Kind,
})
if len(r.Errors) > 0 {
return esbuild.OnResolveResult{}, fmt.Errorf("asset-url: cannot resolve %q: %s", real, r.Errors[0].Text)
}
return esbuild.OnResolveResult{Path: r.Path, Namespace: ns}, nil
})
build.OnLoad(esbuild.OnLoadOptions{Filter: `.*`, Namespace: ns}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
data, err := os.ReadFile(args.Path)
if err != nil {
return esbuild.OnLoadResult{}, err
}
contents := string(data)
loader := esbuild.LoaderFile
return esbuild.OnLoadResult{Contents: &contents, Loader: loader}, nil
})
},
}
}