Skip to content

Commit ccf346a

Browse files
Add basic pnpm lockfile parsers
Add parsers for pnpm-lock.yaml v5 and v6, and shrinkwrap.yaml specs with examples and package assembly. Reference: #3766 Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent 4f49985 commit ccf346a

19 files changed

+96136
-0
lines changed

src/packagedcode/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@
149149
npm.NpmShrinkwrapJsonHandler,
150150
npm.YarnLockV1Handler,
151151
npm.YarnLockV2Handler,
152+
npm.PnpmShrinkwrapYamlHandler,
153+
npm.PnpmLockYamlHandler,
152154

153155
nuget.NugetNupkgHandler,
154156
nuget.NugetNuspecHandler,

src/packagedcode/npm.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def assemble(cls, package_data, resource, codebase, package_adder):
8686
'.package-lock.json',
8787
'npm-shrinkwrap.json',
8888
'yarn.lock',
89+
'shrinkwrap.yaml',
90+
'pnpm-lock.yaml'
8991
}
9092

9193
package_resource = None
@@ -723,6 +725,121 @@ def parse(cls, location, package_only=False):
723725
yield models.PackageData.from_data(package_data, package_only)
724726

725727

728+
class BasePnpmLockHandler(BaseNpmHandler):
729+
730+
@classmethod
731+
def parse(cls, location, package_only=False):
732+
"""
733+
Parses and yields package dependencies for all lockfile versions
734+
present in the spec: https://github.com/pnpm/spec/blob/master/lockfile/
735+
"""
736+
737+
with open(location) as yl:
738+
lock_data = saneyaml.load(yl.read())
739+
740+
lockfile_version = lock_data.get("lockfileVersion")
741+
is_shrinkwrap = False
742+
if not lockfile_version:
743+
lockfile_version = lock_data.get("shrinkwrapVersion")
744+
lockfile_minor_version = lock_data.get("shrinkwrapMinorVersion")
745+
if lockfile_minor_version:
746+
lockfile_version = f"{lockfile_version}.{lockfile_minor_version}"
747+
is_shrinkwrap = True
748+
749+
extra_data = {
750+
"lockfileVersion": lockfile_version,
751+
}
752+
major_v, minor_v = lockfile_version.split(".")
753+
754+
resolved_packages = lock_data.get("packages", [])
755+
dependencies_by_purl = {}
756+
757+
for purl_fields, data in resolved_packages.items():
758+
if major_v == "6":
759+
clean_purl_fields = purl_fields.split("(")[0]
760+
elif major_v == "5" or is_shrinkwrap:
761+
clean_purl_fields = purl_fields.split("_")[0]
762+
else:
763+
clean_purl_fields = purl_fields
764+
raise Exception(lockfile_version, purl_fields)
765+
766+
sections = clean_purl_fields.split("/")
767+
name_version= None
768+
if major_v == "6":
769+
if len(sections) == 2:
770+
namespace = None
771+
_, name_version = sections
772+
elif len(sections) == 3:
773+
_, namespace, name_version = sections
774+
775+
name, version = name_version.split("@")
776+
elif major_v == "5" or is_shrinkwrap:
777+
if len(sections) == 3:
778+
_, name, version = sections
779+
elif len(sections) == 4:
780+
_, namespace, name, version = sections
781+
782+
purl = PackageURL(
783+
type=cls.default_package_type,
784+
name=name,
785+
namespace=namespace,
786+
version=version,
787+
).to_string()
788+
789+
# TODO: add resolved_package and dependencies from the following:
790+
# 'peerDependencies', 'optionalDependencies', 'dependencies',
791+
# 'transitivePeerDependencies', 'peerDependenciesMeta'
792+
# add sha512 from 'resolution'
793+
extra_data_fields = ["cpu", "os", "engines", "deprecated", "hasBin"]
794+
795+
is_dev = data.get("dev", False)
796+
is_runtime = not is_dev
797+
is_optional = data.get("optional", False)
798+
799+
extra_data_deps = {}
800+
for key in extra_data_fields:
801+
value = data.get(key, None)
802+
if value is not None:
803+
extra_data_deps[key] = value
804+
805+
dependency_data = models.DependentPackage(
806+
purl=purl,
807+
is_optional=is_optional,
808+
is_runtime=is_runtime,
809+
is_resolved=True,
810+
extra_data=extra_data_deps,
811+
)
812+
dependencies_by_purl[purl] = dependency_data
813+
814+
dependencies = list(dependencies_by_purl.values())
815+
root_package_data = dict(
816+
datasource_id=cls.datasource_id,
817+
type=cls.default_package_type,
818+
primary_language=cls.default_primary_language,
819+
dependencies=dependencies,
820+
extra_data=extra_data,
821+
)
822+
yield models.PackageData.from_data(root_package_data)
823+
824+
825+
class PnpmShrinkwrapYamlHandler(BasePnpmLockHandler):
826+
datasource_id = 'pnpm_shrinkwrap_yaml'
827+
path_patterns = ('*/shrinkwrap.yaml',)
828+
default_package_type = 'npm'
829+
default_primary_language = 'JavaScript'
830+
description = 'pnpm shrinkwrap.yaml lockfile'
831+
documentation_url = 'https://github.com/pnpm/spec/blob/master/lockfile/4.md'
832+
833+
834+
class PnpmLockYamlHandler(BasePnpmLockHandler):
835+
datasource_id = 'pnpm_lock_yaml'
836+
path_patterns = ('*/pnpm-lock.yaml',)
837+
default_package_type = 'npm'
838+
default_primary_language = 'JavaScript'
839+
description = 'pnpm pnpm-lock.yaml lockfile'
840+
documentation_url = 'https://github.com/pnpm/spec/blob/master/lockfile/6.0.md'
841+
842+
726843
def get_checksum_and_url(url):
727844
"""
728845
Return a mapping of {download_url, sha1} where the checksum can be a

0 commit comments

Comments
 (0)