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:
maximilize
2026-06-27 22:41:46 +02:00
committed by GitHub
parent 0f5102427e
commit d392fb1438
2 changed files with 76 additions and 1 deletions
+27
View File
@@ -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"])
})
}