rman 1.2.1 → 1.2.2
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/constants.js +1 -1
- package/core/config.js +81 -32
- package/core/merge-config.d.ts +20 -9
- package/core/merge-config.js +29 -33
- package/package.json +1 -1
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '1.2.
|
|
1
|
+
export const version = '1.2.2';
|
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,
|
|
11
|
+
import { finalizeConfig, mergeConfig, 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
|
|
@@ -349,7 +349,8 @@ export function interpolateConfig(config, scope, options) {
|
|
|
349
349
|
}
|
|
350
350
|
resolving.push(key);
|
|
351
351
|
try {
|
|
352
|
-
const
|
|
352
|
+
const chain = config[PREVIOUS_VALUES];
|
|
353
|
+
const value = walkWithPrevious(config[key], chain?.[key], scope, context, [...base, key], skip);
|
|
353
354
|
resolved.set(key, value);
|
|
354
355
|
return value;
|
|
355
356
|
}
|
|
@@ -515,17 +516,19 @@ function walk(value, scope, context, at, skip) {
|
|
|
515
516
|
* function form exists to draw. */
|
|
516
517
|
if (isCodePath(at))
|
|
517
518
|
return value;
|
|
518
|
-
return callValueFn(value,
|
|
519
|
+
return callValueFn(value, context, at);
|
|
519
520
|
}
|
|
520
521
|
if (typeof value === 'string')
|
|
521
522
|
return interpolateString(value, context, at);
|
|
522
523
|
if (Array.isArray(value))
|
|
523
524
|
return value.map((item, i) => walk(item, scope, context, [...at, i], skip));
|
|
524
525
|
if (value && typeof value === 'object') {
|
|
526
|
+
const chain = value[PREVIOUS_VALUES];
|
|
525
527
|
return withScopedVars(value, scope, context, at, skip, () => {
|
|
526
528
|
const result = {};
|
|
527
|
-
for (const [key, item] of Object.entries(value))
|
|
528
|
-
result[key] =
|
|
529
|
+
for (const [key, item] of Object.entries(value)) {
|
|
530
|
+
result[key] = walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip);
|
|
531
|
+
}
|
|
529
532
|
return result;
|
|
530
533
|
});
|
|
531
534
|
}
|
|
@@ -609,6 +612,71 @@ function isPlainObject(value) {
|
|
|
609
612
|
* level, which costs a script that would have been called `vars`: `run.vars` is a scope, not a
|
|
610
613
|
* script. Nothing enumerates `run`'s keys as a list of script names, so the cost stops there. */
|
|
611
614
|
const VARS_KEY = 'vars';
|
|
615
|
+
/**
|
|
616
|
+
* Walks one key of an object with **`value` bound** to whatever the layers below it resolved to.
|
|
617
|
+
*
|
|
618
|
+
* Bound on the interpolation context rather than passed as an argument, because an expression reads
|
|
619
|
+
* it as a global (`"${{ [...value, 'x'] }}"`) - a function then picks the same binding up through
|
|
620
|
+
* its prototype, so the two spellings cannot disagree about what `value` is. It was function-only
|
|
621
|
+
* at first, on the reasoning that a string cannot carry an array back; that was wrong, since a
|
|
622
|
+
* string which is *nothing but* one expression keeps the value's own type.
|
|
623
|
+
*
|
|
624
|
+
* **Always bound, even with nothing underneath.** Left unbound, an expression naming it fails with
|
|
625
|
+
* V8's `value is not defined`, which reads as "there is no such thing" rather than "nothing below
|
|
626
|
+
* this layer set it" - two different mistakes needing two different fixes.
|
|
627
|
+
*
|
|
628
|
+
* The chain resolves bottom-up, so a layer deriving from a layer that itself derived from something
|
|
629
|
+
* is handed the finished value rather than a half-resolved expression.
|
|
630
|
+
*/
|
|
631
|
+
function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
632
|
+
const resolved = previous === undefined ? undefined : walkWithPrevious(previous.value, previous.previous, scope, context, at, skip);
|
|
633
|
+
const outer = Object.getOwnPropertyDescriptor(context, VALUE_KEY);
|
|
634
|
+
/** A getter, so the catch below can tell whether the value **actually read `value`**: the hint is
|
|
635
|
+
* irrelevant to any other failure, and attaching it anyway is the send-the-reader-to-the-wrong-
|
|
636
|
+
* place mistake it exists to prevent. Recorded, never matched on V8's wording. */
|
|
637
|
+
let wasRead = false;
|
|
638
|
+
Object.defineProperty(context, VALUE_KEY, {
|
|
639
|
+
configurable: true,
|
|
640
|
+
enumerable: true,
|
|
641
|
+
get: () => {
|
|
642
|
+
wasRead = true;
|
|
643
|
+
return resolved;
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
try {
|
|
647
|
+
return walk(item, scope, context, at, skip);
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
/**
|
|
651
|
+
* **`value` is `undefined` when no layer underneath set this key**, and a value written to
|
|
652
|
+
* extend an inherited list is also the *first* layer in a repository that inherits nothing.
|
|
653
|
+
* V8 reports that as `value is not iterable`, naming neither the key nor the reason.
|
|
654
|
+
*
|
|
655
|
+
* Here rather than in `callValueFn`, so the expression and the function spelling get the same
|
|
656
|
+
* sentence from the same place. `rmanValueHint` keeps a rethrow from stacking it twice as the
|
|
657
|
+
* error passes back up through the enclosing keys.
|
|
658
|
+
*
|
|
659
|
+
* Not papered over by defaulting `value` to `[]`: that would be a guess about the key's type,
|
|
660
|
+
* and wrong for every key that is not a list.
|
|
661
|
+
*/
|
|
662
|
+
if (wasRead && resolved === undefined && !e?.rmanValueHint) {
|
|
663
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
664
|
+
e.rmanValueHint = true;
|
|
665
|
+
e.message =
|
|
666
|
+
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${where}".` +
|
|
667
|
+
`\n Write \`value ?? []\` (or \`?? ''\`) if it has to work as the first layer too.`;
|
|
668
|
+
}
|
|
669
|
+
throw e;
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
if (outer)
|
|
673
|
+
Object.defineProperty(context, VALUE_KEY, outer);
|
|
674
|
+
else
|
|
675
|
+
delete context[VALUE_KEY];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** What a layer deriving from the one below it reads - see `walkWithPrevious`. */
|
|
679
|
+
const VALUE_KEY = 'value';
|
|
612
680
|
/**
|
|
613
681
|
* Whether a function at `at` is **code** - a step to run later, or part of a plugin - rather than a
|
|
614
682
|
* value to compute now.
|
|
@@ -646,12 +714,12 @@ function isCodePath(at) {
|
|
|
646
714
|
* anything. That is the same reason `FileScope` offers no way to change anything. Work goes in a
|
|
647
715
|
* step, which is the one thing rman runs on purpose and which can also be a function.
|
|
648
716
|
*/
|
|
649
|
-
function callValueFn(fn,
|
|
650
|
-
|
|
717
|
+
function callValueFn(fn, context, at) {
|
|
718
|
+
/** `value` arrives through the prototype, bound by `walkWithPrevious` for exactly this key - so
|
|
719
|
+
* nothing here may *read* it. Passing it in as an argument did, which tripped the "was it read"
|
|
720
|
+
* getter before the function ran and put the `value` hint on every unrelated failure (caught by
|
|
721
|
+
* the spec that exists for precisely that). */
|
|
651
722
|
const arg = Object.create(context);
|
|
652
|
-
/** Resolved the same way any other value is, so an inherited `${{ }}` string or a function under
|
|
653
|
-
* it is already finished by the time this one is handed it. */
|
|
654
|
-
const resolvedPrevious = previous === undefined ? undefined : walk(previous, scope, context, at, skip);
|
|
655
723
|
/**
|
|
656
724
|
* A getter only so the catch below can tell whether the function **actually read `value`**.
|
|
657
725
|
*
|
|
@@ -661,33 +729,14 @@ function callValueFn(fn, scope, context, at, skip) {
|
|
|
661
729
|
* exists to prevent. Recorded rather than inferred from the message, because matching on V8's
|
|
662
730
|
* wording is the other way to get this wrong.
|
|
663
731
|
*/
|
|
664
|
-
let valueRead = false;
|
|
665
|
-
Object.defineProperty(arg, 'value', {
|
|
666
|
-
enumerable: true,
|
|
667
|
-
get: () => {
|
|
668
|
-
valueRead = true;
|
|
669
|
-
return resolvedPrevious;
|
|
670
|
-
},
|
|
671
|
-
});
|
|
672
732
|
try {
|
|
673
733
|
return fn(arg);
|
|
674
734
|
}
|
|
675
735
|
catch (e) {
|
|
676
736
|
const where = at.length ? formatPath(at) : 'the config root';
|
|
677
|
-
/**
|
|
678
|
-
*
|
|
679
|
-
|
|
680
|
-
* nothing, and V8's report for that is `value is not iterable` - which names neither the key nor
|
|
681
|
-
* the reason, and sends the reader looking at their spread instead of at what is missing.
|
|
682
|
-
*
|
|
683
|
-
* Told rather than papered over: defaulting `value` to `[]` would be a guess about the key's
|
|
684
|
-
* type, and wrong for every key that is not a list.
|
|
685
|
-
*/
|
|
686
|
-
const hint = valueRead && resolvedPrevious === undefined
|
|
687
|
-
? `\n \`value\` is undefined here - nothing below this layer sets "${where}".` +
|
|
688
|
-
`\n Write \`value ?? []\` (or \`?? ''\`) if the function has to work as the first layer too.`
|
|
689
|
-
: '';
|
|
690
|
-
throw new Error(`Config function in "${where}" failed: ${e?.message}${hint}`, { cause: e });
|
|
737
|
+
/** The `value` hint comes from `walkWithPrevious`, which wraps this call and is the one place
|
|
738
|
+
* that knows whether `value` was read - so the expression spelling gets the same sentence. */
|
|
739
|
+
throw new Error(`Config function in "${where}" failed: ${e?.message}`, { cause: e });
|
|
691
740
|
}
|
|
692
741
|
}
|
|
693
742
|
function interpolateString(value, context, at) {
|
package/core/merge-config.d.ts
CHANGED
|
@@ -15,18 +15,29 @@ export declare const APPEND_PREFIX = "+";
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const ALWAYS_APPEND: readonly string[];
|
|
17
17
|
/**
|
|
18
|
-
* Where a
|
|
18
|
+
* Where a key keeps what it is replacing, so the replacement can be handed it back as `value`.
|
|
19
19
|
*
|
|
20
|
-
* A **symbol on
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* A **symbol-keyed chain on the containing object**, one entry per key, rather than something
|
|
21
|
+
* attached to the value itself. It started as a wrapper around a *function*, which is the only kind
|
|
22
|
+
* of value you can hang a property on - and that is exactly why it had to change: `value` belongs to
|
|
23
|
+
* an expression (`"${{ [...value, 'x'] }}"`) just as much as to a function, and a string cannot
|
|
24
|
+
* carry one.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* A symbol is invisible to `Object.entries`, `JSON.stringify` and js-yaml, so the chain travels
|
|
27
|
+
* through `mergeConfig`, `finalizeConfig` and `rman config` without any of them having to know it
|
|
28
|
+
* is there.
|
|
29
|
+
*
|
|
30
|
+
* Each entry is a link, not a single slot: three layers each deriving from the one below need
|
|
31
|
+
* `A <- expr2 <- expr3`, and one slot would have lost `A` the moment `expr3` arrived.
|
|
28
32
|
*/
|
|
29
|
-
export declare const
|
|
33
|
+
export declare const PREVIOUS_VALUES: unique symbol;
|
|
34
|
+
/** One link: the raw value this key held, and whatever *it* was derived from. */
|
|
35
|
+
export interface PreviousValue {
|
|
36
|
+
value: unknown;
|
|
37
|
+
previous?: PreviousValue;
|
|
38
|
+
}
|
|
39
|
+
/** Only these two can ask for `value`, so only these two are worth remembering a previous for. */
|
|
40
|
+
export declare function carriesPreviousValue(value: unknown): boolean;
|
|
30
41
|
/** `"+before"` -> `"before"`, or `undefined` for a key that isn't an append. */
|
|
31
42
|
export declare function appendTarget(key: string): string | undefined;
|
|
32
43
|
/**
|
package/core/merge-config.js
CHANGED
|
@@ -15,18 +15,26 @@ export const APPEND_PREFIX = '+';
|
|
|
15
15
|
*/
|
|
16
16
|
export const ALWAYS_APPEND = ['plugins'];
|
|
17
17
|
/**
|
|
18
|
-
* Where a
|
|
18
|
+
* Where a key keeps what it is replacing, so the replacement can be handed it back as `value`.
|
|
19
19
|
*
|
|
20
|
-
* A **symbol on
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* A **symbol-keyed chain on the containing object**, one entry per key, rather than something
|
|
21
|
+
* attached to the value itself. It started as a wrapper around a *function*, which is the only kind
|
|
22
|
+
* of value you can hang a property on - and that is exactly why it had to change: `value` belongs to
|
|
23
|
+
* an expression (`"${{ [...value, 'x'] }}"`) just as much as to a function, and a string cannot
|
|
24
|
+
* carry one.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* A symbol is invisible to `Object.entries`, `JSON.stringify` and js-yaml, so the chain travels
|
|
27
|
+
* through `mergeConfig`, `finalizeConfig` and `rman config` without any of them having to know it
|
|
28
|
+
* is there.
|
|
29
|
+
*
|
|
30
|
+
* Each entry is a link, not a single slot: three layers each deriving from the one below need
|
|
31
|
+
* `A <- expr2 <- expr3`, and one slot would have lost `A` the moment `expr3` arrived.
|
|
28
32
|
*/
|
|
29
|
-
export const
|
|
33
|
+
export const PREVIOUS_VALUES = Symbol('rman.previousValues');
|
|
34
|
+
/** Only these two can ask for `value`, so only these two are worth remembering a previous for. */
|
|
35
|
+
export function carriesPreviousValue(value) {
|
|
36
|
+
return typeof value === 'function' || (typeof value === 'string' && value.includes('${{'));
|
|
37
|
+
}
|
|
30
38
|
/** `"+before"` -> `"before"`, or `undefined` for a key that isn't an append. */
|
|
31
39
|
export function appendTarget(key) {
|
|
32
40
|
return key.length > APPEND_PREFIX.length && key.startsWith(APPEND_PREFIX)
|
|
@@ -121,6 +129,11 @@ export function finalizeConfig(config) {
|
|
|
121
129
|
const pending = finalizeConfig(value);
|
|
122
130
|
result[plain] = plain in result ? [...toList(result[plain]), ...toList(pending)] : toList(pending);
|
|
123
131
|
}
|
|
132
|
+
/** Carried across by hand: this rebuilds the object from `Object.entries`, which does not see a
|
|
133
|
+
* symbol - and dropping it here would lose every `value` chain the merge just recorded. */
|
|
134
|
+
const chain = config[PREVIOUS_VALUES];
|
|
135
|
+
if (chain)
|
|
136
|
+
Object.defineProperty(result, PREVIOUS_VALUES, { value: chain });
|
|
124
137
|
return result;
|
|
125
138
|
}
|
|
126
139
|
function assignMerged(target, key, value) {
|
|
@@ -131,41 +144,24 @@ function assignMerged(target, key, value) {
|
|
|
131
144
|
return;
|
|
132
145
|
}
|
|
133
146
|
/**
|
|
134
|
-
* A
|
|
135
|
-
* given it back as `value` when the config resolves:
|
|
147
|
+
* A value that can ask for `value` **replaces** like any other - and remembers what it replaced:
|
|
136
148
|
*
|
|
137
149
|
* ```js
|
|
138
|
-
* '[*]':
|
|
139
|
-
* '[ws:*]':
|
|
150
|
+
* '[*]': { clean: { include: ({ vars }) => [vars.buildDir] } }
|
|
151
|
+
* '[ws:*]': { clean: { include: "${{ [...value, pkg.basename + '.log'] }}" } }
|
|
140
152
|
* ```
|
|
141
153
|
*
|
|
142
154
|
* Chained here rather than at resolution time because only the merge knows the order of the
|
|
143
155
|
* layers - by the time `interpolateConfig` sees the config they have collapsed into one object,
|
|
144
156
|
* and whatever a closer layer said has already taken the place of what it was derived from.
|
|
145
157
|
*/
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
if (carriesPreviousValue(value) && key in target) {
|
|
159
|
+
const carrier = target;
|
|
160
|
+
const chain = (carrier[PREVIOUS_VALUES] ??= {});
|
|
161
|
+
chain[key] = { value: target[key], previous: chain[key] };
|
|
149
162
|
}
|
|
150
163
|
target[key] = Array.isArray(value) ? [...value] : value;
|
|
151
164
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Wraps `fn` so it carries `previous`, leaving `fn` itself alone.
|
|
154
|
-
*
|
|
155
|
-
* The wrapper forwards every argument unchanged, which is what lets one rule cover both kinds of
|
|
156
|
-
* function a config can hold: a **value** function is called by `interpolateConfig` with the config
|
|
157
|
-
* scope, a **step** function by `RunService` with a `RunStepContext`, and neither needs to know it
|
|
158
|
-
* has been wrapped. `name` is copied over because a step's label is its function's name.
|
|
159
|
-
*/
|
|
160
|
-
function chainValueFn(fn, previous) {
|
|
161
|
-
const wrapper = (...args) => fn(...args);
|
|
162
|
-
Object.defineProperty(wrapper, 'name', { value: fn.name, configurable: true });
|
|
163
|
-
/** Only when there *is* one: an own property set to `undefined` is indistinguishable from an
|
|
164
|
-
* inherited value that genuinely resolved to nothing. */
|
|
165
|
-
if (previous !== undefined)
|
|
166
|
-
Object.defineProperty(wrapper, PREVIOUS_VALUE, { value: previous });
|
|
167
|
-
return wrapper;
|
|
168
|
-
}
|
|
169
165
|
/**
|
|
170
166
|
* Appends `value` to `target[key]`, de-duplicating **only** an `ALWAYS_APPEND` key.
|
|
171
167
|
*
|