mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
222 lines
5.7 KiB
TypeScript
222 lines
5.7 KiB
TypeScript
import { cliError } from '@jahands/cli-tools'
|
|
import Table from 'cli-table3'
|
|
import * as toml from 'smol-toml'
|
|
import { match } from 'ts-pattern'
|
|
import { z } from 'zod'
|
|
|
|
import { getRepoRoot } from './path'
|
|
import { PackageJson } from './pkg'
|
|
|
|
type UpdateResult = {
|
|
type: string
|
|
updated: number
|
|
failed: number
|
|
files: string[]
|
|
errors: string[]
|
|
}
|
|
|
|
export async function updatePnpm() {
|
|
const $$ = $({
|
|
verbose: false,
|
|
stdio: 'pipe',
|
|
})
|
|
|
|
const repoRoot = getRepoRoot()
|
|
cd(repoRoot)
|
|
const miseAvailable = await which('mise')
|
|
echo(chalk.white(`Checking for pnpm updates...`))
|
|
const res = await fetch('https://registry.npmjs.org/pnpm')
|
|
if (!res.ok) {
|
|
throw cliError(`Failed to fetch pnpm registry: ${res.status}`)
|
|
}
|
|
const body = await res.json()
|
|
const pnpm = NpmRegistryPnpmResponse.parse(body)
|
|
const latest = pnpm['dist-tags'].latest
|
|
echo(chalk.blue(`Latest pnpm version: ${latest}`))
|
|
|
|
const results: UpdateResult[] = []
|
|
|
|
if (miseAvailable) {
|
|
const miseResult = await updateMiseTomlFiles(repoRoot, latest)
|
|
results.push(miseResult)
|
|
}
|
|
|
|
const packageJsonResult = await updatePackageJsonFiles(repoRoot, latest)
|
|
results.push(packageJsonResult)
|
|
|
|
if (results.some((r) => r.updated > 0)) {
|
|
echo(chalk.blue(`Fixing formatting...`))
|
|
await $$`runx fix --format`
|
|
}
|
|
|
|
// Display summary table
|
|
const table = new Table({
|
|
head: [
|
|
chalk.whiteBright('File Type'),
|
|
chalk.whiteBright('Updated'),
|
|
chalk.whiteBright('Failed'),
|
|
chalk.whiteBright('Status'),
|
|
],
|
|
})
|
|
|
|
let totalUpdated = 0
|
|
let totalFailed = 0
|
|
for (const result of results) {
|
|
totalUpdated += result.updated
|
|
totalFailed += result.failed
|
|
|
|
const status = match(result)
|
|
.when(
|
|
(r) => r.failed > 0,
|
|
() => chalk.red('Failed')
|
|
)
|
|
.when(
|
|
(r) => r.updated > 0,
|
|
() => chalk.green('Updated')
|
|
)
|
|
.otherwise(() => chalk.gray('No changes'))
|
|
|
|
table.push([result.type, result.updated.toString(), result.failed.toString(), status])
|
|
}
|
|
|
|
// Show detailed errors if any
|
|
for (const result of results) {
|
|
if (result.errors.length > 0) {
|
|
echo(chalk.red(`\nErrors in ${result.type}:`))
|
|
for (const error of result.errors) {
|
|
echo(chalk.red(` ${error}`))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (totalUpdated > 0) {
|
|
if (miseAvailable) {
|
|
echo(chalk.blue('\nRunning mise up...'))
|
|
await $$`mise up`
|
|
}
|
|
|
|
echo(chalk.blue(`Fixing formatting...`))
|
|
await $$`runx fix --format`
|
|
|
|
echo(chalk.greenBright(`\nSuccessfully updated pnpm to ${latest}`))
|
|
} else if (totalFailed > 0) {
|
|
echo(chalk.red(`\nFailed to update some files. See errors above.`))
|
|
process.exit(1)
|
|
} else {
|
|
echo(chalk.green(`\nNo pnpm updates needed, already on ${latest}`))
|
|
}
|
|
|
|
echo('\n' + table.toString())
|
|
}
|
|
|
|
const Semver = z.string().regex(/^\d+\.\d+\.\d+$/)
|
|
const NpmRegistryPnpmResponse = z.object({
|
|
_id: z.literal('pnpm'),
|
|
name: z.literal('pnpm'),
|
|
'dist-tags': z.object({
|
|
latest: Semver.describe('pnpm latest version, e.g. 9.5.0'),
|
|
}),
|
|
})
|
|
|
|
type MiseToml = z.infer<typeof MiseToml>
|
|
const MiseToml = z
|
|
.object({
|
|
tools: z
|
|
.object({
|
|
pnpm: Semver.describe('pnpm version, e.g. 9.5.0').optional(),
|
|
})
|
|
.catchall(z.string()),
|
|
alias: z.record(z.string(), z.string()).optional(),
|
|
})
|
|
.loose()
|
|
|
|
/**
|
|
* Update package.json files with the new pnpm version
|
|
* @param repoRoot - The root directory of the repository
|
|
* @param newVersion - The new pnpm version
|
|
* @returns UpdateResult with details about the operation
|
|
*/
|
|
async function updatePackageJsonFiles(repoRoot: string, newVersion: string): Promise<UpdateResult> {
|
|
const result: UpdateResult = {
|
|
type: 'package.json',
|
|
updated: 0,
|
|
failed: 0,
|
|
files: [],
|
|
errors: [],
|
|
}
|
|
|
|
const packageJsonFiles = await glob('**/package.json', {
|
|
cwd: repoRoot,
|
|
gitignore: true,
|
|
ignore: ['**/node_modules/**', 'turbo/generators/**'],
|
|
})
|
|
|
|
for (const file of packageJsonFiles) {
|
|
const filePath = `${repoRoot}/${file}`
|
|
|
|
try {
|
|
const packageJson = PackageJson.loose().parse(await Bun.file(filePath).json())
|
|
|
|
if (packageJson.packageManager?.startsWith('pnpm@')) {
|
|
const currentVersion = packageJson.packageManager.replace('pnpm@', '')
|
|
if (currentVersion !== newVersion) {
|
|
echo(chalk.blue(`Updating ${file} packageManager to pnpm@${newVersion}`))
|
|
packageJson.packageManager = `pnpm@${newVersion}`
|
|
await Bun.file(filePath).write(JSON.stringify(packageJson, null, 2) + '\n')
|
|
result.updated++
|
|
result.files.push(file)
|
|
}
|
|
}
|
|
} catch (e) {
|
|
result.failed++
|
|
result.errors.push(`Failed to update ${file}: ${e instanceof Error ? e.message : String(e)}`)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Update mise.toml and .mise.toml files with the new pnpm version
|
|
* @param repoRoot - The root directory of the repository
|
|
* @param newVersion - The new pnpm version
|
|
* @returns UpdateResult with details about the operation
|
|
*/
|
|
async function updateMiseTomlFiles(repoRoot: string, newVersion: string): Promise<UpdateResult> {
|
|
const result: UpdateResult = {
|
|
type: 'mise.toml',
|
|
updated: 0,
|
|
failed: 0,
|
|
files: [],
|
|
errors: [],
|
|
}
|
|
|
|
const miseTomlFiles = await glob('**/{.mise.toml,mise.toml}', {
|
|
cwd: repoRoot,
|
|
gitignore: true,
|
|
ignore: ['**/node_modules/**'],
|
|
})
|
|
|
|
for (const file of miseTomlFiles) {
|
|
const filePath = `${repoRoot}/${file}`
|
|
|
|
try {
|
|
const miseToml = MiseToml.parse(toml.parse(await Bun.file(filePath).text()))
|
|
|
|
if (miseToml.tools.pnpm && miseToml.tools.pnpm !== newVersion) {
|
|
echo(chalk.blue(`Updating ${file} to pnpm@${newVersion}`))
|
|
miseToml.tools.pnpm = newVersion
|
|
const miseString = toml.stringify(miseToml) + '\n'
|
|
await Bun.file(filePath).write(miseString.replaceAll('"', "'"))
|
|
result.updated++
|
|
result.files.push(file)
|
|
}
|
|
} catch (e) {
|
|
result.failed++
|
|
result.errors.push(`Failed to update ${file}: ${e instanceof Error ? e.message : String(e)}`)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|