diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e9ea75775031e..a89cf69beefcf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,17 @@ jobs: node-version: '12.x' - run: npm install -g markdownlint-cli@0.25.0 - run: markdownlint '**/*.md' --ignore node_modules + frontmatter: + name: 🍒 Frontmatter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3.1.0 + - name: 🚀 Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.x' + - run: yarn install --frozen-lockfile --ignore-scripts + - run: yarn lint:frontmatter yamllint: name: 🍏 YAML runs-on: ubuntu-latest diff --git a/package.json b/package.json index 24681a8f15a7d..cd5e20fe3c8ee 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "serve:doc": "yarn workspace doc docusaurus serve", "serve:blog:zh": "yarn workspace blog docusaurus serve zh", "serve:blog:en": "yarn workspace blog docusaurus serve en", + "lint:frontmatter": "node scripts/lint-frontmatter.js", "build:website:preview:serve": "yarn build:website:preview && yarn serve:website", "build:doc:preview:serve": "yarn build:doc:preview && yarn serve:doc", "build:blog:zh:preview:serve": "yarn build:blog:zh:preview && yarn serve:blog:zh", @@ -60,6 +61,7 @@ "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-yml": "^0.14.0", "husky": ">=6", + "js-yaml": "^4.1.0", "lint-staged": ">=10", "remark": "^14.0.2", "remark-cli": "^11.0.0", diff --git a/scripts/lint-frontmatter.js b/scripts/lint-frontmatter.js new file mode 100644 index 0000000000000..9464beeda0177 --- /dev/null +++ b/scripts/lint-frontmatter.js @@ -0,0 +1,70 @@ +/* eslint-disable no-console */ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const ROOT = path.resolve(__dirname, '..'); +const IGNORED_DIRS = new Set([ + '.git', + 'build', + 'dist', + 'node_modules', +]); +const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']); + +function walk(dir, files = []) { + fs.readdirSync(dir, { withFileTypes: true }).forEach((entry) => { + if (entry.isDirectory()) { + if (!IGNORED_DIRS.has(entry.name)) { + walk(path.join(dir, entry.name), files); + } + return; + } + + if (entry.isFile() && MARKDOWN_EXTENSIONS.has(path.extname(entry.name))) { + files.push(path.join(dir, entry.name)); + } + }); + + return files; +} + +function extractFrontmatter(content) { + if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) { + return null; + } + + const lines = content.split(/\r?\n/); + const closingIndex = lines.slice(1).findIndex((line) => line.trim() === '---'); + if (closingIndex >= 0) { + return lines.slice(1, closingIndex + 1).join('\n'); + } + + throw new Error('Missing closing frontmatter delimiter'); +} + +function main() { + const failures = []; + + walk(ROOT).forEach((file) => { + const relativePath = path.relative(ROOT, file); + const content = fs.readFileSync(file, 'utf8'); + + try { + const frontmatter = extractFrontmatter(content); + if (frontmatter !== null) { + yaml.load(frontmatter); + } + } catch (error) { + failures.push(`${relativePath}: ${error.message}`); + } + }); + + if (failures.length > 0) { + console.error('Invalid Markdown frontmatter found:\n'); + failures.forEach((failure) => console.error(`- ${failure}`)); + process.exit(1); + } +} + +main();