Skip to content

Commit a5f17e0

Browse files
Add a new parsing engine, supporting repos without Releases
As a result, add support for svelte-preprocess, rollup-plugin-svelte and prettier-plugin-svelte
1 parent ada13df commit a5f17e0

File tree

6 files changed

+368
-30
lines changed

6 files changed

+368
-30
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ If you think I missed a package, you can either open an issue or directly contri
3434
- Must be by the Svelte team or their members
3535
- Must be on GitHub
3636
- Must _not_ be an internal package used only by the Svelte team
37-
- Must have releases on GitHub
37+
- Must either have releases on GitHub or at least have tags and a `CHANGELOG.md` file at the root of the repository
3838

3939
### How to contribute
4040

4141
Fork the repo, edit the `/src/routes/+layout.ts` file, and open a PR.
42-
**If the repo is not from the `sveltejs` organization, please open an issue instead.**
42+
**If the repo is not in the `sveltejs` GitHub organization, please open an issue instead.**
4343

4444
The code architecture is made to be as flexible as possible, here's how it works:
4545

@@ -54,13 +54,15 @@ const repos: Record<Tab, { name: string; repos: Repo[] }> = {
5454
...
5555
},
5656
{
57+
changesMode: "releases", // Optional line, the way to get the changes; either "releases" or "changelog", defaults to "releases"
5758
repoName: "your-repo", // The name of the repo on GitHub, as it appears in the URL: https://github.com/sveltejs/your-repo
5859
dataFilter: ({ tag_name }) => true, // Optional line, return false to exclude a version from its tag name
59-
versionFromTag: tag => "..." // Return the version from the tag name; must be a valid semver
60+
versionFromTag: tag => "...", // Return the version from the tag name; must be a valid semver
61+
changelogContentsReplacer: contents => contents, // Optional line, replace the contents of the changelog file before parsing it; only used if `changesMode` is "changelog"
6062
}
6163
]
6264
}
6365
};
6466
```
6567

66-
And that's it! The rest of the site will automatically adapt to the new package(s).
68+
And that's it! The site will automatically adapt to the new package(s).

src/lib/news/news.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"id": 2,
1616
"content": "Manual token authentication is no more: you can now use the new, shiny \"Login with GitHub\" button!",
1717
"endDate": "2024-09-09"
18+
},
19+
{
20+
"id": 3,
21+
"content": "New! Improved engine brings support for svelte-preprocess, rollup-plugin-svelte and prettier-plugin-svelte",
22+
"endDate": "2024-09-09"
1823
}
1924
]
2025
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Gently stolen from https://github.com/fl-client/changelog-parser as this “fixed” version
2+
// is not available on npm.
3+
// Removed the `filePath` option as I don't need it, bring in the types and fix the linting errors.
4+
5+
const EOL = "\n";
6+
7+
// patterns
8+
const semver = /\[?v?([\w.-]+\.[\w.-]+[a-zA-Z0-9])]?/;
9+
const date = /.* \(?(\d\d?\d?\d?[-/.]\d\d?[-/.]\d\d?\d?\d?)\)?.*/;
10+
const subhead = /^###/;
11+
const listItem = /^[*-]/;
12+
13+
type Changelog = {
14+
title: string;
15+
description: string;
16+
versions: {
17+
version: string | null;
18+
title: string;
19+
date: string | null;
20+
body: string;
21+
parsed: Record<string, string[]>;
22+
}[];
23+
};
24+
25+
type PrivateVersion = Changelog["versions"][number] & {
26+
_private?: {
27+
activeSubhead: string | null;
28+
};
29+
};
30+
31+
type ProcessingData = {
32+
log: Changelog;
33+
current: PrivateVersion | null;
34+
};
35+
36+
/**
37+
* Changelog parser.
38+
*
39+
* @param text changelog text
40+
* @param callback optional callback
41+
* @returns parsed changelog object
42+
*/
43+
export default function parseChangelog(
44+
text: string,
45+
callback?: (error: string | null, result?: Changelog) => void
46+
): Promise<Changelog> {
47+
const changelog = parse(text);
48+
49+
if (typeof callback === "function") {
50+
changelog.then(log => callback(null, log)).catch((err: string) => callback(err));
51+
}
52+
53+
// otherwise, invoke callback
54+
return changelog;
55+
}
56+
57+
/**
58+
* Internal parsing logic.
59+
*
60+
* @param text the changelog text
61+
* @returns the parsed changelog object
62+
*/
63+
function parse(text: string): Promise<Changelog> {
64+
let data: ReturnType<typeof handleLine> = {
65+
log: {
66+
title: "",
67+
description: "",
68+
versions: []
69+
},
70+
current: null
71+
};
72+
73+
return new Promise(resolve => {
74+
function done() {
75+
// push last version into log
76+
if (data.current) {
77+
pushCurrent(data);
78+
}
79+
80+
// clean up description
81+
data.log.description = clean(data.log.description);
82+
83+
resolve(data.log);
84+
}
85+
86+
if (text) {
87+
text.split(/\r\n?|\n/gm).forEach(line => (data = handleLine(line, data)));
88+
done();
89+
}
90+
});
91+
}
92+
93+
/**
94+
* Handles each line and mutates the data object (bound to `this`) as needed.
95+
*
96+
* @param line line from the changelog file
97+
* @param data the current processing data
98+
*/
99+
function handleLine(line: string, data: ProcessingData): ProcessingData {
100+
// skip line if it's a link label
101+
if (RegExp(/^\[[^[\]]*] *?:/).exec(line)) return data;
102+
103+
// set the title if it's there
104+
if (!data.log.title && RegExp(/^# ?[^#]/).exec(line)) {
105+
data.log.title = line.substring(1).trim();
106+
return data;
107+
}
108+
109+
// new version found!
110+
if (RegExp(/^##? ?[^#]/).exec(line)) {
111+
if (data.current?.title) pushCurrent(data);
112+
113+
data.current = versionFactory();
114+
115+
if (semver.exec(line)) data.current.version = semver.exec(line)?.at(1) ?? null;
116+
117+
data.current.title = line.substring(2).trim();
118+
119+
if (data.current.title && date.exec(data.current.title))
120+
data.current.date = date.exec(data.current.title)?.at(1) ?? null;
121+
122+
return data;
123+
}
124+
125+
// deal with body or description content
126+
if (data.current) {
127+
data.current.body += line + EOL;
128+
129+
// handle case where current line is a 'subhead':
130+
// - 'handleize' subhead.
131+
// - add subhead to 'parsed' data if not already present.
132+
if (subhead.exec(line)) {
133+
const key = line.replace("###", "").trim();
134+
135+
if (!data.current.parsed[key]) {
136+
data.current.parsed[key] = [];
137+
data.current._private ||= { activeSubhead: key };
138+
}
139+
}
140+
141+
// handle case where current line is a 'list item':
142+
if (listItem.exec(line)) {
143+
// add line to 'catch all' array
144+
data.current.parsed._?.push(line);
145+
146+
// add line to 'active subhead' if applicable (eg. 'Added', 'Changed', etc.)
147+
if (data.current._private?.activeSubhead) {
148+
data.current.parsed[data.current._private.activeSubhead]?.push(line);
149+
}
150+
}
151+
} else {
152+
data.log.description = (data.log.description || "") + line + EOL;
153+
}
154+
155+
return data;
156+
}
157+
158+
function versionFactory(): PrivateVersion {
159+
return {
160+
version: null,
161+
title: "",
162+
date: null,
163+
body: "",
164+
parsed: {
165+
_: []
166+
},
167+
_private: {
168+
activeSubhead: null
169+
}
170+
};
171+
}
172+
173+
function pushCurrent(data: ProcessingData) {
174+
if (!data.current) return;
175+
// remove private properties
176+
delete data.current._private;
177+
178+
data.current.body = clean(data.current.body);
179+
data.log.versions.push(data.current);
180+
}
181+
182+
function clean(str: string) {
183+
if (!str) return "";
184+
185+
return (
186+
str
187+
// trim
188+
.trim()
189+
// remove leading newlines
190+
.replace(new RegExp("[" + EOL + "]*"), "")
191+
// remove trailing newlines
192+
.replace(new RegExp("[" + EOL + "]*$"), "")
193+
);
194+
}

src/lib/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export type Repo = {
2+
/**
3+
* Mode to fetch the releases of the repo.
4+
* - `releases`: Fetches from the Releases page
5+
* - `changelog`: Fetches the changelog of the repo, if available
6+
*/
7+
changesMode?: "releases" | "changelog";
28
/**
39
* Repository name on GitHub
410
*/
@@ -17,6 +23,14 @@ export type Repo = {
1723
* @param tag The tag name to extract the version from
1824
*/
1925
versionFromTag: (tag: string) => string;
26+
/**
27+
* Replaces the contents of the changelog file.
28+
* Only used when `changesMode` is set to `changelog`.
29+
* By default, no replacement is performed.
30+
*
31+
* @param file The contents of the changelog file
32+
*/
33+
changelogContentsReplacer?: (file: string) => string;
2034
};
2135

2236
export const availableTabs = ["svelte", "kit", "others"] as const;

src/routes/+layout.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ const repos: Record<Tab, { name: string; repos: Repo[] }> = {
5555
{
5656
repoName: "svelte-devtools",
5757
versionFromTag: tag => tag.replace(/^v/, "")
58+
},
59+
{
60+
changesMode: "changelog",
61+
repoName: "svelte-preprocess",
62+
versionFromTag: tag => tag.replace(/^v/, ""),
63+
changelogContentsReplacer: file => file.replace(/^# \[/gm, "## [")
64+
},
65+
{
66+
changesMode: "changelog",
67+
repoName: "rollup-plugin-svelte",
68+
versionFromTag: tag => tag.replace(/^v/, "")
69+
},
70+
{
71+
changesMode: "changelog",
72+
repoName: "prettier-plugin-svelte",
73+
versionFromTag: tag => tag.replace(/^v/, "")
5874
}
5975
]
6076
}

0 commit comments

Comments
 (0)