diff --git a/docs/verify-links.js b/docs/verify-links.js index 9bf8a67ad8..442d43aa37 100755 --- a/docs/verify-links.js +++ b/docs/verify-links.js @@ -15,13 +15,13 @@ * limitations under the License. */ -const { resolve: resolvePath, dirname } = require('path'); +const { resolve: resolvePath, join: joinPath, dirname } = require('path'); const fs = require('fs-extra'); const recursive = require('recursive-readdir'); const projectRoot = resolvePath(__dirname, '..'); -async function verifyUrl(basePath, url) { +async function verifyUrl(basePath, url, docPages) { // Avoid having absolute URL links within docs/, so that links work on the site if ( url.match( @@ -29,7 +29,7 @@ async function verifyUrl(basePath, url) { ) && basePath.match(/^(?:docs|microsite)\//) ) { - return { url, basePath, problem: 'absolute' }; + return { url, basePath, problem: 'github' }; } url = url.replace(/#.*$/, ''); @@ -49,8 +49,16 @@ async function verifyUrl(basePath, url) { let path = ''; if (url.startsWith('/')) { - if (url.startsWith('/docs/') && basePath.match(/^(?:docs|microsite)\//)) { - return { url, basePath, problem: 'not-relative' }; + if (url.startsWith('/docs/')) { + if (basePath.match(/^(?:docs)\//)) { + return { url, basePath, problem: 'not-relative' }; + } + if (basePath.startsWith('microsite/')) { + if (docPages.has(url)) { + return; + } + return { url, basePath, problem: 'doc-missing' }; + } } const staticPath = resolvePath(projectRoot, 'microsite/static', `.${url}`); @@ -71,14 +79,14 @@ async function verifyUrl(basePath, url) { return; } -async function verifyFile(filePath) { +async function verifyFile(filePath, docPages) { const content = await fs.readFile(filePath, 'utf8'); const mdLinks = content.match(/\[.+?\]\(.+?\)/g) || []; const badUrls = []; for (const mdLink of mdLinks) { const url = mdLink.match(/\[.+\]\((.+)\)/)[1].trim(); - const badUrl = await verifyUrl(filePath, url); + const badUrl = await verifyUrl(filePath, url, docPages); if (badUrl) { badUrls.push(badUrl); } @@ -87,20 +95,46 @@ async function verifyFile(filePath) { return badUrls; } +// This discovers the doc paths as they will be available on the microsite. +// It is used to validate microsite links from outside /docs/, as those +// are not transformed from the markdown file representation by docusaurus. +async function findExternalDocsLinks(dir) { + const allFiles = await recursive(dir); + const mdFiles = allFiles.filter(p => p.endsWith('.md')); + + const paths = new Map(); + + for (const file of mdFiles) { + const content = await fs.readFile(file, 'utf8'); + const url = `/${file}`; + const match = content.match(/---(?:\r|\n|.)*^id: (.*)$/m); + + // Both docs with an id and without should remove trailing /index + const realPath = (match + ? joinPath(dirname(url), match[1]) + : url.replace(/\.md$/, '') + ).replace(/\/index$/, ''); + + paths.set(url, realPath); + if (url.endsWith('/index.md')) { + paths.set(url.replace(/\/index\.md$/, ''), realPath); + } + } + + return paths; +} + async function main() { process.chdir(projectRoot); - const files = await recursive('.', [ - 'node_modules', - 'dist', - 'bin', - 'microsite', - ]); + const files = await recursive('.', ['node_modules', 'dist', 'bin']); const mdFiles = files.filter(f => f.endsWith('.md')); const badUrls = []; + const docPages = await findExternalDocsLinks('docs'); + for (const mdFile of mdFiles) { - const badFileUrls = await verifyFile(mdFile); + const badFileUrls = await verifyFile(mdFile, new Set(docPages.values())); badUrls.push(...badFileUrls); } @@ -111,12 +145,24 @@ async function main() { console.error( `Unable to reach ${url} from root or microsite/static/, linked from ${basePath}`, ); - } else if (problem === 'not-relative') { - console.error('Links to /docs/ must be relative'); + } else if (problem === 'doc-missing') { + const suggestion = + docPages.get(url) || + docPages.get(new URL(url, 'http://localhost').pathname); + console.error('Links into /docs/ must use an externally reachable ID'); console.error(` From: ${basePath}`); console.error(` To: ${url}`); - } else if (problem === 'absolute') { - console.error(`Link to docs/ should be replaced by a relative URL`); + if (suggestion) { + console.error(` Replace With: ${suggestion}`); + } + } else if (problem === 'not-relative') { + console.error('Links within /docs/ must be relative'); + console.error(` From: ${basePath}`); + console.error(` To: ${url}`); + } else if (problem === 'github') { + console.error( + `Link to docs/ should not use a GitHub URL, use a relative URL instead`, + ); console.error(` From: ${basePath}`); console.error(` To: ${url}`); }