Skip to content
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
75 changes: 36 additions & 39 deletions scripts/check-image-locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ const path = require('path');
const MARKDOWN_IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
const JSX_IMG_SRC_REGEX = /<img[^>]+src=["']([^"']+)["']/g;
const FRAME_IMG_REGEX = /<Frame[^>]*>[\s\S]*?<img[^>]+src=["']([^"']+)["']/g;
const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;

const VALID_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];

function removeCodeBlocks(content) {
// Replace code blocks with same number of newlines to preserve line numbers
return content.replace(CODE_BLOCK_REGEX, (match) => {
const newlineCount = (match.match(/\n/g) || []).length;
return '\n'.repeat(newlineCount);
});
}

function findMDXFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);

Expand All @@ -42,28 +51,31 @@ function findMDXFiles(dir, fileList = []) {
function extractImageReferences(content, filePath) {
const images = [];

// Remove code blocks to avoid flagging example images
const contentWithoutCodeBlocks = removeCodeBlocks(content);

// Extract markdown images: ![alt](path)
let match;
while ((match = MARKDOWN_IMAGE_REGEX.exec(content)) !== null) {
while ((match = MARKDOWN_IMAGE_REGEX.exec(contentWithoutCodeBlocks)) !== null) {
const imagePath = match[2];
if (!imagePath.startsWith('http://') && !imagePath.startsWith('https://')) {
images.push({
alt: match[1],
path: imagePath,
type: 'markdown',
line: content.substring(0, match.index).split('\n').length
line: contentWithoutCodeBlocks.substring(0, match.index).split('\n').length
});
}
}

// Extract JSX image src
while ((match = JSX_IMG_SRC_REGEX.exec(content)) !== null) {
while ((match = JSX_IMG_SRC_REGEX.exec(contentWithoutCodeBlocks)) !== null) {
const imagePath = match[1];
if (!imagePath.startsWith('http://') && !imagePath.startsWith('https://')) {
images.push({
path: imagePath,
type: 'jsx',
line: content.substring(0, match.index).split('\n').length
line: contentWithoutCodeBlocks.substring(0, match.index).split('\n').length
});
}
}
Expand Down Expand Up @@ -212,12 +224,8 @@ async function main() {
!excludedPaths.some(excluded => file.includes(excluded))
);

console.log(`📄 Found ${filteredFiles.length} documentation files\n`);

// First pass: identify shared images
console.log('🔍 Identifying shared images...\n');
const { sharedImages, imageUsageMap } = buildSharedImagesMap(filteredFiles);
console.log(`📊 Found ${sharedImages.size} images used by multiple pages\n`);

const allIssues = [];
let totalImages = 0;
Expand Down Expand Up @@ -264,62 +272,51 @@ async function main() {
}
}

// Display results
// Display summary header first
if (allIssues.length === 0) {
console.log('✅ All images are in the correct locations!\n');
console.log('✅ No image issues found!\n');
} else {
console.log(`❌ Found ${allIssues.length} image issues:\n`);
console.log(`❌ Found ${allIssues.length} image issue(s):\n`);
console.log(` • ${missingImages} missing image(s)`);
console.log(` • ${misplacedImages} misplaced image(s)`);
console.log(` • ${invalidTypes} invalid file type(s)\n`);

// Group by issue type
const missingIssues = allIssues.filter(i => i.type === 'missing');
const locationIssues = allIssues.filter(i => i.type === 'wrong-location');
const typeIssues = allIssues.filter(i => i.type === 'invalid-type');

if (missingIssues.length > 0) {
console.log(`📁 Missing Images (${missingIssues.length}):\n`);
missingIssues.forEach(({ file, line, imagePath, message }) => {
console.log('─'.repeat(40));
console.log('MISSING IMAGES:\n');
missingIssues.forEach(({ file, line, imagePath }) => {
console.log(` 📄 ${file}:${line}`);
console.log(` 🔗 ${imagePath}`);
console.log(` ❌ ${message}\n`);
console.log(` ${imagePath}\n`);
});
}

if (locationIssues.length > 0) {
console.log(`📍 Misplaced Images (${locationIssues.length}):\n`);
locationIssues.forEach(({ file, line, imagePath, expectedDir, actualDir, suggestion }) => {
console.log('─'.repeat(40));
console.log('MISPLACED IMAGES:\n');
locationIssues.forEach(({ file, line, imagePath, expectedDir, actualDir }) => {
console.log(` 📄 ${file}:${line}`);
console.log(` 🔗 ${imagePath}`);
console.log(` ❌ Expected in: ${expectedDir}/`);
console.log(` 📍 Actually in: ${actualDir}/`);
console.log(` 💡 ${suggestion}\n`);
console.log(` ${imagePath}`);
console.log(` Expected: ${expectedDir}/`);
console.log(` Actual: ${actualDir}/\n`);
});
}

if (typeIssues.length > 0) {
console.log(`⚠️ Invalid File Types (${typeIssues.length}):\n`);
console.log('─'.repeat(40));
console.log('INVALID FILE TYPES:\n');
typeIssues.forEach(({ file, line, imagePath, message }) => {
console.log(` 📄 ${file}:${line}`);
console.log(` 🔗 ${imagePath}`);
console.log(` ${message}\n`);
console.log(` ${imagePath}`);
console.log(` ${message}\n`);
});
}
}

// Summary
console.log('─'.repeat(60));
console.log(`Total images checked: ${totalImages}`);
console.log(`Shared images (used by 2+ files): ${sharedImagesCount}`);
console.log(`Missing images: ${missingImages}`);
console.log(`Misplaced images: ${misplacedImages}`);
console.log(`Invalid file types: ${invalidTypes}`);
console.log('─'.repeat(60));

console.log('\n💡 Image Placement Rules:');
console.log(' • Images should mirror page structure');
console.log(' • guides/dashboard.mdx → images/guides/dashboard/');
console.log(' • Shared images (used by multiple pages) are automatically allowed');
console.log(' • See CONTRIBUTING.md for full guidelines\n');

// Exit with error if issues found
process.exit(allIssues.length > 0 ? 1 : 0);
}
Expand Down
96 changes: 46 additions & 50 deletions scripts/check-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const CHECK_EXTERNAL = process.argv.includes('--external');
const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
const JSX_LINK_REGEX = /(?:href|src)=["']([^"']+)["']/g;
const ANCHOR_REGEX = /#[^)\s"']*/;
const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;

function removeCodeBlocks(content) {
// Replace code blocks with same number of newlines to preserve line numbers
return content.replace(CODE_BLOCK_REGEX, (match) => {
const newlineCount = (match.match(/\n/g) || []).length;
return '\n'.repeat(newlineCount);
});
}

function findMDXFiles(dir, fileList = []) {
const files = fs.readdirSync(dir);
Expand All @@ -36,23 +45,26 @@ function findMDXFiles(dir, fileList = []) {
function extractLinks(content, filePath) {
const links = [];

// Remove code blocks to avoid flagging example links
const contentWithoutCodeBlocks = removeCodeBlocks(content);

// Extract markdown links: [text](url)
let match;
while ((match = MARKDOWN_LINK_REGEX.exec(content)) !== null) {
while ((match = MARKDOWN_LINK_REGEX.exec(contentWithoutCodeBlocks)) !== null) {
links.push({
text: match[1],
url: match[2],
type: 'markdown',
line: content.substring(0, match.index).split('\n').length
line: contentWithoutCodeBlocks.substring(0, match.index).split('\n').length
});
}

// Extract JSX links: href="..." or src="..."
while ((match = JSX_LINK_REGEX.exec(content)) !== null) {
while ((match = JSX_LINK_REGEX.exec(contentWithoutCodeBlocks)) !== null) {
links.push({
url: match[1],
type: 'jsx',
line: content.substring(0, match.index).split('\n').length
line: contentWithoutCodeBlocks.substring(0, match.index).split('\n').length
});
}

Expand Down Expand Up @@ -275,8 +287,6 @@ async function main() {
!excludedPaths.some(excluded => file.includes(excluded))
);

console.log(`📄 Found ${filteredFiles.length} documentation files\n`);

const brokenLinks = [];
const externalLinks = [];
let totalLinks = 0;
Expand Down Expand Up @@ -314,56 +324,54 @@ async function main() {
}

// Check for orphaned pages
console.log('🔍 Checking for orphaned pages (not in docs.json)...\n');
const docsJson = loadDocsJson();
const orphanedPages = findOrphanedPages(filteredFiles, docsJson);

// Check redirects
console.log('🔍 Checking redirects in docs.json...\n');
const redirects = extractRedirects(docsJson);
const redirectIssues = validateRedirects(redirects);

// Display results
if (brokenLinks.length === 0) {
console.log('✅ No broken internal links found!\n');
// Calculate total issues for summary header
const totalIssues = brokenLinks.length + orphanedPages.length + redirectIssues.length;

// Print summary header first
if (totalIssues === 0) {
console.log('✅ No issues found!\n');
} else {
console.log(`❌ Found ${brokenLinks.length} broken internal links:\n`);

brokenLinks.forEach(({ file, url, line, type, triedPaths }) => {
console.log(`📄 ${file}:${line}`);
console.log(` 🔗 Broken link: ${url}`);
console.log(` 📍 Type: ${type}`);
if (triedPaths) {
console.log(` 🔍 Tried paths:`);
triedPaths.slice(0, 2).forEach(p => {
console.log(` - ${path.relative(process.cwd(), p)}`);
});
}
console.log('');
console.log(`❌ Found ${totalIssues} issue(s):\n`);
console.log(` • ${brokenLinks.length} broken link(s)`);
console.log(` • ${orphanedPages.length} orphaned page(s)`);
console.log(` • ${redirectIssues.length} invalid redirect(s)\n`);
}

// Display broken links details
if (brokenLinks.length > 0) {
console.log('─'.repeat(40));
console.log('BROKEN LINKS:\n');
brokenLinks.forEach(({ file, url, line }) => {
console.log(` 📄 ${file}:${line}`);
console.log(` ${url}\n`);
});
}

// Display orphaned pages
if (orphanedPages.length === 0) {
console.log('✅ No orphaned pages found!\n');
} else {
console.log(`⚠️ Found ${orphanedPages.length} orphaned pages (exist but not in docs.json):\n`);
// Display orphaned pages details
if (orphanedPages.length > 0) {
console.log('─'.repeat(40));
console.log('ORPHANED PAGES (not in docs.json):\n');
orphanedPages.forEach(page => {
console.log(` 📄 ${page}.mdx`);
});
console.log('\n💡 These pages exist but are not linked in docs.json navigation.\n');
console.log('');
}

// Display redirect issues
if (redirectIssues.length === 0) {
console.log('✅ All redirects are valid!\n');
} else {
console.log(`❌ Found ${redirectIssues.length} invalid redirects in docs.json:\n`);
// Display redirect issues details
if (redirectIssues.length > 0) {
console.log('─'.repeat(40));
console.log('INVALID REDIRECTS:\n');
redirectIssues.forEach(({ source, destination, issue }) => {
console.log(` 🔀 ${source} → ${destination}`);
console.log(` ${issue}\n`);
console.log(` ${issue}\n`);
});
console.log('💡 When moving pages, ensure redirect destinations exist.\n');
}

// Check external links if requested
Expand Down Expand Up @@ -391,20 +399,8 @@ async function main() {
}
}

// Summary
console.log('─'.repeat(60));
console.log(`Total links checked: ${totalLinks}`);
console.log(`Broken internal links: ${brokenLinks.length}`);
console.log(`Orphaned pages: ${orphanedPages.length}`);
console.log(`Invalid redirects: ${redirectIssues.length}`);
if (CHECK_EXTERNAL) {
console.log(`External links checked: ${externalLinks.length}`);
}
console.log('─'.repeat(60));

// Exit with error if issues found
const hasIssues = brokenLinks.length > 0 || orphanedPages.length > 0 || redirectIssues.length > 0;
process.exit(hasIssues ? 1 : 0);
process.exit(totalIssues > 0 ? 1 : 0);
}

main();