Skip to content

Commit c783819

Browse files
authored
Fix pkg.pr.new redirects (#1204)
1 parent f29067f commit c783819

File tree

6 files changed

+207
-125
lines changed

6 files changed

+207
-125
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ You can also add the `?standalone` flag to bundle the module along with all its
147147
import { Button } from "https://esm.sh/antd?standalone";
148148
```
149149

150+
You can disable the default transforming/bundling behavior by adding `?raw` query to the import URL.
151+
152+
```js
153+
import { render } from "https://esm.sh/preact?raw";
154+
```
155+
156+
> [!TIP]
157+
> The `?raw` query is useful when you want to import the raw JavaScript source code of a package, as-is, without transformation into ES modules.
158+
150159
### Tree Shaking
151160

152161
By default, esm.sh exports a module with all its exported members. However, if you want to import only a specific set of
@@ -329,8 +338,9 @@ In development mode (open the page on localhost), `esm.sh/tsx` uses [@esm.sh/tsx
329338
330339
## Escape Hatch: Raw Source Files
331340

332-
In rare cases, you may want to request JS source files from packages, as-is, without transformation into ES modules. To
333-
do so, you need to add a `?raw` query to the request URL.
341+
By default, esm.sh transforms (and bundles if necessary) the JavaScript source code. However, in rare cases, you may want to request JS source files from packages, as-is, without transformation into ES modules. To do so, you need to add a `?raw` query to the request URL.
342+
343+
The `raw` mode works just like other CDN services, unpkg.com(https://unpkg.com/), jsdelivr.net(https://www.jsdelivr.net/), etc.
334344

335345
```html
336346
<script src="https://esm.sh/p5/lib/p5.min.js?raw"></script>

internal/fetch/fetch.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ func NewClient(userAgent string, timeout int, reserveRedirect bool, allowedHosts
3636
return http.ErrUseLastResponse
3737
}
3838
}
39-
if len(via) >= 3 {
40-
return errors.New("stopped after 3 redirects")
39+
if len(via) >= 6 {
40+
return errors.New("too many redirects")
4141
}
4242
return nil
4343
}

server/npmrc.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,23 +508,25 @@ RETRY:
508508
time.Sleep(time.Duration(retryTimes) * 100 * time.Millisecond)
509509
goto RETRY
510510
}
511+
err = fmt.Errorf("failed to download tarball of package '%s': %v", pkgName, err)
511512
return
512513
}
513514
defer res.Body.Close()
514515

515516
if res.StatusCode == 404 || res.StatusCode == 401 {
516-
err = fmt.Errorf("tarball of package '%s' not found", path.Base(installDir))
517+
err = fmt.Errorf("tarball of package '%s' not found", pkgName)
517518
return
518519
}
519520

520521
if res.StatusCode != 200 {
521522
msg, _ := io.ReadAll(res.Body)
522-
err = fmt.Errorf("could not download tarball of package '%s' (%s: %s)", path.Base(installDir), res.Status, string(msg))
523+
err = fmt.Errorf("could not download tarball of package '%s' (%s: %s)", pkgName, res.Status, string(msg))
523524
return
524525
}
525526

526527
err = extractPackageTarball(installDir, pkgName, io.LimitReader(res.Body, maxPackageTarballSize))
527528
if err != nil {
529+
err = fmt.Errorf("failed to extract tarball of package '%s': %v", pkgName, err)
528530
// clear installDir if failed to extract tarball
529531
os.RemoveAll(installDir)
530532
}

server/path.go

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package server
33
import (
44
"errors"
55
"fmt"
6+
"net/http"
67
"net/url"
8+
"regexp"
79
"sort"
810
"strings"
11+
"time"
912

1013
"github.com/Masterminds/semver/v3"
14+
"github.com/esm-dev/esm.sh/internal/fetch"
1115
"github.com/esm-dev/esm.sh/internal/npm"
1216
"github.com/ije/gox/set"
1317
"github.com/ije/gox/utils"
@@ -70,7 +74,6 @@ func parseEsmPath(npmrc *NpmRC, pathname string) (esm EsmPath, extraQuery string
7074
err = errors.New("invalid path")
7175
return
7276
}
73-
exactVersion = true
7477
hasTargetSegment = validateTargetSegment(strings.Split(subPath, "/"))
7578
esm = EsmPath{
7679
PkgName: pkgName,
@@ -79,6 +82,17 @@ func parseEsmPath(npmrc *NpmRC, pathname string) (esm EsmPath, extraQuery string
7982
SubModuleName: stripEntryModuleExt(subPath),
8083
PrPrefix: true,
8184
}
85+
if isCommitish(esm.PkgVersion) {
86+
exactVersion = true
87+
return
88+
}
89+
esm.PkgVersion, err = resolvePrPackageVersion(esm)
90+
if err != nil {
91+
return
92+
}
93+
if !isCommitish(esm.PkgVersion) {
94+
err = errors.New("pkg.pr.new: tag or branch not found")
95+
}
8296
return
8397
}
8498

@@ -151,69 +165,28 @@ func parseEsmPath(npmrc *NpmRC, pathname string) (esm EsmPath, extraQuery string
151165
}
152166

153167
if ghPrefix {
154-
if npm.IsExactVersion(strings.TrimPrefix(esm.PkgVersion, "v")) {
168+
if npm.IsExactVersion(strings.TrimPrefix(esm.PkgVersion, "v")) || isCommitish(esm.PkgVersion) {
155169
exactVersion = true
156170
return
157171
}
158-
var refs []GitRef
159-
refs, err = listGhRepoRefs(fmt.Sprintf("https://github.com/%s", esm.PkgName))
172+
173+
esm.PkgVersion, err = resolveGhPackageVersion(esm)
160174
if err != nil {
161175
return
162176
}
163-
if esm.PkgVersion == "" {
164-
for _, ref := range refs {
165-
if ref.Ref == "HEAD" {
166-
esm.PkgVersion = ref.Sha[:7]
167-
return
168-
}
169-
}
170-
} else {
171-
// try to find the exact tag or branch
172-
for _, ref := range refs {
173-
if ref.Ref == "refs/tags/"+esm.PkgVersion || ref.Ref == "refs/heads/"+esm.PkgVersion {
174-
esm.PkgVersion = ref.Sha[:7]
175-
return
176-
}
177-
}
178-
// try to find the 'semver' tag
179-
if semv, erro := semver.NewConstraint(strings.TrimPrefix(esm.PkgVersion, "semver:")); erro == nil {
180-
semtags := make([]*semver.Version, len(refs))
181-
i := 0
182-
for _, ref := range refs {
183-
if strings.HasPrefix(ref.Ref, "refs/tags/") {
184-
v, e := semver.NewVersion(strings.TrimPrefix(ref.Ref, "refs/tags/"))
185-
if e == nil && semv.Check(v) {
186-
semtags[i] = v
187-
i++
188-
}
189-
}
190-
}
191-
if i > 0 {
192-
semtags = semtags[:i]
193-
if i > 1 {
194-
sort.Sort(semver.Collection(semtags))
195-
}
196-
esm.PkgVersion = semtags[i-1].String()
197-
return
198-
}
199-
}
200-
}
201177

202178
if !isCommitish(esm.PkgVersion) {
203-
err = errors.New("git: tag or branch not found")
204-
return
179+
err = errors.New("github: tag or branch not found")
205180
}
206-
207-
exactVersion = true
208181
return
209182
}
210183

211184
originalExactVersion := len(esm.PkgVersion) > 0 && npm.IsExactVersion(esm.PkgVersion)
212185
exactVersion = originalExactVersion
213-
186+
214187
// Check if version is a date format (yyyy-mm-dd)
215188
isDateVersion := npm.IsDateVersion(esm.PkgVersion)
216-
189+
217190
if !originalExactVersion {
218191
var p *npm.PackageJSON
219192
if isDateVersion {
@@ -286,3 +259,79 @@ func isPackageInExternalNamespace(pkgName string, external set.ReadOnlySet[strin
286259
}
287260
return false
288261
}
262+
263+
func resolveGhPackageVersion(esm EsmPath) (version string, err error) {
264+
return withCache("gh/"+esm.PkgName+"@"+esm.PkgVersion, time.Duration(config.NpmQueryCacheTTL)*time.Second, func() (version string, aliasKey string, err error) {
265+
var refs []GitRef
266+
refs, err = listGhRepoRefs(fmt.Sprintf("https://github.com/%s", esm.PkgName))
267+
if err != nil {
268+
return
269+
}
270+
if esm.PkgVersion == "" {
271+
for _, ref := range refs {
272+
if ref.Ref == "HEAD" {
273+
version = ref.Sha[:7]
274+
return
275+
}
276+
}
277+
} else {
278+
// try to find the exact tag or branch
279+
for _, ref := range refs {
280+
if ref.Ref == "refs/tags/"+esm.PkgVersion || ref.Ref == "refs/heads/"+esm.PkgVersion {
281+
version = ref.Sha[:7]
282+
return
283+
}
284+
}
285+
// try to find the 'semver' tag
286+
if semv, erro := semver.NewConstraint(strings.TrimPrefix(esm.PkgVersion, "semver:")); erro == nil {
287+
semtags := make([]*semver.Version, len(refs))
288+
i := 0
289+
for _, ref := range refs {
290+
if after, ok := strings.CutPrefix(ref.Ref, "refs/tags/"); ok {
291+
v, e := semver.NewVersion(after)
292+
if e == nil && semv.Check(v) {
293+
semtags[i] = v
294+
i++
295+
}
296+
}
297+
}
298+
if i > 0 {
299+
semtags = semtags[:i]
300+
if i > 1 {
301+
sort.Sort(semver.Collection(semtags))
302+
}
303+
version = semtags[i-1].String()
304+
return
305+
}
306+
}
307+
}
308+
version = esm.PkgVersion
309+
return
310+
})
311+
}
312+
313+
func resolvePrPackageVersion(esm EsmPath) (version string, err error) {
314+
return withCache("pr/"+esm.PkgName+"@"+esm.PkgVersion, time.Duration(config.NpmQueryCacheTTL)*time.Second, func() (version string, aliasKey string, err error) {
315+
u, err := url.Parse(fmt.Sprintf("https://pkg.pr.new/%s@%s", esm.PkgName, esm.PkgVersion))
316+
if err != nil {
317+
return
318+
}
319+
versionRegex := regexp.MustCompile(`[^/]@([\da-f]{7,})$`)
320+
client, recycle := fetch.NewClient("esmd/"+VERSION, 30, false, nil)
321+
version = esm.PkgVersion
322+
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
323+
match := versionRegex.FindStringSubmatch(req.URL.Path)
324+
if len(match) > 1 {
325+
version = match[1][:7]
326+
}
327+
if len(via) >= 6 {
328+
return errors.New("too many redirects")
329+
}
330+
return nil
331+
}
332+
defer recycle()
333+
334+
_, err = client.Fetch(u, nil)
335+
return
336+
})
337+
}

server/router.go

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,53 @@ func esmRouter(db Database, esmStorage storage.Storage, logger *log.Logger) rex.
10261026
ctx.SetHeader("Cache-Control", fmt.Sprintf("public, max-age=%d", config.NpmQueryCacheTTL))
10271027
return redirect(ctx, fmt.Sprintf("%s%s/%s@%s%s%s", origin, registryPrefix, pkgName, pkgVersion, subPath, query), false)
10281028
}
1029-
} else {
1029+
}
1030+
1031+
// fix url that is related to `import.meta.url`
1032+
if hasTargetSegment && isExactVersion && pathKind == RawFile && !rawFlag {
1033+
extname := path.Ext(esm.SubPath)
1034+
dir := path.Join(npmrc.StoreDir(), esm.Name())
1035+
if !existsDir(dir) {
1036+
_, err := npmrc.installPackage(esm.Package())
1037+
if err != nil {
1038+
return rex.Status(500, err.Error())
1039+
}
1040+
}
1041+
pkgRoot := path.Join(dir, "node_modules", esm.PkgName)
1042+
files, err := findFiles(pkgRoot, "", func(fp string) bool {
1043+
return strings.HasSuffix(fp, extname)
1044+
})
1045+
if err != nil {
1046+
return rex.Status(500, err.Error())
1047+
}
1048+
var file string
1049+
if l := len(files); l == 1 {
1050+
file = files[0]
1051+
} else if l > 1 {
1052+
for _, f := range files {
1053+
if strings.HasSuffix(esm.SubPath, f) {
1054+
file = f
1055+
break
1056+
}
1057+
}
1058+
if file == "" {
1059+
for _, f := range files {
1060+
if path.Base(esm.SubPath) == path.Base(f) {
1061+
file = f
1062+
break
1063+
}
1064+
}
1065+
}
1066+
}
1067+
if file == "" {
1068+
return rex.Status(404, "File not found")
1069+
}
1070+
url := fmt.Sprintf("%s%s/%s@%s/%s", origin, registryPrefix, esm.PkgName, esm.PkgVersion, file)
1071+
return redirect(ctx, url, true)
1072+
}
1073+
1074+
// try to serve package static files if the version is exact
1075+
if isExactVersion {
10301076
// return wasm file as an es6 module when `?module` query is present (requires `top-level-await` support)
10311077
if pathKind == RawFile && strings.HasSuffix(esm.SubPath, ".wasm") && query.Has("module") {
10321078
wasmUrl := origin + pathname
@@ -1066,50 +1112,7 @@ func esmRouter(db Database, esmStorage storage.Storage, logger *log.Logger) rex.
10661112
return buf
10671113
}
10681114

1069-
// fix url that is related to `import.meta.url`
1070-
if hasTargetSegment && pathKind == RawFile && !rawFlag {
1071-
extname := path.Ext(esm.SubPath)
1072-
dir := path.Join(npmrc.StoreDir(), esm.Name())
1073-
if !existsDir(dir) {
1074-
_, err := npmrc.installPackage(esm.Package())
1075-
if err != nil {
1076-
return rex.Status(500, err.Error())
1077-
}
1078-
}
1079-
pkgRoot := path.Join(dir, "node_modules", esm.PkgName)
1080-
files, err := findFiles(pkgRoot, "", func(fp string) bool {
1081-
return strings.HasSuffix(fp, extname)
1082-
})
1083-
if err != nil {
1084-
return rex.Status(500, err.Error())
1085-
}
1086-
var file string
1087-
if l := len(files); l == 1 {
1088-
file = files[0]
1089-
} else if l > 1 {
1090-
for _, f := range files {
1091-
if strings.HasSuffix(esm.SubPath, f) {
1092-
file = f
1093-
break
1094-
}
1095-
}
1096-
if file == "" {
1097-
for _, f := range files {
1098-
if path.Base(esm.SubPath) == path.Base(f) {
1099-
file = f
1100-
break
1101-
}
1102-
}
1103-
}
1104-
}
1105-
if file == "" {
1106-
return rex.Status(404, "File not found")
1107-
}
1108-
url := fmt.Sprintf("%s%s/%s@%s/%s", origin, registryPrefix, esm.PkgName, esm.PkgVersion, file)
1109-
return redirect(ctx, url, true)
1110-
}
1111-
1112-
// package raw files
1115+
// serve package raw files
11131116
if pathKind == RawFile {
11141117
if esm.SubPath == "" {
11151118
b := &BuildContext{
@@ -1192,7 +1195,7 @@ func esmRouter(db Database, esmStorage storage.Storage, logger *log.Logger) rex.
11921195
return content // auto closed
11931196
}
11941197

1195-
// build/dts files
1198+
// serve build/dts files
11961199
if pathKind == EsmBuild || pathKind == EsmSourceMap || pathKind == EsmDts {
11971200
var savePath string
11981201
if asteriskPrefix {

0 commit comments

Comments
 (0)