Skip to content

Instantly share code, notes, and snippets.

@SgtPooki
Last active August 12, 2025 16:45
Show Gist options
  • Save SgtPooki/4611ccfc906dde8fb1c86a644883c0f8 to your computer and use it in GitHub Desktop.
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
#!/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