UNPKG

@juit/check-updates

Version:

Small and fast utility to update package dependencies

261 lines (260 loc) 10.1 kB
// updater.ts import assert from "node:assert"; import { EventEmitter } from "node:events"; import { readFile, writeFile } from "node:fs/promises"; import { relative, resolve } from "node:path"; import semver from "semver"; import { B, G, R, X, Y, makeDebug } from "./debug.mjs"; import { readNpmRc } from "./npmrc.mjs"; var dependencyTypes = [ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies" ]; var Workspaces = class { _versions = {}; _emitter = new EventEmitter().setMaxListeners(100); get length() { return Object.entries(this._versions).length; } onUpdate(handler) { this._emitter.on("update", handler); } register(name, version) { assert(!this._versions[name], `Package "${name}" already registered`); this._versions[name] = version || "0.0.0"; } update(name, version) { assert(this._versions[name], `Package "${name}" not registered`); const oldVersion = this._versions[name] || "0.0.0"; assert(semver.gte(version, oldVersion), `Package "${name}" new version ${version} less than old ${oldVersion}`); if (semver.eq(version, oldVersion)) return; this._versions[name] = version; this._emitter.emit("update", name, version); } has(name) { return !!this._versions[name]; } *[Symbol.iterator]() { for (const [name, version] of Object.entries(this._versions)) { yield [name, version]; } } }; var Updater = class _Updater { constructor(_packageFile, _options, _cache, _workspaces = new Workspaces()) { this._packageFile = _packageFile; this._options = _options; this._cache = _cache; this._workspaces = _workspaces; this._packageFile = resolve(_packageFile); this._debug = makeDebug(_options.debug); this._children = []; _workspaces.onUpdate((name, version) => { if (!this._packageData) return; for (const type of dependencyTypes) { const dependencies = this._packageData[type]; if (!dependencies) continue; if (!dependencies[name]) continue; if (dependencies[name] === version) continue; dependencies[name] = version; this._changed = true; this._bump(); } }); } _packageData; _npmRc; _originalVersion; _debug; _children; _changed = false; get name() { assert(this._packageData, "Updater not initialized"); return this._packageData.name; } get version() { assert(this._packageData, "Updater not initialized"); return this._packageData.version || "0.0.0"; } set version(version) { assert(this._originalVersion && this._packageData, "Updater not initialized"); assert(semver.lte(this._originalVersion, version), [ `Unable to set version for "${this.packageFile}" to "${version}"`, `as it's less than original version "${this._originalVersion}"` ].join(" ")); if (semver.eq(this._originalVersion, version)) return; if (this._packageData.version === version) return; this._changed = true; console.log(`Updating ${this._details} version to ${Y}${version}${X}`); this._packageData.version = version; if (this.name) this._workspaces.update(this.name, version); } get packageFile() { return relative(process.cwd(), this._packageFile); } get changed() { if (this._changed) return true; return this._children.reduce((changed, child) => changed || child.changed, false); } async init() { this._debug("Reading package file", this.packageFile); const json = await readFile(this._packageFile, "utf8"); const data = this._packageData = JSON.parse(json); assert( data && typeof data === "object" && !Array.isArray(data), `File ${this.packageFile} is not a valid "pacakge.json" file` ); const npmrc = await readNpmRc(this._packageFile); if (data.name) this._workspaces.register(data.name, data.version); this._originalVersion = data.version || "0.0.0"; if (this._options.workspaces && data.workspaces) { for (const path of data.workspaces) { const packageFile = resolve(this._packageFile, "..", path, "package.json"); const updater = new _Updater(packageFile, this._options, this._cache, this._workspaces); this._children.push(await updater.init()); } } this._packageData = data; this._npmRc = npmrc; return this; } get _details() { let string = `${G}${this.packageFile}${X}`; if (this.name || this.version) { string += ` [${Y}`; if (this.name) string += `${this.name}`; if (this.name && this.version) string += " "; if (this.version) string += `${this.version}`; string += `${X}]`; } return string; } _bump() { assert(this._originalVersion, "Updater not initialized"); if (this._options.bump) { this.version = semver.inc(this._originalVersion, this._options.bump) || this._originalVersion; } } /** Update a single dependency, returning the highest matching version */ async _updateDependency(name, rangeString) { assert(this._npmRc, "Updater not initialized"); if (this._workspaces.has(name)) { this._debug(`Not processing workspace package ${Y}${name}${X}`); return rangeString; } const match = /^\s*([~^])\s*(\d+(\.\d+(\.\d+)?)?)(-(alpha|beta|rc)[.-]\d+)?\s*$/.exec(rangeString); if (!match) { this._debug(`Not processing range ${G}${rangeString}${X} for ${Y}${name}${X}`); return rangeString; } const [, specifier = "", version = "", , , label = ""] = match; if (!this._options.strict) { const r = rangeString; rangeString = `>=${version}${label}`; if (specifier === "~") rangeString += ` <${semver.inc(version, "major")}`; this._debug(`Extending version for ${Y}${name}${X} from ${G}${r}${X} to ${G}${rangeString}${X}`); } const range = new semver.Range(rangeString); const versions = await this._cache.getVersions(name, this._npmRc, false); for (const v of versions) { if (range.test(v)) return `${specifier}${v}`; } if (label) { const versions2 = await this._cache.getVersions(name, this._npmRc, true); for (const v of versions2) { if (range.test(v)) return `${specifier}${v}`; } } return `${specifier}${version}${label}`; } /** Update a dependencies group, populating the "updated" version field */ async _updateDependenciesGroup(type) { assert(this._packageData, "Updater not initialized"); const dependencies = this._packageData[type]; if (!dependencies) return []; const promises = Object.entries(dependencies).map(async ([name, declared]) => { const updated = await this._updateDependency(name, declared); if (!this._options.debug) process.stdout.write("."); if (updated === declared) return; dependencies[name] = updated; return { name, declared, updated, type }; }); return (await Promise.all(promises)).filter((change) => !!change); } /** Update dependencies and return the version number of this package */ async update() { assert(this._packageData, "Updater not initialized"); for (const child of this._children) await child.update(); process.stdout.write(`Processing ${this._details} `); if (this._options.debug) process.stdout.write("\n"); const changes = await this._updateDependenciesGroup("dependencies"); if (changes.length || !this._options.quick) { changes.push(...await this._updateDependenciesGroup("devDependencies")); changes.push(...await this._updateDependenciesGroup("optionalDependencies")); changes.push(...await this._updateDependenciesGroup("peerDependencies")); } if (this._options.debug) process.stdout.write("Updated with"); if (!changes.length) return void console.log(` ${R}no changes${X}`); this._changed = true; changes.sort(({ name: a }, { name: b }) => a < b ? -1 : a > b ? 1 : 0); console.log(` ${R}${changes.length} changes${X}`); let lname = 0; let ldeclared = 0; let lupdated = 0; for (const { name, declared, updated } of changes) { lname = lname > name.length ? lname : name.length; ldeclared = ldeclared > declared.length ? ldeclared : declared.length; lupdated = lupdated > updated.length ? lupdated : updated.length; } for (const { name, declared, updated, type } of changes) { const kind = type === "devDependencies" ? "dev" : type === "peerDependencies" ? "peer" : type === "optionalDependencies" ? "optional" : "main"; console.log([ ` * ${Y}${name.padEnd(lname)}${X}`, ` : ${G}${declared.padStart(ldeclared)}${X}`, ` -> ${G}${updated.padEnd(lupdated)} ${B}${kind}${X}` ].join("")); } this._bump(); } align(version) { if (this._workspaces.length < 2) { return this._debug(`No workspaces found in ${this._details}`); } if (!version) { let aligned = "0.0.0"; for (const [, version2] of this._workspaces) { if (semver.gt(version2, aligned)) aligned = version2; } version = aligned; } this.version = version; for (const child of this._children) child.version = version; console.log(`Workspaces versions aligned to ${Y}${version}${X}`); } /** Write out the new package file */ async write() { assert(this._packageData, "Updater not initialized"); for (const type of dependencyTypes) { const dependencies = Object.entries(this._packageData[type] || {}); if (dependencies.length) { this._packageData[type] = dependencies.sort(([nameA], [nameB]) => nameA.localeCompare(nameB)).reduce((deps, [name, version]) => { deps[name] = version; return deps; }, {}); } else { delete this._packageData[type]; } } const json = JSON.stringify(this._packageData, null, 2); this._debug(`${Y}>>>`, this.packageFile, `<<<${X} ${json}`); await writeFile(this.packageFile, json + "\n"); for (const child of this._children) await child.write(); } }; export { Updater }; //# sourceMappingURL=updater.mjs.map