rman 2.19.0 → 2.20.0

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.
@@ -13,6 +13,8 @@ export declare namespace PublishService {
13
13
  /** A package with uncommitted local changes is excluded (status `'skip'`) instead of aborting
14
14
  * the whole plan (status `'error'`) - same as `version`'s own option. Default false. */
15
15
  ignoreDirty?: boolean;
16
+ /** Which packages are the npm target's (`shipsTo`) - every one the filters keep when omitted. */
17
+ shipsHere?: (pkg: Package) => boolean;
16
18
  /** Registry to check against (and, in `applyPlan`, publish to) - `.npmrc`'s own configured
17
19
  * registry is used when omitted. */
18
20
  registry?: string;
@@ -195,7 +195,7 @@ export var PublishService;
195
195
  PublishService.registryLabel = registryLabel;
196
196
  async function getPlan(repository, options = {}, deps = {}) {
197
197
  const git = new GitHelper({ cwd: repository.dirname });
198
- const packages = filterPackages(repository.getPackages({ toposort: true }), options);
198
+ const packages = filterPackages(repository.getPackages({ toposort: true }), options).filter(pkg => options.shipsHere?.(pkg) ?? true);
199
199
  const dirtyFiles = await git.listDirtyFiles({ absolute: true });
200
200
  const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
201
201
  /** Per package, not per run: `resolveRegistry` reads the package's own `publishConfig.registry`
@@ -1,5 +1,5 @@
1
1
  import type { Package } from '../../../core/classes/package.js';
2
- import type { PublishTarget } from '../../../core/interfaces/publish-target.js';
2
+ import { type PublishTarget } from '../../../core/interfaces/publish-target.js';
3
3
  import { PublishService } from '../../platforms/node/services/publish.service.js';
4
4
  /** The name this target answers to in `publish.target` and `--target`. */
5
5
  export declare const NPM_TARGET = "npm";
@@ -1,3 +1,4 @@
1
+ import { shipsTo } from '../../../core/interfaces/publish-target.js';
1
2
  import { CiService } from '../../platforms/node/services/ci.service.js';
2
3
  import { PublishService } from '../../platforms/node/services/publish.service.js';
3
4
  /** The name this target answers to in `publish.target` and `--target`. */
@@ -105,6 +106,9 @@ export class NpmPublishTarget {
105
106
  planOptions(ctx) {
106
107
  return {
107
108
  ...ctx.options,
109
+ /** `publish` drops the rest from the plan anyway; asked here too so a docker-only package
110
+ * costs no `npm view`. */
111
+ shipsHere: pkg => shipsTo(pkg, this),
108
112
  registry: ctx.args.registry,
109
113
  userconfig: ctx.args.userconfig,
110
114
  /** Read at *plan* time too, not only where the publish command is built: the plan is what
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import readline from 'node:readline/promises';
3
3
  import colors from 'ansi-colors';
4
- import { unknownTargets } from '../core/interfaces/publish-target.js';
4
+ import { shipsTo, unknownTargets } from '../core/interfaces/publish-target.js';
5
5
  import { registerCommand } from '../interfaces/rman-config.interface.js';
6
6
  import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
7
7
  import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
@@ -121,8 +121,14 @@ const publishCommand = registerCommand(app => {
121
121
  };
122
122
  assertDeclaredTargetsExist(app, repository.getPackages());
123
123
  const plans = new Map();
124
- for (const target of selected)
125
- plans.set(target, await target.getPlan(ctx));
124
+ /** A target's plan keeps only the packages that ship to it, whatever the target itself
125
+ * filtered - see `shipsTo`. Asked here, once for every target, because leaving it to each
126
+ * one let them disagree: the npm target never asked, so a package declaring
127
+ * `publish.target: ['docker']` was planned for npm too while `rman list` and the docker
128
+ * target called it docker-only (panates/rman#40). */
129
+ for (const target of selected) {
130
+ plans.set(target, (await target.getPlan(ctx)).filter(entry => shipsTo(entry.package, target)));
131
+ }
126
132
  if (args.json) {
127
133
  console.log(JSON.stringify(jsonPlan(plans), undefined, 2));
128
134
  }
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.19.0';
1
+ export const version = '2.20.0';
@@ -81,6 +81,8 @@ export declare class ProgressPanel implements TerminalRegion {
81
81
  * `TerminalRegion` half of the takeover, so a `runBin` child's output goes somewhere sane while
82
82
  * a panel is up. */
83
83
  passThrough(text: string): void;
84
+ /** Every line of a replayed log moved two columns right - an empty line left empty. */
85
+ protected indentLog(text: string): string;
84
86
  private render;
85
87
  /** Stops the redraw loop and erases the panel, leaving the cursor where it started. */
86
88
  stop(): void;
@@ -79,6 +79,13 @@ export class ProgressPanel {
79
79
  process.stdout.write(text);
80
80
  this.render();
81
81
  }
82
+ /** Every line of a replayed log moved two columns right - an empty line left empty. */
83
+ indentLog(text) {
84
+ return text
85
+ .split('\n')
86
+ .map(line => (line ? ` ${line}` : line))
87
+ .join('\n');
88
+ }
82
89
  render() {
83
90
  this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
84
91
  const spinner = colors.cyan(SPINNER_FRAMES[this.spinnerFrame]);
@@ -273,9 +280,12 @@ export class ProgressPanel {
273
280
  * the comma after it red.
274
281
  *
275
282
  * The `X` above already says the item failed, and the step that failed printed its own
276
- * error. */
283
+ * error.
284
+ *
285
+ * **Indented two columns**, so the log reads as belonging to the `X` line above it - level
286
+ * with the recap's own lines it ran together with them, a nested rman's recap most of all. */
277
287
  if (item.log.length)
278
- console.log(item.log.join('\n'));
288
+ console.log(this.indentLog(item.log.join('\n')));
279
289
  }
280
290
  }
281
291
  else if (this.live.enabled) {
@@ -89,6 +89,15 @@ export const STEP_PATHS = [
89
89
  'version.before.command',
90
90
  'version.exec.command',
91
91
  'version.after.command',
92
+ /** A step object's own `if`, for the reason `run.*.if` is here: called while the config loads it
93
+ * would collapse to a boolean and stop being a condition. `run.*.if` already covers the
94
+ * shorthand's (`run: { build: [{ if, command }] }`), which arrives at the same path. */
95
+ 'run.*.before.if',
96
+ 'run.*.exec.if',
97
+ 'run.*.after.if',
98
+ 'version.before.if',
99
+ 'version.exec.if',
100
+ 'version.after.if',
92
101
  ];
93
102
  /**
94
103
  * Config paths left untouched when a repository's config is first resolved, and evaluated only by
@@ -47,6 +47,16 @@ export interface RunStepContext {
47
47
  * `ctx.runBin` is already bound to this directory, so a binary run through it needs no such care.
48
48
  */
49
49
  cwd: string;
50
+ /**
51
+ * The package the run was narrowed to because rman was started inside it, or `undefined` when it
52
+ * covers the repository - started at the root, or with `--from-root`.
53
+ *
54
+ * What a step asks to tell "the whole repository is building" from "just this package is":
55
+ * `repository.currentPackage` answers where rman was started and cannot see `--from-root`. Set by
56
+ * `run` and the commands built on it (`build`, `test`); `undefined` in a `version` hook, which
57
+ * always covers the repository.
58
+ */
59
+ scopedTo?: Package;
50
60
  /**
51
61
  * The repository's locally installed binaries, already carrying this run's `cwd` and log level -
52
62
  * handed over rather than imported, for the reason `CommandContext.runBin` is.
@@ -107,6 +117,17 @@ export interface RunStepObject {
107
117
  * with no package graph to join, and the key is refused there rather than quietly doing nothing.
108
118
  */
109
119
  topo?: boolean;
120
+ /**
121
+ * Whether this one step runs, asked when its turn comes - the same two forms
122
+ * `run.<script>.if` takes: a condition (`changed`, `dirty and not committed`) or a function
123
+ * handed the step's context. A step that says no is passed over, and the ones after it still
124
+ * run.
125
+ *
126
+ * @example
127
+ * // lint the package only when the build was started inside it
128
+ * { if: ({ scopedTo }) => !!scopedTo, command: 'rman lint' }
129
+ */
130
+ if?: string | RunConditionFn;
110
131
  }
111
132
  /** One entry of a `before`/`exec`/`after` slot: a shell command, a function, or a
112
133
  * [`RunStepObject`](#RunStepObject) saying something about the step. A list of them runs in
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.19.0",
4
+ "version": "2.20.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -4,7 +4,7 @@ import type { Package } from '../core/classes/package.js';
4
4
  import { ProgressPanel } from '../core/classes/progress-panel.js';
5
5
  import type { Repository } from '../core/classes/repository.js';
6
6
  import { Service } from '../core/classes/service.js';
7
- import type { RunStepContext, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
7
+ import type { RunConditionFn, RunStepContext, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
8
8
  import { type PackageFilterOptions } from '../utils/package-filter.js';
9
9
  /**
10
10
  * A service class - see `ListService` for the shape and `Service` for the three measured
@@ -160,6 +160,8 @@ export declare namespace RunService {
160
160
  steps: ScriptStep[];
161
161
  /** The directory the steps run in - the package's own, or the repository root for a bookend. */
162
162
  cwd: string;
163
+ /** The package the run was narrowed to by the directory it started in - `RunStepContext.scopedTo`. */
164
+ scopedTo?: Package;
163
165
  topo: boolean;
164
166
  bail: boolean;
165
167
  logLevel: LogLevel;
@@ -210,6 +212,8 @@ export declare namespace RunService {
210
212
  * and the three are three different answers, which is why this is not a plain boolean. See
211
213
  * `RunStepObject.topo`. */
212
214
  topo?: boolean;
215
+ /** The step's own condition, from its object form - see `RunStepObject.if`. */
216
+ if?: string | RunConditionFn;
213
217
  }
214
218
  export interface CommandStep extends StepBase {
215
219
  command: string;
@@ -306,7 +310,7 @@ export declare namespace RunService {
306
310
  * `onCommand` is told what the step is spawning, for as long as it runs - the argv while a child
307
311
  * is up, `undefined` when it returns.
308
312
  */
309
- export function createStepContext(pkg: Package, cwd: string, onCommand?: (command: string | undefined) => void, onLine?: (line: string, stream?: 'stdout' | 'stderr') => void): RunStepContext;
313
+ export function createStepContext(pkg: Package, cwd: string, onCommand?: (command: string | undefined) => void, onLine?: (line: string, stream?: 'stdout' | 'stderr') => void, scopedTo?: Package): RunStepContext;
310
314
  /**
311
315
  * `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
312
316
  * instead of a full expression language (less to get wrong, still covers what's asked for) -
@@ -60,7 +60,7 @@ export class RunService extends Service {
60
60
  const rootSkipped = !!cwdScope ||
61
61
  !repository.monorepo ||
62
62
  rootCfg.skip === true ||
63
- !(await passesIf(repository, repository.rootPackage, rootCfg.if, repository.dirname, ifStatusCache));
63
+ !(await passesIf(repository, repository.rootPackage, rootCfg.if, repository.dirname, ifStatusCache, cwdScope));
64
64
  const rootSteps = rootSkipped ? [] : getScriptSteps(repository.rootPackage, script);
65
65
  /** Filtered on the slot, not on `'pre' + script`: the step labels are `before`/`exec`/`after`
66
66
  * now - the same words the config uses - rather than npm's `pre<script>` naming, which moved
@@ -75,6 +75,7 @@ export class RunService extends Service {
75
75
  label: 'root',
76
76
  steps,
77
77
  cwd: repository.dirname,
78
+ scopedTo: cwdScope,
78
79
  topo: false,
79
80
  bail: resolveBail(options.bail, repository.rootPackage, script, true),
80
81
  logLevel: resolveLogLevel(options.logLevel, repository.rootPackage, script, logLevelDefault),
@@ -87,7 +88,7 @@ export class RunService extends Service {
87
88
  const pkgCfg = RunService.getConfig(pkg, script);
88
89
  if (pkgCfg.skip === true)
89
90
  continue;
90
- if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache)))
91
+ if (!(await passesIf(repository, pkg, pkgCfg.if, pkg.dirname, ifStatusCache, cwdScope)))
91
92
  continue;
92
93
  const steps = getScriptSteps(pkg, script);
93
94
  if (!steps.length)
@@ -98,6 +99,7 @@ export class RunService extends Service {
98
99
  label: pkg.name,
99
100
  steps,
100
101
  cwd: pkg.dirname,
102
+ scopedTo: cwdScope,
101
103
  topo: resolveBool(options.topo, pkg, script, 'topo', topo),
102
104
  bail: resolveBail(options.bail, pkg, script, true),
103
105
  logLevel: resolveLogLevel(options.logLevel, pkg, script, logLevelDefault),
@@ -162,6 +164,8 @@ export class RunService extends Service {
162
164
  * abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
163
165
  * `bail` is a single blanket policy for the whole batch, it can't vary per package. */
164
166
  let rootTask;
167
+ /** One `git` query per reference for every step's `if: changed`, as the scripts' share one. */
168
+ const stepIfCache = new Map();
165
169
  const runSteps = async (ctx, plan, steps,
166
170
  /**
167
171
  * Which slice of the package's steps this call runs, when a `topo` step split it in two.
@@ -170,7 +174,7 @@ export class RunService extends Service {
170
174
  * one must not mark the package finished.
171
175
  */
172
176
  part) => {
173
- const { pkg, cwd, bail: pkgBail, logLevel: pkgLogLevel, label: pkgLabel } = plan;
177
+ const { pkg, cwd, bail: pkgBail, logLevel: pkgLogLevel, label: pkgLabel, scopedTo } = plan;
174
178
  const from = part?.from ?? 0;
175
179
  /** Only the first part starts the clock: the row's elapsed time is the package's, and a
176
180
  * package waiting at a `topo` barrier is waiting as part of its own run. */
@@ -181,6 +185,12 @@ export class RunService extends Service {
181
185
  try {
182
186
  for (let i = from; i < steps.length; i++) {
183
187
  const step = steps[i];
188
+ /** Asked when the step's turn comes, not when the plan is made - a condition may read
189
+ * what the steps before it left behind. A step that says no is passed over in silence:
190
+ * it is not a failure, and announcing it on every package would bury the steps that ran. */
191
+ if (step.if !== undefined && !(await passesIf(pkg.repository, pkg, step.if, cwd, stepIfCache, scopedTo))) {
192
+ continue;
193
+ }
184
194
  ctx.currentStep = step.name;
185
195
  /** The command for a shell step, the function's own name for a JS one - and a bare
186
196
  * marker for an anonymous function, which is still worth saying: the slot label alone
@@ -217,7 +227,7 @@ export class RunService extends Service {
217
227
  * run step should.
218
228
  */
219
229
  if (step.run)
220
- await runFunctionStep(step.run, pkg, cwd, onLine, onCommand);
230
+ await runFunctionStep(step.run, pkg, cwd, onLine, onCommand, scopedTo);
221
231
  else {
222
232
  await exec(step.command, {
223
233
  cwd,
@@ -583,6 +593,8 @@ export class RunService extends Service {
583
593
  * (`{ command }` / `{ run }`) reaches here too and only one place knows the three spellings.
584
594
  * `topo` cannot be set on this path - `normalizeScriptValue` refuses it for a version hook. */
585
595
  const step = toStep(slot, value);
596
+ if (step.if !== undefined && !(await passesIf(pkg.repository, pkg, step.if, pkg.dirname, new Map())))
597
+ continue;
586
598
  if (step.run) {
587
599
  await step.run(createStepContext(pkg, pkg.dirname));
588
600
  continue;
@@ -720,12 +732,13 @@ export class RunService extends Service {
720
732
  * whole of a build, because that is all the slot knows; what a reader wants is the `tsc -b
721
733
  * <tsconfig>` it is sitting in. A function step is the shape every shared config uses, so this is
722
734
  * the common case rather than an edge one. */
723
- function createStepContext(pkg, cwd, onCommand, onLine) {
735
+ function createStepContext(pkg, cwd, onCommand, onLine, scopedTo) {
724
736
  const logLevel = resolveRootLogLevel(pkg.repository);
725
737
  return {
726
738
  pkg,
727
739
  repository: pkg.repository,
728
740
  cwd,
741
+ scopedTo,
729
742
  runBin: async (bin, argv, opts) => {
730
743
  onCommand?.([bin, ...argv].join(' '));
731
744
  const result = await runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, onLine, ...opts });
@@ -830,10 +843,12 @@ function describeValue(value) {
830
843
  * the slot's own word for one passed inline, which has no name at all. */
831
844
  function toStep(slot, value) {
832
845
  const topo = typeof value === 'object' ? value.topo : undefined;
846
+ const condition = typeof value === 'object' ? value.if : undefined;
833
847
  const step = typeof value === 'object' ? value.command : value;
834
- if (typeof step === 'function')
835
- return { name: slot, label: step.name || `${slot} (js)`, run: step, topo };
836
- return { name: slot, label: step, command: step, topo };
848
+ if (typeof step === 'function') {
849
+ return { name: slot, label: step.name || `${slot} (js)`, run: step, topo, if: condition };
850
+ }
851
+ return { name: slot, label: step, command: step, topo, if: condition };
837
852
  }
838
853
  /**
839
854
  * Checks a step written as an object and hands it back - a `command` that is a shell command or a
@@ -873,10 +888,20 @@ function assertStepObject(item, where, allowTopo) {
873
888
  throw new Error(`"${where}" must set "topo" to true or false, but it is ${describeValue(item.topo)}.`);
874
889
  }
875
890
  }
876
- return { command: item.command, topo: item.topo };
891
+ /** The same two forms `run.<script>.if` takes. An empty string is refused rather than read as
892
+ * "no condition": a key written and left blank is more likely a mistake than a decision. */
893
+ if (item.if !== undefined && typeof item.if !== 'function' && (typeof item.if !== 'string' || !item.if.trim())) {
894
+ throw new Error(`"${where}" must set "if" to a condition (\`changed\`, \`dirty and not committed\`) or a function, ` +
895
+ `but it is ${describeValue(item.if)}.`);
896
+ }
897
+ return {
898
+ command: item.command,
899
+ topo: item.topo,
900
+ ...(item.if !== undefined && { if: item.if }),
901
+ };
877
902
  }
878
- /** What a step object may hold - the step itself, and the marker beside it. */
879
- const STEP_OBJECT_KEYS = ['command', 'topo'];
903
+ /** What a step object may hold - the step itself, and the markers beside it. */
904
+ const STEP_OBJECT_KEYS = ['command', 'topo', 'if'];
880
905
  /**
881
906
  * Runs a function step, with its `console` captured into `onLine` - the step's own lines, routed to
882
907
  * whichever reporters the run has, beside those of the children it spawns.
@@ -894,8 +919,8 @@ const STEP_OBJECT_KEYS = ['command', 'topo'];
894
919
  * nowhere at all. Written to `onLine` on stderr, ahead of the step's `end` - the order a shell step
895
920
  * already produces.
896
921
  */
897
- async function runFunctionStep(run, pkg, cwd, onLine, onCommand) {
898
- const context = RunService.createStepContext(pkg, cwd, onCommand, onLine);
922
+ async function runFunctionStep(run, pkg, cwd, onLine, onCommand, scopedTo) {
923
+ const context = RunService.createStepContext(pkg, cwd, onCommand, onLine, scopedTo);
899
924
  try {
900
925
  await RunService.withCapturedConsole(onLine, () => run(context));
901
926
  }
@@ -920,15 +945,16 @@ function format(args) {
920
945
  return args.map(arg => (typeof arg === 'string' ? arg : inspect(arg))).join(' ');
921
946
  }
922
947
  /**
923
- * Whether a script runs for `pkg` at all - `run.<script>.if`, in either of its two forms.
948
+ * Whether a script runs for `pkg` at all - `run.<script>.if` - or a single step does (a step
949
+ * object's own `if`), in either of their two forms.
924
950
  *
925
951
  * The function form is checked **first**: `parseIfExpr` answers `undefined` for anything that is
926
952
  * not a string, which the caller reads as "no condition given", so a function reaching it would be
927
953
  * a condition that silently always passed.
928
954
  */
929
- async function passesIf(repository, pkg, raw, cwd, statusCache) {
955
+ async function passesIf(repository, pkg, raw, cwd, statusCache, scopedTo) {
930
956
  if (typeof raw === 'function') {
931
- return !!(await raw(RunService.createStepContext(pkg, cwd)));
957
+ return !!(await raw(RunService.createStepContext(pkg, cwd, undefined, undefined, scopedTo)));
932
958
  }
933
959
  const node = RunService.parseIfExpr(raw);
934
960
  return node ? RunService.evaluateIf(repository, pkg, node, statusCache) : true;