Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add a deadlink checker for nav/sidebar items #1236

Merged
merged 3 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
Expand Down
72 changes: 72 additions & 0 deletions scripts/checkLinks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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.');
}
23 changes: 13 additions & 10 deletions scripts/format.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
},
);
105 changes: 69 additions & 36 deletions scripts/markdown-js-snippets-linter.mjs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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(':') &&
Expand All @@ -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++) {
Expand All @@ -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();

Expand All @@ -99,22 +113,24 @@ 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',
});
}
}

return issues;
};


const lintMarkdownFile = async (filePath, fix = false) => {
try {
const content = await fs.readFile(filePath, 'utf8');
Expand All @@ -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) {
Expand All @@ -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);
}
}
Expand All @@ -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}`);
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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 <glob_pattern> [--fix]");
console.error('Usage: node linter.js <glob_pattern> [--fix]');
process.exit(1);
}

Expand Down
Loading