Skip to content

Commit d47813b

Browse files
committed
add generate-changelog script that auto-generated changelog.mdx
1 parent 1ca1c1d commit d47813b

File tree

4 files changed

+201
-136
lines changed

4 files changed

+201
-136
lines changed

apps/docs/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
"name": "docs",
33
"version": "0.0.0",
44
"private": true,
5+
"type": "module",
56
"scripts": {
6-
"dev": "mintlify dev"
7+
"dev": "mintlify dev",
8+
"generate-changelog": "tsx ./scripts/generate-changelog.ts"
79
},
810
"dependencies": {
911
"mintlify": "4.2.218",
1012
"zod": "3.24.3"
13+
},
14+
"devDependencies": {
15+
"tsx": "4.20.3"
1116
}
1217
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { spawnSync } from 'node:child_process';
2+
import fs from 'node:fs/promises';
3+
import path from 'node:path';
4+
5+
const packages = [
6+
'body',
7+
'button',
8+
'code-block',
9+
'code-inline',
10+
'column',
11+
'components',
12+
'container',
13+
'create-email',
14+
'font',
15+
'head',
16+
'heading',
17+
'hr',
18+
'html',
19+
'img',
20+
'link',
21+
'markdown',
22+
'preview',
23+
'preview-server',
24+
'react-email',
25+
'render',
26+
'row',
27+
'section',
28+
'tailwind',
29+
'text',
30+
];
31+
32+
interface PackageLog {
33+
package: string;
34+
version: string;
35+
contents: string;
36+
}
37+
38+
const changesPerDate = new Map<string, PackageLog[]>();
39+
40+
for await (const packageName of packages) {
41+
console.log(`Gathering changelog data for ${packageName}...`);
42+
const packageChangelogPath = path.resolve(
43+
import.meta.dirname,
44+
'../../../packages',
45+
packageName,
46+
'CHANGELOG.md',
47+
);
48+
const packageChangelog = await fs.readFile(packageChangelogPath, 'utf8');
49+
50+
const logStartMatches = Array.from(
51+
packageChangelog.matchAll(
52+
/^##\s*(?<version>\d+\.\d+\.\d+(?<preRelease>-\w+(\.\d+)?)?)/gm,
53+
),
54+
);
55+
56+
for (const [i, match] of logStartMatches.entries()) {
57+
const matchLine = packageChangelog
58+
.slice(0, match.index)
59+
.split(/\r\n|\n|\r/).length;
60+
61+
if (match.groups.preRelease) {
62+
continue;
63+
}
64+
65+
const date = (() => {
66+
const blameAuthorTimeMatch = spawnSync(
67+
`git blame --line-porcelain -L ${matchLine},${matchLine} ${packageChangelogPath}`,
68+
{
69+
shell: true,
70+
stdio: 'pipe',
71+
},
72+
)
73+
.stdout.toString()
74+
.match(/author-time (?<timestamp>\d+)/);
75+
return new Date(
76+
Number.parseInt(blameAuthorTimeMatch.groups.timestamp, 10) * 1000,
77+
);
78+
})();
79+
80+
let contents: string = (() => {
81+
const nextMatch = logStartMatches[i + 1];
82+
const rawContents = nextMatch
83+
? packageChangelog.slice(match.index, nextMatch.index)
84+
: packageChangelog.slice(match.index);
85+
86+
return rawContents
87+
.replaceAll(match[0], '')
88+
.replaceAll(/\[?[a-f0-9]{7}\]?(: )?/g, '')
89+
.trim();
90+
})();
91+
if (contents.length > 0) {
92+
contents = `${contents}\n\n`;
93+
}
94+
95+
const monthNames = [
96+
'January',
97+
'February',
98+
'March',
99+
'April',
100+
'May',
101+
'June',
102+
'July',
103+
'August',
104+
'September',
105+
'October',
106+
'November',
107+
'December',
108+
];
109+
const formattedDate = `${monthNames[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
110+
111+
if (!changesPerDate.has(formattedDate)) {
112+
changesPerDate.set(formattedDate, []);
113+
}
114+
115+
changesPerDate.get(formattedDate)!.push({
116+
package: packageName,
117+
version: match.groups.version,
118+
contents,
119+
});
120+
}
121+
}
122+
123+
const sortedChanges = Array.from(changesPerDate.entries()).toSorted(
124+
([a], [b]) => new Date(b).getTime() - new Date(a).getTime(),
125+
);
126+
127+
console.log(
128+
'changes on dates ',
129+
sortedChanges.map((c) => c[0]),
130+
);
131+
132+
let changelog = `---
133+
title: "Changelog"
134+
sidebarTitle: "Changelog"
135+
description: "New features, bug fixes, and improvements made to each package."
136+
"og:image": "https://react.email/static/covers/react-email.png"
137+
icon: "list-check"
138+
---
139+
140+
`;
141+
142+
console.info('Writing changelog.mdx file...');
143+
for (const [formattedDate, changes] of sortedChanges) {
144+
changelog += `<Update label="${formattedDate}">\n`;
145+
146+
for (const change of changes) {
147+
const prettyName = change.package
148+
.replaceAll(/-+([a-z])/g, (_, character) => ` ${character.toUpperCase()}`)
149+
.replace(/^([a-z])/, (_, character) => character.toUpperCase());
150+
changelog += `## ${prettyName} ${change.version}\n\n`;
151+
changelog += change.contents.replaceAll(/(`[^`]*`)|<|>/g, (match) => {
152+
if (match.startsWith('`')) {
153+
return match; // Keep inline code unchanged
154+
}
155+
return match === '<' ? '\\<' : '\\>';
156+
});
157+
}
158+
159+
changelog += '</Update>\n';
160+
}
161+
162+
await fs.writeFile('changelog.mdx', changelog, 'utf8');

apps/docs/tsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"module": "esnext",
4+
"moduleResolution": "bundler",
5+
"target": "esnext"
6+
}
7+
}

0 commit comments

Comments
 (0)