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/plugin.js
CHANGED
|
@@ -1,189 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Identity helper for authoring a plugin with full type-checking - the `defineConfig`/
|
|
13
|
-
* `defineCommand` pattern again, for the same reason. Returns `plugin` unchanged.
|
|
14
|
-
*
|
|
15
|
-
* **A plugin package's entry point exports a *config*, not this**, so that a package is an
|
|
16
|
-
* `.rmanrc` like any other and can carry a second plugin later without changing shape:
|
|
17
|
-
*
|
|
18
|
-
* ```js
|
|
19
|
-
* // rman-node's entry point
|
|
20
|
-
* import { defineConfig, definePlugin } from 'rman';
|
|
21
|
-
* import publishCommand from './commands/publish.js';
|
|
22
|
-
*
|
|
23
|
-
* export const nodePlugin = definePlugin({ name: 'rman-node', commands: [publishCommand] });
|
|
24
|
-
* export default defineConfig({ plugins: [nodePlugin] });
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* **A module exporting the plugin itself is refused**, with a message saying so. Accepting both
|
|
28
|
-
* would mean telling a plugin from a config at runtime, and `name` is a key either may have - the
|
|
29
|
-
* test would be a guess, and guessing "plugin" registers nothing while reporting success.
|
|
30
|
-
*/
|
|
1
|
+
/** Whether `value` came from `definePlatform` or `definePlugin`. */
|
|
2
|
+
export function isDeclared(value) {
|
|
3
|
+
return !!value && typeof value === 'object' && value[declaredKey()] === true;
|
|
4
|
+
}
|
|
5
|
+
/** Declares a **platform** - one technology, whole. Returns it unchanged apart from the mark. */
|
|
6
|
+
export function definePlatform(platform) {
|
|
7
|
+
return brand(platform);
|
|
8
|
+
}
|
|
9
|
+
/** Declares a **plugin** - whatever a package contributes, platforms included. Returns it unchanged
|
|
10
|
+
* apart from the mark. */
|
|
31
11
|
export function definePlugin(plugin) {
|
|
32
|
-
return plugin;
|
|
12
|
+
return brand(plugin);
|
|
13
|
+
}
|
|
14
|
+
/** Whether `value` is a platform rather than the broader plugin - `manifestProvider` is what makes
|
|
15
|
+
* one, and it is the only required member either type has beyond `name`. */
|
|
16
|
+
export function isPlatform(value) {
|
|
17
|
+
return !!value.manifestProvider;
|
|
33
18
|
}
|
|
34
19
|
/**
|
|
35
|
-
*
|
|
20
|
+
* The platform a package gets when **none** claimed its directory - a repository naming no
|
|
21
|
+
* platform, or a directory none of the named ones recognized.
|
|
36
22
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
23
|
+
* It exists so `Package.platform` need not be optional. `Package.provider` was an empty string for
|
|
24
|
+
* exactly this case, and every reader had to know that; an object with an empty `name` says the
|
|
25
|
+
* same thing without a guard, and `pkg.provider === 'node'` - the check CLAUDE.md prescribes -
|
|
26
|
+
* reads the same either way.
|
|
41
27
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
28
|
+
* **Every documented behaviour of "no plugin" survives unchanged**, because the absences are the
|
|
29
|
+
* behaviour:
|
|
44
30
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
31
|
+
* - a reader that recognizes nothing -> `Manifest.read` falls through to its own empty manifest,
|
|
32
|
+
* exactly as it does when no plugin answers;
|
|
33
|
+
* - no `getWorkspace` -> a repository naming no plugin has no packages beyond itself, which is the
|
|
34
|
+
* boundary working rather than failing (`workspaces` in a `package.json` is npm's idea);
|
|
35
|
+
* - no `getBinPaths` -> nothing is prepended to a child process's PATH, so the inherited one
|
|
36
|
+
* stands on its own rather than being guessed at;
|
|
37
|
+
* - no `getRunSteps` -> a package's steps come from its `.rmanrc` alone, the core's only source;
|
|
38
|
+
* - no `versionPlanner` -> `version`/`changed` fail naming the key, rather than releasing a
|
|
39
|
+
* plausible but untrue set of packages from a default nobody chose.
|
|
53
40
|
*/
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
41
|
+
export const basePlatform = definePlatform({
|
|
42
|
+
name: '',
|
|
43
|
+
manifestProvider: {
|
|
44
|
+
name: '',
|
|
45
|
+
fileName: '',
|
|
46
|
+
/** Recognizes nothing, which is the point: `Manifest.read` falls through to its own empty
|
|
47
|
+
* manifest exactly as it does when no plugin answers. */
|
|
48
|
+
read: () => undefined,
|
|
49
|
+
write: () => undefined,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
60
52
|
/**
|
|
61
|
-
*
|
|
53
|
+
* **Marks an object as declared through one of the factories below**, so `loadPlugins` can tell a
|
|
54
|
+
* 2.x contribution from anything else that happens to have the same shape.
|
|
62
55
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
56
|
+
* It exists for one measured case, and there is no structural check that could replace it: an rman
|
|
57
|
+
* **1.x plugin was `{ name, init }`**, and under this split a 2.x plugin that only runs an `init`
|
|
58
|
+
* is *also* `{ name, init }`. They are indistinguishable by shape. The guard used to be
|
|
59
|
+
* `manifestProvider` being required - which only worked while the one type was both halves.
|
|
67
60
|
*
|
|
68
|
-
*
|
|
69
|
-
* `
|
|
70
|
-
* `extends` follows.
|
|
61
|
+
* Non-enumerable, so it never reaches `JSON.stringify`, `rman config` or a `toEqual` diff, the same
|
|
62
|
+
* way `ORIGINS` and `PREVIOUS_VALUES` travel.
|
|
71
63
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*/
|
|
76
|
-
async function loadInto(commands, config, from, seen) {
|
|
77
|
-
const declared = config?.[PLUGINS_KEY];
|
|
78
|
-
if (declared === undefined)
|
|
79
|
-
return;
|
|
80
|
-
for (const entry of Array.isArray(declared) ? declared : [declared]) {
|
|
81
|
-
/**
|
|
82
|
-
* The object form: a JS config handing a plugin over directly, and what a plugin package's own
|
|
83
|
-
* config holds. Nothing to resolve or import.
|
|
84
|
-
*
|
|
85
|
-
* An object here **is** a plugin - it is not guessed at. The one thing checked is that it has a
|
|
86
|
-
* `name`, because everything downstream (the registration guard, `--help` grouping, every error
|
|
87
|
-
* message) is keyed by it.
|
|
88
|
-
*/
|
|
89
|
-
if (isPlainObject(entry)) {
|
|
90
|
-
if (typeof entry.name !== 'string' || !entry.name) {
|
|
91
|
-
throw new Error(`A plugin object in "${PLUGINS_KEY}" has no "name" - every other message is keyed by it.`);
|
|
92
|
-
}
|
|
93
|
-
register(commands, entry, entry.name, from, seen);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (typeof entry !== 'string' || !entry.trim()) {
|
|
97
|
-
throw new Error(`"${PLUGINS_KEY}" takes a package name, a path, or a plugin object - not ${JSON.stringify(entry)}`);
|
|
98
|
-
}
|
|
99
|
-
const file = resolveConfigTarget(entry, from, PLUGINS_KEY);
|
|
100
|
-
/** A config naming itself, or two naming each other, would otherwise recurse forever. Keyed by
|
|
101
|
-
* resolved file, so the same package reached by two names is still loaded once. */
|
|
102
|
-
if (seen.files.has(file))
|
|
103
|
-
continue;
|
|
104
|
-
seen.files.add(file);
|
|
105
|
-
const mod = await import(pathToFileURL(file).href);
|
|
106
|
-
const exported = mod?.default ?? mod?.plugin;
|
|
107
|
-
/**
|
|
108
|
-
* **A module exports one thing: an rman config.** Not a plugin, and not either-or.
|
|
109
|
-
*
|
|
110
|
-
* Accepting both meant having to *tell them apart*, and there is no reliable way to - `name` is
|
|
111
|
-
* a key a config may have as well, so the test came down to "a name plus at least one of the
|
|
112
|
-
* things a plugin contributes", which is a guess. Guess wrong in the direction of "plugin" and
|
|
113
|
-
* nothing is registered while the command reports success, which is the worst outcome on offer.
|
|
114
|
-
* One shape, one rule, one error.
|
|
115
|
-
*/
|
|
116
|
-
if (!isPlainObject(exported) || exported.plugins === undefined) {
|
|
117
|
-
throw new Error(`Plugin "${entry}" must export an rman config - \`export default defineConfig({ plugins: [ ... ] })\`. ` +
|
|
118
|
-
describeExport(exported));
|
|
119
|
-
}
|
|
120
|
-
await loadInto(commands, exported, file, seen);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Everything a plugin contributes, in one place - so the object and the imported forms cannot drift
|
|
125
|
-
* apart in what they support.
|
|
64
|
+
* The cost, stated rather than hidden: a plain object literal in `plugins` is no longer accepted.
|
|
65
|
+
* `definePlatform`/`definePlugin` is one import and one call, and it is the explicit declaration
|
|
66
|
+
* that a runtime check needs - a type cannot reach a JavaScript config.
|
|
126
67
|
*
|
|
127
|
-
* **
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
133
|
-
function register(commands, plugin, label, specifier, seen) {
|
|
134
|
-
if (seen.names.has(plugin.name))
|
|
135
|
-
return;
|
|
136
|
-
seen.names.add(plugin.name);
|
|
137
|
-
if (plugin.runSteps)
|
|
138
|
-
RunService.addStepSource(plugin.runSteps);
|
|
139
|
-
if (plugin.manifest)
|
|
140
|
-
Manifest.addProvider(plugin.manifest);
|
|
141
|
-
if (plugin.workspace)
|
|
142
|
-
Workspace.addProvider(plugin.workspace);
|
|
143
|
-
if (plugin.binPaths)
|
|
144
|
-
BinPath.addProvider(plugin.binPaths);
|
|
145
|
-
if (plugin.versionPlanner)
|
|
146
|
-
VersionPlanService.setPlanner(plugin.versionPlanner);
|
|
147
|
-
for (const command of plugin.commands ?? []) {
|
|
148
|
-
commands.push(toLoadedCommand(command, label || specifier, specifier));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* The second half of the error above - what the module *did* export, so the author can see how far
|
|
153
|
-
* off it was.
|
|
68
|
+
* **A function rather than a `const`, and that is not a preference.** The file layout puts private
|
|
69
|
+
* declarations below the exported ones, and `basePlatform` is an exported const whose initializer
|
|
70
|
+
* *calls* `definePlatform` - so a `const DECLARED` below it sits in its temporal dead zone while the
|
|
71
|
+
* module is still evaluating. Measured: the whole suite failed to load with
|
|
72
|
+
* `ReferenceError: Cannot access 'DECLARED' before initialization`. A function declaration hoists,
|
|
73
|
+
* so the key is resolved when it is asked for instead of when the module reaches this line.
|
|
154
74
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
75
|
+
* **`Symbol.for`, so two copies of rman in one process agree about the mark.** A per-module symbol
|
|
76
|
+
* would have a plugin declared against one copy refused by the other - with a message about rman
|
|
77
|
+
* 1.x plugins, which is the one explanation guaranteed to be wrong.
|
|
158
78
|
*/
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
return 'It has no default export.';
|
|
162
|
-
if (!isPlainObject(exported))
|
|
163
|
-
return `Its default export is a ${typeof exported}.`;
|
|
164
|
-
const looksLikePlugin = PLUGIN_SEAMS.some(seam => exported[seam] !== undefined);
|
|
165
|
-
return looksLikePlugin
|
|
166
|
-
? `Its default export looks like the plugin itself - put it in a config's "${PLUGINS_KEY}".`
|
|
167
|
-
: `Its default export has no "${PLUGINS_KEY}".`;
|
|
168
|
-
}
|
|
169
|
-
const PLUGIN_SEAMS = ['commands', 'runSteps', 'workspace', 'manifest', 'versionPlanner', 'binPaths'];
|
|
170
|
-
/** A config object, as opposed to an array or anything with its own prototype. */
|
|
171
|
-
function isPlainObject(value) {
|
|
172
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
79
|
+
function declaredKey() {
|
|
80
|
+
return Symbol.for('rman.declared');
|
|
173
81
|
}
|
|
174
|
-
/**
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!declared) {
|
|
179
|
-
throw new Error(`Plugin "${pluginName}" has a command with no "command" name - it cannot be registered.`);
|
|
180
|
-
}
|
|
181
|
-
if (typeof command.handler !== 'function') {
|
|
182
|
-
throw new Error(`Plugin "${pluginName}" command "${declared}" has no "handler" function.`);
|
|
183
|
-
}
|
|
184
|
-
if (typeof command.describe !== 'string' || !command.describe) {
|
|
185
|
-
throw new Error(`Plugin "${pluginName}" command "${declared}" has no "describe" - \`rman --help\` would have ` +
|
|
186
|
-
`nothing to list it by.`);
|
|
187
|
-
}
|
|
188
|
-
return { ...command, command: declared, name: declared.split(/\s+/)[0], file: specifier };
|
|
82
|
+
/** Stamps the mark, non-enumerably, and hands the object back. */
|
|
83
|
+
function brand(value) {
|
|
84
|
+
Object.defineProperty(value, declaredKey(), { value: true, enumerable: false, configurable: true });
|
|
85
|
+
return value;
|
|
189
86
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
2
|
+
import type { PackageFilterOptions } from '../utils/package-filter.js';
|
|
3
|
+
import type { RmanApplication } from './application.js';
|
|
4
|
+
import type { Package } from './package.js';
|
|
5
|
+
import type { Repository } from './repository.js';
|
|
6
|
+
/**
|
|
7
|
+
* **Where a package's artifact ships, as a contribution.**
|
|
8
|
+
*
|
|
9
|
+
* `publish` used to be `rman-node`'s command, and that was backwards in a way worth stating: the
|
|
10
|
+
* command itself is about a *repository* - which packages are candidates, which versions are
|
|
11
|
+
* already out there, what order to push them in, what to print - and none of that is npm's. What is
|
|
12
|
+
* npm's is one answer to "is this version on the registry, and how do I push it", which is exactly
|
|
13
|
+
* the size of this interface. Docker publishing was in the core all along and could only be reached
|
|
14
|
+
* through a Node plugin's command; a Cargo repository could reach neither.
|
|
15
|
+
*
|
|
16
|
+
* So: the core ships `publish` and the `docker` target, the `node` built-in contributes `npm`, and a
|
|
17
|
+
* repository in any other ecosystem contributes its own without either package knowing about it.
|
|
18
|
+
*
|
|
19
|
+
* A target owns three things and no more:
|
|
20
|
+
*
|
|
21
|
+
* - **Which packages are its own by default** (`claims`) - see below.
|
|
22
|
+
* - **Its own CLI options**, merged into `publish`'s by the command. Every flag that only meant
|
|
23
|
+
* something to npm (`--access`, `--tag`, `--otp`, `--registry`, `--userconfig`, `--contents`,
|
|
24
|
+
* `--package-manager`) is declared here now instead of on a command the core would otherwise have
|
|
25
|
+
* to know them all in advance.
|
|
26
|
+
* - **`getPlan`/`applyPlan`** - the same two-step shape every other rman service uses, and the
|
|
27
|
+
* reason `--dry-run` and the confirmation prompt are the command's business rather than each
|
|
28
|
+
* target's.
|
|
29
|
+
*/
|
|
30
|
+
export interface PublishTarget {
|
|
31
|
+
/** The name `publish.target` and `--target` use, and the label printed beside a package. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** One line for `--target`'s help - a reader asking `rman publish --help` in a polyglot
|
|
34
|
+
* repository has no other way to find out which targets are even installed. */
|
|
35
|
+
readonly describe?: string;
|
|
36
|
+
/**
|
|
37
|
+
* This target's own flags, merged into `publish`'s options.
|
|
38
|
+
*
|
|
39
|
+
* Flat, not namespaced: `--tag` reads better than `--npm-tag`, and two targets colliding on a
|
|
40
|
+
* name is refused loudly by the command rather than resolved by a rule nobody would remember.
|
|
41
|
+
* Name a flag for the target when it genuinely belongs to one (`--docker-namespace`).
|
|
42
|
+
*/
|
|
43
|
+
readonly options?: Record<string, RmanConfig.CommandOption>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a package that declares **no** `publish.target` at all ships here.
|
|
46
|
+
*
|
|
47
|
+
* Absent means never - the target is opt-in, which is what `docker` is. `npm`'s answer is
|
|
48
|
+
* `pkg.provider === 'node'`, and that is the fix for a real bug rather than a nicety: the default
|
|
49
|
+
* used to be a hardcoded `['npm']` in the core, so `rman list --json` reported
|
|
50
|
+
* `publishTargets: ["npm"]` for a Cargo package and `publish` treated it as an npm candidate.
|
|
51
|
+
* A default only the ecosystem can state had been written down by someone who could not know it.
|
|
52
|
+
*/
|
|
53
|
+
claims?(pkg: Package): boolean;
|
|
54
|
+
/** What this target *would* do - never publishes. Called even under `--dry-run`, which is the
|
|
55
|
+
* whole point of the split. */
|
|
56
|
+
getPlan(ctx: PublishTarget.Context): Promise<PublishTarget.Entry[]>;
|
|
57
|
+
/** Publishes every `'publish'` entry in `plan`, and returns what it did. */
|
|
58
|
+
applyPlan(ctx: PublishTarget.Context, plan: PublishTarget.Entry[]): Promise<PublishTarget.Entry[]>;
|
|
59
|
+
}
|
|
60
|
+
export declare namespace PublishTarget {
|
|
61
|
+
/**
|
|
62
|
+
* What a target is handed, once per call.
|
|
63
|
+
*
|
|
64
|
+
* `args` is the parsed argv, untyped on purpose: a target declared its own options, so it is the
|
|
65
|
+
* only thing that knows their names and types, and the core cannot be made to know them without
|
|
66
|
+
* becoming the thing this interface exists to stop it being.
|
|
67
|
+
*/
|
|
68
|
+
interface Context {
|
|
69
|
+
readonly app: RmanApplication;
|
|
70
|
+
readonly repository: Repository;
|
|
71
|
+
/** The filters every target shares, read off argv once by the command. */
|
|
72
|
+
readonly options: Options;
|
|
73
|
+
/** The whole parsed argv - a target reads the options it declared out of this. */
|
|
74
|
+
readonly args: Record<string, any>;
|
|
75
|
+
}
|
|
76
|
+
interface Options extends PackageFilterOptions {
|
|
77
|
+
/** A package with uncommitted local changes is excluded (`'skip'`) instead of aborting the
|
|
78
|
+
* whole plan (`'error'`). */
|
|
79
|
+
ignoreDirty?: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* One package's outcome for one target.
|
|
83
|
+
*
|
|
84
|
+
* `'skip'` and `'error'` differ in what they do to the run: an error aborts before anything is
|
|
85
|
+
* published, a skip is a package the plan deliberately leaves alone. A target may add fields of
|
|
86
|
+
* its own (docker carries the resolved image reference); `detail` is the one the command prints,
|
|
87
|
+
* so whatever a target wants shown beside a package goes there.
|
|
88
|
+
*/
|
|
89
|
+
interface Entry {
|
|
90
|
+
package: Package;
|
|
91
|
+
version: string;
|
|
92
|
+
status: 'publish' | 'skip' | 'up-to-date' | 'error';
|
|
93
|
+
/** Printed after the package name - docker's resolved `<namespace>/<image>`, for instance. */
|
|
94
|
+
detail?: string;
|
|
95
|
+
reason?: string;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* **A target's name, and deliberately not a union.**
|
|
100
|
+
*
|
|
101
|
+
* It was `'npm' | 'docker'`, the type half of a bug whose runtime half was a hardcoded `['npm']`
|
|
102
|
+
* default, so `rman list --json` reported `publishTargets: ["npm"]` for a Cargo package. Both are
|
|
103
|
+
* gone together - which targets exist is whatever the repository's plugins contribute
|
|
104
|
+
* (`RmanApplication.publishTargets`), so a union here would mean the core naming plugins it cannot
|
|
105
|
+
* know about, exactly as `Package.provider` must not.
|
|
106
|
+
*
|
|
107
|
+
* A name nothing implements is caught where the facts are, by `publish` itself, naming the targets
|
|
108
|
+
* this repository does have.
|
|
109
|
+
*/
|
|
110
|
+
export type PublishTargetName = string;
|
|
111
|
+
/**
|
|
112
|
+
* The targets a package **declares**, or `undefined` when it declares none - which is not the same
|
|
113
|
+
* as an empty list, and the difference is what `claims` is asked about.
|
|
114
|
+
*/
|
|
115
|
+
export declare function declaredTargets(pkg: Package): string[] | undefined;
|
|
116
|
+
/** Whether `pkg` ships to `target`: its own `publish.target` decides when it has one, and the
|
|
117
|
+
* target's own `claims` decides when it does not. */
|
|
118
|
+
export declare function shipsTo(pkg: Package, target: PublishTarget): boolean;
|
|
119
|
+
/** Every registered target `pkg` ships to, in registration order. The single answer to "where does
|
|
120
|
+
* this package go", so `publish` and `list --json` cannot disagree about it. */
|
|
121
|
+
export declare function targetsOf(app: RmanApplication, pkg: Package): PublishTarget[];
|
|
122
|
+
/** Names in a package's `publish.target` that no registered target answers to - a misconfiguration
|
|
123
|
+
* that used to be caught by a `choices: ['npm', 'docker']` the core can no longer write down. */
|
|
124
|
+
export declare function unknownTargets(app: RmanApplication, pkg: Package): string[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The targets a package **declares**, or `undefined` when it declares none - which is not the same
|
|
3
|
+
* as an empty list, and the difference is what `claims` is asked about.
|
|
4
|
+
*/
|
|
5
|
+
export function declaredTargets(pkg) {
|
|
6
|
+
const declared = pkg.config.publish?.target;
|
|
7
|
+
if (declared === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
return Array.isArray(declared) ? [...declared] : [declared];
|
|
10
|
+
}
|
|
11
|
+
/** Whether `pkg` ships to `target`: its own `publish.target` decides when it has one, and the
|
|
12
|
+
* target's own `claims` decides when it does not. */
|
|
13
|
+
export function shipsTo(pkg, target) {
|
|
14
|
+
const declared = declaredTargets(pkg);
|
|
15
|
+
return declared ? declared.includes(target.name) : !!target.claims?.(pkg);
|
|
16
|
+
}
|
|
17
|
+
/** Every registered target `pkg` ships to, in registration order. The single answer to "where does
|
|
18
|
+
* this package go", so `publish` and `list --json` cannot disagree about it. */
|
|
19
|
+
export function targetsOf(app, pkg) {
|
|
20
|
+
return app.publishTargets.all.filter(target => shipsTo(pkg, target));
|
|
21
|
+
}
|
|
22
|
+
/** Names in a package's `publish.target` that no registered target answers to - a misconfiguration
|
|
23
|
+
* that used to be caught by a `choices: ['npm', 'docker']` the core can no longer write down. */
|
|
24
|
+
export function unknownTargets(app, pkg) {
|
|
25
|
+
const declared = declaredTargets(pkg);
|
|
26
|
+
if (!declared)
|
|
27
|
+
return [];
|
|
28
|
+
const known = new Set(app.publishTargets.all.map(t => t.name));
|
|
29
|
+
return declared.filter(name => !known.has(name));
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ordered collection of contributions, held **on the application** rather than in a module.
|
|
3
|
+
*
|
|
4
|
+
* Every registry rman had was a module-level array (`const providers: Provider[] = []` in
|
|
5
|
+
* `manifest.ts`, `workspace.ts`, `bin-path.ts`, `run.service.ts`), which made it process-global:
|
|
6
|
+
* two repositories in one process shared it. The test suite needed a root hook emptying five of
|
|
7
|
+
* them before every single test, and without it whichever spec ran first decided the answer for
|
|
8
|
+
* the rest - so the core appeared to work in tests that had registered nothing.
|
|
9
|
+
*
|
|
10
|
+
* An instance per `RmanApplication` closes the whole class: a new application starts empty, and
|
|
11
|
+
* nothing has to be cleaned up afterwards - the root hook is deleted.
|
|
12
|
+
*
|
|
13
|
+
* **Registry, not service.** The distinction is multiplicity: a registry is for a question whose
|
|
14
|
+
* answer is the *sum* of what was contributed (every provider's bin directories, the first provider
|
|
15
|
+
* that recognizes a directory). A question with exactly one answer is a service, replaceable
|
|
16
|
+
* through `RmanApplication.getService`.
|
|
17
|
+
*/
|
|
18
|
+
export declare class Registry<T> implements Iterable<T> {
|
|
19
|
+
private readonly items;
|
|
20
|
+
/** Contributions in registration order, which for a plugin is `plugins` declaration order. */
|
|
21
|
+
get all(): readonly T[];
|
|
22
|
+
get size(): number;
|
|
23
|
+
/** Idempotent by identity: a plugin may both declare a contribution and register it itself, and
|
|
24
|
+
* the same function arriving twice would make the registry lie about what is in it. */
|
|
25
|
+
add(item: T): this;
|
|
26
|
+
/** The first contribution `ask` gives an answer for - "first that recognizes it" resolution,
|
|
27
|
+
* which is how a directory finds the manifest reader and the workspace layout that claim it. */
|
|
28
|
+
first<R>(ask: (item: T) => R | undefined): R | undefined;
|
|
29
|
+
[Symbol.iterator](): Iterator<T>;
|
|
30
|
+
}
|
package/core/registry.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ordered collection of contributions, held **on the application** rather than in a module.
|
|
3
|
+
*
|
|
4
|
+
* Every registry rman had was a module-level array (`const providers: Provider[] = []` in
|
|
5
|
+
* `manifest.ts`, `workspace.ts`, `bin-path.ts`, `run.service.ts`), which made it process-global:
|
|
6
|
+
* two repositories in one process shared it. The test suite needed a root hook emptying five of
|
|
7
|
+
* them before every single test, and without it whichever spec ran first decided the answer for
|
|
8
|
+
* the rest - so the core appeared to work in tests that had registered nothing.
|
|
9
|
+
*
|
|
10
|
+
* An instance per `RmanApplication` closes the whole class: a new application starts empty, and
|
|
11
|
+
* nothing has to be cleaned up afterwards - the root hook is deleted.
|
|
12
|
+
*
|
|
13
|
+
* **Registry, not service.** The distinction is multiplicity: a registry is for a question whose
|
|
14
|
+
* answer is the *sum* of what was contributed (every provider's bin directories, the first provider
|
|
15
|
+
* that recognizes a directory). A question with exactly one answer is a service, replaceable
|
|
16
|
+
* through `RmanApplication.getService`.
|
|
17
|
+
*/
|
|
18
|
+
export class Registry {
|
|
19
|
+
items = [];
|
|
20
|
+
/** Contributions in registration order, which for a plugin is `plugins` declaration order. */
|
|
21
|
+
get all() {
|
|
22
|
+
return this.items;
|
|
23
|
+
}
|
|
24
|
+
get size() {
|
|
25
|
+
return this.items.length;
|
|
26
|
+
}
|
|
27
|
+
/** Idempotent by identity: a plugin may both declare a contribution and register it itself, and
|
|
28
|
+
* the same function arriving twice would make the registry lie about what is in it. */
|
|
29
|
+
add(item) {
|
|
30
|
+
if (!this.items.includes(item))
|
|
31
|
+
this.items.push(item);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** The first contribution `ask` gives an answer for - "first that recognizes it" resolution,
|
|
35
|
+
* which is how a directory finds the manifest reader and the workspace layout that claim it. */
|
|
36
|
+
first(ask) {
|
|
37
|
+
for (const item of this.items) {
|
|
38
|
+
const answer = ask(item);
|
|
39
|
+
if (answer !== undefined)
|
|
40
|
+
return answer;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
[Symbol.iterator]() {
|
|
45
|
+
return this.items[Symbol.iterator]();
|
|
46
|
+
}
|
|
47
|
+
}
|