rman 1.0.10 → 1.1.1

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.
Files changed (98) hide show
  1. package/README.md +116 -85
  2. package/cli.js +225 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +60 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +257 -6
  17. package/core/config.js +409 -17
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +57 -0
  25. package/core/merge-config.js +146 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +85 -4
  31. package/core/repository.js +258 -81
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/version-scheme.d.ts +134 -0
  35. package/core/version-scheme.js +148 -0
  36. package/core/workspace.d.ts +68 -0
  37. package/core/workspace.js +83 -0
  38. package/index.d.ts +54 -8
  39. package/index.js +42 -7
  40. package/interfaces/rman-config.interface.d.ts +226 -28
  41. package/package.json +15 -7
  42. package/services/change-hash.service.d.ts +88 -0
  43. package/services/change-hash.service.js +112 -0
  44. package/services/changelog.service.d.ts +8 -13
  45. package/services/changelog.service.js +12 -11
  46. package/services/conventional-commits.service.d.ts +73 -0
  47. package/services/conventional-commits.service.js +116 -0
  48. package/services/docker-publish.service.js +1 -1
  49. package/services/exec.service.js +1 -1
  50. package/services/github-release.service.d.ts +2 -2
  51. package/services/github-release.service.js +10 -5
  52. package/services/list.service.js +5 -2
  53. package/services/run.service.d.ts +68 -3
  54. package/services/run.service.js +162 -70
  55. package/services/system-info.d.ts +22 -7
  56. package/services/system-info.js +8 -23
  57. package/services/version-plan.service.d.ts +244 -0
  58. package/services/version-plan.service.js +414 -0
  59. package/services/version.service.d.ts +92 -82
  60. package/services/version.service.js +233 -383
  61. package/services.d.ts +5 -3
  62. package/services.js +5 -3
  63. package/utils/bin-path.d.ts +59 -0
  64. package/utils/bin-path.js +82 -0
  65. package/utils/child-tracker.d.ts +16 -0
  66. package/utils/child-tracker.js +30 -0
  67. package/utils/exec.d.ts +13 -2
  68. package/utils/exec.js +17 -17
  69. package/utils/git.d.ts +9 -3
  70. package/utils/git.js +10 -2
  71. package/utils/package-filter.d.ts +33 -2
  72. package/utils/package-filter.js +47 -7
  73. package/utils/release-version.js +3 -3
  74. package/utils/run-bin.d.ts +46 -0
  75. package/utils/run-bin.js +63 -0
  76. package/utils/version-stamp.d.ts +48 -0
  77. package/utils/version-stamp.js +88 -0
  78. package/commands/ci.command.js +0 -30
  79. package/commands/clean.command.d.ts +0 -3
  80. package/commands/clean.command.js +0 -36
  81. package/commands/publish.command.d.ts +0 -3
  82. package/commands/publish.command.js +0 -225
  83. package/rmanrc.schema.json +0 -375
  84. package/services/ci.service.d.ts +0 -40
  85. package/services/ci.service.js +0 -204
  86. package/services/clean.service.d.ts +0 -42
  87. package/services/clean.service.js +0 -226
  88. package/services/publish.service.d.ts +0 -79
  89. package/services/publish.service.js +0 -207
  90. package/utils/change-hash.d.ts +0 -68
  91. package/utils/change-hash.js +0 -98
  92. package/utils/conventional-commits.d.ts +0 -52
  93. package/utils/conventional-commits.js +0 -90
  94. package/utils/npm-run-path.d.ts +0 -67
  95. package/utils/npm-run-path.js +0 -63
  96. package/utils/workspace-range.d.ts +0 -17
  97. package/utils/workspace-range.js +0 -28
  98. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -1,3 +1,5 @@
1
+ import { type ConfigScope, type GitScope, type PackageScope, type RepositoryScope } from './config.js';
2
+ import type { LoadedCommand } from './custom-command.js';
1
3
  import { Package } from './package.js';
2
4
  export declare class Repository extends Package {
3
5
  readonly dirname: string;
@@ -8,6 +10,20 @@ export declare class Repository extends Package {
8
10
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
9
11
  readonly cwd: string;
10
12
  readonly rootPackage: Package;
13
+ /** Commands the repository's plugins contributed, loaded during `create` because the workspace
14
+ * providers they bring are needed before any package can be found. `cli.ts` registers them. */
15
+ pluginCommands: LoadedCommand[];
16
+ /**
17
+ * Cached repository scope - see `_repositoryScope`.
18
+ *
19
+ * **Non-enumerable**, and for the same reason `targetVersion` itself is: this cache holds a
20
+ * `PackageScope` per package, each carrying that throwing getter, and it hangs off a `Repository`
21
+ * which every `Package` points back at. Left enumerable, *any* deep walk of a package reached it
22
+ * and threw - measured through a test's own `toEqual` diff, but a `JSON.stringify` or a debugger
23
+ * would do it too, with an error about `version` that has nothing to do with what the caller did.
24
+ * An internal cache has no business being walked anyway.
25
+ */
26
+ private _repoScope?;
11
27
  protected constructor(dirname: string, monorepo: boolean, packages: Package[],
12
28
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
13
29
  * resolved repository root, possibly several levels up), this is where the user's shell
@@ -37,12 +53,78 @@ export declare class Repository extends Package {
37
53
  hash?: string;
38
54
  }): Promise<Record<string, Repository.PackageStatus>>;
39
55
  /**
40
- * Resolves the effective rman config for the repository root and every
41
- * package, cascading root -> intermediate directories -> package directory,
42
- * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
56
+ * The scope a `${{ ... }}` expression is evaluated against for `pkg`, optionally with the version
57
+ * a run is about to write bound into it.
58
+ *
59
+ * `version` is the only caller that passes one, and it has to: the config was resolved before its
60
+ * plan existed, so `pkg.targetVersion` had nothing to be. Re-evaluating that raw value against
61
+ * this is how that one binding gets filled in, without every other command paying for it - or
62
+ * seeing a value that means nothing to them.
63
+ */
64
+ configScope(pkg: Package, options?: {
65
+ targetVersion?: string;
66
+ }): ConfigScope;
67
+ /**
68
+ * Resolves the effective rman config for the repository root and every package, cascading
69
+ * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
70
+ * that path overrides the levels above it.
71
+ *
72
+ * Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
73
+ * block matches against - see `resolveConfig`. The root package is resolved by name too: in a
74
+ * single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
75
+ * nothing under `getPackages()` is the root, so only its own unmarked config applies.
76
+ */
77
+ /**
78
+ * Gives every package its `repository` and `parent`, before any config is resolved - a config
79
+ * expression or a provider may already want to navigate from a package outwards.
80
+ *
81
+ * A repository's own `repository` is itself, which reads oddly and is the honest answer:
82
+ * `Repository extends Package`, so the repository *is* a package of its own repository.
43
83
  */
84
+ protected _linkPackages(): void;
44
85
  protected _resolveConfigs(): Promise<void>;
86
+ protected _topoSortPackages(packages: Package[]): void;
87
+ protected _packageScope(pkg: Package, targetVersion?: string): PackageScope;
88
+ /** Built once and reused: it is the same for every package, and its `git` getter caches too, so a
89
+ * repository whose config never mentions git spawns none. */
90
+ protected _repositoryScope(): RepositoryScope;
91
+ /**
92
+ * One `.rmanrc "dependencies"` entry to a package: its **name** first, then a
93
+ * **repository-relative directory**.
94
+ *
95
+ * The path form is what makes the key usable outside npm. A name identifies a package only where
96
+ * the ecosystem guarantees uniqueness - the same reason `Package.dependencies` holds references
97
+ * and `Workspace.Layout` carries paths - so a repository whose names collide, or whose packages
98
+ * have no names rman can read, states the edge by directory instead.
99
+ *
100
+ * Name first because that is what a Node repository writes and there is no ambiguity in practice:
101
+ * a package name that is also an existing directory path in the same repository does not occur.
102
+ * An entry matching neither is ignored, as an unknown name always was - the graph is a statement
103
+ * about packages that exist.
104
+ */
105
+ protected _resolveDeclaredPackage(entry: string): Package | undefined;
45
106
  protected _updateDependencies(): void;
107
+ /** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
108
+ * getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
109
+ * error, just a repository without one. */
110
+ protected _readGitScope(dirname: string): GitScope;
111
+ /**
112
+ * Opens the repository containing `root` (default: the current directory).
113
+ *
114
+ * Three steps, in this order because each needs the one before it:
115
+ *
116
+ * 1. **Find the root** without knowing any ecosystem - see `Workspace.findRoot`. It cannot be
117
+ * otherwise: the plugins that know what a package is are named in the config file this step
118
+ * is looking for.
119
+ * 2. **Load the plugins** the root's config names, which registers their workspace providers
120
+ * (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
121
+ * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
122
+ * the one package.
123
+ *
124
+ * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
125
+ * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
126
+ * takes `plugins: ['rman-node']` to be read as one.
127
+ */
46
128
  static create(root?: string, options?: {
47
129
  deep?: number;
48
130
  }): Promise<Repository>;
@@ -50,7 +132,6 @@ export declare class Repository extends Package {
50
132
  * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
51
133
  * before the dependency graph is built from it. */
52
134
  private static _init;
53
- protected static _resolvePackages(dirname: string, patterns: string[]): Package[];
54
135
  }
55
136
  export declare namespace Repository {
56
137
  type PackageStatus = 'dirty' | 'committed' | 'changed' | 'clean';
@@ -1,15 +1,32 @@
1
- import glob from 'fast-glob';
2
- import fs from 'fs';
1
+ import { execFileSync } from 'node:child_process';
3
2
  import path from 'path';
3
+ import semver from 'semver';
4
4
  import { GitHelper } from '../utils/git.js';
5
- import { resolveConfig } from './config.js';
5
+ import { createFileScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
6
+ import { Manifest } from './manifest.js';
6
7
  import { Package } from './package.js';
8
+ import { loadPlugins } from './plugin.js';
9
+ import { Workspace } from './workspace.js';
7
10
  export class Repository extends Package {
8
11
  dirname;
9
12
  monorepo;
10
13
  packages;
11
14
  cwd;
12
15
  rootPackage;
16
+ /** Commands the repository's plugins contributed, loaded during `create` because the workspace
17
+ * providers they bring are needed before any package can be found. `cli.ts` registers them. */
18
+ pluginCommands = [];
19
+ /**
20
+ * Cached repository scope - see `_repositoryScope`.
21
+ *
22
+ * **Non-enumerable**, and for the same reason `targetVersion` itself is: this cache holds a
23
+ * `PackageScope` per package, each carrying that throwing getter, and it hangs off a `Repository`
24
+ * which every `Package` points back at. Left enumerable, *any* deep walk of a package reached it
25
+ * and threw - measured through a test's own `toEqual` diff, but a `JSON.stringify` or a debugger
26
+ * would do it too, with an error about `version` that has nothing to do with what the caller did.
27
+ * An internal cache has no business being walked anyway.
28
+ */
29
+ _repoScope;
13
30
  constructor(dirname, monorepo, packages,
14
31
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
15
32
  * resolved repository root, possibly several levels up), this is where the user's shell
@@ -52,7 +69,7 @@ export class Repository extends Package {
52
69
  result = result.filter(p => scopes.includes(p.name));
53
70
  }
54
71
  if (options?.toposort)
55
- topoSortPackages(result);
72
+ this._topoSortPackages(result);
56
73
  return result;
57
74
  }
58
75
  getPackage(name) {
@@ -87,51 +104,201 @@ export class Repository extends Package {
87
104
  return result;
88
105
  }
89
106
  /**
90
- * Resolves the effective rman config for the repository root and every
91
- * package, cascading root -> intermediate directories -> package directory,
92
- * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
107
+ * The scope a `${{ ... }}` expression is evaluated against for `pkg`, optionally with the version
108
+ * a run is about to write bound into it.
109
+ *
110
+ * `version` is the only caller that passes one, and it has to: the config was resolved before its
111
+ * plan existed, so `pkg.targetVersion` had nothing to be. Re-evaluating that raw value against
112
+ * this is how that one binding gets filled in, without every other command paying for it - or
113
+ * seeing a value that means nothing to them.
93
114
  */
115
+ configScope(pkg, options) {
116
+ return {
117
+ pkg: this._packageScope(pkg, options?.targetVersion),
118
+ repository: this._repositoryScope(),
119
+ /** `pkg.dirname`, not the repository root: a `"[*]"` block asking whether
120
+ * `tsconfig-build.json` exists has to be answered per package. */
121
+ file: createFileScope(pkg.dirname),
122
+ env: { ...process.env },
123
+ semver,
124
+ path,
125
+ };
126
+ }
127
+ /**
128
+ * Resolves the effective rman config for the repository root and every package, cascading
129
+ * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
130
+ * that path overrides the levels above it.
131
+ *
132
+ * Each package is resolved *by name* as well as by directory, since that is what a `"[selector]"`
133
+ * block matches against - see `resolveConfig`. The root package is resolved by name too: in a
134
+ * single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
135
+ * nothing under `getPackages()` is the root, so only its own unmarked config applies.
136
+ */
137
+ /**
138
+ * Gives every package its `repository` and `parent`, before any config is resolved - a config
139
+ * expression or a provider may already want to navigate from a package outwards.
140
+ *
141
+ * A repository's own `repository` is itself, which reads oddly and is the honest answer:
142
+ * `Repository extends Package`, so the repository *is* a package of its own repository.
143
+ */
144
+ _linkPackages() {
145
+ this.repository = this;
146
+ this.rootPackage.repository = this;
147
+ for (const pkg of this.packages) {
148
+ pkg.repository = this;
149
+ /** The deepest package that strictly contains it - the root for an ordinary member, an
150
+ * enclosing package for a nested one. Longest containing path wins, the same rule
151
+ * `currentPackage` uses to resolve "the package I am standing in". */
152
+ let parent = this.monorepo ? this.rootPackage : undefined;
153
+ for (const other of this.packages) {
154
+ if (other === pkg)
155
+ continue;
156
+ const rel = path.relative(other.dirname, pkg.dirname);
157
+ const contains = !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
158
+ if (contains && (!parent || other.dirname.length > parent.dirname.length))
159
+ parent = other;
160
+ }
161
+ pkg.parent = pkg === this.rootPackage ? undefined : parent;
162
+ }
163
+ }
94
164
  async _resolveConfigs() {
95
165
  const cache = new Map();
96
- const rootConfig = await resolveConfig(this.dirname, this.dirname, cache);
97
- this.config = rootConfig;
98
- this.rootPackage.config = rootConfig;
166
+ /**
167
+ * `DEFERRED_PATHS` are left *raw* in the result (see `interpolateConfig`'s `walk`), so the
168
+ * resolved config is the only copy anyone needs - `version` reads its own hooks straight off
169
+ * `config` and interpolates them itself once `pkg.targetVersion` exists. There used to be a
170
+ * second `rawConfig` copy of every package's config for that one reader; measured identical.
171
+ */
172
+ /**
173
+ * The root is resolved **with its name**, like every other package, because selectors now speak
174
+ * to it: `"[/]"` names it and `"[*]"` includes it. It used to be resolved without one, which is
175
+ * what made `"[*]"` quietly mean "the workspace packages" - `"[ws:*]"` is that, spelled.
176
+ */
177
+ const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.name);
178
+ this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
99
179
  for (const pkg of this.packages) {
100
- if (pkg === this.rootPackage)
101
- continue;
102
- pkg.config = await resolveConfig(this.dirname, pkg.dirname, cache);
180
+ const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name);
181
+ pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
103
182
  }
183
+ if (this.monorepo)
184
+ this.rootPackage.config = this.config;
185
+ }
186
+ _topoSortPackages(packages) {
187
+ packages.sort((a, b) => {
188
+ if (b.dependencies.includes(a))
189
+ return -1;
190
+ if (a.dependencies.includes(b))
191
+ return 1;
192
+ return 0;
193
+ });
194
+ }
195
+ _packageScope(pkg, targetVersion) {
196
+ // A manifest without a "name" is unusual but legal, and `info` prints such a package rather
197
+ // than refusing it - so the scope has to survive one too.
198
+ const name = pkg.name ?? '';
199
+ /** Splitting `@scope/name` is npm's convention, not a universal - the provider decides. */
200
+ const { scope: nameScope, unscopedName } = Manifest.splitName(pkg.dirname, name);
201
+ const scope = {
202
+ name,
203
+ scope: nameScope,
204
+ unscopedName,
205
+ version: pkg.version ?? '',
206
+ basename: path.basename(pkg.dirname),
207
+ dirname: pkg.dirname,
208
+ relativeDir: path.relative(this.dirname, pkg.dirname),
209
+ /** The same `Package.provider`, so a `"[*]"` declaration can say something for one ecosystem
210
+ * only - `if: "${{ pkg.provider === 'node' }}"` in a polyglot repository. Omitting it would
211
+ * leave the expression scope and the `Package` disagreeing about what a package is. */
212
+ provider: pkg.provider,
213
+ // A copy: an expression has no business mutating the package rman is about to act on.
214
+ manifest: { ...pkg.manifest.raw },
215
+ };
216
+ if (targetVersion !== undefined) {
217
+ scope.targetVersion = targetVersion;
218
+ return scope;
219
+ }
220
+ /** Otherwise a getter that *throws*, rather than a missing key handing back `undefined` and
221
+ * letting a tag come out as "app:undefined". Defined rather than assigned because there is no
222
+ * value to assign - outside a `version` run there is no target version to name.
223
+ *
224
+ * Non-enumerable, and that is load-bearing: spreading an object runs its enumerable getters,
225
+ * so an enumerable one threw the moment `_repositoryScope` spread the root's scope - which is
226
+ * every `configScope()` call, for every command. Property access still triggers it, which is
227
+ * the only thing it exists for. */
228
+ Object.defineProperty(scope, 'targetVersion', {
229
+ enumerable: false,
230
+ get() {
231
+ throw new Error('pkg.targetVersion is only available while "version" is running - no other command has a target version');
232
+ },
233
+ });
234
+ return scope;
235
+ }
236
+ /** Built once and reused: it is the same for every package, and its `git` getter caches too, so a
237
+ * repository whose config never mentions git spawns none. */
238
+ _repositoryScope() {
239
+ if (this._repoScope)
240
+ return this._repoScope;
241
+ const packageScopes = this.packages.map(p => this._packageScope(p));
242
+ const repoDir = this.dirname;
243
+ let gitScope;
244
+ const _this = this;
245
+ const built = {
246
+ ...this._packageScope(this.rootPackage),
247
+ monorepo: this.monorepo,
248
+ packages: packageScopes,
249
+ package: (name) => packageScopes.find(p => p.name === name),
250
+ get git() {
251
+ return (gitScope ??= _this._readGitScope(repoDir));
252
+ },
253
+ };
254
+ /** Defined rather than assigned, so the cache stays out of every enumeration of this object -
255
+ * see the field's own doc for what walked into it. */
256
+ Object.defineProperty(this, '_repoScope', { value: built, enumerable: false, writable: true });
257
+ return built;
258
+ }
259
+ /**
260
+ * One `.rmanrc "dependencies"` entry to a package: its **name** first, then a
261
+ * **repository-relative directory**.
262
+ *
263
+ * The path form is what makes the key usable outside npm. A name identifies a package only where
264
+ * the ecosystem guarantees uniqueness - the same reason `Package.dependencies` holds references
265
+ * and `Workspace.Layout` carries paths - so a repository whose names collide, or whose packages
266
+ * have no names rman can read, states the edge by directory instead.
267
+ *
268
+ * Name first because that is what a Node repository writes and there is no ambiguity in practice:
269
+ * a package name that is also an existing directory path in the same repository does not occur.
270
+ * An entry matching neither is ignored, as an unknown name always was - the graph is a statement
271
+ * about packages that exist.
272
+ */
273
+ _resolveDeclaredPackage(entry) {
274
+ const byName = this.getPackage(entry);
275
+ if (byName)
276
+ return byName;
277
+ const dir = path.resolve(this.dirname, entry);
278
+ return this.packages.find(p => path.resolve(p.dirname) === dir);
104
279
  }
105
280
  _updateDependencies() {
106
281
  const deps = {};
107
282
  for (const pkg of this.packages) {
108
- const o = {
109
- ...pkg.json.dependencies,
110
- ...pkg.json.devDependencies,
111
- ...pkg.json.peerDependencies,
112
- ...pkg.json.optionalDependencies,
113
- };
114
- const configDeps = pkg.config.packages?.[pkg.name]?.dependencies;
115
- if (configDeps) {
116
- if (Array.isArray(configDeps))
117
- configDeps.forEach(x => (o[x] = o[x] || '*'));
118
- else
119
- Object.assign(o, configDeps);
283
+ /** Which manifest fields hold dependencies is the ecosystem's business, so the provider
284
+ * reads them - npm's four field names used to be spelled out right here. */
285
+ const dependencies = [...Manifest.dependenciesOf(pkg, this.packages)];
286
+ /** `.rmanrc "dependencies"` on top, and it works with no provider at all: a repository rman
287
+ * cannot read the manifests of can still declare its graph by hand. */
288
+ const declared = pkg.config.dependencies ?? [];
289
+ for (const entry of declared) {
290
+ const p = this._resolveDeclaredPackage(entry);
291
+ if (p && p !== pkg && !dependencies.includes(p))
292
+ dependencies.push(p);
120
293
  }
121
- const dependencies = [];
122
- for (const k of Object.keys(o)) {
123
- const p = this.getPackage(k);
124
- if (p)
125
- dependencies.push(k);
126
- }
127
- deps[pkg.name] = dependencies;
294
+ deps[pkg.name] = dependencies.map(d => d.name);
128
295
  pkg.dependencies = dependencies;
129
296
  }
130
297
  let circularCheck;
131
298
  const deepFindDependencies = (pkg, target) => {
132
- if (circularCheck.includes(pkg.name))
299
+ if (circularCheck.includes(pkg))
133
300
  return;
134
- circularCheck.push(pkg.name);
301
+ circularCheck.push(pkg);
135
302
  for (const s of pkg.dependencies) {
136
303
  /** `target` starts out *as* the top-level package's own `dependencies` array, so its
137
304
  * direct entries are trivially "already in target" - recursing only when newly-added
@@ -140,9 +307,7 @@ export class Repository extends Package {
140
307
  * and never let the top-level package end up depending on itself via a cycle. */
141
308
  if (s !== circularCheck[0] && !target.includes(s))
142
309
  target.push(s);
143
- const p = this.getPackage(s);
144
- if (p)
145
- deepFindDependencies(p, target);
310
+ deepFindDependencies(s, target);
146
311
  }
147
312
  };
148
313
  for (const pkg of this.packages) {
@@ -150,25 +315,63 @@ export class Repository extends Package {
150
315
  deepFindDependencies(pkg, pkg.dependencies);
151
316
  }
152
317
  }
153
- static async create(root, options) {
154
- const dirname = root || process.cwd();
155
- let deep = options?.deep ?? 10;
156
- let pkgDirname = dirname;
157
- while (deep-- >= 0 && fs.existsSync(pkgDirname)) {
158
- const f = path.join(pkgDirname, 'package.json');
159
- if (fs.existsSync(f)) {
160
- const pkgJson = JSON.parse(fs.readFileSync(f, 'utf-8'));
161
- if (Array.isArray(pkgJson.workspaces)) {
162
- const packages = this._resolvePackages(pkgDirname, pkgJson.workspaces);
163
- return Repository._init(new Repository(pkgDirname, true, packages, dirname));
164
- }
165
- /** If we reach to the root of the project */
166
- if (fs.existsSync(path.join(pkgDirname, '.git')))
167
- break;
318
+ /** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
319
+ * getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
320
+ * error, just a repository without one. */
321
+ _readGitScope(dirname) {
322
+ const run = (args) => {
323
+ try {
324
+ return execFileSync('git', args, { cwd: dirname, stdio: ['ignore', 'pipe', 'ignore'] })
325
+ .toString()
326
+ .trim();
168
327
  }
169
- pkgDirname = path.resolve(pkgDirname, '..');
170
- }
171
- return Repository._init(new Repository(dirname, false, [], dirname));
328
+ catch {
329
+ return undefined;
330
+ }
331
+ };
332
+ const sha = run(['rev-parse', 'HEAD']);
333
+ if (sha === undefined)
334
+ return { branch: undefined, sha: undefined, shortSha: undefined, dirty: undefined };
335
+ const status = run(['status', '--porcelain']);
336
+ return {
337
+ // Empty on a detached HEAD, which is what a CI checkout often is - reported as undefined
338
+ // rather than an empty string, so `?? 'detached'` in an expression works.
339
+ branch: run(['branch', '--show-current']) || undefined,
340
+ sha,
341
+ shortSha: sha.slice(0, 7),
342
+ dirty: status === undefined ? undefined : status.length > 0,
343
+ };
344
+ }
345
+ /**
346
+ * Opens the repository containing `root` (default: the current directory).
347
+ *
348
+ * Three steps, in this order because each needs the one before it:
349
+ *
350
+ * 1. **Find the root** without knowing any ecosystem - see `Workspace.findRoot`. It cannot be
351
+ * otherwise: the plugins that know what a package is are named in the config file this step
352
+ * is looking for.
353
+ * 2. **Load the plugins** the root's config names, which registers their workspace providers
354
+ * (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
355
+ * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
356
+ * the one package.
357
+ *
358
+ * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
359
+ * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
360
+ * takes `plugins: ['rman-node']` to be read as one.
361
+ */
362
+ static async create(root, options) {
363
+ const from = root || process.cwd();
364
+ const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
365
+ /** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
366
+ * package list (which does not exist yet) nor expression interpolation. */
367
+ const rootConfig = await readDirConfig(rootDir);
368
+ const pluginCommands = await loadPlugins(rootDir, rootConfig);
369
+ const layout = Workspace.resolve(rootDir);
370
+ const packages = (layout?.packageDirs ?? []).map(dir => new Package(dir));
371
+ const repo = new Repository(layout?.root ?? rootDir, packages.length > 0, packages, from);
372
+ repo.pluginCommands = pluginCommands;
373
+ repo._linkPackages();
374
+ return Repository._init(repo);
172
375
  }
173
376
  /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
174
377
  * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
@@ -178,30 +381,4 @@ export class Repository extends Package {
178
381
  repo._updateDependencies();
179
382
  return repo;
180
383
  }
181
- static _resolvePackages(dirname, patterns) {
182
- const packages = [];
183
- for (const pattern of patterns) {
184
- const dirs = glob.sync(pattern, {
185
- cwd: dirname,
186
- absolute: true,
187
- deep: 0,
188
- onlyDirectories: true,
189
- });
190
- for (const dir of dirs) {
191
- const f = path.join(dir, 'package.json');
192
- if (fs.existsSync(f))
193
- packages.push(new Package(dir));
194
- }
195
- }
196
- return packages;
197
- }
198
- }
199
- function topoSortPackages(packages) {
200
- packages.sort((a, b) => {
201
- if (b.dependencies.includes(a.name))
202
- return -1;
203
- if (a.dependencies.includes(b.name))
204
- return 1;
205
- return 0;
206
- });
207
384
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolves a config-supplied module reference - an `extends` base, a `plugins` entry - **relative
3
+ * to the file that named it**, not to rman's own location.
4
+ *
5
+ * That distinction is the whole reason this is a function: resolving from rman would look in rman's
6
+ * own dependencies, where a repository's shared config or plugin has no reason to be. A bare
7
+ * specifier therefore goes through `from`'s own `node_modules` (a package's `exports` subpaths
8
+ * included); anything path-like resolves against `from`'s directory.
9
+ *
10
+ * `label` names the config key in the error, so a failure says which setting to go and look at.
11
+ */
12
+ export declare function resolveConfigTarget(target: string, from: string, label: string): string;
@@ -0,0 +1,33 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ /** File forms a path-like target may take when it names no extension of its own. */
6
+ const TARGET_EXTENSIONS = ['', '.yml', '.yaml', '.json', '.cjs', '.mjs', '.js'];
7
+ /**
8
+ * Resolves a config-supplied module reference - an `extends` base, a `plugins` entry - **relative
9
+ * to the file that named it**, not to rman's own location.
10
+ *
11
+ * That distinction is the whole reason this is a function: resolving from rman would look in rman's
12
+ * own dependencies, where a repository's shared config or plugin has no reason to be. A bare
13
+ * specifier therefore goes through `from`'s own `node_modules` (a package's `exports` subpaths
14
+ * included); anything path-like resolves against `from`'s directory.
15
+ *
16
+ * `label` names the config key in the error, so a failure says which setting to go and look at.
17
+ */
18
+ export function resolveConfigTarget(target, from, label) {
19
+ const dir = path.dirname(path.resolve(from));
20
+ if (target.startsWith('.') || path.isAbsolute(target)) {
21
+ const candidate = path.resolve(dir, target);
22
+ const found = TARGET_EXTENSIONS.map(ext => candidate + ext).find(f => fs.existsSync(f) && fs.statSync(f).isFile());
23
+ if (!found)
24
+ throw new Error(`"${label}" target "${target}" was not found, resolved from "${from}"`);
25
+ return found;
26
+ }
27
+ try {
28
+ return createRequire(pathToFileURL(path.join(dir, 'noop.js'))).resolve(target);
29
+ }
30
+ catch {
31
+ throw new Error(`"${label}" target "${target}" could not be resolved from "${from}" - is it installed in this repository?`);
32
+ }
33
+ }