rman 2.18.2 → 2.19.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.
- package/builtins/platforms/node/node-dependency-updater.d.ts +6 -0
- package/builtins/platforms/node/node-dependency-updater.js +29 -5
- package/builtins/platforms/node/services/publish.service.d.ts +20 -0
- package/builtins/platforms/node/services/publish.service.js +35 -1
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +2 -1
- package/builtins/publish-targets/npm/npm-publish-target.js +11 -1
- package/builtins/publish-targets/npm/npm-view.d.ts +1 -0
- package/builtins/publish-targets/npm/npm-view.js +4 -0
- package/cli.js +61 -1
- package/commands/deps.command.js +68 -31
- package/commands/publish.command.js +42 -4
- package/constants.js +1 -1
- package/core/classes/repository.d.ts +3 -0
- package/core/classes/repository.js +2 -0
- package/core/config/config-reader.d.ts +12 -0
- package/core/config/config-reader.js +26 -1
- package/core/interfaces/publish-target.d.ts +8 -0
- package/package.json +1 -1
- package/utils/key-prompt.d.ts +10 -0
- package/utils/key-prompt.js +35 -0
|
@@ -68,11 +68,17 @@ export declare class NodeDependencyUpdater implements DependencyUpdater {
|
|
|
68
68
|
protected resolve(decisions: readonly NodeDependencyUpdater.Decision[], ctx: DependencyUpdater.Context): void;
|
|
69
69
|
/** One broken rule, mended by one step down. Whether anything moved. */
|
|
70
70
|
protected settle(about: NodeDependencyUpdater.Decision, stating: NodeDependencyUpdater.Decision | undefined, range: string, rule: string): boolean;
|
|
71
|
+
/** `rule`, plus why `about` cannot meet it when the version that would is one its own settings
|
|
72
|
+
* leave out - `0.14.0: major - deps.target is "minor"`. */
|
|
73
|
+
protected explain(rule: string, about: NodeDependencyUpdater.Decision, range: string): string;
|
|
71
74
|
/** Moves `d` down to `index` (`-1` keeps what is declared), remembering why it left its newest. */
|
|
72
75
|
protected stepTo(d: NodeDependencyUpdater.Decision, index: number, rule: string): void;
|
|
73
76
|
/** The version `d` resolves to: the one it moves to, or else the newest release its declarations
|
|
74
77
|
* already allow, which is what an install picks. */
|
|
75
78
|
protected effective(d: NodeDependencyUpdater.Decision): string | undefined;
|
|
79
|
+
/** The newest release `d`'s declarations already allow - what an install picks if `d` does not
|
|
80
|
+
* move. */
|
|
81
|
+
protected resolvedDeclared(d: NodeDependencyUpdater.Decision): string | undefined;
|
|
76
82
|
/** What `d` declares in `field` once it has moved. */
|
|
77
83
|
protected rangeFor(d: NodeDependencyUpdater.Decision, field: string): string;
|
|
78
84
|
/** The in-repo packages `pkg` declares a dependency on. */
|
|
@@ -357,15 +357,36 @@ export class NodeDependencyUpdater {
|
|
|
357
357
|
return false;
|
|
358
358
|
if (about.index >= 0) {
|
|
359
359
|
const fits = about.candidates.findIndex((v, i) => i > about.index && semver.satisfies(v, range));
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
if (fits >= 0) {
|
|
361
|
+
this.stepTo(about, fits, rule);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
const declared = this.resolvedDeclared(about);
|
|
365
|
+
if (declared && semver.satisfies(declared, range, { includePrerelease: true })) {
|
|
366
|
+
this.stepTo(about, -1, rule);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
/* Nothing `about` may move to keeps the rule - so stepping it down gains nothing, and since a
|
|
370
|
+
* decision only ever steps down it would never come back up once the side stating the rule
|
|
371
|
+
* had given way. Measured on `abisena/syncbridge-iomt`: `@syncbridge/builtins@0.14.13` needs
|
|
372
|
+
* `@syncbridge/common ^0.14.0`, which `deps.target: minor` leaves out (a 0.x minor is a
|
|
373
|
+
* major), and `common`'s own `0.13.9` was dropped for it - then `builtins` stepped back too,
|
|
374
|
+
* and both read "not updated" with a reason pointing at a move that never happened. */
|
|
362
375
|
}
|
|
363
376
|
if (stating && stating.index >= 0) {
|
|
364
|
-
this.stepTo(stating, stating.index + 1 < stating.candidates.length ? stating.index + 1 : -1, rule);
|
|
377
|
+
this.stepTo(stating, stating.index + 1 < stating.candidates.length ? stating.index + 1 : -1, this.explain(rule, about, range));
|
|
365
378
|
return true;
|
|
366
379
|
}
|
|
367
380
|
return false;
|
|
368
381
|
}
|
|
382
|
+
/** `rule`, plus why `about` cannot meet it when the version that would is one its own settings
|
|
383
|
+
* leave out - `0.14.0: major - deps.target is "minor"`. */
|
|
384
|
+
explain(rule, about, range) {
|
|
385
|
+
const newest = about.available;
|
|
386
|
+
if (!newest || !about.skipReason || !semver.satisfies(newest, range, { includePrerelease: true }))
|
|
387
|
+
return rule;
|
|
388
|
+
return `${rule} (${about.name}@${newest}: ${about.skipReason})`;
|
|
389
|
+
}
|
|
369
390
|
/** Moves `d` down to `index` (`-1` keeps what is declared), remembering why it left its newest. */
|
|
370
391
|
stepTo(d, index, rule) {
|
|
371
392
|
if (d.index === 0 && !d.reason)
|
|
@@ -375,8 +396,11 @@ export class NodeDependencyUpdater {
|
|
|
375
396
|
/** The version `d` resolves to: the one it moves to, or else the newest release its declarations
|
|
376
397
|
* already allow, which is what an install picks. */
|
|
377
398
|
effective(d) {
|
|
378
|
-
|
|
379
|
-
|
|
399
|
+
return d.index >= 0 ? d.candidates[d.index] : this.resolvedDeclared(d);
|
|
400
|
+
}
|
|
401
|
+
/** The newest release `d`'s declarations already allow - what an install picks if `d` does not
|
|
402
|
+
* move. */
|
|
403
|
+
resolvedDeclared(d) {
|
|
380
404
|
if (!d.releases?.size)
|
|
381
405
|
return undefined;
|
|
382
406
|
const ranges = [...d.fields.values()].filter(r => semver.validRange(r));
|
|
@@ -13,6 +13,8 @@ export declare namespace PublishService {
|
|
|
13
13
|
/** A package with uncommitted local changes is excluded (status `'skip'`) instead of aborting
|
|
14
14
|
* the whole plan (status `'error'`) - same as `version`'s own option. Default false. */
|
|
15
15
|
ignoreDirty?: boolean;
|
|
16
|
+
/** Which packages are the npm target's (`shipsTo`) - every one the filters keep when omitted. */
|
|
17
|
+
shipsHere?: (pkg: Package) => boolean;
|
|
16
18
|
/** Registry to check against (and, in `applyPlan`, publish to) - `.npmrc`'s own configured
|
|
17
19
|
* registry is used when omitted. */
|
|
18
20
|
registry?: string;
|
|
@@ -138,6 +140,24 @@ export declare namespace PublishService {
|
|
|
138
140
|
* place of `npm`. */
|
|
139
141
|
function registryLabel(pkg: Package): string | undefined;
|
|
140
142
|
function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
|
|
143
|
+
interface WaitOptions {
|
|
144
|
+
/** As in `Options` - the registry each package is asked on is resolved the same way. */
|
|
145
|
+
registry?: string;
|
|
146
|
+
userconfig?: string;
|
|
147
|
+
/** How long to keep asking, in milliseconds. Default five minutes. */
|
|
148
|
+
timeoutMs?: number;
|
|
149
|
+
/** How long between rounds, in milliseconds. Default five seconds. */
|
|
150
|
+
intervalMs?: number;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Asks each package's registry, every `intervalMs`, whether it serves the version just published,
|
|
154
|
+
* until all of them do or `timeoutMs` passes. Returns the entries still missing - none when every
|
|
155
|
+
* version showed up.
|
|
156
|
+
*
|
|
157
|
+
* A staged entry is not waited for: it stays in npm's queue until a maintainer approves it, which
|
|
158
|
+
* no wait in a release job can outlast.
|
|
159
|
+
*/
|
|
160
|
+
function waitUntilAvailable(entries: Entry[], options?: WaitOptions, deps?: Deps): Promise<Entry[]>;
|
|
141
161
|
/**
|
|
142
162
|
* Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
|
|
143
163
|
* see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
|
|
@@ -195,7 +195,7 @@ export var PublishService;
|
|
|
195
195
|
PublishService.registryLabel = registryLabel;
|
|
196
196
|
async function getPlan(repository, options = {}, deps = {}) {
|
|
197
197
|
const git = new GitHelper({ cwd: repository.dirname });
|
|
198
|
-
const packages = filterPackages(repository.getPackages({ toposort: true }), options);
|
|
198
|
+
const packages = filterPackages(repository.getPackages({ toposort: true }), options).filter(pkg => options.shipsHere?.(pkg) ?? true);
|
|
199
199
|
const dirtyFiles = await git.listDirtyFiles({ absolute: true });
|
|
200
200
|
const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
201
201
|
/** Per package, not per run: `resolveRegistry` reads the package's own `publishConfig.registry`
|
|
@@ -290,6 +290,40 @@ export var PublishService;
|
|
|
290
290
|
return packages.map(pkg => entries.get(pkg.name));
|
|
291
291
|
}
|
|
292
292
|
PublishService.getPlan = getPlan;
|
|
293
|
+
/**
|
|
294
|
+
* Asks each package's registry, every `intervalMs`, whether it serves the version just published,
|
|
295
|
+
* until all of them do or `timeoutMs` passes. Returns the entries still missing - none when every
|
|
296
|
+
* version showed up.
|
|
297
|
+
*
|
|
298
|
+
* A staged entry is not waited for: it stays in npm's queue until a maintainer approves it, which
|
|
299
|
+
* no wait in a release job can outlast.
|
|
300
|
+
*/
|
|
301
|
+
/* `--prefer-online`, or npm answers from the copy of the package document it cached before the
|
|
302
|
+
* publish. One round asks about every pending package at once. */
|
|
303
|
+
async function waitUntilAvailable(entries, options = {}, deps = {}) {
|
|
304
|
+
const pending = new Set(entries.filter(e => e.status === 'publish' && !e.staged));
|
|
305
|
+
const deadline = Date.now() + (options.timeoutMs ?? 300_000);
|
|
306
|
+
const interval = options.intervalMs ?? 5_000;
|
|
307
|
+
const view = (pkg) => deps.npmViewPackage
|
|
308
|
+
? deps.npmViewPackage(pkg.name, pkg.dirname)
|
|
309
|
+
: npmViewPackage(pkg.name, pkg.dirname, {
|
|
310
|
+
userconfig: options.userconfig,
|
|
311
|
+
registry: resolveRegistry(pkg, options.registry),
|
|
312
|
+
preferOnline: true,
|
|
313
|
+
});
|
|
314
|
+
while (pending.size) {
|
|
315
|
+
await Promise.all([...pending].map(async (entry) => {
|
|
316
|
+
if ((await view(entry.package))?.versions.includes(entry.version))
|
|
317
|
+
pending.delete(entry);
|
|
318
|
+
}));
|
|
319
|
+
const left = deadline - Date.now();
|
|
320
|
+
if (!pending.size || left <= 0)
|
|
321
|
+
break;
|
|
322
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(interval, left)));
|
|
323
|
+
}
|
|
324
|
+
return [...pending];
|
|
325
|
+
}
|
|
326
|
+
PublishService.waitUntilAvailable = waitUntilAvailable;
|
|
293
327
|
/**
|
|
294
328
|
* Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
|
|
295
329
|
* see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Package } from '../../../core/classes/package.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type PublishTarget } from '../../../core/interfaces/publish-target.js';
|
|
3
3
|
import { PublishService } from '../../platforms/node/services/publish.service.js';
|
|
4
4
|
/** The name this target answers to in `publish.target` and `--target`. */
|
|
5
5
|
export declare const NPM_TARGET = "npm";
|
|
@@ -79,5 +79,6 @@ export declare class NpmPublishTarget implements PublishTarget {
|
|
|
79
79
|
applyPlan(ctx: PublishTarget.Context, plan: PublishTarget.Entry[]): Promise<PublishService.Entry[]>;
|
|
80
80
|
/** The shared filters plus the two flags both halves need - read here rather than in the
|
|
81
81
|
* service, which keeps taking a plain options object and stays callable without a CLI. */
|
|
82
|
+
waitUntilAvailable(ctx: PublishTarget.Context, published: PublishTarget.Entry[]): Promise<PublishService.Entry[]>;
|
|
82
83
|
protected planOptions(ctx: PublishTarget.Context): PublishService.Options;
|
|
83
84
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shipsTo } from '../../../core/interfaces/publish-target.js';
|
|
1
2
|
import { CiService } from '../../platforms/node/services/ci.service.js';
|
|
2
3
|
import { PublishService } from '../../platforms/node/services/publish.service.js';
|
|
3
4
|
/** The name this target answers to in `publish.target` and `--target`. */
|
|
@@ -31,7 +32,7 @@ export class NpmPublishTarget {
|
|
|
31
32
|
packageManager: {
|
|
32
33
|
target: 'cli',
|
|
33
34
|
cliName: 'package-manager',
|
|
34
|
-
describe: 'Package manager to publish with (default:
|
|
35
|
+
describe: 'Package manager to publish with (default: .rmanrc "packageManager.node", else npm)',
|
|
35
36
|
choices: CiService.PACKAGE_MANAGERS,
|
|
36
37
|
},
|
|
37
38
|
access: {
|
|
@@ -96,9 +97,18 @@ export class NpmPublishTarget {
|
|
|
96
97
|
}
|
|
97
98
|
/** The shared filters plus the two flags both halves need - read here rather than in the
|
|
98
99
|
* service, which keeps taking a plain options object and stays callable without a CLI. */
|
|
100
|
+
waitUntilAvailable(ctx, published) {
|
|
101
|
+
return PublishService.waitUntilAvailable(published, {
|
|
102
|
+
registry: ctx.args.registry,
|
|
103
|
+
userconfig: ctx.args.userconfig,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
99
106
|
planOptions(ctx) {
|
|
100
107
|
return {
|
|
101
108
|
...ctx.options,
|
|
109
|
+
/** `publish` drops the rest from the plan anyway; asked here too so a docker-only package
|
|
110
|
+
* costs no `npm view`. */
|
|
111
|
+
shipsHere: pkg => shipsTo(pkg, this),
|
|
102
112
|
registry: ctx.args.registry,
|
|
103
113
|
userconfig: ctx.args.userconfig,
|
|
104
114
|
/** Read at *plan* time too, not only where the publish command is built: the plan is what
|
|
@@ -31,6 +31,7 @@ export interface NpmPackageView {
|
|
|
31
31
|
export declare function npmViewPackage(name: string, cwd: string, options?: {
|
|
32
32
|
registry?: string;
|
|
33
33
|
userconfig?: string;
|
|
34
|
+
preferOnline?: boolean;
|
|
34
35
|
}): Promise<NpmPackageView | undefined>;
|
|
35
36
|
/**
|
|
36
37
|
* `npm view <name> version` - the **`latest` dist-tag's** version, or `undefined` for anything that
|
|
@@ -27,6 +27,10 @@ export async function npmViewPackage(name, cwd, options = {}) {
|
|
|
27
27
|
argv.push('--registry', options.registry);
|
|
28
28
|
if (options.userconfig)
|
|
29
29
|
argv.push('--userconfig', options.userconfig);
|
|
30
|
+
/** Past npm's local cache - a caller polling for a version it just published would otherwise be
|
|
31
|
+
* answered from the copy fetched before the publish. */
|
|
32
|
+
if (options.preferOnline)
|
|
33
|
+
argv.push('--prefer-online');
|
|
30
34
|
try {
|
|
31
35
|
const { stdout } = await execFileAsync('npm', argv, { cwd });
|
|
32
36
|
const text = stdout.trim();
|
package/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { assertNoBuiltinShadowing, defaultCommandGlobs, describeCommandSource, l
|
|
|
19
19
|
import { checkCustomCommand } from './core/plugin-loader.js';
|
|
20
20
|
import { commandRegistry } from './interfaces/rman-config.interface.js';
|
|
21
21
|
import { colorYaml } from './utils/color-yaml.js';
|
|
22
|
+
import { askEnterOrEsc } from './utils/key-prompt.js';
|
|
22
23
|
import { filterPackages, readFromRootOption, readPackageFilterOptions } from './utils/package-filter.js';
|
|
23
24
|
import { printableConfig, withoutContributions } from './utils/printable-config.js';
|
|
24
25
|
import { runBin } from './utils/run-bin.js';
|
|
@@ -41,7 +42,14 @@ export async function runCli(options) {
|
|
|
41
42
|
/** One application per run, made here so `--log-level` reaches its logger, and handed to
|
|
42
43
|
* `Repository.create` rather than found through a global. */
|
|
43
44
|
const app = options?.app ?? new RmanApplication();
|
|
44
|
-
const
|
|
45
|
+
const missingPresets = [];
|
|
46
|
+
const repository = await Repository.create(options?.cwd, {
|
|
47
|
+
app,
|
|
48
|
+
presets: options?.presets,
|
|
49
|
+
onMissingExtends: (target, from) => missingPresets.push({ target, from }),
|
|
50
|
+
});
|
|
51
|
+
if (missingPresets.length)
|
|
52
|
+
await confirmMissingPresets(missingPresets, _argv);
|
|
45
53
|
const program = yargs(_argv)
|
|
46
54
|
.scriptName('rman')
|
|
47
55
|
.version(version)
|
|
@@ -68,6 +76,14 @@ export async function runCli(options) {
|
|
|
68
76
|
describe: 'Write the log as JSON Lines - one event per line on stdout, and no panel, spinner or prose. ' +
|
|
69
77
|
'A command that prints a result (list, version, publish, ...) prints that result as JSON',
|
|
70
78
|
type: 'boolean',
|
|
79
|
+
})
|
|
80
|
+
/** **Global**, like `--json`: a command that asks for confirmation declares its own `--yes`
|
|
81
|
+
* with the same meaning, and this one also answers the question asked before any command
|
|
82
|
+
* runs - whether to go on without a preset that cannot be found. */
|
|
83
|
+
.option('yes', {
|
|
84
|
+
alias: 'y',
|
|
85
|
+
describe: 'Answer yes to every question - including going on without an "extends" preset that cannot be found',
|
|
86
|
+
type: 'boolean',
|
|
71
87
|
})
|
|
72
88
|
.option('log-file', {
|
|
73
89
|
describe: 'Also write the log to this file - JSON Lines under --json, plain text lines otherwise. ' +
|
|
@@ -639,6 +655,50 @@ function commandContext(repository, app, args) {
|
|
|
639
655
|
parallel: (tasks, options) => run.parallel(tasks, options ?? {}),
|
|
640
656
|
};
|
|
641
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Warns about the `extends` presets that could not be found - their settings are left out - and
|
|
660
|
+
* asks whether to go on: **Enter** continues, **Esc** cancels. `--yes` goes on without asking; with
|
|
661
|
+
* no terminal to ask on, it is an error that says to pass `--yes`.
|
|
662
|
+
*
|
|
663
|
+
* `--help` is never held up: it is what someone reaches for in exactly this situation.
|
|
664
|
+
*/
|
|
665
|
+
/* **It was an error outright**, and the case that made that wrong is a repository whose
|
|
666
|
+
* `node_modules` is not installed yet: its shared preset is nowhere to be found, so `rman ci` - the
|
|
667
|
+
* command that would install it - could not start. Without a terminal there is nobody to ask, and a
|
|
668
|
+
* release in CI quietly built without its preset is the worse outcome, so the error stays there
|
|
669
|
+
* unless `--yes` says otherwise. */
|
|
670
|
+
async function confirmMissingPresets(missing, argv) {
|
|
671
|
+
if (!argv.length || argv.some(arg => arg === '-h' || arg === '--help'))
|
|
672
|
+
return;
|
|
673
|
+
const byTarget = new Map();
|
|
674
|
+
for (const { target, from } of missing) {
|
|
675
|
+
const where = path.relative(process.cwd(), from) || from;
|
|
676
|
+
byTarget.set(target, [...new Set([...(byTarget.get(target) ?? []), where])]);
|
|
677
|
+
}
|
|
678
|
+
const width = Math.max(...[...byTarget.keys()].map(t => t.length));
|
|
679
|
+
console.error(colors.yellow('These presets could not be found, so their settings are left out:'));
|
|
680
|
+
for (const [target, files] of byTarget) {
|
|
681
|
+
console.error(` ${colors.cyan(target.padEnd(width))} ${colors.gray(`extends in ${files.join(', ')}`)}`);
|
|
682
|
+
}
|
|
683
|
+
console.error(colors.gray('Are they installed? "rman ci" installs the repository\'s dependencies.'));
|
|
684
|
+
if (argv.some(arg => arg === '--yes' || arg === '-y')) {
|
|
685
|
+
console.error(colors.gray('Continuing without them (--yes).'));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (!process.stdin.isTTY) {
|
|
689
|
+
throw logged('No terminal to ask on - pass --yes to continue without them.');
|
|
690
|
+
}
|
|
691
|
+
if (!(await askEnterOrEsc(colors.yellow('Press Enter to continue without them, Esc to cancel. ')))) {
|
|
692
|
+
throw logged('Cancelled.');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** The `logged` convention: printed here, so the caller's catch does not print it a second time. */
|
|
696
|
+
function logged(message) {
|
|
697
|
+
console.error(colors.red(message));
|
|
698
|
+
const err = new Error(message);
|
|
699
|
+
err.logged = true;
|
|
700
|
+
return err;
|
|
701
|
+
}
|
|
642
702
|
/**
|
|
643
703
|
* `--log-level` read straight off argv, for a message printed **before** yargs parses anything.
|
|
644
704
|
*
|
package/commands/deps.command.js
CHANGED
|
@@ -190,52 +190,89 @@ function jsonPlan(entries) {
|
|
|
190
190
|
}));
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* One
|
|
194
|
-
*
|
|
193
|
+
* One table per package with something to say, under a header: what each dependency would move to
|
|
194
|
+
* and how far, and for one that stays, the newest release and why it is not taken.
|
|
195
195
|
*/
|
|
196
|
-
/* **
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
196
|
+
/* **One row per dependency, the change a column.** It was a list per size (`patch`, `minor`) and a
|
|
197
|
+
* second list headed "not updated" - wrong for a command that, without `-u`, updates nothing, and a
|
|
198
|
+
* dependency that moved part of the way appeared in both. The `Change` column says what a row is:
|
|
199
|
+
* the size of the move, or `held` (another dependency's rule refuses it), `skipped` (the package's
|
|
200
|
+
* own settings leave the newer version out) or `error`. Moves first, smallest first, then the rest. */
|
|
200
201
|
function printPlan(entries, root) {
|
|
201
202
|
const updated = entries.filter(e => e.status === 'update');
|
|
202
|
-
const
|
|
203
|
-
/** Said first when nothing moves, so a run whose only
|
|
203
|
+
const shown = entries.filter(e => e.status !== 'up-to-date');
|
|
204
|
+
/** Said first when nothing moves, so a run whose only rows are majors left behind does not read
|
|
204
205
|
* as a list of things to do. */
|
|
205
206
|
if (!updated.length) {
|
|
206
207
|
console.log(colors.green('All dependencies are up to date') +
|
|
207
|
-
(
|
|
208
|
-
if (!
|
|
208
|
+
(shown.length ? colors.gray(' - newer versions their settings leave out, or another rule holds back:') : '.'));
|
|
209
|
+
if (!shown.length)
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
const header = ['Dependency', 'Current', 'Upgrade', 'Latest', 'Change'];
|
|
213
|
+
const cells = (e) => [
|
|
214
|
+
e.name,
|
|
215
|
+
e.current,
|
|
216
|
+
e.status === 'update' ? (e.target ?? '') : '-',
|
|
217
|
+
e.status === 'error' ? '-' : (e.available ?? e.latest ?? ''),
|
|
218
|
+
changeOf(e),
|
|
219
|
+
];
|
|
220
|
+
const widths = header.map((h, i) => Math.max(h.length, ...shown.map(e => cells(e)[i].length)));
|
|
221
|
+
/** Padded to the column, except a last value nothing follows - trailing spaces inside a colour
|
|
222
|
+
* code are past the reach of `trimEnd`. */
|
|
223
|
+
const line = (values, paint = text => text) => ' ' + values.map((v, i) => paint(i === values.length - 1 ? v : v.padEnd(widths[i]), i)).join(' ');
|
|
215
224
|
for (const pkg of [...new Set(shown.map(e => e.package))]) {
|
|
225
|
+
const sizes = pkg.versionScheme.bumpNames;
|
|
226
|
+
const rank = (e) => e.status === 'update' ? sizes.indexOf(e.bump) : sizes.length + ['held', 'skipped', 'error'].indexOf(e.status);
|
|
227
|
+
const rows = shown.filter(e => e.package === pkg).sort((x, y) => rank(x) - rank(y));
|
|
216
228
|
const where = path.relative(root, pkg.dirname);
|
|
217
229
|
console.log(colors.cyan(pkg.name) + (where ? colors.gray(` (${where})`) : ''));
|
|
218
|
-
|
|
219
|
-
for (const
|
|
220
|
-
const
|
|
221
|
-
|
|
230
|
+
console.log(colors.gray(line([...header, 'Note']).trimEnd()));
|
|
231
|
+
for (const e of rows) {
|
|
232
|
+
const paint = e.status === 'update' ? sizeColor(e.bump, sizes) : changeColor(e.status);
|
|
233
|
+
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) {
|
|
238
|
+
console.log(text);
|
|
222
239
|
continue;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
}
|
|
241
|
+
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))}`);
|
|
227
244
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A note cut into lines of at most `width` characters, at spaces, so a long one continues under its
|
|
249
|
+
* own column instead of wrapping to the terminal's left edge. One line when `width` is too narrow to
|
|
250
|
+
* be worth it, or 0 - output that is not a terminal has no edge to wrap at.
|
|
251
|
+
*/
|
|
252
|
+
function wrapNote(note, width) {
|
|
253
|
+
if (width < 20 || note.length <= width)
|
|
254
|
+
return [note];
|
|
255
|
+
const lines = [];
|
|
256
|
+
let current = '';
|
|
257
|
+
for (const word of note.split(' ')) {
|
|
258
|
+
if (current && current.length + 1 + word.length > width) {
|
|
259
|
+
lines.push(current);
|
|
260
|
+
current = word;
|
|
237
261
|
}
|
|
262
|
+
else
|
|
263
|
+
current = current ? `${current} ${word}` : word;
|
|
238
264
|
}
|
|
265
|
+
if (current)
|
|
266
|
+
lines.push(current);
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
/** What the `Change` column says: the size of the move, or why there is none. */
|
|
270
|
+
function changeOf(e) {
|
|
271
|
+
return e.status === 'update' ? (e.bump ?? 'other') : e.status;
|
|
272
|
+
}
|
|
273
|
+
/** `held` and `skipped` in yellow, an error in red - a row with no move to colour by size. */
|
|
274
|
+
function changeColor(status) {
|
|
275
|
+
return status === 'error' ? colors.red : colors.yellow;
|
|
239
276
|
}
|
|
240
277
|
/** The colours `npm-check-updates` uses - the largest size red, the next cyan, the rest green - read
|
|
241
278
|
* off the scheme's own order so a scheme with other names still gets them. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import readline from 'node:readline/promises';
|
|
3
3
|
import colors from 'ansi-colors';
|
|
4
|
-
import { unknownTargets } from '../core/interfaces/publish-target.js';
|
|
4
|
+
import { shipsTo, unknownTargets } from '../core/interfaces/publish-target.js';
|
|
5
5
|
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
6
6
|
import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
7
7
|
import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
|
|
@@ -121,8 +121,14 @@ const publishCommand = registerCommand(app => {
|
|
|
121
121
|
};
|
|
122
122
|
assertDeclaredTargetsExist(app, repository.getPackages());
|
|
123
123
|
const plans = new Map();
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
/** A target's plan keeps only the packages that ship to it, whatever the target itself
|
|
125
|
+
* filtered - see `shipsTo`. Asked here, once for every target, because leaving it to each
|
|
126
|
+
* one let them disagree: the npm target never asked, so a package declaring
|
|
127
|
+
* `publish.target: ['docker']` was planned for npm too while `rman list` and the docker
|
|
128
|
+
* target called it docker-only (panates/rman#40). */
|
|
129
|
+
for (const target of selected) {
|
|
130
|
+
plans.set(target, (await target.getPlan(ctx)).filter(entry => shipsTo(entry.package, target)));
|
|
131
|
+
}
|
|
126
132
|
if (args.json) {
|
|
127
133
|
console.log(JSON.stringify(jsonPlan(plans), undefined, 2));
|
|
128
134
|
}
|
|
@@ -165,9 +171,19 @@ const publishCommand = registerCommand(app => {
|
|
|
165
171
|
if (!proceed)
|
|
166
172
|
return;
|
|
167
173
|
const outcomes = [];
|
|
174
|
+
const publishedBy = new Map();
|
|
175
|
+
let waited = false;
|
|
168
176
|
for (const [target, plan] of plans) {
|
|
177
|
+
/** Once, before the first target built from what the others published - see
|
|
178
|
+
* `PublishTarget.waitUntilAvailable`. */
|
|
179
|
+
if (target.publishesLast && !waited && plan.some(e => e.status === 'publish')) {
|
|
180
|
+
waited = true;
|
|
181
|
+
await waitForPublished(ctx, publishedBy);
|
|
182
|
+
}
|
|
169
183
|
const applied = await target.applyPlan(ctx, plan);
|
|
170
|
-
|
|
184
|
+
const done = printApplied(applied, plan, target.name);
|
|
185
|
+
outcomes.push(...done);
|
|
186
|
+
publishedBy.set(target, done.filter(o => o.entry.status === 'publish').map(o => o.entry));
|
|
171
187
|
}
|
|
172
188
|
if (!args.json)
|
|
173
189
|
printRecap(outcomes);
|
|
@@ -274,6 +290,28 @@ function printPlan(entries, label) {
|
|
|
274
290
|
}
|
|
275
291
|
}
|
|
276
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Waits for every target that can say so to serve what it just published, so a target building from
|
|
295
|
+
* those packages finds them. On stderr, as progress rather than part of the answer; a version still
|
|
296
|
+
* missing at the end is reported and the build goes ahead - it may have arrived since, and if not,
|
|
297
|
+
* the build's own error now says which version it could not find.
|
|
298
|
+
*/
|
|
299
|
+
async function waitForPublished(ctx, publishedBy) {
|
|
300
|
+
for (const [target, published] of publishedBy) {
|
|
301
|
+
if (!target.waitUntilAvailable || !published.length)
|
|
302
|
+
continue;
|
|
303
|
+
const started = Date.now();
|
|
304
|
+
console.error(colors.gray(`waiting for ${published.length} package(s) to be served by [${target.name}]...`));
|
|
305
|
+
const missing = await target.waitUntilAvailable(ctx, published);
|
|
306
|
+
const took = ((Date.now() - started) / 1000).toFixed(1);
|
|
307
|
+
if (!missing.length) {
|
|
308
|
+
console.error(colors.gray(`[${target.name}] serves all ${published.length} (${took}s)`));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
console.error(colors.yellow(`[${target.name}] still does not serve ${missing.map(e => `${e.package.name}@${e.version}`).join(', ')} ` +
|
|
312
|
+
`after ${took}s - building anyway`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
277
315
|
/**
|
|
278
316
|
* What one target actually did, printed as it is known. Returns the outcome of every package the
|
|
279
317
|
* *plan* said to publish.
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.19.1';
|
|
@@ -236,6 +236,9 @@ export declare class Repository extends Package {
|
|
|
236
236
|
deep?: number;
|
|
237
237
|
app?: RmanApplication;
|
|
238
238
|
presets?: readonly string[];
|
|
239
|
+
/** See `ConfigReader`'s constructor: an `extends` package that cannot be found is reported
|
|
240
|
+
* here and left out, rather than failing the read. */
|
|
241
|
+
onMissingExtends?: (target: string, from: string) => void;
|
|
239
242
|
}): Promise<Repository>;
|
|
240
243
|
/**
|
|
241
244
|
* Takes the workspace's packages as this repository's own - the back-references a `Package` needs
|
|
@@ -7,6 +7,7 @@ import { RmanApplication } from '../application.js';
|
|
|
7
7
|
import { ConfigFileScope } from '../config/config-file-scope.js';
|
|
8
8
|
import { ConfigInterpolator } from '../config/config-interpolator.js';
|
|
9
9
|
import { DEFERRED_PATHS } from '../config/config-paths.js';
|
|
10
|
+
import { ConfigReader } from '../config/config-reader.js';
|
|
10
11
|
import { Manifest } from '../interfaces/manifest.js';
|
|
11
12
|
import { Package } from './package.js';
|
|
12
13
|
import { Workspace } from './workspace.js';
|
|
@@ -596,6 +597,7 @@ export class Repository extends Package {
|
|
|
596
597
|
deep: options?.deep,
|
|
597
598
|
platforms: [...app.platforms],
|
|
598
599
|
presets: options?.presets,
|
|
600
|
+
reader: options?.onMissingExtends ? new ConfigReader({ onMissingExtends: options.onMissingExtends }) : undefined,
|
|
599
601
|
});
|
|
600
602
|
/** What the configs named, onto the application the commands read from. */
|
|
601
603
|
for (const platform of workspace.platforms)
|
|
@@ -8,6 +8,18 @@ export declare class ConfigReader {
|
|
|
8
8
|
* @protected
|
|
9
9
|
*/
|
|
10
10
|
protected readonly configFiles: readonly string[];
|
|
11
|
+
/** Told about an `extends` package that cannot be found, which is then left out - see the
|
|
12
|
+
* constructor. */
|
|
13
|
+
protected readonly onMissingExtends?: (target: string, from: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* `onMissingExtends`, when given, turns an `extends` naming a **package** that cannot be found -
|
|
16
|
+
* `@scope/preset` or `rman:<name>` - into a call to it, and the config is read without that base.
|
|
17
|
+
* Without it such a target is an error, as it always was. A relative path that does not exist is
|
|
18
|
+
* an error either way: it is the repository's own file, and missing it is a typo.
|
|
19
|
+
*/
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
onMissingExtends?: (target: string, from: string) => void;
|
|
22
|
+
});
|
|
11
23
|
/**
|
|
12
24
|
* Whether a config key names **packages** rather than a setting: `"[*]"`, `"[/]"`, `"[pkg-a]"`.
|
|
13
25
|
*/
|
|
@@ -39,6 +39,21 @@ export class ConfigReader {
|
|
|
39
39
|
'.rmanrc.mjs',
|
|
40
40
|
'.rmanrc.js',
|
|
41
41
|
];
|
|
42
|
+
/** Told about an `extends` package that cannot be found, which is then left out - see the
|
|
43
|
+
* constructor. */
|
|
44
|
+
onMissingExtends;
|
|
45
|
+
/**
|
|
46
|
+
* `onMissingExtends`, when given, turns an `extends` naming a **package** that cannot be found -
|
|
47
|
+
* `@scope/preset` or `rman:<name>` - into a call to it, and the config is read without that base.
|
|
48
|
+
* Without it such a target is an error, as it always was. A relative path that does not exist is
|
|
49
|
+
* an error either way: it is the repository's own file, and missing it is a typo.
|
|
50
|
+
*/
|
|
51
|
+
/* For the CLI, which asks before going on: a repository whose `node_modules` is not installed yet
|
|
52
|
+
* has its shared preset nowhere to be found, and refusing to start left `rman ci` - the command
|
|
53
|
+
* that installs it - unable to run. A library caller keeps the error unless it asks otherwise. */
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.onMissingExtends = options?.onMissingExtends;
|
|
56
|
+
}
|
|
42
57
|
/**
|
|
43
58
|
* Whether a config key names **packages** rather than a setting: `"[*]"`, `"[/]"`, `"[pkg-a]"`.
|
|
44
59
|
*/
|
|
@@ -460,7 +475,17 @@ export class ConfigReader {
|
|
|
460
475
|
}
|
|
461
476
|
const base = {};
|
|
462
477
|
for (const target of targets) {
|
|
463
|
-
|
|
478
|
+
let file;
|
|
479
|
+
try {
|
|
480
|
+
file = this._resolveExtendTarget(target, from, EXTENDS_KEY);
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
const isPath = target.startsWith('.') || path.isAbsolute(target);
|
|
484
|
+
if (!this.onMissingExtends || isPath)
|
|
485
|
+
throw e;
|
|
486
|
+
this.onMissingExtends(target, from);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
464
489
|
if (seen.includes(file)) {
|
|
465
490
|
throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
|
|
466
491
|
}
|
|
@@ -79,6 +79,14 @@ export interface PublishTarget {
|
|
|
79
79
|
* registry's host, `npm.pkg.github.com` for GitHub Packages. `undefined` shows `name`.
|
|
80
80
|
*/
|
|
81
81
|
labelFor?(pkg: Package): string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Waits until what `applyPlan` just published is served by this target's registry, and returns
|
|
84
|
+
* the entries that still are not when it gives up - none when everything showed up.
|
|
85
|
+
*
|
|
86
|
+
* Asked by `publish` before a `publishesLast` target runs, so an image built from these packages
|
|
87
|
+
* finds them. Absent, nothing waits for this target.
|
|
88
|
+
*/
|
|
89
|
+
waitUntilAvailable?(ctx: PublishTarget.Context, published: PublishTarget.Entry[]): Promise<PublishTarget.Entry[]>;
|
|
82
90
|
/** What this target *would* do - never publishes. Called even under `--dry-run`, which is the
|
|
83
91
|
* whole point of the split. */
|
|
84
92
|
getPlan(ctx: PublishTarget.Context): Promise<PublishTarget.Entry[]>;
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `question` to stderr and waits for one key: **Enter** answers `true`, **Esc** or
|
|
3
|
+
* **Ctrl+C** answers `false`. Any other key is ignored.
|
|
4
|
+
*
|
|
5
|
+
* Only for a terminal - the caller checks `process.stdin.isTTY` first, since there is no key to
|
|
6
|
+
* wait for in a pipe.
|
|
7
|
+
*/
|
|
8
|
+
export declare function askEnterOrEsc(question: string,
|
|
9
|
+
/** Where the key comes from - the terminal, unless a caller (a spec) brings its own. */
|
|
10
|
+
stdin?: Pick<NodeJS.ReadStream, 'isRaw' | 'setRawMode' | 'resume' | 'pause' | 'on' | 'off'>): Promise<boolean>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `question` to stderr and waits for one key: **Enter** answers `true`, **Esc** or
|
|
3
|
+
* **Ctrl+C** answers `false`. Any other key is ignored.
|
|
4
|
+
*
|
|
5
|
+
* Only for a terminal - the caller checks `process.stdin.isTTY` first, since there is no key to
|
|
6
|
+
* wait for in a pipe.
|
|
7
|
+
*/
|
|
8
|
+
/* One key rather than a typed answer, because the question is a gate in front of the whole run and
|
|
9
|
+
* the two answers are "go on" and "stop". An escape sequence (an arrow key arrives as `\x1b[A`) is
|
|
10
|
+
* not Esc: only a lone `\x1b` cancels. */
|
|
11
|
+
export function askEnterOrEsc(question,
|
|
12
|
+
/** Where the key comes from - the terminal, unless a caller (a spec) brings its own. */
|
|
13
|
+
stdin = process.stdin) {
|
|
14
|
+
process.stderr.write(question);
|
|
15
|
+
return new Promise(resolve => {
|
|
16
|
+
const wasRaw = stdin.isRaw;
|
|
17
|
+
stdin.setRawMode(true);
|
|
18
|
+
stdin.resume();
|
|
19
|
+
const finish = (answer) => {
|
|
20
|
+
stdin.off('data', onKey);
|
|
21
|
+
stdin.setRawMode(wasRaw);
|
|
22
|
+
stdin.pause();
|
|
23
|
+
process.stderr.write('\n');
|
|
24
|
+
resolve(answer);
|
|
25
|
+
};
|
|
26
|
+
const onKey = (data) => {
|
|
27
|
+
const key = data.toString();
|
|
28
|
+
if (key === '\r' || key === '\n')
|
|
29
|
+
finish(true);
|
|
30
|
+
else if (key === '\x1b' || key === '\x03')
|
|
31
|
+
finish(false);
|
|
32
|
+
};
|
|
33
|
+
stdin.on('data', onKey);
|
|
34
|
+
});
|
|
35
|
+
}
|