rman 1.3.0 → 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.
- package/README.md +63 -19
- package/cli.d.ts +5 -0
- package/cli.js +214 -88
- package/commands/build.command.d.ts +177 -3
- package/commands/build.command.js +20 -10
- package/commands/changed.command.d.ts +80 -3
- package/commands/changed.command.js +19 -12
- package/commands/changelog.command.d.ts +192 -3
- package/commands/changelog.command.js +87 -43
- package/commands/config.command.d.ts +44 -3
- package/commands/config.command.js +30 -19
- package/commands/diff.command.d.ts +37 -3
- package/commands/diff.command.js +25 -16
- package/commands/exec.command.d.ts +193 -3
- package/commands/exec.command.js +60 -58
- package/commands/github-release.command.d.ts +154 -3
- package/commands/github-release.command.js +67 -37
- package/commands/import.command.d.ts +36 -3
- package/commands/import.command.js +28 -20
- package/commands/info.command.d.ts +35 -7
- package/commands/info.command.js +36 -30
- package/commands/list.command.d.ts +163 -3
- package/commands/list.command.js +109 -71
- package/commands/publish.command.d.ts +231 -0
- package/commands/publish.command.js +304 -0
- package/commands/run.command.d.ts +186 -6
- package/commands/run.command.js +26 -72
- package/commands/test.command.d.ts +173 -3
- package/commands/test.command.js +16 -10
- package/commands/version.command.d.ts +317 -3
- package/commands/version.command.js +149 -69
- package/commands.d.ts +32 -0
- package/commands.js +28 -0
- package/constants.js +1 -1
- package/core/application.d.ts +116 -0
- package/core/application.js +143 -0
- package/core/command-builder.d.ts +14 -0
- package/core/command-builder.js +78 -0
- package/core/config.d.ts +140 -19
- package/core/config.js +258 -74
- package/core/core-services.d.ts +14 -0
- package/core/core-services.js +30 -0
- package/core/core-targets.d.ts +14 -0
- package/core/core-targets.js +16 -0
- package/core/custom-command.d.ts +42 -6
- package/core/custom-command.js +44 -17
- package/core/extends-config.d.ts +13 -5
- package/core/extends-config.js +52 -12
- package/core/load-config-module.d.ts +28 -0
- package/core/load-config-module.js +42 -0
- package/core/manifest.d.ts +47 -25
- package/core/manifest.js +51 -69
- package/core/merge-config.d.ts +33 -34
- package/core/merge-config.js +137 -92
- package/core/package.d.ts +145 -17
- package/core/package.js +128 -36
- package/core/plugin-loader.d.ts +65 -0
- package/core/plugin-loader.js +234 -0
- package/core/plugin.d.ts +135 -90
- package/core/plugin.js +70 -173
- package/core/publish-target.d.ts +124 -0
- package/core/publish-target.js +30 -0
- package/core/registry.d.ts +30 -0
- package/core/registry.js +47 -0
- package/core/repository.d.ts +72 -9
- package/core/repository.js +293 -44
- package/core/resolve-target.d.ts +1 -1
- package/core/resolve-target.js +1 -1
- package/core/service.d.ts +49 -0
- package/core/service.js +40 -0
- package/core/version-scheme.d.ts +23 -1
- package/core/version-scheme.js +29 -1
- package/core/workspace.d.ts +84 -33
- package/core/workspace.js +63 -22
- package/index.d.ts +111 -14
- package/index.js +85 -9
- package/interfaces/rman-config.interface.d.ts +725 -202
- package/interfaces/rman-config.interface.js +61 -1
- package/package.json +2 -1
- package/plugins/builtins.d.ts +44 -0
- package/plugins/builtins.js +33 -0
- package/plugins/detect.d.ts +78 -0
- package/plugins/detect.js +70 -0
- package/plugins/node/augmentation/rman.augmentation.d.ts +84 -0
- package/plugins/node/augmentation/rman.augmentation.js +1 -0
- package/plugins/node/augmentation/system-info.augmentation.d.ts +26 -0
- package/plugins/node/augmentation/system-info.augmentation.js +79 -0
- package/plugins/node/commands/ci.command.d.ts +131 -0
- package/plugins/node/commands/ci.command.js +59 -0
- package/plugins/node/commands/clean.command.d.ts +183 -0
- package/plugins/node/commands/clean.command.js +73 -0
- package/plugins/node/index.d.ts +29 -0
- package/plugins/node/index.js +40 -0
- package/plugins/node/node-config.interface.d.ts +77 -0
- package/plugins/node/node-config.interface.js +7 -0
- package/plugins/node/node-manifest.provider.d.ts +68 -0
- package/plugins/node/node-manifest.provider.js +125 -0
- package/plugins/node/node.platform.d.ts +53 -0
- package/plugins/node/node.platform.js +134 -0
- package/plugins/node/npm-publish-target.d.ts +73 -0
- package/plugins/node/npm-publish-target.js +96 -0
- package/plugins/node/services/ci.service.d.ts +47 -0
- package/plugins/node/services/ci.service.js +213 -0
- package/plugins/node/services/clean.service.d.ts +53 -0
- package/plugins/node/services/clean.service.js +237 -0
- package/plugins/node/services/publish.service.d.ts +114 -0
- package/plugins/node/services/publish.service.js +371 -0
- package/plugins/node/services/version-plan.service.d.ts +44 -0
- package/plugins/node/services/version-plan.service.js +58 -0
- package/plugins/node/utils/npm-view.d.ts +48 -0
- package/plugins/node/utils/npm-view.js +71 -0
- package/plugins/node/utils/workspace-range.d.ts +26 -0
- package/plugins/node/utils/workspace-range.js +28 -0
- package/services/change-hash.service.d.ts +2 -2
- package/services/change-hash.service.js +2 -2
- package/services/changelog.service.d.ts +62 -51
- package/services/changelog.service.js +14 -11
- package/services/docker-publish.service.d.ts +50 -29
- package/services/docker-publish.service.js +43 -20
- package/services/exec.service.d.ts +23 -12
- package/services/exec.service.js +14 -9
- package/services/github-release.service.d.ts +44 -33
- package/services/github-release.service.js +13 -10
- package/services/import.service.d.ts +25 -14
- package/services/import.service.js +9 -5
- package/services/list.service.d.ts +62 -10
- package/services/list.service.js +62 -15
- package/services/run.service.d.ts +22 -13
- package/services/run.service.js +262 -223
- package/services/version-plan.service.d.ts +27 -4
- package/services/version-plan.service.js +42 -15
- package/services/version.service.d.ts +31 -11
- package/services/version.service.js +29 -13
- package/targets/docker.target.d.ts +53 -0
- package/targets/docker.target.js +40 -0
- package/utils/bin-path.d.ts +6 -7
- package/utils/bin-path.js +7 -18
- package/utils/branch-guard.d.ts +29 -0
- package/utils/branch-guard.js +31 -0
- package/utils/exec.d.ts +10 -0
- package/utils/exec.js +1 -1
- package/utils/logger.d.ts +1 -1
- package/utils/logger.js +1 -1
- package/utils/package-filter.d.ts +127 -7
- package/utils/package-filter.js +197 -16
- package/utils/printable-config.d.ts +1 -1
- package/utils/printable-config.js +1 -1
- package/utils/run-bin.d.ts +10 -0
- package/utils/run-bin.js +1 -1
- package/utils/run-options.d.ts +97 -0
- package/utils/run-options.js +81 -0
- package/utils/version-stamp.d.ts +1 -1
- package/utils/version-stamp.js +1 -1
package/core/config.js
CHANGED
|
@@ -2,13 +2,13 @@ import { DOMParser } from '@xmldom/xmldom';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import ini from 'ini';
|
|
4
4
|
import * as yaml from 'js-yaml';
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import semver from 'semver';
|
|
8
|
-
import { pathToFileURL } from 'url';
|
|
9
7
|
import vm from 'vm';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { DETECTED_BUILTIN } from '../plugins/detect.js';
|
|
9
|
+
import { assertSelectorBlocks, EXTENDS_KEY, resolveExtends } from './extends-config.js';
|
|
10
|
+
import { loadConfigModule } from './load-config-module.js';
|
|
11
|
+
import { mergeConfig, ORIGINS, PREVIOUS_VALUES } from './merge-config.js';
|
|
12
12
|
/**
|
|
13
13
|
* Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
|
|
14
14
|
* autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
|
|
@@ -31,35 +31,18 @@ export function defineConfig(config) {
|
|
|
31
31
|
* or a `.js` under a `"type": "commonjs"` package.json) and native ESM (`.mjs`, or a `.js` under
|
|
32
32
|
* `"type": "module"`) are supported - the reason `readDirConfig`/`resolveConfig` are async at all. */
|
|
33
33
|
const JS_CONFIG_FILES = ['.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
|
|
34
|
-
const requireJsConfig = createRequire(import.meta.url);
|
|
35
|
-
/**
|
|
36
|
-
* Loads `file`'s config object. Tries `require()` first - not just an optimization: a CommonJS
|
|
37
|
-
* module's `module.exports` is more reliably observed this way than through dynamic `import()`'s
|
|
38
|
-
* CJS-interop synthesis, which some ESM loader hooks (e.g. ts-node/swc-node-style transpilers
|
|
39
|
-
* registered via `--import`) can end up short-circuiting into an empty object. `require()` throws
|
|
40
|
-
* `ERR_REQUIRE_ESM` for a genuinely-ESM file (`.mjs`, or `.js` under `"type": "module"`) - only
|
|
41
|
-
* then does this fall back to `import()`, the one case that actually needs it. Either path can
|
|
42
|
-
* hand back an ES module namespace instead of a plain object (Node's `require(esm)` support does
|
|
43
|
-
* this too, not just `import()`), so `.default` is preferred whenever present.
|
|
44
|
-
*/
|
|
45
|
-
async function loadJsConfig(file) {
|
|
46
|
-
let mod;
|
|
47
|
-
try {
|
|
48
|
-
mod = requireJsConfig(file);
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
if (e?.code !== 'ERR_REQUIRE_ESM')
|
|
52
|
-
throw e;
|
|
53
|
-
mod = await import(pathToFileURL(file).href);
|
|
54
|
-
}
|
|
55
|
-
return mod?.default ?? mod;
|
|
56
|
-
}
|
|
57
34
|
/**
|
|
58
35
|
* Reads the rman configuration defined at a single directory level, merging
|
|
59
36
|
* (in increasing precedence): `package.json#rman`, `.rmanrc.yml`, `.rmanrc`,
|
|
60
37
|
* then `.rmanrc.cjs`/`.rmanrc.mjs`/`.rmanrc.js` (whichever exist, in that order).
|
|
38
|
+
*
|
|
39
|
+
* `options.inject` supplies a built-in for a repository that declared no technology - **already
|
|
40
|
+
* decided**, rather than a "please detect" flag. The decision needs the application (a programmatic
|
|
41
|
+
* caller or a spec may have registered a technology without writing it in a config), which this
|
|
42
|
+
* function has no business knowing about; `Repository.create` makes it once and hands the answer
|
|
43
|
+
* to every read that has to agree with it. See `detectBuiltin`.
|
|
61
44
|
*/
|
|
62
|
-
export async function readDirConfig(dirname) {
|
|
45
|
+
export async function readDirConfig(dirname, options) {
|
|
63
46
|
const result = {};
|
|
64
47
|
/** The file an `extends` in this directory resolves relative to. The last form that actually
|
|
65
48
|
* declared one wins, which matters only for the unusual directory holding several. */
|
|
@@ -68,7 +51,7 @@ export async function readDirConfig(dirname) {
|
|
|
68
51
|
if (fs.existsSync(pkgJsonFile)) {
|
|
69
52
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
|
|
70
53
|
if (pkgJson && typeof pkgJson.rman === 'object') {
|
|
71
|
-
|
|
54
|
+
assertSelectorBlocks(pkgJson.rman, pkgJsonFile);
|
|
72
55
|
if (EXTENDS_KEY in pkgJson.rman)
|
|
73
56
|
extendsFrom = pkgJsonFile;
|
|
74
57
|
mergeConfig(result, pkgJson.rman, pkgJsonFile);
|
|
@@ -78,7 +61,7 @@ export async function readDirConfig(dirname) {
|
|
|
78
61
|
if (fs.existsSync(ymlFile)) {
|
|
79
62
|
const obj = yaml.load(fs.readFileSync(ymlFile, 'utf-8'));
|
|
80
63
|
if (obj && typeof obj === 'object') {
|
|
81
|
-
|
|
64
|
+
assertSelectorBlocks(obj, ymlFile);
|
|
82
65
|
if (EXTENDS_KEY in obj)
|
|
83
66
|
extendsFrom = ymlFile;
|
|
84
67
|
mergeConfig(result, obj, ymlFile);
|
|
@@ -88,7 +71,7 @@ export async function readDirConfig(dirname) {
|
|
|
88
71
|
if (fs.existsSync(rcFile)) {
|
|
89
72
|
const obj = JSON.parse(fs.readFileSync(rcFile, 'utf-8'));
|
|
90
73
|
if (obj && typeof obj === 'object') {
|
|
91
|
-
|
|
74
|
+
assertSelectorBlocks(obj, rcFile);
|
|
92
75
|
if (EXTENDS_KEY in obj)
|
|
93
76
|
extendsFrom = rcFile;
|
|
94
77
|
mergeConfig(result, obj, rcFile);
|
|
@@ -97,9 +80,9 @@ export async function readDirConfig(dirname) {
|
|
|
97
80
|
for (const jsFileName of JS_CONFIG_FILES) {
|
|
98
81
|
const jsFile = path.join(dirname, jsFileName);
|
|
99
82
|
if (fs.existsSync(jsFile)) {
|
|
100
|
-
const obj = await
|
|
83
|
+
const obj = await loadConfigModule(jsFile);
|
|
101
84
|
if (obj && typeof obj === 'object') {
|
|
102
|
-
|
|
85
|
+
assertSelectorBlocks(obj, jsFile);
|
|
103
86
|
if (EXTENDS_KEY in obj)
|
|
104
87
|
extendsFrom = jsFile;
|
|
105
88
|
mergeConfig(result, obj, jsFile);
|
|
@@ -110,7 +93,107 @@ export async function readDirConfig(dirname) {
|
|
|
110
93
|
* one of them sits on, and the directory chain then layers on top as it always did. Each form
|
|
111
94
|
* was checked for a misplaced `extends` as it was read, so that error can name the file holding
|
|
112
95
|
* it rather than whichever form happened to declare the real one. */
|
|
113
|
-
|
|
96
|
+
const resolved = await resolveExtends(result, extendsFrom);
|
|
97
|
+
/**
|
|
98
|
+
* **After `extends`, because a base may be what declares the technology** - a shared config
|
|
99
|
+
* naming `plugins` is a statement, and detecting on top of it would be guessing over an answer.
|
|
100
|
+
* `plugins` being *present* is what counts, so `plugins: []` is a repository saying "none".
|
|
101
|
+
*/
|
|
102
|
+
const detected = options?.inject && resolved.plugins === undefined ? options.inject : undefined;
|
|
103
|
+
if (detected)
|
|
104
|
+
resolved.plugins = [detected.name];
|
|
105
|
+
const expanded = await expandBuiltinPlugins(resolved);
|
|
106
|
+
/**
|
|
107
|
+
* **Marked after the expansion, not before, because the expansion rebuilds the object.**
|
|
108
|
+
* `expandBuiltinPlugins` merges the built-in's config underneath and returns a *new* config, so a
|
|
109
|
+
* symbol set on the way in is simply gone on the way out - measured: detection worked and the
|
|
110
|
+
* "detected" line never printed. The same trap `PREVIOUS_VALUES` and `ORIGINS` document from the
|
|
111
|
+
* other side, where `mergeConfig` has to copy them across by hand.
|
|
112
|
+
*/
|
|
113
|
+
if (detected) {
|
|
114
|
+
Object.defineProperty(expanded, DETECTED_BUILTIN, { value: detected, enumerable: false, configurable: true });
|
|
115
|
+
}
|
|
116
|
+
return expanded;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Turns a built-in **name** in `plugins` into what that built-in contributes - `['node']` into the
|
|
120
|
+
* node plugin, its two commands and its publish target.
|
|
121
|
+
*
|
|
122
|
+
* **Here, beside `extends`, because it is the same operation**: something named brings a config,
|
|
123
|
+
* and that config sits *underneath* the one naming it. Doing it anywhere later would not reach far
|
|
124
|
+
* enough - `commands` is read off the resolved root package by `cli.ts`, not off the raw config
|
|
125
|
+
* `Repository.create` hands to `loadPlugins`, so a built-in expanded only there would register its
|
|
126
|
+
* technology and silently lose its commands.
|
|
127
|
+
*
|
|
128
|
+
* **The name is consumed.** `plugins` always appends, so leaving the string beside the instance it
|
|
129
|
+
* expanded into would hand `loadPlugins` a glob that matches no file - the built-in would load and
|
|
130
|
+
* then the run would fail saying it did not.
|
|
131
|
+
*
|
|
132
|
+
* Runs per directory, like `extends`, but only the root's `plugins` is ever read (`loadPlugins`
|
|
133
|
+
* needs the technologies before any package exists). The cost of walking a key that is almost
|
|
134
|
+
* always absent is one `Array.isArray`.
|
|
135
|
+
*/
|
|
136
|
+
async function expandBuiltinPlugins(config) {
|
|
137
|
+
const declared = config.plugins;
|
|
138
|
+
const own = Array.isArray(declared) ? declared : declared === undefined ? [] : [declared];
|
|
139
|
+
const platform = declaredPlatform(config);
|
|
140
|
+
if (!own.some(e => typeof e === 'string') && platform === undefined)
|
|
141
|
+
return config;
|
|
142
|
+
/**
|
|
143
|
+
* **Imported here rather than at the top, and that is a cycle rather than a style.** A built-in
|
|
144
|
+
* pulls in its commands and services, which read config - so a static import would have
|
|
145
|
+
* `config.ts` and the plugin subtree initialising each other, which in ESM half-works and fails
|
|
146
|
+
* silently. Node caches the module, so the cost is one resolution on a config that names one.
|
|
147
|
+
*/
|
|
148
|
+
const { BUILTIN_PLUGINS, isBuiltinPlugin } = await import('../plugins/builtins.js');
|
|
149
|
+
/**
|
|
150
|
+
* **A `platform` naming a built-in puts it at the front of `plugins`.**
|
|
151
|
+
*
|
|
152
|
+
* Saying which technology this repository is *is* saying it has it, so making the author write
|
|
153
|
+
* both was a distinction only rman could see. Measured on the repository this was noticed in:
|
|
154
|
+
* `platform: 'node'` alone gave a working `rman list` with a `node` column and
|
|
155
|
+
* `Unknown arguments: clean`, because the technology had loaded and its commands had not.
|
|
156
|
+
*
|
|
157
|
+
* **At the front, not the back**, and that is what makes it a statement rather than an addition:
|
|
158
|
+
* `platformFor` takes the first registered platform that recognizes a directory, so the one this
|
|
159
|
+
* repository says it *is* should win over anything a shared config brought along.
|
|
160
|
+
*
|
|
161
|
+
* **Only a built-in**, checked after the import above for exactly this reason: a `platform` naming
|
|
162
|
+
* a third-party technology is answered by the `plugins` entry that loads it, and pushing the bare
|
|
163
|
+
* name in here would hand `loadPlugins` a glob matching no file - the failure would read as the
|
|
164
|
+
* plugin being missing when it is registered perfectly well.
|
|
165
|
+
*
|
|
166
|
+
* Already named, and nothing happens: `plugins` de-duplicates, and this keeps the author's own
|
|
167
|
+
* ordering rather than promoting an entry they placed deliberately.
|
|
168
|
+
*/
|
|
169
|
+
const entries = platform !== undefined && isBuiltinPlugin(platform) && !own.includes(platform) ? [platform, ...own] : own;
|
|
170
|
+
const named = entries.filter((e) => typeof e === 'string' && isBuiltinPlugin(e));
|
|
171
|
+
if (!named.length)
|
|
172
|
+
return config;
|
|
173
|
+
const base = {};
|
|
174
|
+
/** De-duplicated first: two layers naming the same built-in is ordinary (a shared config and the
|
|
175
|
+
* repository that inherits it), and registering a plugin twice defines its commands twice. */
|
|
176
|
+
for (const name of [...new Set(named)])
|
|
177
|
+
mergeConfig(base, BUILTIN_PLUGINS[name].contribute());
|
|
178
|
+
const result = { ...config };
|
|
179
|
+
result.plugins = entries.filter(e => !(typeof e === 'string' && isBuiltinPlugin(e)));
|
|
180
|
+
return mergeConfig(base, result);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The **root's** declared platform, however it was spelled - unmarked, or inside a `"[/]"` block.
|
|
184
|
+
*
|
|
185
|
+
* Both, because both are the root saying what it is and a reader would not expect one to bring the
|
|
186
|
+
* built-in and the other not. `"[/]"` is the precise spelling (it speaks for the root package
|
|
187
|
+
* alone, where an unmarked key also cascades to every package below), so leaving it out would have
|
|
188
|
+
* punished the more careful author.
|
|
189
|
+
*
|
|
190
|
+
* A glob block is not consulted and cannot be: `assertSelectorBlocks` refuses `platform` there,
|
|
191
|
+
* since the glob matches a selector the key is upstream of.
|
|
192
|
+
*/
|
|
193
|
+
function declaredPlatform(config) {
|
|
194
|
+
const root = config[`[${ROOT_SELECTOR_INNER}]`];
|
|
195
|
+
const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
|
|
196
|
+
return typeof declared === 'string' && declared.trim() ? declared.trim() : undefined;
|
|
114
197
|
}
|
|
115
198
|
/**
|
|
116
199
|
* Resolves the effective config for the package at `targetDir`, cascading from `rootDir` down to
|
|
@@ -139,10 +222,27 @@ export async function readDirConfig(dirname) {
|
|
|
139
222
|
* repo-wide bookend therefore belongs under `"[/]"`, where its audience is visible; that is the
|
|
140
223
|
* migration this change asks for, and the only one that is not mechanical.
|
|
141
224
|
*
|
|
142
|
-
* `
|
|
143
|
-
*
|
|
225
|
+
* **`selector` is what a `"[glob]"` block matches** - `Package.selector`, which is the package's
|
|
226
|
+
* `.rmanrc "name"` if it declares one and its platform's answer otherwise. Without it, glob blocks
|
|
227
|
+
* contribute nothing: the walk resolves config for a directory *before* the package exists, since
|
|
228
|
+
* that is where `platform` and `name` are read from, and a glob has nothing to match against yet.
|
|
229
|
+
*
|
|
230
|
+
* **`"[/]"` needs no selector, and that is the documented rule rather than an exception.** The root
|
|
231
|
+
* is addressed structurally - its directory *is* the repository root - which is the whole reason it
|
|
232
|
+
* is `/` and not a name. So a root block applies whenever the target is the root, named or not, and
|
|
233
|
+
* `platform` under `"[/]"` therefore works during the walk. It did not until this was noticed: the
|
|
234
|
+
* gate was `if (packageName)`, so the walk skipped every selector block including that one, and the
|
|
235
|
+
* key documented as "keeps it on the root package alone" silently did nothing.
|
|
236
|
+
*
|
|
237
|
+
* It was called `packageName`, which was wrong twice over: a package is not guaranteed to have a
|
|
238
|
+
* name (that is an ecosystem's promise, not rman's), and what this matches is the selector, which a
|
|
239
|
+
* repository can assign itself.
|
|
144
240
|
*/
|
|
145
|
-
export async function resolveConfig(rootDir, targetDir, cache = new Map(),
|
|
241
|
+
export async function resolveConfig(rootDir, targetDir, cache = new Map(), selector,
|
|
242
|
+
/** The built-in `Repository.create` decided on, for a repository that declared no technology.
|
|
243
|
+
* Applied at the **root level only** - `plugins` is read nowhere else, and this is the read whose
|
|
244
|
+
* result becomes `pkg.config`, which is where `cli.ts` finds a built-in's `commands`. */
|
|
245
|
+
inject) {
|
|
146
246
|
const result = {};
|
|
147
247
|
const target = path.resolve(targetDir);
|
|
148
248
|
/** The root *package* is the one whose directory is the repository root - no other test is
|
|
@@ -152,7 +252,9 @@ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packa
|
|
|
152
252
|
for (const dir of dirChain(rootDir, targetDir)) {
|
|
153
253
|
let local = cache.get(dir);
|
|
154
254
|
if (!local) {
|
|
155
|
-
local = await readDirConfig(dir
|
|
255
|
+
local = await readDirConfig(dir, {
|
|
256
|
+
inject: path.resolve(dir) === path.resolve(rootDir) ? inject : undefined,
|
|
257
|
+
});
|
|
156
258
|
cache.set(dir, local);
|
|
157
259
|
}
|
|
158
260
|
/**
|
|
@@ -167,16 +269,14 @@ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packa
|
|
|
167
269
|
* layer that feeds the directories below it.
|
|
168
270
|
*/
|
|
169
271
|
mergeConfig(result, stripSelectors(local));
|
|
170
|
-
/** Then the selector blocks, **in the order they were written** - see `matchingSelectors`.
|
|
171
|
-
|
|
172
|
-
|
|
272
|
+
/** Then the selector blocks, **in the order they were written** - see `matchingSelectors`.
|
|
273
|
+
* Reached for the root even with no selector, since `"[/]"` is structural. */
|
|
274
|
+
if (selector !== undefined || isRoot) {
|
|
275
|
+
for (const block of matchingSelectors(local, selector, isRoot))
|
|
173
276
|
mergeConfig(result, block);
|
|
174
277
|
}
|
|
175
278
|
}
|
|
176
|
-
|
|
177
|
-
* and becomes the value itself. Done here rather than per layer: until the chain is finished,
|
|
178
|
-
* the key it appends to may still be coming. */
|
|
179
|
-
return finalizeConfig(result);
|
|
279
|
+
return result;
|
|
180
280
|
}
|
|
181
281
|
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[pkg-a]"`. The
|
|
182
282
|
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
@@ -236,13 +336,15 @@ export function selectorToRegExp(key) {
|
|
|
236
336
|
* The cost, which the docs state rather than hide: a catch-all written *below* a narrower block now
|
|
237
337
|
* overrides it. Writing catch-alls first is a convention, not a rule - the file reads top to bottom.
|
|
238
338
|
*/
|
|
239
|
-
function matchingSelectors(config,
|
|
339
|
+
function matchingSelectors(config, selector, isRoot) {
|
|
240
340
|
const matches = [];
|
|
241
341
|
for (const [key, value] of Object.entries(config)) {
|
|
242
342
|
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
243
343
|
continue;
|
|
244
344
|
const { scope, test } = parseSelector(key);
|
|
245
|
-
|
|
345
|
+
/** A root block asks only whether this *is* the root - no selector needed, which is what makes
|
|
346
|
+
* `/` structural. A glob has to have something to match, and during the walk it does not. */
|
|
347
|
+
if (scope === 'root' ? !isRoot : isRoot || selector === undefined || !test(selector))
|
|
246
348
|
continue;
|
|
247
349
|
matches.push(value);
|
|
248
350
|
}
|
|
@@ -287,6 +389,11 @@ function dirChain(rootDir, targetDir) {
|
|
|
287
389
|
}
|
|
288
390
|
return dirs;
|
|
289
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* **Returns `Resolved<T>`, not `T`, because resolving is what it does.** Calling every value
|
|
394
|
+
* function is half this function's job, so the type it hands back is the one where they are gone -
|
|
395
|
+
* which is what makes `pkg.config` a `ResolvedConfig` without a cast anywhere between.
|
|
396
|
+
*/
|
|
290
397
|
export function interpolateConfig(config, scope, options) {
|
|
291
398
|
const skip = options?.skip ?? [];
|
|
292
399
|
/**
|
|
@@ -313,8 +420,8 @@ export function interpolateConfig(config, scope, options) {
|
|
|
313
420
|
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
314
421
|
return walk(config, scope, context, base, skip);
|
|
315
422
|
/**
|
|
316
|
-
* The config's own top-level keys, readable bare: `${{
|
|
317
|
-
* restates another - `after: "cp README.md ${{
|
|
423
|
+
* The config's own top-level keys, readable bare: `${{ changelog.filePath }}`. So a value that
|
|
424
|
+
* restates another - `after: "cp README.md ${{ changelog.filePath }}/"` - stops being a second
|
|
318
425
|
* copy that drifts when the first one changes.
|
|
319
426
|
*
|
|
320
427
|
* Resolved **on demand**, one key at a time, and memoized. Interpolating the config in tree order
|
|
@@ -374,6 +481,13 @@ export function interpolateConfig(config, scope, options) {
|
|
|
374
481
|
const result = {};
|
|
375
482
|
for (const key of Object.keys(config))
|
|
376
483
|
result[key] = resolve(key);
|
|
484
|
+
/**
|
|
485
|
+
* **The one cast in the whole two-view split, and it is here rather than at every read.** No type
|
|
486
|
+
* can prove that a runtime walk turned `T` into `Resolved<T>`; this walk is what makes it true.
|
|
487
|
+
* Putting it at this single return is what keeps `pkg.config` honest without a cast in any of the
|
|
488
|
+
* commands - which is the arrangement the alternative (widening `RmanConfig` alone) gave up, six
|
|
489
|
+
* read sites at a time.
|
|
490
|
+
*/
|
|
377
491
|
return result;
|
|
378
492
|
}
|
|
379
493
|
/**
|
|
@@ -401,7 +515,7 @@ export const DEFERRED_PATHS = ['version.before', 'version.exec', 'version.after'
|
|
|
401
515
|
* ```
|
|
402
516
|
*
|
|
403
517
|
* **The key decides, and it already did.** `run.build.exec: 'tsc -b'` is a shell command and
|
|
404
|
-
* `publish.directory: 'build'` is a path - not because of anything about the strings, but because of
|
|
518
|
+
* `publish.npm.directory: 'build'` is a path - not because of anything about the strings, but because of
|
|
405
519
|
* where they sit. A function inherits the same rule, so nothing new has to be learned and no marker
|
|
406
520
|
* has to be remembered. The alternative was inspecting the function (arity, parameter names), which
|
|
407
521
|
* is the kind of guess `loadPlugins` refuses to make about a module's export for the same reason:
|
|
@@ -429,16 +543,23 @@ export const STEP_PATHS = [
|
|
|
429
543
|
];
|
|
430
544
|
/**
|
|
431
545
|
* Keys whose **whole subtree** is code rather than config, so no function under them is a value to
|
|
432
|
-
* compute.
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
546
|
+
* compute.
|
|
547
|
+
*
|
|
548
|
+
* The three contribution keys, and each has to be here: an entry may be the *instance* itself, and
|
|
549
|
+
* a `Plugin` is almost entirely functions - `manifestProvider.read`, `getWorkspace`,
|
|
550
|
+
* `getBinPaths`, `versionPlanner` - while a command is often a bare factory and a publish target
|
|
551
|
+
* carries `getPlan`/`applyPlan`.
|
|
552
|
+
*
|
|
553
|
+
* Measured twice, once per shape. With `plugins` walked like any other key, resolving the config
|
|
554
|
+
* of a repository that named a plugin called that plugin's yargs builder with the config scope:
|
|
555
|
+
* `Config function in "plugins[0].commands[0].builder" failed: cmd.option is not a function`. And
|
|
556
|
+
* with `commands` left out of this list, a declarative command - which *is* a function - was
|
|
557
|
+
* invoked with the interpolation scope instead of the application, so its handler closed over a
|
|
558
|
+
* repository that was not one: `repository.getPackages is not a function`, from inside `clean`.
|
|
559
|
+
*
|
|
560
|
+
* These entries are loaded by `loadPlugins` and `cli.ts`, never read as settings.
|
|
440
561
|
*/
|
|
441
|
-
export const CODE_SUBTREES = ['plugins'];
|
|
562
|
+
export const CODE_SUBTREES = ['plugins', 'commands', 'publishTargets'];
|
|
442
563
|
const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
|
|
443
564
|
/** A config key an expression could actually name. Anything else - a `"[selector]"` block, a
|
|
444
565
|
* `"lint:fix"` - is unreachable as a bare identifier anyway, so it is not bound. */
|
|
@@ -656,13 +777,14 @@ function shortenOrigin(file) {
|
|
|
656
777
|
*
|
|
657
778
|
* **Always bound, even with nothing underneath.** Left unbound, an expression naming it fails with
|
|
658
779
|
* V8's `value is not defined`, which reads as "there is no such thing" rather than "nothing below
|
|
659
|
-
* this layer set it" - two different mistakes needing two different fixes.
|
|
780
|
+
* this layer set it" - two different mistakes needing two different fixes. With nothing underneath
|
|
781
|
+
* it is `unsetValue()` rather than `undefined` - see there.
|
|
660
782
|
*
|
|
661
783
|
* The chain resolves bottom-up, so a layer deriving from a layer that itself derived from something
|
|
662
784
|
* is handed the finished value rather than a half-resolved expression.
|
|
663
785
|
*/
|
|
664
786
|
function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
665
|
-
const resolved = previous === undefined ? undefined : walkWithPrevious(previous.value, previous.previous, scope, context, at, skip);
|
|
787
|
+
const resolved = previousValue(previous === undefined ? undefined : walkWithPrevious(previous.value, previous.previous, scope, context, at, skip), at);
|
|
666
788
|
const outer = Object.getOwnPropertyDescriptor(context, VALUE_KEY);
|
|
667
789
|
/** A getter, so the catch below can tell whether the value **actually read `value`**: the hint is
|
|
668
790
|
* irrelevant to any other failure, and attaching it anyway is the send-the-reader-to-the-wrong-
|
|
@@ -681,22 +803,18 @@ function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
|
681
803
|
}
|
|
682
804
|
catch (e) {
|
|
683
805
|
/**
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
* Here rather than in `callValueFn`, so the expression and the function spelling get the same
|
|
689
|
-
* sentence from the same place. `rmanValueHint` keeps a rethrow from stacking it twice as the
|
|
690
|
-
* error passes back up through the enclosing keys.
|
|
806
|
+
* **The hint is only about *reading* `value`**, so it is attached only when the value did -
|
|
807
|
+
* recorded through the getter above, never matched on V8's wording. Attaching it to any other
|
|
808
|
+
* failure is the send-the-reader-to-the-wrong-place mistake it exists to prevent.
|
|
691
809
|
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
810
|
+
* The sentence itself comes from `unsetValue`, which knows the key and throws at the exact
|
|
811
|
+
* point of misuse; all this adds is the case the sentinel cannot catch, where a value reads
|
|
812
|
+
* `value` and fails for a reason of its own. `rmanValueHint` keeps a rethrow from stacking it
|
|
813
|
+
* twice as the error passes back up through the enclosing keys.
|
|
694
814
|
*/
|
|
695
|
-
if (wasRead && resolved
|
|
815
|
+
if (wasRead && isUnsetValue(resolved) && !e?.rmanValueHint) {
|
|
696
816
|
e.rmanValueHint = true;
|
|
697
|
-
e.message =
|
|
698
|
-
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${describeAt(at)}.` +
|
|
699
|
-
`\n Write \`value ?? []\` (or \`?? ''\`) if it has to work as the first layer too.`;
|
|
817
|
+
e.message = `${e.message}\n Note: nothing below this layer sets "${describeAt(at)}, so \`value\` is empty.`;
|
|
700
818
|
}
|
|
701
819
|
throw e;
|
|
702
820
|
}
|
|
@@ -937,3 +1055,69 @@ function deepFreeze(value) {
|
|
|
937
1055
|
for (const item of Object.values(value))
|
|
938
1056
|
deepFreeze(item);
|
|
939
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* What a layer is handed as `value`: **the list form of whatever is underneath it.**
|
|
1060
|
+
*
|
|
1061
|
+
* `value` exists for one job - extending what a closer layer inherited, the general form of `+key`,
|
|
1062
|
+
* and `+key` only ever meant append. So the shape a spread wants is the shape to hand over:
|
|
1063
|
+
* `[...value, 'x']` works with no guard whether the layers below said nothing, said `'build'`, or
|
|
1064
|
+
* said `['build']`.
|
|
1065
|
+
*
|
|
1066
|
+
* **Coercing a scalar into a one-element list is not a guess about the key's type.** Every key this
|
|
1067
|
+
* is reached for is declared `X | X[]` - `clean.include`, `clean.exclude`, `version.stamp`,
|
|
1068
|
+
* `version.before`/`.exec`/`.after` - where the list is the type and the scalar is *shorthand*.
|
|
1069
|
+
* `CleanService` and `RunService` already normalize it; doing it here as well decides nothing new.
|
|
1070
|
+
* And spreading a string into its characters, which is what handing the raw value over did, is not
|
|
1071
|
+
* something any key wants.
|
|
1072
|
+
*
|
|
1073
|
+
* It reads as the scalar wherever a scalar is what makes sense, through `Symbol.toPrimitive`:
|
|
1074
|
+
* `` `${value}-x` `` is `'build-x'` and `value + 1` is `6`. A *list* underneath refuses both, since
|
|
1075
|
+
* splicing `a,b` into a sentence is a mistake worth naming; so does nothing-underneath.
|
|
1076
|
+
*
|
|
1077
|
+
* **A boolean is handed over as itself**, the one carve-out, because it is never a list nor a
|
|
1078
|
+
* list's shorthand - and an object cannot be fixed up for it: `!value` and `value ? :` use
|
|
1079
|
+
* ToBoolean, which has no hook and answers `true` for every object, so a wrapped `false` would read
|
|
1080
|
+
* as `true`. Measured. `[...value]` on one then throws, which is right - spreading a boolean means
|
|
1081
|
+
* nothing.
|
|
1082
|
+
*
|
|
1083
|
+
* **The cost, stated rather than hidden: strict equality and string methods on an inherited
|
|
1084
|
+
* scalar.** `value === 'build'` is `false` and `value.includes('bui')` is `false` (an array's
|
|
1085
|
+
* `includes` matches elements, not substrings). `value == 'build'`, `` `${value}` === 'build' `` and
|
|
1086
|
+
* `String(value).includes('bui')` all work, and `value.length` is the number of layers' worth of
|
|
1087
|
+
* entries rather than a string's length. That is the trade for the append case never needing a
|
|
1088
|
+
* guard; `value` was introduced for the append case.
|
|
1089
|
+
*/
|
|
1090
|
+
function previousValue(raw, at) {
|
|
1091
|
+
/** Never a list, and unfixable as one - see above. */
|
|
1092
|
+
if (typeof raw === 'boolean')
|
|
1093
|
+
return raw;
|
|
1094
|
+
const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
|
|
1095
|
+
Object.defineProperty(list, UNSET_MARKER, { value: raw === undefined });
|
|
1096
|
+
return Object.defineProperty(list, Symbol.toPrimitive, {
|
|
1097
|
+
value: (hint) => {
|
|
1098
|
+
if (raw === undefined) {
|
|
1099
|
+
throw unusable(at, 'nothing below this layer sets it, so it is empty', "`value ?? ''`, `value ?? 0`");
|
|
1100
|
+
}
|
|
1101
|
+
if (typeof raw !== 'string' && typeof raw !== 'number') {
|
|
1102
|
+
throw unusable(at, `the layer below it is ${Array.isArray(raw) ? 'a list' : 'an object'}`, '`value.join(", ")` for a list');
|
|
1103
|
+
}
|
|
1104
|
+
return hint === 'string' ? String(raw) : raw;
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
/** The one sentence both refusals share: what was asked for, why it cannot be done, what to write
|
|
1109
|
+
* instead. Marked `rmanValueHint` so `walkWithPrevious`'s catch leaves it alone - that note exists
|
|
1110
|
+
* to explain an empty `value` to an error that does not mention it, and this error *is* that
|
|
1111
|
+
* explanation. */
|
|
1112
|
+
function unusable(at, because, instead) {
|
|
1113
|
+
const error = new Error(`\`value\` cannot be used as a string or a number here - ${because}, for "${describeAt(at)}. ` +
|
|
1114
|
+
`It spreads as a list (\`[...value, x]\`); to use it as something else, say what it should be - ${instead}.`);
|
|
1115
|
+
error.rmanValueHint = true;
|
|
1116
|
+
return error;
|
|
1117
|
+
}
|
|
1118
|
+
/** Whether `value` stands for "no layer underneath set this key" - by the marker `previousValue`
|
|
1119
|
+
* puts on it, never by emptiness, since a layer may legitimately resolve to `[]`. */
|
|
1120
|
+
function isUnsetValue(value) {
|
|
1121
|
+
return Array.isArray(value) && value[UNSET_MARKER] === true;
|
|
1122
|
+
}
|
|
1123
|
+
const UNSET_MARKER = Symbol('rman.valueUnset');
|
|
@@ -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
|
+
}
|
package/core/custom-command.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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(
|
|
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:
|
|
166
|
+
export declare function assertNoBuiltinShadowing(commands: readonly {
|
|
167
|
+
name: string;
|
|
168
|
+
file: string;
|
|
169
|
+
}[], builtins: readonly string[]): void;
|