import assert from 'node:assert/strict'; import {readFileSync, readdirSync, mkdirSync, writeFileSync} from 'node:fs'; import {join} from 'node:path'; const normalize = path => path.replace(/\/$/, ''); const read = path => JSON.parse(readFileSync(path, 'utf8')); // Use Docusaurus's resolved IDs/routes and sidebar trees, not filename guesses. export function auditSidebars(instances, entrySidebars) { const docs = new Map(); const menus = new Map(); for (const {plugin, version, documents, sidebars} of instances) { const ids = new Set(); for (const doc of documents) { assert(!ids.has(doc.id), `Duplicate document ID: ${plugin}:${version}:${doc.id}`); ids.add(doc.id); const route = normalize(doc.path); assert(!docs.has(route), `Duplicate document route: ${route}`); docs.set(route, {...doc, plugin, version, menu: `${plugin}:${version}:${doc.sidebar}`}); } for (const [name, items] of Object.entries(sidebars)) menus.set(`${plugin}:${version}:${name}`, items); } const links = new Map(); for (const [menu, items] of menus) { const found = new Set(); const walk = items => { for (const item of items) { assert(['category', 'link', 'html'].includes(item.type), `Unresolved sidebar type: ${item.type}`); assert(!item.unlisted, `Hidden sidebar item: ${item.label}`); if (item.href && !/^(https?:|mailto:)/.test(item.href)) { const route = normalize(item.href); if (item.docId) assert.equal(docs.get(route)?.id, item.docId, `Sidebar ID/route mismatch: ${item.docId}`); assert(!found.has(route), `Duplicate sidebar route in ${menu}: ${route}`); found.add(route); } if (item.type === 'category') walk(item.items); } }; walk(items); links.set(menu, found); } const reachable = new Set(entrySidebars); for (const menu of reachable) { assert(menus.has(menu), `Unknown navigation sidebar: ${menu}`); for (const route of links.get(menu)) { const doc = docs.get(route); if (doc) reachable.add(doc.menu); } } for (const [route, doc] of docs) { assert(reachable.has(doc.menu), `Unreachable sidebar for ${doc.plugin}:${doc.id}`); assert(links.get(doc.menu)?.has(route), `Missing sidebar document: ${doc.plugin}:${doc.id}`); } return [...docs.entries()].map(([route, doc]) => ({route, id:doc.id, plugin:doc.plugin, sidebar:doc.sidebar})); } export async function verifySidebarCoverage() { const global = read('.docusaurus/globalData.json')['docusaurus-plugin-content-docs']; assert(global && Object.keys(global).length, 'No generated document inventory'); const instances = []; for (const [plugin, data] of Object.entries(global)) { const folder = `.docusaurus/docusaurus-plugin-content-docs/${plugin}/p`; const resolved = readdirSync(folder).filter(f => f.endsWith('.json')).map(f => read(join(folder, f)).version); for (const version of data.versions) { const matches = resolved.filter(v => v.version === version.name); assert.equal(matches.length, 1, `Missing/duplicate resolved version: ${plugin}:${version.name}`); instances.push({plugin, version:version.name, documents:version.docs, sidebars:matches[0].docsSidebars}); } } const {default:config} = await import('../.docusaurus/docusaurus.config.mjs'); const entries = config.themeConfig.navbar.items.filter(i => i.type === 'docSidebar').map(i => `${i.docsPluginId || 'default'}:current:${i.sidebarId}`); const documents = auditSidebars(instances, entries); const routes = new Set(documents.map(d => `${d.route.replace(config.baseUrl, '')}/index.html`)); const exclusions = []; const html = readdirSync('build', {recursive:true}).filter(f => f.endsWith('.html')).sort(); for (const file of html) { if (routes.delete(file)) continue; let reason; if (file === 'index.html') reason = 'Dedicated React homepage, not a documentation page'; else if (file === '404.html') reason = 'Docusaurus not-found system page'; else if (file === 'search/index.html') reason = 'Local-search results UI, not a documentation page'; else if (file === 'strategy/index.html') { assert(readFileSync(join('build',file),'utf8').includes('http-equiv="refresh"'), 'Strategy alias must remain a redirect'); reason = 'Legacy Strategy redirect alias to Target'; } else if (file === 'strategy/reference/measurement-contracts/index.html') { const body = readFileSync(join('build',file),'utf8'); assert(body.includes('window.location.replace') && body.includes('name="robots" content="noindex"') && !body.includes(' ${href}`); } } mkdirSync('.cache', {recursive:true}); writeFileSync('.cache/sidebar-coverage.json', JSON.stringify({documents, exclusions}, null, 2)); console.log(`PASS: ${documents.length} published docs reachable in resolved sidebars; ${exclusions.length} classified non-doc HTML routes; all doc links/anchors checked`); return {documents, exclusions}; }