Skip to content

Instantly share code, notes, and snippets.

@ypresto
Last active July 23, 2025 14:39
Show Gist options
  • Save ypresto/50192a510af018ac1510236828c7041d to your computer and use it in GitHub Desktop.
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.
#!/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();
#!/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