rman 1.2.4 → 1.2.5
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.js +49 -15
- package/core/extends-config.js +1 -1
- package/core/merge-config.d.ts +17 -1
- package/core/merge-config.js +38 -8
- 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.2.
|
|
1
|
+
export const version = '1.2.5';
|
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
|
}
|
|
@@ -350,7 +350,8 @@ export function interpolateConfig(config, scope, options) {
|
|
|
350
350
|
resolving.push(key);
|
|
351
351
|
try {
|
|
352
352
|
const chain = config[PREVIOUS_VALUES];
|
|
353
|
-
const
|
|
353
|
+
const origins = config[ORIGINS];
|
|
354
|
+
const value = withOrigin(origins?.[key], () => walkWithPrevious(config[key], chain?.[key], scope, context, [...base, key], skip));
|
|
354
355
|
resolved.set(key, value);
|
|
355
356
|
return value;
|
|
356
357
|
}
|
|
@@ -526,8 +527,9 @@ function walk(value, scope, context, at, skip) {
|
|
|
526
527
|
const chain = value[PREVIOUS_VALUES];
|
|
527
528
|
return withScopedVars(value, scope, context, at, skip, () => {
|
|
528
529
|
const result = {};
|
|
530
|
+
const origins = value[ORIGINS];
|
|
529
531
|
for (const [key, item] of Object.entries(value)) {
|
|
530
|
-
result[key] = walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip);
|
|
532
|
+
result[key] = withOrigin(origins?.[key], () => walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip));
|
|
531
533
|
}
|
|
532
534
|
return result;
|
|
533
535
|
});
|
|
@@ -612,6 +614,42 @@ function isPlainObject(value) {
|
|
|
612
614
|
* level, which costs a script that would have been called `vars`: `run.vars` is a scope, not a
|
|
613
615
|
* script. Nothing enumerates `run`'s keys as a list of script names, so the cost stops there. */
|
|
614
616
|
const VARS_KEY = 'vars';
|
|
617
|
+
/**
|
|
618
|
+
* The file the key being walked was written in, for the errors below to name.
|
|
619
|
+
*
|
|
620
|
+
* A config is merged from several files before anything reads it - a directory's own forms, an
|
|
621
|
+
* `extends` base, every `"[selector]"` block, one layer per directory - so `version.commitMessage`
|
|
622
|
+
* alone does not say where to go and look. `mergeConfig` records the file per key (`ORIGINS`); this
|
|
623
|
+
* is the depth-first cursor over that, kept in a module variable rather than threaded through
|
|
624
|
+
* `walk`'s signature because every error site would otherwise have to carry a parameter it only
|
|
625
|
+
* passes on.
|
|
626
|
+
*
|
|
627
|
+
* Nested keys inherit the enclosing file when the merge recorded none of their own, which is what a
|
|
628
|
+
* nested object in one file means.
|
|
629
|
+
*/
|
|
630
|
+
let currentOrigin;
|
|
631
|
+
function withOrigin(origin, body) {
|
|
632
|
+
const outer = currentOrigin;
|
|
633
|
+
if (origin !== undefined)
|
|
634
|
+
currentOrigin = origin;
|
|
635
|
+
try {
|
|
636
|
+
return body();
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
currentOrigin = outer;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/** `"version.commitMessage" (.rmanrc.yml)`, or just the path when nothing recorded a file - a
|
|
643
|
+
* caller interpolating a fragment it built itself, say. Relative to the repository when it sits
|
|
644
|
+
* inside one, since an absolute path is noise in a message about the repository you are in. */
|
|
645
|
+
function describeAt(at) {
|
|
646
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
647
|
+
return currentOrigin ? `${where}" (${shortenOrigin(currentOrigin)})` : `${where}"`;
|
|
648
|
+
}
|
|
649
|
+
function shortenOrigin(file) {
|
|
650
|
+
const relative = path.relative(process.cwd(), file);
|
|
651
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
|
|
652
|
+
}
|
|
615
653
|
/**
|
|
616
654
|
* Walks one key of an object with **`value` bound** to whatever the layers below it resolved to.
|
|
617
655
|
*
|
|
@@ -660,10 +698,9 @@ function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
|
660
698
|
* and wrong for every key that is not a list.
|
|
661
699
|
*/
|
|
662
700
|
if (wasRead && resolved === undefined && !e?.rmanValueHint) {
|
|
663
|
-
const where = at.length ? formatPath(at) : 'the config root';
|
|
664
701
|
e.rmanValueHint = true;
|
|
665
702
|
e.message =
|
|
666
|
-
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${
|
|
703
|
+
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${describeAt(at)}.` +
|
|
667
704
|
`\n Write \`value ?? []\` (or \`?? ''\`) if it has to work as the first layer too.`;
|
|
668
705
|
}
|
|
669
706
|
throw e;
|
|
@@ -733,10 +770,9 @@ function callValueFn(fn, context, at) {
|
|
|
733
770
|
return fn(arg);
|
|
734
771
|
}
|
|
735
772
|
catch (e) {
|
|
736
|
-
const where = at.length ? formatPath(at) : 'the config root';
|
|
737
773
|
/** The `value` hint comes from `walkWithPrevious`, which wraps this call and is the one place
|
|
738
774
|
* that knows whether `value` was read - so the expression spelling gets the same sentence. */
|
|
739
|
-
throw new Error(`Config function in "${
|
|
775
|
+
throw new Error(`Config function in "${describeAt(at)} failed: ${e?.message}`, { cause: e });
|
|
740
776
|
}
|
|
741
777
|
}
|
|
742
778
|
function interpolateString(value, context, at) {
|
|
@@ -758,8 +794,7 @@ function interpolateString(value, context, at) {
|
|
|
758
794
|
* tag like `app:undefined` that looks plausible and is wrong - the exact silent-mistake shape
|
|
759
795
|
* this evaluator exists to avoid. `?? 'fallback'` says what was meant. */
|
|
760
796
|
if (result === undefined || result === null) {
|
|
761
|
-
|
|
762
|
-
throw new Error(`Expression in "${where}" is ${result} inside a string: ${value.trim()}\n` +
|
|
797
|
+
throw new Error(`Expression in "${describeAt(at)} is ${result} inside a string: ${value.trim()}\n` +
|
|
763
798
|
` \${{${expr}}} has no value here - give it a fallback (\${{${expr.trim()} ?? '...'}}).`);
|
|
764
799
|
}
|
|
765
800
|
return String(result);
|
|
@@ -772,8 +807,7 @@ function evaluate(expr, source, context, at) {
|
|
|
772
807
|
return vm.runInContext(expr, context, { timeout: EXPRESSION_TIMEOUT });
|
|
773
808
|
}
|
|
774
809
|
catch (e) {
|
|
775
|
-
|
|
776
|
-
throw new Error(`Invalid expression in "${where}": ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
|
|
810
|
+
throw new Error(`Invalid expression in "${describeAt(at)}: ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
|
|
777
811
|
}
|
|
778
812
|
}
|
|
779
813
|
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
|
/**
|