diff --git a/internal/importmap/importmap.go b/internal/importmap/importmap.go index ffa6cdf4c..c47c7daed 100644 --- a/internal/importmap/importmap.go +++ b/internal/importmap/importmap.go @@ -374,6 +374,8 @@ func walkDependencies(deps map[string]string, callback func(specifier, pkgName, prefix = "/gh" } else if pkg.PkgPrNew { prefix = "/pr" + } else if pkg.Tgz { + prefix = "/tgz" } callback(specifier, pkgName, pkgVersion, prefix) } diff --git a/internal/npm/npm.go b/internal/npm/npm.go index 951c2066d..ded1190cb 100644 --- a/internal/npm/npm.go +++ b/internal/npm/npm.go @@ -32,6 +32,7 @@ type Package struct { Version string Github bool PkgPrNew bool + Tgz bool } func (p *Package) String() string { @@ -42,6 +43,9 @@ func (p *Package) String() string { if p.PkgPrNew { return "pr/" + s } + if p.Tgz { + return "tgz/" + s + } return s } diff --git a/server/build.go b/server/build.go index 981926436..2044e0d0b 100644 --- a/server/build.go +++ b/server/build.go @@ -1138,6 +1138,8 @@ REBUILD: header.WriteString("github:") } else if ctx.esmPath.PrPrefix { header.WriteString("pkg.pr.new/") + } else if ctx.esmPath.TgzPrefix { + header.WriteString("tgz/") } header.WriteString(ctx.esmPath.PkgName) if ctx.esmPath.GhPrefix { @@ -1350,7 +1352,7 @@ REBUILD: finalJS.Write(jsContent) // check if the package is deprecated - if !ctx.esmPath.GhPrefix && !ctx.esmPath.PrPrefix { + if !ctx.esmPath.GhPrefix && !ctx.esmPath.PrPrefix && !ctx.esmPath.TgzPrefix { deprecated, _ := ctx.npmrc.isDeprecated(ctx.pkgJson.Name, ctx.pkgJson.Version) if deprecated != "" { fmt.Fprintf(finalJS, `console.warn("%%c[esm.sh]%%c %%cdeprecated%%c %s@%s: " + %s, "color:grey", "", "color:red", "");%s`, ctx.esmPath.PkgName, ctx.esmPath.PkgVersion, utils.MustEncodeJSON(deprecated), "\n") @@ -1459,7 +1461,7 @@ func (ctx *BuildContext) install() (err error) { return err } - if ctx.esmPath.GhPrefix || ctx.esmPath.PrPrefix { + if ctx.esmPath.GhPrefix || ctx.esmPath.PrPrefix || ctx.esmPath.TgzPrefix { // if the name in package.json is not the same as the repository name if p.Name != ctx.esmPath.PkgName { p.PkgName = p.Name diff --git a/server/build_args.go b/server/build_args.go index c2aaa7149..84de046ac 100644 --- a/server/build_args.go +++ b/server/build_args.go @@ -136,7 +136,7 @@ func resolveBuildArgs(npmrc *NpmRC, installDir string, args *BuildArgs, esm EsmP if err == nil { p = raw.ToNpmPackage() } - } else if esm.GhPrefix || esm.PrPrefix { + } else if esm.GhPrefix || esm.PrPrefix || esm.TgzPrefix { p, err = npmrc.installPackage(esm.Package()) } else { p, err = npmrc.getPackageInfo(esm.PkgName, esm.PkgVersion) @@ -254,7 +254,7 @@ func walkDeps(npmrc *NpmRC, installDir string, pkg npm.Package, mark *set.Set[st if err == nil { p = raw.ToNpmPackage() } - } else if pkg.Github || pkg.PkgPrNew { + } else if pkg.Github || pkg.PkgPrNew || pkg.Tgz { p, err = npmrc.installPackage(pkg) } else { p, err = npmrc.getPackageInfo(pkg.Name, pkg.Version) diff --git a/server/build_resolver.go b/server/build_resolver.go index 656115aa9..6acdfefbc 100644 --- a/server/build_resolver.go +++ b/server/build_resolver.go @@ -708,6 +708,7 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind esbuild.Re PkgVersion: pkgJson.Version, GhPrefix: ctx.esmPath.GhPrefix, PrPrefix: ctx.esmPath.PrPrefix, + TgzPrefix: ctx.esmPath.TgzPrefix, }, ctx.getBuildArgsPrefix(false), ctx.externalAll) return } @@ -718,6 +719,7 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind esbuild.Re subModule := EsmPath{ GhPrefix: ctx.esmPath.GhPrefix, PrPrefix: ctx.esmPath.PrPrefix, + TgzPrefix: ctx.esmPath.TgzPrefix, PkgName: ctx.esmPath.PkgName, PkgVersion: ctx.esmPath.PkgVersion, SubPath: subPath, @@ -788,6 +790,7 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind esbuild.Re if p.Name != "" { dep.GhPrefix = p.Github dep.PrPrefix = p.PkgPrNew + dep.TgzPrefix = p.Tgz dep.PkgName = p.Name dep.PkgVersion = p.Version } @@ -846,7 +849,7 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind esbuild.Re var exactVersion bool if dep.GhPrefix { exactVersion = isCommitish(dep.PkgVersion) || npm.IsExactVersion(strings.TrimPrefix(dep.PkgVersion, "v")) - } else if dep.PrPrefix { + } else if dep.PrPrefix || dep.TgzPrefix { exactVersion = true } else { exactVersion = npm.IsExactVersion(dep.PkgVersion) diff --git a/server/npmrc.go b/server/npmrc.go index a80db83fd..059aec6d2 100644 --- a/server/npmrc.go +++ b/server/npmrc.go @@ -333,6 +333,11 @@ func (npmrc *NpmRC) installPackage(pkg npm.Package) (packageJson *npm.PackageJSO return } } + } else if pkg.Tgz { + url, err := url.PathUnescape(pkg.Version) + if err == nil { + err = fetchPackageTarball(&NpmRegistry{}, installDir, pkg.Name, url) + } } else if pkg.PkgPrNew { err = fetchPackageTarball(&NpmRegistry{}, installDir, pkg.Name, "https://pkg.pr.new/"+pkg.Name+"@"+pkg.Version) } else { @@ -391,7 +396,7 @@ func (npmrc *NpmRC) installDependencies(wd string, pkgJson *npm.PackageJSON, npm // skip installing `@types/*` packages return } - if !npm.IsExactVersion(pkg.Version) && !pkg.Github && !pkg.PkgPrNew { + if !npm.IsExactVersion(pkg.Version) && !pkg.Github && !pkg.PkgPrNew && !pkg.Tgz { p, e := npmrc.getPackageInfo(pkg.Name, pkg.Version) if e != nil { return diff --git a/server/path.go b/server/path.go index 8bd707c6c..2ea698cad 100644 --- a/server/path.go +++ b/server/path.go @@ -15,6 +15,7 @@ import ( type EsmPath struct { GhPrefix bool PrPrefix bool + TgzPrefix bool PkgName string PkgVersion string SubPath string @@ -25,6 +26,7 @@ func (p EsmPath) Package() npm.Package { return npm.Package{ Github: p.GhPrefix, PkgPrNew: p.PrPrefix, + Tgz: p.TgzPrefix, Name: p.PkgName, Version: p.PkgVersion, } @@ -41,6 +43,9 @@ func (p EsmPath) Name() string { if p.PrPrefix { return "pr/" + name } + if p.TgzPrefix { + return "tgz/" + name + } return name } @@ -52,6 +57,36 @@ func (p EsmPath) Specifier() string { } func praseEsmPath(npmrc *NpmRC, pathname string) (esm EsmPath, extraQuery string, exactVersion bool, hasTargetSegment bool, err error) { + if strings.HasPrefix(pathname, "/tgz/") { + pathname = pathname[5:] + var pkgName string + var subPath string + var rest string + if pathname[0] == '@' { + var scope string + scope, rest = utils.SplitByFirstByte(pathname, '/') + pkgName, rest = utils.SplitByFirstByte(rest, '@') + pkgName = scope + "/" + pkgName + } else { + pkgName, rest = utils.SplitByFirstByte(pathname, '@') + } + tarballUrl, subPath := utils.SplitByFirstByte(rest, '/') + tarballUrl, err = url.PathUnescape(tarballUrl) + if err != nil { + return + } + tarballUrl = url.PathEscape(tarballUrl) + exactVersion = true + hasTargetSegment = validateTargetSegment(strings.Split(subPath, "/")) + esm = EsmPath{ + PkgName: pkgName, + PkgVersion: tarballUrl, + SubPath: subPath, + SubModuleName: stripEntryModuleExt(subPath), + TgzPrefix: true, + } + return + } // see https://pkg.pr.new if strings.HasPrefix(pathname, "/pr/") || strings.HasPrefix(pathname, "/pkg.pr.new/") { if strings.HasPrefix(pathname, "/pr/") { diff --git a/server/router.go b/server/router.go index 6febc4fa9..47d498593 100644 --- a/server/router.go +++ b/server/router.go @@ -69,7 +69,7 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re ) return func(ctx *rex.Context) any { - pathname := ctx.R.URL.Path + pathname := ctx.R.URL.EscapedPath() // ban malicious requests if strings.HasPrefix(pathname, "/.") || strings.HasSuffix(pathname, ".env") || strings.HasSuffix(pathname, ".php") { @@ -175,12 +175,12 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re i := len(pathname) - 1 j := 0 for { - if i < 0 || pathname[i] == '/' { - break - } if pathname[i] == ':' { j = i } + if i < 0 || pathname[i] < '0' || pathname[i] > '9' { + break + } i-- } if j > 0 { @@ -852,6 +852,8 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re registryPrefix = "/gh" } else if esm.PrPrefix { registryPrefix = "/pr" + } else if esm.TgzPrefix { + registryPrefix = "/tgz" } // redirect `/@types/PKG` to it's main dts file @@ -964,6 +966,8 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re if asteriskPrefix { if esm.GhPrefix || esm.PrPrefix { pkgName = pkgName[0:3] + "*" + pkgName[3:] + } else if esm.TgzPrefix { + pkgName = pkgName[0:5] + "*" + pkgName[5:] } else { pkgName = "*" + pkgName } @@ -991,6 +995,8 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re if asteriskPrefix { if esm.GhPrefix || esm.PrPrefix { pkgName = pkgName[0:3] + "*" + pkgName[3:] + } else if esm.TgzPrefix { + pkgName = pkgName[0:5] + "*" + pkgName[5:] } else { pkgName = "*" + pkgName } @@ -1299,6 +1305,8 @@ func esmRouter(db Database, buildStorage storage.Storage, logger *log.Logger) re if asteriskPrefix { if esm.GhPrefix || esm.PrPrefix { pkgName = pkgName[0:3] + "*" + pkgName[3:] + } else if esm.TgzPrefix { + pkgName = pkgName[0:5] + "*" + pkgName[5:] } else { pkgName = "*" + pkgName } diff --git a/test/tgz/test.ts b/test/tgz/test.ts new file mode 100644 index 000000000..c59851824 --- /dev/null +++ b/test/tgz/test.ts @@ -0,0 +1,36 @@ +import { assertEquals, assertStringIncludes } from "jsr:@std/assert"; + +Deno.test("tgz", async () => { + const res = await fetch("http://localhost:8080/tgz/preact@https%3A%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz", { headers: { "user-agent": "i'm a browser" } }); + assertEquals(res.status, 200); + assertEquals(res.headers.get("content-type"), "application/javascript; charset=utf-8"); + assertEquals(res.headers.get("cache-control"), "public, max-age=31536000, immutable"); + assertEquals(res.headers.get("x-typescript-types"), "http://localhost:8080/tgz/preact@https:%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz/src/index.d.ts"); + const text = await res.text(); + assertStringIncludes(text, "/es2022/preact.mjs"); +}); + +Deno.test("tgz dts", async () => { + const res = await fetch("http://localhost:8080/tgz/preact@https%3A%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz/src/index.d.ts"); + assertEquals(res.status, 200); + assertEquals(res.headers.get("content-type"), "application/typescript; charset=utf-8"); + assertEquals(res.headers.get("cache-control"), "public, max-age=31536000, immutable"); + const text = await res.text(); + assertStringIncludes(text, "export abstract class Component {"); +}); + +Deno.test("tgz routing", async () => { + { + const { h } = await import("http://localhost:8080/tgz/preact@https%3A%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz"); + assertEquals(typeof h, "function"); + } + { + const { h } = await import("http://localhost:8080/tgz/preact@https:%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz"); + assertEquals(typeof h, "function"); + } +}); + +Deno.test("access tgz raw files", async () => { + const { name } = await fetch("http://localhost:8080/tgz/preact@https:%2F%2Fregistry.yarnpkg.com%2Fpreact%2F-%2Fpreact-10.26.6.tgz/package.json").then((res) => res.json()); + assertEquals(name, "preact"); +});