github.com/google/osv-scalibr@v0.4.1/clients/resolution/npm_registry_client.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package resolution 16 17 import ( 18 "context" 19 "slices" 20 "strings" 21 22 "deps.dev/util/resolve" 23 "deps.dev/util/resolve/dep" 24 "deps.dev/util/semver" 25 "github.com/google/osv-scalibr/clients/datasource" 26 ) 27 28 // NPMRegistryClient is a client to fetch data from NPM registry. 29 type NPMRegistryClient struct { 30 api *datasource.NPMRegistryAPIClient 31 } 32 33 // NewNPMRegistryClient makes a new NPMRegistryClient. 34 // projectDir is the directory (on disk) to read the project-level .npmrc config file from (for registries). 35 func NewNPMRegistryClient(projectDir string) (*NPMRegistryClient, error) { 36 api, err := datasource.NewNPMRegistryAPIClient(projectDir) 37 if err != nil { 38 return nil, err 39 } 40 41 return &NPMRegistryClient{api: api}, nil 42 } 43 44 // Version returns metadata of a version specified by the VersionKey. 45 func (c *NPMRegistryClient) Version(ctx context.Context, vk resolve.VersionKey) (resolve.Version, error) { 46 return resolve.Version{VersionKey: vk}, nil 47 } 48 49 // Versions returns all the available versions of the package specified by the given PackageKey. 50 func (c *NPMRegistryClient) Versions(ctx context.Context, pk resolve.PackageKey) ([]resolve.Version, error) { 51 if isNPMBundle(pk) { // bundled dependencies 52 return nil, nil 53 } 54 55 vers, err := c.api.Versions(ctx, pk.Name) 56 if err != nil { 57 return nil, err 58 } 59 60 vks := make([]resolve.Version, len(vers.Versions)) 61 for i, v := range vers.Versions { 62 vks[i] = resolve.Version{ 63 VersionKey: resolve.VersionKey{ 64 PackageKey: pk, 65 Version: v, 66 VersionType: resolve.Concrete, 67 }} 68 } 69 70 slices.SortFunc(vks, func(a, b resolve.Version) int { return semver.NPM.Compare(a.Version, b.Version) }) 71 72 return vks, nil 73 } 74 75 // Requirements returns requirements of a version specified by the VersionKey. 76 func (c *NPMRegistryClient) Requirements(ctx context.Context, vk resolve.VersionKey) ([]resolve.RequirementVersion, error) { 77 if isNPMBundle(vk.PackageKey) { // bundled dependencies, return an empty set of requirements as a placeholder 78 return []resolve.RequirementVersion{}, nil 79 } 80 dependencies, err := c.api.Dependencies(ctx, vk.Name, vk.Version) 81 if err != nil { 82 return nil, err 83 } 84 85 // Preallocate the dependency slice, which will hold all the dependencies of each type. 86 // The npm resolver expects bundled dependencies included twice in different forms: 87 // {foo@*|Scope="bundle"} and {mangled-name-of>0.1.2>foo@1.2.3}, hence the 2*len(bundled) 88 depCount := len(dependencies.Dependencies) + len(dependencies.DevDependencies) + 89 len(dependencies.OptionalDependencies) + len(dependencies.PeerDependencies) + 90 2*len(dependencies.BundleDependencies) 91 deps := make([]resolve.RequirementVersion, 0, depCount) 92 addDeps := func(ds map[string]string, t dep.Type) { 93 for name, req := range ds { 94 typ := t.Clone() 95 if r, ok := strings.CutPrefix(req, "npm:"); ok { 96 // This dependency is aliased, add it as a 97 // dependency on the actual name, with the 98 // KnownAs attribute set to the alias. 99 typ.AddAttr(dep.KnownAs, name) 100 name = r 101 req = "" 102 if i := strings.LastIndex(r, "@"); i > 0 { 103 name = r[:i] 104 req = r[i+1:] 105 } 106 } 107 deps = append(deps, resolve.RequirementVersion{ 108 Type: typ, 109 VersionKey: resolve.VersionKey{ 110 PackageKey: resolve.PackageKey{ 111 System: resolve.NPM, 112 Name: name, 113 }, 114 VersionType: resolve.Requirement, 115 Version: req, 116 }, 117 }) 118 } 119 } 120 addDeps(dependencies.Dependencies, dep.NewType()) 121 addDeps(dependencies.DevDependencies, dep.NewType(dep.Dev)) 122 addDeps(dependencies.OptionalDependencies, dep.NewType(dep.Opt)) 123 124 peerType := dep.NewType() 125 peerType.AddAttr(dep.Scope, "peer") 126 addDeps(dependencies.PeerDependencies, peerType) 127 128 // TODO(#678): Support for bundled dependencies not implemented. 129 // // The resolver expects bundleDependencies to be present as regular 130 // // dependencies with a "*" version specifier, even if they were already 131 // // in the regular dependencies. 132 // bundleType := dep.NewType() 133 // bundleType.AddAttr(dep.Scope, "bundle") 134 // for _, name := range dependencies.BundleDependencies { 135 // deps = append(deps, resolve.RequirementVersion{ 136 // Type: bundleType, 137 // VersionKey: resolve.VersionKey{ 138 // PackageKey: resolve.PackageKey{ 139 // System: resolve.NPM, 140 // Name: name, 141 // }, 142 // VersionType: resolve.Requirement, 143 // Version: "*", 144 // }, 145 // }) 146 147 // // Correctly resolving the bundled dependencies would require downloading the package. 148 // // Instead, just manually add a placeholder dependency with the mangled name. 149 // mangledName := fmt.Sprintf("%s>%s>%s", vk.PackageKey.Name, vk.Version, name) 150 // deps = append(deps, resolve.RequirementVersion{ 151 // Type: dep.NewType(), 152 // VersionKey: resolve.VersionKey{ 153 // PackageKey: resolve.PackageKey{ 154 // System: resolve.NPM, 155 // Name: mangledName, 156 // }, 157 // VersionType: resolve.Requirement, 158 // Version: "0.0.0", 159 // }, 160 // }) 161 // } 162 163 resolve.SortDependencies(deps) 164 165 return deps, nil 166 } 167 168 // MatchingVersions returns versions matching the requirement specified by the VersionKey. 169 func (c *NPMRegistryClient) MatchingVersions(ctx context.Context, vk resolve.VersionKey) ([]resolve.Version, error) { 170 if isNPMBundle(vk.PackageKey) { // bundled dependencies 171 return nil, nil 172 } 173 174 versions, err := c.api.Versions(ctx, vk.Name) 175 if err != nil { 176 return nil, err 177 } 178 179 if concVer, ok := versions.Tags[vk.Version]; ok { 180 // matched a tag, return just the concrete version of the tag 181 return []resolve.Version{{ 182 VersionKey: resolve.VersionKey{ 183 PackageKey: vk.PackageKey, 184 Version: concVer, 185 VersionType: resolve.Concrete, 186 }, 187 }}, nil 188 } 189 190 resVersions := make([]resolve.Version, len(versions.Versions)) 191 for i, v := range versions.Versions { 192 resVersions[i] = resolve.Version{ 193 VersionKey: resolve.VersionKey{ 194 PackageKey: vk.PackageKey, 195 Version: v, 196 VersionType: resolve.Concrete, 197 }, 198 } 199 } 200 201 return resolve.MatchRequirement(vk, resVersions), nil 202 } 203 204 func isNPMBundle(pk resolve.PackageKey) bool { 205 // Bundles are represented in resolution with a 'mangled' name containing its origin e.g. "root-pkg>1.0.0>bundled-package" 206 // '>' is not a valid character for a npm package, so it'll only be found here. 207 return strings.Contains(pk.Name, ">") 208 }