mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-04 21:32:15 +00:00
chore: standardised the eslint config structure accross the project (#11814)
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import { register } from 'node:module';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { writeFileSync, existsSync, writeSync } from 'node:fs';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
|
||||
register('./scripts/loader.mjs', pathToFileURL('./'));
|
||||
register('fumadocs-mdx/node/loader', import.meta.url);
|
||||
|
||||
import {
|
||||
type FileObject,
|
||||
printErrors,
|
||||
validateFiles,
|
||||
} from 'next-validate-link';
|
||||
|
||||
try {
|
||||
// Dynamic import to ensure loader is registered before importing source
|
||||
const { source, versionSource } = await import('../lib/source');
|
||||
|
||||
const PUBLIC_ROOT = resolve('public');
|
||||
|
||||
// Simple p-limit implementation to avoid adding dependency
|
||||
function pLimit(concurrency: number) {
|
||||
const queue: (() => void)[] = [];
|
||||
let active = 0;
|
||||
|
||||
const next = () => {
|
||||
active--;
|
||||
if (queue.length > 0) {
|
||||
queue.shift()!();
|
||||
}
|
||||
};
|
||||
|
||||
const run = async <T,>(fn: () => Promise<T>): Promise<T> => {
|
||||
if (active >= concurrency) {
|
||||
await new Promise<void>((resolve) => queue.push(resolve));
|
||||
}
|
||||
active++;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
return run;
|
||||
}
|
||||
|
||||
async function checkLinks() {
|
||||
const pages = [...source.getPages(), ...versionSource.getPages()];
|
||||
console.log(`Total pages found: ${pages.length}`);
|
||||
|
||||
const allFiles = await getFiles();
|
||||
const filesToCheck = allFiles.filter((f) => !f.isVersioned);
|
||||
const allFilesAsTargets = allFiles;
|
||||
|
||||
// Load redirects and add them to the scanned URLs
|
||||
let redirects: any[] = [];
|
||||
if (existsSync('redirects.json')) {
|
||||
redirects = JSON.parse(await readFile('redirects.json', 'utf-8'));
|
||||
}
|
||||
|
||||
const scanned = {
|
||||
urls: new Map<string, { hashes: string[] }>([
|
||||
// Add /docs specifically as it's the home page and might be linked to
|
||||
['/docs', { hashes: [] }],
|
||||
// Load redirects first
|
||||
...redirects.map((r: any): [string, { hashes: string[] }] => [r.source, { hashes: [] }]),
|
||||
// Load files second, so they overwrite redirects with actual headings
|
||||
...allFilesAsTargets.map((f): [string, { hashes: string[] }] => [f.url!, { hashes: getHeadings(f) }]),
|
||||
]),
|
||||
fallbackUrls: [],
|
||||
};
|
||||
|
||||
writeFileSync('scanned-urls.json', JSON.stringify(Array.from(scanned.urls.keys()), null, 2));
|
||||
|
||||
console.log(`Manually populated URLs count: ${scanned.urls.size}`);
|
||||
|
||||
console.time('validateFiles');
|
||||
let linkErrors: any[] = [];
|
||||
let currentFile = '';
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
writeSync(1, `\n\n!!! CRITICAL ERROR in ${currentFile} !!!\n`);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
for (const file of filesToCheck) {
|
||||
currentFile = file.path;
|
||||
writeSync(1, `Checking: ${file.path}\n`);
|
||||
try {
|
||||
const errors = await validateFiles([file], {
|
||||
scanned,
|
||||
baseUrl: '/docs',
|
||||
markdown: {
|
||||
components: {
|
||||
Card: { attributes: ['href'] },
|
||||
},
|
||||
},
|
||||
checkRelativePaths: 'as-url',
|
||||
});
|
||||
linkErrors.push(...errors);
|
||||
} catch (err: any) {
|
||||
console.error(`\nFAILED file: ${file.path}`);
|
||||
console.error('Error Details:', err);
|
||||
if (file.content) {
|
||||
console.error('First 15 lines of content:');
|
||||
console.error(file.content.split('\n').slice(0, 15).join('\n'));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
console.timeEnd('validateFiles');
|
||||
|
||||
printErrors(linkErrors, false);
|
||||
|
||||
console.time('checkImages');
|
||||
const imageErrors = await checkImages(filesToCheck);
|
||||
console.timeEnd('checkImages');
|
||||
|
||||
// Check for .md files
|
||||
const mdFiles = filesToCheck.filter(f => f.path.endsWith('.md'));
|
||||
if (mdFiles.length > 0) {
|
||||
console.error('\n❌ Error: Found .md files. All content files must use .mdx extension:');
|
||||
mdFiles.forEach(f => console.error(` - ${f.path}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (linkErrors.length === 0 && !imageErrors) {
|
||||
console.log('\n✅ All checks passed: No broken links or images found.');
|
||||
}
|
||||
|
||||
if (linkErrors.length > 0 || imageErrors) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkImages(files: (FileObject & { isVersioned?: boolean })[]): Promise<boolean> {
|
||||
let hasErrors = false;
|
||||
const limit = pLimit(50); // Limit to 50 concurrent file checks
|
||||
|
||||
// Flat list of all image checks
|
||||
const allChecks: Promise<void>[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
allChecks.push(limit(async () => {
|
||||
const content = file.content;
|
||||
const imageRegex = /!\[.*?\]\((.*?)\)|<img.*?src=[{"'](.*?)["'}]/g;
|
||||
const matches = Array.from(content.matchAll(imageRegex));
|
||||
|
||||
for (const match of matches) {
|
||||
const isMarkdown = !!match[1];
|
||||
let imagePath = match[1] || match[2];
|
||||
|
||||
if (match[1]) {
|
||||
imagePath = imagePath.trim().split(/\s+/)[0];
|
||||
}
|
||||
|
||||
if (imagePath.startsWith('http') || imagePath.startsWith('https') || imagePath.startsWith('data:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
imagePath = imagePath.split('?')[0].split('#')[0];
|
||||
imagePath = decodeURIComponent(imagePath);
|
||||
|
||||
// HTML Image Rules
|
||||
if (!isMarkdown) {
|
||||
if (imagePath.includes('public/')) {
|
||||
console.error(`Broken HTML image link in ${file.path}: ${imagePath} (HTML tags cannot reference 'public/' directly, use absolute path '/docs/img/...')`);
|
||||
hasErrors = true;
|
||||
continue;
|
||||
}
|
||||
if (!imagePath.startsWith('/docs/')) {
|
||||
console.error(`Broken HTML image link in ${file.path}: ${imagePath} (HTML tags must use absolute path starting with '/docs/, e.g. '/docs/img/...')`);
|
||||
hasErrors = true;
|
||||
continue;
|
||||
}
|
||||
// Check existence for absolute /docs/ path
|
||||
const relativeToPublic = imagePath.slice(5); // Remove /docs
|
||||
const fullPath = join(PUBLIC_ROOT, relativeToPublic);
|
||||
try {
|
||||
await stat(fullPath);
|
||||
} catch {
|
||||
console.error(`Broken image link in ${file.path}: ${imagePath} (File not found at ${fullPath})`);
|
||||
hasErrors = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Markdown Image Rules
|
||||
let fullPath;
|
||||
if (imagePath.startsWith('/')) {
|
||||
// Markdown absolute paths are relative to public (usually) or not supported by webpack import if not aliased
|
||||
// We'll assume they map to public if they start with slash, but fumadocs might prefer relative.
|
||||
// For now, let's just check existence relative to public if absolute.
|
||||
// BUT, if they start with /docs/, that's likely wrong for Markdown import if it expects file path.
|
||||
if (imagePath.startsWith('/docs/')) {
|
||||
// Be lenient or strict? User had issues with imports.
|
||||
// Let's rely on standard resolution.
|
||||
const relativeToPublic = imagePath.slice(5);
|
||||
fullPath = join(PUBLIC_ROOT, relativeToPublic);
|
||||
} else {
|
||||
fullPath = join(PUBLIC_ROOT, imagePath.startsWith('/') ? imagePath.slice(1) : imagePath);
|
||||
}
|
||||
} else {
|
||||
fullPath = resolve(dirname(file.path), imagePath);
|
||||
}
|
||||
|
||||
try {
|
||||
await stat(fullPath);
|
||||
} catch {
|
||||
console.error(`Broken image link in ${file.path}: ${imagePath}`);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(allChecks);
|
||||
return hasErrors;
|
||||
}
|
||||
|
||||
function getHeadings({ data, content }: any): string[] {
|
||||
const headings = new Set<string>();
|
||||
|
||||
if (data.toc && data.toc.length > 0) {
|
||||
data.toc.forEach((item: any) => headings.add(item.url.slice(1)));
|
||||
}
|
||||
if (data.structuredData?.headings) {
|
||||
data.structuredData.headings.forEach((h: any) => headings.add(h.id));
|
||||
}
|
||||
|
||||
// Fallback: parse content directly
|
||||
if (content) {
|
||||
const lines = content.split('\n');
|
||||
let inCodeBlock = false;
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('```')) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) continue;
|
||||
|
||||
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (match) {
|
||||
const title = match[2].trim();
|
||||
const slug = title
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/_/g, '-')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
headings.add(slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(headings);
|
||||
}
|
||||
|
||||
async function getFiles() {
|
||||
const pages = [...source.getPages(), ...versionSource.getPages()];
|
||||
const promises = pages.map(
|
||||
async (page: any): Promise<FileObject & { isVersioned: boolean }> => {
|
||||
const path = page.file?.path || page.absolutePath;
|
||||
return {
|
||||
path,
|
||||
content: await readFile(path, 'utf8'),
|
||||
// REMOVED /docs prefix here to ensure strict link checking against actual routes
|
||||
url: page.url.startsWith('/') ? page.url : '/' + page.url,
|
||||
data: page.data,
|
||||
isVersioned: path.includes('/v4.') || page.url.startsWith('v4.'),
|
||||
};
|
||||
},
|
||||
);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
await checkLinks();
|
||||
} catch (err) {
|
||||
writeSync(1, `\n\n!!! TOP-LEVEL ERROR !!!\n`);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user