versionary 0.31.0 → 1.0.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.
@@ -4,34 +4,33 @@ const artifactRuleSchema = z
4
4
  type: z.enum(["json", "toml", "yaml", "nix", "regex"]),
5
5
  path: z.string().min(1),
6
6
  "field-path": z.string().optional(),
7
- jsonpath: z.string().optional(),
8
7
  pattern: z.string().optional(),
8
+ replacement: z.string().optional(),
9
9
  })
10
10
  .superRefine((value, ctx) => {
11
- const needsJsonPath = value.type === "json" ||
11
+ const needsFieldPath = value.type === "json" ||
12
12
  value.type === "toml" ||
13
13
  value.type === "yaml" ||
14
14
  value.type === "nix";
15
- const hasFieldPath = Boolean(value["field-path"] ?? value.jsonpath);
16
- if (needsJsonPath && !hasFieldPath) {
15
+ if (needsFieldPath && !value["field-path"]) {
17
16
  ctx.addIssue({
18
17
  code: z.ZodIssueCode.custom,
19
- message: `${value.type} artifact rules require "field-path" (or deprecated "jsonpath").`,
18
+ message: `${value.type} artifact rules require "field-path".`,
20
19
  path: ["field-path"],
21
20
  });
22
21
  }
23
- if (needsJsonPath && value.pattern) {
22
+ if (needsFieldPath && value.pattern) {
24
23
  ctx.addIssue({
25
24
  code: z.ZodIssueCode.custom,
26
25
  message: `${value.type} artifact rules do not support "pattern".`,
27
26
  path: ["pattern"],
28
27
  });
29
28
  }
30
- if (value["field-path"] && value.jsonpath) {
29
+ if (needsFieldPath && value.replacement) {
31
30
  ctx.addIssue({
32
31
  code: z.ZodIssueCode.custom,
33
- message: 'Specify only one of "field-path" or deprecated "jsonpath".',
34
- path: ["field-path"],
32
+ message: `${value.type} artifact rules do not support "replacement".`,
33
+ path: ["replacement"],
35
34
  });
36
35
  }
37
36
  if (value.type === "regex" && !value.pattern) {
@@ -41,10 +40,10 @@ const artifactRuleSchema = z
41
40
  path: ["pattern"],
42
41
  });
43
42
  }
44
- if (value.type === "regex" && (value["field-path"] || value.jsonpath)) {
43
+ if (value.type === "regex" && value["field-path"]) {
45
44
  ctx.addIssue({
46
45
  code: z.ZodIssueCode.custom,
47
- message: 'regex artifact rules do not support "field-path" or deprecated "jsonpath".',
46
+ message: 'regex artifact rules do not support "field-path".',
48
47
  path: ["field-path"],
49
48
  });
50
49
  }
@@ -67,7 +66,7 @@ export const configSchema = z
67
66
  .object({
68
67
  $schema: z.string().optional(),
69
68
  version: z.literal(1),
70
- "review-mode": z.enum(["direct", "pr", "review"]).optional(),
69
+ "review-mode": z.enum(["direct", "pr"]).optional(),
71
70
  "version-file": z.string().optional(),
72
71
  "changelog-file": z.string().optional(),
73
72
  "changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
@@ -77,7 +76,6 @@ export const configSchema = z
77
76
  .optional(),
78
77
  "release-branch": z.string().optional(),
79
78
  "baseline-file": z.string().optional(),
80
- "next-release-file": z.string().optional(),
81
79
  "bootstrap-sha": z.string().optional(),
82
80
  "monorepo-mode": z.enum(["independent", "fixed"]).optional(),
83
81
  "bump-minor-pre-major": z.boolean().optional(),
@@ -1,3 +1,3 @@
1
1
  import type { VersionaryConfig } from "../types/config.js";
2
- import type { SimplePlan } from "./plan.js";
3
- export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: SimplePlan): string[];
2
+ import type { ReleasePlan } from "./plan.js";
3
+ export declare function applyConfiguredArtifactRules(cwd: string, config: VersionaryConfig, plan: ReleasePlan): string[];
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
4
4
  import YAML from "yaml";
5
+ import { parseVersion } from "./semver.js";
5
6
  const WILDCARD = Symbol("wildcard");
6
7
  function isRecord(value) {
7
8
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -142,9 +143,9 @@ function setVersionAtJsonPath(document, fieldPath, version) {
142
143
  }
143
144
  }
144
145
  function resolveFieldPath(rule) {
145
- const fieldPath = rule["field-path"] ?? rule.jsonpath;
146
+ const fieldPath = rule["field-path"];
146
147
  if (!fieldPath) {
147
- throw new Error(`${rule.type} artifact rules require "field-path" (or deprecated "jsonpath").`);
148
+ throw new Error(`${rule.type} artifact rules require "field-path".`);
148
149
  }
149
150
  return fieldPath;
150
151
  }
@@ -160,12 +161,34 @@ function parseRegexPattern(pattern) {
160
161
  }
161
162
  return new RegExp(pattern, "m");
162
163
  }
163
- function applyRegexRule(content, pattern, version) {
164
+ const REPLACEMENT_TOKEN_PATTERN = /\{\{\s*([A-Za-z]+)\s*\}\}/gu;
165
+ function renderReplacementTemplate(template, version) {
166
+ const parsed = parseVersion(version);
167
+ const tokens = {
168
+ version,
169
+ major: String(parsed.major),
170
+ minor: String(parsed.minor),
171
+ patch: String(parsed.patch),
172
+ prerelease: parsed.prerelease.join("."),
173
+ build: parsed.build.join("."),
174
+ };
175
+ return template.replace(REPLACEMENT_TOKEN_PATTERN, (_whole, name) => {
176
+ const key = name.toLowerCase();
177
+ const value = tokens[key];
178
+ if (value === undefined) {
179
+ throw new Error(`Unknown replacement token "{{${name}}}". Supported tokens: ${Object.keys(tokens)
180
+ .map((token) => `{{${token}}}`)
181
+ .join(", ")}.`);
182
+ }
183
+ return value;
184
+ });
185
+ }
186
+ function applyRegexRule(content, pattern, version, replacementTemplate) {
164
187
  const regex = parseRegexPattern(pattern);
165
188
  const matchFlags = regex.flags.includes("g")
166
189
  ? regex.flags
167
190
  : `${regex.flags}g`;
168
- const globalRegex = new RegExp(regex.source, matchFlags);
191
+ const globalRegex = new RegExp(regex.source, matchFlags.includes("d") ? matchFlags : `${matchFlags}d`);
169
192
  const matches = [...content.matchAll(globalRegex)];
170
193
  if (matches.length !== 1) {
171
194
  throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
@@ -179,9 +202,21 @@ function applyRegexRule(content, pattern, version) {
179
202
  throw new Error("Regex match did not include an index.");
180
203
  }
181
204
  const full = match[0];
182
- const groupOne = match[1];
183
- const replacement = typeof groupOne === "string" ? full.replace(groupOne, version) : version;
184
- return `${content.slice(0, start)}${replacement}${content.slice(start + full.length)}`;
205
+ // With a replacement template, render it and replace the entire match.
206
+ if (replacementTemplate !== undefined) {
207
+ const rendered = renderReplacementTemplate(replacementTemplate, version);
208
+ return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`;
209
+ }
210
+ // Without a template: substitute the full version into the first capture
211
+ // group, leaving the rest of the match intact. Splice by group indices rather than
212
+ // `String.replace` so literal `$` sequences and repeated group content are
213
+ // handled correctly.
214
+ const groupIndices = match.indices?.[1];
215
+ if (!groupIndices) {
216
+ return `${content.slice(0, start)}${version}${content.slice(start + full.length)}`;
217
+ }
218
+ const [groupStart, groupEnd] = groupIndices;
219
+ return `${content.slice(0, groupStart)}${version}${content.slice(groupEnd)}`;
185
220
  }
186
221
  function applyTomlRulePreservingFormatting(content, fieldPath, version) {
187
222
  const simplePath = fieldPath.match(/^\$\.([A-Za-z0-9_-]+)$/u);
@@ -368,7 +403,7 @@ function applyArtifactRuleToContent(content, rule, version) {
368
403
  if (!rule.pattern) {
369
404
  throw new Error('regex artifact rules require "pattern".');
370
405
  }
371
- return applyRegexRule(content, rule.pattern, version);
406
+ return applyRegexRule(content, rule.pattern, version, rule.replacement);
372
407
  }
373
408
  if (rule.type === "json") {
374
409
  const parsed = JSON.parse(content);
@@ -1,6 +1,6 @@
1
1
  import { type ParsedCommit } from "../git/commits.js";
2
2
  import type { VersionaryChangelogFormat } from "../types/config.js";
3
- import { type ReleasePlan, type SimplePlan } from "./plan.js";
3
+ import { type ReleasePlan } from "./plan.js";
4
4
  export declare function renderReleaseNotesSection(input: {
5
5
  currentVersion: string;
6
6
  nextVersion: string;
@@ -23,8 +23,6 @@ export declare function renderReleasePlanChangelog(plan: ReleasePlan, options?:
23
23
  cwd?: string;
24
24
  highlights?: string;
25
25
  }): string;
26
- /** @deprecated Use renderReleasePlanChangelog. */
27
- export declare function renderSimpleChangelog(plan: SimplePlan): string;
28
26
  /**
29
27
  * Locate a manual-notes block at the top of an existing changelog and split it
30
28
  * out. The notes block is the first release-level heading whose text is not a
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { applyRevertSuppression, inferReleaseTypeFromParsedCommit, parseConventionalCommitMessage, } from "../git/commits.js";
4
4
  import { resolveRepositoryWebBaseUrl } from "../git/repo-url.js";
5
- import { resolvePackageDependencies, } from "./plan.js";
5
+ import { resolvePackageDependencies } from "./plan.js";
6
6
  import { isVersionHeading } from "./semver.js";
7
7
  const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
8
8
  function formatDate() {
@@ -68,6 +68,7 @@ function groupCommitLines(commits, repoUrl) {
68
68
  const fixes = [];
69
69
  const performance = [];
70
70
  const reverts = [];
71
+ const other = [];
71
72
  const effectiveCommits = applyRevertSuppression(commits);
72
73
  const getRevertedSubject = (commit) => {
73
74
  const normalizedDescription = (commit.description ?? "")
@@ -93,9 +94,6 @@ function groupCommitLines(commits, repoUrl) {
93
94
  };
94
95
  for (const commit of effectiveCommits) {
95
96
  const type = inferReleaseTypeFromParsedCommit(commit);
96
- if (!type) {
97
- continue;
98
- }
99
97
  const { label, message } = formatCommitMessage(commit.subject);
100
98
  const short = commit.hash.slice(0, 7);
101
99
  const hashLabel = repoUrl
@@ -106,6 +104,10 @@ function groupCommitLines(commits, repoUrl) {
106
104
  const line = `- ${label}${message} (${hashLabel})${referencesSuffix}`;
107
105
  const commitType = (commit.type ?? "").toLowerCase();
108
106
  const isBreaking = type === "major";
107
+ if (!type) {
108
+ other.push(line);
109
+ continue;
110
+ }
109
111
  if (commit.isRevert) {
110
112
  if (!shouldIncludeRevert(commit)) {
111
113
  continue;
@@ -128,7 +130,23 @@ function groupCommitLines(commits, repoUrl) {
128
130
  fixes.push(line);
129
131
  }
130
132
  }
131
- return { breaking, features, fixes, performance, reverts };
133
+ return { breaking, features, fixes, performance, reverts, other };
134
+ }
135
+ /**
136
+ * A forced bump — a stale dependency, or a propagated requirement rewrite —
137
+ * can carry no release-worthy commits at all. Rather than publish a bare
138
+ * version heading, such a release falls back to listing whatever else is
139
+ * shipping under it.
140
+ */
141
+ function needsOtherChangesFallback(grouped, hasHighlights, dependencyCount) {
142
+ if (hasHighlights || dependencyCount > 0 || grouped.other.length === 0) {
143
+ return false;
144
+ }
145
+ return (grouped.breaking.length === 0 &&
146
+ grouped.features.length === 0 &&
147
+ grouped.fixes.length === 0 &&
148
+ grouped.performance.length === 0 &&
149
+ grouped.reverts.length === 0);
132
150
  }
133
151
  export function renderReleaseNotesSection(input, options = {}) {
134
152
  const repoUrl = resolveRepositoryWebBaseUrl(input.cwd ?? process.cwd());
@@ -161,6 +179,9 @@ export function renderReleaseNotesSection(input, options = {}) {
161
179
  if (input.dependencies && input.dependencies.length > 0) {
162
180
  sections.push("### Dependencies", ...input.dependencies.map((dependency) => `- updated ${dependency.name} to v${dependency.version}`), "");
163
181
  }
182
+ if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, input.dependencies?.length ?? 0)) {
183
+ sections.push("### Other changes", ...grouped.other, "");
184
+ }
164
185
  const body = [header, "", ...sections].join("\n").trimEnd();
165
186
  if (options.includeFooter) {
166
187
  return `${body}\n\n${renderReviewRequestFooter()}`;
@@ -198,10 +219,6 @@ export function renderReleasePlanChangelog(plan, options = {}) {
198
219
  includeFooter: options.includeFooter,
199
220
  });
200
221
  }
201
- /** @deprecated Use renderReleasePlanChangelog. */
202
- export function renderSimpleChangelog(plan) {
203
- return renderReleasePlanChangelog(plan);
204
- }
205
222
  /**
206
223
  * Locate a manual-notes block at the top of an existing changelog and split it
207
224
  * out. The notes block is the first release-level heading whose text is not a
@@ -292,6 +309,9 @@ export function renderRNewsReleaseNotes(input) {
292
309
  if (grouped.reverts.length > 0) {
293
310
  sections.push("## Reverts", "", ...grouped.reverts, "");
294
311
  }
312
+ if (needsOtherChangesFallback(grouped, trimmedHighlights.length > 0, 0)) {
313
+ sections.push("## Other changes", "", ...grouped.other, "");
314
+ }
295
315
  return [`# ${input.packageName} ${normalizedVersion}`, "", ...sections]
296
316
  .join("\n")
297
317
  .trimEnd();
@@ -1,6 +1,7 @@
1
1
  import { type ParsedCommit } from "../git/commits.js";
2
2
  import type { VersionaryChangelogFormat, VersionaryConfig } from "../types/config.js";
3
3
  import { type ReleaseType } from "./semver.js";
4
+ type BumpReason = "direct" | "dependency-propagation" | "stale-dependency" | "follows" | "release-as";
4
5
  export interface ReleasePlan {
5
6
  mode: "simple";
6
7
  releaseType: ReleaseType;
@@ -18,13 +19,11 @@ export interface ReleasePlan {
18
19
  releaseType: ReleaseType;
19
20
  currentVersion: string;
20
21
  nextVersion: string | null;
21
- bumpReason?: "direct" | "dependency-propagation" | "follows";
22
+ bumpReason?: BumpReason;
22
23
  dependencySourcePaths?: string[];
23
24
  commits: ParsedCommit[];
24
25
  }>;
25
26
  }
26
- /** @deprecated Use ReleasePlan. */
27
- export type SimplePlan = ReleasePlan;
28
27
  export declare function getChangelogDefaults(config: {
29
28
  "release-type"?: VersionaryConfig["release-type"];
30
29
  "changelog-file"?: VersionaryConfig["changelog-file"];
@@ -35,9 +34,8 @@ export declare function getChangelogDefaults(config: {
35
34
  changelogFormat: VersionaryChangelogFormat;
36
35
  };
37
36
  export declare function createReleasePlan(cwd?: string): ReleasePlan;
38
- /** @deprecated Use createReleasePlan. */
39
- export declare function createSimplePlan(cwd?: string): ReleasePlan;
40
37
  export declare function resolvePackageDependencies(plan: ReleasePlan, packagePath: string): Array<{
41
38
  name: string;
42
39
  version: string;
43
40
  }>;
41
+ export {};