#!/usr/bin/env node // Copyright 2017-2025 @pezkuwi/dev authors & contributors // SPDX-License-Identifier: Apache-2.0 import fs from 'node:fs'; import path from 'node:path'; /** @typedef {{ dependencies: Record; devDependencies: Record; peerDependencies: Record; optionalDependencies: Record; resolutions: Record; name: string; version: string; versions: { git: string; npm: string; } }} PkgJson */ // The keys to look for in package.json const PKG_PATHS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'resolutions']; const versions = {}; const paths = []; let updated = 0; /** * Returns the path of the package.json inside the supplied directory * * @param {string} dir * @returns {string} */ function packageJsonPath (dir) { return path.join(dir, 'package.json'); } /** * Update the supplied dependency map with the latest (version map) versions * * @param {string} dir * @param {boolean} hasDirLogged * @param {string} key * @param {Record} dependencies * @returns {[number, Record]} */ function updateDependencies (dir, hasDirLogged, key, dependencies) { let count = 0; const adjusted = Object .keys(dependencies) .sort() .reduce((result, name) => { const current = dependencies[name]; const version = !current.startsWith('^') || current.endsWith('-x') ? current : versions[name] || current; if (version !== current) { if (count === 0) { if (!hasDirLogged) { console.log('\t', dir); } console.log('\t\t', key); } console.log('\t\t\t', name.padStart(30), '\t', current.padStart(8), '->', version); count++; updated++; } result[name] = version; return result; }, {}); return [count, adjusted]; } /** * Returns a parsed package.json * * @param {string} dir * @returns {PkgJson} */ function parsePackage (dir) { return JSON.parse( fs.readFileSync(packageJsonPath(dir), 'utf-8') ); } /** * Outputs the supplied package.json * * @param {string} dir * @param {Record} json */ function writePackage (dir, json) { fs.writeFileSync(packageJsonPath(dir), `${JSON.stringify(json, null, 2)}\n`); } /** * Rerite the package.json with updated dependecies * * @param {string} dir */ function updatePackage (dir) { const json = parsePackage(dir); let hasDirLogged = false; writePackage(dir, Object .keys(json) .reduce((result, key) => { if (PKG_PATHS.includes(key)) { const [count, adjusted] = updateDependencies(dir, hasDirLogged, key, json[key]); result[key] = adjusted; if (count) { hasDirLogged = true; } } else { result[key] = json[key]; } return result; }, {}) ); } /** * Loop through package/*, extracting the package names and their versions * * @param {string} dir */ function findPackages (dir) { const pkgsDir = path.join(dir, 'packages'); paths.push(dir); if (!fs.existsSync(pkgsDir)) { return; } const { versions: { npm: lastVersion } } = parsePackage(dir); fs .readdirSync(pkgsDir) .filter((entry) => { const full = path.join(pkgsDir, entry); return !['.', '..'].includes(entry) && fs.lstatSync(full).isDirectory() && fs.existsSync(path.join(full, 'package.json')); }) .forEach((dir) => { const full = path.join(pkgsDir, dir); const pkgJson = parsePackage(full); paths.push(full); versions[pkgJson.name] = `^${lastVersion}`; // for dev we want to pull through the additionals, i.e. we want to // align deps found in dev/others with those in dev as master if (pkgJson.name === '@pezkuwi/dev') { PKG_PATHS.forEach((depPath) => { Object .entries(pkgJson[depPath] || {}) .filter(([, version]) => version.startsWith('^')) .forEach(([pkg, version]) => { versions[pkg] ??= version; }); }); } }); } console.log('Extracting ...'); fs .readdirSync('.') .filter((name) => !['.', '..'].includes(name) && fs.existsSync(packageJsonPath(name)) ) .sort() .forEach(findPackages); console.log('\t', Object.keys(versions).length, 'packages found'); console.log('Updating ...'); paths.forEach(updatePackage); console.log('\t', updated, 'versions adjusted');