rman 1.0.12 → 1.1.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.
Files changed (98) hide show
  1. package/README.md +90 -70
  2. package/cli.js +225 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +60 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +142 -13
  17. package/core/config.js +266 -43
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +57 -0
  25. package/core/merge-config.js +146 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +77 -1
  31. package/core/repository.js +242 -129
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/version-scheme.d.ts +134 -0
  35. package/core/version-scheme.js +148 -0
  36. package/core/workspace.d.ts +68 -0
  37. package/core/workspace.js +83 -0
  38. package/index.d.ts +54 -8
  39. package/index.js +42 -7
  40. package/interfaces/rman-config.interface.d.ts +171 -36
  41. package/package.json +15 -7
  42. package/services/change-hash.service.d.ts +88 -0
  43. package/services/change-hash.service.js +112 -0
  44. package/services/changelog.service.d.ts +8 -13
  45. package/services/changelog.service.js +12 -11
  46. package/services/conventional-commits.service.d.ts +73 -0
  47. package/services/conventional-commits.service.js +116 -0
  48. package/services/docker-publish.service.js +1 -1
  49. package/services/exec.service.js +1 -1
  50. package/services/github-release.service.d.ts +2 -2
  51. package/services/github-release.service.js +10 -5
  52. package/services/list.service.js +5 -2
  53. package/services/run.service.d.ts +60 -0
  54. package/services/run.service.js +109 -65
  55. package/services/system-info.d.ts +22 -7
  56. package/services/system-info.js +8 -23
  57. package/services/version-plan.service.d.ts +244 -0
  58. package/services/version-plan.service.js +414 -0
  59. package/services/version.service.d.ts +92 -82
  60. package/services/version.service.js +219 -433
  61. package/services.d.ts +5 -3
  62. package/services.js +5 -3
  63. package/utils/bin-path.d.ts +59 -0
  64. package/utils/bin-path.js +82 -0
  65. package/utils/child-tracker.d.ts +16 -0
  66. package/utils/child-tracker.js +30 -0
  67. package/utils/exec.d.ts +13 -2
  68. package/utils/exec.js +17 -17
  69. package/utils/git.d.ts +9 -3
  70. package/utils/git.js +10 -2
  71. package/utils/package-filter.d.ts +33 -2
  72. package/utils/package-filter.js +47 -7
  73. package/utils/release-version.js +3 -3
  74. package/utils/run-bin.d.ts +46 -0
  75. package/utils/run-bin.js +63 -0
  76. package/utils/version-stamp.d.ts +14 -6
  77. package/utils/version-stamp.js +25 -13
  78. package/commands/ci.command.js +0 -30
  79. package/commands/clean.command.d.ts +0 -3
  80. package/commands/clean.command.js +0 -36
  81. package/commands/publish.command.d.ts +0 -3
  82. package/commands/publish.command.js +0 -225
  83. package/rmanrc.schema.json +0 -392
  84. package/services/ci.service.d.ts +0 -40
  85. package/services/ci.service.js +0 -204
  86. package/services/clean.service.d.ts +0 -42
  87. package/services/clean.service.js +0 -226
  88. package/services/publish.service.d.ts +0 -79
  89. package/services/publish.service.js +0 -273
  90. package/utils/change-hash.d.ts +0 -68
  91. package/utils/change-hash.js +0 -98
  92. package/utils/conventional-commits.d.ts +0 -52
  93. package/utils/conventional-commits.js +0 -90
  94. package/utils/npm-run-path.d.ts +0 -67
  95. package/utils/npm-run-path.js +0 -63
  96. package/utils/workspace-range.d.ts +0 -17
  97. package/utils/workspace-range.js +0 -28
  98. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -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
+ }
@@ -0,0 +1,150 @@
1
+ import path from 'node:path';
2
+ import { semverScheme } from './version-scheme.js';
3
+ /**
4
+ * The registry, merged onto the `Manifest` interface so one name carries both the shape and the
5
+ * operations - `Manifest.read(dir)` returns a `Manifest`.
6
+ *
7
+ * A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
8
+ * is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
9
+ * `rman-node` already adds to `SystemInfo`, and anything this seam grows later can arrive the
10
+ * same way instead of as another top-level export.
11
+ */
12
+ export var Manifest;
13
+ (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
+ /**
28
+ * Reads `dir`'s manifest through the first provider that recognizes it, with the scheme that
29
+ * provider brings.
30
+ *
31
+ * **With no provider registered, or none recognizing the directory**, the fallback is a package
32
+ * named after its own directory at version `0.0.0`. That is deliberately the least it can claim:
33
+ * the directory name is a fact, and `0.0.0` is the version a thing has when nothing says
34
+ * otherwise. The alternative - refusing to construct a package at all - would make `rman info` and
35
+ * `rman list` fail in a repository whose `.rmanrc` simply names no plugin yet, which is exactly
36
+ * when someone needs to run them.
37
+ */
38
+ function read(dir) {
39
+ for (const provider of providers) {
40
+ const manifest = provider.read(dir);
41
+ if (manifest) {
42
+ return {
43
+ manifest,
44
+ versionScheme: provider.versionScheme ?? semverScheme,
45
+ fileName: provider.fileName,
46
+ provider: provider.name,
47
+ };
48
+ }
49
+ }
50
+ return {
51
+ manifest: { name: path.basename(dir), version: '0.0.0', raw: {} },
52
+ versionScheme: semverScheme,
53
+ /** Nothing was read, so nothing can be named - a caller listing "the file I changed" has no
54
+ * file to list, which is correct rather than a placeholder that does not exist. */
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: '',
60
+ };
61
+ }
62
+ Manifest.read = read;
63
+ /** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
64
+ * nowhere is worse than one that fails, since the caller has already decided the new version. */
65
+ function write(dir, manifest) {
66
+ for (const provider of providers) {
67
+ if (provider.read(dir)) {
68
+ provider.write(dir, manifest);
69
+ return;
70
+ }
71
+ }
72
+ throw new Error(`No manifest provider recognizes "${dir}", so there is nowhere to write its version.\n` +
73
+ ` A repository's ".rmanrc" names its providers - see "plugins" (e.g. ['rman-node']).`);
74
+ }
75
+ Manifest.write = write;
76
+ /**
77
+ * The packages `pkg` declares a dependency on, via whichever provider recognizes it.
78
+ *
79
+ * No provider, or one with no opinion, means no declared dependencies - `.rmanrc "dependencies"`
80
+ * is then the only source, which is exactly right: a repository rman cannot read the manifests
81
+ * of can still describe its own graph by hand.
82
+ */
83
+ function dependenciesOf(pkg, candidates) {
84
+ return providerOf(pkg)?.dependencies?.(pkg.manifest, candidates) ?? [];
85
+ }
86
+ Manifest.dependenciesOf = dependenciesOf;
87
+ /** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
88
+ * "no scope, the name is its own unscoped form" when none has an opinion. */
89
+ function splitName(dir, name) {
90
+ for (const provider of providers) {
91
+ if (!provider.read(dir))
92
+ continue;
93
+ return provider.splitName?.(name) ?? { unscopedName: name };
94
+ }
95
+ return { unscopedName: name };
96
+ }
97
+ Manifest.splitName = splitName;
98
+ /**
99
+ * Refreshes `pkg`'s references to the packages in `bumped`, via whichever provider recognizes it.
100
+ *
101
+ * No provider, or one without an opinion, means nothing to rewrite - a repository whose packages
102
+ * reference each other by path has nothing here to go stale.
103
+ */
104
+ function updateDependencyVersions(pkg, bumped) {
105
+ providerOf(pkg)?.updateDependencyVersions?.(pkg.manifest, bumped);
106
+ }
107
+ Manifest.updateDependencyVersions = updateDependencyVersions;
108
+ /**
109
+ * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
110
+ * `ManifestProvider.stampVersion`.
111
+ *
112
+ * `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
113
+ * provider claimed the package, the provider has no opinion about stamping, or it looked and found
114
+ * nothing to change. All three mean "this file was not stamped", which is what `version` reports.
115
+ */
116
+ function stampVersion(pkg, file, content, version, options) {
117
+ return providerOf(pkg)?.stampVersion?.(file, content, version, options);
118
+ }
119
+ Manifest.stampVersion = stampVersion;
120
+ /**
121
+ * What `pkg`'s own ecosystem's registry says its current version is - see
122
+ * `ManifestProvider.publishedVersion` for the one thing this is for and the one thing it must
123
+ * never be used for.
124
+ *
125
+ * `undefined` when the provider has no opinion, which includes every repository that names no
126
+ * plugin: git tags then answer the boundary question alone.
127
+ */
128
+ async function publishedVersion(pkg) {
129
+ return providerOf(pkg)?.publishedVersion?.(pkg);
130
+ }
131
+ Manifest.publishedVersion = publishedVersion;
132
+ /** The file names providers look for, for an error message that can say what was expected. */
133
+ function fileNames() {
134
+ return providers.map(p => p.fileName);
135
+ }
136
+ Manifest.fileNames = fileNames;
137
+ const providers = [];
138
+ /**
139
+ * The provider that claimed `pkg`, found by the name it reported as `pkg.provider` - exact, and
140
+ * without re-reading the manifest off disk to work it out again.
141
+ *
142
+ * The probe is the fallback, not the rule: it covers a package constructed *before* its provider
143
+ * was registered, which `Repository.create` cannot produce (plugins load first) but a test
144
+ * arranging providers by hand can.
145
+ */
146
+ function providerOf(pkg) {
147
+ const byName = pkg.provider ? providers.find(p => p.name === pkg.provider) : undefined;
148
+ return byName ?? providers.find(p => p.read(pkg.dirname));
149
+ }
150
+ })(Manifest || (Manifest = {}));
@@ -0,0 +1,57 @@
1
+ /** The prefix that turns a key into an append instead of a replacement: `+before` adds to whatever
2
+ * `before` already resolved to, rather than taking its place. */
3
+ export declare const APPEND_PREFIX = "+";
4
+ /**
5
+ * Keys that **append whether or not you ask** - `+plugins` is accepted and means nothing extra.
6
+ *
7
+ * `plugins` is the whole list because it is the one key where replacing is never what anyone meant:
8
+ * every other setting has a value a closer layer can sensibly overrule, while a plugin *adds
9
+ * commands and seams*, and a repository naming one has no wish to lose the ones its shared config
10
+ * brought. Replacing was the silent failure - `extends`-ing a toolchain config and then adding a
11
+ * plugin of your own dropped the toolchain's, and what you noticed was `Unknown argument: publish`.
12
+ *
13
+ * Do not extend this list casually: a key that always appends can never be *un*-said by a closer
14
+ * layer, which is only acceptable where the value is a set of contributions rather than a decision.
15
+ */
16
+ export declare const ALWAYS_APPEND: readonly string[];
17
+ /** `"+before"` -> `"before"`, or `undefined` for a key that isn't an append. */
18
+ export declare function appendTarget(key: string): string | undefined;
19
+ /**
20
+ * Merges `source` onto `target` in place, the way every layer of rman config is combined - a
21
+ * directory's own file forms, the directory chain, `"[selector]"` blocks, and an `extends` base.
22
+ * One implementation for all of them, so they cannot disagree about what an append means.
23
+ *
24
+ * Plain keys replace, and nested objects merge recursively, as a deep merge always has. What this
25
+ * adds is `+key`:
26
+ *
27
+ * ```yaml
28
+ * # the root says before: "rm ./build"
29
+ * # a package adds +before: "rm ./cache"
30
+ * # it resolves to before: ["rm ./build", "rm ./cache"]
31
+ * ```
32
+ *
33
+ * Appending is the half a shared config can't live without: a base that declares
34
+ * `before: ["rm ./build"]` otherwise forces every repository wanting one more step to restate the
35
+ * whole list - which is a copy of the base, silently frozen at the version it was copied from.
36
+ *
37
+ * A scalar is promoted to a list on the way, so neither side has to be written as an array. With
38
+ * nothing inherited, `+key` simply produces a one-element list.
39
+ *
40
+ * On a value that isn't a list, the prefix is **ignored** rather than an error, because there the
41
+ * two spellings already mean the same thing: objects merge whether or not you asked them to, and a
42
+ * scalar has nothing to append to, so `+key` behaves exactly as `key` would. Nothing is silently
43
+ * dropped - the value still lands.
44
+ *
45
+ * `key` and `+key` in the same object are both honored, in that order: the replacement happens
46
+ * first, then the append lands on top of it.
47
+ */
48
+ export declare function mergeConfig(target: Record<string, any>, source: Record<string, any>): Record<string, any>;
49
+ /**
50
+ * Turns any `+key` still outstanding into its plain key, as a list - the "nothing was inherited"
51
+ * case, where an append simply is the whole value.
52
+ *
53
+ * Called once on a fully resolved config, and only there: until then an outstanding append may
54
+ * still find the layer it belongs to, and a config handed to a command must carry no `+key` at all
55
+ * or every reader would have to know about them.
56
+ */
57
+ export declare function finalizeConfig<T>(config: T): T;