Last active
August 12, 2025 16:45
-
-
Save SgtPooki/4611ccfc906dde8fb1c86a644883c0f8 to your computer and use it in GitHub Desktop.
get details about dependency changes when upgrading dependencies.. based on diff of package.json
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 | |
import { execSync } from 'child_process' | |
// Function to extract package updates from git diff | |
function getPackageUpdates (startCommit, endCommit) { | |
try { | |
let diff | |
if (!startCommit && !endCommit) { | |
// No arguments: show HEAD commit changes | |
diff = execSync('git show HEAD -- package.json', { encoding: 'utf8' }) | |
} else if (startCommit && !endCommit) { | |
// One argument: show from startCommit to HEAD | |
diff = execSync(`git diff ${startCommit} HEAD -- package.json`, { encoding: 'utf8' }) | |
} else { | |
// Two arguments: show from startCommit to endCommit | |
diff = execSync(`git diff ${startCommit} ${endCommit} -- package.json`, { encoding: 'utf8' }) | |
} | |
const updates = [] | |
// Parse the diff to find package updates | |
const lines = diff.split('\n') | |
let currentPackage = null | |
for (const line of lines) { | |
// Look for package name changes (lines starting with - or +) | |
if (line.startsWith('- "') && line.includes('": "^')) { | |
const match = line.match(/"([^"]+)": "\^([^"]+)"/) | |
if (match) { | |
currentPackage = { | |
name: match[1], | |
oldVersion: match[2], | |
newVersion: null | |
} | |
} | |
} else if (line.startsWith('+ "') && line.includes('": "^') && currentPackage) { | |
const match = line.match(/"([^"]+)": "\^([^"]+)"/) | |
if (match && match[1] === currentPackage.name) { | |
currentPackage.newVersion = match[2] | |
updates.push(currentPackage) | |
currentPackage = null | |
} | |
} | |
} | |
return updates | |
} catch (error) { | |
console.error('Error getting git diff:', error.message) | |
return [] | |
} | |
} | |
// Function to get GitHub repo URL for a package | |
async function getGitHubRepo (packageName) { | |
try { | |
const response = await fetch(`https://registry.npmjs.org/${packageName}`) | |
const data = await response.json() | |
if (data.repository && data.repository.url) { | |
let url = data.repository.url | |
// Convert git URLs to GitHub URLs | |
if (url.startsWith('git+')) { | |
url = url.replace('git+', '') | |
} | |
if (url.includes('github.com')) { | |
return url.replace('.git', '') | |
} | |
} | |
return null | |
} catch (error) { | |
console.error(`Error getting repo for ${packageName}:`, error.message) | |
return null | |
} | |
} | |
// Function to check if version bump is major | |
function isMajorVersionBump (oldVersion, newVersion) { | |
const oldMajor = parseInt(oldVersion.split('.')[0]) | |
const newMajor = parseInt(newVersion.split('.')[0]) | |
return newMajor > oldMajor | |
} | |
// Function to parse release notes and extract categorized items | |
function parseReleaseNotes (body, version) { | |
if (!body) return {} | |
// Clean up the release notes | |
const cleanBody = body | |
// Remove GitHub compare links | |
.replace(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s\)]+/g, '') | |
// Remove published date lines | |
.replace(/^\d{4}-\d{2}-\d{2}.*$/gm, '') | |
// Remove empty lines and clean up formatting | |
.split('\n') | |
.filter(line => line.trim() !== '') | |
.map(line => line.trim()) | |
.join('\n') | |
const categories = {} | |
// Split into sections | |
const sections = cleanBody.split(/(?=^## )/m) | |
for (const section of sections) { | |
const lines = section.split('\n') | |
if (lines.length === 0) continue | |
const header = lines[0] | |
const content = lines.slice(1).join('\n') | |
// Determine category based on header | |
let category = 'Other' | |
if (header.includes('Breaking Changes') || header.includes('BREAKING')) { | |
category = 'Breaking Changes' | |
} else if (header.includes('Bug Fixes') || header.includes('Fixes')) { | |
category = 'Bug Fixes' | |
} else if (header.includes('Features') || header.includes('Enhancements')) { | |
category = 'Features' | |
} else if (header.includes('Dependencies') || header.includes('Deps')) { | |
category = 'Dependencies' | |
} else if (header.includes('Security')) { | |
category = 'Security' | |
} else if (header.includes('Performance')) { | |
category = 'Performance' | |
} else if (header.includes('Deprecation') || header.includes('Deprecated')) { | |
category = 'Deprecations' | |
} | |
if (!categories[category]) { | |
categories[category] = [] | |
} | |
// Extract individual items (lines starting with *) | |
const items = content.split('\n') | |
.filter(line => line.trim().startsWith('*')) | |
.map(line => line.trim().substring(1).trim()) | |
.filter(item => item.length > 0) | |
if (items.length > 0) { | |
categories[category].push(...items.map(item => `[${version}] ${item}`)) | |
} | |
} | |
// Also look for breaking changes in the main content (not just in ## sections) | |
const breakingPatterns = [ | |
/### β BREAKING CHANGES\s*\n((?:\* .*\n?)*)/g, | |
/### BREAKING CHANGES\s*\n((?:\* .*\n?)*)/g, | |
/β BREAKING CHANGES\s*\n((?:\* .*\n?)*)/g | |
] | |
for (const pattern of breakingPatterns) { | |
const matches = cleanBody.matchAll(pattern) | |
for (const match of matches) { | |
if (match[1]) { | |
const items = match[1].split('\n') | |
.filter(line => line.trim().startsWith('*')) | |
.map(line => line.trim().substring(1).trim()) | |
.filter(item => item.length > 0) | |
if (items.length > 0) { | |
if (!categories['Breaking Changes']) { | |
categories['Breaking Changes'] = [] | |
} | |
categories['Breaking Changes'].push(...items.map(item => `[${version}] ${item}`)) | |
} | |
} | |
} | |
} | |
return categories | |
} | |
// Script to get detailed dependency changes for all updated packages | |
async function getDependencyChanges (startCommit, endCommit) { | |
let rangeDescription | |
if (!startCommit && !endCommit) { | |
rangeDescription = 'HEAD commit' | |
} else if (startCommit && !endCommit) { | |
rangeDescription = `${startCommit} to HEAD` | |
} else { | |
rangeDescription = `${startCommit} to ${endCommit}` | |
} | |
console.log(`π Getting detailed dependency changes for all updated packages in ${rangeDescription}...\n`) | |
const updates = getPackageUpdates(startCommit, endCommit) | |
if (updates.length === 0) { | |
console.log('No package updates found in the specified commit range.') | |
return | |
} | |
console.log(`Found ${updates.length} package updates:\n`) | |
for (const update of updates) { | |
console.log(`π¦ ${update.name}: ${update.oldVersion} β ${update.newVersion}`) | |
if (isMajorVersionBump(update.oldVersion, update.newVersion)) { | |
console.log(' β οΈ MAJOR VERSION BUMP - Potential breaking changes!') | |
} | |
const repoUrl = await getGitHubRepo(update.name) | |
if (repoUrl) { | |
console.log(`π GitHub: ${repoUrl}`) | |
console.log(`π Compare: ${repoUrl}/compare/v${update.oldVersion}...v${update.newVersion}`) | |
try { | |
const response = await fetch(`https://api.github.com/repos/${repoUrl.replace('https://github.com/', '')}/releases`) | |
const releases = await response.json() | |
if (Array.isArray(releases)) { | |
const relevantReleases = releases.filter(r => { | |
const version = r.tag_name.replace('v', '') | |
const versionParts = version.split('.').map(Number) | |
const oldParts = update.oldVersion.split('.').map(Number) | |
const newParts = update.newVersion.split('.').map(Number) | |
// Check if version is greater than oldVersion and less than or equal to newVersion | |
for (let i = 0; i < Math.max(versionParts.length, oldParts.length); i++) { | |
const v = versionParts[i] || 0 | |
const o = oldParts[i] || 0 | |
if (v > o) break | |
if (v < o) return false | |
} | |
for (let i = 0; i < Math.max(versionParts.length, newParts.length); i++) { | |
const v = versionParts[i] || 0 | |
const n = newParts[i] || 0 | |
if (v < n) break | |
if (v > n) return false | |
} | |
return true | |
}).sort((a, b) => { | |
const aVersion = a.tag_name.replace('v', '').split('.').map(Number) | |
const bVersion = b.tag_name.replace('v', '').split('.').map(Number) | |
for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) { | |
const a = aVersion[i] || 0 | |
const b = bVersion[i] || 0 | |
if (a !== b) return b - a | |
} | |
return 0 | |
}) | |
if (relevantReleases.length > 0) { | |
console.log(`π Releases between v${update.oldVersion} and v${update.newVersion}:`) | |
// Collect all categorized items | |
const allCategories = {} | |
for (const release of relevantReleases) { | |
const version = release.tag_name.replace('v', '') | |
const categories = parseReleaseNotes(release.body, version) | |
// Merge into allCategories | |
for (const [category, items] of Object.entries(categories)) { | |
if (!allCategories[category]) { | |
allCategories[category] = [] | |
} | |
allCategories[category].push(...items) | |
} | |
} | |
// Display organized by category | |
const categoryOrder = [ | |
'Breaking Changes', | |
'Deprecations', | |
'Security', | |
'Features', | |
'Bug Fixes', | |
'Performance', | |
'Dependencies', | |
'Other' | |
] | |
for (const category of categoryOrder) { | |
if (allCategories[category] && allCategories[category].length > 0) { | |
console.log(`\n ${category}:`) | |
// Remove duplicates while preserving order | |
const uniqueItems = [] | |
const seen = new Set() | |
for (const item of allCategories[category]) { | |
if (!seen.has(item)) { | |
seen.add(item) | |
uniqueItems.push(item) | |
} | |
} | |
for (const item of uniqueItems) { | |
console.log(` β’ ${item}`) | |
} | |
} | |
} | |
} else { | |
console.log(` π No releases found between v${update.oldVersion} and v${update.newVersion}`) | |
} | |
} else { | |
console.log(' π No releases found for this package') | |
} | |
} catch (error) { | |
console.error(`Error getting release notes for ${update.name}:`, error.message) | |
console.log(' π Unable to fetch release notes') | |
} | |
} else { | |
console.log(' π No GitHub repository found') | |
} | |
console.log('\n' + '='.repeat(80) + '\n') | |
} | |
} | |
// Get commit hashes from command line arguments | |
const startCommit = process.argv[2] || null | |
const endCommit = process.argv[3] || null | |
getDependencyChanges(startCommit, endCommit).catch(console.error) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment