Skip to content

Commit 86ab1de

Browse files
authored
Merge pull request #56 from beeman/beeman/sync-package-json
feat: add script to sync versions in package.json files
2 parents d6cd69d + 1f32f3e commit 86ab1de

12 files changed

+412
-0
lines changed

package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "program-examples",
3+
"version": "1.0.0",
4+
"description": "### :crab: Rust. :snake: Python. :ice_cube: Solidity. :link: All on-chain.",
5+
"scripts": {
6+
"sync-package-json": "ts-node scripts/sync-package-json.ts"
7+
},
8+
"keywords": [],
9+
"author": "Solana Foundation",
10+
"license": "MIT",
11+
"devDependencies": {
12+
"@types/node": "^20.9.0",
13+
"picocolors": "^1.0.0",
14+
"ts-node": "^10.9.1",
15+
"typescript": "^5.2.2"
16+
}
17+
}

scripts/lib/change-package-version.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { readFileSync } from 'node:fs'
2+
3+
export function changePackageVersion(file: string, pkgName: string, pkgVersion: string): [boolean, string] {
4+
const content = JSON.parse(readFileSync(file).toString('utf-8'))
5+
if (content.dependencies && content.dependencies[pkgName] && content.dependencies[pkgName] !== pkgVersion) {
6+
content.dependencies[pkgName] = pkgVersion
7+
return [true, content]
8+
}
9+
if (content.devDependencies && content.devDependencies[pkgName] && content.devDependencies[pkgName] !== pkgVersion) {
10+
content.devDependencies[pkgName] = pkgVersion
11+
return [true, content]
12+
}
13+
return [false, content]
14+
}

scripts/lib/command-check.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { basename } from 'node:path'
2+
import * as p from 'picocolors'
3+
import { getDepsCount } from './get-deps-count'
4+
import { getRecursiveFileList } from './get-recursive-file-list'
5+
6+
export function commandCheck(path: string = '.') {
7+
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
8+
const depsCounter = getDepsCount(files)
9+
10+
const single: string[] = []
11+
const multiple: string[] = []
12+
13+
Object.keys(depsCounter)
14+
.sort()
15+
.map((pkg) => {
16+
const versions = depsCounter[pkg]
17+
const versionMap = Object.keys(versions).sort()
18+
const versionsLength = versionMap.length
19+
20+
if (versionsLength === 1) {
21+
const count = versions[versionMap[0]].length
22+
single.push(`${p.green(`✔`)} ${pkg}@${versionMap[0]} (${count})`)
23+
return
24+
}
25+
26+
const versionCount: { version: string; count: number }[] = []
27+
for (const version of versionMap) {
28+
versionCount.push({ version, count: versions[version].length })
29+
}
30+
versionCount.sort((a, b) => b.count - a.count)
31+
32+
multiple.push(`${p.yellow(`⚠`)} ${pkg} has ${versionsLength} versions:`)
33+
34+
for (const { count, version } of versionCount) {
35+
multiple.push(` - ${p.bold(version)} (${count})`)
36+
}
37+
})
38+
39+
for (const string of [...single.sort(), ...multiple]) {
40+
console.log(string)
41+
}
42+
}

scripts/lib/command-help.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function commandHelp() {
2+
console.log(`Usage: yarn sync-package-json <command> [options]`)
3+
console.log(``)
4+
console.log(`Commands:`)
5+
console.log(` check <path> Check package.json files`)
6+
console.log(` help Show this help`)
7+
console.log(` list <path> List package.json files`)
8+
console.log(` set [ver] <path> Set specific version in package.json files`)
9+
console.log(` update <path> <pkgs> Update all versions in package.json files`)
10+
console.log(``)
11+
console.log(`Arguments:`)
12+
console.log(` path Path to directory`)
13+
console.log(``)
14+
console.log(`Examples:`)
15+
console.log(` yarn sync-package-json check`)
16+
console.log(` yarn sync-package-json check basics`)
17+
console.log(` yarn sync-package-json list`)
18+
console.log(` yarn sync-package-json list basics`)
19+
console.log(` yarn sync-package-json help`)
20+
console.log(` yarn sync-package-json set @coral-xyz/[email protected]`)
21+
console.log(` yarn sync-package-json set @coral-xyz/[email protected] basics`)
22+
console.log(` yarn sync-package-json update`)
23+
console.log(` yarn sync-package-json update basics`)
24+
console.log(` yarn sync-package-json update . @solana/web3.js @solana/spl-token`)
25+
process.exit(0)
26+
}

scripts/lib/command-list.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { basename } from 'node:path'
2+
import { getRecursiveFileList } from './get-recursive-file-list'
3+
4+
export function commandList(path: string) {
5+
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
6+
for (const file of files) {
7+
console.log(file)
8+
}
9+
}

scripts/lib/command-set.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { writeFileSync } from 'fs'
2+
import { basename } from 'node:path'
3+
import { changePackageVersion } from './change-package-version'
4+
import { getRecursiveFileList } from './get-recursive-file-list'
5+
6+
export function commandSet(version: string, path: string = '.') {
7+
if (!version) {
8+
console.error(`Version is required`)
9+
process.exit(1)
10+
}
11+
if (
12+
!version
13+
// Strip first character if it's a `@`
14+
.replace(/^@/, '')
15+
.includes('@')
16+
) {
17+
console.error(`Invalid package version: ${version}. Provide package with version, e.g. @solana/[email protected]`)
18+
process.exit(1)
19+
}
20+
// Take anything after the second `@` as the version, the rest is the package name
21+
const [pkg, ...rest] = version.split('@').reverse()
22+
const pkgName = rest.reverse().join('@')
23+
24+
// Make sure pkgVersions has a ^ prefix, if not add it
25+
const pkgVersion = pkg.startsWith('^') ? pkg : `^${pkg}`
26+
27+
console.log(`Setting package ${pkgName} to ${pkgVersion} in ${path}`)
28+
29+
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
30+
let count = 0
31+
for (const file of files) {
32+
const [changed, content] = changePackageVersion(file, pkgName, pkgVersion)
33+
if (changed) {
34+
writeFileSync(file, JSON.stringify(content, null, 2) + '\n')
35+
count++
36+
}
37+
}
38+
if (count === 0) {
39+
console.log(`No files updated`)
40+
} else {
41+
console.log(`Updated ${count} files`)
42+
}
43+
}

scripts/lib/command-update.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { execSync } from 'child_process'
2+
import { writeFileSync } from 'fs'
3+
import { basename } from 'node:path'
4+
import * as p from 'picocolors'
5+
import { changePackageVersion } from './change-package-version'
6+
7+
import { getDepsCount } from './get-deps-count'
8+
import { getRecursiveFileList } from './get-recursive-file-list'
9+
10+
export function commandUpdate(path: string = '.', packageNames: string[] = []) {
11+
const files = getRecursiveFileList(path).filter((file) => basename(file) === 'package.json')
12+
const depsCounter = getDepsCount(files)
13+
const pkgNames = Object.keys(depsCounter).sort()
14+
if (packageNames.length > 0) {
15+
console.log(`Updating ${packageNames.join(', ')} in ${files.length} files`)
16+
}
17+
18+
let total = 0
19+
for (const pkgName of pkgNames.filter((pkgName) => packageNames.length === 0 || packageNames.includes(pkgName))) {
20+
// Get latest version from npm
21+
const npmVersion = execSync(`npm view ${pkgName} version`).toString().trim()
22+
23+
let count = 0
24+
for (const file of files) {
25+
const [changed, content] = changePackageVersion(file, pkgName, `^${npmVersion}`)
26+
if (changed) {
27+
writeFileSync(file, JSON.stringify(content, null, 2) + '\n')
28+
count++
29+
}
30+
}
31+
total += count
32+
33+
if (count === 0) {
34+
console.log(p.dim(`Package ${pkgName} is up to date ${npmVersion}`))
35+
continue
36+
}
37+
console.log(p.green(` -> Updated ${count} files with ${pkgName} ${npmVersion}`))
38+
}
39+
40+
if (total === 0) {
41+
console.log(`No files updated`)
42+
} else {
43+
console.log(`Updated ${total} files`)
44+
}
45+
}

scripts/lib/get-deps-count.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readFileSync } from 'node:fs'
2+
3+
export function getDepsCount(files: string[] = []): Record<string, Record<string, string[]>> {
4+
const map: Record<string, JSON> = {}
5+
const depsCounter: Record<string, Record<string, string[]>> = {}
6+
7+
for (const file of files) {
8+
const content = JSON.parse(readFileSync(file).toString('utf-8'))
9+
map[file] = content
10+
11+
const deps = content.dependencies ?? {}
12+
const devDeps = content.devDependencies ?? {}
13+
14+
const merged = { ...deps, ...devDeps }
15+
16+
Object.keys(merged)
17+
.sort()
18+
.map((pkg) => {
19+
const pkgVersion = merged[pkg]
20+
if (!depsCounter[pkg]) {
21+
depsCounter[pkg] = { [pkgVersion]: [file] }
22+
return
23+
}
24+
if (!depsCounter[pkg][pkgVersion]) {
25+
depsCounter[pkg][pkgVersion] = [file]
26+
return
27+
}
28+
depsCounter[pkg][pkgVersion] = [...depsCounter[pkg][pkgVersion], file]
29+
})
30+
}
31+
return depsCounter
32+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Point method at path and return a list of all the files in the directory recursively
2+
import { readdirSync, statSync } from 'node:fs'
3+
4+
export function getRecursiveFileList(path: string): string[] {
5+
const ignore = ['.git', '.github', '.idea', '.next', '.vercel', '.vscode', 'coverage', 'dist', 'node_modules']
6+
const files: string[] = []
7+
8+
const items = readdirSync(path)
9+
items.forEach((item) => {
10+
if (ignore.includes(item)) {
11+
return
12+
}
13+
// Check out if it's a directory or a file
14+
const isDir = statSync(`${path}/${item}`).isDirectory()
15+
if (isDir) {
16+
// If it's a directory, recursively call the method
17+
files.push(...getRecursiveFileList(`${path}/${item}`))
18+
} else {
19+
// If it's a file, add it to the array of files
20+
files.push(`${path}/${item}`)
21+
}
22+
})
23+
24+
return files.filter((file) => {
25+
// Remove package.json from the root directory
26+
return path === '.' ? file !== './package.json' : true
27+
})
28+
}

scripts/lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './command-check'
2+
export * from './command-help'
3+
export * from './command-list'
4+
export * from './command-set'
5+
export * from './command-update'

scripts/sync-package-json.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { commandCheck, commandHelp, commandList, commandSet, commandUpdate } from './lib'
2+
3+
const params: string[] = process.argv.slice(3)
4+
5+
switch (process.argv[2]) {
6+
case 'check':
7+
commandCheck(params[0])
8+
break
9+
case 'list':
10+
commandList(params[0])
11+
break
12+
case 'set':
13+
commandSet(params[0], params[1])
14+
break
15+
case 'update':
16+
commandUpdate(params[0], params.slice(1))
17+
break
18+
case 'help':
19+
default:
20+
commandHelp()
21+
break
22+
}

0 commit comments

Comments
 (0)