@juit/check-updates
Version:
Small and fast utility to update package dependencies
261 lines (260 loc) • 10.1 kB
JavaScript
// 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