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/package.js
CHANGED
|
@@ -4,11 +4,11 @@ import { semverScheme } from './version-scheme.js';
|
|
|
4
4
|
export class Package {
|
|
5
5
|
dirname;
|
|
6
6
|
/**
|
|
7
|
-
* This package's identity, read through whichever `
|
|
7
|
+
* This package's identity, read through whichever `Plugin`'s manifest members recognizes its directory.
|
|
8
8
|
*
|
|
9
9
|
* There is no `json` here any more, and that is the point: `package.json` is npm's answer to
|
|
10
10
|
* "where is a package's name and version written", not rman's. `manifest.raw` is still the whole
|
|
11
|
-
* document for a command that knows its own ecosystem - `
|
|
11
|
+
* document for a command that knows its own ecosystem - the `node` built-in reads `scripts` and
|
|
12
12
|
* `publishConfig` off it - but the core only ever touches `name`, `version` and `private`.
|
|
13
13
|
*/
|
|
14
14
|
manifest;
|
|
@@ -22,24 +22,30 @@ export class Package {
|
|
|
22
22
|
* whatever the ecosystem, and comparing by identity removes a lookup from every consumer.
|
|
23
23
|
*/
|
|
24
24
|
dependencies = [];
|
|
25
|
-
/** Effective rman config for this package, cascaded from the repository root, with every
|
|
26
|
-
* `${{ ... }}` expression already evaluated. */
|
|
27
|
-
config = {};
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Effective rman config for this package, cascaded from the repository root, with every
|
|
27
|
+
* `${{ ... }}` expression already evaluated.
|
|
31
28
|
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
34
|
-
* it
|
|
29
|
+
* **`ResolvedConfig`, not `RmanConfig`, and that is the whole two-view split in one line.**
|
|
30
|
+
* `RmanConfig` is what an *author* writes, where a value may be a function; this is what a
|
|
31
|
+
* *reader* gets, where it has already been called. Derived from the one the author writes, so
|
|
32
|
+
* there is no second type to keep in step - see `Resolved`.
|
|
35
33
|
*/
|
|
36
|
-
|
|
34
|
+
config = {};
|
|
37
35
|
/**
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* The packages whose directories sit directly inside this one - **the tree edge**, in the order
|
|
37
|
+
* the platform that claimed this directory gave them.
|
|
38
|
+
*
|
|
39
|
+
* This is the shape discovery actually has, and it used to be flattened away: a provider was
|
|
40
|
+
* asked once at the repository root and returned one list, so a package nested inside another was
|
|
41
|
+
* only reconstructible by comparing path prefixes (which `_linkPackages` and `currentPackage`
|
|
42
|
+
* both did, separately). `Workspace.walk` descends, so the containment is known as it is found -
|
|
43
|
+
* and `Repository.packages` is now this tree flattened rather than the other way round.
|
|
44
|
+
*
|
|
45
|
+
* Empty for a leaf, and for every package in an ordinary flat monorepo - where the root's
|
|
46
|
+
* `children` is the whole member list.
|
|
41
47
|
*/
|
|
42
|
-
|
|
48
|
+
children = [];
|
|
43
49
|
/**
|
|
44
50
|
* How this package's versions are numbered - `pkg.versionScheme.next(pkg.version, 'minor')`.
|
|
45
51
|
*
|
|
@@ -56,26 +62,85 @@ export class Package {
|
|
|
56
62
|
* changed (a commit's path list). Empty when no provider recognized this directory. */
|
|
57
63
|
manifestFileName;
|
|
58
64
|
/**
|
|
59
|
-
* **
|
|
60
|
-
*
|
|
65
|
+
* **What addresses this package**: what a `.rmanrc "[glob]"` block and `--scope`/`--ignore` match
|
|
66
|
+
* against, and what must be unique within the repository.
|
|
67
|
+
*
|
|
68
|
+
* Three sources, first one that answers:
|
|
69
|
+
*
|
|
70
|
+
* 1. the package's own `.rmanrc "name"` - the repository assigning one;
|
|
71
|
+
* 2. its platform's `manifestProvider.selector`;
|
|
72
|
+
* 3. the manifest's own `name`, which is the default that answer falls back to.
|
|
73
|
+
*
|
|
74
|
+
* **Not `name`, and the split is the point.** `name` is what the package calls itself, read from
|
|
75
|
+
* its manifest, and it is an *ecosystem's* promise that such a thing exists and identifies the
|
|
76
|
+
* package - npm's promise, not rman's. Selectors matched `pkg.name` until this existed, so a
|
|
77
|
+
* repository whose technology has no name concept had packages it could not address at all, and
|
|
78
|
+
* one whose names are not unique (or are import paths) could only address them badly. They
|
|
79
|
+
* coincide for every Node repository, which is why nothing had to change for one.
|
|
80
|
+
*
|
|
81
|
+
* **The root's is nearly unused, deliberately.** A glob never matches the root and `"[/]"` needs
|
|
82
|
+
* no name - the root is addressed structurally, which is the whole reason it is `/`. It still has
|
|
83
|
+
* one so that nothing has to special-case it.
|
|
84
|
+
*
|
|
85
|
+
* Assigned by `Repository.create`, which is what has the config; a bare `new Package(dir, app)`
|
|
86
|
+
* gets the manifest's name, which is sources 2 and 3 with no config to consult.
|
|
87
|
+
*/
|
|
88
|
+
selector;
|
|
89
|
+
/**
|
|
90
|
+
* **The technology this package belongs to** - the platform whose manifest provider claimed the
|
|
91
|
+
* directory, or `basePlatform` when none did.
|
|
92
|
+
*
|
|
93
|
+
* Per *package*, not per repository: the question is asked per directory, so a polyglot monorepo
|
|
94
|
+
* can hold a `node` package beside a `cargo` one and a command sweeping `getPackages()` can tell
|
|
95
|
+
* them apart. It is also what carries the rest of the technology's answers - where its binaries
|
|
96
|
+
* live, where its scripts come from, how its releases are planned - so anything that used to walk
|
|
97
|
+
* four separate registries asking "is this yours?" now asks the package it already has.
|
|
98
|
+
*/
|
|
99
|
+
platform;
|
|
100
|
+
/**
|
|
101
|
+
* **Which ecosystem this package belongs to** - `'node'` for one the `node` built-in read. Empty when
|
|
102
|
+
* no stack claimed the directory.
|
|
61
103
|
*
|
|
62
104
|
* The escape hatch for code that legitimately knows one technology: `if (pkg.provider === 'node')`
|
|
63
|
-
* before reaching into `manifest.raw` for something only npm has.
|
|
64
|
-
* repository, because `Manifest.read` asks per directory - a polyglot monorepo can hold a `node`
|
|
65
|
-
* package beside a `cargo` one, and a command sweeping over `getPackages()` has to be able to
|
|
66
|
-
* tell.
|
|
105
|
+
* before reaching into `manifest.raw` for something only npm has.
|
|
67
106
|
*
|
|
68
107
|
* Not a union type on purpose: the set of ecosystems is whatever the repository's `plugins`
|
|
69
108
|
* contribute, so narrowing it here would mean the core listing plugins it cannot know about.
|
|
70
109
|
*/
|
|
71
|
-
provider
|
|
72
|
-
|
|
110
|
+
get provider() {
|
|
111
|
+
return this.platform.name;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* **Takes the application but does not keep it.** A package needs it once, to find out which
|
|
115
|
+
* technology claims its directory; afterwards it holds only data, so nothing that has a package
|
|
116
|
+
* can reach a service through it. Data down, behaviour up - the work belongs to services, and a
|
|
117
|
+
* piece of code holding only a package is not doing any.
|
|
118
|
+
*
|
|
119
|
+
* It also keeps the package out of every spread and serialization the application would
|
|
120
|
+
* otherwise be dragged into: `{...pkg}` and `pkg.manifest.raw` are both real, and an `app` field
|
|
121
|
+
* here would carry the whole world into them.
|
|
122
|
+
*/
|
|
123
|
+
constructor(dirname, app,
|
|
124
|
+
/**
|
|
125
|
+
* The technology that claimed this directory, when the caller already knows - which
|
|
126
|
+
* `Repository.create` does, because the walk that found the directory is what established it.
|
|
127
|
+
*
|
|
128
|
+
* Optional so a bare `new Package(dir, app)` still works: that is what the fixtures build, and
|
|
129
|
+
* it is also the honest fallback for anyone constructing a package outside a walk. It costs one
|
|
130
|
+
* search of the registry (`app.platformFor`), which is what *every* construction used to do.
|
|
131
|
+
*/
|
|
132
|
+
platform) {
|
|
73
133
|
this.dirname = dirname;
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
134
|
+
this.platform = platform ?? app.platformFor(dirname);
|
|
135
|
+
this._readManifest();
|
|
136
|
+
this.selector = this.platformSelector();
|
|
137
|
+
}
|
|
138
|
+
/** What this package's **platform** says addresses it, before any config is consulted - the
|
|
139
|
+
* manifest's own name when the platform has no opinion, and the directory when it has no name
|
|
140
|
+
* either (which is unique among siblings and is all there is to go on). */
|
|
141
|
+
platformSelector() {
|
|
142
|
+
const asked = this.platform.manifestProvider.selector?.(this.manifest, this.dirname);
|
|
143
|
+
return asked || this.manifest.name || this.basename;
|
|
79
144
|
}
|
|
80
145
|
get basename() {
|
|
81
146
|
return path.basename(this.dirname);
|
|
@@ -89,18 +154,45 @@ export class Package {
|
|
|
89
154
|
get isPrivate() {
|
|
90
155
|
return !!this.manifest.private;
|
|
91
156
|
}
|
|
92
|
-
/**
|
|
93
|
-
*
|
|
157
|
+
/**
|
|
158
|
+
* Whether this is the repository's **own root package** - what `--scope /` and `.rmanrc`'s
|
|
159
|
+
* `"[/]"` both select.
|
|
160
|
+
*
|
|
161
|
+
* **By directory, and deliberately not by name or by identity.** The documented rule is that the
|
|
162
|
+
* root package is the one whose directory *is* the repository root, because a name can be
|
|
163
|
+
* anything - and identity (`this === repository.rootPackage`) is not equivalent either:
|
|
164
|
+
* `Repository extends Package` while holding a separate `rootPackage` instance for the same
|
|
165
|
+
* directory, so a comparison by reference answers `false` for one of the two objects that are
|
|
166
|
+
* both, truthfully, the root.
|
|
167
|
+
*
|
|
168
|
+
* `false` before `Repository.create` has assigned `repository` - a bare `new Package(dir, app)`
|
|
169
|
+
* (which the test fixtures build) belongs to no repository yet, so there is no root for it to be.
|
|
170
|
+
*/
|
|
171
|
+
get isRoot() {
|
|
172
|
+
const repository = this.repository;
|
|
173
|
+
return !!repository && path.resolve(this.dirname) === path.resolve(repository.dirname);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Re-reads from disk - for a command that has just written the manifest itself and wants the
|
|
177
|
+
* package to agree with the file again.
|
|
178
|
+
*
|
|
179
|
+
* Through **its own** technology's provider, like the constructor: the package already knows
|
|
180
|
+
* which platform claimed it, and a second opinion on a re-read was never wanted.
|
|
181
|
+
*/
|
|
94
182
|
reloadManifest() {
|
|
95
|
-
|
|
96
|
-
this.manifest = manifest;
|
|
97
|
-
this.versionScheme = versionScheme;
|
|
98
|
-
this.manifestFileName = fileName ? path.join(this.dirname, fileName) : '';
|
|
99
|
-
this.provider = provider;
|
|
183
|
+
this._readManifest();
|
|
100
184
|
return this.manifest;
|
|
101
185
|
}
|
|
102
186
|
/** Writes the current manifest back through its provider. */
|
|
103
187
|
writeManifest() {
|
|
104
|
-
|
|
188
|
+
this.platform.manifestProvider.write(this.dirname, this.manifest);
|
|
189
|
+
}
|
|
190
|
+
/** The three fields a manifest read sets, in one place - so construction and `reloadManifest`
|
|
191
|
+
* cannot disagree about what "reading the manifest" means. */
|
|
192
|
+
_readManifest() {
|
|
193
|
+
const { manifest, versionScheme, fileName } = Manifest.read(this.platform, this.dirname);
|
|
194
|
+
this.manifest = manifest;
|
|
195
|
+
this.versionScheme = versionScheme;
|
|
196
|
+
this.manifestFileName = fileName ? path.join(this.dirname, fileName) : '';
|
|
105
197
|
}
|
|
106
198
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
2
|
+
import type { RmanApplication } from './application.js';
|
|
3
|
+
import { type Platform, type Plugin } from './plugin.js';
|
|
4
|
+
/** The `.rmanrc` key naming plugin packages to load. */
|
|
5
|
+
export declare const PLUGINS_KEY = "plugins";
|
|
6
|
+
/**
|
|
7
|
+
* The repository's plugins and publish targets, registered onto `app`.
|
|
8
|
+
*
|
|
9
|
+
* **Three keys share one shape** - `plugins`, `publishTargets` and `commands` each take an
|
|
10
|
+
* instance or a **glob** naming `.js` modules that `export default` one, and each appends rather
|
|
11
|
+
* than replaces. Two of them are read here; `commands` is read by `cli.ts`, and the split is not
|
|
12
|
+
* arbitrary:
|
|
13
|
+
*
|
|
14
|
+
* - **`plugins` and `publishTargets` are root-level, because they have to be.** This runs inside
|
|
15
|
+
* `Repository.create` *before* the packages are known, since a plugin's `getWorkspace` is what
|
|
16
|
+
* finds them - so there are no package configs to read yet. A `plugins` entry in a package's own
|
|
17
|
+
* `.rmanrc` is never seen, which has always been true and is now true for the same reason.
|
|
18
|
+
* - **`commands` is read at any level**, after the packages exist, which is why `cli.ts` owns it:
|
|
19
|
+
* it already holds the whole command pipeline - the `.rman/` default, de-duplication by name,
|
|
20
|
+
* and the built-in shadow check. Reading it here as well would register every config-declared
|
|
21
|
+
* command twice, and yargs does not survive that.
|
|
22
|
+
*
|
|
23
|
+
* **Order within this function is load-bearing**: plugins first, then targets. rman's own
|
|
24
|
+
* `publish` builds its `--target` choices from `app.publishTargets` when its factory runs, and a
|
|
25
|
+
* target contributed by a plugin's `init` has to be there before the list is read.
|
|
26
|
+
*
|
|
27
|
+
* **A contribution that cannot be loaded throws**, unlike a broken `.rman/*.mjs`, which is warned
|
|
28
|
+
* about and skipped. The consequence differs: a skipped local command affects only itself, while a
|
|
29
|
+
* missing plugin silently removes commands the repository is built around - `rman publish` would
|
|
30
|
+
* simply not exist, and "not a known command" sends the reader looking in the wrong place.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadPlugins(app: RmanApplication, rootConfig: RmanConfig): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* **Puts one plugin onto an application** - what `plugins` does with an entry once it has been
|
|
35
|
+
* found and vetted, and the only place that knows how.
|
|
36
|
+
*
|
|
37
|
+
* **A bare `Platform` is sugar**, which is what almost every entry is - a plugin that provides one
|
|
38
|
+
* technology and nothing else. Normalized here, so nothing downstream deals in two shapes.
|
|
39
|
+
*
|
|
40
|
+
* **Exported because a spec must not reimplement it.** The registration is two registries plus a
|
|
41
|
+
* planner assignment, and a fixture writing that out by hand is a second implementation that
|
|
42
|
+
* drifts - which is exactly how a fixture ends up proving the core works when it does not. A spec
|
|
43
|
+
* brings its own technology through this, the same door a config's does.
|
|
44
|
+
*
|
|
45
|
+
* **`init` is not called here, and that is why this returns the normalized plugin.** An `init` runs
|
|
46
|
+
* once, when a *config* brought the plugin in, and it may be asynchronous - so it belongs to the
|
|
47
|
+
* loader above rather than to a function a fixture calls synchronously while building an
|
|
48
|
+
* application.
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerPlugin(app: RmanApplication, entry: Plugin | Platform): Plugin;
|
|
51
|
+
/**
|
|
52
|
+
* A plugin's `CustomCommand`, checked the same way a `.rman/*.mjs` one is - the name it answers to
|
|
53
|
+
* comes from its own `command` string, since a plugin has no file name to fall back on.
|
|
54
|
+
*
|
|
55
|
+
* Exported because `cli.ts` runs the identical checks on what a *declarative* command's factory
|
|
56
|
+
* returns, and they must not drift: a plugin written in JavaScript reaches both forms with no type
|
|
57
|
+
* checker in the way.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkCustomCommand<T extends {
|
|
60
|
+
command?: string;
|
|
61
|
+
describe?: unknown;
|
|
62
|
+
handler?: unknown;
|
|
63
|
+
}>(command: T, pluginName: string): T & {
|
|
64
|
+
command: string;
|
|
65
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import fastGlob from 'fast-glob';
|
|
4
|
+
import { COMMANDS_KEY } from './merge-config.js';
|
|
5
|
+
import { isDeclared, isPlatform } from './plugin.js';
|
|
6
|
+
/** The `.rmanrc` key naming plugin packages to load. */
|
|
7
|
+
export const PLUGINS_KEY = 'plugins';
|
|
8
|
+
/**
|
|
9
|
+
* The repository's plugins and publish targets, registered onto `app`.
|
|
10
|
+
*
|
|
11
|
+
* **Three keys share one shape** - `plugins`, `publishTargets` and `commands` each take an
|
|
12
|
+
* instance or a **glob** naming `.js` modules that `export default` one, and each appends rather
|
|
13
|
+
* than replaces. Two of them are read here; `commands` is read by `cli.ts`, and the split is not
|
|
14
|
+
* arbitrary:
|
|
15
|
+
*
|
|
16
|
+
* - **`plugins` and `publishTargets` are root-level, because they have to be.** This runs inside
|
|
17
|
+
* `Repository.create` *before* the packages are known, since a plugin's `getWorkspace` is what
|
|
18
|
+
* finds them - so there are no package configs to read yet. A `plugins` entry in a package's own
|
|
19
|
+
* `.rmanrc` is never seen, which has always been true and is now true for the same reason.
|
|
20
|
+
* - **`commands` is read at any level**, after the packages exist, which is why `cli.ts` owns it:
|
|
21
|
+
* it already holds the whole command pipeline - the `.rman/` default, de-duplication by name,
|
|
22
|
+
* and the built-in shadow check. Reading it here as well would register every config-declared
|
|
23
|
+
* command twice, and yargs does not survive that.
|
|
24
|
+
*
|
|
25
|
+
* **Order within this function is load-bearing**: plugins first, then targets. rman's own
|
|
26
|
+
* `publish` builds its `--target` choices from `app.publishTargets` when its factory runs, and a
|
|
27
|
+
* target contributed by a plugin's `init` has to be there before the list is read.
|
|
28
|
+
*
|
|
29
|
+
* **A contribution that cannot be loaded throws**, unlike a broken `.rman/*.mjs`, which is warned
|
|
30
|
+
* about and skipped. The consequence differs: a skipped local command affects only itself, while a
|
|
31
|
+
* missing plugin silently removes commands the repository is built around - `rman publish` would
|
|
32
|
+
* simply not exist, and "not a known command" sends the reader looking in the wrong place.
|
|
33
|
+
*/
|
|
34
|
+
export async function loadPlugins(app, rootConfig) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const { value, from } of await resolveEntries(rootConfig.plugins, PLUGINS_KEY)) {
|
|
37
|
+
const plugin = value;
|
|
38
|
+
if (!isPlainObject(plugin) || typeof plugin.name !== 'string' || !plugin.name) {
|
|
39
|
+
throw new Error(`"${PLUGINS_KEY}" takes a plugin or a glob naming modules that export one - ${from} gave ` +
|
|
40
|
+
`${describeExport(plugin)}. Every message about a plugin is keyed by its \`name\`.`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* **Declared through `definePlatform`/`definePlugin`, and checked for it.**
|
|
44
|
+
*
|
|
45
|
+
* The guard used to be `manifestProvider` being required, which worked only while one type was
|
|
46
|
+
* both halves. A 2.x plugin that contributes nothing but an `init` is `{ name, init }` - and so
|
|
47
|
+
* was an rman **1.x plugin**, exactly. They are indistinguishable by shape, so a shape test
|
|
48
|
+
* cannot tell them apart and the declaration has to be explicit.
|
|
49
|
+
*
|
|
50
|
+
* Measured on the real case before there was any guard: `@panates/rman-node`'s 1.x plugin
|
|
51
|
+
* loaded, registered, and died inside its own `init` with
|
|
52
|
+
* `TypeError: ctx.addCommand is not a function`, fifteen times over, naming neither the plugin
|
|
53
|
+
* nor the version it was written against.
|
|
54
|
+
*/
|
|
55
|
+
if (!isDeclared(plugin)) {
|
|
56
|
+
throw new Error(`Plugin "${plugin.name}" (${from}) was not declared with definePlatform() or ` +
|
|
57
|
+
`definePlugin(). A plain object is how an rman 1.x plugin looks, and a 1.x plugin has ` +
|
|
58
|
+
`nothing left to do: commands, publish targets and other plugins are "${COMMANDS_KEY}", ` +
|
|
59
|
+
`"publishTargets" and "${PLUGINS_KEY}" keys of a config now. Wrap a technology in ` +
|
|
60
|
+
`definePlatform({ name, manifestProvider, ... }), anything else in definePlugin({ ... }).`);
|
|
61
|
+
}
|
|
62
|
+
/** One registration per name. Twice would define the same commands twice, which yargs does not
|
|
63
|
+
* survive - and two layers naming one plugin is ordinary rather than a mistake. */
|
|
64
|
+
if (seen.has(plugin.name))
|
|
65
|
+
continue;
|
|
66
|
+
seen.add(plugin.name);
|
|
67
|
+
await registerPlugin(app, plugin).init?.({ app });
|
|
68
|
+
}
|
|
69
|
+
for (const { value, from } of await resolveEntries(rootConfig.publishTargets, 'publishTargets')) {
|
|
70
|
+
const target = value;
|
|
71
|
+
if (!isPlainObject(value) || typeof target.name !== 'string' || !target.name) {
|
|
72
|
+
throw new Error(`"publishTargets" takes a target or a glob naming modules that export one - ${from} gave ` +
|
|
73
|
+
`${describeExport(target)}.`);
|
|
74
|
+
}
|
|
75
|
+
app.publishTargets.add(target);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* **Puts one plugin onto an application** - what `plugins` does with an entry once it has been
|
|
80
|
+
* found and vetted, and the only place that knows how.
|
|
81
|
+
*
|
|
82
|
+
* **A bare `Platform` is sugar**, which is what almost every entry is - a plugin that provides one
|
|
83
|
+
* technology and nothing else. Normalized here, so nothing downstream deals in two shapes.
|
|
84
|
+
*
|
|
85
|
+
* **Exported because a spec must not reimplement it.** The registration is two registries plus a
|
|
86
|
+
* planner assignment, and a fixture writing that out by hand is a second implementation that
|
|
87
|
+
* drifts - which is exactly how a fixture ends up proving the core works when it does not. A spec
|
|
88
|
+
* brings its own technology through this, the same door a config's does.
|
|
89
|
+
*
|
|
90
|
+
* **`init` is not called here, and that is why this returns the normalized plugin.** An `init` runs
|
|
91
|
+
* once, when a *config* brought the plugin in, and it may be asynchronous - so it belongs to the
|
|
92
|
+
* loader above rather than to a function a fixture calls synchronously while building an
|
|
93
|
+
* application.
|
|
94
|
+
*/
|
|
95
|
+
export function registerPlugin(app, entry) {
|
|
96
|
+
const declared = isPlatform(entry) ? { name: entry.name, platforms: [entry] } : entry;
|
|
97
|
+
app.plugins.add(declared);
|
|
98
|
+
for (const platform of declared.platforms ?? []) {
|
|
99
|
+
app.platforms.add(platform);
|
|
100
|
+
/**
|
|
101
|
+
* **The orchestrator, and only that.** A plan is computed for the whole repository at once -
|
|
102
|
+
* groups span packages, the ripple crosses them - so one planner drives the traversal and the
|
|
103
|
+
* last registration wins it.
|
|
104
|
+
*
|
|
105
|
+
* The two decisions that belong to a *technology* are not taken from here: `detectBoundary`
|
|
106
|
+
* and `cascade` are asked of `pkg.platform.versionPlanner` per package, which is why a platform
|
|
107
|
+
* still declares one even when it is not the last to register.
|
|
108
|
+
*/
|
|
109
|
+
if (platform.versionPlanner)
|
|
110
|
+
app.versionPlanner = platform.versionPlanner;
|
|
111
|
+
}
|
|
112
|
+
return declared;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The shared half of the three keys: an instance is itself, a string is a glob whose matches are
|
|
116
|
+
* imported and unwrapped.
|
|
117
|
+
*
|
|
118
|
+
* **A glob is already absolute** by the time it gets here - `mergeConfig` anchors it to the file
|
|
119
|
+
* that declared it as the config is read, which is the last moment that is knowable (see
|
|
120
|
+
* `anchorContributions`). Nothing here resolves a path.
|
|
121
|
+
*
|
|
122
|
+
* **`.js` only.** rman imports these in its own process with no loader registered, so a `.ts`
|
|
123
|
+
* module cannot be one; a TypeScript repository compiles first or writes `.mjs`. Stated rather
|
|
124
|
+
* than attempted, because a `.ts` that happens to load under a test runner's loader and not under
|
|
125
|
+
* the CLI is the worst of both.
|
|
126
|
+
*/
|
|
127
|
+
async function resolveEntries(declared, key) {
|
|
128
|
+
if (declared === undefined)
|
|
129
|
+
return [];
|
|
130
|
+
const out = [];
|
|
131
|
+
for (const entry of (Array.isArray(declared) ? declared : [declared])) {
|
|
132
|
+
if (typeof entry !== 'string') {
|
|
133
|
+
out.push({ value: entry, from: `"${key}"` });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!entry.trim())
|
|
137
|
+
throw new Error(`"${key}" has an empty glob.`);
|
|
138
|
+
const files = await fastGlob(entry.split(path.sep).join('/'), { absolute: true, onlyFiles: true });
|
|
139
|
+
/**
|
|
140
|
+
* **A glob matching nothing is an error here**, unlike in `commands`, and the asymmetry is the
|
|
141
|
+
* point. Losing a plugin silently removes commands and seams the repository is built around -
|
|
142
|
+
* `rman publish` would simply not exist, and "not a known command" sends the reader looking in
|
|
143
|
+
* the wrong place. `commands` can match nothing legitimately: `.rman/*.js` is its default, and
|
|
144
|
+
* most repositories have no such directory.
|
|
145
|
+
*/
|
|
146
|
+
if (!files.length) {
|
|
147
|
+
/**
|
|
148
|
+
* **A package name gets the answer it is actually asking for.** A bare `rman-node` here is a
|
|
149
|
+
* glob that matches nothing, and "matched no file" sends the reader to check their paths -
|
|
150
|
+
* when what they wrote is a *package*, whose config reaches a repository through `extends`.
|
|
151
|
+
* Two documents promised this message named the fix (`docs/rman.md`, `docs/cli-rman.md`) and
|
|
152
|
+
* it did not; measured, `plugins: ['rman-node']` exited 1 saying only that a glob matched
|
|
153
|
+
* nothing.
|
|
154
|
+
*
|
|
155
|
+
* Reaching this intact takes `mergeConfig`'s help: it anchors a contribution glob to the file
|
|
156
|
+
* that declared it, which used to turn `rman-node` into `<dir>/rman-node` and erase the
|
|
157
|
+
* evidence. `looksLikePackageName` there leaves this shape alone; the same predicate is
|
|
158
|
+
* spelled again here rather than shared, because the two modules answer different questions
|
|
159
|
+
* with it and a merge concern importing a loader concern (or the reverse) is the coupling
|
|
160
|
+
* neither wants.
|
|
161
|
+
*/
|
|
162
|
+
const isPackageName = /^(?:@[a-z0-9-~][\w.-]*\/)?[a-z0-9-~][\w.-]*$/i.test(entry) && !/\.[cm]?js$/i.test(entry);
|
|
163
|
+
if (isPackageName) {
|
|
164
|
+
throw new Error(`"${key}" entry "${entry}" looks like a package name, and this key does not take one - ` +
|
|
165
|
+
`it takes a plugin, or a glob naming modules that export one. A plugin package exports ` +
|
|
166
|
+
`an rman config, so write \`extends: "${entry}"\` instead, which merges everything that ` +
|
|
167
|
+
`package declares underneath your own config.`);
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`"${key}" glob "${entry}" matched no file. A plugin that does not load is not a plugin.`);
|
|
170
|
+
}
|
|
171
|
+
for (const file of [...new Set(files.map(f => path.resolve(f)))].sort()) {
|
|
172
|
+
const mod = await import(pathToFileURL(file).href);
|
|
173
|
+
const exported = mod?.default;
|
|
174
|
+
if (exported === undefined) {
|
|
175
|
+
throw new Error(`"${key}" matched "${file}", which has no default export. ${describeExport(mod)}`);
|
|
176
|
+
}
|
|
177
|
+
out.push({ value: exported, from: file });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The second half of the error above - what the module *did* export, so the author can see how far
|
|
184
|
+
* off it was.
|
|
185
|
+
*
|
|
186
|
+
* It recognizes a plugin object only to **say so in a message**. That is the one safe use for this
|
|
187
|
+
* shape test: it decides nothing, so a wrong guess costs a slightly less helpful sentence rather
|
|
188
|
+
* than a plugin that silently does not load.
|
|
189
|
+
*/
|
|
190
|
+
function describeExport(exported) {
|
|
191
|
+
if (exported === undefined)
|
|
192
|
+
return 'It has no default export.';
|
|
193
|
+
if (!isPlainObject(exported))
|
|
194
|
+
return `Its default export is a ${typeof exported}.`;
|
|
195
|
+
const looksLikePlugin = PLUGIN_SEAMS.some(seam => exported[seam] !== undefined);
|
|
196
|
+
return looksLikePlugin
|
|
197
|
+
? `Its default export looks like the plugin itself - put it in a config's "${PLUGINS_KEY}".`
|
|
198
|
+
: `Its default export has no "${PLUGINS_KEY}".`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The members only a plugin has, for that sentence alone.
|
|
202
|
+
*
|
|
203
|
+
* **`name` is deliberately not one of them**, and it used to be: a config may carry a `name` too,
|
|
204
|
+
* so anything with one "looked like a plugin" - which sent the author of a perfectly ordinary
|
|
205
|
+
* config to go and wrap it in `plugins`. Each of these belongs to a technology and to nothing
|
|
206
|
+
* else, `manifestProvider` first because it is the one a plugin cannot be without.
|
|
207
|
+
*/
|
|
208
|
+
const PLUGIN_SEAMS = ['manifestProvider', 'getWorkspace', 'getBinPaths', 'getRunSteps', 'versionPlanner'];
|
|
209
|
+
/** A config object, as opposed to an array or anything with its own prototype. */
|
|
210
|
+
function isPlainObject(value) {
|
|
211
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A plugin's `CustomCommand`, checked the same way a `.rman/*.mjs` one is - the name it answers to
|
|
215
|
+
* comes from its own `command` string, since a plugin has no file name to fall back on.
|
|
216
|
+
*
|
|
217
|
+
* Exported because `cli.ts` runs the identical checks on what a *declarative* command's factory
|
|
218
|
+
* returns, and they must not drift: a plugin written in JavaScript reaches both forms with no type
|
|
219
|
+
* checker in the way.
|
|
220
|
+
*/
|
|
221
|
+
export function checkCustomCommand(command, pluginName) {
|
|
222
|
+
const declared = command?.command?.trim();
|
|
223
|
+
if (!declared) {
|
|
224
|
+
throw new Error(`Plugin "${pluginName}" has a command with no "command" name - it cannot be registered.`);
|
|
225
|
+
}
|
|
226
|
+
if (typeof command.handler !== 'function') {
|
|
227
|
+
throw new Error(`Plugin "${pluginName}" command "${declared}" has no "handler" function.`);
|
|
228
|
+
}
|
|
229
|
+
if (typeof command.describe !== 'string' || !command.describe) {
|
|
230
|
+
throw new Error(`Plugin "${pluginName}" command "${declared}" has no "describe" - \`rman --help\` would have ` +
|
|
231
|
+
`nothing to list it by.`);
|
|
232
|
+
}
|
|
233
|
+
return { ...command, command: declared };
|
|
234
|
+
}
|