From c553399f96e321418c5cda352c99809f7747d3c0 Mon Sep 17 00:00:00 2001 From: Shekhar Date: Tue, 18 Nov 2025 14:56:41 +0530 Subject: [PATCH] packagedcode: add NpmrcHandler to parse .npmrc files (#4494) Add NpmrcHandler in src/packagedcode/npm.py to parse .npmrc configuration files and yield a PackageData with parsed key/value pairs in extra_data. Add unit test and fixtures under tests/packagedcode/data/npm/basic/. Signed-off-by: Shekhar --- src/packagedcode/npm.py | 42 +++++++++++++++ tests/packagedcode/data/npm/basic/.npmrc | 8 +++ .../data/npm/basic/.npmrc.expected | 53 +++++++++++++++++++ tests/packagedcode/test_npmrc.py | 15 ++++++ 4 files changed, 118 insertions(+) create mode 100644 tests/packagedcode/data/npm/basic/.npmrc create mode 100644 tests/packagedcode/data/npm/basic/.npmrc.expected create mode 100644 tests/packagedcode/test_npmrc.py diff --git a/src/packagedcode/npm.py b/src/packagedcode/npm.py index 779ff281668..09e7b454c19 100644 --- a/src/packagedcode/npm.py +++ b/src/packagedcode/npm.py @@ -500,6 +500,48 @@ def update_workspace_members(cls, workspace_members, codebase): for member in workspace_members: member.save(codebase) +class NpmrcHandler(BaseNpmHandler): + datasource_id = 'npmrc' + path_patterns = ('*/.npmrc',) + default_package_type = 'npm' + default_primary_language = None + description = 'npm .npmrc configuration file' + documentation_url = 'https://docs.npmjs.com/cli/v11/configuring-npm/npmrc' + + @classmethod + def parse(cls, location, package_only=False): + """ + parse [.npmrc] file and store result in key : value pair. + convert key : value pair to object and return it. + """ + extra_data = {} + with io.open(location, encoding='utf-8') as lines: + for line in lines: + line = line.strip() + if not line or line.startswith(';') or line.startswith('#'): + continue + if '=' not in line: + continue + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + # ignore empty key but allow empty values + if not key: + continue + # if value is in single quote or in double quote, strip them + if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): + if len(value) >= 2: + value = value[1:-1] + extra_data[key] = value + + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language=cls.default_primary_language, + description=cls.description, + extra_data=extra_data, + ) + yield models.PackageData.from_data(package_data, package_only) def get_urls(namespace, name, version, **kwargs): return dict( diff --git a/tests/packagedcode/data/npm/basic/.npmrc b/tests/packagedcode/data/npm/basic/.npmrc new file mode 100644 index 00000000000..de005c84b5e --- /dev/null +++ b/tests/packagedcode/data/npm/basic/.npmrc @@ -0,0 +1,8 @@ +; sample .npmrc for tests +# a comment line +registry=https://registry.npmjs.org/ +cache=~/.npm +strict-ssl=true +//registry.npmjs.org/:_authToken="abc123" +init.author.name=John Doe +emptykey= diff --git a/tests/packagedcode/data/npm/basic/.npmrc.expected b/tests/packagedcode/data/npm/basic/.npmrc.expected new file mode 100644 index 00000000000..860d83a5a96 --- /dev/null +++ b/tests/packagedcode/data/npm/basic/.npmrc.expected @@ -0,0 +1,53 @@ +[ + { + "type": "npm", + "namespace": null, + "name": null, + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": null, + "description": "npm .npmrc configuration file", + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "registry": "https://registry.npmjs.org/", + "cache": "~/.npm", + "strict-ssl": "true", + "//registry.npmjs.org/:_authToken": "abc123", + "init.author.name": "John Doe", + "emptykey": "" + }, + "dependencies": [], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "npmrc", + "purl": null + } +] diff --git a/tests/packagedcode/test_npmrc.py b/tests/packagedcode/test_npmrc.py new file mode 100644 index 00000000000..f00c9eb45bb --- /dev/null +++ b/tests/packagedcode/test_npmrc.py @@ -0,0 +1,15 @@ +import os + +from packagedcode import npm +from packages_test_utils import PackageTester +from scancode_config import REGEN_TEST_FIXTURES + + +class TestNpmrc(PackageTester): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_parse_basic_npmrc(self): + test_file = self.get_test_loc('npm/basic/.npmrc') + expected_loc = self.get_test_loc('npm/basic/.npmrc.expected') + packages_data = npm.NpmrcHandler.parse(test_file) + self.check_packages_data(packages_data, expected_loc, regen=REGEN_TEST_FIXTURES)