rman 1.0.12 → 1.2.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.
- package/README.md +90 -70
- package/cli.js +226 -14
- package/commands/build.command.js +1 -0
- package/commands/changed.command.js +2 -2
- package/commands/changelog.command.js +13 -15
- package/commands/config.command.js +61 -0
- package/commands/diff.command.js +9 -4
- package/commands/exec.command.js +2 -8
- package/commands/github-release.command.js +1 -0
- package/commands/info.command.d.ts +9 -0
- package/commands/info.command.js +12 -2
- package/commands/run.command.js +5 -8
- package/commands/test.command.js +1 -0
- package/commands/version.command.js +53 -14
- package/constants.js +1 -1
- package/core/config.d.ts +265 -17
- package/core/config.js +651 -76
- package/core/custom-command.d.ts +133 -0
- package/core/custom-command.js +99 -0
- package/core/extends-config.d.ts +27 -0
- package/core/extends-config.js +89 -0
- package/core/manifest.d.ts +222 -0
- package/core/manifest.js +150 -0
- package/core/merge-config.d.ts +70 -0
- package/core/merge-config.js +193 -0
- package/core/package.d.ts +73 -7
- package/core/package.js +86 -24
- package/core/plugin.d.ts +112 -0
- package/core/plugin.js +189 -0
- package/core/repository.d.ts +91 -1
- package/core/repository.js +277 -132
- package/core/resolve-target.d.ts +12 -0
- package/core/resolve-target.js +33 -0
- package/core/run-step.d.ts +75 -0
- package/core/run-step.js +1 -0
- package/core/version-scheme.d.ts +134 -0
- package/core/version-scheme.js +148 -0
- package/core/workspace.d.ts +68 -0
- package/core/workspace.js +83 -0
- package/index.d.ts +55 -8
- package/index.js +42 -7
- package/interfaces/rman-config.interface.d.ts +222 -46
- package/package.json +16 -7
- package/services/change-hash.service.d.ts +88 -0
- package/services/change-hash.service.js +112 -0
- package/services/changelog.service.d.ts +8 -13
- package/services/changelog.service.js +12 -11
- package/services/conventional-commits.service.d.ts +73 -0
- package/services/conventional-commits.service.js +116 -0
- package/services/docker-publish.service.js +1 -1
- package/services/exec.service.js +1 -1
- package/services/github-release.service.d.ts +2 -2
- package/services/github-release.service.js +10 -5
- package/services/list.service.js +5 -2
- package/services/run.service.d.ts +112 -6
- package/services/run.service.js +265 -89
- package/services/system-info.d.ts +22 -7
- package/services/system-info.js +8 -23
- package/services/version-plan.service.d.ts +244 -0
- package/services/version-plan.service.js +414 -0
- package/services/version.service.d.ts +102 -82
- package/services/version.service.js +226 -434
- package/services.d.ts +5 -3
- package/services.js +5 -3
- package/utils/bin-path.d.ts +59 -0
- package/utils/bin-path.js +82 -0
- package/utils/child-tracker.d.ts +16 -0
- package/utils/child-tracker.js +30 -0
- package/utils/exec.d.ts +13 -2
- package/utils/exec.js +17 -17
- package/utils/git.d.ts +9 -3
- package/utils/git.js +10 -2
- package/utils/package-filter.d.ts +33 -2
- package/utils/package-filter.js +47 -7
- package/utils/printable-config.d.ts +15 -0
- package/utils/printable-config.js +42 -0
- package/utils/release-version.js +3 -3
- package/utils/run-bin.d.ts +46 -0
- package/utils/run-bin.js +63 -0
- package/utils/version-stamp.d.ts +14 -6
- package/utils/version-stamp.js +25 -13
- package/commands/ci.command.js +0 -30
- package/commands/clean.command.d.ts +0 -3
- package/commands/clean.command.js +0 -36
- package/commands/publish.command.d.ts +0 -3
- package/commands/publish.command.js +0 -225
- package/rmanrc.schema.json +0 -392
- package/services/ci.service.d.ts +0 -40
- package/services/ci.service.js +0 -204
- package/services/clean.service.d.ts +0 -42
- package/services/clean.service.js +0 -226
- package/services/publish.service.d.ts +0 -79
- package/services/publish.service.js +0 -273
- package/utils/change-hash.d.ts +0 -68
- package/utils/change-hash.js +0 -98
- package/utils/conventional-commits.d.ts +0 -52
- package/utils/conventional-commits.js +0 -90
- package/utils/npm-run-path.d.ts +0 -67
- package/utils/npm-run-path.js +0 -63
- package/utils/workspace-range.d.ts +0 -17
- package/utils/workspace-range.js +0 -28
- /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
package/core/repository.js
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import glob from 'fast-glob';
|
|
3
|
-
import fs from 'fs';
|
|
4
2
|
import path from 'path';
|
|
5
3
|
import semver from 'semver';
|
|
6
4
|
import { GitHelper } from '../utils/git.js';
|
|
7
|
-
import { interpolateConfig, resolveConfig } from './config.js';
|
|
5
|
+
import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
|
|
6
|
+
import { Manifest } from './manifest.js';
|
|
8
7
|
import { Package } from './package.js';
|
|
8
|
+
import { loadPlugins } from './plugin.js';
|
|
9
|
+
import { Workspace } from './workspace.js';
|
|
9
10
|
export class Repository extends Package {
|
|
10
11
|
dirname;
|
|
11
12
|
monorepo;
|
|
12
13
|
packages;
|
|
13
14
|
cwd;
|
|
14
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;
|
|
30
|
+
/** Cached `${{ git.* }}` facts - see `_gitScope`. Non-enumerable for the same reason as above,
|
|
31
|
+
* and because reading it is a subprocess: a deep walk of a package must not spawn one. */
|
|
32
|
+
_git;
|
|
33
|
+
/**
|
|
34
|
+
* Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
|
|
35
|
+
* of the bytes - see `readStructuredFile`.
|
|
36
|
+
*
|
|
37
|
+
* **On the repository rather than per scope, and that is the whole point of it**: `configScope`
|
|
38
|
+
* is built once per package, so a cache living there would re-read a repository-level file once
|
|
39
|
+
* for every package that mentions it.
|
|
40
|
+
*/
|
|
41
|
+
_readCache = new Map();
|
|
15
42
|
constructor(dirname, monorepo, packages,
|
|
16
43
|
/** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
|
|
17
44
|
* resolved repository root, possibly several levels up), this is where the user's shell
|
|
@@ -54,7 +81,7 @@ export class Repository extends Package {
|
|
|
54
81
|
result = result.filter(p => scopes.includes(p.name));
|
|
55
82
|
}
|
|
56
83
|
if (options?.toposort)
|
|
57
|
-
|
|
84
|
+
this._topoSortPackages(result);
|
|
58
85
|
return result;
|
|
59
86
|
}
|
|
60
87
|
getPackage(name) {
|
|
@@ -88,6 +115,53 @@ export class Repository extends Package {
|
|
|
88
115
|
}
|
|
89
116
|
return result;
|
|
90
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The scope a `${{ ... }}` expression is evaluated against for `pkg`, optionally with the version
|
|
120
|
+
* a run is about to write bound into it.
|
|
121
|
+
*
|
|
122
|
+
* `version` is the only caller that passes one, and it has to: the config was resolved before its
|
|
123
|
+
* plan existed, so `pkg.targetVersion` had nothing to be. Re-evaluating that raw value against
|
|
124
|
+
* this is how that one binding gets filled in, without every other command paying for it - or
|
|
125
|
+
* seeing a value that means nothing to them.
|
|
126
|
+
*/
|
|
127
|
+
configScope(pkg, options) {
|
|
128
|
+
const _this = this;
|
|
129
|
+
return {
|
|
130
|
+
pkg: this._packageScope(pkg, options?.targetVersion),
|
|
131
|
+
repository: this._repositoryScope(),
|
|
132
|
+
/** `pkg.dirname`, not the repository root: a `"[*]"` block asking whether
|
|
133
|
+
* `tsconfig-build.json` exists has to be answered per package. */
|
|
134
|
+
file: createFileScope(pkg.dirname),
|
|
135
|
+
/** Same base directory as `file`, so one `"[*]"` declaration reads each package's own copy -
|
|
136
|
+
* and the cache is the repository's, so a file they *share* is parsed once. */
|
|
137
|
+
read: createReadScope(pkg.dirname, this._readCache),
|
|
138
|
+
env: { ...process.env },
|
|
139
|
+
semver,
|
|
140
|
+
path,
|
|
141
|
+
/**
|
|
142
|
+
* A getter, and cached on the **repository** rather than in this closure: `configScope` is
|
|
143
|
+
* called once per package, so a per-scope cache would still shell out once per package in a
|
|
144
|
+
* monorepo that mentions git at all.
|
|
145
|
+
*
|
|
146
|
+
* Enumerable, unlike `pkg.targetVersion` - it has a real answer everywhere, so nothing needs
|
|
147
|
+
* hiding. What keeps it lazy is `interpolateConfig` building its context from property
|
|
148
|
+
* descriptors instead of spreading; see the note there.
|
|
149
|
+
*/
|
|
150
|
+
get git() {
|
|
151
|
+
return _this._gitScope();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** `${{ git.* }}`, read at most once per repository per process. */
|
|
156
|
+
_gitScope() {
|
|
157
|
+
if (this._git)
|
|
158
|
+
return this._git;
|
|
159
|
+
const built = this._readGitScope(this.dirname);
|
|
160
|
+
/** Defined rather than assigned, so the cache stays out of every enumeration of the repository
|
|
161
|
+
* - the same reason `_repoScope` is defined this way. */
|
|
162
|
+
Object.defineProperty(this, '_git', { value: built, enumerable: false, writable: true });
|
|
163
|
+
return built;
|
|
164
|
+
}
|
|
91
165
|
/**
|
|
92
166
|
* Resolves the effective rman config for the repository root and every package, cascading
|
|
93
167
|
* root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
|
|
@@ -98,77 +172,165 @@ export class Repository extends Package {
|
|
|
98
172
|
* single-package repository it *is* the one package, so `"[*]"` has to reach it; in a monorepo
|
|
99
173
|
* nothing under `getPackages()` is the root, so only its own unmarked config applies.
|
|
100
174
|
*/
|
|
175
|
+
/**
|
|
176
|
+
* Gives every package its `repository` and `parent`, before any config is resolved - a config
|
|
177
|
+
* expression or a provider may already want to navigate from a package outwards.
|
|
178
|
+
*
|
|
179
|
+
* A repository's own `repository` is itself, which reads oddly and is the honest answer:
|
|
180
|
+
* `Repository extends Package`, so the repository *is* a package of its own repository.
|
|
181
|
+
*/
|
|
182
|
+
_linkPackages() {
|
|
183
|
+
this.repository = this;
|
|
184
|
+
this.rootPackage.repository = this;
|
|
185
|
+
for (const pkg of this.packages) {
|
|
186
|
+
pkg.repository = this;
|
|
187
|
+
/** The deepest package that strictly contains it - the root for an ordinary member, an
|
|
188
|
+
* enclosing package for a nested one. Longest containing path wins, the same rule
|
|
189
|
+
* `currentPackage` uses to resolve "the package I am standing in". */
|
|
190
|
+
let parent = this.monorepo ? this.rootPackage : undefined;
|
|
191
|
+
for (const other of this.packages) {
|
|
192
|
+
if (other === pkg)
|
|
193
|
+
continue;
|
|
194
|
+
const rel = path.relative(other.dirname, pkg.dirname);
|
|
195
|
+
const contains = !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
196
|
+
if (contains && (!parent || other.dirname.length > parent.dirname.length))
|
|
197
|
+
parent = other;
|
|
198
|
+
}
|
|
199
|
+
pkg.parent = pkg === this.rootPackage ? undefined : parent;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
101
202
|
async _resolveConfigs() {
|
|
102
203
|
const cache = new Map();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
204
|
+
/**
|
|
205
|
+
* `DEFERRED_PATHS` are left *raw* in the result (see `interpolateConfig`'s `walk`), so the
|
|
206
|
+
* resolved config is the only copy anyone needs - `version` reads its own hooks straight off
|
|
207
|
+
* `config` and interpolates them itself once `pkg.targetVersion` exists. There used to be a
|
|
208
|
+
* second `rawConfig` copy of every package's config for that one reader; measured identical.
|
|
209
|
+
*/
|
|
210
|
+
/**
|
|
211
|
+
* The root is resolved **with its name**, like every other package, because selectors now speak
|
|
212
|
+
* to it: `"[/]"` names it and `"[*]"` includes it. It used to be resolved without one, which is
|
|
213
|
+
* what made `"[*]"` quietly mean "the workspace packages" - `"[ws:*]"` is that, spelled.
|
|
214
|
+
*/
|
|
215
|
+
const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.name);
|
|
216
|
+
this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
|
|
217
|
+
for (const pkg of this.packages) {
|
|
218
|
+
const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name);
|
|
219
|
+
pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
|
|
220
|
+
}
|
|
221
|
+
if (this.monorepo)
|
|
222
|
+
this.rootPackage.config = this.config;
|
|
223
|
+
}
|
|
224
|
+
_topoSortPackages(packages) {
|
|
225
|
+
packages.sort((a, b) => {
|
|
226
|
+
if (b.dependencies.includes(a))
|
|
227
|
+
return -1;
|
|
228
|
+
if (a.dependencies.includes(b))
|
|
229
|
+
return 1;
|
|
230
|
+
return 0;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
_packageScope(pkg, targetVersion) {
|
|
234
|
+
// A manifest without a "name" is unusual but legal, and `info` prints such a package rather
|
|
235
|
+
// than refusing it - so the scope has to survive one too.
|
|
236
|
+
const name = pkg.name ?? '';
|
|
237
|
+
/** Splitting `@scope/name` is npm's convention, not a universal - the provider decides. */
|
|
238
|
+
const { scope: nameScope, unscopedName } = Manifest.splitName(pkg.dirname, name);
|
|
239
|
+
const scope = {
|
|
240
|
+
name,
|
|
241
|
+
scope: nameScope,
|
|
242
|
+
unscopedName,
|
|
243
|
+
version: pkg.version ?? '',
|
|
244
|
+
basename: path.basename(pkg.dirname),
|
|
245
|
+
dirname: pkg.dirname,
|
|
246
|
+
relativeDir: path.relative(this.dirname, pkg.dirname),
|
|
247
|
+
/** The same `Package.provider`, so a `"[*]"` declaration can say something for one ecosystem
|
|
248
|
+
* only - `if: "${{ pkg.provider === 'node' }}"` in a polyglot repository. Omitting it would
|
|
249
|
+
* leave the expression scope and the `Package` disagreeing about what a package is. */
|
|
250
|
+
provider: pkg.provider,
|
|
251
|
+
// A copy: an expression has no business mutating the package rman is about to act on.
|
|
252
|
+
manifest: { ...pkg.manifest.raw },
|
|
120
253
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
254
|
+
if (targetVersion !== undefined) {
|
|
255
|
+
scope.targetVersion = targetVersion;
|
|
256
|
+
return scope;
|
|
257
|
+
}
|
|
258
|
+
/** Otherwise a getter that *throws*, rather than a missing key handing back `undefined` and
|
|
259
|
+
* letting a tag come out as "app:undefined". Defined rather than assigned because there is no
|
|
260
|
+
* value to assign - outside a `version` run there is no target version to name.
|
|
261
|
+
*
|
|
262
|
+
* Non-enumerable, and that is load-bearing: spreading an object runs its enumerable getters,
|
|
263
|
+
* so an enumerable one threw the moment `_repositoryScope` spread the root's scope - which is
|
|
264
|
+
* every `configScope()` call, for every command. Property access still triggers it, which is
|
|
265
|
+
* the only thing it exists for. */
|
|
266
|
+
Object.defineProperty(scope, 'targetVersion', {
|
|
267
|
+
enumerable: false,
|
|
268
|
+
get() {
|
|
269
|
+
throw new Error('pkg.targetVersion is only available while "version" is running - no other command has a target version');
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
return scope;
|
|
273
|
+
}
|
|
274
|
+
/** Built once and reused: it is the same for every package, and its `git` getter caches too, so a
|
|
275
|
+
* repository whose config never mentions git spawns none. */
|
|
276
|
+
_repositoryScope() {
|
|
277
|
+
if (this._repoScope)
|
|
278
|
+
return this._repoScope;
|
|
279
|
+
const packageScopes = this.packages.map(p => this._packageScope(p));
|
|
280
|
+
const built = {
|
|
281
|
+
...this._packageScope(this.rootPackage),
|
|
125
282
|
monorepo: this.monorepo,
|
|
126
283
|
packages: packageScopes,
|
|
127
284
|
package: (name) => packageScopes.find(p => p.name === name),
|
|
128
|
-
// A getter, so a repository whose config never mentions git spawns no git at all - and every
|
|
129
|
-
// command resolves config, not just the ones that care.
|
|
130
|
-
get git() {
|
|
131
|
-
return (gitScope ??= readGitScope(repoDir));
|
|
132
|
-
},
|
|
133
285
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
286
|
+
/** Defined rather than assigned, so the cache stays out of every enumeration of this object -
|
|
287
|
+
* see the field's own doc for what walked into it. */
|
|
288
|
+
Object.defineProperty(this, '_repoScope', { value: built, enumerable: false, writable: true });
|
|
289
|
+
return built;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* One `.rmanrc "dependencies"` entry to a package: its **name** first, then a
|
|
293
|
+
* **repository-relative directory**.
|
|
294
|
+
*
|
|
295
|
+
* The path form is what makes the key usable outside npm. A name identifies a package only where
|
|
296
|
+
* the ecosystem guarantees uniqueness - the same reason `Package.dependencies` holds references
|
|
297
|
+
* and `Workspace.Layout` carries paths - so a repository whose names collide, or whose packages
|
|
298
|
+
* have no names rman can read, states the edge by directory instead.
|
|
299
|
+
*
|
|
300
|
+
* Name first because that is what a Node repository writes and there is no ambiguity in practice:
|
|
301
|
+
* a package name that is also an existing directory path in the same repository does not occur.
|
|
302
|
+
* An entry matching neither is ignored, as an unknown name always was - the graph is a statement
|
|
303
|
+
* about packages that exist.
|
|
304
|
+
*/
|
|
305
|
+
_resolveDeclaredPackage(entry) {
|
|
306
|
+
const byName = this.getPackage(entry);
|
|
307
|
+
if (byName)
|
|
308
|
+
return byName;
|
|
309
|
+
const dir = path.resolve(this.dirname, entry);
|
|
310
|
+
return this.packages.find(p => path.resolve(p.dirname) === dir);
|
|
141
311
|
}
|
|
142
312
|
_updateDependencies() {
|
|
143
313
|
const deps = {};
|
|
144
314
|
for (const pkg of this.packages) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
else
|
|
156
|
-
Object.assign(o, configDeps);
|
|
315
|
+
/** Which manifest fields hold dependencies is the ecosystem's business, so the provider
|
|
316
|
+
* reads them - npm's four field names used to be spelled out right here. */
|
|
317
|
+
const dependencies = [...Manifest.dependenciesOf(pkg, this.packages)];
|
|
318
|
+
/** `.rmanrc "dependencies"` on top, and it works with no provider at all: a repository rman
|
|
319
|
+
* cannot read the manifests of can still declare its graph by hand. */
|
|
320
|
+
const declared = pkg.config.dependencies ?? [];
|
|
321
|
+
for (const entry of declared) {
|
|
322
|
+
const p = this._resolveDeclaredPackage(entry);
|
|
323
|
+
if (p && p !== pkg && !dependencies.includes(p))
|
|
324
|
+
dependencies.push(p);
|
|
157
325
|
}
|
|
158
|
-
|
|
159
|
-
for (const k of Object.keys(o)) {
|
|
160
|
-
const p = this.getPackage(k);
|
|
161
|
-
if (p)
|
|
162
|
-
dependencies.push(k);
|
|
163
|
-
}
|
|
164
|
-
deps[pkg.name] = dependencies;
|
|
326
|
+
deps[pkg.name] = dependencies.map(d => d.name);
|
|
165
327
|
pkg.dependencies = dependencies;
|
|
166
328
|
}
|
|
167
329
|
let circularCheck;
|
|
168
330
|
const deepFindDependencies = (pkg, target) => {
|
|
169
|
-
if (circularCheck.includes(pkg
|
|
331
|
+
if (circularCheck.includes(pkg))
|
|
170
332
|
return;
|
|
171
|
-
circularCheck.push(pkg
|
|
333
|
+
circularCheck.push(pkg);
|
|
172
334
|
for (const s of pkg.dependencies) {
|
|
173
335
|
/** `target` starts out *as* the top-level package's own `dependencies` array, so its
|
|
174
336
|
* direct entries are trivially "already in target" - recursing only when newly-added
|
|
@@ -177,9 +339,7 @@ export class Repository extends Package {
|
|
|
177
339
|
* and never let the top-level package end up depending on itself via a cycle. */
|
|
178
340
|
if (s !== circularCheck[0] && !target.includes(s))
|
|
179
341
|
target.push(s);
|
|
180
|
-
|
|
181
|
-
if (p)
|
|
182
|
-
deepFindDependencies(p, target);
|
|
342
|
+
deepFindDependencies(s, target);
|
|
183
343
|
}
|
|
184
344
|
};
|
|
185
345
|
for (const pkg of this.packages) {
|
|
@@ -187,25 +347,63 @@ export class Repository extends Package {
|
|
|
187
347
|
deepFindDependencies(pkg, pkg.dependencies);
|
|
188
348
|
}
|
|
189
349
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const packages = this._resolvePackages(pkgDirname, pkgJson.workspaces);
|
|
200
|
-
return Repository._init(new Repository(pkgDirname, true, packages, dirname));
|
|
201
|
-
}
|
|
202
|
-
/** If we reach to the root of the project */
|
|
203
|
-
if (fs.existsSync(path.join(pkgDirname, '.git')))
|
|
204
|
-
break;
|
|
350
|
+
/** `git` facts for a `${{ git.* }}` expression. Synchronous on purpose: it backs a lazy
|
|
351
|
+
* getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
|
|
352
|
+
* error, just a repository without one. */
|
|
353
|
+
_readGitScope(dirname) {
|
|
354
|
+
const run = (args) => {
|
|
355
|
+
try {
|
|
356
|
+
return execFileSync('git', args, { cwd: dirname, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
357
|
+
.toString()
|
|
358
|
+
.trim();
|
|
205
359
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
360
|
+
catch {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const sha = run(['rev-parse', 'HEAD']);
|
|
365
|
+
if (sha === undefined)
|
|
366
|
+
return { branch: undefined, sha: undefined, shortSha: undefined, dirty: undefined };
|
|
367
|
+
const status = run(['status', '--porcelain']);
|
|
368
|
+
return {
|
|
369
|
+
// Empty on a detached HEAD, which is what a CI checkout often is - reported as undefined
|
|
370
|
+
// rather than an empty string, so `?? 'detached'` in an expression works.
|
|
371
|
+
branch: run(['branch', '--show-current']) || undefined,
|
|
372
|
+
sha,
|
|
373
|
+
shortSha: sha.slice(0, 7),
|
|
374
|
+
dirty: status === undefined ? undefined : status.length > 0,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Opens the repository containing `root` (default: the current directory).
|
|
379
|
+
*
|
|
380
|
+
* Three steps, in this order because each needs the one before it:
|
|
381
|
+
*
|
|
382
|
+
* 1. **Find the root** without knowing any ecosystem - see `Workspace.findRoot`. It cannot be
|
|
383
|
+
* otherwise: the plugins that know what a package is are named in the config file this step
|
|
384
|
+
* is looking for.
|
|
385
|
+
* 2. **Load the plugins** the root's config names, which registers their workspace providers
|
|
386
|
+
* (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
|
|
387
|
+
* 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
|
|
388
|
+
* the one package.
|
|
389
|
+
*
|
|
390
|
+
* **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
|
|
391
|
+
* boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
|
|
392
|
+
* takes `plugins: ['rman-node']` to be read as one.
|
|
393
|
+
*/
|
|
394
|
+
static async create(root, options) {
|
|
395
|
+
const from = root || process.cwd();
|
|
396
|
+
const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
|
|
397
|
+
/** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
|
|
398
|
+
* package list (which does not exist yet) nor expression interpolation. */
|
|
399
|
+
const rootConfig = await readDirConfig(rootDir);
|
|
400
|
+
const pluginCommands = await loadPlugins(rootDir, rootConfig);
|
|
401
|
+
const layout = Workspace.resolve(rootDir);
|
|
402
|
+
const packages = (layout?.packageDirs ?? []).map(dir => new Package(dir));
|
|
403
|
+
const repo = new Repository(layout?.root ?? rootDir, packages.length > 0, packages, from);
|
|
404
|
+
repo.pluginCommands = pluginCommands;
|
|
405
|
+
repo._linkPackages();
|
|
406
|
+
return Repository._init(repo);
|
|
209
407
|
}
|
|
210
408
|
/** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
|
|
211
409
|
* `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
|
|
@@ -215,57 +413,4 @@ export class Repository extends Package {
|
|
|
215
413
|
repo._updateDependencies();
|
|
216
414
|
return repo;
|
|
217
415
|
}
|
|
218
|
-
static _resolvePackages(dirname, patterns) {
|
|
219
|
-
const packages = [];
|
|
220
|
-
for (const pattern of patterns) {
|
|
221
|
-
const dirs = glob.sync(pattern, {
|
|
222
|
-
cwd: dirname,
|
|
223
|
-
absolute: true,
|
|
224
|
-
deep: 0,
|
|
225
|
-
onlyDirectories: true,
|
|
226
|
-
});
|
|
227
|
-
for (const dir of dirs) {
|
|
228
|
-
const f = path.join(dir, 'package.json');
|
|
229
|
-
if (fs.existsSync(f))
|
|
230
|
-
packages.push(new Package(dir));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return packages;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function topoSortPackages(packages) {
|
|
237
|
-
packages.sort((a, b) => {
|
|
238
|
-
if (b.dependencies.includes(a.name))
|
|
239
|
-
return -1;
|
|
240
|
-
if (a.dependencies.includes(b.name))
|
|
241
|
-
return 1;
|
|
242
|
-
return 0;
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
/** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
|
|
246
|
-
* getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
|
|
247
|
-
* error, just a repository without one. */
|
|
248
|
-
function readGitScope(dirname) {
|
|
249
|
-
const run = (args) => {
|
|
250
|
-
try {
|
|
251
|
-
return execFileSync('git', args, { cwd: dirname, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
252
|
-
.toString()
|
|
253
|
-
.trim();
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
const sha = run(['rev-parse', 'HEAD']);
|
|
260
|
-
if (sha === undefined)
|
|
261
|
-
return { branch: undefined, sha: undefined, shortSha: undefined, dirty: undefined };
|
|
262
|
-
const status = run(['status', '--porcelain']);
|
|
263
|
-
return {
|
|
264
|
-
// Empty on a detached HEAD, which is what a CI checkout often is - reported as undefined
|
|
265
|
-
// rather than an empty string, so `?? 'detached'` in an expression works.
|
|
266
|
-
branch: run(['branch', '--show-current']) || undefined,
|
|
267
|
-
sha,
|
|
268
|
-
shortSha: sha.slice(0, 7),
|
|
269
|
-
dirty: status === undefined ? undefined : status.length > 0,
|
|
270
|
-
};
|
|
271
416
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Logger } from '../utils/logger.js';
|
|
2
|
+
import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
|
|
3
|
+
import type { Package } from './package.js';
|
|
4
|
+
import type { Repository } from './repository.js';
|
|
5
|
+
/**
|
|
6
|
+
* What a **function step** is handed - a `run.<script>.before`/`.exec`/`.after` or a
|
|
7
|
+
* `version.<slot>` written as JavaScript instead of a shell command, and the `if` deciding whether
|
|
8
|
+
* a script runs at all.
|
|
9
|
+
*
|
|
10
|
+
* An object rather than loose parameters, for the reason `CommandContext` is one: a member added
|
|
11
|
+
* later must not break every step already written against it.
|
|
12
|
+
*
|
|
13
|
+
* **The whole point of the function form is *when* it runs.** A `${{ }}` expression is evaluated
|
|
14
|
+
* while the repository's config resolves - which every command does, `rman list` included - so it
|
|
15
|
+
* can only ever answer questions about the state the config was loaded in, and anything it *did*
|
|
16
|
+
* would happen on every invocation. A function step runs when its turn comes, with the real
|
|
17
|
+
* objects, in the package's own directory. Reach for it exactly when that difference matters;
|
|
18
|
+
* a shell command is still the right shape for a shell command.
|
|
19
|
+
*/
|
|
20
|
+
export interface RunStepContext {
|
|
21
|
+
/**
|
|
22
|
+
* The package this step is running for - `pkg`, matching `${{ pkg }}` in an expression rather
|
|
23
|
+
* than `CommandContext.package`. `package` is a reserved word, so that spelling forces every
|
|
24
|
+
* author to rename it while destructuring (`{ package: current }`), which is friction paid at
|
|
25
|
+
* every call site for no benefit.
|
|
26
|
+
*
|
|
27
|
+
* Always set, unlike `CommandContext.package`: a step belongs to a package by construction, and
|
|
28
|
+
* the repo-wide bookend belongs to the root package.
|
|
29
|
+
*/
|
|
30
|
+
pkg: Package;
|
|
31
|
+
repository: Repository;
|
|
32
|
+
/**
|
|
33
|
+
* The directory this step is *about* - the package's own, or the repository root for a monorepo's
|
|
34
|
+
* bookend. The same directory a shell step in this slot is spawned in.
|
|
35
|
+
*
|
|
36
|
+
* **`process.cwd()` is NOT changed, and cannot be.** A shell step gets a real working directory
|
|
37
|
+
* because it is a child process; a function step runs inside rman's own, and `run` executes
|
|
38
|
+
* packages **concurrently** - one step calling `process.chdir()` would move the ground under
|
|
39
|
+
* every other step running at that moment. So a relative path resolves against wherever rman was
|
|
40
|
+
* invoked, which is almost never what the step meant:
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* fs.writeFileSync('out.txt', data) // the repository root. Measured, and wrong.
|
|
44
|
+
* fs.writeFileSync(path.join(ctx.cwd, 'out.txt'), data) // the package
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* `ctx.runBin` is already bound to this directory, so a binary run through it needs no such care.
|
|
48
|
+
*/
|
|
49
|
+
cwd: string;
|
|
50
|
+
/**
|
|
51
|
+
* The repository's locally installed binaries, already carrying this run's `cwd` and log level -
|
|
52
|
+
* handed over rather than imported, for the reason `CommandContext.runBin` is.
|
|
53
|
+
*/
|
|
54
|
+
runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
|
|
55
|
+
/** Logger at this run's resolved level. **Prefer it to `console`**: with the live progress panel
|
|
56
|
+
* on, a direct write lands beside the panel rather than in the step's own log. */
|
|
57
|
+
logger: Logger;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A step written as JavaScript. **Failure is a throw** - the return value means nothing, exactly as
|
|
61
|
+
* a non-zero exit is what fails a shell step and what `runBin` rejects on. A step that can report
|
|
62
|
+
* trouble only by returning something nobody reads is a step that passes while doing nothing.
|
|
63
|
+
*/
|
|
64
|
+
export type RunStepFn = (context: RunStepContext) => void | Promise<void>;
|
|
65
|
+
/** One entry of a `before`/`exec`/`after` slot: a shell command, or a function. A list of them runs
|
|
66
|
+
* in sequence, and the two forms mix freely within one list. */
|
|
67
|
+
export type RunStepValue = string | RunStepFn;
|
|
68
|
+
/**
|
|
69
|
+
* A `run.<script>.if` written as JavaScript, deciding whether the script runs for this package.
|
|
70
|
+
*
|
|
71
|
+
* The string form is a small closed grammar (`changed and not private`) which cannot express an
|
|
72
|
+
* arbitrary condition, and a `${{ }}` one is frozen at config-load time. This is evaluated per
|
|
73
|
+
* package, when the run reaches it - the same context a step gets.
|
|
74
|
+
*/
|
|
75
|
+
export type RunConditionFn = (context: RunStepContext) => boolean | Promise<boolean>;
|
package/core/run-step.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|