rman 2.0.0-beta.2 → 2.0.0-beta.4
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 +2 -12
- package/{plugins/node/augmentation/rman.augmentation.d.ts → builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts} +4 -5
- package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.js +1 -1
- package/{plugins → builtins/platforms}/node/commands/ci.command.d.ts +5 -3
- package/{plugins → builtins/platforms}/node/commands/ci.command.js +6 -5
- package/{plugins → builtins/platforms}/node/commands/clean.command.d.ts +6 -4
- package/{plugins → builtins/platforms}/node/commands/clean.command.js +6 -5
- package/builtins/platforms/node/index.d.ts +8 -0
- package/builtins/platforms/node/index.js +8 -0
- package/{plugins → builtins/platforms}/node/node-config.interface.d.ts +1 -2
- package/{plugins → builtins/platforms}/node/node-manifest.provider.d.ts +3 -3
- package/{plugins → builtins/platforms}/node/node-manifest.provider.js +3 -3
- package/{plugins → builtins/platforms}/node/node.platform.d.ts +3 -3
- package/{plugins → builtins/platforms}/node/services/ci.service.d.ts +3 -3
- package/{plugins → builtins/platforms}/node/services/ci.service.js +4 -4
- package/{plugins → builtins/platforms}/node/services/clean.service.d.ts +3 -3
- package/{plugins → builtins/platforms}/node/services/clean.service.js +3 -3
- package/{plugins → builtins/platforms}/node/services/publish.service.d.ts +4 -4
- package/{plugins → builtins/platforms}/node/services/publish.service.js +13 -7
- package/{plugins → builtins/platforms}/node/services/version-plan.service.d.ts +3 -3
- package/{plugins → builtins/platforms}/node/services/version-plan.service.js +2 -2
- 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 +6 -6
- package/{services → builtins/publish-targets/docker}/docker-publish.service.js +9 -9
- package/{targets → builtins/publish-targets/docker}/docker.target.d.ts +6 -5
- package/{targets → builtins/publish-targets/docker}/docker.target.js +1 -1
- 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/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.d.ts +4 -4
- package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.js +4 -5
- package/cli.d.ts +3 -0
- package/cli.js +16 -7
- package/commands/changelog.command.d.ts +106 -2
- package/commands/changelog.command.js +40 -0
- package/commands/config.command.js +23 -5
- package/commands/list.command.js +1 -1
- package/commands/publish.command.d.ts +1 -1
- package/commands/publish.command.js +1 -1
- package/commands/run.command.js +1 -1
- package/commands/version.command.d.ts +16 -3
- package/commands/version.command.js +143 -9
- package/commands.d.ts +0 -1
- package/commands.js +0 -1
- package/constants.js +1 -1
- package/core/application.d.ts +10 -10
- package/core/application.js +28 -11
- package/core/{package.d.ts → classes/package.d.ts} +12 -4
- package/core/{package.js → classes/package.js} +17 -1
- package/core/{repository.d.ts → classes/repository.d.ts} +22 -36
- package/core/{repository.js → classes/repository.js} +112 -241
- package/core/{service.d.ts → classes/service.d.ts} +2 -2
- package/core/classes/workspace.d.ts +277 -0
- package/core/classes/workspace.js +585 -0
- package/core/command-builder.js +17 -1
- 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 +215 -0
- package/core/config/config-reader.js +851 -0
- package/core/{merge-config.js → config/merge-config.js} +4 -8
- package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +5 -5
- package/core/{manifest.d.ts → interfaces/manifest.d.ts} +3 -3
- package/core/{manifest.js → interfaces/manifest.js} +2 -2
- package/core/{plugin.d.ts → interfaces/plugin.d.ts} +44 -27
- package/core/{plugin.js → interfaces/plugin.js} +48 -10
- package/core/{publish-target.d.ts → interfaces/publish-target.d.ts} +24 -12
- package/core/{publish-target.js → interfaces/publish-target.js} +16 -3
- package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
- package/core/plugin-loader.d.ts +11 -51
- package/core/plugin-loader.js +21 -202
- package/index.d.ts +29 -28
- package/index.js +20 -46
- package/interfaces/config-scope.interface.d.ts +223 -0
- package/interfaces/config-scope.interface.js +1 -0
- package/interfaces/rman-config.interface.d.ts +100 -5
- package/interfaces/rman-config.interface.js +17 -0
- package/package.json +3 -2
- package/services/change-hash.service.d.ts +40 -6
- package/services/change-hash.service.js +90 -11
- package/services/changelog.service.d.ts +31 -3
- package/services/changelog.service.js +448 -65
- package/services/config-service.d.ts +8 -0
- package/services/config-service.js +3 -0
- package/services/exec.service.d.ts +2 -2
- package/services/exec.service.js +1 -1
- package/services/github-release.service.d.ts +3 -3
- package/services/github-release.service.js +3 -3
- package/services/import.service.d.ts +2 -2
- package/services/import.service.js +1 -1
- package/services/list.service.d.ts +9 -7
- package/services/list.service.js +2 -2
- package/services/run.service.d.ts +15 -11
- package/services/run.service.js +62 -18
- package/services/system-info.d.ts +1 -1
- package/services/version-plan.service.d.ts +8 -4
- package/services/version-plan.service.js +54 -21
- package/services/version.service.d.ts +5 -5
- package/services/version.service.js +7 -4
- package/services.d.ts +1 -1
- package/services.js +1 -1
- package/utils/branch-guard.d.ts +1 -1
- package/utils/color-yaml.d.ts +8 -0
- package/utils/color-yaml.js +86 -0
- package/utils/git.d.ts +26 -0
- package/utils/git.js +65 -0
- package/utils/logger.d.ts +1 -1
- package/utils/package-filter.d.ts +8 -1
- package/utils/package-filter.js +7 -0
- package/utils/printable-config.d.ts +8 -0
- package/utils/printable-config.js +45 -0
- 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-options.js +1 -1
- package/utils/version-group.d.ts +31 -0
- package/utils/version-group.js +43 -0
- package/commands/changed.command.d.ts +0 -80
- package/commands/changed.command.js +0 -36
- package/core/config.d.ts +0 -530
- package/core/config.js +0 -1123
- package/core/core-services.d.ts +0 -14
- package/core/core-services.js +0 -30
- package/core/core-targets.d.ts +0 -14
- package/core/core-targets.js +0 -16
- package/core/extends-config.d.ts +0 -35
- package/core/extends-config.js +0 -129
- package/core/load-config-module.d.ts +0 -28
- package/core/load-config-module.js +0 -42
- package/core/workspace.d.ts +0 -119
- package/core/workspace.js +0 -124
- package/plugins/builtins.d.ts +0 -44
- package/plugins/builtins.js +0 -33
- package/plugins/detect.d.ts +0 -78
- package/plugins/detect.js +0 -70
- package/plugins/node/index.d.ts +0 -29
- package/plugins/node/index.js +0 -40
- /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
- /package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.d.ts +0 -0
- /package/{plugins → builtins/platforms}/node/node-config.interface.js +0 -0
- /package/{plugins → builtins/platforms}/node/node.platform.js +0 -0
- /package/{plugins → builtins/platforms}/node/utils/workspace-range.d.ts +0 -0
- /package/{plugins → builtins/platforms}/node/utils/workspace-range.js +0 -0
- /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.d.ts +0 -0
- /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.js +0 -0
- /package/core/{registry.d.ts → classes/registry.d.ts} +0 -0
- /package/core/{registry.js → classes/registry.js} +0 -0
- /package/core/{service.js → classes/service.js} +0 -0
- /package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +0 -0
- /package/core/{version-scheme.js → classes/version-scheme.js} +0 -0
- /package/core/{merge-config.d.ts → config/merge-config.d.ts} +0 -0
- /package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +0 -0
- /package/core/{resolve-target.js → config/resolve-config-target.js} +0 -0
- /package/core/{custom-command.js → interfaces/custom-command.js} +0 -0
- /package/{plugins/node/augmentation/rman.augmentation.js → core/interfaces/run-step.js} +0 -0
|
@@ -0,0 +1,585 @@
|
|
|
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
|
+
/** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
|
|
294
|
+
* is dropped whole, and what is inside it is the caller's business. */
|
|
295
|
+
/* **The symbols travel with it, and leaving them behind was a measured loss.** `mergeConfig` reads
|
|
296
|
+
* a key's origin and its `value` chain off the *source* object under `ORIGINS` / `PREVIOUS_VALUES`,
|
|
297
|
+
* so a copy that carries only string keys arrives with neither. Measured on one repository before
|
|
298
|
+
* this line existed: a bad expression in an unmarked key reported `Invalid expression in "group"`
|
|
299
|
+
* with no file, while the identical expression inside a `"[*]"` block reported
|
|
300
|
+
* `... ("group" (.rmanrc.yml))` - because a selector block was merged as itself and only the
|
|
301
|
+
* unmarked layer came through here. Nesting would have spread that to the blocks too. */
|
|
302
|
+
_stripSelectors(config) {
|
|
303
|
+
const result = {};
|
|
304
|
+
for (const [key, value] of Object.entries(config))
|
|
305
|
+
if (!ConfigReader.isSelectorKey(key))
|
|
306
|
+
result[key] = value;
|
|
307
|
+
for (const symbol of Object.getOwnPropertySymbols(config)) {
|
|
308
|
+
Object.defineProperty(result, symbol, Object.getOwnPropertyDescriptor(config, symbol));
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* The layers in `config` that speak for this package, in the order they are merged.
|
|
314
|
+
*
|
|
315
|
+
* `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
|
|
316
|
+
* root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
|
|
317
|
+
* where its own audience and every audience it sits inside all match.
|
|
318
|
+
*/
|
|
319
|
+
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
320
|
+
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
321
|
+
* setting to a root with no build directory to apply it to. A root block needs no selector, which
|
|
322
|
+
* is what makes `/` structural.
|
|
323
|
+
*
|
|
324
|
+
* **Nesting is depth-first pre-order, which is the level's own rule applied again rather than a
|
|
325
|
+
* new one**: a block's own keys are its floor, then the blocks written inside it in declaration
|
|
326
|
+
* order, then the next block beside it. So the layer order for
|
|
327
|
+
* `"[platform:node]" { a, "[pkg-*]" { b } }, "[*]" { c }` is `a, b, c` - and the last word is
|
|
328
|
+
* still whatever was written last, *not* whatever is most specific. That is the same cost the
|
|
329
|
+
* dropped specificity ranking already documents (a catch-all written below a narrower block
|
|
330
|
+
* overrides it), and nesting neither adds to it nor asks the tiebreak question again: two blocks
|
|
331
|
+
* that both match are siblings whatever depth they sit at, and declaration order answers them.
|
|
332
|
+
*
|
|
333
|
+
* **A matching block is contributed *stripped*, not whole.** Merging it as it stands put its
|
|
334
|
+
* nested keys into the resolved config as literal `'[pkg-*]'` entries, which is precisely how the
|
|
335
|
+
* old no-op showed up in `rman config` looking as though it had worked.
|
|
336
|
+
*
|
|
337
|
+
* **A platform block speaks for the root too, and only a glob is held off it.** `"[platform:node]"`
|
|
338
|
+
* asks `pkg.platform.name` and the root is a package with a platform, so
|
|
339
|
+
* `"[platform:node]" > "[/]"` is *the root, when the root is a node package* and
|
|
340
|
+
* `"[platform:node]" > "[*]"` is *its packages* - which is the whole of a technology's shared
|
|
341
|
+
* config in one block, `vars` included.
|
|
342
|
+
*
|
|
343
|
+
* A narrower rule was tried first and measured wrong: let a chain reach the root only where it
|
|
344
|
+
* *names* `"[/]"`, keeping a plain `"[platform:node]"` off the root as before. It answers the pair
|
|
345
|
+
* correctly and still breaks the case it was written for - the nested `"[/]"` could not read the
|
|
346
|
+
* `vars` its parent declared (`vars is not defined`), because those are the parent's own keys.
|
|
347
|
+
* Narrowing an audience and hiding the enclosing block's settings from it are different things,
|
|
348
|
+
* and only the first is what nesting means.
|
|
349
|
+
*
|
|
350
|
+
* The cost, stated rather than hidden: a package-shaped setting written **directly** under a
|
|
351
|
+
* platform block now reaches the root as well - the trap that keeps globs off it. It is not the
|
|
352
|
+
* same trap, because neither half of that one applies here: `platform:node` is not a name and
|
|
353
|
+
* cannot match by accident, and it is not a catch-all. And with nesting the spelling for
|
|
354
|
+
* package-shaped settings is `"[platform:node]" > "[*]"`, which says so. */
|
|
355
|
+
_matchingSelectors(config, audience) {
|
|
356
|
+
const matches = [];
|
|
357
|
+
for (const [key, value] of Object.entries(config)) {
|
|
358
|
+
if (!ConfigReader.isSelectorKey(key) || !value || typeof value !== 'object')
|
|
359
|
+
continue;
|
|
360
|
+
const { scope, test } = ConfigReader.parseSelector(key);
|
|
361
|
+
if (!this._speaksFor(scope, test, audience))
|
|
362
|
+
continue;
|
|
363
|
+
const block = value;
|
|
364
|
+
matches.push(this._stripSelectors(block));
|
|
365
|
+
matches.push(...this._matchingSelectors(block, audience));
|
|
366
|
+
}
|
|
367
|
+
return matches;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Whether one selector block speaks for this package.
|
|
371
|
+
*
|
|
372
|
+
* `"[/]"` asks only whether this *is* the root. Everything else matches packages **below** the
|
|
373
|
+
* root and never the root itself, which is addressed structurally or not at all.
|
|
374
|
+
*/
|
|
375
|
+
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
376
|
+
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
377
|
+
* setting to a root with no build directory to apply it to. Neither trap exists for a platform
|
|
378
|
+
* block - `platform:node` is not a name and cannot match by accident, and it is not a catch-all -
|
|
379
|
+
* so **a platform link answers about the root like any other package**, and what keeps a plain
|
|
380
|
+
* `"[platform:node]"` off the root is `_matchingSelectors`' chain rule rather than a refusal here.
|
|
381
|
+
* Asking `isRoot` first was what made `"[platform:node]" > "[/]"` unanswerable: the pair says *the
|
|
382
|
+
* root, when it is a node package*, and the short-circuit never let the platform question be put.
|
|
383
|
+
*
|
|
384
|
+
* A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
|
|
385
|
+
* platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
|
|
386
|
+
* empty selector quietly match it. */
|
|
387
|
+
_speaksFor(scope, test, audience) {
|
|
388
|
+
if (scope === 'root')
|
|
389
|
+
return audience.isRoot;
|
|
390
|
+
if (scope === 'platform')
|
|
391
|
+
return test(audience.platform);
|
|
392
|
+
return !audience.isRoot && audience.selector !== undefined && test(audience.selector);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The platform that claims a directory - the one its config names, or the first loaded platform
|
|
396
|
+
* whose manifest provider recognizes it. `basePlatform` when none does, so a caller needs no
|
|
397
|
+
* guard; a *declared* platform no plugin provides is an error.
|
|
398
|
+
*/
|
|
399
|
+
/* `ConfigReader` already decided the name, from a declaration or by asking the loaded plugins, so
|
|
400
|
+
* this only has to find the platform that answers to it. */
|
|
401
|
+
_platformFor(dirname, config) {
|
|
402
|
+
const declared = this._declaredPlatformName(config);
|
|
403
|
+
if (declared === undefined) {
|
|
404
|
+
return this.platforms.find(p => p.manifestProvider.read(dirname)) ?? basePlatform;
|
|
405
|
+
}
|
|
406
|
+
const named = this.platforms.find(p => p.name === declared);
|
|
407
|
+
if (!named) {
|
|
408
|
+
const have = this.platforms.map(p => p.name).filter(Boolean);
|
|
409
|
+
throw new Error(`"platform" names "${declared}" (${this._originOf(config, 'platform')}), which is not a ` +
|
|
410
|
+
`platform this repository has. ` +
|
|
411
|
+
`${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
|
|
412
|
+
`Contribute it with "platforms", or inherit it with "extends".`);
|
|
413
|
+
}
|
|
414
|
+
/** **A declaration is held to the directory it names.** The failure it replaces is invisible:
|
|
415
|
+
* the manifest reads as nothing, so the package is named after its directory at `0.0.0` and
|
|
416
|
+
* the repository looks fine. The message names the file the platform looked for, because that
|
|
417
|
+
* is the thing to go and check. */
|
|
418
|
+
if (!named.manifestProvider.read(dirname)) {
|
|
419
|
+
throw new Error(`"${dirname}" declares \`platform: '${declared}'\` (${this._originOf(config, 'platform')}), ` +
|
|
420
|
+
`and "${declared}" does not recognize it - it looks for "${named.manifestProvider.fileName}".`);
|
|
421
|
+
}
|
|
422
|
+
return named;
|
|
423
|
+
}
|
|
424
|
+
/** A platform named unmarked or under `"[/]"` - both are the directory saying what it is. */
|
|
425
|
+
/* **An expression is refused rather than read as a literal.** `platform` is consulted before any
|
|
426
|
+
* package exists - it is what decides what a package *is* - so there is no `pkg` for an
|
|
427
|
+
* expression to be about, and interpolation runs long afterwards. Passed through, a `${{ }}` here
|
|
428
|
+
* reached the lookup as its raw text and failed as an unknown platform name, which sends the
|
|
429
|
+
* reader to check their `platforms` instead of the line they wrote.
|
|
430
|
+
*
|
|
431
|
+
* A value that is not a usable string is refused too, rather than read as "declared nothing": the
|
|
432
|
+
* two are opposite answers, and the silent one leaves a directory claimed by whichever technology
|
|
433
|
+
* happened to recognize it. */
|
|
434
|
+
_declaredPlatformName(config) {
|
|
435
|
+
const root = config['[/]'];
|
|
436
|
+
const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
|
|
437
|
+
if (declared === undefined)
|
|
438
|
+
return undefined;
|
|
439
|
+
const where = this._originOf(config, 'platform');
|
|
440
|
+
if (typeof declared !== 'string' || !declared.trim()) {
|
|
441
|
+
throw new Error(`"platform" takes a platform's name - ${where} gave ${typeof declared}.`);
|
|
442
|
+
}
|
|
443
|
+
if (declared.includes('${{')) {
|
|
444
|
+
throw new Error(`"platform" cannot be an expression (${where}) - it is read while the packages are still ` +
|
|
445
|
+
`being found, so there is no package for one to be about. Write the name, and use a ` +
|
|
446
|
+
`package's own ".rmanrc" where the answer differs.`);
|
|
447
|
+
}
|
|
448
|
+
return declared.trim();
|
|
449
|
+
}
|
|
450
|
+
/** Which file a key came from, for an error that can be acted on. */
|
|
451
|
+
/* A config is merged from a directory's own forms, an `extends` base, every `"[selector]"` block
|
|
452
|
+
* and one layer per directory before anything reads it, so the key alone leaves the reader
|
|
453
|
+
* searching all of them. `mergeConfig` records one file per key under `ORIGINS`. */
|
|
454
|
+
_originOf(config, key) {
|
|
455
|
+
const origins = config[ORIGINS];
|
|
456
|
+
return origins?.[key] ? `in "${origins[key]}"` : `the "${key}" key`;
|
|
457
|
+
}
|
|
458
|
+
/** Where a `Package` is made, and the only place - override it to build a subclass of `Package`
|
|
459
|
+
* and keep the rest of the walk. */
|
|
460
|
+
_createPackage(dirname, platform) {
|
|
461
|
+
return new Package(dirname, this.options.app, platform);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* How a repository is laid out, and who decides.
|
|
466
|
+
*
|
|
467
|
+
* A namespace for the same reason `Manifest` is one: it is what a plugin can *augment*
|
|
468
|
+
* (`declare module 'rman' { namespace Workspace { ... } }`), so whatever this seam grows later
|
|
469
|
+
* arrives as a member here rather than as another top-level export. It merges with the class above,
|
|
470
|
+
* so `Workspace.walk` and `Workspace.create` are reached the same way.
|
|
471
|
+
*/
|
|
472
|
+
(function (Workspace) {
|
|
473
|
+
/**
|
|
474
|
+
* **The walk**: descend from `rootDir`, asking each directory's own technology where its children
|
|
475
|
+
* are, and repeating for each answer.
|
|
476
|
+
*
|
|
477
|
+
* One step, applied recursively:
|
|
478
|
+
*
|
|
479
|
+
* 1. take the directory's **declared** platform if it has one, and otherwise the first registered
|
|
480
|
+
* platform whose manifest provider recognizes it (`app.platformFor`, `basePlatform` if none);
|
|
481
|
+
* 2. ask **that** platform's `getWorkspace` for the directories below it holding packages;
|
|
482
|
+
* 3. do the same for each of them.
|
|
483
|
+
*
|
|
484
|
+
* **A declaration wins, and is then held to it.** A platform named for a directory it does not
|
|
485
|
+
* recognize is a statement that is simply untrue, and the failure it would otherwise become is
|
|
486
|
+
* invisible: the manifest reads as nothing, so the package is named after its directory at
|
|
487
|
+
* `0.0.0` and the repository looks like it works. The error names the directory, the platform and
|
|
488
|
+
* the file that platform looked for.
|
|
489
|
+
*
|
|
490
|
+
* The root node always exists - a repository is a package whatever its technology - so this never
|
|
491
|
+
* returns `undefined`. A repository nobody recognizes is a root with no children, which is the
|
|
492
|
+
* single-package answer arrived at rather than guessed.
|
|
493
|
+
*
|
|
494
|
+
* **A directory is visited once.** A provider may legitimately name a directory another one
|
|
495
|
+
* already claimed (two globs overlapping, a symlinked package), and a provider naming an ancestor
|
|
496
|
+
* would otherwise recurse forever. First visit wins, so a package sits where it was first found.
|
|
497
|
+
*
|
|
498
|
+
* `deep` bounds the descent for the same reason `findRoot` bounds its climb: a provider computing
|
|
499
|
+
* paths rather than reading them can produce a chain that never ends, and a guessed depth is
|
|
500
|
+
* better than a hang with nothing printed.
|
|
501
|
+
*/
|
|
502
|
+
async function walk(app, rootDir, options) {
|
|
503
|
+
const visited = new Set();
|
|
504
|
+
const descend = async (dir, remaining) => {
|
|
505
|
+
const resolved = path.resolve(dir);
|
|
506
|
+
visited.add(resolved);
|
|
507
|
+
const declared = await options?.declared?.(resolved);
|
|
508
|
+
if (declared && !declared.manifestProvider.read(resolved)) {
|
|
509
|
+
throw new Error(`"${resolved}" declares \`platform: '${declared.name}'\`, and that platform does not ` +
|
|
510
|
+
`recognize it - it looks for "${declared.manifestProvider.fileName}". Either the ` +
|
|
511
|
+
`directory is not a ${declared.name} package, or the declaration belongs one level down.`);
|
|
512
|
+
}
|
|
513
|
+
const platform = declared ?? app.platformFor(resolved);
|
|
514
|
+
const node = { dirname: resolved, platform, children: [] };
|
|
515
|
+
if (remaining <= 0)
|
|
516
|
+
return node;
|
|
517
|
+
/**
|
|
518
|
+
* **Its own platform is asked, and nobody else.** A platform that did not claim the directory
|
|
519
|
+
* has no standing to say what is under it - that was the old first-wins rule, one level up.
|
|
520
|
+
* `basePlatform` has no `getWorkspace`, so a directory no technology claimed has no children,
|
|
521
|
+
* which is the documented behaviour of a repository naming no plugin.
|
|
522
|
+
*/
|
|
523
|
+
for (const child of platform.getWorkspace?.(resolved) ?? []) {
|
|
524
|
+
const childDir = path.resolve(child);
|
|
525
|
+
if (visited.has(childDir))
|
|
526
|
+
continue;
|
|
527
|
+
node.children.push(await descend(childDir, remaining - 1));
|
|
528
|
+
}
|
|
529
|
+
return node;
|
|
530
|
+
};
|
|
531
|
+
return descend(rootDir, options?.deep ?? 10);
|
|
532
|
+
}
|
|
533
|
+
Workspace.walk = walk;
|
|
534
|
+
/** Every node below `node`, depth-first, excluding `node` itself - the flat list `Repository`
|
|
535
|
+
* reports as its packages, since the root is not one of its own members. */
|
|
536
|
+
function flatten(node) {
|
|
537
|
+
return node.children.flatMap(child => [child, ...flatten(child)]);
|
|
538
|
+
}
|
|
539
|
+
Workspace.flatten = flatten;
|
|
540
|
+
/**
|
|
541
|
+
* Where the repository starts, decided **without knowing anything about any ecosystem** - it has
|
|
542
|
+
* to be, because the plugins that do know are named in the config file this walk is looking for.
|
|
543
|
+
*
|
|
544
|
+
* Walking up from `from`, stopping after a directory holding `.git`, the root is:
|
|
545
|
+
*
|
|
546
|
+
* 1. the **outermost** directory in that chain holding an `.rmanrc*` - outermost, because a
|
|
547
|
+
* *package* may have its own `.rmanrc` (that is a supported thing), and from inside such a
|
|
548
|
+
* package the nearest one is the package's, not the repository's;
|
|
549
|
+
* 2. otherwise the `.git` directory itself, the ordinary meaning of "repository root";
|
|
550
|
+
* 3. otherwise `from`, which is all that is left to go on.
|
|
551
|
+
*
|
|
552
|
+
* **What "outermost" costs**, since the two cases genuinely conflict and only one can win: a
|
|
553
|
+
* self-contained project nested inside a larger git repository *and sharing its `.git`* resolves
|
|
554
|
+
* to the outer root, not to itself (measured). That is the same repository by any definition git
|
|
555
|
+
* recognizes, so it is the defensible answer - and a nested project with a `.git` of its own is
|
|
556
|
+
* found correctly, because the walk stops there before the outer `.rmanrc` is ever seen (also
|
|
557
|
+
* measured). A per-package `.rmanrc` is the common case and it is the one served.
|
|
558
|
+
*/
|
|
559
|
+
function findRoot(from, deep = 10) {
|
|
560
|
+
const chain = [];
|
|
561
|
+
let dir = path.resolve(from);
|
|
562
|
+
let remaining = deep;
|
|
563
|
+
while (remaining-- >= 0 && fs.existsSync(dir)) {
|
|
564
|
+
chain.push(dir);
|
|
565
|
+
if (fs.existsSync(path.join(dir, '.git')))
|
|
566
|
+
break;
|
|
567
|
+
const parent = path.resolve(dir, '..');
|
|
568
|
+
if (parent === dir)
|
|
569
|
+
break;
|
|
570
|
+
dir = parent;
|
|
571
|
+
}
|
|
572
|
+
const withConfig = chain.filter(hasRmanConfig);
|
|
573
|
+
if (withConfig.length)
|
|
574
|
+
return withConfig[withConfig.length - 1];
|
|
575
|
+
const gitRoot = chain.find(d => fs.existsSync(path.join(d, '.git')));
|
|
576
|
+
return gitRoot ?? path.resolve(from);
|
|
577
|
+
}
|
|
578
|
+
Workspace.findRoot = findRoot;
|
|
579
|
+
/** Every file form a `.rmanrc` comes in - `package.json#rman` is deliberately *not* one of them
|
|
580
|
+
* here: it would make the root question npm-shaped again. */
|
|
581
|
+
const CONFIG_FILES = ['.rmanrc', '.rmanrc.yml', '.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
|
|
582
|
+
function hasRmanConfig(dir) {
|
|
583
|
+
return CONFIG_FILES.some(name => fs.existsSync(path.join(dir, name)));
|
|
584
|
+
}
|
|
585
|
+
})(Workspace || (Workspace = {}));
|
package/core/command-builder.js
CHANGED
|
@@ -46,7 +46,23 @@ export function toYargsCommand(meta) {
|
|
|
46
46
|
* narrower one is the useful one, and this is the single place the widening is admitted -
|
|
47
47
|
* previously it was an `as` per option read, in every handler.
|
|
48
48
|
*/
|
|
49
|
-
handler: meta
|
|
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);
|
|
50
66
|
};
|
|
51
67
|
}
|
|
52
68
|
/** A command's own name - the first word of its `command` string, before any positional. */
|