Skip to content

Commit feb93d6

Browse files
committed
add automated linting for roadmap markdown files
1 parent f66f571 commit feb93d6

File tree

6 files changed

+364
-41
lines changed

6 files changed

+364
-41
lines changed

lint-staged.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const config = {
22
"*": "prettier --ignore-unknown --write",
33
"*.{js,jsx,ts,tsx}": "eslint --fix",
44
"**/*.ts?(x)": () => "tsc -p tsconfig.json --noEmit",
5+
"data/roadmap/*.md": "tsx scripts/lint-roadmap.ts",
56
}
67

78
export default config

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
"dev": "next dev",
1010
"build": "next build",
1111
"start": "next start",
12-
"lint": "yarn typecheck && yarn lint-js",
12+
"lint": "yarn typecheck && yarn lint-js && yarn lint-roadmap",
1313
"lint-js": "eslint .",
14+
"lint-roadmap": "tsx scripts/lint-roadmap.ts",
1415
"fix": "yarn lint --fix",
1516
"format": "prettier --write --log-level warn --ignore-unknown .",
1617
"update-sponsors": "node --experimental-fetch patreon.mjs",
@@ -49,7 +50,7 @@
4950
"@tanstack/react-query-devtools": "^5.75.2",
5051
"@types/lodash": "^4.17.16",
5152
"@types/node": "^22.15.6",
52-
"@types/react": "18.3.25",
53+
"@types/react": "^19.2.2",
5354
"@types/react-dom": "^19.1.2",
5455
"@typescript-eslint/eslint-plugin": "^8.32.0",
5556
"@typescript-eslint/parser": "^8.32.0",
@@ -64,6 +65,7 @@
6465
"prettier-plugin-tailwindcss": "^0.6.11",
6566
"tailwindcss": "^3.4.17",
6667
"tailwindcss-logical": "^3.0.1",
68+
"tsx": "^4.20.6",
6769
"typescript": "^5.8.3"
6870
},
6971
"packageManager": "[email protected]"

pages/roadmap.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import Head from "next/head"
2-
import { resolve } from "node:path"
3-
import { readdir, readFile } from "node:fs/promises"
42
import { FormattedMessage, useIntl } from "react-intl"
5-
import matter from "gray-matter"
63
import { InferGetStaticPropsType } from "next"
7-
import z from "zod"
84
import Markdown from "react-markdown"
95

106
import Hero from "../components/Hero"
117
import Layout from "../components/Layout"
128
import { RoadmapStatus } from "../components/RoadmapStatus"
139
import { withDefaultStaticProps } from "../utils/defaultStaticProps"
10+
import { loadRoadmap } from "../utils/roadmap"
1411

1512
const Roadmap = ({
1613
features,
@@ -102,27 +99,8 @@ const allowedElements: ReadonlyArray<string> = [
10299
"li",
103100
]
104101

105-
const featureSchema = z.object({
106-
data: z.object({
107-
title: z.string(),
108-
status: z.enum(["exploring", "working", "next", "released"]),
109-
}),
110-
content: z.string().trim(),
111-
})
112-
113102
export const getStaticProps = withDefaultStaticProps(async () => {
114-
const files = await readdir(resolve(process.cwd(), "data/roadmap"))
115-
const features = await Promise.all(
116-
files
117-
.filter((file) => file.endsWith(".md"))
118-
.map(async (file) => {
119-
const contents = await readFile(
120-
resolve(process.cwd(), "data/roadmap", file),
121-
"utf-8"
122-
)
123-
return featureSchema.parse(matter(contents))
124-
})
125-
)
103+
const features = await loadRoadmap()
126104
return {
127105
props: { features },
128106
}

scripts/lint-roadmap.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { loadRoadmap } from "../utils/roadmap"
2+
3+
async function main() {
4+
const features = await loadRoadmap()
5+
console.log(`Validated ${features.length} features from the roadmap.`)
6+
}
7+
8+
main()
9+
.then(() => process.exit())
10+
.catch((error) => {
11+
console.error(error)
12+
process.exit(1)
13+
})

utils/roadmap.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { readdir, readFile } from "node:fs/promises"
2+
import { resolve } from "node:path"
3+
4+
import matter from "gray-matter"
5+
import z from "zod"
6+
7+
export const featureSchema = z.object({
8+
data: z.object({
9+
title: z.string(),
10+
status: z.enum(["exploring", "working", "next", "released"]),
11+
}),
12+
content: z.string().trim(),
13+
})
14+
15+
export async function loadRoadmap(root = process.cwd()) {
16+
const files = await readdir(resolve(root, "data/roadmap"))
17+
return Promise.all(
18+
files
19+
.filter((file) => file.endsWith(".md"))
20+
.map(async (file) => {
21+
const contents = await readFile(
22+
resolve(root, "data/roadmap", file),
23+
"utf-8"
24+
)
25+
try {
26+
return featureSchema.parse(matter(contents))
27+
} catch (err: unknown) {
28+
if (err instanceof z.ZodError) {
29+
const issues = err.issues.reduce(
30+
(acc, issue) =>
31+
acc + `- [${issue.path.join(".")}]: ${issue.message}\n`,
32+
""
33+
)
34+
throw new Error(`Invalid feature ${file}:\n${issues}`)
35+
}
36+
throw new Error(`Invalid feature ${file}: ${err}`)
37+
}
38+
})
39+
)
40+
}

0 commit comments

Comments
 (0)