rman 1.0.12 → 1.2.1
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 +90 -70
- package/cli.js +226 -14
- package/commands/build.command.js +1 -0
- package/commands/changed.command.js +2 -2
- package/commands/changelog.command.js +13 -15
- package/commands/config.command.js +61 -0
- package/commands/diff.command.js +9 -4
- package/commands/exec.command.js +2 -8
- package/commands/github-release.command.js +1 -0
- package/commands/info.command.d.ts +9 -0
- package/commands/info.command.js +12 -2
- package/commands/run.command.js +5 -8
- package/commands/test.command.js +1 -0
- package/commands/version.command.js +53 -14
- package/constants.js +1 -1
- package/core/config.d.ts +265 -17
- package/core/config.js +651 -76
- package/core/custom-command.d.ts +133 -0
- package/core/custom-command.js +99 -0
- package/core/extends-config.d.ts +27 -0
- package/core/extends-config.js +89 -0
- package/core/manifest.d.ts +222 -0
- package/core/manifest.js +150 -0
- package/core/merge-config.d.ts +70 -0
- package/core/merge-config.js +193 -0
- package/core/package.d.ts +73 -7
- package/core/package.js +86 -24
- package/core/plugin.d.ts +112 -0
- package/core/plugin.js +189 -0
- package/core/repository.d.ts +91 -1
- package/core/repository.js +277 -132
- package/core/resolve-target.d.ts +12 -0
- package/core/resolve-target.js +33 -0
- package/core/run-step.d.ts +75 -0
- package/core/run-step.js +1 -0
- package/core/version-scheme.d.ts +134 -0
- package/core/version-scheme.js +148 -0
- package/core/workspace.d.ts +68 -0
- package/core/workspace.js +83 -0
- package/index.d.ts +55 -8
- package/index.js +42 -7
- package/interfaces/rman-config.interface.d.ts +222 -46
- package/package.json +16 -7
- package/services/change-hash.service.d.ts +88 -0
- package/services/change-hash.service.js +112 -0
- package/services/changelog.service.d.ts +8 -13
- package/services/changelog.service.js +12 -11
- package/services/conventional-commits.service.d.ts +73 -0
- package/services/conventional-commits.service.js +116 -0
- package/services/docker-publish.service.js +1 -1
- package/services/exec.service.js +1 -1
- package/services/github-release.service.d.ts +2 -2
- package/services/github-release.service.js +10 -5
- package/services/list.service.js +5 -2
- package/services/run.service.d.ts +112 -6
- package/services/run.service.js +265 -89
- package/services/system-info.d.ts +22 -7
- package/services/system-info.js +8 -23
- package/services/version-plan.service.d.ts +244 -0
- package/services/version-plan.service.js +414 -0
- package/services/version.service.d.ts +102 -82
- package/services/version.service.js +226 -434
- package/services.d.ts +5 -3
- package/services.js +5 -3
- package/utils/bin-path.d.ts +59 -0
- package/utils/bin-path.js +82 -0
- package/utils/child-tracker.d.ts +16 -0
- package/utils/child-tracker.js +30 -0
- package/utils/exec.d.ts +13 -2
- package/utils/exec.js +17 -17
- package/utils/git.d.ts +9 -3
- package/utils/git.js +10 -2
- package/utils/package-filter.d.ts +33 -2
- package/utils/package-filter.js +47 -7
- package/utils/printable-config.d.ts +15 -0
- package/utils/printable-config.js +42 -0
- package/utils/release-version.js +3 -3
- package/utils/run-bin.d.ts +46 -0
- package/utils/run-bin.js +63 -0
- package/utils/version-stamp.d.ts +14 -6
- package/utils/version-stamp.js +25 -13
- package/commands/ci.command.js +0 -30
- package/commands/clean.command.d.ts +0 -3
- package/commands/clean.command.js +0 -36
- package/commands/publish.command.d.ts +0 -3
- package/commands/publish.command.js +0 -225
- package/rmanrc.schema.json +0 -392
- package/services/ci.service.d.ts +0 -40
- package/services/ci.service.js +0 -204
- package/services/clean.service.d.ts +0 -42
- package/services/clean.service.js +0 -226
- package/services/publish.service.d.ts +0 -79
- package/services/publish.service.js +0 -273
- package/utils/change-hash.d.ts +0 -68
- package/utils/change-hash.js +0 -98
- package/utils/conventional-commits.d.ts +0 -52
- package/utils/conventional-commits.js +0 -90
- package/utils/npm-run-path.d.ts +0 -67
- package/utils/npm-run-path.js +0 -63
- package/utils/workspace-range.d.ts +0 -17
- package/utils/workspace-range.js +0 -28
- /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ArgumentsCamelCase, Argv } from 'yargs';
|
|
2
|
+
import type { Logger } from '../utils/logger.js';
|
|
3
|
+
import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
|
|
4
|
+
import type { Package } from './package.js';
|
|
5
|
+
import type { Repository } from './repository.js';
|
|
6
|
+
/** Where a repository keeps its own commands - one module per command, named after it. */
|
|
7
|
+
export declare const CUSTOM_COMMAND_DIR = ".rman";
|
|
8
|
+
/**
|
|
9
|
+
* What a repository's own command is handed. An object rather than loose parameters so later
|
|
10
|
+
* additions don't break every command already written against it.
|
|
11
|
+
*/
|
|
12
|
+
export interface CommandContext {
|
|
13
|
+
repository: Repository;
|
|
14
|
+
/**
|
|
15
|
+
* The package whose directory rman was invoked from, or `undefined` at the repository root (and
|
|
16
|
+
* in a single-package repository, which is always "at the root") - the same
|
|
17
|
+
* `Repository.currentPackage` the built-in commands scope themselves by. A command that only
|
|
18
|
+
* makes sense inside a package should say so itself rather than assume.
|
|
19
|
+
*/
|
|
20
|
+
package: Package | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Runs one of the repository's locally installed binaries - `runBin` (see
|
|
23
|
+
* `../utils/run-bin.ts`), already carrying **this run's** settings: `cwd` defaults to the
|
|
24
|
+
* repository root, and `logLevel` to the level resolved from `--log-level` and `.rmanrc
|
|
25
|
+
* logLevel`. Either can still be overridden per call.
|
|
26
|
+
*
|
|
27
|
+
* Handed over here rather than left to be imported, because those settings are the whole point:
|
|
28
|
+
* importing `runBin` straight from `'rman'` gets a helper that knows neither, so
|
|
29
|
+
* `--log-level silent` would quietly not apply to the one part of the command that produces
|
|
30
|
+
* output. Anything else a run turns out to carry is added here the same way, and no command
|
|
31
|
+
* written against this breaks.
|
|
32
|
+
*/
|
|
33
|
+
runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
|
|
34
|
+
/** Logger at this run's resolved level, for a command's own narration. */
|
|
35
|
+
logger: Logger;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Which `.rmanrc` keys a command reads, for `--config` to print instead of running it - a dotted
|
|
39
|
+
* path each (`'run.build'`, `'publish.docker'`), or a function of the parsed argv when the answer
|
|
40
|
+
* depends on it (`run <script>` reads `run.<script>`).
|
|
41
|
+
*
|
|
42
|
+
* **Declared beside the command rather than in a list somewhere central**, so it cannot drift out
|
|
43
|
+
* of step with the code that does the reading, and so a plugin's command or a `.rman/*.mjs` one can
|
|
44
|
+
* say it too. Omitted, `--config` prints the whole effective config - the honest answer when
|
|
45
|
+
* nothing has said which part matters.
|
|
46
|
+
*/
|
|
47
|
+
export type ConfigKeys = string[] | ((args: ArgumentsCamelCase) => string[]);
|
|
48
|
+
export interface CustomCommand {
|
|
49
|
+
/** yargs command string, for a command taking positionals (`'deploy <stage>'`). Defaults to the
|
|
50
|
+
* module's own file name, which is the whole point of the directory. */
|
|
51
|
+
command?: string;
|
|
52
|
+
/** Required: without it `rman --help` has nothing to list the command by. */
|
|
53
|
+
describe: string;
|
|
54
|
+
builder?: (argv: Argv) => Argv;
|
|
55
|
+
handler: (context: CommandContext, args: ArgumentsCamelCase) => void | Promise<void>;
|
|
56
|
+
/** See `ConfigKeys` - what `rman <this command> --config` narrows its output to. */
|
|
57
|
+
configKeys?: ConfigKeys;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The same field on **yargs's own** command object, which is what the built-in commands pass.
|
|
61
|
+
*
|
|
62
|
+
* By augmentation rather than a wrapper type of ours: `program.command({ ... })` takes a literal,
|
|
63
|
+
* and TypeScript's excess-property check fires on a literal however the parameter is typed - so a
|
|
64
|
+
* field yargs does not know about is a compile error even though it is ignored at runtime
|
|
65
|
+
* (measured, on nine commands at once). One block, here, beside `ConfigKeys` itself.
|
|
66
|
+
*/
|
|
67
|
+
declare module 'yargs' {
|
|
68
|
+
interface CommandModule<T = {}, U = {}> {
|
|
69
|
+
configKeys?: ConfigKeys;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Identity helper for authoring a `.rman/<name>.mjs` command with full type-checking and
|
|
74
|
+
* autocomplete - the same `defineConfig` pattern, for the same reason. Returns `command`
|
|
75
|
+
* unchanged.
|
|
76
|
+
*
|
|
77
|
+
* ```js
|
|
78
|
+
* // .rman/deploy.mjs
|
|
79
|
+
* import { defineCommand, VersionService } from 'rman';
|
|
80
|
+
*
|
|
81
|
+
* export default defineCommand({
|
|
82
|
+
* describe: 'Ships what was just published to the staging cluster',
|
|
83
|
+
* builder: y => y.option('stage', { choices: ['dev', 'prod'], demandOption: true }),
|
|
84
|
+
* async handler({ repository, runBin, logger }, args) {
|
|
85
|
+
* const plan = await VersionService.getPlan(repository);
|
|
86
|
+
* for (const entry of plan.filter(e => e.status === 'bump')) {
|
|
87
|
+
* logger.info(`${entry.package.name} -> ${args.stage}`);
|
|
88
|
+
* }
|
|
89
|
+
* await runBin('helm', ['upgrade', '--install', args.stage, './chart']);
|
|
90
|
+
* },
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Take `runBin` from the context rather than importing it: the one on the context already carries
|
|
95
|
+
* this run's `cwd` (the repository root) and log level.
|
|
96
|
+
*
|
|
97
|
+
* This is for **one repository-level operation with logic of its own** - branching, its own CLI
|
|
98
|
+
* options, rman's services. Running a shell step across every package is what `.rmanrc
|
|
99
|
+
* "run.<script>"` already does, with the scheduling, topological order, `bail` and progress panel
|
|
100
|
+
* that come with it; reimplementing that loop here would only lose them.
|
|
101
|
+
*/
|
|
102
|
+
export declare function defineCommand(command: CustomCommand): CustomCommand;
|
|
103
|
+
export interface LoadedCommand extends CustomCommand {
|
|
104
|
+
/** The command's name - its file's basename, or the first word of an explicit `command`. */
|
|
105
|
+
name: string;
|
|
106
|
+
file: string;
|
|
107
|
+
}
|
|
108
|
+
/** A module that couldn't be loaded or doesn't look like a command. Reported, never thrown: one
|
|
109
|
+
* unparseable file must not take `rman publish` down with it. */
|
|
110
|
+
export interface CommandLoadError {
|
|
111
|
+
file: string;
|
|
112
|
+
reason: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
|
|
116
|
+
* no scan, no imports - which matters because this runs on *every* rman invocation, `info`
|
|
117
|
+
* included.
|
|
118
|
+
*
|
|
119
|
+
* Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
|
|
120
|
+
* warns about them. What is *not* tolerated is a module that would shadow a built-in - see
|
|
121
|
+
* `assertNoBuiltinShadowing`.
|
|
122
|
+
*/
|
|
123
|
+
export declare function loadCustomCommands(rootDir: string): Promise<{
|
|
124
|
+
commands: LoadedCommand[];
|
|
125
|
+
errors: CommandLoadError[];
|
|
126
|
+
}>;
|
|
127
|
+
/**
|
|
128
|
+
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
129
|
+
* this one is thrown: the file is fine, the *name* is the mistake, and there is no reading of
|
|
130
|
+
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
131
|
+
* it unable to tell which ran.
|
|
132
|
+
*/
|
|
133
|
+
export declare function assertNoBuiltinShadowing(commands: LoadedCommand[], builtins: readonly string[]): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
/** Where a repository keeps its own commands - one module per command, named after it. */
|
|
5
|
+
export const CUSTOM_COMMAND_DIR = '.rman';
|
|
6
|
+
/** Loadable module forms, matching what a `.rmanrc.cjs`/`.mjs`/`.js` config already accepts. A
|
|
7
|
+
* `.ts` command would need a loader registered in rman's own process, which is a separate
|
|
8
|
+
* question from this one. */
|
|
9
|
+
const EXTENSIONS = ['.js', '.mjs', '.cjs'];
|
|
10
|
+
/**
|
|
11
|
+
* Identity helper for authoring a `.rman/<name>.mjs` command with full type-checking and
|
|
12
|
+
* autocomplete - the same `defineConfig` pattern, for the same reason. Returns `command`
|
|
13
|
+
* unchanged.
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* // .rman/deploy.mjs
|
|
17
|
+
* import { defineCommand, VersionService } from 'rman';
|
|
18
|
+
*
|
|
19
|
+
* export default defineCommand({
|
|
20
|
+
* describe: 'Ships what was just published to the staging cluster',
|
|
21
|
+
* builder: y => y.option('stage', { choices: ['dev', 'prod'], demandOption: true }),
|
|
22
|
+
* async handler({ repository, runBin, logger }, args) {
|
|
23
|
+
* const plan = await VersionService.getPlan(repository);
|
|
24
|
+
* for (const entry of plan.filter(e => e.status === 'bump')) {
|
|
25
|
+
* logger.info(`${entry.package.name} -> ${args.stage}`);
|
|
26
|
+
* }
|
|
27
|
+
* await runBin('helm', ['upgrade', '--install', args.stage, './chart']);
|
|
28
|
+
* },
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Take `runBin` from the context rather than importing it: the one on the context already carries
|
|
33
|
+
* this run's `cwd` (the repository root) and log level.
|
|
34
|
+
*
|
|
35
|
+
* This is for **one repository-level operation with logic of its own** - branching, its own CLI
|
|
36
|
+
* options, rman's services. Running a shell step across every package is what `.rmanrc
|
|
37
|
+
* "run.<script>"` already does, with the scheduling, topological order, `bail` and progress panel
|
|
38
|
+
* that come with it; reimplementing that loop here would only lose them.
|
|
39
|
+
*/
|
|
40
|
+
export function defineCommand(command) {
|
|
41
|
+
return command;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
|
|
45
|
+
* no scan, no imports - which matters because this runs on *every* rman invocation, `info`
|
|
46
|
+
* included.
|
|
47
|
+
*
|
|
48
|
+
* Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
|
|
49
|
+
* warns about them. What is *not* tolerated is a module that would shadow a built-in - see
|
|
50
|
+
* `assertNoBuiltinShadowing`.
|
|
51
|
+
*/
|
|
52
|
+
export async function loadCustomCommands(rootDir) {
|
|
53
|
+
const dir = path.join(rootDir, CUSTOM_COMMAND_DIR);
|
|
54
|
+
const commands = [];
|
|
55
|
+
const errors = [];
|
|
56
|
+
if (!fs.existsSync(dir))
|
|
57
|
+
return { commands, errors };
|
|
58
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
59
|
+
if (!entry.isFile() || !EXTENSIONS.includes(path.extname(entry.name)))
|
|
60
|
+
continue;
|
|
61
|
+
const file = path.join(dir, entry.name);
|
|
62
|
+
try {
|
|
63
|
+
const mod = await import(pathToFileURL(file).href);
|
|
64
|
+
const command = mod?.default ?? mod?.command;
|
|
65
|
+
if (!command || typeof command !== 'object') {
|
|
66
|
+
throw new Error('no default export - end the module with `export default defineCommand({ ... })`');
|
|
67
|
+
}
|
|
68
|
+
if (typeof command.handler !== 'function')
|
|
69
|
+
throw new Error('"handler" is missing, or is not a function');
|
|
70
|
+
if (typeof command.describe !== 'string' || !command.describe) {
|
|
71
|
+
throw new Error('"describe" is missing - `rman --help` has nothing to list the command by without it');
|
|
72
|
+
}
|
|
73
|
+
const declared = command.command?.trim();
|
|
74
|
+
commands.push({
|
|
75
|
+
...command,
|
|
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],
|
|
78
|
+
file,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
errors.push({ file, reason: e?.message ?? String(e) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { commands, errors };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
89
|
+
* this one is thrown: the file is fine, the *name* is the mistake, and there is no reading of
|
|
90
|
+
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
91
|
+
* it unable to tell which ran.
|
|
92
|
+
*/
|
|
93
|
+
export function assertNoBuiltinShadowing(commands, builtins) {
|
|
94
|
+
const clash = commands.find(c => builtins.includes(c.name));
|
|
95
|
+
if (!clash)
|
|
96
|
+
return;
|
|
97
|
+
throw new Error(`"${path.relative(process.cwd(), clash.file)}" would shadow rman's built-in "${clash.name}" command.\n` +
|
|
98
|
+
` Rename the file, or give it its own name with \`command: '<name>'\`.`);
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RmanConfig } from '../interfaces/rman-config.interface.js';
|
|
2
|
+
/** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
|
|
3
|
+
export declare const EXTENDS_KEY = "extends";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves `config`'s own `extends` into it: every named config is loaded, merged in declaration
|
|
6
|
+
* order, and `config`'s own keys land on top. Returns a new object with no `extends` left in it.
|
|
7
|
+
*
|
|
8
|
+
* The point is a shared package - `extends: "@panates/rman-monorepo"` - so a repository declares
|
|
9
|
+
* its house rules once instead of restating them. `+key` is what makes that liveable (see
|
|
10
|
+
* `mergeConfig`): without it, adding one step to a base's list means copying the list.
|
|
11
|
+
*
|
|
12
|
+
* `from` is the file the `extends` was written in, and everything resolves relative to **it**: a
|
|
13
|
+
* bare specifier through that file's own `node_modules`, a relative path against its directory.
|
|
14
|
+
* Resolving from rman's own location instead would look in rman's dependencies, where a
|
|
15
|
+
* repository's shared config has no reason to be.
|
|
16
|
+
*
|
|
17
|
+
* `seen` carries the chain being resolved, so a config that extends its way back to itself is
|
|
18
|
+
* reported rather than recursed into forever.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveExtends(config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
|
|
21
|
+
/**
|
|
22
|
+
* Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
|
|
23
|
+
* `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
|
|
24
|
+
* that quietly does nothing is worse than one that won't load. Inheritance is a statement about
|
|
25
|
+
* the file, not about the packages it happens to name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertNoSelectorExtends(config: RmanConfig, file: string): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
import { isSelectorKey } from './config.js';
|
|
6
|
+
import { mergeConfig } from './merge-config.js';
|
|
7
|
+
import { resolveConfigTarget } from './resolve-target.js';
|
|
8
|
+
/** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
|
|
9
|
+
export const EXTENDS_KEY = 'extends';
|
|
10
|
+
/**
|
|
11
|
+
* Resolves `config`'s own `extends` into it: every named config is loaded, merged in declaration
|
|
12
|
+
* order, and `config`'s own keys land on top. Returns a new object with no `extends` left in it.
|
|
13
|
+
*
|
|
14
|
+
* The point is a shared package - `extends: "@panates/rman-monorepo"` - so a repository declares
|
|
15
|
+
* its house rules once instead of restating them. `+key` is what makes that liveable (see
|
|
16
|
+
* `mergeConfig`): without it, adding one step to a base's list means copying the list.
|
|
17
|
+
*
|
|
18
|
+
* `from` is the file the `extends` was written in, and everything resolves relative to **it**: a
|
|
19
|
+
* bare specifier through that file's own `node_modules`, a relative path against its directory.
|
|
20
|
+
* Resolving from rman's own location instead would look in rman's dependencies, where a
|
|
21
|
+
* repository's shared config has no reason to be.
|
|
22
|
+
*
|
|
23
|
+
* `seen` carries the chain being resolved, so a config that extends its way back to itself is
|
|
24
|
+
* reported rather than recursed into forever.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveExtends(config, from, seen = []) {
|
|
27
|
+
const declared = config[EXTENDS_KEY];
|
|
28
|
+
if (declared === undefined)
|
|
29
|
+
return config;
|
|
30
|
+
const targets = Array.isArray(declared) ? declared : [declared];
|
|
31
|
+
for (const target of targets) {
|
|
32
|
+
if (typeof target !== 'string' || !target.trim()) {
|
|
33
|
+
throw new Error(`"extends" in "${from}" must be a config name or path, or an array of them`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const base = {};
|
|
37
|
+
for (const target of targets) {
|
|
38
|
+
const file = resolveConfigTarget(target, from, EXTENDS_KEY);
|
|
39
|
+
if (seen.includes(file)) {
|
|
40
|
+
throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
|
|
41
|
+
}
|
|
42
|
+
const loaded = await loadConfigFile(file);
|
|
43
|
+
assertNoSelectorExtends(loaded, file);
|
|
44
|
+
// Recursive: a shared config may itself be built on another.
|
|
45
|
+
mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]));
|
|
46
|
+
}
|
|
47
|
+
const own = { ...config };
|
|
48
|
+
delete own[EXTENDS_KEY];
|
|
49
|
+
return mergeConfig(base, own);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
|
|
53
|
+
* `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
|
|
54
|
+
* that quietly does nothing is worse than one that won't load. Inheritance is a statement about
|
|
55
|
+
* the file, not about the packages it happens to name.
|
|
56
|
+
*/
|
|
57
|
+
export function assertNoSelectorExtends(config, file) {
|
|
58
|
+
for (const [key, value] of Object.entries(config)) {
|
|
59
|
+
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
60
|
+
continue;
|
|
61
|
+
if (EXTENDS_KEY in value) {
|
|
62
|
+
throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
|
|
63
|
+
`statement about this config rather than about the packages the selector names.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Loads one resolved target. YAML and JSON are read directly; anything else goes through the
|
|
68
|
+
* module loader, so a shared config can be a `defineConfig` module with real logic in it. */
|
|
69
|
+
async function loadConfigFile(file) {
|
|
70
|
+
const ext = path.extname(file);
|
|
71
|
+
if (ext === '.yml' || ext === '.yaml') {
|
|
72
|
+
const obj = yaml.load(fs.readFileSync(file, 'utf-8'));
|
|
73
|
+
return asConfig(obj, file);
|
|
74
|
+
}
|
|
75
|
+
if (ext === '.json')
|
|
76
|
+
return asConfig(JSON.parse(fs.readFileSync(file, 'utf-8')), file);
|
|
77
|
+
const mod = await import(pathToFileURL(file).href);
|
|
78
|
+
return asConfig(mod?.default ?? mod, file);
|
|
79
|
+
}
|
|
80
|
+
function asConfig(value, file) {
|
|
81
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
82
|
+
throw new Error(`"${file}" does not export an rman config object`);
|
|
83
|
+
}
|
|
84
|
+
const config = { ...value };
|
|
85
|
+
// Editor tooling only - meaningless once merged, and `additionalProperties` would reject it
|
|
86
|
+
// wherever it ended up.
|
|
87
|
+
delete config.$schema;
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Package } from './package.js';
|
|
2
|
+
import { type VersionScheme } from './version-scheme.js';
|
|
3
|
+
/**
|
|
4
|
+
* A package's identity, however its ecosystem happens to record it.
|
|
5
|
+
*
|
|
6
|
+
* Three fields rman genuinely needs of every package, plus the raw document for the commands that
|
|
7
|
+
* know what ecosystem they are in. Everything rman's core does - grouping, tagging, changelogs,
|
|
8
|
+
* release identity - is expressed in terms of these three and nothing else.
|
|
9
|
+
*/
|
|
10
|
+
export interface Manifest {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
/** Excluded from publishing by the package's own declaration (`package.json#private`). Not the
|
|
14
|
+
* same as `.rmanrc "publish.skip"`, which is the *repository's* declaration about it. */
|
|
15
|
+
private?: boolean;
|
|
16
|
+
/** The document as the ecosystem wrote it. `rman-node`'s own commands read `package.json` fields
|
|
17
|
+
* the core has no opinion about (`scripts`, `publishConfig`, `engines`) off this. */
|
|
18
|
+
raw: any;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where a package's identity is written, and how to change it.
|
|
22
|
+
*
|
|
23
|
+
* **The core has no provider.** "The name and version live in a `package.json`" is true of npm and
|
|
24
|
+
* of nothing else - a `Cargo.toml`, a `pyproject.toml` and a `go.mod` each say the same thing
|
|
25
|
+
* differently, and the version is not even in the same *kind* of place in all of them.
|
|
26
|
+
* `rman-node` contributes the `package.json` one.
|
|
27
|
+
*
|
|
28
|
+
* Paired with `VersionScheme` on purpose: the ecosystem that decides *where* a version is written
|
|
29
|
+
* is the one that decides *how* it is numbered, so a provider supplies both and a package gets a
|
|
30
|
+
* matching pair rather than a `pyproject.toml` read with semver's arithmetic.
|
|
31
|
+
*/
|
|
32
|
+
export interface ManifestProvider {
|
|
33
|
+
/**
|
|
34
|
+
* **The ecosystem this provider speaks for**, surfaced on every package it reads as
|
|
35
|
+
* `Package.provider` - `'node'` for `rman-node`. Short and about the technology, not about the
|
|
36
|
+
* file: `fileName` already says `package.json`, and a name repeating it would tell a caller
|
|
37
|
+
* nothing it did not have.
|
|
38
|
+
*
|
|
39
|
+
* This is what lets code that *does* know one ecosystem check before acting on a package -
|
|
40
|
+
* `if (pkg.provider === 'node')` - which matters most in a repository holding more than one,
|
|
41
|
+
* since `read` is asked per directory and two packages can legitimately answer to different
|
|
42
|
+
* providers.
|
|
43
|
+
*/
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** The file this looks for, relative to a package directory - `'package.json'`. Used to report
|
|
46
|
+
* what was missing, and by `import` when grafting an external repository in. */
|
|
47
|
+
readonly fileName: string;
|
|
48
|
+
/** `undefined` when this directory holds no package of this kind, so another provider gets a
|
|
49
|
+
* turn rather than this one having to throw. */
|
|
50
|
+
read(dir: string): Manifest | undefined;
|
|
51
|
+
/** Writes `manifest` back. Only ever called with a manifest this provider produced. */
|
|
52
|
+
write(dir: string, manifest: Manifest): void;
|
|
53
|
+
/** How this ecosystem numbers versions. Defaults to semver when a provider has no opinion -
|
|
54
|
+
* which is right for Cargo and Go, and wrong for PEP 440, whose provider should say so. */
|
|
55
|
+
readonly versionScheme?: VersionScheme;
|
|
56
|
+
/**
|
|
57
|
+
* The version of `pkg` this ecosystem's registry currently reports, or `undefined` for anything
|
|
58
|
+
* that is not an answer (never published, no network, private with no access).
|
|
59
|
+
*
|
|
60
|
+
* **Read for exactly one purpose, and it is not "has this been published"**: `detectChangeHash`
|
|
61
|
+
* borrows the version string to *guess a tag name*, and uses it only if a tag by that name
|
|
62
|
+
* actually exists in git. The case it covers is rman being adopted onto a repository whose
|
|
63
|
+
* releases predate it - a tag exists but is not in HEAD's ancestry (cut on another branch,
|
|
64
|
+
* rewritten history, a shallow clone), so `git describe` cannot see it. Never compare this
|
|
65
|
+
* against the local manifest version; that is `publish`'s question, and mixing the two is the
|
|
66
|
+
* measured bug the A/B split exists to prevent.
|
|
67
|
+
*
|
|
68
|
+
* Here rather than behind a repository-wide hook because a registry belongs to an *ecosystem*: a
|
|
69
|
+
* polyglot repository asks npm about its `node` packages and crates.io about its `cargo` ones, and
|
|
70
|
+
* only a per-package provider can do that. Optional - an ecosystem with no registry to ask (or a
|
|
71
|
+
* repository that would rather not reach the network) simply leaves git tags as the only source,
|
|
72
|
+
* which is the honest answer rather than a diminished one.
|
|
73
|
+
*/
|
|
74
|
+
publishedVersion?(pkg: Package): Promise<string | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* Rewrites `content` so the version it hard-codes reads `version`, for a file `.rmanrc
|
|
77
|
+
* "version.stamp"` lists - returning `undefined` when there is nothing to change, so the caller
|
|
78
|
+
* writes nothing and can report a file that matched nothing.
|
|
79
|
+
*
|
|
80
|
+
* **Here because how a version is *declared* is the language's**, and this provider is already
|
|
81
|
+
* the ecosystem's representative for exactly that: `write` says where the manifest keeps it,
|
|
82
|
+
* `versionScheme` says how it is numbered, and this says what it looks like in source. Measured
|
|
83
|
+
* before it moved: the core's one pattern stamped a Go `const version = "…"` and a Gradle
|
|
84
|
+
* `version = "…"` but silently missed `const Version`, `__version__`, Rust's
|
|
85
|
+
* `pub const VERSION: &str = "…"` and `pom.xml`'s `<version>` - and "silently" is the part that
|
|
86
|
+
* mattered, since a listed file that matches nothing looked exactly like a file with no version
|
|
87
|
+
* in it.
|
|
88
|
+
*
|
|
89
|
+
* `file` is the absolute path, for a provider that keys off the extension (a `.ts` constant and a
|
|
90
|
+
* `Chart.yaml` are both npm-adjacent and are not the same rewrite). `options.constant` is the
|
|
91
|
+
* identifier the repository says holds it, from the config - a provider whose format has no
|
|
92
|
+
* identifier ignores it.
|
|
93
|
+
*
|
|
94
|
+
* `stampVersionConstant` is exported for the common case; delegating to it is one line.
|
|
95
|
+
*/
|
|
96
|
+
stampVersion?(file: string, content: string, version: string, options?: {
|
|
97
|
+
constant?: string;
|
|
98
|
+
}): string | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Which of `candidates` this package declares a dependency on.
|
|
101
|
+
*
|
|
102
|
+
* The *field names* are the ecosystem's: npm spreads four of them
|
|
103
|
+
* (`dependencies`/`devDependencies`/`peerDependencies`/`optionalDependencies`), Cargo has
|
|
104
|
+
* `[dev-dependencies]` and `[build-dependencies]`, `go.mod` has one `require` block. rman only
|
|
105
|
+
* wants the edges of the graph, so the provider reads its own manifest and returns the packages.
|
|
106
|
+
*
|
|
107
|
+
* Returning **packages rather than names** for the same reason `Package.dependencies` holds
|
|
108
|
+
* them: a name identifies a package only where the ecosystem guarantees uniqueness, and the
|
|
109
|
+
* provider is the only thing that knows how its own ecosystem refers to a dependency.
|
|
110
|
+
*
|
|
111
|
+
* Omit it and a package has no declared dependencies beyond `.rmanrc "dependencies"`.
|
|
112
|
+
*/
|
|
113
|
+
dependencies?(manifest: Manifest, candidates: readonly Package[]): Package[];
|
|
114
|
+
/**
|
|
115
|
+
* Splits a package name into the parts a config expression can ask for - `${{ pkg.scope }}` and
|
|
116
|
+
* `${{ pkg.unscopedName }}`.
|
|
117
|
+
*
|
|
118
|
+
* npm's `@scope/name` is a convention, not a universal: a Go module path (`github.com/x/y`)
|
|
119
|
+
* split on `/` would report a scope of `github.com/x`, and a Cargo crate has no such notion at
|
|
120
|
+
* all. Omit it and a name has no scope and is its own unscoped form, which is the honest answer
|
|
121
|
+
* for an ecosystem without the concept.
|
|
122
|
+
*/
|
|
123
|
+
splitName?(name: string): {
|
|
124
|
+
scope?: string;
|
|
125
|
+
unscopedName: string;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Rewrites this manifest's references to in-repo packages that just got a new version.
|
|
129
|
+
*
|
|
130
|
+
* `bumped` maps a package to the version it is being given. What a "reference" looks like is
|
|
131
|
+
* entirely the ecosystem's: npm has four dependency fields and a `"workspace:"` protocol whose
|
|
132
|
+
* bare selectors resolve at publish time and must *not* be rewritten; Cargo has `path`
|
|
133
|
+
* dependencies that carry no version at all. rman only knows that a bump may leave siblings
|
|
134
|
+
* pointing at the old number.
|
|
135
|
+
*
|
|
136
|
+
* Mutates `manifest.raw` in place - the caller writes it out afterwards, in the same pass that
|
|
137
|
+
* wrote the new version, so there is one file write rather than two.
|
|
138
|
+
*
|
|
139
|
+
* Omit it and nothing is rewritten, which is correct for an ecosystem that references siblings
|
|
140
|
+
* by path.
|
|
141
|
+
*/
|
|
142
|
+
updateDependencyVersions?(manifest: Manifest, bumped: ReadonlyMap<Package, string>): void;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The registry, merged onto the `Manifest` interface so one name carries both the shape and the
|
|
146
|
+
* operations - `Manifest.read(dir)` returns a `Manifest`.
|
|
147
|
+
*
|
|
148
|
+
* A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
|
|
149
|
+
* is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
|
|
150
|
+
* `rman-node` already adds to `SystemInfo`, and anything this seam grows later can arrive the
|
|
151
|
+
* same way instead of as another top-level export.
|
|
152
|
+
*/
|
|
153
|
+
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
|
+
/**
|
|
160
|
+
* Reads `dir`'s manifest through the first provider that recognizes it, with the scheme that
|
|
161
|
+
* provider brings.
|
|
162
|
+
*
|
|
163
|
+
* **With no provider registered, or none recognizing the directory**, the fallback is a package
|
|
164
|
+
* named after its own directory at version `0.0.0`. That is deliberately the least it can claim:
|
|
165
|
+
* the directory name is a fact, and `0.0.0` is the version a thing has when nothing says
|
|
166
|
+
* otherwise. The alternative - refusing to construct a package at all - would make `rman info` and
|
|
167
|
+
* `rman list` fail in a repository whose `.rmanrc` simply names no plugin yet, which is exactly
|
|
168
|
+
* when someone needs to run them.
|
|
169
|
+
*/
|
|
170
|
+
function read(dir: string): {
|
|
171
|
+
manifest: Manifest;
|
|
172
|
+
versionScheme: VersionScheme;
|
|
173
|
+
fileName: string;
|
|
174
|
+
provider: string;
|
|
175
|
+
};
|
|
176
|
+
/** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
|
|
177
|
+
* nowhere is worse than one that fails, since the caller has already decided the new version. */
|
|
178
|
+
function write(dir: string, manifest: Manifest): void;
|
|
179
|
+
/**
|
|
180
|
+
* The packages `pkg` declares a dependency on, via whichever provider recognizes it.
|
|
181
|
+
*
|
|
182
|
+
* No provider, or one with no opinion, means no declared dependencies - `.rmanrc "dependencies"`
|
|
183
|
+
* is then the only source, which is exactly right: a repository rman cannot read the manifests
|
|
184
|
+
* of can still describe its own graph by hand.
|
|
185
|
+
*/
|
|
186
|
+
function dependenciesOf(pkg: Package, candidates: readonly Package[]): Package[];
|
|
187
|
+
/** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
|
|
188
|
+
* "no scope, the name is its own unscoped form" when none has an opinion. */
|
|
189
|
+
function splitName(dir: string, name: string): {
|
|
190
|
+
scope?: string;
|
|
191
|
+
unscopedName: string;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Refreshes `pkg`'s references to the packages in `bumped`, via whichever provider recognizes it.
|
|
195
|
+
*
|
|
196
|
+
* No provider, or one without an opinion, means nothing to rewrite - a repository whose packages
|
|
197
|
+
* reference each other by path has nothing here to go stale.
|
|
198
|
+
*/
|
|
199
|
+
function updateDependencyVersions(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
|
|
200
|
+
/**
|
|
201
|
+
* Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
|
|
202
|
+
* `ManifestProvider.stampVersion`.
|
|
203
|
+
*
|
|
204
|
+
* `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
|
|
205
|
+
* provider claimed the package, the provider has no opinion about stamping, or it looked and found
|
|
206
|
+
* nothing to change. All three mean "this file was not stamped", which is what `version` reports.
|
|
207
|
+
*/
|
|
208
|
+
function stampVersion(pkg: Package, file: string, content: string, version: string, options?: {
|
|
209
|
+
constant?: string;
|
|
210
|
+
}): string | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* What `pkg`'s own ecosystem's registry says its current version is - see
|
|
213
|
+
* `ManifestProvider.publishedVersion` for the one thing this is for and the one thing it must
|
|
214
|
+
* never be used for.
|
|
215
|
+
*
|
|
216
|
+
* `undefined` when the provider has no opinion, which includes every repository that names no
|
|
217
|
+
* plugin: git tags then answer the boundary question alone.
|
|
218
|
+
*/
|
|
219
|
+
function publishedVersion(pkg: Package): Promise<string | undefined>;
|
|
220
|
+
/** The file names providers look for, for an error message that can say what was expected. */
|
|
221
|
+
function fileNames(): string[];
|
|
222
|
+
}
|