rman 2.24.1 → 2.25.1

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.
@@ -33,6 +33,15 @@ export declare class NodeManifestProvider implements ManifestProvider {
33
33
  * Only an explicit version after `workspace:` needs bumping, like a plain range.
34
34
  */
35
35
  updateDependencyVersions(manifest: Manifest, bumped: ReadonlyMap<Package, string>): void;
36
+ /**
37
+ * Every field naming `dependency`, with npm's answer to whether its range accepts `version` and
38
+ * the range with its floor raised to it - see `ManifestProvider.referencesTo`.
39
+ *
40
+ * A bare `"workspace:*"`/`"^"`/`"~"` is `dynamic`: it already follows the sibling, so there is
41
+ * nothing to keep or raise. An explicit `"workspace:<range>"` is judged by the range after it.
42
+ */
43
+ referencesTo(manifest: Manifest, dependency: string, version: string): ManifestProvider.Reference[];
44
+ setReference(manifest: Manifest, field: string, dependency: string, range: string): void;
36
45
  /**
37
46
  * What the npm registry says this package's current version is - used *only* by
38
47
  * `detectChangeHash`, to guess a tag name for a package that has no git tag yet. See
@@ -55,6 +64,15 @@ export declare class NodeManifestProvider implements ManifestProvider {
55
64
  stampVersion(file: string, content: string, version: string, options?: {
56
65
  constant?: string;
57
66
  }): string | undefined;
67
+ /**
68
+ * `range` with its lower bound moved up to `version`, keeping its upper bound and its operator -
69
+ * `undefined` when the floor is already at or above it, `null` for a shape with no single floor
70
+ * to move.
71
+ *
72
+ * The shapes a hand-written range takes: `^a`, `~a`, `>=a`, `>=a <b` (or `<=b`) and `a - b`. An
73
+ * exact pin never needs moving - one that accepts `version` already is it.
74
+ */
75
+ protected raiseFloor(range: string, version: string): string | null | undefined;
58
76
  write(dir: string, manifest: Manifest): void;
59
77
  /** The peers this manifest marks `"optional": true`, which `dependencies` leaves out of the
60
78
  * graph. `optionalDependencies` is deliberately *not* read the same way: an optional peer says
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import semver from 'semver';
3
4
  import { Manifest } from '../../../core/interfaces/manifest.js';
4
5
  import { stampVersionConstant } from '../../../utils/version-stamp.js';
5
6
  import { npmViewVersion } from '../../publish-targets/npm/npm-view.js';
@@ -91,6 +92,44 @@ export class NodeManifestProvider {
91
92
  }
92
93
  }
93
94
  }
95
+ /**
96
+ * Every field naming `dependency`, with npm's answer to whether its range accepts `version` and
97
+ * the range with its floor raised to it - see `ManifestProvider.referencesTo`.
98
+ *
99
+ * A bare `"workspace:*"`/`"^"`/`"~"` is `dynamic`: it already follows the sibling, so there is
100
+ * nothing to keep or raise. An explicit `"workspace:<range>"` is judged by the range after it.
101
+ */
102
+ referencesTo(manifest, dependency, version) {
103
+ const refs = [];
104
+ for (const field of DEPENDENCY_KEYS) {
105
+ const declared = manifest.raw[field]?.[dependency];
106
+ if (typeof declared !== 'string')
107
+ continue;
108
+ const workspace = parseWorkspaceRange(declared);
109
+ if (workspace && workspace.selector !== 'explicit') {
110
+ refs.push({ field, range: declared, dynamic: true, accepts: true });
111
+ continue;
112
+ }
113
+ const range = workspace ? workspace.range : declared;
114
+ const raised = this.raiseFloor(range, version);
115
+ refs.push({
116
+ field,
117
+ range: declared,
118
+ accepts: semver.satisfies(version, range),
119
+ ...(raised === null
120
+ ? { unraisable: true }
121
+ : raised
122
+ ? { raised: workspace ? `workspace:${raised}` : raised }
123
+ : {}),
124
+ });
125
+ }
126
+ return refs;
127
+ }
128
+ setReference(manifest, field, dependency, range) {
129
+ const deps = manifest.raw[field];
130
+ if (deps && typeof deps === 'object')
131
+ deps[dependency] = range;
132
+ }
94
133
  /**
95
134
  * What the npm registry says this package's current version is - used *only* by
96
135
  * `detectChangeHash`, to guess a tag name for a package that has no git tag yet. See
@@ -115,6 +154,45 @@ export class NodeManifestProvider {
115
154
  stampVersion(file, content, version, options) {
116
155
  return stampVersionConstant(content, version, options?.constant);
117
156
  }
157
+ /**
158
+ * `range` with its lower bound moved up to `version`, keeping its upper bound and its operator -
159
+ * `undefined` when the floor is already at or above it, `null` for a shape with no single floor
160
+ * to move.
161
+ *
162
+ * The shapes a hand-written range takes: `^a`, `~a`, `>=a`, `>=a <b` (or `<=b`) and `a - b`. An
163
+ * exact pin never needs moving - one that accepts `version` already is it.
164
+ */
165
+ /* **`null` rather than a guess for anything else**, `||` above all: which alternative's floor is
166
+ * meant cannot be read off the range, and rewriting the wrong one changes what it accepts.
167
+ * `version` reports it instead, so the range can be raised by hand. */
168
+ raiseFloor(range, version) {
169
+ const r = range.trim();
170
+ let floor;
171
+ try {
172
+ floor = semver.minVersion(r);
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ if (!floor)
178
+ return null;
179
+ if (semver.lte(version, floor))
180
+ return undefined;
181
+ if (semver.valid(r))
182
+ return undefined;
183
+ const caret = /^([\^~])\s*v?\d[^\s|]*$/.exec(r);
184
+ if (caret)
185
+ return `${caret[1]}${version}`;
186
+ if (/^>=\s*v?\d\S*$/.test(r))
187
+ return `>=${version}`;
188
+ const bounded = /^>=\s*v?\d\S*\s+(<=?\s*v?\d\S*)$/.exec(r);
189
+ if (bounded)
190
+ return `>=${version} ${bounded[1]}`;
191
+ const hyphen = /^v?\d\S*\s+-\s+(v?\d\S*)$/.exec(r);
192
+ if (hyphen)
193
+ return `${version} - ${hyphen[1]}`;
194
+ return null;
195
+ }
118
196
  write(dir, manifest) {
119
197
  /**
120
198
  * The version is written back into `raw` before serializing, because `raw` is the document and
@@ -93,7 +93,14 @@ export async function npmViewReleases(name, range, cwd, options = {}) {
93
93
  const list = Array.isArray(releases) ? releases : releases ? [releases] : [];
94
94
  const meta = (options.withTime ? tags : { 'dist-tags': tags });
95
95
  return {
96
- releases: list.filter((r) => typeof r?.version === 'string'),
96
+ /** **A bare version string is a release too.** Asked for several fields, npm answers with one
97
+ * object per version - unless no version in the range has any of them set, and then it
98
+ * collapses to a plain list of version strings. Measured on `ts-gems@>=3.12.0`: six strings,
99
+ * so every release was dropped and `rman deps` reported 4.0.4 as nothing to offer - not even
100
+ * a skipped major. */
101
+ releases: list
102
+ .map((r) => (typeof r === 'string' ? { version: r } : r))
103
+ .filter((r) => typeof r?.version === 'string'),
97
104
  latest: meta['dist-tags']?.latest,
98
105
  time: meta.time,
99
106
  };
@@ -86,6 +86,12 @@ declare const config: {
86
86
  type: "string";
87
87
  choices: readonly ["changed", "dependents", "group"];
88
88
  };
89
+ dependentRanges: {
90
+ target: "config";
91
+ describe: string;
92
+ type: "string";
93
+ choices: readonly ["rewrite", "keep"];
94
+ };
89
95
  releaseTagPattern: {
90
96
  target: "config";
91
97
  describe: string;
@@ -292,6 +298,12 @@ declare const versionCommand: (app: import("../index.js").RmanApplication) => {
292
298
  type: "string";
293
299
  choices: readonly ["changed", "dependents", "group"];
294
300
  };
301
+ dependentRanges: {
302
+ target: "config";
303
+ describe: string;
304
+ type: "string";
305
+ choices: readonly ["rewrite", "keep"];
306
+ };
295
307
  releaseTagPattern: {
296
308
  target: "config";
297
309
  describe: string;
@@ -5,6 +5,7 @@ import { registerCommand } from '../interfaces/rman-config.interface.js';
5
5
  import { VersionPlanService } from '../services/version-plan.service.js';
6
6
  import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
7
7
  import { fitTable } from '../utils/fit-table.js';
8
+ import { UNCOMMITTED } from '../utils/git.js';
8
9
  import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
9
10
  import { isNamedGroupKey, isSoloGroupKey, ROOT_GROUP_KEY } from '../utils/version-group.js';
10
11
  /**
@@ -119,6 +120,17 @@ const config = {
119
120
  type: 'string',
120
121
  choices: ['changed', 'dependents', 'group'],
121
122
  },
123
+ dependentRanges: {
124
+ target: 'config',
125
+ describe: "What happens to this package's range on an in-repo dependency that is getting a new " +
126
+ 'version: "rewrite" (the default) sets it to the new version and releases this package for ' +
127
+ 'it; "keep" leaves a range that still accepts the new version alone and does not release ' +
128
+ "this package for it, refuses the run when the range does not, and raises the range's " +
129
+ 'lower bound when this package is released for changes of its own. A prerelease never ' +
130
+ "fails a range and never raises one. Per-package cascaded - the range is the package's own.",
131
+ type: 'string',
132
+ choices: ['rewrite', 'keep'],
133
+ },
122
134
  releaseTagPattern: {
123
135
  target: 'config',
124
136
  describe: "Tag naming the repository's own release, as opposed to the per-package tags " +
@@ -235,8 +247,11 @@ const versionCommand = registerCommand(app => {
235
247
  printPlan(plan);
236
248
  const errors = plan.filter(e => e.status === 'error');
237
249
  if (errors.length) {
238
- const message = `${errors.length} package(s) have uncommitted local changes ` +
239
- '(pass --ignore-dirty to exclude them instead of aborting)';
250
+ const dirty = errors.filter(e => e.reason?.startsWith(UNCOMMITTED)).length;
251
+ const message = dirty === errors.length
252
+ ? `${errors.length} package(s) have uncommitted local changes ` +
253
+ '(pass --ignore-dirty to exclude them instead of aborting)'
254
+ : `${errors.length} package(s) cannot be versioned - see the Reason column`;
240
255
  console.log(colors.red(message));
241
256
  const err = new Error(message);
242
257
  err.logged = true;
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.24.1';
1
+ export const version = '2.25.1';
@@ -159,6 +159,42 @@ export interface ManifestProvider {
159
159
  * by path.
160
160
  */
161
161
  updateDependencyVersions?(manifest: Manifest, bumped: ReadonlyMap<Package, string>): void;
162
+ /**
163
+ * Every reference this manifest makes to the in-repo package `dependency`, one per field naming
164
+ * it, each judged against `version` - whether the range written there accepts it, and the range
165
+ * with its lower bound raised to it.
166
+ *
167
+ * What `.rmanrc "version.dependentRanges": keep` asks instead of `updateDependencyVersions`: a
168
+ * hand-written range is left alone while it accepts the new version, refused when it does not,
169
+ * and has its floor raised when the package holding it is released for reasons of its own.
170
+ *
171
+ * Omit it and a package has no ranges to keep, so `keep` behaves like `rewrite` for it.
172
+ */
173
+ referencesTo?(manifest: Manifest, dependency: string, version: string): ManifestProvider.Reference[];
174
+ /**
175
+ * Writes `range` as this manifest's reference to `dependency` in `field` - one of the fields
176
+ * `referencesTo` reported. Mutates `manifest.raw`, like `updateDependencyVersions`.
177
+ */
178
+ setReference?(manifest: Manifest, field: string, dependency: string, range: string): void;
179
+ }
180
+ export declare namespace ManifestProvider {
181
+ /** One field's reference to an in-repo package - see `ManifestProvider.referencesTo`. */
182
+ interface Reference {
183
+ /** Where it is written - npm's `dependencies`, `peerDependencies`, ... */
184
+ field: string;
185
+ /** The range as written. */
186
+ range: string;
187
+ /** It follows the dependency by itself (npm's bare `"workspace:^"`) - always accepts, never
188
+ * rewritten, and keeps `rewrite`'s handling under `keep` too. */
189
+ dynamic?: boolean;
190
+ /** Whether the range accepts the version asked about. */
191
+ accepts: boolean;
192
+ /** The range with its lower bound raised to the version asked about, when that moves it.
193
+ * Unset when the floor is already there, or when `unraisable`. */
194
+ raised?: string;
195
+ /** The range's shape has no single lower bound this provider knows how to move. */
196
+ unraisable?: boolean;
197
+ }
162
198
  }
163
199
  /**
164
200
  * The registry, merged onto the `Manifest` interface so one name carries both the shape and the
@@ -218,6 +254,11 @@ export declare namespace Manifest {
218
254
  * reference each other by path has nothing here to go stale.
219
255
  */
220
256
  function updateDependencyVersions(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
257
+ /** `pkg`'s references to `dependency`, judged against `version` - see
258
+ * `ManifestProvider.referencesTo`. Empty where the provider has no opinion. */
259
+ function referencesTo(pkg: Package, dependency: string, version: string): ManifestProvider.Reference[];
260
+ /** Writes one of `pkg`'s references - see `ManifestProvider.setReference`. */
261
+ function setReference(pkg: Package, field: string, dependency: string, range: string): void;
221
262
  /**
222
263
  * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
223
264
  * `Plugin.stampVersion`.
@@ -88,6 +88,17 @@ export var Manifest;
88
88
  platformOf(pkg)?.manifestProvider.updateDependencyVersions?.(pkg.manifest, bumped);
89
89
  }
90
90
  Manifest.updateDependencyVersions = updateDependencyVersions;
91
+ /** `pkg`'s references to `dependency`, judged against `version` - see
92
+ * `ManifestProvider.referencesTo`. Empty where the provider has no opinion. */
93
+ function referencesTo(pkg, dependency, version) {
94
+ return platformOf(pkg)?.manifestProvider.referencesTo?.(pkg.manifest, dependency, version) ?? [];
95
+ }
96
+ Manifest.referencesTo = referencesTo;
97
+ /** Writes one of `pkg`'s references - see `ManifestProvider.setReference`. */
98
+ function setReference(pkg, field, dependency, range) {
99
+ platformOf(pkg)?.manifestProvider.setReference?.(pkg.manifest, field, dependency, range);
100
+ }
101
+ Manifest.setReference = setReference;
91
102
  /**
92
103
  * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
93
104
  * `Plugin.stampVersion`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.24.1",
4
+ "version": "2.25.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -142,6 +142,26 @@ export declare abstract class VersionPlanService {
142
142
  */
143
143
  protected declaredPreid(members: Package[], label: string): string | undefined;
144
144
  protected declaredCascade(members: Package[]): VersionPlanService.Cascade | undefined;
145
+ /**
146
+ * Whether `dependent` keeps its range on `dependency` (`.rmanrc "version.dependentRanges": keep`)
147
+ * and that range still accepts `version` - so the new version reaches it without a release of
148
+ * its own. `undefined` means "release it as `rewrite` would": the key says `rewrite`, the range
149
+ * no longer accepts the version (refused later, by `checkKeptRanges`), or there is no range to
150
+ * judge - a reference that follows the dependency by itself, or an edge `.rmanrc
151
+ * "dependencies"` declared with no range at all.
152
+ *
153
+ * A prerelease is accepted whatever the range says: a range written for stable releases is not
154
+ * meant to take a beta, and the dependent keeps resolving the stable one it was built against.
155
+ */
156
+ protected rangeVerdict(dependent: Package, dependency: Package, version: string): 'accepts' | undefined;
157
+ /**
158
+ * The plan's last word on kept ranges: every package that keeps its ranges is checked against
159
+ * the versions its in-repo dependencies are getting. A range that refuses one makes the package
160
+ * an `'error'` naming the field, the range and the version, so `version` writes nothing and the
161
+ * range can be widened by hand. One left out of the release because its range still accepts says
162
+ * so in its reason, and one being released notes a range whose floor cannot be raised.
163
+ */
164
+ protected checkKeptRanges(packages: Package[], entries: Map<string, VersionPlanService.Entry>): void;
145
165
  /**
146
166
  * The largest bump `commits` ask for, in `scheme`'s own names.
147
167
  *
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { RmanApplication } from '../core/application.js';
3
3
  import { assertOneScheme, semverScheme, VersionScheme } from '../core/classes/version-scheme.js';
4
+ import { Manifest } from '../core/interfaces/manifest.js';
4
5
  import { skipReasonFor, targetsOf } from '../core/interfaces/publish-target.js';
5
6
  import { dirtyReason, filesUnder, GitHelper } from '../utils/git.js';
6
7
  import { filterPackages } from '../utils/package-filter.js';
@@ -185,6 +186,7 @@ export class VersionPlanService {
185
186
  this.computeGroupPlan(key, members, changeByPackage, explicitVersion, preid, entries);
186
187
  }
187
188
  this.rippleCrossGroup(packages, entries, options.preid);
189
+ this.checkKeptRanges(packages, entries);
188
190
  const result = packages.map(pkg => entries.get(pkg.name));
189
191
  if (repository.monorepo) {
190
192
  result.push(this.buildRootEntry(repository, result, {
@@ -364,6 +366,86 @@ export class VersionPlanService {
364
366
  return widest;
365
367
  return undefined;
366
368
  }
369
+ /**
370
+ * Whether `dependent` keeps its range on `dependency` (`.rmanrc "version.dependentRanges": keep`)
371
+ * and that range still accepts `version` - so the new version reaches it without a release of
372
+ * its own. `undefined` means "release it as `rewrite` would": the key says `rewrite`, the range
373
+ * no longer accepts the version (refused later, by `checkKeptRanges`), or there is no range to
374
+ * judge - a reference that follows the dependency by itself, or an edge `.rmanrc
375
+ * "dependencies"` declared with no range at all.
376
+ *
377
+ * A prerelease is accepted whatever the range says: a range written for stable releases is not
378
+ * meant to take a beta, and the dependent keeps resolving the stable one it was built against.
379
+ */
380
+ /* **Why `keep` exists**, asked for by `panates/syncbridge`: nine extensions declare
381
+ * `"@syncbridge/common": ">=1.1.0 <2"` so that a release of `common` does not have to release
382
+ * all nine. `rewrite` turned the range back into `^1.2.0` and released every one of them anyway.
383
+ * That narrows the cascade npm's own table asks for, and the cost is stated rather than hidden: a
384
+ * dependent that bundles its dependency keeps shipping the older code until it is released
385
+ * itself. Choosing `keep` is the repository accepting that; it is not the default. */
386
+ rangeVerdict(dependent, dependency, version) {
387
+ if (dependent.config?.version?.dependentRanges !== 'keep')
388
+ return undefined;
389
+ const refs = Manifest.referencesTo(dependent, dependency.name, version).filter(r => !r.dynamic);
390
+ if (!refs.length)
391
+ return undefined;
392
+ if (dependency.versionScheme.isPrerelease(version))
393
+ return 'accepts';
394
+ return refs.every(r => r.accepts) ? 'accepts' : undefined;
395
+ }
396
+ /**
397
+ * The plan's last word on kept ranges: every package that keeps its ranges is checked against
398
+ * the versions its in-repo dependencies are getting. A range that refuses one makes the package
399
+ * an `'error'` naming the field, the range and the version, so `version` writes nothing and the
400
+ * range can be widened by hand. One left out of the release because its range still accepts says
401
+ * so in its reason, and one being released notes a range whose floor cannot be raised.
402
+ */
403
+ /* **Every kept package, released or not - a `publish.skip` one included.** A range that no longer
404
+ * accepts its sibling breaks more than a publish: npm workspaces link a sibling only while the
405
+ * range accepts its version, and otherwise install it from the registry, silently. */
406
+ checkKeptRanges(packages, entries) {
407
+ for (const pkg of packages) {
408
+ if (pkg.config?.version?.dependentRanges !== 'keep')
409
+ continue;
410
+ const entry = entries.get(pkg.name);
411
+ if (!entry)
412
+ continue;
413
+ const refused = [];
414
+ const kept = [];
415
+ const unraisable = [];
416
+ for (const dep of pkg.dependencies) {
417
+ const depEntry = entries.get(dep.name);
418
+ const version = depEntry?.status === 'bump' ? depEntry.to : dep.version;
419
+ const refs = Manifest.referencesTo(pkg, dep.name, version).filter(r => !r.dynamic);
420
+ if (!refs.length || dep.versionScheme.isPrerelease(version))
421
+ continue;
422
+ for (const ref of refs) {
423
+ if (!ref.accepts) {
424
+ /** Only a version this run gives: a range already refusing the version on disk is not
425
+ * this run's doing, and npm reports it at install time. */
426
+ if (depEntry?.status === 'bump') {
427
+ refused.push(`${ref.field} "${dep.name}": "${ref.range}" does not accept ${version}`);
428
+ }
429
+ }
430
+ else if (entry.status === 'bump' && ref.unraisable) {
431
+ unraisable.push(`${dep.name} "${ref.range}"`);
432
+ }
433
+ }
434
+ if (depEntry?.status === 'bump' && refs.every(r => r.accepts))
435
+ kept.push(`${dep.name} "${refs[0].range}"`);
436
+ }
437
+ if (refused.length) {
438
+ entry.status = 'error';
439
+ entry.reason = `range does not accept the new version - ${refused.join('; ')}`;
440
+ }
441
+ else if (entry.status === 'no-change' && kept.length) {
442
+ entry.reason = `range ${kept.join(', ')} still satisfied`;
443
+ }
444
+ else if (unraisable.length) {
445
+ entry.reason = `${entry.reason ? `${entry.reason}; ` : ''}lower bound not raised: ${unraisable.join(', ')}`;
446
+ }
447
+ }
448
+ }
367
449
  /**
368
450
  * The largest bump `commits` ask for, in `scheme`'s own names.
369
451
  *
@@ -478,7 +560,7 @@ export class VersionPlanService {
478
560
  for (const m of members) {
479
561
  if (bumping.has(m))
480
562
  continue;
481
- if (m.dependencies.includes(cur)) {
563
+ if (m.dependencies.includes(cur) && !this.rangeVerdict(m, cur, to)) {
482
564
  bumping.add(m);
483
565
  worklist.push(m);
484
566
  }
@@ -536,6 +618,10 @@ export class VersionPlanService {
536
618
  continue;
537
619
  if (!pkg.dependencies.includes(source.package))
538
620
  continue;
621
+ /** Its range still takes the new version, so nothing it publishes is out of date; one that
622
+ * does not is refused by `checkKeptRanges`, after every bump is known. */
623
+ if (this.rangeVerdict(pkg, source.package, source.to))
624
+ continue;
539
625
  /** The group always contains `pkg` itself, so there is always at least one version here -
540
626
  * the fallback is for the type, not for a case that can happen. */
541
627
  const groupCeiling = pkg.versionScheme.highestVersion(packages
@@ -104,6 +104,13 @@ export declare namespace VersionService {
104
104
  /** `topo` is refused by default here, which is the one way this differs from `RunService`'s: a
105
105
  * version hook has no package ordering to join. */
106
106
  function normalizeScriptValue(value: unknown, at: string): RunStepValue[];
107
+ /**
108
+ * Raises the lower bound of each of `pkg`'s ranges on an in-repo dependency to the version that
109
+ * dependency now has - `bumped`'s for one this run moves, its own otherwise. Upper bounds stay.
110
+ * What `.rmanrc "version.dependentRanges": keep` does for a package released for changes of its
111
+ * own, in place of `Manifest.updateDependencyVersions`.
112
+ */
113
+ function raiseKeptFloors(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
107
114
  /**
108
115
  * Keeps a package's Dockerfile `org.opencontainers.image.version` label in step with the version
109
116
  * just written, returning the absolute path when it actually changed (so the caller can fold it
@@ -98,7 +98,10 @@ export class VersionService extends Service {
98
98
  /** Which fields hold a sibling reference, and what a reference even looks like, is the
99
99
  * ecosystem's - npm's four fields and its `"workspace:"` protocol used to be spelled out
100
100
  * here. See `Plugin.updateDependencyVersions`. */
101
- Manifest.updateDependencyVersions(pkg, bumpedVersions);
101
+ if (pkg.config?.version?.dependentRanges === 'keep')
102
+ VersionService.raiseKeptFloors(pkg, bumpedVersions);
103
+ else
104
+ Manifest.updateDependencyVersions(pkg, bumpedVersions);
102
105
  await hook('exec');
103
106
  pkg.writeManifest();
104
107
  // Before the `after` hook, so a script reacting to the bump sees the whole new state.
@@ -308,6 +311,32 @@ export class VersionService extends Service {
308
311
  return RunService.normalizeScriptValue(value, at, { topo: false });
309
312
  }
310
313
  VersionService.normalizeScriptValue = normalizeScriptValue;
314
+ /**
315
+ * Raises the lower bound of each of `pkg`'s ranges on an in-repo dependency to the version that
316
+ * dependency now has - `bumped`'s for one this run moves, its own otherwise. Upper bounds stay.
317
+ * What `.rmanrc "version.dependentRanges": keep` does for a package released for changes of its
318
+ * own, in place of `Manifest.updateDependencyVersions`.
319
+ */
320
+ /* **Only when the package itself is released**, because that is the only time it can have
321
+ * started using something new: code relying on a newer dependency is a change in the dependent,
322
+ * and a change is what releases it. Left unreleased, the range it published is still true. The
323
+ * floor may end up higher than strictly needed - that errs toward what was built and tested, and
324
+ * the upper bound keeps every consumer installable.
325
+ *
326
+ * A prerelease never raises one: a stable range floored on a beta would no longer install for
327
+ * anyone on stable releases. */
328
+ function raiseKeptFloors(pkg, bumped) {
329
+ for (const dep of pkg.dependencies) {
330
+ const version = bumped.get(dep) ?? dep.version;
331
+ if (dep.versionScheme.isPrerelease(version))
332
+ continue;
333
+ for (const ref of Manifest.referencesTo(pkg, dep.name, version)) {
334
+ if (ref.raised && ref.accepts)
335
+ Manifest.setReference(pkg, ref.field, dep.name, ref.raised);
336
+ }
337
+ }
338
+ }
339
+ VersionService.raiseKeptFloors = raiseKeptFloors;
311
340
  /**
312
341
  * Keeps a package's Dockerfile `org.opencontainers.image.version` label in step with the version
313
342
  * just written, returning the absolute path when it actually changed (so the caller can fold it