enhance(packages): implement npm single-version API and add per-version repository (#39267)

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
silverwind
2026-09-08 18:25:29 +00:00
committed by GitHub
co-authored by wxiaoguang Lunny Xiao
parent d93bd06d0c
commit 459fc13783
5 changed files with 81 additions and 23 deletions
+4 -3
View File
@@ -4,6 +4,7 @@
package npm
import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
@@ -13,7 +14,6 @@ import (
packages_model "gitea.dev/models/packages"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/setting"
)
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -78,6 +78,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
Maintainers: []npm_module.User{{Name: pd.Owner.Name}},
Homepage: metadata.ProjectURL,
License: metadata.License,
Repository: metadata.Repository,
Keywords: metadata.Keywords,
Dependencies: metadata.Dependencies,
BundleDependencies: metadata.BundleDependencies,
@@ -104,7 +105,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
}
}
func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
func createPackageSearchResponse(ctx context.Context, pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
objects := make([]*npm_module.PackageSearchObject, 0, len(pds))
for _, pd := range pds {
metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd)
@@ -126,7 +127,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
Maintainers: []npm_module.User{}, // npm cli needs this field
Keywords: metadata.Keywords,
Links: &npm_module.PackageSearchPackageLinks{
Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
Registry: buildNpmRegistryURL(ctx, pd.Owner),
Homepage: metadata.ProjectURL,
},
},
+17 -4
View File
@@ -9,6 +9,7 @@ import (
packages_model "gitea.dev/models/packages"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/timeutil"
@@ -17,13 +18,14 @@ import (
)
func TestCreatePackageMetadataResponse(t *testing.T) {
descriptor := func(v string, publishedUnix int64) *packages_model.PackageDescriptor {
repository := npm_module.Repository{Type: "git", URL: "https://gitea.dev/alice/test.git"}
descriptor := func(v string, publishedUnix int64, repo npm_module.Repository) *packages_model.PackageDescriptor {
return &packages_model.PackageDescriptor{
Package: &packages_model.Package{Name: "@scope/test"},
Owner: &user_model.User{Name: "alice"},
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
SemVer: version.Must(version.NewVersion(v)),
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}},
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}, Repository: repo},
Files: []*packages_model.PackageFileDescriptor{{
File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"},
Blob: &packages_model.PackageBlob{},
@@ -32,8 +34,8 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
}
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
descriptor("1.1.0", 1000),
descriptor("1.0.0", 2000),
descriptor("1.1.0", 1000, npm_module.Repository{}),
descriptor("1.0.0", 2000, repository),
})
assert.Equal(t, map[string]time.Time{
@@ -50,4 +52,15 @@ func TestCreatePackageMetadataResponse(t *testing.T) {
"https://gitea.dev/api/packages/alice/npm/@scope%2Ftest/-/1.0.0/test-1.0.0.tgz",
result.Versions["1.0.0"].Dist.Tarball,
)
assert.Equal(t, repository, result.Versions["1.0.0"].Repository)
withoutRepository, err := json.Marshal(result.Versions["1.1.0"])
assert.NoError(t, err)
assert.NotContains(t, string(withoutRepository), `"repository"`)
raw, err := json.Marshal(result)
assert.NoError(t, err)
doc := map[string]any{}
assert.NoError(t, json.Unmarshal(raw, &doc))
assert.NotContains(t, doc, "repository")
}
+40 -11
View File
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"gitea.dev/models/db"
@@ -17,11 +18,12 @@ import (
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
@@ -59,6 +61,10 @@ func packageNameFromParams(ctx *context.Context) string {
return fullOrSub // id is the full package name, e.g.: "@angular/core" or "lodash"
}
func buildNpmRegistryURL(ctx std_ctx.Context, owner *user_model.User) string {
return httplib.GuessCurrentAppURL(ctx) + "api/packages/" + url.PathEscape(owner.Name) + "/npm"
}
// PackageMetadata returns the metadata for a single package
func PackageMetadata(ctx *context.Context) {
packageName := packageNameFromParams(ctx)
@@ -79,16 +85,42 @@ func PackageMetadata(ctx *context.Context) {
return
}
resp := createPackageMetadataResponse(
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/npm",
pds,
)
resp := createPackageMetadataResponse(buildNpmRegistryURL(ctx, ctx.Package.Owner), pds)
ctx.JSON(http.StatusOK, resp)
}
// PackageVersionMetadata returns the metadata for a single version or dist-tag
func PackageVersionMetadata(ctx *context.Context) {
ctx.HTTPError(http.StatusNotImplemented, "not implemented")
versionOrTag := ctx.PathParam("version")
opts := &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNpm,
Name: packages_model.SearchValue{ExactMatch: true, Value: packageNameFromParams(ctx)},
IsInternal: optional.Some(false),
}
if _, err := version.NewVersion(versionOrTag); err == nil {
opts.Version = packages_model.SearchValue{ExactMatch: true, Value: versionOrTag}
} else { // a tag, since setPackageTag rejects version-like names
opts.Properties = map[string]string{npm_module.TagProperty: versionOrTag}
}
pvs, _, err := packages_model.SearchVersions(ctx, opts)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pvs) == 0 {
apiError(ctx, http.StatusNotFound, "version not found: "+versionOrTag)
return
}
pd, err := packages_model.GetPackageDescriptor(ctx, pvs[0])
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, createPackageMetadataVersion(buildNpmRegistryURL(ctx, ctx.Package.Owner), pd))
}
// DownloadPackageFile serves the content of a package
@@ -527,10 +559,7 @@ func PackageSearch(ctx *context.Context) {
return
}
resp := createPackageSearchResponse(
pds,
total,
)
resp := createPackageSearchResponse(ctx, pds, total)
ctx.JSON(http.StatusOK, resp)
}