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  }