diff --git a/HOSTING.md b/HOSTING.md index 8e104802d..d0630f529 100644 --- a/HOSTING.md +++ b/HOSTING.md @@ -33,7 +33,7 @@ You will need [Go](https://golang.org/dl) 1.22+ to compile and run the server. go run main.go --config=config.json ``` -Then you can import `React` from . +Then you can import `React` from . ## Deploy the Server to a Single Machine diff --git a/server/build.go b/server/build.go index 02e0d4826..f9ed7ef8a 100644 --- a/server/build.go +++ b/server/build.go @@ -453,9 +453,9 @@ func (ctx *BuildContext) buildModule(analyzeMode bool) (meta *BuildMeta, include pkgName, _, subPath, _ := splitEsmPath(specifier) if pkgName == ctx.esm.PkgName { version = ctx.esm.PkgVersion - } else if v, ok := ctx.pkgJson.Dependencies[pkgName]; ok && regexpVersionStrict.MatchString(v) { + } else if v, ok := ctx.pkgJson.Dependencies[pkgName]; ok && isExactVersion(v) { version = v - } else if v, ok := ctx.pkgJson.PeerDependencies[pkgName]; ok && regexpVersionStrict.MatchString(v) { + } else if v, ok := ctx.pkgJson.PeerDependencies[pkgName]; ok && isExactVersion(v) { version = v } p := pkgName @@ -866,7 +866,7 @@ func (ctx *BuildContext) buildModule(analyzeMode bool) (meta *BuildMeta, include } else if version, ok := ctx.pkgJson.PeerDependencies["svelte"]; ok { svelteVersion = version } - if !regexpVersionStrict.MatchString(svelteVersion) { + if !isExactVersion(svelteVersion) { info, err := ctx.npmrc.getPackageInfo("svelte", svelteVersion) if err != nil { return esbuild.OnLoadResult{}, errors.New("failed to get svelte package info") @@ -900,7 +900,7 @@ func (ctx *BuildContext) buildModule(analyzeMode bool) (meta *BuildMeta, include } else if version, ok := ctx.pkgJson.PeerDependencies["vue"]; ok { vueVersion = version } - if !regexpVersionStrict.MatchString(vueVersion) { + if !isExactVersion(vueVersion) { info, err := ctx.npmrc.getPackageInfo("vue", vueVersion) if err != nil { return esbuild.OnLoadResult{}, errors.New("failed to get vue package info") diff --git a/server/build_resolver.go b/server/build_resolver.go index 1a5d18298..940c21883 100644 --- a/server/build_resolver.go +++ b/server/build_resolver.go @@ -923,11 +923,11 @@ func (ctx *BuildContext) resolveExternalModule(specifier string, kind api.Resolv var isFixedVersion bool if dep.GhPrefix { - isFixedVersion = isCommitish(dep.PkgVersion) || regexpVersionStrict.MatchString(strings.TrimPrefix(dep.PkgVersion, "v")) + isFixedVersion = isCommitish(dep.PkgVersion) || isExactVersion(strings.TrimPrefix(dep.PkgVersion, "v")) } else if dep.PrPrefix { isFixedVersion = true } else { - isFixedVersion = regexpVersionStrict.MatchString(dep.PkgVersion) + isFixedVersion = isExactVersion(dep.PkgVersion) } if isFixedVersion { buildArgsPrefix := "" @@ -1013,7 +1013,7 @@ func (ctx *BuildContext) resloveDTS(entry BuildEntry) (string, error) { } // lookup types in @types scope - if pkgJson := ctx.pkgJson; pkgJson.Types == "" && !strings.HasPrefix(pkgJson.Name, "@types/") && regexpVersionStrict.MatchString(pkgJson.Version) { + if pkgJson := ctx.pkgJson; pkgJson.Types == "" && !strings.HasPrefix(pkgJson.Name, "@types/") && isExactVersion(pkgJson.Version) { versionParts := strings.Split(pkgJson.Version, ".") versions := []string{ versionParts[0] + "." + versionParts[1], // major.minor diff --git a/server/cjs_module_lexer.go b/server/cjs_module_lexer.go index b729ed273..2e23f9e3b 100644 --- a/server/cjs_module_lexer.go +++ b/server/cjs_module_lexer.go @@ -140,7 +140,7 @@ RETRY: if strings.HasPrefix(line, "@") { ret.Reexport = line[1:] break - } else if regexpJSIdent.MatchString(line) && !isJsReservedWord(line) { + } else if isJsIdentifier(line) && !isJsReservedWord(line) { ret.Exports = append(ret.Exports, line) } } diff --git a/server/legacy_router.go b/server/legacy_router.go index 783003210..e904159a6 100644 --- a/server/legacy_router.go +++ b/server/legacy_router.go @@ -107,10 +107,9 @@ func legacyESM(ctx *rex.Context, pathname string) any { if ctx.R.URL.RawQuery != "" { query = "?" + ctx.R.URL.RawQuery } - isFixedVersion := regexpVersionStrict.MatchString(pkgVersion) - if !isFixedVersion { + if !isExactVersion(pkgVersion) { npmrc := DefaultNpmRC() - pkgInfo, err := npmrc.fetchPackageInfo(pkgName, pkgVersion) + pkgInfo, err := npmrc.getPackageInfo(pkgName, pkgVersion) if err != nil { if strings.Contains(err.Error(), " not found") { return rex.Status(404, err.Error()) diff --git a/server/loader_implements.go b/server/loader_implements.go index 4d00893fe..a5520bef7 100644 --- a/server/loader_implements.go +++ b/server/loader_implements.go @@ -79,7 +79,7 @@ func resolveSvelteVersion(npmrc *NpmRC, importMap common.ImportMap) (svelteVersi } } } - if !regexpVersionStrict.MatchString(svelteVersion) { + if !isExactVersion(svelteVersion) { var info *PackageJSON info, err = npmrc.getPackageInfo("svelte", svelteVersion) if err != nil { @@ -282,7 +282,7 @@ func resolveVueVersion(npmrc *NpmRC, importMap common.ImportMap) (vueVersion str } } } - if !regexpVersionStrict.MatchString(vueVersion) { + if !isExactVersion(vueVersion) { var info *PackageJSON info, err = npmrc.getPackageInfo("vue", vueVersion) if err != nil { diff --git a/server/npm.go b/server/npm.go index be4b7b291..47bffc539 100644 --- a/server/npm.go +++ b/server/npm.go @@ -30,8 +30,9 @@ const ( ) var ( - npmNaming = valid.Validator{valid.Range{'a', 'z'}, valid.Range{'A', 'Z'}, valid.Range{'0', '9'}, valid.Eq('_'), valid.Eq('$'), valid.Eq('.'), valid.Eq('-'), valid.Eq('+'), valid.Eq('!'), valid.Eq('~'), valid.Eq('*'), valid.Eq('('), valid.Eq(')')} - installMutex = syncx.KeyedMutex{} + npmNaming = valid.Validator{valid.Range{'a', 'z'}, valid.Range{'A', 'Z'}, valid.Range{'0', '9'}, valid.Eq('_'), valid.Eq('.'), valid.Eq('-'), valid.Eq('+'), valid.Eq('$'), valid.Eq('!')} + npmVersioning = valid.Validator{valid.Range{'a', 'z'}, valid.Range{'A', 'Z'}, valid.Range{'0', '9'}, valid.Eq('_'), valid.Eq('.'), valid.Eq('-'), valid.Eq('+')} + installMutex = syncx.KeyedMutex{} ) type Package struct { @@ -330,26 +331,6 @@ func (rc *NpmRC) StoreDir() string { return path.Join(config.WorkDir, "npm") } -func (rc *NpmRC) getPackageInfo(name string, semver string) (info *PackageJSON, err error) { - // strip leading `=` or `v` from semver - if (strings.HasPrefix(semver, "=") || strings.HasPrefix(semver, "v")) && regexpVersionStrict.MatchString(semver[1:]) { - semver = semver[1:] - } - - // check if the package has been installed - if regexpVersionStrict.MatchString(semver) { - var raw PackageJSONRaw - pkgJsonPath := path.Join(rc.StoreDir(), name+"@"+semver, "node_modules", name, "package.json") - if utils.ParseJSONFile(pkgJsonPath, &raw) == nil { - info = raw.ToNpmPackage() - return - } - } - - info, err = rc.fetchPackageInfo(name, semver) - return -} - func (npmrc *NpmRC) getRegistryByPackageName(packageName string) *NpmRegistry { if strings.HasPrefix(packageName, "@") { scope, _ := utils.SplitByFirstByte(packageName, '/') @@ -361,31 +342,28 @@ func (npmrc *NpmRC) getRegistryByPackageName(packageName string) *NpmRegistry { return &npmrc.NpmRegistry } -func (npmrc *NpmRC) fetchPackageInfo(packageName string, semverOrDistTag string) (packageJson *PackageJSON, err error) { - a := strings.Split(strings.Trim(packageName, "/"), "/") - packageName = a[0] - if strings.HasPrefix(packageName, "@") && len(a) > 1 { - packageName = a[0] + "/" + a[1] +func (npmrc *NpmRC) getPackageInfo(pkgName string, version string) (packageJson *PackageJSON, err error) { + reg := npmrc.getRegistryByPackageName(pkgName) + getCacheKey := func(pkgName string, pkgVersion string) string { + return reg.Registry + pkgName + "@" + pkgVersion } - if semverOrDistTag == "" || semverOrDistTag == "*" { - semverOrDistTag = "latest" - } else if (strings.HasPrefix(semverOrDistTag, "=") || strings.HasPrefix(semverOrDistTag, "v")) && regexpVersionStrict.MatchString(semverOrDistTag[1:]) { - // strip leading `=` or `v` from semver - semverOrDistTag = semverOrDistTag[1:] - } - - reg := npmrc.getRegistryByPackageName(packageName) - getCacheKey := func(packageName string, packageVersion string) string { - return reg.Registry + packageName + "@" + packageVersion + "?auth=" + reg.Token + "|" + reg.User + ":" + reg.Password - } + version = normalizePackageVersion(version) + return withCache(getCacheKey(pkgName, version), time.Duration(config.NpmQueryCacheTTL)*time.Second, func() (*PackageJSON, string, error) { + // check if the package has been installed + if !isDistTag(version) && isExactVersion(version) { + var raw PackageJSONRaw + pkgJsonPath := path.Join(npmrc.StoreDir(), pkgName+"@"+version, "node_modules", pkgName, "package.json") + if utils.ParseJSONFile(pkgJsonPath, &raw) == nil { + return raw.ToNpmPackage(), "", nil + } + } - return withCache(getCacheKey(packageName, semverOrDistTag), time.Duration(config.NpmQueryCacheTTL)*time.Second, func() (*PackageJSON, string, error) { - regUrl := reg.Registry + packageName - isWellknownVersion := (regexpVersionStrict.MatchString(semverOrDistTag) || isDistTag(semverOrDistTag)) && strings.HasPrefix(regUrl, npmRegistry) + regUrl := reg.Registry + pkgName + isWellknownVersion := (isExactVersion(version) || isDistTag(version)) && strings.HasPrefix(regUrl, npmRegistry) if isWellknownVersion { // npm registry supports url like `https://registry.npmjs.org//` - regUrl += "/" + semverOrDistTag + regUrl += "/" + version } u, err := url.Parse(regUrl) @@ -418,16 +396,16 @@ func (npmrc *NpmRC) fetchPackageInfo(packageName string, semverOrDistTag string) if res.StatusCode == 404 || res.StatusCode == 401 { if isWellknownVersion { - err = fmt.Errorf("version %s of '%s' not found", semverOrDistTag, packageName) + err = fmt.Errorf("version %s of '%s' not found", version, pkgName) } else { - err = fmt.Errorf("package '%s' not found", packageName) + err = fmt.Errorf("package '%s' not found", pkgName) } return nil, "", err } if res.StatusCode != 200 { msg, _ := io.ReadAll(res.Body) - return nil, "", fmt.Errorf("could not get metadata of package '%s' (%s: %s)", packageName, res.Status, string(msg)) + return nil, "", fmt.Errorf("could not get metadata of package '%s' (%s: %s)", pkgName, res.Status, string(msg)) } if isWellknownVersion { @@ -436,7 +414,7 @@ func (npmrc *NpmRC) fetchPackageInfo(packageName string, semverOrDistTag string) if err != nil { return nil, "", err } - return raw.ToNpmPackage(), getCacheKey(packageName, raw.Version), nil + return raw.ToNpmPackage(), getCacheKey(pkgName, raw.Version), nil } var metadata NpmPackageMetadata @@ -446,32 +424,32 @@ func (npmrc *NpmRC) fetchPackageInfo(packageName string, semverOrDistTag string) } if len(metadata.Versions) == 0 { - return nil, "", fmt.Errorf("version %s of '%s' not found", semverOrDistTag, packageName) + return nil, "", fmt.Errorf("version %s of '%s' not found", version, pkgName) } CHECK: - distVersion, ok := metadata.DistTags[semverOrDistTag] + distVersion, ok := metadata.DistTags[version] if ok { raw, ok := metadata.Versions[distVersion] if ok { - return raw.ToNpmPackage(), getCacheKey(packageName, raw.Version), nil + return raw.ToNpmPackage(), getCacheKey(pkgName, raw.Version), nil } } else { - if semverOrDistTag == "lastest" { - return nil, "", fmt.Errorf("version %s of '%s' not found", semverOrDistTag, packageName) + if version == "lastest" { + return nil, "", fmt.Errorf("version %s of '%s' not found", version, pkgName) } var c *semver.Constraints - c, err = semver.NewConstraint(semverOrDistTag) + c, err = semver.NewConstraint(version) if err != nil { // fallback to latest if semverOrDistTag is not a valid semver - semverOrDistTag = "latest" + version = "latest" goto CHECK } vs := make([]*semver.Version, len(metadata.Versions)) i := 0 for v := range metadata.Versions { // ignore prerelease versions - if !strings.ContainsRune(semverOrDistTag, '-') && strings.ContainsRune(v, '-') { + if !strings.ContainsRune(version, '-') && strings.ContainsRune(v, '-') { continue } var ver *semver.Version @@ -491,16 +469,16 @@ func (npmrc *NpmRC) fetchPackageInfo(packageName string, semverOrDistTag string) } raw, ok := metadata.Versions[vs[i-1].String()] if ok { - return raw.ToNpmPackage(), getCacheKey(packageName, raw.Version), nil + return raw.ToNpmPackage(), getCacheKey(pkgName, raw.Version), nil } } } - return nil, "", fmt.Errorf("version %s of '%s' not found", semverOrDistTag, packageName) + return nil, "", fmt.Errorf("version %s of '%s' not found", version, pkgName) }) } -func (rc *NpmRC) installPackage(pkg Package) (packageJson *PackageJSON, err error) { - installDir := path.Join(rc.StoreDir(), pkg.String()) +func (npmrc *NpmRC) installPackage(pkg Package) (packageJson *PackageJSON, err error) { + installDir := path.Join(npmrc.StoreDir(), pkg.String()) packageJsonPath := path.Join(installDir, "node_modules", pkg.Name, "package.json") // check if the package has been installed @@ -533,16 +511,16 @@ func (rc *NpmRC) installPackage(pkg Package) (packageJson *PackageJSON, err erro fmt.Fprintf(f, `{"name":"%s","version":"%s"}`, pkg.Name, pkg.Version) } } else if pkg.PkgPrNew { - err = rc.downloadTarball(&NpmRegistry{}, installDir, pkg.Name, "https://pkg.pr.new/"+pkg.Name+"@"+pkg.Version) + err = fetchPackageTarball(&NpmRegistry{}, installDir, pkg.Name, "https://pkg.pr.new/"+pkg.Name+"@"+pkg.Version) } else { - info, fetchErr := rc.fetchPackageInfo(pkg.Name, pkg.Version) + info, fetchErr := npmrc.getPackageInfo(pkg.Name, pkg.Version) if fetchErr != nil { return nil, fetchErr } if info.Deprecated != "" { os.WriteFile(path.Join(installDir, "deprecated.txt"), []byte(info.Deprecated), 0644) } - err = rc.downloadTarball(rc.getRegistryByPackageName(pkg.Name), installDir, info.Name, info.Dist.Tarball) + err = fetchPackageTarball(npmrc.getRegistryByPackageName(pkg.Name), installDir, info.Name, info.Dist.Tarball) } if err != nil { return @@ -559,7 +537,7 @@ func (rc *NpmRC) installPackage(pkg Package) (packageJson *PackageJSON, err erro return } -func (rc *NpmRC) installDependencies(wd string, pkgJson *PackageJSON, npmMode bool, mark *set.Set[string]) { +func (npmrc *NpmRC) installDependencies(wd string, pkgJson *PackageJSON, npmMode bool, mark *set.Set[string]) { wg := sync.WaitGroup{} dependencies := map[string]string{} for name, version := range pkgJson.Dependencies { @@ -590,8 +568,8 @@ func (rc *NpmRC) installDependencies(wd string, pkgJson *PackageJSON, npmMode bo // skip installing `@types/*` packages return } - if !regexpVersionStrict.MatchString(pkg.Version) && !pkg.Github && !pkg.PkgPrNew { - p, e := rc.fetchPackageInfo(pkg.Name, pkg.Version) + if !isExactVersion(pkg.Version) && !pkg.Github && !pkg.PkgPrNew { + p, e := npmrc.getPackageInfo(pkg.Name, pkg.Version) if e != nil { return } @@ -602,7 +580,7 @@ func (rc *NpmRC) installDependencies(wd string, pkgJson *PackageJSON, npmMode bo return } mark.Add(markId) - installed, err := rc.installPackage(pkg) + installed, err := npmrc.installPackage(pkg) if err != nil { return } @@ -613,18 +591,31 @@ func (rc *NpmRC) installDependencies(wd string, pkgJson *PackageJSON, npmMode bo if strings.ContainsRune(name, '/') { ensureDir(path.Dir(linkDir)) } - os.Symlink(path.Join(rc.StoreDir(), pkg.String(), "node_modules", pkg.Name), linkDir) + os.Symlink(path.Join(npmrc.StoreDir(), pkg.String(), "node_modules", pkg.Name), linkDir) } // install dependencies recursively if len(installed.Dependencies) > 0 || (len(installed.PeerDependencies) > 0 && npmMode) { - rc.installDependencies(wd, installed, npmMode, mark) + npmrc.installDependencies(wd, installed, npmMode, mark) } }(name, version) } wg.Wait() } -func (rc *NpmRC) downloadTarball(reg *NpmRegistry, installDir string, pkgName string, tarballUrl string) (err error) { +// If the package is deprecated, a depreacted.txt file will be created by the `intallPackage` function +func (npmrc *NpmRC) isDeprecated(pkgName string, pkgVersion string) (string, error) { + installDir := path.Join(npmrc.StoreDir(), pkgName+"@"+pkgVersion) + data, err := os.ReadFile(path.Join(installDir, "deprecated.txt")) + if err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", err + } + return string(data), nil +} + +func fetchPackageTarball(reg *NpmRegistry, installDir string, pkgName string, tarballUrl string) (err error) { u, err := url.Parse(tarballUrl) if err != nil { return @@ -725,19 +716,6 @@ func extractPackageTarball(installDir string, packname string, tarball io.Reader return nil } -// If the package is deprecated, a depreacted.txt file will be created by the `intallPackage` function -func (npmrc *NpmRC) isDeprecated(pkgName string, pkgVersion string) (string, error) { - installDir := path.Join(npmrc.StoreDir(), pkgName+"@"+pkgVersion) - data, err := os.ReadFile(path.Join(installDir, "deprecated.txt")) - if err != nil { - if os.IsNotExist(err) { - return "", nil - } - return "", err - } - return string(data), nil -} - // resolveDependencyVersion resolves the version of a dependency // e.g. "react": "npm:react@19.0.0" // e.g. "react": "github:facebook/react#semver:19.0.0" @@ -803,7 +781,7 @@ func resolveDependencyVersion(v string) (Package, error) { return Package{}, errors.New("unsupported http dependency") } version, _ := utils.SplitByFirstByte(rest, '/') - if version == "" || !regexpVersion.MatchString(version) { + if version == "" || !npmNaming.Match(version) { return Package{}, errors.New("unsupported http dependency") } return Package{ @@ -824,6 +802,19 @@ func resolveDependencyVersion(v string) (Package, error) { return Package{}, nil } +func normalizePackageVersion(version string) string { + // strip leading `=` or `v` + if strings.HasPrefix(version, "=") { + version = version[1:] + } else if strings.HasPrefix(version, "v") && isExactVersion(version[1:]) { + version = version[1:] + } + if version == "" || version == "*" { + return "latest" + } + return version +} + func isDistTag(s string) bool { switch s { case "latest", "next", "beta", "alpha", "canary", "rc", "experimental": diff --git a/server/path.go b/server/path.go index 0321dd770..f0aadf796 100644 --- a/server/path.go +++ b/server/path.go @@ -50,7 +50,7 @@ func (p EsmPath) Specifier() string { return p.Name() } -func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery string, isFixedVersion bool, hasTargetSegment bool, err error) { +func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery string, withExactVersion bool, hasTargetSegment bool, err error) { // see https://pkg.pr.new if strings.HasPrefix(pathname, "/pr/") || strings.HasPrefix(pathname, "/pkg.pr.new/") { if strings.HasPrefix(pathname, "/pr/") { @@ -64,12 +64,12 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st return } version, subPath := utils.SplitByFirstByte(rest, '/') - if version == "" || !regexpVersion.MatchString(version) { + if version == "" || !npmVersioning.Match(version) { err = errors.New("invalid path") return } + withExactVersion = true hasTargetSegment = validateTargetSegment(strings.Split(subPath, "/")) - isFixedVersion = true esmPath = EsmPath{ PkgName: pkgName, PkgVersion: version, @@ -139,8 +139,8 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st } if ghPrefix { - if isCommitish(esmPath.PkgVersion) || regexpVersionStrict.MatchString(strings.TrimPrefix(esmPath.PkgVersion, "v")) { - isFixedVersion = true + if isCommitish(esmPath.PkgVersion) || isExactVersion(strings.TrimPrefix(esmPath.PkgVersion, "v")) { + withExactVersion = true return } var refs []GitRef @@ -192,10 +192,10 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st return } - isFixedVersion = regexpVersionStrict.MatchString(esmPath.PkgVersion) - if !isFixedVersion { + withExactVersion = isExactVersion(esmPath.PkgVersion) + if !withExactVersion { var p *PackageJSON - p, err = npmrc.fetchPackageInfo(pkgName, esmPath.PkgVersion) + p, err = npmrc.getPackageInfo(pkgName, esmPath.PkgVersion) if err == nil { esmPath.PkgVersion = p.Version } diff --git a/server/router.go b/server/router.go index 10f4ab0ae..008c20845 100644 --- a/server/router.go +++ b/server/router.go @@ -13,7 +13,6 @@ import ( "net/url" "os" "path" - "regexp" "sort" "strings" "syscall" @@ -56,12 +55,6 @@ const ( ctTypeScript = "application/typescript; charset=utf-8" ) -var ( - regexpVersion = regexp.MustCompile(`^[\w\+\-\.]+$`) - regexpVersionStrict = regexp.MustCompile(`^\d+\.\d+\.\d+(-[\w\+\-\.]+)?$`) - regexpJSIdent = regexp.MustCompile(`^[a-zA-Z_$][\w$]*$`) -) - func esmRouter() rex.Handle { var ( startTime = time.Now() @@ -162,7 +155,7 @@ func esmRouter() rex.Handle { if packageName == "" { return rex.Err(400, "param `package` is required") } - if version != "" && !regexpVersion.MatchString(version) { + if version != "" && !npmVersioning.Match(version) { return rex.Err(400, "invalid version") } prefix := "" @@ -556,7 +549,7 @@ func esmRouter() rex.Handle { target = "es2022" } v := query.Get("v") - if v != "" && (!regexpVersion.MatchString(v) || len(v) > 32) { + if v != "" && (!npmVersioning.Match(v) || len(v) > 32) { return rex.Status(400, "Invalid Version Param") } fetchClient, recycle := NewFetchClient(15, ctx.UserAgent()) @@ -851,7 +844,7 @@ func esmRouter() rex.Handle { pathname = "/pr/" + pathname[13:] } - esm, extraQuery, isFixedVersion, hasTargetSegment, err := praseEsmPath(npmrc, pathname) + esm, extraQuery, isExactVersion, hasTargetSegment, err := praseEsmPath(npmrc, pathname) if err != nil { status := 500 message := err.Error() @@ -889,16 +882,21 @@ func esmRouter() rex.Handle { types = info.Types } else if info.Typings != "" { types = info.Typings - } else if info.Main != "" && strings.HasSuffix(info.Main, ".d.ts") { + } else if info.Main != "" && endsWith(info.Main, ".d.ts", ".d.mts") { types = info.Main } - return redirect(ctx, fmt.Sprintf("%s/%s@%s%s", origin, info.Name, info.Version, utils.NormalizePathname(types)), isFixedVersion) + if strings.HasSuffix(types, ".d") { + types += ".ts" + } else if !endsWith(types, ".d.ts", ".d.mts") { + types += ".d.ts" + } + return redirect(ctx, fmt.Sprintf("%s/%s@%s%s", origin, info.Name, info.Version, utils.NormalizePathname(types)), isExactVersion) } // redirect to the main css path for CSS packages if css := cssPackages[esm.PkgName]; css != "" && esm.SubModuleName == "" { url := fmt.Sprintf("%s/%s/%s", origin, esm.Specifier(), css) - return redirect(ctx, url, isFixedVersion) + return redirect(ctx, url, isExactVersion) } // store the raw query @@ -974,8 +972,8 @@ func esmRouter() rex.Handle { pathKind = RawFile } - // redirect to the url with fixed package version - if !isFixedVersion { + // redirect to the url with exact package version + if !isExactVersion { if hasTargetSegment { pkgName := esm.Name() subPath := "" @@ -1230,7 +1228,7 @@ func esmRouter() rex.Handle { if query.Has("exports") { for _, p := range strings.Split(query.Get("exports"), ",") { p = strings.TrimSpace(p) - if regexpJSIdent.MatchString(p) { + if isJsIdentifier(p) { jsIndentSet.Add(p) } } @@ -1302,8 +1300,8 @@ func esmRouter() rex.Handle { target = getBuildTargetByUA(ctx.UserAgent()) } - // redirect to the url with fixed package version for `deno` and `denonext` target - if !isFixedVersion && (target == "denonext" || target == "deno") { + // redirect to the url with exact package version for `deno` and `denonext` target + if !isExactVersion && (target == "denonext" || target == "deno") { pkgName := esm.PkgName pkgVersion := esm.PkgVersion subPath := "" @@ -1544,7 +1542,7 @@ func esmRouter() rex.Handle { target = maybeTarget } else { url := fmt.Sprintf("%s/%s", origin, esm.Specifier()) - return redirect(ctx, url, isFixedVersion) + return redirect(ctx, url, isExactVersion) } } else { if submodule == basename { @@ -1616,7 +1614,7 @@ func esmRouter() rex.Handle { return rex.Status(404, "Package CSS not found") } url := fmt.Sprintf("%s%s.css", origin, strings.TrimSuffix(buildCtx.Path(), ".mjs")) - return redirect(ctx, url, isFixedVersion) + return redirect(ctx, url, isExactVersion) } // check `?exports` query @@ -1624,7 +1622,7 @@ func esmRouter() rex.Handle { if query.Has("exports") { for _, p := range strings.Split(query.Get("exports"), ",") { p = strings.TrimSpace(p) - if regexpJSIdent.MatchString(p) { + if isJsIdentifier(p) { jsIdentSet.Add(p) } } @@ -1738,7 +1736,7 @@ func esmRouter() rex.Handle { if targetFromUA { appendVaryHeader(ctx.W.Header(), "User-Agent") } - if isFixedVersion { + if isExactVersion { ctx.Header.Set("Cache-Control", ccImmutable) } else { ctx.Header.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", config.NpmQueryCacheTTL)) diff --git a/server/utils.go b/server/utils.go index 024697621..6a500a51b 100644 --- a/server/utils.go +++ b/server/utils.go @@ -12,6 +12,7 @@ import ( "sync" "github.com/Masterminds/semver/v3" + "github.com/ije/gox/utils" "github.com/ije/gox/valid" ) @@ -59,6 +60,34 @@ func normalizeImportSpecifier(specifier string) string { return specifier } +// isExactVersion returns true if the given version is an exact version. +func isExactVersion(version string) bool { + a := strings.SplitN(version, ".", 3) + if len(a) != 3 { + return false + } + if !valid.IsDigtalOnlyString(a[0]) || !valid.IsDigtalOnlyString(a[1]) { + return false + } + p := a[2] + if len(p) == 0 { + return false + } + d, e := utils.SplitByFirstByte(p, '-') + if !valid.IsDigtalOnlyString(d) { + return false + } + if e == "" { + return p[len(p)-1] != '-' + } + for _, c := range e { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-' || c == '+') { + return false + } + } + return true +} + // semverLessThan returns true if the version a is less than the version b. func semverLessThan(a string, b string) bool { return semver.MustParse(a).LessThan(semver.MustParse(b)) @@ -83,6 +112,24 @@ func isJsReservedWord(word string) bool { return false } +// isJsIdentifier returns true if the given string is a valid JavaScript identifier. +func isJsIdentifier(s string) bool { + if len(s) == 0 { + return false + } + leadingChar := s[0] + if !((leadingChar >= 'a' && leadingChar <= 'z') || (leadingChar >= 'A' && leadingChar <= 'Z') || leadingChar == '_' || leadingChar == '$') { + return false + } + for i := 1; i < len(s); i++ { + c := s[i] + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '$') { + return false + } + } + return true +} + // endsWith returns true if the given string ends with any of the suffixes. func endsWith(s string, suffixs ...string) bool { for _, suffix := range suffixs {