forked from remix-run/remix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcompile-release-notes.mjs
167 lines (149 loc) · 4.43 KB
/
compile-release-notes.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import * as fs from "node:fs";
import path from "node:path";
import * as url from "node:url";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import rehypeStringify from "remark-stringify";
import remarkFrontmatter from "remark-frontmatter";
import { unified } from "unified";
import parseFrontMatter from "front-matter";
// Wrote this quick and dirty, gets the job done, plz don't judge me. The idea
// is not to auto-generate release notes but to quickly compile all changesets
// into a single document from which we can easily reference changes to write
// the release notes. Much faster than going back and forth between files! The
// generated markdown file should be in .gitignore, as it's only there as a
// reference.
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const rootDir = path.join(__dirname, "..");
const changesetsDir = path.join(rootDir, ".changeset");
const releaseNotesPath = path.join(rootDir, "RELEASENOTES.md");
main();
async function main() {
let changesets = getChangesetPaths();
/** @type {ReleaseNotes[]} */
let majorReleaseNotes = [];
/** @type {ReleaseNotes[]} */
let minorReleaseNotes = [];
/** @type {ReleaseNotes[]} */
let patchReleaseNotes = [];
/** @type {import('unified').Processor} */
let markdownProcessor = await unified()
.use({
settings: {
fences: true,
listItemIndent: "one",
tightDefinitions: true,
},
})
// We have multiple versions of remark-parse, TS resolves the wrong one
// @ts-expect-error
.use(remarkParse)
.use(remarkGfm)
.use(remarkFrontmatter, ["yaml", "toml"])
// same problem
// @ts-expect-error
.use(rehypeStringify, {
bullet: "-",
emphasis: "_",
listItemIndent: "one",
});
for (let changeset of changesets) {
let fileContents = fs.readFileSync(changeset, "utf-8");
let markdown = await markdownProcessor.process(fileContents);
/** @type {{attributes: unknown; body: string}} */
let { attributes, body } = parseFrontMatter(markdown.toString());
if (!isPlainObject(attributes)) {
// 🤷♀️
continue;
}
let affectedPackages = Object.keys(attributes);
let releaseTypes = Object.values(attributes);
if (releaseTypes.includes("major")) {
majorReleaseNotes.push({ affectedPackages, body });
} else if (releaseTypes.includes("minor")) {
minorReleaseNotes.push({ affectedPackages, body });
} else {
patchReleaseNotes.push({ affectedPackages, body });
}
}
let i = 0;
let fileContents = "";
for (let releaseNotes of [
majorReleaseNotes,
minorReleaseNotes,
patchReleaseNotes,
]) {
if (releaseNotes.length === 0) {
i++;
continue;
}
let heading =
"## " +
(i === 0
? "Major Changes"
: i === 1
? "Minor Changes"
: "Patch Changes") +
"\n";
let body = "";
/** @type {string[]} */
let affectedPackages = [];
for (let note of releaseNotes) {
affectedPackages = uniq(affectedPackages, note.affectedPackages);
body += `${note.body
.split("\n")
.filter(Boolean)
.map(bulletize)
.join("\n")}\n`;
}
body = `- Affected packages: \n - ${affectedPackages
.map((p) => "`" + p + "`")
.join("\n - ")}\n${body}`;
fileContents += heading + "\n" + body + "\n";
i++;
}
await fs.promises.writeFile(releaseNotesPath, fileContents.trim(), "utf-8");
console.log("✅ Donezo");
}
/**
* @param {string} fileName
* @returns
*/
function isChangeset(fileName) {
return fileName.endsWith(".md") && path.basename(fileName) !== "README.md";
}
function getChangesetPaths() {
return fs
.readdirSync(changesetsDir)
.filter((fileName) => isChangeset(fileName))
.map((fileName) => path.join(changesetsDir, fileName));
}
/**
*
* @param {unknown} obj
* @returns {obj is Record<keyof any, unknown>}
*/
function isPlainObject(obj) {
return !!obj && Object.prototype.toString.call(obj) === "[object Object]";
}
/** @typedef {{ affectedPackages: string[]; body: string }} ReleaseNotes */
/**
* @param {...any} arrays
* @returns
*/
function uniq(...arrays) {
return [...new Set(arrays.flat())];
}
/**
* @param {string} str
* @param {number} i
*/
function bulletize(str, i) {
if (i === 0) {
return "- " + str.trim().replace(/^- /, "");
}
if (str.startsWith("- ")) {
return " " + str.trim();
}
return " - " + str.trim();
}