rman 1.3.0 → 2.0.0-beta.3

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.
Files changed (187) hide show
  1. package/README.md +65 -31
  2. package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
  3. package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
  4. package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
  5. package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
  6. package/builtins/platforms/node/commands/ci.command.js +60 -0
  7. package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
  8. package/builtins/platforms/node/commands/clean.command.js +74 -0
  9. package/builtins/platforms/node/index.d.ts +8 -0
  10. package/builtins/platforms/node/index.js +8 -0
  11. package/builtins/platforms/node/node-config.interface.d.ts +76 -0
  12. package/builtins/platforms/node/node-config.interface.js +7 -0
  13. package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
  14. package/builtins/platforms/node/node-manifest.provider.js +125 -0
  15. package/builtins/platforms/node/node.platform.d.ts +53 -0
  16. package/builtins/platforms/node/node.platform.js +134 -0
  17. package/builtins/platforms/node/services/ci.service.d.ts +47 -0
  18. package/builtins/platforms/node/services/ci.service.js +213 -0
  19. package/builtins/platforms/node/services/clean.service.d.ts +53 -0
  20. package/builtins/platforms/node/services/clean.service.js +237 -0
  21. package/builtins/platforms/node/services/publish.service.d.ts +114 -0
  22. package/builtins/platforms/node/services/publish.service.js +371 -0
  23. package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
  24. package/builtins/platforms/node/services/version-plan.service.js +58 -0
  25. package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
  26. package/builtins/platforms/node/utils/workspace-range.js +28 -0
  27. package/builtins/presets/index.d.ts +15 -0
  28. package/builtins/presets/index.js +57 -0
  29. package/builtins/presets/node.d.ts +16 -0
  30. package/builtins/presets/node.js +57 -0
  31. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
  32. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
  33. package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
  34. package/builtins/publish-targets/docker/docker.target.js +40 -0
  35. package/builtins/publish-targets/docker/index.d.ts +2 -0
  36. package/builtins/publish-targets/docker/index.js +2 -0
  37. package/builtins/publish-targets/npm/index.d.ts +2 -0
  38. package/builtins/publish-targets/npm/index.js +2 -0
  39. package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
  40. package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
  41. package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
  42. package/builtins/publish-targets/npm/npm-view.js +71 -0
  43. package/cli.d.ts +8 -0
  44. package/cli.js +228 -93
  45. package/commands/build.command.d.ts +177 -3
  46. package/commands/build.command.js +20 -10
  47. package/commands/changelog.command.d.ts +296 -3
  48. package/commands/changelog.command.js +127 -43
  49. package/commands/config.command.d.ts +44 -3
  50. package/commands/config.command.js +53 -24
  51. package/commands/diff.command.d.ts +37 -3
  52. package/commands/diff.command.js +25 -16
  53. package/commands/exec.command.d.ts +193 -3
  54. package/commands/exec.command.js +60 -58
  55. package/commands/github-release.command.d.ts +154 -3
  56. package/commands/github-release.command.js +67 -37
  57. package/commands/import.command.d.ts +36 -3
  58. package/commands/import.command.js +28 -20
  59. package/commands/info.command.d.ts +35 -7
  60. package/commands/info.command.js +36 -30
  61. package/commands/lint.command.d.ts +176 -0
  62. package/commands/lint.command.js +34 -0
  63. package/commands/list.command.d.ts +163 -3
  64. package/commands/list.command.js +109 -71
  65. package/commands/publish.command.d.ts +231 -0
  66. package/commands/publish.command.js +304 -0
  67. package/commands/run.command.d.ts +186 -6
  68. package/commands/run.command.js +26 -72
  69. package/commands/test.command.d.ts +173 -3
  70. package/commands/test.command.js +16 -10
  71. package/commands/version.command.d.ts +330 -3
  72. package/commands/version.command.js +292 -78
  73. package/commands.d.ts +32 -0
  74. package/commands.js +28 -0
  75. package/constants.js +1 -1
  76. package/core/application.d.ts +116 -0
  77. package/core/application.js +160 -0
  78. package/core/classes/package.d.ts +219 -0
  79. package/core/classes/package.js +214 -0
  80. package/core/classes/registry.d.ts +30 -0
  81. package/core/classes/registry.js +47 -0
  82. package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
  83. package/core/{repository.js → classes/repository.js} +195 -75
  84. package/core/classes/service.d.ts +49 -0
  85. package/core/classes/service.js +40 -0
  86. package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
  87. package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
  88. package/core/classes/workspace.d.ts +275 -0
  89. package/core/classes/workspace.js +537 -0
  90. package/core/command-builder.d.ts +14 -0
  91. package/core/command-builder.js +94 -0
  92. package/core/config/config-file-scope.d.ts +107 -0
  93. package/core/config/config-file-scope.js +226 -0
  94. package/core/config/config-interpolator.d.ts +164 -0
  95. package/core/config/config-interpolator.js +577 -0
  96. package/core/config/config-paths.d.ts +65 -0
  97. package/core/config/config-paths.js +78 -0
  98. package/core/config/config-reader.d.ts +192 -0
  99. package/core/config/config-reader.js +727 -0
  100. package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
  101. package/core/config/merge-config.js +260 -0
  102. package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
  103. package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
  104. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
  105. package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
  106. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
  107. package/core/interfaces/manifest.js +132 -0
  108. package/core/interfaces/plugin.d.ts +174 -0
  109. package/core/interfaces/plugin.js +124 -0
  110. package/core/interfaces/publish-target.d.ts +136 -0
  111. package/core/interfaces/publish-target.js +43 -0
  112. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  113. package/core/interfaces/run-step.js +1 -0
  114. package/core/plugin-loader.d.ts +25 -0
  115. package/core/plugin-loader.js +53 -0
  116. package/index.d.ts +122 -24
  117. package/index.js +66 -16
  118. package/interfaces/config-scope.interface.d.ts +223 -0
  119. package/interfaces/config-scope.interface.js +1 -0
  120. package/interfaces/rman-config.interface.d.ts +821 -203
  121. package/interfaces/rman-config.interface.js +78 -1
  122. package/package.json +3 -1
  123. package/services/change-hash.service.d.ts +42 -8
  124. package/services/change-hash.service.js +92 -13
  125. package/services/changelog.service.d.ts +91 -52
  126. package/services/changelog.service.js +460 -74
  127. package/services/config-service.d.ts +8 -0
  128. package/services/config-service.js +3 -0
  129. package/services/exec.service.d.ts +23 -12
  130. package/services/exec.service.js +14 -9
  131. package/services/github-release.service.d.ts +45 -34
  132. package/services/github-release.service.js +15 -12
  133. package/services/import.service.d.ts +25 -14
  134. package/services/import.service.js +9 -5
  135. package/services/list.service.d.ts +67 -13
  136. package/services/list.service.js +62 -15
  137. package/services/run.service.d.ts +35 -22
  138. package/services/run.service.js +316 -233
  139. package/services/system-info.d.ts +1 -1
  140. package/services/version-plan.service.d.ts +35 -8
  141. package/services/version-plan.service.js +96 -36
  142. package/services/version.service.d.ts +34 -14
  143. package/services/version.service.js +35 -16
  144. package/services.d.ts +1 -1
  145. package/services.js +1 -1
  146. package/utils/bin-path.d.ts +6 -7
  147. package/utils/bin-path.js +7 -18
  148. package/utils/branch-guard.d.ts +30 -1
  149. package/utils/branch-guard.js +31 -0
  150. package/utils/color-yaml.d.ts +8 -0
  151. package/utils/color-yaml.js +86 -0
  152. package/utils/exec.d.ts +10 -0
  153. package/utils/exec.js +1 -1
  154. package/utils/git.d.ts +26 -0
  155. package/utils/git.js +65 -0
  156. package/utils/logger.d.ts +2 -2
  157. package/utils/logger.js +1 -1
  158. package/utils/package-filter.d.ts +135 -8
  159. package/utils/package-filter.js +204 -16
  160. package/utils/printable-config.d.ts +9 -1
  161. package/utils/printable-config.js +46 -1
  162. package/utils/progress-panel.d.ts +9 -0
  163. package/utils/progress-panel.js +23 -1
  164. package/utils/release-version.d.ts +1 -1
  165. package/utils/run-bin.d.ts +10 -0
  166. package/utils/run-bin.js +1 -1
  167. package/utils/run-options.d.ts +97 -0
  168. package/utils/run-options.js +81 -0
  169. package/utils/version-group.d.ts +31 -0
  170. package/utils/version-group.js +43 -0
  171. package/utils/version-stamp.d.ts +1 -1
  172. package/utils/version-stamp.js +1 -1
  173. package/commands/changed.command.d.ts +0 -3
  174. package/commands/changed.command.js +0 -29
  175. package/core/config.d.ts +0 -409
  176. package/core/config.js +0 -939
  177. package/core/extends-config.d.ts +0 -27
  178. package/core/extends-config.js +0 -89
  179. package/core/manifest.js +0 -150
  180. package/core/merge-config.js +0 -219
  181. package/core/package.d.ts +0 -83
  182. package/core/package.js +0 -106
  183. package/core/plugin.d.ts +0 -112
  184. package/core/plugin.js +0 -189
  185. package/core/workspace.d.ts +0 -68
  186. package/core/workspace.js +0 -83
  187. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.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];