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
@@ -1,15 +1,14 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import colors from 'ansi-colors';
3
2
  import path from 'path';
4
3
  import semver from 'semver';
5
- import { detectBuiltin } from '../plugins/detect.js';
6
- import { GitHelper } from '../utils/git.js';
7
- import { RmanApplication } from './application.js';
8
- import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
9
- import { Manifest } from './manifest.js';
10
- import { ORIGINS } from './merge-config.js';
4
+ import {} from '../../interfaces/config-scope.interface.js';
5
+ import { GitHelper } from '../../utils/git.js';
6
+ import { RmanApplication } from '../application.js';
7
+ import { ConfigFileScope } from '../config/config-file-scope.js';
8
+ import { ConfigInterpolator } from '../config/config-interpolator.js';
9
+ import { DEFERRED_PATHS } from '../config/config-paths.js';
10
+ import { Manifest } from '../interfaces/manifest.js';
11
11
  import { Package } from './package.js';
12
- import { loadPlugins, registerPlugin } from './plugin-loader.js';
13
12
  import { Workspace } from './workspace.js';
14
13
  export class Repository extends Package {
15
14
  dirname;
@@ -17,8 +16,6 @@ export class Repository extends Package {
17
16
  packages;
18
17
  cwd;
19
18
  rootPackage;
20
- /** See the `defineProperty` in the constructor - what detection supplied, when it did. */
21
- detectedBuiltin;
22
19
  /** Commands the repository's plugins contributed, loaded during `create` because the workspace
23
20
  * providers they bring are needed before any package can be found. `cli.ts` registers them. */
24
21
  /**
@@ -36,14 +33,15 @@ export class Repository extends Package {
36
33
  * and because reading it is a subprocess: a deep walk of a package must not spawn one. */
37
34
  _git;
38
35
  /**
39
- * Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
40
- * of the bytes - see `readStructuredFile`.
36
+ * What `${{ file... }}` and `${{ read(...) }}` answer from, for every package.
41
37
  *
42
- * **On the repository rather than per scope, and that is the whole point of it**: `configScope`
43
- * is built once per package, so a cache living there would re-read a repository-level file once
44
- * for every package that mentions it.
38
+ * **One per repository rather than one per scope, and that is the whole point of it**:
39
+ * `configScope` is built once per package, so a parse cache living there would re-read a
40
+ * repository-level file once for every package that mentions it. The cache inside is keyed by the
41
+ * identity of the bytes (`mtimeNs:size`), because rman writes JSON while it runs - `version`
42
+ * rewrites every bumped manifest and then re-interpolates its deferred hooks.
45
43
  */
46
- _readCache = new Map();
44
+ _files = new ConfigFileScope();
47
45
  /**
48
46
  * The application this repository belongs to - its services, its technologies, its logger.
49
47
  *
@@ -68,15 +66,6 @@ export class Repository extends Package {
68
66
  this.packages = packages;
69
67
  this.cwd = cwd;
70
68
  Object.defineProperty(this, 'app', { value: app, enumerable: false, writable: false });
71
- /**
72
- * What detection decided for this repository, or `undefined` when it declared its own
73
- * technology (or when there was nothing to detect). Carried here because `_resolveConfigs` runs
74
- * later and every read of the root's config has to agree with the one decision `create` made.
75
- *
76
- * Non-enumerable, like `app` and for the same reason: `{...repository}` and `toEqual` both walk
77
- * a repository, and bookkeeping that shows up there turns spec failures into diffs about it.
78
- */
79
- Object.defineProperty(this, 'detectedBuiltin', { value: undefined, enumerable: false, writable: true });
80
69
  this.rootPackage = new Package(dirname, app, platform);
81
70
  if (!monorepo)
82
71
  this.packages = [this.rootPackage];
@@ -170,10 +159,10 @@ export class Repository extends Package {
170
159
  repository: this._repositoryScope(),
171
160
  /** `pkg.dirname`, not the repository root: a `"[*]"` block asking whether
172
161
  * `tsconfig-build.json` exists has to be answered per package. */
173
- file: createFileScope(pkg.dirname),
162
+ file: this._files.fileScope(pkg.dirname),
174
163
  /** Same base directory as `file`, so one `"[*]"` declaration reads each package's own copy -
175
164
  * and the cache is the repository's, so a file they *share* is parsed once. */
176
- read: createReadScope(pkg.dirname, this._readCache),
165
+ read: this._files.readScope(pkg.dirname),
177
166
  env: { ...process.env },
178
167
  semver,
179
168
  path,
@@ -258,75 +247,25 @@ export class Repository extends Package {
258
247
  Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
259
248
  }
260
249
  /**
261
- * Gives every package the selector a `"[glob]"` block and `--scope` match it by, and refuses two
262
- * packages that would answer to the same one.
263
- *
264
- * **Its own step, before any config is resolved by a selector**, which is the ordering that makes
265
- * the rest work: `_resolveConfigs` asks `resolveConfig` for each package *by selector*, so an
266
- * address assigned afterwards would be applied to nothing.
267
- *
268
- * **`name` comes from the unmarked cascade only** - the same read `platform` gets, from the same
269
- * cache, for the same reason. A `"[glob]"` block cannot set it (`assertSelectorBlocks` refuses
270
- * one) because the glob matches the very thing the block would be setting.
271
- *
272
- * **Uniqueness is checked, and the cascade is the mistake it usually catches.** `name` cascades
273
- * like every unmarked key, so one declaration above two packages gives both the same address -
274
- * and the failure would otherwise be silent in the worst way: the config reaches both and
275
- * `getPackage` returns whichever came first. The message names both directories, and says the
276
- * cascade out loud when the two got it from one declaration.
250
+ * Bakes every package's config: the raw one the workspace cascaded, with every `${{ }}` and value
251
+ * function resolved against that package's scope.
277
252
  */
278
- async _assignSelectors(rootDir, cache) {
279
- const declaredBy = new Map();
280
- for (const pkg of [this.rootPackage, ...this.packages]) {
281
- const config = await resolveConfig(rootDir, pkg.dirname, cache, undefined);
282
- const declared = config.name;
283
- if (declared !== undefined && (typeof declared !== 'string' || !declared.trim())) {
284
- throw new Error(`"name" takes the selector this package answers to - "${pkg.dirname}" gave ${typeof declared}.`);
285
- }
286
- pkg.selector = declared?.trim() || pkg.platformSelector();
287
- declaredBy.set(pkg, declared !== undefined);
288
- }
289
- /** The root is left out: a glob never matches it and `"[/]"` needs no name, so it shares an
290
- * address with nobody - see `Package.selector`. */
291
- const bySelector = new Map();
292
- for (const pkg of this.packages) {
293
- const clash = bySelector.get(pkg.selector);
294
- if (clash) {
295
- const cascaded = declaredBy.get(pkg) && declaredBy.get(clash);
296
- throw new Error(`Two packages answer to the selector "${pkg.selector}":\n ${clash.dirname}\n ${pkg.dirname}\n` +
297
- ` A selector has to be unique - "[${pkg.selector}]" and \`--scope ${pkg.selector}\` ` +
298
- `cannot mean two packages.` +
299
- (cascaded
300
- ? `\n Both got it from one cascading "name" declaration above them; declare it in ` +
301
- `each package's own ".rmanrc" instead.`
302
- : ''));
303
- }
304
- bySelector.set(pkg.selector, pkg);
305
- }
306
- }
253
+ /* **The one step the workspace deliberately stops short of.** Resolving an expression needs
254
+ * `pkg`, `repository`, `file` and `git` - which belong to a repository rather than to its layout,
255
+ * and `configScope` is where the measured subtleties of building that scope live.
256
+ *
257
+ * `DEFERRED_PATHS` are left raw, so the resolved config is the only copy anyone needs - `version`
258
+ * reads its own hooks straight off it and interpolates them once `pkg.targetVersion` exists. */
307
259
  async _resolveConfigs() {
308
- const cache = new Map();
309
- /**
310
- * `DEFERRED_PATHS` are left *raw* in the result (see `interpolateConfig`'s `walk`), so the
311
- * resolved config is the only copy anyone needs - `version` reads its own hooks straight off
312
- * `config` and interpolates them itself once `pkg.targetVersion` exists. There used to be a
313
- * second `rawConfig` copy of every package's config for that one reader; measured identical.
314
- */
315
- /**
316
- * **By selector, not by name** - `"[glob]"` matches `Package.selector`, which is the package's
317
- * own `.rmanrc "name"` when it assigned one and its platform's answer otherwise. They coincide
318
- * for every Node repository; they are not the same question, and `name` was the wrong one to
319
- * ask, since a package having one at all is an ecosystem's promise rather than rman's.
320
- *
321
- * The root passes its own too, although no glob can match it: `"[/]"` is applied on the
322
- * strength of the target *being* the root and needs no selector at all (see `resolveConfig`).
323
- */
324
- const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.selector, this.detectedBuiltin);
325
- this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
326
- for (const pkg of this.packages) {
327
- const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.selector, this.detectedBuiltin);
328
- pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
329
- }
260
+ const interpolator = new ConfigInterpolator();
261
+ const bake = (pkg) => interpolator.interpolate({
262
+ config: pkg.rawConfig,
263
+ scope: this.configScope(pkg),
264
+ skip: DEFERRED_PATHS,
265
+ });
266
+ this.config = bake(this.rootPackage);
267
+ for (const pkg of this.packages)
268
+ pkg.config = bake(pkg);
330
269
  if (this.monorepo)
331
270
  this.rootPackage.config = this.config;
332
271
  }
@@ -496,171 +435,103 @@ export class Repository extends Package {
496
435
  * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
497
436
  * the one package.
498
437
  *
499
- * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
438
+ * **A repository no technology recognizes has no packages beyond itself**, and that is the
500
439
  * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
501
- * takes `plugins: ['node']` - or detection reading the directory as a Node one - for it to be
502
- * read as a workspace at all.
440
+ * takes a platform that reads one for the directory to be a workspace at all. rman's own presets
441
+ * are laid under every root (see `DEFAULT_PRESETS`), so a plain Node repository needs no config
442
+ * file to say so; anything else arrives through `extends` or `platforms`.
503
443
  */
444
+ /* **`presets` is an opt-out, and it exists for the specs that must not have one.** rman's own
445
+ * presets are laid under every root, so a caller that brought its own ecosystem would otherwise
446
+ * get the `node` technology, `clean`, `ci` and the npm publish target beside it. That is right
447
+ * for a repository and wrong for a synthetic one: measured, the core's `publish` specs ran a
448
+ * **real `npm publish` against registry.npmjs.org**, and only a 404 stopped it - the failure
449
+ * `useLocalBin`'s doc already records for `docker`, arriving by a new route. `test/_fixture.ts`
450
+ * passes `[]`. */
504
451
  static async create(root, options) {
505
452
  const from = root || process.cwd();
506
453
  const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
507
- /**
508
- * One application per repository, made here unless the caller brought one.
509
- *
510
- * `runCli` passes its own so that `--log-level` reaches the logger; a spec that only wants a
511
- * repository lets this make one, which is also what keeps two repositories in a single process
512
- * from sharing anything - the thing that used to need five `clear*()` calls before every test.
513
- */
514
454
  const app = options?.app ?? new RmanApplication();
515
- /** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
516
- * package list (which does not exist yet) nor expression interpolation. */
517
- const declared = await readDirConfig(rootDir);
518
- /**
519
- * **What this repository looks like, when nothing said** - the other half of shipping the
520
- * built-ins in the box. See `detectBuiltin`.
521
- *
522
- * **Two conditions, and the second is the one that is easy to miss.** The config declaring no
523
- * `plugins` is not the same as the *repository* having no technology: a programmatic caller -
524
- * and every spec in this suite - registers one straight onto the application without writing a
525
- * config at all. Guessing on top of that registers a second technology, and the first provider
526
- * that recognizes a directory decides whether it holds a package. Measured with only the config
527
- * condition: ten specs changed answer, seven of them about config cascading and three about
528
- * `publish`'s flags.
529
- *
530
- * **`platforms`, not `plugins`, and the difference is what detection produces.** What would be
531
- * added here is a *platform*, so what must not already be there is a platform - a plugin that
532
- * only registers a command says nothing about which directories hold packages, and letting it
533
- * suppress the guess would leave a Node repository undetected for having added a command.
534
- *
535
- * **A root `platform` counts as having said something**, like `plugins: []` does. Detection
536
- * exists for a repository that stated nothing; one naming its technology has stated the very
537
- * thing detection would be guessing at, and guessing anyway would register a *second* platform
538
- * beside the declared one - which then competes for every directory the declaration did not
539
- * cover.
540
- *
541
- * Decided **once**, here, and handed to every read that has to agree - `readDirConfig` has no
542
- * business knowing about an application.
543
- */
544
- const saidSomething = declared.plugins !== undefined || declared.platform !== undefined;
545
- const detected = !saidSomething && app.platforms.size === 0 ? await detectBuiltin(rootDir) : undefined;
546
- const rootConfig = detected ? await readDirConfig(rootDir, { inject: detected }) : declared;
547
455
  /**
548
- * **Said out loud, because a guess the reader cannot see is one they cannot correct.**
456
+ * **The whole of discovery and config reading is the workspace's.** It reads each level's
457
+ * config, loads whatever that level names, decides which technology claims each directory,
458
+ * assigns the selectors and cascades every package's **raw** config.
549
459
  *
550
- * To **stderr**, not through `app.logger`: that writes with `console.log`, and `rman list
551
- * --json` has to stay a parseable document on stdout - measured, its output is pure JSON, and
552
- * one line of prose in front of it breaks every `| jq`. The same reason `--help`'s degradation
553
- * notice goes to stderr. Not at `silent`, where the caller asked for no narration.
460
+ * Technologies already on the application are handed over: a programmatic caller - and every
461
+ * spec in this suite - registers one without writing a config, and the walk has to see it.
554
462
  */
555
- if (detected && app.logger.level !== 'silent') {
556
- const line = `${detected.name} repository detected (${detected.because}) - ` +
557
- `write \`plugins: ['${detected.name}']\` in .rmanrc to state it, or \`plugins: []\` for none.`;
558
- console.error(process.stderr.isTTY ? colors.gray(line) : line);
559
- }
560
- await loadPlugins(app, rootConfig);
561
- /**
562
- * **Its own cache, deliberately not shared with `_resolveConfigs`'.**
563
- *
564
- * Both cache `readDirConfig` per directory, and the two reads of the *root* are not the same
565
- * read: `_resolveConfigs` passes `detectedBuiltin` as `inject` and this one passes nothing,
566
- * since a built-in's contribution has no bearing on which platform a directory declares. The
567
- * cache is keyed by directory alone, so one shared map would hand whichever ran first to the
568
- * other - and for the root that is a config with or without the whole node built-in merged in.
569
- *
570
- * The cost is one extra read per directory in the chain, on a repository that is about to read
571
- * every one of them again per package anyway.
572
- */
573
- const platformCache = new Map();
574
- /**
575
- * **The walk**, which is where the package list comes from now - descending from the root,
576
- * asking each directory's own technology where its children are. `Workspace.resolve` asked the
577
- * root once, through whichever platform recognized it first; the tree is the shape discovery
578
- * actually has, and it is what makes a nested package of another technology findable at all.
579
- */
580
- const tree = await Workspace.walk(app, rootDir, {
463
+ const workspace = await Workspace.create(rootDir, {
464
+ app,
581
465
  deep: options?.deep,
582
- declared: dir => declaredPlatformAt(app, rootDir, dir, platformCache),
466
+ platforms: [...app.platforms],
467
+ presets: options?.presets,
583
468
  });
584
- const nodes = Workspace.flatten(tree);
585
- const packages = nodes.map(node => new Package(node.dirname, app, node.platform));
586
- const repo = new Repository(app, tree.dirname, packages.length > 0, packages, from, tree.platform);
587
- repo._linkPackages(tree, nodes, packages);
588
- /** Before `_resolveConfigs`, because a `"[glob]"` block is matched against the selector - so
589
- * the addresses have to be settled before anything is resolved by them. */
590
- await repo._assignSelectors(rootDir, platformCache);
591
- /** The application is what the plugins registered into a moment ago; from here on it can hand
592
- * out services, which need the repository to work on. */
593
- repo.detectedBuiltin = detected;
469
+ /** What the configs named, onto the application the commands read from. */
470
+ for (const platform of workspace.platforms)
471
+ app.platforms.add(platform);
472
+ for (const plugin of workspace.plugins) {
473
+ app.plugins.add(plugin);
474
+ await plugin.afterInitApplication?.({ app });
475
+ }
476
+ /** One loop, because `workspace.platforms` already holds every technology in play whatever
477
+ * route it arrived by. There used to be a second one over each plugin's own `platforms`, and
478
+ * by the end it was pure duplication of this. */
479
+ for (const platform of workspace.platforms) {
480
+ if (platform.versionPlanner)
481
+ app.versionPlanner = platform.versionPlanner;
482
+ }
483
+ /** **Publish targets the root config declared.** `publish` builds its `--target` choices from
484
+ * `app.publishTargets` when its factory runs, so they have to be there before the commands
485
+ * are. Read off the root's own level rather than a package's: a target is the repository's. */
486
+ for (const target of toList(workspace.rootPackage.rawConfig.publishTargets)) {
487
+ if (!target || typeof target !== 'object' || typeof target.name !== 'string') {
488
+ throw new Error(`"publishTargets" takes a target or a glob naming modules that export one - got ` +
489
+ `${target === null ? 'null' : typeof target}.`);
490
+ }
491
+ app.publishTargets.add(target);
492
+ }
493
+ const packages = workspace.packages;
494
+ const repo = new Repository(app, rootDir, packages.length > 0, packages, from, workspace.rootPackage.platform);
495
+ repo._adopt(workspace);
594
496
  app.attachRepository(repo);
595
497
  return Repository._init(repo);
596
498
  }
597
- /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
598
- * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
599
- * before the dependency graph is built from it. */
499
+ /**
500
+ * Takes the workspace's packages as this repository's own - the back-references a `Package` needs
501
+ * and the parent/child edges the walk established.
502
+ */
503
+ /* `Repository extends Package` while the workspace built its own root package for the same
504
+ * directory, so both are, truthfully, the root. This keeps the workspace's - it is the one the
505
+ * walk handed a platform and a selector - and leaves `Repository` itself pointing at the same
506
+ * children. */
507
+ _adopt(workspace) {
508
+ const link = (pkg) => void Object.defineProperty(pkg, 'repository', {
509
+ value: this,
510
+ enumerable: false,
511
+ configurable: true,
512
+ writable: true,
513
+ });
514
+ link(this);
515
+ this.rootPackage = workspace.rootPackage;
516
+ link(this.rootPackage);
517
+ for (const pkg of this.packages)
518
+ link(pkg);
519
+ if (!this.monorepo)
520
+ this.packages = [this.rootPackage];
521
+ Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
522
+ }
600
523
  static async _init(repo) {
601
524
  await repo._resolveConfigs();
602
525
  repo._updateDependencies();
526
+ /** **Last, and that is the whole point of the second stage.** Every package is found, every
527
+ * config is baked and the dependency graph is linked - so a plugin handed this repository can
528
+ * read it rather than being told to wait for a command it may not contribute. */
529
+ for (const plugin of repo.app.plugins)
530
+ await plugin.afterInitRepository?.({ app: repo.app, repository: repo });
603
531
  return repo;
604
532
  }
605
533
  }
606
- /**
607
- * The platform `dir` declares in its cascaded `.rmanrc "platform"`, loaded if it has to be -
608
- * `undefined` when the directory declares none, so the walk falls back to its guess.
609
- *
610
- * **The unmarked cascade only**, which is what `resolveConfig` gives with no package name: a
611
- * selector matches a package *name*, and this runs while the packages are still being found - the
612
- * name is not known yet, and it is read *from* a manifest whose reader this key decides. So
613
- * `"[/]"` reaches the root (its directory is the repository root, which needs no name) and a glob
614
- * block contributes nothing here, which is the honest answer rather than a half-applied one.
615
- */
616
- async function declaredPlatformAt(app, rootDir, dir, cache) {
617
- const config = await resolveConfig(rootDir, dir, cache);
618
- const declared = config.platform;
619
- if (declared === undefined)
620
- return undefined;
621
- if (typeof declared !== 'string' || !declared.trim()) {
622
- throw new Error(`"platform" takes a platform's name - ${originOf(config)} gave ${typeof declared}.`);
623
- }
624
- /**
625
- * **An expression is refused rather than read as a literal.** `platform` is consulted before any
626
- * package exists - it is what decides what a package *is* - so there is no `pkg` for an
627
- * expression to be about, and `interpolateConfig` runs long afterwards. Passed through, a
628
- * `${{ }}` here reached the lookup below as the raw text and failed as an unknown platform name,
629
- * which sends the reader to check their `plugins`.
630
- */
631
- if (declared.includes('${{')) {
632
- throw new Error(`"platform" cannot be an expression (${originOf(config)}) - it is read while ` +
633
- `the packages are still being found, so there is no package for one to be about. Write the ` +
634
- `name, and use a package's own ".rmanrc" where the answer differs.`);
635
- }
636
- const registered = [...app.platforms].find(p => p.name === declared);
637
- if (registered)
638
- return registered;
639
- /**
640
- * **A built-in is loaded on the strength of being named**, from any level - `platform: 'node'` is
641
- * enough, and it is what a repository holding one Node package among others writes.
642
- *
643
- * `platform()` and not `contribute()`, and that split is why the two halves exist: what arrives
644
- * is the technology alone. Commands and publish targets come from root `plugins`, because they
645
- * are repository-wide - a Node package inside a Cargo repository wants npm's manifest read, not
646
- * an `rman clean` that would sweep the whole tree.
647
- */
648
- const { BUILTIN_PLUGINS, builtinPluginNames } = await import('../plugins/builtins.js');
649
- const builtin = BUILTIN_PLUGINS[declared];
650
- if (builtin) {
651
- const platform = builtin.platform();
652
- registerPlugin(app, platform);
653
- return platform;
654
- }
655
- const have = [...app.platforms].map(p => p.name).filter(Boolean);
656
- throw new Error(`"platform" names "${declared}" (${originOf(config)}), which is not a platform ` +
657
- `this repository has. ${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
658
- `rman ships ${builtinPluginNames().join(', ')}; anything else arrives through "plugins".`);
659
- }
660
- /** Which file the `platform` key came from, for an error that can be acted on - `mergeConfig`
661
- * records one per key under `ORIGINS`, and a config is merged from a directory's own four forms,
662
- * an `extends` base and one layer per directory before anything reads it. */
663
- function originOf(config) {
664
- const origins = config[ORIGINS];
665
- return origins?.platform ? `in "${origins.platform}"` : 'the "platform" key';
534
+ /** A key that takes one value or a list of them, as a list. */
535
+ function toList(value) {
536
+ return value === undefined ? [] : Array.isArray(value) ? value : [value];
666
537
  }
@@ -1,5 +1,5 @@
1
- import type { Logger } from '../utils/logger.js';
2
- import type { RmanApplication } from './application.js';
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { RmanApplication } from '../application.js';
3
3
  import type { Repository } from './repository.js';
4
4
  /**
5
5
  * **The one service shape.** Everything that does work on a repository is a class extending this,