diff --git a/server/build.go b/server/build.go index 9e25cc0f6..ea56b4dcf 100644 --- a/server/build.go +++ b/server/build.go @@ -14,6 +14,7 @@ import ( "sync" "time" + "github.com/esm-dev/esm.sh/server/storage" "github.com/evanw/esbuild/pkg/api" "github.com/ije/gox/utils" ) @@ -111,8 +112,8 @@ func (ctx *BuildContext) Query() (BuildResult, bool) { } else { _, err = fs.Stat(normalizeSavePath(ctx.zoneId, path.Join("types", b.Dts))) } - // ensure the build files exist - if err == nil || os.IsExist(err) { + // ensure the build file exists + if err == nil || err != storage.ErrNotFound { return b, true } } @@ -175,9 +176,14 @@ func (ctx *BuildContext) Build() (ret BuildResult, err error) { } func (ctx *BuildContext) install() (err error) { - if ctx.wd == "" { + if ctx.wd == "" || ctx.pkgJson.Name == "" { ctx.wd = path.Join(ctx.npmrc.Dir(), ctx.pkg.Fullname()) ctx.pkgDir = path.Join(ctx.wd, "node_modules", ctx.pkg.Name) + if rp, e := os.Readlink(ctx.pkgDir); e == nil { + ctx.pnpmPkgDir = path.Join(path.Dir(ctx.pkgDir), rp) + } else { + ctx.pnpmPkgDir = ctx.pkgDir + } err = ctx.npmrc.installPackage(ctx.pkg) if err != nil { return @@ -188,11 +194,6 @@ func (ctx *BuildContext) install() (err error) { return } ctx.pkgJson = ctx.normalizePackageJSON(pkgJson) - if rp, e := os.Readlink(ctx.pkgDir); e == nil { - ctx.pnpmPkgDir = path.Join(path.Dir(ctx.pkgDir), rp) - } else { - ctx.pnpmPkgDir = ctx.pkgDir - } } return } @@ -222,19 +223,25 @@ func (ctx *BuildContext) buildModule() (result BuildResult, err error) { } entry := ctx.resolveEntry(ctx.pkg) - log.Debugf("build(%s): Entry%+v", ctx.pkg, entry) - - result, reexport, err := ctx.lexer(&entry, false) - if err != nil && !strings.HasPrefix(err.Error(), "cjsLexer: Can't resolve") { + if entry.isEmpty() { + err = fmt.Errorf("could not resolve entry") return } + log.Debugf("build(%s): Entry%+v", ctx.pkg, entry) - if result.TypesOnly { + typesOnly := strings.HasPrefix(ctx.pkgJson.Name, "@types/") || (entry.esm == "" && entry.cjs == "" && entry.dts != "") + if typesOnly { + result.TypesOnly = true result.Dts = "/" + ctx.pkg.ghPrefix() + ctx.pkg.Fullname() + entry.dts[1:] ctx.transformDTS(entry.dts) return } + result, reexport, err := ctx.lexer(&entry, false) + if err != nil && !strings.HasPrefix(err.Error(), "cjsLexer: Can't resolve") { + return + } + // cjs reexport if reexport != "" { pkg, _, _, e := ctx.lookupDep(reexport) @@ -265,7 +272,7 @@ func (ctx *BuildContext) buildModule() (result BuildResult, err error) { if err != nil { return } - result.Dts = ctx.lookupTypes(entry) + result.Dts, err = ctx.resloveDTS(entry) return } @@ -278,10 +285,6 @@ func (ctx *BuildContext) buildModule() (result BuildResult, err error) { } if entry.esm == "" { - if entry.cjs == "" { - err = fmt.Errorf("could not resolve \"%s\"", entryModuleSpecifier) - return - } buf := bytes.NewBuffer(nil) fmt.Fprintf(buf, `import * as __module from "%s";`, entryModuleSpecifier) if len(result.NamedExports) > 0 { @@ -862,6 +865,7 @@ func (ctx *BuildContext) buildModule() (result BuildResult, err error) { if ctx.isDenoTarget() { conditions = append(conditions, "deno") } + minify := config.Minify == nil || !bytes.Equal(config.Minify, []byte("false")) options := api.BuildOptions{ Outdir: "/esbuild", Write: false, @@ -870,9 +874,9 @@ func (ctx *BuildContext) buildModule() (result BuildResult, err error) { Format: api.FormatESModule, Target: targets[ctx.target], Platform: api.PlatformBrowser, - MinifyWhitespace: !ctx.dev, - MinifyIdentifiers: !ctx.dev, - MinifySyntax: !ctx.dev, + MinifyWhitespace: minify, + MinifyIdentifiers: minify, + MinifySyntax: minify, KeepNames: ctx.args.keepNames, // prevent class/function names erasing IgnoreAnnotations: ctx.args.ignoreAnnotations, // some libs maybe use wrong side-effect annotations Conditions: conditions, @@ -1207,20 +1211,7 @@ rebuild: deps.Sort() result.Deps = deps - result.Dts = ctx.lookupTypes(entry) - return -} - -func (ctx *BuildContext) LookupTypes() (dts string, err error) { - // install the package - ctx.stage = "install" - err = ctx.install() - if err != nil { - return - } - - entry := ctx.resolveEntry(ctx.pkg) - dts = ctx.lookupTypes(entry) + result.Dts, err = ctx.resloveDTS(entry) return } @@ -1252,56 +1243,6 @@ func (ctx *BuildContext) buildTypes() (ret BuildResult, err error) { return } -func (ctx *BuildContext) lookupTypes(entry BuildEntry) string { - if entry.dts != "" { - if !ctx.existsPkgFile(entry.dts) { - return "" - } - return fmt.Sprintf( - "/%s%s/%s%s", - ctx.pkg.ghPrefix(), - ctx.pkg.Fullname(), - ctx.getBuildArgsPrefix(ctx.pkg, true), - strings.TrimPrefix(entry.dts, "./"), - ) - } - - // use types from package "@types/[task.npm.Name]" if it exists - if ctx.pkgJson.Types == "" && !strings.HasPrefix(ctx.pkgJson.Name, "@types/") && regexpFullVersion.MatchString(ctx.pkgJson.Version) { - versionParts := strings.Split(ctx.pkgJson.Version, ".") - versions := []string{ - versionParts[0] + "." + versionParts[1], // major.minor - versionParts[0], // major - } - typesPkgName := toTypesPkgName(ctx.pkgJson.Name) - pkgVersion, ok := ctx.args.deps[typesPkgName] - if ok { - // use the version of the `?deps` query if it exists - versions = append([]string{pkgVersion}, versions...) - } - for _, version := range versions { - p, err := ctx.npmrc.getPackageInfo(typesPkgName, version) - if err == nil { - typesPkg := Pkg{ - Name: typesPkgName, - Version: p.Version, - SubPath: ctx.pkg.SubPath, - SubModule: ctx.pkg.SubModule, - } - b := NewBuildContext(ctx.zoneId, ctx.npmrc, typesPkg, ctx.args, "types", BundleFalse, false, false) - dts, _ := b.LookupTypes() - if dts != "" { - // use tilde semver range instead of the exact version - return strings.ReplaceAll(dts, fmt.Sprintf("%s@%s", typesPkgName, p.Version), fmt.Sprintf("%s@~%s", typesPkgName, p.Version)) - } - break - } - } - } - - return "" -} - func (ctx *BuildContext) transformDTS(types string) (err error) { start := time.Now() buildArgsPrefix := ctx.getBuildArgsPrefix(ctx.pkg, true) diff --git a/server/build_resolver.go b/server/build_resolver.go index 528a89459..f464a1660 100644 --- a/server/build_resolver.go +++ b/server/build_resolver.go @@ -21,6 +21,11 @@ type BuildEntry struct { dts string } +// isEmpty checks if the entry is empty +func (entry *BuildEntry) isEmpty() bool { + return entry.esm == "" && entry.cjs == "" && entry.dts == "" +} + // hasEntry checks if the entrypoint of the given type exists func (entry *BuildEntry) hasEntry(entryType string) bool { switch entryType { @@ -576,18 +581,18 @@ func (ctx *BuildContext) resolveEntry(pkg Pkg) (entry BuildEntry) { normalizeBuildEntry(ctx, &entry) if entry.esm != "" { m, ok := ctx.pkgJson.Browser[entry.esm] - if ok && m != "" { + if ok && isRelativeSpecifier(m) { entry.esm = m } } if entry.cjs != "" { m, ok := ctx.pkgJson.Browser[entry.cjs] - if ok && m != "" { + if ok && isRelativeSpecifier(m) { entry.cjs = m } } if pkg.SubModule == "" { - if m, ok := ctx.pkgJson.Browser["."]; ok && m != "" { + if m, ok := ctx.pkgJson.Browser["."]; ok && isRelativeSpecifier(m) { if ctx.pkgJson.Type == "module" || strings.HasSuffix(m, ".mjs") { entry.esm = m } else { @@ -922,6 +927,62 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind api.Resolv return } +func (ctx *BuildContext) resloveDTS(entry BuildEntry) (string, error) { + if entry.dts != "" { + if !ctx.existsPkgFile(entry.dts) { + return "", nil + } + return fmt.Sprintf( + "/%s%s/%s%s", + ctx.pkg.ghPrefix(), + ctx.pkg.Fullname(), + ctx.getBuildArgsPrefix(ctx.pkg, true), + strings.TrimPrefix(entry.dts, "./"), + ), nil + } + + // use types from package "@types/[task.npm.Name]" if it exists + if ctx.pkgJson.Types == "" && !strings.HasPrefix(ctx.pkgJson.Name, "@types/") && regexpFullVersion.MatchString(ctx.pkgJson.Version) { + versionParts := strings.Split(ctx.pkgJson.Version, ".") + versions := []string{ + versionParts[0] + "." + versionParts[1], // major.minor + versionParts[0], // major + } + typesPkgName := toTypesPkgName(ctx.pkgJson.Name) + pkgVersion, ok := ctx.args.deps[typesPkgName] + if ok { + // use the version of the `?deps` query if it exists + versions = append([]string{pkgVersion}, versions...) + } + for _, version := range versions { + p, err := ctx.npmrc.getPackageInfo(typesPkgName, version) + if err == nil { + typesPkg := Pkg{ + Name: typesPkgName, + Version: p.Version, + SubPath: ctx.pkg.SubPath, + SubModule: ctx.pkg.SubModule, + } + b := NewBuildContext(ctx.zoneId, ctx.npmrc, typesPkg, ctx.args, "types", BundleFalse, false, false) + err := b.install() + if err != nil { + return "", err + } + dts, err := b.resloveDTS(b.resolveEntry(typesPkg)) + if err != nil { + return "", err + } + if dts != "" { + // use tilde semver range instead of the exact version + return strings.ReplaceAll(dts, fmt.Sprintf("%s@%s", typesPkgName, p.Version), fmt.Sprintf("%s@~%s", typesPkgName, p.Version)), nil + } + } + } + } + + return "", nil +} + func (ctx *BuildContext) normalizePackageJSON(p PackageJSON) PackageJSON { if ctx.pkg.FromGithub { // if the name in package.json is not the same as the repository name @@ -992,14 +1053,6 @@ func (ctx *BuildContext) normalizePackageJSON(p PackageJSON) PackageJSON { } func (ctx *BuildContext) lexer(entry *BuildEntry, forceCjsOnly bool) (ret BuildResult, reexport string, err error) { - pkgJson := ctx.pkgJson - typesOnly := strings.HasPrefix(pkgJson.Name, "@types/") || (entry.esm == "" && entry.cjs == "" && entry.dts != "") - - if typesOnly { - ret.TypesOnly = true - return - } - if entry.esm != "" && !forceCjsOnly { isESM, namedExports, erro := ctx.esmLexer(entry.esm) if erro != nil { @@ -1025,7 +1078,7 @@ func (ctx *BuildContext) lexer(entry *BuildEntry, forceCjsOnly bool) (ret BuildR entry.cjs = entry.esm entry.esm = "" reexport = r.ReExport - log.Warnf("fake ES module '%s' of '%s'", entry.cjs, pkgJson.Name) + log.Warnf("fake ES module '%s' of '%s'", entry.cjs, ctx.pkgJson.Name) return } diff --git a/server/config.go b/server/config.go index e0e3214d5..d2435869c 100644 --- a/server/config.go +++ b/server/config.go @@ -19,6 +19,7 @@ type Config struct { BanList BanList `json:"banList"` BuildConcurrency uint16 `json:"buildConcurrency"` BuildTimeout uint16 `json:"buildTimeout"` + Minify json.RawMessage `json:"minify"` DisableSourceMap bool `json:"disableSourceMap"` DisableCompression bool `json:"disableCompression"` Cache string `json:"cache"` @@ -99,10 +100,13 @@ func fixConfig(c *Config) *Config { c.AuthSecret = os.Getenv("AUTH_SECRET") } if !c.DisableCompression { - c.DisableCompression = os.Getenv("DISABLE_COMPRESSION") != "" + c.DisableCompression = os.Getenv("DISABLE_COMPRESSION") == "true" } if !c.DisableSourceMap { - c.DisableSourceMap = os.Getenv("DISABLE_SOURCEMAP") != "" + c.DisableSourceMap = os.Getenv("DISABLE_SOURCEMAP") == "true" + } + if c.Minify == nil && os.Getenv("MINIFY") == "false" { + c.Minify = []byte("false") } if c.BuildConcurrency == 0 { c.BuildConcurrency = uint16(runtime.NumCPU()) diff --git a/server/dts_transform.go b/server/dts_transform.go index 2634faa94..8f8ac12a6 100644 --- a/server/dts_transform.go +++ b/server/dts_transform.go @@ -197,7 +197,11 @@ func transformDTS(ctx *BuildContext, dts string, buildArgsPrefix string, marker exports: NewStringSet(), } b := NewBuildContext(ctx.zoneId, ctx.npmrc, depPkg, args, "types", BundleFalse, false, false) - dts, err := b.LookupTypes() + err = b.install() + if err != nil { + return "", err + } + dts, err = b.resloveDTS(b.resolveEntry(depPkg)) if err != nil { return "", err } diff --git a/server/dts_walker.go b/server/dts_walker.go index a63429ebe..781358581 100644 --- a/server/dts_walker.go +++ b/server/dts_walker.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "regexp" - "strings" ) var ( @@ -74,9 +73,9 @@ func walkDts(r io.Reader, buf *bytes.Buffer, resolve func(specifier string, kind if err != nil { return } - if format == "types" && strings.HasPrefix(res, "{ESM_CDN_ORIGIN}") { - format = "path" - } + // if format == "types" && strings.HasPrefix(res, "{ESM_CDN_ORIGIN}") { + // format = "path" + // } fmt.Fprintf(buf, `/// `, format, res) } else { buf.Write(line) diff --git a/server/embed/polyfills/node_filename_resolver.js b/server/embed/polyfills/node_filename_resolver.js index 00d678451..76e951f87 100644 --- a/server/embed/polyfills/node_filename_resolver.js +++ b/server/embed/polyfills/node_filename_resolver.js @@ -14,9 +14,9 @@ export function __dirname$(dirname) { export async function __downloadPackageTarball$(tar) { const [registry, pkgName, pkgVersion] = tar.split(" "); - const cwd = path.join(os.homedir(), ".cache/esm.sh", pkgName + "@" + pkgVersion); + const savePath = path.join(os.homedir(), ".cache/esm.sh", pkgName + "@" + pkgVersion); try { - await fs.promises.access(path.join(cwd, "package.json")); + await fs.promises.access(path.join(savePath, "package.json")); return; } catch (error) { if (error.code !== "ENOENT") { @@ -33,14 +33,12 @@ export async function __downloadPackageTarball$(tar) { const DecompressionStream = globalThis.DecompressionStream || await import("node:stream/web").DecompressionStream; const readable = Readable.fromWeb(res.body.pipeThrough(new DecompressionStream("gzip"))); try { - // ensure the `cwd` directory exists - await fs.promises.mkdir(cwd, { recursive: true }); + // ensure the `savePath` directory exists + await fs.promises.mkdir(savePath, { recursive: true }); } catch { // ignore } await new Promise((resolve, reject) => { - readable.pipe(extract({ C: cwd, strip: 1 })) - .on("end", resolve) - .on("error", reject); + readable.pipe(extract({ C: savePath, strip: 1 })).on("error", reject).on("end", resolve); }); } diff --git a/test/dev-mode/test.ts b/test/dev-mode/test.ts index 8f2689784..b29e59fb8 100644 --- a/test/dev-mode/test.ts +++ b/test/dev-mode/test.ts @@ -1,11 +1,19 @@ import { assertStringIncludes } from "https://deno.land/std@0.220.0/assert/mod.ts"; -Deno.test("support exports `development` condition", async () => { +Deno.test('`NODE_ENV==="development"`', async () => { + const res = await fetch("http://localhost:8080/react@18.2.0?dev&target=es2022"); + assertStringIncludes(await res.text(), `"/react@18.2.0/es2022/react.development.mjs"`); + const code = await fetch("http://localhost:8080/react@18.2.0/es2022/react.development.mjs").then((res) => res.text()); - assertStringIncludes(code, "/react/cjs/react.development.js"); + assertStringIncludes(code, "react/cjs/react.development.js"); +}); +Deno.test("using `development` condition in `exports`", async () => { const code2 = await fetch("http://localhost:8080/solid-js@1.6.16/es2022/solid-js.development.mjs").then((res) => res.text() ); - assertStringIncludes(code2, "/solid-js/dist/dev.js"); + assertStringIncludes( + code2, + `console.warn("You appear to have multiple instances of Solid. This can lead to unexpected behavior.")`, + ); }); diff --git a/test/esm-worker/test.ts b/test/esm-worker/test.ts index b6c7bb2aa..63f3d57c6 100644 --- a/test/esm-worker/test.ts +++ b/test/esm-worker/test.ts @@ -490,8 +490,6 @@ Deno.test("esm-worker", { sanitizeOps: false, sanitizeResources: false }, async assertStringIncludes(ret.code, `"https://esm.sh/preact-render-to-string6.0.2"`); assertStringIncludes(ret.map, `"mappings":`); - console.log("transformed", hash, ret); - const res2 = await fetch(`${workerOrigin}/+${hash}.mjs`, { headers: { "User-Agent": "Chrome/90.0.4430.212" }, });