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.
- package/README.md +90 -70
- package/cli.js +225 -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 +60 -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 +142 -13
- package/core/config.js +266 -43
- 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 +57 -0
- package/core/merge-config.js +146 -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 +77 -1
- package/core/repository.js +242 -129
- package/core/resolve-target.d.ts +12 -0
- package/core/resolve-target.js +33 -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 +54 -8
- package/index.js +42 -7
- package/interfaces/rman-config.interface.d.ts +171 -36
- package/package.json +15 -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 +60 -0
- package/services/run.service.js +109 -65
- 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 +92 -82
- package/services/version.service.js +219 -433
- 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/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,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
|
+
}
|
package/core/manifest.js
ADDED
|
@@ -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;
|