rman 1.1.1 → 1.2.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/cli.js CHANGED
@@ -24,6 +24,7 @@ import { assertNoBuiltinShadowing, loadCustomCommands } from './core/custom-comm
24
24
  import { Repository } from './core/repository.js';
25
25
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
26
26
  import { filterPackages, readPackageFilterOptions, readRootOption } from './utils/package-filter.js';
27
+ import { printableConfig } from './utils/printable-config.js';
27
28
  import { runBin } from './utils/run-bin.js';
28
29
  export async function runCli(options) {
29
30
  try {
@@ -207,7 +208,7 @@ function printCommandConfig(repository, spec, args) {
207
208
  console.log(`command: ${name}`);
208
209
  const options = readOptions(args);
209
210
  console.log(Object.keys(options).length
210
- ? `options:\n${indent(yaml.dump(options, { noRefs: true }).trimEnd())}`
211
+ ? `options:\n${indent(yaml.dump(printableConfig(options), { noRefs: true }).trimEnd())}`
211
212
  : `options: {}${comment(' # nothing but defaults')}`);
212
213
  const targets = commandTargets(repository, args);
213
214
  console.log(`packages: ${targets.length ? `[${targets.map(p => p.name).join(', ')}]` : '[]'}`);
@@ -225,7 +226,7 @@ function printCommandConfig(repository, spec, args) {
225
226
  const config = {};
226
227
  for (const pkg of shown)
227
228
  config[pkg.name] = keys?.length ? pick(pkg.config, keys) : pkg.config;
228
- console.log(indent(yaml.dump(config, { noRefs: true, lineWidth: 100 }).trimEnd()));
229
+ console.log(indent(yaml.dump(printableConfig(config), { noRefs: true, lineWidth: 100 }).trimEnd()));
229
230
  if (!targets.includes(repository.rootPackage)) {
230
231
  console.log(comment(`# "${repository.rootPackage.name}" is the root - listed because repo-wide keys are read there.`));
231
232
  }
@@ -3,6 +3,7 @@ import colors from 'ansi-colors';
3
3
  import * as yaml from 'js-yaml';
4
4
  import { DEFERRED_PATHS } from '../core/config.js';
5
5
  import { applyRootOption, readRootOption } from '../utils/package-filter.js';
6
+ import { printableConfig } from '../utils/printable-config.js';
6
7
  export function initCli(repository, program) {
7
8
  program.command({
8
9
  command: 'config',
@@ -19,7 +20,7 @@ export function initCli(repository, program) {
19
20
  handler: args => {
20
21
  const target = (!readRootOption(args) && repository.currentPackage) || repository.rootPackage;
21
22
  if (args.json) {
22
- console.log(JSON.stringify(target.config, undefined, 2));
23
+ console.log(JSON.stringify(printableConfig(target.config), undefined, 2));
23
24
  return;
24
25
  }
25
26
  /**
@@ -38,7 +39,7 @@ export function initCli(repository, program) {
38
39
  /** `noRefs`: a value appearing twice in the config is the *same object* after merging, and
39
40
  * js-yaml would otherwise emit the second as an `*anchor` reference - valid YAML that reads
40
41
  * as a mistake in something meant to be looked at. */
41
- console.log(yaml.dump(target.config, { noRefs: true, lineWidth: 100 }).trimEnd());
42
+ console.log(yaml.dump(printableConfig(target.config), { noRefs: true, lineWidth: 100 }).trimEnd());
42
43
  },
43
44
  });
44
45
  }
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '1.1.1';
1
+ export const version = '1.2.2';
package/core/config.d.ts CHANGED
@@ -96,6 +96,17 @@ export declare function selectorToRegExp(key: string): RegExp;
96
96
  * # first of these that exists, and an error naming the config path if none do
97
97
  * exec: 'tsc -b ${{ file.exists("tsconfig-build.json") || file.resolve("tsconfig.json") }}'
98
98
  * ```
99
+ *
100
+ * **Every member asks a question, and none may ever change anything - no `copy`, no `write`, no
101
+ * `mkdir`.** Not a matter of taste: this is evaluated when the config *resolves*, which every
102
+ * command does, so a member that acted would act on `rman list`, `rman info` and `rman config`.
103
+ *
104
+ * That has been tried, in the only way a missing function can be: a shared config reaching for a
105
+ * `file.copyMany(...)` that does not exist made **every** rman command exit 1 - and had it existed,
106
+ * the quieter outcome would have been files copied by `rman list`. Work belongs in a step
107
+ * (`run.<script>`'s slots, `version`'s hooks), which is the one place rman runs anything, and a
108
+ * step can now be a function - so there is nothing this would enable that is not already possible
109
+ * at the right moment.
99
110
  */
100
111
  export interface FileScope {
101
112
  /**
@@ -175,10 +186,6 @@ export interface RepositoryScope extends PackageScope {
175
186
  packages: PackageScope[];
176
187
  /** One package by name, or `undefined` - for reaching a sibling's directory. */
177
188
  package(name: string): PackageScope | undefined;
178
- /** Read from git only if an expression actually asks for it, then remembered: a repository that
179
- * never mentions these pays nothing, and every command resolves config. All `undefined` outside
180
- * a git checkout, which is a legitimate state rather than an error. */
181
- git: GitScope;
182
189
  }
183
190
  export interface GitScope {
184
191
  branch: string | undefined;
@@ -208,6 +215,23 @@ export interface ConfigScope {
208
215
  repository: RepositoryScope;
209
216
  /** Paths, resolved against the package the config was resolved for. */
210
217
  file: FileScope;
218
+ /**
219
+ * The **contents** of a structured file - `${{ read('tsconfig.json').compilerOptions.outDir }}` -
220
+ * where `file` answers only where one is. Resolved against `pkg.dirname` like `file`, so a
221
+ * `"[*]"` block asks each package about its own; a repository-level file is reached through
222
+ * `read(path.join(repository.dirname, ...))`.
223
+ *
224
+ * `.json`, `.yml`/`.yaml` and `.ini` by extension, or name it for a file that does not say
225
+ * (`read('.npmrc', 'ini')`). **Throws** when the file is absent, as `file.resolve` does - compose
226
+ * with `file.exists` when its absence is a case to handle.
227
+ *
228
+ * **A manifest is `pkg.manifest`, not this.** `read('package.json')` works and is the wrong
229
+ * answer: which file a package's identity lives in belongs to the ecosystem, so that expression
230
+ * is already wrong in a Cargo package sitting beside a Node one.
231
+ *
232
+ * The result is **deeply frozen and shared** - see `readStructuredFile`. Spread it to change it.
233
+ */
234
+ read: ReadFile;
211
235
  env: Record<string, string | undefined>;
212
236
  /** rman's own `semver`, for the arithmetic every release config eventually wants
213
237
  * (`semver.major(pkg.version)`). */
@@ -221,6 +245,23 @@ export interface ConfigScope {
221
245
  * genuinely needs one of them (a Docker image path, say, which is always posix).
222
246
  */
223
247
  path: typeof path;
248
+ /**
249
+ * The checkout: branch, sha, whether the tree is dirty.
250
+ *
251
+ * **Top level, not `repository.git`** - which is where it used to be, and the move is the point.
252
+ * `repository` shares its shape with `pkg` because the repository root *is* a package, and its
253
+ * only other members (`monorepo`, `packages`, `package()`) say something about the repository as
254
+ * a container of packages. A branch name says nothing about any package; it describes the
255
+ * working tree every one of them happens to be sitting in - the same kind of ambient fact as
256
+ * `env`, and it belongs beside it.
257
+ *
258
+ * **Read from git only if an expression actually asks**, then remembered for the whole run: every
259
+ * command resolves config, and a repository that never mentions git must not pay for one. See
260
+ * `Repository.configScope` for the getter, and `interpolateConfig` for why the context is built
261
+ * from property descriptors rather than a spread - a spread would fire this getter on every
262
+ * command, which is exactly what moving it up here risked.
263
+ */
264
+ git: GitScope;
224
265
  }
225
266
  /**
226
267
  * Evaluates every `${{ ... }}` expression in **every** string value of a resolved config, against
@@ -264,9 +305,13 @@ export interface ConfigScope {
264
305
  * A failing expression throws with the config path that holds it, rather than being left in place:
265
306
  * silently passing through a mistake is how a config ends up quietly doing nothing.
266
307
  */
267
- export declare function interpolateConfig<T>(config: T, scope: ConfigScope, options?: {
308
+ export interface InterpolateOptions {
309
+ /** Config paths to leave entirely untouched - `DEFERRED_PATHS`, when the whole config is walked. */
268
310
  skip?: string[];
269
- }): T;
311
+ /** Where `config` sits in the whole config, for a caller interpolating a fragment. */
312
+ at?: string[];
313
+ }
314
+ export declare function interpolateConfig<T>(config: T, scope: ConfigScope, options?: InterpolateOptions): T;
270
315
  /**
271
316
  * Config paths left untouched when a repository's config is first resolved, and evaluated only by
272
317
  * the command that runs them.
@@ -277,5 +322,79 @@ export declare function interpolateConfig<T>(config: T, scope: ConfigScope, opti
277
322
  * command at all would fail on a config that mentions it.
278
323
  */
279
324
  export declare const DEFERRED_PATHS: string[];
325
+ /**
326
+ * Paths whose value is a **step** - something to run later - rather than a setting to compute now.
327
+ * `*` matches one path segment (`run.<script>.exec`).
328
+ *
329
+ * This is what tells a step function from a value function, and the two live side by side in one
330
+ * config:
331
+ *
332
+ * ```js
333
+ * '[ws:*]': {
334
+ * clean: { include: ({ vars, value }) => [...value, vars.buildDir] }, // a value: called here
335
+ * run: { build: { after: ({ pkg }) => copyDocs(pkg) } }, // a step: called by `run`
336
+ * }
337
+ * ```
338
+ *
339
+ * **The key decides, and it already did.** `run.build.exec: 'tsc -b'` is a shell command and
340
+ * `publish.directory: 'build'` is a path - not because of anything about the strings, but because of
341
+ * where they sit. A function inherits the same rule, so nothing new has to be learned and no marker
342
+ * has to be remembered. The alternative was inspecting the function (arity, parameter names), which
343
+ * is the kind of guess `loadPlugins` refuses to make about a module's export for the same reason:
344
+ * guessing wrong here means running build-time code while merely loading the repository, or
345
+ * silently never running it.
346
+ *
347
+ * A **string** at one of these paths is still interpolated - `exec: 'tsc -b ${{ file.resolve(...) }}'`
348
+ * has to keep working - so this is narrower than `DEFERRED_PATHS`, which skips its paths entirely.
349
+ */
350
+ export declare const STEP_PATHS: string[];
351
+ /**
352
+ * Keys whose **whole subtree** is code rather than config, so no function under them is a value to
353
+ * compute. `plugins` is the only one, and it has to be here: an entry may be the plugin *object*
354
+ * itself, and an `RmanPlugin` is almost entirely functions - `manifest.read`, `workspace.resolve`,
355
+ * `versionPlanner`, `binPaths`, and every command's `builder` and `handler`.
356
+ *
357
+ * Measured, and it is why this exists: with `plugins` walked like any other key, resolving the
358
+ * config of a repository that named a plugin called that plugin's yargs builder with the config
359
+ * scope - `Config function in "plugins[0].commands[0].builder" failed: cmd.option is not a
360
+ * function`. A `plugins` entry is loaded by `loadPlugins`, never read as a setting.
361
+ */
362
+ export declare const CODE_SUBTREES: string[];
280
363
  /** The `file` namespace for one package's directory - see `FileScope`. */
364
+ /**
365
+ * `read` in a `${{ ... }}` expression (and in a value function): a structured file's **contents**,
366
+ * parsed - where `file` answers only where a path is.
367
+ *
368
+ * ```yaml
369
+ * "[*]":
370
+ * run:
371
+ * build:
372
+ * exec: 'tsc --outDir ${{ read("tsconfig.json").compilerOptions.outDir }}'
373
+ * ```
374
+ *
375
+ * `cache` is shared across every package (see `Repository.configScope`) and keyed by what the file
376
+ * *is*, not merely where - so the same file read by twenty packages is parsed once, and a file rman
377
+ * itself rewrites mid-run is re-read rather than remembered. See `readStructuredFile`.
378
+ */
379
+ export declare function createReadScope(dirname: string, cache: Map<string, CachedFile>): ReadFile;
380
+ /**
381
+ * What `read` can parse.
382
+ *
383
+ * **`.env` is deliberately absent, and that is the durable part of this list**: `env` is already in
384
+ * scope, and a `.env` file exists to be loaded *into* an environment by something else - a config
385
+ * reading one as data would mean two different things called the environment.
386
+ *
387
+ * Nothing else is excluded on principle. `xml` arrived because a `pom.xml` or a `.csproj` holds a
388
+ * version exactly the way a `package.json` does, and rman is language-agnostic; the earlier line
389
+ * ("no new parsers") did not survive it, since xmldom *is* a new one.
390
+ */
391
+ export type FileFormat = 'json' | 'yaml' | 'ini' | 'xml';
392
+ /** `read(path)`, or `read(path, 'ini')` for a file whose name does not say what it is (`.npmrc`). */
393
+ export type ReadFile = (target: string, format?: FileFormat) => unknown;
394
+ /** One parsed file, kept against the identity of the bytes it came from - see `readStructuredFile`. */
395
+ export interface CachedFile {
396
+ /** `mtimeNs:size`. */
397
+ stamp: string;
398
+ value: unknown;
399
+ }
281
400
  export declare function createFileScope(dirname: string): FileScope;