@wpelevator/wp-release 0.12.0 → 0.13.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 (49) hide show
  1. package/README.md +90 -6
  2. package/dist/changeset-config.d.ts +86 -2
  3. package/dist/changeset-config.js +101 -6
  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 +45 -8
  9. package/dist/changeset-packages.js +73 -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 +38 -3
  13. package/dist/changeset-release.js +88 -11
  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 +18 -5
  18. package/dist/commands/changeset-add.d.ts +5 -2
  19. package/dist/commands/changeset-add.js +108 -9
  20. package/dist/commands/changeset-args.d.ts +24 -5
  21. package/dist/commands/changeset-args.js +57 -14
  22. package/dist/commands/changeset-status.d.ts +2 -0
  23. package/dist/commands/changeset-tag.js +2 -1
  24. package/dist/commands/changeset-version.d.ts +6 -0
  25. package/dist/commands/changeset-version.js +15 -3
  26. package/dist/commands/command.d.ts +3 -0
  27. package/dist/commands/deploy-git.d.ts +24 -4
  28. package/dist/commands/deploy-git.js +56 -3
  29. package/dist/commands/deploy-svn.d.ts +24 -4
  30. package/dist/commands/deploy-svn.js +56 -2
  31. package/dist/commands/package-deploys.d.ts +78 -0
  32. package/dist/commands/package-deploys.js +108 -0
  33. package/dist/git-deploy.d.ts +32 -0
  34. package/dist/git-deploy.js +57 -1
  35. package/dist/index.d.ts +5 -2
  36. package/dist/index.js +2 -1
  37. package/dist/package-deploy.d.ts +105 -0
  38. package/dist/package-deploy.js +288 -0
  39. package/dist/release-repository.d.ts +14 -2
  40. package/dist/release-repository.js +46 -7
  41. package/dist/release-tags.d.ts +3 -1
  42. package/dist/release-tags.js +5 -2
  43. package/dist/tag-format.d.ts +3 -3
  44. package/dist/tag-format.js +1 -1
  45. package/dist/version-files.d.ts +2 -1
  46. package/dist/version-files.js +12 -7
  47. package/dist/versions.d.ts +28 -3
  48. package/dist/versions.js +84 -9
  49. package/package.json +1 -1
@@ -28,17 +28,19 @@ export async function readChangesets(directory) {
28
28
  /**
29
29
  * Pick the changesets that were added or changed since a ref.
30
30
  *
31
- * @param repository Repository of the changesets.
32
- * @param directory Changeset directory.
33
- * @param changesets Pending changesets.
34
- * @param since Base ref, like origin/main.
31
+ * @param repository Repository of the changesets.
32
+ * @param directory Changeset directory.
33
+ * @param changesets Pending changesets.
34
+ * @param since Base ref, like origin/main.
35
+ * @param uncommitted Whether to include uncommitted and untracked changesets.
35
36
  * @return Changesets of the diff.
36
37
  */
37
- export async function readChangesetsSince(repository, directory, changesets, since) {
38
+ export async function readChangesetsSince(repository, directory, changesets, since, uncommitted = false) {
38
39
  const relativeDirectory = repository.relativePath(directory);
39
40
  const changedFiles = new Set(await repository.getChangedFiles(since, {
40
41
  directory,
41
42
  diffFilter: 'ACMR',
43
+ uncommitted,
42
44
  }));
43
45
  return changesets.filter((changeset) => changedFiles.has(path.posix.join(relativeDirectory, changeset.fileName)));
44
46
  }
@@ -1,4 +1,4 @@
1
- import type { ChangesetConfig, ChangesetPackageGroup } from './changeset-config.ts';
1
+ import type { ChangesetConfig, ChangesetPackageGroup, DeployPackage, PackageVersionConfig } from './changeset-config.ts';
2
2
  import type { ChangesetPackage } from './changesets.ts';
3
3
  import type { ReleaseRepository } from './release-repository.ts';
4
4
  /**
@@ -6,7 +6,14 @@ import type { ReleaseRepository } from './release-repository.ts';
6
6
  */
7
7
  export interface ConfiguredPackage extends ChangesetPackage {
8
8
  group: ChangesetPackageGroup;
9
- versionConfig: ChangesetPackageGroup['version'];
9
+ versionConfig: PackageVersionConfig;
10
+ }
11
+ /**
12
+ * A discovered package of a group with deploy targets, which may have no
13
+ * version configuration.
14
+ */
15
+ export interface ConfiguredDeployPackage extends DeployPackage {
16
+ group: ChangesetPackageGroup;
10
17
  }
11
18
  export interface ChangedPackages {
12
19
  /** Packages with changed files, in the configured order. */
@@ -16,7 +23,8 @@ export interface ChangedPackages {
16
23
  }
17
24
  /**
18
25
  * Find the packages of the configured package groups, with their current
19
- * versions.
26
+ * versions. Groups without a version configuration aren't released with
27
+ * changesets, so they're left out.
20
28
  *
21
29
  * @param repository Repository the packages are in.
22
30
  * @param config Repository release configuration.
@@ -24,6 +32,16 @@ export interface ChangedPackages {
24
32
  * @throws {ValidationError} For a glob without matches or a duplicate package name.
25
33
  */
26
34
  export declare function discoverPackages(repository: ReleaseRepository, config: ChangesetConfig): Promise<ConfiguredPackage[]>;
35
+ /**
36
+ * Find the packages of the package groups with deploy targets, including the
37
+ * groups without a version configuration, without reading their versions.
38
+ *
39
+ * @param repository Repository the packages are in.
40
+ * @param config Repository release configuration.
41
+ * @return Packages in the configured order.
42
+ * @throws {ValidationError} For a glob without matches or a duplicate package name.
43
+ */
44
+ export declare function discoverDeployPackages(repository: ReleaseRepository, config: ChangesetConfig): Promise<ConfiguredDeployPackage[]>;
27
45
  /**
28
46
  * Fail for package names that aren't configured.
29
47
  *
@@ -33,15 +51,34 @@ export declare function discoverPackages(repository: ReleaseRepository, config:
33
51
  */
34
52
  export declare function assertKnownPackages(packages: ChangesetPackage[], names: string[]): void;
35
53
  /**
36
- * Find the packages with files changed between the merge base of a ref and
37
- * HEAD.
54
+ * Find a package by its identifier, or by its path under the configured root,
55
+ * so that contributors can name a package by the directory they changed.
38
56
  *
39
- * @param repository Repository the packages are in.
40
57
  * @param packages Discovered packages.
41
- * @param since Base ref, like origin/main.
58
+ * @param identifier Package identifier or path, like plugins/alpha or plugins/alpha/.
59
+ * @return The package, or undefined when none matches.
60
+ */
61
+ export declare function findPackage<Package extends Pick<ChangesetPackage, 'name' | 'path'>>(packages: Package[], identifier: string): Package | undefined;
62
+ /**
63
+ * Resolve package identifiers or paths to the package identifiers.
64
+ *
65
+ * @param packages Discovered packages.
66
+ * @param identifiers Package identifiers or paths under the configured root.
67
+ * @return Package identifiers, in the given order.
68
+ * @throws {ValidationError} For an unknown package.
69
+ */
70
+ export declare function resolvePackageNames(packages: ChangesetPackage[], identifiers: string[]): string[];
71
+ /**
72
+ * Find the packages with files changed between the merge base of a ref and
73
+ * HEAD, or the working tree.
74
+ *
75
+ * @param repository Repository the packages are in.
76
+ * @param packages Discovered packages.
77
+ * @param since Base ref, like origin/main.
78
+ * @param uncommitted Whether to include uncommitted and untracked files.
42
79
  * @return Changed packages and their changed files.
43
80
  */
44
- export declare function getChangedPackages(repository: ReleaseRepository, packages: ConfiguredPackage[], since: string): Promise<ChangedPackages>;
81
+ export declare function getChangedPackages(repository: ReleaseRepository, packages: ConfiguredPackage[], since: string, uncommitted?: boolean): Promise<ChangedPackages>;
45
82
  /**
46
83
  * Remove the release configuration from a package passed to config callbacks.
47
84
  *
@@ -7,7 +7,8 @@ import { readVersion } from "./version-files.js";
7
7
  const SKIPPED_DIRECTORIES = ['.git', 'node_modules', 'vendor', 'dist'];
8
8
  /**
9
9
  * Find the packages of the configured package groups, with their current
10
- * versions.
10
+ * versions. Groups without a version configuration aren't released with
11
+ * changesets, so they're left out.
11
12
  *
12
13
  * @param repository Repository the packages are in.
13
14
  * @param config Repository release configuration.
@@ -15,13 +16,38 @@ const SKIPPED_DIRECTORIES = ['.git', 'node_modules', 'vendor', 'dist'];
15
16
  * @throws {ValidationError} For a glob without matches or a duplicate package name.
16
17
  */
17
18
  export async function discoverPackages(repository, config) {
19
+ const found = await findGroupPackages(repository, config, config.packages.filter((group) => group.version));
20
+ const packages = [];
21
+ for (const pkg of found) {
22
+ const versionConfig = pkg.group.version;
23
+ packages.push({
24
+ ...pkg,
25
+ currentVersion: await readVersion(repository, pkg.directory, versionConfig.from, pkg.path, pkg.group.tagType),
26
+ versionConfig,
27
+ });
28
+ }
29
+ return packages;
30
+ }
31
+ /**
32
+ * Find the packages of the package groups with deploy targets, including the
33
+ * groups without a version configuration, without reading their versions.
34
+ *
35
+ * @param repository Repository the packages are in.
36
+ * @param config Repository release configuration.
37
+ * @return Packages in the configured order.
38
+ * @throws {ValidationError} For a glob without matches or a duplicate package name.
39
+ */
40
+ export async function discoverDeployPackages(repository, config) {
41
+ return findGroupPackages(repository, config, config.packages.filter((group) => group.deploy));
42
+ }
43
+ async function findGroupPackages(repository, config, groups) {
18
44
  const root = path.resolve(repository.cwd, config.root ?? '.');
19
45
  const packages = [];
20
- const hasRecursiveGlob = config.packages.some((item) => item.glob.split('/').includes('**'));
46
+ const hasRecursiveGlob = groups.some((item) => item.glob.split('/').includes('**'));
21
47
  const directories = await findDirectories(root, '', hasRecursiveGlob
22
48
  ? Infinity
23
- : Math.max(1, ...config.packages.map((item) => item.glob.split('/').length + 2)));
24
- for (const group of config.packages) {
49
+ : Math.max(1, ...groups.map((item) => item.glob.split('/').length + 2)));
50
+ for (const group of groups) {
25
51
  const matcher = globRegExp(group.glob);
26
52
  const candidates = matcher.test('.')
27
53
  ? ['.', ...directories]
@@ -36,7 +62,6 @@ export async function discoverPackages(repository, config) {
36
62
  for (const relativePath of matches) {
37
63
  const directory = path.join(root, relativePath);
38
64
  const name = await packageName(directory, relativePath, group.idFrom ?? 'path');
39
- const currentVersion = await readVersion(repository, directory, group.version.from, relativePath, group.tagType);
40
65
  if (packages.some((pkg) => pkg.name === name)) {
41
66
  throw new ValidationError(`Package identifier "${name}" is configured more than once.`);
42
67
  }
@@ -45,9 +70,7 @@ export async function discoverPackages(repository, config) {
45
70
  directory,
46
71
  path: relativePath,
47
72
  type: packageType(group),
48
- currentVersion,
49
73
  group,
50
- versionConfig: group.version,
51
74
  });
52
75
  }
53
76
  }
@@ -68,16 +91,52 @@ export function assertKnownPackages(packages, names) {
68
91
  });
69
92
  }
70
93
  /**
71
- * Find the packages with files changed between the merge base of a ref and
72
- * HEAD.
94
+ * Find a package by its identifier, or by its path under the configured root,
95
+ * so that contributors can name a package by the directory they changed.
73
96
  *
74
- * @param repository Repository the packages are in.
75
97
  * @param packages Discovered packages.
76
- * @param since Base ref, like origin/main.
98
+ * @param identifier Package identifier or path, like plugins/alpha or plugins/alpha/.
99
+ * @return The package, or undefined when none matches.
100
+ */
101
+ export function findPackage(packages, identifier) {
102
+ const packagePath = identifier
103
+ .replaceAll('\\', '/')
104
+ .replace(/^\.\//, '')
105
+ .replace(/\/+$/, '');
106
+ return (packages.find((pkg) => pkg.name === identifier) ??
107
+ packages.find((pkg) => pkg.path === packagePath));
108
+ }
109
+ /**
110
+ * Resolve package identifiers or paths to the package identifiers.
111
+ *
112
+ * @param packages Discovered packages.
113
+ * @param identifiers Package identifiers or paths under the configured root.
114
+ * @return Package identifiers, in the given order.
115
+ * @throws {ValidationError} For an unknown package.
116
+ */
117
+ export function resolvePackageNames(packages, identifiers) {
118
+ return identifiers.map((identifier) => {
119
+ const pkg = findPackage(packages, identifier);
120
+ if (!pkg) {
121
+ throw new ValidationError(`Unknown package "${identifier}".`);
122
+ }
123
+ return pkg.name;
124
+ });
125
+ }
126
+ /**
127
+ * Find the packages with files changed between the merge base of a ref and
128
+ * HEAD, or the working tree.
129
+ *
130
+ * @param repository Repository the packages are in.
131
+ * @param packages Discovered packages.
132
+ * @param since Base ref, like origin/main.
133
+ * @param uncommitted Whether to include uncommitted and untracked files.
77
134
  * @return Changed packages and their changed files.
78
135
  */
79
- export async function getChangedPackages(repository, packages, since) {
80
- const changedFiles = await repository.getChangedFiles(since);
136
+ export async function getChangedPackages(repository, packages, since, uncommitted = false) {
137
+ const changedFiles = await repository.getChangedFiles(since, {
138
+ uncommitted,
139
+ });
81
140
  const filesByPackage = {};
82
141
  const changed = packages.filter((pkg) => {
83
142
  const packageFromRepo = repository.relativePath(pkg.directory);
@@ -172,7 +231,7 @@ async function packageName(directory, relativePath, idFrom) {
172
231
  return name;
173
232
  }
174
233
  function packageType(group) {
175
- const kind = group.version.from.kind;
234
+ const kind = group.version?.from.kind;
176
235
  if ('wordpress-plugin-header' === kind) {
177
236
  return 'plugin';
178
237
  }
@@ -0,0 +1,119 @@
1
+ import type { ChangesetPackage, ChangesetPackages } from './changesets.ts';
2
+ import type { OutputStream } from './logger.ts';
3
+ /** Terminal that the prompts read answers from and write questions to. */
4
+ export interface PromptTerminal {
5
+ input: NodeJS.ReadableStream;
6
+ output: OutputStream;
7
+ }
8
+ /** Packages to choose from, and the changed ones that are preselected. */
9
+ export interface ChangesetPromptInput {
10
+ /** Configured packages. */
11
+ packages: ChangesetPackage[];
12
+ /** Identifiers of the changed packages without a changeset. */
13
+ changed: string[];
14
+ /** Base ref that the changed packages were compared with. */
15
+ since?: string;
16
+ /** Release note to use instead of asking for one. */
17
+ summary?: string;
18
+ }
19
+ /** Answers of the prompts: no packages for an empty changeset. */
20
+ export interface ChangesetPromptAnswers {
21
+ packages: ChangesetPackages;
22
+ summary: string;
23
+ }
24
+ /**
25
+ * Ask for the packages, bumps and release note of a changeset, line by line,
26
+ * so that it works in any terminal and with piped input.
27
+ */
28
+ export declare class ChangesetPrompt {
29
+ readonly terminal: PromptTerminal;
30
+ private lines;
31
+ private readline;
32
+ /**
33
+ * @param terminal Input to read answers from and output for the questions.
34
+ */
35
+ constructor(terminal: PromptTerminal);
36
+ /**
37
+ * Ask for the changeset, preselecting the changed packages.
38
+ *
39
+ * @param input Packages, changed packages and an optional release note.
40
+ * @return Package bumps, empty for no release, and the release note.
41
+ * @throws {ValidationError} If the input ends before the changeset is complete.
42
+ */
43
+ ask(input: ChangesetPromptInput): Promise<ChangesetPromptAnswers>;
44
+ /**
45
+ * Stop reading the input, so that the process can exit.
46
+ */
47
+ close(): void;
48
+ /**
49
+ * Print the numbered packages, marking the changed ones.
50
+ *
51
+ * @param input Packages and changed packages.
52
+ */
53
+ private listPackages;
54
+ /**
55
+ * Get the package question, with the changed packages as the default.
56
+ *
57
+ * @param input Packages and changed packages.
58
+ * @return Question text.
59
+ */
60
+ private getPackagesQuestion;
61
+ /**
62
+ * Parse the chosen packages.
63
+ *
64
+ * @param answer Comma or space separated numbers, identifiers or paths.
65
+ * @param input Packages and changed packages.
66
+ * @return Package identifiers, empty for no release.
67
+ * @throws {ValidationError} For unknown, repeated or missing packages.
68
+ */
69
+ private parsePackages;
70
+ /**
71
+ * Parse one chosen package.
72
+ *
73
+ * @param token Package number, identifier or path.
74
+ * @param packages Configured packages.
75
+ * @return Package identifier.
76
+ * @throws {ValidationError} For an unknown package.
77
+ */
78
+ private parsePackage;
79
+ /**
80
+ * Parse a bump, patch by default.
81
+ *
82
+ * @param answer Bump name.
83
+ * @return Bump.
84
+ * @throws {ValidationError} For an unknown bump.
85
+ */
86
+ private parseBump;
87
+ /**
88
+ * Parse a release note.
89
+ *
90
+ * @param answer Release note.
91
+ * @return Trimmed release note.
92
+ * @throws {ValidationError} For an empty release note.
93
+ */
94
+ private parseSummary;
95
+ /**
96
+ * Ask a question until the answer parses, printing why it didn't.
97
+ *
98
+ * @param question Question text.
99
+ * @param parse Parser that throws a ValidationError for invalid answers.
100
+ * @return Parsed answer.
101
+ */
102
+ private askUntilValid;
103
+ /**
104
+ * Write a question and read the next line of the input.
105
+ *
106
+ * @param question Question text.
107
+ * @return Answer line.
108
+ * @throws {ValidationError} If the input ended.
109
+ */
110
+ private readAnswer;
111
+ /**
112
+ * Get the lines of the input, reading it from the first question on. The
113
+ * interface stays in line mode, so the terminal echoes and edits the
114
+ * answers and Ctrl+C ends the process.
115
+ *
116
+ * @return Line iterator.
117
+ */
118
+ private getLines;
119
+ }
@@ -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
+ }
@@ -10,7 +10,7 @@ export interface ChangesetReleaseOptions extends ChangesetRepository {
10
10
  git?: GitRunner;
11
11
  }
12
12
  export interface ChangesetAddOptions {
13
- /** Bump of each package. */
13
+ /** Bump of each package, by package identifier or path under the configured root. */
14
14
  packages: Record<string, ChangesetBump>;
15
15
  /** Changelog summary. */
16
16
  summary: string;
@@ -20,10 +20,22 @@ export interface ChangesetAddOptions {
20
20
  export interface ChangesetVersionOptions {
21
21
  dryRun?: boolean;
22
22
  date?: string;
23
- /** Version only these packages, leaving the other changesets pending. */
23
+ /** Version only these packages, by identifier or path under the configured root, leaving the other changesets pending. */
24
24
  packages?: string[];
25
25
  /** Whether the selected sources release their configured dependents; defaults to true. */
26
26
  dependents?: boolean;
27
+ /**
28
+ * Release pre-releases of the next versions, with this identifier and an
29
+ * optional number, like rc or rc.1, numbered from the package tags without one.
30
+ */
31
+ prerelease?: string;
32
+ /**
33
+ * Release snapshots of the next versions, like 1.3.0-pr-251.20261012183000,
34
+ * with this identifier and the UTC time, for test builds that aren't committed.
35
+ */
36
+ snapshot?: string;
37
+ /** Time of a snapshot, now by default. */
38
+ snapshotTime?: Date;
27
39
  }
28
40
  export type ChangesetVersionInput = ChangesetRepository & ChangesetVersionOptions;
29
41
  /** Versioned releases, the applied changesets and whether files were changed. */
@@ -38,7 +50,7 @@ export interface ChangesetTagOptions {
38
50
  untagged?: boolean;
39
51
  push?: boolean;
40
52
  since?: string;
41
- /** Tag only these packages. */
53
+ /** Tag only these packages, by identifier or path under the configured root. */
42
54
  packages?: string[];
43
55
  /** Expected version of the single package in `packages`. */
44
56
  version?: string;
@@ -61,8 +73,12 @@ export interface ChangesetTagResult {
61
73
  export interface ChangesetStatusOptions {
62
74
  /** Base ref to diff against, origin/<baseBranch> by default. */
63
75
  since?: string;
76
+ /** Whether to include uncommitted and untracked files, like before adding a changeset. */
77
+ uncommitted?: boolean;
64
78
  }
65
79
  export interface ChangesetStatusResult {
80
+ /** Base ref that the changed files were compared with, if any. */
81
+ since?: string;
66
82
  changesets: Changeset[];
67
83
  releases: PackageRelease[];
68
84
  changedPackages: string[];
@@ -135,6 +151,25 @@ export declare class ChangesetRelease {
135
151
  * @throws {ValidationError} Without a config.
136
152
  */
137
153
  getStatus(options?: ChangesetStatusOptions): Promise<ChangesetStatusResult>;
154
+ /**
155
+ * Turn the next versions of releases into pre-releases, like 1.3.0-rc.1
156
+ * for 1.3.0, numbered from the existing tags of each package. Dependents
157
+ * become pre-releases too, so they bundle the pre-releases of their sources.
158
+ *
159
+ * @param context Release context.
160
+ * @param releases Releases with their stable next versions.
161
+ * @param prerelease Pre-release identifier with an optional number, like rc or rc.1.
162
+ * @return Releases with their pre-release next versions.
163
+ * @throws {ValidationError} For an invalid pre-release or an existing version.
164
+ */
165
+ private toPrereleases;
166
+ /**
167
+ * List the versions of the tags with a prefix.
168
+ *
169
+ * @param prefix Tag prefix of a package, like `plugins/alpha/`.
170
+ * @return Tagged versions.
171
+ */
172
+ private listTaggedVersions;
138
173
  /**
139
174
  * Write the versions, dependency specs and changelogs of the releases and
140
175
  * consume the applied changesets, restoring every changed file when a