128 lines
4.7 KiB
Go
128 lines
4.7 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 and merges vendor.json from every vendor dir (kjol
|
|
// runtime first, app vendor second), returning spec -> ABSOLUTE entrypoint path.
|
|
// Higher-precedence dirs win on conflict, so the shared solid-js always resolves
|
|
// to the kjol runtime copy (a split reactive instance silently breaks effects).
|
|
// A missing vendor.json in a dir is skipped.
|
|
func loadVendorManifest(vendorDirs []string) (map[string]string, error) {
|
|
abs := map[string]string{}
|
|
for _, dir := range vendorDirs {
|
|
data, err := os.ReadFile(filepath.Join(dir, "vendor.json"))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
var m VendorManifest
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return nil, fmt.Errorf("parsing %s/vendor.json: %w", dir, err)
|
|
}
|
|
for spec, rel := range m.Entrypoints {
|
|
if _, exists := abs[spec]; exists {
|
|
continue // earlier (higher-precedence) dir wins
|
|
}
|
|
p, _ := filepath.Abs(filepath.Join(dir, filepath.FromSlash(rel)))
|
|
abs[spec] = filepath.ToSlash(p)
|
|
}
|
|
}
|
|
return abs, nil
|
|
}
|
|
|
|
// vendorManifestPlugin resolves bare imports from the merged vendor manifest: a
|
|
// package listed in a vendor.json resolves to its pinned (absolute) entrypoint,
|
|
// 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 resolve to the real file via NodePaths; anything not vendored
|
|
// is left external for the import map.
|
|
func vendorManifestPlugin(entrypoints map[string]string, vendorDirs []string) esbuild.Plugin {
|
|
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 := entrypoints[args.Path]; ok {
|
|
return esbuild.OnResolveResult{Path: target}, nil // pinned entrypoint
|
|
}
|
|
if vendorHasPackage(vendorDirs, 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 any vendor dir, honouring @scope/name.
|
|
func vendorHasPackage(vendorDirs []string, spec string) bool {
|
|
parts := strings.Split(spec, "/")
|
|
pkg := parts[0]
|
|
if strings.HasPrefix(pkg, "@") && len(parts) > 1 {
|
|
pkg = pkg + "/" + parts[1]
|
|
}
|
|
for _, dir := range vendorDirs {
|
|
info, err := os.Stat(filepath.Join(dir, filepath.FromSlash(pkg)))
|
|
if err == nil && info.IsDir() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
})
|
|
},
|
|
}
|
|
}
|