mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-29 14:21:25 +00:00
fix(packages): accept npm "repository" and "bin" in string form (#38236)
## What npm allows `repository` and `bin` in `package.json` to be either an object or a plain string (npm docs: [repository](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#repository), [bin](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#bin)). The npm registry creator modeled `repository` as a struct and `bin` as `map[string]string`, so publishing a package whose `package.json` uses the string form failed with: ``` json: cannot unmarshal string into Go struct field PackageMetadataVersion.PackageMetadata.versions.bin of type map[string]string ``` ## Fix `modules/packages/npm/creator.go`: add `UnmarshalJSON` to `Repository` (string → `URL`) and a `Bin` type with `UnmarshalJSON` (string → a single command named after the package, per npm semantics), mirroring the existing `License` / `User` string-or-object handling. The stored `Metadata` field types are unchanged. `bundledDependencies` as a boolean (also noted in #38235) is left out of scope — it is rare and semantically different (`true` = bundle all deps). ## Test `TestParsePackage/ValidRepositoryAndBinAsString` parses a package with string `repository` and `bin`: it fails on `main` with the error above and passes with this change. The full `modules/packages/npm` suite is green and `gofmt` is clean. Fixes #38235 _AI disclosure: prepared with AI assistance; I reviewed and verified it (reproduction + tests) and can explain and defend the change._
This commit is contained in:
@@ -103,7 +103,7 @@ type PackageMetadataVersion struct {
|
||||
DevDependencies map[string]string `json:"devDependencies,omitempty"`
|
||||
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
|
||||
PeerDependenciesMeta map[string]any `json:"peerDependenciesMeta,omitempty"`
|
||||
Bin map[string]string `json:"bin,omitempty"`
|
||||
Bin Bin `json:"bin,omitempty"`
|
||||
OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Dist PackageDistribution `json:"dist"`
|
||||
@@ -188,6 +188,49 @@ type Repository struct {
|
||||
Directory string `json:"directory,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON is needed because the repository field can be a string or an object.
|
||||
func (r *Repository) UnmarshalJSON(data []byte) error {
|
||||
switch data[0] {
|
||||
case '"':
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
r.URL = value
|
||||
case '{':
|
||||
type repositoryAlias Repository // avoid recursion into this method
|
||||
var value repositoryAlias
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*r = Repository(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bin maps command names to executable files. npm also allows a single string,
|
||||
// in which case the command is named after the package (resolved in ParsePackage).
|
||||
type Bin map[string]string
|
||||
|
||||
// UnmarshalJSON is needed because the bin field can be a string or an object.
|
||||
func (b *Bin) UnmarshalJSON(data []byte) error {
|
||||
switch data[0] {
|
||||
case '"':
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*b = Bin{"": value}
|
||||
case '{':
|
||||
var value map[string]string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*b = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
|
||||
type PackageAttachment struct {
|
||||
ContentType string `json:"content_type"`
|
||||
@@ -229,6 +272,11 @@ func ParsePackage(r io.Reader) (*Package, error) {
|
||||
meta.Homepage = ""
|
||||
}
|
||||
|
||||
// A string "bin" means a single executable named after the package.
|
||||
if cmd, ok := meta.Bin[""]; ok && len(meta.Bin) == 1 {
|
||||
meta.Bin = Bin{name: cmd}
|
||||
}
|
||||
|
||||
p := &Package{
|
||||
Name: meta.Name,
|
||||
Version: v.String(),
|
||||
|
||||
@@ -326,4 +326,31 @@ func TestParsePackage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "MIT", string(p.Metadata.License))
|
||||
})
|
||||
|
||||
t.Run("ValidRepositoryAndBinAsString", func(t *testing.T) {
|
||||
// npm allows "repository" and "bin" to be plain strings, not only objects.
|
||||
packageJSON := `{
|
||||
"versions": {
|
||||
"0.1.1": {
|
||||
"name": "dev-null",
|
||||
"version": "0.1.1",
|
||||
"bin": "./cli.js",
|
||||
"repository": "https://gitea.io/gitea/test.git",
|
||||
"dist": {
|
||||
"integrity": "sha256-"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_attachments": {
|
||||
"foo": {
|
||||
"data": "AAAA"
|
||||
}
|
||||
}
|
||||
}`
|
||||
p, err := ParsePackage(strings.NewReader(packageJSON))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
|
||||
// a string bin is named after the package
|
||||
require.Equal(t, "./cli.js", p.Metadata.Bin["dev-null"])
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user