rman 1.2.5 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +180 -50
- package/core/config.js +332 -153
- 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 +138 -93
- 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 -43
- 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 +739 -212
- 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,79 +93,192 @@ 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
|
|
117
200
|
* it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
|
|
118
201
|
* chain does.
|
|
119
202
|
*
|
|
120
|
-
* Every level contributes in two ways
|
|
121
|
-
*
|
|
122
|
-
* - **
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
203
|
+
* Every level contributes in two ways:
|
|
204
|
+
*
|
|
205
|
+
* - **An unmarked key configures that directory and every package under it.** What a parent says
|
|
206
|
+
* reaches the children, which is what every directory-scoped config in the ecosystem does and
|
|
207
|
+
* what a reader expects without being told.
|
|
208
|
+
* - **A `"[selector]"` block narrows the audience** - `"[/]"` to the root package alone, `"[*]"` or
|
|
209
|
+
* a glob to the packages below (never the root, which is nobody's child). See `parseSelector`.
|
|
210
|
+
*
|
|
211
|
+
* **The root used to be the one directory whose unmarked config did *not* cascade**, on the
|
|
212
|
+
* reasoning that a setting means different things to a package and to the repository - and the
|
|
213
|
+
* reasoning is sound, but the rule it produced was not readable: an intermediate `packages/`
|
|
214
|
+
* cascaded while the root did not, so what a file meant depended on whether a `package.json` sat
|
|
215
|
+
* beside it. `vars` then had to be carved out as an exception, which is what a rule fighting itself
|
|
216
|
+
* looks like. One sentence now covers both: what is written above reaches below, and `"[/]"` is how
|
|
217
|
+
* a statement stays at the root.
|
|
218
|
+
*
|
|
219
|
+
* **The cost is real and lands on one subtree.** `run.<script>`'s hooks on the root are a repo-wide
|
|
220
|
+
* bookend, run once at the repository root; on a package they are that package's own hook, run in
|
|
221
|
+
* its directory. Cascaded, one declaration is both - once at the root and once per package. A
|
|
222
|
+
* repo-wide bookend therefore belongs under `"[/]"`, where its audience is visible; that is the
|
|
223
|
+
* migration this change asks for, and the only one that is not mechanical.
|
|
224
|
+
*
|
|
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.
|
|
139
240
|
*/
|
|
140
|
-
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) {
|
|
141
246
|
const result = {};
|
|
142
247
|
const target = path.resolve(targetDir);
|
|
143
248
|
/** The root *package* is the one whose directory is the repository root - no other test is
|
|
144
249
|
* needed, and none would be as reliable: a name can be anything. In a single-package repository
|
|
145
|
-
* that is the only package, so `"[/]"` reaches it and `"[
|
|
250
|
+
* that is the only package, so `"[/]"` reaches it and `"[*]"` reaches nothing. */
|
|
146
251
|
const isRoot = target === path.resolve(rootDir);
|
|
147
252
|
for (const dir of dirChain(rootDir, targetDir)) {
|
|
148
253
|
let local = cache.get(dir);
|
|
149
254
|
if (!local) {
|
|
150
|
-
local = await readDirConfig(dir
|
|
255
|
+
local = await readDirConfig(dir, {
|
|
256
|
+
inject: path.resolve(dir) === path.resolve(rootDir) ? inject : undefined,
|
|
257
|
+
});
|
|
151
258
|
cache.set(dir, local);
|
|
152
259
|
}
|
|
153
|
-
// A directory holding a package speaks for that package only - which is what keeps the root's
|
|
154
|
-
// own config off every package under it. A directory that holds none (an intermediate
|
|
155
|
-
// `packages/`, say) has no package to speak for, so its unmarked config can only mean
|
|
156
|
-
// "everything below" and still cascades.
|
|
157
|
-
const ownsAPackage = fs.existsSync(path.join(dir, 'package.json'));
|
|
158
|
-
const speaksForTarget = !ownsAPackage || path.resolve(dir) === target;
|
|
159
260
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
261
|
+
* **Unmarked first, because it is the widest thing this level says** - and that is an inversion
|
|
262
|
+
* of the order this loop used to run in, where a directory's own plain config beat a selector
|
|
263
|
+
* declared beside it. Under the old reading "unmarked" meant *this package* and so was the
|
|
264
|
+
* narrower of the two; it now means *this package and everything below*, which is the wider.
|
|
265
|
+
* Precedence follows the audience, not the spelling, so it had to move.
|
|
165
266
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
267
|
+
* Its position in the file is deliberately not consulted: a selector block written above the
|
|
268
|
+
* plain keys still wins. Unmarked is not a fourth selector - it is the level's floor, and the
|
|
269
|
+
* layer that feeds the directories below it.
|
|
168
270
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for (const block of matchingSelectors(local, packageName, isRoot))
|
|
271
|
+
mergeConfig(result, stripSelectors(local));
|
|
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))
|
|
175
276
|
mergeConfig(result, block);
|
|
176
277
|
}
|
|
177
|
-
if (speaksForTarget)
|
|
178
|
-
mergeConfig(result, stripSelectors(local));
|
|
179
278
|
}
|
|
180
|
-
|
|
181
|
-
* and becomes the value itself. Done here rather than per layer: until the chain is finished,
|
|
182
|
-
* the key it appends to may still be coming. */
|
|
183
|
-
return finalizeConfig(result);
|
|
279
|
+
return result;
|
|
184
280
|
}
|
|
185
|
-
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[
|
|
281
|
+
/** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[pkg-a]"`. The
|
|
186
282
|
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
187
283
|
* one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
|
|
188
284
|
* parses as a flow sequence. */
|
|
@@ -190,74 +286,77 @@ export function isSelectorKey(key) {
|
|
|
190
286
|
return key.length > 2 && key.startsWith('[') && key.endsWith(']');
|
|
191
287
|
}
|
|
192
288
|
/**
|
|
193
|
-
* **Which packages a selector speaks for.**
|
|
289
|
+
* **Which packages a selector speaks for.** Two audiences, and the second is a glob:
|
|
194
290
|
*
|
|
195
291
|
* | | |
|
|
196
292
|
* | --- | --- |
|
|
197
|
-
* | `"[/]"` | the **root package**
|
|
198
|
-
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | **
|
|
199
|
-
* | `"[ws:*]"`, `"[workspace:pkg-*]"` | every **non-root** package the glob matches |
|
|
293
|
+
* | `"[/]"` | the **root package** alone |
|
|
294
|
+
* | `"[*]"`, `"[pkg-a]"`, `"[*-dialect]"` | the packages **below** this directory that the glob matches |
|
|
200
295
|
*
|
|
201
296
|
* `/` for the root because that is what a repository root is called everywhere else, and it cannot
|
|
202
|
-
* collide with a package name.
|
|
203
|
-
* `*`, so `"[ws:pkg-*]"` means what it looks like.
|
|
297
|
+
* collide with a package name.
|
|
204
298
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
299
|
+
* **The root is never selected by name, and that one rule removes two traps.** A glob matches
|
|
300
|
+
* package names, and the root is nobody's child - so `"[my-*]"` cannot quietly pick up a repository
|
|
301
|
+
* whose root package happens to be called `my-repo`, and `"[*]"` cannot hand a package-shaped
|
|
302
|
+
* setting to a root that has no build directory to apply it to. The root is addressed structurally
|
|
303
|
+
* or not at all.
|
|
304
|
+
*
|
|
305
|
+
* **`"[ws:*]"` / `"[workspace:*]"` is accepted and means exactly `"[*]"`.** The qualifier existed to
|
|
306
|
+
* say "not the root" back when a bare glob included it; the shape of the set says that now, so it
|
|
307
|
+
* has nothing left to add. Accepted rather than rejected because the two spellings resolve to the
|
|
308
|
+
* same packages - an error would be friction with no reader to protect.
|
|
209
309
|
*/
|
|
210
310
|
export function parseSelector(key) {
|
|
211
311
|
const inner = key.slice(1, -1);
|
|
212
312
|
if (inner === ROOT_SELECTOR_INNER)
|
|
213
313
|
return { scope: 'root', test: () => true };
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const re = globToRegExp(inner.slice(prefix.length));
|
|
217
|
-
return { scope: 'workspace', test: name => re.test(name) };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const re = globToRegExp(inner);
|
|
221
|
-
return { scope: 'all', test: name => re.test(name) };
|
|
314
|
+
const re = globToRegExp(stripWorkspacePrefix(inner));
|
|
315
|
+
return { scope: 'package', test: name => re.test(name) };
|
|
222
316
|
}
|
|
223
317
|
/** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
|
|
224
318
|
* `mysql-dialect` but not `my-dialect-helper`. Glob rather than regex, to match every other
|
|
225
319
|
* pattern in rman (`allowBranch`, `changelog.tagPattern`, `clean.include`). */
|
|
226
320
|
export function selectorToRegExp(key) {
|
|
227
|
-
return globToRegExp(key.slice(1, -1));
|
|
321
|
+
return globToRegExp(stripWorkspacePrefix(key.slice(1, -1)));
|
|
228
322
|
}
|
|
229
323
|
/**
|
|
230
|
-
* Every selector block in `config` that speaks for this package, in
|
|
324
|
+
* Every selector block in `config` that speaks for this package, **in the order they were written**
|
|
325
|
+
* - later wins, the way `overrides` works in eslint, prettier and babel, and the way a `.gitignore`
|
|
326
|
+
* rule does.
|
|
327
|
+
*
|
|
328
|
+
* **There used to be a ranking** (`"[*]"` lowest, then a catch-all `"[ws:*]"`, then the rest by
|
|
329
|
+
* declaration), so that "everything" could not beat a rule about one package by being written last.
|
|
330
|
+
* It was dropped because the ordering it implies does not exist: specificity only ranks sets that
|
|
331
|
+
* nest, and globs do not. For a package called `pkg-dialect`, neither `"[pkg-*]"` nor
|
|
332
|
+
* `"[*-dialect]"` contains the other, so any answer is an invented tiebreak - and an invented
|
|
333
|
+
* tiebreak is worse than the order the author typed. What was left was already declaration order
|
|
334
|
+
* with one case lifted out of it; this removes the exception rather than generalizing it.
|
|
231
335
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* over both, and two equally specific globs resolve by the order they were written in. A catch-all
|
|
235
|
-
* is ranked rather than left to declaration order on purpose: where you happen to write "everything"
|
|
236
|
-
* should not decide whether it beats a rule about one package.
|
|
336
|
+
* The cost, which the docs state rather than hide: a catch-all written *below* a narrower block now
|
|
337
|
+
* overrides it. Writing catch-alls first is a convention, not a rule - the file reads top to bottom.
|
|
237
338
|
*/
|
|
238
|
-
function matchingSelectors(config,
|
|
339
|
+
function matchingSelectors(config, selector, isRoot) {
|
|
239
340
|
const matches = [];
|
|
240
341
|
for (const [key, value] of Object.entries(config)) {
|
|
241
342
|
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
242
343
|
continue;
|
|
243
344
|
const { scope, test } = parseSelector(key);
|
|
244
|
-
|
|
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))
|
|
245
348
|
continue;
|
|
246
|
-
|
|
247
|
-
continue;
|
|
248
|
-
if (!test(packageName))
|
|
249
|
-
continue;
|
|
250
|
-
matches.push([selectorRank(key), value]);
|
|
349
|
+
matches.push(value);
|
|
251
350
|
}
|
|
252
|
-
return matches
|
|
351
|
+
return matches;
|
|
253
352
|
}
|
|
254
|
-
/**
|
|
255
|
-
*
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return
|
|
353
|
+
/** `"[ws:*]"` and `"[workspace:*]"` are the pre-2.x spelling of "not the root", kept working
|
|
354
|
+
* because they now name the same set a bare glob does. Stripped here so one code path serves both. */
|
|
355
|
+
function stripWorkspacePrefix(inner) {
|
|
356
|
+
for (const prefix of WORKSPACE_PREFIXES)
|
|
357
|
+
if (inner.startsWith(prefix))
|
|
358
|
+
return inner.slice(prefix.length);
|
|
359
|
+
return inner;
|
|
261
360
|
}
|
|
262
361
|
function globToRegExp(glob) {
|
|
263
362
|
const source = glob
|
|
@@ -273,12 +372,10 @@ function stripSelectors(config) {
|
|
|
273
372
|
result[key] = value;
|
|
274
373
|
return result;
|
|
275
374
|
}
|
|
276
|
-
const CATCH_ALL = '[*]';
|
|
277
375
|
/** `"[/]"` - the root package, spelled the way a repository root is spelled everywhere else, and
|
|
278
376
|
* unable to collide with a package name. */
|
|
279
377
|
const ROOT_SELECTOR_INNER = '/';
|
|
280
|
-
/**
|
|
281
|
-
* someone else has to understand; the short one is what gets typed. */
|
|
378
|
+
/** Accepted spellings of the retired "not the root" qualifier - see `stripWorkspacePrefix`. */
|
|
282
379
|
const WORKSPACE_PREFIXES = ['workspace:', 'ws:'];
|
|
283
380
|
function dirChain(rootDir, targetDir) {
|
|
284
381
|
const rel = path.relative(rootDir, targetDir);
|
|
@@ -292,6 +389,11 @@ function dirChain(rootDir, targetDir) {
|
|
|
292
389
|
}
|
|
293
390
|
return dirs;
|
|
294
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
|
+
*/
|
|
295
397
|
export function interpolateConfig(config, scope, options) {
|
|
296
398
|
const skip = options?.skip ?? [];
|
|
297
399
|
/**
|
|
@@ -318,8 +420,8 @@ export function interpolateConfig(config, scope, options) {
|
|
|
318
420
|
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
319
421
|
return walk(config, scope, context, base, skip);
|
|
320
422
|
/**
|
|
321
|
-
* The config's own top-level keys, readable bare: `${{
|
|
322
|
-
* 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
|
|
323
425
|
* copy that drifts when the first one changes.
|
|
324
426
|
*
|
|
325
427
|
* Resolved **on demand**, one key at a time, and memoized. Interpolating the config in tree order
|
|
@@ -379,6 +481,13 @@ export function interpolateConfig(config, scope, options) {
|
|
|
379
481
|
const result = {};
|
|
380
482
|
for (const key of Object.keys(config))
|
|
381
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
|
+
*/
|
|
382
491
|
return result;
|
|
383
492
|
}
|
|
384
493
|
/**
|
|
@@ -399,14 +508,14 @@ export const DEFERRED_PATHS = ['version.before', 'version.exec', 'version.after'
|
|
|
399
508
|
* config:
|
|
400
509
|
*
|
|
401
510
|
* ```js
|
|
402
|
-
* '[
|
|
511
|
+
* '[*]': {
|
|
403
512
|
* clean: { include: ({ vars, value }) => [...value, vars.buildDir] }, // a value: called here
|
|
404
513
|
* run: { build: { after: ({ pkg }) => copyDocs(pkg) } }, // a step: called by `run`
|
|
405
514
|
* }
|
|
406
515
|
* ```
|
|
407
516
|
*
|
|
408
517
|
* **The key decides, and it already did.** `run.build.exec: 'tsc -b'` is a shell command and
|
|
409
|
-
* `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
|
|
410
519
|
* where they sit. A function inherits the same rule, so nothing new has to be learned and no marker
|
|
411
520
|
* has to be remembered. The alternative was inspecting the function (arity, parameter names), which
|
|
412
521
|
* is the kind of guess `loadPlugins` refuses to make about a module's export for the same reason:
|
|
@@ -434,16 +543,23 @@ export const STEP_PATHS = [
|
|
|
434
543
|
];
|
|
435
544
|
/**
|
|
436
545
|
* Keys whose **whole subtree** is code rather than config, so no function under them is a value to
|
|
437
|
-
* compute.
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
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.
|
|
445
561
|
*/
|
|
446
|
-
export const CODE_SUBTREES = ['plugins'];
|
|
562
|
+
export const CODE_SUBTREES = ['plugins', 'commands', 'publishTargets'];
|
|
447
563
|
const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
|
|
448
564
|
/** A config key an expression could actually name. Anything else - a `"[selector]"` block, a
|
|
449
565
|
* `"lint:fix"` - is unreachable as a bare identifier anyway, so it is not bound. */
|
|
@@ -661,13 +777,14 @@ function shortenOrigin(file) {
|
|
|
661
777
|
*
|
|
662
778
|
* **Always bound, even with nothing underneath.** Left unbound, an expression naming it fails with
|
|
663
779
|
* V8's `value is not defined`, which reads as "there is no such thing" rather than "nothing below
|
|
664
|
-
* 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.
|
|
665
782
|
*
|
|
666
783
|
* The chain resolves bottom-up, so a layer deriving from a layer that itself derived from something
|
|
667
784
|
* is handed the finished value rather than a half-resolved expression.
|
|
668
785
|
*/
|
|
669
786
|
function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
670
|
-
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);
|
|
671
788
|
const outer = Object.getOwnPropertyDescriptor(context, VALUE_KEY);
|
|
672
789
|
/** A getter, so the catch below can tell whether the value **actually read `value`**: the hint is
|
|
673
790
|
* irrelevant to any other failure, and attaching it anyway is the send-the-reader-to-the-wrong-
|
|
@@ -686,22 +803,18 @@ function walkWithPrevious(item, previous, scope, context, at, skip) {
|
|
|
686
803
|
}
|
|
687
804
|
catch (e) {
|
|
688
805
|
/**
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
* Here rather than in `callValueFn`, so the expression and the function spelling get the same
|
|
694
|
-
* sentence from the same place. `rmanValueHint` keeps a rethrow from stacking it twice as the
|
|
695
|
-
* 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.
|
|
696
809
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
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.
|
|
699
814
|
*/
|
|
700
|
-
if (wasRead && resolved
|
|
815
|
+
if (wasRead && isUnsetValue(resolved) && !e?.rmanValueHint) {
|
|
701
816
|
e.rmanValueHint = true;
|
|
702
|
-
e.message =
|
|
703
|
-
`${e.message}\n \`value\` is undefined here - nothing below this layer sets "${describeAt(at)}.` +
|
|
704
|
-
`\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.`;
|
|
705
818
|
}
|
|
706
819
|
throw e;
|
|
707
820
|
}
|
|
@@ -942,3 +1055,69 @@ function deepFreeze(value) {
|
|
|
942
1055
|
for (const item of Object.values(value))
|
|
943
1056
|
deepFreeze(item);
|
|
944
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');
|