rman 2.8.0 → 2.10.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.
- package/builtins/platforms/node/node-manifest.provider.d.ts +10 -0
- package/builtins/platforms/node/node-manifest.provider.js +32 -5
- package/builtins/platforms/node/services/clean.service.js +104 -23
- package/cli.js +31 -13
- package/commands/changelog.command.d.ts +15 -0
- package/constants.js +1 -1
- package/core/classes/repository.d.ts +48 -0
- package/core/classes/repository.js +139 -8
- package/core/command-builder.d.ts +9 -1
- package/core/command-builder.js +24 -11
- package/core/config/config-paths.js +24 -0
- package/core/interfaces/custom-command.d.ts +39 -0
- package/core/interfaces/run-step.d.ts +50 -3
- package/index.d.ts +7 -1
- package/index.js +11 -0
- package/interfaces/rman-config.interface.d.ts +21 -3
- package/package.json +1 -1
- package/services/changelog.service.js +120 -14
- package/services/run.service.d.ts +125 -1
- package/services/run.service.js +388 -120
- package/services/version.service.d.ts +2 -0
- package/services/version.service.js +7 -2
- package/utils/run-options.d.ts +44 -0
- package/utils/run-options.js +55 -0
package/core/command-builder.js
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
* switches - arrives as data now, so a typo in any of it is a compile error at the command rather
|
|
8
8
|
* than a flag that silently never existed.
|
|
9
9
|
*/
|
|
10
|
-
export function toYargsCommand(meta
|
|
10
|
+
export function toYargsCommand(meta,
|
|
11
|
+
/**
|
|
12
|
+
* Builds the `CommandContext` the handler gets as its **second** parameter, per invocation.
|
|
13
|
+
*
|
|
14
|
+
* Optional because a caller with no repository in hand (a spec listing the registrations) has
|
|
15
|
+
* nothing to build one from, and a handler that never reads it cannot tell.
|
|
16
|
+
*/
|
|
17
|
+
contextFor) {
|
|
11
18
|
return {
|
|
12
19
|
command: meta.command,
|
|
13
20
|
aliases: meta.aliases,
|
|
@@ -51,23 +58,29 @@ export function toYargsCommand(meta) {
|
|
|
51
58
|
* narrower one is the useful one, and this is the single place the widening is admitted -
|
|
52
59
|
* previously it was an `as` per option read, in every handler.
|
|
53
60
|
*/
|
|
54
|
-
handler:
|
|
61
|
+
handler: wrapHandler(meta, contextFor),
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
|
-
*
|
|
65
|
+
* The handler yargs is given: carries `CommandMetadata.platforms` into argv, and hands the command
|
|
66
|
+
* its `CommandContext` as a second parameter.
|
|
67
|
+
*
|
|
68
|
+
* **`(args, context)`, not `(context, args)`** - the opposite order to `CustomCommand.handler`, and
|
|
69
|
+
* decided by compatibility rather than taste: every declared command in existence is `handler(args)`,
|
|
70
|
+
* so appending a parameter breaks none of them while prepending one breaks all of them. The two
|
|
71
|
+
* authoring forms therefore disagree about the order and agree about everything in the object,
|
|
72
|
+
* which is the cheaper of the two inconsistencies.
|
|
59
73
|
*
|
|
60
|
-
*
|
|
61
|
-
* of which knows which command is running - `options` is all they see. One wrap
|
|
62
|
-
* one of them and changes none.
|
|
74
|
+
* `platforms` goes through argv rather than a parameter because `filterPackages` is called by ten
|
|
75
|
+
* *services*, none of which knows which command is running - `options` is all they see. One wrap
|
|
76
|
+
* here reaches every one of them and changes none.
|
|
63
77
|
*/
|
|
64
|
-
function
|
|
78
|
+
function wrapHandler(meta, contextFor) {
|
|
65
79
|
const handler = meta.handler;
|
|
66
|
-
if (!meta.platforms?.length)
|
|
67
|
-
return handler;
|
|
68
80
|
return (args) => {
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
if (meta.platforms?.length)
|
|
82
|
+
args.commandPlatforms = meta.platforms;
|
|
83
|
+
return handler(args, contextFor?.(args));
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
86
|
/** A command's own name - the first word of its `command` string, before any positional. */
|
|
@@ -65,6 +65,30 @@ export const STEP_PATHS = [
|
|
|
65
65
|
'version.before',
|
|
66
66
|
'version.exec',
|
|
67
67
|
'version.after',
|
|
68
|
+
/**
|
|
69
|
+
* The step inside a step's **object** form (`{ topo: true, command: buildWithTsc() }`).
|
|
70
|
+
*
|
|
71
|
+
* Array indices are dropped before matching but object keys are not, so `run.build.after[0].command`
|
|
72
|
+
* arrives as `run.build.after.command` - one segment longer than `run.*.after`, which matches by
|
|
73
|
+
* length. Without these the function would be taken for a *value* function and called while the
|
|
74
|
+
* repository loads, which is the exact failure the step/value split exists to prevent.
|
|
75
|
+
*
|
|
76
|
+
* **Measured on `@panates/rman-preset`**, whose build hook is a function under `command`: without
|
|
77
|
+
* these entries every command in a repository extending it died with
|
|
78
|
+
* `Config function in "run.build.after.command" (node/config.js) failed: Cannot read properties
|
|
79
|
+
* of undefined (reading 'vars')` - the hook called with the config scope instead of a step
|
|
80
|
+
* context, while merely resolving the config.
|
|
81
|
+
*
|
|
82
|
+
* A string at these paths is interpolated either way, which is what keeps
|
|
83
|
+
* `command: 'tsc -b ${{ file.resolve(...) }}'` working.
|
|
84
|
+
*/
|
|
85
|
+
'run.*.command',
|
|
86
|
+
'run.*.before.command',
|
|
87
|
+
'run.*.exec.command',
|
|
88
|
+
'run.*.after.command',
|
|
89
|
+
'version.before.command',
|
|
90
|
+
'version.exec.command',
|
|
91
|
+
'version.after.command',
|
|
68
92
|
];
|
|
69
93
|
/**
|
|
70
94
|
* Config paths left untouched when a repository's config is first resolved, and evaluated only by
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ArgumentsCamelCase, Argv } from 'yargs';
|
|
2
2
|
import type { RmanConfig as CommandDeclaration } from '../../interfaces/rman-config.interface.js';
|
|
3
|
+
import type { RunService } from '../../services/run.service.js';
|
|
3
4
|
import type { Logger } from '../../utils/logger.js';
|
|
4
5
|
import type { RunBinOptions, RunBinResult } from '../../utils/run-bin.js';
|
|
5
6
|
import type { Package } from '../classes/package.js';
|
|
6
7
|
import type { Repository } from '../classes/repository.js';
|
|
8
|
+
import type { RunStepFn } from './run-step.js';
|
|
7
9
|
/** Where a repository keeps its own commands - one module per command, named after it. */
|
|
8
10
|
export declare const CUSTOM_COMMAND_DIR = ".rman";
|
|
9
11
|
/**
|
|
@@ -34,6 +36,43 @@ export interface CommandContext {
|
|
|
34
36
|
runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
|
|
35
37
|
/** Logger at this run's resolved level, for a command's own narration. */
|
|
36
38
|
logger: Logger;
|
|
39
|
+
/**
|
|
40
|
+
* Runs `fn` once per package under the scheduler `run` uses - `--parallel`, `--bail`, dependency
|
|
41
|
+
* order on request, the progress panel, and `console` routed to the right row. Throws when any
|
|
42
|
+
* package failed, with the recap already printed.
|
|
43
|
+
*
|
|
44
|
+
* **A `for` loop is what this replaces, and the loop is worse in more ways than it looks.** It
|
|
45
|
+
* ignores `--parallel` outright, draws no panel, and leaves the command to re-implement `--bail`
|
|
46
|
+
* and a summary line. `rman check` was that loop: measured on a nineteen-package repository at
|
|
47
|
+
* 0.4-0.9s per package, eleven seconds of wall clock for work that is entirely independent.
|
|
48
|
+
*
|
|
49
|
+
* `fn` is handed exactly what a **function step** gets - `pkg`, `cwd`, a `runBin` already bound to
|
|
50
|
+
* that package's directory and this run's log level, and a `logger`. An imported `runBin` knows
|
|
51
|
+
* none of that, which is why a command using one had to thread `cwd`, `app` and `logLevel`
|
|
52
|
+
* through by hand at every call.
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* await context.forEachPackage(checkable, async ({ pkg, runBin }) => {
|
|
56
|
+
* await runBin('dpdm', [...FLAGS, entry]);
|
|
57
|
+
* }, { label: 'check', parallel: args.parallel, bail: args.bail });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
forEachPackage: (packages: readonly Package[], fn: RunStepFn, options?: RunService.ForEachOptions) => Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Runs `tasks` with at most `parallel` of them in flight - `Promise.all` under the repository's
|
|
63
|
+
* own concurrency rule.
|
|
64
|
+
*
|
|
65
|
+
* **The lower-level half, and the second thing to reach for.** It is the answer for work that is
|
|
66
|
+
* not per package (sharding a file list), and it buys nothing else: a bare list of functions
|
|
67
|
+
* carries no name, so there is no panel row to label, no package to name in a failure, and no
|
|
68
|
+
* dependency to order by. Anything per package wants `forEachPackage`.
|
|
69
|
+
*
|
|
70
|
+
* Rejects with the first failure once the tasks already started have settled, so nothing is left
|
|
71
|
+
* running behind the throw.
|
|
72
|
+
*/
|
|
73
|
+
parallel: <T>(tasks: readonly (() => Promise<T>)[], options?: {
|
|
74
|
+
parallel?: boolean | number;
|
|
75
|
+
}) => Promise<T[]>;
|
|
37
76
|
}
|
|
38
77
|
/**
|
|
39
78
|
* Which `.rmanrc` keys a command reads, for `--config` to print instead of running it - a dotted
|
|
@@ -62,9 +62,56 @@ export interface RunStepContext {
|
|
|
62
62
|
* trouble only by returning something nobody reads is a step that passes while doing nothing.
|
|
63
63
|
*/
|
|
64
64
|
export type RunStepFn = (context: RunStepContext) => void | Promise<void>;
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* One step written as an object, which is how a step says something *about itself* - today, whether
|
|
67
|
+
* it has to wait for the package's dependencies.
|
|
68
|
+
*
|
|
69
|
+
* ```yml
|
|
70
|
+
* run:
|
|
71
|
+
* build:
|
|
72
|
+
* before:
|
|
73
|
+
* - { topo: false, command: eslint . } # nothing to wait for
|
|
74
|
+
* exec: { topo: true, command: tsc -b } # cannot start before the dependencies are built
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* `command` is the step itself and takes the same two forms the plain value does - a shell command
|
|
78
|
+
* or a function - so the object adds the marker and takes nothing away. An unknown key is refused
|
|
79
|
+
* rather than ignored.
|
|
80
|
+
*/
|
|
81
|
+
export interface RunStepObject {
|
|
82
|
+
/**
|
|
83
|
+
* What the step runs: a shell command, or a function ([`RunStepFn`](#RunStepFn)), exactly as the
|
|
84
|
+
* plain value form. `${{ }}` in a string is interpolated as usual.
|
|
85
|
+
*
|
|
86
|
+
* One key for both, because `run.<script>.exec` is already one key taking both - a second name
|
|
87
|
+
* for the function case would mean two spellings of one thing and a rule about which to use.
|
|
88
|
+
*/
|
|
89
|
+
command?: string | RunStepFn;
|
|
90
|
+
/**
|
|
91
|
+
* Whether this step waits for every package this one depends on to finish. `true` is the useful
|
|
92
|
+
* one - a `tsc -b` cannot start before the packages it compiles against are built, while an
|
|
93
|
+
* `eslint .` beside it has nothing to wait for.
|
|
94
|
+
*
|
|
95
|
+
* **The wait is for a dependency's whole script, so the *first* `true` is where the package
|
|
96
|
+
* actually blocks.** Everything after that has its dependencies behind it already, which makes a
|
|
97
|
+
* later `topo: false` a true statement about the step that changes nothing about when it runs -
|
|
98
|
+
* worth writing as intent, not a lever.
|
|
99
|
+
*
|
|
100
|
+
* **A script whose steps mention `topo` at all is taken as the whole statement**: the first
|
|
101
|
+
* `true` is the barrier, and if no step says `true` there is no wait. A script where no step
|
|
102
|
+
* mentions it falls back to `run.<script>.topo` - on by default - waiting before its first step,
|
|
103
|
+
* which is where the wait has always been. `run.<script>.topo: false` and `--no-topo` still turn
|
|
104
|
+
* ordering off outright.
|
|
105
|
+
*
|
|
106
|
+
* Only in `run.<script>`: a `version` hook runs for one package around its own version write,
|
|
107
|
+
* with no package graph to join, and the key is refused there rather than quietly doing nothing.
|
|
108
|
+
*/
|
|
109
|
+
topo?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** One entry of a `before`/`exec`/`after` slot: a shell command, a function, or a
|
|
112
|
+
* [`RunStepObject`](#RunStepObject) saying something about the step. A list of them runs in
|
|
113
|
+
* sequence, and the forms mix freely within one list. */
|
|
114
|
+
export type RunStepValue = string | RunStepFn | RunStepObject;
|
|
68
115
|
/**
|
|
69
116
|
* A `run.<script>.if` written as JavaScript, deciding whether the script runs for this package.
|
|
70
117
|
*
|
package/index.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export { Registry } from './core/classes/registry.js';
|
|
|
66
66
|
export { Repository } from './core/classes/repository.js';
|
|
67
67
|
export { Service, type ServiceFactory, type ServiceMap } from './core/classes/service.js';
|
|
68
68
|
export { declaredTargets, type PublishTarget, shipsTo, targetsOf, unknownTargets, } from './core/interfaces/publish-target.js';
|
|
69
|
-
export type { RunConditionFn, RunStepContext, RunStepFn, RunStepValue } from './core/interfaces/run-step.js';
|
|
69
|
+
export type { RunConditionFn, RunStepContext, RunStepFn, RunStepObject, RunStepValue, } from './core/interfaces/run-step.js';
|
|
70
70
|
/**
|
|
71
71
|
* **`Platform` is one technology, whole**; **`Plugin` is whatever a package contributes**, platforms
|
|
72
72
|
* among them.
|
|
@@ -124,6 +124,12 @@ export * from './services.js';
|
|
|
124
124
|
/** Branch guarding: `allowBranch`/`ignoreBranch`, so a plugin's release command refuses to run on
|
|
125
125
|
* the wrong branch exactly as `publish` and `version` do. */
|
|
126
126
|
export { applyBranchGuardOptions, assertAllowedBranch, type BranchGuardOptions, branchGuardOptions, readBranchGuardOptions, } from './utils/branch-guard.js';
|
|
127
|
+
/**
|
|
128
|
+
* The flags every `run`-shaped command shares - `--parallel`, `--bail`, `--topo`, `--progress`,
|
|
129
|
+
* `--changed`, `--changed-since`, plus the package filters - and the reader that turns them into
|
|
130
|
+
* `RunService.Options`.
|
|
131
|
+
*/
|
|
132
|
+
export { parallelOptions, readParallelOptions, readRunOptions, runOptions } from './utils/run-options.js';
|
|
127
133
|
/** Where a repository's locally installed binaries live - the core spells the PATH variable, a
|
|
128
134
|
* plugin says which directories go on it (see `Plugin.binPaths`). */
|
|
129
135
|
export { BinPath } from './utils/bin-path.js';
|
package/index.js
CHANGED
|
@@ -106,6 +106,17 @@ export * from './services.js';
|
|
|
106
106
|
/** Branch guarding: `allowBranch`/`ignoreBranch`, so a plugin's release command refuses to run on
|
|
107
107
|
* the wrong branch exactly as `publish` and `version` do. */
|
|
108
108
|
export { applyBranchGuardOptions, assertAllowedBranch, branchGuardOptions, readBranchGuardOptions, } from './utils/branch-guard.js';
|
|
109
|
+
/**
|
|
110
|
+
* The flags every `run`-shaped command shares - `--parallel`, `--bail`, `--topo`, `--progress`,
|
|
111
|
+
* `--changed`, `--changed-since`, plus the package filters - and the reader that turns them into
|
|
112
|
+
* `RunService.Options`.
|
|
113
|
+
*/
|
|
114
|
+
/* **Exported because a plugin cannot otherwise write an alias for `run <script>`.** `build` is one
|
|
115
|
+
* (`configKeys: ['run.build']`, a handler that calls `runScript`) and it reaches these through a
|
|
116
|
+
* relative import; a preset adding `compile` or `docs` had no way to, so it would restate the six
|
|
117
|
+
* flags and the reader - two lists free to disagree, which is how a flag ends up meaning one thing
|
|
118
|
+
* on `rman build` and nothing on the alias beside it. */
|
|
119
|
+
export { parallelOptions, readParallelOptions, readRunOptions, runOptions } from './utils/run-options.js';
|
|
109
120
|
/** Where a repository's locally installed binaries live - the core spells the PATH variable, a
|
|
110
121
|
* plugin says which directories go on it (see `Plugin.binPaths`). */
|
|
111
122
|
export { BinPath } from './utils/bin-path.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as yargs from 'yargs';
|
|
2
2
|
import type { RmanApplication } from '../core/application.js';
|
|
3
3
|
import type { CodeSubtree } from '../core/config/config-paths.js';
|
|
4
|
-
import type { CustomCommand } from '../core/interfaces/custom-command.js';
|
|
4
|
+
import type { CommandContext, CustomCommand } from '../core/interfaces/custom-command.js';
|
|
5
5
|
import type { Platform, Plugin } from '../core/interfaces/plugin.js';
|
|
6
6
|
import type { PublishTarget } from '../core/interfaces/publish-target.js';
|
|
7
7
|
import type { RunConditionFn, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
|
|
@@ -577,7 +577,16 @@ export declare namespace RmanConfig {
|
|
|
577
577
|
* `toYargsCommand` casts the handler to yargs' own and that cast is the single place the two
|
|
578
578
|
* descriptions of argv are admitted to disagree.
|
|
579
579
|
*/
|
|
580
|
-
|
|
580
|
+
/**
|
|
581
|
+
* The second parameter is the [`CommandContext`](../core/interfaces/custom-command.ts) - this
|
|
582
|
+
* run's `runBin`, `logger`, `forEachPackage` and `parallel` - and is **optional**, so a command
|
|
583
|
+
* that does not take it still satisfies this.
|
|
584
|
+
*
|
|
585
|
+
* `(args, context)` is the opposite order to `CustomCommand.handler`'s `(context, args)`, and
|
|
586
|
+
* the reason is compatibility rather than taste: every declared command in existence was
|
|
587
|
+
* `handler(args)`, so appending a parameter breaks none and prepending one breaks all.
|
|
588
|
+
*/
|
|
589
|
+
handler: (argv: never, context?: CommandContext) => void;
|
|
581
590
|
};
|
|
582
591
|
/**
|
|
583
592
|
* The names a command string declares as positionals: `'run <script> [args..]'` is
|
|
@@ -783,7 +792,16 @@ export declare namespace RmanConfig {
|
|
|
783
792
|
* (`{ [K in string as K extends 'vars' ? never : K]: ... }`) was tried and does not help - the
|
|
784
793
|
* remap still produces an index signature that claims `vars`.
|
|
785
794
|
*/
|
|
786
|
-
|
|
795
|
+
/**
|
|
796
|
+
* **The bare shorthand is a command, a function or a list - never a single step *object*.** At
|
|
797
|
+
* this position an object is the long form (`{ exec, before, after, topo, ... }`), so admitting a
|
|
798
|
+
* `RunStepObject` beside it would make `run: { build: { topo: true, command: 'tsc -b' } }`
|
|
799
|
+
* type-check and then do nothing: `getConfig` reads an object as the options block, finds no
|
|
800
|
+
* `exec`, and the package reports declaring no such script. A list keeps the step object
|
|
801
|
+
* (`run: { build: [{ topo: true, command: 'tsc -b' }] }`), where there is nothing to confuse it
|
|
802
|
+
* with.
|
|
803
|
+
*/
|
|
804
|
+
type RunConfig = Record<string, string | RunStepFn | RunStepValue[] | RunScriptOptions>;
|
|
787
805
|
interface RunScriptOptions extends RunScriptOptionsKeys, ScopedVars {
|
|
788
806
|
}
|
|
789
807
|
/**
|
package/package.json
CHANGED
|
@@ -600,33 +600,139 @@ function resolveFilePath(pkg, optionsFilePath) {
|
|
|
600
600
|
*/
|
|
601
601
|
function partitionTargets(repository, targets, options) {
|
|
602
602
|
const grouping = changelogGroupBy(repository, options);
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
else
|
|
610
|
-
byKey.set(key, [pkg]);
|
|
611
|
-
}
|
|
603
|
+
const groupFiles = grouping === 'group' ? readGroupFiles(repository, options) : {};
|
|
604
|
+
/** Checked against the **whole** repository, not against `targets`: a collision is a fact about
|
|
605
|
+
* the configuration, and a `--scope` that happens to leave one of the two groups out must not be
|
|
606
|
+
* what lets it through. */
|
|
607
|
+
if (grouping === 'group')
|
|
608
|
+
assertGroupFiles(repository, groupFiles, options);
|
|
612
609
|
const repositoryLabel = `${path.basename(repository.dirname)} repository`;
|
|
613
|
-
return [...
|
|
610
|
+
return [...groupTargets(targets, grouping)].map(([key, members]) => {
|
|
614
611
|
/** `targets` starts at the root, so a group holding it has it first - which is what makes the
|
|
615
612
|
* root the `home` of the default group without a second rule saying so. */
|
|
616
613
|
const home = members[0];
|
|
617
614
|
const grouped = !key.startsWith('solo:');
|
|
618
|
-
const named =
|
|
619
|
-
const filePkg = grouped ? repository.rootPackage : home;
|
|
615
|
+
const named = namedGroupOf(key);
|
|
620
616
|
return {
|
|
621
617
|
home,
|
|
622
|
-
filePkg,
|
|
618
|
+
filePkg: grouped ? repository.rootPackage : home,
|
|
623
619
|
grouped,
|
|
624
620
|
members: new Set(members),
|
|
625
621
|
label: named ?? (home === repository.rootPackage ? repositoryLabel : home.name),
|
|
626
|
-
file:
|
|
622
|
+
file: groupFile(repository, key, home, groupFiles, options),
|
|
627
623
|
};
|
|
628
624
|
});
|
|
629
625
|
}
|
|
626
|
+
/** The packages cut into what each file is about - by `.rmanrc group` under `groupBy: 'group'`, one
|
|
627
|
+
* package each otherwise. Insertion order, so the first member of each group is the first of
|
|
628
|
+
* `packages` to belong to it. */
|
|
629
|
+
function groupTargets(packages, grouping) {
|
|
630
|
+
const byKey = new Map();
|
|
631
|
+
for (const pkg of packages) {
|
|
632
|
+
const key = grouping === 'group' ? groupKeyOf(pkg) : `solo:${pkg.name}`;
|
|
633
|
+
const members = byKey.get(key);
|
|
634
|
+
if (members)
|
|
635
|
+
members.push(pkg);
|
|
636
|
+
else
|
|
637
|
+
byKey.set(key, [pkg]);
|
|
638
|
+
}
|
|
639
|
+
return byKey;
|
|
640
|
+
}
|
|
641
|
+
/** `named:core` -> `core`; `undefined` for the default group and for a package that is its own. */
|
|
642
|
+
function namedGroupOf(key) {
|
|
643
|
+
return key.startsWith('named:') ? key.slice('named:'.length) : undefined;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Where one group's changelog is written, absolute.
|
|
647
|
+
*
|
|
648
|
+
* In order: a named group listed in `changelog.groupFiles` goes where it says, relative to the
|
|
649
|
+
* repository root; any other named group takes the default file's name with its own suffixed
|
|
650
|
+
* (`CHANGELOG-core.md`), at the root; the default group takes the root's `changelog.filePath`; and a
|
|
651
|
+
* package that is a group of itself takes its own, in its own directory.
|
|
652
|
+
*
|
|
653
|
+
* **An explicit `--file-path` beats `groupFiles`**, as it already beats `changelog.filePath`: it
|
|
654
|
+
* names this run's file, and a run asking for one shape of name should not find half its groups
|
|
655
|
+
* somewhere a config put them. Named groups are still suffixed under it, so two cannot collide.
|
|
656
|
+
*/
|
|
657
|
+
function groupFile(repository, key, home, groupFiles, options) {
|
|
658
|
+
if (key.startsWith('solo:'))
|
|
659
|
+
return path.join(home.dirname, resolveFilePath(home, options.filePath));
|
|
660
|
+
const named = namedGroupOf(key);
|
|
661
|
+
const listed = named && !options.filePath ? groupFiles[named] : undefined;
|
|
662
|
+
if (listed)
|
|
663
|
+
return path.resolve(repository.dirname, listed);
|
|
664
|
+
return path.join(repository.dirname, suffixFileName(resolveFilePath(home, options.filePath), named));
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* `.rmanrc changelog.groupFiles`, off the repository root and nowhere else - a map from a named
|
|
668
|
+
* group to the file its changelog is written to, relative to the repository root.
|
|
669
|
+
*
|
|
670
|
+
* **The root's for the reason `groupBy` is the root's**: which file a group shares is a layout, and
|
|
671
|
+
* cascaded per package two members of one group could disagree about it, which has no answer.
|
|
672
|
+
*/
|
|
673
|
+
function readGroupFiles(repository, options) {
|
|
674
|
+
/** `--file-path` takes the whole decision - see `groupFile` - so the map is not even read. A
|
|
675
|
+
* mistake in it is still a mistake, but not one this run acts on. */
|
|
676
|
+
if (options.filePath)
|
|
677
|
+
return {};
|
|
678
|
+
const declared = repository.rootPackage.config?.changelog?.groupFiles;
|
|
679
|
+
if (declared === undefined || declared === null)
|
|
680
|
+
return {};
|
|
681
|
+
if (typeof declared !== 'object' || Array.isArray(declared)) {
|
|
682
|
+
throw new Error(`.rmanrc "changelog.groupFiles" must map a group name to a file path, ` +
|
|
683
|
+
`e.g. { core: "packages/core/CHANGELOG.md" }.`);
|
|
684
|
+
}
|
|
685
|
+
for (const [group, file] of Object.entries(declared)) {
|
|
686
|
+
if (typeof file !== 'string' || !file) {
|
|
687
|
+
throw new Error(`.rmanrc "changelog.groupFiles.${group}" must be a file path, relative to the repository root.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return declared;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Refuses a `changelog.groupFiles` that would write somewhere wrong - before anything is written.
|
|
694
|
+
*
|
|
695
|
+
* Three ways, and each is silent otherwise:
|
|
696
|
+
*
|
|
697
|
+
* - **A key naming no group.** rman drops an unknown config key without a word, so a typo here would
|
|
698
|
+
* leave the group on the default rule and nothing would say so. Here the groups are known, so the
|
|
699
|
+
* key can be checked against them.
|
|
700
|
+
* - **A path leaving the repository.** `version --changelog` commits the file with the release; a
|
|
701
|
+
* file outside the working tree is one it cannot add.
|
|
702
|
+
* - **Two groups sharing one file**, including the default group and any package that is a group of
|
|
703
|
+
* itself. The `rman:documented-up-to` marker is one per file, so two groups writing into it would
|
|
704
|
+
* each read the other's marker as its own boundary - entries missing or written twice, with
|
|
705
|
+
* nothing failing. The default rule cannot produce a collision; an explicit map can, and an
|
|
706
|
+
* unlisted group's suffixed name can land on a listed one's path.
|
|
707
|
+
*/
|
|
708
|
+
function assertGroupFiles(repository, groupFiles, options) {
|
|
709
|
+
const all = groupTargets([repository.rootPackage, ...repository.packages], 'group');
|
|
710
|
+
const named = [...all.keys()].map(namedGroupOf).filter((name) => !!name);
|
|
711
|
+
for (const [group, file] of Object.entries(groupFiles)) {
|
|
712
|
+
if (!named.includes(group)) {
|
|
713
|
+
throw new Error(`.rmanrc "changelog.groupFiles" names a group "${group}" that no package belongs to. ` +
|
|
714
|
+
(named.length ? `Named groups here: ${named.join(', ')}.` : 'This repository has no named groups.'));
|
|
715
|
+
}
|
|
716
|
+
const rel = path.relative(repository.dirname, path.resolve(repository.dirname, file));
|
|
717
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
718
|
+
throw new Error(`.rmanrc "changelog.groupFiles.${group}" points outside the repository (${file}). ` +
|
|
719
|
+
`The file is committed with the release, so it has to be inside the working tree.`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const owner = new Map();
|
|
723
|
+
for (const [key, members] of all) {
|
|
724
|
+
const file = groupFile(repository, key, members[0], groupFiles, options);
|
|
725
|
+
const label = namedGroupOf(key) ?? (key === 'default' ? 'the default group' : members[0].name);
|
|
726
|
+
const other = owner.get(file);
|
|
727
|
+
if (other) {
|
|
728
|
+
throw new Error(`Two changelogs would be written to one file: ${other} and ${label} both resolve to ` +
|
|
729
|
+
`${path.relative(repository.dirname, file)}.\n` +
|
|
730
|
+
` Each file carries one "documented up to" marker, so two groups sharing it would read each ` +
|
|
731
|
+
`other's boundary. Give each its own path in .rmanrc "changelog.groupFiles".`);
|
|
732
|
+
}
|
|
733
|
+
owner.set(file, label);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
630
736
|
/** `CHANGELOG.md` + `core` -> `CHANGELOG-core.md`, keeping whatever directory and extension
|
|
631
737
|
* `changelog.filePath` chose (`docs/HISTORY.md` -> `docs/HISTORY-core.md`). */
|
|
632
738
|
function suffixFileName(filePath, suffix) {
|
|
@@ -4,6 +4,7 @@ import { Service } from '../core/classes/service.js';
|
|
|
4
4
|
import type { RunStepContext, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
|
|
5
5
|
import { type LogLevel } from '../utils/logger.js';
|
|
6
6
|
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
7
|
+
import { ProgressPanel } from '../utils/progress-panel.js';
|
|
7
8
|
/**
|
|
8
9
|
* A service class - see `ListService` for the shape and `Service` for the three measured
|
|
9
10
|
* consequences a namespace had.
|
|
@@ -17,6 +18,66 @@ export declare class RunService extends Service {
|
|
|
17
18
|
runScript(script: string, options?: RunService.Options & {
|
|
18
19
|
commandName?: string;
|
|
19
20
|
}): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* **The scheduler, shared by every driver that runs work per package.**
|
|
23
|
+
*
|
|
24
|
+
* `runScript` is one driver and `forEachPackage` is the other; both hand over a plan per package
|
|
25
|
+
* and get the same machinery, which is the point of this being a method rather than a shape each
|
|
26
|
+
* one builds for itself. Four things have had to be fixed in it this month alone - the status
|
|
27
|
+
* region handover, the depth-counted console patch, the per-row command, the failed list - and a
|
|
28
|
+
* second copy would have needed each fix twice, with nothing reporting the one that was missed.
|
|
29
|
+
*
|
|
30
|
+
* Throws when any package failed, with `logged` set: the panel has already printed the recap.
|
|
31
|
+
*/
|
|
32
|
+
protected schedule(options: {
|
|
33
|
+
panel: ProgressPanel;
|
|
34
|
+
concurrency: number;
|
|
35
|
+
/** What the panel-off log lines call this run (`run`, `build`, `check`). */
|
|
36
|
+
commandName: string;
|
|
37
|
+
/** Named in the failure thrown when a package fails - `"build" failed`. */
|
|
38
|
+
failureLabel: string;
|
|
39
|
+
/** A repo-wide bookend run first, alone, which every package then waits for. */
|
|
40
|
+
before?: RunService.PackagePlan;
|
|
41
|
+
plans: RunService.PackagePlan[];
|
|
42
|
+
/** A repo-wide bookend run last, alone, after every package. */
|
|
43
|
+
after?: RunService.PackagePlan;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Runs `fn` once per package, under the same scheduler `run` uses - concurrency, `--bail`,
|
|
47
|
+
* dependency order, the progress panel, and `console` routed to the right row.
|
|
48
|
+
*
|
|
49
|
+
* **This is what a command reaches for instead of a `for` loop.** `check` was that loop: `await
|
|
50
|
+
* runBin(...)` per package, which ignores `--parallel` outright, draws no panel, and had
|
|
51
|
+
* re-implemented `--bail`, the package filters, the two-endings rule and a summary line - about
|
|
52
|
+
* sixty lines of this service, already drifted (its own tally counted packages a bail had skipped
|
|
53
|
+
* as successes, a bug fixed here separately). Measured on `panates/opra`: nineteen packages at
|
|
54
|
+
* 0.4-0.9s each, so eleven seconds of wall clock where the work is entirely independent.
|
|
55
|
+
*
|
|
56
|
+
* `fn` is handed exactly what a **function step** gets - `pkg`, `cwd`, a `runBin` already bound to
|
|
57
|
+
* that package's directory and this run's log level, and a `logger`. That is the answer to the
|
|
58
|
+
* complaint this exists for: an imported `runBin` knows neither, and a command reaching for one
|
|
59
|
+
* had to thread `cwd`, `app` and `logLevel` through by hand at every call.
|
|
60
|
+
*
|
|
61
|
+
* Ordering is **off** unless `topo` asks for it: a command sweeping packages with an independent
|
|
62
|
+
* tool is the common case, and a wait nobody asked for is the more expensive mistake here.
|
|
63
|
+
*/
|
|
64
|
+
forEachPackage(packages: readonly Package[], fn: RunStepFn, options?: RunService.ForEachOptions): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Runs `tasks` with at most `parallel` of them in flight - `Promise.all` under the repository's
|
|
67
|
+
* own concurrency rule.
|
|
68
|
+
*
|
|
69
|
+
* **The low-level half of `forEachPackage`, for work that is not per package** - sharding a file
|
|
70
|
+
* list across cores, say. It buys the concurrency limit and nothing else: a bare list of functions
|
|
71
|
+
* carries no name, so there is no panel row to label, nothing to name in a failure, and no
|
|
72
|
+
* dependency to order by. That is why it is the second thing to reach for, not the first.
|
|
73
|
+
*
|
|
74
|
+
* **Settles the tasks already in flight before rejecting.** A plain `Promise.all` rejects on the
|
|
75
|
+
* first failure while its siblings keep running - with child processes behind them, that leaves
|
|
76
|
+
* output arriving after the command has reported and exited.
|
|
77
|
+
*/
|
|
78
|
+
parallel<T>(tasks: readonly (() => Promise<T>)[], options?: {
|
|
79
|
+
parallel?: boolean | number;
|
|
80
|
+
}): Promise<T[]>;
|
|
20
81
|
}
|
|
21
82
|
export declare namespace RunService {
|
|
22
83
|
export interface Options extends PackageFilterOptions {
|
|
@@ -41,6 +102,49 @@ export declare namespace RunService {
|
|
|
41
102
|
* root, or outside any known package. */
|
|
42
103
|
fromRoot?: boolean;
|
|
43
104
|
}
|
|
105
|
+
/** What `forEachPackage` takes - the scheduling half of `Options`, with no script to read it from
|
|
106
|
+
* the config and no package filtering, which the caller has already done. */
|
|
107
|
+
export interface ForEachOptions {
|
|
108
|
+
/** Max packages at once: `true`/omitted = CPU count, a number = that many, `false` = serial. */
|
|
109
|
+
parallel?: boolean | number;
|
|
110
|
+
/** Stop the batch at the first failure. Default `true`, as `run`'s is. */
|
|
111
|
+
bail?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Wait for a package's dependencies before running it. **Default `false`**, unlike `run`'s: a
|
|
114
|
+
* command sweeping packages with an independent tool is the common case here, and a wait nobody
|
|
115
|
+
* asked for costs the whole point of the call.
|
|
116
|
+
*/
|
|
117
|
+
topo?: boolean;
|
|
118
|
+
/** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
|
|
119
|
+
progress?: boolean;
|
|
120
|
+
logLevel?: LogLevel;
|
|
121
|
+
/** The panel's title, the row's step name and the word in the failure - the command's own name
|
|
122
|
+
* (`check`) reads best. */
|
|
123
|
+
label?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* One package's share of a scheduled run: what to do, where, and under which rules.
|
|
127
|
+
*
|
|
128
|
+
* **The unit the scheduler takes, and why it is a package rather than a bare function.** A row has
|
|
129
|
+
* to be named for the panel to label it, for the summary to say which package failed, and for the
|
|
130
|
+
* failed list to have something to list; a dependency edge has to name a package for ordering to
|
|
131
|
+
* mean anything. A `(() => Promise<void>)[]` carries neither, which is what makes it the wrong
|
|
132
|
+
* shape for this even though it is the obvious one.
|
|
133
|
+
*/
|
|
134
|
+
export interface PackagePlan {
|
|
135
|
+
pkg: Package;
|
|
136
|
+
/** The panel row's name and the task's - unique within the run, and a package's own name, which
|
|
137
|
+
* is what lets dependency edges be handed to power-tasks as names. */
|
|
138
|
+
name: string;
|
|
139
|
+
/** What the panel-off log line calls it: the package's name, or `root` for a bookend. */
|
|
140
|
+
label: string;
|
|
141
|
+
steps: ScriptStep[];
|
|
142
|
+
/** The directory the steps run in - the package's own, or the repository root for a bookend. */
|
|
143
|
+
cwd: string;
|
|
144
|
+
topo: boolean;
|
|
145
|
+
bail: boolean;
|
|
146
|
+
logLevel: LogLevel;
|
|
147
|
+
}
|
|
44
148
|
/**
|
|
45
149
|
* A package's `.rmanrc` (cascaded) can configure `run.<script>.*` - e.g.
|
|
46
150
|
* run:
|
|
@@ -83,6 +187,10 @@ export declare namespace RunService {
|
|
|
83
187
|
/** What the progress panel and the per-step log print: the command itself, or the function's
|
|
84
188
|
* own name. */
|
|
85
189
|
label: string;
|
|
190
|
+
/** What a step written as an object said about waiting, `undefined` for one that said nothing -
|
|
191
|
+
* and the three are three different answers, which is why this is not a plain boolean. See
|
|
192
|
+
* `RunStepObject.topo`. */
|
|
193
|
+
topo?: boolean;
|
|
86
194
|
}
|
|
87
195
|
export interface CommandStep extends StepBase {
|
|
88
196
|
command: string;
|
|
@@ -167,7 +275,9 @@ export declare namespace RunService {
|
|
|
167
275
|
* Exported, and the only implementation: `VersionService` used to carry a second one that behaved
|
|
168
276
|
* differently, which is how `version.<slot>` came to join its array with `' && '`.
|
|
169
277
|
*/
|
|
170
|
-
export function normalizeScriptValue(value: unknown, at: string
|
|
278
|
+
export function normalizeScriptValue(value: unknown, at: string, options?: {
|
|
279
|
+
topo?: boolean;
|
|
280
|
+
}): RunStepValue[];
|
|
171
281
|
/**
|
|
172
282
|
* Runs `fn` with `console` captured into `onLine`, and leaves the real `console` behind when the
|
|
173
283
|
* last concurrent step is done.
|
|
@@ -224,6 +334,20 @@ export declare function resolveBool(cliValue: boolean | undefined, pkg: Package,
|
|
|
224
334
|
*/
|
|
225
335
|
export declare function resolveBail(cliValue: boolean | undefined, pkg: Package, script: string, fallback: boolean): boolean;
|
|
226
336
|
export declare function resolveNumber(cliValue: number | undefined, pkg: Package, script: string, key: string, fallback: number): number;
|
|
337
|
+
/**
|
|
338
|
+
* How many packages run at once: what the flag said, or `whenUnset` where it said nothing.
|
|
339
|
+
*
|
|
340
|
+
* **`--parallel` and `concurrency` are deliberately different names**, so this is where the two
|
|
341
|
+
* meet: the flag is `boolean | number` (omit/`true` = CPU count, `false` = serially), the key is the
|
|
342
|
+
* number it resolves to. It was written out at `runScript`'s top and two more callers need the same
|
|
343
|
+
* answer - three copies of a four-branch ternary is how a flag comes to mean something slightly
|
|
344
|
+
* different depending on which command you typed.
|
|
345
|
+
*
|
|
346
|
+
* **Where the unset case comes from is the caller's**, and deliberately not read here: `run`'s is
|
|
347
|
+
* `run.<script>.concurrency`, while a command calling `forEachPackage` owns a key of its own
|
|
348
|
+
* (`check.concurrency`) that this function has no business guessing the name of.
|
|
349
|
+
*/
|
|
350
|
+
export declare function resolveConcurrency(parallel: boolean | number | undefined, whenUnset: number): number;
|
|
227
351
|
export declare function resolveLogLevel(cliValue: LogLevel | undefined, pkg: Package, script: string, fallback: LogLevel): LogLevel;
|
|
228
352
|
declare module '../core/classes/service.js' {
|
|
229
353
|
interface ServiceMap {
|