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