From 646516ee58f6d83c6f0db1c4e75ca09524da2786 Mon Sep 17 00:00:00 2001 From: Je Xia Date: Mon, 6 Jan 2025 12:24:37 +0800 Subject: [PATCH] Support 'major.minor.patch+build' versioning (#985) --- server/build_resolver.go | 66 ++++++++++++++++++++++++++++--- server/npm.go | 42 ++++++++++++++++++++ server/path.go | 34 ++++++++-------- server/router.go | 12 +++--- server/utils.go | 84 ---------------------------------------- test/jsr/other.ts | 6 +++ 6 files changed, 132 insertions(+), 112 deletions(-) create mode 100644 test/jsr/other.ts diff --git a/server/build_resolver.go b/server/build_resolver.go index 291c063ee..d5279b037 100644 --- a/server/build_resolver.go +++ b/server/build_resolver.go @@ -12,9 +12,11 @@ import ( "strconv" "strings" + "github.com/Masterminds/semver/v3" "github.com/evanw/esbuild/pkg/api" "github.com/ije/gox/set" "github.com/ije/gox/utils" + "github.com/ije/gox/valid" ) // BuildEntry represents the build entrypoints of a module @@ -1204,11 +1206,11 @@ func (ctx *BuildContext) analyzeSplitting() (err error) { refs := map[string]Ref{} for _, exportName := range exportNames.Values() { - esmPath := ctx.esm - esmPath.SubPath = exportName - esmPath.SubModuleName = stripEntryModuleExt(exportName) + esm := ctx.esm + esm.SubPath = exportName + esm.SubModuleName = stripEntryModuleExt(exportName) b := &BuildContext{ - esm: esmPath, + esm: esm, npmrc: ctx.npmrc, args: ctx.args, externalAll: ctx.externalAll, @@ -1220,7 +1222,7 @@ func (ctx *BuildContext) analyzeSplitting() (err error) { } _, includes, err := b.buildModule(true) if err != nil { - return fmt.Errorf("failed to analyze %s: %v", esmPath.Specifier(), err) + return fmt.Errorf("failed to analyze %s: %v", esm.Specifier(), err) } for _, include := range includes { module, importer := include[0], include[1] @@ -1349,3 +1351,57 @@ func normalizeSavePath(zoneId string, pathname string) string { } return strings.Join(segs, "/") } + +// normalizeImportSpecifier normalizes the given specifier. +func normalizeImportSpecifier(specifier string) string { + specifier = strings.TrimPrefix(specifier, "npm:") + specifier = strings.TrimPrefix(specifier, "./node_modules/") + if specifier == "." { + specifier = "./index" + } else if specifier == ".." { + specifier = "../index" + } + if nodeBuiltinModules[specifier] { + return "node:" + specifier + } + return specifier +} + +// isHttpSepcifier returns true if the specifier is a remote URL. +func isHttpSepcifier(specifier string) bool { + return strings.HasPrefix(specifier, "https://") || strings.HasPrefix(specifier, "http://") +} + +// isRelPathSpecifier returns true if the specifier is a local path. +func isRelPathSpecifier(specifier string) bool { + return strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") +} + +// isAbsPathSpecifier returns true if the specifier is an absolute path. +func isAbsPathSpecifier(specifier string) bool { + return strings.HasPrefix(specifier, "/") || strings.HasPrefix(specifier, "file://") +} + +// isJsModuleSpecifier returns true if the specifier is a json module. +func isJsonModuleSpecifier(specifier string) bool { + if !strings.HasSuffix(specifier, ".json") { + return false + } + _, _, subpath, _ := splitEsmPath(specifier) + return subpath != "" && strings.HasSuffix(subpath, ".json") +} + +// isJsModuleSpecifier checks if the given specifier is a node.js built-in module. +func isNodeBuiltInModule(specifier string) bool { + return strings.HasPrefix(specifier, "node:") && nodeBuiltinModules[specifier[5:]] +} + +// isCommitish returns true if the given string is a commit hash. +func isCommitish(s string) bool { + return len(s) >= 7 && len(s) <= 40 && valid.IsHexString(s) && containsDigit(s) +} + +// 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)) +} diff --git a/server/npm.go b/server/npm.go index 47bffc539..c661a91f2 100644 --- a/server/npm.go +++ b/server/npm.go @@ -824,6 +824,48 @@ func isDistTag(s string) bool { } } +// 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 len(a[0]) == 0 || !isNumericString(a[0]) || len(a[1]) == 0 || !isNumericString(a[1]) { + return false + } + p := a[2] + if len(p) == 0 { + return false + } + patchEnd := false + for i, c := range p { + if !patchEnd { + if c == '-' || c == '+' { + if i == 0 || i == len(p)-1 { + return false + } + patchEnd = true + } else if c < '0' || c > '9' { + return false + } + } else { + if !(c == '.' || c == '_' || c == '-' || c == '+' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { + return false + } + } + } + return true +} + +func isNumericString(s string) bool { + for _, c := range s { + if c < '0' || c > '9' { + return false + } + } + return true +} + // based on https://github.com/npm/validate-npm-package-name func validatePackageName(pkgName string) bool { if len(pkgName) > 214 { diff --git a/server/path.go b/server/path.go index 3c9a32627..eb18c6e48 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, withExactVersion bool, hasTargetSegment bool, err error) { +func praseEsmPath(npmrc *NpmRC, pathname string) (esm 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/") { @@ -70,7 +70,7 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st } withExactVersion = true hasTargetSegment = validateTargetSegment(strings.Split(subPath, "/")) - esmPath = EsmPath{ + esm = EsmPath{ PkgName: pkgName, PkgVersion: version, SubPath: subPath, @@ -131,11 +131,11 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st } version, extraQuery := utils.SplitByFirstByte(maybeVersion, '&') - if v, e := url.QueryUnescape(version); e == nil { + if v, e := url.PathUnescape(version); e == nil { version = v } - esmPath = EsmPath{ + esm = EsmPath{ PkgName: pkgName, PkgVersion: version, SubPath: subPath, @@ -144,38 +144,38 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st } // workaround for es5-ext "../#/.." path - if esmPath.SubModuleName != "" && esmPath.PkgName == "es5-ext" { - esmPath.SubModuleName = strings.ReplaceAll(esmPath.SubModuleName, "/%23/", "/#/") + if esm.SubModuleName != "" && esm.PkgName == "es5-ext" { + esm.SubModuleName = strings.ReplaceAll(esm.SubModuleName, "/%23/", "/#/") } if ghPrefix { - if isCommitish(esmPath.PkgVersion) || isExactVersion(strings.TrimPrefix(esmPath.PkgVersion, "v")) { + if isCommitish(esm.PkgVersion) || isExactVersion(strings.TrimPrefix(esm.PkgVersion, "v")) { withExactVersion = true return } var refs []GitRef - refs, err = listRepoRefs(fmt.Sprintf("https://github.com/%s", esmPath.PkgName)) + refs, err = listRepoRefs(fmt.Sprintf("https://github.com/%s", esm.PkgName)) if err != nil { return } - if esmPath.PkgVersion == "" { + if esm.PkgVersion == "" { for _, ref := range refs { if ref.Ref == "HEAD" { - esmPath.PkgVersion = ref.Sha[:7] + esm.PkgVersion = ref.Sha[:7] return } } } else { // try to find the exact tag or branch for _, ref := range refs { - if ref.Ref == "refs/tags/"+esmPath.PkgVersion || ref.Ref == "refs/heads/"+esmPath.PkgVersion { - esmPath.PkgVersion = ref.Sha[:7] + if ref.Ref == "refs/tags/"+esm.PkgVersion || ref.Ref == "refs/heads/"+esm.PkgVersion { + esm.PkgVersion = ref.Sha[:7] return } } // try to find the semver tag var c *semver.Constraints - c, err = semver.NewConstraint(strings.TrimPrefix(esmPath.PkgVersion, "semver:")) + c, err = semver.NewConstraint(strings.TrimPrefix(esm.PkgVersion, "semver:")) if err == nil { vs := make([]*semver.Version, len(refs)) i := 0 @@ -193,7 +193,7 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st if i > 1 { sort.Sort(semver.Collection(vs)) } - esmPath.PkgVersion = vs[i-1].String() + esm.PkgVersion = vs[i-1].String() return } } @@ -202,12 +202,12 @@ func praseEsmPath(npmrc *NpmRC, pathname string) (esmPath EsmPath, extraQuery st return } - withExactVersion = isExactVersion(esmPath.PkgVersion) + withExactVersion = len(esm.PkgVersion) > 0 && isExactVersion(esm.PkgVersion) if !withExactVersion { var p *PackageJSON - p, err = npmrc.getPackageInfo(pkgName, esmPath.PkgVersion) + p, err = npmrc.getPackageInfo(pkgName, esm.PkgVersion) if err == nil { - esmPath.PkgVersion = p.Version + esm.PkgVersion = p.Version } } return diff --git a/server/router.go b/server/router.go index 407805877..2b8660c05 100644 --- a/server/router.go +++ b/server/router.go @@ -1711,17 +1711,17 @@ func esmRouter() rex.Handle { fmt.Fprintf(buf, "import \"%s\";\n", dep) } } - esmPath := buildCtx.Path() + esm := buildCtx.Path() if !ret.CJS && len(exports) > 0 { - esmPath += "?exports=" + strings.Join(exports, ",") + esm += "?exports=" + strings.Join(exports, ",") } - ctx.Header.Set("X-ESM-Path", esmPath) - fmt.Fprintf(buf, "export * from \"%s\";\n", esmPath) + ctx.Header.Set("X-ESM-Path", esm) + fmt.Fprintf(buf, "export * from \"%s\";\n", esm) if ret.ExportDefault && (len(exports) == 0 || stringInSlice(exports, "default")) { - fmt.Fprintf(buf, "export { default } from \"%s\";\n", esmPath) + fmt.Fprintf(buf, "export { default } from \"%s\";\n", esm) } if ret.CJS && len(exports) > 0 { - fmt.Fprintf(buf, "import _ from \"%s\";\n", esmPath) + fmt.Fprintf(buf, "import _ from \"%s\";\n", esm) fmt.Fprintf(buf, "export const { %s } = _;\n", strings.Join(exports, ", ")) } if !noDts && ret.Dts != "" { diff --git a/server/utils.go b/server/utils.go index 6a500a51b..b97ea017f 100644 --- a/server/utils.go +++ b/server/utils.go @@ -11,98 +11,14 @@ import ( "strings" "sync" - "github.com/Masterminds/semver/v3" - "github.com/ije/gox/utils" "github.com/ije/gox/valid" ) -// isHttpSepcifier returns true if the specifier is a remote URL. -func isHttpSepcifier(specifier string) bool { - return strings.HasPrefix(specifier, "https://") || strings.HasPrefix(specifier, "http://") -} - -// isRelPathSpecifier returns true if the specifier is a local path. -func isRelPathSpecifier(specifier string) bool { - return strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") -} - -// isAbsPathSpecifier returns true if the specifier is an absolute path. -func isAbsPathSpecifier(specifier string) bool { - return strings.HasPrefix(specifier, "/") || strings.HasPrefix(specifier, "file://") -} - -// isJsModuleSpecifier returns true if the specifier is a json module. -func isJsonModuleSpecifier(specifier string) bool { - if !strings.HasSuffix(specifier, ".json") { - return false - } - _, _, subpath, _ := splitEsmPath(specifier) - return subpath != "" && strings.HasSuffix(subpath, ".json") -} - -// isJsModuleSpecifier checks if the given specifier is a node.js built-in module. -func isNodeBuiltInModule(specifier string) bool { - return strings.HasPrefix(specifier, "node:") && nodeBuiltinModules[specifier[5:]] -} - -// normalizeImportSpecifier normalizes the given specifier. -func normalizeImportSpecifier(specifier string) string { - specifier = strings.TrimPrefix(specifier, "npm:") - specifier = strings.TrimPrefix(specifier, "./node_modules/") - if specifier == "." { - specifier = "./index" - } else if specifier == ".." { - specifier = "../index" - } - if nodeBuiltinModules[specifier] { - return "node:" + specifier - } - 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)) -} - // checks if the given hostname is a local address. func isLocalhost(hostname string) bool { return hostname == "localhost" || hostname == "127.0.0.1" || (valid.IsIPv4(hostname) && strings.HasPrefix(hostname, "192.168.")) } -// isCommitish returns true if the given string is a commit hash. -func isCommitish(s string) bool { - return len(s) >= 7 && len(s) <= 40 && valid.IsHexString(s) && containsDigit(s) -} - // isJsReservedWord returns true if the given string is a reserved word in JavaScript. func isJsReservedWord(word string) bool { switch word { diff --git a/test/jsr/other.ts b/test/jsr/other.ts new file mode 100644 index 000000000..0bd90f0af --- /dev/null +++ b/test/jsr/other.ts @@ -0,0 +1,6 @@ +import { assertExists } from "jsr:@std/assert"; + +Deno.test("jsr:@bids/schema", async () => { + const { schema } = await import("http://localhost:8080/jsr/@bids/schema@0.11.3+2"); + assertExists(schema.objects); +});