rman 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +34 -1
- package/constants.js +1 -1
- package/core/config.d.ts +40 -31
- package/core/config.js +125 -96
- package/core/extends-config.js +1 -1
- package/core/merge-config.d.ts +17 -1
- package/core/merge-config.js +39 -9
- package/core/repository.js +4 -3
- package/interfaces/rman-config.interface.d.ts +14 -10
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -27,9 +27,21 @@ import { filterPackages, readPackageFilterOptions, readRootOption } from './util
|
|
|
27
27
|
import { printableConfig } from './utils/printable-config.js';
|
|
28
28
|
import { runBin } from './utils/run-bin.js';
|
|
29
29
|
export async function runCli(options) {
|
|
30
|
+
const _argv = options?.argv || hideBin(process.argv);
|
|
31
|
+
/**
|
|
32
|
+
* **Answered before the repository is touched**, because neither question is about a repository.
|
|
33
|
+
*
|
|
34
|
+
* `rman -v` is what you reach for when something is wrong - to find out which rman is even
|
|
35
|
+
* installed - and it was the one thing a broken repository took away: resolution happens during
|
|
36
|
+
* `Repository.create`, long before yargs sees the flag, so `rman -v` in a repository whose
|
|
37
|
+
* `.rmanrc` named a plugin it could not resolve answered with that error and exit 1. Measured.
|
|
38
|
+
*/
|
|
39
|
+
if (_argv.some(arg => arg === '-v' || arg === '--version')) {
|
|
40
|
+
console.log(version);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
30
43
|
try {
|
|
31
44
|
const repository = await Repository.create(options?.cwd);
|
|
32
|
-
const _argv = options?.argv || hideBin(process.argv);
|
|
33
45
|
const program = yargs(_argv)
|
|
34
46
|
.scriptName('rman')
|
|
35
47
|
.version(version)
|
|
@@ -156,6 +168,27 @@ export async function runCli(options) {
|
|
|
156
168
|
await program.parseAsync();
|
|
157
169
|
}
|
|
158
170
|
catch (e) {
|
|
171
|
+
/**
|
|
172
|
+
* **`--help` still answers**, because a broken repository is the moment you most want it. The
|
|
173
|
+
* command list is the part that genuinely needs the repository - every built-in's `initCli`
|
|
174
|
+
* closes over it, and a plugin's commands *are* the repository's - so help degrades to the
|
|
175
|
+
* global options and says plainly why the rest is missing, rather than failing outright.
|
|
176
|
+
*
|
|
177
|
+
* The reason goes to stderr, so `rman --help | less` is still just help.
|
|
178
|
+
*/
|
|
179
|
+
if (_argv.some(arg => arg === '-h' || arg === '--help')) {
|
|
180
|
+
console.error(colors.yellow(`Repository could not be read: ${e.message}`));
|
|
181
|
+
console.error(colors.yellow('Commands are not listed - they come from this repository and its plugins.\n'));
|
|
182
|
+
await yargs(_argv)
|
|
183
|
+
.scriptName('rman')
|
|
184
|
+
.version(version)
|
|
185
|
+
.alias('version', 'v')
|
|
186
|
+
.usage('$0 <cmd> [options...]')
|
|
187
|
+
.help('help')
|
|
188
|
+
.alias('help', 'h')
|
|
189
|
+
.showHelp(text => console.log(text));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
159
192
|
/** Setup failures - no `package.json` to be found, a `.rman` command shadowing a built-in -
|
|
160
193
|
* used to be printed and then swallowed, so the shell saw success: `rman info` in the wrong
|
|
161
194
|
* directory reported failure on stdout and 0 to whatever called it. Printed once (unless the
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '1.
|
|
1
|
+
export const version = '1.3.0';
|
package/core/config.d.ts
CHANGED
|
@@ -27,52 +27,61 @@ export declare function readDirConfig(dirname: string): Promise<RmanConfig>;
|
|
|
27
27
|
* it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
|
|
28
28
|
* chain does.
|
|
29
29
|
*
|
|
30
|
-
* Every level contributes in two ways
|
|
31
|
-
*
|
|
32
|
-
* - **
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
30
|
+
* Every level contributes in two ways:
|
|
31
|
+
*
|
|
32
|
+
* - **An unmarked key configures that directory and every package under it.** What a parent says
|
|
33
|
+
* reaches the children, which is what every directory-scoped config in the ecosystem does and
|
|
34
|
+
* what a reader expects without being told.
|
|
35
|
+
* - **A `"[selector]"` block narrows the audience** - `"[/]"` to the root package alone, `"[*]"` or
|
|
36
|
+
* a glob to the packages below (never the root, which is nobody's child). See `parseSelector`.
|
|
37
|
+
*
|
|
38
|
+
* **The root used to be the one directory whose unmarked config did *not* cascade**, on the
|
|
39
|
+
* reasoning that a setting means different things to a package and to the repository - and the
|
|
40
|
+
* reasoning is sound, but the rule it produced was not readable: an intermediate `packages/`
|
|
41
|
+
* cascaded while the root did not, so what a file meant depended on whether a `package.json` sat
|
|
42
|
+
* beside it. `vars` then had to be carved out as an exception, which is what a rule fighting itself
|
|
43
|
+
* looks like. One sentence now covers both: what is written above reaches below, and `"[/]"` is how
|
|
44
|
+
* a statement stays at the root.
|
|
45
|
+
*
|
|
46
|
+
* **The cost is real and lands on one subtree.** `run.<script>`'s hooks on the root are a repo-wide
|
|
47
|
+
* bookend, run once at the repository root; on a package they are that package's own hook, run in
|
|
48
|
+
* its directory. Cascaded, one declaration is both - once at the root and once per package. A
|
|
49
|
+
* repo-wide bookend therefore belongs under `"[/]"`, where its audience is visible; that is the
|
|
50
|
+
* migration this change asks for, and the only one that is not mechanical.
|
|
46
51
|
*
|
|
47
52
|
* `packageName` is what selectors match against; without it, selector blocks contribute nothing at
|
|
48
|
-
* all. The root package passes its own, since `"[/]"`
|
|
53
|
+
* all. The root package passes its own, since `"[/]"` speaks to it.
|
|
49
54
|
*/
|
|
50
55
|
export declare function resolveConfig(rootDir: string, targetDir: string, cache?: Map<string, RmanConfig>, packageName?: string): Promise<RmanConfig>;
|
|
51
|
-
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[
|
|
56
|
+
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[pkg-a]"`. The
|
|
52
57
|
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
53
58
|
* one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
|
|
54
59
|
* parses as a flow sequence. */
|
|
55
60
|
export declare function isSelectorKey(key: string): boolean;
|
|
56
61
|
/**
|
|
57
|
-
* **Which packages a selector speaks for.**
|
|
62
|
+
* **Which packages a selector speaks for.** Two audiences, and the second is a glob:
|
|
58
63
|
*
|
|
59
64
|
* | | |
|
|
60
65
|
* | --- | --- |
|
|
61
|
-
* | `"[/]"` | the **root package**
|
|
62
|
-
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | **
|
|
63
|
-
* | `"[ws:*]"`, `"[workspace:pkg-*]"` | every **non-root** package the glob matches |
|
|
66
|
+
* | `"[/]"` | the **root package** alone |
|
|
67
|
+
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | the packages **below** this directory that the glob matches |
|
|
64
68
|
*
|
|
65
69
|
* `/` for the root because that is what a repository root is called everywhere else, and it cannot
|
|
66
|
-
* collide with a package name.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* collide with a package name.
|
|
71
|
+
*
|
|
72
|
+
* **The root is never selected by name, and that one rule removes two traps.** A glob matches
|
|
73
|
+
* package names, and the root is nobody's child - so `"[my-*]"` cannot quietly pick up a repository
|
|
74
|
+
* whose root package happens to be called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
75
|
+
* setting to a root that has no build directory to apply it to. The root is addressed structurally
|
|
76
|
+
* or not at all.
|
|
77
|
+
*
|
|
78
|
+
* **`"[ws:*]"` / `"[workspace:*]"` is accepted and means exactly `"[*]"`.** The qualifier existed to
|
|
79
|
+
* say "not the root" back when a bare glob included it; the shape of the set says that now, so it
|
|
80
|
+
* has nothing left to add. Accepted rather than rejected because the two spellings resolve to the
|
|
81
|
+
* same packages - an error would be friction with no reader to protect.
|
|
73
82
|
*/
|
|
74
83
|
export declare function parseSelector(key: string): {
|
|
75
|
-
scope: 'root' | '
|
|
84
|
+
scope: 'root' | 'package';
|
|
76
85
|
test: (name: string) => boolean;
|
|
77
86
|
};
|
|
78
87
|
/** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
|
|
@@ -330,7 +339,7 @@ export declare const DEFERRED_PATHS: string[];
|
|
|
330
339
|
* config:
|
|
331
340
|
*
|
|
332
341
|
* ```js
|
|
333
|
-
* '[
|
|
342
|
+
* '[*]': {
|
|
334
343
|
* clean: { include: ({ vars, value }) => [...value, vars.buildDir] }, // a value: called here
|
|
335
344
|
* run: { build: { after: ({ pkg }) => copyDocs(pkg) } }, // a step: called by `run`
|
|
336
345
|
* }
|
package/core/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import semver from 'semver';
|
|
|
8
8
|
import { pathToFileURL } from 'url';
|
|
9
9
|
import vm from 'vm';
|
|
10
10
|
import { assertNoSelectorExtends, EXTENDS_KEY, resolveExtends } from './extends-config.js';
|
|
11
|
-
import { finalizeConfig, mergeConfig, PREVIOUS_VALUES } from './merge-config.js';
|
|
11
|
+
import { finalizeConfig, mergeConfig, ORIGINS, PREVIOUS_VALUES } from './merge-config.js';
|
|
12
12
|
/**
|
|
13
13
|
* Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
|
|
14
14
|
* autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
|
|
@@ -71,7 +71,7 @@ export async function readDirConfig(dirname) {
|
|
|
71
71
|
assertNoSelectorExtends(pkgJson.rman, pkgJsonFile);
|
|
72
72
|
if (EXTENDS_KEY in pkgJson.rman)
|
|
73
73
|
extendsFrom = pkgJsonFile;
|
|
74
|
-
mergeConfig(result, pkgJson.rman);
|
|
74
|
+
mergeConfig(result, pkgJson.rman, pkgJsonFile);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
const ymlFile = path.join(dirname, '.rmanrc.yml');
|
|
@@ -81,7 +81,7 @@ export async function readDirConfig(dirname) {
|
|
|
81
81
|
assertNoSelectorExtends(obj, ymlFile);
|
|
82
82
|
if (EXTENDS_KEY in obj)
|
|
83
83
|
extendsFrom = ymlFile;
|
|
84
|
-
mergeConfig(result, obj);
|
|
84
|
+
mergeConfig(result, obj, ymlFile);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
const rcFile = path.join(dirname, '.rmanrc');
|
|
@@ -91,7 +91,7 @@ export async function readDirConfig(dirname) {
|
|
|
91
91
|
assertNoSelectorExtends(obj, rcFile);
|
|
92
92
|
if (EXTENDS_KEY in obj)
|
|
93
93
|
extendsFrom = rcFile;
|
|
94
|
-
mergeConfig(result, obj);
|
|
94
|
+
mergeConfig(result, obj, rcFile);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
for (const jsFileName of JS_CONFIG_FILES) {
|
|
@@ -102,7 +102,7 @@ export async function readDirConfig(dirname) {
|
|
|
102
102
|
assertNoSelectorExtends(obj, jsFile);
|
|
103
103
|
if (EXTENDS_KEY in obj)
|
|
104
104
|
extendsFrom = jsFile;
|
|
105
|
-
mergeConfig(result, obj);
|
|
105
|
+
mergeConfig(result, obj, jsFile);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -117,32 +117,37 @@ export async function readDirConfig(dirname) {
|
|
|
117
117
|
* it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
|
|
118
118
|
* chain does.
|
|
119
119
|
*
|
|
120
|
-
* Every level contributes in two ways
|
|
120
|
+
* Every level contributes in two ways:
|
|
121
121
|
*
|
|
122
|
-
* - **
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* included), `"[ws:*]"` for every one but the root, `"[/]"` for the root alone, `"[*-dialect]"`
|
|
128
|
-
* for a glob over package names. See `parseSelector`. This is the only way a directory speaks
|
|
129
|
-
* about anything but its own package.
|
|
122
|
+
* - **An unmarked key configures that directory and every package under it.** What a parent says
|
|
123
|
+
* reaches the children, which is what every directory-scoped config in the ecosystem does and
|
|
124
|
+
* what a reader expects without being told.
|
|
125
|
+
* - **A `"[selector]"` block narrows the audience** - `"[/]"` to the root package alone, `"[*]"` or
|
|
126
|
+
* a glob to the packages below (never the root, which is nobody's child). See `parseSelector`.
|
|
130
127
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
128
|
+
* **The root used to be the one directory whose unmarked config did *not* cascade**, on the
|
|
129
|
+
* reasoning that a setting means different things to a package and to the repository - and the
|
|
130
|
+
* reasoning is sound, but the rule it produced was not readable: an intermediate `packages/`
|
|
131
|
+
* cascaded while the root did not, so what a file meant depended on whether a `package.json` sat
|
|
132
|
+
* beside it. `vars` then had to be carved out as an exception, which is what a rule fighting itself
|
|
133
|
+
* looks like. One sentence now covers both: what is written above reaches below, and `"[/]"` is how
|
|
134
|
+
* a statement stays at the root.
|
|
135
|
+
*
|
|
136
|
+
* **The cost is real and lands on one subtree.** `run.<script>`'s hooks on the root are a repo-wide
|
|
137
|
+
* bookend, run once at the repository root; on a package they are that package's own hook, run in
|
|
138
|
+
* its directory. Cascaded, one declaration is both - once at the root and once per package. A
|
|
139
|
+
* repo-wide bookend therefore belongs under `"[/]"`, where its audience is visible; that is the
|
|
140
|
+
* migration this change asks for, and the only one that is not mechanical.
|
|
136
141
|
*
|
|
137
142
|
* `packageName` is what selectors match against; without it, selector blocks contribute nothing at
|
|
138
|
-
* all. The root package passes its own, since `"[/]"`
|
|
143
|
+
* all. The root package passes its own, since `"[/]"` speaks to it.
|
|
139
144
|
*/
|
|
140
145
|
export async function resolveConfig(rootDir, targetDir, cache = new Map(), packageName) {
|
|
141
146
|
const result = {};
|
|
142
147
|
const target = path.resolve(targetDir);
|
|
143
148
|
/** The root *package* is the one whose directory is the repository root - no other test is
|
|
144
149
|
* needed, and none would be as reliable: a name can be anything. In a single-package repository
|
|
145
|
-
* that is the only package, so `"[/]"` reaches it and `"[
|
|
150
|
+
* that is the only package, so `"[/]"` reaches it and `"[*]"` reaches nothing. */
|
|
146
151
|
const isRoot = target === path.resolve(rootDir);
|
|
147
152
|
for (const dir of dirChain(rootDir, targetDir)) {
|
|
148
153
|
let local = cache.get(dir);
|
|
@@ -150,39 +155,30 @@ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packa
|
|
|
150
155
|
local = await readDirConfig(dir);
|
|
151
156
|
cache.set(dir, local);
|
|
152
157
|
}
|
|
153
|
-
// A directory holding a package speaks for that package only - which is what keeps the root's
|
|
154
|
-
// own config off every package under it. A directory that holds none (an intermediate
|
|
155
|
-
// `packages/`, say) has no package to speak for, so its unmarked config can only mean
|
|
156
|
-
// "everything below" and still cascades.
|
|
157
|
-
const ownsAPackage = fs.existsSync(path.join(dir, 'package.json'));
|
|
158
|
-
const speaksForTarget = !ownsAPackage || path.resolve(dir) === target;
|
|
159
158
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
159
|
+
* **Unmarked first, because it is the widest thing this level says** - and that is an inversion
|
|
160
|
+
* of the order this loop used to run in, where a directory's own plain config beat a selector
|
|
161
|
+
* declared beside it. Under the old reading "unmarked" meant *this package* and so was the
|
|
162
|
+
* narrower of the two; it now means *this package and everything below*, which is the wider.
|
|
163
|
+
* Precedence follows the audience, not the spelling, so it had to move.
|
|
165
164
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
165
|
+
* Its position in the file is deliberately not consulted: a selector block written above the
|
|
166
|
+
* plain keys still wins. Unmarked is not a fourth selector - it is the level's floor, and the
|
|
167
|
+
* layer that feeds the directories below it.
|
|
168
168
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Selectors next, so a directory's own unmarked config still wins over a selector declared
|
|
172
|
-
// alongside it - "this package" is a more specific statement than "packages matching a glob".
|
|
169
|
+
mergeConfig(result, stripSelectors(local));
|
|
170
|
+
/** Then the selector blocks, **in the order they were written** - see `matchingSelectors`. */
|
|
173
171
|
if (packageName) {
|
|
174
172
|
for (const block of matchingSelectors(local, packageName, isRoot))
|
|
175
173
|
mergeConfig(result, block);
|
|
176
174
|
}
|
|
177
|
-
if (speaksForTarget)
|
|
178
|
-
mergeConfig(result, stripSelectors(local));
|
|
179
175
|
}
|
|
180
176
|
/** Every layer has had its turn, so an append still outstanding has nothing left to attach to
|
|
181
177
|
* and becomes the value itself. Done here rather than per layer: until the chain is finished,
|
|
182
178
|
* the key it appends to may still be coming. */
|
|
183
179
|
return finalizeConfig(result);
|
|
184
180
|
}
|
|
185
|
-
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[
|
|
181
|
+
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[pkg-a]"`. The
|
|
186
182
|
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
187
183
|
* one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
|
|
188
184
|
* parses as a flow sequence. */
|
|
@@ -190,50 +186,55 @@ export function isSelectorKey(key) {
|
|
|
190
186
|
return key.length > 2 && key.startsWith('[') && key.endsWith(']');
|
|
191
187
|
}
|
|
192
188
|
/**
|
|
193
|
-
* **Which packages a selector speaks for.**
|
|
189
|
+
* **Which packages a selector speaks for.** Two audiences, and the second is a glob:
|
|
194
190
|
*
|
|
195
191
|
* | | |
|
|
196
192
|
* | --- | --- |
|
|
197
|
-
* | `"[/]"` | the **root package**
|
|
198
|
-
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | **
|
|
199
|
-
* | `"[ws:*]"`, `"[workspace:pkg-*]"` | every **non-root** package the glob matches |
|
|
193
|
+
* | `"[/]"` | the **root package** alone |
|
|
194
|
+
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | the packages **below** this directory that the glob matches |
|
|
200
195
|
*
|
|
201
196
|
* `/` for the root because that is what a repository root is called everywhere else, and it cannot
|
|
202
|
-
* collide with a package name.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
197
|
+
* collide with a package name.
|
|
198
|
+
*
|
|
199
|
+
* **The root is never selected by name, and that one rule removes two traps.** A glob matches
|
|
200
|
+
* package names, and the root is nobody's child - so `"[my-*]"` cannot quietly pick up a repository
|
|
201
|
+
* whose root package happens to be called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
202
|
+
* setting to a root that has no build directory to apply it to. The root is addressed structurally
|
|
203
|
+
* or not at all.
|
|
204
|
+
*
|
|
205
|
+
* **`"[ws:*]"` / `"[workspace:*]"` is accepted and means exactly `"[*]"`.** The qualifier existed to
|
|
206
|
+
* say "not the root" back when a bare glob included it; the shape of the set says that now, so it
|
|
207
|
+
* has nothing left to add. Accepted rather than rejected because the two spellings resolve to the
|
|
208
|
+
* same packages - an error would be friction with no reader to protect.
|
|
209
209
|
*/
|
|
210
210
|
export function parseSelector(key) {
|
|
211
211
|
const inner = key.slice(1, -1);
|
|
212
212
|
if (inner === ROOT_SELECTOR_INNER)
|
|
213
213
|
return { scope: 'root', test: () => true };
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const re = globToRegExp(inner.slice(prefix.length));
|
|
217
|
-
return { scope: 'workspace', test: name => re.test(name) };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const re = globToRegExp(inner);
|
|
221
|
-
return { scope: 'all', test: name => re.test(name) };
|
|
214
|
+
const re = globToRegExp(stripWorkspacePrefix(inner));
|
|
215
|
+
return { scope: 'package', test: name => re.test(name) };
|
|
222
216
|
}
|
|
223
217
|
/** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
|
|
224
218
|
* `mysql-dialect` but not `my-dialect-helper`. Glob rather than regex, to match every other
|
|
225
219
|
* pattern in rman (`allowBranch`, `changelog.tagPattern`, `clean.include`). */
|
|
226
220
|
export function selectorToRegExp(key) {
|
|
227
|
-
return globToRegExp(key.slice(1, -1));
|
|
221
|
+
return globToRegExp(stripWorkspacePrefix(key.slice(1, -1)));
|
|
228
222
|
}
|
|
229
223
|
/**
|
|
230
|
-
* Every selector block in `config` that speaks for this package, in
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
224
|
+
* Every selector block in `config` that speaks for this package, **in the order they were written**
|
|
225
|
+
* - later wins, the way `overrides` works in eslint, prettier and babel, and the way a `.gitignore`
|
|
226
|
+
* rule does.
|
|
227
|
+
*
|
|
228
|
+
* **There used to be a ranking** (`"[*]"` lowest, then a catch-all `"[ws:*]"`, then the rest by
|
|
229
|
+
* declaration), so that "everything" could not beat a rule about one package by being written last.
|
|
230
|
+
* It was dropped because the ordering it implies does not exist: specificity only ranks sets that
|
|
231
|
+
* nest, and globs do not. For a package called `pkg-dialect`, neither `"[pkg-*]"` nor
|
|
232
|
+
* `"[*-dialect]"` contains the other, so any answer is an invented tiebreak - and an invented
|
|
233
|
+
* tiebreak is worse than the order the author typed. What was left was already declaration order
|
|
234
|
+
* with one case lifted out of it; this removes the exception rather than generalizing it.
|
|
235
|
+
*
|
|
236
|
+
* The cost, which the docs state rather than hide: a catch-all written *below* a narrower block now
|
|
237
|
+
* overrides it. Writing catch-alls first is a convention, not a rule - the file reads top to bottom.
|
|
237
238
|
*/
|
|
238
239
|
function matchingSelectors(config, packageName, isRoot) {
|
|
239
240
|
const matches = [];
|
|
@@ -241,23 +242,19 @@ function matchingSelectors(config, packageName, isRoot) {
|
|
|
241
242
|
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
242
243
|
continue;
|
|
243
244
|
const { scope, test } = parseSelector(key);
|
|
244
|
-
if (scope === 'root'
|
|
245
|
+
if (scope === 'root' ? !isRoot : isRoot || !test(packageName))
|
|
245
246
|
continue;
|
|
246
|
-
|
|
247
|
-
continue;
|
|
248
|
-
if (!test(packageName))
|
|
249
|
-
continue;
|
|
250
|
-
matches.push([selectorRank(key), value]);
|
|
247
|
+
matches.push(value);
|
|
251
248
|
}
|
|
252
|
-
return matches
|
|
249
|
+
return matches;
|
|
253
250
|
}
|
|
254
|
-
/**
|
|
255
|
-
*
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
251
|
+
/** `"[ws:*]"` and `"[workspace:*]"` are the pre-2.x spelling of "not the root", kept working
|
|
252
|
+
* because they now name the same set a bare glob does. Stripped here so one code path serves both. */
|
|
253
|
+
function stripWorkspacePrefix(inner) {
|
|
254
|
+
for (const prefix of WORKSPACE_PREFIXES)
|
|
255
|
+
if (inner.startsWith(prefix))
|
|
256
|
+
return inner.slice(prefix.length);
|
|
257
|
+
return inner;
|
|
261
258
|
}
|
|
262
259
|
function globToRegExp(glob) {
|
|
263
260
|
const source = glob
|
|
@@ -273,12 +270,10 @@ function stripSelectors(config) {
|
|
|
273
270
|
result[key] = value;
|
|
274
271
|
return result;
|
|
275
272
|
}
|
|
276
|
-
const CATCH_ALL = '[*]';
|
|
277
273
|
/** `"[/]"` - the root package, spelled the way a repository root is spelled everywhere else, and
|
|
278
274
|
* unable to collide with a package name. */
|
|
279
275
|
const ROOT_SELECTOR_INNER = '/';
|
|
280
|
-
/**
|
|
281
|
-
* someone else has to understand; the short one is what gets typed. */
|
|
276
|
+
/** Accepted spellings of the retired "not the root" qualifier - see `stripWorkspacePrefix`. */
|
|
282
277
|
const WORKSPACE_PREFIXES = ['workspace:', 'ws:'];
|
|
283
278
|
function dirChain(rootDir, targetDir) {
|
|
284
279
|
const rel = path.relative(rootDir, targetDir);
|
|
@@ -350,7 +345,8 @@ export function interpolateConfig(config, scope, options) {
|
|
|
350
345
|
resolving.push(key);
|
|
351
346
|
try {
|
|
352
347
|
const chain = config[PREVIOUS_VALUES];
|
|
353
|
-
const
|
|
348
|
+
const origins = config[ORIGINS];
|
|
349
|
+
const value = withOrigin(origins?.[key], () => walkWithPrevious(config[key], chain?.[key], scope, context, [...base, key], skip));
|
|
354
350
|
resolved.set(key, value);
|
|
355
351
|
return value;
|
|
356
352
|
}
|
|
@@ -398,7 +394,7 @@ export const DEFERRED_PATHS = ['version.before', 'version.exec', 'version.after'
|
|
|
398
394
|
* config:
|
|
399
395
|
*
|
|
400
396
|
* ```js
|
|
401
|
-
* '[
|
|
397
|
+
* '[*]': {
|
|
402
398
|
* clean: { include: ({ vars, value }) => [...value, vars.buildDir] }, // a value: called here
|
|
403
399
|
* run: { build: { after: ({ pkg }) => copyDocs(pkg) } }, // a step: called by `run`
|
|
404
400
|
* }
|
|
@@ -526,8 +522,9 @@ function walk(value, scope, context, at, skip) {
|
|
|
526
522
|
const chain = value[PREVIOUS_VALUES];
|
|
527
523
|
return withScopedVars(value, scope, context, at, skip, () => {
|
|
528
524
|
const result = {};
|
|
525
|
+
const origins = value[ORIGINS];
|
|
529
526
|
for (const [key, item] of Object.entries(value)) {
|
|
530
|
-
result[key] = walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip);
|
|
527
|
+
result[key] = withOrigin(origins?.[key], () => walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip));
|
|
531
528
|
}
|
|
532
529
|
return result;
|
|
533
530
|
});
|
|
@@ -612,6 +609,42 @@ function isPlainObject(value) {
|
|
|
612
609
|
* level, which costs a script that would have been called `vars`: `run.vars` is a scope, not a
|
|
613
610
|
* script. Nothing enumerates `run`'s keys as a list of script names, so the cost stops there. */
|
|
614
611
|
const VARS_KEY = 'vars';
|
|
612
|
+
/**
|
|
613
|
+
* The file the key being walked was written in, for the errors below to name.
|
|
614
|
+
*
|
|
615
|
+
* A config is merged from several files before anything reads it - a directory's own forms, an
|
|
616
|
+
* `extends` base, every `"[selector]"` block, one layer per directory - so `version.commitMessage`
|
|
617
|
+
* alone does not say where to go and look. `mergeConfig` records the file per key (`ORIGINS`); this
|
|
618
|
+
* is the depth-first cursor over that, kept in a module variable rather than threaded through
|
|
619
|
+
* `walk`'s signature because every error site would otherwise have to carry a parameter it only
|
|
620
|
+
* passes on.
|
|
621
|
+
*
|
|
622
|
+
* Nested keys inherit the enclosing file when the merge recorded none of their own, which is what a
|
|
623
|
+
* nested object in one file means.
|
|
624
|
+
*/
|
|
625
|
+
let currentOrigin;
|
|
626
|
+
function withOrigin(origin, body) {
|
|
627
|
+
const outer = currentOrigin;
|
|
628
|
+
if (origin !== undefined)
|
|
629
|
+
currentOrigin = origin;
|
|
630
|
+
try {
|
|
631
|
+
return body();
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
currentOrigin = outer;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** `"version.commitMessage" (.rmanrc.yml)`, or just the path when nothing recorded a file - a
|
|
638
|
+
* caller interpolating a fragment it built itself, say. Relative to the repository when it sits
|
|
639
|
+
* inside one, since an absolute path is noise in a message about the repository you are in. */
|
|
640
|
+
function describeAt(at) {
|
|
641
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
642
|
+
return currentOrigin ? `${where}" (${shortenOrigin(currentOrigin)})` : `${where}"`;
|
|
643
|
+
}
|
|
644
|
+
function shortenOrigin(file) {
|
|
645
|
+
const relative = path.relative(process.cwd(), file);
|
|
646
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
|
|
647
|
+
}
|
|
615
648
|
/**
|
|
616
649
|
* Walks one key of an object with **`value` bound** to whatever the layers below it resolved to.
|
|
617
650
|
*
|
|
@@ -660,10 +693,9 @@ function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
|
660
693
|
* and wrong for every key that is not a list.
|
|
661
694
|
*/
|
|
662
695
|
if (wasRead && resolved === undefined && !e?.rmanValueHint) {
|
|
663
|
-
const where = at.length ? formatPath(at) : 'the config root';
|
|
664
696
|
e.rmanValueHint = true;
|
|
665
697
|
e.message =
|
|
666
|
-
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${
|
|
698
|
+
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${describeAt(at)}.` +
|
|
667
699
|
`\n Write \`value ?? []\` (or \`?? ''\`) if it has to work as the first layer too.`;
|
|
668
700
|
}
|
|
669
701
|
throw e;
|
|
@@ -733,10 +765,9 @@ function callValueFn(fn, context, at) {
|
|
|
733
765
|
return fn(arg);
|
|
734
766
|
}
|
|
735
767
|
catch (e) {
|
|
736
|
-
const where = at.length ? formatPath(at) : 'the config root';
|
|
737
768
|
/** The `value` hint comes from `walkWithPrevious`, which wraps this call and is the one place
|
|
738
769
|
* that knows whether `value` was read - so the expression spelling gets the same sentence. */
|
|
739
|
-
throw new Error(`Config function in "${
|
|
770
|
+
throw new Error(`Config function in "${describeAt(at)} failed: ${e?.message}`, { cause: e });
|
|
740
771
|
}
|
|
741
772
|
}
|
|
742
773
|
function interpolateString(value, context, at) {
|
|
@@ -758,8 +789,7 @@ function interpolateString(value, context, at) {
|
|
|
758
789
|
* tag like `app:undefined` that looks plausible and is wrong - the exact silent-mistake shape
|
|
759
790
|
* this evaluator exists to avoid. `?? 'fallback'` says what was meant. */
|
|
760
791
|
if (result === undefined || result === null) {
|
|
761
|
-
|
|
762
|
-
throw new Error(`Expression in "${where}" is ${result} inside a string: ${value.trim()}\n` +
|
|
792
|
+
throw new Error(`Expression in "${describeAt(at)} is ${result} inside a string: ${value.trim()}\n` +
|
|
763
793
|
` \${{${expr}}} has no value here - give it a fallback (\${{${expr.trim()} ?? '...'}}).`);
|
|
764
794
|
}
|
|
765
795
|
return String(result);
|
|
@@ -772,8 +802,7 @@ function evaluate(expr, source, context, at) {
|
|
|
772
802
|
return vm.runInContext(expr, context, { timeout: EXPRESSION_TIMEOUT });
|
|
773
803
|
}
|
|
774
804
|
catch (e) {
|
|
775
|
-
|
|
776
|
-
throw new Error(`Invalid expression in "${where}": ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
|
|
805
|
+
throw new Error(`Invalid expression in "${describeAt(at)}: ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
|
|
777
806
|
}
|
|
778
807
|
}
|
|
779
808
|
function formatPath(at) {
|
package/core/extends-config.js
CHANGED
|
@@ -42,7 +42,7 @@ export async function resolveExtends(config, from, seen = []) {
|
|
|
42
42
|
const loaded = await loadConfigFile(file);
|
|
43
43
|
assertNoSelectorExtends(loaded, file);
|
|
44
44
|
// Recursive: a shared config may itself be built on another.
|
|
45
|
-
mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]));
|
|
45
|
+
mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]), file);
|
|
46
46
|
}
|
|
47
47
|
const own = { ...config };
|
|
48
48
|
delete own[EXTENDS_KEY];
|
package/core/merge-config.d.ts
CHANGED
|
@@ -31,6 +31,19 @@ export declare const ALWAYS_APPEND: readonly string[];
|
|
|
31
31
|
* `A <- expr2 <- expr3`, and one slot would have lost `A` the moment `expr3` arrived.
|
|
32
32
|
*/
|
|
33
33
|
export declare const PREVIOUS_VALUES: unique symbol;
|
|
34
|
+
/**
|
|
35
|
+
* Which file each key came from, so an error can name it.
|
|
36
|
+
*
|
|
37
|
+
* A config is merged from several files before anything reads it - a directory's own four forms, an
|
|
38
|
+
* `extends` base, every `"[selector]"` block, and one layer per directory from the root down - so by
|
|
39
|
+
* the time an expression fails, `version.commitMessage` could have been written in any of them.
|
|
40
|
+
* Saying only the key sends the reader looking through all of them.
|
|
41
|
+
*
|
|
42
|
+
* Recorded the same way `PREVIOUS_VALUES` is, for the same reason: a symbol on the containing
|
|
43
|
+
* object, invisible to `Object.entries`, `JSON.stringify` and js-yaml, so it travels with the config
|
|
44
|
+
* without any reader having to know it is there.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ORIGINS: unique symbol;
|
|
34
47
|
/** One link: the raw value this key held, and whatever *it* was derived from. */
|
|
35
48
|
export interface PreviousValue {
|
|
36
49
|
value: unknown;
|
|
@@ -69,7 +82,10 @@ export declare function appendTarget(key: string): string | undefined;
|
|
|
69
82
|
* `key` and `+key` in the same object are both honored, in that order: the replacement happens
|
|
70
83
|
* first, then the append lands on top of it.
|
|
71
84
|
*/
|
|
72
|
-
export declare function mergeConfig(target: Record<string, any>, source: Record<string, any
|
|
85
|
+
export declare function mergeConfig(target: Record<string, any>, source: Record<string, any>,
|
|
86
|
+
/** The file `source` was read from, recorded per key - see `ORIGINS`. A caller merging a value it
|
|
87
|
+
* built rather than read (a selector block already carrying its own origins) passes nothing. */
|
|
88
|
+
origin?: string): Record<string, any>;
|
|
73
89
|
/**
|
|
74
90
|
* Turns any `+key` still outstanding into its plain key, as a list - the "nothing was inherited"
|
|
75
91
|
* case, where an append simply is the whole value.
|
package/core/merge-config.js
CHANGED
|
@@ -31,6 +31,19 @@ export const ALWAYS_APPEND = ['plugins'];
|
|
|
31
31
|
* `A <- expr2 <- expr3`, and one slot would have lost `A` the moment `expr3` arrived.
|
|
32
32
|
*/
|
|
33
33
|
export const PREVIOUS_VALUES = Symbol('rman.previousValues');
|
|
34
|
+
/**
|
|
35
|
+
* Which file each key came from, so an error can name it.
|
|
36
|
+
*
|
|
37
|
+
* A config is merged from several files before anything reads it - a directory's own four forms, an
|
|
38
|
+
* `extends` base, every `"[selector]"` block, and one layer per directory from the root down - so by
|
|
39
|
+
* the time an expression fails, `version.commitMessage` could have been written in any of them.
|
|
40
|
+
* Saying only the key sends the reader looking through all of them.
|
|
41
|
+
*
|
|
42
|
+
* Recorded the same way `PREVIOUS_VALUES` is, for the same reason: a symbol on the containing
|
|
43
|
+
* object, invisible to `Object.entries`, `JSON.stringify` and js-yaml, so it travels with the config
|
|
44
|
+
* without any reader having to know it is there.
|
|
45
|
+
*/
|
|
46
|
+
export const ORIGINS = Symbol('rman.origins');
|
|
34
47
|
/** Only these two can ask for `value`, so only these two are worth remembering a previous for. */
|
|
35
48
|
export function carriesPreviousValue(value) {
|
|
36
49
|
return typeof value === 'function' || (typeof value === 'string' && value.includes('${{'));
|
|
@@ -70,7 +83,10 @@ export function appendTarget(key) {
|
|
|
70
83
|
* `key` and `+key` in the same object are both honored, in that order: the replacement happens
|
|
71
84
|
* first, then the append lands on top of it.
|
|
72
85
|
*/
|
|
73
|
-
export function mergeConfig(target, source
|
|
86
|
+
export function mergeConfig(target, source,
|
|
87
|
+
/** The file `source` was read from, recorded per key - see `ORIGINS`. A caller merging a value it
|
|
88
|
+
* built rather than read (a selector block already carrying its own origins) passes nothing. */
|
|
89
|
+
origin) {
|
|
74
90
|
// Plain keys first, so a `+key` alongside its own `key` appends to that replacement rather than
|
|
75
91
|
// to whatever the previous layer had.
|
|
76
92
|
for (const [key, value] of Object.entries(source)) {
|
|
@@ -81,7 +97,7 @@ export function mergeConfig(target, source) {
|
|
|
81
97
|
appendList(target, key, value);
|
|
82
98
|
continue;
|
|
83
99
|
}
|
|
84
|
-
assignMerged(target, key, value);
|
|
100
|
+
assignMerged(target, key, value, source, origin);
|
|
85
101
|
}
|
|
86
102
|
for (const [key, value] of Object.entries(source)) {
|
|
87
103
|
const plain = appendTarget(key);
|
|
@@ -90,7 +106,7 @@ export function mergeConfig(target, source) {
|
|
|
90
106
|
// An object merges either way, so the prefix asks for nothing extra - resolve it now and let
|
|
91
107
|
// the two spellings coincide.
|
|
92
108
|
if (isPlainObject(value) || isPlainObject(target[plain])) {
|
|
93
|
-
assignMerged(target, plain, value);
|
|
109
|
+
assignMerged(target, plain, value, source, origin);
|
|
94
110
|
continue;
|
|
95
111
|
}
|
|
96
112
|
if (plain in target) {
|
|
@@ -131,16 +147,30 @@ export function finalizeConfig(config) {
|
|
|
131
147
|
}
|
|
132
148
|
/** Carried across by hand: this rebuilds the object from `Object.entries`, which does not see a
|
|
133
149
|
* symbol - and dropping it here would lose every `value` chain the merge just recorded. */
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
for (const carried of [PREVIOUS_VALUES, ORIGINS]) {
|
|
151
|
+
const value = config[carried];
|
|
152
|
+
if (value)
|
|
153
|
+
Object.defineProperty(result, carried, { value });
|
|
154
|
+
}
|
|
137
155
|
return result;
|
|
138
156
|
}
|
|
139
|
-
function assignMerged(target, key, value) {
|
|
157
|
+
function assignMerged(target, key, value, source, origin) {
|
|
158
|
+
/** A source that already carries origins wins over the caller's: an `extends` base keeps the file
|
|
159
|
+
* its own keys were written in, rather than being attributed to the file that named it. */
|
|
160
|
+
const from = source[ORIGINS]?.[key] ?? origin;
|
|
161
|
+
if (from !== undefined) {
|
|
162
|
+
const carrier = target;
|
|
163
|
+
/** **Non-enumerable**, like `PREVIOUS_VALUES`: `expect`'s `toEqual` compares symbol properties,
|
|
164
|
+
* so a plain assignment turned every config-shape assertion in the suite into a diff about
|
|
165
|
+
* bookkeeping (measured, five specs at once). Nothing should see this but the error messages. */
|
|
166
|
+
if (!carrier[ORIGINS])
|
|
167
|
+
Object.defineProperty(target, ORIGINS, { value: {}, writable: true });
|
|
168
|
+
carrier[ORIGINS][key] = from;
|
|
169
|
+
}
|
|
140
170
|
if (isPlainObject(value)) {
|
|
141
171
|
if (!isPlainObject(target[key]))
|
|
142
172
|
target[key] = {};
|
|
143
|
-
mergeConfig(target[key], value);
|
|
173
|
+
mergeConfig(target[key], value, from);
|
|
144
174
|
return;
|
|
145
175
|
}
|
|
146
176
|
/**
|
|
@@ -148,7 +178,7 @@ function assignMerged(target, key, value) {
|
|
|
148
178
|
*
|
|
149
179
|
* ```js
|
|
150
180
|
* '[*]': { clean: { include: ({ vars }) => [vars.buildDir] } }
|
|
151
|
-
* '[
|
|
181
|
+
* '[*]': { clean: { include: "${{ [...value, pkg.basename + '.log'] }}" } }
|
|
152
182
|
* ```
|
|
153
183
|
*
|
|
154
184
|
* Chained here rather than at resolution time because only the merge knows the order of the
|
package/core/repository.js
CHANGED
|
@@ -208,9 +208,10 @@ export class Repository extends Package {
|
|
|
208
208
|
* second `rawConfig` copy of every package's config for that one reader; measured identical.
|
|
209
209
|
*/
|
|
210
210
|
/**
|
|
211
|
-
* The root is resolved **with its name**, like every other package
|
|
212
|
-
*
|
|
213
|
-
*
|
|
211
|
+
* The root is resolved **with its name**, like every other package. Not because a glob could
|
|
212
|
+
* match it - `"[*]"` and every other name pattern speak only to the packages below - but because
|
|
213
|
+
* `resolveConfig` needs a name to run `matchingSelectors` at all, and `"[/]"` is a selector.
|
|
214
|
+
* Passing none would silently drop the root's own block.
|
|
214
215
|
*/
|
|
215
216
|
const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.name);
|
|
216
217
|
this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
|
|
@@ -166,17 +166,21 @@ export interface RmanConfigKeys {
|
|
|
166
166
|
*/
|
|
167
167
|
dependencies?: string[];
|
|
168
168
|
/**
|
|
169
|
-
* Config for **
|
|
170
|
-
* package
|
|
171
|
-
* Everything else in this object
|
|
172
|
-
*
|
|
173
|
-
*
|
|
169
|
+
* Config for a **narrower audience**, keyed by a `"[selector]"` naming it - `"[/]"` for the root
|
|
170
|
+
* package alone, `"[*]"` for the packages below this directory, `"[*-dialect]"` for a glob over
|
|
171
|
+
* their names, `"[pkg-a]"` for one. Everything else in this object reaches this directory *and*
|
|
172
|
+
* every package under it, so a selector is how a statement stops being everyone's.
|
|
173
|
+
*
|
|
174
|
+
* A glob never matches the root, which is nobody's child - so a package-shaped setting cannot
|
|
175
|
+
* reach a root that has no package directory to apply it to, and `"[/]"` is the only way to
|
|
176
|
+
* address the root.
|
|
174
177
|
*
|
|
175
178
|
* ```yaml
|
|
176
179
|
* # the repository root's own .rmanrc.yml
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
+
* "[/]":
|
|
181
|
+
* run:
|
|
182
|
+
* build:
|
|
183
|
+
* before: node support/generate.cjs # a repo-wide bookend, run once at the root
|
|
180
184
|
* "[*]":
|
|
181
185
|
* run:
|
|
182
186
|
* build:
|
|
@@ -184,8 +188,8 @@ export interface RmanConfigKeys {
|
|
|
184
188
|
* ```
|
|
185
189
|
*
|
|
186
190
|
* In YAML the quotes are **required**: a bare `[*]` parses as a flow sequence, and `*` as an
|
|
187
|
-
* alias indicator. Precedence
|
|
188
|
-
*
|
|
191
|
+
* alias indicator. Precedence: the unmarked keys first, then these blocks **in the order they
|
|
192
|
+
* were written** - later wins. A directory level closer to the package wins over all of them.
|
|
189
193
|
*
|
|
190
194
|
* Recursive, mirroring the schema's own `"$ref": "#"`: whatever a `.rmanrc` may say about its own
|
|
191
195
|
* package it may say here about the ones it names - nested selectors included. Typed as
|