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,577 @@
|
|
|
1
|
+
import nodePath from 'node:path';
|
|
2
|
+
import vm from 'node:vm';
|
|
3
|
+
import { isPlainObject } from '@jsopen/objects';
|
|
4
|
+
import { CODE_SUBTREES, STEP_PATHS } from './config-paths.js';
|
|
5
|
+
import { ORIGINS, PREVIOUS_VALUES } from './merge-config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a config's values - every `${{ ... }}` expression and every value function - against a
|
|
8
|
+
* scope.
|
|
9
|
+
*
|
|
10
|
+
* Where `ConfigReader` answers *what the files say*, this answers *what those words evaluate to*.
|
|
11
|
+
* It takes a `ConfigScope` rather than a package, so whoever holds the repository builds the scope;
|
|
12
|
+
* `Repository.configScope` is that today.
|
|
13
|
+
*
|
|
14
|
+
* Two passes: {@link interpolate} resolves everything the scope can answer now, and
|
|
15
|
+
* {@link resolveDeferred} resolves what `defer(...)` left for later.
|
|
16
|
+
*/
|
|
17
|
+
/* The split from `ConfigReader` is a matter of lifetime rather than tidiness: reading happens per
|
|
18
|
+
* directory, before any package exists, while `pkg`, `repository`, `file` and `git` only exist
|
|
19
|
+
* afterwards. One class holding both would have a method that throws when called in the wrong
|
|
20
|
+
* order.
|
|
21
|
+
*
|
|
22
|
+
* **A `ConfigScope` rather than a `Package`** keeps this file free of `core/package.ts` - `core/`
|
|
23
|
+
* has a recorded history of fatal ESM cycles (`core/config` reaching into `plugins/` broke the
|
|
24
|
+
* built CLI on every command while the suite stayed green). Whoever builds the scope owns the
|
|
25
|
+
* measured subtleties in doing so: property descriptors rather than a spread, so `git`'s lazy
|
|
26
|
+
* getter does not shell out on a config that never mentions git, and `pkg.targetVersion` staying a
|
|
27
|
+
* non-enumerable throwing getter.
|
|
28
|
+
*
|
|
29
|
+
* **Every step is a `protected` method**, so a subclass can replace one and a spec can stub it. As
|
|
30
|
+
* module-level functions none of them would be reachable from outside this file. */
|
|
31
|
+
export class ConfigInterpolator {
|
|
32
|
+
/** How long one expression may run, in milliseconds. Raise it for a repository with genuinely
|
|
33
|
+
* slow `read()` calls. */
|
|
34
|
+
/* Guards against an expression that never returns (`while(true)`) taking the whole command with
|
|
35
|
+
* it - a typo rather than an attack, but the failure mode is identical. */
|
|
36
|
+
expressionTimeout = 1000;
|
|
37
|
+
/** The file the key being walked was written in, for an error message to name. */
|
|
38
|
+
/* A config is merged from several files before anything reads it, so `version.commitMessage`
|
|
39
|
+
* alone does not say where to go and look. `mergeConfig` records the file per key (`ORIGINS`);
|
|
40
|
+
* this is the depth-first cursor over that, kept as a field rather than threaded through every
|
|
41
|
+
* signature because every error site would otherwise carry a parameter it only passes on.
|
|
42
|
+
*
|
|
43
|
+
* **An instance field, where it used to be a module variable** - a module variable is shared by
|
|
44
|
+
* every interpolation in the process, so a failed walk left its cursor behind for the next one. */
|
|
45
|
+
currentOrigin;
|
|
46
|
+
/** How many `defer(...)` calls have been rewritten so far. */
|
|
47
|
+
/* `_walk` compares it before and after a key to learn whether anything under that key deferred,
|
|
48
|
+
* which is what `DEFERRED` records - see `resolveDeferred`. */
|
|
49
|
+
deferCount = 0;
|
|
50
|
+
/**
|
|
51
|
+
* Pass one: resolves everything the scope can answer now, and leaves `defer(...)` for
|
|
52
|
+
* {@link resolveDeferred}.
|
|
53
|
+
*
|
|
54
|
+
* A string that is *nothing but* one expression keeps that value's own type -
|
|
55
|
+
* `"${{ pkg.private }}"` is a boolean, not the word `true`. Embedded in surrounding text it is
|
|
56
|
+
* stringified, and a nullish result there is an error rather than the word `undefined`.
|
|
57
|
+
*
|
|
58
|
+
* Returns `Resolved<T>`: every value function has been called, so they are gone from the type.
|
|
59
|
+
*/
|
|
60
|
+
/* The sole-expression rule is what makes a boolean setting like `run.<script>.skip` reachable
|
|
61
|
+
* from an expression at all - without it this could only ever produce strings.
|
|
62
|
+
*
|
|
63
|
+
* `Resolved<T>` is what lets a reader hold a resolved config with no cast anywhere between. */
|
|
64
|
+
interpolate(args) {
|
|
65
|
+
const { config, scope } = args;
|
|
66
|
+
const base = args.at ?? [];
|
|
67
|
+
const skip = args.skip ?? [];
|
|
68
|
+
const context = this._createContext(scope);
|
|
69
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
70
|
+
return this._walk(config, scope, context, base, skip);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The config's own top-level keys, readable bare: `${{ changelog.filePath }}`. So a value that
|
|
74
|
+
* restates another stops being a second copy that drifts.
|
|
75
|
+
*
|
|
76
|
+
* Resolved **on demand**, one key at a time, and memoized. Interpolating in tree order and
|
|
77
|
+
* handing an expression whatever happened to be ready would make the answer depend on key order
|
|
78
|
+
* in the file - a key declared above reading as resolved and one below as raw.
|
|
79
|
+
*/
|
|
80
|
+
const resolved = new Map();
|
|
81
|
+
const resolving = [];
|
|
82
|
+
/**
|
|
83
|
+
* A cycle is **recorded here rather than thrown from the getter**: a host getter that throws
|
|
84
|
+
* inside a `vm` property interceptor has its exception swallowed, and V8 then reports the
|
|
85
|
+
* global as absent - so a self-referencing key came out as `publish is not defined`, sending
|
|
86
|
+
* the reader after a missing key instead of a loop.
|
|
87
|
+
*/
|
|
88
|
+
let cycle;
|
|
89
|
+
const deferredKeys = {};
|
|
90
|
+
const resolve = (key) => {
|
|
91
|
+
if (resolved.has(key))
|
|
92
|
+
return resolved.get(key);
|
|
93
|
+
if (resolving.includes(key)) {
|
|
94
|
+
cycle ??= new Error(`Config expression forms a cycle: ${[...resolving, key].join(' -> ')}\n` +
|
|
95
|
+
` A value cannot be derived from itself, directly or through another key.`);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
resolving.push(key);
|
|
99
|
+
const before = this.deferCount;
|
|
100
|
+
try {
|
|
101
|
+
const chain = config[PREVIOUS_VALUES];
|
|
102
|
+
const origins = config[ORIGINS];
|
|
103
|
+
const value = this._withOrigin(origins?.[key], () => this._walkWithPrevious(config[key], chain?.[key], scope, context, [...base, key], skip));
|
|
104
|
+
if (this.deferCount > before)
|
|
105
|
+
deferredKeys[key] = true;
|
|
106
|
+
resolved.set(key, value);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
/** Not cleared: a cycle aborts the whole interpolation, and each level up would otherwise
|
|
111
|
+
* re-swallow its own replacement - leaving it set lets the outermost frame report it. */
|
|
112
|
+
if (cycle)
|
|
113
|
+
throw new Error(`${String(e?.message).split('\n')[0]}\n ${cycle.message}`, { cause: e });
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
resolving.pop();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const key of Object.keys(config)) {
|
|
121
|
+
/** A scope binding wins: `pkg`/`repository`/`env`/`semver` are not config keys, so nothing
|
|
122
|
+
* collides today, and a future key that did must not silently take over the namespace. */
|
|
123
|
+
if (key in scope || !IDENTIFIER.test(key))
|
|
124
|
+
continue;
|
|
125
|
+
Object.defineProperty(context, key, { enumerable: true, configurable: true, get: () => resolve(key) });
|
|
126
|
+
}
|
|
127
|
+
/** Built through the same memo the getters use, so every key is walked exactly once whether an
|
|
128
|
+
* expression asked for it first or the result did. */
|
|
129
|
+
const result = {};
|
|
130
|
+
for (const key of Object.keys(config))
|
|
131
|
+
result[key] = resolve(key);
|
|
132
|
+
this._markDeferred(result, deferredKeys);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Pass two: resolves what pass one deferred, against a scope that can now answer it.
|
|
137
|
+
*
|
|
138
|
+
* `version` is the case it exists for - `${{ defer(pkg.targetVersion) }}` in a `version` hook
|
|
139
|
+
* names a version that does not exist while the config is being read.
|
|
140
|
+
*
|
|
141
|
+
* Only the keys pass one marked are walked; a `${{` that arrived any other way is left exactly as
|
|
142
|
+
* it is. A string handed over on its own has no containing object to carry a mark, so it is
|
|
143
|
+
* interpolated outright - the caller passing one has already decided.
|
|
144
|
+
*/
|
|
145
|
+
/* **Walking marks rather than scanning for `${{` is the whole safety of running twice.** Pass
|
|
146
|
+
* one's output legitimately contains `${{` in cases that have nothing to do with `defer`: the
|
|
147
|
+
* documented way to emit a literal is `${{ '${{' }}` (a `helm template --set tag=...` line needs
|
|
148
|
+
* it), and `read()` can return a string holding one. Scanning would execute both. */
|
|
149
|
+
resolveDeferred(args) {
|
|
150
|
+
const context = this._createContext(args.scope);
|
|
151
|
+
return this._walkDeferred(args.config, context, args.at ?? []);
|
|
152
|
+
}
|
|
153
|
+
/** Whether anything under `config` is waiting for {@link resolveDeferred} - `false` for every
|
|
154
|
+
* config that never writes `defer(...)`, so the second pass can be skipped. */
|
|
155
|
+
hasDeferred(config) {
|
|
156
|
+
if (!isPlainObject(config))
|
|
157
|
+
return false;
|
|
158
|
+
return config[DEFERRED] !== undefined;
|
|
159
|
+
}
|
|
160
|
+
/** The V8 context an expression is evaluated in - a clean scope holding only `scope`'s bindings. */
|
|
161
|
+
/* **Built from `scope`'s property descriptors, never `{ ...scope }`.** A spread reads every
|
|
162
|
+
* property, so a lazy getter on the scope stops being lazy the moment one is added - and `git` is
|
|
163
|
+
* exactly that: it shells out to `git rev-parse`, and a spread here would do it on `rman list`,
|
|
164
|
+
* `rman info` and every other command, in a repository whose config never mentions git. Measured
|
|
165
|
+
* both ways on one build: 0 git reads with descriptors, 1 with a spread.
|
|
166
|
+
*
|
|
167
|
+
* A clean scope, **not a sandbox** - `node:vm` is explicitly not a security mechanism, and none is
|
|
168
|
+
* called for: a config that can say `exec: "..."` already runs arbitrary shell, so the expression
|
|
169
|
+
* evaluator adds no trust boundary that was not already wide open. */
|
|
170
|
+
_createContext(scope) {
|
|
171
|
+
return vm.createContext(Object.defineProperties({}, Object.getOwnPropertyDescriptors(scope)));
|
|
172
|
+
}
|
|
173
|
+
_walk(value, scope, context, at, skip) {
|
|
174
|
+
/** Compared on the key path rather than the value, so a skipped key's whole subtree - a single
|
|
175
|
+
* command or an array of them - is handed on untouched. */
|
|
176
|
+
if (at.length && skip.includes(at.filter(p => typeof p === 'string').join('.')))
|
|
177
|
+
return value;
|
|
178
|
+
if (typeof value === 'function') {
|
|
179
|
+
/** Code, not a value: a step for `run`/`version` to call in its own time, or a plugin's own
|
|
180
|
+
* function. Calling it here would run build-time work while merely *loading* the repository,
|
|
181
|
+
* which is the whole distinction the function form exists to draw. */
|
|
182
|
+
if (this._isCodePath(at))
|
|
183
|
+
return value;
|
|
184
|
+
return this._callValueFn(value, context, at);
|
|
185
|
+
}
|
|
186
|
+
if (typeof value === 'string')
|
|
187
|
+
return this._interpolateString(value, context, at);
|
|
188
|
+
if (Array.isArray(value))
|
|
189
|
+
return value.map((item, i) => this._walk(item, scope, context, [...at, i], skip));
|
|
190
|
+
if (value && typeof value === 'object') {
|
|
191
|
+
const chain = value[PREVIOUS_VALUES];
|
|
192
|
+
return this._withScopedVars(value, scope, context, at, skip, () => {
|
|
193
|
+
const result = {};
|
|
194
|
+
const origins = value[ORIGINS];
|
|
195
|
+
const deferredKeys = {};
|
|
196
|
+
for (const [key, item] of Object.entries(value)) {
|
|
197
|
+
const before = this.deferCount;
|
|
198
|
+
result[key] = this._withOrigin(origins?.[key], () => this._walkWithPrevious(item, chain?.[key], scope, context, [...at, key], skip));
|
|
199
|
+
if (this.deferCount > before)
|
|
200
|
+
deferredKeys[key] = true;
|
|
201
|
+
}
|
|
202
|
+
this._markDeferred(result, deferredKeys);
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Runs `body` with `vars` scoped to this node: a fresh copy at every level, with the node's own
|
|
210
|
+
* `vars` block - if it declares one - merged **per key** over what the level above resolved to,
|
|
211
|
+
* so redeclaring one var keeps the rest.
|
|
212
|
+
*
|
|
213
|
+
* The node's own block is resolved against the outer scope before being installed, so
|
|
214
|
+
* `vars: { out: '${{ vars.x }}/dist' }` refines the `x` it inherits rather than reading itself.
|
|
215
|
+
*/
|
|
216
|
+
/* **Copied at every node, not only where a `vars` block appears**, and that is the difference
|
|
217
|
+
* between scoping and leaking: a value function is handed this object, so one that writes to it
|
|
218
|
+
* must not be writing into the level above. */
|
|
219
|
+
_withScopedVars(node, scope, context, at, skip, body) {
|
|
220
|
+
/**
|
|
221
|
+
* **A `vars` block does not scope itself.** Resolving one walks its own values, and without
|
|
222
|
+
* this that walk asks for the scope it is in the middle of producing - which the cycle guard
|
|
223
|
+
* catches and reports as `vars -> vars`. It recovered, but left the cycle *flag* set, and the
|
|
224
|
+
* next genuine error in that key came out wearing `Config expression forms a cycle`.
|
|
225
|
+
*/
|
|
226
|
+
if (at.some(segment => segment === VARS_KEY))
|
|
227
|
+
return body();
|
|
228
|
+
const outer = context[VARS_KEY];
|
|
229
|
+
const own = node[VARS_KEY];
|
|
230
|
+
/** Nothing to shadow and nothing to protect: a node with no object below it can hold no
|
|
231
|
+
* function either, so the copy would be pure cost. */
|
|
232
|
+
if (own === undefined && !hasObjectChild(node))
|
|
233
|
+
return body();
|
|
234
|
+
const resolvedOwn = own === undefined ? undefined : this._walk(own, scope, context, [...at, VARS_KEY], skip);
|
|
235
|
+
const scoped = { ...outer, ...(isPlainObject(resolvedOwn) ? resolvedOwn : undefined) };
|
|
236
|
+
const previous = Object.getOwnPropertyDescriptor(context, VARS_KEY);
|
|
237
|
+
Object.defineProperty(context, VARS_KEY, { value: scoped, enumerable: true, configurable: true, writable: true });
|
|
238
|
+
try {
|
|
239
|
+
return body();
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
if (previous)
|
|
243
|
+
Object.defineProperty(context, VARS_KEY, previous);
|
|
244
|
+
else
|
|
245
|
+
delete context[VARS_KEY];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Walks one key with `value` bound to what the layers underneath resolved to.
|
|
250
|
+
*
|
|
251
|
+
* Always bound, even with nothing underneath - see {@link _previousValue} for what it is then.
|
|
252
|
+
* The chain resolves bottom-up, so a layer deriving from a layer that itself derived from
|
|
253
|
+
* something is handed the finished value rather than a half-resolved expression.
|
|
254
|
+
*/
|
|
255
|
+
/* Left unbound, an expression naming `value` fails with V8's `value is not defined`, which reads
|
|
256
|
+
* as "there is no such thing" rather than "nothing below this layer set it" - two different
|
|
257
|
+
* mistakes needing two different fixes. */
|
|
258
|
+
_walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
259
|
+
const resolved = this._previousValue(previous === undefined
|
|
260
|
+
? undefined
|
|
261
|
+
: this._walkWithPrevious(previous.value, previous.previous, scope, context, at, skip), at);
|
|
262
|
+
const outer = Object.getOwnPropertyDescriptor(context, VALUE_KEY);
|
|
263
|
+
/** A getter, so the catch below can tell whether the value **actually read `value`**: the hint
|
|
264
|
+
* is irrelevant to any other failure, and attaching it anyway is the send-the-reader-to-the-
|
|
265
|
+
* wrong-place mistake it exists to prevent. Recorded, never matched on V8's wording. */
|
|
266
|
+
let wasRead = false;
|
|
267
|
+
Object.defineProperty(context, VALUE_KEY, {
|
|
268
|
+
configurable: true,
|
|
269
|
+
enumerable: true,
|
|
270
|
+
get: () => {
|
|
271
|
+
wasRead = true;
|
|
272
|
+
return resolved;
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
return this._walk(item, scope, context, at, skip);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
if (wasRead && isUnsetValue(resolved) && !e?.rmanValueHint) {
|
|
280
|
+
e.rmanValueHint = true;
|
|
281
|
+
e.message = `${e.message}\n Note: nothing below this layer sets "${this._describeAt(at)}, so \`value\` is empty.`;
|
|
282
|
+
}
|
|
283
|
+
throw e;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
if (outer)
|
|
287
|
+
Object.defineProperty(context, VALUE_KEY, outer);
|
|
288
|
+
else
|
|
289
|
+
delete context[VALUE_KEY];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Whether a function at `at` is **code** - a step to run later, or part of a plugin - rather than
|
|
294
|
+
* a value to compute now.
|
|
295
|
+
*
|
|
296
|
+
* Array indices are dropped before matching, so a function inside a *list* of steps is still a
|
|
297
|
+
* step; `*` in a `STEP_PATHS` entry matches any one segment (`run.<script>.exec`).
|
|
298
|
+
*/
|
|
299
|
+
_isCodePath(at) {
|
|
300
|
+
const segments = at.filter((p) => typeof p === 'string');
|
|
301
|
+
if (CODE_SUBTREES.includes(segments[0]))
|
|
302
|
+
return true;
|
|
303
|
+
return STEP_PATHS.some(pattern => {
|
|
304
|
+
const parts = pattern.split('.');
|
|
305
|
+
return parts.length === segments.length && parts.every((part, i) => part === '*' || part === segments[i]);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Calls a **value** function: the JS spelling of a `${{ }}` expression, answering the same
|
|
310
|
+
* question at the same moment.
|
|
311
|
+
*
|
|
312
|
+
* It is handed one object carrying everything an expression can name, plus `value` - what this
|
|
313
|
+
* key resolved to in the layers underneath.
|
|
314
|
+
*
|
|
315
|
+
* A value function **computes and returns; it must never act.** This runs while the config
|
|
316
|
+
* resolves, which *every* command does - so one that writes a file writes it on `rman list`,
|
|
317
|
+
* `rman info` and `rman config` too, once per package, with nothing having asked. Work goes in a
|
|
318
|
+
* step.
|
|
319
|
+
*/
|
|
320
|
+
/* The argument is built with the interpolation context as its **prototype**, not copied from it.
|
|
321
|
+
* The top-level keys are lazy memoized getters; spreading them into a new object would fire every
|
|
322
|
+
* one on every call, including the ones a function never reads - and one of those throwing would
|
|
323
|
+
* blame the wrong key. */
|
|
324
|
+
_callValueFn(fn, context, at) {
|
|
325
|
+
/** `value` arrives through the prototype, bound by `_walkWithPrevious` for exactly this key -
|
|
326
|
+
* so nothing here may *read* it. Passing it in as an argument did, which tripped the "was it
|
|
327
|
+
* read" getter before the function ran and put the `value` hint on every unrelated failure. */
|
|
328
|
+
const arg = Object.create(context);
|
|
329
|
+
try {
|
|
330
|
+
return fn(arg);
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
throw new Error(`Config function in "${this._describeAt(at)} failed: ${e?.message}`, { cause: e });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
_interpolateString(value, context, at) {
|
|
337
|
+
if (!value.includes('${{'))
|
|
338
|
+
return value;
|
|
339
|
+
const found = [...value.matchAll(EXPRESSION)];
|
|
340
|
+
if (!found.length)
|
|
341
|
+
return value;
|
|
342
|
+
/** Counted rather than matched with an anchored `^...$` regex: a lazy quantifier still
|
|
343
|
+
* backtracks to satisfy an end anchor, so `"${{ a }} and ${{ b }}"` looked like *one*
|
|
344
|
+
* expression whose body ran from `a` to `b`, brace-ends and all - invalid JavaScript. */
|
|
345
|
+
const soleExpression = found.length === 1 && found[0][0] === value.trim();
|
|
346
|
+
// Alone, a nullish result is just "this setting is unset" - a legitimate answer.
|
|
347
|
+
if (soleExpression)
|
|
348
|
+
return this._evaluate(found[0][1], value, context, at);
|
|
349
|
+
return value.replace(EXPRESSION, (_, expr) => {
|
|
350
|
+
const result = this._evaluate(expr, value, context, at);
|
|
351
|
+
/** Embedded in text it never is: splicing in the word "undefined" produces a path or tag like
|
|
352
|
+
* `app:undefined` that looks plausible and is wrong - the exact silent-mistake shape this
|
|
353
|
+
* evaluator exists to avoid. `?? 'fallback'` says what was meant. */
|
|
354
|
+
if (result === undefined || result === null) {
|
|
355
|
+
throw new Error(`Expression in "${this._describeAt(at)} is ${result} inside a string: ${value.trim()}\n` +
|
|
356
|
+
` \${{${expr}}} has no value here - give it a fallback (\${{${expr.trim()} ?? '...'}}).`);
|
|
357
|
+
}
|
|
358
|
+
return String(result);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/** Evaluates one expression, rewriting any `defer(...)` in it first. A failure names the config
|
|
362
|
+
* path and the expression. */
|
|
363
|
+
/* Naming the path matters: an error saying only "x is not defined" sends the reader hunting
|
|
364
|
+
* through a file that may hold dozens of them. */
|
|
365
|
+
_evaluate(expr, source, context, at) {
|
|
366
|
+
const prepared = this._rewriteDefer(expr, source, at);
|
|
367
|
+
try {
|
|
368
|
+
return vm.runInContext(prepared, context, { timeout: this.expressionTimeout });
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
throw new Error(`Invalid expression in "${this._describeAt(at)}: ${source.trim()}\n ${e?.message ?? e}`, {
|
|
372
|
+
cause: e,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Turns `defer(<inner>)` into a string literal holding `${{ <inner> }}`, so the expression around
|
|
378
|
+
* it resolves now and the deferred part survives as an expression for pass two.
|
|
379
|
+
*
|
|
380
|
+
* ```
|
|
381
|
+
* '_' + defer(pkg.targetVersion) + '-' + pkg.name
|
|
382
|
+
* '_' + "${{ pkg.targetVersion }}" + '-' + pkg.name // rewritten
|
|
383
|
+
* _${{ pkg.targetVersion }}-mypackage // evaluated
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* `defer` is a marker the source is rewritten around, not a function in scope - so
|
|
387
|
+
* `defer(() => x)` is not a thing to write. A nested `defer` is refused.
|
|
388
|
+
*/
|
|
389
|
+
/* **Why a source rewrite rather than a function.** JavaScript evaluates arguments first, so
|
|
390
|
+
* `defer(pkg.targetVersion)` would read `pkg.targetVersion` - a *throwing* getter until `version`
|
|
391
|
+
* binds it - before `defer` ran. A function also only ever receives its argument's value, never
|
|
392
|
+
* the source text that has to survive.
|
|
393
|
+
*
|
|
394
|
+
* **The closing parenthesis is found by asking V8, not by counting.** `defer(x ? ')' : y)` and
|
|
395
|
+
* `defer(read('a)b.json').v)` both hold a `)` that closes nothing, and a hand-rolled scanner has
|
|
396
|
+
* to know about strings, template literals and regex literals to see that. Each candidate span is
|
|
397
|
+
* handed to the parser instead, and the first that parses is the answer - correct by construction,
|
|
398
|
+
* since the parser is the authority on what is a complete expression. The same family of mistake
|
|
399
|
+
* is already recorded here for `${{ }}` itself, where an anchored regex backtracked across two
|
|
400
|
+
* expressions. */
|
|
401
|
+
_rewriteDefer(expr, source, at) {
|
|
402
|
+
if (!DEFER_TOKEN.test(expr))
|
|
403
|
+
return expr;
|
|
404
|
+
/**
|
|
405
|
+
* **A fresh `/g` regex per call, never a shared one.** A global regex carries `lastIndex`
|
|
406
|
+
* between uses, so one that throws mid-scan leaves it set and the *next* expression is searched
|
|
407
|
+
* from the middle - measured here: a case that refused a malformed `defer()` left the cursor
|
|
408
|
+
* behind, and the following `${{ defer(pkg.x) }}` was not recognized at all and reached V8 as
|
|
409
|
+
* `defer is not defined`. `DEFER_TOKEN` itself is deliberately not global for the same reason.
|
|
410
|
+
*/
|
|
411
|
+
const token = new RegExp(DEFER_TOKEN.source, 'g');
|
|
412
|
+
let out = '';
|
|
413
|
+
let cursor = 0;
|
|
414
|
+
for (let match = token.exec(expr); match; match = token.exec(expr)) {
|
|
415
|
+
const open = match.index + match[0].length; // first character inside the parentheses
|
|
416
|
+
const inner = this._findDeferArgument(expr, open, source, at);
|
|
417
|
+
out += expr.slice(cursor, match.index) + match[1];
|
|
418
|
+
out += JSON.stringify(`\${{ ${inner.trim()} }}`);
|
|
419
|
+
cursor = open + inner.length + 1; // past the closing parenthesis
|
|
420
|
+
token.lastIndex = cursor;
|
|
421
|
+
this.deferCount++;
|
|
422
|
+
}
|
|
423
|
+
return out + expr.slice(cursor);
|
|
424
|
+
}
|
|
425
|
+
/** The text between `defer(` and the parenthesis that closes it. */
|
|
426
|
+
/* The parser decides where that is rather than a counter - see `_rewriteDefer`. */
|
|
427
|
+
_findDeferArgument(expr, open, source, at) {
|
|
428
|
+
for (let end = expr.indexOf(')', open); end !== -1; end = expr.indexOf(')', end + 1)) {
|
|
429
|
+
const inner = expr.slice(open, end);
|
|
430
|
+
try {
|
|
431
|
+
new Function(`return (${inner})`);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* **A nested `defer` is refused rather than half-handled** - it would need a third pass and
|
|
438
|
+
* means nothing anyone has asked for.
|
|
439
|
+
*
|
|
440
|
+
* There is deliberately no check for `}}` inside the argument, though it would close the
|
|
441
|
+
* expression being written: it cannot arrive. `EXPRESSION` is non-greedy, so `${{ }}` has
|
|
442
|
+
* already been cut at the first `}}` long before this sees the text - which is a limitation of
|
|
443
|
+
* the delimiter itself and has nothing to do with `defer`.
|
|
444
|
+
*/
|
|
445
|
+
if (DEFER_TOKEN.test(inner)) {
|
|
446
|
+
throw new Error(`Nested defer() in "${this._describeAt(at)}: ${source.trim()}`);
|
|
447
|
+
}
|
|
448
|
+
return inner;
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`Unclosed defer() in "${this._describeAt(at)}: ${source.trim()}`);
|
|
451
|
+
}
|
|
452
|
+
/** Records which of `node`'s keys deferred, invisibly to anything but pass two. */
|
|
453
|
+
/* **Non-enumerable**, like `ORIGINS` and `PREVIOUS_VALUES`: `expect`'s `toEqual` compares symbol
|
|
454
|
+
* properties, and `js-yaml` and `JSON.stringify` would otherwise carry it into `rman config`. */
|
|
455
|
+
_markDeferred(node, keys) {
|
|
456
|
+
if (!Object.keys(keys).length)
|
|
457
|
+
return;
|
|
458
|
+
Object.defineProperty(node, DEFERRED, { value: keys, enumerable: false, configurable: true });
|
|
459
|
+
}
|
|
460
|
+
/** Pass two's walk: only the keys pass one marked, so a `${{` that arrived from anywhere else is
|
|
461
|
+
* left exactly as it is. */
|
|
462
|
+
_walkDeferred(value, context, at) {
|
|
463
|
+
if (typeof value === 'string')
|
|
464
|
+
return this._interpolateString(value, context, at);
|
|
465
|
+
if (Array.isArray(value))
|
|
466
|
+
return value.map((item, i) => this._walkDeferred(item, context, [...at, i]));
|
|
467
|
+
if (!isPlainObject(value))
|
|
468
|
+
return value;
|
|
469
|
+
const marks = value[DEFERRED];
|
|
470
|
+
if (!marks)
|
|
471
|
+
return value;
|
|
472
|
+
const result = { ...value };
|
|
473
|
+
for (const key of Object.keys(marks)) {
|
|
474
|
+
result[key] = this._walkDeferred(value[key], context, [...at, key]);
|
|
475
|
+
}
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
_withOrigin(origin, body) {
|
|
479
|
+
const outer = this.currentOrigin;
|
|
480
|
+
if (origin !== undefined)
|
|
481
|
+
this.currentOrigin = origin;
|
|
482
|
+
try {
|
|
483
|
+
return body();
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
this.currentOrigin = outer;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** `"version.commitMessage" (.rmanrc.yml)`, or just the path when nothing recorded a file. The
|
|
490
|
+
* file is relative to the working directory when it sits inside it. */
|
|
491
|
+
_describeAt(at) {
|
|
492
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
493
|
+
return this.currentOrigin ? `${where}" (${shortenOrigin(this.currentOrigin)})` : `${where}"`;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* What a layer deriving from the one below it reads as `value`.
|
|
497
|
+
*
|
|
498
|
+
* Spreads as empty when nothing below set the key, so `[...value, 'x']` needs no guard. Used as a
|
|
499
|
+
* string or a number it throws, naming the key and what to write instead.
|
|
500
|
+
*/
|
|
501
|
+
/* The empty case is not exotic: a value written to extend an inherited list is also the *first*
|
|
502
|
+
* layer in a repository that inherits nothing.
|
|
503
|
+
*
|
|
504
|
+
* Refusing to become a string or a number is a correction to an older `undefined`: `undefined + 1`
|
|
505
|
+
* is `NaN`, which serialized to `null` and read like a configured value. */
|
|
506
|
+
_previousValue(raw, at) {
|
|
507
|
+
/** Never a list, and unfixable as one. */
|
|
508
|
+
if (typeof raw === 'boolean')
|
|
509
|
+
return raw;
|
|
510
|
+
const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
|
|
511
|
+
Object.defineProperty(list, UNSET_MARKER, { value: raw === undefined });
|
|
512
|
+
return Object.defineProperty(list, Symbol.toPrimitive, {
|
|
513
|
+
value: (hint) => {
|
|
514
|
+
if (raw === undefined) {
|
|
515
|
+
throw this._unusable(at, 'nothing below this layer sets it, so it is empty', "`value ?? ''`, `value ?? 0`");
|
|
516
|
+
}
|
|
517
|
+
if (typeof raw !== 'string' && typeof raw !== 'number') {
|
|
518
|
+
throw this._unusable(at, `the layer below it is ${Array.isArray(raw) ? 'a list' : 'an object'}`, '`value.join(", ")` for a list');
|
|
519
|
+
}
|
|
520
|
+
return hint === 'string' ? String(raw) : raw;
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
/** The one sentence both `value` refusals share: what was asked for, why it cannot be done, what
|
|
525
|
+
* to write instead. */
|
|
526
|
+
/* Marked `rmanValueHint` so `_walkWithPrevious`'s catch leaves it alone - that note exists to
|
|
527
|
+
* explain an empty `value` to an error that does not mention it, and this error *is* that
|
|
528
|
+
* explanation. */
|
|
529
|
+
_unusable(at, because, instead) {
|
|
530
|
+
const error = new Error(`\`value\` cannot be used as a string or a number here - ${because}, for "${this._describeAt(at)}. ` +
|
|
531
|
+
`It spreads as a list (\`[...value, x]\`); to use it as something else, say what it should be - ${instead}.`);
|
|
532
|
+
error.rmanValueHint = true;
|
|
533
|
+
return error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** `${{ ... }}`, deliberately not `{{ ... }}`: a config value may legitimately carry `{{...}}` meant
|
|
537
|
+
* for something else (`helm template --set tag={{.Values.tag}}`), and with the plainer delimiter
|
|
538
|
+
* rman would try to evaluate it. Non-greedy, and never used with an end anchor - see
|
|
539
|
+
* `_interpolateString`. */
|
|
540
|
+
const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
|
|
541
|
+
/** `defer(` as a *token*: preceded by nothing that would make it a property (`x.defer(`) or part of
|
|
542
|
+
* a longer name (`mydefer(`). Group 1 is that preceding character, put back by the rewrite.
|
|
543
|
+
*
|
|
544
|
+
* **Not global**, so `.test()` on it carries no `lastIndex` between calls; `_rewriteDefer` builds
|
|
545
|
+
* its own `/g` copy for the one scan that needs one. */
|
|
546
|
+
const DEFER_TOKEN = /(^|[^A-Za-z0-9_$.])defer\s*\(/;
|
|
547
|
+
/** A config key readable bare inside an expression has to be a valid identifier - `"[*]"` is not. */
|
|
548
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
549
|
+
/** The one key that scopes rather than configures. Reserved at **every** level, which costs a
|
|
550
|
+
* script that would have been called `vars`: `run.vars` is a scope, not a script. */
|
|
551
|
+
const VARS_KEY = 'vars';
|
|
552
|
+
/** What a layer deriving from the one below it reads. */
|
|
553
|
+
const VALUE_KEY = 'value';
|
|
554
|
+
/** Marks that a layer set nothing - by the marker `_previousValue` puts on it, never by emptiness,
|
|
555
|
+
* since a layer may legitimately resolve to `[]`. */
|
|
556
|
+
const UNSET_MARKER = Symbol('rman.valueUnset');
|
|
557
|
+
/** Which of an object's keys hold something `defer(...)` left for pass two. */
|
|
558
|
+
const DEFERRED = Symbol('rman.deferred');
|
|
559
|
+
function isUnsetValue(value) {
|
|
560
|
+
return Array.isArray(value) && value[UNSET_MARKER] === true;
|
|
561
|
+
}
|
|
562
|
+
function hasObjectChild(node) {
|
|
563
|
+
for (const item of Object.values(node)) {
|
|
564
|
+
if (typeof item === 'function')
|
|
565
|
+
return true;
|
|
566
|
+
if (item && typeof item === 'object')
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
function formatPath(at) {
|
|
572
|
+
return at.reduce((acc, part) => (typeof part === 'number' ? `${acc}[${part}]` : acc ? `${acc}.${part}` : String(part)), '');
|
|
573
|
+
}
|
|
574
|
+
function shortenOrigin(file) {
|
|
575
|
+
const relative = nodePath.relative(process.cwd(), file);
|
|
576
|
+
return !relative.startsWith('..') && !nodePath.isAbsolute(relative) ? relative : file;
|
|
577
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Which config paths are code rather than data**, and which are left for later.
|
|
3
|
+
*
|
|
4
|
+
* Three lists, read by `ConfigInterpolator` as it walks and by the commands that own the keys. Not
|
|
5
|
+
* members of that class, and that is the exception CLAUDE.md records: `CodeSubtree` is derived from
|
|
6
|
+
* `CODE_SUBTREES` with `(typeof ...)[number]`, and a class field has no type to derive from. The
|
|
7
|
+
* test is whether a *type* reads the value, not whether the value feels constant.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Keys whose **whole subtree** is code rather than config, so no function under them is a value to
|
|
11
|
+
* compute.
|
|
12
|
+
*
|
|
13
|
+
* The three contribution keys, and each has to be here: an entry may be the *instance* itself, and
|
|
14
|
+
* a `Plugin` is almost entirely functions - `manifestProvider.read`, `getWorkspace`,
|
|
15
|
+
* `getBinPaths`, `versionPlanner` - while a command is often a bare factory and a publish target
|
|
16
|
+
* carries `getPlan`/`applyPlan`.
|
|
17
|
+
*
|
|
18
|
+
* Measured twice, once per shape. With `plugins` walked like any other key, resolving the config
|
|
19
|
+
* of a repository that named a plugin called that plugin's yargs builder with the config scope:
|
|
20
|
+
* `Config function in "plugins[0].commands[0].builder" failed: cmd.option is not a function`. And
|
|
21
|
+
* with `commands` left out of this list, a declarative command - which *is* a function - was
|
|
22
|
+
* invoked with the interpolation scope instead of the application, so its handler closed over a
|
|
23
|
+
* repository that was not one: `repository.getPackages is not a function`, from inside `clean`.
|
|
24
|
+
*
|
|
25
|
+
* These entries are loaded by `loadPlugins` and `cli.ts`, never read as settings.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CODE_SUBTREES: readonly ["plugins", "platforms", "commands", "publishTargets"];
|
|
28
|
+
/**
|
|
29
|
+
* Paths whose value is a **step** - something to run later - rather than a setting to compute now.
|
|
30
|
+
* `*` matches one path segment (`run.<script>.exec`).
|
|
31
|
+
*
|
|
32
|
+
* This is what tells a step function from a value function, and the two live side by side in one
|
|
33
|
+
* config:
|
|
34
|
+
*
|
|
35
|
+
* ```js
|
|
36
|
+
* '[*]': {
|
|
37
|
+
* clean: { include: ({ vars, value }) => [...value, vars.buildDir] }, // a value: called here
|
|
38
|
+
* run: { build: { after: ({ pkg }) => copyDocs(pkg) } }, // a step: called by `run`
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* **The key decides, and it already did.** `run.build.exec: 'tsc -b'` is a shell command and
|
|
43
|
+
* `publish.npm.directory: 'build'` is a path - not because of anything about the strings, but because of
|
|
44
|
+
* where they sit. A function inherits the same rule, so nothing new has to be learned and no marker
|
|
45
|
+
* has to be remembered. The alternative was inspecting the function (arity, parameter names), which
|
|
46
|
+
* is the kind of guess `loadPlugins` refuses to make about a module's export for the same reason:
|
|
47
|
+
* guessing wrong here means running build-time code while merely loading the repository, or
|
|
48
|
+
* silently never running it.
|
|
49
|
+
*
|
|
50
|
+
* A **string** at one of these paths is still interpolated - `exec: 'tsc -b ${{ file.resolve(...) }}'`
|
|
51
|
+
* has to keep working - so this is narrower than `DEFERRED_PATHS`, which skips its paths entirely.
|
|
52
|
+
*/
|
|
53
|
+
export declare const STEP_PATHS: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Config paths left untouched when a repository's config is first resolved, and evaluated only by
|
|
56
|
+
* the command that runs them.
|
|
57
|
+
*
|
|
58
|
+
* `version`'s own hooks are the one place `${{ pkg.targetVersion }}` makes sense, and the version
|
|
59
|
+
* being written is not known until `version` has computed its plan - long after the config was
|
|
60
|
+
* resolved. Evaluating these eagerly would throw while merely *loading* the repository, so any
|
|
61
|
+
* command at all would fail on a config that mentions it.
|
|
62
|
+
*/
|
|
63
|
+
export declare const DEFERRED_PATHS: string[];
|
|
64
|
+
/** One of the keys whose whole subtree is code - see {@link CODE_SUBTREES}. */
|
|
65
|
+
export type CodeSubtree = (typeof CODE_SUBTREES)[number];
|