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