From e65f11d86ed9c1895e5b7f63a2699f7aebdf1a5c Mon Sep 17 00:00:00 2001 From: Mujahid Khan <106528609+mujahidkay@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:20:08 +0500 Subject: [PATCH 1/3] lint: add script to check deadlinks --- scripts/checkLinks.mjs | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 scripts/checkLinks.mjs diff --git a/scripts/checkLinks.mjs b/scripts/checkLinks.mjs new file mode 100644 index 000000000..bb3932022 --- /dev/null +++ b/scripts/checkLinks.mjs @@ -0,0 +1,66 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const extractLinks = (content) => { + const noSingleLineComments = content.replace(/\/\/.*$/gm, ''); + const noComments = noSingleLineComments.replace(/\/\*[\s\S]*?\*\//g, ''); + const linkRegex = /link:\s*(['"])([^'"]+)\1/g; + const links = []; + let match; + while ((match = linkRegex.exec(noComments)) !== null) { + links.push(match[2]); + } + return links; +} + +const fileExists = (filePath) => { + try { + return fs.existsSync(filePath); + } catch (err) { + return false; + } +} + +const checkLink = (link) => { + if (link.startsWith('http')) { + return true; + } + + const basePath = path.join(__dirname, '../main'); + const cleanLink = link.replace(/^\//, '').replace(/\/$/, ''); + + // Check for index.md in directory + const indexPath = path.join(basePath, cleanLink, 'index.md'); + if (fileExists(indexPath)) { + return true; + } + + // Check for .md file + const mdPath = path.join(basePath, `${cleanLink}.md`); + if (fileExists(mdPath)) { + return true; + } + + return false; +} + +const navContent = fs.readFileSync(path.join(__dirname, '../main/.vitepress/themeConfig/nav.js'), 'utf8'); +const configContent = fs.readFileSync(path.join(__dirname, '../main/.vitepress/config.mjs'), 'utf8'); + +const navLinks = extractLinks(navContent); +const configLinks = extractLinks(configContent); +const allLinks = [...new Set([...navLinks, ...configLinks])]; + +const deadLinks = allLinks.filter(link => !checkLink(link)); + +if (deadLinks.length > 0) { + console.error('Dead links found:'); + deadLinks.forEach(link => console.error(link)); + process.exit(1); +} else { + console.log('All links are valid.'); +} From 3fc7dd86c6388abc8cc40aa5c2e72ef4851df2a9 Mon Sep 17 00:00:00 2001 From: Mujahid Khan <106528609+mujahidkay@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:21:01 +0500 Subject: [PATCH 2/3] lint: update scripts --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f413a82c7..d93247020 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,15 @@ "type": "module", "scripts": { "docs:dev": "NODE_OPTIONS=--openssl-legacy-provider vitepress dev main", - "docs:build": "NODE_OPTIONS=--openssl-legacy-provider vitepress build main", + "docs:build": "yarn lint:check-links && NODE_OPTIONS=--openssl-legacy-provider vitepress build main", "docs:preview": "NODE_OPTIONS=--openssl-legacy-provider vitepress preview main", - "docs:build-cf": "DEBUG='vitepress:*' NODE_OPTIONS=--openssl-legacy-provider vitepress build main && cp _redirects dist/", + "docs:build-cf": "yarn lint:check-links && DEBUG='vitepress:*' NODE_OPTIONS=--openssl-legacy-provider vitepress build main && cp _redirects dist/", "test": "ava", "lint-fix": "yarn lint --fix", "lint": "eslint 'snippets/**/*.js'", "format": "node scripts/markdown-js-snippets-linter.mjs 'main/**/*.md' --fix && prettier --write '**/*.md' --config .prettierrc.json", "lint:format": "node scripts/format.mjs", + "lint:check-links": "node scripts/checkLinks.mjs", "build": "exit 0" }, "packageManager": "yarn@4.5.0", From 26875df55fefa2ac6614debef35d717913507111 Mon Sep 17 00:00:00 2001 From: Mujahid Khan <106528609+mujahidkay@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:02:28 +0500 Subject: [PATCH 3/3] lint(format): scripts --- scripts/checkLinks.mjs | 22 +++-- scripts/format.mjs | 23 +++--- scripts/markdown-js-snippets-linter.mjs | 105 ++++++++++++++++-------- 3 files changed, 96 insertions(+), 54 deletions(-) diff --git a/scripts/checkLinks.mjs b/scripts/checkLinks.mjs index bb3932022..22d734764 100644 --- a/scripts/checkLinks.mjs +++ b/scripts/checkLinks.mjs @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const extractLinks = (content) => { +const extractLinks = content => { const noSingleLineComments = content.replace(/\/\/.*$/gm, ''); const noComments = noSingleLineComments.replace(/\/\*[\s\S]*?\*\//g, ''); const linkRegex = /link:\s*(['"])([^'"]+)\1/g; @@ -15,17 +15,17 @@ const extractLinks = (content) => { links.push(match[2]); } return links; -} +}; -const fileExists = (filePath) => { +const fileExists = filePath => { try { return fs.existsSync(filePath); } catch (err) { return false; } -} +}; -const checkLink = (link) => { +const checkLink = link => { if (link.startsWith('http')) { return true; } @@ -46,10 +46,16 @@ const checkLink = (link) => { } return false; -} +}; -const navContent = fs.readFileSync(path.join(__dirname, '../main/.vitepress/themeConfig/nav.js'), 'utf8'); -const configContent = fs.readFileSync(path.join(__dirname, '../main/.vitepress/config.mjs'), 'utf8'); +const navContent = fs.readFileSync( + path.join(__dirname, '../main/.vitepress/themeConfig/nav.js'), + 'utf8', +); +const configContent = fs.readFileSync( + path.join(__dirname, '../main/.vitepress/config.mjs'), + 'utf8', +); const navLinks = extractLinks(navContent); const configLinks = extractLinks(configContent); diff --git a/scripts/format.mjs b/scripts/format.mjs index 097f19f78..4f3c3884e 100644 --- a/scripts/format.mjs +++ b/scripts/format.mjs @@ -1,12 +1,15 @@ import { exec } from 'child_process'; -exec('node scripts/markdown-js-snippets-linter.mjs "main/**/*.md" && prettier --check "**/*.md" --config .prettierrc.json', (err, stdout, stderr) => { - if (err) { - const modifiedStderr = stderr.replace( - 'Run Prettier with --write to fix', - 'Run `yarn format` to fix' - ); - console.warn(modifiedStderr); - process.exit(1); - } -}); +exec( + 'node scripts/markdown-js-snippets-linter.mjs "main/**/*.md" && prettier --check "**/*.md" --config .prettierrc.json', + (err, stdout, stderr) => { + if (err) { + const modifiedStderr = stderr.replace( + 'Run Prettier with --write to fix', + 'Run `yarn format` to fix', + ); + console.warn(modifiedStderr); + process.exit(1); + } + }, +); diff --git a/scripts/markdown-js-snippets-linter.mjs b/scripts/markdown-js-snippets-linter.mjs index 20593b599..354075237 100644 --- a/scripts/markdown-js-snippets-linter.mjs +++ b/scripts/markdown-js-snippets-linter.mjs @@ -1,10 +1,10 @@ -import {promises as fs} from 'fs'; +import { promises as fs } from 'fs'; import glob from 'glob'; import util from 'util'; const globPromise = util.promisify(glob); -const extractJsSnippets = (markdownContent) => { +const extractJsSnippets = markdownContent => { const pattern = /```(?:js|javascript)\n([\s\S]*?)```/g; const matches = []; let match; @@ -15,7 +15,7 @@ const extractJsSnippets = (markdownContent) => { start: match.index, end: match.index + match[0].length, startLine: startLine, - language: match[0].startsWith('```javascript') ? 'javascript' : 'js' + language: match[0].startsWith('```javascript') ? 'javascript' : 'js', }); } return matches; @@ -29,17 +29,22 @@ const checkSemicolonsAndEllipsis = (code, startLine) => { let openSquareBrackets = 0; let inMultiLineComment = false; - const isJSDocOrComment = (line) => { - return line.trim().startsWith('*') || + const isJSDocOrComment = line => { + return ( + line.trim().startsWith('*') || line.trim().startsWith('/**') || line.trim().startsWith('*/') || - line.trim().startsWith('//'); + line.trim().startsWith('//') + ); }; const isStatementEnd = (line, nextLine) => { const strippedLine = line.replace(/\/\/.*$/, '').trim(); - const strippedNextLine = nextLine ? nextLine.replace(/\/\/.*$/, '').trim() : ''; - return strippedLine && + const strippedNextLine = nextLine + ? nextLine.replace(/\/\/.*$/, '').trim() + : ''; + return ( + strippedLine && !strippedLine.endsWith('{') && !strippedLine.endsWith('}') && !strippedLine.endsWith(':') && @@ -61,19 +66,25 @@ const checkSemicolonsAndEllipsis = (code, startLine) => { !strippedNextLine.trim().startsWith('.finally') && openBrackets === 0 && openParens === 0 && - openSquareBrackets === 0; + openSquareBrackets === 0 + ); }; const shouldHaveSemicolon = (line, nextLine) => { const strippedLine = line.replace(/\/\/.*$/, '').trim(); - return (strippedLine.startsWith('const ') || - strippedLine.startsWith('let ') || - strippedLine.startsWith('var ') || - strippedLine.includes('=') || - /\bawait\b/.test(strippedLine) || - (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(strippedLine) && !strippedLine.endsWith('.')) || - (/^[a-zA-Z_$][a-zA-Z0-9_$]*\[[0-9]+\]$/.test(strippedLine))) && - isStatementEnd(line, nextLine); + return ( + (strippedLine.startsWith('const ') || + strippedLine.startsWith('let ') || + strippedLine.startsWith('var ') || + strippedLine.includes('=') || + /\bawait\b/.test(strippedLine) || + (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test( + strippedLine, + ) && + !strippedLine.endsWith('.')) || + /^[a-zA-Z_$][a-zA-Z0-9_$]*\[[0-9]+\]$/.test(strippedLine)) && + isStatementEnd(line, nextLine) + ); }; for (let i = 0; i < lines.length; i++) { @@ -87,9 +98,12 @@ const checkSemicolonsAndEllipsis = (code, startLine) => { } if (inMultiLineComment || isJSDocOrComment(line)) continue; - openBrackets += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; - openParens += (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length; - openSquareBrackets += (line.match(/\[/g) || []).length - (line.match(/\]/g) || []).length; + openBrackets += + (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; + openParens += + (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length; + openSquareBrackets += + (line.match(/\[/g) || []).length - (line.match(/\]/g) || []).length; const codeWithoutComment = line.replace(/\/\/.*$/, '').trim(); @@ -99,14 +113,17 @@ const checkSemicolonsAndEllipsis = (code, startLine) => { line: startLine + i, original: line.trim(), fixed: '// ...', - type: 'ellipsis' + type: 'ellipsis', }); - } else if (shouldHaveSemicolon(line, nextLine) && !codeWithoutComment.endsWith(';')) { + } else if ( + shouldHaveSemicolon(line, nextLine) && + !codeWithoutComment.endsWith(';') + ) { issues.push({ line: startLine + i, original: line.trim(), fixed: `${codeWithoutComment};${line.includes('//') ? ' ' + line.split('//')[1] : ''}`, - type: 'semicolon' + type: 'semicolon', }); } } @@ -114,7 +131,6 @@ const checkSemicolonsAndEllipsis = (code, startLine) => { return issues; }; - const lintMarkdownFile = async (filePath, fix = false) => { try { const content = await fs.readFile(filePath, 'utf8'); @@ -124,7 +140,10 @@ const lintMarkdownFile = async (filePath, fix = false) => { for (let i = jsSnippets.length - 1; i >= 0; i--) { const snippet = jsSnippets[i]; - const issues = checkSemicolonsAndEllipsis(snippet.content, snippet.startLine); + const issues = checkSemicolonsAndEllipsis( + snippet.content, + snippet.startLine, + ); allIssues.push(...issues.map(issue => ({ ...issue, snippet: i + 1 }))); if (fix) { @@ -134,8 +153,11 @@ const lintMarkdownFile = async (filePath, fix = false) => { fixedLines[lineIndex] = issue.fixed; }); const fixedSnippet = fixedLines.join('\n'); - fixedContent = fixedContent.slice(0, snippet.start) + - '```js\n' + fixedSnippet + '```' + + fixedContent = + fixedContent.slice(0, snippet.start) + + '```js\n' + + fixedSnippet + + '```' + fixedContent.slice(snippet.end); } } @@ -156,7 +178,7 @@ const lintMarkdownFile = async (filePath, fix = false) => { filePath, issues: allIssues, fixedContent: fix ? fixedContent : null, - javascriptCount: javascriptCount + javascriptCount: javascriptCount, }; } catch (error) { console.error(`Error processing file ${filePath}: ${error.message}`); @@ -176,7 +198,10 @@ const processFiles = async (globPattern, fix = false) => { let hasErrors = false; for (const file of files) { - const { issues, error, javascriptCount } = await lintMarkdownFile(file, fix); + const { issues, error, javascriptCount } = await lintMarkdownFile( + file, + fix, + ); if (error) { console.error(`\nError in file ${file}:`); console.error(error); @@ -187,13 +212,17 @@ const processFiles = async (globPattern, fix = false) => { issues.forEach(issue => { console.error(`\nSnippet ${issue.snippet}, Line ${issue.line}:`); console.error(`Original: ${issue.original}`); - console.error(`${fix ? 'Fixed: ' : 'Suggested:'} ${issue.fixed}`); + console.error( + `${fix ? 'Fixed: ' : 'Suggested:'} ${issue.fixed}`, + ); }); totalIssues += issues.length; hasErrors = true; } if (javascriptCount > 0) { - console.error(`\nFound ${javascriptCount} instance(s) of \`\`\`javascript in ${file}`); + console.error( + `\nFound ${javascriptCount} instance(s) of \`\`\`javascript in ${file}`, + ); totalJavascriptInstances += javascriptCount; hasErrors = true; } @@ -202,14 +231,18 @@ const processFiles = async (globPattern, fix = false) => { if (totalIssues > 0 || totalJavascriptInstances > 0) { console.error(`\nTotal errors found: ${totalIssues}`); - console.error(`Total \`\`\`javascript instances found: ${totalJavascriptInstances}`); + console.error( + `Total \`\`\`javascript instances found: ${totalJavascriptInstances}`, + ); if (fix) { - console.log("All matching files have been updated with the necessary changes."); + console.log( + 'All matching files have been updated with the necessary changes.', + ); } else { - console.error("Run `yarn format` to automatically fix these errors"); + console.error('Run `yarn format` to automatically fix these errors'); } } else { - console.log("No errors found in any of the matching files."); + console.log('No errors found in any of the matching files.'); } if (hasErrors && !fix) { @@ -223,7 +256,7 @@ const processFiles = async (globPattern, fix = false) => { const main = async () => { if (process.argv.length < 3 || process.argv.length > 4) { - console.error("Usage: node linter.js [--fix]"); + console.error('Usage: node linter.js [--fix]'); process.exit(1); }