rman 1.2.5 → 2.0.0-beta.2

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 (153) hide show
  1. package/README.md +63 -19
  2. package/cli.d.ts +5 -0
  3. package/cli.js +214 -88
  4. package/commands/build.command.d.ts +177 -3
  5. package/commands/build.command.js +20 -10
  6. package/commands/changed.command.d.ts +80 -3
  7. package/commands/changed.command.js +19 -12
  8. package/commands/changelog.command.d.ts +192 -3
  9. package/commands/changelog.command.js +87 -43
  10. package/commands/config.command.d.ts +44 -3
  11. package/commands/config.command.js +30 -19
  12. package/commands/diff.command.d.ts +37 -3
  13. package/commands/diff.command.js +25 -16
  14. package/commands/exec.command.d.ts +193 -3
  15. package/commands/exec.command.js +60 -58
  16. package/commands/github-release.command.d.ts +154 -3
  17. package/commands/github-release.command.js +67 -37
  18. package/commands/import.command.d.ts +36 -3
  19. package/commands/import.command.js +28 -20
  20. package/commands/info.command.d.ts +35 -7
  21. package/commands/info.command.js +36 -30
  22. package/commands/list.command.d.ts +163 -3
  23. package/commands/list.command.js +109 -71
  24. package/commands/publish.command.d.ts +231 -0
  25. package/commands/publish.command.js +304 -0
  26. package/commands/run.command.d.ts +186 -6
  27. package/commands/run.command.js +26 -72
  28. package/commands/test.command.d.ts +173 -3
  29. package/commands/test.command.js +16 -10
  30. package/commands/version.command.d.ts +317 -3
  31. package/commands/version.command.js +149 -69
  32. package/commands.d.ts +32 -0
  33. package/commands.js +28 -0
  34. package/constants.js +1 -1
  35. package/core/application.d.ts +116 -0
  36. package/core/application.js +143 -0
  37. package/core/command-builder.d.ts +14 -0
  38. package/core/command-builder.js +78 -0
  39. package/core/config.d.ts +180 -50
  40. package/core/config.js +332 -153
  41. package/core/core-services.d.ts +14 -0
  42. package/core/core-services.js +30 -0
  43. package/core/core-targets.d.ts +14 -0
  44. package/core/core-targets.js +16 -0
  45. package/core/custom-command.d.ts +42 -6
  46. package/core/custom-command.js +44 -17
  47. package/core/extends-config.d.ts +13 -5
  48. package/core/extends-config.js +52 -12
  49. package/core/load-config-module.d.ts +28 -0
  50. package/core/load-config-module.js +42 -0
  51. package/core/manifest.d.ts +47 -25
  52. package/core/manifest.js +51 -69
  53. package/core/merge-config.d.ts +33 -34
  54. package/core/merge-config.js +138 -93
  55. package/core/package.d.ts +145 -17
  56. package/core/package.js +128 -36
  57. package/core/plugin-loader.d.ts +65 -0
  58. package/core/plugin-loader.js +234 -0
  59. package/core/plugin.d.ts +135 -90
  60. package/core/plugin.js +70 -173
  61. package/core/publish-target.d.ts +124 -0
  62. package/core/publish-target.js +30 -0
  63. package/core/registry.d.ts +30 -0
  64. package/core/registry.js +47 -0
  65. package/core/repository.d.ts +72 -9
  66. package/core/repository.js +293 -43
  67. package/core/resolve-target.d.ts +1 -1
  68. package/core/resolve-target.js +1 -1
  69. package/core/service.d.ts +49 -0
  70. package/core/service.js +40 -0
  71. package/core/version-scheme.d.ts +23 -1
  72. package/core/version-scheme.js +29 -1
  73. package/core/workspace.d.ts +84 -33
  74. package/core/workspace.js +63 -22
  75. package/index.d.ts +111 -14
  76. package/index.js +85 -9
  77. package/interfaces/rman-config.interface.d.ts +739 -212
  78. package/interfaces/rman-config.interface.js +61 -1
  79. package/package.json +2 -1
  80. package/plugins/builtins.d.ts +44 -0
  81. package/plugins/builtins.js +33 -0
  82. package/plugins/detect.d.ts +78 -0
  83. package/plugins/detect.js +70 -0
  84. package/plugins/node/augmentation/rman.augmentation.d.ts +84 -0
  85. package/plugins/node/augmentation/rman.augmentation.js +1 -0
  86. package/plugins/node/augmentation/system-info.augmentation.d.ts +26 -0
  87. package/plugins/node/augmentation/system-info.augmentation.js +79 -0
  88. package/plugins/node/commands/ci.command.d.ts +131 -0
  89. package/plugins/node/commands/ci.command.js +59 -0
  90. package/plugins/node/commands/clean.command.d.ts +183 -0
  91. package/plugins/node/commands/clean.command.js +73 -0
  92. package/plugins/node/index.d.ts +29 -0
  93. package/plugins/node/index.js +40 -0
  94. package/plugins/node/node-config.interface.d.ts +77 -0
  95. package/plugins/node/node-config.interface.js +7 -0
  96. package/plugins/node/node-manifest.provider.d.ts +68 -0
  97. package/plugins/node/node-manifest.provider.js +125 -0
  98. package/plugins/node/node.platform.d.ts +53 -0
  99. package/plugins/node/node.platform.js +134 -0
  100. package/plugins/node/npm-publish-target.d.ts +73 -0
  101. package/plugins/node/npm-publish-target.js +96 -0
  102. package/plugins/node/services/ci.service.d.ts +47 -0
  103. package/plugins/node/services/ci.service.js +213 -0
  104. package/plugins/node/services/clean.service.d.ts +53 -0
  105. package/plugins/node/services/clean.service.js +237 -0
  106. package/plugins/node/services/publish.service.d.ts +114 -0
  107. package/plugins/node/services/publish.service.js +371 -0
  108. package/plugins/node/services/version-plan.service.d.ts +44 -0
  109. package/plugins/node/services/version-plan.service.js +58 -0
  110. package/plugins/node/utils/npm-view.d.ts +48 -0
  111. package/plugins/node/utils/npm-view.js +71 -0
  112. package/plugins/node/utils/workspace-range.d.ts +26 -0
  113. package/plugins/node/utils/workspace-range.js +28 -0
  114. package/services/change-hash.service.d.ts +2 -2
  115. package/services/change-hash.service.js +2 -2
  116. package/services/changelog.service.d.ts +62 -51
  117. package/services/changelog.service.js +14 -11
  118. package/services/docker-publish.service.d.ts +50 -29
  119. package/services/docker-publish.service.js +43 -20
  120. package/services/exec.service.d.ts +23 -12
  121. package/services/exec.service.js +14 -9
  122. package/services/github-release.service.d.ts +44 -33
  123. package/services/github-release.service.js +13 -10
  124. package/services/import.service.d.ts +25 -14
  125. package/services/import.service.js +9 -5
  126. package/services/list.service.d.ts +62 -10
  127. package/services/list.service.js +62 -15
  128. package/services/run.service.d.ts +22 -13
  129. package/services/run.service.js +262 -223
  130. package/services/version-plan.service.d.ts +27 -4
  131. package/services/version-plan.service.js +42 -15
  132. package/services/version.service.d.ts +31 -11
  133. package/services/version.service.js +29 -13
  134. package/targets/docker.target.d.ts +53 -0
  135. package/targets/docker.target.js +40 -0
  136. package/utils/bin-path.d.ts +6 -7
  137. package/utils/bin-path.js +7 -18
  138. package/utils/branch-guard.d.ts +29 -0
  139. package/utils/branch-guard.js +31 -0
  140. package/utils/exec.d.ts +10 -0
  141. package/utils/exec.js +1 -1
  142. package/utils/logger.d.ts +1 -1
  143. package/utils/logger.js +1 -1
  144. package/utils/package-filter.d.ts +127 -7
  145. package/utils/package-filter.js +197 -16
  146. package/utils/printable-config.d.ts +1 -1
  147. package/utils/printable-config.js +1 -1
  148. package/utils/run-bin.d.ts +10 -0
  149. package/utils/run-bin.js +1 -1
  150. package/utils/run-options.d.ts +97 -0
  151. package/utils/run-options.js +81 -0
  152. package/utils/version-stamp.d.ts +1 -1
  153. package/utils/version-stamp.js +1 -1
package/core/manifest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { RmanApplication } from './application.js';
2
3
  import { semverScheme } from './version-scheme.js';
3
4
  /**
4
5
  * The registry, merged onto the `Manifest` interface so one name carries both the shape and the
@@ -6,71 +7,53 @@ import { semverScheme } from './version-scheme.js';
6
7
  *
7
8
  * A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
8
9
  * is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
9
- * `rman-node` already adds to `SystemInfo`, and anything this seam grows later can arrive the
10
+ * the `node` built-in already adds to `SystemInfo`, and anything this seam grows later can arrive the
10
11
  * same way instead of as another top-level export.
11
12
  */
12
13
  export var Manifest;
13
14
  (function (Manifest) {
14
- /** Registers a provider. Called by `loadPlugins` for each plugin's `manifest`, in `plugins`
15
- * declaration order - so which one answers is a function of the repository's own config. */
16
- function addProvider(provider) {
17
- if (providers.includes(provider))
18
- return;
19
- providers.push(provider);
20
- }
21
- Manifest.addProvider = addProvider;
22
- /** For tests, which would otherwise leak a provider into every later case in the process. */
23
- function clearProviders() {
24
- providers.length = 0;
25
- }
26
- Manifest.clearProviders = clearProviders;
27
15
  /**
28
- * Reads `dir`'s manifest through the first provider that recognizes it, with the scheme that
29
- * provider brings.
16
+ * Reads `dir`'s manifest through **the platform that already claimed it**, with the scheme that
17
+ * platform brings.
18
+ *
19
+ * **It takes a platform rather than searching for one**, and that is where the walk changed
20
+ * things. This used to loop over every registered platform, once per `Package` constructed - so a
21
+ * package asked "who am I?" and the answer was whoever recognized it first, re-derived at every
22
+ * construction. The walk decides it once per directory now (`Workspace.walk` ->
23
+ * `app.platformFor`) and hands it to the package, which is also what lets a nested Cargo package
24
+ * sit inside a Node monorepo: the platform is a fact about the directory, established by whoever
25
+ * found it, not re-guessed by whoever reads it.
30
26
  *
31
- * **With no provider registered, or none recognizing the directory**, the fallback is a package
32
- * named after its own directory at version `0.0.0`. That is deliberately the least it can claim:
33
- * the directory name is a fact, and `0.0.0` is the version a thing has when nothing says
34
- * otherwise. The alternative - refusing to construct a package at all - would make `rman info` and
35
- * `rman list` fail in a repository whose `.rmanrc` simply names no plugin yet, which is exactly
36
- * when someone needs to run them.
27
+ * **When that platform reads nothing** - `basePlatform`, or a provider that claimed the directory
28
+ * and then found nothing in it - the fallback is a package named after its own directory at
29
+ * version `0.0.0`. Deliberately the least it can claim: the directory name is a fact, and `0.0.0`
30
+ * is the version a thing has when nothing says otherwise. The alternative - refusing to construct
31
+ * a package at all - would make `rman info` and `rman list` fail in a repository whose `.rmanrc`
32
+ * simply names no plugin yet, which is exactly when someone needs to run them.
37
33
  */
38
- function read(dir) {
39
- for (const provider of providers) {
40
- const manifest = provider.read(dir);
41
- if (manifest) {
42
- return {
43
- manifest,
44
- versionScheme: provider.versionScheme ?? semverScheme,
45
- fileName: provider.fileName,
46
- provider: provider.name,
47
- };
48
- }
49
- }
34
+ function read(platform, dir) {
35
+ const provider = platform.manifestProvider;
36
+ const manifest = provider.read(dir);
50
37
  return {
51
- manifest: { name: path.basename(dir), version: '0.0.0', raw: {} },
52
- versionScheme: semverScheme,
53
- /** Nothing was read, so nothing can be named - a caller listing "the file I changed" has no
54
- * file to list, which is correct rather than a placeholder that does not exist. */
55
- fileName: '',
56
- /** Same reasoning: no provider claimed this directory, so it belongs to no ecosystem. Empty
57
- * rather than a sentinel like `'unknown'`, which would read as an ecosystem's name and could
58
- * collide with a real provider's. */
59
- provider: '',
38
+ manifest: manifest ?? { name: path.basename(dir), version: '0.0.0', raw: {} },
39
+ versionScheme: provider.versionScheme ?? semverScheme,
40
+ /** Nothing read means nothing to name - a caller listing "the file I changed" has no file to
41
+ * list, which is correct rather than a placeholder that does not exist. */
42
+ fileName: manifest ? provider.fileName : '',
60
43
  };
61
44
  }
62
45
  Manifest.read = read;
63
46
  /** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
64
47
  * nowhere is worse than one that fails, since the caller has already decided the new version. */
65
- function write(dir, manifest) {
66
- for (const provider of providers) {
67
- if (provider.read(dir)) {
68
- provider.write(dir, manifest);
48
+ function write(app, dir, manifest) {
49
+ for (const platform of app.platforms) {
50
+ if (platform.manifestProvider.read(dir)) {
51
+ platform.manifestProvider.write(dir, manifest);
69
52
  return;
70
53
  }
71
54
  }
72
55
  throw new Error(`No manifest provider recognizes "${dir}", so there is nowhere to write its version.\n` +
73
- ` A repository's ".rmanrc" names its providers - see "plugins" (e.g. ['rman-node']).`);
56
+ ` A repository's ".rmanrc" names its technologies - see "plugins" (e.g. ['node']).`);
74
57
  }
75
58
  Manifest.write = write;
76
59
  /**
@@ -81,16 +64,16 @@ export var Manifest;
81
64
  * of can still describe its own graph by hand.
82
65
  */
83
66
  function dependenciesOf(pkg, candidates) {
84
- return providerOf(pkg)?.dependencies?.(pkg.manifest, candidates) ?? [];
67
+ return platformOf(pkg)?.manifestProvider.dependencies?.(pkg.manifest, candidates) ?? [];
85
68
  }
86
69
  Manifest.dependenciesOf = dependenciesOf;
87
70
  /** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
88
71
  * "no scope, the name is its own unscoped form" when none has an opinion. */
89
- function splitName(dir, name) {
90
- for (const provider of providers) {
91
- if (!provider.read(dir))
72
+ function splitName(app, dir, name) {
73
+ for (const platform of app.platforms) {
74
+ if (!platform.manifestProvider.read(dir))
92
75
  continue;
93
- return provider.splitName?.(name) ?? { unscopedName: name };
76
+ return platform.manifestProvider.splitName?.(name) ?? { unscopedName: name };
94
77
  }
95
78
  return { unscopedName: name };
96
79
  }
@@ -102,19 +85,19 @@ export var Manifest;
102
85
  * reference each other by path has nothing here to go stale.
103
86
  */
104
87
  function updateDependencyVersions(pkg, bumped) {
105
- providerOf(pkg)?.updateDependencyVersions?.(pkg.manifest, bumped);
88
+ platformOf(pkg)?.manifestProvider.updateDependencyVersions?.(pkg.manifest, bumped);
106
89
  }
107
90
  Manifest.updateDependencyVersions = updateDependencyVersions;
108
91
  /**
109
92
  * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
110
- * `ManifestProvider.stampVersion`.
93
+ * `Plugin.stampVersion`.
111
94
  *
112
95
  * `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
113
96
  * provider claimed the package, the provider has no opinion about stamping, or it looked and found
114
97
  * nothing to change. All three mean "this file was not stamped", which is what `version` reports.
115
98
  */
116
99
  function stampVersion(pkg, file, content, version, options) {
117
- return providerOf(pkg)?.stampVersion?.(file, content, version, options);
100
+ return platformOf(pkg)?.manifestProvider.stampVersion?.(file, content, version, options);
118
101
  }
119
102
  Manifest.stampVersion = stampVersion;
120
103
  /**
@@ -126,25 +109,24 @@ export var Manifest;
126
109
  * plugin: git tags then answer the boundary question alone.
127
110
  */
128
111
  async function publishedVersion(pkg) {
129
- return providerOf(pkg)?.publishedVersion?.(pkg);
112
+ return platformOf(pkg)?.manifestProvider.publishedVersion?.(pkg);
130
113
  }
131
114
  Manifest.publishedVersion = publishedVersion;
132
- /** The file names providers look for, for an error message that can say what was expected. */
133
- function fileNames() {
134
- return providers.map(p => p.fileName);
115
+ /** The file names the registered technologies look for, for an error message that can say what
116
+ * was expected. */
117
+ function fileNames(app) {
118
+ return [...app.platforms].map(platform => platform.manifestProvider.fileName);
135
119
  }
136
120
  Manifest.fileNames = fileNames;
137
- const providers = [];
138
121
  /**
139
- * The provider that claimed `pkg`, found by the name it reported as `pkg.provider` - exact, and
140
- * without re-reading the manifest off disk to work it out again.
122
+ * The technology that claimed `pkg`, or `undefined` when none did - with nothing to look up.
141
123
  *
142
- * The probe is the fallback, not the rule: it covers a package constructed *before* its provider
143
- * was registered, which `Repository.create` cannot produce (plugins load first) but a test
144
- * arranging providers by hand can.
124
+ * This used to search a registry by `pkg.provider` and fall back to re-reading the directory,
125
+ * because a package could be constructed before the provider that would claim it was registered.
126
+ * It cannot now: a package is handed its `Platform` at construction, by the application that
127
+ * resolved it. The `name` test is what "none claimed it" looks like - see `basePlatform`.
145
128
  */
146
- function providerOf(pkg) {
147
- const byName = pkg.provider ? providers.find(p => p.name === pkg.provider) : undefined;
148
- return byName ?? providers.find(p => p.read(pkg.dirname));
129
+ function platformOf(pkg) {
130
+ return pkg.platform.name ? pkg.platform : undefined;
149
131
  }
150
132
  })(Manifest || (Manifest = {}));
@@ -1,8 +1,19 @@
1
- /** The prefix that turns a key into an append instead of a replacement: `+before` adds to whatever
2
- * `before` already resolved to, rather than taking its place. */
3
- export declare const APPEND_PREFIX = "+";
1
+ /** Where a repository keeps command modules of its own, as a glob or a list of them. */
2
+ export declare const COMMANDS_KEY = "commands";
4
3
  /**
5
- * Keys that **append whether or not you ask** - `+plugins` is accepted and means nothing extra.
4
+ * Keys that **append rather than replace**, which is the only append rman has.
5
+ *
6
+ * **There used to be a `+key` prefix on every key, and it is gone.** It said "add to what this
7
+ * resolved to below", which is exactly what `value` says - and `value` says it better: it composes
8
+ * (three layers each deriving from the one under them), it can reorder or filter rather than only
9
+ * append, and it does not need the merge to keep an append *outstanding* until the layer it belongs
10
+ * to turns up. The prefix also carried a bug `value` does not: appending onto a value that was a
11
+ * sole `${{ }}` expression returning an array nested it, because the merge promoted the expression
12
+ * *string* to a list and interpolation only later turned that element into the array (measured,
13
+ * `[['a','b'],'c']` where a literal list gave `['a','b','c']`).
14
+ *
15
+ * What is left is this list, where appending is not a choice the author makes per layer but what
16
+ * the key *means*.
6
17
  *
7
18
  * `plugins` is the whole list because it is the one key where replacing is never what anyone meant:
8
19
  * every other setting has a value a closer layer can sensibly overrule, while a plugin *adds
@@ -10,6 +21,12 @@ export declare const APPEND_PREFIX = "+";
10
21
  * brought. Replacing was the silent failure - `extends`-ing a toolchain config and then adding a
11
22
  * plugin of your own dropped the toolchain's, and what you noticed was `Unknown argument: publish`.
12
23
  *
24
+ * `commands` and `publishTargets` are the same kind of statement - what this repository has, in
25
+ * instances or in globs naming them - so they append for the same reason: a repository adding one
26
+ * of its own never means "and stop loading the ones my shared config brought". All three are what
27
+ * a config *contributes*, and a contribution list is exactly the case where replacing is never
28
+ * what anyone meant.
29
+ *
13
30
  * Do not extend this list casually: a key that always appends can never be *un*-said by a closer
14
31
  * layer, which is only acceptable where the value is a set of contributions rather than a decision.
15
32
  */
@@ -24,7 +41,7 @@ export declare const ALWAYS_APPEND: readonly string[];
24
41
  * carry one.
25
42
  *
26
43
  * A symbol is invisible to `Object.entries`, `JSON.stringify` and js-yaml, so the chain travels
27
- * through `mergeConfig`, `finalizeConfig` and `rman config` without any of them having to know it
44
+ * through `mergeConfig` and `rman config` without either of them having to know it
28
45
  * is there.
29
46
  *
30
47
  * Each entry is a link, not a single slot: three layers each deriving from the one below need
@@ -51,47 +68,29 @@ export interface PreviousValue {
51
68
  }
52
69
  /** Only these two can ask for `value`, so only these two are worth remembering a previous for. */
53
70
  export declare function carriesPreviousValue(value: unknown): boolean;
54
- /** `"+before"` -> `"before"`, or `undefined` for a key that isn't an append. */
55
- export declare function appendTarget(key: string): string | undefined;
56
71
  /**
57
72
  * Merges `source` onto `target` in place, the way every layer of rman config is combined - a
58
73
  * directory's own file forms, the directory chain, `"[selector]"` blocks, and an `extends` base.
59
74
  * One implementation for all of them, so they cannot disagree about what an append means.
60
75
  *
61
- * Plain keys replace, and nested objects merge recursively, as a deep merge always has. What this
62
- * adds is `+key`:
76
+ * Plain keys replace and nested objects merge recursively, as a deep merge always has. The two
77
+ * things it adds are the contribution keys, which append instead (`ALWAYS_APPEND`), and the record
78
+ * of what each key held underneath, so a closer layer can derive from it as `value`.
63
79
  *
64
80
  * ```yaml
65
- * # the root says before: "rm ./build"
66
- * # a package adds +before: "rm ./cache"
67
- * # it resolves to before: ["rm ./build", "rm ./cache"]
81
+ * # the root says before: "rm ./build"
82
+ * # a package says before: "${{ [...value, 'rm ./cache'] }}"
83
+ * # it resolves to before: ["rm ./build", "rm ./cache"]
68
84
  * ```
69
85
  *
70
- * Appending is the half a shared config can't live without: a base that declares
71
- * `before: ["rm ./build"]` otherwise forces every repository wanting one more step to restate the
72
- * whole list - which is a copy of the base, silently frozen at the version it was copied from.
73
- *
74
- * A scalar is promoted to a list on the way, so neither side has to be written as an array. With
75
- * nothing inherited, `+key` simply produces a one-element list.
86
+ * That is the half a shared config cannot live without: a base declaring `before: ["rm ./build"]`
87
+ * otherwise forces every repository wanting one more step to restate the whole list - which is a
88
+ * copy of the base, silently frozen at the version it was copied from.
76
89
  *
77
- * On a value that isn't a list, the prefix is **ignored** rather than an error, because there the
78
- * two spellings already mean the same thing: objects merge whether or not you asked them to, and a
79
- * scalar has nothing to append to, so `+key` behaves exactly as `key` would. Nothing is silently
80
- * dropped - the value still lands.
81
- *
82
- * `key` and `+key` in the same object are both honored, in that order: the replacement happens
83
- * first, then the append lands on top of it.
90
+ * **A `+key` prefix used to mean this and is refused now**, naming the key and what to write
91
+ * instead - see the check at the top of the loop.
84
92
  */
85
93
  export declare function mergeConfig(target: Record<string, any>, source: Record<string, any>,
86
94
  /** The file `source` was read from, recorded per key - see `ORIGINS`. A caller merging a value it
87
95
  * built rather than read (a selector block already carrying its own origins) passes nothing. */
88
96
  origin?: string): Record<string, any>;
89
- /**
90
- * Turns any `+key` still outstanding into its plain key, as a list - the "nothing was inherited"
91
- * case, where an append simply is the whole value.
92
- *
93
- * Called once on a fully resolved config, and only there: until then an outstanding append may
94
- * still find the layer it belongs to, and a config handed to a command must carry no `+key` at all
95
- * or every reader would have to know about them.
96
- */
97
- export declare function finalizeConfig<T>(config: T): T;
@@ -1,8 +1,23 @@
1
- /** The prefix that turns a key into an append instead of a replacement: `+before` adds to whatever
2
- * `before` already resolved to, rather than taking its place. */
3
- export const APPEND_PREFIX = '+';
1
+ import path from 'node:path';
2
+ /** Where a repository keeps command modules of its own, as a glob or a list of them. */
3
+ export const COMMANDS_KEY = 'commands';
4
+ /** The keys whose string entries are globs, and so have to be anchored to the file that wrote
5
+ * them - see `anchorContributions`. The same three that always append. */
6
+ const GLOB_KEYS = ['plugins', COMMANDS_KEY, 'publishTargets'];
4
7
  /**
5
- * Keys that **append whether or not you ask** - `+plugins` is accepted and means nothing extra.
8
+ * Keys that **append rather than replace**, which is the only append rman has.
9
+ *
10
+ * **There used to be a `+key` prefix on every key, and it is gone.** It said "add to what this
11
+ * resolved to below", which is exactly what `value` says - and `value` says it better: it composes
12
+ * (three layers each deriving from the one under them), it can reorder or filter rather than only
13
+ * append, and it does not need the merge to keep an append *outstanding* until the layer it belongs
14
+ * to turns up. The prefix also carried a bug `value` does not: appending onto a value that was a
15
+ * sole `${{ }}` expression returning an array nested it, because the merge promoted the expression
16
+ * *string* to a list and interpolation only later turned that element into the array (measured,
17
+ * `[['a','b'],'c']` where a literal list gave `['a','b','c']`).
18
+ *
19
+ * What is left is this list, where appending is not a choice the author makes per layer but what
20
+ * the key *means*.
6
21
  *
7
22
  * `plugins` is the whole list because it is the one key where replacing is never what anyone meant:
8
23
  * every other setting has a value a closer layer can sensibly overrule, while a plugin *adds
@@ -10,10 +25,16 @@ export const APPEND_PREFIX = '+';
10
25
  * brought. Replacing was the silent failure - `extends`-ing a toolchain config and then adding a
11
26
  * plugin of your own dropped the toolchain's, and what you noticed was `Unknown argument: publish`.
12
27
  *
28
+ * `commands` and `publishTargets` are the same kind of statement - what this repository has, in
29
+ * instances or in globs naming them - so they append for the same reason: a repository adding one
30
+ * of its own never means "and stop loading the ones my shared config brought". All three are what
31
+ * a config *contributes*, and a contribution list is exactly the case where replacing is never
32
+ * what anyone meant.
33
+ *
13
34
  * Do not extend this list casually: a key that always appends can never be *un*-said by a closer
14
35
  * layer, which is only acceptable where the value is a set of contributions rather than a decision.
15
36
  */
16
- export const ALWAYS_APPEND = ['plugins'];
37
+ export const ALWAYS_APPEND = ['plugins', COMMANDS_KEY, 'publishTargets'];
17
38
  /**
18
39
  * Where a key keeps what it is replacing, so the replacement can be handed it back as `value`.
19
40
  *
@@ -24,7 +45,7 @@ export const ALWAYS_APPEND = ['plugins'];
24
45
  * carry one.
25
46
  *
26
47
  * A symbol is invisible to `Object.entries`, `JSON.stringify` and js-yaml, so the chain travels
27
- * through `mergeConfig`, `finalizeConfig` and `rman config` without any of them having to know it
48
+ * through `mergeConfig` and `rman config` without either of them having to know it
28
49
  * is there.
29
50
  *
30
51
  * Each entry is a link, not a single slot: three layers each deriving from the one below need
@@ -48,112 +69,61 @@ export const ORIGINS = Symbol('rman.origins');
48
69
  export function carriesPreviousValue(value) {
49
70
  return typeof value === 'function' || (typeof value === 'string' && value.includes('${{'));
50
71
  }
51
- /** `"+before"` -> `"before"`, or `undefined` for a key that isn't an append. */
52
- export function appendTarget(key) {
53
- return key.length > APPEND_PREFIX.length && key.startsWith(APPEND_PREFIX)
54
- ? key.slice(APPEND_PREFIX.length)
55
- : undefined;
56
- }
57
72
  /**
58
73
  * Merges `source` onto `target` in place, the way every layer of rman config is combined - a
59
74
  * directory's own file forms, the directory chain, `"[selector]"` blocks, and an `extends` base.
60
75
  * One implementation for all of them, so they cannot disagree about what an append means.
61
76
  *
62
- * Plain keys replace, and nested objects merge recursively, as a deep merge always has. What this
63
- * adds is `+key`:
77
+ * Plain keys replace and nested objects merge recursively, as a deep merge always has. The two
78
+ * things it adds are the contribution keys, which append instead (`ALWAYS_APPEND`), and the record
79
+ * of what each key held underneath, so a closer layer can derive from it as `value`.
64
80
  *
65
81
  * ```yaml
66
- * # the root says before: "rm ./build"
67
- * # a package adds +before: "rm ./cache"
68
- * # it resolves to before: ["rm ./build", "rm ./cache"]
82
+ * # the root says before: "rm ./build"
83
+ * # a package says before: "${{ [...value, 'rm ./cache'] }}"
84
+ * # it resolves to before: ["rm ./build", "rm ./cache"]
69
85
  * ```
70
86
  *
71
- * Appending is the half a shared config can't live without: a base that declares
72
- * `before: ["rm ./build"]` otherwise forces every repository wanting one more step to restate the
73
- * whole list - which is a copy of the base, silently frozen at the version it was copied from.
74
- *
75
- * A scalar is promoted to a list on the way, so neither side has to be written as an array. With
76
- * nothing inherited, `+key` simply produces a one-element list.
87
+ * That is the half a shared config cannot live without: a base declaring `before: ["rm ./build"]`
88
+ * otherwise forces every repository wanting one more step to restate the whole list - which is a
89
+ * copy of the base, silently frozen at the version it was copied from.
77
90
  *
78
- * On a value that isn't a list, the prefix is **ignored** rather than an error, because there the
79
- * two spellings already mean the same thing: objects merge whether or not you asked them to, and a
80
- * scalar has nothing to append to, so `+key` behaves exactly as `key` would. Nothing is silently
81
- * dropped - the value still lands.
82
- *
83
- * `key` and `+key` in the same object are both honored, in that order: the replacement happens
84
- * first, then the append lands on top of it.
91
+ * **A `+key` prefix used to mean this and is refused now**, naming the key and what to write
92
+ * instead - see the check at the top of the loop.
85
93
  */
86
94
  export function mergeConfig(target, source,
87
95
  /** The file `source` was read from, recorded per key - see `ORIGINS`. A caller merging a value it
88
96
  * built rather than read (a selector block already carrying its own origins) passes nothing. */
89
97
  origin) {
90
- // Plain keys first, so a `+key` alongside its own `key` appends to that replacement rather than
91
- // to whatever the previous layer had.
92
98
  for (const [key, value] of Object.entries(source)) {
93
- if (appendTarget(key))
94
- continue;
95
- /** `plugins`: additive at every layer, so the closer one adds rather than takes over. */
99
+ /**
100
+ * **A retired `+key` is refused, not ignored.** rman validates no config keys at all - there is
101
+ * no schema behind `.rmanrc`/`.rmanrc.yml` any more - so an unknown key is silent, and a
102
+ * repository upgrading from 1.x with `+before:` in its config would simply lose that step with
103
+ * nothing said. Measured before this check: `+include: ['extra']` resolved to the inherited
104
+ * list unchanged, exactly as if the line were not there.
105
+ *
106
+ * Here rather than in a validator, because this is the one function every layer passes through,
107
+ * and it is the only place that still knows which file the key came from.
108
+ */
109
+ if (key.length > 1 && key.startsWith('+')) {
110
+ const plain = key.slice(1);
111
+ throw new Error(`"${key}" is no longer a config key${origin ? ` (${origin})` : ''}. The \`+key\` prefix is ` +
112
+ `gone: write "${plain}" as a value that derives from what it inherited - ` +
113
+ `\`${plain}: ({ value }) => [...value, 'x']\`, or \`"\${{ [...value, 'x'] }}"\` in YAML. ` +
114
+ `\`value\` is the layers below this one, and spreads as empty when there are none.`);
115
+ }
116
+ /** `plugins`/`commands`/`publishTargets`: additive at every layer, so the closer one adds
117
+ * rather than takes over. A glob among them is anchored to its own file on the way in - see
118
+ * `anchorContributions`. */
96
119
  if (ALWAYS_APPEND.includes(key)) {
97
- appendList(target, key, value);
120
+ appendList(target, key, GLOB_KEYS.includes(key) ? anchorContributions(value, origin) : value);
98
121
  continue;
99
122
  }
100
123
  assignMerged(target, key, value, source, origin);
101
124
  }
102
- for (const [key, value] of Object.entries(source)) {
103
- const plain = appendTarget(key);
104
- if (!plain)
105
- continue;
106
- // An object merges either way, so the prefix asks for nothing extra - resolve it now and let
107
- // the two spellings coincide.
108
- if (isPlainObject(value) || isPlainObject(target[plain])) {
109
- assignMerged(target, plain, value, source, origin);
110
- continue;
111
- }
112
- if (plain in target) {
113
- appendList(target, plain, value);
114
- continue;
115
- }
116
- /** Nothing to append to *yet*. Kept as an append rather than collapsed into the plain key,
117
- * because the layer that provides it may still be coming: a directory's own file forms are
118
- * merged into an empty object long before the selector blocks and parent directories they
119
- * append to are. Collapsing here lost both of those - measured. `finalizeConfig` turns
120
- * whatever is still outstanding at the end into a plain list. */
121
- target[key] = [...toList(target[key] ?? []), ...toList(value)];
122
- }
123
125
  return target;
124
126
  }
125
- /**
126
- * Turns any `+key` still outstanding into its plain key, as a list - the "nothing was inherited"
127
- * case, where an append simply is the whole value.
128
- *
129
- * Called once on a fully resolved config, and only there: until then an outstanding append may
130
- * still find the layer it belongs to, and a config handed to a command must carry no `+key` at all
131
- * or every reader would have to know about them.
132
- */
133
- export function finalizeConfig(config) {
134
- if (Array.isArray(config))
135
- return config.map(finalizeConfig);
136
- if (!isPlainObject(config))
137
- return config;
138
- const result = {};
139
- for (const [key, value] of Object.entries(config)) {
140
- const plain = appendTarget(key);
141
- if (!plain) {
142
- result[key] = finalizeConfig(value);
143
- continue;
144
- }
145
- const pending = finalizeConfig(value);
146
- result[plain] = plain in result ? [...toList(result[plain]), ...toList(pending)] : toList(pending);
147
- }
148
- /** Carried across by hand: this rebuilds the object from `Object.entries`, which does not see a
149
- * symbol - and dropping it here would lose every `value` chain the merge just recorded. */
150
- for (const carried of [PREVIOUS_VALUES, ORIGINS]) {
151
- const value = config[carried];
152
- if (value)
153
- Object.defineProperty(result, carried, { value });
154
- }
155
- return result;
156
- }
157
127
  function assignMerged(target, key, value, source, origin) {
158
128
  /** A source that already carries origins wins over the caller's: an `extends` base keeps the file
159
129
  * its own keys were written in, rather than being attributed to the file that named it. */
@@ -178,25 +148,55 @@ function assignMerged(target, key, value, source, origin) {
178
148
  *
179
149
  * ```js
180
150
  * '[*]': { clean: { include: ({ vars }) => [vars.buildDir] } }
181
- * '[ws:*]': { clean: { include: "${{ [...value, pkg.basename + '.log'] }}" } }
151
+ * '[*]': { clean: { include: "${{ [...value, pkg.basename + '.log'] }}" } }
182
152
  * ```
183
153
  *
184
154
  * Chained here rather than at resolution time because only the merge knows the order of the
185
155
  * layers - by the time `interpolateConfig` sees the config they have collapsed into one object,
186
156
  * and whatever a closer layer said has already taken the place of what it was derived from.
187
157
  */
188
- if (carriesPreviousValue(value) && key in target) {
158
+ /**
159
+ * **A chain already on `source` is carried over**, the way its `ORIGINS` are just above - and for
160
+ * the same reason: `source` may itself be several layers already merged, and only the merge knows
161
+ * their order.
162
+ *
163
+ * Without this, a chain recorded *inside* a source was silently dropped whenever the key was
164
+ * absent from the target, because the link below was only ever recorded for a key being
165
+ * *replaced*. The shape that hit it is the ordinary one for a shared config: a base and its
166
+ * consumer both writing `"[*]"`. Those two blocks merge into one before `matchingSelectors` sees
167
+ * them - recording the chain on the merged block - and that block is then merged into a `result`
168
+ * which has no such key, so the chain went nowhere.
169
+ *
170
+ * Measured, on a base declaring `clean.include: ['build']` and a consumer's `"[*]"` deriving from
171
+ * it: `${{ [...value, 'dist'] }}` answered `['dist']`, losing `build` outright. It answers
172
+ * `['build', 'dist']` now. Two *different* selectors (`"[*]"` then `"[pkg-a]"`) always worked,
173
+ * and so did an unmarked key, which is why this went unnoticed - those merge into a target that
174
+ * already holds the key.
175
+ */
176
+ const sourceChain = source[PREVIOUS_VALUES]?.[key];
177
+ if (carriesPreviousValue(value) && (key in target || sourceChain)) {
189
178
  const carrier = target;
190
179
  const chain = (carrier[PREVIOUS_VALUES] ??= {});
191
- chain[key] = { value: target[key], previous: chain[key] };
180
+ /** The layer the target itself stands for, bottom-most of the three: what it holds now, over
181
+ * whatever *that* was derived from. Absent when the target never had the key. */
182
+ const below = key in target ? { value: target[key], previous: chain[key] } : chain[key];
183
+ /** Bottom-up: the target's layer, then `source`'s own intermediate ones, then `value` on top -
184
+ * so `source`'s chain keeps its internal order and gets the target's spliced beneath it. */
185
+ chain[key] = sourceChain ? graftChain(sourceChain, below) : below;
192
186
  }
193
187
  target[key] = Array.isArray(value) ? [...value] : value;
194
188
  }
189
+ /** `node`'s chain, copied with `tail` spliced in beneath its deepest link - so two layer-stacks
190
+ * join into one without either being mutated (a chain is shared by every package that resolved
191
+ * through it, so grafting in place would rewrite what the others see). */
192
+ function graftChain(node, tail) {
193
+ return { value: node.value, previous: node.previous ? graftChain(node.previous, tail) : tail };
194
+ }
195
195
  /**
196
196
  * Appends `value` to `target[key]`, de-duplicating **only** an `ALWAYS_APPEND` key.
197
197
  *
198
198
  * That asymmetry is the point. `plugins` appends without being asked, so a repository and the config
199
- * it extends both naming `'rman-node'` is the ordinary case rather than a mistake, and the list is
199
+ * it extends both naming `'node'` is the ordinary case rather than a mistake, and the list is
200
200
  * also what `rman config` prints. An explicit `+before`, by contrast, was *written* - repeating a
201
201
  * step that is already there is a strange thing to ask for, but it is what was asked for, and
202
202
  * silently collapsing it would make one layer's list depend on another's contents.
@@ -212,6 +212,51 @@ function appendList(target, key, value) {
212
212
  function toList(value) {
213
213
  return Array.isArray(value) ? value : [value];
214
214
  }
215
+ /**
216
+ * Anchors every glob in `plugins`/`commands`/`publishTargets` to the directory of the file that
217
+ * declared it, on the way in. A non-string entry - an instance written straight into the config -
218
+ * passes through untouched.
219
+ *
220
+ * **Done here, at the merge, because this is the last moment the answer is known.** `commands`
221
+ * always appends, so one resolved list ends up holding entries from the repository's own
222
+ * `.rmanrc`, from each `extends` base, and from every directory above - and `ORIGINS` records one
223
+ * file per *key*, not per element, so after the merge there is nothing left to attribute them by.
224
+ * Rewriting each glob as it arrives makes the merge trivially correct and costs one `path.resolve`.
225
+ *
226
+ * It is what lets a **shared config ship its own contributions**: `commands: './commands/*.js'`
227
+ * in a published package means that package's directory, wherever the repository inheriting it
228
+ * happens to sit. All three keys behave the same way; `plugins` used to resolve every entry
229
+ * against the repository root instead, whatever file declared it, and that asymmetry is gone.
230
+ *
231
+ * `origin` is absent when a caller merges a value it built rather than read (a selector block, the
232
+ * directory chain layering already-resolved configs); those globs have been anchored already, and
233
+ * an absolute path is left alone by `path.resolve` anyway.
234
+ */
235
+ function anchorContributions(value, origin) {
236
+ if (!origin)
237
+ return value;
238
+ const dir = path.dirname(origin);
239
+ const anchor = (entry) => typeof entry === 'string' && !looksLikePackageName(entry) ? path.resolve(dir, entry) : entry;
240
+ return Array.isArray(value) ? value.map(anchor) : anchor(value);
241
+ }
242
+ /**
243
+ * Whether an entry is shaped like a **package name** rather than a glob - `rman-node`,
244
+ * `@panates/rman-node`, but not `*.js`, `./x.js` or `commands/*.mjs`.
245
+ *
246
+ * Such an entry is left unanchored, because it is not a relative path and turning it into one
247
+ * destroys the only evidence of what the author meant. A package name is never valid in these keys
248
+ * (a package's config arrives through `extends`), so the whole value of keeping it intact is the
249
+ * error message: anchored, `plugins: ['rman-node']` failed with `glob ".../rman-node" matched no
250
+ * file`, which sends the reader to check their paths. `loadPlugins` can now say what they actually
251
+ * wrote and what to write instead - which two doc pages already promised it did.
252
+ *
253
+ * Shape, not a resolver call: decidable without touching the disk, and a wrong guess only chooses
254
+ * which of two error messages a failing entry gets. An extension is excluded so a bare `plugin.js`
255
+ * beside the config is still anchored as the relative path it is.
256
+ */
257
+ function looksLikePackageName(entry) {
258
+ return /^(?:@[a-z0-9-~][\w.-]*\/)?[a-z0-9-~][\w.-]*$/i.test(entry) && !/\.[cm]?js$/i.test(entry);
259
+ }
215
260
  /** A config object, as opposed to an array or anything with its own prototype - only the former
216
261
  * merges key by key. */
217
262
  function isPlainObject(value) {