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
@@ -0,0 +1,14 @@
1
+ import type { RmanApplication } from './application.js';
2
+ /**
3
+ * Every service the core brings, registered as an application is built.
4
+ *
5
+ * **Factories rather than instances**, so nothing is constructed until something asks: `rman info`
6
+ * has no business building the changelog or release services, and services reach each other through
7
+ * the application, so resolving at call time is also what keeps that from being a construction
8
+ * cycle.
9
+ *
10
+ * Here rather than inside `RmanApplication` so the composition list is one readable file, and so
11
+ * the application itself imports no service - only this does. A plugin adds its own the same way,
12
+ * from its own package, with `app.setService`.
13
+ */
14
+ export declare function registerCoreServices(app: RmanApplication): void;
@@ -0,0 +1,30 @@
1
+ import { ChangelogService } from '../services/changelog.service.js';
2
+ import { DockerPublishService } from '../services/docker-publish.service.js';
3
+ import { ExecService } from '../services/exec.service.js';
4
+ import { GithubReleaseService } from '../services/github-release.service.js';
5
+ import { ImportService } from '../services/import.service.js';
6
+ import { ListService } from '../services/list.service.js';
7
+ import { RunService } from '../services/run.service.js';
8
+ import { VersionService } from '../services/version.service.js';
9
+ /**
10
+ * Every service the core brings, registered as an application is built.
11
+ *
12
+ * **Factories rather than instances**, so nothing is constructed until something asks: `rman info`
13
+ * has no business building the changelog or release services, and services reach each other through
14
+ * the application, so resolving at call time is also what keeps that from being a construction
15
+ * cycle.
16
+ *
17
+ * Here rather than inside `RmanApplication` so the composition list is one readable file, and so
18
+ * the application itself imports no service - only this does. A plugin adds its own the same way,
19
+ * from its own package, with `app.setService`.
20
+ */
21
+ export function registerCoreServices(app) {
22
+ app.setService('changelog', a => new ChangelogService(a));
23
+ app.setService('dockerPublish', a => new DockerPublishService(a));
24
+ app.setService('githubRelease', a => new GithubReleaseService(a));
25
+ app.setService('exec', a => new ExecService(a));
26
+ app.setService('import', a => new ImportService(a));
27
+ app.setService('list', a => new ListService(a));
28
+ app.setService('run', a => new RunService(a));
29
+ app.setService('version', a => new VersionService(a));
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { RmanApplication } from './application.js';
2
+ /**
3
+ * Every publish target the core brings, registered as an application is built - `registerCoreServices`
4
+ * for the other half of what a fresh application starts with.
5
+ *
6
+ * Exactly one today, and that is the point: `docker` is the target that belongs to no ecosystem, so
7
+ * it is the only one the core can honestly ship. `npm` arrives with the `node` built-in, and a Cargo or
8
+ * Maven target would arrive from its own plugin the same way.
9
+ *
10
+ * A separate file from `core-services.ts` rather than a second call inside it: a target is not a
11
+ * service (it is a contribution summed with others, not one replaceable answer), and a function
12
+ * called `registerCoreServices` that also registered targets would have to be read to be believed.
13
+ */
14
+ export declare function registerCoreTargets(app: RmanApplication): void;
@@ -0,0 +1,16 @@
1
+ import { dockerPublishTarget } from '../targets/docker.target.js';
2
+ /**
3
+ * Every publish target the core brings, registered as an application is built - `registerCoreServices`
4
+ * for the other half of what a fresh application starts with.
5
+ *
6
+ * Exactly one today, and that is the point: `docker` is the target that belongs to no ecosystem, so
7
+ * it is the only one the core can honestly ship. `npm` arrives with the `node` built-in, and a Cargo or
8
+ * Maven target would arrive from its own plugin the same way.
9
+ *
10
+ * A separate file from `core-services.ts` rather than a second call inside it: a target is not a
11
+ * service (it is a contribution summed with others, not one replaceable answer), and a function
12
+ * called `registerCoreServices` that also registered targets would have to be read to be believed.
13
+ */
14
+ export function registerCoreTargets(app) {
15
+ app.publishTargets.add(dockerPublishTarget);
16
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ArgumentsCamelCase, Argv } from 'yargs';
2
+ import type { RmanConfig as CommandDeclaration } from '../interfaces/rman-config.interface.js';
2
3
  import type { Logger } from '../utils/logger.js';
3
4
  import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
4
5
  import type { Package } from './package.js';
@@ -100,10 +101,25 @@ declare module 'yargs' {
100
101
  * that come with it; reimplementing that loop here would only lose them.
101
102
  */
102
103
  export declare function defineCommand(command: CustomCommand): CustomCommand;
103
- export interface LoadedCommand extends CustomCommand {
104
+ /**
105
+ * One command module that loaded, in whichever form it exported.
106
+ *
107
+ * Both forms are accepted, and the same pair is accepted for a command written straight into
108
+ * `.rmanrc "commands"` - one key, one set of rules. The declarative factory is what rman asks a
109
+ * command author to write; a repository's own command should not be stuck on the older object
110
+ * shape just because it lives in a file rather than in a config.
111
+ */
112
+ export interface LoadedCommand {
104
113
  /** The command's name - its file's basename, or the first word of an explicit `command`. */
105
114
  name: string;
106
115
  file: string;
116
+ /** The declarative form (`app => ({ ... })`). `cli.ts` runs it where a plugin's and a built-in's
117
+ * own factories run, because it wants `app.repository` and loading happens before one exists. */
118
+ register?: CommandDeclaration.CommandRegisterFunction;
119
+ /** The `defineCommand({ ... })` object form, already checked and named. */
120
+ custom?: CustomCommand & {
121
+ command: string;
122
+ };
107
123
  }
108
124
  /** A module that couldn't be loaded or doesn't look like a command. Reported, never thrown: one
109
125
  * unparseable file must not take `rman publish` down with it. */
@@ -112,15 +128,32 @@ export interface CommandLoadError {
112
128
  reason: string;
113
129
  }
114
130
  /**
115
- * Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
116
- * no scan, no imports - which matters because this runs on *every* rman invocation, `info`
117
- * included.
131
+ * The globs a repository's own commands are loaded from when it names none: `.rman/*.{js,mjs,cjs}`
132
+ * under the repository root.
133
+ *
134
+ * **`.rman/` is this default, not a second mechanism.** It used to be a hardcoded directory scan
135
+ * beside which `commands` would have been a third source of repository-level commands - and a
136
+ * third precedence question. Making it the default value instead leaves one source, one slot, and
137
+ * a zero-config path that behaves exactly as it did.
138
+ */
139
+ export declare function defaultCommandGlobs(rootDir: string): string[];
140
+ /**
141
+ * Loads every command module matching `patterns` - absolute globs, already anchored to whichever
142
+ * config file declared them (see `anchorContributions`).
143
+ *
144
+ * A repository matching nothing pays for one glob and no imports, which matters because this runs
145
+ * on *every* rman invocation, `info` included.
146
+ *
147
+ * **Deduplicated by resolved path**, because `commands` appends at every level and cascades: the
148
+ * root's glob reaches each package's resolved config too, so the same file is named more than once
149
+ * as a matter of course rather than as a mistake. Loading it twice would register the command
150
+ * twice, which yargs does not survive.
118
151
  *
119
152
  * Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
120
153
  * warns about them. What is *not* tolerated is a module that would shadow a built-in - see
121
154
  * `assertNoBuiltinShadowing`.
122
155
  */
123
- export declare function loadCustomCommands(rootDir: string): Promise<{
156
+ export declare function loadCustomCommands(patterns: string[]): Promise<{
124
157
  commands: LoadedCommand[];
125
158
  errors: CommandLoadError[];
126
159
  }>;
@@ -130,4 +163,7 @@ export declare function loadCustomCommands(rootDir: string): Promise<{
130
163
  * `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
131
164
  * it unable to tell which ran.
132
165
  */
133
- export declare function assertNoBuiltinShadowing(commands: LoadedCommand[], builtins: readonly string[]): void;
166
+ export declare function assertNoBuiltinShadowing(commands: readonly {
167
+ name: string;
168
+ file: string;
169
+ }[], builtins: readonly string[]): void;
@@ -1,6 +1,6 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { pathToFileURL } from 'node:url';
3
+ import fastGlob from 'fast-glob';
4
4
  /** Where a repository keeps its own commands - one module per command, named after it. */
5
5
  export const CUSTOM_COMMAND_DIR = '.rman';
6
6
  /** Loadable module forms, matching what a `.rmanrc.cjs`/`.mjs`/`.js` config already accepts. A
@@ -41,30 +41,58 @@ export function defineCommand(command) {
41
41
  return command;
42
42
  }
43
43
  /**
44
- * Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
45
- * no scan, no imports - which matters because this runs on *every* rman invocation, `info`
46
- * included.
44
+ * The globs a repository's own commands are loaded from when it names none: `.rman/*.{js,mjs,cjs}`
45
+ * under the repository root.
46
+ *
47
+ * **`.rman/` is this default, not a second mechanism.** It used to be a hardcoded directory scan
48
+ * beside which `commands` would have been a third source of repository-level commands - and a
49
+ * third precedence question. Making it the default value instead leaves one source, one slot, and
50
+ * a zero-config path that behaves exactly as it did.
51
+ */
52
+ export function defaultCommandGlobs(rootDir) {
53
+ return [path.join(rootDir, CUSTOM_COMMAND_DIR, `*{${EXTENSIONS.join(',')}}`)];
54
+ }
55
+ /**
56
+ * Loads every command module matching `patterns` - absolute globs, already anchored to whichever
57
+ * config file declared them (see `anchorContributions`).
58
+ *
59
+ * A repository matching nothing pays for one glob and no imports, which matters because this runs
60
+ * on *every* rman invocation, `info` included.
61
+ *
62
+ * **Deduplicated by resolved path**, because `commands` appends at every level and cascades: the
63
+ * root's glob reaches each package's resolved config too, so the same file is named more than once
64
+ * as a matter of course rather than as a mistake. Loading it twice would register the command
65
+ * twice, which yargs does not survive.
47
66
  *
48
67
  * Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
49
68
  * warns about them. What is *not* tolerated is a module that would shadow a built-in - see
50
69
  * `assertNoBuiltinShadowing`.
51
70
  */
52
- export async function loadCustomCommands(rootDir) {
53
- const dir = path.join(rootDir, CUSTOM_COMMAND_DIR);
71
+ export async function loadCustomCommands(patterns) {
54
72
  const commands = [];
55
73
  const errors = [];
56
- if (!fs.existsSync(dir))
74
+ if (!patterns.length)
57
75
  return { commands, errors };
58
- for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
59
- if (!entry.isFile() || !EXTENSIONS.includes(path.extname(entry.name)))
60
- continue;
61
- const file = path.join(dir, entry.name);
76
+ /** Sorted so the order a command is registered in does not depend on the filesystem, and
77
+ * `absolute` because a pattern may name a directory outside the repository entirely - which is
78
+ * exactly what a shared config shipping its own commands does. */
79
+ const files = await fastGlob(patterns.map(p => p.split(path.sep).join('/')), { absolute: true, onlyFiles: true });
80
+ for (const file of [...new Set(files.map(f => path.resolve(f)))].sort()) {
62
81
  try {
63
82
  const mod = await import(pathToFileURL(file).href);
64
- const command = mod?.default ?? mod?.command;
65
- if (!command || typeof command !== 'object') {
66
- throw new Error('no default export - end the module with `export default defineCommand({ ... })`');
83
+ const exported = mod?.default ?? mod?.command;
84
+ const basename = path.basename(file, path.extname(file));
85
+ /** The declarative form. Nothing to check here beyond its shape - the factory has not run,
86
+ * so there is no metadata to validate yet; `cli.ts` checks what it returns. */
87
+ if (typeof exported === 'function') {
88
+ commands.push({ name: basename, file, register: exported });
89
+ continue;
90
+ }
91
+ if (!exported || typeof exported !== 'object') {
92
+ throw new Error('no command exported - end the module with `export default defineCommand({ ... })`, or with ' +
93
+ 'the declarative `export default app => ({ ... })`');
67
94
  }
95
+ const command = exported;
68
96
  if (typeof command.handler !== 'function')
69
97
  throw new Error('"handler" is missing, or is not a function');
70
98
  if (typeof command.describe !== 'string' || !command.describe) {
@@ -72,10 +100,9 @@ export async function loadCustomCommands(rootDir) {
72
100
  }
73
101
  const declared = command.command?.trim();
74
102
  commands.push({
75
- ...command,
76
- command: declared || path.basename(entry.name, path.extname(entry.name)),
77
- name: (declared || path.basename(entry.name, path.extname(entry.name))).split(/\s+/)[0],
103
+ name: (declared || basename).split(/\s+/)[0],
78
104
  file,
105
+ custom: { ...command, command: declared || basename },
79
106
  });
80
107
  }
81
108
  catch (e) {
@@ -19,9 +19,17 @@ export declare const EXTENDS_KEY = "extends";
19
19
  */
20
20
  export declare function resolveExtends(config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
21
21
  /**
22
- * Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
23
- * `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
24
- * that quietly does nothing is worse than one that won't load. Inheritance is a statement about
25
- * the file, not about the packages it happens to name.
22
+ * **Refuses the keys a `"[selector]"` block cannot carry** - each for its own reason, and the
23
+ * reason is the same shape every time: a selector block is typed as a whole `RmanConfig`, so any
24
+ * key looks valid there, and one that can never be read is a config that quietly does nothing.
25
+ * That is worse than one that will not load.
26
+ *
27
+ * - **`extends`** is a statement about the *file*, not about the packages a selector happens to
28
+ * name, and it belongs at the top level where it would actually be resolved.
29
+ * - **`platform` and `name`** are what the selector is derived *from*, so a glob block setting
30
+ * either would need its own answer in order to be matched. See below; `"[/]"` is exempt.
31
+ *
32
+ * Called on every config form a directory can hold and on every `extends` base, so the file in the
33
+ * message is the one the line was written in.
26
34
  */
27
- export declare function assertNoSelectorExtends(config: RmanConfig, file: string): void;
35
+ export declare function assertSelectorBlocks(config: RmanConfig, file: string): void;
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
3
  import * as yaml from 'js-yaml';
5
- import { isSelectorKey } from './config.js';
4
+ import { isSelectorKey, parseSelector } from './config.js';
5
+ import { loadConfigModule } from './load-config-module.js';
6
6
  import { mergeConfig } from './merge-config.js';
7
7
  import { resolveConfigTarget } from './resolve-target.js';
8
8
  /** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
@@ -40,7 +40,7 @@ export async function resolveExtends(config, from, seen = []) {
40
40
  throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
41
41
  }
42
42
  const loaded = await loadConfigFile(file);
43
- assertNoSelectorExtends(loaded, file);
43
+ assertSelectorBlocks(loaded, file);
44
44
  // Recursive: a shared config may itself be built on another.
45
45
  mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]), file);
46
46
  }
@@ -49,12 +49,20 @@ export async function resolveExtends(config, from, seen = []) {
49
49
  return mergeConfig(base, own);
50
50
  }
51
51
  /**
52
- * Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
53
- * `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
54
- * that quietly does nothing is worse than one that won't load. Inheritance is a statement about
55
- * the file, not about the packages it happens to name.
52
+ * **Refuses the keys a `"[selector]"` block cannot carry** - each for its own reason, and the
53
+ * reason is the same shape every time: a selector block is typed as a whole `RmanConfig`, so any
54
+ * key looks valid there, and one that can never be read is a config that quietly does nothing.
55
+ * That is worse than one that will not load.
56
+ *
57
+ * - **`extends`** is a statement about the *file*, not about the packages a selector happens to
58
+ * name, and it belongs at the top level where it would actually be resolved.
59
+ * - **`platform` and `name`** are what the selector is derived *from*, so a glob block setting
60
+ * either would need its own answer in order to be matched. See below; `"[/]"` is exempt.
61
+ *
62
+ * Called on every config form a directory can hold and on every `extends` base, so the file in the
63
+ * message is the one the line was written in.
56
64
  */
57
- export function assertNoSelectorExtends(config, file) {
65
+ export function assertSelectorBlocks(config, file) {
58
66
  for (const [key, value] of Object.entries(config)) {
59
67
  if (!isSelectorKey(key) || !value || typeof value !== 'object')
60
68
  continue;
@@ -62,10 +70,40 @@ export function assertNoSelectorExtends(config, file) {
62
70
  throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
63
71
  `statement about this config rather than about the packages the selector names.`);
64
72
  }
73
+ /**
74
+ * **The two keys a selector cannot carry, because the selector is downstream of them.**
75
+ *
76
+ * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
77
+ * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
78
+ * the packages are still being found, so a glob block setting either would need its own answer
79
+ * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
80
+ * would simply never be read.
81
+ *
82
+ * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
83
+ * the repository root - so a root block needs no selector and is applied during the walk.
84
+ */
85
+ if (parseSelector(key).scope === 'root')
86
+ continue;
87
+ for (const identity of IDENTITY_KEYS) {
88
+ if (identity in value) {
89
+ throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
90
+ `and "${identity}" is what the selector is derived from, so the block could never be ` +
91
+ `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
92
+ `under "[/]" for the root package.`);
93
+ }
94
+ }
65
95
  }
66
96
  }
67
- /** Loads one resolved target. YAML and JSON are read directly; anything else goes through the
68
- * module loader, so a shared config can be a `defineConfig` module with real logic in it. */
97
+ /**
98
+ * Loads one resolved target. YAML and JSON are read directly; anything else goes through
99
+ * `loadConfigModule`, so a shared config can be a `defineConfig` module with real logic in it.
100
+ *
101
+ * **Through that function rather than a bare `await import()`, which is what this used to do.**
102
+ * The two are not equivalent under an ESM loader hook: a `.cjs` base came back as an empty object,
103
+ * and an empty object is a valid config, so it contributed nothing and said nothing. See there for
104
+ * the measurement - the same file loaded correctly when it was a *directory's* own `.rmanrc.cjs`,
105
+ * because that path always used the careful loader.
106
+ */
69
107
  async function loadConfigFile(file) {
70
108
  const ext = path.extname(file);
71
109
  if (ext === '.yml' || ext === '.yaml') {
@@ -74,8 +112,7 @@ async function loadConfigFile(file) {
74
112
  }
75
113
  if (ext === '.json')
76
114
  return asConfig(JSON.parse(fs.readFileSync(file, 'utf-8')), file);
77
- const mod = await import(pathToFileURL(file).href);
78
- return asConfig(mod?.default ?? mod, file);
115
+ return asConfig(await loadConfigModule(file), file);
79
116
  }
80
117
  function asConfig(value, file) {
81
118
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -87,3 +124,6 @@ function asConfig(value, file) {
87
124
  delete config.$schema;
88
125
  return config;
89
126
  }
127
+ /** The keys that decide *what a package is* and *what addresses it*, read from the unmarked
128
+ * cascade before any package exists - see `assertSelectorBlocks`. */
129
+ const IDENTITY_KEYS = ['platform', 'name'];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Loads a config module - a `.rmanrc.cjs`/`.mjs`/`.js`, or an `extends` target that is one.
3
+ *
4
+ * **`require()` first, and that is not an optimization.** A CommonJS module's `module.exports` is
5
+ * more reliably observed this way than through dynamic `import()`'s CJS-interop synthesis, which
6
+ * some ESM loader hooks - ts-node/swc-node-style transpilers registered via `--import` - can
7
+ * short-circuit into an **empty object**. `require()` throws `ERR_REQUIRE_ESM` for a genuinely-ESM
8
+ * file (`.mjs`, or `.js` under `"type": "module"`), and only then does this fall back to
9
+ * `import()`, the one case that actually needs it.
10
+ *
11
+ * Either path can hand back an ES module namespace rather than a plain object - Node's
12
+ * `require(esm)` support does this too, not just `import()` - so `.default` is preferred whenever
13
+ * present.
14
+ *
15
+ * **It lives here because `extends` needs it too, and did not have it.** A directory's own
16
+ * `.rmanrc.cjs` went through this while `extends-config.ts` used a bare `await import()`, so the
17
+ * *same file* loaded correctly as a directory's config and came back empty when another config
18
+ * named it - and an empty object is a valid config, so nothing was reported. Measured under mocha:
19
+ * a base declaring `"[*]": { version: { stamp: ['build'] } }`, reached by
20
+ * `extends: './base.cjs'`, contributed nothing; the identical fixture with `base.json` contributed
21
+ * normally, and the same `.cjs` worked from the CLI, where no loader hook is registered.
22
+ *
23
+ * `createRequire` is based on this module's own URL rather than on the config file. That is right
24
+ * here and *not* right for resolving a bare specifier - see `resolveConfigTarget`, which is based
25
+ * on the config file so a package name resolves through the repository's `node_modules` rather than
26
+ * rman's own. By the time a file reaches this function it is already an absolute path.
27
+ */
28
+ export declare function loadConfigModule(file: string): Promise<any>;
@@ -0,0 +1,42 @@
1
+ import { createRequire } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
3
+ /**
4
+ * Loads a config module - a `.rmanrc.cjs`/`.mjs`/`.js`, or an `extends` target that is one.
5
+ *
6
+ * **`require()` first, and that is not an optimization.** A CommonJS module's `module.exports` is
7
+ * more reliably observed this way than through dynamic `import()`'s CJS-interop synthesis, which
8
+ * some ESM loader hooks - ts-node/swc-node-style transpilers registered via `--import` - can
9
+ * short-circuit into an **empty object**. `require()` throws `ERR_REQUIRE_ESM` for a genuinely-ESM
10
+ * file (`.mjs`, or `.js` under `"type": "module"`), and only then does this fall back to
11
+ * `import()`, the one case that actually needs it.
12
+ *
13
+ * Either path can hand back an ES module namespace rather than a plain object - Node's
14
+ * `require(esm)` support does this too, not just `import()` - so `.default` is preferred whenever
15
+ * present.
16
+ *
17
+ * **It lives here because `extends` needs it too, and did not have it.** A directory's own
18
+ * `.rmanrc.cjs` went through this while `extends-config.ts` used a bare `await import()`, so the
19
+ * *same file* loaded correctly as a directory's config and came back empty when another config
20
+ * named it - and an empty object is a valid config, so nothing was reported. Measured under mocha:
21
+ * a base declaring `"[*]": { version: { stamp: ['build'] } }`, reached by
22
+ * `extends: './base.cjs'`, contributed nothing; the identical fixture with `base.json` contributed
23
+ * normally, and the same `.cjs` worked from the CLI, where no loader hook is registered.
24
+ *
25
+ * `createRequire` is based on this module's own URL rather than on the config file. That is right
26
+ * here and *not* right for resolving a bare specifier - see `resolveConfigTarget`, which is based
27
+ * on the config file so a package name resolves through the repository's `node_modules` rather than
28
+ * rman's own. By the time a file reaches this function it is already an absolute path.
29
+ */
30
+ export async function loadConfigModule(file) {
31
+ let mod;
32
+ try {
33
+ mod = requireConfigModule(file);
34
+ }
35
+ catch (e) {
36
+ if (e?.code !== 'ERR_REQUIRE_ESM')
37
+ throw e;
38
+ mod = await import(pathToFileURL(file).href);
39
+ }
40
+ return mod?.default ?? mod;
41
+ }
42
+ const requireConfigModule = createRequire(import.meta.url);
@@ -1,4 +1,6 @@
1
+ import { RmanApplication } from './application.js';
1
2
  import type { Package } from './package.js';
3
+ import type { Platform } from './plugin.js';
2
4
  import { type VersionScheme } from './version-scheme.js';
3
5
  /**
4
6
  * A package's identity, however its ecosystem happens to record it.
@@ -13,7 +15,7 @@ export interface Manifest {
13
15
  /** Excluded from publishing by the package's own declaration (`package.json#private`). Not the
14
16
  * same as `.rmanrc "publish.skip"`, which is the *repository's* declaration about it. */
15
17
  private?: boolean;
16
- /** The document as the ecosystem wrote it. `rman-node`'s own commands read `package.json` fields
18
+ /** The document as the ecosystem wrote it. The `node` built-in's own commands read `package.json` fields
17
19
  * the core has no opinion about (`scripts`, `publishConfig`, `engines`) off this. */
18
20
  raw: any;
19
21
  }
@@ -23,7 +25,7 @@ export interface Manifest {
23
25
  * **The core has no provider.** "The name and version live in a `package.json`" is true of npm and
24
26
  * of nothing else - a `Cargo.toml`, a `pyproject.toml` and a `go.mod` each say the same thing
25
27
  * differently, and the version is not even in the same *kind* of place in all of them.
26
- * `rman-node` contributes the `package.json` one.
28
+ * The `node` built-in contributes the `package.json` one.
27
29
  *
28
30
  * Paired with `VersionScheme` on purpose: the ecosystem that decides *where* a version is written
29
31
  * is the one that decides *how* it is numbered, so a provider supplies both and a package gets a
@@ -32,7 +34,7 @@ export interface Manifest {
32
34
  export interface ManifestProvider {
33
35
  /**
34
36
  * **The ecosystem this provider speaks for**, surfaced on every package it reads as
35
- * `Package.provider` - `'node'` for `rman-node`. Short and about the technology, not about the
37
+ * `Package.provider` - `'node'` for the built-in of that name. Short and about the technology, not about the
36
38
  * file: `fileName` already says `package.json`, and a name repeating it would tell a caller
37
39
  * nothing it did not have.
38
40
  *
@@ -124,6 +126,23 @@ export interface ManifestProvider {
124
126
  scope?: string;
125
127
  unscopedName: string;
126
128
  };
129
+ /**
130
+ * **What addresses this package** - what a `"[glob]"` block and `--scope`/`--ignore` match
131
+ * against, and what has to be unique within a repository.
132
+ *
133
+ * **The platform's job, because a name is an ecosystem's promise and not rman's.** npm guarantees
134
+ * `package.json#name` exists and identifies the package, so the node built-in has nothing to do
135
+ * here and the default - the manifest's own name - is already its answer. An ecosystem where a
136
+ * package need not be named, or where the name is not unique, returns `undefined` and the
137
+ * repository assigns one with `.rmanrc "name"`.
138
+ *
139
+ * Separate from `Manifest.name`, which is what the package *calls itself*: those coincide for npm
140
+ * and need not anywhere else. A Go module's name is an import path, and `--scope github.com/x/y`
141
+ * is not how anyone would want to address it.
142
+ *
143
+ * Omit it and the manifest's name is used, so this seam existing changes nothing.
144
+ */
145
+ selector?(manifest: Manifest, dir: string): string | undefined;
127
146
  /**
128
147
  * Rewrites this manifest's references to in-repo packages that just got a new version.
129
148
  *
@@ -147,35 +166,37 @@ export interface ManifestProvider {
147
166
  *
148
167
  * A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
149
168
  * is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
150
- * `rman-node` already adds to `SystemInfo`, and anything this seam grows later can arrive the
169
+ * the `node` built-in already adds to `SystemInfo`, and anything this seam grows later can arrive the
151
170
  * same way instead of as another top-level export.
152
171
  */
153
172
  export declare namespace Manifest {
154
- /** Registers a provider. Called by `loadPlugins` for each plugin's `manifest`, in `plugins`
155
- * declaration order - so which one answers is a function of the repository's own config. */
156
- function addProvider(provider: ManifestProvider): void;
157
- /** For tests, which would otherwise leak a provider into every later case in the process. */
158
- function clearProviders(): void;
159
173
  /**
160
- * Reads `dir`'s manifest through the first provider that recognizes it, with the scheme that
161
- * provider brings.
162
- *
163
- * **With no provider registered, or none recognizing the directory**, the fallback is a package
164
- * named after its own directory at version `0.0.0`. That is deliberately the least it can claim:
165
- * the directory name is a fact, and `0.0.0` is the version a thing has when nothing says
166
- * otherwise. The alternative - refusing to construct a package at all - would make `rman info` and
167
- * `rman list` fail in a repository whose `.rmanrc` simply names no plugin yet, which is exactly
168
- * when someone needs to run them.
174
+ * Reads `dir`'s manifest through **the platform that already claimed it**, with the scheme that
175
+ * platform brings.
176
+ *
177
+ * **It takes a platform rather than searching for one**, and that is where the walk changed
178
+ * things. This used to loop over every registered platform, once per `Package` constructed - so a
179
+ * package asked "who am I?" and the answer was whoever recognized it first, re-derived at every
180
+ * construction. The walk decides it once per directory now (`Workspace.walk` ->
181
+ * `app.platformFor`) and hands it to the package, which is also what lets a nested Cargo package
182
+ * sit inside a Node monorepo: the platform is a fact about the directory, established by whoever
183
+ * found it, not re-guessed by whoever reads it.
184
+ *
185
+ * **When that platform reads nothing** - `basePlatform`, or a provider that claimed the directory
186
+ * and then found nothing in it - the fallback is a package named after its own directory at
187
+ * version `0.0.0`. Deliberately the least it can claim: the directory name is a fact, and `0.0.0`
188
+ * is the version a thing has when nothing says otherwise. The alternative - refusing to construct
189
+ * a package at all - would make `rman info` and `rman list` fail in a repository whose `.rmanrc`
190
+ * simply names no plugin yet, which is exactly when someone needs to run them.
169
191
  */
170
- function read(dir: string): {
192
+ function read(platform: Platform, dir: string): {
171
193
  manifest: Manifest;
172
194
  versionScheme: VersionScheme;
173
195
  fileName: string;
174
- provider: string;
175
196
  };
176
197
  /** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
177
198
  * nowhere is worse than one that fails, since the caller has already decided the new version. */
178
- function write(dir: string, manifest: Manifest): void;
199
+ function write(app: RmanApplication, dir: string, manifest: Manifest): void;
179
200
  /**
180
201
  * The packages `pkg` declares a dependency on, via whichever provider recognizes it.
181
202
  *
@@ -186,7 +207,7 @@ export declare namespace Manifest {
186
207
  function dependenciesOf(pkg: Package, candidates: readonly Package[]): Package[];
187
208
  /** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
188
209
  * "no scope, the name is its own unscoped form" when none has an opinion. */
189
- function splitName(dir: string, name: string): {
210
+ function splitName(app: RmanApplication, dir: string, name: string): {
190
211
  scope?: string;
191
212
  unscopedName: string;
192
213
  };
@@ -199,7 +220,7 @@ export declare namespace Manifest {
199
220
  function updateDependencyVersions(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
200
221
  /**
201
222
  * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
202
- * `ManifestProvider.stampVersion`.
223
+ * `Plugin.stampVersion`.
203
224
  *
204
225
  * `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
205
226
  * provider claimed the package, the provider has no opinion about stamping, or it looked and found
@@ -217,6 +238,7 @@ export declare namespace Manifest {
217
238
  * plugin: git tags then answer the boundary question alone.
218
239
  */
219
240
  function publishedVersion(pkg: Package): Promise<string | undefined>;
220
- /** The file names providers look for, for an error message that can say what was expected. */
221
- function fileNames(): string[];
241
+ /** The file names the registered technologies look for, for an error message that can say what
242
+ * was expected. */
243
+ function fileNames(app: RmanApplication): string[];
222
244
  }