mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
feat: add yarn lock dev dep detection; fixed #4548
--------- Signed-off-by: Rez Moss <hi@rezmoss.com> Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
parent
48ee12be0c
commit
c185657d71
@ -35,7 +35,8 @@ func TestYarnPackageLockDirectory(t *testing.T) {
|
||||
sbom, _ := catalogDirectory(t, "test-fixtures/yarn-lock")
|
||||
|
||||
foundPackages := strset.New()
|
||||
expectedPackages := strset.New("async@0.9.2", "async@3.2.3", "merge-objects@1.0.5", "should-type@1.3.0", "@4lolo/resize-observer-polyfill@1.5.2")
|
||||
// merge-objects and should-type are devDependencies in package.json and are excluded by default
|
||||
expectedPackages := strset.New("async@0.9.2", "async@3.2.3", "@4lolo/resize-observer-polyfill@1.5.2")
|
||||
|
||||
for actualPkg := range sbom.Artifacts.Packages.Enumerate(pkg.NpmPkg) {
|
||||
for _, actualLocation := range actualPkg.Locations.ToSlice() {
|
||||
|
||||
@ -23,20 +23,21 @@ var _ generic.Parser = parsePackageJSON
|
||||
|
||||
// packageJSON represents a JavaScript package.json file
|
||||
type packageJSON struct {
|
||||
Version string `json:"version"`
|
||||
Latest []string `json:"latest"`
|
||||
Author person `json:"author"`
|
||||
Authors people `json:"authors"`
|
||||
Contributors people `json:"contributors"`
|
||||
Maintainers people `json:"maintainers"`
|
||||
License json.RawMessage `json:"license"`
|
||||
Licenses json.RawMessage `json:"licenses"`
|
||||
Name string `json:"name"`
|
||||
Homepage string `json:"homepage"`
|
||||
Description string `json:"description"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Repository repository `json:"repository"`
|
||||
Private bool `json:"private"`
|
||||
Version string `json:"version"`
|
||||
Latest []string `json:"latest"`
|
||||
Author person `json:"author"`
|
||||
Authors people `json:"authors"`
|
||||
Contributors people `json:"contributors"`
|
||||
Maintainers people `json:"maintainers"`
|
||||
License json.RawMessage `json:"license"`
|
||||
Licenses json.RawMessage `json:"licenses"`
|
||||
Name string `json:"name"`
|
||||
Homepage string `json:"homepage"`
|
||||
Description string `json:"description"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
Repository repository `json:"repository"`
|
||||
Private bool `json:"private"`
|
||||
}
|
||||
|
||||
type person struct {
|
||||
|
||||
@ -3,6 +3,7 @@ package javascript
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
@ -51,7 +53,7 @@ type yarnPackage struct {
|
||||
Version string
|
||||
Resolved string
|
||||
Integrity string
|
||||
Dependencies map[string]string // We don't currently support dependencies for yarn v1 lock files
|
||||
Dependencies map[string]string
|
||||
}
|
||||
|
||||
type yarnV2PackageEntry struct {
|
||||
@ -71,6 +73,98 @@ func newGenericYarnLockAdapter(cfg CatalogerConfig) genericYarnLockAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// readPackageJSONDeps reads the package.json adjacent to the given lockfile location.
|
||||
// NOTE: in yarn workspaces, only the root package.json is consulted. Packages declared
|
||||
// as devDependencies only in a workspace package.json (not the root) will not be detected
|
||||
// as dev-only. This is a safe degradation — those packages will be included in the SBOM
|
||||
// rather than incorrectly filtered out.
|
||||
func readPackageJSONDeps(resolver file.Resolver, lockfileLocation file.Location) (prod, dev map[string]string) {
|
||||
prod = make(map[string]string)
|
||||
dev = make(map[string]string)
|
||||
|
||||
if resolver == nil {
|
||||
return prod, dev
|
||||
}
|
||||
|
||||
pkgJSONLocation := resolver.RelativeFileByPath(lockfileLocation, "package.json")
|
||||
if pkgJSONLocation == nil {
|
||||
log.WithFields("lockfile", lockfileLocation.RealPath).Debug("could not find package.json for dev dependency detection")
|
||||
return prod, dev
|
||||
}
|
||||
|
||||
reader, err := resolver.FileContentsByLocation(*pkgJSONLocation)
|
||||
if err != nil {
|
||||
log.WithFields("location", pkgJSONLocation.RealPath, "error", err).Debug("could not read package.json for dev dependency detection")
|
||||
return prod, dev
|
||||
}
|
||||
defer internal.CloseAndLogError(reader, pkgJSONLocation.RealPath)
|
||||
|
||||
var pkgJSON packageJSON
|
||||
if err := json.NewDecoder(reader).Decode(&pkgJSON); err != nil {
|
||||
log.WithFields("location", pkgJSONLocation.RealPath, "error", err).Debug("could not parse package.json for dev dependency detection")
|
||||
return prod, dev
|
||||
}
|
||||
|
||||
if pkgJSON.Dependencies != nil {
|
||||
prod = pkgJSON.Dependencies
|
||||
}
|
||||
if pkgJSON.DevDependencies != nil {
|
||||
dev = pkgJSON.DevDependencies
|
||||
}
|
||||
|
||||
return prod, dev
|
||||
}
|
||||
|
||||
// findReachable returns all package names reachable from the given roots via BFS
|
||||
// through the dependency graph.
|
||||
func findReachable(roots map[string]string, pkgByName map[string]yarnPackage) map[string]bool {
|
||||
visited := make(map[string]bool)
|
||||
queue := make([]string, 0, len(roots))
|
||||
|
||||
for name := range roots {
|
||||
queue = append(queue, name)
|
||||
}
|
||||
|
||||
for len(queue) > 0 {
|
||||
name := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
if visited[name] {
|
||||
continue
|
||||
}
|
||||
visited[name] = true
|
||||
|
||||
if pkg, exists := pkgByName[name]; exists {
|
||||
for depName := range pkg.Dependencies {
|
||||
if !visited[depName] {
|
||||
queue = append(queue, depName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visited
|
||||
}
|
||||
|
||||
func findDevOnlyPkgs(yarnPkgs []yarnPackage, prodDeps, devDeps map[string]string) map[string]bool {
|
||||
pkgByName := make(map[string]yarnPackage)
|
||||
for _, p := range yarnPkgs {
|
||||
pkgByName[p.Name] = p
|
||||
}
|
||||
|
||||
prodTransitive := findReachable(prodDeps, pkgByName)
|
||||
devTransitive := findReachable(devDeps, pkgByName)
|
||||
|
||||
devOnly := make(map[string]bool)
|
||||
for name := range devTransitive {
|
||||
if !prodTransitive[name] {
|
||||
devOnly[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return devOnly
|
||||
}
|
||||
|
||||
func parseYarnV1LockFile(reader io.ReadCloser) ([]yarnPackage, error) {
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
@ -190,9 +284,17 @@ func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file
|
||||
return nil, nil, fmt.Errorf("failed to parse yarn.lock file: %w", err)
|
||||
}
|
||||
|
||||
packages := make([]pkg.Package, len(yarnPkgs))
|
||||
for i, p := range yarnPkgs {
|
||||
packages[i] = newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Resolved, p.Integrity, p.Dependencies)
|
||||
// get dependencies from sibling package.json for dev dependency detection
|
||||
prodDeps, devDeps := readPackageJSONDeps(resolver, reader.Location)
|
||||
|
||||
devOnlyPkgs := findDevOnlyPkgs(yarnPkgs, prodDeps, devDeps)
|
||||
|
||||
packages := make([]pkg.Package, 0, len(yarnPkgs))
|
||||
for _, p := range yarnPkgs {
|
||||
if devOnlyPkgs[p.Name] && !a.cfg.IncludeDevDependencies {
|
||||
continue
|
||||
}
|
||||
packages = append(packages, newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Resolved, p.Integrity, p.Dependencies))
|
||||
}
|
||||
|
||||
pkg.Sort(packages)
|
||||
|
||||
@ -739,6 +739,274 @@ func TestParseYarnFindPackageNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYarnLock_DevDependencies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixtureDir string
|
||||
includeDev bool
|
||||
expected func(file.LocationSet) ([]pkg.Package, []artifact.Relationship)
|
||||
}{
|
||||
{
|
||||
name: "v1 include dev dependencies",
|
||||
fixtureDir: "test-fixtures/yarn-dev-deps",
|
||||
includeDev: true,
|
||||
expected: func(locations file.LocationSet) ([]pkg.Package, []artifact.Relationship) {
|
||||
pkgs := []pkg.Package{
|
||||
{
|
||||
Name: "dev-only-transitive",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/dev-only-transitive@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/dev-only-transitive/-/dev-only-transitive-1.0.0.tgz#abc123",
|
||||
Integrity: "sha512-devonlytransitive==",
|
||||
Dependencies: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dev-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/dev-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/dev-pkg/-/dev-pkg-1.0.0.tgz#def456",
|
||||
Integrity: "sha512-devpkg==",
|
||||
Dependencies: map[string]string{
|
||||
"dev-only-transitive": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "prod-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/prod-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/prod-pkg/-/prod-pkg-1.0.0.tgz#ghi789",
|
||||
Integrity: "sha512-prodpkg==",
|
||||
Dependencies: map[string]string{
|
||||
"shared-pkg": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "shared-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/shared-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/shared-pkg/-/shared-pkg-1.0.0.tgz#jkl012",
|
||||
Integrity: "sha512-sharedpkg==",
|
||||
Dependencies: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
rels := []artifact.Relationship{
|
||||
{
|
||||
From: pkgs[0], // dev-only-transitive
|
||||
To: pkgs[1], // dev-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: pkgs[3], // shared-pkg
|
||||
To: pkgs[2], // prod-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
return pkgs, rels
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 exclude dev dependencies",
|
||||
fixtureDir: "test-fixtures/yarn-dev-deps",
|
||||
includeDev: false,
|
||||
expected: func(locations file.LocationSet) ([]pkg.Package, []artifact.Relationship) {
|
||||
pkgs := []pkg.Package{
|
||||
{
|
||||
Name: "prod-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/prod-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/prod-pkg/-/prod-pkg-1.0.0.tgz#ghi789",
|
||||
Integrity: "sha512-prodpkg==",
|
||||
Dependencies: map[string]string{
|
||||
"shared-pkg": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "shared-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/shared-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "https://registry.yarnpkg.com/shared-pkg/-/shared-pkg-1.0.0.tgz#jkl012",
|
||||
Integrity: "sha512-sharedpkg==",
|
||||
Dependencies: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
rels := []artifact.Relationship{
|
||||
{
|
||||
From: pkgs[1], // shared-pkg
|
||||
To: pkgs[0], // prod-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
return pkgs, rels
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v2 (berry) include dev dependencies",
|
||||
fixtureDir: "test-fixtures/yarn-berry-dev-deps",
|
||||
includeDev: true,
|
||||
expected: func(locations file.LocationSet) ([]pkg.Package, []artifact.Relationship) {
|
||||
pkgs := []pkg.Package{
|
||||
{
|
||||
Name: "dev-only-transitive",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/dev-only-transitive@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "dev-only-transitive@npm:1.0.0",
|
||||
Integrity: "abc123",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dev-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/dev-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "dev-pkg@npm:1.0.0",
|
||||
Integrity: "def456",
|
||||
Dependencies: map[string]string{
|
||||
"dev-only-transitive": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "prod-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/prod-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "prod-pkg@npm:1.0.0",
|
||||
Integrity: "ghi789",
|
||||
Dependencies: map[string]string{
|
||||
"shared-pkg": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "shared-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/shared-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "shared-pkg@npm:1.0.0",
|
||||
Integrity: "jkl012",
|
||||
},
|
||||
},
|
||||
}
|
||||
rels := []artifact.Relationship{
|
||||
{
|
||||
From: pkgs[0], // dev-only-transitive
|
||||
To: pkgs[1], // dev-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
{
|
||||
From: pkgs[3], // shared-pkg
|
||||
To: pkgs[2], // prod-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
return pkgs, rels
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v2 (berry) exclude dev dependencies",
|
||||
fixtureDir: "test-fixtures/yarn-berry-dev-deps",
|
||||
includeDev: false,
|
||||
expected: func(locations file.LocationSet) ([]pkg.Package, []artifact.Relationship) {
|
||||
pkgs := []pkg.Package{
|
||||
{
|
||||
Name: "prod-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/prod-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "prod-pkg@npm:1.0.0",
|
||||
Integrity: "ghi789",
|
||||
Dependencies: map[string]string{
|
||||
"shared-pkg": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "shared-pkg",
|
||||
Version: "1.0.0",
|
||||
Locations: locations,
|
||||
PURL: "pkg:npm/shared-pkg@1.0.0",
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.YarnLockEntry{
|
||||
Resolved: "shared-pkg@npm:1.0.0",
|
||||
Integrity: "jkl012",
|
||||
},
|
||||
},
|
||||
}
|
||||
rels := []artifact.Relationship{
|
||||
{
|
||||
From: pkgs[1], // shared-pkg
|
||||
To: pkgs[0], // prod-pkg
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
return pkgs, rels
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fixture := tt.fixtureDir + "/yarn.lock"
|
||||
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||
expectedPkgs, expectedRels := tt.expected(locations)
|
||||
|
||||
adapter := newGenericYarnLockAdapter(CatalogerConfig{IncludeDevDependencies: tt.includeDev})
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, tt.fixtureDir).
|
||||
FromFile(t, fixture).
|
||||
Expects(expectedPkgs, expectedRels).
|
||||
TestParser(t, adapter.parseYarnLock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateMockYarnRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "test-project",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"prod-pkg": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dev-pkg": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 6
|
||||
cacheKey: 8
|
||||
|
||||
"dev-only-transitive@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "dev-only-transitive@npm:1.0.0"
|
||||
checksum: abc123
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dev-pkg@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "dev-pkg@npm:1.0.0"
|
||||
dependencies:
|
||||
dev-only-transitive: ^1.0.0
|
||||
checksum: def456
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prod-pkg@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "prod-pkg@npm:1.0.0"
|
||||
dependencies:
|
||||
shared-pkg: ^1.0.0
|
||||
checksum: ghi789
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shared-pkg@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "shared-pkg@npm:1.0.0"
|
||||
checksum: jkl012
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "test-project",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"prod-pkg": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dev-pkg": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
dev-only-transitive@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dev-only-transitive/-/dev-only-transitive-1.0.0.tgz#abc123"
|
||||
integrity sha512-devonlytransitive==
|
||||
|
||||
dev-pkg@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dev-pkg/-/dev-pkg-1.0.0.tgz#def456"
|
||||
integrity sha512-devpkg==
|
||||
dependencies:
|
||||
dev-only-transitive "^1.0.0"
|
||||
|
||||
prod-pkg@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prod-pkg/-/prod-pkg-1.0.0.tgz#ghi789"
|
||||
integrity sha512-prodpkg==
|
||||
dependencies:
|
||||
shared-pkg "^1.0.0"
|
||||
|
||||
shared-pkg@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shared-pkg/-/shared-pkg-1.0.0.tgz#jkl012"
|
||||
integrity sha512-sharedpkg==
|
||||
Loading…
x
Reference in New Issue
Block a user