Files
kjol/go/webbundler/export_shim.go

90 lines
2.9 KiB
Go

package webbundler
// 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"
}