diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 2425e2f5ef2..c286c8d7543 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -66,7 +66,7 @@ type PackageMetadata struct { Time map[string]time.Time `json:"time,omitempty"` Homepage string `json:"homepage,omitempty"` Keywords []string `json:"keywords,omitempty"` - Repository Repository `json:"repository"` + Repository Repository `json:"repository,omitzero"` Author User `json:"author"` ReadmeFilename string `json:"readmeFilename,omitempty"` Users map[string]bool `json:"users,omitempty"` @@ -104,7 +104,7 @@ type PackageMetadataVersion struct { Author User `json:"author"` Homepage string `json:"homepage,omitempty"` License License `json:"license,omitempty"` - Repository Repository `json:"repository"` + Repository Repository `json:"repository,omitzero"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` BundleDependencies []string `json:"bundleDependencies,omitempty"` diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index a2768646cfd..b88a2cf70f8 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -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, }, }, diff --git a/routers/api/packages/npm/api_test.go b/routers/api/packages/npm/api_test.go index 09364a2bdda..5c0215f7c9c 100644 --- a/routers/api/packages/npm/api_test.go +++ b/routers/api/packages/npm/api_test.go @@ -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") } diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 980bb085763..0202f8abccf 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -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) } diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index 8928d448e85..6b7bd2c0171 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -182,9 +182,6 @@ func TestPackageNpm(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("%s/-/%s", root, filename)).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) assert.Equal(t, b, resp.Body.Bytes()) - - req = NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, packageVersion)).AddTokenAuth(token) - MakeRequest(t, req, http.StatusNotImplemented) } pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm) assert.NoError(t, err) @@ -236,6 +233,24 @@ func TestPackageNpm(t *testing.T) { assert.Empty(t, pmv.Deprecated) }) + t.Run("PackageVersionMetadata", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + for _, selector := range []string{packageVersion, packageTag} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, selector)).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + pmv := DecodeJSON(t, resp, &npm.PackageMetadataVersion{}) + assert.Equal(t, fmt.Sprintf("%s@%s", packageName, packageVersion), pmv.ID) + assert.Equal(t, npm.Repository{Type: repoType, URL: repoURL, Directory: repoDirectory}, pmv.Repository) + } + + for _, missing := range []string{"9.9.9", "no-such-tag"} { + req := NewRequest(t, "GET", root+"/"+missing).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + } + }) + t.Run("AddTag", func(t *testing.T) { defer tests.PrintCurrentTest(t)()