Skip to content

Commit cda03f7

Browse files
authored
Replace release-utils markdown parsing with regex (#1962)
1 parent c64ae36 commit cda03f7

6 files changed

Lines changed: 145 additions & 414 deletions

File tree

.changeset/long-badgers-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@changesets/release-utils": minor
3+
---
4+
5+
Replace markdown parsing with regex

packages/release-utils/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@
1212
"@changesets/read": "workspace:^",
1313
"@changesets/types": "workspace:^",
1414
"@manypkg/get-packages": "^3.0.0",
15-
"mdast-util-from-markdown": "^2.0.2",
16-
"mdast-util-to-markdown": "^2.1.2",
17-
"mdast-util-to-string": "^4.0.0",
1815
"semver": "^7.5.3",
1916
"tinyexec": "^1.0.2"
2017
},

packages/release-utils/src/__snapshots__/utils.test.ts.snap

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`getChangelogEntry > it sorts the things right 1`] = `
3+
exports[`getChangelogEntry > it works 1`] = `
4+
"### Major Changes
5+
6+
- [2164a779](https://github.com/keystonejs/keystone-5/commit/2164a779):
7+
8+
- Replace jade with pug because Jade was renamed to Pug, and \`jade\` package is outdated
9+
10+
### Patch Changes
11+
12+
- [81dc0be5](https://github.com/keystonejs/keystone-5/commit/81dc0be5):
13+
14+
- Update dependencies"
15+
`;
16+
17+
exports[`getChangelogEntry > it works again! 1`] = `
18+
"### Patch Changes
19+
20+
- [19fe6c1b](https://github.com/keystonejs/keystone-5/commit/19fe6c1b):
21+
22+
Move frontmatter in docs into comments"
23+
`;
24+
25+
exports[`getChangelogEntry > it works with code blocks 1`] = `
26+
"### Major Changes
27+
28+
\`\`\`
29+
# not a heading
30+
## not a heading
31+
### not a heading
32+
\`\`\`"
33+
`;
34+
35+
exports[`getChangelogEntry > it works with nested code blocks 1`] = `
36+
"### Major Changes
37+
38+
\`\`\`\`
39+
# not a heading
40+
## not a heading
41+
### not a heading
42+
43+
\`\`\`
44+
# not a heading
45+
## not a heading
46+
### not a heading
47+
\`\`\`
48+
49+
\`\`\`\`"
50+
`;
51+
52+
exports[`sortChangelogEntries > it sorts the things right 1`] = `
453
[
554
{
655
"highestLevel": 3,
@@ -19,27 +68,3 @@ exports[`getChangelogEntry > it sorts the things right 1`] = `
1968
},
2069
]
2170
`;
22-
23-
exports[`getChangelogEntry > it works 1`] = `
24-
"### Major Changes
25-
26-
* [2164a779](https://github.com/keystonejs/keystone-5/commit/2164a779):
27-
28-
* Replace jade with pug because Jade was renamed to Pug, and \`jade\` package is outdated
29-
30-
### Patch Changes
31-
32-
* [81dc0be5](https://github.com/keystonejs/keystone-5/commit/81dc0be5):
33-
34-
* Update dependencies
35-
"
36-
`;
37-
38-
exports[`getChangelogEntry > it works again! 1`] = `
39-
"### Patch Changes
40-
41-
* [19fe6c1b](https://github.com/keystonejs/keystone-5/commit/19fe6c1b):
42-
43-
Move frontmatter in docs into comments
44-
"
45-
`;

packages/release-utils/src/utils.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,57 @@ describe("getChangelogEntry", () => {
8585
expect(entry.highestLevel).toBe(BumpLevels.patch);
8686
});
8787

88+
test("it works with code blocks", () => {
89+
const changelogWithCodeBlocks = `# Changelog
90+
91+
## 1.0.0
92+
93+
### Major Changes
94+
95+
\`\`\`
96+
# not a heading
97+
## not a heading
98+
### not a heading
99+
\`\`\`
100+
101+
## 0.0.1
102+
103+
Initial release`;
104+
const entry = getChangelogEntry(changelogWithCodeBlocks, "1.0.0");
105+
expect(entry.content).toMatchSnapshot();
106+
expect(entry.highestLevel).toBe(BumpLevels.major);
107+
});
108+
109+
test("it works with nested code blocks", () => {
110+
const changelogWithCodeBlocks = `# Changelog
111+
112+
## 1.0.0
113+
114+
### Major Changes
115+
116+
\`\`\`\`
117+
# not a heading
118+
## not a heading
119+
### not a heading
120+
121+
\`\`\`
122+
# not a heading
123+
## not a heading
124+
### not a heading
125+
\`\`\`
126+
127+
\`\`\`\`
128+
129+
## 0.0.1
130+
131+
Initial release`;
132+
const entry = getChangelogEntry(changelogWithCodeBlocks, "1.0.0");
133+
expect(entry.content).toMatchSnapshot();
134+
expect(entry.highestLevel).toBe(BumpLevels.major);
135+
});
136+
});
137+
138+
describe("sortChangelogEntries", () => {
88139
test("it sorts the things right", () => {
89140
const things = [
90141
{

packages/release-utils/src/utils.ts

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import type { Package } from "@changesets/types";
22
import { getPackages } from "@manypkg/get-packages";
3-
import { fromMarkdown as stringToMdast } from "mdast-util-from-markdown";
4-
import { toMarkdown as mdastToString } from "mdast-util-to-markdown";
5-
import { toString as mdastNodeToString } from "mdast-util-to-string";
63

74
export const BumpLevels = {
85
dep: 0,
@@ -34,53 +31,54 @@ export async function getChangedPackages(
3431
}
3532

3633
export function getChangelogEntry(changelog: string, version: string) {
37-
const ast = stringToMdast(changelog);
38-
3934
let highestLevel: number = BumpLevels.dep;
40-
41-
const nodes = ast.children;
42-
let headingStartInfo:
43-
| {
44-
index: number;
45-
depth: number;
46-
}
47-
| undefined;
35+
let headingStartInfo: { index: number; depth: number } | undefined;
4836
let endIndex: number | undefined;
4937

50-
for (let i = 0; i < nodes.length; i++) {
51-
const node = nodes[i];
52-
if (node.type === "heading") {
53-
const stringified: string = mdastNodeToString(node);
54-
const match = stringified.toLowerCase().match(/(major|minor|patch)/);
55-
if (match != null) {
56-
const level = BumpLevels[match[0] as "major" | "minor" | "patch"];
57-
highestLevel = Math.max(level, highestLevel);
58-
}
59-
if (headingStartInfo == null && stringified === version) {
60-
headingStartInfo = {
61-
index: i,
62-
depth: node.depth,
63-
};
38+
// Iterate through each headings and code blocks (for skipping its contents)
39+
const regex = /^(#{1,6})\s(.*)$|^(`{3,})/gm;
40+
let match: RegExpExecArray | null;
41+
while ((match = regex.exec(changelog)) != null) {
42+
// Skip over code blocks so we don't match any headings inside of them
43+
if (match[3]) {
44+
const endOfCodeBlockRegex = new RegExp(`^${match[3]}`, "gm");
45+
endOfCodeBlockRegex.lastIndex = regex.lastIndex;
46+
const endMatch = endOfCodeBlockRegex.exec(changelog);
47+
if (endMatch) {
48+
// Start next search for headings after the end of the code block
49+
regex.lastIndex = endOfCodeBlockRegex.lastIndex;
6450
continue;
65-
}
66-
if (
67-
endIndex == null &&
68-
headingStartInfo != null &&
69-
headingStartInfo.depth === node.depth
70-
) {
71-
endIndex = i;
51+
} else {
52+
// Can't find end of code block, probably malformed
7253
break;
7354
}
7455
}
56+
57+
const headingDepth = match[1].length;
58+
const headingText = match[2].trim();
59+
60+
// Search for the highest bump level in the entire changelog
61+
const levelMatch = /(major|minor|patch)/.exec(headingText.toLowerCase());
62+
if (levelMatch != null) {
63+
const level = BumpLevels[levelMatch[0] as "major" | "minor" | "patch"];
64+
highestLevel = Math.max(level, highestLevel);
65+
}
66+
67+
// Search for heading of the entry
68+
if (headingText === version) {
69+
headingStartInfo = { index: regex.lastIndex, depth: headingDepth };
70+
continue;
71+
}
72+
73+
// If we've found the entry heading, search for the closing heading with the same depth
74+
if (headingStartInfo && headingDepth === headingStartInfo.depth) {
75+
endIndex = match.index;
76+
break;
77+
}
7578
}
76-
if (headingStartInfo) {
77-
ast.children = (ast.children as any).slice(
78-
headingStartInfo.index + 1,
79-
endIndex,
80-
);
81-
}
79+
8280
return {
83-
content: mdastToString(ast),
81+
content: changelog.slice(headingStartInfo?.index, endIndex).trim(),
8482
highestLevel,
8583
};
8684
}

0 commit comments

Comments
 (0)