@afoures/auto-release 0.2.10 → 0.2.12

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.
@@ -29,34 +29,24 @@ var ChangeFile = class {
29
29
  };
30
30
  async function save_change_file(change_file, to) {
31
31
  const file_path = join(to, change_file.filename);
32
- await write_file(file_path, change_file.summary);
32
+ const text = change_file.summary.split("\n").map((line) => line.slice(2)).join("\n");
33
+ await write_file(file_path, text);
33
34
  return file_path;
34
35
  }
35
36
  async function parse_change_file(path) {
36
37
  const filename = basename(path);
37
38
  const match = CHANGE_FILENAME_REGEX.exec(filename);
38
39
  if (!match) return /* @__PURE__ */ new Error(`Invalid change filename format: ${path} (expected: <kind>.<slug>.md)`);
39
- const content = await read_file(path);
40
- if (content === null) return /* @__PURE__ */ new Error(`Change file is missing: ${path}`);
41
- const non_empty_lines = content.split("\n").filter((line) => line.trim() !== "");
42
- if (non_empty_lines.length === 0) return /* @__PURE__ */ new Error("Change file is empty");
43
- const first_line = non_empty_lines[0].trim();
44
- if (non_empty_lines.length === 1) return new ChangeFile({
45
- slug: match.groups.slug,
46
- kind: match.groups.kind,
47
- summary: first_line
48
- });
49
- const remaining_lines = non_empty_lines.slice(1);
50
- const invalid_lines = [];
51
- remaining_lines.forEach((line, index) => {
52
- if (!line.trim().match(/^[-*+]\s/)) invalid_lines.push(index + 2);
53
- });
54
- if (invalid_lines.length > 0) return /* @__PURE__ */ new Error(`Change file ${path} has invalid format: lines ${invalid_lines.join(", ")} should be list items (starting with -, *, or +)`);
55
- const summary = [first_line, ...remaining_lines].join("\n");
40
+ let file_content = await read_file(path);
41
+ file_content = file_content?.trim() ?? null;
42
+ if (file_content === null) return /* @__PURE__ */ new Error(`Change file is missing: ${path}`);
43
+ if (!file_content) return /* @__PURE__ */ new Error("Change file is empty");
44
+ const [title, ...rest] = file_content.split("\n");
45
+ const text = [`- ${title}`, ...rest.map((line) => ` ${line}`)].join("\n");
56
46
  return new ChangeFile({
57
47
  slug: match.groups.slug,
58
48
  kind: match.groups.kind,
59
- summary
49
+ summary: text
60
50
  });
61
51
  }
62
52
  async function find_change_files(folder, { allowed_kinds }) {
@@ -66,7 +56,10 @@ async function find_change_files(folder, { allowed_kinds }) {
66
56
  for (const filename of files) {
67
57
  const file_path = join(folder, filename);
68
58
  const change_file_or_error = await parse_change_file(file_path);
69
- if (change_file_or_error instanceof Error) continue;
59
+ if (change_file_or_error instanceof Error) {
60
+ warnings.push(`Change file ${file_path} is invalid: ${change_file_or_error.message}`);
61
+ continue;
62
+ }
70
63
  const change_file = change_file_or_error;
71
64
  if (!allowed_kinds.includes(change_file.kind)) {
72
65
  warnings.push(`Change file ${file_path} has an invalid kind: ${change_file.kind}`);
@@ -72,7 +72,7 @@ const check = create_command({
72
72
  const valid = errors.length === 0;
73
73
  if (valid) logger.success("All validations passed!");
74
74
  else {
75
- logger.error("Validation failed:");
75
+ logger.error("Detected errors:");
76
76
  errors.forEach((err) => logger.error(` ${err}`));
77
77
  }
78
78
  if (valid) return {
@@ -81,7 +81,7 @@ const check = create_command({
81
81
  };
82
82
  else return {
83
83
  status: "error",
84
- error: errors.join("; ")
84
+ error: "Validation failed"
85
85
  };
86
86
  }
87
87
  });
@@ -1,14 +1,12 @@
1
1
  import { create_command } from "../cli.mjs";
2
2
  import { create_logger } from "../utils/logger.mjs";
3
- import { delete_file, read_file, write_file } from "../utils/fs.mjs";
3
+ import { delete_all_files_from_folder, read_file, write_file } from "../utils/fs.mjs";
4
4
  import { find_nearest_config } from "../config.mjs";
5
5
  import { find_change_files } from "../change-file.mjs";
6
6
  import { diff, reset } from "../utils/git.mjs";
7
7
  import { compute_current_version } from "../utils/version.mjs";
8
+ import { parse_markdown } from "../utils/mdast.mjs";
8
9
  import { join } from "node:path";
9
- import { fromMarkdown } from "mdast-util-from-markdown";
10
- import { gfmFromMarkdown } from "mdast-util-gfm";
11
- import { gfm } from "micromark-extension-gfm";
12
10
 
13
11
  //#region src/lib/commands/generate-release-pr.ts
14
12
  const generate_release_pr = create_command({
@@ -47,7 +45,8 @@ const generate_release_pr = create_command({
47
45
  message: "No apps to release"
48
46
  };
49
47
  for (const app of filtered_applications) {
50
- const changes = await find_change_files(join(config.changes_dir, app.name), { allowed_kinds: app.versioning.allowed_changes });
48
+ const changes_dir = join(config.changes_dir, app.name);
49
+ const changes = await find_change_files(changes_dir, { allowed_kinds: app.versioning.allowed_changes });
51
50
  if (changes.warnings.length > 0) for (const warning of changes.warnings) logger.warn(warning);
52
51
  const current_version = await compute_current_version(app, { get_file_content: (file_path) => read_file(file_path) }) ?? app.versioning.initial_version;
53
52
  const next_version = app.versioning.bump({
@@ -66,14 +65,11 @@ const generate_release_pr = create_command({
66
65
  for (const [kind, kind_changes] of changes_by_kind.entries()) {
67
66
  const label = display_map[kind]?.plural ?? display_map[kind]?.singular ?? kind;
68
67
  message_lines.push(`\n${label}:`);
69
- for (const change of kind_changes) message_lines.push(` ${change.summary}`);
68
+ for (const change of kind_changes) message_lines.push(` ${change.summary}`);
70
69
  }
71
70
  logger.note(`Release ${app.name} ${next_version}`, message_lines.join("\n"));
72
71
  if (dry_run) continue;
73
- for (const change of changes.list) {
74
- const change_file_path = join(config.changes_dir, app.name, change.filename);
75
- await delete_file(change_file_path);
76
- }
72
+ await delete_all_files_from_folder(changes_dir);
77
73
  for (const component of app.components) for (const part of component.parts) {
78
74
  const initial_content = await read_file(part.file);
79
75
  if (initial_content === null) continue;
@@ -81,11 +77,9 @@ const generate_release_pr = create_command({
81
77
  await write_file(part.file, updated_content);
82
78
  }
83
79
  const formatter = app.versioning.formatter;
84
- const changelog_as_mdast = fromMarkdown(await read_file(app.changelog) ?? "", {
85
- extensions: [gfm()],
86
- mdastExtensions: [gfmFromMarkdown()]
87
- });
88
- const changelog = formatter.transform_markdown(changelog_as_mdast);
80
+ const initial_changelog_content = await read_file(app.changelog);
81
+ const changelog_as_mdast = parse_markdown(initial_changelog_content ?? "");
82
+ const changelog = formatter.transform_markdown(changelog_as_mdast, initial_changelog_content ?? "");
89
83
  const updated_changelog_content = formatter.format_changelog({
90
84
  ...changelog,
91
85
  releases: [{
@@ -1,15 +1,13 @@
1
1
  import { create_command } from "../cli.mjs";
2
- import { delete_file, read_file, write_file } from "../utils/fs.mjs";
2
+ import { delete_all_files_from_folder, read_file, write_file } from "../utils/fs.mjs";
3
3
  import { find_nearest_config } from "../config.mjs";
4
4
  import { find_change_files } from "../change-file.mjs";
5
5
  import { commit, create_tag, get_current_branch, get_head_and_parent_shas, get_staged_diff, has_uncommitted_changes, reset, stage_files, tag_exists } from "../utils/git.mjs";
6
6
  import { compute_current_version } from "../utils/version.mjs";
7
+ import { parse_markdown } from "../utils/mdast.mjs";
7
8
  import { is_ci } from "../utils/branch-protection.mjs";
8
9
  import { cancel, confirm, intro, isCancel, log, note, outro, select, text } from "@clack/prompts";
9
10
  import { join } from "node:path";
10
- import { fromMarkdown } from "mdast-util-from-markdown";
11
- import { gfmFromMarkdown } from "mdast-util-gfm";
12
- import { gfm } from "micromark-extension-gfm";
13
11
 
14
12
  //#region src/lib/commands/manual-release.ts
15
13
  const manual_release = create_command({
@@ -128,14 +126,14 @@ const manual_release = create_command({
128
126
  log.warn(`Could not check remote tag existence: ${error.message}`);
129
127
  log.warn("Continuing with local tag check only...");
130
128
  }
131
- const changes_summary = changes.list.map((change) => ` ${change.summary.split("\n").join("\n ")}`).join("\n");
129
+ const changes_summary = changes.list.map((change) => ` ${change.summary.split("\n").join("\n ")}`).join("\n");
132
130
  note(`Manual release plan:
133
131
  - App: ${app_name}
134
132
  - Current version: ${current_version}
135
133
  - Next version: ${next_version}
136
134
  - Tag: ${tag}
137
135
  - Change files:
138
- ${changes_summary}`, "Release details");
136
+ ${changes_summary || " No changes in this release."}`, "Release details");
139
137
  const proceed_confirmation = await confirm({
140
138
  message: "Proceed with generating changelog and version bump?",
141
139
  initialValue: false
@@ -146,12 +144,9 @@ ${changes_summary}`, "Release details");
146
144
  }
147
145
  const files_to_stage = [];
148
146
  const changes_dir = join(config.changes_dir, app_name);
149
- for (const change of changes.list) {
150
- const change_file_path = join(changes_dir, change.filename);
151
- await delete_file(change_file_path);
152
- files_to_stage.push(change_file_path);
153
- }
154
- log.success("Deleted change files");
147
+ const deleted_change_files = await delete_all_files_from_folder(changes_dir);
148
+ files_to_stage.push(...deleted_change_files);
149
+ log.success(`Deleted ${deleted_change_files.length} change file(s)`);
155
150
  for (const component of app.components) for (const part of component.parts) {
156
151
  const initial_content = await read_file(part.file);
157
152
  if (initial_content === null) continue;
@@ -161,11 +156,9 @@ ${changes_summary}`, "Release details");
161
156
  }
162
157
  log.success("Updated component versions");
163
158
  const formatter = app.versioning.formatter;
164
- const changelog_as_mdast = fromMarkdown(await read_file(app.changelog) ?? "", {
165
- extensions: [gfm()],
166
- mdastExtensions: [gfmFromMarkdown()]
167
- });
168
- const changelog = formatter.transform_markdown(changelog_as_mdast);
159
+ const initial_changelog_content = await read_file(app.changelog);
160
+ const changelog_as_mdast = parse_markdown(initial_changelog_content ?? "");
161
+ const changelog = formatter.transform_markdown(changelog_as_mdast, initial_changelog_content ?? "");
169
162
  const updated_changelog_content = formatter.format_changelog({
170
163
  ...changelog,
171
164
  releases: [{
@@ -195,7 +188,7 @@ ${changes_summary}`, "Release details");
195
188
  if (diff) note(diff, "Changes to be committed");
196
189
  const confirm_commit = await confirm({
197
190
  message: "Review the changes above. Proceed with commit?",
198
- initialValue: true
191
+ initialValue: false
199
192
  });
200
193
  if (isCancel(confirm_commit) || !confirm_commit) {
201
194
  await reset(root);
@@ -69,7 +69,7 @@ const CONFIG_CANDIDATES = [
69
69
  "auto-release.config.ts",
70
70
  "auto-release.config.mts",
71
71
  "auto-release.config.cts",
72
- "auto-release.config.ts",
72
+ "auto-release.config.js",
73
73
  "auto-release.config.mjs",
74
74
  "auto-release.config.cjs"
75
75
  ];
@@ -1,11 +1,7 @@
1
1
  import { ChangeFile } from "./change-file.mjs";
2
+ import { as_text, to_plain_text } from "./utils/mdast.mjs";
2
3
 
3
4
  //#region src/lib/formatter.ts
4
- function to_plain_text(node) {
5
- if ("children" in node && Array.isArray(node.children)) return node.children.map((child) => to_plain_text(child)).join("");
6
- if ("value" in node && typeof node.value === "string") return node.value;
7
- return "";
8
- }
9
5
  function normalize_label(label) {
10
6
  return label.trim().toLowerCase();
11
7
  }
@@ -20,13 +16,6 @@ function resolve_change_kind({ heading_text, allowed_changes, display_map }) {
20
16
  }
21
17
  return null;
22
18
  }
23
- function extract_list_item_text(item) {
24
- const [title, ...description] = to_plain_text(item).trim().split("\n").map((line) => line.trim()).filter(Boolean);
25
- return {
26
- title: title ?? "",
27
- description
28
- };
29
- }
30
19
  function create_empty_changelog() {
31
20
  return {
32
21
  root: {
@@ -46,7 +35,7 @@ function default_formatter({ allowed_changes, display_map }) {
46
35
  return map;
47
36
  }, {});
48
37
  return {
49
- transform_markdown(tree) {
38
+ transform_markdown(tree, original_text) {
50
39
  const changelog = create_empty_changelog();
51
40
  let current_release = null;
52
41
  let current_kind = null;
@@ -55,7 +44,7 @@ function default_formatter({ allowed_changes, display_map }) {
55
44
  };
56
45
  for (const node of tree.children) {
57
46
  if (node.type === "heading" && node.depth === 1) {
58
- changelog.root.title = to_plain_text(node).trim();
47
+ changelog.root.title = as_text(node);
59
48
  continue;
60
49
  }
61
50
  if (node.type === "heading" && node.depth === 2) {
@@ -78,17 +67,19 @@ function default_formatter({ allowed_changes, display_map }) {
78
67
  if (node.type === "list" && current_release) {
79
68
  const kind_for_items = current_kind ?? allowed_changes[0] ?? "default";
80
69
  for (const item of node.children) {
81
- const { title } = extract_list_item_text(item);
82
- if (!title) continue;
70
+ const start = item.position?.start.offset ?? 0;
71
+ const end = item.position?.end.offset ?? 0;
72
+ const summary = original_text.slice(start, end).trim();
73
+ if (!summary) continue;
83
74
  current_release.changes.push(new ChangeFile({
84
75
  kind: kind_for_items,
85
- summary: title
76
+ summary
86
77
  }));
87
78
  }
88
79
  continue;
89
80
  }
90
81
  if (!current_release && node.type === "paragraph") {
91
- const text = to_plain_text(node).trim();
82
+ const text = as_text(node);
92
83
  if (text) changelog.root.description.push(text);
93
84
  }
94
85
  }
@@ -97,10 +88,12 @@ function default_formatter({ allowed_changes, display_map }) {
97
88
  },
98
89
  format_changelog(changelog, context) {
99
90
  const lines = [];
100
- if (changelog.root.title) lines.push(`# ${changelog.root.title}`);
101
- else lines.push(`# \`${context.app.name}\` Changelog`);
102
- if (changelog.root.description.length > 0) lines.push(changelog.root.description.join("\n"));
103
- else lines.push(`This is the changelog for \`${context.app.name}\`.`);
91
+ if (changelog.root.title) lines.push(changelog.root.title);
92
+ else lines.push(`# \`${context.app.name}\` changelog`);
93
+ if (changelog.root.description.length > 0) {
94
+ console.log(changelog.root.description);
95
+ lines.push(changelog.root.description.join("\n"));
96
+ } else lines.push(`This is the changelog for \`${context.app.name}\`.`);
104
97
  for (const release of changelog.releases) {
105
98
  const release_lines = [`## ${release.version}`, ""];
106
99
  for (const change_kind of allowed_changes) {
@@ -109,9 +102,9 @@ function default_formatter({ allowed_changes, display_map }) {
109
102
  const labels = resolved_display_map[change_kind];
110
103
  const heading = labels?.plural ?? labels?.singular ?? change_kind;
111
104
  release_lines.push(`### ${heading}`, "");
112
- for (const change of kind_changes) release_lines.push(`- ${change.summary.split("\n").join("\n ")}`, "");
105
+ for (const change of kind_changes) release_lines.push(change.summary, "");
113
106
  }
114
- if (release.changes.length === 0) release_lines.push("No changes in this release.");
107
+ if (release.changes.length === 0) release_lines.push("No changes in this release.", "");
115
108
  lines.push(release_lines.join("\n"));
116
109
  }
117
110
  return lines.join("\n\n");
@@ -138,7 +131,10 @@ function default_formatter({ allowed_changes, display_map }) {
138
131
  const heading = labels?.plural ?? labels?.singular ?? kind;
139
132
  lines.push("");
140
133
  lines.push(`## ${heading}`);
141
- for (const change of items) lines.push(`- ${change.summary}`);
134
+ for (const change of items) {
135
+ lines.push(change.summary);
136
+ lines.push("");
137
+ }
142
138
  }
143
139
  if (grouped.size === 0) {
144
140
  lines.push("");
@@ -30,6 +30,16 @@ async function delete_file(path) {
30
30
  throw new Error(`Failed to delete file ${path}`, { cause: error });
31
31
  }
32
32
  }
33
+ async function delete_all_files_from_folder(folder) {
34
+ const files = await list_files(folder);
35
+ const deleted_files = [];
36
+ for (const file of files) {
37
+ const file_path = join(folder, file);
38
+ await delete_file(file_path);
39
+ deleted_files.push(file_path);
40
+ }
41
+ return deleted_files;
42
+ }
33
43
  async function list_files(dir, options) {
34
44
  try {
35
45
  const files = await readdir(dir);
@@ -61,4 +71,4 @@ async function list_files(dir, options) {
61
71
  }
62
72
 
63
73
  //#endregion
64
- export { delete_file, exists, list_files, read_file, write_file };
74
+ export { delete_all_files_from_folder, exists, list_files, read_file, write_file };
@@ -194,7 +194,7 @@ async function get_staged_diff(cwd) {
194
194
  const options = cwd ? { cwd } : void 0;
195
195
  try {
196
196
  const { stdout } = await exec("git diff --cached --stat", options);
197
- return stdout.trim();
197
+ return stdout;
198
198
  } catch {
199
199
  return "";
200
200
  }
@@ -0,0 +1,27 @@
1
+ import { toMarkdown } from "mdast-util-to-markdown";
2
+ import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
3
+ import { fromMarkdown } from "mdast-util-from-markdown";
4
+ import { gfm } from "micromark-extension-gfm";
5
+
6
+ //#region src/lib/utils/mdast.ts
7
+ function as_text(node, options) {
8
+ return toMarkdown(node, {
9
+ extensions: [gfmToMarkdown()],
10
+ bullet: "-",
11
+ ...options
12
+ }).trim();
13
+ }
14
+ function parse_markdown(text) {
15
+ return fromMarkdown(text, {
16
+ extensions: [gfm()],
17
+ mdastExtensions: [gfmFromMarkdown()]
18
+ });
19
+ }
20
+ function to_plain_text(node) {
21
+ if ("children" in node && Array.isArray(node.children)) return node.children.map((child) => to_plain_text(child)).join("");
22
+ if ("value" in node && typeof node.value === "string") return node.value;
23
+ return "";
24
+ }
25
+
26
+ //#endregion
27
+ export { as_text, parse_markdown, to_plain_text };
@@ -17,7 +17,7 @@ type Formatter<change_kinds extends string = string, parsed_changelog extends {
17
17
  * Transform the mdast tree into a custom changelog data structure.
18
18
  * @param tree - The markdown tree to transform
19
19
  */
20
- transform_markdown(tree: Root): parsed_changelog;
20
+ transform_markdown(tree: Root, original_text: string): parsed_changelog;
21
21
  /**
22
22
  * Format the changelog data into markdown.
23
23
  * The changelog releases will be sorted by version in ascending order automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afoures/auto-release",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "A file based release management tool for monorepos",
5
5
  "homepage": "https://github.com/afoures/auto-release#readme",
6
6
  "bugs": {