package jsbundler // defaultExportShimPlugin bridges two ESM-strictness mismatches that // existed in the previous custom bundler: // // 1. `export default function Name` also emits `export { Name }` so // callers using `import { Name } from "..."` resolve correctly. // 2. If the source declares `export function/class/const Name` (or // `Name`) where Name matches the file's basename, synthesize // `export { Name as default }` so callers using `import Name from // "..."` resolve correctly. The old bundler made every IIFE // return value available as both the named symbol AND the default // import; standard ESM does not, so esbuild needs the shim until // callers migrate to named imports throughout. import ( "os" "path/filepath" "regexp" "strings" esbuild "github.com/evanw/esbuild/pkg/api" ) func defaultExportShimPlugin() esbuild.Plugin { return esbuild.Plugin{ Name: "default-export-shim", Setup: func(b esbuild.PluginBuild) { b.OnLoad(esbuild.OnLoadOptions{Filter: `\.(js|ts)$`}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) { raw, err := os.ReadFile(args.Path) if err != nil { return esbuild.OnLoadResult{}, err } out := addNamedExportForDefault(string(raw)) out = addDefaultExportForFilenameMatch(out, args.Path) loader := esbuild.LoaderJS if strings.HasSuffix(args.Path, ".ts") { loader = esbuild.LoaderTS } return esbuild.OnLoadResult{Contents: &out, Loader: loader}, nil }) }, } } var reFilenameMatchExport = regexp.MustCompile(`(?m)^export\s+(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)`) var reExistingDefault = regexp.MustCompile(`(?m)^export\s+default\b`) // addDefaultExportForFilenameMatch synthesizes `export { Name as // default }` if the source declares a top-level export whose name // matches the file's basename and the file does not already have a // default export. This preserves the old bundler's behavior where // `import Foo from "./Foo.js"` resolved against `export function // Foo(...)`. func addDefaultExportForFilenameMatch(src, path string) string { if reExistingDefault.MatchString(src) { return src } base := filepath.Base(path) base = strings.TrimSuffix(base, filepath.Ext(base)) if base == "" { return src } for _, m := range reFilenameMatchExport.FindAllStringSubmatch(src, -1) { if m[1] == base { return src + "\nexport { " + base + " as default };\n" } } return src } var reExportDefaultDecl = regexp.MustCompile(`(?m)^export\s+default\s+(?:async\s+)?(?:function|class)\s+(\w+)`) func addNamedExportForDefault(src string) string { matches := reExportDefaultDecl.FindAllStringSubmatch(src, -1) if len(matches) == 0 { return src } names := make([]string, 0, len(matches)) seen := map[string]bool{} for _, m := range matches { if seen[m[1]] { continue } seen[m[1]] = true names = append(names, m[1]) } return src + "\nexport { " + strings.Join(names, ", ") + " };\n" }