package webbundler 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 "?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 }) }, } }