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/custom-command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import fastGlob from 'fast-glob';
|
|
4
4
|
/** Where a repository keeps its own commands - one module per command, named after it. */
|
|
5
5
|
export const CUSTOM_COMMAND_DIR = '.rman';
|
|
6
6
|
/** Loadable module forms, matching what a `.rmanrc.cjs`/`.mjs`/`.js` config already accepts. A
|
|
@@ -41,30 +41,58 @@ export function defineCommand(command) {
|
|
|
41
41
|
return command;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* The globs a repository's own commands are loaded from when it names none: `.rman/*.{js,mjs,cjs}`
|
|
45
|
+
* under the repository root.
|
|
46
|
+
*
|
|
47
|
+
* **`.rman/` is this default, not a second mechanism.** It used to be a hardcoded directory scan
|
|
48
|
+
* beside which `commands` would have been a third source of repository-level commands - and a
|
|
49
|
+
* third precedence question. Making it the default value instead leaves one source, one slot, and
|
|
50
|
+
* a zero-config path that behaves exactly as it did.
|
|
51
|
+
*/
|
|
52
|
+
export function defaultCommandGlobs(rootDir) {
|
|
53
|
+
return [path.join(rootDir, CUSTOM_COMMAND_DIR, `*{${EXTENSIONS.join(',')}}`)];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Loads every command module matching `patterns` - absolute globs, already anchored to whichever
|
|
57
|
+
* config file declared them (see `anchorContributions`).
|
|
58
|
+
*
|
|
59
|
+
* A repository matching nothing pays for one glob and no imports, which matters because this runs
|
|
60
|
+
* on *every* rman invocation, `info` included.
|
|
61
|
+
*
|
|
62
|
+
* **Deduplicated by resolved path**, because `commands` appends at every level and cascades: the
|
|
63
|
+
* root's glob reaches each package's resolved config too, so the same file is named more than once
|
|
64
|
+
* as a matter of course rather than as a mistake. Loading it twice would register the command
|
|
65
|
+
* twice, which yargs does not survive.
|
|
47
66
|
*
|
|
48
67
|
* Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
|
|
49
68
|
* warns about them. What is *not* tolerated is a module that would shadow a built-in - see
|
|
50
69
|
* `assertNoBuiltinShadowing`.
|
|
51
70
|
*/
|
|
52
|
-
export async function loadCustomCommands(
|
|
53
|
-
const dir = path.join(rootDir, CUSTOM_COMMAND_DIR);
|
|
71
|
+
export async function loadCustomCommands(patterns) {
|
|
54
72
|
const commands = [];
|
|
55
73
|
const errors = [];
|
|
56
|
-
if (!
|
|
74
|
+
if (!patterns.length)
|
|
57
75
|
return { commands, errors };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
/** Sorted so the order a command is registered in does not depend on the filesystem, and
|
|
77
|
+
* `absolute` because a pattern may name a directory outside the repository entirely - which is
|
|
78
|
+
* exactly what a shared config shipping its own commands does. */
|
|
79
|
+
const files = await fastGlob(patterns.map(p => p.split(path.sep).join('/')), { absolute: true, onlyFiles: true });
|
|
80
|
+
for (const file of [...new Set(files.map(f => path.resolve(f)))].sort()) {
|
|
62
81
|
try {
|
|
63
82
|
const mod = await import(pathToFileURL(file).href);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
const exported = mod?.default ?? mod?.command;
|
|
84
|
+
const basename = path.basename(file, path.extname(file));
|
|
85
|
+
/** The declarative form. Nothing to check here beyond its shape - the factory has not run,
|
|
86
|
+
* so there is no metadata to validate yet; `cli.ts` checks what it returns. */
|
|
87
|
+
if (typeof exported === 'function') {
|
|
88
|
+
commands.push({ name: basename, file, register: exported });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!exported || typeof exported !== 'object') {
|
|
92
|
+
throw new Error('no command exported - end the module with `export default defineCommand({ ... })`, or with ' +
|
|
93
|
+
'the declarative `export default app => ({ ... })`');
|
|
67
94
|
}
|
|
95
|
+
const command = exported;
|
|
68
96
|
if (typeof command.handler !== 'function')
|
|
69
97
|
throw new Error('"handler" is missing, or is not a function');
|
|
70
98
|
if (typeof command.describe !== 'string' || !command.describe) {
|
|
@@ -72,10 +100,9 @@ export async function loadCustomCommands(rootDir) {
|
|
|
72
100
|
}
|
|
73
101
|
const declared = command.command?.trim();
|
|
74
102
|
commands.push({
|
|
75
|
-
|
|
76
|
-
command: declared || path.basename(entry.name, path.extname(entry.name)),
|
|
77
|
-
name: (declared || path.basename(entry.name, path.extname(entry.name))).split(/\s+/)[0],
|
|
103
|
+
name: (declared || basename).split(/\s+/)[0],
|
|
78
104
|
file,
|
|
105
|
+
custom: { ...command, command: declared || basename },
|
|
79
106
|
});
|
|
80
107
|
}
|
|
81
108
|
catch (e) {
|
package/core/extends-config.d.ts
CHANGED
|
@@ -19,9 +19,17 @@ export declare const EXTENDS_KEY = "extends";
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function resolveExtends(config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
|
|
21
21
|
/**
|
|
22
|
-
* Refuses
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* **Refuses the keys a `"[selector]"` block cannot carry** - each for its own reason, and the
|
|
23
|
+
* reason is the same shape every time: a selector block is typed as a whole `RmanConfig`, so any
|
|
24
|
+
* key looks valid there, and one that can never be read is a config that quietly does nothing.
|
|
25
|
+
* That is worse than one that will not load.
|
|
26
|
+
*
|
|
27
|
+
* - **`extends`** is a statement about the *file*, not about the packages a selector happens to
|
|
28
|
+
* name, and it belongs at the top level where it would actually be resolved.
|
|
29
|
+
* - **`platform` and `name`** are what the selector is derived *from*, so a glob block setting
|
|
30
|
+
* either would need its own answer in order to be matched. See below; `"[/]"` is exempt.
|
|
31
|
+
*
|
|
32
|
+
* Called on every config form a directory can hold and on every `extends` base, so the file in the
|
|
33
|
+
* message is the one the line was written in.
|
|
26
34
|
*/
|
|
27
|
-
export declare function
|
|
35
|
+
export declare function assertSelectorBlocks(config: RmanConfig, file: string): void;
|
package/core/extends-config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
3
|
import * as yaml from 'js-yaml';
|
|
5
|
-
import { isSelectorKey } from './config.js';
|
|
4
|
+
import { isSelectorKey, parseSelector } from './config.js';
|
|
5
|
+
import { loadConfigModule } from './load-config-module.js';
|
|
6
6
|
import { mergeConfig } from './merge-config.js';
|
|
7
7
|
import { resolveConfigTarget } from './resolve-target.js';
|
|
8
8
|
/** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
|
|
@@ -40,7 +40,7 @@ export async function resolveExtends(config, from, seen = []) {
|
|
|
40
40
|
throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
|
|
41
41
|
}
|
|
42
42
|
const loaded = await loadConfigFile(file);
|
|
43
|
-
|
|
43
|
+
assertSelectorBlocks(loaded, file);
|
|
44
44
|
// Recursive: a shared config may itself be built on another.
|
|
45
45
|
mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]), file);
|
|
46
46
|
}
|
|
@@ -49,12 +49,20 @@ export async function resolveExtends(config, from, seen = []) {
|
|
|
49
49
|
return mergeConfig(base, own);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Refuses
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* **Refuses the keys a `"[selector]"` block cannot carry** - each for its own reason, and the
|
|
53
|
+
* reason is the same shape every time: a selector block is typed as a whole `RmanConfig`, so any
|
|
54
|
+
* key looks valid there, and one that can never be read is a config that quietly does nothing.
|
|
55
|
+
* That is worse than one that will not load.
|
|
56
|
+
*
|
|
57
|
+
* - **`extends`** is a statement about the *file*, not about the packages a selector happens to
|
|
58
|
+
* name, and it belongs at the top level where it would actually be resolved.
|
|
59
|
+
* - **`platform` and `name`** are what the selector is derived *from*, so a glob block setting
|
|
60
|
+
* either would need its own answer in order to be matched. See below; `"[/]"` is exempt.
|
|
61
|
+
*
|
|
62
|
+
* Called on every config form a directory can hold and on every `extends` base, so the file in the
|
|
63
|
+
* message is the one the line was written in.
|
|
56
64
|
*/
|
|
57
|
-
export function
|
|
65
|
+
export function assertSelectorBlocks(config, file) {
|
|
58
66
|
for (const [key, value] of Object.entries(config)) {
|
|
59
67
|
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
60
68
|
continue;
|
|
@@ -62,10 +70,40 @@ export function assertNoSelectorExtends(config, file) {
|
|
|
62
70
|
throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
|
|
63
71
|
`statement about this config rather than about the packages the selector names.`);
|
|
64
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* **The two keys a selector cannot carry, because the selector is downstream of them.**
|
|
75
|
+
*
|
|
76
|
+
* A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
|
|
77
|
+
* directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
|
|
78
|
+
* the packages are still being found, so a glob block setting either would need its own answer
|
|
79
|
+
* in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
|
|
80
|
+
* would simply never be read.
|
|
81
|
+
*
|
|
82
|
+
* `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
|
|
83
|
+
* the repository root - so a root block needs no selector and is applied during the walk.
|
|
84
|
+
*/
|
|
85
|
+
if (parseSelector(key).scope === 'root')
|
|
86
|
+
continue;
|
|
87
|
+
for (const identity of IDENTITY_KEYS) {
|
|
88
|
+
if (identity in value) {
|
|
89
|
+
throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
|
|
90
|
+
`and "${identity}" is what the selector is derived from, so the block could never be ` +
|
|
91
|
+
`matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
|
|
92
|
+
`under "[/]" for the root package.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
}
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
97
|
+
/**
|
|
98
|
+
* Loads one resolved target. YAML and JSON are read directly; anything else goes through
|
|
99
|
+
* `loadConfigModule`, so a shared config can be a `defineConfig` module with real logic in it.
|
|
100
|
+
*
|
|
101
|
+
* **Through that function rather than a bare `await import()`, which is what this used to do.**
|
|
102
|
+
* The two are not equivalent under an ESM loader hook: a `.cjs` base came back as an empty object,
|
|
103
|
+
* and an empty object is a valid config, so it contributed nothing and said nothing. See there for
|
|
104
|
+
* the measurement - the same file loaded correctly when it was a *directory's* own `.rmanrc.cjs`,
|
|
105
|
+
* because that path always used the careful loader.
|
|
106
|
+
*/
|
|
69
107
|
async function loadConfigFile(file) {
|
|
70
108
|
const ext = path.extname(file);
|
|
71
109
|
if (ext === '.yml' || ext === '.yaml') {
|
|
@@ -74,8 +112,7 @@ async function loadConfigFile(file) {
|
|
|
74
112
|
}
|
|
75
113
|
if (ext === '.json')
|
|
76
114
|
return asConfig(JSON.parse(fs.readFileSync(file, 'utf-8')), file);
|
|
77
|
-
|
|
78
|
-
return asConfig(mod?.default ?? mod, file);
|
|
115
|
+
return asConfig(await loadConfigModule(file), file);
|
|
79
116
|
}
|
|
80
117
|
function asConfig(value, file) {
|
|
81
118
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -87,3 +124,6 @@ function asConfig(value, file) {
|
|
|
87
124
|
delete config.$schema;
|
|
88
125
|
return config;
|
|
89
126
|
}
|
|
127
|
+
/** The keys that decide *what a package is* and *what addresses it*, read from the unmarked
|
|
128
|
+
* cascade before any package exists - see `assertSelectorBlocks`. */
|
|
129
|
+
const IDENTITY_KEYS = ['platform', 'name'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads a config module - a `.rmanrc.cjs`/`.mjs`/`.js`, or an `extends` target that is one.
|
|
3
|
+
*
|
|
4
|
+
* **`require()` first, and that is not an optimization.** A CommonJS module's `module.exports` is
|
|
5
|
+
* more reliably observed this way than through dynamic `import()`'s CJS-interop synthesis, which
|
|
6
|
+
* some ESM loader hooks - ts-node/swc-node-style transpilers registered via `--import` - can
|
|
7
|
+
* short-circuit into an **empty object**. `require()` throws `ERR_REQUIRE_ESM` for a genuinely-ESM
|
|
8
|
+
* file (`.mjs`, or `.js` under `"type": "module"`), and only then does this fall back to
|
|
9
|
+
* `import()`, the one case that actually needs it.
|
|
10
|
+
*
|
|
11
|
+
* Either path can hand back an ES module namespace rather than a plain object - Node's
|
|
12
|
+
* `require(esm)` support does this too, not just `import()` - so `.default` is preferred whenever
|
|
13
|
+
* present.
|
|
14
|
+
*
|
|
15
|
+
* **It lives here because `extends` needs it too, and did not have it.** A directory's own
|
|
16
|
+
* `.rmanrc.cjs` went through this while `extends-config.ts` used a bare `await import()`, so the
|
|
17
|
+
* *same file* loaded correctly as a directory's config and came back empty when another config
|
|
18
|
+
* named it - and an empty object is a valid config, so nothing was reported. Measured under mocha:
|
|
19
|
+
* a base declaring `"[*]": { version: { stamp: ['build'] } }`, reached by
|
|
20
|
+
* `extends: './base.cjs'`, contributed nothing; the identical fixture with `base.json` contributed
|
|
21
|
+
* normally, and the same `.cjs` worked from the CLI, where no loader hook is registered.
|
|
22
|
+
*
|
|
23
|
+
* `createRequire` is based on this module's own URL rather than on the config file. That is right
|
|
24
|
+
* here and *not* right for resolving a bare specifier - see `resolveConfigTarget`, which is based
|
|
25
|
+
* on the config file so a package name resolves through the repository's `node_modules` rather than
|
|
26
|
+
* rman's own. By the time a file reaches this function it is already an absolute path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadConfigModule(file: string): Promise<any>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
/**
|
|
4
|
+
* Loads a config module - a `.rmanrc.cjs`/`.mjs`/`.js`, or an `extends` target that is one.
|
|
5
|
+
*
|
|
6
|
+
* **`require()` first, and that is not an optimization.** A CommonJS module's `module.exports` is
|
|
7
|
+
* more reliably observed this way than through dynamic `import()`'s CJS-interop synthesis, which
|
|
8
|
+
* some ESM loader hooks - ts-node/swc-node-style transpilers registered via `--import` - can
|
|
9
|
+
* short-circuit into an **empty object**. `require()` throws `ERR_REQUIRE_ESM` for a genuinely-ESM
|
|
10
|
+
* file (`.mjs`, or `.js` under `"type": "module"`), and only then does this fall back to
|
|
11
|
+
* `import()`, the one case that actually needs it.
|
|
12
|
+
*
|
|
13
|
+
* Either path can hand back an ES module namespace rather than a plain object - Node's
|
|
14
|
+
* `require(esm)` support does this too, not just `import()` - so `.default` is preferred whenever
|
|
15
|
+
* present.
|
|
16
|
+
*
|
|
17
|
+
* **It lives here because `extends` needs it too, and did not have it.** A directory's own
|
|
18
|
+
* `.rmanrc.cjs` went through this while `extends-config.ts` used a bare `await import()`, so the
|
|
19
|
+
* *same file* loaded correctly as a directory's config and came back empty when another config
|
|
20
|
+
* named it - and an empty object is a valid config, so nothing was reported. Measured under mocha:
|
|
21
|
+
* a base declaring `"[*]": { version: { stamp: ['build'] } }`, reached by
|
|
22
|
+
* `extends: './base.cjs'`, contributed nothing; the identical fixture with `base.json` contributed
|
|
23
|
+
* normally, and the same `.cjs` worked from the CLI, where no loader hook is registered.
|
|
24
|
+
*
|
|
25
|
+
* `createRequire` is based on this module's own URL rather than on the config file. That is right
|
|
26
|
+
* here and *not* right for resolving a bare specifier - see `resolveConfigTarget`, which is based
|
|
27
|
+
* on the config file so a package name resolves through the repository's `node_modules` rather than
|
|
28
|
+
* rman's own. By the time a file reaches this function it is already an absolute path.
|
|
29
|
+
*/
|
|
30
|
+
export async function loadConfigModule(file) {
|
|
31
|
+
let mod;
|
|
32
|
+
try {
|
|
33
|
+
mod = requireConfigModule(file);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e?.code !== 'ERR_REQUIRE_ESM')
|
|
37
|
+
throw e;
|
|
38
|
+
mod = await import(pathToFileURL(file).href);
|
|
39
|
+
}
|
|
40
|
+
return mod?.default ?? mod;
|
|
41
|
+
}
|
|
42
|
+
const requireConfigModule = createRequire(import.meta.url);
|
package/core/manifest.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { RmanApplication } from './application.js';
|
|
1
2
|
import type { Package } from './package.js';
|
|
3
|
+
import type { Platform } from './plugin.js';
|
|
2
4
|
import { type VersionScheme } from './version-scheme.js';
|
|
3
5
|
/**
|
|
4
6
|
* A package's identity, however its ecosystem happens to record it.
|
|
@@ -13,7 +15,7 @@ export interface Manifest {
|
|
|
13
15
|
/** Excluded from publishing by the package's own declaration (`package.json#private`). Not the
|
|
14
16
|
* same as `.rmanrc "publish.skip"`, which is the *repository's* declaration about it. */
|
|
15
17
|
private?: boolean;
|
|
16
|
-
/** The document as the ecosystem wrote it. `
|
|
18
|
+
/** The document as the ecosystem wrote it. The `node` built-in's own commands read `package.json` fields
|
|
17
19
|
* the core has no opinion about (`scripts`, `publishConfig`, `engines`) off this. */
|
|
18
20
|
raw: any;
|
|
19
21
|
}
|
|
@@ -23,7 +25,7 @@ export interface Manifest {
|
|
|
23
25
|
* **The core has no provider.** "The name and version live in a `package.json`" is true of npm and
|
|
24
26
|
* of nothing else - a `Cargo.toml`, a `pyproject.toml` and a `go.mod` each say the same thing
|
|
25
27
|
* differently, and the version is not even in the same *kind* of place in all of them.
|
|
26
|
-
* `
|
|
28
|
+
* The `node` built-in contributes the `package.json` one.
|
|
27
29
|
*
|
|
28
30
|
* Paired with `VersionScheme` on purpose: the ecosystem that decides *where* a version is written
|
|
29
31
|
* is the one that decides *how* it is numbered, so a provider supplies both and a package gets a
|
|
@@ -32,7 +34,7 @@ export interface Manifest {
|
|
|
32
34
|
export interface ManifestProvider {
|
|
33
35
|
/**
|
|
34
36
|
* **The ecosystem this provider speaks for**, surfaced on every package it reads as
|
|
35
|
-
* `Package.provider` - `'node'` for
|
|
37
|
+
* `Package.provider` - `'node'` for the built-in of that name. Short and about the technology, not about the
|
|
36
38
|
* file: `fileName` already says `package.json`, and a name repeating it would tell a caller
|
|
37
39
|
* nothing it did not have.
|
|
38
40
|
*
|
|
@@ -124,6 +126,23 @@ export interface ManifestProvider {
|
|
|
124
126
|
scope?: string;
|
|
125
127
|
unscopedName: string;
|
|
126
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* **What addresses this package** - what a `"[glob]"` block and `--scope`/`--ignore` match
|
|
131
|
+
* against, and what has to be unique within a repository.
|
|
132
|
+
*
|
|
133
|
+
* **The platform's job, because a name is an ecosystem's promise and not rman's.** npm guarantees
|
|
134
|
+
* `package.json#name` exists and identifies the package, so the node built-in has nothing to do
|
|
135
|
+
* here and the default - the manifest's own name - is already its answer. An ecosystem where a
|
|
136
|
+
* package need not be named, or where the name is not unique, returns `undefined` and the
|
|
137
|
+
* repository assigns one with `.rmanrc "name"`.
|
|
138
|
+
*
|
|
139
|
+
* Separate from `Manifest.name`, which is what the package *calls itself*: those coincide for npm
|
|
140
|
+
* and need not anywhere else. A Go module's name is an import path, and `--scope github.com/x/y`
|
|
141
|
+
* is not how anyone would want to address it.
|
|
142
|
+
*
|
|
143
|
+
* Omit it and the manifest's name is used, so this seam existing changes nothing.
|
|
144
|
+
*/
|
|
145
|
+
selector?(manifest: Manifest, dir: string): string | undefined;
|
|
127
146
|
/**
|
|
128
147
|
* Rewrites this manifest's references to in-repo packages that just got a new version.
|
|
129
148
|
*
|
|
@@ -147,35 +166,37 @@ export interface ManifestProvider {
|
|
|
147
166
|
*
|
|
148
167
|
* A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
|
|
149
168
|
* is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
|
|
150
|
-
* `
|
|
169
|
+
* the `node` built-in already adds to `SystemInfo`, and anything this seam grows later can arrive the
|
|
151
170
|
* same way instead of as another top-level export.
|
|
152
171
|
*/
|
|
153
172
|
export declare namespace Manifest {
|
|
154
|
-
/** Registers a provider. Called by `loadPlugins` for each plugin's `manifest`, in `plugins`
|
|
155
|
-
* declaration order - so which one answers is a function of the repository's own config. */
|
|
156
|
-
function addProvider(provider: ManifestProvider): void;
|
|
157
|
-
/** For tests, which would otherwise leak a provider into every later case in the process. */
|
|
158
|
-
function clearProviders(): void;
|
|
159
173
|
/**
|
|
160
|
-
* Reads `dir`'s manifest through the
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* **
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* `
|
|
168
|
-
*
|
|
174
|
+
* Reads `dir`'s manifest through **the platform that already claimed it**, with the scheme that
|
|
175
|
+
* platform brings.
|
|
176
|
+
*
|
|
177
|
+
* **It takes a platform rather than searching for one**, and that is where the walk changed
|
|
178
|
+
* things. This used to loop over every registered platform, once per `Package` constructed - so a
|
|
179
|
+
* package asked "who am I?" and the answer was whoever recognized it first, re-derived at every
|
|
180
|
+
* construction. The walk decides it once per directory now (`Workspace.walk` ->
|
|
181
|
+
* `app.platformFor`) and hands it to the package, which is also what lets a nested Cargo package
|
|
182
|
+
* sit inside a Node monorepo: the platform is a fact about the directory, established by whoever
|
|
183
|
+
* found it, not re-guessed by whoever reads it.
|
|
184
|
+
*
|
|
185
|
+
* **When that platform reads nothing** - `basePlatform`, or a provider that claimed the directory
|
|
186
|
+
* and then found nothing in it - the fallback is a package named after its own directory at
|
|
187
|
+
* version `0.0.0`. Deliberately the least it can claim: the directory name is a fact, and `0.0.0`
|
|
188
|
+
* is the version a thing has when nothing says otherwise. The alternative - refusing to construct
|
|
189
|
+
* a package at all - would make `rman info` and `rman list` fail in a repository whose `.rmanrc`
|
|
190
|
+
* simply names no plugin yet, which is exactly when someone needs to run them.
|
|
169
191
|
*/
|
|
170
|
-
function read(dir: string): {
|
|
192
|
+
function read(platform: Platform, dir: string): {
|
|
171
193
|
manifest: Manifest;
|
|
172
194
|
versionScheme: VersionScheme;
|
|
173
195
|
fileName: string;
|
|
174
|
-
provider: string;
|
|
175
196
|
};
|
|
176
197
|
/** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
|
|
177
198
|
* nowhere is worse than one that fails, since the caller has already decided the new version. */
|
|
178
|
-
function write(dir: string, manifest: Manifest): void;
|
|
199
|
+
function write(app: RmanApplication, dir: string, manifest: Manifest): void;
|
|
179
200
|
/**
|
|
180
201
|
* The packages `pkg` declares a dependency on, via whichever provider recognizes it.
|
|
181
202
|
*
|
|
@@ -186,7 +207,7 @@ export declare namespace Manifest {
|
|
|
186
207
|
function dependenciesOf(pkg: Package, candidates: readonly Package[]): Package[];
|
|
187
208
|
/** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
|
|
188
209
|
* "no scope, the name is its own unscoped form" when none has an opinion. */
|
|
189
|
-
function splitName(dir: string, name: string): {
|
|
210
|
+
function splitName(app: RmanApplication, dir: string, name: string): {
|
|
190
211
|
scope?: string;
|
|
191
212
|
unscopedName: string;
|
|
192
213
|
};
|
|
@@ -199,7 +220,7 @@ export declare namespace Manifest {
|
|
|
199
220
|
function updateDependencyVersions(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
|
|
200
221
|
/**
|
|
201
222
|
* Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
|
|
202
|
-
* `
|
|
223
|
+
* `Plugin.stampVersion`.
|
|
203
224
|
*
|
|
204
225
|
* `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
|
|
205
226
|
* provider claimed the package, the provider has no opinion about stamping, or it looked and found
|
|
@@ -217,6 +238,7 @@ export declare namespace Manifest {
|
|
|
217
238
|
* plugin: git tags then answer the boundary question alone.
|
|
218
239
|
*/
|
|
219
240
|
function publishedVersion(pkg: Package): Promise<string | undefined>;
|
|
220
|
-
/** The file names
|
|
221
|
-
|
|
241
|
+
/** The file names the registered technologies look for, for an error message that can say what
|
|
242
|
+
* was expected. */
|
|
243
|
+
function fileNames(app: RmanApplication): string[];
|
|
222
244
|
}
|
package/core/manifest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { RmanApplication } from './application.js';
|
|
2
3
|
import { semverScheme } from './version-scheme.js';
|
|
3
4
|
/**
|
|
4
5
|
* The registry, merged onto the `Manifest` interface so one name carries both the shape and the
|
|
@@ -6,71 +7,53 @@ import { semverScheme } from './version-scheme.js';
|
|
|
6
7
|
*
|
|
7
8
|
* A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
|
|
8
9
|
* is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
|
|
9
|
-
* `
|
|
10
|
+
* the `node` built-in already adds to `SystemInfo`, and anything this seam grows later can arrive the
|
|
10
11
|
* same way instead of as another top-level export.
|
|
11
12
|
*/
|
|
12
13
|
export var Manifest;
|
|
13
14
|
(function (Manifest) {
|
|
14
|
-
/** Registers a provider. Called by `loadPlugins` for each plugin's `manifest`, in `plugins`
|
|
15
|
-
* declaration order - so which one answers is a function of the repository's own config. */
|
|
16
|
-
function addProvider(provider) {
|
|
17
|
-
if (providers.includes(provider))
|
|
18
|
-
return;
|
|
19
|
-
providers.push(provider);
|
|
20
|
-
}
|
|
21
|
-
Manifest.addProvider = addProvider;
|
|
22
|
-
/** For tests, which would otherwise leak a provider into every later case in the process. */
|
|
23
|
-
function clearProviders() {
|
|
24
|
-
providers.length = 0;
|
|
25
|
-
}
|
|
26
|
-
Manifest.clearProviders = clearProviders;
|
|
27
15
|
/**
|
|
28
|
-
* Reads `dir`'s manifest through the
|
|
29
|
-
*
|
|
16
|
+
* Reads `dir`'s manifest through **the platform that already claimed it**, with the scheme that
|
|
17
|
+
* platform brings.
|
|
18
|
+
*
|
|
19
|
+
* **It takes a platform rather than searching for one**, and that is where the walk changed
|
|
20
|
+
* things. This used to loop over every registered platform, once per `Package` constructed - so a
|
|
21
|
+
* package asked "who am I?" and the answer was whoever recognized it first, re-derived at every
|
|
22
|
+
* construction. The walk decides it once per directory now (`Workspace.walk` ->
|
|
23
|
+
* `app.platformFor`) and hands it to the package, which is also what lets a nested Cargo package
|
|
24
|
+
* sit inside a Node monorepo: the platform is a fact about the directory, established by whoever
|
|
25
|
+
* found it, not re-guessed by whoever reads it.
|
|
30
26
|
*
|
|
31
|
-
* **
|
|
32
|
-
*
|
|
33
|
-
* the directory name is a fact, and `0.0.0`
|
|
34
|
-
*
|
|
35
|
-
* `rman list` fail in a repository whose `.rmanrc`
|
|
36
|
-
* when someone needs to run them.
|
|
27
|
+
* **When that platform reads nothing** - `basePlatform`, or a provider that claimed the directory
|
|
28
|
+
* and then found nothing in it - the fallback is a package named after its own directory at
|
|
29
|
+
* version `0.0.0`. Deliberately the least it can claim: the directory name is a fact, and `0.0.0`
|
|
30
|
+
* is the version a thing has when nothing says otherwise. The alternative - refusing to construct
|
|
31
|
+
* a package at all - would make `rman info` and `rman list` fail in a repository whose `.rmanrc`
|
|
32
|
+
* simply names no plugin yet, which is exactly when someone needs to run them.
|
|
37
33
|
*/
|
|
38
|
-
function read(dir) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (manifest) {
|
|
42
|
-
return {
|
|
43
|
-
manifest,
|
|
44
|
-
versionScheme: provider.versionScheme ?? semverScheme,
|
|
45
|
-
fileName: provider.fileName,
|
|
46
|
-
provider: provider.name,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
}
|
|
34
|
+
function read(platform, dir) {
|
|
35
|
+
const provider = platform.manifestProvider;
|
|
36
|
+
const manifest = provider.read(dir);
|
|
50
37
|
return {
|
|
51
|
-
manifest: { name: path.basename(dir), version: '0.0.0', raw: {} },
|
|
52
|
-
versionScheme: semverScheme,
|
|
53
|
-
/** Nothing
|
|
54
|
-
*
|
|
55
|
-
fileName: '',
|
|
56
|
-
/** Same reasoning: no provider claimed this directory, so it belongs to no ecosystem. Empty
|
|
57
|
-
* rather than a sentinel like `'unknown'`, which would read as an ecosystem's name and could
|
|
58
|
-
* collide with a real provider's. */
|
|
59
|
-
provider: '',
|
|
38
|
+
manifest: manifest ?? { name: path.basename(dir), version: '0.0.0', raw: {} },
|
|
39
|
+
versionScheme: provider.versionScheme ?? semverScheme,
|
|
40
|
+
/** Nothing read means nothing to name - a caller listing "the file I changed" has no file to
|
|
41
|
+
* list, which is correct rather than a placeholder that does not exist. */
|
|
42
|
+
fileName: manifest ? provider.fileName : '',
|
|
60
43
|
};
|
|
61
44
|
}
|
|
62
45
|
Manifest.read = read;
|
|
63
46
|
/** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
|
|
64
47
|
* nowhere is worse than one that fails, since the caller has already decided the new version. */
|
|
65
|
-
function write(dir, manifest) {
|
|
66
|
-
for (const
|
|
67
|
-
if (
|
|
68
|
-
|
|
48
|
+
function write(app, dir, manifest) {
|
|
49
|
+
for (const platform of app.platforms) {
|
|
50
|
+
if (platform.manifestProvider.read(dir)) {
|
|
51
|
+
platform.manifestProvider.write(dir, manifest);
|
|
69
52
|
return;
|
|
70
53
|
}
|
|
71
54
|
}
|
|
72
55
|
throw new Error(`No manifest provider recognizes "${dir}", so there is nowhere to write its version.\n` +
|
|
73
|
-
` A repository's ".rmanrc" names its
|
|
56
|
+
` A repository's ".rmanrc" names its technologies - see "plugins" (e.g. ['node']).`);
|
|
74
57
|
}
|
|
75
58
|
Manifest.write = write;
|
|
76
59
|
/**
|
|
@@ -81,16 +64,16 @@ export var Manifest;
|
|
|
81
64
|
* of can still describe its own graph by hand.
|
|
82
65
|
*/
|
|
83
66
|
function dependenciesOf(pkg, candidates) {
|
|
84
|
-
return
|
|
67
|
+
return platformOf(pkg)?.manifestProvider.dependencies?.(pkg.manifest, candidates) ?? [];
|
|
85
68
|
}
|
|
86
69
|
Manifest.dependenciesOf = dependenciesOf;
|
|
87
70
|
/** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
|
|
88
71
|
* "no scope, the name is its own unscoped form" when none has an opinion. */
|
|
89
|
-
function splitName(dir, name) {
|
|
90
|
-
for (const
|
|
91
|
-
if (!
|
|
72
|
+
function splitName(app, dir, name) {
|
|
73
|
+
for (const platform of app.platforms) {
|
|
74
|
+
if (!platform.manifestProvider.read(dir))
|
|
92
75
|
continue;
|
|
93
|
-
return
|
|
76
|
+
return platform.manifestProvider.splitName?.(name) ?? { unscopedName: name };
|
|
94
77
|
}
|
|
95
78
|
return { unscopedName: name };
|
|
96
79
|
}
|
|
@@ -102,19 +85,19 @@ export var Manifest;
|
|
|
102
85
|
* reference each other by path has nothing here to go stale.
|
|
103
86
|
*/
|
|
104
87
|
function updateDependencyVersions(pkg, bumped) {
|
|
105
|
-
|
|
88
|
+
platformOf(pkg)?.manifestProvider.updateDependencyVersions?.(pkg.manifest, bumped);
|
|
106
89
|
}
|
|
107
90
|
Manifest.updateDependencyVersions = updateDependencyVersions;
|
|
108
91
|
/**
|
|
109
92
|
* Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
|
|
110
|
-
* `
|
|
93
|
+
* `Plugin.stampVersion`.
|
|
111
94
|
*
|
|
112
95
|
* `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
|
|
113
96
|
* provider claimed the package, the provider has no opinion about stamping, or it looked and found
|
|
114
97
|
* nothing to change. All three mean "this file was not stamped", which is what `version` reports.
|
|
115
98
|
*/
|
|
116
99
|
function stampVersion(pkg, file, content, version, options) {
|
|
117
|
-
return
|
|
100
|
+
return platformOf(pkg)?.manifestProvider.stampVersion?.(file, content, version, options);
|
|
118
101
|
}
|
|
119
102
|
Manifest.stampVersion = stampVersion;
|
|
120
103
|
/**
|
|
@@ -126,25 +109,24 @@ export var Manifest;
|
|
|
126
109
|
* plugin: git tags then answer the boundary question alone.
|
|
127
110
|
*/
|
|
128
111
|
async function publishedVersion(pkg) {
|
|
129
|
-
return
|
|
112
|
+
return platformOf(pkg)?.manifestProvider.publishedVersion?.(pkg);
|
|
130
113
|
}
|
|
131
114
|
Manifest.publishedVersion = publishedVersion;
|
|
132
|
-
/** The file names
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
/** The file names the registered technologies look for, for an error message that can say what
|
|
116
|
+
* was expected. */
|
|
117
|
+
function fileNames(app) {
|
|
118
|
+
return [...app.platforms].map(platform => platform.manifestProvider.fileName);
|
|
135
119
|
}
|
|
136
120
|
Manifest.fileNames = fileNames;
|
|
137
|
-
const providers = [];
|
|
138
121
|
/**
|
|
139
|
-
* The
|
|
140
|
-
* without re-reading the manifest off disk to work it out again.
|
|
122
|
+
* The technology that claimed `pkg`, or `undefined` when none did - with nothing to look up.
|
|
141
123
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
124
|
+
* This used to search a registry by `pkg.provider` and fall back to re-reading the directory,
|
|
125
|
+
* because a package could be constructed before the provider that would claim it was registered.
|
|
126
|
+
* It cannot now: a package is handed its `Platform` at construction, by the application that
|
|
127
|
+
* resolved it. The `name` test is what "none claimed it" looks like - see `basePlatform`.
|
|
145
128
|
*/
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
return byName ?? providers.find(p => p.read(pkg.dirname));
|
|
129
|
+
function platformOf(pkg) {
|
|
130
|
+
return pkg.platform.name ? pkg.platform : undefined;
|
|
149
131
|
}
|
|
150
132
|
})(Manifest || (Manifest = {}));
|