diff --git a/server/build.go b/server/build.go index 5afc08edf..3d19c9305 100644 --- a/server/build.go +++ b/server/build.go @@ -41,6 +41,7 @@ type BuildContext struct { wd string pkgJson *PackageJSON path string + rawPath string status string splitting *set.ReadOnlySet[string] esmImports [][2]string @@ -98,57 +99,7 @@ func (ctx *BuildContext) Path() string { return ctx.path } - asteriskPrefix := "" - if ctx.externalAll { - asteriskPrefix = "*" - } - - esm := ctx.esm - if ctx.target == "types" { - if strings.HasSuffix(esm.SubPath, ".d.ts") { - ctx.path = fmt.Sprintf( - "/%s%s/%s%s", - asteriskPrefix, - esm.Name(), - ctx.getBuildArgsPrefix(true), - esm.SubPath, - ) - } else { - ctx.path = "/" + esm.Specifier() - } - return ctx.path - } - - name := strings.TrimSuffix(path.Base(esm.PkgName), ".js") - if esm.SubModuleName != "" { - if esm.SubModuleName == name { - // if the sub-module name is same as the package name - name = "__" + esm.SubModuleName - } else { - name = esm.SubModuleName - } - // workaround for es5-ext "../#/.." path - if esm.PkgName == "es5-ext" { - name = strings.ReplaceAll(name, "/#/", "/%23/") - } - } - - if ctx.dev { - name += ".development" - } - if ctx.bundleMode == BundleAll { - name += ".bundle" - } else if ctx.bundleMode == BundleFalse { - name += ".nobundle" - } - ctx.path = fmt.Sprintf( - "/%s%s/%s%s/%s.mjs", - asteriskPrefix, - esm.Name(), - ctx.getBuildArgsPrefix(ctx.target == "types"), - ctx.target, - name, - ) + ctx.buildPath() return ctx.path } @@ -232,6 +183,60 @@ func (ctx *BuildContext) Build() (meta *BuildMeta, err error) { return } +func (ctx *BuildContext) buildPath() { + asteriskPrefix := "" + if ctx.externalAll { + asteriskPrefix = "*" + } + + esm := ctx.esm + if ctx.target == "types" { + if strings.HasSuffix(esm.SubPath, ".d.ts") { + ctx.path = fmt.Sprintf( + "/%s%s/%s%s", + asteriskPrefix, + esm.Name(), + ctx.getBuildArgsPrefix(true), + esm.SubPath, + ) + } else { + ctx.path = "/" + esm.Specifier() + } + return + } + + name := strings.TrimSuffix(path.Base(esm.PkgName), ".js") + if esm.SubModuleName != "" { + if esm.SubModuleName == name { + // if the sub-module name is same as the package name + name = "__" + esm.SubModuleName + } else { + name = esm.SubModuleName + } + // workaround for es5-ext "../#/.." path + if esm.PkgName == "es5-ext" { + name = strings.ReplaceAll(name, "/#/", "/%23/") + } + } + + if ctx.dev { + name += ".development" + } + if ctx.bundleMode == BundleAll { + name += ".bundle" + } else if ctx.bundleMode == BundleFalse { + name += ".nobundle" + } + ctx.path = fmt.Sprintf( + "/%s%s/%s%s/%s.mjs", + asteriskPrefix, + esm.Name(), + ctx.getBuildArgsPrefix(ctx.target == "types"), + ctx.target, + name, + ) +} + func (ctx *BuildContext) buildModule(analyzeMode bool) (meta *BuildMeta, includes [][2]string, err error) { entry := ctx.resolveEntry(ctx.esm) if entry.isEmpty() { @@ -1443,10 +1448,9 @@ func (ctx *BuildContext) buildTypes() (ret *BuildMeta, err error) { func (ctx *BuildContext) install() (err error) { if ctx.wd == "" || ctx.pkgJson == nil { - var p *PackageJSON - p, err = ctx.npmrc.installPackage(ctx.esm.Package()) + p, err := ctx.npmrc.installPackage(ctx.esm.Package()) if err != nil { - return + return err } if ctx.esm.GhPrefix || ctx.esm.PrPrefix { @@ -1491,6 +1495,7 @@ func (ctx *BuildContext) install() (err error) { if isMainModule { ctx.esm.SubModuleName = "" ctx.esm.SubPath = "" + ctx.rawPath = ctx.path ctx.path = "" } } diff --git a/server/build_queue.go b/server/build_queue.go index bc6325930..7967e27ed 100644 --- a/server/build_queue.go +++ b/server/build_queue.go @@ -110,6 +110,10 @@ func (q *BuildQueue) run(task *BuildTask) { q.lock.Lock() q.queue.Remove(task.el) delete(q.tasks, task.ctx.Path()) + if task.ctx.rawPath != "" { + // the `Build` function may have changed the path + delete(q.tasks, task.ctx.rawPath) + } q.chann += 1 q.lock.Unlock() diff --git a/server/build_resolver.go b/server/build_resolver.go index 33e2c82f4..010b8955f 100644 --- a/server/build_resolver.go +++ b/server/build_resolver.go @@ -80,22 +80,43 @@ func (ctx *BuildContext) resolveEntry(esm EsmPath) (entry BuildEntry) { // reslove sub-module using `exports` conditions if exists // see https://nodejs.org/api/packages.html#package-entry-points if pkgJson.Exports.Len() > 0 { - exportEntry := BuildEntry{} - for _, name := range pkgJson.Exports.keys { - conditions, ok := pkgJson.Exports.values[name] - if ok { - if name == "./"+subModuleName || stripEntryModuleExt(name) == "./"+subModuleName { + var exportEntry BuildEntry + conditions, ok := pkgJson.Exports.Get("./" + subModuleName) + if ok { + if s, ok := conditions.(string); ok { + /** + exports: { + "./lib/foo": "./lib/foo.js" + } + */ + exportEntry.update(s, pkgJson.Type == "module") + } else if obj, ok := conditions.(JSONObject); ok { + /** + exports: { + "./lib/foo": { + "require": "./lib/foo.js", + "import": "./esm/foo.js", + "types": "./types/foo.d.ts" + } + } + */ + exportEntry = ctx.resolveConditionExportEntry(obj, pkgJson.Type) + } + } else { + for _, name := range pkgJson.Exports.keys { + conditions := pkgJson.Exports.values[name] + if stripEntryModuleExt(name) == "./"+subModuleName { if s, ok := conditions.(string); ok { /** exports: { - "./lib/foo": "./lib/foo.js" + "./lib/foo.js": "./lib/foo.js" } */ exportEntry.update(s, pkgJson.Type == "module") } else if obj, ok := conditions.(JSONObject); ok { /** exports: { - "./lib/foo": { + "./lib/foo.js": { "require": "./lib/foo.js", "import": "./esm/foo.js", "types": "./types/foo.d.ts" diff --git a/server/router.go b/server/router.go index 68d3cb662..fcadebe44 100644 --- a/server/router.go +++ b/server/router.go @@ -186,25 +186,27 @@ func esmRouter() rex.Handle { } // strip trailing slash - if last := len(pathname) - 1; pathname != "/" && pathname[last] == '/' { - pathname = pathname[:last] + if pl := len(pathname); pl > 1 && pathname[pl-1] == '/' { + pathname = pathname[:pl-1] } // strip loc suffix // e.g. https://esm.sh/react/es2022/react.mjs:2:3 - i := len(pathname) - 1 - j := 0 - for { - if i < 0 || pathname[i] == '/' { - break + { + i := len(pathname) - 1 + j := 0 + for { + if i < 0 || pathname[i] == '/' { + break + } + if pathname[i] == ':' { + j = i + } + i-- } - if pathname[i] == ':' { - j = i + if j > 0 { + pathname = pathname[:j] } - i-- - } - if j > 0 { - pathname = pathname[:j] } // static routes @@ -1612,7 +1614,7 @@ func esmRouter() rex.Handle { if !ret.HasCSS { return rex.Status(404, "Package CSS not found") } - url := fmt.Sprintf("%s%s.css", origin, strings.TrimSuffix(buildCtx.Path(), ".mjs")) + url := origin + strings.TrimSuffix(buildCtx.Path(), ".mjs") + ".css" return redirect(ctx, url, isExactVersion) } @@ -1631,6 +1633,18 @@ func esmRouter() rex.Handle { // if the path is `ESMBuild`, return the built js/css content if pathKind == EsmBuild { + // if the build + if esm.SubPath != buildCtx.esm.SubPath { + buf, recycle := NewBuffer() + defer recycle() + fmt.Fprintf(buf, "export * from \"%s\";\n", buildCtx.Path()) + if ret.ExportDefault { + fmt.Fprintf(buf, "export { default } from \"%s\";\n", buildCtx.Path()) + } + ctx.Header.Set("Content-Type", ctJavaScript) + ctx.Header.Set("Cache-Control", ccImmutable) + return buf.Bytes() + } savePath := buildCtx.getSavepath() if strings.HasSuffix(esm.SubPath, ".css") { path, _ := utils.SplitByLastByte(savePath, '.') diff --git a/test/fix-url/test.ts b/test/fix-url/test.ts index fb3585be5..2816f3ac1 100644 --- a/test/fix-url/test.ts +++ b/test/fix-url/test.ts @@ -145,3 +145,11 @@ Deno.test("dts-transformer: support `.d` extension", async () => { const dts = await res.text(); assertStringIncludes(dts, "'./config.d.ts'"); }); + +Deno.test("fix rewritten build path", async () => { + for (let i = 0; i < 2; i++) { + const res = await fetch("http://localhost:8080/@ucanto/core@10.0.1/denonext/src/lib.mjs"); + const js = await res.text(); + assertEquals(js.trim(), `export * from "/@ucanto/core@10.0.1/denonext/core.mjs";`); + } +}); diff --git a/test/issue-787/test.ts b/test/issue-787/test.ts new file mode 100644 index 000000000..53ecd8111 --- /dev/null +++ b/test/issue-787/test.ts @@ -0,0 +1,8 @@ +import { assertExists } from "jsr:@std/assert"; + +import * as capabilities from "http://localhost:8080/@web3-storage/capabilities@^18.0.0/index?target=denonext"; + +Deno.test("issue #787", () => { + assertExists(capabilities.add); + assertExists(capabilities.add.can); +});