pkg/nodejs/registry.go (119 lines of code) (raw):
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nodejs
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/GoogleCloudPlatform/buildpacks/pkg/version"
"github.com/hashicorp/go-retryablehttp"
)
// npmRegistryURL responds with the registry metadata for a given NPM package.
var npmRegistryURL = "https://registry.npmjs.org/%s"
// yarnTagsURL responds with all available Yarn versions >=2.0.0.
var yarnTagsURL = "https://repo.yarnpkg.com/tags"
// packageMetadata contains registry information about an npm package. For more information see
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-metadata-format.
type packageMetadata struct {
Name string `json:"name"`
Modified string `json:"modified"`
DistTags struct {
Latest string `json:"latest"`
} `json:"dist-tags"`
Versions map[string]struct {
Name string `json:"name"`
Version string `json:"version"`
Deprecated string `json:"deprecated"`
} `json:"versions"`
}
type yarnTags struct {
Latest struct {
Stable string `json:"stable"`
Latest string `json:"latest"`
} `json:"dist-tags"`
Tags []string `json:"tags"`
}
// latestPackageVersion returns latest available version of an NPM package.
func latestPackageVersion(pkg string) (string, error) {
metadata, err := fetchPackageMetadata(pkg)
if err != nil {
return "", err
}
return metadata.DistTags.Latest, nil
}
// resolvePackageVersion returns the newest available version of an NPM package that satisfies the
// provided version constraint.
func resolvePackageVersion(pkg, verConstraint string) (string, error) {
if version.IsExactSemver(verConstraint) {
return verConstraint, nil
}
metadata, err := fetchPackageMetadata(pkg)
if err != nil {
return "", err
}
// After v2 yarn stopped publishing packages to the NPM registry so we need to fetch these from
// elsewhere. We do not include the additional versions for the * or "" wildcard versions to avoid
// breaking customers implicitly pinned to v1.x.x.
var extraVersions []string
if pkg == "yarn" && verConstraint != "" && verConstraint != "*" {
extraVersions, err = fetchYarnTags()
if err != nil {
return "", err
}
}
versions := make([]string, 0, len(metadata.Versions)+len(extraVersions))
for v, meta := range metadata.Versions {
// If metadata includes a deprecated field, it was pulled from registry and should be ignored.
if meta.Deprecated == "" {
versions = append(versions, v)
}
}
for _, v := range extraVersions {
versions = append(versions, v)
}
return version.ResolveVersion(verConstraint, versions)
}
// fetchYarnTags fetches metadata available Yarn versions >=2.0.0.
func fetchYarnTags() ([]string, error) {
bytes, err := sendRequest(yarnTagsURL, http.Header{})
if err != nil {
return nil, fmt.Errorf("getting url %q: %w", yarnTagsURL, err)
}
var tags yarnTags
if err := json.Unmarshal(bytes, &tags); err != nil {
return nil, fmt.Errorf("unmarshalling response from %q: %w", yarnTagsURL, err)
}
return tags.Tags, nil
}
// fetchPackageMetadata fetches metadata about an NPM package published to the NPM registry.
func fetchPackageMetadata(pkg string) (*packageMetadata, error) {
url := fmt.Sprintf(npmRegistryURL, pkg)
header := http.Header{
"Accept": []string{"application/vnd.npm.install-v1+json"},
}
bytes, err := sendRequest(url, header)
if err != nil {
return nil, fmt.Errorf("getting url %q: %w", url, err)
}
var metadata packageMetadata
if err := json.Unmarshal(bytes, &metadata); err != nil {
return nil, fmt.Errorf("unmarshalling response from %q: %w", url, err)
}
return &metadata, nil
}
func sendRequest(url string, header http.Header) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request for %q: %w", url, err)
}
req.Header = header
client := newRetryableHTTPClient()
response, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("getting %q: %w", url, err)
}
defer response.Body.Close()
bytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
msg := fmt.Sprintf("unexpected status code: %d (%s)", response.StatusCode, http.StatusText(response.StatusCode))
if len(bytes) > 0 {
msg = fmt.Sprintf("%v: %v", msg, string(bytes))
}
return nil, fmt.Errorf(msg)
}
return bytes, nil
}
func newRetryableHTTPClient() *http.Client {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 3
return retryClient.StandardClient()
}