Skip to content

Commit

Permalink
feat: add script to sync versions in package.json files
Browse files Browse the repository at this point in the history
  • Loading branch information
beeman committed Dec 12, 2023
1 parent d6cd69d commit 1f32f3e
Show file tree
Hide file tree
Showing 12 changed files with 412 additions and 0 deletions.
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "program-examples",
"version": "1.0.0",
"description": "### :crab: Rust. :snake: Python. :ice_cube: Solidity. :link: All on-chain.",
"scripts": {
"sync-package-json": "ts-node scripts/sync-package-json.ts"
},
"keywords": [],
"author": "Solana Foundation",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.9.0",
"picocolors": "^1.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
14 changes: 14 additions & 0 deletions scripts/lib/change-package-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFileSync } from 'node:fs'

export function changePackageVersion(file: string, pkgName: string, pkgVersion: string): [boolean, string] {
const content = JSON.parse(readFileSync(file).toString('utf-8'))
if (content.dependencies && content.dependencies[pkgName] && content.dependencies[pkgName] !== pkgVersion) {
content.dependencies[pkgName] = pkgVersion
return [true, content]
}
if (content.devDependencies && content.devDependencies[pkgName] && content.devDependencies[pkgName] !== pkgVersion) {
content.devDependencies[pkgName] = pkgVersion
return [true, content]
}
return [false, content]
}
42 changes: 42 additions & 0 deletions scripts/lib/command-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { basename } from 'node:path'
import * as p from 'picocolors'
import { getDepsCount } from './get-deps-count'
import { getRecursiveFileList } from './get-recursive-file-list'

export function commandCheck(path: string = '.') {
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
const depsCounter = getDepsCount(files)

const single: string[] = []
const multiple: string[] = []

Object.keys(depsCounter)
.sort()
.map((pkg) => {
const versions = depsCounter[pkg]
const versionMap = Object.keys(versions).sort()
const versionsLength = versionMap.length

if (versionsLength === 1) {
const count = versions[versionMap[0]].length
single.push(`${p.green(`✔`)} ${pkg}@${versionMap[0]} (${count})`)
return
}

const versionCount: { version: string; count: number }[] = []
for (const version of versionMap) {
versionCount.push({ version, count: versions[version].length })
}
versionCount.sort((a, b) => b.count - a.count)

multiple.push(`${p.yellow(`⚠`)} ${pkg} has ${versionsLength} versions:`)

for (const { count, version } of versionCount) {
multiple.push(` - ${p.bold(version)} (${count})`)
}
})

for (const string of [...single.sort(), ...multiple]) {
console.log(string)
}
}
26 changes: 26 additions & 0 deletions scripts/lib/command-help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function commandHelp() {
console.log(`Usage: yarn sync-package-json <command> [options]`)
console.log(``)
console.log(`Commands:`)
console.log(` check <path> Check package.json files`)
console.log(` help Show this help`)
console.log(` list <path> List package.json files`)
console.log(` set [ver] <path> Set specific version in package.json files`)
console.log(` update <path> <pkgs> Update all versions in package.json files`)
console.log(``)
console.log(`Arguments:`)
console.log(` path Path to directory`)
console.log(``)
console.log(`Examples:`)
console.log(` yarn sync-package-json check`)
console.log(` yarn sync-package-json check basics`)
console.log(` yarn sync-package-json list`)
console.log(` yarn sync-package-json list basics`)
console.log(` yarn sync-package-json help`)
console.log(` yarn sync-package-json set @coral-xyz/[email protected]`)
console.log(` yarn sync-package-json set @coral-xyz/[email protected] basics`)
console.log(` yarn sync-package-json update`)
console.log(` yarn sync-package-json update basics`)
console.log(` yarn sync-package-json update . @solana/web3.js @solana/spl-token`)
process.exit(0)
}
9 changes: 9 additions & 0 deletions scripts/lib/command-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { basename } from 'node:path'
import { getRecursiveFileList } from './get-recursive-file-list'

export function commandList(path: string) {
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
for (const file of files) {
console.log(file)
}
}
43 changes: 43 additions & 0 deletions scripts/lib/command-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { writeFileSync } from 'fs'
import { basename } from 'node:path'
import { changePackageVersion } from './change-package-version'
import { getRecursiveFileList } from './get-recursive-file-list'

export function commandSet(version: string, path: string = '.') {
if (!version) {
console.error(`Version is required`)
process.exit(1)
}
if (
!version
// Strip first character if it's a `@`
.replace(/^@/, '')
.includes('@')
) {
console.error(`Invalid package version: ${version}. Provide package with version, e.g. @solana/[email protected]`)
process.exit(1)
}
// Take anything after the second `@` as the version, the rest is the package name
const [pkg, ...rest] = version.split('@').reverse()
const pkgName = rest.reverse().join('@')

// Make sure pkgVersions has a ^ prefix, if not add it
const pkgVersion = pkg.startsWith('^') ? pkg : `^${pkg}`

console.log(`Setting package ${pkgName} to ${pkgVersion} in ${path}`)

const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
let count = 0
for (const file of files) {
const [changed, content] = changePackageVersion(file, pkgName, pkgVersion)
if (changed) {
writeFileSync(file, JSON.stringify(content, null, 2) + '\n')
count++
}
}
if (count === 0) {
console.log(`No files updated`)
} else {
console.log(`Updated ${count} files`)
}
}
45 changes: 45 additions & 0 deletions scripts/lib/command-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { execSync } from 'child_process'
import { writeFileSync } from 'fs'
import { basename } from 'node:path'
import * as p from 'picocolors'
import { changePackageVersion } from './change-package-version'

import { getDepsCount } from './get-deps-count'
import { getRecursiveFileList } from './get-recursive-file-list'

export function commandUpdate(path: string = '.', packageNames: string[] = []) {
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
const depsCounter = getDepsCount(files)
const pkgNames = Object.keys(depsCounter).sort()
if (packageNames.length > 0) {
console.log(`Updating ${packageNames.join(', ')} in ${files.length} files`)
}

let total = 0
for (const pkgName of pkgNames.filter((pkgName) => packageNames.length === 0 || packageNames.includes(pkgName))) {
// Get latest version from npm
const npmVersion = execSync(`npm view ${pkgName} version`).toString().trim()

let count = 0
for (const file of files) {
const [changed, content] = changePackageVersion(file, pkgName, `^${npmVersion}`)
if (changed) {
writeFileSync(file, JSON.stringify(content, null, 2) + '\n')
count++
}
}
total += count

if (count === 0) {
console.log(p.dim(`Package ${pkgName} is up to date ${npmVersion}`))
continue
}
console.log(p.green(` -> Updated ${count} files with ${pkgName} ${npmVersion}`))
}

if (total === 0) {
console.log(`No files updated`)
} else {
console.log(`Updated ${total} files`)
}
}
32 changes: 32 additions & 0 deletions scripts/lib/get-deps-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { readFileSync } from 'node:fs'

export function getDepsCount(files: string[] = []): Record<string, Record<string, string[]>> {
const map: Record<string, JSON> = {}
const depsCounter: Record<string, Record<string, string[]>> = {}

for (const file of files) {
const content = JSON.parse(readFileSync(file).toString('utf-8'))
map[file] = content

const deps = content.dependencies ?? {}
const devDeps = content.devDependencies ?? {}

const merged = { ...deps, ...devDeps }

Object.keys(merged)
.sort()
.map((pkg) => {
const pkgVersion = merged[pkg]
if (!depsCounter[pkg]) {
depsCounter[pkg] = { [pkgVersion]: [file] }
return
}
if (!depsCounter[pkg][pkgVersion]) {
depsCounter[pkg][pkgVersion] = [file]
return
}
depsCounter[pkg][pkgVersion] = [...depsCounter[pkg][pkgVersion], file]
})
}
return depsCounter
}
28 changes: 28 additions & 0 deletions scripts/lib/get-recursive-file-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Point method at path and return a list of all the files in the directory recursively
import { readdirSync, statSync } from 'node:fs'

export function getRecursiveFileList(path: string): string[] {
const ignore = ['.git', '.github', '.idea', '.next', '.vercel', '.vscode', 'coverage', 'dist', 'node_modules']
const files: string[] = []

const items = readdirSync(path)
items.forEach((item) => {
if (ignore.includes(item)) {
return
}
// Check out if it's a directory or a file
const isDir = statSync(`${path}/${item}`).isDirectory()
if (isDir) {
// If it's a directory, recursively call the method
files.push(...getRecursiveFileList(`${path}/${item}`))
} else {
// If it's a file, add it to the array of files
files.push(`${path}/${item}`)
}
})

return files.filter((file) => {
// Remove package.json from the root directory
return path === '.' ? file !== './package.json' : true
})
}
5 changes: 5 additions & 0 deletions scripts/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './command-check'
export * from './command-help'
export * from './command-list'
export * from './command-set'
export * from './command-update'
22 changes: 22 additions & 0 deletions scripts/sync-package-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { commandCheck, commandHelp, commandList, commandSet, commandUpdate } from './lib'

const params: string[] = process.argv.slice(3)

switch (process.argv[2]) {
case 'check':
commandCheck(params[0])
break
case 'list':
commandList(params[0])
break
case 'set':
commandSet(params[0], params[1])
break
case 'update':
commandUpdate(params[0], params.slice(1))
break
case 'help':
default:
commandHelp()
break
}
Loading

0 comments on commit 1f32f3e

Please sign in to comment.