Setting the file. One moment.
Check Citations · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page scripts/check-citations.mjs
scripts/ check-citations.mjs
JavaScript · 81 lines · 3 KB
+
:
[\w-]
+$
/
;
9 const BANNED_STALE_URLS = new Set ([
10 'https://nextjs.org/docs/app/api-reference/functions/cache-life' ,
11 'https://nextjs.org/docs/app/api-reference/functions/cache-tag' ,
12 'https://nextjs.org/docs/app/api-reference/functions/revalidate-tag' ,
13 'https://nextjs.org/docs/app/api-reference/functions/revalidate-path' ,
14 'https://nextjs.org/docs/app/api-reference/functions/cache' ,
15 ]);
16
17 async function main () {
18 const lib = await loadLibrary ();
19 const errors = [];
20
21 if ( ! Array. isArray (lib.urls)) errors. push ( 'docs-library.urls must be an array' );
22 if ( ! Array. isArray (lib.ruleSkillRefs)) errors. push ( 'docs-library.ruleSkillRefs must be an array' );
23
24 for ( const [ i , entry ] of (lib.urls ?? []). entries ()) {
25 const label = `urls[${ i }]` ;
26 if ( ! URL_RE . test (entry?.url ?? '' )) errors. push ( `${ label }.url must be an https URL` );
27 if ( BANNED_STALE_URLS . has (entry?.url)) {
28 errors. push ( `${ label }.url uses a stale Next.js docs path: ${ entry . url }` );
29 }
30 if ( typeof entry.topic !== 'string' || entry.topic. trim () === '' ) errors. push ( `${ label }.topic is required` );
31 if ( ! Array. isArray (entry.appliesTo)) errors. push ( `${ label }.appliesTo must be an array` );
32 validateFrameworks (entry.applicableFrameworks, `${ label }.applicableFrameworks` , errors);
33 }
34
35 const seenRules = new Set ();
36 for ( const [ i , entry ] of (lib.ruleSkillRefs ?? []). entries ()) {
37 const label = `ruleSkillRefs[${ i }]` ;
38 const ref = `${ entry ?. skill ?? ''}:${ entry ?. rule ?? ''}` ;
39 if ( ! SKILL_REF_RE . test (ref)) errors. push ( `${ label } must contain skill + rule identifiers` );
40 if (seenRules. has (ref)) errors. push ( `${ label } duplicate: ${ ref }` );
41 seenRules. add (ref);
42 if ( typeof (entry.description ?? entry.topic) !== 'string' || (entry.description ?? entry.topic). trim () === '' ) {
43 errors. push ( `${ label }.topic or .description is required` );
44 }
45 validateFrameworks (entry.applicableFrameworks, `${ label }.applicableFrameworks` , errors);
46 }
47
48 if (errors. length > 0 ) {
49 for ( const error of errors) console. error ( `[check-citations] ${ error }` );
50 process. exit ( 1 );
51 }
52
53 console. error ( `[check-citations] OK — ${ lib . urls . length } URLs, ${ lib . ruleSkillRefs . length } skill-rule refs` );
54 }
55
56 function validateFrameworks ( patterns , label , errors ) {
57 if ( ! Array. isArray (patterns) || patterns. length === 0 ) {
58 errors. push ( `${ label } must be a non-empty array` );
59 return ;
60 }
61 for ( const pattern of patterns) {
62 if ( typeof pattern !== 'string' || pattern. trim () === '' ) {
63 errors. push ( `${ label } contains an empty pattern` );
64 continue ;
65 }
66 if (pattern === '*' ) continue ;
67 // Smoke-check parser coverage with a modern Next version. Unknown framework
68 // patterns are still valid as long as the syntax is recognizable.
69 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)) {
70 errors. push ( `${ label } has unsupported pattern: ${ pattern }` );
71 continue ;
72 }
73 matchesFrameworkVersion (pattern, 'next' , '16.0.0' );
74 }
75 }
76
77 main (). catch (( err ) => {
78 console. error ( '[check-citations] FAILED:' , err.message);
79 console. error (err.stack);
80 process. exit ( 1 );
81 });