1+ #!/usr/bin/env node
2+ 'use strict' ;
3+
4+ const fs = require ( 'fs' ) ;
5+ const path = require ( 'path' ) ;
6+
7+ const safeRequire = ( name ) => {
8+ try {
9+ return require ( name ) ;
10+ } catch ( error ) {
11+ if ( error && error . code === 'MODULE_NOT_FOUND' ) {
12+ console . log ( `Error: Cannot find module '${ name } ', have you installed the dependencies?` ) ;
13+ process . exit ( 1 ) ;
14+ }
15+ throw error ;
16+ }
17+ } ;
18+
19+ const Ajv = safeRequire ( 'ajv' ) . default ;
20+ const betterAjvErrors = safeRequire ( 'better-ajv-errors' ) . default ;
21+ const chalk = safeRequire ( 'chalk' ) ;
22+ const YAML = safeRequire ( 'yaml' ) ;
23+ const addFormats = safeRequire ( 'ajv-formats' ) ;
24+
25+ // https://www.peterbe.com/plog/nodejs-fs-walk-or-glob-or-fast-glob
26+ function walk ( directory , ext , filepaths = [ ] ) {
27+ const files = fs . readdirSync ( directory ) ;
28+ for ( const filename of files ) {
29+ const filepath = path . join ( directory , filename ) ;
30+ if ( fs . statSync ( filepath ) . isDirectory ( ) ) {
31+ walk ( filepath , ext , filepaths ) ;
32+ } else if ( path . extname ( filename ) === ext && ! filename . includes ( 'config' ) ) {
33+ filepaths . push ( filepath ) ;
34+ }
35+ }
36+ return filepaths ;
37+ }
38+
39+ // https://stackoverflow.com/a/53833620
40+ const isSorted = arr => arr . every ( ( v , i , a ) => ! i || a [ i - 1 ] <= v ) ;
41+
42+ class Validator {
43+ constructor ( flags ) {
44+ this . allowDeprecations = flags . includes ( '-d' ) ;
45+ this . stopOnError = ! flags . includes ( '-a' ) ;
46+ this . sortedURLs = flags . includes ( '-s' ) ;
47+ this . verbose = flags . includes ( '-v' ) ;
48+
49+ const schemaPath = path . resolve ( __dirname , './plugin.schema.json' ) ;
50+ this . schema = JSON . parse ( fs . readFileSync ( schemaPath , 'utf8' ) ) ;
51+ this . ajv = new Ajv ( {
52+ // allErrors: true,
53+ allowUnionTypes : true , // Use allowUnionTypes instead of ignoreKeywordsWithRef
54+ strict : true ,
55+ allowMatchingProperties : true , // Allow properties that match a pattern
56+ } ) ;
57+ addFormats ( this . ajv ) ;
58+ }
59+
60+ run ( files ) {
61+ let plugins ;
62+
63+ if ( files && Array . isArray ( files ) && files . length > 0 ) {
64+ plugins = files . map ( file => path . resolve ( file ) ) ;
65+ } else {
66+ const pluginsDir = path . resolve ( __dirname , '../plugins' ) ;
67+ const themesDir = path . resolve ( __dirname , '../themes' ) ;
68+ plugins = walk ( pluginsDir , '.yml' ) . concat ( walk ( themesDir , '.yml' ) ) ;
69+ }
70+
71+ let result = true ;
72+ const validate = this . ajv . compile ( this . schema ) ;
73+
74+ for ( const file of plugins ) {
75+ const relPath = path . relative ( process . cwd ( ) , file ) ;
76+ let contents , data ;
77+ try {
78+ contents = fs . readFileSync ( file , 'utf8' ) ;
79+ data = YAML . parse ( contents ) ;
80+ } catch ( error ) {
81+ console . error ( `${ chalk . red ( chalk . bold ( 'ERROR' ) ) } in: ${ relPath } :` ) ;
82+ error . stack = null ;
83+ console . error ( error ) ;
84+ result = result && false ;
85+ if ( this . stopOnError ) break ;
86+ else continue ;
87+ }
88+
89+ let valid = validate ( data ) ;
90+
91+ // Output validation errors
92+ if ( ! valid ) {
93+ const output = betterAjvErrors ( this . schema , data , validate . errors , { indent : 2 } ) ;
94+ console . log ( output ) ;
95+
96+ // Detailed error checks
97+ validate . errors . forEach ( err => {
98+ switch ( err . keyword ) {
99+ case 'required' :
100+ console . error ( `${ chalk . red ( 'Missing Required Property:' ) } ${ err . params . missingProperty } ` ) ;
101+ break ;
102+ case 'type' :
103+ console . error ( `${ chalk . red ( 'Type Mismatch:' ) } ${ err . dataPath } should be ${ err . params . type } ` ) ;
104+ break ;
105+ case 'pattern' :
106+ console . error ( `${ chalk . red ( 'Pattern Mismatch:' ) } ${ err . dataPath } should match pattern ${ err . params . pattern } ` ) ;
107+ break ;
108+ case 'enum' :
109+ console . error ( `${ chalk . red ( 'Enum Violation:' ) } ${ err . dataPath } should be one of ${ err . params . allowedValues . join ( ', ' ) } ` ) ;
110+ break ;
111+ case 'additionalProperties' :
112+ console . error ( `${ chalk . red ( 'Additional Properties:' ) } ${ err . params . additionalProperty } is not allowed` ) ;
113+ break ;
114+ case '$ref' :
115+ console . error ( `${ chalk . red ( 'Invalid Reference:' ) } ${ err . dataPath } ${ err . message } ` ) ;
116+ break ;
117+ case 'items' :
118+ console . error ( `${ chalk . red ( 'Array Item Type Mismatch:' ) } ${ err . dataPath } ${ err . message } ` ) ;
119+ break ;
120+ case 'format' :
121+ console . error ( `${ chalk . red ( 'Invalid Format:' ) } ${ err . dataPath } should match format ${ err . params . format } ` ) ;
122+ break ;
123+ default :
124+ console . error ( `${ chalk . red ( 'Validation Error:' ) } ${ err . dataPath } ${ err . message } ` ) ;
125+ }
126+ } ) ;
127+ }
128+
129+ if ( this . verbose || ! valid ) {
130+ const validColor = valid ? chalk . green : chalk . red ;
131+ console . log ( `${ relPath } Valid: ${ validColor ( valid ) } ` ) ;
132+ }
133+
134+ result = result && valid ;
135+
136+ if ( ! valid && this . stopOnError ) break ;
137+ }
138+
139+ if ( ! this . verbose && result ) {
140+ console . log ( chalk . green ( 'Validation passed!' ) ) ;
141+ }
142+
143+ return result ;
144+ }
145+ }
146+
147+ function main ( flags , files ) {
148+ const args = process . argv . slice ( 2 )
149+ flags = ( flags === undefined ) ? args . filter ( arg => arg . startsWith ( '-' ) ) : flags ;
150+ files = ( files === undefined ) ? args . filter ( arg => ! arg . startsWith ( '-' ) ) : files ;
151+ const validator = new Validator ( flags ) ;
152+ const result = validator . run ( files ) ;
153+ if ( flags . includes ( '--ci' ) ) {
154+ process . exit ( result ? 0 : 1 ) ;
155+ }
156+ }
157+
158+ if ( require . main === module ) {
159+ main ( ) ;
160+ }
161+
162+ module . exports = main ;
163+ module . exports . Validator = Validator ;
0 commit comments