rman 0.38.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +371 -21
- package/cli.js +51 -29
- package/commands/build.command.d.ts +3 -0
- package/commands/build.command.js +14 -0
- package/commands/changed.command.d.ts +3 -0
- package/commands/changed.command.js +29 -0
- package/commands/changelog.command.d.ts +3 -0
- package/commands/changelog.command.js +63 -0
- package/commands/ci.command.d.ts +3 -0
- package/commands/ci.command.js +30 -0
- package/commands/clean.command.d.ts +3 -0
- package/commands/clean.command.js +36 -0
- package/commands/diff.command.d.ts +3 -0
- package/commands/diff.command.js +51 -0
- package/commands/exec.command.d.ts +3 -0
- package/commands/exec.command.js +83 -0
- package/commands/import.command.d.ts +3 -0
- package/commands/import.command.js +27 -0
- package/commands/info.command.d.ts +3 -0
- package/commands/info.command.js +61 -0
- package/commands/list.command.d.ts +3 -0
- package/commands/list.command.js +113 -0
- package/commands/publish.command.d.ts +3 -0
- package/commands/publish.command.js +147 -0
- package/commands/run.command.d.ts +6 -0
- package/commands/run.command.js +80 -0
- package/commands/test.command.d.ts +3 -0
- package/commands/test.command.js +14 -0
- package/commands/version.command.d.ts +3 -0
- package/commands/version.command.js +125 -0
- package/constants.js +1 -1
- package/core/config.d.ts +13 -0
- package/core/config.js +61 -0
- package/core/package.d.ts +2 -0
- package/core/package.js +2 -0
- package/core/repository.d.ts +36 -3
- package/core/repository.js +90 -45
- package/index.d.ts +17 -1
- package/index.js +15 -1
- package/package.json +8 -4
- package/rmanrc.schema.json +202 -0
- package/services/changelog.service.d.ts +99 -0
- package/services/changelog.service.js +272 -0
- package/services/ci.service.d.ts +40 -0
- package/services/ci.service.js +204 -0
- package/services/clean.service.d.ts +42 -0
- package/services/clean.service.js +226 -0
- package/services/exec.service.d.ts +35 -0
- package/services/exec.service.js +114 -0
- package/services/import.service.d.ts +30 -0
- package/services/import.service.js +84 -0
- package/services/list.service.d.ts +27 -0
- package/services/list.service.js +25 -0
- package/services/publish.service.d.ts +79 -0
- package/services/publish.service.js +199 -0
- package/services/run.service.d.ts +99 -0
- package/services/run.service.js +432 -0
- package/services/system-info.d.ts +16 -0
- package/services/system-info.js +25 -0
- package/services/version.service.d.ts +95 -0
- package/services/version.service.js +450 -0
- package/services.d.ts +10 -0
- package/services.js +10 -0
- package/utils/branch-guard.d.ts +25 -0
- package/utils/branch-guard.js +61 -0
- package/utils/change-hash.d.ts +55 -0
- package/utils/change-hash.js +80 -0
- package/utils/conventional-commits.d.ts +39 -0
- package/utils/conventional-commits.js +43 -0
- package/utils/exec.d.ts +5 -6
- package/utils/exec.js +45 -59
- package/utils/git.d.ts +86 -0
- package/utils/git.js +276 -0
- package/utils/live-region.d.ts +16 -0
- package/utils/live-region.js +59 -0
- package/utils/logger.d.ts +24 -0
- package/utils/logger.js +39 -0
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/npm-run-path.js +10 -10
- package/utils/package-filter.d.ts +32 -0
- package/utils/package-filter.js +73 -0
- package/utils/progress-panel.d.ts +59 -0
- package/utils/progress-panel.js +149 -0
- package/utils/workspace-range.d.ts +17 -0
- package/utils/workspace-range.js +28 -0
- package/commands/build-command.d.ts +0 -11
- package/commands/build-command.js +0 -26
- package/commands/changed-command.d.ts +0 -16
- package/commands/changed-command.js +0 -35
- package/commands/ci-command.d.ts +0 -24
- package/commands/ci-command.js +0 -71
- package/commands/execute-command.d.ts +0 -19
- package/commands/execute-command.js +0 -75
- package/commands/info-command.d.ts +0 -10
- package/commands/info-command.js +0 -57
- package/commands/list-command.d.ts +0 -38
- package/commands/list-command.js +0 -137
- package/commands/multi-task-command.d.ts +0 -22
- package/commands/multi-task-command.js +0 -68
- package/commands/publish-command.d.ts +0 -24
- package/commands/publish-command.js +0 -135
- package/commands/run-command.d.ts +0 -28
- package/commands/run-command.js +0 -134
- package/commands/version-command.d.ts +0 -25
- package/commands/version-command.js +0 -198
- package/core/command.d.ts +0 -35
- package/core/command.js +0 -117
- package/core/constants.d.ts +0 -1
- package/core/constants.js +0 -2
- package/core/logger.d.ts +0 -12
- package/core/logger.js +0 -2
- package/utils/file-utils.d.ts +0 -4
- package/utils/file-utils.js +0 -29
- package/utils/git-utils.d.ts +0 -25
- package/utils/git-utils.js +0 -66
- package/utils/npm-utils.d.ts +0 -11
- package/utils/npm-utils.js +0 -33
- package/utils/package-not-found-error.d.ts +0 -2
- package/utils/package-not-found-error.js +0 -2
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import parseNpmScript from '@netlify/parse-npm-script';
|
|
3
|
+
import colors from 'ansi-colors';
|
|
4
|
+
import { tokenize } from 'fast-tokenizer';
|
|
5
|
+
import { Task } from 'power-tasks';
|
|
6
|
+
import { exec } from '../utils/exec.js';
|
|
7
|
+
import { LOG_LEVELS, resolveRootLogLevel } from '../utils/logger.js';
|
|
8
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
9
|
+
import { ProgressPanel } from '../utils/progress-panel.js';
|
|
10
|
+
export var RunService;
|
|
11
|
+
(function (RunService) {
|
|
12
|
+
/**
|
|
13
|
+
* A package's `.rmanrc` (cascaded) can configure `run.<script>.*` - e.g.
|
|
14
|
+
* run:
|
|
15
|
+
* build:
|
|
16
|
+
* concurrency: 2
|
|
17
|
+
* lint:
|
|
18
|
+
* topo: false
|
|
19
|
+
* bail: false
|
|
20
|
+
* and a package can opt itself out of a script entirely:
|
|
21
|
+
* run:
|
|
22
|
+
* build:
|
|
23
|
+
* skip: true
|
|
24
|
+
* or supply the command(s) to run when its own package.json doesn't define this script (or its
|
|
25
|
+
* pre/post hooks) at all - a single string, or an array to run several in sequence - see
|
|
26
|
+
* `getScriptSteps`:
|
|
27
|
+
* run:
|
|
28
|
+
* build:
|
|
29
|
+
* script: tsc -b
|
|
30
|
+
* preScript: [node ./generate.js, node ./validate.js]
|
|
31
|
+
* postScript: node ./copy-assets.js
|
|
32
|
+
* override: true # use these even if the package *does* define its own
|
|
33
|
+
*/
|
|
34
|
+
function getConfig(pkg, script) {
|
|
35
|
+
const runCfg = pkg.config?.run;
|
|
36
|
+
const cfg = runCfg && typeof runCfg === 'object' ? runCfg[script] : undefined;
|
|
37
|
+
return cfg && typeof cfg === 'object' ? cfg : {};
|
|
38
|
+
}
|
|
39
|
+
RunService.getConfig = getConfig;
|
|
40
|
+
function resolveEnvPlaceholders(value) {
|
|
41
|
+
return value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '');
|
|
42
|
+
}
|
|
43
|
+
/** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
|
|
44
|
+
* and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
|
|
45
|
+
function parseIfExpr(raw) {
|
|
46
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
47
|
+
return undefined;
|
|
48
|
+
const tokens = tokenizeIf(raw);
|
|
49
|
+
if (!tokens.length)
|
|
50
|
+
return undefined;
|
|
51
|
+
let i = 0;
|
|
52
|
+
const peek = () => tokens[i];
|
|
53
|
+
const next = () => tokens[i++];
|
|
54
|
+
function parseUnary() {
|
|
55
|
+
if (peek()?.toLowerCase() === 'not') {
|
|
56
|
+
next();
|
|
57
|
+
return { kind: 'not', node: parseUnary() };
|
|
58
|
+
}
|
|
59
|
+
const tok = next() ?? '';
|
|
60
|
+
/** A token containing whitespace can only be a collapsed `(...)` group - plain
|
|
61
|
+
* atom/operator tokens never do, since they're themselves split on whitespace. */
|
|
62
|
+
if (/\s/.test(tok)) {
|
|
63
|
+
const inner = parseIfExpr(tok);
|
|
64
|
+
if (!inner)
|
|
65
|
+
throw new Error(`Empty group in "if" expression: "${raw}"`);
|
|
66
|
+
return inner;
|
|
67
|
+
}
|
|
68
|
+
let value;
|
|
69
|
+
if (peek() === '=') {
|
|
70
|
+
next();
|
|
71
|
+
value = resolveEnvPlaceholders(next() ?? '');
|
|
72
|
+
}
|
|
73
|
+
return { kind: 'atom', name: tok, value };
|
|
74
|
+
}
|
|
75
|
+
function parseAnd() {
|
|
76
|
+
let node = parseUnary();
|
|
77
|
+
while (peek()?.toLowerCase() === 'and') {
|
|
78
|
+
next();
|
|
79
|
+
node = { kind: 'and', left: node, right: parseUnary() };
|
|
80
|
+
}
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
function parseOr() {
|
|
84
|
+
let node = parseAnd();
|
|
85
|
+
while (peek()?.toLowerCase() === 'or') {
|
|
86
|
+
next();
|
|
87
|
+
node = { kind: 'or', left: node, right: parseAnd() };
|
|
88
|
+
}
|
|
89
|
+
return node;
|
|
90
|
+
}
|
|
91
|
+
return parseOr();
|
|
92
|
+
}
|
|
93
|
+
RunService.parseIfExpr = parseIfExpr;
|
|
94
|
+
/** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
|
|
95
|
+
* for the same reference hash across packages/scripts in a single run. */
|
|
96
|
+
async function evaluateIf(repository, pkg, node, statusCache) {
|
|
97
|
+
if (node.kind === 'and') {
|
|
98
|
+
if (!(await evaluateIf(repository, pkg, node.left, statusCache)))
|
|
99
|
+
return false;
|
|
100
|
+
return evaluateIf(repository, pkg, node.right, statusCache);
|
|
101
|
+
}
|
|
102
|
+
if (node.kind === 'or') {
|
|
103
|
+
if (await evaluateIf(repository, pkg, node.left, statusCache))
|
|
104
|
+
return true;
|
|
105
|
+
return evaluateIf(repository, pkg, node.right, statusCache);
|
|
106
|
+
}
|
|
107
|
+
if (node.kind === 'not') {
|
|
108
|
+
return !(await evaluateIf(repository, pkg, node.node, statusCache));
|
|
109
|
+
}
|
|
110
|
+
return evaluateIfAtom(repository, pkg, node.name, node.value, statusCache);
|
|
111
|
+
}
|
|
112
|
+
RunService.evaluateIf = evaluateIf;
|
|
113
|
+
async function runScript(repository, script, options = {}) {
|
|
114
|
+
const commandName = options.commandName || 'run';
|
|
115
|
+
const rootCfg = getConfig(repository.rootPackage, script);
|
|
116
|
+
const logLevelDefault = resolveRootLogLevel(repository);
|
|
117
|
+
/** Standing inside a single package's own directory scopes the run to just that package
|
|
118
|
+
* (and drops the root bookend below) unless `--root` asks for the whole repository anyway -
|
|
119
|
+
* a no-op when already at the root, or outside any known package. */
|
|
120
|
+
const cwdScope = options.root ? undefined : repository.currentPackage;
|
|
121
|
+
/** Global fallback for topo - individual packages can still override their own linking below,
|
|
122
|
+
* but the initial sort (topological vs alphabetical) has to be decided for the whole list at once. */
|
|
123
|
+
const topo = resolveBool(options.topo, repository.rootPackage, script, 'topo', true);
|
|
124
|
+
let packages = repository.getPackages({ toposort: topo, scope: cwdScope?.name });
|
|
125
|
+
if (!topo)
|
|
126
|
+
packages = [...packages].sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
packages = filterPackages(packages, options);
|
|
128
|
+
const changed = resolveBool(options.changed, repository.rootPackage, script, 'changed', false);
|
|
129
|
+
const changedSince = options.changedSince ?? (typeof rootCfg.changedSince === 'string' ? rootCfg.changedSince : undefined);
|
|
130
|
+
if (changed || changedSince) {
|
|
131
|
+
const status = await repository.listStatus({ hash: changedSince });
|
|
132
|
+
packages = packages.filter(p => status[p.name] !== 'clean');
|
|
133
|
+
}
|
|
134
|
+
const concurrency = options.parallel === false
|
|
135
|
+
? 1
|
|
136
|
+
: typeof options.parallel === 'number'
|
|
137
|
+
? options.parallel
|
|
138
|
+
: options.parallel === true
|
|
139
|
+
? os.cpus().length
|
|
140
|
+
: resolveNumber(undefined, repository.rootPackage, script, 'concurrency', os.cpus().length);
|
|
141
|
+
const progress = resolveBool(options.progress, repository.rootPackage, script, 'progress', true);
|
|
142
|
+
const panel = new ProgressPanel(`RUN ${script}`, !!process.stdout.isTTY && progress);
|
|
143
|
+
/** Set once the aggregate Task exists, so a package's own failure can trigger a manual
|
|
144
|
+
* abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
|
|
145
|
+
* `bail` is a single blanket policy for the whole batch, it can't vary per package. */
|
|
146
|
+
let rootTask;
|
|
147
|
+
const runSteps = async (ctx, pkgLabel, steps, cwd, pkgBail, pkgLogLevel) => {
|
|
148
|
+
ctx.status = 'running';
|
|
149
|
+
ctx.startedAt = Date.now();
|
|
150
|
+
try {
|
|
151
|
+
for (let i = 0; i < steps.length; i++) {
|
|
152
|
+
const step = steps[i];
|
|
153
|
+
ctx.currentStep = step.name;
|
|
154
|
+
ctx.stepIndex = i;
|
|
155
|
+
if (panel.enabled) {
|
|
156
|
+
await exec(step.command, {
|
|
157
|
+
cwd,
|
|
158
|
+
stdio: 'pipe',
|
|
159
|
+
onLine: (line, stdio) => {
|
|
160
|
+
ctx.log.push(line);
|
|
161
|
+
ctx.lastLine = line;
|
|
162
|
+
void stdio;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
/** Match the classic rman output: raw command output streams straight through
|
|
168
|
+
* (unbuffered, unprefixed), followed by our own one-line-per-step summary. */
|
|
169
|
+
printLegacyExecutingLine(commandName, pkgLabel, step, pkgLogLevel);
|
|
170
|
+
const stepStart = Date.now();
|
|
171
|
+
let stepError;
|
|
172
|
+
try {
|
|
173
|
+
await exec(step.command, { cwd, stdio: 'inherit' });
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
stepError = e;
|
|
177
|
+
}
|
|
178
|
+
printLegacyStepLine(commandName, pkgLabel, step, Date.now() - stepStart, pkgLogLevel, stepError);
|
|
179
|
+
if (stepError)
|
|
180
|
+
throw stepError;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
ctx.status = 'success';
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
ctx.status = 'failed';
|
|
187
|
+
if (pkgBail)
|
|
188
|
+
rootTask?.abort();
|
|
189
|
+
throw e;
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
ctx.finishedAt = Date.now();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const children = [];
|
|
196
|
+
/** Shared across all `if: changed[ = hash]` evaluations so the same reference is only `git`-queried once. */
|
|
197
|
+
const ifStatusCache = new Map();
|
|
198
|
+
/** Repo-wide bookend: root's own pre/post hooks run once each, exclusively, around every package
|
|
199
|
+
* (unless root itself opts out via `run.<script>.skip`, fails its own `run.<script>.if`, or the
|
|
200
|
+
* run is scoped to a single package by `cwdScope` - a repo-wide bookend has no place there). */
|
|
201
|
+
const rootIf = !cwdScope && parseIfExpr(rootCfg.if);
|
|
202
|
+
const rootIfPasses = rootIf ? await evaluateIf(repository, repository.rootPackage, rootIf, ifStatusCache) : true;
|
|
203
|
+
const rootSkipped = !!cwdScope || rootCfg.skip === true || !rootIfPasses;
|
|
204
|
+
const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
|
|
205
|
+
const rootPre = rootSteps.filter(s => s.name === 'pre' + script);
|
|
206
|
+
const rootPost = rootSteps.filter(s => s.name === 'post' + script);
|
|
207
|
+
const rootPreName = 'root (pre)';
|
|
208
|
+
const rootPostName = 'root (post)';
|
|
209
|
+
if (rootPre.length) {
|
|
210
|
+
const ctx = panel.addItem(rootPreName, rootPre.length);
|
|
211
|
+
const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
|
|
212
|
+
const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
|
|
213
|
+
children.push(new Task(() => runSteps(ctx, 'root', rootPre, repository.dirname, pkgBail, pkgLogLevel), {
|
|
214
|
+
name: ctx.name,
|
|
215
|
+
exclusive: true,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
const stepsByPackage = new Map();
|
|
219
|
+
for (const pkg of packages) {
|
|
220
|
+
const pkgCfg = getConfig(pkg, script);
|
|
221
|
+
if (pkgCfg.skip === true)
|
|
222
|
+
continue;
|
|
223
|
+
const pkgIf = parseIfExpr(pkgCfg.if);
|
|
224
|
+
if (pkgIf && !(await evaluateIf(repository, pkg, pkgIf, ifStatusCache)))
|
|
225
|
+
continue;
|
|
226
|
+
const steps = getScriptSteps(pkg, script);
|
|
227
|
+
if (steps.length)
|
|
228
|
+
stepsByPackage.set(pkg.name, steps);
|
|
229
|
+
}
|
|
230
|
+
for (const pkg of packages) {
|
|
231
|
+
const steps = stepsByPackage.get(pkg.name);
|
|
232
|
+
if (!steps)
|
|
233
|
+
continue;
|
|
234
|
+
const ctx = panel.addItem(pkg.name, steps.length);
|
|
235
|
+
const pkgTopo = resolveBool(options.topo, pkg, script, 'topo', topo);
|
|
236
|
+
const pkgBail = resolveBail(options.bail, pkg, script, true);
|
|
237
|
+
const pkgLogLevel = resolveLogLevel(options.logLevel, pkg, script, logLevelDefault);
|
|
238
|
+
const dependencies = pkgTopo ? pkg.dependencies.filter(d => stepsByPackage.has(d)) : [];
|
|
239
|
+
if (rootPre.length)
|
|
240
|
+
dependencies.push(rootPreName);
|
|
241
|
+
children.push(new Task(() => runSteps(ctx, pkg.name, steps, pkg.dirname, pkgBail, pkgLogLevel), {
|
|
242
|
+
name: ctx.name,
|
|
243
|
+
dependencies,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
if (rootPost.length) {
|
|
247
|
+
const ctx = panel.addItem(rootPostName, rootPost.length);
|
|
248
|
+
const pkgBail = resolveBail(options.bail, repository.rootPackage, script, true);
|
|
249
|
+
const pkgLogLevel = resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault);
|
|
250
|
+
children.push(new Task(() => runSteps(ctx, 'root', rootPost, repository.dirname, pkgBail, pkgLogLevel), {
|
|
251
|
+
name: ctx.name,
|
|
252
|
+
exclusive: true,
|
|
253
|
+
/** Must wait for every package task to finish, not just be "exclusive" once it starts. */
|
|
254
|
+
dependencies: [...stepsByPackage.keys()],
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
if (!children.length) {
|
|
258
|
+
console.log(colors.gray(`No package defines a "${script}" script.`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
panel.start();
|
|
262
|
+
let failed = false;
|
|
263
|
+
try {
|
|
264
|
+
/** bail:false here - each package's own resolved bail setting decides whether to call
|
|
265
|
+
* `rootTask.abort()` itself (see `runSteps`), since power-tasks' own `bail` can't vary per package. */
|
|
266
|
+
rootTask = new Task(children, { concurrency, bail: false });
|
|
267
|
+
await rootTask.toPromise();
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
failed = true;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
panel.stop();
|
|
274
|
+
}
|
|
275
|
+
panel.printSummary();
|
|
276
|
+
if (failed) {
|
|
277
|
+
const err = new Error(`"${script}" failed`);
|
|
278
|
+
err.logged = true;
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
RunService.runScript = runScript;
|
|
283
|
+
})(RunService || (RunService = {}));
|
|
284
|
+
/**
|
|
285
|
+
* Classic one-line-per-step log ("info build pkg ┆ step success ┆ command (123 ms)"),
|
|
286
|
+
* matching rman's original npmlog-based output. Used when the live panel is off.
|
|
287
|
+
*
|
|
288
|
+
* Respects `logLevel`: 'verbose' also prints an "executing" line before the step runs
|
|
289
|
+
* (rman's old behavior, hidden by default); 'error' suppresses success lines; 'silent'
|
|
290
|
+
* suppresses everything but a real failure.
|
|
291
|
+
*/
|
|
292
|
+
function printLegacyExecutingLine(commandName, pkgLabel, step, level) {
|
|
293
|
+
if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('verbose'))
|
|
294
|
+
return;
|
|
295
|
+
const sep = colors.gray('┆');
|
|
296
|
+
console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), sep, step.command);
|
|
297
|
+
}
|
|
298
|
+
function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, error) {
|
|
299
|
+
if (!error && LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('info'))
|
|
300
|
+
return;
|
|
301
|
+
if (error && level === 'silent')
|
|
302
|
+
return;
|
|
303
|
+
const sep = colors.gray('┆');
|
|
304
|
+
const levelLabel = error ? colors.red('error') : colors.green('info');
|
|
305
|
+
const status = error ? colors.red.bold('failed') : colors.green.bold('success');
|
|
306
|
+
console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, sep, step.command, colors.yellow(` (${durationMs} ms)`));
|
|
307
|
+
}
|
|
308
|
+
/** A `run.<script>.script`/`.preScript`/`.postScript` value: one command, or several to run in sequence. */
|
|
309
|
+
function normalizeScriptValue(value) {
|
|
310
|
+
if (typeof value === 'string')
|
|
311
|
+
return value || undefined;
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
// joined with "&&", same as a multi-command package.json script - each entry becomes its
|
|
314
|
+
// own step below, and an earlier failure stops the rest (npm's own semantics).
|
|
315
|
+
const parts = value.filter((v) => typeof v === 'string' && !!v);
|
|
316
|
+
return parts.length ? parts.join(' && ') : undefined;
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Resolves a package's steps for `script`, including its own pre/post hooks (npm's own
|
|
322
|
+
* convention) - `.rmanrc run.<script>.script`/`.preScript`/`.postScript` (cascaded) can each supply
|
|
323
|
+
* a command (or an array of them, run in sequence) to use when the package's own package.json has
|
|
324
|
+
* none for that slot (`script`/`pre<script>`/`post<script>`), letting a package run a script it
|
|
325
|
+
* never defined at all. `run.<script>.override` goes further: with it, the config's command
|
|
326
|
+
* replaces the package's own for a slot even when the package *does* define one there.
|
|
327
|
+
*/
|
|
328
|
+
function getScriptSteps(pkg, script) {
|
|
329
|
+
const cfg = RunService.getConfig(pkg, script);
|
|
330
|
+
const json = { ...pkg.json, scripts: { ...(pkg.json.scripts || {}) } };
|
|
331
|
+
const applyConfigScript = (key, rawValue) => {
|
|
332
|
+
const value = normalizeScriptValue(rawValue);
|
|
333
|
+
if (!value)
|
|
334
|
+
return;
|
|
335
|
+
const hasOwn = typeof json.scripts[key] === 'string' && json.scripts[key];
|
|
336
|
+
if (cfg.override === true || !hasOwn)
|
|
337
|
+
json.scripts[key] = value;
|
|
338
|
+
};
|
|
339
|
+
applyConfigScript(script, cfg.script);
|
|
340
|
+
applyConfigScript('pre' + script, cfg.preScript);
|
|
341
|
+
applyConfigScript('post' + script, cfg.postScript);
|
|
342
|
+
json.scripts[script] = json.scripts[script] || '#';
|
|
343
|
+
const info = parseNpmScript(json, 'npm run ' + script);
|
|
344
|
+
if (!info?.raw?.length)
|
|
345
|
+
return [];
|
|
346
|
+
const steps = [];
|
|
347
|
+
for (const s of info.steps) {
|
|
348
|
+
const parsed = Array.isArray(s.parsed) ? s.parsed : [s.parsed];
|
|
349
|
+
for (const command of parsed) {
|
|
350
|
+
if (command === '#')
|
|
351
|
+
continue;
|
|
352
|
+
steps.push({ name: s.name, command });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return steps;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Splits on whitespace, but a `(...)` group collapses to a single token holding its inner text
|
|
359
|
+
* (parens stripped, inner whitespace kept) - `evaluate` below re-tokenizes any token that still
|
|
360
|
+
* contains whitespace, which is what makes nested groups "just work" without a separate grammar
|
|
361
|
+
* for parens: `((a or b) and c)` peels off one bracket layer per recursive `tokenizeIf` call.
|
|
362
|
+
*/
|
|
363
|
+
function tokenizeIf(raw) {
|
|
364
|
+
return tokenize(raw, { delimiters: /\s+/, brackets: true, keepBrackets: false }).all();
|
|
365
|
+
}
|
|
366
|
+
const warnedUnknownIf = new Set();
|
|
367
|
+
const warnedEmptyIfValue = new Set();
|
|
368
|
+
async function evaluateIfAtom(repository, pkg, name, value, statusCache) {
|
|
369
|
+
switch (name) {
|
|
370
|
+
case 'changed':
|
|
371
|
+
case 'dirty':
|
|
372
|
+
case 'committed': {
|
|
373
|
+
if (value !== undefined && !value) {
|
|
374
|
+
const warnKey = `${pkg.name}:${name}`;
|
|
375
|
+
if (!warnedEmptyIfValue.has(warnKey)) {
|
|
376
|
+
warnedEmptyIfValue.add(warnKey);
|
|
377
|
+
console.log(colors.yellow(`[${pkg.name}] "if: ${name} = ..." resolved to an empty value - treating as not matched.`));
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
const cacheKey = value || '';
|
|
382
|
+
let statusMap = statusCache.get(cacheKey);
|
|
383
|
+
if (!statusMap) {
|
|
384
|
+
statusMap = await repository.listStatus(value ? { hash: value } : undefined);
|
|
385
|
+
statusCache.set(cacheKey, statusMap);
|
|
386
|
+
}
|
|
387
|
+
const status = statusMap[pkg.name];
|
|
388
|
+
/** With a hash, status only ever resolves to 'changed'/'clean' (see Repository.listStatus) -
|
|
389
|
+
* the dirty/committed distinction only exists relative to upstream, so those two conditions
|
|
390
|
+
* naturally never match when a hash is given. */
|
|
391
|
+
if (name === 'changed')
|
|
392
|
+
return status !== 'clean';
|
|
393
|
+
return status === name;
|
|
394
|
+
}
|
|
395
|
+
default:
|
|
396
|
+
if (!warnedUnknownIf.has(name)) {
|
|
397
|
+
warnedUnknownIf.add(name);
|
|
398
|
+
console.log(colors.yellow(`Unknown "if" condition "${name}" - ignoring, package will still run.`));
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
|
|
404
|
+
export function resolveBool(cliValue, pkg, script, key, fallback) {
|
|
405
|
+
if (cliValue !== undefined)
|
|
406
|
+
return cliValue;
|
|
407
|
+
const v = RunService.getConfig(pkg, script)[key];
|
|
408
|
+
return typeof v === 'boolean' ? v : fallback;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Bail is the one setting where a package's own `.rmanrc` outranks even an explicit CLI flag:
|
|
412
|
+
* "this package's failure must always stop the batch" is a more specific, intentional statement
|
|
413
|
+
* than a broad `--no-bail` meant for the run as a whole, and shouldn't be silently overridable by it.
|
|
414
|
+
*/
|
|
415
|
+
export function resolveBail(cliValue, pkg, script, fallback) {
|
|
416
|
+
const v = RunService.getConfig(pkg, script).bail;
|
|
417
|
+
if (typeof v === 'boolean')
|
|
418
|
+
return v;
|
|
419
|
+
return cliValue !== undefined ? cliValue : fallback;
|
|
420
|
+
}
|
|
421
|
+
export function resolveNumber(cliValue, pkg, script, key, fallback) {
|
|
422
|
+
if (cliValue !== undefined)
|
|
423
|
+
return cliValue;
|
|
424
|
+
const v = RunService.getConfig(pkg, script)[key];
|
|
425
|
+
return typeof v === 'number' ? v : fallback;
|
|
426
|
+
}
|
|
427
|
+
export function resolveLogLevel(cliValue, pkg, script, fallback) {
|
|
428
|
+
if (cliValue !== undefined)
|
|
429
|
+
return cliValue;
|
|
430
|
+
const v = RunService.getConfig(pkg, script).logLevel;
|
|
431
|
+
return typeof v === 'string' && LOG_LEVELS.includes(v) ? v : fallback;
|
|
432
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import envinfo from 'envinfo';
|
|
2
|
+
import type { Repository } from '../core/repository.js';
|
|
3
|
+
export declare namespace SystemInfo {
|
|
4
|
+
interface RepositoryInfo {
|
|
5
|
+
type: 'monorepo' | 'package';
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
root: string;
|
|
9
|
+
packageCount: number;
|
|
10
|
+
}
|
|
11
|
+
/** Whatever `envinfo` reports (OS/CPU/Memory/Shell, Node/Yarn/npm, git, installed
|
|
12
|
+
* rman/typescript versions), grouped by category - shape is `envinfo`'s own, not ours. */
|
|
13
|
+
type SystemInfo = Record<string, Record<string, unknown>>;
|
|
14
|
+
function getSystemInfo(options?: envinfo.RunConfig): Promise<SystemInfo.SystemInfo>;
|
|
15
|
+
function getRepositoryInfo(repository: Repository): SystemInfo.RepositoryInfo;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import envinfo from 'envinfo';
|
|
2
|
+
export var SystemInfo;
|
|
3
|
+
(function (SystemInfo) {
|
|
4
|
+
async function getSystemInfo(options) {
|
|
5
|
+
return JSON.parse(await envinfo.run({
|
|
6
|
+
System: ['OS', 'CPU', 'Memory', 'Shell'],
|
|
7
|
+
Binaries: ['Node', 'Yarn', 'npm'],
|
|
8
|
+
Utilities: ['Git'],
|
|
9
|
+
npmPackages: ['rman', 'typescript'],
|
|
10
|
+
npmGlobalPackages: ['typescript'],
|
|
11
|
+
...options,
|
|
12
|
+
}, { json: true }));
|
|
13
|
+
}
|
|
14
|
+
SystemInfo.getSystemInfo = getSystemInfo;
|
|
15
|
+
function getRepositoryInfo(repository) {
|
|
16
|
+
return {
|
|
17
|
+
type: repository.monorepo ? 'monorepo' : 'package',
|
|
18
|
+
name: repository.rootPackage.name,
|
|
19
|
+
version: repository.rootPackage.version,
|
|
20
|
+
root: repository.dirname,
|
|
21
|
+
packageCount: repository.getPackages().length,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
SystemInfo.getRepositoryInfo = getRepositoryInfo;
|
|
25
|
+
})(SystemInfo || (SystemInfo = {}));
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Package } from '../core/package.js';
|
|
2
|
+
import type { Repository } from '../core/repository.js';
|
|
3
|
+
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
4
|
+
export declare namespace VersionService {
|
|
5
|
+
type BumpKeyword = 'patch' | 'minor' | 'major';
|
|
6
|
+
function isBumpKeyword(value: unknown): value is BumpKeyword;
|
|
7
|
+
interface Options extends PackageFilterOptions {
|
|
8
|
+
/** A release-type keyword (applied as the severity for every group that has real changes) or
|
|
9
|
+
* a concrete semver version (applied as the literal new version wherever something changed) -
|
|
10
|
+
* either way, this replaces auto-detection entirely. Omit to auto-detect the severity per
|
|
11
|
+
* group from conventional-commit subjects since each package's/group's last release tag. */
|
|
12
|
+
bump?: string;
|
|
13
|
+
/** A package with uncommitted local changes is excluded from bumping (status `'skip'`)
|
|
14
|
+
* instead of aborting the whole plan (status `'error'`). Default false. */
|
|
15
|
+
ignoreDirty?: boolean;
|
|
16
|
+
/** Makes every computed bump a prerelease (`1.2.3` -> `1.3.0-beta.0` for a `minor`, say)
|
|
17
|
+
* tagged with this identifier, instead of a normal release - same idea as `npm version
|
|
18
|
+
* <type> --preid <name>`. A group already sitting on a matching prerelease (same identifier)
|
|
19
|
+
* just has its prerelease counter incremented instead of jumping to a new base version - see
|
|
20
|
+
* `incVersion`. Has no effect when `bump` is an explicit semver version rather than a keyword
|
|
21
|
+
* (there's no severity left to "pre-fix" at that point). */
|
|
22
|
+
preid?: string;
|
|
23
|
+
}
|
|
24
|
+
interface ApplyOptions {
|
|
25
|
+
/** Push the resulting commit(s) and tag(s) to the remote once applied. Default false - same
|
|
26
|
+
* as a plain `npm version`, which never pushes on its own either. */
|
|
27
|
+
push?: boolean;
|
|
28
|
+
/** Overrides `.rmanrc version.commitMessage` (and the built-in default) for every group's
|
|
29
|
+
* commit this run produces - `{version}` is still substituted the same way. */
|
|
30
|
+
message?: string;
|
|
31
|
+
/** Also writes each bumped package's `CHANGELOG.md` (via `ChangelogService.generateToFile`,
|
|
32
|
+
* scoped to just the packages this run actually bumped) and folds those file changes into the
|
|
33
|
+
* same per-group commit, instead of requiring a separate `rman changelog --write` run. */
|
|
34
|
+
changelog?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** One package's outcome in a version plan - see `getPlan`. */
|
|
37
|
+
interface Entry {
|
|
38
|
+
package: Package;
|
|
39
|
+
/** Internal group identity packages are batched by (not for display) - see `resolveGroupKey`. */
|
|
40
|
+
groupKey: string;
|
|
41
|
+
/** Human-readable group name: `'default'` for the implicit repo-wide group, the configured
|
|
42
|
+
* name for a named `group`, or the package's own name when it isn't grouped with anyone. */
|
|
43
|
+
group: string;
|
|
44
|
+
status: 'bump' | 'skip' | 'error' | 'no-change';
|
|
45
|
+
from: string;
|
|
46
|
+
/** Only set when `status === 'bump'`. */
|
|
47
|
+
to?: string;
|
|
48
|
+
/** Human-readable explanation - e.g. why a package was skipped, or why it's being bumped
|
|
49
|
+
* despite having no commits of its own (a dependency of it changed elsewhere). */
|
|
50
|
+
reason?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Computes what a version bump *would* do, across every package `.rmanrc group` puts together -
|
|
54
|
+
* never writes anything (no package.json edits, no git commits/tags) and safe to call any time,
|
|
55
|
+
* including as the "preview" a bare `rman version` (no bump given) stops at.
|
|
56
|
+
*
|
|
57
|
+
* Packages are partitioned into groups by their resolved `group` value (cascaded): `true`
|
|
58
|
+
* (the default) puts every such package into one implicit repo-wide group; a string joins
|
|
59
|
+
* exactly the other packages sharing that same string, regardless of the repo's default; `false`
|
|
60
|
+
* makes a package its own solo group. Each group's "current version" is always the highest
|
|
61
|
+
* version currently found among its own members (never persisted anywhere) - see
|
|
62
|
+
* `resolveGroupKey`.
|
|
63
|
+
*
|
|
64
|
+
* Within a group, a member with real commits since its own last release tag (or an explicit
|
|
65
|
+
* `bump`) sets the group's severity to the highest found among changed members; the new version
|
|
66
|
+
* is that current version bumped by that severity. Which members actually receive it depends on
|
|
67
|
+
* the severity: **patch** only the changed member(s) (a caret dependency range already tolerates
|
|
68
|
+
* a patch bump, no republish needed downstream); **minor** also every transitive in-group
|
|
69
|
+
* dependent; **major** the entire group, changed or not - see `computeGroupPlan`.
|
|
70
|
+
*
|
|
71
|
+
* Across groups: a package depending on another group's bumped package always gets exactly a
|
|
72
|
+
* **patch** bump of its own (never inheriting the source's severity) - the dependency reference
|
|
73
|
+
* itself is the only thing that changed for it. This never re-triggers *its own* group's
|
|
74
|
+
* minor/major cascade (a patch never cascades), but can itself ripple into a third group, and so
|
|
75
|
+
* on, until nothing new is affected - see `rippleCrossGroup`.
|
|
76
|
+
*
|
|
77
|
+
* A monorepo's root package is never a real member of any group (it's never published on its
|
|
78
|
+
* own) - it gets one trailing informational entry instead, always `'bump'`ed to whatever single
|
|
79
|
+
* version every group ended up sharing, or the overall highest version when groups diverged.
|
|
80
|
+
*/
|
|
81
|
+
function getPlan(repository: Repository, options?: Options): Promise<Entry[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Same as `getPlan`, and additionally writes every `'bump'` entry's new version into its own
|
|
84
|
+
* `package.json` (and refreshes any other bumped package's dependency range on it), runs that
|
|
85
|
+
* package's `version.preScript`/`.script`/`.postScript` (or its own real `preversion`/`version`/
|
|
86
|
+
* `postversion` npm scripts) around the write, then commits and tags **once per group** - so
|
|
87
|
+
* independently-versioned groups each get their own clean commit/tag rather than one entangled
|
|
88
|
+
* commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
|
|
89
|
+
* plain `npm version`, this never reaches the network on its own otherwise.
|
|
90
|
+
*/
|
|
91
|
+
function applyPlan(repository: Repository, plan: Entry[], options?: ApplyOptions): Promise<Entry[]>;
|
|
92
|
+
}
|
|
93
|
+
/** Every `package.json` field holding dependency ranges - shared with `PublishService`'s own
|
|
94
|
+
* `"workspace:"` rewrite-for-publish step, since it needs to scan the same fields. */
|
|
95
|
+
export declare const DEPENDENCY_KEYS: readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|