epic-cli
Version:
Commands useful for everyday web development with node.
100 lines (83 loc) • 3.67 kB
text/typescript
import { execSync } from 'node:child_process'
import { styleText } from 'node:util'
import Bun from 'bun'
import { diff, neq, valid } from 'semver'
const skipUpdate = process.argv.includes('--no-install')
const updatedDependencies: { name: string; previous: string; latest: string; breaking: boolean }[] = []
let processedDependencies = 0
const versionRangeRegex = /(\^|~|>=|>|<=|<)?\d+(\.\d+){0,2}(\.\*)?/
const versionRangeReplaceRegex = /^(\^|~|>=|>|<=|<)/
const packageJson = await Bun.file('./package.json').json()
const dependencyTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'bundleDependencies']
const exactDependencies = [] as { name: string; version: string }[]
const dependencyCount = dependencyTypes.reduce((total, type) => total + (packageJson[type] ? Object.keys(packageJson[type]).length : 0), 0)
if (dependencyCount === 0) {
console.log('No dependencies found.')
process.exit(0)
}
process.stdout.write(`Updating ${dependencyCount} dependencies...\r`)
await Promise.all(
dependencyTypes.map(async (type) => {
const dependencies = packageJson[type] as { [key: string]: string }
if (!dependencies) {
return
}
const fetchPromises = Object.keys(dependencies).map(async (name) => {
const previousVersionRange = dependencies[name]
if (!previousVersionRange) {
return
}
const previousVersion = previousVersionRange.replace(versionRangeReplaceRegex, '')
if (!valid(previousVersion)) {
return // Ignore invalid versions
}
const versionRange = (previousVersionRange.match(versionRangeRegex) ?? [])[1]
if (!versionRange) {
exactDependencies.push({ name, version: previousVersionRange })
return // Keep exact versions as they are
}
const { version } = (await (await fetch(`https://registry.npmjs.org/${name}/latest`)).json()) as { version?: string }
if (version && neq(version, previousVersion)) {
dependencies[name] = `${versionRange || ''}${version}`
updatedDependencies.push({
name,
previous: previousVersionRange,
latest: `${versionRange || ''}${version}`,
breaking: diff(previousVersion, version) === 'major',
})
}
processedDependencies += 1
process.stdout.write(`Updating ${processedDependencies}/${dependencyCount} dependencies...\r`)
})
await Promise.all(fetchPromises)
}),
)
process.stdout.write('\r\x1b[K') // Clear progress output.
if (updatedDependencies.length > 0) {
console.log(`${updatedDependencies.length} dependencies updated.`)
for (const update of updatedDependencies) {
console.log(
`${styleText('bold', update.name)} ${update.previous} ➜ ${update.latest}${
update.breaking ? styleText('bold', styleText('red', ' BREAKING CHANGE!')) : ''
}`,
)
}
Bun.write('./package.json', `${JSON.stringify(packageJson, null, 2)}\n`)
if (!skipUpdate) {
console.log('Installing new dependencies with Bun.')
// Using bun shell here will not exit in tests...
execSync('bun update', {
stdio: 'inherit',
})
// NOTE write package again after "bun update" as it will turn ~ ranges into ^ ranges.
Bun.write('./package.json', `${JSON.stringify(packageJson, null, 2)}\n`)
}
} else {
console.log('All dependencies are up to date.')
}
if (exactDependencies.length > 0) {
console.log(
`Warning: Dependencies ${exactDependencies.map(({ name, version }) => `${styleText('bold', name)} (${version})`).join(',')} won't be updated as they are exact. Add a version range to ensure automatic updates.`,
)
}