|
| 1 | +import fs from 'node:fs'; |
| 2 | +import path from 'node:path'; |
| 3 | +import { fileURLToPath } from 'node:url'; |
| 4 | + |
| 5 | +const __filename = fileURLToPath(import.meta.url); |
| 6 | +const __dirname = path.dirname(__filename); |
| 7 | +const rootDir = path.resolve(__dirname, '..'); |
| 8 | +const docsJsonPath = path.join(rootDir, 'docs.json'); |
| 9 | +const docsDir = path.join(rootDir, 'docs'); |
| 10 | + |
| 11 | +console.log('🧪 Starting Mintlify docs.json Test Suite...\n'); |
| 12 | + |
| 13 | +let errorCount = 0; |
| 14 | + |
| 15 | +function assert(condition, message) { |
| 16 | + if (!condition) { |
| 17 | + console.error(`❌ FAIL: ${message}`); |
| 18 | + errorCount++; |
| 19 | + } else { |
| 20 | + console.log(`✅ PASS: ${message}`); |
| 21 | + } |
| 22 | +} |
| 23 | + |
| 24 | +// 1. Validate docs.json existence and JSON syntax |
| 25 | +assert(fs.existsSync(docsJsonPath), 'docs.json exists'); |
| 26 | + |
| 27 | +let config; |
| 28 | +try { |
| 29 | + const raw = fs.readFileSync(docsJsonPath, 'utf-8'); |
| 30 | + config = JSON.parse(raw); |
| 31 | + assert(true, 'docs.json is valid JSON'); |
| 32 | +} catch (err) { |
| 33 | + assert(false, `Failed to parse docs.json: ${err.message}`); |
| 34 | + process.exit(1); |
| 35 | +} |
| 36 | + |
| 37 | +// 2. Validate Required Top-Level Fields |
| 38 | +assert(config.$schema === 'https://mintlify.com/docs.json', '$schema is set to https://mintlify.com/docs.json'); |
| 39 | +assert(typeof config.name === 'string' && config.name.length > 0, 'name is non-empty string'); |
| 40 | +assert(typeof config.theme === 'string' && config.theme.length > 0, 'theme is defined'); |
| 41 | + |
| 42 | +// 3. Validate Colors |
| 43 | +assert(Boolean(config.colors), 'colors configuration is present'); |
| 44 | +if (config.colors) { |
| 45 | + const hexRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/; |
| 46 | + assert(hexRegex.test(config.colors.primary), `colors.primary (${config.colors.primary}) is valid hex`); |
| 47 | + if (config.colors.light) { |
| 48 | + assert(hexRegex.test(config.colors.light), `colors.light (${config.colors.light}) is valid hex`); |
| 49 | + } |
| 50 | + if (config.colors.dark) { |
| 51 | + assert(hexRegex.test(config.colors.dark), `colors.dark (${config.colors.dark}) is valid hex`); |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +// 4. Validate Asset Paths |
| 56 | +function resolveAssetPath(assetUri) { |
| 57 | + if (!assetUri) return null; |
| 58 | + const rel = assetUri.startsWith('/') ? assetUri.slice(1) : assetUri; |
| 59 | + return path.join(rootDir, rel); |
| 60 | +} |
| 61 | + |
| 62 | +if (config.favicon) { |
| 63 | + const faviconPath = resolveAssetPath(typeof config.favicon === 'string' ? config.favicon : config.favicon.light); |
| 64 | + assert(fs.existsSync(faviconPath), `favicon file exists at ${faviconPath}`); |
| 65 | +} |
| 66 | + |
| 67 | +if (config.logo) { |
| 68 | + const logoLightPath = resolveAssetPath(typeof config.logo === 'string' ? config.logo : config.logo.light); |
| 69 | + const logoDarkPath = resolveAssetPath(typeof config.logo === 'string' ? config.logo : config.logo.dark); |
| 70 | + assert(fs.existsSync(logoLightPath), `logo.light exists at ${logoLightPath}`); |
| 71 | + assert(fs.existsSync(logoDarkPath), `logo.dark exists at ${logoDarkPath}`); |
| 72 | +} |
| 73 | + |
| 74 | +// 5. Validate Navigation Structure & Page Paths |
| 75 | +assert(Boolean(config.navigation), 'navigation configuration is present'); |
| 76 | + |
| 77 | +const referencedPages = new Set(); |
| 78 | + |
| 79 | +function extractPages(node) { |
| 80 | + if (!node) return; |
| 81 | + if (typeof node === 'string') { |
| 82 | + referencedPages.add(node); |
| 83 | + return; |
| 84 | + } |
| 85 | + if (Array.isArray(node)) { |
| 86 | + for (const item of node) { |
| 87 | + extractPages(item); |
| 88 | + } |
| 89 | + return; |
| 90 | + } |
| 91 | + if (typeof node === 'object') { |
| 92 | + if (Array.isArray(node.pages)) { |
| 93 | + for (const p of node.pages) { |
| 94 | + extractPages(p); |
| 95 | + } |
| 96 | + } |
| 97 | + if (Array.isArray(node.groups)) { |
| 98 | + for (const g of node.groups) { |
| 99 | + extractPages(g); |
| 100 | + } |
| 101 | + } |
| 102 | + if (Array.isArray(node.tabs)) { |
| 103 | + for (const t of node.tabs) { |
| 104 | + extractPages(t); |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +extractPages(config.navigation); |
| 111 | +assert(referencedPages.size > 0, `navigation defines ${referencedPages.size} total page references`); |
| 112 | + |
| 113 | +// Check that every referenced page exists on disk |
| 114 | +for (const pageRef of referencedPages) { |
| 115 | + const pagePathMd = path.join(rootDir, `${pageRef}.md`); |
| 116 | + const pagePathMdx = path.join(rootDir, `${pageRef}.mdx`); |
| 117 | + const pagePathHtml = path.join(rootDir, `${pageRef}.html`); |
| 118 | + const exists = fs.existsSync(pagePathMd) || fs.existsSync(pagePathMdx) || fs.existsSync(pagePathHtml); |
| 119 | + assert(exists, `Page exists on disk: ${pageRef} (.md / .mdx)`); |
| 120 | +} |
| 121 | + |
| 122 | +// 6. Check that all files in docs/ are included in navigation |
| 123 | +function getAllDocFiles(dir) { |
| 124 | + const entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 125 | + let files = []; |
| 126 | + for (const entry of entries) { |
| 127 | + const full = path.join(dir, entry.name); |
| 128 | + if (entry.isDirectory()) { |
| 129 | + files = files.concat(getAllDocFiles(full)); |
| 130 | + } else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { |
| 131 | + const relToRoot = path.relative(rootDir, full).replace(/\.(md|mdx)$/, ''); |
| 132 | + files.push(relToRoot); |
| 133 | + } |
| 134 | + } |
| 135 | + return files; |
| 136 | +} |
| 137 | + |
| 138 | +const allDocFiles = getAllDocFiles(docsDir); |
| 139 | +assert(allDocFiles.length >= 18, `Found ${allDocFiles.length} documentation files in docs/`); |
| 140 | + |
| 141 | +let unreferencedCount = 0; |
| 142 | +for (const docFile of allDocFiles) { |
| 143 | + if (!referencedPages.has(docFile)) { |
| 144 | + console.error(`⚠️ Unreferenced doc in docs.json: ${docFile}`); |
| 145 | + unreferencedCount++; |
| 146 | + } |
| 147 | +} |
| 148 | +assert(unreferencedCount === 0, `All ${allDocFiles.length} docs are mapped in docs.json navigation`); |
| 149 | + |
| 150 | +// 7. Validate Navbar and Footer |
| 151 | +if (config.navbar) { |
| 152 | + assert(Array.isArray(config.navbar.links) || Boolean(config.navbar.primary), 'navbar has links or primary CTA'); |
| 153 | +} |
| 154 | + |
| 155 | +if (config.footer) { |
| 156 | + assert(Boolean(config.footer.socials) || Array.isArray(config.footer.links), 'footer has socials or links'); |
| 157 | +} |
| 158 | + |
| 159 | +console.log('\n========================================'); |
| 160 | +if (errorCount === 0) { |
| 161 | + console.log('🎉 ALL MINTLIFY DOCS.JSON TESTS PASSED CLEANLY!\n'); |
| 162 | + process.exit(0); |
| 163 | +} else { |
| 164 | + console.error(`💥 ${errorCount} test(s) failed in docs.json.\n`); |
| 165 | + process.exit(1); |
| 166 | +} |
0 commit comments