From 612b9df99a8e5229fe3ac8612d927c7d7f407930 Mon Sep 17 00:00:00 2001 From: X Date: Wed, 28 Dec 2022 00:49:00 +0800 Subject: [PATCH] Support `browser` field (close #450) --- server/build.go | 58 +++++++++++++++++++++++++++++++++++++----------- server/esm.go | 9 ++++---- server/nodejs.go | 22 +++++++++++++++--- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/server/build.go b/server/build.go index 6314ea9d..c88005ee 100644 --- a/server/build.go +++ b/server/build.go @@ -253,7 +253,7 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { externalDeps := newStringSet() extraExternal := newStringSet() esmResolverPlugin := api.Plugin{ - Name: "esm.sh-resolver", + Name: "esm-resolver", Setup: func(build api.PluginBuild) { build.OnResolve( api.OnResolveOptions{Filter: ".*"}, @@ -268,6 +268,29 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { // resolve nodejs builtin modules like `node:path` specifier = strings.TrimPrefix(specifier, "node:") + // use `browser` field + if len(npm.Browser) > 0 && task.Target != "deno" && task.Target != "node" { + spec := specifier + if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") || specifier == ".." { + fullpath := path.Join(path.Dir(args.Importer), specifier) + // in macOS, the dir `/private/var/` is equal to `/var/` + if strings.HasPrefix(fullpath, "/private/var/") { + fullpath = strings.TrimPrefix(fullpath, "/private") + } + spec = "." + strings.TrimPrefix(fullpath, path.Join(task.wd, "node_modules", npm.Name)) + } + if name, ok := npm.Browser[spec]; ok { + if name == "" { + // browser exclude + return api.OnResolveResult{Namespace: "browser-exclude"}, nil + } else if strings.HasPrefix(name, "./") { + specifier = path.Join(task.wd, "node_modules", npm.Name, name) + } else { + specifier = name + } + } + } + // use `?alias` query if len(task.alias) > 0 { if name, ok := task.alias[specifier]; ok { @@ -317,14 +340,14 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { // splits modules based on the `exports` defines in package.json, // see https://nodejs.org/api/packages.html if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") || specifier == ".." { - resolvedPath := path.Join(path.Dir(args.Importer), specifier) + fullpath := path.Join(path.Dir(args.Importer), specifier) // in macOS, the dir `/private/var/` is equal to `/var/` - if strings.HasPrefix(resolvedPath, "/private/var/") { - resolvedPath = strings.TrimPrefix(resolvedPath, "/private") + if strings.HasPrefix(fullpath, "/private/var/") { + fullpath = strings.TrimPrefix(fullpath, "/private") } - specInPkg := "." + strings.TrimPrefix(resolvedPath, path.Join(task.wd, "node_modules", npm.Name)) + spec := "." + strings.TrimPrefix(fullpath, path.Join(task.wd, "node_modules", npm.Name)) // bundle {pkgName}/{pkgName}.js - if specInPkg == fmt.Sprintf("./%s.js", task.Pkg.Name) { + if spec == fmt.Sprintf("./%s.js", task.Pkg.Name) { return api.OnResolveResult{}, nil } v, ok := npm.DefinedExports.(map[string]interface{}) @@ -335,16 +358,16 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { for _, value := range m { s, ok := value.(string) if ok && s != "" { - match := specInPkg == s || specInPkg+".js" == s || specInPkg+".mjs" == s + match := spec == s || spec+".js" == s || spec+".mjs" == s if !match { if a := strings.Split(s, "*"); len(a) == 2 { prefix := a[0] suffix := a[1] - if (strings.HasPrefix(specInPkg, prefix)) && - (strings.HasSuffix(specInPkg, suffix) || - strings.HasSuffix(specInPkg+".js", suffix) || - strings.HasSuffix(specInPkg+".mjs", suffix)) { - matchName := strings.TrimPrefix(strings.TrimSuffix(specInPkg, suffix), prefix) + if (strings.HasPrefix(spec, prefix)) && + (strings.HasSuffix(spec, suffix) || + strings.HasSuffix(spec+".js", suffix) || + strings.HasSuffix(spec+".mjs", suffix)) { + matchName := strings.TrimPrefix(strings.TrimSuffix(spec, suffix), prefix) export = strings.Replace(export, "*", matchName, -1) match = true } @@ -400,6 +423,15 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { }, ) + // for browser exclude + build.OnLoad( + api.OnLoadOptions{Filter: ".*", Namespace: "browser-exclude"}, + func(args api.OnLoadArgs) (ret api.OnLoadResult, err error) { + contents := "export default undefined;" + return api.OnLoadResult{Contents: &contents, Loader: api.LoaderJS}, nil + }, + ) + // workaround for prisma build if npm.Name == "prisma" { build.OnLoad( @@ -429,7 +461,7 @@ func (task *BuildTask) build(tracing *stringSet) (esm *ESM, err error) { }, } esmBundlerPlugin := api.Plugin{ - Name: "esm.sh-bundler", + Name: "esm-bundler", Setup: func(build api.PluginBuild) { build.OnResolve( api.OnResolveOptions{Filter: ".*"}, diff --git a/server/esm.go b/server/esm.go index 9a77dc1a..9f996db5 100644 --- a/server/esm.go +++ b/server/esm.go @@ -329,11 +329,12 @@ func fixNpmPackage(wd string, np *NpmPackage, target string, isDev bool) *NpmPac } } - if np.Browser != "" && fileExists(path.Join(nmDir, np.Name, np.Browser)) { - isEsm, _, _ := parseJS(path.Join(nmDir, np.Name, np.Browser)) + browserMain := np.Browser["."] + if browserMain != "" && fileExists(path.Join(nmDir, np.Name, browserMain)) { + isEsm, _, _ := parseJS(path.Join(nmDir, np.Name, browserMain)) if isEsm { - log.Infof("%s@%s: use `browser` field as module: %s", np.Name, np.Version, np.Browser) - np.Module = np.Browser + log.Infof("%s@%s: use `browser` field as module: %s", np.Name, np.Version, browserMain) + np.Module = browserMain } } diff --git a/server/nodejs.go b/server/nodejs.go index ca71e874..8a2d75ac 100644 --- a/server/nodejs.go +++ b/server/nodejs.go @@ -194,7 +194,6 @@ type NpmPackageTemp struct { PeerDependencies map[string]string `json:"peerDependencies,omitempty"` Imports map[string]interface{} `json:"imports,omitempty"` DefinedExports interface{} `json:"exports,omitempty"` - // todo: support `browser` field } func (a *StringOrMap) MainValue() string { @@ -214,17 +213,34 @@ func (a *StringOrMap) MainValue() string { } func (a *NpmPackageTemp) ToNpmPackage() *NpmPackage { + browser := map[string]string{} + if a.Browser.Value != "" { + browser["."] = a.Browser.Value + } + if a.Browser.Map != nil { + for k, v := range a.Browser.Map { + s, isStr := v.(string) + if isStr { + browser[k] = s + } else { + b, ok := v.(bool) + if ok && !b { + browser[k] = "" + } + } + } + } return &NpmPackage{ Name: a.Name, Version: a.Version, Type: a.Type, Main: a.Main, - Browser: a.Browser.MainValue(), Module: a.Module.MainValue(), ES2015: a.ES2015.MainValue(), JsNextMain: a.JsNextMain, Types: a.Types, Typings: a.Typings, + Browser: browser, Dependencies: a.Dependencies, PeerDependencies: a.PeerDependencies, Imports: a.Imports, @@ -238,12 +254,12 @@ type NpmPackage struct { Version string Type string Main string - Browser string Module string ES2015 string JsNextMain string Types string Typings string + Browser map[string]string Dependencies map[string]string PeerDependencies map[string]string Imports map[string]interface{}