rman 2.0.0-beta.3 → 2.0.0-beta.4

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.
@@ -72,8 +72,14 @@ function preparePublishManifest(pkg, publishDir, packagesByName) {
72
72
  * builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
73
73
  * consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
74
74
  * tarball.
75
- * - `private` - rman refuses to publish a private package in the first place, so carrying the flag
76
- * into a manifest that is being published can only be wrong.
75
+ * - `private` - it is a statement about the **source tree**, not about the artifact derived from it,
76
+ * and a manifest being published is by definition not private. rman's own gate normally stops a
77
+ * private package before this runs, so the line looks unreachable; it is not, because `private`
78
+ * is also how a repository keeps a stray `npm publish` out of the package directory while
79
+ * publishing `build/` on purpose. That is a real configuration - this repository ran it until
80
+ * `rman publish` became its own release path and the gate started refusing the thing it ships.
81
+ * A narrower guard (`prepublishOnly`, which never reaches a consumer either) says the same thing
82
+ * without claiming the artifact is unpublishable.
77
83
  * - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
78
84
  */
79
85
  function derivePublishManifest(pkg, packagesByName) {
package/commands.d.ts CHANGED
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
20
20
  import './commands/github-release.command.js';
21
21
  import './commands/import.command.js';
22
22
  import './commands/info.command.js';
23
- import './commands/lint.command.js';
24
23
  import './commands/list.command.js';
25
24
  import './commands/publish.command.js';
26
25
  import './commands/run.command.js';
package/commands.js CHANGED
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
20
20
  import './commands/github-release.command.js';
21
21
  import './commands/import.command.js';
22
22
  import './commands/info.command.js';
23
- import './commands/lint.command.js';
24
23
  import './commands/list.command.js';
25
24
  import './commands/publish.command.js';
26
25
  import './commands/run.command.js';
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.0.0-beta.3';
1
+ export const version = '2.0.0-beta.4';
@@ -95,13 +95,15 @@ export declare class Workspace {
95
95
  /** Root, then every directory down to `targetDir`, inclusive. A target outside the root is the
96
96
  * root alone. */
97
97
  protected _dirChain(targetDir: string): string[];
98
- /** A level's own settings - everything that is not a `"[...]"` block. */
98
+ /** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
99
+ * is dropped whole, and what is inside it is the caller's business. */
99
100
  protected _stripSelectors(config: RmanConfig): RmanConfig;
100
101
  /**
101
- * The blocks in `config` that speak for this package, in declaration order.
102
+ * The layers in `config` that speak for this package, in the order they are merged.
102
103
  *
103
104
  * `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
104
- * root.
105
+ * root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
106
+ * where its own audience and every audience it sits inside all match.
105
107
  */
106
108
  protected _matchingSelectors(config: RmanConfig, audience: Workspace.Audience): RmanConfig[];
107
109
  /**
@@ -290,24 +290,68 @@ export class Workspace {
290
290
  }
291
291
  return dirs;
292
292
  }
293
- /** A level's own settings - everything that is not a `"[...]"` block. */
293
+ /** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
294
+ * is dropped whole, and what is inside it is the caller's business. */
295
+ /* **The symbols travel with it, and leaving them behind was a measured loss.** `mergeConfig` reads
296
+ * a key's origin and its `value` chain off the *source* object under `ORIGINS` / `PREVIOUS_VALUES`,
297
+ * so a copy that carries only string keys arrives with neither. Measured on one repository before
298
+ * this line existed: a bad expression in an unmarked key reported `Invalid expression in "group"`
299
+ * with no file, while the identical expression inside a `"[*]"` block reported
300
+ * `... ("group" (.rmanrc.yml))` - because a selector block was merged as itself and only the
301
+ * unmarked layer came through here. Nesting would have spread that to the blocks too. */
294
302
  _stripSelectors(config) {
295
303
  const result = {};
296
304
  for (const [key, value] of Object.entries(config))
297
305
  if (!ConfigReader.isSelectorKey(key))
298
306
  result[key] = value;
307
+ for (const symbol of Object.getOwnPropertySymbols(config)) {
308
+ Object.defineProperty(result, symbol, Object.getOwnPropertyDescriptor(config, symbol));
309
+ }
299
310
  return result;
300
311
  }
301
312
  /**
302
- * The blocks in `config` that speak for this package, in declaration order.
313
+ * The layers in `config` that speak for this package, in the order they are merged.
303
314
  *
304
315
  * `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
305
- * root.
316
+ * root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
317
+ * where its own audience and every audience it sits inside all match.
306
318
  */
307
319
  /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
308
320
  * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
309
321
  * setting to a root with no build directory to apply it to. A root block needs no selector, which
310
- * is what makes `/` structural. */
322
+ * is what makes `/` structural.
323
+ *
324
+ * **Nesting is depth-first pre-order, which is the level's own rule applied again rather than a
325
+ * new one**: a block's own keys are its floor, then the blocks written inside it in declaration
326
+ * order, then the next block beside it. So the layer order for
327
+ * `"[platform:node]" { a, "[pkg-*]" { b } }, "[*]" { c }` is `a, b, c` - and the last word is
328
+ * still whatever was written last, *not* whatever is most specific. That is the same cost the
329
+ * dropped specificity ranking already documents (a catch-all written below a narrower block
330
+ * overrides it), and nesting neither adds to it nor asks the tiebreak question again: two blocks
331
+ * that both match are siblings whatever depth they sit at, and declaration order answers them.
332
+ *
333
+ * **A matching block is contributed *stripped*, not whole.** Merging it as it stands put its
334
+ * nested keys into the resolved config as literal `'[pkg-*]'` entries, which is precisely how the
335
+ * old no-op showed up in `rman config` looking as though it had worked.
336
+ *
337
+ * **A platform block speaks for the root too, and only a glob is held off it.** `"[platform:node]"`
338
+ * asks `pkg.platform.name` and the root is a package with a platform, so
339
+ * `"[platform:node]" > "[/]"` is *the root, when the root is a node package* and
340
+ * `"[platform:node]" > "[*]"` is *its packages* - which is the whole of a technology's shared
341
+ * config in one block, `vars` included.
342
+ *
343
+ * A narrower rule was tried first and measured wrong: let a chain reach the root only where it
344
+ * *names* `"[/]"`, keeping a plain `"[platform:node]"` off the root as before. It answers the pair
345
+ * correctly and still breaks the case it was written for - the nested `"[/]"` could not read the
346
+ * `vars` its parent declared (`vars is not defined`), because those are the parent's own keys.
347
+ * Narrowing an audience and hiding the enclosing block's settings from it are different things,
348
+ * and only the first is what nesting means.
349
+ *
350
+ * The cost, stated rather than hidden: a package-shaped setting written **directly** under a
351
+ * platform block now reaches the root as well - the trap that keeps globs off it. It is not the
352
+ * same trap, because neither half of that one applies here: `platform:node` is not a name and
353
+ * cannot match by accident, and it is not a catch-all. And with nesting the spelling for
354
+ * package-shaped settings is `"[platform:node]" > "[*]"`, which says so. */
311
355
  _matchingSelectors(config, audience) {
312
356
  const matches = [];
313
357
  for (const [key, value] of Object.entries(config)) {
@@ -316,7 +360,9 @@ export class Workspace {
316
360
  const { scope, test } = ConfigReader.parseSelector(key);
317
361
  if (!this._speaksFor(scope, test, audience))
318
362
  continue;
319
- matches.push(value);
363
+ const block = value;
364
+ matches.push(this._stripSelectors(block));
365
+ matches.push(...this._matchingSelectors(block, audience));
320
366
  }
321
367
  return matches;
322
368
  }
@@ -328,8 +374,12 @@ export class Workspace {
328
374
  */
329
375
  /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
330
376
  * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
331
- * setting to a root with no build directory to apply it to. A platform block is held to the same
332
- * line for consistency - the root has a platform, but it also has `"[/]"`.
377
+ * setting to a root with no build directory to apply it to. Neither trap exists for a platform
378
+ * block - `platform:node` is not a name and cannot match by accident, and it is not a catch-all -
379
+ * so **a platform link answers about the root like any other package**, and what keeps a plain
380
+ * `"[platform:node]"` off the root is `_matchingSelectors`' chain rule rather than a refusal here.
381
+ * Asking `isRoot` first was what made `"[platform:node]" > "[/]"` unanswerable: the pair says *the
382
+ * root, when it is a node package*, and the short-circuit never let the platform question be put.
333
383
  *
334
384
  * A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
335
385
  * platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
@@ -337,11 +387,9 @@ export class Workspace {
337
387
  _speaksFor(scope, test, audience) {
338
388
  if (scope === 'root')
339
389
  return audience.isRoot;
340
- if (audience.isRoot)
341
- return false;
342
390
  if (scope === 'platform')
343
391
  return test(audience.platform);
344
- return audience.selector !== undefined && test(audience.selector);
392
+ return !audience.isRoot && audience.selector !== undefined && test(audience.selector);
345
393
  }
346
394
  /**
347
395
  * The platform that claims a directory - the one its config names, or the first loaded platform
@@ -13,7 +13,8 @@ export declare class ConfigReader {
13
13
  */
14
14
  static isSelectorKey(key: string): boolean;
15
15
  /**
16
- * **Which packages a selector key speaks for.** Two audiences, and the second is a glob:
16
+ * **Which packages a selector key speaks for.** Three audiences, and what each is matched
17
+ * against:
17
18
  *
18
19
  * | | matches | `test` is given |
19
20
  * | --- | --- | --- |
@@ -58,7 +59,27 @@ export declare class ConfigReader {
58
59
  * default export when there is one, the module's own exports object otherwise.
59
60
  */
60
61
  protected _loadConfigModule(file: string): Promise<any>;
62
+ /**
63
+ * Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
64
+ * whether the pair could ever match a package.
65
+ */
61
66
  protected _assertSelectorBlocks(config: RmanConfig, file: string): void;
67
+ /**
68
+ * One walk, refusing a selector key under a *setting* and checking each legal one's contents.
69
+ *
70
+ * @param at the key path to `node`.
71
+ * @param enclosing the selector keys `node` sits inside, outermost first.
72
+ */
73
+ protected _assertSelectorKeys(node: unknown, file: string, at: string[], enclosing: string[]): void;
74
+ /** What one selector block may carry, and - given `outer` - whether it can match anything. */
75
+ protected _assertSelectorBlock(key: string, value: unknown, file: string, outer: string | undefined): void;
76
+ /**
77
+ * Refuses a nested pair that could never match a package together.
78
+ *
79
+ * @param outer the enclosing selector key.
80
+ * @param inner the key written inside it.
81
+ */
82
+ protected _assertNestable(outer: string, inner: string, file: string): void;
62
83
  protected _resolveExtends(context: ConfigReader.Context, config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
63
84
  /**
64
85
  * Lays `options.presets` underneath this level's own config, exactly as an `extends` naming them
@@ -152,6 +173,8 @@ export declare namespace ConfigReader {
152
173
  /** Whether a package's selector is one this key speaks for. Always `true` for a root key, which
153
174
  * is matched by *being* the root rather than by name. */
154
175
  test(name: string): boolean;
176
+ /** The technologies a `"[platform:...]"` key names, and nothing for the other two scopes. */
177
+ names?: readonly string[];
155
178
  }
156
179
  interface Options {
157
180
  /** Plugins to start with - the application's. Optional like `platforms`, which it was not: a
@@ -6,6 +6,7 @@ import fastGlob from 'fast-glob';
6
6
  import * as yaml from 'js-yaml';
7
7
  import { presetNames, PRESETS_DIR } from '../../builtins/presets/index.js';
8
8
  import { declaredKind, isDeclared, isPlatform } from '../interfaces/plugin.js';
9
+ import { CODE_SUBTREES } from './config-paths.js';
9
10
  import { mergeConfig } from './merge-config.js';
10
11
  const requireConfigModule = createRequire(import.meta.url);
11
12
  const IDENTITY_KEYS = ['platform', 'name'];
@@ -21,6 +22,8 @@ const PRESET_EXTENSIONS = ['.js', '.ts'];
21
22
  /** What an error about a default preset names itself as - there is no config key to quote, since
22
23
  * these are laid down by the caller rather than written by anybody. */
23
24
  const PRESETS_LABEL = 'presets';
25
+ /** Free-form by contract - see `_assertSelectorKeys`. */
26
+ const VARS_KEY = 'vars';
24
27
  /** Keys that only an rman **config** has, for telling one from a contribution once it has already
25
28
  * been refused - see `_assertLoaded`. `name` is not among them: a plugin has one too. */
26
29
  const CONFIG_SHAPED = ['extends', PLUGINS_KEY, PLATFORMS_KEY, 'commands', 'publishTargets'];
@@ -46,7 +49,8 @@ export class ConfigReader {
46
49
  return key.length > 2 && key.startsWith('[') && key.endsWith(']');
47
50
  }
48
51
  /**
49
- * **Which packages a selector key speaks for.** Two audiences, and the second is a glob:
52
+ * **Which packages a selector key speaks for.** Three audiences, and what each is matched
53
+ * against:
50
54
  *
51
55
  * | | matches | `test` is given |
52
56
  * | --- | --- | --- |
@@ -82,7 +86,7 @@ export class ConfigReader {
82
86
  .split(',')
83
87
  .map(n => n.trim())
84
88
  .filter(Boolean);
85
- return { scope: 'platform', test: (name) => !!name && names.includes(name) };
89
+ return { scope: 'platform', test: (name) => !!name && names.includes(name), names };
86
90
  }
87
91
  const re = globToRegExp(stripWorkspacePrefix(inner));
88
92
  return { scope: 'package', test: (name) => re.test(name) };
@@ -292,38 +296,158 @@ export class ConfigReader {
292
296
  }
293
297
  return mod?.default ?? mod;
294
298
  }
299
+ /**
300
+ * Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
301
+ * whether the pair could ever match a package.
302
+ */
295
303
  _assertSelectorBlocks(config, file) {
296
- for (const [key, value] of Object.entries(config)) {
297
- if (!ConfigReader.isSelectorKey(key) || !value || typeof value !== 'object')
304
+ this._assertSelectorKeys(config, file, [], []);
305
+ }
306
+ /**
307
+ * One walk, refusing a selector key under a *setting* and checking each legal one's contents.
308
+ *
309
+ * @param at the key path to `node`.
310
+ * @param enclosing the selector keys `node` sits inside, outermost first.
311
+ */
312
+ /* **A selector key is legal at the top level of a config and directly inside another selector
313
+ * key, and nowhere else** - which is the whole of the placement rule, and `at.length >
314
+ * enclosing.length` is the whole of the test: the two arrays stay equal for as long as every
315
+ * ancestor is a selector, and `at` runs ahead the moment one is not.
316
+ *
317
+ * ```yaml
318
+ * "[platform:node]":
319
+ * "[pkg-*]": { group: x } # an intersection
320
+ * run:
321
+ * "[pkg-*]": { ... } # refused - a setting is not an audience
322
+ * ```
323
+ *
324
+ * **The nested form used to be refused outright and that was the wrong call**, on a reason that
325
+ * does not survive being written out: "selectors do not intersect, because specificity ranking was
326
+ * dropped". Ranking answers which of two *siblings* wins, and nesting asks nothing of the sort -
327
+ * a nested block is resolved depth-first in declaration order, which is the rule already in force
328
+ * one level up (`Workspace._matchingSelectors`). What the refusal cost was the only way to say
329
+ * "these packages, but only the ones that are also X" for a whole block: `if:` is per key and
330
+ * reaches neither `vars` nor any key that is not a run step.
331
+ *
332
+ * What the refusal *did* get right was that silence is unacceptable - before it, the inner block
333
+ * reached every package the outer one did, as a literal `'[pkg-*]'` key sitting in the resolved
334
+ * config where `rman config` showed it looking as though it had worked. Both halves are answered
335
+ * now: the shape works, and the shapes that could never work are refused here rather than
336
+ * quietly matching nothing.
337
+ *
338
+ * **`vars` and the contribution keys are skipped**, and both for the same reason: their contents
339
+ * are not config keys. `vars` is free-form by contract and `CODE_SUBTREES` hold plugins, commands
340
+ * and publish targets - arbitrary objects whose key space rman does not own. A bracketed name in
341
+ * either is data. */
342
+ _assertSelectorKeys(node, file, at, enclosing) {
343
+ if (!node || typeof node !== 'object' || Array.isArray(node))
344
+ return;
345
+ for (const [key, value] of Object.entries(node)) {
346
+ if (ConfigReader.isSelectorKey(key)) {
347
+ if (at.length > enclosing.length) {
348
+ throw new Error(`"${key}" in "${file}" is nested under "${at.join('.')}", which is a setting rather ` +
349
+ `than an audience - a selector is read at the top level of a config and inside ` +
350
+ `another selector, so this one would never be applied. Move it out, or use an ` +
351
+ `expression where the value itself depends on the package.`);
352
+ }
353
+ this._assertSelectorBlock(key, value, file, enclosing.at(-1));
354
+ this._assertSelectorKeys(value, file, [...at, key], [...enclosing, key]);
298
355
  continue;
299
- if (EXTENDS_KEY in value) {
300
- throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
301
- `statement about this config rather than about the packages the selector names.`);
302
356
  }
303
- /**
304
- * **The two keys a selector cannot carry, because the selector is downstream of them.**
305
- *
306
- * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
307
- * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
308
- * the packages are still being found, so a glob block setting either would need its own answer
309
- * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
310
- * would simply never be read.
311
- *
312
- * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
313
- * the repository root - so a root block needs no selector and is applied during the walk.
314
- */
315
- if (ConfigReader.parseSelector(key).scope === 'root')
357
+ if (key === VARS_KEY || CODE_SUBTREES.includes(key))
316
358
  continue;
317
- for (const identity of IDENTITY_KEYS) {
318
- if (identity in value) {
319
- throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
320
- `and "${identity}" is what the selector is derived from, so the block could never be ` +
321
- `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
322
- `under "[/]" for the root package.`);
323
- }
359
+ this._assertSelectorKeys(value, file, [...at, key], enclosing);
360
+ }
361
+ }
362
+ /** What one selector block may carry, and - given `outer` - whether it can match anything. */
363
+ _assertSelectorBlock(key, value, file, outer) {
364
+ if (!value || typeof value !== 'object')
365
+ return;
366
+ if (outer !== undefined)
367
+ this._assertNestable(outer, key, file);
368
+ if (EXTENDS_KEY in value) {
369
+ throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
370
+ `statement about this config rather than about the packages the selector names.`);
371
+ }
372
+ /**
373
+ * **The two keys a selector cannot carry, because the selector is downstream of them.**
374
+ *
375
+ * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
376
+ * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
377
+ * the packages are still being found, so a glob block setting either would need its own answer
378
+ * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
379
+ * would simply never be read.
380
+ *
381
+ * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
382
+ * the repository root - so a root block needs no selector and is applied during the walk.
383
+ */
384
+ if (ConfigReader.parseSelector(key).scope === 'root') {
385
+ /** **Except for `platform` in a *nested* `"[/]"`, which is read nowhere.**
386
+ * `Workspace._declaredPlatformName` looks at a level's own `platform` and at a **top-level**
387
+ * `"[/]"`, because which technology claims a directory has to be settled before any block is
388
+ * matched. Inside `"[platform:node]"` it is also circular - the answer decides whether the
389
+ * block that declares it applies at all. */
390
+ if (outer !== undefined && 'platform' in value) {
391
+ throw new Error(`"${key}" in "${file}" is nested inside "${outer}" and cannot set "platform" - which ` +
392
+ `technology claims a directory is settled before any selector is matched, so it is ` +
393
+ `read from the level's own keys or a top-level "[/]" and nowhere else. Write it there.`);
394
+ }
395
+ return;
396
+ }
397
+ for (const identity of IDENTITY_KEYS) {
398
+ if (identity in value) {
399
+ throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
400
+ `and "${identity}" is what the selector is derived from, so the block could never be ` +
401
+ `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
402
+ `under "[/]" for the root package.`);
324
403
  }
325
404
  }
326
405
  }
406
+ /**
407
+ * Refuses a nested pair that could never match a package together.
408
+ *
409
+ * @param outer the enclosing selector key.
410
+ * @param inner the key written inside it.
411
+ */
412
+ /* **Nesting is an AND, so a pair naming disjoint sets is a block that runs for nobody** - which is
413
+ * the silence worth refusing. Two pairs are decidable and both are:
414
+ *
415
+ * - **`"[/]"` paired with a glob**, either way round. A glob matches a package's name and the root
416
+ * is nobody's child, so it never matches the root whatever it is written beside; `"[/]"`
417
+ * matches nothing else. The pair is empty in both directions.
418
+ * - **Two platform blocks naming nothing in common.** A package carries one `platform.name`, so
419
+ * `"[platform:node]" > "[platform:cargo]"` is empty; `"[platform:node,cargo]" > "[platform:node]"`
420
+ * narrows and is fine.
421
+ *
422
+ * **`"[/]"` inside a platform block is the pair this deliberately allows**, and getting that wrong
423
+ * once is why the reasoning is written out. It reads as *the root, when the root is a node
424
+ * package*, which is a perfectly well-defined audience - it was unreachable only because
425
+ * `_speaksFor` asked `isRoot` before it asked the platform question. Refusing it took the natural
426
+ * shape of a technology's shared config away: a preset wanting `vars` over a whole node
427
+ * repository, the root's own settings, and the packages' settings had to hoist `vars` to the top
428
+ * level, where they also reach packages of every other technology.
429
+ *
430
+ * **A glob pair is deliberately not checked**, and the asymmetry is the point rather than an
431
+ * omission: whether two globs intersect is a real computation with a wrong answer available in
432
+ * both directions, while a platform set is `includes`. `"[pkg-*]" > "[lib-*]"` therefore loads and
433
+ * matches nothing - the same thing a top-level `"[lib-*]"` does in a repository with no such
434
+ * package, which nothing reports either. */
435
+ _assertNestable(outer, inner, file) {
436
+ const a = ConfigReader.parseSelector(outer);
437
+ const b = ConfigReader.parseSelector(inner);
438
+ const [root, glob] = a.scope === 'root' ? [outer, b.scope === 'package' && inner] : [inner, a.scope === 'package' && outer];
439
+ if ((a.scope === 'root' || b.scope === 'root') && glob) {
440
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", and "${glob}" can never match the ` +
441
+ `same package "${root}" does - a glob matches a package's name and the root is nobody's ` +
442
+ `child, so the pair is empty. A platform block nests with "[/]" ("the root, when it is ` +
443
+ `of that technology"); a glob does not.`);
444
+ }
445
+ if (a.scope === 'platform' && b.scope === 'platform' && !b.names?.some(name => a.test(name))) {
446
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", which names no technology in common ` +
447
+ `with it - a package carries one platform, so this block could never be applied. Nesting ` +
448
+ `narrows: name a technology the outer block already speaks for.`);
449
+ }
450
+ }
327
451
  async _resolveExtends(context, config, from, seen = []) {
328
452
  const declared = config[EXTENDS_KEY];
329
453
  if (declared === undefined)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.0.0-beta.3",
4
+ "version": "2.0.0-beta.4",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -71,4 +71,4 @@
71
71
  "publishConfig": {
72
72
  "access": "public"
73
73
  }
74
- }
74
+ }
@@ -1,176 +0,0 @@
1
- import { type RmanConfig } from '../interfaces/rman-config.interface.js';
2
- declare const COMMAND: "lint";
3
- declare const config: {
4
- parallel: {
5
- target: "cli";
6
- describe: string;
7
- coerce: (v: unknown) => boolean | number | undefined;
8
- };
9
- bail: {
10
- target: "cli";
11
- describe: string;
12
- type: "boolean";
13
- };
14
- topo: {
15
- target: "cli";
16
- describe: string;
17
- type: "boolean";
18
- };
19
- progress: {
20
- target: "cli";
21
- describe: string;
22
- type: "boolean";
23
- };
24
- changed: {
25
- target: "cli";
26
- alias: string;
27
- describe: string;
28
- type: "boolean";
29
- };
30
- changedSince: {
31
- target: "cli";
32
- cliName: string;
33
- describe: string;
34
- type: "string";
35
- conflicts: string;
36
- };
37
- fromRoot: {
38
- target: "cli";
39
- cliName: string;
40
- alias: string;
41
- describe: string;
42
- type: "boolean";
43
- };
44
- allowBranch: {
45
- target: "cli";
46
- cliName: string;
47
- describe: string;
48
- type: "string";
49
- };
50
- ignoreBranch: {
51
- target: "cli";
52
- cliName: string;
53
- describe: string;
54
- type: "string";
55
- };
56
- scope: {
57
- target: "cli";
58
- describe: string;
59
- type: "string";
60
- };
61
- ignore: {
62
- target: "cli";
63
- describe: string;
64
- type: "string";
65
- };
66
- platform: {
67
- target: "cli";
68
- describe: string;
69
- type: "string";
70
- };
71
- deps: {
72
- target: "cli";
73
- describe: string;
74
- type: "boolean";
75
- };
76
- dependents: {
77
- target: "cli";
78
- describe: string;
79
- type: "boolean";
80
- };
81
- };
82
- type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
83
- /**
84
- * `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
85
- */
86
- declare const lintCommand: (app: import("../index.js").RmanApplication) => {
87
- command: "lint";
88
- describe: string;
89
- /** Read, not owned - see the note above. */
90
- configKeys: string[];
91
- config: {
92
- parallel: {
93
- target: "cli";
94
- describe: string;
95
- coerce: (v: unknown) => boolean | number | undefined;
96
- };
97
- bail: {
98
- target: "cli";
99
- describe: string;
100
- type: "boolean";
101
- };
102
- topo: {
103
- target: "cli";
104
- describe: string;
105
- type: "boolean";
106
- };
107
- progress: {
108
- target: "cli";
109
- describe: string;
110
- type: "boolean";
111
- };
112
- changed: {
113
- target: "cli";
114
- alias: string;
115
- describe: string;
116
- type: "boolean";
117
- };
118
- changedSince: {
119
- target: "cli";
120
- cliName: string;
121
- describe: string;
122
- type: "string";
123
- conflicts: string;
124
- };
125
- fromRoot: {
126
- target: "cli";
127
- cliName: string;
128
- alias: string;
129
- describe: string;
130
- type: "boolean";
131
- };
132
- allowBranch: {
133
- target: "cli";
134
- cliName: string;
135
- describe: string;
136
- type: "string";
137
- };
138
- ignoreBranch: {
139
- target: "cli";
140
- cliName: string;
141
- describe: string;
142
- type: "string";
143
- };
144
- scope: {
145
- target: "cli";
146
- describe: string;
147
- type: "string";
148
- };
149
- ignore: {
150
- target: "cli";
151
- describe: string;
152
- type: "string";
153
- };
154
- platform: {
155
- target: "cli";
156
- describe: string;
157
- type: "string";
158
- };
159
- deps: {
160
- target: "cli";
161
- describe: string;
162
- type: "boolean";
163
- };
164
- dependents: {
165
- target: "cli";
166
- describe: string;
167
- type: "boolean";
168
- };
169
- };
170
- examples: {
171
- command: string;
172
- description: string;
173
- }[];
174
- handler: (args: Args) => Promise<void>;
175
- };
176
- export default lintCommand;
@@ -1,34 +0,0 @@
1
- import { registerCommand } from '../interfaces/rman-config.interface.js';
2
- import { assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
- import { readRunOptions, runOptions } from '../utils/run-options.js';
4
- const COMMAND = 'lint';
5
- const config = runOptions;
6
- /**
7
- * `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
8
- */
9
- /* **Core, not the `node` preset, and the test is what the command *knows*.** It knows a script
10
- * name and nothing else: a Cargo repository declaring `lint: 'cargo clippy'` and a Go one
11
- * declaring `golangci-lint run` are served by exactly this file. That is the line `clean` is on the
12
- * other side of - everything `clean` knows how to delete is a TypeScript fact - and it is why
13
- * `build` and `test` are here too.
14
- *
15
- * **It owns no config key**, for the reason `build` documents: `lint` is `run lint` under another
16
- * name, so its settings are `run.lint`, which belongs to `run`. Two commands cannot contribute
17
- * under one top-level key - interface merging is not a deep merge - and these three never needed
18
- * to. */
19
- const lintCommand = registerCommand(app => {
20
- const repository = app.repository;
21
- return {
22
- command: COMMAND,
23
- describe: 'Alias for "run lint"',
24
- /** Read, not owned - see the note above. */
25
- configKeys: ['run.lint'],
26
- config,
27
- examples: [{ command: '$0 lint', description: '# Lints packages' }],
28
- handler: async (args) => {
29
- await assertAllowedBranch(repository, readBranchGuardOptions(args));
30
- await app.getService('run').runScript('lint', { ...readRunOptions(args), commandName: 'lint' });
31
- },
32
- };
33
- });
34
- export default lintCommand;