Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 'major.minor.patch+build' versioning #985

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions server/build_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down Expand Up @@ -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))
}
42 changes: 42 additions & 0 deletions server/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 17 additions & 17 deletions server/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/") {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
}
}
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
84 changes: 0 additions & 84 deletions server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions test/jsr/other.ts
Original file line number Diff line number Diff line change
@@ -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/[email protected]+2");
assertExists(schema.objects);
});
Loading