rman 1.0.9 → 1.0.12
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 +43 -25
- package/cli.js +2 -0
- package/commands/github-release.command.d.ts +3 -0
- package/commands/github-release.command.js +119 -0
- package/commands/publish.command.js +6 -41
- package/constants.js +1 -1
- package/core/config.d.ts +128 -6
- package/core/config.js +176 -7
- package/core/repository.d.ts +8 -3
- package/core/repository.js +75 -11
- package/interfaces/rman-config.interface.d.ts +98 -30
- package/package.json +1 -1
- package/rmanrc.schema.json +63 -47
- package/services/github-release.service.d.ts +10 -9
- package/services/github-release.service.js +15 -22
- package/services/publish.service.js +113 -47
- package/services/run.service.d.ts +8 -3
- package/services/run.service.js +63 -15
- package/services/version.service.js +67 -3
- package/utils/change-hash.d.ts +2 -2
- package/utils/change-hash.js +2 -2
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/version-stamp.d.ts +40 -0
- package/utils/version-stamp.js +76 -0
package/core/config.js
CHANGED
|
@@ -3,7 +3,9 @@ import * as yaml from 'js-yaml';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import merge from 'putil-merge';
|
|
6
|
+
import semver from 'semver';
|
|
6
7
|
import { pathToFileURL } from 'url';
|
|
8
|
+
import vm from 'vm';
|
|
7
9
|
/**
|
|
8
10
|
* Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
|
|
9
11
|
* autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
|
|
@@ -85,24 +87,93 @@ export async function readDirConfig(dirname) {
|
|
|
85
87
|
return result;
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
|
-
* Resolves the effective config for `targetDir
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Resolves the effective config for the package at `targetDir`, cascading from `rootDir` down to
|
|
91
|
+
* it (inclusive) - each directory level overrides the ones above it, the way tsconfig's `extends`
|
|
92
|
+
* chain does.
|
|
93
|
+
*
|
|
94
|
+
* Every level contributes in two ways, and the difference is the whole model:
|
|
95
|
+
*
|
|
96
|
+
* - **Unmarked keys configure the package of the directory that declares them.** The root's own
|
|
97
|
+
* `.rmanrc` therefore configures the *root package* - which is where repo-wide settings
|
|
98
|
+
* (`packageManager`, `allowBranch`, `version.*`, `githubRelease.*`) are read from anyway - and
|
|
99
|
+
* not, silently, every package under it.
|
|
100
|
+
* - **A `"[selector]"` block configures the packages it names** (`"[*]"` for all of them,
|
|
101
|
+
* `"[*-dialect]"` for a glob over package names). This is the only way a directory speaks about
|
|
102
|
+
* anything but its own package.
|
|
103
|
+
*
|
|
104
|
+
* Splitting the two matters because the same key means different things to the two audiences. The
|
|
105
|
+
* clearest case is `run.<script>.postScript`: on a package it's that package's build hook, run in
|
|
106
|
+
* its own directory; on the root it's a repo-wide bookend run once at the repository root. A
|
|
107
|
+
* cascade that fed one declaration to both ran a package-relative command (`node
|
|
108
|
+
* ../../support/postbuild.cjs`) at the root, where it cannot resolve.
|
|
109
|
+
*
|
|
110
|
+
* `packageName` is what selectors match against; without it (resolving the root's own config, say)
|
|
111
|
+
* selector blocks contribute nothing at all.
|
|
93
112
|
*/
|
|
94
|
-
export async function resolveConfig(rootDir, targetDir, cache = new Map()) {
|
|
113
|
+
export async function resolveConfig(rootDir, targetDir, cache = new Map(), packageName) {
|
|
95
114
|
const result = {};
|
|
115
|
+
const target = path.resolve(targetDir);
|
|
96
116
|
for (const dir of dirChain(rootDir, targetDir)) {
|
|
97
117
|
let local = cache.get(dir);
|
|
98
118
|
if (!local) {
|
|
99
119
|
local = await readDirConfig(dir);
|
|
100
120
|
cache.set(dir, local);
|
|
101
121
|
}
|
|
102
|
-
|
|
122
|
+
// Selectors first, so a directory's own unmarked config still wins over a selector declared
|
|
123
|
+
// alongside it - "this package" is a more specific statement than "packages matching a glob".
|
|
124
|
+
if (packageName) {
|
|
125
|
+
for (const block of matchingSelectors(local, packageName))
|
|
126
|
+
merge(result, block, { deep: true });
|
|
127
|
+
}
|
|
128
|
+
// A directory holding a package speaks for that package only - which is what keeps the root's
|
|
129
|
+
// own config off every package under it. A directory that holds none (an intermediate
|
|
130
|
+
// `packages/`, say) has no package to speak for, so its unmarked config can only mean
|
|
131
|
+
// "everything below" and still cascades.
|
|
132
|
+
const ownsAPackage = fs.existsSync(path.join(dir, 'package.json'));
|
|
133
|
+
if (!ownsAPackage || path.resolve(dir) === target)
|
|
134
|
+
merge(result, stripSelectors(local), { deep: true });
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/** A config key naming packages rather than settings: `"[*]"`, `"[*-dialect]"`, `"[pkg-a]"`. The
|
|
139
|
+
* brackets are what keep this space from colliding with real config keys - no setting starts with
|
|
140
|
+
* one - and in YAML they also mean the key always needs quoting (`"[*]":`), since a bare `[*]`
|
|
141
|
+
* parses as a flow sequence. */
|
|
142
|
+
export function isSelectorKey(key) {
|
|
143
|
+
return key.length > 2 && key.startsWith('[') && key.endsWith(']');
|
|
144
|
+
}
|
|
145
|
+
/** The glob inside a selector key, as a `RegExp` anchored at both ends - so `"[*-dialect]"` matches
|
|
146
|
+
* `mysql-dialect` but not `my-dialect-helper`. Glob rather than regex, to match every other
|
|
147
|
+
* pattern in rman (`allowBranch`, `changelog.tagPattern`, `clean.include`). */
|
|
148
|
+
export function selectorToRegExp(key) {
|
|
149
|
+
const glob = key.slice(1, -1);
|
|
150
|
+
const source = glob
|
|
151
|
+
.split('*')
|
|
152
|
+
.map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
153
|
+
.join('.*');
|
|
154
|
+
return new RegExp(`^${source}$`);
|
|
155
|
+
}
|
|
156
|
+
/** Every selector block in `config` matching `packageName`, in increasing precedence: `"[*]"` first
|
|
157
|
+
* and the rest in declaration order - so a specific glob overrides the catch-all, and two equally
|
|
158
|
+
* specific ones resolve by the order they were written in. */
|
|
159
|
+
function matchingSelectors(config, packageName) {
|
|
160
|
+
const matches = [];
|
|
161
|
+
for (const [key, value] of Object.entries(config)) {
|
|
162
|
+
if (!isSelectorKey(key) || !value || typeof value !== 'object')
|
|
163
|
+
continue;
|
|
164
|
+
if (selectorToRegExp(key).test(packageName))
|
|
165
|
+
matches.push([key, value]);
|
|
103
166
|
}
|
|
167
|
+
return matches.sort((a, b) => Number(b[0] === CATCH_ALL) - Number(a[0] === CATCH_ALL)).map(([, block]) => block);
|
|
168
|
+
}
|
|
169
|
+
function stripSelectors(config) {
|
|
170
|
+
const result = {};
|
|
171
|
+
for (const [key, value] of Object.entries(config))
|
|
172
|
+
if (!isSelectorKey(key))
|
|
173
|
+
result[key] = value;
|
|
104
174
|
return result;
|
|
105
175
|
}
|
|
176
|
+
const CATCH_ALL = '[*]';
|
|
106
177
|
function dirChain(rootDir, targetDir) {
|
|
107
178
|
const rel = path.relative(rootDir, targetDir);
|
|
108
179
|
if (!rel || rel === '.' || rel.startsWith('..'))
|
|
@@ -115,3 +186,101 @@ function dirChain(rootDir, targetDir) {
|
|
|
115
186
|
}
|
|
116
187
|
return dirs;
|
|
117
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Evaluates every `${{ ... }}` expression in **every** string value of a resolved config, against
|
|
191
|
+
* the package it was resolved for:
|
|
192
|
+
*
|
|
193
|
+
* ```yaml
|
|
194
|
+
* "[*]":
|
|
195
|
+
* clean:
|
|
196
|
+
* include: ["build", "../../coverage/${{ pkg.basename }}"]
|
|
197
|
+
* publish:
|
|
198
|
+
* docker:
|
|
199
|
+
* image: "panates/${{ pkg.basename }}:${{ semver.major(pkg.version) }}"
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* Every string, with no list of "interpolated keys" to memorize - a rule with exceptions is a rule
|
|
203
|
+
* nobody remembers.
|
|
204
|
+
*
|
|
205
|
+
* The contents are **real JavaScript**, not a template mini-language, so there is no growing list
|
|
206
|
+
* of substitutions to keep adding (`{{major}}`, `{{scope}}`, ...) - see `ConfigScope` for what is
|
|
207
|
+
* in scope.
|
|
208
|
+
*
|
|
209
|
+
* **`${{ }}`, deliberately not `{{ }}`.** A config value may legitimately carry `{{...}}` meant for
|
|
210
|
+
* something else entirely (`helm template --set tag={{.Values.tag}}`); with the plainer delimiter
|
|
211
|
+
* rman would try to evaluate it. To emit a literal, let an expression produce it, the way GitHub
|
|
212
|
+
* Actions does: `${{ '${{' }}`.
|
|
213
|
+
*
|
|
214
|
+
* A string that is *nothing but* one expression keeps the value's own type (`"${{ pkg.private }}"`
|
|
215
|
+
* -> a boolean), since otherwise this could only ever produce strings and settings like
|
|
216
|
+
* `run.<script>.skip` would be unreachable. Embedded in surrounding text it is stringified.
|
|
217
|
+
*
|
|
218
|
+
* Evaluation happens in a fresh V8 context holding only the scope's bindings. That is a clean
|
|
219
|
+
* scope, **not a sandbox** - `node:vm` is explicitly not a security mechanism, and no sandbox is
|
|
220
|
+
* called for here anyway: a `.rmanrc` that can say `exec: "..."` already runs arbitrary shell, so
|
|
221
|
+
* the expression evaluator adds no trust boundary that wasn't already wide open.
|
|
222
|
+
*
|
|
223
|
+
* A failing expression throws with the config path that holds it, rather than being left in place:
|
|
224
|
+
* silently passing through a mistake is how a config ends up quietly doing nothing.
|
|
225
|
+
*/
|
|
226
|
+
export function interpolateConfig(config, scope) {
|
|
227
|
+
const context = vm.createContext({ ...scope });
|
|
228
|
+
return walk(config, scope, context, []);
|
|
229
|
+
}
|
|
230
|
+
const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
|
|
231
|
+
function walk(value, scope, context, at) {
|
|
232
|
+
if (typeof value === 'string')
|
|
233
|
+
return interpolateString(value, context, at);
|
|
234
|
+
if (Array.isArray(value))
|
|
235
|
+
return value.map((item, i) => walk(item, scope, context, [...at, i]));
|
|
236
|
+
if (value && typeof value === 'object') {
|
|
237
|
+
const result = {};
|
|
238
|
+
for (const [key, item] of Object.entries(value))
|
|
239
|
+
result[key] = walk(item, scope, context, [...at, key]);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
function interpolateString(value, context, at) {
|
|
245
|
+
if (!value.includes('${{'))
|
|
246
|
+
return value;
|
|
247
|
+
const found = [...value.matchAll(EXPRESSION)];
|
|
248
|
+
if (!found.length)
|
|
249
|
+
return value;
|
|
250
|
+
/** Counted rather than matched with an anchored `^...$` regex: a lazy quantifier still backtracks
|
|
251
|
+
* to satisfy an end anchor, so `"${{ a }} and ${{ b }}"` looked like *one* expression whose body
|
|
252
|
+
* ran from `a` to `b`, brace-ends and all - invalid JavaScript. */
|
|
253
|
+
const soleExpression = found.length === 1 && found[0][0] === value.trim();
|
|
254
|
+
// Alone, a nullish result is just "this setting is unset" - a legitimate answer.
|
|
255
|
+
if (soleExpression)
|
|
256
|
+
return evaluate(found[0][1], value, context, at);
|
|
257
|
+
return value.replace(EXPRESSION, (_, expr) => {
|
|
258
|
+
const result = evaluate(expr, value, context, at);
|
|
259
|
+
/** Embedded in text, though, it never is: splicing in the word "undefined" produces a path or
|
|
260
|
+
* tag like `app:undefined` that looks plausible and is wrong - the exact silent-mistake shape
|
|
261
|
+
* this evaluator exists to avoid. `?? 'fallback'` says what was meant. */
|
|
262
|
+
if (result === undefined || result === null) {
|
|
263
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
264
|
+
throw new Error(`Expression in "${where}" is ${result} inside a string: ${value.trim()}\n` +
|
|
265
|
+
` \${{${expr}}} has no value here - give it a fallback (\${{${expr.trim()} ?? '...'}}).`);
|
|
266
|
+
}
|
|
267
|
+
return String(result);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** Names the config path as well as the expression: an error saying only "x is not defined" sends
|
|
271
|
+
* the reader hunting through a file that may hold dozens of them. */
|
|
272
|
+
function evaluate(expr, source, context, at) {
|
|
273
|
+
try {
|
|
274
|
+
return vm.runInContext(expr, context, { timeout: EXPRESSION_TIMEOUT });
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
const where = at.length ? formatPath(at) : 'the config root';
|
|
278
|
+
throw new Error(`Invalid expression in "${where}": ${source.trim()}\n ${e?.message ?? e}`, { cause: e });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function formatPath(at) {
|
|
282
|
+
return at.reduce((acc, part) => (typeof part === 'number' ? `${acc}[${part}]` : acc ? `${acc}.${part}` : String(part)), '');
|
|
283
|
+
}
|
|
284
|
+
/** Guards against an expression that never returns (`while(true)`) taking the whole command with
|
|
285
|
+
* it - a typo, not an attack, but the failure mode is identical. */
|
|
286
|
+
const EXPRESSION_TIMEOUT = 1000;
|
package/core/repository.d.ts
CHANGED
|
@@ -37,9 +37,14 @@ export declare class Repository extends Package {
|
|
|
37
37
|
hash?: string;
|
|
38
38
|
}): Promise<Record<string, Repository.PackageStatus>>;
|
|
39
39
|
/**
|
|
40
|
-
* Resolves the effective rman config for the repository root and every
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Resolves the effective rman config for the repository root and every package, cascading
|
|
41
|
+
* root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
|
|
42
|
+
* that path overrides the levels above it.
|
|
43
|
+
*
|
|
44
|
+
* Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
|
|
45
|
+
* block matches against - see `resolveConfig`. The root package is resolved by name too: in a
|
|
46
|
+
* single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
|
|
47
|
+
* nothing under `getPackages()` is the root, so only its own unmarked config applies.
|
|
43
48
|
*/
|
|
44
49
|
protected _resolveConfigs(): Promise<void>;
|
|
45
50
|
protected _updateDependencies(): void;
|
package/core/repository.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import glob from 'fast-glob';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import path from 'path';
|
|
5
|
+
import semver from 'semver';
|
|
4
6
|
import { GitHelper } from '../utils/git.js';
|
|
5
|
-
import { resolveConfig } from './config.js';
|
|
7
|
+
import { interpolateConfig, resolveConfig } from './config.js';
|
|
6
8
|
import { Package } from './package.js';
|
|
7
9
|
export class Repository extends Package {
|
|
8
10
|
dirname;
|
|
@@ -87,20 +89,55 @@ export class Repository extends Package {
|
|
|
87
89
|
return result;
|
|
88
90
|
}
|
|
89
91
|
/**
|
|
90
|
-
* Resolves the effective rman config for the repository root and every
|
|
91
|
-
*
|
|
92
|
-
*
|
|
92
|
+
* Resolves the effective rman config for the repository root and every package, cascading
|
|
93
|
+
* root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
|
|
94
|
+
* that path overrides the levels above it.
|
|
95
|
+
*
|
|
96
|
+
* Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
|
|
97
|
+
* block matches against - see `resolveConfig`. The root package is resolved by name too: in a
|
|
98
|
+
* single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
|
|
99
|
+
* nothing under `getPackages()` is the root, so only its own unmarked config applies.
|
|
93
100
|
*/
|
|
94
101
|
async _resolveConfigs() {
|
|
95
102
|
const cache = new Map();
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
const repoDir = this.dirname;
|
|
104
|
+
const scopeOf = (pkg) => {
|
|
105
|
+
// A package.json without a "name" is unusual but legal, and `info` prints such a package
|
|
106
|
+
// rather than refusing it - so the scope has to survive one too.
|
|
107
|
+
const name = pkg.name ?? '';
|
|
108
|
+
const at = name.lastIndexOf('/');
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
scope: at > 0 ? name.slice(0, at) : undefined,
|
|
112
|
+
unscopedName: at > 0 ? name.slice(at + 1) : name,
|
|
113
|
+
version: pkg.version ?? '',
|
|
114
|
+
basename: path.basename(pkg.dirname),
|
|
115
|
+
dirname: pkg.dirname,
|
|
116
|
+
relativeDir: path.relative(this.dirname, pkg.dirname),
|
|
117
|
+
// A copy: an expression has no business mutating the package rman is about to act on.
|
|
118
|
+
json: { ...pkg.json },
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
const packageScopes = this.packages.map(scopeOf);
|
|
122
|
+
let gitScope;
|
|
123
|
+
const repository = {
|
|
124
|
+
...scopeOf(this.rootPackage),
|
|
125
|
+
monorepo: this.monorepo,
|
|
126
|
+
packages: packageScopes,
|
|
127
|
+
package: (name) => packageScopes.find(p => p.name === name),
|
|
128
|
+
// A getter, so a repository whose config never mentions git spawns no git at all - and every
|
|
129
|
+
// command resolves config, not just the ones that care.
|
|
130
|
+
get git() {
|
|
131
|
+
return (gitScope ??= readGitScope(repoDir));
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const withVars = (pkg, config) => interpolateConfig(config, { pkg: scopeOf(pkg), repository, env: { ...process.env }, semver });
|
|
135
|
+
this.config = withVars(this.rootPackage, await resolveConfig(this.dirname, this.dirname, cache));
|
|
99
136
|
for (const pkg of this.packages) {
|
|
100
|
-
|
|
101
|
-
continue;
|
|
102
|
-
pkg.config = await resolveConfig(this.dirname, pkg.dirname, cache);
|
|
137
|
+
pkg.config = withVars(pkg, await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name));
|
|
103
138
|
}
|
|
139
|
+
if (this.monorepo)
|
|
140
|
+
this.rootPackage.config = this.config;
|
|
104
141
|
}
|
|
105
142
|
_updateDependencies() {
|
|
106
143
|
const deps = {};
|
|
@@ -111,7 +148,7 @@ export class Repository extends Package {
|
|
|
111
148
|
...pkg.json.peerDependencies,
|
|
112
149
|
...pkg.json.optionalDependencies,
|
|
113
150
|
};
|
|
114
|
-
const configDeps = pkg.config.
|
|
151
|
+
const configDeps = pkg.config.dependencies;
|
|
115
152
|
if (configDeps) {
|
|
116
153
|
if (Array.isArray(configDeps))
|
|
117
154
|
configDeps.forEach(x => (o[x] = o[x] || '*'));
|
|
@@ -205,3 +242,30 @@ function topoSortPackages(packages) {
|
|
|
205
242
|
return 0;
|
|
206
243
|
});
|
|
207
244
|
}
|
|
245
|
+
/** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
|
|
246
|
+
* getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
|
|
247
|
+
* error, just a repository without one. */
|
|
248
|
+
function readGitScope(dirname) {
|
|
249
|
+
const run = (args) => {
|
|
250
|
+
try {
|
|
251
|
+
return execFileSync('git', args, { cwd: dirname, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
252
|
+
.toString()
|
|
253
|
+
.trim();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const sha = run(['rev-parse', 'HEAD']);
|
|
260
|
+
if (sha === undefined)
|
|
261
|
+
return { branch: undefined, sha: undefined, shortSha: undefined, dirty: undefined };
|
|
262
|
+
const status = run(['status', '--porcelain']);
|
|
263
|
+
return {
|
|
264
|
+
// Empty on a detached HEAD, which is what a CI checkout often is - reported as undefined
|
|
265
|
+
// rather than an empty string, so `?? 'detached'` in an expression works.
|
|
266
|
+
branch: run(['branch', '--show-current']) || undefined,
|
|
267
|
+
sha,
|
|
268
|
+
shortSha: sha.slice(0, 7),
|
|
269
|
+
dirty: status === undefined ? undefined : status.length > 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -16,10 +16,45 @@ export interface RmanConfig {
|
|
|
16
16
|
changelog?: RmanConfig.ChangelogOptions;
|
|
17
17
|
clean?: RmanConfig.CleanOptions;
|
|
18
18
|
publish?: RmanConfig.PublishOptions;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
githubRelease?: RmanConfig.GithubReleaseOptions;
|
|
20
|
+
/** Keyed by npm script name (e.g. `"build"`, `"lint"`, `"test"`). A bare string (or array of
|
|
21
|
+
* them) is shorthand for `{ exec: ... }` - `test: "mocha"` and `test: { exec: "mocha" }` mean
|
|
22
|
+
* exactly the same thing. */
|
|
23
|
+
run?: Record<string, string | string[] | RmanConfig.RunScriptOptions>;
|
|
24
|
+
/** In-repo packages this one depends on beyond what its real `package.json` declares - purely
|
|
25
|
+
* for rman's own dependency graph (topo-sort, `--deps`/`--dependents`, `run`'s scheduling). An
|
|
26
|
+
* array defaults each entry's range to `"*"`; an object gives an explicit name -> range map.
|
|
27
|
+
* Declared from the root via a selector (`"[pkg-a]": { dependencies: [...] }`) or in the
|
|
28
|
+
* package's own `.rmanrc`. */
|
|
29
|
+
dependencies?: string[] | Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Config for **other** packages, keyed by a `"[selector]"` naming them - `"[*]"` for every
|
|
32
|
+
* package in the repository, `"[*-dialect]"` for a glob over package names, `"[pkg-a]"` for one.
|
|
33
|
+
* Everything else in this object configures the package of the directory declaring it, so this
|
|
34
|
+
* is the only way a `.rmanrc` speaks about anything but its own package - most usefully the
|
|
35
|
+
* repository root's, which otherwise configures the root package alone.
|
|
36
|
+
*
|
|
37
|
+
* ```yaml
|
|
38
|
+
* # the repository root's own .rmanrc.yml
|
|
39
|
+
* run:
|
|
40
|
+
* build:
|
|
41
|
+
* before: node support/generate.cjs # a repo-wide bookend, run once at the root
|
|
42
|
+
* "[*]":
|
|
43
|
+
* run:
|
|
44
|
+
* build:
|
|
45
|
+
* after: node ../../support/postbuild.cjs # run in each package's own directory
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* In YAML the quotes are **required**: a bare `[*]` parses as a flow sequence, and `*` as an
|
|
49
|
+
* alias indicator. Precedence, lowest first: `"[*]"`, then other selectors in declaration order,
|
|
50
|
+
* then the package's own unmarked config.
|
|
51
|
+
*
|
|
52
|
+
* Recursive, mirroring the schema's own `"$ref": "#"`: whatever a `.rmanrc` may say about its own
|
|
53
|
+
* package it may say here about the ones it names - nested selectors included. Typed as
|
|
54
|
+
* `RmanConfig` rather than `unknown` so the contents are actually checked; `unknown` let any
|
|
55
|
+
* shape through, which is the opposite of the point.
|
|
56
|
+
*/
|
|
57
|
+
[selector: `[${string}]`]: RmanConfig;
|
|
23
58
|
}
|
|
24
59
|
export declare namespace RmanConfig {
|
|
25
60
|
interface VersionOptions {
|
|
@@ -35,9 +70,29 @@ export declare namespace RmanConfig {
|
|
|
35
70
|
* any package's own `changelog.tagPattern`, or that package's changelog boundary will resolve
|
|
36
71
|
* to the repository release instead of its own last release. */
|
|
37
72
|
releaseTagPattern?: string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
/** Keep this package's Dockerfile `org.opencontainers.image.version` label in step with the
|
|
74
|
+
* version being written. Per-package cascaded. Default `true` - the label's value is, by
|
|
75
|
+
* specification, the version of the packaged software, so there is only ever one correct
|
|
76
|
+
* value for it, and `version` is what knows it. Only ever *rewrites* a label the Dockerfile
|
|
77
|
+
* already declares (never inserts one), and reads the same path `publish --target docker`
|
|
78
|
+
* builds from (`publish.docker.dockerfile`), so a package without one is a no-op. */
|
|
79
|
+
stampDockerfile?: boolean;
|
|
80
|
+
/** Source files whose `version` constant is rewritten to the version being written, in the same
|
|
81
|
+
* commit as the bump - paths relative to the package's own directory (e.g.
|
|
82
|
+
* `["src/constants.ts"]`). Per-package cascaded; a listed file a package doesn't have is a
|
|
83
|
+
* silent no-op, so one `"[*]"` declaration covers a repo where only some packages carry one.
|
|
84
|
+
*
|
|
85
|
+
* Stamping the source, not the build output: a build-time rewrite leaves the checked-in file
|
|
86
|
+
* claiming a placeholder, so anything running from source reports that placeholder, git never
|
|
87
|
+
* records the released version, and the rewrite has to be redone on every build. */
|
|
88
|
+
stamp?: string | string[];
|
|
89
|
+
/** Command(s) run as this package's own `version` npm-lifecycle step, when its `package.json`
|
|
90
|
+
* doesn't define one itself. An array runs them in sequence. */
|
|
91
|
+
exec?: string | string[];
|
|
92
|
+
/** Same, for `preversion`. */
|
|
93
|
+
before?: string | string[];
|
|
94
|
+
/** Same, for `postversion`. */
|
|
95
|
+
after?: string | string[];
|
|
41
96
|
}
|
|
42
97
|
interface ChangelogOptions {
|
|
43
98
|
ignoreTypes?: string[];
|
|
@@ -59,26 +114,36 @@ export declare namespace RmanConfig {
|
|
|
59
114
|
changedSince?: string;
|
|
60
115
|
skip?: boolean;
|
|
61
116
|
if?: string;
|
|
62
|
-
script
|
|
63
|
-
|
|
64
|
-
|
|
117
|
+
/** Command(s) to run as this script itself, when the package's `package.json` doesn't define
|
|
118
|
+
* it. An array runs them in sequence. */
|
|
119
|
+
exec?: string | string[];
|
|
120
|
+
/** Same, for this script's `pre<script>` hook. */
|
|
121
|
+
before?: string | string[];
|
|
122
|
+
/** Same, for its `post<script>` hook. */
|
|
123
|
+
after?: string | string[];
|
|
65
124
|
override?: boolean;
|
|
66
125
|
}
|
|
67
|
-
interface PackageOptions {
|
|
68
|
-
dependencies?: string[] | Record<string, string>;
|
|
69
|
-
}
|
|
70
126
|
interface PublishOptions {
|
|
71
|
-
/**
|
|
127
|
+
/** Which **registry** `publish` ships this package to - default `['npm']` (every existing repo
|
|
72
128
|
* keeps working unchanged). A package that only ever wants Docker images (typically also
|
|
73
|
-
* `"private": true`, since it's not meant for npm at all) sets `['docker']`;
|
|
74
|
-
* shipped as GitHub Release assets - or deployed elsewhere entirely, with the release only
|
|
75
|
-
* recording that it happened - sets `['github']`; any combination works (`['npm', 'github']`).
|
|
129
|
+
* `"private": true`, since it's not meant for npm at all) sets `['docker']`; both works too.
|
|
76
130
|
* Each target answers "is this version already out there?" against its own registry, so a
|
|
77
|
-
* package is never left without one: npm via `npm view`, docker via `docker manifest inspect
|
|
78
|
-
*
|
|
131
|
+
* package is never left without one: npm via `npm view`, docker via `docker manifest inspect`.
|
|
132
|
+
*
|
|
133
|
+
* Note this is strictly about *package distribution*. The repository's GitHub Release is not
|
|
134
|
+
* a target here - it isn't a place a package ships to, it's the repository's own record that
|
|
135
|
+
* a release happened, and it is never opted into: see `githubRelease` and the
|
|
136
|
+
* `github-release` command. */
|
|
79
137
|
target?: PublishTarget | PublishTarget[];
|
|
138
|
+
/** Where this package's publishable output lives, relative to its own directory (e.g.
|
|
139
|
+
* `"build"`). Per-package cascaded, so a root `"[*]"` block can say it once for the whole
|
|
140
|
+
* repository instead of repeating `publishConfig.directory` in every `package.json` - which
|
|
141
|
+
* still wins when a package declares it, being the more specific statement.
|
|
142
|
+
*
|
|
143
|
+
* Publishing from such a directory means the manifest there is **generated by `publish`**,
|
|
144
|
+
* from the package's own - see `PublishService`. There is nothing to configure about it. */
|
|
145
|
+
directory?: string;
|
|
80
146
|
docker?: DockerPublishOptions;
|
|
81
|
-
github?: GithubPublishOptions;
|
|
82
147
|
/** Excludes this package from `publish` entirely (every target), regardless of
|
|
83
148
|
* `target`/`"private"` - a single, explicit "never published" statement, e.g. for a package
|
|
84
149
|
* released through some separate, unrelated process. `changelog` also skips it by default
|
|
@@ -87,7 +152,7 @@ export declare namespace RmanConfig {
|
|
|
87
152
|
* package can still be meaningfully versioned without ever being published. */
|
|
88
153
|
skip?: boolean;
|
|
89
154
|
}
|
|
90
|
-
type PublishTarget = 'npm' | 'docker'
|
|
155
|
+
type PublishTarget = 'npm' | 'docker';
|
|
91
156
|
/** Required once `"docker"` is one of this package's `publish.target`s - `publish --target
|
|
92
157
|
* docker` errors clearly on a package that opts in here but leaves this out. */
|
|
93
158
|
interface DockerPublishOptions {
|
|
@@ -112,19 +177,22 @@ export declare namespace RmanConfig {
|
|
|
112
177
|
* full description, if present. Default `"DOCKER_README.md"`. */
|
|
113
178
|
readme?: string;
|
|
114
179
|
}
|
|
115
|
-
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
180
|
+
/** Entirely optional - `github-release` needs no configuration at all, since every required fact
|
|
181
|
+
* (which tag, which repository, what the notes say) already has a sensible source. Nothing here
|
|
182
|
+
* decides *whether* a release is cut: a release records that the repository shipped, so it is
|
|
183
|
+
* always cut, and these are only details about how. */
|
|
184
|
+
interface GithubReleaseOptions {
|
|
119
185
|
/** Files to attach to the release, as glob patterns relative to the package's own directory
|
|
120
|
-
* (e.g. `["dist/*.tar.gz"]`).
|
|
121
|
-
*
|
|
186
|
+
* (e.g. `["dist/*.tar.gz"]`). Read from **every** package, since one release covers the whole
|
|
187
|
+
* source tree. A release with no assets at all is still perfectly valid - it records that the
|
|
188
|
+
* version shipped, which is all a deploy-elsewhere package needs. */
|
|
122
189
|
assets?: string[];
|
|
123
|
-
/** `owner/repo`. Default: parsed from the `origin` remote's URL. */
|
|
190
|
+
/** `owner/repo`. Default: parsed from the `origin` remote's URL. Root-level only. */
|
|
124
191
|
repository?: string;
|
|
125
|
-
/** Create the release as an unpublished draft. Default `false`. */
|
|
192
|
+
/** Create the release as an unpublished draft. Default `false`. Root-level only. */
|
|
126
193
|
draft?: boolean;
|
|
127
|
-
/** Default: whether the version being released is itself a semver prerelease (`1.3.0-beta.0`).
|
|
194
|
+
/** Default: whether the version being released is itself a semver prerelease (`1.3.0-beta.0`).
|
|
195
|
+
* Root-level only. */
|
|
128
196
|
prerelease?: boolean;
|
|
129
197
|
}
|
|
130
198
|
}
|