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.
Files changed (162) hide show
  1. package/README.md +2 -12
  2. package/{plugins/node/augmentation/rman.augmentation.d.ts → builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts} +4 -5
  3. package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.js +1 -1
  4. package/{plugins → builtins/platforms}/node/commands/ci.command.d.ts +5 -3
  5. package/{plugins → builtins/platforms}/node/commands/ci.command.js +6 -5
  6. package/{plugins → builtins/platforms}/node/commands/clean.command.d.ts +6 -4
  7. package/{plugins → builtins/platforms}/node/commands/clean.command.js +6 -5
  8. package/builtins/platforms/node/index.d.ts +8 -0
  9. package/builtins/platforms/node/index.js +8 -0
  10. package/{plugins → builtins/platforms}/node/node-config.interface.d.ts +1 -2
  11. package/{plugins → builtins/platforms}/node/node-manifest.provider.d.ts +3 -3
  12. package/{plugins → builtins/platforms}/node/node-manifest.provider.js +3 -3
  13. package/{plugins → builtins/platforms}/node/node.platform.d.ts +3 -3
  14. package/{plugins → builtins/platforms}/node/services/ci.service.d.ts +3 -3
  15. package/{plugins → builtins/platforms}/node/services/ci.service.js +4 -4
  16. package/{plugins → builtins/platforms}/node/services/clean.service.d.ts +3 -3
  17. package/{plugins → builtins/platforms}/node/services/clean.service.js +3 -3
  18. package/{plugins → builtins/platforms}/node/services/publish.service.d.ts +4 -4
  19. package/{plugins → builtins/platforms}/node/services/publish.service.js +13 -7
  20. package/{plugins → builtins/platforms}/node/services/version-plan.service.d.ts +3 -3
  21. package/{plugins → builtins/platforms}/node/services/version-plan.service.js +2 -2
  22. package/builtins/presets/index.d.ts +15 -0
  23. package/builtins/presets/index.js +57 -0
  24. package/builtins/presets/node.d.ts +16 -0
  25. package/builtins/presets/node.js +57 -0
  26. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +6 -6
  27. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +9 -9
  28. package/{targets → builtins/publish-targets/docker}/docker.target.d.ts +6 -5
  29. package/{targets → builtins/publish-targets/docker}/docker.target.js +1 -1
  30. package/builtins/publish-targets/docker/index.d.ts +2 -0
  31. package/builtins/publish-targets/docker/index.js +2 -0
  32. package/builtins/publish-targets/npm/index.d.ts +2 -0
  33. package/builtins/publish-targets/npm/index.js +2 -0
  34. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.d.ts +4 -4
  35. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.js +4 -5
  36. package/cli.d.ts +3 -0
  37. package/cli.js +16 -7
  38. package/commands/changelog.command.d.ts +106 -2
  39. package/commands/changelog.command.js +40 -0
  40. package/commands/config.command.js +23 -5
  41. package/commands/list.command.js +1 -1
  42. package/commands/publish.command.d.ts +1 -1
  43. package/commands/publish.command.js +1 -1
  44. package/commands/run.command.js +1 -1
  45. package/commands/version.command.d.ts +16 -3
  46. package/commands/version.command.js +143 -9
  47. package/commands.d.ts +0 -1
  48. package/commands.js +0 -1
  49. package/constants.js +1 -1
  50. package/core/application.d.ts +10 -10
  51. package/core/application.js +28 -11
  52. package/core/{package.d.ts → classes/package.d.ts} +12 -4
  53. package/core/{package.js → classes/package.js} +17 -1
  54. package/core/{repository.d.ts → classes/repository.d.ts} +22 -36
  55. package/core/{repository.js → classes/repository.js} +112 -241
  56. package/core/{service.d.ts → classes/service.d.ts} +2 -2
  57. package/core/classes/workspace.d.ts +277 -0
  58. package/core/classes/workspace.js +585 -0
  59. package/core/command-builder.js +17 -1
  60. package/core/config/config-file-scope.d.ts +107 -0
  61. package/core/config/config-file-scope.js +226 -0
  62. package/core/config/config-interpolator.d.ts +164 -0
  63. package/core/config/config-interpolator.js +577 -0
  64. package/core/config/config-paths.d.ts +65 -0
  65. package/core/config/config-paths.js +78 -0
  66. package/core/config/config-reader.d.ts +215 -0
  67. package/core/config/config-reader.js +851 -0
  68. package/core/{merge-config.js → config/merge-config.js} +4 -8
  69. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +5 -5
  70. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +3 -3
  71. package/core/{manifest.js → interfaces/manifest.js} +2 -2
  72. package/core/{plugin.d.ts → interfaces/plugin.d.ts} +44 -27
  73. package/core/{plugin.js → interfaces/plugin.js} +48 -10
  74. package/core/{publish-target.d.ts → interfaces/publish-target.d.ts} +24 -12
  75. package/core/{publish-target.js → interfaces/publish-target.js} +16 -3
  76. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  77. package/core/plugin-loader.d.ts +11 -51
  78. package/core/plugin-loader.js +21 -202
  79. package/index.d.ts +29 -28
  80. package/index.js +20 -46
  81. package/interfaces/config-scope.interface.d.ts +223 -0
  82. package/interfaces/config-scope.interface.js +1 -0
  83. package/interfaces/rman-config.interface.d.ts +100 -5
  84. package/interfaces/rman-config.interface.js +17 -0
  85. package/package.json +3 -2
  86. package/services/change-hash.service.d.ts +40 -6
  87. package/services/change-hash.service.js +90 -11
  88. package/services/changelog.service.d.ts +31 -3
  89. package/services/changelog.service.js +448 -65
  90. package/services/config-service.d.ts +8 -0
  91. package/services/config-service.js +3 -0
  92. package/services/exec.service.d.ts +2 -2
  93. package/services/exec.service.js +1 -1
  94. package/services/github-release.service.d.ts +3 -3
  95. package/services/github-release.service.js +3 -3
  96. package/services/import.service.d.ts +2 -2
  97. package/services/import.service.js +1 -1
  98. package/services/list.service.d.ts +9 -7
  99. package/services/list.service.js +2 -2
  100. package/services/run.service.d.ts +15 -11
  101. package/services/run.service.js +62 -18
  102. package/services/system-info.d.ts +1 -1
  103. package/services/version-plan.service.d.ts +8 -4
  104. package/services/version-plan.service.js +54 -21
  105. package/services/version.service.d.ts +5 -5
  106. package/services/version.service.js +7 -4
  107. package/services.d.ts +1 -1
  108. package/services.js +1 -1
  109. package/utils/branch-guard.d.ts +1 -1
  110. package/utils/color-yaml.d.ts +8 -0
  111. package/utils/color-yaml.js +86 -0
  112. package/utils/git.d.ts +26 -0
  113. package/utils/git.js +65 -0
  114. package/utils/logger.d.ts +1 -1
  115. package/utils/package-filter.d.ts +8 -1
  116. package/utils/package-filter.js +7 -0
  117. package/utils/printable-config.d.ts +8 -0
  118. package/utils/printable-config.js +45 -0
  119. package/utils/progress-panel.d.ts +9 -0
  120. package/utils/progress-panel.js +23 -1
  121. package/utils/release-version.d.ts +1 -1
  122. package/utils/run-options.js +1 -1
  123. package/utils/version-group.d.ts +31 -0
  124. package/utils/version-group.js +43 -0
  125. package/commands/changed.command.d.ts +0 -80
  126. package/commands/changed.command.js +0 -36
  127. package/core/config.d.ts +0 -530
  128. package/core/config.js +0 -1123
  129. package/core/core-services.d.ts +0 -14
  130. package/core/core-services.js +0 -30
  131. package/core/core-targets.d.ts +0 -14
  132. package/core/core-targets.js +0 -16
  133. package/core/extends-config.d.ts +0 -35
  134. package/core/extends-config.js +0 -129
  135. package/core/load-config-module.d.ts +0 -28
  136. package/core/load-config-module.js +0 -42
  137. package/core/workspace.d.ts +0 -119
  138. package/core/workspace.js +0 -124
  139. package/plugins/builtins.d.ts +0 -44
  140. package/plugins/builtins.js +0 -33
  141. package/plugins/detect.d.ts +0 -78
  142. package/plugins/detect.js +0 -70
  143. package/plugins/node/index.d.ts +0 -29
  144. package/plugins/node/index.js +0 -40
  145. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
  146. /package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.d.ts +0 -0
  147. /package/{plugins → builtins/platforms}/node/node-config.interface.js +0 -0
  148. /package/{plugins → builtins/platforms}/node/node.platform.js +0 -0
  149. /package/{plugins → builtins/platforms}/node/utils/workspace-range.d.ts +0 -0
  150. /package/{plugins → builtins/platforms}/node/utils/workspace-range.js +0 -0
  151. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.d.ts +0 -0
  152. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.js +0 -0
  153. /package/core/{registry.d.ts → classes/registry.d.ts} +0 -0
  154. /package/core/{registry.js → classes/registry.js} +0 -0
  155. /package/core/{service.js → classes/service.js} +0 -0
  156. /package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +0 -0
  157. /package/core/{version-scheme.js → classes/version-scheme.js} +0 -0
  158. /package/core/{merge-config.d.ts → config/merge-config.d.ts} +0 -0
  159. /package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +0 -0
  160. /package/core/{resolve-target.js → config/resolve-config-target.js} +0 -0
  161. /package/core/{custom-command.js → interfaces/custom-command.js} +0 -0
  162. /package/{plugins/node/augmentation/rman.augmentation.js → core/interfaces/run-step.js} +0 -0
@@ -0,0 +1,851 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import fastGlob from 'fast-glob';
6
+ import * as yaml from 'js-yaml';
7
+ import { presetNames, PRESETS_DIR } from '../../builtins/presets/index.js';
8
+ import { declaredKind, isDeclared, isPlatform } from '../interfaces/plugin.js';
9
+ import { CODE_SUBTREES } from './config-paths.js';
10
+ import { mergeConfig } from './merge-config.js';
11
+ const requireConfigModule = createRequire(import.meta.url);
12
+ const IDENTITY_KEYS = ['platform', 'name'];
13
+ const ROOT_SELECTOR_INNER = '/';
14
+ const EXTEND_EXTENSIONS = ['', '.yml', '.yaml', '.json', '.cjs', '.mjs', '.js'];
15
+ const EXTENDS_KEY = 'extends';
16
+ const PLUGINS_KEY = 'plugins';
17
+ const PLATFORMS_KEY = 'platforms';
18
+ /** What marks an `extends` target as one of rman's own presets: `extends: "rman:node"`. */
19
+ const PRESET_PREFIX = 'rman:';
20
+ /** `.js` is the published layout; `.ts` is the source tree a spec runs in. */
21
+ const PRESET_EXTENSIONS = ['.js', '.ts'];
22
+ /** What an error about a default preset names itself as - there is no config key to quote, since
23
+ * these are laid down by the caller rather than written by anybody. */
24
+ const PRESETS_LABEL = 'presets';
25
+ /** Free-form by contract - see `_assertSelectorKeys`. */
26
+ const VARS_KEY = 'vars';
27
+ /** Keys that only an rman **config** has, for telling one from a contribution once it has already
28
+ * been refused - see `_assertLoaded`. `name` is not among them: a plugin has one too. */
29
+ const CONFIG_SHAPED = ['extends', PLUGINS_KEY, PLATFORMS_KEY, 'commands', 'publishTargets'];
30
+ export class ConfigReader {
31
+ /**
32
+ *
33
+ * @protected
34
+ */
35
+ configFiles = [
36
+ '.rmanrc',
37
+ '.rmanrc.yml',
38
+ '.rmanrc.cjs',
39
+ '.rmanrc.mjs',
40
+ '.rmanrc.js',
41
+ ];
42
+ /**
43
+ * Whether a config key names **packages** rather than a setting: `"[*]"`, `"[/]"`, `"[pkg-a]"`.
44
+ */
45
+ /* The brackets are what keep this space from colliding with real config keys - no setting starts
46
+ * with one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare
47
+ * `[*]` parses as a flow sequence and `*` is an alias indicator. */
48
+ static isSelectorKey(key) {
49
+ return key.length > 2 && key.startsWith('[') && key.endsWith(']');
50
+ }
51
+ /**
52
+ * **Which packages a selector key speaks for.** Three audiences, and what each is matched
53
+ * against:
54
+ *
55
+ * | | matches | `test` is given |
56
+ * | --- | --- | --- |
57
+ * | `"[/]"` | the **root package** alone, structurally | - |
58
+ * | `"[platform:node]"`, `"[platform:node,cargo]"` | packages of those **technologies** | `pkg.platform.name` |
59
+ * | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | the packages **below** this directory the glob matches | `pkg.selector` |
60
+ *
61
+ * The glob is anchored at both ends, so `"[*-dialect]"` matches `mysql-dialect` but not
62
+ * `my-dialect-helper`. `"[ws:*]"` and `"[workspace:*]"` are accepted and mean exactly `"[*]"`.
63
+ */
64
+ /* `/` for the root because that is what a repository root is called everywhere else, and it
65
+ * cannot collide with a package name.
66
+ *
67
+ * **The root is never selected by name, and that one rule removes two traps.** A glob matches
68
+ * package names, and the root is nobody's child - so `"[my-*]"` cannot quietly pick up a
69
+ * repository whose root package happens to be called `my-repo`, and `"[*]"` cannot hand a
70
+ * package-shaped setting to a root that has no build directory to apply it to.
71
+ *
72
+ * The `ws:`/`workspace:` qualifier existed to say "not the root" back when a bare glob included
73
+ * it; the shape of the set says that now, so it has nothing left to add. Accepted rather than
74
+ * rejected because the two spellings resolve to the same packages - an error would be friction
75
+ * with no reader to protect.
76
+ *
77
+ * Glob rather than regex, to match every other pattern in rman (`allowBranch`,
78
+ * `changelog.tagPattern`, `clean.include`). */
79
+ static parseSelector(key) {
80
+ const inner = key.slice(1, -1);
81
+ if (inner === ROOT_SELECTOR_INNER)
82
+ return { scope: 'root', test: () => true };
83
+ if (inner.startsWith(PLATFORM_PREFIX)) {
84
+ const names = inner
85
+ .slice(PLATFORM_PREFIX.length)
86
+ .split(',')
87
+ .map(n => n.trim())
88
+ .filter(Boolean);
89
+ return { scope: 'platform', test: (name) => !!name && names.includes(name), names };
90
+ }
91
+ const re = globToRegExp(stripWorkspacePrefix(inner));
92
+ return { scope: 'package', test: (name) => re.test(name) };
93
+ }
94
+ /**
95
+ *
96
+ */
97
+ async resolve(dirname, options) {
98
+ const context = {
99
+ dirname,
100
+ plugins: [],
101
+ platforms: [...(options?.platforms || [])],
102
+ };
103
+ /** Seeded through `_addPlugin` rather than copied, so a plugin handed in carries its
104
+ * technologies into `context.platforms` exactly as one a config names does - the caller's
105
+ * plugins are the *first* thing in play, and a list they were missing from would have a
106
+ * preset's platform claiming a directory theirs recognizes. */
107
+ for (const plugin of options?.plugins || [])
108
+ this._addPlugin(plugin, context);
109
+ const config = await this._resolve(context, options);
110
+ return { config, plugins: context.plugins, platforms: context.platforms };
111
+ }
112
+ /**
113
+ *
114
+ * @protected
115
+ */
116
+ async _resolve(context, options) {
117
+ const source = this._findConfigSource(context.dirname);
118
+ const raw = source ? await this._readConfigFile(source) : {};
119
+ let resolved = {};
120
+ /* Read config file */
121
+ if (source) {
122
+ this._assertSelectorBlocks(raw, source);
123
+ /**
124
+ * `extends` names the base this file sits on, so it is resolved on its own and the file's own
125
+ * keys land on top of the answer.
126
+ */
127
+ const declared = raw[EXTENDS_KEY];
128
+ /**
129
+ * **Only the key it reads is handed over, never the whole config.** `_resolveExtends` merges
130
+ * its argument's own keys onto the bases it resolved, so passing `raw` applied them there and
131
+ * then again below - measured, a value function chained onto its own result and
132
+ * `[...value, 'dist']` answered `['build', 'dist', 'dist']`.
133
+ */
134
+ const base = declared === undefined
135
+ ? {}
136
+ : await this._resolveExtends(context, { [EXTENDS_KEY]: declared }, source);
137
+ const own = { ...raw };
138
+ delete own[EXTENDS_KEY];
139
+ /**
140
+ * **Through `mergeConfig` whether or not there is an `extends`**, and that is not symmetry -
141
+ * it is the only thing that anchors a contribution glob and records an origin.
142
+ *
143
+ * `plugins`, `commands` and `publishTargets` take a glob, and a relative one has to be
144
+ * resolved against **the file that declared it** - which `anchorContributions` does inside
145
+ * `mergeConfig`, at the last moment that is knowable. Skipped for a config with no `extends`,
146
+ * `plugins: './p.js'` reached `fastGlob` unanchored and was looked for relative to the
147
+ * *current working directory*: measured, `"plugins" glob "./p.js" matched no file`, in a
148
+ * repository where the file sat right beside the config naming it.
149
+ *
150
+ * The same call is what records `ORIGINS`, so without it every error about this file named no
151
+ * file at all.
152
+ */
153
+ resolved = mergeConfig(base, own, source);
154
+ }
155
+ /* Load the technologies this level names, then the plugins that carry more of them. */
156
+ await this._loadPlatforms(resolved, context);
157
+ /* Load plugins */
158
+ await this._loadPlugins(resolved, context);
159
+ /* Then what rman ships, underneath everything above - see `_applyPresets`. */
160
+ resolved = await this._applyPresets(resolved, context, options);
161
+ /* Which technology claims this directory, when its config has not said. */
162
+ if (!this._getDeclaredPlatform(resolved)) {
163
+ const claimed = context.platforms.find(p => p.manifestProvider.read(context.dirname));
164
+ if (claimed)
165
+ resolved.platform = claimed.name;
166
+ }
167
+ return resolved;
168
+ }
169
+ _getDeclaredPlatform(config) {
170
+ const root = config[`[${ROOT_SELECTOR_INNER}]`];
171
+ const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
172
+ return typeof declared === 'string' && declared.trim() ? declared.trim() : undefined;
173
+ }
174
+ /**
175
+ * The one config file `dirname` declares, or `undefined` for a directory that declares none.
176
+ *
177
+ * **Several is an error**, which is the whole point of reading the directory rather than probing
178
+ * the names in a fixed order: a probe cannot tell "this one" from "this one, and three others I
179
+ * am quietly folding in underneath". The message lists what it found, because the fix is to
180
+ * delete one and the reader has to know which are in play.
181
+ *
182
+ * Public because it answers a question a *reader* has and not only the resolver: `rman config`
183
+ * heads its output with the file you would open to change what it prints.
184
+ */
185
+ findConfigSource(dirname) {
186
+ return this._findConfigSource(dirname);
187
+ }
188
+ /** @see {@link ConfigReader.findConfigSource} */
189
+ _findConfigSource(dirname) {
190
+ let entries;
191
+ try {
192
+ entries = fs.readdirSync(dirname);
193
+ }
194
+ catch {
195
+ return undefined;
196
+ }
197
+ const found = entries.filter(name => this.configFiles.includes(name)).map(name => path.join(dirname, name));
198
+ /** The manifest's own `rman` key. Counted here rather than merged later, and only when the key
199
+ * is actually present - every Node package has a `package.json`, and a `package.json` without
200
+ * that key is not a config source. */
201
+ if (entries.includes('package.json')) {
202
+ const pkgJsonFile = path.join(dirname, 'package.json');
203
+ try {
204
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
205
+ if (pkgJson && typeof pkgJson.rman === 'object' && pkgJson.rman !== null)
206
+ found.push(pkgJsonFile);
207
+ }
208
+ catch {
209
+ /** Not this read's error to raise - a malformed `package.json` is the manifest provider's
210
+ * report to make, and raising it here would blame the config for a broken manifest. */
211
+ }
212
+ }
213
+ if (found.length > 1) {
214
+ const names = found.map(f => (path.basename(f) === 'package.json' ? 'package.json ("rman")' : path.basename(f)));
215
+ throw new Error(`"${dirname}" declares ${found.length} rman configs - ${names.join(', ')}. A directory may ` +
216
+ `declare one. Merging them would produce a config that is in none of the files, under a ` +
217
+ `precedence order nothing states; delete or rename the ones that are not in use.`);
218
+ }
219
+ return found[0];
220
+ }
221
+ /** Reads one config file into a plain object. Nothing is merged and nothing is checked here
222
+ * beyond the file being an object at all - a config that is a list or a string is a mistake
223
+ * worth naming before any key of it is read. */
224
+ async _readConfigFile(file) {
225
+ const name = path.basename(file);
226
+ const ext = path.extname(file);
227
+ if (name === 'package.json') {
228
+ return JSON.parse(fs.readFileSync(file, 'utf-8')).rman;
229
+ }
230
+ /**
231
+ * **A bare `.rmanrc` is JSON, and it has to be named rather than derived.** `path.extname` on a
232
+ * dotfile answers `''` - a leading dot makes a name, not an extension - so without this clause
233
+ * the plain `.rmanrc` fell through to the module loader and was `require()`d as JavaScript.
234
+ * Measured: `{"[/]":{"platform":"declared"}}` came back as `SyntaxError: Unexpected token ':'`,
235
+ * pointing at the config file, from a loader nobody asked for.
236
+ */
237
+ const value = ext === '.json' || name === '.rmanrc'
238
+ ? JSON.parse(fs.readFileSync(file, 'utf-8'))
239
+ : ext === '.yaml' || ext === '.yml'
240
+ ? yaml.load(fs.readFileSync(file, 'utf-8'))
241
+ : await this._loadConfigModule(file);
242
+ /**
243
+ * **A module may export a factory instead of the object**, and the config is then whatever it
244
+ * returns. This is how a preset stays inert until a repository names it.
245
+ */
246
+ /* `presets/node` has to run `augmentSystemInfo()` before handing its config over, and that
247
+ * mutates the core's own `SystemInfo` in place - at import time it would have `rman info` report
248
+ * npm's tooling in a Cargo repository that never named the preset. A factory moves the side
249
+ * effect from "this module was loaded" to "this repository asked".
250
+ *
251
+ * No ambiguity with a value function: those are *keys* of a config, and this is the module's
252
+ * default export, which is the config itself. */
253
+ const resolved = typeof value === 'function' ? await value() : value;
254
+ if (!resolved || typeof resolved !== 'object' || Array.isArray(resolved)) {
255
+ throw new Error(`"${file}" does not hold an rman config object`);
256
+ }
257
+ return resolved;
258
+ }
259
+ /**
260
+ * Loads a config module - a `.rmanrc.cjs`/`.mjs`/`.js`, or an `extends` target that is one. The
261
+ * default export when there is one, the module's own exports object otherwise.
262
+ */
263
+ /* **`require()` first, and that is not an optimization.** A CommonJS module's `module.exports` is
264
+ * more reliably observed this way than through dynamic `import()`'s CJS-interop synthesis, which
265
+ * some ESM loader hooks - ts-node/swc-node-style transpilers registered via `--import` - can
266
+ * short-circuit into an **empty object**. `require()` throws `ERR_REQUIRE_ESM` for a genuinely-ESM
267
+ * file (`.mjs`, or `.js` under `"type": "module"`), and only then does this fall back to
268
+ * `import()`, the one case that actually needs it.
269
+ *
270
+ * Either path can hand back an ES module namespace rather than a plain object - Node's
271
+ * `require(esm)` support does this too, not just `import()` - so `.default` is preferred whenever
272
+ * present.
273
+ *
274
+ * **One loader for a directory's own config and for an `extends` target, which it was not.** The
275
+ * old `extends-config.ts` used a bare `await import()` while a directory's own `.rmanrc.cjs` went
276
+ * through this, so the *same file* loaded correctly as a directory's config and came back empty
277
+ * when another config named it - and an empty object is a valid config, so nothing was reported.
278
+ * Measured under mocha: a base declaring `"[*]": { version: { stamp: ['build'] } }`, reached by
279
+ * `extends: './base.cjs'`, contributed nothing; the identical fixture with `base.json`
280
+ * contributed normally, and the same `.cjs` worked from the CLI, where no loader hook is
281
+ * registered. Both routes are this one method now, so they cannot part again.
282
+ *
283
+ * `createRequire` is based on **this module's** own URL rather than on the config file. That is
284
+ * right here and *not* right for resolving a bare specifier - see `_resolveTarget`, which is
285
+ * based on the config file so a package name resolves through the repository's `node_modules`
286
+ * rather than rman's own. By the time a file reaches here it is already an absolute path. */
287
+ async _loadConfigModule(file) {
288
+ let mod;
289
+ try {
290
+ mod = requireConfigModule(file);
291
+ }
292
+ catch (e) {
293
+ if (e?.code !== 'ERR_REQUIRE_ESM')
294
+ throw e;
295
+ mod = await import(pathToFileURL(file).href);
296
+ }
297
+ return mod?.default ?? mod;
298
+ }
299
+ /**
300
+ * Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
301
+ * whether the pair could ever match a package.
302
+ */
303
+ _assertSelectorBlocks(config, file) {
304
+ this._assertSelectorKeys(config, file, [], []);
305
+ }
306
+ /**
307
+ * One walk, refusing a selector key under a *setting* and checking each legal one's contents.
308
+ *
309
+ * @param at the key path to `node`.
310
+ * @param enclosing the selector keys `node` sits inside, outermost first.
311
+ */
312
+ /* **A selector key is legal at the top level of a config and directly inside another selector
313
+ * key, and nowhere else** - which is the whole of the placement rule, and `at.length >
314
+ * enclosing.length` is the whole of the test: the two arrays stay equal for as long as every
315
+ * ancestor is a selector, and `at` runs ahead the moment one is not.
316
+ *
317
+ * ```yaml
318
+ * "[platform:node]":
319
+ * "[pkg-*]": { group: x } # an intersection
320
+ * run:
321
+ * "[pkg-*]": { ... } # refused - a setting is not an audience
322
+ * ```
323
+ *
324
+ * **The nested form used to be refused outright and that was the wrong call**, on a reason that
325
+ * does not survive being written out: "selectors do not intersect, because specificity ranking was
326
+ * dropped". Ranking answers which of two *siblings* wins, and nesting asks nothing of the sort -
327
+ * a nested block is resolved depth-first in declaration order, which is the rule already in force
328
+ * one level up (`Workspace._matchingSelectors`). What the refusal cost was the only way to say
329
+ * "these packages, but only the ones that are also X" for a whole block: `if:` is per key and
330
+ * reaches neither `vars` nor any key that is not a run step.
331
+ *
332
+ * What the refusal *did* get right was that silence is unacceptable - before it, the inner block
333
+ * reached every package the outer one did, as a literal `'[pkg-*]'` key sitting in the resolved
334
+ * config where `rman config` showed it looking as though it had worked. Both halves are answered
335
+ * now: the shape works, and the shapes that could never work are refused here rather than
336
+ * quietly matching nothing.
337
+ *
338
+ * **`vars` and the contribution keys are skipped**, and both for the same reason: their contents
339
+ * are not config keys. `vars` is free-form by contract and `CODE_SUBTREES` hold plugins, commands
340
+ * and publish targets - arbitrary objects whose key space rman does not own. A bracketed name in
341
+ * either is data. */
342
+ _assertSelectorKeys(node, file, at, enclosing) {
343
+ if (!node || typeof node !== 'object' || Array.isArray(node))
344
+ return;
345
+ for (const [key, value] of Object.entries(node)) {
346
+ if (ConfigReader.isSelectorKey(key)) {
347
+ if (at.length > enclosing.length) {
348
+ throw new Error(`"${key}" in "${file}" is nested under "${at.join('.')}", which is a setting rather ` +
349
+ `than an audience - a selector is read at the top level of a config and inside ` +
350
+ `another selector, so this one would never be applied. Move it out, or use an ` +
351
+ `expression where the value itself depends on the package.`);
352
+ }
353
+ this._assertSelectorBlock(key, value, file, enclosing.at(-1));
354
+ this._assertSelectorKeys(value, file, [...at, key], [...enclosing, key]);
355
+ continue;
356
+ }
357
+ if (key === VARS_KEY || CODE_SUBTREES.includes(key))
358
+ continue;
359
+ this._assertSelectorKeys(value, file, [...at, key], enclosing);
360
+ }
361
+ }
362
+ /** What one selector block may carry, and - given `outer` - whether it can match anything. */
363
+ _assertSelectorBlock(key, value, file, outer) {
364
+ if (!value || typeof value !== 'object')
365
+ return;
366
+ if (outer !== undefined)
367
+ this._assertNestable(outer, key, file);
368
+ if (EXTENDS_KEY in value) {
369
+ throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
370
+ `statement about this config rather than about the packages the selector names.`);
371
+ }
372
+ /**
373
+ * **The two keys a selector cannot carry, because the selector is downstream of them.**
374
+ *
375
+ * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
376
+ * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
377
+ * the packages are still being found, so a glob block setting either would need its own answer
378
+ * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
379
+ * would simply never be read.
380
+ *
381
+ * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
382
+ * the repository root - so a root block needs no selector and is applied during the walk.
383
+ */
384
+ if (ConfigReader.parseSelector(key).scope === 'root') {
385
+ /** **Except for `platform` in a *nested* `"[/]"`, which is read nowhere.**
386
+ * `Workspace._declaredPlatformName` looks at a level's own `platform` and at a **top-level**
387
+ * `"[/]"`, because which technology claims a directory has to be settled before any block is
388
+ * matched. Inside `"[platform:node]"` it is also circular - the answer decides whether the
389
+ * block that declares it applies at all. */
390
+ if (outer !== undefined && 'platform' in value) {
391
+ throw new Error(`"${key}" in "${file}" is nested inside "${outer}" and cannot set "platform" - which ` +
392
+ `technology claims a directory is settled before any selector is matched, so it is ` +
393
+ `read from the level's own keys or a top-level "[/]" and nowhere else. Write it there.`);
394
+ }
395
+ return;
396
+ }
397
+ for (const identity of IDENTITY_KEYS) {
398
+ if (identity in value) {
399
+ throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
400
+ `and "${identity}" is what the selector is derived from, so the block could never be ` +
401
+ `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
402
+ `under "[/]" for the root package.`);
403
+ }
404
+ }
405
+ }
406
+ /**
407
+ * Refuses a nested pair that could never match a package together.
408
+ *
409
+ * @param outer the enclosing selector key.
410
+ * @param inner the key written inside it.
411
+ */
412
+ /* **Nesting is an AND, so a pair naming disjoint sets is a block that runs for nobody** - which is
413
+ * the silence worth refusing. Two pairs are decidable and both are:
414
+ *
415
+ * - **`"[/]"` paired with a glob**, either way round. A glob matches a package's name and the root
416
+ * is nobody's child, so it never matches the root whatever it is written beside; `"[/]"`
417
+ * matches nothing else. The pair is empty in both directions.
418
+ * - **Two platform blocks naming nothing in common.** A package carries one `platform.name`, so
419
+ * `"[platform:node]" > "[platform:cargo]"` is empty; `"[platform:node,cargo]" > "[platform:node]"`
420
+ * narrows and is fine.
421
+ *
422
+ * **`"[/]"` inside a platform block is the pair this deliberately allows**, and getting that wrong
423
+ * once is why the reasoning is written out. It reads as *the root, when the root is a node
424
+ * package*, which is a perfectly well-defined audience - it was unreachable only because
425
+ * `_speaksFor` asked `isRoot` before it asked the platform question. Refusing it took the natural
426
+ * shape of a technology's shared config away: a preset wanting `vars` over a whole node
427
+ * repository, the root's own settings, and the packages' settings had to hoist `vars` to the top
428
+ * level, where they also reach packages of every other technology.
429
+ *
430
+ * **A glob pair is deliberately not checked**, and the asymmetry is the point rather than an
431
+ * omission: whether two globs intersect is a real computation with a wrong answer available in
432
+ * both directions, while a platform set is `includes`. `"[pkg-*]" > "[lib-*]"` therefore loads and
433
+ * matches nothing - the same thing a top-level `"[lib-*]"` does in a repository with no such
434
+ * package, which nothing reports either. */
435
+ _assertNestable(outer, inner, file) {
436
+ const a = ConfigReader.parseSelector(outer);
437
+ const b = ConfigReader.parseSelector(inner);
438
+ const [root, glob] = a.scope === 'root' ? [outer, b.scope === 'package' && inner] : [inner, a.scope === 'package' && outer];
439
+ if ((a.scope === 'root' || b.scope === 'root') && glob) {
440
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", and "${glob}" can never match the ` +
441
+ `same package "${root}" does - a glob matches a package's name and the root is nobody's ` +
442
+ `child, so the pair is empty. A platform block nests with "[/]" ("the root, when it is ` +
443
+ `of that technology"); a glob does not.`);
444
+ }
445
+ if (a.scope === 'platform' && b.scope === 'platform' && !b.names?.some(name => a.test(name))) {
446
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", which names no technology in common ` +
447
+ `with it - a package carries one platform, so this block could never be applied. Nesting ` +
448
+ `narrows: name a technology the outer block already speaks for.`);
449
+ }
450
+ }
451
+ async _resolveExtends(context, config, from, seen = []) {
452
+ const declared = config[EXTENDS_KEY];
453
+ if (declared === undefined)
454
+ return config;
455
+ const targets = Array.isArray(declared) ? declared : [declared];
456
+ for (const target of targets) {
457
+ if (typeof target !== 'string' || !target.trim()) {
458
+ throw new Error(`"extends" in "${from}" must be a config name or path, or an array of them`);
459
+ }
460
+ }
461
+ const base = {};
462
+ for (const target of targets) {
463
+ const file = this._resolveExtendTarget(target, from, EXTENDS_KEY);
464
+ if (seen.includes(file)) {
465
+ throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
466
+ }
467
+ const loaded = await this._readConfigFile(file);
468
+ this._assertSelectorBlocks(loaded, file);
469
+ // Recursive: a shared config may itself be built on another.
470
+ mergeConfig(base, await this._resolveExtends(context, loaded, file, [...seen, file]), file);
471
+ }
472
+ const own = { ...config };
473
+ delete own[EXTENDS_KEY];
474
+ return mergeConfig(base, own);
475
+ }
476
+ /**
477
+ * Lays `options.presets` underneath this level's own config, exactly as an `extends` naming them
478
+ * would - so a repository that declares nothing still has rman's own technologies, commands and
479
+ * publish targets, and one that declares something still wins every key.
480
+ *
481
+ * Nothing happens without `options.presets`, which is how a level that is not the root - and a
482
+ * caller wanting a bare core - says so.
483
+ */
484
+ /* **Loaded last, and that is the one thing here that is not obvious.** `platformFor` takes the
485
+ * first technology that recognizes a directory, so whatever is registered first decides which
486
+ * directories are packages at all. A repository writing `extends: 'rman:cargo'` has said what it
487
+ * is; its platform is loaded in the two steps above and therefore sits ahead of node in
488
+ * `context.platforms`, so a root holding both a `Cargo.toml` and a tooling `package.json`
489
+ * resolves to what the repository declared rather than to what rman happens to ship.
490
+ *
491
+ * **The config's own `platforms` array runs the other way round** - `[node, ...declared]`, because
492
+ * the presets merge underneath like any base - and nothing reads it: `context.platforms` is the
493
+ * order-bearing list, and the array is the record of what was contributed. Worth knowing before
494
+ * reading one off `rman config` and expecting it to say who claims what.
495
+ *
496
+ * **One combined layer rather than one merge per preset**, so several presets keep their
497
+ * declaration order instead of the last one landing deepest. */
498
+ async _applyPresets(config, context, options) {
499
+ if (!options?.presets?.length)
500
+ return config;
501
+ const base = {};
502
+ for (const name of options.presets) {
503
+ const preset = await this._readConfigFile(this._resolvePreset(name, PRESETS_LABEL));
504
+ mergeConfig(base, preset);
505
+ }
506
+ await this._loadPlatforms(base, context);
507
+ await this._loadPlugins(base, context);
508
+ return mergeConfig(base, config);
509
+ }
510
+ /**
511
+ * Loads the technologies a level's `platforms` names, into `context.platforms`.
512
+ *
513
+ * An entry is a `Platform`, or a glob naming `.js` modules that `export default` one. Already
514
+ * present by name, it is skipped - two layers naming one technology is ordinary rather than a
515
+ * mistake, and registering it twice would have two objects answering for one name.
516
+ */
517
+ /* The sibling of `_loadPlugins`, and deliberately not folded into it: `plugins` and `platforms`
518
+ * are different keys with different contents, and a single loader would have to guess which it
519
+ * was holding - the `Plugin | Platform` two-shapes problem that having separate keys removes. */
520
+ async _loadPlatforms(config, context) {
521
+ if (!config.platforms)
522
+ return;
523
+ for (const entry of Array.isArray(config.platforms) ? config.platforms : [config.platforms]) {
524
+ if (typeof entry === 'string') {
525
+ for (const { value, file } of await this._loadByGlob(entry, PLATFORMS_KEY)) {
526
+ this._assertLoaded(value, file, 'platform');
527
+ this._addPlatform(value, context);
528
+ }
529
+ }
530
+ else if (entry && typeof entry === 'object' && isPlatform(entry)) {
531
+ this._addPlatform(entry, context);
532
+ }
533
+ else {
534
+ throw new Error(`"${PLATFORMS_KEY}" takes a platform, or a glob naming modules that export one - got ` +
535
+ `${describeEntry(entry)}. A platform is declared with definePlatform({ name, ` +
536
+ `manifestProvider, ... }); anything broader is a plugin and belongs in "${PLUGINS_KEY}".`);
537
+ }
538
+ }
539
+ }
540
+ /** Appends a technology unless one already answers to its name. */
541
+ _addPlatform(platform, context) {
542
+ if (!context.platforms.some(p => p.name === platform.name))
543
+ context.platforms.push(platform);
544
+ }
545
+ /** Appends a plugin unless one already answers to that name. */
546
+ /* A plugin carries no technologies any more - `platforms` is a config key, so a package shipping
547
+ * one declares it in its own config and the reader loads it through `_loadPlatforms` like any
548
+ * other. What is left of a plugin is a name and two stages. */
549
+ _addPlugin(plugin, context) {
550
+ if (!context.plugins.some(p => p.name === plugin.name))
551
+ context.plugins.push(plugin);
552
+ }
553
+ async _loadPlugins(config, context) {
554
+ if (!config.plugins)
555
+ return;
556
+ const pluginsToLoad = Array.isArray(config.plugins) ? config.plugins : [config.plugins];
557
+ for (const entry of pluginsToLoad) {
558
+ if (entry && typeof entry == 'object' && entry.name) {
559
+ this._assertDeclaredPlugin(entry, `"${PLUGINS_KEY}"`);
560
+ this._addPlugin(entry, context);
561
+ }
562
+ else if (typeof entry == 'string') {
563
+ const plugins = await this._loadPluginsByEntry(entry);
564
+ for (const plugin of plugins)
565
+ this._addPlugin(plugin, context);
566
+ }
567
+ else {
568
+ /**
569
+ * **Anything else is refused rather than skipped**, and the asymmetry it removes is the
570
+ * point: a glob matching no file already throws (`_loadPluginsByEntry`), so ignoring a
571
+ * malformed instance reported the same outcome - a plugin that did not load - one way
572
+ * loudly and the other in silence. Silence is the worse half. Losing a plugin removes the
573
+ * commands and seams a repository is built around, and the symptom surfaces far away as
574
+ * `Unknown argument: clean`, which sends the reader to look at the command rather than at
575
+ * the entry that never loaded.
576
+ *
577
+ * **A nameless entry could not be complained about later even if it were kept.** `name` is
578
+ * the de-duplication key and what every message about a plugin is keyed by, so there would
579
+ * be nothing to call it in the error it eventually causes.
580
+ */
581
+ const got = entry === null
582
+ ? 'null'
583
+ : Array.isArray(entry)
584
+ ? 'an array'
585
+ : typeof entry === 'object'
586
+ ? 'an object with no "name"'
587
+ : `a ${typeof entry}`;
588
+ throw new Error(`"${PLUGINS_KEY}" takes a plugin, or a glob naming modules that export one - got ${got}. ` +
589
+ `Every message about a plugin is keyed by its \`name\`, so an entry without one could ` +
590
+ `not be reported on even where it did load.`);
591
+ }
592
+ }
593
+ }
594
+ /**
595
+ * Refuses a plugin that did not come through `definePlatform`/`definePlugin`.
596
+ *
597
+ * @param from where the entry came from, for the message - the config key for one written into
598
+ * the config, and the file for one a glob loaded.
599
+ */
600
+ /* No structural check could replace this: an rman **1.x plugin was `{ name, init }`**, and under
601
+ * the 2.x split a plugin that contributes nothing but an `init` is *also* `{ name, init }`.
602
+ * Identical objects, identical types - so the declaration has to be explicit, and the factories
603
+ * stamp a non-enumerable `Symbol.for` mark that `isDeclared` is the only reader of.
604
+ *
605
+ * Without it the 1.x plugin loads cleanly, registers, and dies later **inside its own `init`**
606
+ * with `TypeError: ctx.addCommand is not a function`. Measured while converting
607
+ * `@panates/rman-node`: fifteen failures, naming neither the plugin nor the version it was
608
+ * written against.
609
+ *
610
+ * The file is the half of `from` worth having - a glob matching four modules gives the reader
611
+ * nothing to open otherwise. */
612
+ _assertDeclaredPlugin(value, from) {
613
+ if (isDeclared(value))
614
+ return;
615
+ throw new Error(`Plugin "${value.name}" (${from}) was not declared with definePlatform() or definePlugin(). ` +
616
+ `A plain object is how an rman 1.x plugin looks, and a 1.x plugin has nothing left to do: ` +
617
+ `commands, publish targets and other plugins are "commands", "publishTargets" and ` +
618
+ `"${PLUGINS_KEY}" keys of a config now. Wrap a technology in definePlatform({ name, ` +
619
+ `manifestProvider, ... }), anything else in definePlugin({ ... }).`);
620
+ }
621
+ /**
622
+ * Loads the plugins a **string** `plugins` entry names.
623
+ *
624
+ * The string is a glob - never a package name, and already absolute by the time it arrives - so
625
+ * it may match several modules and this returns a list. An entry written as an instance does not
626
+ * come through here; it is already the plugin.
627
+ *
628
+ * Each match must `export default` a plugin declared through `definePlatform`/`definePlugin`.
629
+ * A glob matching nothing is an error.
630
+ */
631
+ /* **The glob is anchored by `mergeConfig`**, to the file that declared it, while the config is
632
+ * read (`anchorContributions`) - the last moment that is knowable, since `plugins` always appends
633
+ * and one resolved list holds entries from this directory's config, every `extends` base and
634
+ * every level above, while `ORIGINS` records one file per *key* rather than per element. Nothing
635
+ * here resolves a path, and adding a resolution step would make a shared config's `'./x.js'` look
636
+ * in the consumer's directory.
637
+ *
638
+ * **`.js` only.** These are imported into rman's own process with no loader registered, so a
639
+ * `.ts` module cannot be one - a TypeScript repository compiles first or writes `.mjs`. Stated
640
+ * rather than attempted: a `.ts` that loads under a test runner's loader and not under the CLI is
641
+ * the worst of both.
642
+ *
643
+ * **The declaration check happens here rather than in the caller** only because this is where the
644
+ * file is still in hand. Nothing else about the result is checked; `name` and the shape are the
645
+ * caller's. */
646
+ async _loadPluginsByEntry(entry) {
647
+ const out = [];
648
+ for (const { value, file } of await this._loadByGlob(entry, PLUGINS_KEY)) {
649
+ this._assertLoaded(value, file, 'plugin');
650
+ out.push(value);
651
+ }
652
+ return out;
653
+ }
654
+ /**
655
+ * Refuses whatever a glob matched when it is not the kind the key takes, naming what it *is*.
656
+ *
657
+ * @param want which key is being loaded, so the message can point at the other one.
658
+ */
659
+ /* **Two of the three answers are facts now, and that is what the declaration kind bought.** The
660
+ * mark used to be a bare `true`, so the only question that could be asked was "declared at all?"
661
+ * - and the answer to everything else was one message about rman 1.x plugins. Measured on two
662
+ * exports: a module returning a *config* (`{ plugins: [] }`) and one returning the string
663
+ * `'oops'` both came back as `Plugin "undefined" … was not declared`, which names a type the
664
+ * value is not and quotes a name it does not have.
665
+ *
666
+ * The remaining guess is the last branch, and it is only about *which sentence* to write - the
667
+ * refusal is already decided, so a wrong guess costs a less helpful message rather than a plugin
668
+ * that loads as the wrong thing. That is the line `plugins` draws elsewhere too: never guess what
669
+ * something *is*, and it is fine to guess what the author meant once you have refused it. */
670
+ _assertLoaded(value, file, want) {
671
+ const kind = declaredKind(value);
672
+ if (kind === want)
673
+ return;
674
+ /** Declared, but as the other one - the commonest mistake, and the only message that can just
675
+ * say so. */
676
+ if (kind) {
677
+ const key = kind === 'platform' ? PLATFORMS_KEY : PLUGINS_KEY;
678
+ throw new Error(`"${want === 'plugin' ? PLUGINS_KEY : PLATFORMS_KEY}" matched "${file}", whose default ` +
679
+ `export is a ${kind}, not a ${want}. Move it to "${key}".`);
680
+ }
681
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
682
+ throw new Error(`"${want === 'plugin' ? PLUGINS_KEY : PLATFORMS_KEY}" takes a ${want} or a glob naming ` +
683
+ `modules that export one - "${file}" exports ${describeEntry(value)}.`);
684
+ }
685
+ /** A config is the near miss worth naming: a package's config reaches a repository through
686
+ * `extends`, which is the key that means "merge this underneath mine" - and reading one out of
687
+ * a `plugins` entry is exactly what 1.x did and what the split removed. */
688
+ if (CONFIG_SHAPED.some(key => key in value)) {
689
+ throw new Error(`"${want === 'plugin' ? PLUGINS_KEY : PLATFORMS_KEY}" matched "${file}", whose default ` +
690
+ `export looks like an rman config rather than a ${want}. A config reaches a repository ` +
691
+ `through "extends", which merges everything it declares underneath your own.`);
692
+ }
693
+ this._assertDeclaredPlugin(value, file);
694
+ }
695
+ /**
696
+ * Every default export a **glob** entry names, with the file each came from.
697
+ *
698
+ * Shared by `plugins` and `platforms`, which take the same two forms and differ only in what they
699
+ * accept back - so the caller checks the shape and this one only finds and imports.
700
+ *
701
+ * The glob is absolute by the time it arrives: `mergeConfig` anchors a contribution glob to the
702
+ * file that declared it (`anchorContributions`). Nothing here resolves a path, and a resolution
703
+ * step would make a preset's `'./x.js'` look in the consumer's directory. `.js` only - these are
704
+ * imported into rman's own process with no loader registered, so a `.ts` module cannot be one.
705
+ */
706
+ async _loadByGlob(entry, key) {
707
+ if (!entry.trim())
708
+ throw new Error(`"${key}" has an empty glob.`);
709
+ /** fast-glob speaks forward slashes on every platform, so a Windows path separator has to be
710
+ * translated rather than passed through as an escape. */
711
+ const files = await fastGlob(entry.split(path.sep).join('/'), { absolute: true, onlyFiles: true });
712
+ /**
713
+ * **A glob matching nothing is an error**, unlike in `commands`, and the asymmetry is the
714
+ * point: losing a plugin silently removes the commands and seams a repository is built around,
715
+ * and "not a known command" sends the reader looking in the wrong place. `commands` can match
716
+ * nothing legitimately - `.rman/*.js` is its default and most repositories have no such
717
+ * directory.
718
+ */
719
+ if (!files.length) {
720
+ /**
721
+ * **A package name gets the answer it is actually asking for.** A bare `rman-node` here is a
722
+ * glob matching nothing, and "matched no file" sends the reader off to check their paths -
723
+ * when what they wrote is a *package*, whose config reaches a repository through `extends`.
724
+ * Measured before this branch existed: `plugins: ['rman-node']` exited 1 saying only that a
725
+ * glob matched nothing, while two documents promised a message naming the fix.
726
+ *
727
+ * It survives to here only because `anchorContributions` leaves a package-shaped entry alone
728
+ * rather than turning it into `<dir>/rman-node`, which would erase the evidence.
729
+ */
730
+ const isPackageName = /^(?:@[a-z0-9-~][\w.-]*\/)?[a-z0-9-~][\w.-]*$/i.test(entry) && !/\.[cm]?js$/i.test(entry);
731
+ if (isPackageName) {
732
+ /** **A name rman itself ships gets pointed at the preset, not at a package.** `node` is
733
+ * also a real package on npm, so the general message would send the reader to install
734
+ * something that has no rman config in it - and the thing they meant is one prefix away. */
735
+ const preset = presetNames().includes(entry) ? `${PRESET_PREFIX}${entry}` : entry;
736
+ throw new Error(`"${key}" entry "${entry}" looks like a name, and this key does not take one - it takes ` +
737
+ `an instance, or a glob naming modules that export one. Write ` +
738
+ `\`extends: "${preset}"\` instead, which merges everything that config declares ` +
739
+ `underneath your own.`);
740
+ }
741
+ throw new Error(`"${key}" glob "${entry}" matched no file. A contribution that does not load is not one.`);
742
+ }
743
+ const out = [];
744
+ /** Sorted and de-duplicated so two globs naming one file load it once, and so the order a
745
+ * repository gets does not depend on the filesystem's. Order decides which platform claims a
746
+ * directory, so it must not vary between machines. */
747
+ for (const file of [...new Set(files.map(f => path.resolve(f)))].sort()) {
748
+ const mod = await import(pathToFileURL(file).href);
749
+ /** `default` only, never the module object: a module's named exports are its API, and taking
750
+ * the whole namespace would accept a file that exports a plugin *among other things* as
751
+ * though the file were one. */
752
+ const exported = mod?.default;
753
+ if (exported === undefined) {
754
+ throw new Error(`"${key}" matched "${file}", which has no default export.`);
755
+ }
756
+ out.push({ value: exported, file });
757
+ }
758
+ return out;
759
+ }
760
+ /**
761
+ *
762
+ * @protected
763
+ */
764
+ _resolveExtendTarget(target, from, label) {
765
+ const dir = path.dirname(path.resolve(from));
766
+ if (target.startsWith(PRESET_PREFIX))
767
+ return this._resolvePreset(target.slice(PRESET_PREFIX.length), label);
768
+ if (target.startsWith('.') || path.isAbsolute(target)) {
769
+ const candidate = path.resolve(dir, target);
770
+ const found = EXTEND_EXTENSIONS.map(ext => candidate + ext).find(f => fs.existsSync(f) && fs.statSync(f).isFile());
771
+ if (!found)
772
+ throw new Error(`"${label}" target "${target}" was not found, resolved from "${from}"`);
773
+ return found;
774
+ }
775
+ try {
776
+ return createRequire(pathToFileURL(path.join(dir, 'noop.js'))).resolve(target);
777
+ }
778
+ catch {
779
+ try {
780
+ return createRequire(import.meta.url).resolve(target);
781
+ }
782
+ catch {
783
+ throw new Error(`"${label}" target "${target}" could not be resolved from "${from}" - is it installed in this repository?`);
784
+ }
785
+ }
786
+ }
787
+ /**
788
+ * Where one of rman's own presets lives - `extends: "rman:node"`.
789
+ *
790
+ * A preset is an ordinary config that happens to ship with rman: it contributes a technology, the
791
+ * commands that belong to it and its publish target, and it reaches a repository the same way any
792
+ * other config does.
793
+ */
794
+ /* **A `rman:` prefix rather than a bare name**, and the reason is what a bare name would cost
795
+ * *later*. `node` is a real package on npm, so `extends: "node"` is already ambiguous - but the
796
+ * durable problem is that adding a preset called `docker` next year would silently change what
797
+ * `extends: "docker"` means for a repository that meant the package. A prefixed namespace is
798
+ * closed: nothing rman adds can ever shadow something a repository already names. The same shape
799
+ * eslint uses for its own (`eslint:recommended`).
800
+ *
801
+ * Resolved against this module rather than the repository - a preset is rman's, wherever rman is
802
+ * installed. */
803
+ _resolvePreset(name, label) {
804
+ if (!/^[a-z][a-z0-9-]*$/i.test(name)) {
805
+ throw new Error(`"${label}" preset name "${name}" is not a name - write \`${PRESET_PREFIX}node\`.`);
806
+ }
807
+ /** Resolved against the presets directory's own location, by extension: `.js` is the published
808
+ * layout and `.ts` is the source tree a spec runs in. A built package never holds the second,
809
+ * so trying it costs one `existsSync` and keeps one code path for both. The directory says
810
+ * where it is (`PRESETS_DIR`) - a path computed here breaks silently when either file moves,
811
+ * which is exactly what happened. */
812
+ const base = path.resolve(PRESETS_DIR, name);
813
+ const found = PRESET_EXTENSIONS.map(ext => base + ext).find(f => fs.existsSync(f) && fs.statSync(f).isFile());
814
+ if (!found)
815
+ throw new Error(`"${label}" has no preset "${name}". rman ships: ${presetNames().join(', ')}.`);
816
+ return found;
817
+ }
818
+ }
819
+ /** What marks a selector as naming technologies rather than packages: `"[platform:node]"`.
820
+ *
821
+ * A `prefix:` inside the brackets rather than a new bracket shape, because the grammar is already
822
+ * there - `"[ws:*]"` used it before the qualifier was retired. */
823
+ const PLATFORM_PREFIX = 'platform:';
824
+ /** Accepted spellings of the retired "not the root" qualifier - see `stripWorkspacePrefix`. */
825
+ const WORKSPACE_PREFIXES = ['workspace:', 'ws:'];
826
+ /** `"[ws:*]"` and `"[workspace:*]"` are the pre-2.x spelling of "not the root", kept working
827
+ * because they now name the same set a bare glob does. Stripped here so one code path serves both. */
828
+ function stripWorkspacePrefix(inner) {
829
+ for (const prefix of WORKSPACE_PREFIXES)
830
+ if (inner.startsWith(prefix))
831
+ return inner.slice(prefix.length);
832
+ return inner;
833
+ }
834
+ /** The glob inside a selector key, anchored at both ends. */
835
+ function globToRegExp(glob) {
836
+ const source = glob
837
+ .split('*')
838
+ .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
839
+ .join('.*');
840
+ return new RegExp(`^${source}$`);
841
+ }
842
+ /** What an entry turned out to be, for a message that has to say how far off it was. */
843
+ function describeEntry(entry) {
844
+ if (entry === null)
845
+ return 'null';
846
+ if (Array.isArray(entry))
847
+ return 'an array';
848
+ if (typeof entry === 'object')
849
+ return 'an object that is not one';
850
+ return `a ${typeof entry}`;
851
+ }