diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ee005c5..60a760be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,11 +21,14 @@ jobs: with: bun-version: latest - run: bun test - fmt: + pretty: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v1 with: bun-version: latest - - run: bun fmt:ci + - name: Format + run: bun fmt:ci + - name: Lint + run: bun install && bun lint diff --git a/.sample/README.md b/.sample/README.md index ebc3e496..387d45b6 100644 --- a/.sample/README.md +++ b/.sample/README.md @@ -11,14 +11,14 @@ tags: [helper] - - ```hcl module "MODULE_NAME" { source = "https://registry.coder.com/modules/MODULE_NAME" } ``` + + ## Examples ### Example 1 diff --git a/bun.lockb b/bun.lockb index dfed9194..0d30fe94 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lint.ts b/lint.ts new file mode 100644 index 00000000..12e733cf --- /dev/null +++ b/lint.ts @@ -0,0 +1,96 @@ +import { readFile, readdir, stat } from "fs/promises"; +import * as path from "path"; +import * as marked from "marked"; +import grayMatter from "gray-matter"; + +const files = await readdir(".", { withFileTypes: true }); +const dirs = files.filter( + (f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules" +); + +let badExit = false; + +// error reports an error to the console and sets badExit to true +// so that the process will exit with a non-zero exit code. +const error = (...data: any[]) => { + console.error(...data); + badExit = true; +} + +// Ensures that each README has the proper format. +// Exits with 0 if all is good! +for (const dir of dirs) { + const readme = path.join(dir.name, "README.md"); + // Ensure exists + try { + await stat(readme); + } catch (ex) { + throw new Error(`Missing README.md in ${dir.name}`); + } + const content = await readFile(readme, "utf8"); + const matter = grayMatter(content); + const data = matter.data as { + display_name?: string; + description?: string; + icon?: string; + maintainer_github?: string; + partner_github?: string; + verified?: boolean; + tags?: string[]; + }; + if (!data.display_name) { + error(dir.name, "missing display_name"); + } + if (!data.description) { + error(dir.name, "missing description"); + } + if (!data.icon) { + error(dir.name, "missing icon"); + } + if (!data.maintainer_github) { + error(dir.name, "missing maintainer_github"); + } + try { + await stat(path.join(".", dir.name, data.icon)); + } catch (ex) { + error(dir.name, "icon does not exist", data.icon); + } + + const tokens = marked.lexer(content); + // Ensure there is an h1 and some text, then a code block + + let h1 = false; + let code = false; + let paragraph = false; + + for (const token of tokens) { + if (token.type === "heading" && token.depth === 1) { + h1 = true; + continue; + } + if (h1 && token.type === "heading") { + break; + } + if (token.type === "paragraph") { + paragraph = true; + continue; + } + if (token.type === "code") { + code = true; + continue; + } + } + if (!h1) { + error(dir.name, "missing h1"); + } + if (!paragraph) { + error(dir.name, "missing paragraph after h1"); + } + if (!code) { + error(dir.name, "missing example code block after paragraph"); + } +} + +if (badExit) { + process.exit(1); +} diff --git a/package.json b/package.json index c42b502b..c9d3a81b 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "scripts": { "test": "bun test", "fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.tf", - "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf" + "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf", + "lint": "bun run lint.ts" }, "devDependencies": { - "bun-types": "^1.0.3" + "bun-types": "^1.0.3", + "gray-matter": "^4.0.3", + "marked": "^9.0.3" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/tsconfig.json b/tsconfig.json index 86140a5d..e7b89cde 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "esnext", "module": "esnext", + "allowSyntheticDefaultImports": true, + "moduleResolution": "nodenext", "types": ["bun-types"] } }