rman 0.37.4 → 1.0.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 +364 -21
- package/cli.d.ts +1 -0
- package/cli.js +52 -28
- package/commands/build.command.d.ts +3 -0
- package/commands/build.command.js +14 -0
- package/commands/changed.command.d.ts +3 -0
- package/commands/changed.command.js +29 -0
- package/commands/changelog.command.d.ts +3 -0
- package/commands/changelog.command.js +63 -0
- package/commands/ci.command.d.ts +3 -0
- package/commands/ci.command.js +30 -0
- package/commands/clean.command.d.ts +3 -0
- package/commands/clean.command.js +36 -0
- package/commands/diff.command.d.ts +3 -0
- package/commands/diff.command.js +51 -0
- package/commands/exec.command.d.ts +3 -0
- package/commands/exec.command.js +83 -0
- package/commands/import.command.d.ts +3 -0
- package/commands/import.command.js +27 -0
- package/commands/info.command.d.ts +3 -0
- package/commands/info.command.js +61 -0
- package/commands/list.command.d.ts +3 -0
- package/commands/list.command.js +113 -0
- package/commands/publish.command.d.ts +3 -0
- package/commands/publish.command.js +147 -0
- package/commands/run.command.d.ts +6 -0
- package/commands/run.command.js +80 -0
- package/commands/test.command.d.ts +3 -0
- package/commands/test.command.js +14 -0
- package/commands/version.command.d.ts +3 -0
- package/commands/version.command.js +125 -0
- package/constants.js +1 -1
- package/core/config.d.ts +13 -0
- package/core/config.js +61 -0
- package/core/package.d.ts +2 -0
- package/core/package.js +2 -0
- package/core/repository.d.ts +36 -3
- package/core/repository.js +90 -45
- package/index.d.ts +17 -1
- package/index.js +15 -1
- package/package.json +11 -8
- package/services/changelog.service.d.ts +99 -0
- package/services/changelog.service.js +272 -0
- package/services/ci.service.d.ts +40 -0
- package/services/ci.service.js +204 -0
- package/services/clean.service.d.ts +42 -0
- package/services/clean.service.js +226 -0
- package/services/exec.service.d.ts +35 -0
- package/services/exec.service.js +114 -0
- package/services/import.service.d.ts +30 -0
- package/services/import.service.js +84 -0
- package/services/list.service.d.ts +27 -0
- package/services/list.service.js +25 -0
- package/services/publish.service.d.ts +79 -0
- package/services/publish.service.js +199 -0
- package/services/run.service.d.ts +99 -0
- package/services/run.service.js +432 -0
- package/services/system-info.d.ts +16 -0
- package/services/system-info.js +25 -0
- package/services/version.service.d.ts +95 -0
- package/services/version.service.js +450 -0
- package/services.d.ts +10 -0
- package/services.js +10 -0
- package/utils/branch-guard.d.ts +25 -0
- package/utils/branch-guard.js +61 -0
- package/utils/change-hash.d.ts +55 -0
- package/utils/change-hash.js +80 -0
- package/utils/conventional-commits.d.ts +39 -0
- package/utils/conventional-commits.js +43 -0
- package/utils/exec.d.ts +5 -6
- package/utils/exec.js +46 -60
- package/utils/git.d.ts +86 -0
- package/utils/git.js +276 -0
- package/utils/live-region.d.ts +16 -0
- package/utils/live-region.js +59 -0
- package/utils/logger.d.ts +24 -0
- package/utils/logger.js +39 -0
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/npm-run-path.js +10 -10
- package/utils/package-filter.d.ts +32 -0
- package/utils/package-filter.js +73 -0
- package/utils/progress-panel.d.ts +59 -0
- package/utils/progress-panel.js +149 -0
- package/utils/workspace-range.d.ts +17 -0
- package/utils/workspace-range.js +28 -0
- package/bin/rman.mjs +0 -4
- package/commands/build-command.d.ts +0 -11
- package/commands/build-command.js +0 -24
- package/commands/changed-command.d.ts +0 -16
- package/commands/changed-command.js +0 -32
- package/commands/ci-command.d.ts +0 -24
- package/commands/ci-command.js +0 -68
- package/commands/execute-command.d.ts +0 -19
- package/commands/execute-command.js +0 -72
- package/commands/info-command.d.ts +0 -10
- package/commands/info-command.js +0 -55
- package/commands/list-command.d.ts +0 -38
- package/commands/list-command.js +0 -134
- package/commands/multi-task-command.d.ts +0 -22
- package/commands/multi-task-command.js +0 -65
- package/commands/publish-command.d.ts +0 -24
- package/commands/publish-command.js +0 -131
- package/commands/run-command.d.ts +0 -28
- package/commands/run-command.js +0 -131
- package/commands/version-command.d.ts +0 -25
- package/commands/version-command.js +0 -195
- package/core/command.d.ts +0 -35
- package/core/command.js +0 -116
- package/core/constants.d.ts +0 -1
- package/core/constants.js +0 -2
- package/core/logger.d.ts +0 -12
- package/core/logger.js +0 -2
- package/utils/file-utils.d.ts +0 -4
- package/utils/file-utils.js +0 -28
- package/utils/git-utils.d.ts +0 -25
- package/utils/git-utils.js +0 -66
- package/utils/npm-utils.d.ts +0 -11
- package/utils/npm-utils.js +0 -33
- package/utils/package-not-found-error.d.ts +0 -2
- package/utils/package-not-found-error.js +0 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { findLatestTag, tagPattern } from '../utils/change-hash.js';
|
|
4
|
+
import { hasBreakingChangeFooter, parseConventionalCommit, parseReleaseAs, VERSION_BUMP_PATTERN, } from '../utils/conventional-commits.js';
|
|
5
|
+
import { exec } from '../utils/exec.js';
|
|
6
|
+
import { GitHelper } from '../utils/git.js';
|
|
7
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
8
|
+
import { parseWorkspaceRange } from '../utils/workspace-range.js';
|
|
9
|
+
import { ChangelogService } from './changelog.service.js';
|
|
10
|
+
export var VersionService;
|
|
11
|
+
(function (VersionService) {
|
|
12
|
+
function isBumpKeyword(value) {
|
|
13
|
+
return value === 'patch' || value === 'minor' || value === 'major';
|
|
14
|
+
}
|
|
15
|
+
VersionService.isBumpKeyword = isBumpKeyword;
|
|
16
|
+
/**
|
|
17
|
+
* Computes what a version bump *would* do, across every package `.rmanrc group` puts together -
|
|
18
|
+
* never writes anything (no package.json edits, no git commits/tags) and safe to call any time,
|
|
19
|
+
* including as the "preview" a bare `rman version` (no bump given) stops at.
|
|
20
|
+
*
|
|
21
|
+
* Packages are partitioned into groups by their resolved `group` value (cascaded): `true`
|
|
22
|
+
* (the default) puts every such package into one implicit repo-wide group; a string joins
|
|
23
|
+
* exactly the other packages sharing that same string, regardless of the repo's default; `false`
|
|
24
|
+
* makes a package its own solo group. Each group's "current version" is always the highest
|
|
25
|
+
* version currently found among its own members (never persisted anywhere) - see
|
|
26
|
+
* `resolveGroupKey`.
|
|
27
|
+
*
|
|
28
|
+
* Within a group, a member with real commits since its own last release tag (or an explicit
|
|
29
|
+
* `bump`) sets the group's severity to the highest found among changed members; the new version
|
|
30
|
+
* is that current version bumped by that severity. Which members actually receive it depends on
|
|
31
|
+
* the severity: **patch** only the changed member(s) (a caret dependency range already tolerates
|
|
32
|
+
* a patch bump, no republish needed downstream); **minor** also every transitive in-group
|
|
33
|
+
* dependent; **major** the entire group, changed or not - see `computeGroupPlan`.
|
|
34
|
+
*
|
|
35
|
+
* Across groups: a package depending on another group's bumped package always gets exactly a
|
|
36
|
+
* **patch** bump of its own (never inheriting the source's severity) - the dependency reference
|
|
37
|
+
* itself is the only thing that changed for it. This never re-triggers *its own* group's
|
|
38
|
+
* minor/major cascade (a patch never cascades), but can itself ripple into a third group, and so
|
|
39
|
+
* on, until nothing new is affected - see `rippleCrossGroup`.
|
|
40
|
+
*
|
|
41
|
+
* A monorepo's root package is never a real member of any group (it's never published on its
|
|
42
|
+
* own) - it gets one trailing informational entry instead, always `'bump'`ed to whatever single
|
|
43
|
+
* version every group ended up sharing, or the overall highest version when groups diverged.
|
|
44
|
+
*/
|
|
45
|
+
async function getPlan(repository, options = {}) {
|
|
46
|
+
const bump = options.bump?.trim();
|
|
47
|
+
const explicitSeverity = bump && isBumpKeyword(bump) ? bump : undefined;
|
|
48
|
+
const explicitVersion = bump && !explicitSeverity ? (semver.valid(bump) ?? undefined) : undefined;
|
|
49
|
+
if (bump && !explicitSeverity && !explicitVersion) {
|
|
50
|
+
throw new Error(`Invalid "bump": "${bump}" (expected "patch", "minor", "major", or a valid semver version)`);
|
|
51
|
+
}
|
|
52
|
+
const git = new GitHelper({ cwd: repository.dirname });
|
|
53
|
+
const packages = filterPackages(repository.getPackages(), options);
|
|
54
|
+
const dirtyFiles = await git.listDirtyFiles({ absolute: true });
|
|
55
|
+
const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
56
|
+
const entries = new Map();
|
|
57
|
+
const eligible = [];
|
|
58
|
+
for (const pkg of packages) {
|
|
59
|
+
if (isDirty(pkg)) {
|
|
60
|
+
entries.set(pkg.name, {
|
|
61
|
+
package: pkg,
|
|
62
|
+
groupKey: resolveGroupKey(pkg),
|
|
63
|
+
group: groupLabel(resolveGroupKey(pkg)),
|
|
64
|
+
status: options.ignoreDirty ? 'skip' : 'error',
|
|
65
|
+
from: pkg.version,
|
|
66
|
+
reason: 'uncommitted local changes',
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
eligible.push(pkg);
|
|
71
|
+
}
|
|
72
|
+
const changeByPackage = new Map();
|
|
73
|
+
await Promise.all(eligible.map(async (pkg) => {
|
|
74
|
+
if (explicitVersion) {
|
|
75
|
+
changeByPackage.set(pkg.name, { severity: undefined, reason: `explicit version ${explicitVersion}` });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const tag = await findLatestTag(git, pkg);
|
|
79
|
+
const commits = tag ? await git.listCommits({ hash: tag }) : await git.listAllCommits();
|
|
80
|
+
const belongsToPkg = (c) => c.files.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
81
|
+
const real = commits.filter(c => belongsToPkg(c) && !VERSION_BUMP_PATTERN.test(c.subject));
|
|
82
|
+
if (!real.length)
|
|
83
|
+
return;
|
|
84
|
+
changeByPackage.set(pkg.name, {
|
|
85
|
+
severity: explicitSeverity ?? detectSeverity(real),
|
|
86
|
+
reason: tag ? `changed since ${tag}` : 'unreleased commits',
|
|
87
|
+
});
|
|
88
|
+
}));
|
|
89
|
+
const groups = new Map();
|
|
90
|
+
for (const pkg of eligible) {
|
|
91
|
+
const key = resolveGroupKey(pkg);
|
|
92
|
+
const list = groups.get(key);
|
|
93
|
+
if (list)
|
|
94
|
+
list.push(pkg);
|
|
95
|
+
else
|
|
96
|
+
groups.set(key, [pkg]);
|
|
97
|
+
}
|
|
98
|
+
for (const [key, members] of groups) {
|
|
99
|
+
computeGroupPlan(key, members, changeByPackage, explicitVersion, options.preid, entries);
|
|
100
|
+
}
|
|
101
|
+
rippleCrossGroup(packages, entries, options.preid);
|
|
102
|
+
const result = packages.map(pkg => entries.get(pkg.name));
|
|
103
|
+
if (repository.monorepo)
|
|
104
|
+
result.push(buildRootEntry(repository, result));
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
VersionService.getPlan = getPlan;
|
|
108
|
+
/**
|
|
109
|
+
* Same as `getPlan`, and additionally writes every `'bump'` entry's new version into its own
|
|
110
|
+
* `package.json` (and refreshes any other bumped package's dependency range on it), runs that
|
|
111
|
+
* package's `version.preScript`/`.script`/`.postScript` (or its own real `preversion`/`version`/
|
|
112
|
+
* `postversion` npm scripts) around the write, then commits and tags **once per group** - so
|
|
113
|
+
* independently-versioned groups each get their own clean commit/tag rather than one entangled
|
|
114
|
+
* commit spanning unrelated version lines. Pushes only when `options.push` is set - same as a
|
|
115
|
+
* plain `npm version`, this never reaches the network on its own otherwise.
|
|
116
|
+
*/
|
|
117
|
+
async function applyPlan(repository, plan, options = {}) {
|
|
118
|
+
const git = new GitHelper({ cwd: repository.dirname });
|
|
119
|
+
// The root's own entry is only ever a real package to write/commit like any other when this
|
|
120
|
+
// *isn't* a monorepo (see `getPlan`) - in a monorepo it's the separate, purely informational
|
|
121
|
+
// entry handled below instead, since it's never published on its own.
|
|
122
|
+
const isRealEntry = (e) => !(repository.monorepo && e.package === repository.rootPackage);
|
|
123
|
+
const bumped = plan.filter(e => e.status === 'bump' && isRealEntry(e));
|
|
124
|
+
const bumpedByName = new Map(bumped.map(e => [e.package.name, e]));
|
|
125
|
+
for (const entry of bumped) {
|
|
126
|
+
const pkg = entry.package;
|
|
127
|
+
await runVersionScript(pkg, 'preScript', 'preversion');
|
|
128
|
+
pkg.json.version = entry.to;
|
|
129
|
+
for (const depKey of DEPENDENCY_KEYS) {
|
|
130
|
+
const deps = pkg.json[depKey];
|
|
131
|
+
if (!deps)
|
|
132
|
+
continue;
|
|
133
|
+
for (const depName of Object.keys(deps)) {
|
|
134
|
+
const depEntry = bumpedByName.get(depName);
|
|
135
|
+
if (!depEntry)
|
|
136
|
+
continue;
|
|
137
|
+
const workspace = parseWorkspaceRange(deps[depName]);
|
|
138
|
+
if (workspace) {
|
|
139
|
+
// A bare "workspace:*"/"^"/"~" selector always resolves to the dependency's *current*
|
|
140
|
+
// version at publish time (see `resolveWorkspaceRange`) - nothing to rewrite here. Only
|
|
141
|
+
// an explicit version/range after "workspace:" needs bumping, same as a plain range.
|
|
142
|
+
if (workspace.selector === 'explicit')
|
|
143
|
+
deps[depName] = `workspace:^${depEntry.to}`;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
deps[depName] = '^' + depEntry.to;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await runVersionScript(pkg, 'script', 'version');
|
|
150
|
+
pkg.writeJson();
|
|
151
|
+
await runVersionScript(pkg, 'postScript', 'postversion');
|
|
152
|
+
}
|
|
153
|
+
const rootEntry = repository.monorepo ? plan.find(e => e.package === repository.rootPackage) : undefined;
|
|
154
|
+
if (rootEntry?.status === 'bump') {
|
|
155
|
+
repository.rootPackage.json.version = rootEntry.to;
|
|
156
|
+
repository.rootPackage.writeJson();
|
|
157
|
+
}
|
|
158
|
+
/** Written before the per-group commits below so each group's changelog file lands in the
|
|
159
|
+
* *same* commit as its version bump, rather than needing a separate `changelog --write` run.
|
|
160
|
+
* Bounded by each package's own *pre-bump* tag (the same one `getPlan` measured "changed
|
|
161
|
+
* since" from - see `expandTag`) rather than `changelog`'s own default auto-detection (an npm
|
|
162
|
+
* registry lookup, falling back to not-yet-pushed commits) - that boundary can drift from the
|
|
163
|
+
* one `version` itself just used, and the not-yet-pushed fallback needs a configured remote
|
|
164
|
+
* `version` never required at all. Falls back to `changelog`'s own default only when this
|
|
165
|
+
* package genuinely has no prior tag (a first-ever release). */
|
|
166
|
+
const changelogFileByPackage = new Map();
|
|
167
|
+
if (options.changelog) {
|
|
168
|
+
for (const entry of bumped) {
|
|
169
|
+
const fromTag = expandTag(entry.package, entry.from);
|
|
170
|
+
const from = (await git.tagExists(fromTag)) ? fromTag : undefined;
|
|
171
|
+
const changelogEntries = await ChangelogService.generateToFile(repository, {
|
|
172
|
+
scope: entry.package.name,
|
|
173
|
+
root: true,
|
|
174
|
+
from,
|
|
175
|
+
});
|
|
176
|
+
for (const ce of changelogEntries) {
|
|
177
|
+
changelogFileByPackage.set(ce.package.name, path.relative(repository.dirname, path.join(ce.package.dirname, ce.filePath)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const byGroup = new Map();
|
|
182
|
+
for (const entry of bumped) {
|
|
183
|
+
const list = byGroup.get(entry.groupKey);
|
|
184
|
+
if (list)
|
|
185
|
+
list.push(entry);
|
|
186
|
+
else
|
|
187
|
+
byGroup.set(entry.groupKey, [entry]);
|
|
188
|
+
}
|
|
189
|
+
for (const [, groupEntries] of byGroup) {
|
|
190
|
+
const files = groupEntries.map(e => path.relative(repository.dirname, e.package.jsonFileName));
|
|
191
|
+
for (const e of groupEntries) {
|
|
192
|
+
const changelogFile = changelogFileByPackage.get(e.package.name);
|
|
193
|
+
if (changelogFile)
|
|
194
|
+
files.push(changelogFile);
|
|
195
|
+
}
|
|
196
|
+
await git.commit(files, buildCommitMessage(repository, groupEntries, options.message));
|
|
197
|
+
const tags = new Set(groupEntries.map(e => expandTag(e.package, e.to)));
|
|
198
|
+
for (const tag of tags)
|
|
199
|
+
if (!(await git.tagExists(tag)))
|
|
200
|
+
await git.createTag(tag);
|
|
201
|
+
}
|
|
202
|
+
/** The root's own informational version write isn't part of any group's release, but still
|
|
203
|
+
* needs to land in *some* commit rather than being left as an uncommitted local edit. */
|
|
204
|
+
if (rootEntry?.status === 'bump') {
|
|
205
|
+
await git.commit([path.relative(repository.dirname, repository.rootPackage.jsonFileName)], `chore: sync root version to ${rootEntry.to}`);
|
|
206
|
+
}
|
|
207
|
+
if (options.push && bumped.length)
|
|
208
|
+
await git.push();
|
|
209
|
+
return plan;
|
|
210
|
+
}
|
|
211
|
+
VersionService.applyPlan = applyPlan;
|
|
212
|
+
})(VersionService || (VersionService = {}));
|
|
213
|
+
/** Every `package.json` field holding dependency ranges - shared with `PublishService`'s own
|
|
214
|
+
* `"workspace:"` rewrite-for-publish step, since it needs to scan the same fields. */
|
|
215
|
+
export const DEPENDENCY_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
216
|
+
const SEVERITY_RANK = { patch: 0, minor: 1, major: 2 };
|
|
217
|
+
/** `.rmanrc group` (cascaded): `true` (the default - see `resolveConfig`'s cascade, this is what a
|
|
218
|
+
* package inherits when nobody sets it at all) puts a package in the one implicit repo-wide
|
|
219
|
+
* group; a non-empty string joins exactly the other packages sharing that string, regardless of
|
|
220
|
+
* the repo's own default; `false` makes it a solo group of one. */
|
|
221
|
+
function resolveGroupKey(pkg) {
|
|
222
|
+
const g = pkg.config?.group;
|
|
223
|
+
if (g === false)
|
|
224
|
+
return `solo:${pkg.name}`;
|
|
225
|
+
if (typeof g === 'string' && g)
|
|
226
|
+
return `named:${g}`;
|
|
227
|
+
return 'default';
|
|
228
|
+
}
|
|
229
|
+
function groupLabel(key) {
|
|
230
|
+
if (key.startsWith('named:'))
|
|
231
|
+
return key.slice('named:'.length);
|
|
232
|
+
if (key.startsWith('solo:'))
|
|
233
|
+
return key.slice('solo:'.length);
|
|
234
|
+
return key;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Highest bump type implied by `commits`: a `Release-As: patch|minor|major` footer (see
|
|
238
|
+
* `parseReleaseAs`) replaces what that one commit's own subject/footers would otherwise imply,
|
|
239
|
+
* entirely - the escape hatch for e.g. a `feat:` that needs to ship as a patch right now, without
|
|
240
|
+
* waiting for the rest of a minor's worth of work. Absent that, a `!` marker or a `BREAKING
|
|
241
|
+
* CHANGE:` footer wins outright; otherwise `feat` implies minor; anything else (a `fix`, an
|
|
242
|
+
* unrecognized type, a non-conventional message) defaults to patch - something changed, so at
|
|
243
|
+
* least a patch release is warranted.
|
|
244
|
+
*/
|
|
245
|
+
function detectSeverity(commits) {
|
|
246
|
+
let severity = 'patch';
|
|
247
|
+
for (const c of commits) {
|
|
248
|
+
const override = parseReleaseAs(c.body);
|
|
249
|
+
if (override === 'major')
|
|
250
|
+
return 'major';
|
|
251
|
+
if (override) {
|
|
252
|
+
if (SEVERITY_RANK[override] > SEVERITY_RANK[severity])
|
|
253
|
+
severity = override;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const parsed = parseConventionalCommit(c.subject);
|
|
257
|
+
if (parsed?.breaking || hasBreakingChangeFooter(c.body))
|
|
258
|
+
return 'major';
|
|
259
|
+
if (parsed?.type === 'feat')
|
|
260
|
+
severity = 'minor';
|
|
261
|
+
}
|
|
262
|
+
return severity;
|
|
263
|
+
}
|
|
264
|
+
function maxVersion(versions) {
|
|
265
|
+
return versions.reduce((m, v) => (semver.gt(v, m) ? v : m), versions[0]);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `semver.inc`, "pre-ified" when `preid` is given: `current` already sitting on a prerelease with
|
|
269
|
+
* that *same* identifier just has its prerelease counter incremented (`'prerelease'`, e.g.
|
|
270
|
+
* `1.2.3-beta.0` -> `1.2.3-beta.1`) rather than jumping to a new base version every time this
|
|
271
|
+
* runs again during the same beta/rc cycle; anything else (a plain release, or a prerelease under
|
|
272
|
+
* a *different* identifier - switching from `beta` to `rc`, say) starts a fresh prerelease of
|
|
273
|
+
* `severity`'s own type (`'prepatch'`/`'preminor'`/`'premajor'`, e.g. `1.2.3` -> `1.3.0-beta.0`
|
|
274
|
+
* for a `minor`). Without `preid`, this is just `semver.inc(current, severity)`.
|
|
275
|
+
*/
|
|
276
|
+
function incVersion(current, severity, preid) {
|
|
277
|
+
if (!preid)
|
|
278
|
+
return semver.inc(current, severity) ?? current;
|
|
279
|
+
const existing = semver.prerelease(current);
|
|
280
|
+
const releaseType = existing && String(existing[0]) === preid ? 'prerelease' : `pre${severity}`;
|
|
281
|
+
return semver.inc(current, releaseType, preid) ?? current;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Decides one group's new version and which of its members actually receive it, writing an
|
|
285
|
+
* `Entry` per member into `entries`. `changeByPackage` holds each eligible package's own detected
|
|
286
|
+
* severity (or `undefined` for one with no real commits since its last tag) - `undefined` here
|
|
287
|
+
* always means "unchanged", never "explicit version" (that path is handled separately below).
|
|
288
|
+
*/
|
|
289
|
+
function computeGroupPlan(key, members, changeByPackage, explicitVersion, preid, entries) {
|
|
290
|
+
const label = groupLabel(key);
|
|
291
|
+
const changed = members.filter(m => changeByPackage.has(m.name));
|
|
292
|
+
if (!changed.length) {
|
|
293
|
+
for (const m of members) {
|
|
294
|
+
entries.set(m.name, { package: m, groupKey: key, group: label, status: 'no-change', from: m.version });
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const current = maxVersion(members.map(m => m.version));
|
|
299
|
+
let to;
|
|
300
|
+
let severity;
|
|
301
|
+
if (explicitVersion) {
|
|
302
|
+
to = explicitVersion;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
severity = changed.reduce((worst, m) => {
|
|
306
|
+
const s = changeByPackage.get(m.name).severity;
|
|
307
|
+
return SEVERITY_RANK[s] > SEVERITY_RANK[worst] ? s : worst;
|
|
308
|
+
}, 'patch');
|
|
309
|
+
to = incVersion(current, severity, preid);
|
|
310
|
+
}
|
|
311
|
+
const bumping = new Set(changed);
|
|
312
|
+
if (!explicitVersion && severity === 'major') {
|
|
313
|
+
for (const m of members)
|
|
314
|
+
bumping.add(m);
|
|
315
|
+
}
|
|
316
|
+
else if (!explicitVersion && severity === 'minor') {
|
|
317
|
+
const worklist = [...changed];
|
|
318
|
+
while (worklist.length) {
|
|
319
|
+
const cur = worklist.pop();
|
|
320
|
+
for (const m of members) {
|
|
321
|
+
if (bumping.has(m))
|
|
322
|
+
continue;
|
|
323
|
+
if (m.dependencies.includes(cur.name)) {
|
|
324
|
+
bumping.add(m);
|
|
325
|
+
worklist.push(m);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const m of members) {
|
|
331
|
+
if (bumping.has(m)) {
|
|
332
|
+
const own = changeByPackage.get(m.name);
|
|
333
|
+
entries.set(m.name, {
|
|
334
|
+
package: m,
|
|
335
|
+
groupKey: key,
|
|
336
|
+
group: label,
|
|
337
|
+
status: 'bump',
|
|
338
|
+
from: m.version,
|
|
339
|
+
to,
|
|
340
|
+
reason: own?.reason ?? `in-group dependent of a ${severity} change`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
entries.set(m.name, { package: m, groupKey: key, group: label, status: 'no-change', from: m.version });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A package depending on another group's bumped package always receives exactly a patch bump of
|
|
350
|
+
* its own, computed from its *own* group's current ceiling (the highest `to`/version among its
|
|
351
|
+
* group right now) - never the source's version, and never the source's severity. Runs as a
|
|
352
|
+
* worklist until nothing new is affected, since patching one package can itself cross into a third
|
|
353
|
+
* group, and so on; never touches a same-group dependent that a plain patch deliberately left
|
|
354
|
+
* alone (see `computeGroupPlan`'s patch case).
|
|
355
|
+
*/
|
|
356
|
+
function rippleCrossGroup(packages, entries, preid) {
|
|
357
|
+
const worklist = [...entries.values()].filter(e => e.status === 'bump');
|
|
358
|
+
while (worklist.length) {
|
|
359
|
+
const source = worklist.shift();
|
|
360
|
+
for (const pkg of packages) {
|
|
361
|
+
const entry = entries.get(pkg.name);
|
|
362
|
+
if (entry.status === 'bump' || entry.groupKey === source.groupKey)
|
|
363
|
+
continue;
|
|
364
|
+
if (!pkg.dependencies.includes(source.package.name))
|
|
365
|
+
continue;
|
|
366
|
+
const groupCeiling = maxVersion(packages
|
|
367
|
+
.filter(p => entries.get(p.name).groupKey === entry.groupKey)
|
|
368
|
+
.map(p => entries.get(p.name).to ?? p.version));
|
|
369
|
+
const next = {
|
|
370
|
+
...entry,
|
|
371
|
+
status: 'bump',
|
|
372
|
+
to: incVersion(groupCeiling, 'patch', preid),
|
|
373
|
+
reason: `depends on ${source.package.name}@${source.to}`,
|
|
374
|
+
};
|
|
375
|
+
entries.set(pkg.name, next);
|
|
376
|
+
worklist.push(next);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** The root's own `version` field is purely informational in a monorepo (it's never published on
|
|
381
|
+
* its own) - it always reflects whatever single version every group ended up sharing, or the
|
|
382
|
+
* overall highest version when groups diverged onto different numbers. Reports `'no-change'`
|
|
383
|
+
* (not `'bump'`) when nothing in the repository changed at all. */
|
|
384
|
+
function buildRootEntry(repository, memberEntries) {
|
|
385
|
+
const root = repository.rootPackage;
|
|
386
|
+
const anyBumped = memberEntries.some(e => e.status === 'bump');
|
|
387
|
+
if (!anyBumped) {
|
|
388
|
+
return { package: root, groupKey: '__root__', group: 'root', status: 'no-change', from: root.version };
|
|
389
|
+
}
|
|
390
|
+
const finalVersions = memberEntries.map(e => e.to ?? e.from);
|
|
391
|
+
const unique = new Set(finalVersions);
|
|
392
|
+
const to = unique.size === 1 ? finalVersions[0] : maxVersion(finalVersions);
|
|
393
|
+
return {
|
|
394
|
+
package: root,
|
|
395
|
+
groupKey: '__root__',
|
|
396
|
+
group: 'root',
|
|
397
|
+
status: 'bump',
|
|
398
|
+
from: root.version,
|
|
399
|
+
to,
|
|
400
|
+
reason: 'informational - monorepo root is never published on its own',
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/** `.rmanrc version.commitMessage` (root-level; `{version}` is replaced when every bumped package
|
|
404
|
+
* in this commit shares one version) - defaults to `"chore(release): v{version}"`, or a plain
|
|
405
|
+
* listing of `name@version` pairs when this particular commit spans different versions (a
|
|
406
|
+
* cross-group ripple can land a lone forced patch in a group that otherwise didn't move). */
|
|
407
|
+
function buildCommitMessage(repository, entries, messageOverride) {
|
|
408
|
+
const versions = new Set(entries.map(e => e.to));
|
|
409
|
+
if (versions.size === 1) {
|
|
410
|
+
const template = messageOverride ?? repository.rootPackage.config?.version?.commitMessage;
|
|
411
|
+
const version = entries[0].to;
|
|
412
|
+
if (typeof template === 'string' && template)
|
|
413
|
+
return template.replace(/\{version\}/g, version);
|
|
414
|
+
return `chore(release): v${version}`;
|
|
415
|
+
}
|
|
416
|
+
if (messageOverride)
|
|
417
|
+
return messageOverride;
|
|
418
|
+
return `chore(release): ${entries.map(e => `${e.package.name}@${e.to}`).join(', ')}`;
|
|
419
|
+
}
|
|
420
|
+
/** Expands `pkg`'s (cascaded) `.rmanrc changelog.tagPattern` into a concrete tag name for
|
|
421
|
+
* `version` - the same pattern `changelog` reads tags back with (see `findLatestTag`), just run
|
|
422
|
+
* forward: `{name}` becomes the package's own name, and `*` becomes `version`. */
|
|
423
|
+
function expandTag(pkg, version) {
|
|
424
|
+
const pattern = tagPattern(pkg).replace('{name}', pkg.name);
|
|
425
|
+
const starIdx = pattern.indexOf('*');
|
|
426
|
+
return starIdx === -1 ? pattern : pattern.slice(0, starIdx) + version + pattern.slice(starIdx + 1);
|
|
427
|
+
}
|
|
428
|
+
/** A `version.<key>` value: one command, or several to run in sequence - same shape as
|
|
429
|
+
* `run.<script>.script`/`.preScript`/`.postScript`. */
|
|
430
|
+
function normalizeScriptValue(value) {
|
|
431
|
+
if (typeof value === 'string')
|
|
432
|
+
return value || undefined;
|
|
433
|
+
if (Array.isArray(value)) {
|
|
434
|
+
const parts = value.filter((v) => typeof v === 'string' && !!v);
|
|
435
|
+
return parts.length ? parts.join(' && ') : undefined;
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Runs `pkg`'s own real npm lifecycle script (`preversion`/`version`/`postversion`) if it defines
|
|
441
|
+
* one for this phase, otherwise its `.rmanrc version.<cfgKey>` equivalent if configured - neither
|
|
442
|
+
* replaces the version write itself (unlike `ci`'s own-script override), they're hooks around a
|
|
443
|
+
* write that always happens, since dependency ranges and tags depend on it happening consistently.
|
|
444
|
+
*/
|
|
445
|
+
async function runVersionScript(pkg, cfgKey, npmScriptName) {
|
|
446
|
+
const own = pkg.json.scripts?.[npmScriptName];
|
|
447
|
+
const command = typeof own === 'string' && own ? own : normalizeScriptValue(pkg.config?.version?.[cfgKey]);
|
|
448
|
+
if (command)
|
|
449
|
+
await exec(command, { cwd: pkg.dirname, stdio: 'inherit' });
|
|
450
|
+
}
|
package/services.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ChangelogService } from './services/changelog.service.js';
|
|
2
|
+
export { CiService } from './services/ci.service.js';
|
|
3
|
+
export { CleanService } from './services/clean.service.js';
|
|
4
|
+
export { ExecService } from './services/exec.service.js';
|
|
5
|
+
export { ImportService } from './services/import.service.js';
|
|
6
|
+
export { ListService } from './services/list.service.js';
|
|
7
|
+
export { PublishService } from './services/publish.service.js';
|
|
8
|
+
export { RunService } from './services/run.service.js';
|
|
9
|
+
export { SystemInfo } from './services/system-info.js';
|
|
10
|
+
export { VersionService } from './services/version.service.js';
|
package/services.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ChangelogService } from './services/changelog.service.js';
|
|
2
|
+
export { CiService } from './services/ci.service.js';
|
|
3
|
+
export { CleanService } from './services/clean.service.js';
|
|
4
|
+
export { ExecService } from './services/exec.service.js';
|
|
5
|
+
export { ImportService } from './services/import.service.js';
|
|
6
|
+
export { ListService } from './services/list.service.js';
|
|
7
|
+
export { PublishService } from './services/publish.service.js';
|
|
8
|
+
export { RunService } from './services/run.service.js';
|
|
9
|
+
export { SystemInfo } from './services/system-info.js';
|
|
10
|
+
export { VersionService } from './services/version.service.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Argv } from 'yargs';
|
|
2
|
+
import type { Repository } from '../core/repository.js';
|
|
3
|
+
/** Same idea as GitHub Actions' own `branches`/`branches-ignore` workflow filters - restricts a
|
|
4
|
+
* command to running only from certain branches (or refuses certain ones), instead of any. */
|
|
5
|
+
export interface BranchGuardOptions {
|
|
6
|
+
allowBranch?: string | string[];
|
|
7
|
+
ignoreBranch?: string | string[];
|
|
8
|
+
}
|
|
9
|
+
/** `--allow-branch`/`--ignore-branch`, the same shape and describe text in every command that
|
|
10
|
+
* supports them - mirrors `package-filter.ts`'s own `applyPackageFilterOptions`. */
|
|
11
|
+
export declare function applyBranchGuardOptions<T>(cmd: Argv<T>): Argv<T>;
|
|
12
|
+
export declare function readBranchGuardOptions(args: any): BranchGuardOptions;
|
|
13
|
+
/**
|
|
14
|
+
* Refuses to let the command continue when the current branch doesn't satisfy `options` - an
|
|
15
|
+
* explicit CLI `allowBranch`/`ignoreBranch` replaces the root's own (cascade-free) `.rmanrc`
|
|
16
|
+
* equivalent entirely, same as `packageManager`'s own CLI-over-config precedence; with neither
|
|
17
|
+
* set anywhere, every branch is allowed (this is purely opt-in, same as GitHub Actions' workflows
|
|
18
|
+
* running on every branch until a `branches:` filter is added). A detached HEAD (or a directory
|
|
19
|
+
* that isn't a git repository at all) is never blocked - there's no branch name to check against.
|
|
20
|
+
*
|
|
21
|
+
* Already prints its own message and marks the error `logged` before throwing (matching every
|
|
22
|
+
* other guard check's convention in this codebase, e.g. `version`'s own dirty-check) - callers
|
|
23
|
+
* just need to `await` it before doing anything else.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertAllowedBranch(repository: Repository, options?: BranchGuardOptions): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import colors from 'ansi-colors';
|
|
2
|
+
import micromatch from 'micromatch';
|
|
3
|
+
import { GitHelper } from './git.js';
|
|
4
|
+
/** `--allow-branch`/`--ignore-branch`, the same shape and describe text in every command that
|
|
5
|
+
* supports them - mirrors `package-filter.ts`'s own `applyPackageFilterOptions`. */
|
|
6
|
+
export function applyBranchGuardOptions(cmd) {
|
|
7
|
+
return cmd
|
|
8
|
+
.option('allow-branch', {
|
|
9
|
+
describe: 'Refuse to run unless the current branch matches this glob (repeatable) - default: .rmanrc ' +
|
|
10
|
+
'"allowBranch", or no restriction at all',
|
|
11
|
+
type: 'string',
|
|
12
|
+
})
|
|
13
|
+
.option('ignore-branch', {
|
|
14
|
+
describe: 'Refuse to run if the current branch matches this glob (repeatable) - default: .rmanrc ' +
|
|
15
|
+
'"ignoreBranch", or no restriction at all',
|
|
16
|
+
type: 'string',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function readBranchGuardOptions(args) {
|
|
20
|
+
return {
|
|
21
|
+
allowBranch: args.allowBranch,
|
|
22
|
+
ignoreBranch: args.ignoreBranch,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Refuses to let the command continue when the current branch doesn't satisfy `options` - an
|
|
27
|
+
* explicit CLI `allowBranch`/`ignoreBranch` replaces the root's own (cascade-free) `.rmanrc`
|
|
28
|
+
* equivalent entirely, same as `packageManager`'s own CLI-over-config precedence; with neither
|
|
29
|
+
* set anywhere, every branch is allowed (this is purely opt-in, same as GitHub Actions' workflows
|
|
30
|
+
* running on every branch until a `branches:` filter is added). A detached HEAD (or a directory
|
|
31
|
+
* that isn't a git repository at all) is never blocked - there's no branch name to check against.
|
|
32
|
+
*
|
|
33
|
+
* Already prints its own message and marks the error `logged` before throwing (matching every
|
|
34
|
+
* other guard check's convention in this codebase, e.g. `version`'s own dirty-check) - callers
|
|
35
|
+
* just need to `await` it before doing anything else.
|
|
36
|
+
*/
|
|
37
|
+
export async function assertAllowedBranch(repository, options = {}) {
|
|
38
|
+
const allow = options.allowBranch ?? repository.config?.allowBranch;
|
|
39
|
+
const ignore = options.ignoreBranch ?? repository.config?.ignoreBranch;
|
|
40
|
+
if (!allow && !ignore)
|
|
41
|
+
return;
|
|
42
|
+
const git = new GitHelper({ cwd: repository.dirname });
|
|
43
|
+
const branch = await git.currentBranch();
|
|
44
|
+
if (!branch)
|
|
45
|
+
return;
|
|
46
|
+
if (allow && !micromatch.isMatch(branch, toArray(allow))) {
|
|
47
|
+
fail(`Branch "${branch}" is not allowed to run this command (must match: ${toArray(allow).join(', ')})`);
|
|
48
|
+
}
|
|
49
|
+
if (ignore && micromatch.isMatch(branch, toArray(ignore))) {
|
|
50
|
+
fail(`Branch "${branch}" is not allowed to run this command (blocked by: ${toArray(ignore).join(', ')})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function toArray(value) {
|
|
54
|
+
return Array.isArray(value) ? value : [value];
|
|
55
|
+
}
|
|
56
|
+
function fail(message) {
|
|
57
|
+
console.log(colors.red(message));
|
|
58
|
+
const err = new Error(message);
|
|
59
|
+
err.logged = true;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Package } from '../core/package.js';
|
|
2
|
+
import type { GitHelper } from './git.js';
|
|
3
|
+
/** `.rmanrc changelog.tagPattern` (cascaded, per-package overridable) - a glob for this package's
|
|
4
|
+
* release tags. `{name}` (if present) is replaced with the package's own name, e.g. `{name}@*`
|
|
5
|
+
* for independent per-package versioning (`@scope/pkg@1.2.3`, the same scheme lerna/changesets
|
|
6
|
+
* use - `@`/`/` are both fine in a git tag name). Without `{name}`, it's a single repo-wide tag
|
|
7
|
+
* shared by every package (e.g. the default `v*`). */
|
|
8
|
+
export declare function tagPattern(pkg: Package): string;
|
|
9
|
+
/** This package's most recent release tag - the `{name}`-bearing pattern looks up that package's
|
|
10
|
+
* *own* tags directly (newest by version sort); a repo-wide pattern instead finds the nearest tag
|
|
11
|
+
* HEAD actually descends from, since no single package "owns" that tag. `undefined` if never
|
|
12
|
+
* tagged at all (a fresh package, or one that's never been released). Shared by `changelog`
|
|
13
|
+
* (reading the last-documented version) and `version` (finding the boundary a bump measures
|
|
14
|
+
* "since"). */
|
|
15
|
+
export declare function findLatestTag(git: GitHelper, pkg: Package): Promise<string | undefined>;
|
|
16
|
+
/** Strips the pattern's literal prefix (everything before its first `*`) from `tag` to get just
|
|
17
|
+
* the version part - e.g. tag `@sqb/builder@1.2.3` against pattern `@sqb/builder@*` -> `1.2.3`.
|
|
18
|
+
* A pattern with no `*` is returned as its own "version" verbatim (an exact tag, nothing to strip). */
|
|
19
|
+
export declare function extractVersion(tag: string, expandedPattern: string): string;
|
|
20
|
+
/** Looks up `name`'s currently-published version on the npm registry, or `undefined` if it isn't
|
|
21
|
+
* published there at all (private, scoped-but-unpublished, no network, ...) - the real npm CLI
|
|
22
|
+
* call `detectChangeHash` uses by default; injectable via its `npmViewVersion` option so tests
|
|
23
|
+
* aren't making real registry calls. */
|
|
24
|
+
export declare function defaultNpmViewVersion(name: string, cwd: string): Promise<string | undefined>;
|
|
25
|
+
export interface DetectChangeHashOptions {
|
|
26
|
+
/** Use this commit/hash directly instead of auto-detecting - applies the same way to every
|
|
27
|
+
* package. `"npm"` (or omitting `from` entirely) triggers auto-detection instead of being
|
|
28
|
+
* treated as a literal ref. */
|
|
29
|
+
from?: string;
|
|
30
|
+
/** Overrides the real npm registry lookup made when auto-detecting - mainly for tests, so they
|
|
31
|
+
* don't depend on network access or a real published package. */
|
|
32
|
+
npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
|
|
33
|
+
/** An existing record of what's already been documented (typically a changelog file) - only
|
|
34
|
+
* consulted while auto-detecting (ignored when `from` is an explicit hash). Guards against a
|
|
35
|
+
* gap: if this file's own last-modifying commit is *older* than the npm-detected tag - e.g. the
|
|
36
|
+
* file was last updated for 1.1.0, but 1.2.0-1.5.0 were released without ever documenting them,
|
|
37
|
+
* and npm now reports 1.5.0 - starting from the tag alone would silently skip everything the
|
|
38
|
+
* file never recorded. The boundary becomes the merge-base of the two, so the result always
|
|
39
|
+
* covers at least as much as the file is missing. Has no effect when the file doesn't exist. */
|
|
40
|
+
catchUpFile?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the commit/hash a package's changes should be measured "since" - the boundary
|
|
44
|
+
* `changelog --from` uses, but reusable anywhere a command wants to answer "what changed for this
|
|
45
|
+
* package". An explicit `options.from` (anything but `"npm"`) is returned as-is, applying the same
|
|
46
|
+
* way to every package. Otherwise, it's auto-detected from the package's currently-published npm
|
|
47
|
+
* version: looked up via `npmViewVersion`, then mapped to a git tag using `.rmanrc
|
|
48
|
+
* changelog.tagPattern` (so independent and fixed monorepo versioning schemes both work - see
|
|
49
|
+
* `tagPattern`) - and, if `catchUpFile` is given and exists, widened to also cover anything that
|
|
50
|
+
* file hasn't caught up on yet (see its doc comment). Returns `undefined` when nothing can be
|
|
51
|
+
* resolved at all (unpublished, no network, no matching tag, no catch-up file) - callers should
|
|
52
|
+
* fall back to their own default in that case (e.g. `GitHelper.listCommits`'s "not yet pushed"
|
|
53
|
+
* default when no hash is given).
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectChangeHash(git: GitHelper, pkg: Package, options?: DetectChangeHashOptions): Promise<string | undefined>;
|