Last active
July 23, 2025 14:39
-
-
Save ypresto/50192a510af018ac1510236828c7041d to your computer and use it in GitHub Desktop.
pnpm-lock.yaml filter by package name and version name. Useful for debugging why multiple installation of same package (of same version) occurs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
// MIT License | |
// | |
// Copyright (c) 2025 Yuya Tanaka | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
const fs = require('fs'); | |
const yaml = require('js-yaml'); | |
function parsePackageWithDeps(packageStr) { | |
// Parse "@connectrpc/[email protected](@bufbuild/[email protected])" | |
// into { name: "@connectrpc/connect", version: "2.0.2", full: "...", deps: "(@bufbuild/[email protected])" } | |
// Handle scoped packages like @org/package | |
let match; | |
if (packageStr.startsWith('@')) { | |
// Scoped package: @org/package@version(deps) | |
match = packageStr.match(/^(@[^@]+\/[^@]+)@([^(]+)(\(.+\))?$/); | |
} else { | |
// Regular package: package@version(deps) | |
match = packageStr.match(/^([^@]+)@([^(]+)(\(.+\))?$/); | |
} | |
if (!match) return null; | |
return { | |
name: match[1], | |
version: match[2], | |
full: packageStr, | |
deps: match[3] || '' | |
}; | |
} | |
function collectPackageInstances(data) { | |
const instances = new Map(); // Map of "name@version" -> Set of full strings | |
// Process snapshots section | |
if (data.snapshots) { | |
Object.keys(data.snapshots).forEach(key => { | |
const parsed = parsePackageWithDeps(key); | |
if (parsed && parsed.deps) { | |
const baseKey = `${parsed.name}@${parsed.version}`; | |
if (!instances.has(baseKey)) { | |
instances.set(baseKey, new Set()); | |
} | |
instances.get(baseKey).add(parsed.full); | |
} | |
}); | |
} | |
// Process importers section for versions with deps | |
function processImporters(obj, path = []) { | |
if (typeof obj === 'string' && obj.includes('(') && obj.includes('@')) { | |
const parsed = parsePackageWithDeps(obj); | |
if (parsed && parsed.deps) { | |
const baseKey = `${parsed.name}@${parsed.version}`; | |
if (!instances.has(baseKey)) { | |
instances.set(baseKey, new Set()); | |
} | |
instances.get(baseKey).add(parsed.full); | |
} | |
} | |
if (typeof obj === 'object' && obj !== null) { | |
Object.entries(obj).forEach(([key, value]) => { | |
processImporters(value, [...path, key]); | |
}); | |
} | |
} | |
if (data.importers) { | |
processImporters(data.importers); | |
} | |
// Filter to only show packages with multiple instances | |
const multipleInstances = new Map(); | |
instances.forEach((variants, baseKey) => { | |
if (variants.size > 1) { | |
multipleInstances.set(baseKey, variants); | |
} | |
}); | |
return multipleInstances; | |
} | |
function displayInstances(instances, showAll = false) { | |
const sortedKeys = Array.from(instances.keys()).sort(); | |
if (sortedKeys.length === 0) { | |
console.log('No packages found with multiple installation instances.'); | |
return; | |
} | |
console.log(`Found ${sortedKeys.length} packages with multiple installation instances:\n`); | |
sortedKeys.forEach(baseKey => { | |
const variants = instances.get(baseKey); | |
console.log(`${baseKey}: (${variants.size} instances)`); | |
const sortedVariants = Array.from(variants).sort(); | |
sortedVariants.forEach(variant => { | |
console.log(` ${variant}`); | |
}); | |
console.log(''); | |
}); | |
} | |
function main() { | |
const args = process.argv.slice(2); | |
if (args.includes('--help') || args.includes('-h')) { | |
console.error('Usage: node pnpm-dep-instances.js [options]'); | |
console.error('\nFinds packages that have multiple installations with different transitive dependencies'); | |
console.error('\nOptions:'); | |
console.error(' --all Show all packages with dependencies (not just multiple instances)'); | |
console.error(' --help Show this help message'); | |
console.error('\nExample:'); | |
console.error(' node pnpm-dep-instances.js'); | |
console.error(' node pnpm-dep-instances.js --all'); | |
process.exit(0); | |
} | |
const showAll = args.includes('--all'); | |
const yamlFile = './pnpm-lock.yaml'; | |
try { | |
console.log(`Analyzing package instances in ${yamlFile}...\n`); | |
const fileContents = fs.readFileSync(yamlFile, 'utf8'); | |
const data = yaml.load(fileContents); | |
const instances = collectPackageInstances(data); | |
if (showAll) { | |
// Show all packages with deps, not just multiples | |
const allInstances = new Map(); | |
if (data.snapshots) { | |
Object.keys(data.snapshots).forEach(key => { | |
const parsed = parsePackageWithDeps(key); | |
if (parsed && parsed.deps) { | |
const baseKey = `${parsed.name}@${parsed.version}`; | |
if (!allInstances.has(baseKey)) { | |
allInstances.set(baseKey, new Set()); | |
} | |
allInstances.get(baseKey).add(parsed.full); | |
} | |
}); | |
} | |
displayInstances(allInstances, true); | |
} else { | |
displayInstances(instances); | |
} | |
} catch (error) { | |
console.error('Error:', error.message); | |
process.exit(1); | |
} | |
} | |
main(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
// MIT License | |
// | |
// Copyright (c) 2025 Yuya Tanaka | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
const fs = require('fs'); | |
const yaml = require('js-yaml'); | |
function findPaths(obj, target, currentPath = [], parentObj = null) { | |
const results = []; | |
// Parse target to separate package name and version | |
let targetPackage = target; | |
let targetVersion = null; | |
const atIndex = target.lastIndexOf('@'); | |
if (atIndex > 0) { // Not a scoped package's first @ | |
targetPackage = target.substring(0, atIndex); | |
targetVersion = target.substring(atIndex + 1); | |
} | |
// Check if current value is a leaf node (string/number/boolean) | |
if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { | |
// Match only if the target is the exact value or starts with target | |
const strValue = String(obj); | |
// For exact package@version searches | |
if (targetVersion) { | |
if (strValue === target || | |
(strValue === targetPackage && parentObj?.version === targetVersion) || | |
(strValue === targetPackage && parentObj?.specifier === targetVersion)) { | |
const result = { path: currentPath, value: strValue }; | |
// Try to get version info from parent object | |
if (parentObj && typeof parentObj === 'object') { | |
if (parentObj.specifier) result.specifier = parentObj.specifier; | |
if (parentObj.version) result.version = parentObj.version; | |
} | |
return [result]; | |
} | |
} else { | |
// Original behavior for package-only searches | |
if (strValue === target || strValue.startsWith(target + '@')) { | |
const result = { path: currentPath, value: strValue }; | |
// Try to get version info from parent object | |
if (parentObj && typeof parentObj === 'object') { | |
if (parentObj.specifier) result.specifier = parentObj.specifier; | |
if (parentObj.version) result.version = parentObj.version; | |
} | |
return [result]; | |
} | |
} | |
return []; | |
} | |
// If it's an array, traverse its elements without adding indices to path | |
if (Array.isArray(obj)) { | |
obj.forEach(value => { | |
const subResults = findPaths(value, target, currentPath, obj); | |
results.push(...subResults); | |
}); | |
return results; | |
} | |
// If it's an object, traverse its properties | |
if (typeof obj === 'object' && obj !== null) { | |
// Check if this object contains our target package | |
if (obj.specifier && obj.version) { | |
const packageName = currentPath[currentPath.length - 1]; | |
const matches = targetVersion | |
? packageName === targetPackage && (obj.version === targetVersion || obj.version.startsWith(targetVersion + '(')) | |
: packageName === target || packageName?.startsWith(target + '@'); | |
if (matches) { | |
results.push({ | |
path: currentPath, | |
value: packageName, | |
specifier: obj.specifier, | |
version: obj.version | |
}); | |
return results; | |
} | |
} | |
// Special handling for snapshots dependencies | |
if (currentPath[0] === 'snapshots' && currentPath.length === 3) { | |
const depType = currentPath[2]; // dependencies or optionalDependencies | |
if ((depType === 'dependencies' || depType === 'optionalDependencies') && | |
typeof obj === 'object') { | |
for (const [pkgName, pkgVersion] of Object.entries(obj)) { | |
const matches = targetVersion | |
? pkgName === targetPackage && pkgVersion === targetVersion | |
: pkgName === target || pkgName.startsWith(target + '@'); | |
if (matches) { | |
results.push({ | |
path: currentPath, // Don't add the package name since it's the value | |
value: pkgName, | |
version: pkgVersion, | |
type: depType | |
}); | |
} | |
} | |
} | |
} | |
for (const [key, value] of Object.entries(obj)) { | |
const newPath = [...currentPath, key]; | |
const subResults = findPaths(value, target, newPath, obj); | |
results.push(...subResults); | |
} | |
} | |
return results; | |
} | |
function displayAsTree(results) { | |
// Group results by the first two segments of their path | |
const groups = {}; | |
results.forEach(result => { | |
// Create group key from first segment (e.g., "importers" or "snapshots") | |
const groupKey = result.path[0] || 'root'; | |
if (!groups[groupKey]) { | |
groups[groupKey] = []; | |
} | |
groups[groupKey].push(result); | |
}); | |
// Display each group | |
Object.entries(groups).forEach(([groupKey, groupResults]) => { | |
console.log(groupKey); | |
// For better organization, group by second-level paths within each main group | |
const subGroups = {}; | |
groupResults.forEach(result => { | |
const subKey = result.path.slice(0, 2).join(' -> '); | |
if (!subGroups[subKey]) { | |
subGroups[subKey] = []; | |
} | |
subGroups[subKey].push(result); | |
}); | |
// Display sub-groups | |
Object.entries(subGroups).forEach(([subKey, results]) => { | |
// Print the path from second segment onwards | |
let lastPrintedPath = [groupKey]; | |
results.forEach(result => { | |
// Print only the segments that differ from the last printed path | |
for (let i = 1; i < result.path.length; i++) { | |
if (i >= lastPrintedPath.length || result.path[i] !== lastPrintedPath[i]) { | |
const indent = ' '.repeat(i); | |
console.log(indent + result.path[i]); | |
} | |
} | |
lastPrintedPath = result.path.slice(); | |
// Display the leaf value | |
const leafIndent = ' '.repeat(result.path.length); | |
if (result.specifier || result.version || result.type) { | |
console.log(leafIndent + '=> ' + result.value); | |
if (result.specifier) { | |
console.log(leafIndent + ' specifier: ' + result.specifier); | |
} | |
if (result.version) { | |
console.log(leafIndent + ' version: ' + result.version); | |
} | |
if (result.type) { | |
console.log(leafIndent + ' type: ' + result.type); | |
} | |
} else { | |
console.log(leafIndent + '=> ' + result.value); | |
} | |
}); | |
}); | |
console.log(''); // Empty line between main groups | |
}); | |
} | |
function main() { | |
const args = process.argv.slice(2); | |
if (args.length === 0) { | |
console.error('Usage: node pnpm-yaml-search.js <search-term>'); | |
console.error('Example: node pnpm-yaml-search.js "@apollo/client"'); | |
process.exit(1); | |
} | |
const searchTerm = args[0]; | |
const yamlFile = 'pnpm-lock.yaml'; | |
try { | |
const fileContents = fs.readFileSync(yamlFile, 'utf8'); | |
const data = yaml.load(fileContents); | |
console.log(`Searching for "${searchTerm}" in ${yamlFile}...\n`); | |
const paths = findPaths(data, searchTerm); | |
if (paths.length === 0) { | |
console.log('No matches found.'); | |
} else { | |
console.log(`Found ${paths.length} match(es):\n`); | |
displayAsTree(paths); | |
} | |
} catch (error) { | |
console.error('Error:', error.message); | |
process.exit(1); | |
} | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment