rman 2.0.0-beta.3 → 2.0.0-beta.5
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.
- package/README.md +20 -7
- package/builtins/platforms/node/node-config.interface.d.ts +13 -0
- package/builtins/platforms/node/services/publish.service.d.ts +26 -0
- package/builtins/platforms/node/services/publish.service.js +44 -9
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +5 -0
- package/builtins/publish-targets/npm/npm-publish-target.js +12 -0
- package/cli.js +16 -11
- package/commands.d.ts +0 -1
- package/commands.js +0 -1
- package/constants.js +1 -1
- package/core/classes/workspace.d.ts +5 -3
- package/core/classes/workspace.js +58 -10
- package/core/config/config-reader.d.ts +24 -1
- package/core/config/config-reader.js +151 -27
- package/core/interfaces/custom-command.d.ts +8 -1
- package/core/interfaces/custom-command.js +22 -3
- package/core/plugin-loader.d.ts +1 -1
- package/core/plugin-loader.js +9 -5
- package/package.json +2 -2
- package/commands/lint.command.d.ts +0 -176
- package/commands/lint.command.js +0 -34
package/README.md
CHANGED
|
@@ -94,17 +94,30 @@ worked examples of every single command, see **[docs/cli-rman.md](https://github
|
|
|
94
94
|
| [`import <path>`](#rman-import-path) | Imports an external git repository as a new package, with history. |
|
|
95
95
|
|
|
96
96
|
**`ci` and `clean` come from the `node` built-in**, which ships inside this package - each is about
|
|
97
|
-
npm or TypeScript rather than about repositories
|
|
98
|
-
repository
|
|
97
|
+
npm or TypeScript rather than about repositories. It needs no declaration: rman lays its own `node`
|
|
98
|
+
preset under every repository, so a clone with no `.rmanrc` at all already has them, plus the `node`
|
|
99
|
+
technology itself and the npm publish target. It used to take a second package (`rman-node`) and a
|
|
100
|
+
`.rmanrc` before anything worked.
|
|
101
|
+
|
|
102
|
+
A preset is an ordinary rman config rather than a type of its own, so another technology arrives the
|
|
103
|
+
same way and a polyglot repository asks for both:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
extends: ['rman:node', 'rman:cargo']
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`plugins: ['node']` is not a way to name a technology and is refused**, with a message saying so.
|
|
110
|
+
That key takes a plugin instance or a glob naming modules that export one; a name is what `extends`
|
|
111
|
+
resolves. The keys that do exist:
|
|
99
112
|
|
|
100
113
|
```yaml
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
platform: node # which technology claims *this directory* - it does not load anything
|
|
115
|
+
platforms: [...] # contributes technologies: an instance, or a glob naming modules exporting one
|
|
103
116
|
```
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
Whatever a repository declares is asked about a directory **before** the default preset, so
|
|
119
|
+
`extends: 'rman:cargo'` in a root holding both a `Cargo.toml` and a tooling `package.json` resolves
|
|
120
|
+
to what it declared.
|
|
108
121
|
|
|
109
122
|
**`publish` is here, but *where* a package ships is a plugin's to say.** A **publish target** is
|
|
110
123
|
one answer to "is this version on the registry, and how do I push it" - rman ships `docker`
|
|
@@ -48,6 +48,19 @@ export declare namespace RmanNodeConfig {
|
|
|
48
48
|
* Publishing from such a directory means the manifest there is **generated by `publish`**,
|
|
49
49
|
* from the package's own - see `PublishService`. There is nothing to configure about it. */
|
|
50
50
|
directory?: ConfigValue<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Publish into npm's **staging queue** (`npm stage publish`) instead of straight to the
|
|
53
|
+
* registry: the version is held pending until a maintainer approves it with `npm stage approve`,
|
|
54
|
+
* which is the step that carries the 2FA challenge.
|
|
55
|
+
*
|
|
56
|
+
* Per package like every other key here, so a repository can stage the one package that matters
|
|
57
|
+
* and publish the rest directly.
|
|
58
|
+
*
|
|
59
|
+
* Requires npm >= 11.15.0 and Node >= 22.14.0 on whatever runs the publish. rman does not check
|
|
60
|
+
* either: the version that matters is the runner's, not the one resolving the config, and npm's
|
|
61
|
+
* own "Unknown command: stage" is a better message than a guess made somewhere else.
|
|
62
|
+
*/
|
|
63
|
+
staged?: ConfigValue<boolean>;
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
/**
|
|
@@ -32,6 +32,17 @@ export declare namespace PublishService {
|
|
|
32
32
|
* a release pipeline gates on both show where a version is going.
|
|
33
33
|
*/
|
|
34
34
|
tag?: string;
|
|
35
|
+
/**
|
|
36
|
+
* `npm stage publish` instead of `npm publish` - the version goes into npm's staging queue and
|
|
37
|
+
* is not on the registry until a maintainer runs `npm stage approve`, which is where the 2FA
|
|
38
|
+
* challenge moves to. `false` forces a direct publish even where the config asks for staging;
|
|
39
|
+
* `undefined` leaves the decision to each package's `.rmanrc "publish.npm.staged"`.
|
|
40
|
+
*
|
|
41
|
+
* **A plan option, for the same reason `tag` is one**: what a run is about to do is the thing a
|
|
42
|
+
* reader confirms and a pipeline gates on, and "this release will not be live when the workflow
|
|
43
|
+
* goes green" is the most important sentence in the plan when it applies.
|
|
44
|
+
*/
|
|
45
|
+
staged?: boolean;
|
|
35
46
|
}
|
|
36
47
|
interface ApplyOptions extends Options {
|
|
37
48
|
packageManager?: CiService.PackageManager;
|
|
@@ -63,6 +74,21 @@ export declare namespace PublishService {
|
|
|
63
74
|
* says; recomputing at publish time would let the two disagree about where a package is going.
|
|
64
75
|
*/
|
|
65
76
|
distTag?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether this entry goes into npm's staging queue rather than straight to the registry -
|
|
79
|
+
* `--staged`, or the package's own `.rmanrc "publish.npm.staged"`.
|
|
80
|
+
*
|
|
81
|
+
* Decided in the plan and read back by `applyPlan`, exactly as `distTag` is: a plan that said
|
|
82
|
+
* "staged" while the command published directly would be the one disagreement that cannot be
|
|
83
|
+
* undone, since a live version cannot be unpublished after 72 hours.
|
|
84
|
+
*
|
|
85
|
+
* **A staged entry still reads `publish`, not a status of its own**, and that is a limitation
|
|
86
|
+
* rather than a decision: rman's question is "is this version on the registry", and a pending
|
|
87
|
+
* version is not - `npm view` does not report the queue. So a second run proposes the same
|
|
88
|
+
* package again, and whether npm accepts a duplicate stage is npm's answer to give, not rman's
|
|
89
|
+
* to guess. Watch this if a run is ever repeated before an approval.
|
|
90
|
+
*/
|
|
91
|
+
staged?: boolean;
|
|
66
92
|
/** `PublishTarget.Entry`'s own field - what the core's `publish` prints beside the package
|
|
67
93
|
* name. Carries the dist-tag, so where a version is going is visible in the plan and in
|
|
68
94
|
* `--dry-run --json` without the core knowing anything about npm. */
|
|
@@ -72,8 +72,14 @@ function preparePublishManifest(pkg, publishDir, packagesByName) {
|
|
|
72
72
|
* builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
|
|
73
73
|
* consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
|
|
74
74
|
* tarball.
|
|
75
|
-
* - `private` -
|
|
76
|
-
*
|
|
75
|
+
* - `private` - it is a statement about the **source tree**, not about the artifact derived from it,
|
|
76
|
+
* and a manifest being published is by definition not private. rman's own gate normally stops a
|
|
77
|
+
* private package before this runs, so the line looks unreachable; it is not, because `private`
|
|
78
|
+
* is also how a repository keeps a stray `npm publish` out of the package directory while
|
|
79
|
+
* publishing `build/` on purpose. That is a real configuration - this repository ran it until
|
|
80
|
+
* `rman publish` became its own release path and the gate started refusing the thing it ships.
|
|
81
|
+
* A narrower guard (`prepublishOnly`, which never reaches a consumer either) says the same thing
|
|
82
|
+
* without claiming the artifact is unpublishable.
|
|
77
83
|
* - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
|
|
78
84
|
*/
|
|
79
85
|
function derivePublishManifest(pkg, packagesByName) {
|
|
@@ -201,14 +207,20 @@ export var PublishService;
|
|
|
201
207
|
* out. Asking it kept proposing an already-published version until npm answered 403. */
|
|
202
208
|
const published = !!view?.versions.includes(pkg.version);
|
|
203
209
|
const distTag = distTagFor(pkg, options.tag).tag;
|
|
210
|
+
const staged = resolveStaged(pkg, options.staged);
|
|
204
211
|
entries.set(pkg.name, {
|
|
205
212
|
package: pkg,
|
|
206
213
|
version: pkg.version,
|
|
207
214
|
registryVersion,
|
|
208
215
|
distTag,
|
|
216
|
+
staged,
|
|
209
217
|
/** Printed beside the package in the plan, so where a version is going is something the
|
|
210
|
-
* reader confirms rather than something they have to infer from the version string.
|
|
211
|
-
|
|
218
|
+
* reader confirms rather than something they have to infer from the version string.
|
|
219
|
+
* Staging is named first because it changes what the run *does* - a reader who reads
|
|
220
|
+
* only the dist-tag would take a staged entry for one that goes live. */
|
|
221
|
+
detail: [staged ? 'staged for approval' : undefined, distTag ? `-> dist-tag "${distTag}"` : undefined]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(', ') || undefined,
|
|
212
224
|
status: published ? 'up-to-date' : 'publish',
|
|
213
225
|
reason: published
|
|
214
226
|
? `registry already has ${pkg.version}`
|
|
@@ -256,10 +268,14 @@ export var PublishService;
|
|
|
256
268
|
const publishDir = resolvePublishDir(pkg, options.contents);
|
|
257
269
|
const restore = preparePublishManifest(pkg, publishDir, packagesByName);
|
|
258
270
|
try {
|
|
259
|
-
/** The plan's own tag, not
|
|
260
|
-
* the reader confirmed that, and `applyPlan` is here to carry it out.
|
|
261
|
-
* the fallback only for a plan built by something other than `getPlan`. */
|
|
262
|
-
await exec(buildPublishCommand(packageManager, {
|
|
271
|
+
/** The plan's own tag and staging decision, not recomputed ones: `getPlan` decided where
|
|
272
|
+
* this package goes, the reader confirmed that, and `applyPlan` is here to carry it out.
|
|
273
|
+
* `options.*` is the fallback only for a plan built by something other than `getPlan`. */
|
|
274
|
+
await exec(buildPublishCommand(packageManager, {
|
|
275
|
+
...options,
|
|
276
|
+
tag: entry.distTag ?? options.tag,
|
|
277
|
+
staged: entry.staged ?? options.staged,
|
|
278
|
+
}), {
|
|
263
279
|
cwd: publishDir,
|
|
264
280
|
app: pkg.repository.app,
|
|
265
281
|
stdio: 'inherit',
|
|
@@ -355,8 +371,17 @@ function distTagFor(pkg, tag) {
|
|
|
355
371
|
'no --tag npm would put it on "latest" - pass --tag <name>',
|
|
356
372
|
};
|
|
357
373
|
}
|
|
374
|
+
/* **`stage publish`, not a `--staged` flag**, because that is npm's own spelling: `npm stage` is a
|
|
375
|
+
* command with `publish`/`list`/`view`/`download`/`approve`/`reject` under it, and rman only ever
|
|
376
|
+
* drives the first. The rest is a maintainer's, at a terminal with a 2FA prompt - which is the whole
|
|
377
|
+
* point of staging, and the reason rman must not grow an `approve`.
|
|
378
|
+
*
|
|
379
|
+
* The publish flags are passed through unchanged. `--otp` is the one that reads oddly beside it,
|
|
380
|
+
* since staging is what *defers* the 2FA challenge to approval time - it is passed anyway rather
|
|
381
|
+
* than refused, because whether npm accepts it there is npm's to answer and a guess here would be a
|
|
382
|
+
* rule rman invented. */
|
|
358
383
|
function buildPublishCommand(packageManager, options) {
|
|
359
|
-
const args = ['publish'];
|
|
384
|
+
const args = options.staged ? ['stage', 'publish'] : ['publish'];
|
|
360
385
|
if (options.access)
|
|
361
386
|
args.push('--access', options.access);
|
|
362
387
|
if (options.tag)
|
|
@@ -369,3 +394,13 @@ function buildPublishCommand(packageManager, options) {
|
|
|
369
394
|
args.push('--userconfig', options.userconfig);
|
|
370
395
|
return `${packageManager} ${args.join(' ')}`;
|
|
371
396
|
}
|
|
397
|
+
/** Whether this package stages: `--staged`/`--no-staged` when given, else its own
|
|
398
|
+
* `.rmanrc "publish.npm.staged"`. */
|
|
399
|
+
/* Per package rather than per run, like `directory` beside it - a repository may well want its one
|
|
400
|
+
* widely-depended-on package held for approval and the rest published directly, and the cascade
|
|
401
|
+
* already makes "all of them" a single `"[*]"` line. */
|
|
402
|
+
function resolveStaged(pkg, override) {
|
|
403
|
+
if (override !== undefined)
|
|
404
|
+
return override;
|
|
405
|
+
return !!pkg.config?.publish?.npm?.staged;
|
|
406
|
+
}
|
|
@@ -50,6 +50,13 @@ export class NpmPublishTarget {
|
|
|
50
50
|
describe: 'npm publish --otp <otp> - a 2FA one-time password, for registries that require it',
|
|
51
51
|
type: 'string',
|
|
52
52
|
},
|
|
53
|
+
staged: {
|
|
54
|
+
target: 'cli',
|
|
55
|
+
describe: "npm stage publish - hold each version in npm's staging queue instead of publishing it, " +
|
|
56
|
+
'until a maintainer runs "npm stage approve" with 2FA. --no-staged forces a direct publish ' +
|
|
57
|
+
'over .rmanrc "publish.npm.staged"',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
},
|
|
53
60
|
registry: {
|
|
54
61
|
target: 'cli',
|
|
55
62
|
describe: 'Registry to check against and publish to (default: whatever .npmrc already configures)',
|
|
@@ -90,6 +97,11 @@ export class NpmPublishTarget {
|
|
|
90
97
|
* decides the dist-tag and refuses the two cases with nothing to derive, so `--dry-run` and
|
|
91
98
|
* the JSON a pipeline gates on have to see the flag. */
|
|
92
99
|
tag: ctx.args.tag,
|
|
100
|
+
/** Plan-time for the same reason, and a stronger one: whether a run leaves the version live
|
|
101
|
+
* or waiting for an approval is the thing a reader is confirming. Left `undefined` when the
|
|
102
|
+
* flag was not given, so each package's own `publish.npm.staged` still decides - `false`
|
|
103
|
+
* here would mean `--no-staged` and silently overrule the config. */
|
|
104
|
+
staged: ctx.args.staged,
|
|
93
105
|
};
|
|
94
106
|
}
|
|
95
107
|
}
|
package/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { version } from './constants.js';
|
|
|
13
13
|
import { RmanApplication } from './core/application.js';
|
|
14
14
|
import { Repository } from './core/classes/repository.js';
|
|
15
15
|
import { commandName, toYargsCommand } from './core/command-builder.js';
|
|
16
|
-
import { assertNoBuiltinShadowing, defaultCommandGlobs, loadCustomCommands, } from './core/interfaces/custom-command.js';
|
|
16
|
+
import { assertNoBuiltinShadowing, defaultCommandGlobs, describeCommandSource, loadCustomCommands, } from './core/interfaces/custom-command.js';
|
|
17
17
|
import { checkCustomCommand } from './core/plugin-loader.js';
|
|
18
18
|
import { commandRegistry } from './interfaces/rman-config.interface.js';
|
|
19
19
|
import { colorYaml } from './utils/color-yaml.js';
|
|
@@ -141,7 +141,7 @@ export async function runCli(options) {
|
|
|
141
141
|
if (!c.register)
|
|
142
142
|
return { name: c.name, file: c.file, module: toCustomModule(c.custom, repository, app) };
|
|
143
143
|
const declared = c.register(app);
|
|
144
|
-
const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, c.file);
|
|
144
|
+
const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, describeCommandSource(c.file));
|
|
145
145
|
return { name: commandName(meta.command), file: c.file, module: toYargsCommand(meta) };
|
|
146
146
|
});
|
|
147
147
|
/**
|
|
@@ -179,7 +179,8 @@ export async function runCli(options) {
|
|
|
179
179
|
const survivors = new Set(commands);
|
|
180
180
|
for (const lost of localModules.filter(c => !survivors.has(c))) {
|
|
181
181
|
const winner = byName.get(lost.name);
|
|
182
|
-
logger.verbose(`"${lost.name}" from ${lost.file} is overridden by
|
|
182
|
+
logger.verbose(`"${lost.name}" from ${describeCommandSource(lost.file)} is overridden by ` +
|
|
183
|
+
`${describeCommandSource(winner.file)}.`);
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
assertNoBuiltinShadowing(commands, builtInNames(builtIns));
|
|
@@ -265,17 +266,21 @@ function commandEntries(repository) {
|
|
|
265
266
|
if (!entry || seen.has(entry))
|
|
266
267
|
continue;
|
|
267
268
|
seen.add(entry);
|
|
268
|
-
/** No file
|
|
269
|
-
*
|
|
269
|
+
/** **No file, and `file: ''` says so** - `describeCommandSource` turns that into
|
|
270
|
+
* `.rmanrc "commands"` wherever a message needs to name the origin. It used to be the
|
|
271
|
+
* literal string `'"commands"'`, which is not a path: `path.relative` mangled it and its
|
|
272
|
+
* own quotes doubled in every message that added a pair.
|
|
273
|
+
*
|
|
274
|
+
* The metadata therefore has to name itself, since there is no basename to fall back on,
|
|
275
|
+
* and `checkCustomCommand` is what says so when it does not. */
|
|
270
276
|
if (typeof entry === 'function') {
|
|
271
|
-
direct.push({ name: '', file: '
|
|
277
|
+
direct.push({ name: '', file: '', register: entry });
|
|
272
278
|
}
|
|
273
279
|
else {
|
|
274
|
-
/**
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
direct.push({ name: commandName(custom.command), file: '"commands"', custom });
|
|
280
|
+
/** Reached here rather than at registration, so an inline command is checked the same way
|
|
281
|
+
* one loaded from a file is. */
|
|
282
|
+
const custom = checkCustomCommand(entry, describeCommandSource(''));
|
|
283
|
+
direct.push({ name: commandName(custom.command), file: '', custom });
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
}
|
package/commands.d.ts
CHANGED
|
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
|
|
|
20
20
|
import './commands/github-release.command.js';
|
|
21
21
|
import './commands/import.command.js';
|
|
22
22
|
import './commands/info.command.js';
|
|
23
|
-
import './commands/lint.command.js';
|
|
24
23
|
import './commands/list.command.js';
|
|
25
24
|
import './commands/publish.command.js';
|
|
26
25
|
import './commands/run.command.js';
|
package/commands.js
CHANGED
|
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
|
|
|
20
20
|
import './commands/github-release.command.js';
|
|
21
21
|
import './commands/import.command.js';
|
|
22
22
|
import './commands/info.command.js';
|
|
23
|
-
import './commands/lint.command.js';
|
|
24
23
|
import './commands/list.command.js';
|
|
25
24
|
import './commands/publish.command.js';
|
|
26
25
|
import './commands/run.command.js';
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.0.0-beta.
|
|
1
|
+
export const version = '2.0.0-beta.5';
|
|
@@ -95,13 +95,15 @@ export declare class Workspace {
|
|
|
95
95
|
/** Root, then every directory down to `targetDir`, inclusive. A target outside the root is the
|
|
96
96
|
* root alone. */
|
|
97
97
|
protected _dirChain(targetDir: string): string[];
|
|
98
|
-
/**
|
|
98
|
+
/** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
|
|
99
|
+
* is dropped whole, and what is inside it is the caller's business. */
|
|
99
100
|
protected _stripSelectors(config: RmanConfig): RmanConfig;
|
|
100
101
|
/**
|
|
101
|
-
* The
|
|
102
|
+
* The layers in `config` that speak for this package, in the order they are merged.
|
|
102
103
|
*
|
|
103
104
|
* `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
|
|
104
|
-
* root.
|
|
105
|
+
* root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
|
|
106
|
+
* where its own audience and every audience it sits inside all match.
|
|
105
107
|
*/
|
|
106
108
|
protected _matchingSelectors(config: RmanConfig, audience: Workspace.Audience): RmanConfig[];
|
|
107
109
|
/**
|
|
@@ -290,24 +290,68 @@ export class Workspace {
|
|
|
290
290
|
}
|
|
291
291
|
return dirs;
|
|
292
292
|
}
|
|
293
|
-
/**
|
|
293
|
+
/** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
|
|
294
|
+
* is dropped whole, and what is inside it is the caller's business. */
|
|
295
|
+
/* **The symbols travel with it, and leaving them behind was a measured loss.** `mergeConfig` reads
|
|
296
|
+
* a key's origin and its `value` chain off the *source* object under `ORIGINS` / `PREVIOUS_VALUES`,
|
|
297
|
+
* so a copy that carries only string keys arrives with neither. Measured on one repository before
|
|
298
|
+
* this line existed: a bad expression in an unmarked key reported `Invalid expression in "group"`
|
|
299
|
+
* with no file, while the identical expression inside a `"[*]"` block reported
|
|
300
|
+
* `... ("group" (.rmanrc.yml))` - because a selector block was merged as itself and only the
|
|
301
|
+
* unmarked layer came through here. Nesting would have spread that to the blocks too. */
|
|
294
302
|
_stripSelectors(config) {
|
|
295
303
|
const result = {};
|
|
296
304
|
for (const [key, value] of Object.entries(config))
|
|
297
305
|
if (!ConfigReader.isSelectorKey(key))
|
|
298
306
|
result[key] = value;
|
|
307
|
+
for (const symbol of Object.getOwnPropertySymbols(config)) {
|
|
308
|
+
Object.defineProperty(result, symbol, Object.getOwnPropertyDescriptor(config, symbol));
|
|
309
|
+
}
|
|
299
310
|
return result;
|
|
300
311
|
}
|
|
301
312
|
/**
|
|
302
|
-
* The
|
|
313
|
+
* The layers in `config` that speak for this package, in the order they are merged.
|
|
303
314
|
*
|
|
304
315
|
* `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
|
|
305
|
-
* root.
|
|
316
|
+
* root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
|
|
317
|
+
* where its own audience and every audience it sits inside all match.
|
|
306
318
|
*/
|
|
307
319
|
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
308
320
|
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
309
321
|
* setting to a root with no build directory to apply it to. A root block needs no selector, which
|
|
310
|
-
* is what makes `/` structural.
|
|
322
|
+
* is what makes `/` structural.
|
|
323
|
+
*
|
|
324
|
+
* **Nesting is depth-first pre-order, which is the level's own rule applied again rather than a
|
|
325
|
+
* new one**: a block's own keys are its floor, then the blocks written inside it in declaration
|
|
326
|
+
* order, then the next block beside it. So the layer order for
|
|
327
|
+
* `"[platform:node]" { a, "[pkg-*]" { b } }, "[*]" { c }` is `a, b, c` - and the last word is
|
|
328
|
+
* still whatever was written last, *not* whatever is most specific. That is the same cost the
|
|
329
|
+
* dropped specificity ranking already documents (a catch-all written below a narrower block
|
|
330
|
+
* overrides it), and nesting neither adds to it nor asks the tiebreak question again: two blocks
|
|
331
|
+
* that both match are siblings whatever depth they sit at, and declaration order answers them.
|
|
332
|
+
*
|
|
333
|
+
* **A matching block is contributed *stripped*, not whole.** Merging it as it stands put its
|
|
334
|
+
* nested keys into the resolved config as literal `'[pkg-*]'` entries, which is precisely how the
|
|
335
|
+
* old no-op showed up in `rman config` looking as though it had worked.
|
|
336
|
+
*
|
|
337
|
+
* **A platform block speaks for the root too, and only a glob is held off it.** `"[platform:node]"`
|
|
338
|
+
* asks `pkg.platform.name` and the root is a package with a platform, so
|
|
339
|
+
* `"[platform:node]" > "[/]"` is *the root, when the root is a node package* and
|
|
340
|
+
* `"[platform:node]" > "[*]"` is *its packages* - which is the whole of a technology's shared
|
|
341
|
+
* config in one block, `vars` included.
|
|
342
|
+
*
|
|
343
|
+
* A narrower rule was tried first and measured wrong: let a chain reach the root only where it
|
|
344
|
+
* *names* `"[/]"`, keeping a plain `"[platform:node]"` off the root as before. It answers the pair
|
|
345
|
+
* correctly and still breaks the case it was written for - the nested `"[/]"` could not read the
|
|
346
|
+
* `vars` its parent declared (`vars is not defined`), because those are the parent's own keys.
|
|
347
|
+
* Narrowing an audience and hiding the enclosing block's settings from it are different things,
|
|
348
|
+
* and only the first is what nesting means.
|
|
349
|
+
*
|
|
350
|
+
* The cost, stated rather than hidden: a package-shaped setting written **directly** under a
|
|
351
|
+
* platform block now reaches the root as well - the trap that keeps globs off it. It is not the
|
|
352
|
+
* same trap, because neither half of that one applies here: `platform:node` is not a name and
|
|
353
|
+
* cannot match by accident, and it is not a catch-all. And with nesting the spelling for
|
|
354
|
+
* package-shaped settings is `"[platform:node]" > "[*]"`, which says so. */
|
|
311
355
|
_matchingSelectors(config, audience) {
|
|
312
356
|
const matches = [];
|
|
313
357
|
for (const [key, value] of Object.entries(config)) {
|
|
@@ -316,7 +360,9 @@ export class Workspace {
|
|
|
316
360
|
const { scope, test } = ConfigReader.parseSelector(key);
|
|
317
361
|
if (!this._speaksFor(scope, test, audience))
|
|
318
362
|
continue;
|
|
319
|
-
|
|
363
|
+
const block = value;
|
|
364
|
+
matches.push(this._stripSelectors(block));
|
|
365
|
+
matches.push(...this._matchingSelectors(block, audience));
|
|
320
366
|
}
|
|
321
367
|
return matches;
|
|
322
368
|
}
|
|
@@ -328,8 +374,12 @@ export class Workspace {
|
|
|
328
374
|
*/
|
|
329
375
|
/* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
|
|
330
376
|
* a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
331
|
-
* setting to a root with no build directory to apply it to.
|
|
332
|
-
*
|
|
377
|
+
* setting to a root with no build directory to apply it to. Neither trap exists for a platform
|
|
378
|
+
* block - `platform:node` is not a name and cannot match by accident, and it is not a catch-all -
|
|
379
|
+
* so **a platform link answers about the root like any other package**, and what keeps a plain
|
|
380
|
+
* `"[platform:node]"` off the root is `_matchingSelectors`' chain rule rather than a refusal here.
|
|
381
|
+
* Asking `isRoot` first was what made `"[platform:node]" > "[/]"` unanswerable: the pair says *the
|
|
382
|
+
* root, when it is a node package*, and the short-circuit never let the platform question be put.
|
|
333
383
|
*
|
|
334
384
|
* A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
|
|
335
385
|
* platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
|
|
@@ -337,11 +387,9 @@ export class Workspace {
|
|
|
337
387
|
_speaksFor(scope, test, audience) {
|
|
338
388
|
if (scope === 'root')
|
|
339
389
|
return audience.isRoot;
|
|
340
|
-
if (audience.isRoot)
|
|
341
|
-
return false;
|
|
342
390
|
if (scope === 'platform')
|
|
343
391
|
return test(audience.platform);
|
|
344
|
-
return audience.selector !== undefined && test(audience.selector);
|
|
392
|
+
return !audience.isRoot && audience.selector !== undefined && test(audience.selector);
|
|
345
393
|
}
|
|
346
394
|
/**
|
|
347
395
|
* The platform that claims a directory - the one its config names, or the first loaded platform
|
|
@@ -13,7 +13,8 @@ export declare class ConfigReader {
|
|
|
13
13
|
*/
|
|
14
14
|
static isSelectorKey(key: string): boolean;
|
|
15
15
|
/**
|
|
16
|
-
* **Which packages a selector key speaks for.**
|
|
16
|
+
* **Which packages a selector key speaks for.** Three audiences, and what each is matched
|
|
17
|
+
* against:
|
|
17
18
|
*
|
|
18
19
|
* | | matches | `test` is given |
|
|
19
20
|
* | --- | --- | --- |
|
|
@@ -58,7 +59,27 @@ export declare class ConfigReader {
|
|
|
58
59
|
* default export when there is one, the module's own exports object otherwise.
|
|
59
60
|
*/
|
|
60
61
|
protected _loadConfigModule(file: string): Promise<any>;
|
|
62
|
+
/**
|
|
63
|
+
* Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
|
|
64
|
+
* whether the pair could ever match a package.
|
|
65
|
+
*/
|
|
61
66
|
protected _assertSelectorBlocks(config: RmanConfig, file: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* One walk, refusing a selector key under a *setting* and checking each legal one's contents.
|
|
69
|
+
*
|
|
70
|
+
* @param at the key path to `node`.
|
|
71
|
+
* @param enclosing the selector keys `node` sits inside, outermost first.
|
|
72
|
+
*/
|
|
73
|
+
protected _assertSelectorKeys(node: unknown, file: string, at: string[], enclosing: string[]): void;
|
|
74
|
+
/** What one selector block may carry, and - given `outer` - whether it can match anything. */
|
|
75
|
+
protected _assertSelectorBlock(key: string, value: unknown, file: string, outer: string | undefined): void;
|
|
76
|
+
/**
|
|
77
|
+
* Refuses a nested pair that could never match a package together.
|
|
78
|
+
*
|
|
79
|
+
* @param outer the enclosing selector key.
|
|
80
|
+
* @param inner the key written inside it.
|
|
81
|
+
*/
|
|
82
|
+
protected _assertNestable(outer: string, inner: string, file: string): void;
|
|
62
83
|
protected _resolveExtends(context: ConfigReader.Context, config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
|
|
63
84
|
/**
|
|
64
85
|
* Lays `options.presets` underneath this level's own config, exactly as an `extends` naming them
|
|
@@ -152,6 +173,8 @@ export declare namespace ConfigReader {
|
|
|
152
173
|
/** Whether a package's selector is one this key speaks for. Always `true` for a root key, which
|
|
153
174
|
* is matched by *being* the root rather than by name. */
|
|
154
175
|
test(name: string): boolean;
|
|
176
|
+
/** The technologies a `"[platform:...]"` key names, and nothing for the other two scopes. */
|
|
177
|
+
names?: readonly string[];
|
|
155
178
|
}
|
|
156
179
|
interface Options {
|
|
157
180
|
/** Plugins to start with - the application's. Optional like `platforms`, which it was not: a
|
|
@@ -6,6 +6,7 @@ import fastGlob from 'fast-glob';
|
|
|
6
6
|
import * as yaml from 'js-yaml';
|
|
7
7
|
import { presetNames, PRESETS_DIR } from '../../builtins/presets/index.js';
|
|
8
8
|
import { declaredKind, isDeclared, isPlatform } from '../interfaces/plugin.js';
|
|
9
|
+
import { CODE_SUBTREES } from './config-paths.js';
|
|
9
10
|
import { mergeConfig } from './merge-config.js';
|
|
10
11
|
const requireConfigModule = createRequire(import.meta.url);
|
|
11
12
|
const IDENTITY_KEYS = ['platform', 'name'];
|
|
@@ -21,6 +22,8 @@ const PRESET_EXTENSIONS = ['.js', '.ts'];
|
|
|
21
22
|
/** What an error about a default preset names itself as - there is no config key to quote, since
|
|
22
23
|
* these are laid down by the caller rather than written by anybody. */
|
|
23
24
|
const PRESETS_LABEL = 'presets';
|
|
25
|
+
/** Free-form by contract - see `_assertSelectorKeys`. */
|
|
26
|
+
const VARS_KEY = 'vars';
|
|
24
27
|
/** Keys that only an rman **config** has, for telling one from a contribution once it has already
|
|
25
28
|
* been refused - see `_assertLoaded`. `name` is not among them: a plugin has one too. */
|
|
26
29
|
const CONFIG_SHAPED = ['extends', PLUGINS_KEY, PLATFORMS_KEY, 'commands', 'publishTargets'];
|
|
@@ -46,7 +49,8 @@ export class ConfigReader {
|
|
|
46
49
|
return key.length > 2 && key.startsWith('[') && key.endsWith(']');
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
49
|
-
* **Which packages a selector key speaks for.**
|
|
52
|
+
* **Which packages a selector key speaks for.** Three audiences, and what each is matched
|
|
53
|
+
* against:
|
|
50
54
|
*
|
|
51
55
|
* | | matches | `test` is given |
|
|
52
56
|
* | --- | --- | --- |
|
|
@@ -82,7 +86,7 @@ export class ConfigReader {
|
|
|
82
86
|
.split(',')
|
|
83
87
|
.map(n => n.trim())
|
|
84
88
|
.filter(Boolean);
|
|
85
|
-
return { scope: 'platform', test: (name) => !!name && names.includes(name) };
|
|
89
|
+
return { scope: 'platform', test: (name) => !!name && names.includes(name), names };
|
|
86
90
|
}
|
|
87
91
|
const re = globToRegExp(stripWorkspacePrefix(inner));
|
|
88
92
|
return { scope: 'package', test: (name) => re.test(name) };
|
|
@@ -292,38 +296,158 @@ export class ConfigReader {
|
|
|
292
296
|
}
|
|
293
297
|
return mod?.default ?? mod;
|
|
294
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
|
|
301
|
+
* whether the pair could ever match a package.
|
|
302
|
+
*/
|
|
295
303
|
_assertSelectorBlocks(config, file) {
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
this._assertSelectorKeys(config, file, [], []);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* One walk, refusing a selector key under a *setting* and checking each legal one's contents.
|
|
308
|
+
*
|
|
309
|
+
* @param at the key path to `node`.
|
|
310
|
+
* @param enclosing the selector keys `node` sits inside, outermost first.
|
|
311
|
+
*/
|
|
312
|
+
/* **A selector key is legal at the top level of a config and directly inside another selector
|
|
313
|
+
* key, and nowhere else** - which is the whole of the placement rule, and `at.length >
|
|
314
|
+
* enclosing.length` is the whole of the test: the two arrays stay equal for as long as every
|
|
315
|
+
* ancestor is a selector, and `at` runs ahead the moment one is not.
|
|
316
|
+
*
|
|
317
|
+
* ```yaml
|
|
318
|
+
* "[platform:node]":
|
|
319
|
+
* "[pkg-*]": { group: x } # an intersection
|
|
320
|
+
* run:
|
|
321
|
+
* "[pkg-*]": { ... } # refused - a setting is not an audience
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* **The nested form used to be refused outright and that was the wrong call**, on a reason that
|
|
325
|
+
* does not survive being written out: "selectors do not intersect, because specificity ranking was
|
|
326
|
+
* dropped". Ranking answers which of two *siblings* wins, and nesting asks nothing of the sort -
|
|
327
|
+
* a nested block is resolved depth-first in declaration order, which is the rule already in force
|
|
328
|
+
* one level up (`Workspace._matchingSelectors`). What the refusal cost was the only way to say
|
|
329
|
+
* "these packages, but only the ones that are also X" for a whole block: `if:` is per key and
|
|
330
|
+
* reaches neither `vars` nor any key that is not a run step.
|
|
331
|
+
*
|
|
332
|
+
* What the refusal *did* get right was that silence is unacceptable - before it, the inner block
|
|
333
|
+
* reached every package the outer one did, as a literal `'[pkg-*]'` key sitting in the resolved
|
|
334
|
+
* config where `rman config` showed it looking as though it had worked. Both halves are answered
|
|
335
|
+
* now: the shape works, and the shapes that could never work are refused here rather than
|
|
336
|
+
* quietly matching nothing.
|
|
337
|
+
*
|
|
338
|
+
* **`vars` and the contribution keys are skipped**, and both for the same reason: their contents
|
|
339
|
+
* are not config keys. `vars` is free-form by contract and `CODE_SUBTREES` hold plugins, commands
|
|
340
|
+
* and publish targets - arbitrary objects whose key space rman does not own. A bracketed name in
|
|
341
|
+
* either is data. */
|
|
342
|
+
_assertSelectorKeys(node, file, at, enclosing) {
|
|
343
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
344
|
+
return;
|
|
345
|
+
for (const [key, value] of Object.entries(node)) {
|
|
346
|
+
if (ConfigReader.isSelectorKey(key)) {
|
|
347
|
+
if (at.length > enclosing.length) {
|
|
348
|
+
throw new Error(`"${key}" in "${file}" is nested under "${at.join('.')}", which is a setting rather ` +
|
|
349
|
+
`than an audience - a selector is read at the top level of a config and inside ` +
|
|
350
|
+
`another selector, so this one would never be applied. Move it out, or use an ` +
|
|
351
|
+
`expression where the value itself depends on the package.`);
|
|
352
|
+
}
|
|
353
|
+
this._assertSelectorBlock(key, value, file, enclosing.at(-1));
|
|
354
|
+
this._assertSelectorKeys(value, file, [...at, key], [...enclosing, key]);
|
|
298
355
|
continue;
|
|
299
|
-
if (EXTENDS_KEY in value) {
|
|
300
|
-
throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
|
|
301
|
-
`statement about this config rather than about the packages the selector names.`);
|
|
302
356
|
}
|
|
303
|
-
|
|
304
|
-
* **The two keys a selector cannot carry, because the selector is downstream of them.**
|
|
305
|
-
*
|
|
306
|
-
* A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
|
|
307
|
-
* directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
|
|
308
|
-
* the packages are still being found, so a glob block setting either would need its own answer
|
|
309
|
-
* in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
|
|
310
|
-
* would simply never be read.
|
|
311
|
-
*
|
|
312
|
-
* `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
|
|
313
|
-
* the repository root - so a root block needs no selector and is applied during the walk.
|
|
314
|
-
*/
|
|
315
|
-
if (ConfigReader.parseSelector(key).scope === 'root')
|
|
357
|
+
if (key === VARS_KEY || CODE_SUBTREES.includes(key))
|
|
316
358
|
continue;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
359
|
+
this._assertSelectorKeys(value, file, [...at, key], enclosing);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** What one selector block may carry, and - given `outer` - whether it can match anything. */
|
|
363
|
+
_assertSelectorBlock(key, value, file, outer) {
|
|
364
|
+
if (!value || typeof value !== 'object')
|
|
365
|
+
return;
|
|
366
|
+
if (outer !== undefined)
|
|
367
|
+
this._assertNestable(outer, key, file);
|
|
368
|
+
if (EXTENDS_KEY in value) {
|
|
369
|
+
throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
|
|
370
|
+
`statement about this config rather than about the packages the selector names.`);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* **The two keys a selector cannot carry, because the selector is downstream of them.**
|
|
374
|
+
*
|
|
375
|
+
* A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
|
|
376
|
+
* directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
|
|
377
|
+
* the packages are still being found, so a glob block setting either would need its own answer
|
|
378
|
+
* in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
|
|
379
|
+
* would simply never be read.
|
|
380
|
+
*
|
|
381
|
+
* `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
|
|
382
|
+
* the repository root - so a root block needs no selector and is applied during the walk.
|
|
383
|
+
*/
|
|
384
|
+
if (ConfigReader.parseSelector(key).scope === 'root') {
|
|
385
|
+
/** **Except for `platform` in a *nested* `"[/]"`, which is read nowhere.**
|
|
386
|
+
* `Workspace._declaredPlatformName` looks at a level's own `platform` and at a **top-level**
|
|
387
|
+
* `"[/]"`, because which technology claims a directory has to be settled before any block is
|
|
388
|
+
* matched. Inside `"[platform:node]"` it is also circular - the answer decides whether the
|
|
389
|
+
* block that declares it applies at all. */
|
|
390
|
+
if (outer !== undefined && 'platform' in value) {
|
|
391
|
+
throw new Error(`"${key}" in "${file}" is nested inside "${outer}" and cannot set "platform" - which ` +
|
|
392
|
+
`technology claims a directory is settled before any selector is matched, so it is ` +
|
|
393
|
+
`read from the level's own keys or a top-level "[/]" and nowhere else. Write it there.`);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
for (const identity of IDENTITY_KEYS) {
|
|
398
|
+
if (identity in value) {
|
|
399
|
+
throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
|
|
400
|
+
`and "${identity}" is what the selector is derived from, so the block could never be ` +
|
|
401
|
+
`matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
|
|
402
|
+
`under "[/]" for the root package.`);
|
|
324
403
|
}
|
|
325
404
|
}
|
|
326
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Refuses a nested pair that could never match a package together.
|
|
408
|
+
*
|
|
409
|
+
* @param outer the enclosing selector key.
|
|
410
|
+
* @param inner the key written inside it.
|
|
411
|
+
*/
|
|
412
|
+
/* **Nesting is an AND, so a pair naming disjoint sets is a block that runs for nobody** - which is
|
|
413
|
+
* the silence worth refusing. Two pairs are decidable and both are:
|
|
414
|
+
*
|
|
415
|
+
* - **`"[/]"` paired with a glob**, either way round. A glob matches a package's name and the root
|
|
416
|
+
* is nobody's child, so it never matches the root whatever it is written beside; `"[/]"`
|
|
417
|
+
* matches nothing else. The pair is empty in both directions.
|
|
418
|
+
* - **Two platform blocks naming nothing in common.** A package carries one `platform.name`, so
|
|
419
|
+
* `"[platform:node]" > "[platform:cargo]"` is empty; `"[platform:node,cargo]" > "[platform:node]"`
|
|
420
|
+
* narrows and is fine.
|
|
421
|
+
*
|
|
422
|
+
* **`"[/]"` inside a platform block is the pair this deliberately allows**, and getting that wrong
|
|
423
|
+
* once is why the reasoning is written out. It reads as *the root, when the root is a node
|
|
424
|
+
* package*, which is a perfectly well-defined audience - it was unreachable only because
|
|
425
|
+
* `_speaksFor` asked `isRoot` before it asked the platform question. Refusing it took the natural
|
|
426
|
+
* shape of a technology's shared config away: a preset wanting `vars` over a whole node
|
|
427
|
+
* repository, the root's own settings, and the packages' settings had to hoist `vars` to the top
|
|
428
|
+
* level, where they also reach packages of every other technology.
|
|
429
|
+
*
|
|
430
|
+
* **A glob pair is deliberately not checked**, and the asymmetry is the point rather than an
|
|
431
|
+
* omission: whether two globs intersect is a real computation with a wrong answer available in
|
|
432
|
+
* both directions, while a platform set is `includes`. `"[pkg-*]" > "[lib-*]"` therefore loads and
|
|
433
|
+
* matches nothing - the same thing a top-level `"[lib-*]"` does in a repository with no such
|
|
434
|
+
* package, which nothing reports either. */
|
|
435
|
+
_assertNestable(outer, inner, file) {
|
|
436
|
+
const a = ConfigReader.parseSelector(outer);
|
|
437
|
+
const b = ConfigReader.parseSelector(inner);
|
|
438
|
+
const [root, glob] = a.scope === 'root' ? [outer, b.scope === 'package' && inner] : [inner, a.scope === 'package' && outer];
|
|
439
|
+
if ((a.scope === 'root' || b.scope === 'root') && glob) {
|
|
440
|
+
throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", and "${glob}" can never match the ` +
|
|
441
|
+
`same package "${root}" does - a glob matches a package's name and the root is nobody's ` +
|
|
442
|
+
`child, so the pair is empty. A platform block nests with "[/]" ("the root, when it is ` +
|
|
443
|
+
`of that technology"); a glob does not.`);
|
|
444
|
+
}
|
|
445
|
+
if (a.scope === 'platform' && b.scope === 'platform' && !b.names?.some(name => a.test(name))) {
|
|
446
|
+
throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", which names no technology in common ` +
|
|
447
|
+
`with it - a package carries one platform, so this block could never be applied. Nesting ` +
|
|
448
|
+
`narrows: name a technology the outer block already speaks for.`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
327
451
|
async _resolveExtends(context, config, from, seen = []) {
|
|
328
452
|
const declared = config[EXTENDS_KEY];
|
|
329
453
|
if (declared === undefined)
|
|
@@ -157,9 +157,16 @@ export declare function loadCustomCommands(patterns: string[]): Promise<{
|
|
|
157
157
|
commands: LoadedCommand[];
|
|
158
158
|
errors: CommandLoadError[];
|
|
159
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* How to name a command's origin in a message: the file it came from, or the config key that
|
|
162
|
+
* declared it.
|
|
163
|
+
*
|
|
164
|
+
* @param file a `LoadedCommand.file` - `''` for a command written into `.rmanrc "commands"`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function describeCommandSource(file: string): string;
|
|
160
167
|
/**
|
|
161
168
|
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
162
|
-
* this one is thrown: the
|
|
169
|
+
* this one is thrown: the source is fine, the *name* is the mistake, and there is no reading of
|
|
163
170
|
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
164
171
|
* it unable to tell which ran.
|
|
165
172
|
*/
|
|
@@ -111,16 +111,35 @@ export async function loadCustomCommands(patterns) {
|
|
|
111
111
|
}
|
|
112
112
|
return { commands, errors };
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* How to name a command's origin in a message: the file it came from, or the config key that
|
|
116
|
+
* declared it.
|
|
117
|
+
*
|
|
118
|
+
* @param file a `LoadedCommand.file` - `''` for a command written into `.rmanrc "commands"`.
|
|
119
|
+
*/
|
|
120
|
+
/* **A command loaded from a config key has no file, and saying so beats inventing one.** `file` was
|
|
121
|
+
* `'"commands"'` for those - a string that is not a path, carrying its own quotes - and every
|
|
122
|
+
* message that formatted it got something wrong: `path.relative` mangled it, the quotes doubled
|
|
123
|
+
* (`""commands""` in the shadowing error), and `checkCustomCommand` announced it as a plugin name.
|
|
124
|
+
* One empty string and one helper is the whole fix. */
|
|
125
|
+
export function describeCommandSource(file) {
|
|
126
|
+
return file ? `"${path.relative(process.cwd(), file)}"` : '.rmanrc "commands"';
|
|
127
|
+
}
|
|
114
128
|
/**
|
|
115
129
|
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
116
|
-
* this one is thrown: the
|
|
130
|
+
* this one is thrown: the source is fine, the *name* is the mistake, and there is no reading of
|
|
117
131
|
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
118
132
|
* it unable to tell which ran.
|
|
119
133
|
*/
|
|
134
|
+
/* The remedy differs by where the command came from, which is the other half of why the origin has
|
|
135
|
+
* to be honest: "rename the file" is the answer for a `.rman/*.mjs` and no answer at all for an
|
|
136
|
+
* entry in a config, where there is no file to rename. */
|
|
120
137
|
export function assertNoBuiltinShadowing(commands, builtins) {
|
|
121
138
|
const clash = commands.find(c => builtins.includes(c.name));
|
|
122
139
|
if (!clash)
|
|
123
140
|
return;
|
|
124
|
-
|
|
125
|
-
`
|
|
141
|
+
const remedy = clash.file
|
|
142
|
+
? `Rename the file, or give it its own name with \`command: '<name>'\`.`
|
|
143
|
+
: `Give it its own name with \`command: '<name>'\`.`;
|
|
144
|
+
throw new Error(`${describeCommandSource(clash.file)} would shadow rman's built-in "${clash.name}" command.\n ${remedy}`);
|
|
126
145
|
}
|
package/core/plugin-loader.d.ts
CHANGED
package/core/plugin-loader.js
CHANGED
|
@@ -37,17 +37,21 @@ export function registerPlugin(app, entry) {
|
|
|
37
37
|
}
|
|
38
38
|
app.plugins.add(entry);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/* **`source` is where the command came from, already worded for a message** - a file in quotes, or
|
|
41
|
+
* `.rmanrc "commands"` - and `describeCommandSource` is what produces it. It was called
|
|
42
|
+
* `pluginName` and the messages said `Plugin "..."`, which neither caller ever passed: both hand
|
|
43
|
+
* over a command's *origin*, so a repository's own `.rman/deploy.mjs` was announced as a plugin and
|
|
44
|
+
* a config entry came out as `Plugin """commands"""`. */
|
|
45
|
+
export function checkCustomCommand(command, source) {
|
|
41
46
|
const declared = command?.command?.trim();
|
|
42
47
|
if (!declared) {
|
|
43
|
-
throw new Error(
|
|
48
|
+
throw new Error(`${source} has a command with no "command" name - it cannot be registered.`);
|
|
44
49
|
}
|
|
45
50
|
if (typeof command.handler !== 'function') {
|
|
46
|
-
throw new Error(
|
|
51
|
+
throw new Error(`${source} command "${declared}" has no "handler" function.`);
|
|
47
52
|
}
|
|
48
53
|
if (typeof command.describe !== 'string' || !command.describe) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`nothing to list it by.`);
|
|
54
|
+
throw new Error(`${source} command "${declared}" has no "describe" - \`rman --help\` would have nothing to ` + `list it by.`);
|
|
51
55
|
}
|
|
52
56
|
return { ...command, command: declared };
|
|
53
57
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rman",
|
|
3
3
|
"description": "Repository manager",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.5",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"dependencies": {
|
|
@@ -71,4 +71,4 @@
|
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public"
|
|
73
73
|
}
|
|
74
|
-
}
|
|
74
|
+
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { type RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
2
|
-
declare const COMMAND: "lint";
|
|
3
|
-
declare const config: {
|
|
4
|
-
parallel: {
|
|
5
|
-
target: "cli";
|
|
6
|
-
describe: string;
|
|
7
|
-
coerce: (v: unknown) => boolean | number | undefined;
|
|
8
|
-
};
|
|
9
|
-
bail: {
|
|
10
|
-
target: "cli";
|
|
11
|
-
describe: string;
|
|
12
|
-
type: "boolean";
|
|
13
|
-
};
|
|
14
|
-
topo: {
|
|
15
|
-
target: "cli";
|
|
16
|
-
describe: string;
|
|
17
|
-
type: "boolean";
|
|
18
|
-
};
|
|
19
|
-
progress: {
|
|
20
|
-
target: "cli";
|
|
21
|
-
describe: string;
|
|
22
|
-
type: "boolean";
|
|
23
|
-
};
|
|
24
|
-
changed: {
|
|
25
|
-
target: "cli";
|
|
26
|
-
alias: string;
|
|
27
|
-
describe: string;
|
|
28
|
-
type: "boolean";
|
|
29
|
-
};
|
|
30
|
-
changedSince: {
|
|
31
|
-
target: "cli";
|
|
32
|
-
cliName: string;
|
|
33
|
-
describe: string;
|
|
34
|
-
type: "string";
|
|
35
|
-
conflicts: string;
|
|
36
|
-
};
|
|
37
|
-
fromRoot: {
|
|
38
|
-
target: "cli";
|
|
39
|
-
cliName: string;
|
|
40
|
-
alias: string;
|
|
41
|
-
describe: string;
|
|
42
|
-
type: "boolean";
|
|
43
|
-
};
|
|
44
|
-
allowBranch: {
|
|
45
|
-
target: "cli";
|
|
46
|
-
cliName: string;
|
|
47
|
-
describe: string;
|
|
48
|
-
type: "string";
|
|
49
|
-
};
|
|
50
|
-
ignoreBranch: {
|
|
51
|
-
target: "cli";
|
|
52
|
-
cliName: string;
|
|
53
|
-
describe: string;
|
|
54
|
-
type: "string";
|
|
55
|
-
};
|
|
56
|
-
scope: {
|
|
57
|
-
target: "cli";
|
|
58
|
-
describe: string;
|
|
59
|
-
type: "string";
|
|
60
|
-
};
|
|
61
|
-
ignore: {
|
|
62
|
-
target: "cli";
|
|
63
|
-
describe: string;
|
|
64
|
-
type: "string";
|
|
65
|
-
};
|
|
66
|
-
platform: {
|
|
67
|
-
target: "cli";
|
|
68
|
-
describe: string;
|
|
69
|
-
type: "string";
|
|
70
|
-
};
|
|
71
|
-
deps: {
|
|
72
|
-
target: "cli";
|
|
73
|
-
describe: string;
|
|
74
|
-
type: "boolean";
|
|
75
|
-
};
|
|
76
|
-
dependents: {
|
|
77
|
-
target: "cli";
|
|
78
|
-
describe: string;
|
|
79
|
-
type: "boolean";
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
|
|
83
|
-
/**
|
|
84
|
-
* `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
|
|
85
|
-
*/
|
|
86
|
-
declare const lintCommand: (app: import("../index.js").RmanApplication) => {
|
|
87
|
-
command: "lint";
|
|
88
|
-
describe: string;
|
|
89
|
-
/** Read, not owned - see the note above. */
|
|
90
|
-
configKeys: string[];
|
|
91
|
-
config: {
|
|
92
|
-
parallel: {
|
|
93
|
-
target: "cli";
|
|
94
|
-
describe: string;
|
|
95
|
-
coerce: (v: unknown) => boolean | number | undefined;
|
|
96
|
-
};
|
|
97
|
-
bail: {
|
|
98
|
-
target: "cli";
|
|
99
|
-
describe: string;
|
|
100
|
-
type: "boolean";
|
|
101
|
-
};
|
|
102
|
-
topo: {
|
|
103
|
-
target: "cli";
|
|
104
|
-
describe: string;
|
|
105
|
-
type: "boolean";
|
|
106
|
-
};
|
|
107
|
-
progress: {
|
|
108
|
-
target: "cli";
|
|
109
|
-
describe: string;
|
|
110
|
-
type: "boolean";
|
|
111
|
-
};
|
|
112
|
-
changed: {
|
|
113
|
-
target: "cli";
|
|
114
|
-
alias: string;
|
|
115
|
-
describe: string;
|
|
116
|
-
type: "boolean";
|
|
117
|
-
};
|
|
118
|
-
changedSince: {
|
|
119
|
-
target: "cli";
|
|
120
|
-
cliName: string;
|
|
121
|
-
describe: string;
|
|
122
|
-
type: "string";
|
|
123
|
-
conflicts: string;
|
|
124
|
-
};
|
|
125
|
-
fromRoot: {
|
|
126
|
-
target: "cli";
|
|
127
|
-
cliName: string;
|
|
128
|
-
alias: string;
|
|
129
|
-
describe: string;
|
|
130
|
-
type: "boolean";
|
|
131
|
-
};
|
|
132
|
-
allowBranch: {
|
|
133
|
-
target: "cli";
|
|
134
|
-
cliName: string;
|
|
135
|
-
describe: string;
|
|
136
|
-
type: "string";
|
|
137
|
-
};
|
|
138
|
-
ignoreBranch: {
|
|
139
|
-
target: "cli";
|
|
140
|
-
cliName: string;
|
|
141
|
-
describe: string;
|
|
142
|
-
type: "string";
|
|
143
|
-
};
|
|
144
|
-
scope: {
|
|
145
|
-
target: "cli";
|
|
146
|
-
describe: string;
|
|
147
|
-
type: "string";
|
|
148
|
-
};
|
|
149
|
-
ignore: {
|
|
150
|
-
target: "cli";
|
|
151
|
-
describe: string;
|
|
152
|
-
type: "string";
|
|
153
|
-
};
|
|
154
|
-
platform: {
|
|
155
|
-
target: "cli";
|
|
156
|
-
describe: string;
|
|
157
|
-
type: "string";
|
|
158
|
-
};
|
|
159
|
-
deps: {
|
|
160
|
-
target: "cli";
|
|
161
|
-
describe: string;
|
|
162
|
-
type: "boolean";
|
|
163
|
-
};
|
|
164
|
-
dependents: {
|
|
165
|
-
target: "cli";
|
|
166
|
-
describe: string;
|
|
167
|
-
type: "boolean";
|
|
168
|
-
};
|
|
169
|
-
};
|
|
170
|
-
examples: {
|
|
171
|
-
command: string;
|
|
172
|
-
description: string;
|
|
173
|
-
}[];
|
|
174
|
-
handler: (args: Args) => Promise<void>;
|
|
175
|
-
};
|
|
176
|
-
export default lintCommand;
|
package/commands/lint.command.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { registerCommand } from '../interfaces/rman-config.interface.js';
|
|
2
|
-
import { assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
|
|
3
|
-
import { readRunOptions, runOptions } from '../utils/run-options.js';
|
|
4
|
-
const COMMAND = 'lint';
|
|
5
|
-
const config = runOptions;
|
|
6
|
-
/**
|
|
7
|
-
* `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
|
|
8
|
-
*/
|
|
9
|
-
/* **Core, not the `node` preset, and the test is what the command *knows*.** It knows a script
|
|
10
|
-
* name and nothing else: a Cargo repository declaring `lint: 'cargo clippy'` and a Go one
|
|
11
|
-
* declaring `golangci-lint run` are served by exactly this file. That is the line `clean` is on the
|
|
12
|
-
* other side of - everything `clean` knows how to delete is a TypeScript fact - and it is why
|
|
13
|
-
* `build` and `test` are here too.
|
|
14
|
-
*
|
|
15
|
-
* **It owns no config key**, for the reason `build` documents: `lint` is `run lint` under another
|
|
16
|
-
* name, so its settings are `run.lint`, which belongs to `run`. Two commands cannot contribute
|
|
17
|
-
* under one top-level key - interface merging is not a deep merge - and these three never needed
|
|
18
|
-
* to. */
|
|
19
|
-
const lintCommand = registerCommand(app => {
|
|
20
|
-
const repository = app.repository;
|
|
21
|
-
return {
|
|
22
|
-
command: COMMAND,
|
|
23
|
-
describe: 'Alias for "run lint"',
|
|
24
|
-
/** Read, not owned - see the note above. */
|
|
25
|
-
configKeys: ['run.lint'],
|
|
26
|
-
config,
|
|
27
|
-
examples: [{ command: '$0 lint', description: '# Lints packages' }],
|
|
28
|
-
handler: async (args) => {
|
|
29
|
-
await assertAllowedBranch(repository, readBranchGuardOptions(args));
|
|
30
|
-
await app.getService('run').runScript('lint', { ...readRunOptions(args), commandName: 'lint' });
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
export default lintCommand;
|