rman 1.3.0 → 2.0.0-beta.3
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/README.md +65 -31
- package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
- package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
- package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
- package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
- package/builtins/platforms/node/commands/ci.command.js +60 -0
- package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
- package/builtins/platforms/node/commands/clean.command.js +74 -0
- package/builtins/platforms/node/index.d.ts +8 -0
- package/builtins/platforms/node/index.js +8 -0
- package/builtins/platforms/node/node-config.interface.d.ts +76 -0
- package/builtins/platforms/node/node-config.interface.js +7 -0
- package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
- package/builtins/platforms/node/node-manifest.provider.js +125 -0
- package/builtins/platforms/node/node.platform.d.ts +53 -0
- package/builtins/platforms/node/node.platform.js +134 -0
- package/builtins/platforms/node/services/ci.service.d.ts +47 -0
- package/builtins/platforms/node/services/ci.service.js +213 -0
- package/builtins/platforms/node/services/clean.service.d.ts +53 -0
- package/builtins/platforms/node/services/clean.service.js +237 -0
- package/builtins/platforms/node/services/publish.service.d.ts +114 -0
- package/builtins/platforms/node/services/publish.service.js +371 -0
- package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
- package/builtins/platforms/node/services/version-plan.service.js +58 -0
- package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
- package/builtins/platforms/node/utils/workspace-range.js +28 -0
- package/builtins/presets/index.d.ts +15 -0
- package/builtins/presets/index.js +57 -0
- package/builtins/presets/node.d.ts +16 -0
- package/builtins/presets/node.js +57 -0
- package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
- package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
- package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
- package/builtins/publish-targets/docker/docker.target.js +40 -0
- package/builtins/publish-targets/docker/index.d.ts +2 -0
- package/builtins/publish-targets/docker/index.js +2 -0
- package/builtins/publish-targets/npm/index.d.ts +2 -0
- package/builtins/publish-targets/npm/index.js +2 -0
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
- package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
- package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
- package/builtins/publish-targets/npm/npm-view.js +71 -0
- package/cli.d.ts +8 -0
- package/cli.js +228 -93
- package/commands/build.command.d.ts +177 -3
- package/commands/build.command.js +20 -10
- package/commands/changelog.command.d.ts +296 -3
- package/commands/changelog.command.js +127 -43
- package/commands/config.command.d.ts +44 -3
- package/commands/config.command.js +53 -24
- package/commands/diff.command.d.ts +37 -3
- package/commands/diff.command.js +25 -16
- package/commands/exec.command.d.ts +193 -3
- package/commands/exec.command.js +60 -58
- package/commands/github-release.command.d.ts +154 -3
- package/commands/github-release.command.js +67 -37
- package/commands/import.command.d.ts +36 -3
- package/commands/import.command.js +28 -20
- package/commands/info.command.d.ts +35 -7
- package/commands/info.command.js +36 -30
- package/commands/lint.command.d.ts +176 -0
- package/commands/lint.command.js +34 -0
- package/commands/list.command.d.ts +163 -3
- package/commands/list.command.js +109 -71
- package/commands/publish.command.d.ts +231 -0
- package/commands/publish.command.js +304 -0
- package/commands/run.command.d.ts +186 -6
- package/commands/run.command.js +26 -72
- package/commands/test.command.d.ts +173 -3
- package/commands/test.command.js +16 -10
- package/commands/version.command.d.ts +330 -3
- package/commands/version.command.js +292 -78
- package/commands.d.ts +32 -0
- package/commands.js +28 -0
- package/constants.js +1 -1
- package/core/application.d.ts +116 -0
- package/core/application.js +160 -0
- package/core/classes/package.d.ts +219 -0
- package/core/classes/package.js +214 -0
- package/core/classes/registry.d.ts +30 -0
- package/core/classes/registry.js +47 -0
- package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
- package/core/{repository.js → classes/repository.js} +195 -75
- package/core/classes/service.d.ts +49 -0
- package/core/classes/service.js +40 -0
- package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
- package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
- package/core/classes/workspace.d.ts +275 -0
- package/core/classes/workspace.js +537 -0
- package/core/command-builder.d.ts +14 -0
- package/core/command-builder.js +94 -0
- package/core/config/config-file-scope.d.ts +107 -0
- package/core/config/config-file-scope.js +226 -0
- package/core/config/config-interpolator.d.ts +164 -0
- package/core/config/config-interpolator.js +577 -0
- package/core/config/config-paths.d.ts +65 -0
- package/core/config/config-paths.js +78 -0
- package/core/config/config-reader.d.ts +192 -0
- package/core/config/config-reader.js +727 -0
- package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
- package/core/config/merge-config.js +260 -0
- package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
- package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
- package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
- package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
- package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
- package/core/interfaces/manifest.js +132 -0
- package/core/interfaces/plugin.d.ts +174 -0
- package/core/interfaces/plugin.js +124 -0
- package/core/interfaces/publish-target.d.ts +136 -0
- package/core/interfaces/publish-target.js +43 -0
- package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
- package/core/interfaces/run-step.js +1 -0
- package/core/plugin-loader.d.ts +25 -0
- package/core/plugin-loader.js +53 -0
- package/index.d.ts +122 -24
- package/index.js +66 -16
- package/interfaces/config-scope.interface.d.ts +223 -0
- package/interfaces/config-scope.interface.js +1 -0
- package/interfaces/rman-config.interface.d.ts +821 -203
- package/interfaces/rman-config.interface.js +78 -1
- package/package.json +3 -1
- package/services/change-hash.service.d.ts +42 -8
- package/services/change-hash.service.js +92 -13
- package/services/changelog.service.d.ts +91 -52
- package/services/changelog.service.js +460 -74
- package/services/config-service.d.ts +8 -0
- package/services/config-service.js +3 -0
- package/services/exec.service.d.ts +23 -12
- package/services/exec.service.js +14 -9
- package/services/github-release.service.d.ts +45 -34
- package/services/github-release.service.js +15 -12
- package/services/import.service.d.ts +25 -14
- package/services/import.service.js +9 -5
- package/services/list.service.d.ts +67 -13
- package/services/list.service.js +62 -15
- package/services/run.service.d.ts +35 -22
- package/services/run.service.js +316 -233
- package/services/system-info.d.ts +1 -1
- package/services/version-plan.service.d.ts +35 -8
- package/services/version-plan.service.js +96 -36
- package/services/version.service.d.ts +34 -14
- package/services/version.service.js +35 -16
- package/services.d.ts +1 -1
- package/services.js +1 -1
- package/utils/bin-path.d.ts +6 -7
- package/utils/bin-path.js +7 -18
- package/utils/branch-guard.d.ts +30 -1
- package/utils/branch-guard.js +31 -0
- package/utils/color-yaml.d.ts +8 -0
- package/utils/color-yaml.js +86 -0
- package/utils/exec.d.ts +10 -0
- package/utils/exec.js +1 -1
- package/utils/git.d.ts +26 -0
- package/utils/git.js +65 -0
- package/utils/logger.d.ts +2 -2
- package/utils/logger.js +1 -1
- package/utils/package-filter.d.ts +135 -8
- package/utils/package-filter.js +204 -16
- package/utils/printable-config.d.ts +9 -1
- package/utils/printable-config.js +46 -1
- package/utils/progress-panel.d.ts +9 -0
- package/utils/progress-panel.js +23 -1
- package/utils/release-version.d.ts +1 -1
- package/utils/run-bin.d.ts +10 -0
- package/utils/run-bin.js +1 -1
- package/utils/run-options.d.ts +97 -0
- package/utils/run-options.js +81 -0
- package/utils/version-group.d.ts +31 -0
- package/utils/version-group.js +43 -0
- package/utils/version-stamp.d.ts +1 -1
- package/utils/version-stamp.js +1 -1
- package/commands/changed.command.d.ts +0 -3
- package/commands/changed.command.js +0 -29
- package/core/config.d.ts +0 -409
- package/core/config.js +0 -939
- package/core/extends-config.d.ts +0 -27
- package/core/extends-config.js +0 -89
- package/core/manifest.js +0 -150
- package/core/merge-config.js +0 -219
- package/core/package.d.ts +0 -83
- package/core/package.js +0 -106
- package/core/plugin.d.ts +0 -112
- package/core/plugin.js +0 -189
- package/core/workspace.d.ts +0 -68
- package/core/workspace.js +0 -83
- /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_PRESETS } from '../../builtins/presets/index.js';
|
|
4
|
+
import { ConfigReader } from '../config/config-reader.js';
|
|
5
|
+
import { mergeConfig, ORIGINS } from '../config/merge-config.js';
|
|
6
|
+
import { basePlatform } from '../interfaces/plugin.js';
|
|
7
|
+
import { Package } from './package.js';
|
|
8
|
+
/**
|
|
9
|
+
* A repository's layout, resolved: which directories hold packages, which technology claims each of
|
|
10
|
+
* them, what each one answers to, and the **raw** config each one ends up with.
|
|
11
|
+
*
|
|
12
|
+
* Built with {@link Workspace.create}, which runs the whole resolution; the constructor is
|
|
13
|
+
* protected, so there is no such thing as a half-built workspace.
|
|
14
|
+
*
|
|
15
|
+
* The configs it produces are **raw** - every `${{ }}` and every value function is still
|
|
16
|
+
* unevaluated. Resolving one needs `pkg`, `repository` and `git`, which belong to the repository
|
|
17
|
+
* rather than to its layout; `ConfigInterpolator` is the step after this.
|
|
18
|
+
*/
|
|
19
|
+
/* The three steps live together because they depend on each other in one direction only: the root's
|
|
20
|
+
* config names the plugins, the plugins decide which directories are packages, and a package's
|
|
21
|
+
* config is the directory chain above it layered together. None of it can be done out of order, and
|
|
22
|
+
* each step is useless on its own.
|
|
23
|
+
*
|
|
24
|
+
* Every step is an overridable `protected` method, so a technology or a spec can replace one and
|
|
25
|
+
* keep the rest. */
|
|
26
|
+
export class Workspace {
|
|
27
|
+
options;
|
|
28
|
+
/** The repository root - where the walk starts and what every directory chain is relative to. */
|
|
29
|
+
rootDir;
|
|
30
|
+
/** Every plugin in play, in load order: the ones passed in first, then each level's own. */
|
|
31
|
+
plugins = [];
|
|
32
|
+
/** Every technology in play, in contribution order - which is what decides who claims a
|
|
33
|
+
* directory, since the first that recognizes it wins. */
|
|
34
|
+
platforms = [];
|
|
35
|
+
/** The root directory's own package. Not one of `packages`, which holds the members. */
|
|
36
|
+
rootPackage;
|
|
37
|
+
/** Every package below the root, depth-first. */
|
|
38
|
+
packages = [];
|
|
39
|
+
/** Each directory's own config, read once - see `_readLevel`. */
|
|
40
|
+
_levels = new Map();
|
|
41
|
+
/** The packages whose selector came from a `"name"` declaration rather than from their platform,
|
|
42
|
+
* for the clash message alone. */
|
|
43
|
+
_declaredSelectors = new Set();
|
|
44
|
+
/** Built on first use, unless `options.reader` supplied one. */
|
|
45
|
+
_configReader;
|
|
46
|
+
/**
|
|
47
|
+
* Builds a workspace and resolves it: discovery, selectors, and every package's raw config.
|
|
48
|
+
*
|
|
49
|
+
* A subclass inherits this and gets an instance of itself. Its declared return type stays
|
|
50
|
+
* `Workspace`, so a subclass wanting its own declares a one-line `create` that calls this.
|
|
51
|
+
*
|
|
52
|
+
* @param rootDir the repository root
|
|
53
|
+
*/
|
|
54
|
+
/* **`new this()`, not `new Workspace()`.** In a static method `this` is the class the call was
|
|
55
|
+
* made on, so a subclass inheriting this factory builds itself; hard-coding the name would let
|
|
56
|
+
* someone override `_init` and never see it run. The spec for it asserts `instanceof` on the
|
|
57
|
+
* subclass.
|
|
58
|
+
*
|
|
59
|
+
* **The return type is a TypeScript limit rather than a choice.** The polymorphic form
|
|
60
|
+
* (`this: new (...) => T`) requires a *publicly* constructible class, and the constructor here is
|
|
61
|
+
* protected on purpose. The two cannot both be had, and the guard is worth more than the inferred
|
|
62
|
+
* type. */
|
|
63
|
+
static async create(rootDir, options) {
|
|
64
|
+
const workspace = new this(rootDir, options);
|
|
65
|
+
await workspace._init();
|
|
66
|
+
return workspace;
|
|
67
|
+
}
|
|
68
|
+
/** Use {@link Workspace.create}. */
|
|
69
|
+
/* Protected because an un-initialized workspace is a half-built object - no packages, no configs,
|
|
70
|
+
* and nothing saying so. */
|
|
71
|
+
constructor(rootDir, options) {
|
|
72
|
+
this.options = options;
|
|
73
|
+
this.rootDir = path.resolve(rootDir);
|
|
74
|
+
this.plugins = [...(options.plugins ?? [])];
|
|
75
|
+
this.platforms = [...(options.platforms ?? [])];
|
|
76
|
+
}
|
|
77
|
+
/** The package at a directory, root included, or `undefined` for one that holds none. */
|
|
78
|
+
packageAt(dirname) {
|
|
79
|
+
const resolved = path.resolve(dirname);
|
|
80
|
+
if (this.rootPackage && this.rootPackage.dirname === resolved)
|
|
81
|
+
return this.rootPackage;
|
|
82
|
+
return this.packages.find(pkg => pkg.dirname === resolved);
|
|
83
|
+
}
|
|
84
|
+
/** The whole resolution, in the one order the steps admit. */
|
|
85
|
+
/* Each line is a `protected` method rather than inline code precisely so this reads as the order
|
|
86
|
+
* and nothing else - and so a subclass replacing one step does not have to restate the rest. */
|
|
87
|
+
async _init() {
|
|
88
|
+
await this._discover();
|
|
89
|
+
await this._resolveSelectors();
|
|
90
|
+
this._assertUniqueSelectors();
|
|
91
|
+
await this._resolveConfigs();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Descends from the root, asking each directory's own technology where its children are, and
|
|
95
|
+
* fills `rootPackage` and `packages`.
|
|
96
|
+
*
|
|
97
|
+
* One step, repeated: read the directory's config (which loads whatever plugins it names and
|
|
98
|
+
* decides its platform), then ask *that* platform's `getWorkspace` for the directories below it
|
|
99
|
+
* holding packages. A directory is visited once, and `options.deep` bounds the descent.
|
|
100
|
+
*/
|
|
101
|
+
/* **First visit wins.** A provider may legitimately name a directory another one already claimed
|
|
102
|
+
* (two overlapping globs, a symlinked package), and one naming an ancestor would otherwise
|
|
103
|
+
* recurse forever.
|
|
104
|
+
*
|
|
105
|
+
* `deep` bounds the descent for the same reason `findRoot` bounds its climb: a provider computing
|
|
106
|
+
* paths rather than reading them can produce a chain that never ends, and a guessed depth beats a
|
|
107
|
+
* hang with nothing printed. */
|
|
108
|
+
async _discover() {
|
|
109
|
+
const visited = new Set();
|
|
110
|
+
const descend = async (dir, parent, remaining) => {
|
|
111
|
+
const dirname = path.resolve(dir);
|
|
112
|
+
visited.add(dirname);
|
|
113
|
+
const level = await this._readLevel(dirname);
|
|
114
|
+
const platform = this._platformFor(dirname, level.config);
|
|
115
|
+
/** The platform is handed over rather than searched for: the walk that found this directory
|
|
116
|
+
* is what established it, and `Package` re-guessing would be a second answer that can
|
|
117
|
+
* disagree with the first. */
|
|
118
|
+
const pkg = this._createPackage(dirname, platform);
|
|
119
|
+
if (dirname === this.rootDir)
|
|
120
|
+
this.rootPackage = pkg;
|
|
121
|
+
else
|
|
122
|
+
this.packages.push(pkg);
|
|
123
|
+
if (parent) {
|
|
124
|
+
/** `children` is enumerable and `parent` is not: one edge, and only one direction can be
|
|
125
|
+
* the one a walk follows - a `Package` that can be serialized needs the back-reference
|
|
126
|
+
* hidden. */
|
|
127
|
+
parent.children.push(pkg);
|
|
128
|
+
Object.defineProperty(pkg, 'parent', { value: parent, enumerable: false, configurable: true });
|
|
129
|
+
}
|
|
130
|
+
if (remaining <= 0)
|
|
131
|
+
return;
|
|
132
|
+
/** **Its own platform is asked, and nobody else.** A platform that did not claim the
|
|
133
|
+
* directory has no standing to say what is under it. A directory no technology claimed has
|
|
134
|
+
* no `getWorkspace`, hence no children - which is a single-package repository arrived at
|
|
135
|
+
* rather than guessed. */
|
|
136
|
+
for (const child of platform.getWorkspace?.(dirname) ?? []) {
|
|
137
|
+
const childDir = path.resolve(child);
|
|
138
|
+
if (visited.has(childDir))
|
|
139
|
+
continue;
|
|
140
|
+
await descend(childDir, pkg, remaining - 1);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
await descend(this.rootDir, undefined, this.options.deep ?? 10);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Gives every package the selector it answers to - its `.rmanrc "name"` when it declares one, and
|
|
147
|
+
* what its platform says otherwise.
|
|
148
|
+
*
|
|
149
|
+
* `"[glob]"` and `--scope` match this, never the package's name: a name is an *ecosystem's*
|
|
150
|
+
* promise rather than rman's.
|
|
151
|
+
*/
|
|
152
|
+
/* **Resolved from a cascade run with no selector at all**, and that is the step whose absence is
|
|
153
|
+
* silent: a `"[pkg-a]"` block is applied on the strength of the selector, and the selector comes
|
|
154
|
+
* from `name`, which is inside the very config being resolved. Reading `name` while applying
|
|
155
|
+
* selector blocks would need the answer in order to find it. So the chain is walked twice -
|
|
156
|
+
* unmarked only, then for real - over one directory cache, which is what keeps the second pass
|
|
157
|
+
* free.
|
|
158
|
+
*
|
|
159
|
+
* `name` is refused inside a glob block for exactly this reason (`_assertSelectorBlocks`), so
|
|
160
|
+
* nothing is lost by the first pass ignoring them. */
|
|
161
|
+
async _resolveSelectors() {
|
|
162
|
+
for (const pkg of [this.rootPackage, ...this.packages]) {
|
|
163
|
+
const config = await this._cascade(pkg, undefined);
|
|
164
|
+
const declared = config.name;
|
|
165
|
+
if (declared !== undefined && (typeof declared !== 'string' || !declared.trim())) {
|
|
166
|
+
throw new Error(`"name" takes the selector this package answers to - "${pkg.dirname}" gave ${typeof declared}.`);
|
|
167
|
+
}
|
|
168
|
+
if (declared !== undefined)
|
|
169
|
+
this._declaredSelectors.add(pkg);
|
|
170
|
+
/** `platformSelector()` is the package's own - the manifest's name, or whatever its platform
|
|
171
|
+
* says addresses it. `Package` already computed it in its constructor; this only replaces it
|
|
172
|
+
* when the config declared one. */
|
|
173
|
+
pkg.selector = declared?.trim() || pkg.platformSelector();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Refuses two packages answering to one selector. The root is exempt - a glob never matches it and
|
|
178
|
+
* `"[/]"` needs no name, so it shares an address with nobody.
|
|
179
|
+
*/
|
|
180
|
+
/* Two packages answering to one selector make `"[that]"` and `--scope that` ambiguous *silently* -
|
|
181
|
+
* the config reaches both and a lookup returns the first.
|
|
182
|
+
*
|
|
183
|
+
* `name` cascades like every unmarked key, so one declaration above two packages is the usual way
|
|
184
|
+
* in - and the message says so when that is what happened, since the fix is different. */
|
|
185
|
+
_assertUniqueSelectors() {
|
|
186
|
+
const bySelector = new Map();
|
|
187
|
+
for (const pkg of this.packages) {
|
|
188
|
+
const clash = bySelector.get(pkg.selector);
|
|
189
|
+
if (clash) {
|
|
190
|
+
const cascaded = this._declaredSelectors.has(pkg) && this._declaredSelectors.has(clash);
|
|
191
|
+
throw new Error(`Two packages answer to the selector "${pkg.selector}":\n ${clash.dirname}\n ${pkg.dirname}\n` +
|
|
192
|
+
` A selector has to be unique - "[${pkg.selector}]" and \`--scope ${pkg.selector}\` ` +
|
|
193
|
+
`cannot mean two packages.` +
|
|
194
|
+
(cascaded
|
|
195
|
+
? `\n Both got it from one cascading "name" declaration above them; declare it in ` +
|
|
196
|
+
`each package's own ".rmanrc" instead.`
|
|
197
|
+
: ''));
|
|
198
|
+
}
|
|
199
|
+
bySelector.set(pkg.selector, pkg);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Cascades every package's config into `Package.rawConfig`, now that the selectors are known.
|
|
203
|
+
* Still raw - `Repository` is what interpolates it into `Package.config`. */
|
|
204
|
+
async _resolveConfigs() {
|
|
205
|
+
for (const pkg of [this.rootPackage, ...this.packages]) {
|
|
206
|
+
pkg.rawConfig = await this._cascade(pkg, pkg.selector);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Every directory from the root down to `pkg`, layered into one config.
|
|
211
|
+
*
|
|
212
|
+
* Two layers per level: the level's unmarked keys first, then the `"[selector]"` blocks that
|
|
213
|
+
* speak for this package, in the order they were written - later wins, as `overrides` does in
|
|
214
|
+
* eslint and prettier.
|
|
215
|
+
*
|
|
216
|
+
* @param selector what the blocks are matched against; `undefined` skips them, except `"[/]"` for
|
|
217
|
+
* the root, which is structural and needs no selector.
|
|
218
|
+
*/
|
|
219
|
+
/* **Unmarked first because it is the widest thing the level says** - it configures that directory
|
|
220
|
+
* *and every package under it*, so it is the level's floor. A selector block written *above* the
|
|
221
|
+
* plain keys still beats them: unmarked is not a fourth selector, it is the layer that also feeds
|
|
222
|
+
* the directories below, and making that depend on key order in the file would be absurd. */
|
|
223
|
+
async _cascade(pkg, selector) {
|
|
224
|
+
const result = {};
|
|
225
|
+
const audience = {
|
|
226
|
+
isRoot: pkg.dirname === this.rootDir,
|
|
227
|
+
selector,
|
|
228
|
+
platform: pkg.platform.name,
|
|
229
|
+
};
|
|
230
|
+
for (const dir of this._dirChain(pkg.dirname)) {
|
|
231
|
+
const level = await this._readLevel(dir);
|
|
232
|
+
mergeConfig(result, this._stripSelectors(level.config));
|
|
233
|
+
for (const block of this._matchingSelectors(level.config, audience)) {
|
|
234
|
+
mergeConfig(result, block);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
/** One directory's own config, read once and cached. Plugins the level names are loaded here and
|
|
240
|
+
* accumulated into `plugins`. */
|
|
241
|
+
/* The cache is what makes the two cascade passes affordable - twenty packages sharing a root
|
|
242
|
+
* means that root is read once rather than forty times. It is also what keeps a level's plugins
|
|
243
|
+
* from being loaded a second time. */
|
|
244
|
+
async _readLevel(dirname) {
|
|
245
|
+
const resolved = path.resolve(dirname);
|
|
246
|
+
const hit = this._levels.get(resolved);
|
|
247
|
+
if (hit)
|
|
248
|
+
return hit;
|
|
249
|
+
const result = await this._reader().resolve(resolved, {
|
|
250
|
+
plugins: this.plugins,
|
|
251
|
+
platforms: this.platforms,
|
|
252
|
+
/** **The root alone**, because that is the level they belong to: everything below it inherits
|
|
253
|
+
* whatever the root settled on, and laying them down again there would put a preset's
|
|
254
|
+
* technology ahead of one an intermediate directory declared. */
|
|
255
|
+
presets: resolved === this.rootDir ? (this.options.presets ?? DEFAULT_PRESETS) : undefined,
|
|
256
|
+
});
|
|
257
|
+
/** Whatever this level added, in the order it was loaded - `plugins` appends, never replaces,
|
|
258
|
+
* so a level naming one never means "and drop the ones above me". The technologies a plugin
|
|
259
|
+
* carries are already in `result.platforms`; the reader puts them there, so this walk and the
|
|
260
|
+
* reader cannot end up with two orders. */
|
|
261
|
+
for (const plugin of result.plugins) {
|
|
262
|
+
if (!this.plugins.some(p => p.name === plugin.name))
|
|
263
|
+
this.plugins.push(plugin);
|
|
264
|
+
}
|
|
265
|
+
for (const platform of result.platforms)
|
|
266
|
+
this._addPlatform(platform);
|
|
267
|
+
this._levels.set(resolved, result);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
/** Appends a technology unless one already answers to its name. */
|
|
271
|
+
_addPlatform(platform) {
|
|
272
|
+
if (!this.platforms.some(p => p.name === platform.name))
|
|
273
|
+
this.platforms.push(platform);
|
|
274
|
+
}
|
|
275
|
+
/** The reader levels are read with - `options.reader`, or a default one. */
|
|
276
|
+
_reader() {
|
|
277
|
+
return (this._configReader ??= this.options.reader ?? new ConfigReader());
|
|
278
|
+
}
|
|
279
|
+
/** Root, then every directory down to `targetDir`, inclusive. A target outside the root is the
|
|
280
|
+
* root alone. */
|
|
281
|
+
_dirChain(targetDir) {
|
|
282
|
+
const rel = path.relative(this.rootDir, targetDir);
|
|
283
|
+
if (!rel || rel === '.' || rel.startsWith('..'))
|
|
284
|
+
return [this.rootDir];
|
|
285
|
+
const dirs = [this.rootDir];
|
|
286
|
+
let dir = this.rootDir;
|
|
287
|
+
for (const segment of rel.split(path.sep)) {
|
|
288
|
+
dir = path.join(dir, segment);
|
|
289
|
+
dirs.push(dir);
|
|
290
|
+
}
|
|
291
|
+
return dirs;
|
|
292
|
+
}
|
|
293
|
+
/** A level's own settings - everything that is not a `"[...]"` block. */
|
|
294
|
+
_stripSelectors(config) {
|
|
295
|
+
const result = {};
|
|
296
|
+
for (const [key, value] of Object.entries(config))
|
|
297
|
+
if (!ConfigReader.isSelectorKey(key))
|
|
298
|
+
result[key] = value;
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The blocks in `config` that speak for this package, in declaration order.
|
|
303
|
+
*
|
|
304
|
+
* `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
|
|
305
|
+
* root.
|
|
306
|
+
*/
|
|
307
|
+
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
308
|
+
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
309
|
+
* setting to a root with no build directory to apply it to. A root block needs no selector, which
|
|
310
|
+
* is what makes `/` structural. */
|
|
311
|
+
_matchingSelectors(config, audience) {
|
|
312
|
+
const matches = [];
|
|
313
|
+
for (const [key, value] of Object.entries(config)) {
|
|
314
|
+
if (!ConfigReader.isSelectorKey(key) || !value || typeof value !== 'object')
|
|
315
|
+
continue;
|
|
316
|
+
const { scope, test } = ConfigReader.parseSelector(key);
|
|
317
|
+
if (!this._speaksFor(scope, test, audience))
|
|
318
|
+
continue;
|
|
319
|
+
matches.push(value);
|
|
320
|
+
}
|
|
321
|
+
return matches;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Whether one selector block speaks for this package.
|
|
325
|
+
*
|
|
326
|
+
* `"[/]"` asks only whether this *is* the root. Everything else matches packages **below** the
|
|
327
|
+
* root and never the root itself, which is addressed structurally or not at all.
|
|
328
|
+
*/
|
|
329
|
+
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
330
|
+
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
331
|
+
* setting to a root with no build directory to apply it to. A platform block is held to the same
|
|
332
|
+
* line for consistency - the root has a platform, but it also has `"[/]"`.
|
|
333
|
+
*
|
|
334
|
+
* A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
|
|
335
|
+
* platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
|
|
336
|
+
* empty selector quietly match it. */
|
|
337
|
+
_speaksFor(scope, test, audience) {
|
|
338
|
+
if (scope === 'root')
|
|
339
|
+
return audience.isRoot;
|
|
340
|
+
if (audience.isRoot)
|
|
341
|
+
return false;
|
|
342
|
+
if (scope === 'platform')
|
|
343
|
+
return test(audience.platform);
|
|
344
|
+
return audience.selector !== undefined && test(audience.selector);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The platform that claims a directory - the one its config names, or the first loaded platform
|
|
348
|
+
* whose manifest provider recognizes it. `basePlatform` when none does, so a caller needs no
|
|
349
|
+
* guard; a *declared* platform no plugin provides is an error.
|
|
350
|
+
*/
|
|
351
|
+
/* `ConfigReader` already decided the name, from a declaration or by asking the loaded plugins, so
|
|
352
|
+
* this only has to find the platform that answers to it. */
|
|
353
|
+
_platformFor(dirname, config) {
|
|
354
|
+
const declared = this._declaredPlatformName(config);
|
|
355
|
+
if (declared === undefined) {
|
|
356
|
+
return this.platforms.find(p => p.manifestProvider.read(dirname)) ?? basePlatform;
|
|
357
|
+
}
|
|
358
|
+
const named = this.platforms.find(p => p.name === declared);
|
|
359
|
+
if (!named) {
|
|
360
|
+
const have = this.platforms.map(p => p.name).filter(Boolean);
|
|
361
|
+
throw new Error(`"platform" names "${declared}" (${this._originOf(config, 'platform')}), which is not a ` +
|
|
362
|
+
`platform this repository has. ` +
|
|
363
|
+
`${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
|
|
364
|
+
`Contribute it with "platforms", or inherit it with "extends".`);
|
|
365
|
+
}
|
|
366
|
+
/** **A declaration is held to the directory it names.** The failure it replaces is invisible:
|
|
367
|
+
* the manifest reads as nothing, so the package is named after its directory at `0.0.0` and
|
|
368
|
+
* the repository looks fine. The message names the file the platform looked for, because that
|
|
369
|
+
* is the thing to go and check. */
|
|
370
|
+
if (!named.manifestProvider.read(dirname)) {
|
|
371
|
+
throw new Error(`"${dirname}" declares \`platform: '${declared}'\` (${this._originOf(config, 'platform')}), ` +
|
|
372
|
+
`and "${declared}" does not recognize it - it looks for "${named.manifestProvider.fileName}".`);
|
|
373
|
+
}
|
|
374
|
+
return named;
|
|
375
|
+
}
|
|
376
|
+
/** A platform named unmarked or under `"[/]"` - both are the directory saying what it is. */
|
|
377
|
+
/* **An expression is refused rather than read as a literal.** `platform` is consulted before any
|
|
378
|
+
* package exists - it is what decides what a package *is* - so there is no `pkg` for an
|
|
379
|
+
* expression to be about, and interpolation runs long afterwards. Passed through, a `${{ }}` here
|
|
380
|
+
* reached the lookup as its raw text and failed as an unknown platform name, which sends the
|
|
381
|
+
* reader to check their `platforms` instead of the line they wrote.
|
|
382
|
+
*
|
|
383
|
+
* A value that is not a usable string is refused too, rather than read as "declared nothing": the
|
|
384
|
+
* two are opposite answers, and the silent one leaves a directory claimed by whichever technology
|
|
385
|
+
* happened to recognize it. */
|
|
386
|
+
_declaredPlatformName(config) {
|
|
387
|
+
const root = config['[/]'];
|
|
388
|
+
const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
|
|
389
|
+
if (declared === undefined)
|
|
390
|
+
return undefined;
|
|
391
|
+
const where = this._originOf(config, 'platform');
|
|
392
|
+
if (typeof declared !== 'string' || !declared.trim()) {
|
|
393
|
+
throw new Error(`"platform" takes a platform's name - ${where} gave ${typeof declared}.`);
|
|
394
|
+
}
|
|
395
|
+
if (declared.includes('${{')) {
|
|
396
|
+
throw new Error(`"platform" cannot be an expression (${where}) - it is read while the packages are still ` +
|
|
397
|
+
`being found, so there is no package for one to be about. Write the name, and use a ` +
|
|
398
|
+
`package's own ".rmanrc" where the answer differs.`);
|
|
399
|
+
}
|
|
400
|
+
return declared.trim();
|
|
401
|
+
}
|
|
402
|
+
/** Which file a key came from, for an error that can be acted on. */
|
|
403
|
+
/* A config is merged from a directory's own forms, an `extends` base, every `"[selector]"` block
|
|
404
|
+
* and one layer per directory before anything reads it, so the key alone leaves the reader
|
|
405
|
+
* searching all of them. `mergeConfig` records one file per key under `ORIGINS`. */
|
|
406
|
+
_originOf(config, key) {
|
|
407
|
+
const origins = config[ORIGINS];
|
|
408
|
+
return origins?.[key] ? `in "${origins[key]}"` : `the "${key}" key`;
|
|
409
|
+
}
|
|
410
|
+
/** Where a `Package` is made, and the only place - override it to build a subclass of `Package`
|
|
411
|
+
* and keep the rest of the walk. */
|
|
412
|
+
_createPackage(dirname, platform) {
|
|
413
|
+
return new Package(dirname, this.options.app, platform);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* How a repository is laid out, and who decides.
|
|
418
|
+
*
|
|
419
|
+
* A namespace for the same reason `Manifest` is one: it is what a plugin can *augment*
|
|
420
|
+
* (`declare module 'rman' { namespace Workspace { ... } }`), so whatever this seam grows later
|
|
421
|
+
* arrives as a member here rather than as another top-level export. It merges with the class above,
|
|
422
|
+
* so `Workspace.walk` and `Workspace.create` are reached the same way.
|
|
423
|
+
*/
|
|
424
|
+
(function (Workspace) {
|
|
425
|
+
/**
|
|
426
|
+
* **The walk**: descend from `rootDir`, asking each directory's own technology where its children
|
|
427
|
+
* are, and repeating for each answer.
|
|
428
|
+
*
|
|
429
|
+
* One step, applied recursively:
|
|
430
|
+
*
|
|
431
|
+
* 1. take the directory's **declared** platform if it has one, and otherwise the first registered
|
|
432
|
+
* platform whose manifest provider recognizes it (`app.platformFor`, `basePlatform` if none);
|
|
433
|
+
* 2. ask **that** platform's `getWorkspace` for the directories below it holding packages;
|
|
434
|
+
* 3. do the same for each of them.
|
|
435
|
+
*
|
|
436
|
+
* **A declaration wins, and is then held to it.** A platform named for a directory it does not
|
|
437
|
+
* recognize is a statement that is simply untrue, and the failure it would otherwise become is
|
|
438
|
+
* invisible: the manifest reads as nothing, so the package is named after its directory at
|
|
439
|
+
* `0.0.0` and the repository looks like it works. The error names the directory, the platform and
|
|
440
|
+
* the file that platform looked for.
|
|
441
|
+
*
|
|
442
|
+
* The root node always exists - a repository is a package whatever its technology - so this never
|
|
443
|
+
* returns `undefined`. A repository nobody recognizes is a root with no children, which is the
|
|
444
|
+
* single-package answer arrived at rather than guessed.
|
|
445
|
+
*
|
|
446
|
+
* **A directory is visited once.** A provider may legitimately name a directory another one
|
|
447
|
+
* already claimed (two globs overlapping, a symlinked package), and a provider naming an ancestor
|
|
448
|
+
* would otherwise recurse forever. First visit wins, so a package sits where it was first found.
|
|
449
|
+
*
|
|
450
|
+
* `deep` bounds the descent for the same reason `findRoot` bounds its climb: a provider computing
|
|
451
|
+
* paths rather than reading them can produce a chain that never ends, and a guessed depth is
|
|
452
|
+
* better than a hang with nothing printed.
|
|
453
|
+
*/
|
|
454
|
+
async function walk(app, rootDir, options) {
|
|
455
|
+
const visited = new Set();
|
|
456
|
+
const descend = async (dir, remaining) => {
|
|
457
|
+
const resolved = path.resolve(dir);
|
|
458
|
+
visited.add(resolved);
|
|
459
|
+
const declared = await options?.declared?.(resolved);
|
|
460
|
+
if (declared && !declared.manifestProvider.read(resolved)) {
|
|
461
|
+
throw new Error(`"${resolved}" declares \`platform: '${declared.name}'\`, and that platform does not ` +
|
|
462
|
+
`recognize it - it looks for "${declared.manifestProvider.fileName}". Either the ` +
|
|
463
|
+
`directory is not a ${declared.name} package, or the declaration belongs one level down.`);
|
|
464
|
+
}
|
|
465
|
+
const platform = declared ?? app.platformFor(resolved);
|
|
466
|
+
const node = { dirname: resolved, platform, children: [] };
|
|
467
|
+
if (remaining <= 0)
|
|
468
|
+
return node;
|
|
469
|
+
/**
|
|
470
|
+
* **Its own platform is asked, and nobody else.** A platform that did not claim the directory
|
|
471
|
+
* has no standing to say what is under it - that was the old first-wins rule, one level up.
|
|
472
|
+
* `basePlatform` has no `getWorkspace`, so a directory no technology claimed has no children,
|
|
473
|
+
* which is the documented behaviour of a repository naming no plugin.
|
|
474
|
+
*/
|
|
475
|
+
for (const child of platform.getWorkspace?.(resolved) ?? []) {
|
|
476
|
+
const childDir = path.resolve(child);
|
|
477
|
+
if (visited.has(childDir))
|
|
478
|
+
continue;
|
|
479
|
+
node.children.push(await descend(childDir, remaining - 1));
|
|
480
|
+
}
|
|
481
|
+
return node;
|
|
482
|
+
};
|
|
483
|
+
return descend(rootDir, options?.deep ?? 10);
|
|
484
|
+
}
|
|
485
|
+
Workspace.walk = walk;
|
|
486
|
+
/** Every node below `node`, depth-first, excluding `node` itself - the flat list `Repository`
|
|
487
|
+
* reports as its packages, since the root is not one of its own members. */
|
|
488
|
+
function flatten(node) {
|
|
489
|
+
return node.children.flatMap(child => [child, ...flatten(child)]);
|
|
490
|
+
}
|
|
491
|
+
Workspace.flatten = flatten;
|
|
492
|
+
/**
|
|
493
|
+
* Where the repository starts, decided **without knowing anything about any ecosystem** - it has
|
|
494
|
+
* to be, because the plugins that do know are named in the config file this walk is looking for.
|
|
495
|
+
*
|
|
496
|
+
* Walking up from `from`, stopping after a directory holding `.git`, the root is:
|
|
497
|
+
*
|
|
498
|
+
* 1. the **outermost** directory in that chain holding an `.rmanrc*` - outermost, because a
|
|
499
|
+
* *package* may have its own `.rmanrc` (that is a supported thing), and from inside such a
|
|
500
|
+
* package the nearest one is the package's, not the repository's;
|
|
501
|
+
* 2. otherwise the `.git` directory itself, the ordinary meaning of "repository root";
|
|
502
|
+
* 3. otherwise `from`, which is all that is left to go on.
|
|
503
|
+
*
|
|
504
|
+
* **What "outermost" costs**, since the two cases genuinely conflict and only one can win: a
|
|
505
|
+
* self-contained project nested inside a larger git repository *and sharing its `.git`* resolves
|
|
506
|
+
* to the outer root, not to itself (measured). That is the same repository by any definition git
|
|
507
|
+
* recognizes, so it is the defensible answer - and a nested project with a `.git` of its own is
|
|
508
|
+
* found correctly, because the walk stops there before the outer `.rmanrc` is ever seen (also
|
|
509
|
+
* measured). A per-package `.rmanrc` is the common case and it is the one served.
|
|
510
|
+
*/
|
|
511
|
+
function findRoot(from, deep = 10) {
|
|
512
|
+
const chain = [];
|
|
513
|
+
let dir = path.resolve(from);
|
|
514
|
+
let remaining = deep;
|
|
515
|
+
while (remaining-- >= 0 && fs.existsSync(dir)) {
|
|
516
|
+
chain.push(dir);
|
|
517
|
+
if (fs.existsSync(path.join(dir, '.git')))
|
|
518
|
+
break;
|
|
519
|
+
const parent = path.resolve(dir, '..');
|
|
520
|
+
if (parent === dir)
|
|
521
|
+
break;
|
|
522
|
+
dir = parent;
|
|
523
|
+
}
|
|
524
|
+
const withConfig = chain.filter(hasRmanConfig);
|
|
525
|
+
if (withConfig.length)
|
|
526
|
+
return withConfig[withConfig.length - 1];
|
|
527
|
+
const gitRoot = chain.find(d => fs.existsSync(path.join(d, '.git')));
|
|
528
|
+
return gitRoot ?? path.resolve(from);
|
|
529
|
+
}
|
|
530
|
+
Workspace.findRoot = findRoot;
|
|
531
|
+
/** Every file form a `.rmanrc` comes in - `package.json#rman` is deliberately *not* one of them
|
|
532
|
+
* here: it would make the root question npm-shaped again. */
|
|
533
|
+
const CONFIG_FILES = ['.rmanrc', '.rmanrc.yml', '.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
|
|
534
|
+
function hasRmanConfig(dir) {
|
|
535
|
+
return CONFIG_FILES.some(name => fs.existsSync(path.join(dir, name)));
|
|
536
|
+
}
|
|
537
|
+
})(Workspace || (Workspace = {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CommandModule } from 'yargs';
|
|
2
|
+
import type { RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turns a command's **declaration** into the yargs registration it describes.
|
|
5
|
+
*
|
|
6
|
+
* This is the one place that knows how the two relate, which is the point of declaring commands
|
|
7
|
+
* instead of building them: a command says what it has, and only this function says what yargs is
|
|
8
|
+
* told. Everything a `builder` used to do by hand - options, positionals, examples, parser
|
|
9
|
+
* switches - arrives as data now, so a typo in any of it is a compile error at the command rather
|
|
10
|
+
* than a flag that silently never existed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toYargsCommand(meta: RmanConfig.CommandMetadata): CommandModule;
|
|
13
|
+
/** A command's own name - the first word of its `command` string, before any positional. */
|
|
14
|
+
export declare function commandName(command: string): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a command's **declaration** into the yargs registration it describes.
|
|
3
|
+
*
|
|
4
|
+
* This is the one place that knows how the two relate, which is the point of declaring commands
|
|
5
|
+
* instead of building them: a command says what it has, and only this function says what yargs is
|
|
6
|
+
* told. Everything a `builder` used to do by hand - options, positionals, examples, parser
|
|
7
|
+
* switches - arrives as data now, so a typo in any of it is a compile error at the command rather
|
|
8
|
+
* than a flag that silently never existed.
|
|
9
|
+
*/
|
|
10
|
+
export function toYargsCommand(meta) {
|
|
11
|
+
return {
|
|
12
|
+
command: meta.command,
|
|
13
|
+
aliases: meta.aliases,
|
|
14
|
+
describe: meta.describe,
|
|
15
|
+
configKeys: configKeysOf(meta),
|
|
16
|
+
builder: (cmd) => {
|
|
17
|
+
if (meta.parserConfiguration)
|
|
18
|
+
cmd.parserConfiguration(meta.parserConfiguration);
|
|
19
|
+
for (const [name, spec] of Object.entries(meta.positionals ?? {}))
|
|
20
|
+
cmd.positional(name, spec);
|
|
21
|
+
for (const [key, option] of Object.entries(meta.config ?? {})) {
|
|
22
|
+
/**
|
|
23
|
+
* **`target: 'config'` means "a `.rmanrc` key, and not a flag"** - `githubRelease.draft` is
|
|
24
|
+
* the first of those. Skipping it here is what makes the field mean something rather than
|
|
25
|
+
* being documentation.
|
|
26
|
+
*/
|
|
27
|
+
if (option.target === 'config')
|
|
28
|
+
continue;
|
|
29
|
+
/** `target` and `cliName` are rman's, not yargs' - dropped rather than passed through. */
|
|
30
|
+
const { cliName, ...rest } = option;
|
|
31
|
+
delete rest.target;
|
|
32
|
+
/**
|
|
33
|
+
* Registered under `cliName` when the flag and the config key are spelled differently
|
|
34
|
+
* (`--ignore-dirty` for `ignoreDirty`). yargs expands the hyphenated form back to camelCase
|
|
35
|
+
* in argv, so the handler reads the key it declared either way.
|
|
36
|
+
*/
|
|
37
|
+
cmd.option(cliName ?? key, rest);
|
|
38
|
+
}
|
|
39
|
+
for (const example of meta.examples ?? [])
|
|
40
|
+
cmd.example(example.command, example.description ?? '');
|
|
41
|
+
return cmd;
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Cast because the two descriptions of argv disagree on purpose: yargs types it as an index
|
|
45
|
+
* signature of `unknown`, and a command annotates it with the options it actually declared. The
|
|
46
|
+
* narrower one is the useful one, and this is the single place the widening is admitted -
|
|
47
|
+
* previously it was an `as` per option read, in every handler.
|
|
48
|
+
*/
|
|
49
|
+
handler: withCommandPlatforms(meta),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Carries `CommandMetadata.platforms` into argv, where `readPackageFilterOptions` picks it up.
|
|
54
|
+
*
|
|
55
|
+
* Through argv rather than a parameter because `filterPackages` is called by ten *services*, none
|
|
56
|
+
* of which knows which command is running - `options` is all they see. One wrap here reaches every
|
|
57
|
+
* one of them and changes none.
|
|
58
|
+
*/
|
|
59
|
+
function withCommandPlatforms(meta) {
|
|
60
|
+
const handler = meta.handler;
|
|
61
|
+
if (!meta.platforms?.length)
|
|
62
|
+
return handler;
|
|
63
|
+
return (args) => {
|
|
64
|
+
args.commandPlatforms = meta.platforms;
|
|
65
|
+
return handler(args);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** A command's own name - the first word of its `command` string, before any positional. */
|
|
69
|
+
export function commandName(command) {
|
|
70
|
+
return command.split(/\s+/)[0];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Which `.rmanrc` keys `--config` shows for a command: the ones it declared reading, plus **its own
|
|
74
|
+
* key when it has one**.
|
|
75
|
+
*
|
|
76
|
+
* `configKeys` is the read-only list on purpose - a command should not have to repeat the key it
|
|
77
|
+
* already owns, and `version` listing `'version'` beside `'changelog'` said nothing about which of
|
|
78
|
+
* the two it defines. Measured before this existed: `rman version --config` reported
|
|
79
|
+
* `changelog, group, allowBranch, ignoreBranch` and left out `version` itself.
|
|
80
|
+
*
|
|
81
|
+
* **Own key only when the command actually declares a config option.** `build` and `test` own
|
|
82
|
+
* nothing - their settings live in `run.build`/`run.test`, which they merely read - so prepending
|
|
83
|
+
* their names would print an empty `build: {}` that reads as "nothing is configured" about a key
|
|
84
|
+
* that does not exist. A `configKeys` given as a *function* is left alone entirely: `run` computes
|
|
85
|
+
* `run.<script>` from argv, which is narrower than its own key and deliberately so.
|
|
86
|
+
*/
|
|
87
|
+
function configKeysOf(meta) {
|
|
88
|
+
if (typeof meta.configKeys === 'function')
|
|
89
|
+
return meta.configKeys;
|
|
90
|
+
const ownsConfig = Object.values(meta.config ?? {}).some(o => o.target !== 'cli');
|
|
91
|
+
const own = ownsConfig ? [meta.configKey ?? commandName(meta.command)] : [];
|
|
92
|
+
const keys = [...own, ...(meta.configKeys ?? [])];
|
|
93
|
+
return keys.length ? keys : undefined;
|
|
94
|
+
}
|