Skip to content

Commit 059c125

Browse files
authored
fix(npm): handle legacy array-form engines field (#47)
Early npm packages (e.g. lodash 0.x) publish `engines` as a legacy array (`["node","rhino"]`) instead of the modern object form (`{"node":">=4"}`). The struct typed it as `map[string]string`, so unmarshalling failed with "cannot unmarshal array into Go struct field versionInfo.versions.engines of type map[string]string" and FetchVersions returned an error for the entire package. Relax the field to `interface{}` (matching Homepage, License, Keywords and Funding, which already tolerate variable shapes). The value only flows opaquely into Version.Metadata["engines"], so both the array and object forms now round-trip without affecting any logic. Adds TestFetchVersions_LegacyEnginesArray as a regression test covering both the legacy array and modern object forms.
1 parent d97553a commit 059c125

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

internal/npm/npm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type versionInfo struct {
7575
Dist distInfo `json:"dist"`
7676
Maintainers []maintainerInfo `json:"maintainers"`
7777
NpmUser map[string]interface{} `json:"_npmUser"`
78-
Engines map[string]string `json:"engines"`
78+
Engines interface{} `json:"engines"`
7979
Funding interface{} `json:"funding"`
8080
}
8181

internal/npm/npm_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,69 @@ func TestFetchVersions_NoProvenance(t *testing.T) {
162162
}
163163
}
164164

165+
// TestFetchVersions_LegacyEnginesArray verifies that versions whose
166+
// "engines" field is a legacy array (e.g. ["node","rhino"], as published
167+
// by early lodash 0.x releases) are parsed without error. The struct field
168+
// must accept both the modern object form and the legacy array form.
169+
func TestFetchVersions_LegacyEnginesArray(t *testing.T) {
170+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
171+
resp := map[string]interface{}{
172+
"_id": "legacy",
173+
"name": "legacy",
174+
"dist-tags": map[string]string{"latest": "0.1.0"},
175+
"versions": map[string]interface{}{
176+
"0.1.0": map[string]interface{}{
177+
"name": "legacy",
178+
"version": "0.1.0",
179+
"license": "MIT",
180+
"engines": []string{"node", "rhino"}, // legacy array form
181+
"dist": map[string]interface{}{"integrity": "sha512-legacy"},
182+
},
183+
"1.0.0": map[string]interface{}{
184+
"name": "legacy",
185+
"version": "1.0.0",
186+
"engines": map[string]string{"node": ">=10"}, // modern object form
187+
"dist": map[string]interface{}{"integrity": "sha512-modern"},
188+
},
189+
},
190+
"time": map[string]string{
191+
"0.1.0": "2012-01-01T00:00:00.000Z",
192+
"1.0.0": "2020-01-01T00:00:00.000Z",
193+
},
194+
}
195+
_ = json.NewEncoder(w).Encode(resp)
196+
}))
197+
defer server.Close()
198+
199+
reg := New(server.URL, core.DefaultClient())
200+
versions, err := reg.FetchVersions(context.Background(), "legacy")
201+
if err != nil {
202+
t.Fatalf("FetchVersions with legacy engines array failed: %v", err)
203+
}
204+
if len(versions) != 2 {
205+
t.Fatalf("versions = %d, want 2", len(versions))
206+
}
207+
208+
byVersion := map[string]map[string]interface{}{}
209+
for _, v := range versions {
210+
byVersion[v.Number] = v.Metadata
211+
}
212+
213+
// Legacy array form should round-trip as a []interface{}.
214+
if engines, ok := byVersion["0.1.0"]["engines"].([]interface{}); !ok {
215+
t.Errorf("0.1.0 engines = %T, want []interface{}", byVersion["0.1.0"]["engines"])
216+
} else if len(engines) != 2 || engines[0] != "node" || engines[1] != "rhino" {
217+
t.Errorf("0.1.0 engines = %+v, want [node rhino]", engines)
218+
}
219+
220+
// Modern object form should round-trip as a map[string]interface{}.
221+
if engines, ok := byVersion["1.0.0"]["engines"].(map[string]interface{}); !ok {
222+
t.Errorf("1.0.0 engines = %T, want map[string]interface{}", byVersion["1.0.0"]["engines"])
223+
} else if engines["node"] != ">=10" {
224+
t.Errorf("1.0.0 engines = %+v, want {node:>=10}", engines)
225+
}
226+
}
227+
165228
func TestFetchPackageScoped(t *testing.T) {
166229
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
167230
// Path can be encoded in different ways depending on the URL library

0 commit comments

Comments
 (0)