#!/usr/bin/env node /** * Validates that every "language" Tabs block in MDX docs has all 4 required variants: * Python, Node.js, CLI, Go. * * A Tabs block is considered a "language" block if it contains at least one TabItem * with value "python", "node", "cli", or "go". * * Run: node scripts/check-code-parity.mjs */ import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const __dirname = dirname(fileURLToPath(import.meta.url)); const docsRoot = join(__dirname, '..'); const REQUIRED_TABS = new Set(['python', 'node', 'cli', 'go']); const IGNORED_PATHS = [ 'node_modules', 'build', '.docusaurus', 'versioned_docs', // skip versioned docs ]; /** * Recursively find all .mdx files under a directory. */ function findMdxFiles(dir) { const results = []; for (const entry of readdirSync(dir)) { if (IGNORED_PATHS.includes(entry)) continue; const full = join(dir, entry); const stat = statSync(full); if (stat.isDirectory()) { results.push(...findMdxFiles(full)); } else if (entry.endsWith('.mdx') || entry.endsWith('.md')) { results.push(full); } } return results; } /** * Parse a single MDX file and return all violations. * A violation is a Tabs block that has at least one language tab but is missing * one or more of the 4 required language variants. */ function checkFile(filePath) { const content = readFileSync(filePath, 'utf8'); const violations = []; // Split content into Tabs blocks. // Strategy: find ... sections and scan for TabItem values. // We use a simple line-by-line state machine. const lines = content.split('\n'); let inTabs = false; let tabsStartLine = -1; let currentTabValues = new Set(); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!inTabs) { // Look for opening tag (not ) if (/^\s*]/.test(line) && !/<\/Tabs/.test(line)) { inTabs = true; tabsStartLine = i + 1; // 1-indexed currentTabValues = new Set(); } } else { // Inside a Tabs block — look for or nested TabItem values if (/^\s*<\/Tabs\s*>/.test(line)) { // End of Tabs block — check if it's a language block const hasLanguageTab = [...currentTabValues].some(v => REQUIRED_TABS.has(v)); if (hasLanguageTab) { const missing = [...REQUIRED_TABS].filter(t => !currentTabValues.has(t)); if (missing.length > 0) { violations.push({ line: tabsStartLine, found: [...currentTabValues].filter(v => REQUIRED_TABS.has(v)), missing, }); } } inTabs = false; currentTabValues = new Set(); } else { // Look for TabItem value attributes // Matches: ]*value=["']([^"']+)["']/); if (match) { currentTabValues.add(match[1]); } } } } return violations; } // ─── Main ──────────────────────────────────────────────────────────────────── const mdxFiles = findMdxFiles(docsRoot); let totalViolations = 0; for (const filePath of mdxFiles) { const violations = checkFile(filePath); if (violations.length > 0) { const rel = relative(docsRoot, filePath); for (const v of violations) { console.error( `[code-parity] ${rel}:${v.line} — Tabs block missing language tabs: ${v.missing.join(', ')} (found: ${v.found.join(', ')})` ); } totalViolations += violations.length; } } if (totalViolations > 0) { console.error(`\n[code-parity] ❌ Found ${totalViolations} Tabs block(s) missing required language variants.`); console.error('[code-parity] Every Tabs block with language tabs must include: python, node, cli, go'); process.exit(1); } else { console.log(`[code-parity] ✅ All ${mdxFiles.length} docs files pass 4-tab parity check.`); }