rman 2.25.1 → 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.
@@ -37,6 +37,12 @@ export declare class NodeDependencyUpdater implements DependencyUpdater {
37
37
  /** Fills in what `d` may move to, from its own settings and the rules that do not depend on what
38
38
  * else moves (steps 1-3). */
39
39
  protected prepare(d: NodeDependencyUpdater.Decision, info: NpmReleases | undefined): void;
40
+ /**
41
+ * The first range of a dependency rman does not move - a pinned `11.7.5`, a `>=2 <3` - that the
42
+ * registry's newest offerable release falls outside, and that release. `undefined` when every
43
+ * such range still takes it, which is nothing worth a row.
44
+ */
45
+ protected outgrown(d: NodeDependencyUpdater.Decision, releases: readonly NpmRelease[]): NodeDependencyUpdater.Decision['unmovable'];
40
46
  /** Whether the registry offers `release` as a move at all: newer than what is declared, not
41
47
  * deprecated, not above the `latest` tag, and not a prerelease unless the range is on one. */
42
48
  protected offerable(d: NodeDependencyUpdater.Decision, release: NpmRelease): boolean;
@@ -84,6 +90,15 @@ export declare class NodeDependencyUpdater implements DependencyUpdater {
84
90
  /** The in-repo packages `pkg` declares a dependency on. */
85
91
  protected siblingsOf(pkg: Package, repoPackages: ReadonlyMap<string, Package>): Package[];
86
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;
87
102
  protected manifestFile(pkg: Package): string;
88
103
  /** `fn` over `items`, at most `limit` at a time. */
89
104
  protected mapConcurrent<T>(items: readonly T[], limit: number, fn: (item: T) => Promise<void>): Promise<void>;
@@ -98,6 +113,16 @@ export declare namespace NodeDependencyUpdater {
98
113
  fields: Map<string, string>;
99
114
  /** The fields whose range is rewritten. */
100
115
  movable: string[];
116
+ /** The fields holding a valid range rman does not rewrite - a pinned version, `>=a <b`. */
117
+ fixed: string[];
118
+ /** Not planned, but listed when a release falls outside a `fixed` range - see `outgrown`. */
119
+ listed?: boolean;
120
+ /** That release, and the range it falls outside. */
121
+ unmovable?: {
122
+ field: string;
123
+ range: string;
124
+ version: string;
125
+ };
101
126
  /** Whether this name is planned at all - rather than only stating rules for the others. */
102
127
  wanted: boolean;
103
128
  /** What is declared now, as a version: the highest floor among the movable ranges. */
@@ -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).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();
@@ -142,6 +150,7 @@ export class NodeDependencyUpdater {
142
150
  settings,
143
151
  fields: new Map(),
144
152
  movable: [],
153
+ fixed: [],
145
154
  wanted: false,
146
155
  allowed: [],
147
156
  candidates: [],
@@ -150,14 +159,19 @@ export class NodeDependencyUpdater {
150
159
  byName.set(name, d);
151
160
  }
152
161
  d.fields.set(field, range);
153
- if (this.movablePattern.test(range) && (!kinds || kinds.includes(field)))
162
+ if (kinds && !kinds.includes(field))
163
+ continue;
164
+ if (this.movablePattern.test(range))
154
165
  d.movable.push(field);
166
+ else if (semver.validRange(range))
167
+ d.fixed.push(field);
155
168
  }
156
169
  }
157
170
  for (const d of byName.values()) {
158
171
  const picked = !options.names?.length || micromatch.isMatch(d.name, options.names);
159
172
  const rejected = settings.reject.length > 0 && micromatch.isMatch(d.name, settings.reject);
160
173
  d.wanted = d.movable.length > 0 && picked && !rejected;
174
+ d.listed = !d.wanted && d.fixed.length > 0 && picked && !rejected;
161
175
  if (d.wanted) {
162
176
  d.floor = d.movable.map(f => semver.minVersion(d.fields.get(f)).version).sort(semver.rcompare)[0];
163
177
  }
@@ -196,6 +210,8 @@ export class NodeDependencyUpdater {
196
210
  prepare(d, info) {
197
211
  d.releases = new Map((info?.releases ?? []).map(r => [r.version, r]));
198
212
  d.latestTag = info?.latest;
213
+ if (d.listed && info)
214
+ d.unmovable = this.outgrown(d, info.releases);
199
215
  if (!d.wanted)
200
216
  return;
201
217
  if (!info) {
@@ -220,6 +236,30 @@ export class NodeDependencyUpdater {
220
236
  }
221
237
  d.index = d.candidates.length ? 0 : -1;
222
238
  }
239
+ /**
240
+ * The first range of a dependency rman does not move - a pinned `11.7.5`, a `>=2 <3` - that the
241
+ * registry's newest offerable release falls outside, and that release. `undefined` when every
242
+ * such range still takes it, which is nothing worth a row.
243
+ */
244
+ /* **Listed, never moved.** Only a `^` or `~` range on a plain version is rewritten, since anything
245
+ * else was written that way on purpose. But leaving such a dependency out of the table entirely
246
+ * hid it: measured on `panates/hl7v2`, `"mocha": "11.7.5"` with 12.x out appeared nowhere, and a
247
+ * person reading the table took that for "up to date". A `skipped` row says what is there and why
248
+ * it stays. */
249
+ outgrown(d, releases) {
250
+ for (const field of d.fixed) {
251
+ const range = d.fields.get(field);
252
+ const newest = releases
253
+ .map(r => semver.parse(r.version))
254
+ .filter((v) => !!v && !v.prerelease.length)
255
+ .filter(v => !releases.find(r => r.version === v.version)?.deprecated)
256
+ .filter(v => !d.latestTag || !semver.valid(d.latestTag) || semver.lte(v, d.latestTag))
257
+ .sort(semver.rcompare)[0];
258
+ if (newest && !semver.satisfies(newest, range))
259
+ return { field, range, version: newest.version };
260
+ }
261
+ return undefined;
262
+ }
223
263
  /** Whether the registry offers `release` as a move at all: newer than what is declared, not
224
264
  * deprecated, not above the `latest` tag, and not a prerelease unless the range is on one. */
225
265
  offerable(d, release) {
@@ -431,6 +471,19 @@ export class NodeDependencyUpdater {
431
471
  return [...result];
432
472
  }
433
473
  toEntry(d) {
474
+ if (!d.wanted && d.unmovable) {
475
+ const { field, range, version } = d.unmovable;
476
+ return {
477
+ package: d.pkg,
478
+ name: d.name,
479
+ types: [field],
480
+ current: range,
481
+ status: 'skipped',
482
+ latest: version,
483
+ available: version,
484
+ reason: 'not a ^ or ~ range - rman moves only those',
485
+ };
486
+ }
434
487
  const entry = {
435
488
  package: d.pkg,
436
489
  name: d.name,
@@ -461,6 +514,41 @@ export class NodeDependencyUpdater {
461
514
  this.planned.set(result, d);
462
515
  return result;
463
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
+ }
464
552
  manifestFile(pkg) {
465
553
  return path.join(pkg.dirname, 'package.json');
466
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.1';
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.1",
4
+ "version": "2.26.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {