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.
- package/README.md +116 -85
- package/cli.js +225 -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 +60 -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 +257 -6
- package/core/config.js +409 -17
- 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 +57 -0
- package/core/merge-config.js +146 -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 +85 -4
- package/core/repository.js +258 -81
- package/core/resolve-target.d.ts +12 -0
- package/core/resolve-target.js +33 -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 +54 -8
- package/index.js +42 -7
- package/interfaces/rman-config.interface.d.ts +226 -28
- package/package.json +15 -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 +68 -3
- package/services/run.service.js +162 -70
- 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 +92 -82
- package/services/version.service.js +233 -383
- 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/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 +48 -0
- package/utils/version-stamp.js +88 -0
- 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 -375
- 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 -207
- 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.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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';
|
package/core/repository.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
299
|
+
if (circularCheck.includes(pkg))
|
|
133
300
|
return;
|
|
134
|
-
circularCheck.push(pkg
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
}
|