Path: blob/master/src/scripts/npm_analyze_duplicates.py
1706 views
#!/usr/bin/env python31"""2Analyze pnpm-lock.yaml for duplicate packages with different versions.3"""45import re6import sys7from collections import defaultdict8from packaging import version9import yaml1011def parse_pnpm_lock(file_path):12"""Parse pnpm-lock.yaml and extract package versions."""13try:14with open(file_path, 'r') as f:15data = yaml.safe_load(f)16except FileNotFoundError:17print(f"Error: {file_path} not found")18return None19except yaml.YAMLError as e:20print(f"Error parsing YAML: {e}")21return None2223packages = {}2425# Extract from packages section26if 'packages' in data:27for pkg_spec, pkg_info in data['packages'].items():28# Parse package name and version from spec like "[email protected]"29match = re.match(r'^(.+?)@([^@]+)$', pkg_spec)30if match:31name, ver = match.groups()32if name not in packages:33packages[name] = []34packages[name].append(ver)3536return packages3738def find_duplicates(packages):39"""Find packages with multiple versions."""40duplicates = {}4142for name, versions in packages.items():43if len(set(versions)) > 1: # More than one unique version44unique_versions = sorted(set(versions), key=lambda v: version.parse(v) if is_valid_version(v) else version.parse("0.0.0"))45duplicates[name] = {46'versions': unique_versions,47'count': len(versions),48'unique_count': len(unique_versions)49}5051return duplicates5253def is_valid_version(ver_str):54"""Check if version string is valid semver."""55try:56version.parse(ver_str)57return True58except version.InvalidVersion:59return False6061def analyze_version_differences(versions):62"""Analyze how different the versions are."""63if len(versions) < 2:64return "single"6566try:67parsed_versions = [version.parse(v) for v in versions if is_valid_version(v)]68if len(parsed_versions) < 2:69return "invalid"7071parsed_versions.sort()7273# Check if only patch versions differ74major_minor_same = all(75(v.major, v.minor) == (parsed_versions[0].major, parsed_versions[0].minor)76for v in parsed_versions77)78if major_minor_same:79return "patch_diff"8081# Check if only minor versions differ (same major)82major_same = all(v.major == parsed_versions[0].major for v in parsed_versions)83if major_same:84return "minor_diff"8586return "major_diff"8788except Exception:89return "unknown"9091def main():92lock_file = "packages/pnpm-lock.yaml"9394print("Analyzing pnpm-lock.yaml for duplicate packages...")9596packages = parse_pnpm_lock(lock_file)97if packages is None:98sys.exit(1)99100duplicates = find_duplicates(packages)101102if not duplicates:103print("No duplicate packages found!")104return105106print(f"\nFound {len(duplicates)} packages with multiple versions:\n")107108# Group by difference type109by_diff_type = defaultdict(list)110111for name, info in duplicates.items():112diff_type = analyze_version_differences(info['versions'])113by_diff_type[diff_type].append((name, info))114115# Report patch differences first (most concerning)116if 'patch_diff' in by_diff_type:117print("🔴 PATCH VERSION DIFFERENCES (most concerning):")118print("=" * 50)119for name, info in sorted(by_diff_type['patch_diff']):120versions_str = " vs ".join(info['versions'])121print(f" {name}: {versions_str} ({info['count']} total installations)")122print()123124if 'minor_diff' in by_diff_type:125print("🟡 MINOR VERSION DIFFERENCES:")126print("=" * 30)127for name, info in sorted(by_diff_type['minor_diff']):128versions_str = " vs ".join(info['versions'])129print(f" {name}: {versions_str} ({info['count']} total installations)")130print()131132if 'major_diff' in by_diff_type:133print("🟠MAJOR VERSION DIFFERENCES (expected for breaking changes):")134print("=" * 60)135for name, info in sorted(by_diff_type['major_diff'])[:10]: # Limit to 10136versions_str = " vs ".join(info['versions'])137print(f" {name}: {versions_str} ({info['count']} total installations)")138if len(by_diff_type['major_diff']) > 10:139print(f" ... and {len(by_diff_type['major_diff']) - 10} more")140print()141142# Summary143total_patch = len(by_diff_type['patch_diff'])144total_minor = len(by_diff_type['minor_diff'])145146print("SUMMARY:")147print(f" 🔴 Patch differences: {total_patch} (should be unified)")148print(f" 🟡 Minor differences: {total_minor} (may need review)")149print(f" 🟠Major differences: {len(by_diff_type['major_diff'])} (usually expected)")150151if __name__ == "__main__":152main()153154