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/repository.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import colors from 'ansi-colors';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import semver from 'semver';
|
|
5
|
+
import { detectBuiltin } from '../plugins/detect.js';
|
|
4
6
|
import { GitHelper } from '../utils/git.js';
|
|
7
|
+
import { RmanApplication } from './application.js';
|
|
5
8
|
import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
|
|
6
9
|
import { Manifest } from './manifest.js';
|
|
10
|
+
import { ORIGINS } from './merge-config.js';
|
|
7
11
|
import { Package } from './package.js';
|
|
8
|
-
import { loadPlugins } from './plugin.js';
|
|
12
|
+
import { loadPlugins, registerPlugin } from './plugin-loader.js';
|
|
9
13
|
import { Workspace } from './workspace.js';
|
|
10
14
|
export class Repository extends Package {
|
|
11
15
|
dirname;
|
|
@@ -13,9 +17,10 @@ export class Repository extends Package {
|
|
|
13
17
|
packages;
|
|
14
18
|
cwd;
|
|
15
19
|
rootPackage;
|
|
20
|
+
/** See the `defineProperty` in the constructor - what detection supplied, when it did. */
|
|
21
|
+
detectedBuiltin;
|
|
16
22
|
/** Commands the repository's plugins contributed, loaded during `create` because the workspace
|
|
17
23
|
* providers they bring are needed before any package can be found. `cli.ts` registers them. */
|
|
18
|
-
pluginCommands = [];
|
|
19
24
|
/**
|
|
20
25
|
* Cached repository scope - see `_repositoryScope`.
|
|
21
26
|
*
|
|
@@ -39,17 +44,40 @@ export class Repository extends Package {
|
|
|
39
44
|
* for every package that mentions it.
|
|
40
45
|
*/
|
|
41
46
|
_readCache = new Map();
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The application this repository belongs to - its services, its technologies, its logger.
|
|
49
|
+
*
|
|
50
|
+
* **Non-enumerable**, like `_repoScope` and `_git` beside it: the two things that walk a
|
|
51
|
+
* repository are `{...pkg}` spreads and the config scope, and an enumerable back-reference to the
|
|
52
|
+
* whole application would be dragged into both. A `Package` deliberately has no such field at
|
|
53
|
+
* all; a repository is never spread or serialized, which is what makes this one safe - measured,
|
|
54
|
+
* rather than assumed.
|
|
55
|
+
*/
|
|
56
|
+
app;
|
|
57
|
+
constructor(app, dirname, monorepo, packages,
|
|
43
58
|
/** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
|
|
44
59
|
* resolved repository root, possibly several levels up), this is where the user's shell
|
|
45
60
|
* really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
|
|
46
|
-
cwd = dirname
|
|
47
|
-
|
|
61
|
+
cwd = dirname,
|
|
62
|
+
/** The root directory's own technology, from the same walk that found the packages - so the
|
|
63
|
+
* repository and its `rootPackage` agree without either searching the registry again. */
|
|
64
|
+
platform) {
|
|
65
|
+
super(dirname, app, platform);
|
|
48
66
|
this.dirname = dirname;
|
|
49
67
|
this.monorepo = monorepo;
|
|
50
68
|
this.packages = packages;
|
|
51
69
|
this.cwd = cwd;
|
|
52
|
-
this
|
|
70
|
+
Object.defineProperty(this, 'app', { value: app, enumerable: false, writable: false });
|
|
71
|
+
/**
|
|
72
|
+
* What detection decided for this repository, or `undefined` when it declared its own
|
|
73
|
+
* technology (or when there was nothing to detect). Carried here because `_resolveConfigs` runs
|
|
74
|
+
* later and every read of the root's config has to agree with the one decision `create` made.
|
|
75
|
+
*
|
|
76
|
+
* Non-enumerable, like `app` and for the same reason: `{...repository}` and `toEqual` both walk
|
|
77
|
+
* a repository, and bookkeeping that shows up there turns spec failures into diffs about it.
|
|
78
|
+
*/
|
|
79
|
+
Object.defineProperty(this, 'detectedBuiltin', { value: undefined, enumerable: false, writable: true });
|
|
80
|
+
this.rootPackage = new Package(dirname, app, platform);
|
|
53
81
|
if (!monorepo)
|
|
54
82
|
this.packages = [this.rootPackage];
|
|
55
83
|
// Config resolution can load a `.rmanrc.cjs`/`.mjs`/`.js` module (dynamic `import()`, always
|
|
@@ -94,11 +122,22 @@ export class Repository extends Package {
|
|
|
94
122
|
* dirty, the reference point decides the rest: without `hash`, `committed`
|
|
95
123
|
* means committed but not yet in the upstream branch; with `hash`, `changed`
|
|
96
124
|
* means it differs from that commit. Otherwise a package is `clean`.
|
|
125
|
+
*
|
|
126
|
+
* **Keyed by `Package.selector`**, which is what addresses a package - it was `name`, and the two
|
|
127
|
+
* coincide wherever a technology names its packages. A repository whose does not had every such
|
|
128
|
+
* package answering to `""`, so one entry stood for all of them.
|
|
129
|
+
*
|
|
130
|
+
* **`includeRoot` is opt-in, and the reason is that the root's answer means something different.**
|
|
131
|
+
* Its directory contains every other package, so the same rule - "does a changed file fall under
|
|
132
|
+
* this directory" - reports `dirty` for the root whenever *anything* in the repository is dirty.
|
|
133
|
+
* That is the honest reading of the rule rather than a bug, and it is not what `run --changed`
|
|
134
|
+
* wants, so only a caller that asked for the root gets it (`rman list`'s table, which shows the
|
|
135
|
+
* root as the tree's own row).
|
|
97
136
|
*/
|
|
98
137
|
async listStatus(options) {
|
|
99
138
|
const hash = options?.hash;
|
|
100
139
|
const git = new GitHelper({ cwd: this.dirname });
|
|
101
|
-
const packages = this.getPackages();
|
|
140
|
+
const packages = options?.includeRoot ? [this.rootPackage, ...this.getPackages()] : this.getPackages();
|
|
102
141
|
const belongsTo = (p, files) => files.some(f => !path.relative(p.dirname, f).startsWith('..'));
|
|
103
142
|
const [dirtyFiles, referenceFiles] = await Promise.all([
|
|
104
143
|
git.listDirtyFiles({ absolute: true }),
|
|
@@ -107,11 +146,11 @@ export class Repository extends Package {
|
|
|
107
146
|
const result = {};
|
|
108
147
|
for (const p of packages) {
|
|
109
148
|
if (belongsTo(p, dirtyFiles))
|
|
110
|
-
result[p.
|
|
149
|
+
result[p.selector] = 'dirty';
|
|
111
150
|
else if (belongsTo(p, referenceFiles))
|
|
112
|
-
result[p.
|
|
151
|
+
result[p.selector] = hash ? 'changed' : 'committed';
|
|
113
152
|
else
|
|
114
|
-
result[p.
|
|
153
|
+
result[p.selector] = 'clean';
|
|
115
154
|
}
|
|
116
155
|
return result;
|
|
117
156
|
}
|
|
@@ -173,30 +212,96 @@ export class Repository extends Package {
|
|
|
173
212
|
* nothing under `getPackages()` is the root, so only its own unmarked config applies.
|
|
174
213
|
*/
|
|
175
214
|
/**
|
|
176
|
-
* Gives every package its `repository
|
|
177
|
-
*
|
|
215
|
+
* Gives every package its `repository`, and hangs the `parent`/`children` tree off the walk that
|
|
216
|
+
* found them - before any config is resolved, since a config expression or a provider may already
|
|
217
|
+
* want to navigate from a package outwards.
|
|
218
|
+
*
|
|
219
|
+
* **The containment is read from the tree rather than recomputed from paths.** It used to be an
|
|
220
|
+
* O(n²) sweep comparing every package's directory against every other's and keeping the longest
|
|
221
|
+
* prefix - which is the same question `Workspace.walk` answers on the way down, asked again
|
|
222
|
+
* afterwards with the answer thrown away. Two places deriving one relationship is two places to
|
|
223
|
+
* disagree; there is one now.
|
|
224
|
+
*
|
|
225
|
+
* **`parent` is defined non-enumerably, `children` is a plain field**, which is the one asymmetry
|
|
226
|
+
* here and it is deliberate: a tree is serialized downwards, so `children` has to be walkable and
|
|
227
|
+
* `parent` must not be, or every `JSON.stringify` is a cycle. The same way `Repository.app` and
|
|
228
|
+
* `ORIGINS` travel.
|
|
178
229
|
*
|
|
179
230
|
* A repository's own `repository` is itself, which reads oddly and is the honest answer:
|
|
180
231
|
* `Repository extends Package`, so the repository *is* a package of its own repository.
|
|
181
232
|
*/
|
|
182
|
-
_linkPackages() {
|
|
183
|
-
|
|
184
|
-
|
|
233
|
+
_linkPackages(tree, nodes, packages) {
|
|
234
|
+
/** Non-enumerable everywhere, including on the repository itself - see `Package.repository`.
|
|
235
|
+
* `writable` because `import` grafts an external repository's packages onto this one. */
|
|
236
|
+
const link = (pkg) => {
|
|
237
|
+
Object.defineProperty(pkg, 'repository', { value: this, enumerable: false, configurable: true, writable: true });
|
|
238
|
+
};
|
|
239
|
+
link(this);
|
|
240
|
+
link(this.rootPackage);
|
|
241
|
+
for (const pkg of this.packages)
|
|
242
|
+
link(pkg);
|
|
243
|
+
/** The walk visits a directory once, so one node is one package and this map is a bijection -
|
|
244
|
+
* which is what lets the edges below be read off the tree instead of guessed from paths. */
|
|
245
|
+
const byNode = new Map(nodes.map((node, i) => [node, packages[i]]));
|
|
246
|
+
byNode.set(tree, this.rootPackage);
|
|
247
|
+
for (const node of [tree, ...nodes]) {
|
|
248
|
+
const pkg = byNode.get(node);
|
|
249
|
+
for (const childNode of node.children) {
|
|
250
|
+
const child = byNode.get(childNode);
|
|
251
|
+
pkg.children.push(child);
|
|
252
|
+
Object.defineProperty(child, 'parent', { value: pkg, enumerable: false, configurable: true });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** `Repository extends Package` while holding a separate `rootPackage` for the same directory,
|
|
256
|
+
* so both are truthfully the root - they share the one array rather than each getting a copy
|
|
257
|
+
* that could drift. */
|
|
258
|
+
Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Gives every package the selector a `"[glob]"` block and `--scope` match it by, and refuses two
|
|
262
|
+
* packages that would answer to the same one.
|
|
263
|
+
*
|
|
264
|
+
* **Its own step, before any config is resolved by a selector**, which is the ordering that makes
|
|
265
|
+
* the rest work: `_resolveConfigs` asks `resolveConfig` for each package *by selector*, so an
|
|
266
|
+
* address assigned afterwards would be applied to nothing.
|
|
267
|
+
*
|
|
268
|
+
* **`name` comes from the unmarked cascade only** - the same read `platform` gets, from the same
|
|
269
|
+
* cache, for the same reason. A `"[glob]"` block cannot set it (`assertSelectorBlocks` refuses
|
|
270
|
+
* one) because the glob matches the very thing the block would be setting.
|
|
271
|
+
*
|
|
272
|
+
* **Uniqueness is checked, and the cascade is the mistake it usually catches.** `name` cascades
|
|
273
|
+
* like every unmarked key, so one declaration above two packages gives both the same address -
|
|
274
|
+
* and the failure would otherwise be silent in the worst way: the config reaches both and
|
|
275
|
+
* `getPackage` returns whichever came first. The message names both directories, and says the
|
|
276
|
+
* cascade out loud when the two got it from one declaration.
|
|
277
|
+
*/
|
|
278
|
+
async _assignSelectors(rootDir, cache) {
|
|
279
|
+
const declaredBy = new Map();
|
|
280
|
+
for (const pkg of [this.rootPackage, ...this.packages]) {
|
|
281
|
+
const config = await resolveConfig(rootDir, pkg.dirname, cache, undefined);
|
|
282
|
+
const declared = config.name;
|
|
283
|
+
if (declared !== undefined && (typeof declared !== 'string' || !declared.trim())) {
|
|
284
|
+
throw new Error(`"name" takes the selector this package answers to - "${pkg.dirname}" gave ${typeof declared}.`);
|
|
285
|
+
}
|
|
286
|
+
pkg.selector = declared?.trim() || pkg.platformSelector();
|
|
287
|
+
declaredBy.set(pkg, declared !== undefined);
|
|
288
|
+
}
|
|
289
|
+
/** The root is left out: a glob never matches it and `"[/]"` needs no name, so it shares an
|
|
290
|
+
* address with nobody - see `Package.selector`. */
|
|
291
|
+
const bySelector = new Map();
|
|
185
292
|
for (const pkg of this.packages) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (contains && (!parent || other.dirname.length > parent.dirname.length))
|
|
197
|
-
parent = other;
|
|
293
|
+
const clash = bySelector.get(pkg.selector);
|
|
294
|
+
if (clash) {
|
|
295
|
+
const cascaded = declaredBy.get(pkg) && declaredBy.get(clash);
|
|
296
|
+
throw new Error(`Two packages answer to the selector "${pkg.selector}":\n ${clash.dirname}\n ${pkg.dirname}\n` +
|
|
297
|
+
` A selector has to be unique - "[${pkg.selector}]" and \`--scope ${pkg.selector}\` ` +
|
|
298
|
+
`cannot mean two packages.` +
|
|
299
|
+
(cascaded
|
|
300
|
+
? `\n Both got it from one cascading "name" declaration above them; declare it in ` +
|
|
301
|
+
`each package's own ".rmanrc" instead.`
|
|
302
|
+
: ''));
|
|
198
303
|
}
|
|
199
|
-
pkg.
|
|
304
|
+
bySelector.set(pkg.selector, pkg);
|
|
200
305
|
}
|
|
201
306
|
}
|
|
202
307
|
async _resolveConfigs() {
|
|
@@ -208,15 +313,18 @@ export class Repository extends Package {
|
|
|
208
313
|
* second `rawConfig` copy of every package's config for that one reader; measured identical.
|
|
209
314
|
*/
|
|
210
315
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
316
|
+
* **By selector, not by name** - `"[glob]"` matches `Package.selector`, which is the package's
|
|
317
|
+
* own `.rmanrc "name"` when it assigned one and its platform's answer otherwise. They coincide
|
|
318
|
+
* for every Node repository; they are not the same question, and `name` was the wrong one to
|
|
319
|
+
* ask, since a package having one at all is an ecosystem's promise rather than rman's.
|
|
320
|
+
*
|
|
321
|
+
* The root passes its own too, although no glob can match it: `"[/]"` is applied on the
|
|
322
|
+
* strength of the target *being* the root and needs no selector at all (see `resolveConfig`).
|
|
215
323
|
*/
|
|
216
|
-
const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.
|
|
324
|
+
const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.selector, this.detectedBuiltin);
|
|
217
325
|
this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
|
|
218
326
|
for (const pkg of this.packages) {
|
|
219
|
-
const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.
|
|
327
|
+
const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.selector, this.detectedBuiltin);
|
|
220
328
|
pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
|
|
221
329
|
}
|
|
222
330
|
if (this.monorepo)
|
|
@@ -236,7 +344,7 @@ export class Repository extends Package {
|
|
|
236
344
|
// than refusing it - so the scope has to survive one too.
|
|
237
345
|
const name = pkg.name ?? '';
|
|
238
346
|
/** Splitting `@scope/name` is npm's convention, not a universal - the provider decides. */
|
|
239
|
-
const { scope: nameScope, unscopedName } = Manifest.splitName(pkg.dirname, name);
|
|
347
|
+
const { scope: nameScope, unscopedName } = Manifest.splitName(this.app, pkg.dirname, name);
|
|
240
348
|
const scope = {
|
|
241
349
|
name,
|
|
242
350
|
scope: nameScope,
|
|
@@ -384,26 +492,106 @@ export class Repository extends Package {
|
|
|
384
492
|
* otherwise: the plugins that know what a package is are named in the config file this step
|
|
385
493
|
* is looking for.
|
|
386
494
|
* 2. **Load the plugins** the root's config names, which registers their workspace providers
|
|
387
|
-
* (
|
|
495
|
+
* (their commands arrive through `.rmanrc "commands"`, which `cli.ts` reads).
|
|
388
496
|
* 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
|
|
389
497
|
* the one package.
|
|
390
498
|
*
|
|
391
499
|
* **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
|
|
392
500
|
* boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
|
|
393
|
-
* takes `plugins: ['
|
|
501
|
+
* takes `plugins: ['node']` - or detection reading the directory as a Node one - for it to be
|
|
502
|
+
* read as a workspace at all.
|
|
394
503
|
*/
|
|
395
504
|
static async create(root, options) {
|
|
396
505
|
const from = root || process.cwd();
|
|
397
506
|
const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
|
|
507
|
+
/**
|
|
508
|
+
* One application per repository, made here unless the caller brought one.
|
|
509
|
+
*
|
|
510
|
+
* `runCli` passes its own so that `--log-level` reaches the logger; a spec that only wants a
|
|
511
|
+
* repository lets this make one, which is also what keeps two repositories in a single process
|
|
512
|
+
* from sharing anything - the thing that used to need five `clear*()` calls before every test.
|
|
513
|
+
*/
|
|
514
|
+
const app = options?.app ?? new RmanApplication();
|
|
398
515
|
/** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
|
|
399
516
|
* package list (which does not exist yet) nor expression interpolation. */
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
517
|
+
const declared = await readDirConfig(rootDir);
|
|
518
|
+
/**
|
|
519
|
+
* **What this repository looks like, when nothing said** - the other half of shipping the
|
|
520
|
+
* built-ins in the box. See `detectBuiltin`.
|
|
521
|
+
*
|
|
522
|
+
* **Two conditions, and the second is the one that is easy to miss.** The config declaring no
|
|
523
|
+
* `plugins` is not the same as the *repository* having no technology: a programmatic caller -
|
|
524
|
+
* and every spec in this suite - registers one straight onto the application without writing a
|
|
525
|
+
* config at all. Guessing on top of that registers a second technology, and the first provider
|
|
526
|
+
* that recognizes a directory decides whether it holds a package. Measured with only the config
|
|
527
|
+
* condition: ten specs changed answer, seven of them about config cascading and three about
|
|
528
|
+
* `publish`'s flags.
|
|
529
|
+
*
|
|
530
|
+
* **`platforms`, not `plugins`, and the difference is what detection produces.** What would be
|
|
531
|
+
* added here is a *platform*, so what must not already be there is a platform - a plugin that
|
|
532
|
+
* only registers a command says nothing about which directories hold packages, and letting it
|
|
533
|
+
* suppress the guess would leave a Node repository undetected for having added a command.
|
|
534
|
+
*
|
|
535
|
+
* **A root `platform` counts as having said something**, like `plugins: []` does. Detection
|
|
536
|
+
* exists for a repository that stated nothing; one naming its technology has stated the very
|
|
537
|
+
* thing detection would be guessing at, and guessing anyway would register a *second* platform
|
|
538
|
+
* beside the declared one - which then competes for every directory the declaration did not
|
|
539
|
+
* cover.
|
|
540
|
+
*
|
|
541
|
+
* Decided **once**, here, and handed to every read that has to agree - `readDirConfig` has no
|
|
542
|
+
* business knowing about an application.
|
|
543
|
+
*/
|
|
544
|
+
const saidSomething = declared.plugins !== undefined || declared.platform !== undefined;
|
|
545
|
+
const detected = !saidSomething && app.platforms.size === 0 ? await detectBuiltin(rootDir) : undefined;
|
|
546
|
+
const rootConfig = detected ? await readDirConfig(rootDir, { inject: detected }) : declared;
|
|
547
|
+
/**
|
|
548
|
+
* **Said out loud, because a guess the reader cannot see is one they cannot correct.**
|
|
549
|
+
*
|
|
550
|
+
* To **stderr**, not through `app.logger`: that writes with `console.log`, and `rman list
|
|
551
|
+
* --json` has to stay a parseable document on stdout - measured, its output is pure JSON, and
|
|
552
|
+
* one line of prose in front of it breaks every `| jq`. The same reason `--help`'s degradation
|
|
553
|
+
* notice goes to stderr. Not at `silent`, where the caller asked for no narration.
|
|
554
|
+
*/
|
|
555
|
+
if (detected && app.logger.level !== 'silent') {
|
|
556
|
+
const line = `${detected.name} repository detected (${detected.because}) - ` +
|
|
557
|
+
`write \`plugins: ['${detected.name}']\` in .rmanrc to state it, or \`plugins: []\` for none.`;
|
|
558
|
+
console.error(process.stderr.isTTY ? colors.gray(line) : line);
|
|
559
|
+
}
|
|
560
|
+
await loadPlugins(app, rootConfig);
|
|
561
|
+
/**
|
|
562
|
+
* **Its own cache, deliberately not shared with `_resolveConfigs`'.**
|
|
563
|
+
*
|
|
564
|
+
* Both cache `readDirConfig` per directory, and the two reads of the *root* are not the same
|
|
565
|
+
* read: `_resolveConfigs` passes `detectedBuiltin` as `inject` and this one passes nothing,
|
|
566
|
+
* since a built-in's contribution has no bearing on which platform a directory declares. The
|
|
567
|
+
* cache is keyed by directory alone, so one shared map would hand whichever ran first to the
|
|
568
|
+
* other - and for the root that is a config with or without the whole node built-in merged in.
|
|
569
|
+
*
|
|
570
|
+
* The cost is one extra read per directory in the chain, on a repository that is about to read
|
|
571
|
+
* every one of them again per package anyway.
|
|
572
|
+
*/
|
|
573
|
+
const platformCache = new Map();
|
|
574
|
+
/**
|
|
575
|
+
* **The walk**, which is where the package list comes from now - descending from the root,
|
|
576
|
+
* asking each directory's own technology where its children are. `Workspace.resolve` asked the
|
|
577
|
+
* root once, through whichever platform recognized it first; the tree is the shape discovery
|
|
578
|
+
* actually has, and it is what makes a nested package of another technology findable at all.
|
|
579
|
+
*/
|
|
580
|
+
const tree = await Workspace.walk(app, rootDir, {
|
|
581
|
+
deep: options?.deep,
|
|
582
|
+
declared: dir => declaredPlatformAt(app, rootDir, dir, platformCache),
|
|
583
|
+
});
|
|
584
|
+
const nodes = Workspace.flatten(tree);
|
|
585
|
+
const packages = nodes.map(node => new Package(node.dirname, app, node.platform));
|
|
586
|
+
const repo = new Repository(app, tree.dirname, packages.length > 0, packages, from, tree.platform);
|
|
587
|
+
repo._linkPackages(tree, nodes, packages);
|
|
588
|
+
/** Before `_resolveConfigs`, because a `"[glob]"` block is matched against the selector - so
|
|
589
|
+
* the addresses have to be settled before anything is resolved by them. */
|
|
590
|
+
await repo._assignSelectors(rootDir, platformCache);
|
|
591
|
+
/** The application is what the plugins registered into a moment ago; from here on it can hand
|
|
592
|
+
* out services, which need the repository to work on. */
|
|
593
|
+
repo.detectedBuiltin = detected;
|
|
594
|
+
app.attachRepository(repo);
|
|
407
595
|
return Repository._init(repo);
|
|
408
596
|
}
|
|
409
597
|
/** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
|
|
@@ -415,3 +603,64 @@ export class Repository extends Package {
|
|
|
415
603
|
return repo;
|
|
416
604
|
}
|
|
417
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* The platform `dir` declares in its cascaded `.rmanrc "platform"`, loaded if it has to be -
|
|
608
|
+
* `undefined` when the directory declares none, so the walk falls back to its guess.
|
|
609
|
+
*
|
|
610
|
+
* **The unmarked cascade only**, which is what `resolveConfig` gives with no package name: a
|
|
611
|
+
* selector matches a package *name*, and this runs while the packages are still being found - the
|
|
612
|
+
* name is not known yet, and it is read *from* a manifest whose reader this key decides. So
|
|
613
|
+
* `"[/]"` reaches the root (its directory is the repository root, which needs no name) and a glob
|
|
614
|
+
* block contributes nothing here, which is the honest answer rather than a half-applied one.
|
|
615
|
+
*/
|
|
616
|
+
async function declaredPlatformAt(app, rootDir, dir, cache) {
|
|
617
|
+
const config = await resolveConfig(rootDir, dir, cache);
|
|
618
|
+
const declared = config.platform;
|
|
619
|
+
if (declared === undefined)
|
|
620
|
+
return undefined;
|
|
621
|
+
if (typeof declared !== 'string' || !declared.trim()) {
|
|
622
|
+
throw new Error(`"platform" takes a platform's name - ${originOf(config)} gave ${typeof declared}.`);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* **An expression is refused rather than read as a literal.** `platform` is consulted before any
|
|
626
|
+
* package exists - it is what decides what a package *is* - so there is no `pkg` for an
|
|
627
|
+
* expression to be about, and `interpolateConfig` runs long afterwards. Passed through, a
|
|
628
|
+
* `${{ }}` here reached the lookup below as the raw text and failed as an unknown platform name,
|
|
629
|
+
* which sends the reader to check their `plugins`.
|
|
630
|
+
*/
|
|
631
|
+
if (declared.includes('${{')) {
|
|
632
|
+
throw new Error(`"platform" cannot be an expression (${originOf(config)}) - it is read while ` +
|
|
633
|
+
`the packages are still being found, so there is no package for one to be about. Write the ` +
|
|
634
|
+
`name, and use a package's own ".rmanrc" where the answer differs.`);
|
|
635
|
+
}
|
|
636
|
+
const registered = [...app.platforms].find(p => p.name === declared);
|
|
637
|
+
if (registered)
|
|
638
|
+
return registered;
|
|
639
|
+
/**
|
|
640
|
+
* **A built-in is loaded on the strength of being named**, from any level - `platform: 'node'` is
|
|
641
|
+
* enough, and it is what a repository holding one Node package among others writes.
|
|
642
|
+
*
|
|
643
|
+
* `platform()` and not `contribute()`, and that split is why the two halves exist: what arrives
|
|
644
|
+
* is the technology alone. Commands and publish targets come from root `plugins`, because they
|
|
645
|
+
* are repository-wide - a Node package inside a Cargo repository wants npm's manifest read, not
|
|
646
|
+
* an `rman clean` that would sweep the whole tree.
|
|
647
|
+
*/
|
|
648
|
+
const { BUILTIN_PLUGINS, builtinPluginNames } = await import('../plugins/builtins.js');
|
|
649
|
+
const builtin = BUILTIN_PLUGINS[declared];
|
|
650
|
+
if (builtin) {
|
|
651
|
+
const platform = builtin.platform();
|
|
652
|
+
registerPlugin(app, platform);
|
|
653
|
+
return platform;
|
|
654
|
+
}
|
|
655
|
+
const have = [...app.platforms].map(p => p.name).filter(Boolean);
|
|
656
|
+
throw new Error(`"platform" names "${declared}" (${originOf(config)}), which is not a platform ` +
|
|
657
|
+
`this repository has. ${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
|
|
658
|
+
`rman ships ${builtinPluginNames().join(', ')}; anything else arrives through "plugins".`);
|
|
659
|
+
}
|
|
660
|
+
/** Which file the `platform` key came from, for an error that can be acted on - `mergeConfig`
|
|
661
|
+
* records one per key under `ORIGINS`, and a config is merged from a directory's own four forms,
|
|
662
|
+
* an `extends` base and one layer per directory before anything reads it. */
|
|
663
|
+
function originOf(config) {
|
|
664
|
+
const origins = config[ORIGINS];
|
|
665
|
+
return origins?.platform ? `in "${origins.platform}"` : 'the "platform" key';
|
|
666
|
+
}
|
package/core/resolve-target.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function resolveConfigTarget(target: string, from: string, label:
|
|
|
15
15
|
* repository cannot resolve it.
|
|
16
16
|
*
|
|
17
17
|
* A globally installed rman's siblings *are* the globally installed packages, so this is what makes
|
|
18
|
-
* `rman ci` work on a fresh clone - the command
|
|
18
|
+
* `rman ci` work on a fresh clone - the command came from `rman-node` then, and `ci` exists to create
|
|
19
19
|
* the very `node_modules` the plugin would otherwise have to be found in. Measured: with both
|
|
20
20
|
* installed globally, a clone answered `"plugins" target "rman-node" could not be resolved ... is
|
|
21
21
|
* it installed in this repository?`, which was true and useless.
|
package/core/resolve-target.js
CHANGED
|
@@ -39,7 +39,7 @@ export function resolveConfigTarget(target, from, label) {
|
|
|
39
39
|
* repository cannot resolve it.
|
|
40
40
|
*
|
|
41
41
|
* A globally installed rman's siblings *are* the globally installed packages, so this is what makes
|
|
42
|
-
* `rman ci` work on a fresh clone - the command
|
|
42
|
+
* `rman ci` work on a fresh clone - the command came from `rman-node` then, and `ci` exists to create
|
|
43
43
|
* the very `node_modules` the plugin would otherwise have to be found in. Measured: with both
|
|
44
44
|
* installed globally, a clone answered `"plugins" target "rman-node" could not be resolved ... is
|
|
45
45
|
* it installed in this repository?`, which was true and useless.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Logger } from '../utils/logger.js';
|
|
2
|
+
import type { RmanApplication } from './application.js';
|
|
3
|
+
import type { Repository } from './repository.js';
|
|
4
|
+
/**
|
|
5
|
+
* **The one service shape.** Everything that does work on a repository is a class extending this,
|
|
6
|
+
* constructed once per `RmanApplication` and reached through `app.getService(name)`.
|
|
7
|
+
*
|
|
8
|
+
* rman's services were `export namespace` blocks, and the split between those and the two classes
|
|
9
|
+
* (`VersionPlanService`, `VersionScheme`) was historical rather than principled: whatever needed to
|
|
10
|
+
* be *extended* became a class, and the rest stayed a namespace. Three consequences, all of them
|
|
11
|
+
* measured in this repository:
|
|
12
|
+
*
|
|
13
|
+
* - **A namespace that needs state can only have global state.** `RunService` is a namespace and
|
|
14
|
+
* acquired three module-level variables - `stepSources` and two "already warned" sets - which is
|
|
15
|
+
* why a warning printed for one repository was suppressed for the next one in the same process.
|
|
16
|
+
* - **A namespace cannot be extended.** The two seams a plugin has to specialize
|
|
17
|
+
* (`NodeVersionPlanService extends VersionPlanService`) are precisely the two that are classes.
|
|
18
|
+
* - **A namespace has to be stubbed by mutating the module**, and CLAUDE.md records that failing:
|
|
19
|
+
* a spec that captured a core function at module scope and restored it put the *un-augmented*
|
|
20
|
+
* version back for the rest of the process and broke a spec two files away. An instance is
|
|
21
|
+
* rebuilt per application, so a stub dies with it.
|
|
22
|
+
*
|
|
23
|
+
* **`repository` is no longer a parameter.** Nearly every service method took it first; it now
|
|
24
|
+
* comes from the application, which is what made the argument redundant in the first place.
|
|
25
|
+
*
|
|
26
|
+
* Pure functions of their arguments stay plain exported functions rather than becoming services -
|
|
27
|
+
* `ConventionalCommitsService.parseSubject('feat: x')` parses a string and will never hold state,
|
|
28
|
+
* and making it `app.getService('conventionalCommits').parseSubject(...)` would be ceremony that
|
|
29
|
+
* also puts an application between a caller and a parser. The line is whether it needs the
|
|
30
|
+
* repository.
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class Service {
|
|
33
|
+
protected readonly app: RmanApplication;
|
|
34
|
+
constructor(app: RmanApplication);
|
|
35
|
+
protected get repository(): Repository;
|
|
36
|
+
protected get logger(): Logger;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every service the application can hand out, by name.
|
|
40
|
+
*
|
|
41
|
+
* Declaration-merged, like `RmanConfigKeys`: the core declares its own here, and a plugin adds its
|
|
42
|
+
* own from its own package with `declare module 'rman'`. That is what keeps `getService` typed -
|
|
43
|
+
* a bare `Map<string, Service>` would need a cast at every call and would turn a misspelled name
|
|
44
|
+
* into a runtime failure instead of a compile error.
|
|
45
|
+
*/
|
|
46
|
+
export interface ServiceMap {
|
|
47
|
+
}
|
|
48
|
+
/** How a service is built when the application first needs it. */
|
|
49
|
+
export type ServiceFactory<T> = (app: RmanApplication) => T;
|
package/core/service.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The one service shape.** Everything that does work on a repository is a class extending this,
|
|
3
|
+
* constructed once per `RmanApplication` and reached through `app.getService(name)`.
|
|
4
|
+
*
|
|
5
|
+
* rman's services were `export namespace` blocks, and the split between those and the two classes
|
|
6
|
+
* (`VersionPlanService`, `VersionScheme`) was historical rather than principled: whatever needed to
|
|
7
|
+
* be *extended* became a class, and the rest stayed a namespace. Three consequences, all of them
|
|
8
|
+
* measured in this repository:
|
|
9
|
+
*
|
|
10
|
+
* - **A namespace that needs state can only have global state.** `RunService` is a namespace and
|
|
11
|
+
* acquired three module-level variables - `stepSources` and two "already warned" sets - which is
|
|
12
|
+
* why a warning printed for one repository was suppressed for the next one in the same process.
|
|
13
|
+
* - **A namespace cannot be extended.** The two seams a plugin has to specialize
|
|
14
|
+
* (`NodeVersionPlanService extends VersionPlanService`) are precisely the two that are classes.
|
|
15
|
+
* - **A namespace has to be stubbed by mutating the module**, and CLAUDE.md records that failing:
|
|
16
|
+
* a spec that captured a core function at module scope and restored it put the *un-augmented*
|
|
17
|
+
* version back for the rest of the process and broke a spec two files away. An instance is
|
|
18
|
+
* rebuilt per application, so a stub dies with it.
|
|
19
|
+
*
|
|
20
|
+
* **`repository` is no longer a parameter.** Nearly every service method took it first; it now
|
|
21
|
+
* comes from the application, which is what made the argument redundant in the first place.
|
|
22
|
+
*
|
|
23
|
+
* Pure functions of their arguments stay plain exported functions rather than becoming services -
|
|
24
|
+
* `ConventionalCommitsService.parseSubject('feat: x')` parses a string and will never hold state,
|
|
25
|
+
* and making it `app.getService('conventionalCommits').parseSubject(...)` would be ceremony that
|
|
26
|
+
* also puts an application between a caller and a parser. The line is whether it needs the
|
|
27
|
+
* repository.
|
|
28
|
+
*/
|
|
29
|
+
export class Service {
|
|
30
|
+
app;
|
|
31
|
+
constructor(app) {
|
|
32
|
+
this.app = app;
|
|
33
|
+
}
|
|
34
|
+
get repository() {
|
|
35
|
+
return this.app.repository;
|
|
36
|
+
}
|
|
37
|
+
get logger() {
|
|
38
|
+
return this.app.logger;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/core/version-scheme.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type ChangeKind = 'fix' | 'feature' | 'breaking';
|
|
|
16
16
|
* "is this version on the registry yet", `"workspace:"` range rewriting all assume versions that
|
|
17
17
|
* can be ordered and incremented. This is the seam for an ecosystem that numbers differently (PEP
|
|
18
18
|
* 440, a date-based scheme, a build counter), and it is a *meaning* seam rather than a storage one:
|
|
19
|
-
* `
|
|
19
|
+
* `Plugin`'s manifest members answers "where is the version written", this answers "what does the next one
|
|
20
20
|
* look like".
|
|
21
21
|
*
|
|
22
22
|
* **`bumpNames` and `bumpFor` are the interesting part.** `fix:` -> patch, `feat:` -> minor,
|
|
@@ -64,6 +64,20 @@ export declare abstract class VersionScheme {
|
|
|
64
64
|
}): string;
|
|
65
65
|
/** Is this version a preview rather than a release? `github-release` reads it. */
|
|
66
66
|
abstract isPrerelease(version: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Which prerelease line this version belongs to - `'beta'` for `2.0.0-beta.1` - or `undefined`
|
|
69
|
+
* when it is not a preview, or is one with no identifier to name (`2.0.0-1`).
|
|
70
|
+
*
|
|
71
|
+
* **The one thing a preview needs beyond "is it one", and it is a *name*, which is why it is
|
|
72
|
+
* here rather than read out of the version with a regex by whoever wants it.** npm's publish
|
|
73
|
+
* target derives its dist-tag from this, so a beta lands on `beta` instead of on `latest`; the
|
|
74
|
+
* identifier is written in the version itself, so that is a reading rather than a guess.
|
|
75
|
+
*
|
|
76
|
+
* Implemented, not abstract, and returning `undefined` by default: a scheme whose previews have
|
|
77
|
+
* no name (or which has no previews at all) is answering honestly, and the caller's job is to
|
|
78
|
+
* say so rather than invent one. `SemverScheme` overrides it.
|
|
79
|
+
*/
|
|
80
|
+
prereleaseId(version: string): string | undefined;
|
|
67
81
|
/**
|
|
68
82
|
* The highest of `versions` - a group's current version is the highest among its members, and a
|
|
69
83
|
* monorepo root's release identity the highest among the groups.
|
|
@@ -111,6 +125,14 @@ export declare class SemverScheme extends VersionScheme {
|
|
|
111
125
|
preid?: string;
|
|
112
126
|
}): string;
|
|
113
127
|
isPrerelease(version: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* semver's first prerelease identifier, when it is a word: `2.0.0-beta.1` -> `'beta'`.
|
|
130
|
+
*
|
|
131
|
+
* **`undefined` for a numeric-only prerelease** (`2.0.0-1`, whose identifiers are `[1]`), because
|
|
132
|
+
* there is no name there to use - and a caller turning that into a dist-tag called `1` would be
|
|
133
|
+
* inventing one. Same answer for a release, which has no prerelease at all.
|
|
134
|
+
*/
|
|
135
|
+
prereleaseId(version: string): string | undefined;
|
|
114
136
|
}
|
|
115
137
|
/**
|
|
116
138
|
* The default, and what every package gets unless something says otherwise - so nothing about
|