82 lines
3.4 KiB
JavaScript
82 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Offline citation-library consistency checks. This intentionally does not
|
|
// fetch URLs; it validates the local allow-list contract used by sanitizers.
|
|
|
|
import { loadLibrary, matchesFrameworkVersion } from '../lib/citations.mjs';
|
|
|
|
const URL_RE = /^https:\/\/[A-Za-z0-9.-]+\/\S*$/;
|
|
const SKILL_REF_RE = /^[\w-]+:[\w-]+$/;
|
|
const BANNED_STALE_URLS = new Set([
|
|
'https://nextjs.org/docs/app/api-reference/functions/cache-life',
|
|
'https://nextjs.org/docs/app/api-reference/functions/cache-tag',
|
|
'https://nextjs.org/docs/app/api-reference/functions/revalidate-tag',
|
|
'https://nextjs.org/docs/app/api-reference/functions/revalidate-path',
|
|
'https://nextjs.org/docs/app/api-reference/functions/cache',
|
|
]);
|
|
|
|
async function main() {
|
|
const lib = await loadLibrary();
|
|
const errors = [];
|
|
|
|
if (!Array.isArray(lib.urls)) errors.push('docs-library.urls must be an array');
|
|
if (!Array.isArray(lib.ruleSkillRefs)) errors.push('docs-library.ruleSkillRefs must be an array');
|
|
|
|
for (const [i, entry] of (lib.urls ?? []).entries()) {
|
|
const label = `urls[${i}]`;
|
|
if (!URL_RE.test(entry?.url ?? '')) errors.push(`${label}.url must be an https URL`);
|
|
if (BANNED_STALE_URLS.has(entry?.url)) {
|
|
errors.push(`${label}.url uses a stale Next.js docs path: ${entry.url}`);
|
|
}
|
|
if (typeof entry.topic !== 'string' || entry.topic.trim() === '') errors.push(`${label}.topic is required`);
|
|
if (!Array.isArray(entry.appliesTo)) errors.push(`${label}.appliesTo must be an array`);
|
|
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
|
|
}
|
|
|
|
const seenRules = new Set();
|
|
for (const [i, entry] of (lib.ruleSkillRefs ?? []).entries()) {
|
|
const label = `ruleSkillRefs[${i}]`;
|
|
const ref = `${entry?.skill ?? ''}:${entry?.rule ?? ''}`;
|
|
if (!SKILL_REF_RE.test(ref)) errors.push(`${label} must contain skill + rule identifiers`);
|
|
if (seenRules.has(ref)) errors.push(`${label} duplicate: ${ref}`);
|
|
seenRules.add(ref);
|
|
if (typeof (entry.description ?? entry.topic) !== 'string' || (entry.description ?? entry.topic).trim() === '') {
|
|
errors.push(`${label}.topic or .description is required`);
|
|
}
|
|
validateFrameworks(entry.applicableFrameworks, `${label}.applicableFrameworks`, errors);
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
for (const error of errors) console.error(`[check-citations] ${error}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.error(`[check-citations] OK — ${lib.urls.length} URLs, ${lib.ruleSkillRefs.length} skill-rule refs`);
|
|
}
|
|
|
|
function validateFrameworks(patterns, label, errors) {
|
|
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
errors.push(`${label} must be a non-empty array`);
|
|
return;
|
|
}
|
|
for (const pattern of patterns) {
|
|
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
errors.push(`${label} contains an empty pattern`);
|
|
continue;
|
|
}
|
|
if (pattern === '*') continue;
|
|
// Smoke-check parser coverage with a modern Next version. Unknown framework
|
|
// patterns are still valid as long as the syntax is recognizable.
|
|
if (!/^[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2})(?:\s*\|\|\s*[\w-]+@(?:\*|\d+(?:\.\d+){0,2}|[<>]=?\s*\d+(?:\.\d+){0,2}))*$/.test(pattern)) {
|
|
errors.push(`${label} has unsupported pattern: ${pattern}`);
|
|
continue;
|
|
}
|
|
matchesFrameworkVersion(pattern, 'next', '16.0.0');
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('[check-citations] FAILED:', err.message);
|
|
console.error(err.stack);
|
|
process.exit(1);
|
|
});
|