rman 2.25.2 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,6 +90,15 @@ export declare class NodeDependencyUpdater implements DependencyUpdater {
90
90
  /** The in-repo packages `pkg` declares a dependency on. */
91
91
  protected siblingsOf(pkg: Package, repoPackages: ReadonlyMap<string, Package>): Package[];
92
92
  protected toEntry(d: NodeDependencyUpdater.Decision): DependencyUpdater.Entry;
93
+ /**
94
+ * The version of `name` that `pkg` resolves today: the first `node_modules/<name>/package.json`
95
+ * walking up from its directory, which is Node's own lookup - so a hoisted copy at a monorepo's
96
+ * root counts, and a nested one under the package wins over it.
97
+ */
98
+ protected installedVersion(pkg: Package, name: string): string | undefined;
99
+ /** The newest release `range` takes, as npm's install would pick it: no prerelease unless the
100
+ * range asks for one, nothing above the `latest` tag, a deprecated one only when nothing else is. */
101
+ protected resolvedVersion(d: NodeDependencyUpdater.Decision, range: string): string | undefined;
93
102
  protected manifestFile(pkg: Package): string;
94
103
  /** `fn` over `items`, at most `limit` at a time. */
95
104
  protected mapConcurrent<T>(items: readonly T[], limit: number, fn: (item: T) => Promise<void>): Promise<void>;
@@ -64,7 +64,15 @@ export class NodeDependencyUpdater {
64
64
  for (const d of decisions)
65
65
  this.prepare(d, registry.get(d.name));
66
66
  this.resolve(decisions, ctx);
67
- return decisions.filter(d => d.wanted || d.unmovable).map(d => this.toEntry(d));
67
+ return decisions
68
+ .filter(d => d.wanted || d.unmovable)
69
+ .map(d => {
70
+ /** Set on the entry itself, never a copy: `planned` is keyed by that object. */
71
+ const entry = this.toEntry(d);
72
+ entry.installed = this.installedVersion(d.pkg, d.name);
73
+ entry.resolved = this.resolvedVersion(d, entry.current);
74
+ return entry;
75
+ });
68
76
  }
69
77
  async applyPlan(ctx, plan) {
70
78
  const originals = new Map();
@@ -506,6 +514,41 @@ export class NodeDependencyUpdater {
506
514
  this.planned.set(result, d);
507
515
  return result;
508
516
  }
517
+ /**
518
+ * The version of `name` that `pkg` resolves today: the first `node_modules/<name>/package.json`
519
+ * walking up from its directory, which is Node's own lookup - so a hoisted copy at a monorepo's
520
+ * root counts, and a nested one under the package wins over it.
521
+ */
522
+ installedVersion(pkg, name) {
523
+ let dir = pkg.dirname;
524
+ for (;;) {
525
+ const file = path.join(dir, 'node_modules', name, 'package.json');
526
+ if (fs.existsSync(file)) {
527
+ try {
528
+ const version = JSON.parse(fs.readFileSync(file, 'utf-8'))?.version;
529
+ return typeof version === 'string' ? version : undefined;
530
+ }
531
+ catch {
532
+ return undefined;
533
+ }
534
+ }
535
+ const parent = path.dirname(dir);
536
+ if (parent === dir)
537
+ return undefined;
538
+ dir = parent;
539
+ }
540
+ }
541
+ /** The newest release `range` takes, as npm's install would pick it: no prerelease unless the
542
+ * range asks for one, nothing above the `latest` tag, a deprecated one only when nothing else is. */
543
+ resolvedVersion(d, range) {
544
+ if (!d.releases || !semver.validRange(range))
545
+ return undefined;
546
+ const taken = [...d.releases.values()]
547
+ .filter(r => semver.satisfies(r.version, range))
548
+ .filter(r => !d.latestTag || !semver.valid(d.latestTag) || semver.lte(r.version, d.latestTag))
549
+ .sort((a, b) => semver.rcompare(a.version, b.version));
550
+ return (taken.find(r => !r.deprecated) ?? taken[0])?.version;
551
+ }
509
552
  manifestFile(pkg) {
510
553
  return path.join(pkg.dirname, 'package.json');
511
554
  }
@@ -182,6 +182,8 @@ function jsonPlan(entries) {
182
182
  types: e.types,
183
183
  status: e.status,
184
184
  current: e.current,
185
+ installed: e.installed,
186
+ resolved: e.resolved,
185
187
  target: e.target,
186
188
  latest: e.latest,
187
189
  available: e.available,
@@ -200,30 +202,40 @@ function jsonPlan(entries) {
200
202
  * own settings leave the newer version out) or `error`. Moves first, smallest first, then the rest. */
201
203
  function printPlan(entries, root) {
202
204
  const updated = entries.filter(e => e.status === 'update');
203
- const shown = entries.filter(e => e.status !== 'up-to-date');
205
+ const shown = entries.filter(e => e.status !== 'up-to-date' || reinstall(e));
204
206
  /** Said first when nothing moves, so a run whose only rows are majors left behind does not read
205
207
  * as a list of things to do. */
206
208
  if (!updated.length) {
207
- console.log(colors.green('All dependencies are up to date') +
208
- (shown.length ? colors.gray(' - newer versions their settings leave out, or another rule holds back:') : '.'));
209
+ const why = [
210
+ ...(shown.some(e => e.status !== 'up-to-date')
211
+ ? ['newer versions their settings leave out, or another rule holds back']
212
+ : []),
213
+ ...(shown.some(reinstall) ? ['installed versions that differ from what the range takes'] : []),
214
+ ];
215
+ console.log(colors.green('All dependencies are up to date') + (why.length ? colors.gray(` - ${why.join('; ')}:`) : '.'));
209
216
  if (!shown.length)
210
217
  return;
211
218
  }
212
- const header = ['Dependency', 'Current', 'Upgrade', 'Latest', 'Change'];
219
+ /** The upgrade last before the note, so the row reads from what is there to what it would become. */
220
+ const header = ['Dependency', 'Current', 'Installed', 'Latest', 'Change', 'Upgrade'];
213
221
  const cells = (e) => [
214
222
  e.name,
215
223
  e.current,
216
- e.status === 'update' ? (e.target ?? '') : '-',
217
- e.status === 'error' ? '-' : (e.available ?? e.latest ?? ''),
224
+ e.installed ?? '-',
225
+ e.status === 'error' ? '-' : (e.available ?? e.latest ?? e.resolved ?? ''),
218
226
  changeOf(e),
227
+ e.status === 'update' ? (e.target ?? '') : '-',
219
228
  ];
229
+ const last = header.length - 1;
220
230
  const widths = header.map((h, i) => Math.max(h.length, ...shown.map(e => cells(e)[i].length)));
221
231
  /** Padded to the column, except a last value nothing follows - trailing spaces inside a colour
222
232
  * code are past the reach of `trimEnd`. */
223
233
  const line = (values, paint = text => text) => ' ' + values.map((v, i) => paint(i === values.length - 1 ? v : v.padEnd(widths[i]), i)).join(' ');
224
234
  for (const pkg of [...new Set(shown.map(e => e.package))]) {
225
235
  const sizes = pkg.versionScheme.bumpNames;
226
- const rank = (e) => e.status === 'update' ? sizes.indexOf(e.bump) : sizes.length + ['held', 'skipped', 'error'].indexOf(e.status);
236
+ const rank = (e) => e.status === 'update'
237
+ ? sizes.indexOf(e.bump)
238
+ : sizes.length + ['held', 'skipped', 'error', 'up-to-date'].indexOf(e.status);
227
239
  const rows = shown.filter(e => e.package === pkg).sort((x, y) => rank(x) - rank(y));
228
240
  const where = path.relative(root, pkg.dirname);
229
241
  console.log(colors.cyan(pkg.name) + (where ? colors.gray(` (${where})`) : ''));
@@ -231,16 +243,17 @@ function printPlan(entries, root) {
231
243
  for (const e of rows) {
232
244
  const paint = e.status === 'update' ? sizeColor(e.bump, sizes) : changeColor(e.status);
233
245
  const values = cells(e);
234
- if (e.reason)
235
- values[4] = values[4].padEnd(widths[4]);
236
- const text = line(values, (v, column) => (column === 2 || column === 4 ? paint(v) : v));
237
- if (!e.reason) {
246
+ const note = e.reason ?? (reinstall(e) ? `"${e.current}" takes ${e.resolved} - install again` : undefined);
247
+ if (note)
248
+ values[last] = values[last].padEnd(widths[last]);
249
+ const text = line(values, (v, column) => (column === 4 || column === 5 ? paint(v) : v));
250
+ if (!note) {
238
251
  console.log(text);
239
252
  continue;
240
253
  }
241
254
  const indent = 2 + widths.reduce((sum, w) => sum + w + 2, 0);
242
- const note = wrapNote(e.reason, process.stdout.isTTY ? process.stdout.columns - indent : 0);
243
- console.log(`${text} ${note.map(l => colors.gray(l)).join('\n' + ' '.repeat(indent))}`);
255
+ const wrapped = wrapNote(note, process.stdout.isTTY ? process.stdout.columns - indent : 0);
256
+ console.log(`${text} ${wrapped.map(l => colors.gray(l)).join('\n' + ' '.repeat(indent))}`);
244
257
  }
245
258
  }
246
259
  }
@@ -268,8 +281,18 @@ function wrapNote(note, width) {
268
281
  }
269
282
  /** What the `Change` column says: the size of the move, or why there is none. */
270
283
  function changeOf(e) {
284
+ if (e.status === 'up-to-date')
285
+ return reinstall(e) ? 'install' : e.status;
271
286
  return e.status === 'update' ? (e.bump ?? 'other') : e.status;
272
287
  }
288
+ /**
289
+ * A range already at the newest version, with something else installed - a `node_modules` behind
290
+ * its manifest. Listed although nothing moves: hidden, it read as up to date while the code ran
291
+ * against the older version.
292
+ */
293
+ function reinstall(e) {
294
+ return e.status === 'up-to-date' && !!e.installed && !!e.resolved && e.installed !== e.resolved;
295
+ }
273
296
  /** `held` and `skipped` in yellow, an error in red - a row with no move to colour by size. */
274
297
  function changeColor(status) {
275
298
  return status === 'error' ? colors.red : colors.yellow;
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.25.2';
1
+ export const version = '2.26.0';
@@ -76,6 +76,12 @@ export declare namespace DependencyUpdater {
76
76
  types: string[];
77
77
  /** The range as the manifest declares it. */
78
78
  current: string;
79
+ /** The version installed now, as the package would resolve it - unset when it is not
80
+ * installed, or the ecosystem cannot say. */
81
+ installed?: string;
82
+ /** The newest version the declared range takes - what an install would put there. An
83
+ * `'up-to-date'` entry whose `installed` differs from it is still listed. */
84
+ resolved?: string;
79
85
  status: 'update' | 'held' | 'skipped' | 'up-to-date' | 'error';
80
86
  /** The range it is rewritten to, on an `'update'`. */
81
87
  target?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.25.2",
4
+ "version": "2.26.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {