@wpelevator/wp-release 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +150 -19
  2. package/dist/changeset-config.d.ts +91 -5
  3. package/dist/changeset-config.js +108 -11
  4. package/dist/changeset-dependents.d.ts +19 -8
  5. package/dist/changeset-dependents.js +96 -34
  6. package/dist/changeset-files.d.ts +6 -5
  7. package/dist/changeset-files.js +7 -5
  8. package/dist/changeset-packages.d.ts +55 -8
  9. package/dist/changeset-packages.js +85 -14
  10. package/dist/changeset-prompt.d.ts +119 -0
  11. package/dist/changeset-prompt.js +203 -0
  12. package/dist/changeset-release.d.ts +43 -5
  13. package/dist/changeset-release.js +99 -16
  14. package/dist/changesets.d.ts +6 -2
  15. package/dist/changesets.js +18 -12
  16. package/dist/cli.d.ts +7 -1
  17. package/dist/cli.js +51 -14
  18. package/dist/commands/args.d.ts +0 -21
  19. package/dist/commands/args.js +1 -35
  20. package/dist/commands/changeset-add.d.ts +5 -2
  21. package/dist/commands/changeset-add.js +109 -9
  22. package/dist/commands/changeset-args.d.ts +24 -5
  23. package/dist/commands/changeset-args.js +57 -14
  24. package/dist/commands/changeset-status.d.ts +3 -1
  25. package/dist/commands/changeset-status.js +2 -1
  26. package/dist/commands/changeset-tag.d.ts +1 -1
  27. package/dist/commands/changeset-tag.js +6 -3
  28. package/dist/commands/changeset-version.d.ts +7 -1
  29. package/dist/commands/changeset-version.js +17 -4
  30. package/dist/commands/checksums.js +2 -2
  31. package/dist/commands/command.d.ts +5 -0
  32. package/dist/commands/deploy-git.d.ts +24 -4
  33. package/dist/commands/deploy-git.js +57 -3
  34. package/dist/commands/deploy-svn.d.ts +24 -4
  35. package/dist/commands/deploy-svn.js +57 -2
  36. package/dist/commands/keygen.d.ts +3 -0
  37. package/dist/commands/keygen.js +6 -1
  38. package/dist/commands/package-deploys.d.ts +86 -0
  39. package/dist/commands/package-deploys.js +113 -0
  40. package/dist/commands/sign.d.ts +19 -1
  41. package/dist/commands/sign.js +47 -12
  42. package/dist/commands/verify.d.ts +3 -0
  43. package/dist/commands/verify.js +43 -24
  44. package/dist/commands/zip.d.ts +12 -8
  45. package/dist/commands/zip.js +135 -78
  46. package/dist/ed25519.d.ts +25 -0
  47. package/dist/ed25519.js +44 -0
  48. package/dist/file-selection.d.ts +21 -0
  49. package/dist/file-selection.js +35 -0
  50. package/dist/git-deploy.d.ts +32 -0
  51. package/dist/git-deploy.js +57 -1
  52. package/dist/git.d.ts +17 -0
  53. package/dist/git.js +23 -0
  54. package/dist/index.d.ts +13 -5
  55. package/dist/index.js +7 -3
  56. package/dist/minisign.d.ts +41 -0
  57. package/dist/minisign.js +128 -0
  58. package/dist/package-deploy.d.ts +133 -0
  59. package/dist/package-deploy.js +323 -0
  60. package/dist/package-zip.d.ts +52 -0
  61. package/dist/package-zip.js +78 -0
  62. package/dist/release-repository.d.ts +18 -5
  63. package/dist/release-repository.js +51 -11
  64. package/dist/release-tags.d.ts +3 -1
  65. package/dist/release-tags.js +5 -2
  66. package/dist/signing.d.ts +64 -9
  67. package/dist/signing.js +113 -40
  68. package/dist/ssh-signature.d.ts +43 -0
  69. package/dist/ssh-signature.js +156 -0
  70. package/dist/summary.d.ts +3 -3
  71. package/dist/summary.js +2 -2
  72. package/dist/tag-format.d.ts +23 -14
  73. package/dist/tag-format.js +26 -40
  74. package/dist/version-files.d.ts +8 -8
  75. package/dist/version-files.js +22 -18
  76. package/dist/versions.d.ts +28 -3
  77. package/dist/versions.js +84 -9
  78. package/package.json +2 -2
@@ -0,0 +1,203 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { findPackage } from "./changeset-packages.js";
3
+ import { CHANGESET_BUMPS } from "./changesets.js";
4
+ import { ValidationError } from "./errors.js";
5
+ /**
6
+ * Ask for the packages, bumps and release note of a changeset, line by line,
7
+ * so that it works in any terminal and with piped input.
8
+ */
9
+ export class ChangesetPrompt {
10
+ terminal;
11
+ lines;
12
+ readline;
13
+ /**
14
+ * @param terminal Input to read answers from and output for the questions.
15
+ */
16
+ constructor(terminal) {
17
+ this.terminal = terminal;
18
+ }
19
+ /**
20
+ * Ask for the changeset, preselecting the changed packages.
21
+ *
22
+ * @param input Packages, changed packages and an optional release note.
23
+ * @return Package bumps, empty for no release, and the release note.
24
+ * @throws {ValidationError} If the input ends before the changeset is complete.
25
+ */
26
+ async ask(input) {
27
+ this.listPackages(input);
28
+ const names = await this.askUntilValid(this.getPackagesQuestion(input), (answer) => this.parsePackages(answer, input));
29
+ const packages = {};
30
+ for (const name of names) {
31
+ packages[name] = await this.askUntilValid(`Bump for ${name} (patch, minor or major) [patch]: `, (answer) => this.parseBump(answer));
32
+ }
33
+ const summary = input.summary ??
34
+ (names.length
35
+ ? await this.askUntilValid('Release note: ', (answer) => this.parseSummary(answer))
36
+ : '');
37
+ return { packages, summary };
38
+ }
39
+ /**
40
+ * Stop reading the input, so that the process can exit.
41
+ */
42
+ close() {
43
+ this.readline?.close();
44
+ }
45
+ /**
46
+ * Print the numbered packages, marking the changed ones.
47
+ *
48
+ * @param input Packages and changed packages.
49
+ */
50
+ listPackages(input) {
51
+ const width = String(input.packages.length).length;
52
+ const rows = input.packages.map((pkg, index) => {
53
+ const marker = input.changed.includes(pkg.name) ? '*' : ' ';
54
+ const label = pkg.name === pkg.path
55
+ ? pkg.name
56
+ : `${pkg.name} (${pkg.path})`;
57
+ return ` ${String(index + 1).padStart(width)} ${marker} ${label}`;
58
+ });
59
+ const legend = input.changed.length
60
+ ? `* changed${input.since ? ` since ${input.since}` : ''} without a changeset`
61
+ : 'No changed packages without a changeset.';
62
+ this.terminal.output.write(`Packages:\n${rows.join('\n')}\n${legend}\n`);
63
+ }
64
+ /**
65
+ * Get the package question, with the changed packages as the default.
66
+ *
67
+ * @param input Packages and changed packages.
68
+ * @return Question text.
69
+ */
70
+ getPackagesQuestion(input) {
71
+ const defaults = input.changed.length
72
+ ? ` [${input.changed.join(', ')}]`
73
+ : '';
74
+ return `Packages to release, by number, identifier or path, comma-separated, or "none" for no release${defaults}: `;
75
+ }
76
+ /**
77
+ * Parse the chosen packages.
78
+ *
79
+ * @param answer Comma or space separated numbers, identifiers or paths.
80
+ * @param input Packages and changed packages.
81
+ * @return Package identifiers, empty for no release.
82
+ * @throws {ValidationError} For unknown, repeated or missing packages.
83
+ */
84
+ parsePackages(answer, input) {
85
+ const tokens = answer.split(/[\s,]+/).filter(Boolean);
86
+ let names = input.changed;
87
+ if (1 === tokens.length && 'none' === tokens[0]) {
88
+ names = [];
89
+ }
90
+ else if (tokens.length) {
91
+ names = tokens.map((token) => this.parsePackage(token, input.packages));
92
+ }
93
+ else if (!names.length) {
94
+ throw new ValidationError('Choose at least one package, or "none" for no release.');
95
+ }
96
+ const repeated = names.find((name, index) => names.indexOf(name) !== index);
97
+ if (repeated) {
98
+ throw new ValidationError(`Package "${repeated}" was chosen more than once.`);
99
+ }
100
+ return names;
101
+ }
102
+ /**
103
+ * Parse one chosen package.
104
+ *
105
+ * @param token Package number, identifier or path.
106
+ * @param packages Configured packages.
107
+ * @return Package identifier.
108
+ * @throws {ValidationError} For an unknown package.
109
+ */
110
+ parsePackage(token, packages) {
111
+ const pkg = /^\d+$/.test(token)
112
+ ? packages[Number(token) - 1]
113
+ : findPackage(packages, token);
114
+ if (!pkg) {
115
+ throw new ValidationError(`Unknown package "${token}".`);
116
+ }
117
+ return pkg.name;
118
+ }
119
+ /**
120
+ * Parse a bump, patch by default.
121
+ *
122
+ * @param answer Bump name.
123
+ * @return Bump.
124
+ * @throws {ValidationError} For an unknown bump.
125
+ */
126
+ parseBump(answer) {
127
+ const bump = answer.trim().toLowerCase() || 'patch';
128
+ if (!CHANGESET_BUMPS.includes(bump)) {
129
+ throw new ValidationError(`Invalid bump "${bump}"; expected patch, minor or major.`);
130
+ }
131
+ return bump;
132
+ }
133
+ /**
134
+ * Parse a release note.
135
+ *
136
+ * @param answer Release note.
137
+ * @return Trimmed release note.
138
+ * @throws {ValidationError} For an empty release note.
139
+ */
140
+ parseSummary(answer) {
141
+ const summary = answer.trim();
142
+ if (!summary) {
143
+ throw new ValidationError('The release note must not be empty.');
144
+ }
145
+ return summary;
146
+ }
147
+ /**
148
+ * Ask a question until the answer parses, printing why it didn't.
149
+ *
150
+ * @param question Question text.
151
+ * @param parse Parser that throws a ValidationError for invalid answers.
152
+ * @return Parsed answer.
153
+ */
154
+ async askUntilValid(question, parse) {
155
+ let parsed;
156
+ while (undefined === parsed) {
157
+ const answer = await this.readAnswer(question);
158
+ try {
159
+ parsed = { answer: parse(answer) };
160
+ }
161
+ catch (error) {
162
+ if (!(error instanceof ValidationError)) {
163
+ throw error;
164
+ }
165
+ this.terminal.output.write(`${error.message}\n`);
166
+ }
167
+ }
168
+ return parsed.answer;
169
+ }
170
+ /**
171
+ * Write a question and read the next line of the input.
172
+ *
173
+ * @param question Question text.
174
+ * @return Answer line.
175
+ * @throws {ValidationError} If the input ended.
176
+ */
177
+ async readAnswer(question) {
178
+ this.terminal.output.write(question);
179
+ const line = await this.getLines().next();
180
+ if (line.done) {
181
+ throw new ValidationError('The input ended before the changeset was complete.');
182
+ }
183
+ return line.value;
184
+ }
185
+ /**
186
+ * Get the lines of the input, reading it from the first question on. The
187
+ * interface stays in line mode, so the terminal echoes and edits the
188
+ * answers and Ctrl+C ends the process.
189
+ *
190
+ * @return Line iterator.
191
+ */
192
+ getLines() {
193
+ if (!this.lines) {
194
+ this.readline = createInterface({
195
+ input: this.terminal.input,
196
+ terminal: false,
197
+ crlfDelay: Infinity,
198
+ });
199
+ this.lines = this.readline[Symbol.asyncIterator]();
200
+ }
201
+ return this.lines;
202
+ }
203
+ }
@@ -4,13 +4,15 @@ import { ReleaseRepository } from './release-repository.ts';
4
4
  export interface ChangesetRepository {
5
5
  cwd: string;
6
6
  configFile?: string;
7
+ /** Tag format that replaces the tagFormat of the config. */
8
+ tagFormat?: string;
7
9
  }
8
10
  export interface ChangesetReleaseOptions extends ChangesetRepository {
9
11
  /** Git runner, spawning git by default. */
10
12
  git?: GitRunner;
11
13
  }
12
14
  export interface ChangesetAddOptions {
13
- /** Bump of each package. */
15
+ /** Bump of each package, by package identifier or path under the configured root. */
14
16
  packages: Record<string, ChangesetBump>;
15
17
  /** Changelog summary. */
16
18
  summary: string;
@@ -20,10 +22,22 @@ export interface ChangesetAddOptions {
20
22
  export interface ChangesetVersionOptions {
21
23
  dryRun?: boolean;
22
24
  date?: string;
23
- /** Version only these packages, leaving the other changesets pending. */
25
+ /** Version only these packages, by identifier or path under the configured root, leaving the other changesets pending. */
24
26
  packages?: string[];
25
27
  /** Whether the selected sources release their configured dependents; defaults to true. */
26
28
  dependents?: boolean;
29
+ /**
30
+ * Release pre-releases of the next versions, with this identifier and an
31
+ * optional number, like rc or rc.1, numbered from the package tags without one.
32
+ */
33
+ prerelease?: string;
34
+ /**
35
+ * Release snapshots of the next versions, like 1.3.0-pr-251.20261012183000,
36
+ * with this identifier and the UTC time, for test builds that aren't committed.
37
+ */
38
+ snapshot?: string;
39
+ /** Time of a snapshot, now by default. */
40
+ snapshotTime?: Date;
27
41
  }
28
42
  export type ChangesetVersionInput = ChangesetRepository & ChangesetVersionOptions;
29
43
  /** Versioned releases, the applied changesets and whether files were changed. */
@@ -38,7 +52,7 @@ export interface ChangesetTagOptions {
38
52
  untagged?: boolean;
39
53
  push?: boolean;
40
54
  since?: string;
41
- /** Tag only these packages. */
55
+ /** Tag only these packages, by identifier or path under the configured root. */
42
56
  packages?: string[];
43
57
  /** Expected version of the single package in `packages`. */
44
58
  version?: string;
@@ -59,10 +73,14 @@ export interface ChangesetTagResult {
59
73
  }>;
60
74
  }
61
75
  export interface ChangesetStatusOptions {
62
- /** Base ref to diff against, origin/<baseBranch> by default. */
76
+ /** Base ref to diff against, <remote>/<baseBranch> by default. */
63
77
  since?: string;
78
+ /** Whether to include uncommitted and untracked files, like before adding a changeset. */
79
+ uncommitted?: boolean;
64
80
  }
65
81
  export interface ChangesetStatusResult {
82
+ /** Base ref that the changed files were compared with, if any. */
83
+ since?: string;
66
84
  changesets: Changeset[];
67
85
  releases: PackageRelease[];
68
86
  changedPackages: string[];
@@ -81,6 +99,7 @@ export interface ChangesetStatusResult {
81
99
  export declare class ChangesetRelease {
82
100
  readonly cwd: string;
83
101
  readonly configFile: string | undefined;
102
+ readonly tagFormat: string | undefined;
84
103
  readonly repository: ReleaseRepository;
85
104
  /**
86
105
  * @param options Repository root, config file and git runner.
@@ -118,7 +137,7 @@ export declare class ChangesetRelease {
118
137
  */
119
138
  version(options?: ChangesetVersionOptions): Promise<VersionChangesetsResult>;
120
139
  /**
121
- * Create missing release tags, optionally pushing them to the origin
140
+ * Create missing release tags, optionally pushing them to the configured
122
141
  * remote. Each tag comes with the title and notes of its release, for a
123
142
  * workflow step to create the GitHub Release.
124
143
  *
@@ -135,6 +154,25 @@ export declare class ChangesetRelease {
135
154
  * @throws {ValidationError} Without a config.
136
155
  */
137
156
  getStatus(options?: ChangesetStatusOptions): Promise<ChangesetStatusResult>;
157
+ /**
158
+ * Turn the next versions of releases into pre-releases, like 1.3.0-rc.1
159
+ * for 1.3.0, numbered from the existing tags of each package. Dependents
160
+ * become pre-releases too, so they bundle the pre-releases of their sources.
161
+ *
162
+ * @param context Release context.
163
+ * @param releases Releases with their stable next versions.
164
+ * @param prerelease Pre-release identifier with an optional number, like rc or rc.1.
165
+ * @return Releases with their pre-release next versions.
166
+ * @throws {ValidationError} For an invalid pre-release or an existing version.
167
+ */
168
+ private toPrereleases;
169
+ /**
170
+ * List the versions of the tags with a prefix.
171
+ *
172
+ * @param prefix Tag prefix of a package, like `plugins/alpha/`.
173
+ * @return Tagged versions.
174
+ */
175
+ private listTaggedVersions;
138
176
  /**
139
177
  * Write the versions, dependency specs and changelogs of the releases and
140
178
  * consume the applied changesets, restoring every changed file when a
@@ -3,15 +3,15 @@ import { getVersionedReleases, prependChangelog } from "./changelog.js";
3
3
  import { loadChangesetConfig } from "./changeset-config.js";
4
4
  import { applyDependents, assertDependentsSourcesReleased, collectPinDriftWarnings, expandDependentsPlan, findBundledPinChanges, hasChangeset, selectReleasePlan, stripDependentsPlan, updateDependencyDeclaration, } from "./changeset-dependents.js";
5
5
  import { consumeChangeset, readChangesets, readChangesetsSince, writeChangeset, } from "./changeset-files.js";
6
- import { assertKnownPackages, discoverPackages, getChangedPackages, toChangesetPackage, } from "./changeset-packages.js";
6
+ import { discoverPackages, getChangedPackages, resolvePackageNames, toChangesetPackage, } from "./changeset-packages.js";
7
7
  import { getChangesetBump, getNextVersion } from "./changesets.js";
8
8
  import { ValidationError } from "./errors.js";
9
9
  import { FileJournal } from "./file-journal.js";
10
10
  import { getPreviousTag, getReleaseNotes, getTagVersion, selectTagPackages, } from "./release-tags.js";
11
11
  import { ReleaseRepository } from "./release-repository.js";
12
- import { formatTag } from "./tag-format.js";
12
+ import { DEFAULT_TAG_FORMAT, formatTag } from "./tag-format.js";
13
13
  import { verifyVersionTargets, writeVersion } from "./version-files.js";
14
- import { isPrerelease } from "./versions.js";
14
+ import { getPrereleaseVersion, getSnapshotVersion, isPrerelease, } from "./versions.js";
15
15
  const MISSING_CONFIG = 'No wp-release config found. Add wp-release.config.js or pass --config <path>.';
16
16
  /**
17
17
  * Changeset releases of a repository: recording changesets, versioning the
@@ -24,6 +24,7 @@ const MISSING_CONFIG = 'No wp-release config found. Add wp-release.config.js or
24
24
  export class ChangesetRelease {
25
25
  cwd;
26
26
  configFile;
27
+ tagFormat;
27
28
  repository;
28
29
  /**
29
30
  * @param options Repository root, config file and git runner.
@@ -31,6 +32,7 @@ export class ChangesetRelease {
31
32
  constructor(options) {
32
33
  this.cwd = options.cwd;
33
34
  this.configFile = options.configFile;
35
+ this.tagFormat = options.tagFormat;
34
36
  this.repository = new ReleaseRepository({
35
37
  cwd: options.cwd,
36
38
  runner: options.git,
@@ -67,8 +69,17 @@ export class ChangesetRelease {
67
69
  async add(options) {
68
70
  const config = await this.requireConfig();
69
71
  const packages = await discoverPackages(this.repository, config);
70
- assertKnownPackages(packages, Object.keys(options.packages));
71
- return writeChangeset(this.getChangesetDirectory(config), options.packages, options.summary, options.name);
72
+ const entries = Object.entries(options.packages);
73
+ const names = resolvePackageNames(packages, entries.map(([name]) => name));
74
+ const resolved = {};
75
+ entries.forEach(([, bump], index) => {
76
+ const name = names[index] ?? '';
77
+ if (Object.hasOwn(resolved, name)) {
78
+ throw new ValidationError(`Package "${name}" was passed more than once.`);
79
+ }
80
+ resolved[name] = bump;
81
+ });
82
+ return writeChangeset(this.getChangesetDirectory(config), resolved, options.summary, options.name);
72
83
  }
73
84
  /**
74
85
  * Apply pending package versions and changelogs. A failed run restores
@@ -82,9 +93,12 @@ export class ChangesetRelease {
82
93
  const context = await this.requireContext();
83
94
  const { config, packages } = context;
84
95
  const withDependents = options.dependents ?? true;
96
+ const selected = options.packages
97
+ ? resolvePackageNames(packages, options.packages)
98
+ : undefined;
85
99
  let releasePlan = context.plan;
86
- if (options.packages) {
87
- releasePlan = expandDependentsPlan(await selectReleasePlan(context.plan, packages, options.packages, config, withDependents));
100
+ if (selected) {
101
+ releasePlan = expandDependentsPlan(await selectReleasePlan(context.plan, packages, selected, config, withDependents));
88
102
  }
89
103
  else if (!withDependents) {
90
104
  releasePlan = stripDependentsPlan(context.plan);
@@ -96,10 +110,31 @@ export class ChangesetRelease {
96
110
  if (withDependents && config.dependents) {
97
111
  await assertDependentsSourcesReleased(releasePlan, context.plan, packages, config);
98
112
  }
113
+ if (undefined !== options.prerelease &&
114
+ undefined !== options.snapshot) {
115
+ throw new ValidationError('A release cannot be both a pre-release and a snapshot.');
116
+ }
117
+ if (undefined !== options.snapshot) {
118
+ const id = options.snapshot;
119
+ const time = options.snapshotTime ?? new Date();
120
+ releasePlan = {
121
+ ...releasePlan,
122
+ releases: releasePlan.releases.map((release) => ({
123
+ ...release,
124
+ nextVersion: getSnapshotVersion(release.nextVersion, id, time),
125
+ })),
126
+ };
127
+ }
128
+ if (undefined !== options.prerelease) {
129
+ releasePlan = {
130
+ ...releasePlan,
131
+ releases: await this.toPrereleases(context, releasePlan.releases, options.prerelease),
132
+ };
133
+ }
99
134
  const date = options.date ?? new Date().toISOString().slice(0, 10);
100
135
  const releases = await getVersionedReleases(this.repository, config, releasePlan.releases, this.getChangesetDirectory(config));
101
136
  if (!options.dryRun) {
102
- await this.writeReleases(context, releasePlan, releases, date, options.packages);
137
+ await this.writeReleases(context, releasePlan, releases, date, selected);
103
138
  }
104
139
  return {
105
140
  changesets: releasePlan.changesets,
@@ -111,7 +146,7 @@ export class ChangesetRelease {
111
146
  };
112
147
  }
113
148
  /**
114
- * Create missing release tags, optionally pushing them to the origin
149
+ * Create missing release tags, optionally pushing them to the configured
115
150
  * remote. Each tag comes with the title and notes of its release, for a
116
151
  * workflow step to create the GitHub Release.
117
152
  *
@@ -124,13 +159,13 @@ export class ChangesetRelease {
124
159
  throw new ValidationError('Listing untagged releases cannot be combined with pushing tags.');
125
160
  }
126
161
  const context = await this.requireContext();
127
- const selected = selectTagPackages(context.packages, options.packages ?? [], options.version);
162
+ const selected = selectTagPackages(context.packages, resolvePackageNames(context.packages, options.packages ?? []), options.version);
128
163
  const packages = options.since
129
164
  ? (await getChangedPackages(this.repository, selected, options.since)).packages
130
165
  : selected;
131
166
  const results = [];
132
167
  const dryRun = options.dryRun || options.untagged;
133
- const tagFormat = context.config.tagFormat ?? '{path}/{version}';
168
+ const tagFormat = context.config.tagFormat ?? DEFAULT_TAG_FORMAT;
134
169
  for (const pkg of packages) {
135
170
  const version = await getTagVersion(pkg, options.version);
136
171
  if (undefined !== options.version &&
@@ -163,7 +198,7 @@ export class ChangesetRelease {
163
198
  // tags when more than three are pushed at once.
164
199
  if (options.push && !dryRun) {
165
200
  for (const item of results.filter((result) => result.created)) {
166
- await this.repository.pushTag(item.tag);
201
+ await this.repository.pushTag(item.tag, context.config.remote ?? 'origin');
167
202
  }
168
203
  }
169
204
  return {
@@ -183,16 +218,27 @@ export class ChangesetRelease {
183
218
  const { config, packages, plan } = await this.requireContext();
184
219
  const { changesets, releases } = plan;
185
220
  const since = options.since ??
186
- (config.baseBranch ? `origin/${config.baseBranch}` : undefined);
221
+ (config.baseBranch
222
+ ? `${config.remote ?? 'origin'}/${config.baseBranch}`
223
+ : undefined);
187
224
  const changed = since
188
- ? await getChangedPackages(this.repository, packages, since)
225
+ ? await getChangedPackages(this.repository, packages, since, options.uncommitted)
189
226
  : { packages: [], filesByPackage: {} };
190
227
  const changedPackages = changed.packages.map((pkg) => pkg.name);
191
228
  const changesetsInDiff = since
192
- ? await readChangesetsSince(this.repository, this.getChangesetDirectory(config), changesets, since)
229
+ ? await readChangesetsSince(this.repository, this.getChangesetDirectory(config), changesets, since, options.uncommitted)
193
230
  : [];
194
231
  const hasEmptyChangeset = changesetsInDiff.some((changeset) => 0 === Object.keys(changeset.packages).length);
195
- const bundled = await findBundledPinChanges(changed, changesetsInDiff, packages, config);
232
+ const bundled = since
233
+ ? await findBundledPinChanges({
234
+ changed,
235
+ changesetsInDiff,
236
+ packages,
237
+ config,
238
+ repository: this.repository,
239
+ since,
240
+ })
241
+ : new Set();
196
242
  const missingChangesets = hasEmptyChangeset
197
243
  ? []
198
244
  : changedPackages.filter((name) => !hasChangeset(changesetsInDiff, name) &&
@@ -203,6 +249,7 @@ export class ChangesetRelease {
203
249
  ? await collectPinDriftWarnings(config, packages)
204
250
  : [];
205
251
  return {
252
+ ...(since ? { since } : {}),
206
253
  changesets,
207
254
  releases,
208
255
  changedPackages,
@@ -211,6 +258,40 @@ export class ChangesetRelease {
211
258
  warnings,
212
259
  };
213
260
  }
261
+ /**
262
+ * Turn the next versions of releases into pre-releases, like 1.3.0-rc.1
263
+ * for 1.3.0, numbered from the existing tags of each package. Dependents
264
+ * become pre-releases too, so they bundle the pre-releases of their sources.
265
+ *
266
+ * @param context Release context.
267
+ * @param releases Releases with their stable next versions.
268
+ * @param prerelease Pre-release identifier with an optional number, like rc or rc.1.
269
+ * @return Releases with their pre-release next versions.
270
+ * @throws {ValidationError} For an invalid pre-release or an existing version.
271
+ */
272
+ async toPrereleases(context, releases, prerelease) {
273
+ const tagFormat = context.config.tagFormat ?? DEFAULT_TAG_FORMAT;
274
+ return Promise.all(releases.map(async (release) => {
275
+ const configured = context.packages.find((pkg) => pkg.name === release.package.name);
276
+ const versions = configured
277
+ ? await this.listTaggedVersions(formatTag(tagFormat, configured, ''))
278
+ : [];
279
+ return {
280
+ ...release,
281
+ nextVersion: getPrereleaseVersion(release.nextVersion, prerelease, versions),
282
+ };
283
+ }));
284
+ }
285
+ /**
286
+ * List the versions of the tags with a prefix.
287
+ *
288
+ * @param prefix Tag prefix of a package, like `plugins/alpha/`.
289
+ * @return Tagged versions.
290
+ */
291
+ async listTaggedVersions(prefix) {
292
+ const tags = await this.repository.listTags(prefix);
293
+ return tags.map((tag) => tag.slice(prefix.length));
294
+ }
214
295
  /**
215
296
  * Write the versions, dependency specs and changelogs of the releases and
216
297
  * consume the applied changesets, restoring every changed file when a
@@ -287,6 +368,7 @@ export class ChangesetRelease {
287
368
  const loaded = await loadChangesetConfig({
288
369
  cwd: this.cwd,
289
370
  ...(this.configFile ? { configFile: this.configFile } : {}),
371
+ ...(this.tagFormat ? { tagFormat: this.tagFormat } : {}),
290
372
  });
291
373
  if (!loaded) {
292
374
  return undefined;
@@ -352,6 +434,7 @@ export class ChangesetRelease {
352
434
  const loaded = await loadChangesetConfig({
353
435
  cwd: this.cwd,
354
436
  ...(this.configFile ? { configFile: this.configFile } : {}),
437
+ ...(this.tagFormat ? { tagFormat: this.tagFormat } : {}),
355
438
  });
356
439
  if (!loaded) {
357
440
  throw new ValidationError(MISSING_CONFIG);
@@ -83,11 +83,15 @@ export declare function formatChangelogList(entries: string[]): string;
83
83
  */
84
84
  export declare function getChangesetBump(changesets: Changeset[], packageName: string): ChangesetBump | undefined;
85
85
  /**
86
- * Calculate the next stable semantic version using literal SemVer bump rules.
86
+ * Calculate the next stable version using literal SemVer bump rules. Besides
87
+ * x.y.z, it accepts the x.y and x.y.z.w versions common in WordPress plugins:
88
+ * the bumped part is raised, the parts after it reset to 0, and the version
89
+ * keeps its number of parts, so a minor bump of 2.1 is 2.2 and of 1.2.3.4 is
90
+ * 1.3.0.0. A patch bump of x.y adds the third part, so 2.1 becomes 2.1.1.
87
91
  *
88
92
  * @param currentVersion Current package version.
89
93
  * @param bump Requested bump.
90
94
  * @return Next version.
91
- * @throws {ValidationError} If currentVersion is not a stable x.y.z version.
95
+ * @throws {ValidationError} If currentVersion is not a stable x.y, x.y.z or x.y.z.w version.
92
96
  */
93
97
  export declare function getNextVersion(currentVersion: string, bump: ChangesetBump): string;
@@ -99,26 +99,32 @@ export function getChangesetBump(changesets, packageName) {
99
99
  return bumps.sort((a, b) => BUMP_RANK[b] - BUMP_RANK[a])[0];
100
100
  }
101
101
  /**
102
- * Calculate the next stable semantic version using literal SemVer bump rules.
102
+ * Calculate the next stable version using literal SemVer bump rules. Besides
103
+ * x.y.z, it accepts the x.y and x.y.z.w versions common in WordPress plugins:
104
+ * the bumped part is raised, the parts after it reset to 0, and the version
105
+ * keeps its number of parts, so a minor bump of 2.1 is 2.2 and of 1.2.3.4 is
106
+ * 1.3.0.0. A patch bump of x.y adds the third part, so 2.1 becomes 2.1.1.
103
107
  *
104
108
  * @param currentVersion Current package version.
105
109
  * @param bump Requested bump.
106
110
  * @return Next version.
107
- * @throws {ValidationError} If currentVersion is not a stable x.y.z version.
111
+ * @throws {ValidationError} If currentVersion is not a stable x.y, x.y.z or x.y.z.w version.
108
112
  */
109
113
  export function getNextVersion(currentVersion, bump) {
110
114
  if (!CHANGESET_BUMPS.includes(bump)) {
111
115
  throw new ValidationError(`Invalid bump "${bump}"; expected patch, minor or major.`);
112
116
  }
113
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(currentVersion);
114
- if (!match) {
115
- throw new ValidationError(`Cannot bump "${currentVersion}": expected a stable semantic version in x.y.z format.`);
117
+ if (!/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){1,3}$/.test(currentVersion)) {
118
+ throw new ValidationError(`Cannot bump "${currentVersion}": expected a stable version in x.y, x.y.z or x.y.z.w format.`);
116
119
  }
117
- if ('major' === bump) {
118
- return `${Number(match[1]) + 1}.0.0`;
119
- }
120
- if ('minor' === bump) {
121
- return `${match[1]}.${Number(match[2]) + 1}.0`;
122
- }
123
- return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
120
+ const parts = currentVersion.split('.').map(Number);
121
+ const index = { major: 0, minor: 1, patch: 2 }[bump];
122
+ const length = Math.max(parts.length, index + 1);
123
+ return Array.from({ length }, (_, position) => {
124
+ const part = parts[position] ?? 0;
125
+ if (position === index) {
126
+ return part + 1;
127
+ }
128
+ return position < index ? part : 0;
129
+ }).join('.');
124
130
  }
package/dist/cli.d.ts CHANGED
@@ -6,7 +6,13 @@ import type { ChangesetVersionResult } from './commands/changeset-version.ts';
6
6
  import { Logger } from './logger.ts';
7
7
  import type { OutputStream } from './logger.ts';
8
8
  import type { Command, CommandOutcome } from './commands/command.ts';
9
+ /** Input stream that interactive prompts read from in a terminal. */
10
+ export type InputStream = NodeJS.ReadableStream & {
11
+ isTTY?: boolean;
12
+ };
9
13
  export interface CliIo {
14
+ /** Input for interactive prompts, used when it and stdout are a terminal. */
15
+ stdin?: InputStream;
10
16
  /** Stream for regular output. */
11
17
  stdout?: OutputStream;
12
18
  /** Stream for errors. */
@@ -74,7 +80,7 @@ export declare function execute<Name extends keyof CommandResults>(name: Name, a
74
80
  * Run the CLI.
75
81
  *
76
82
  * @param args CLI arguments without the node and script paths.
77
- * @param io Output streams and working directory.
83
+ * @param io Input and output streams and working directory.
78
84
  * @return Exit code.
79
85
  */
80
86
  export declare function run(args: string[], io?: CliIo): Promise<number>;