1010import SimpleGit from 'simple-git' ;
1111
1212import { run } from '@kbn/dev-cli-runner' ;
13- import { createFlagError , combineErrors } from '@kbn/dev-cli-errors' ;
13+ import { createFlagError } from '@kbn/dev-cli-errors' ;
1414import { REPO_ROOT } from '@kbn/repo-info' ;
1515import * as Eslint from './eslint' ;
1616import * as Stylelint from './stylelint' ;
1717import { getFilesForCommit , checkFileCasing } from './precommit_hook' ;
18+ import { load as yamlLoad } from 'js-yaml' ;
19+ import { readFile } from 'fs/promises' ;
20+ import { extname } from 'path' ;
21+
22+ class CheckResult {
23+ constructor ( checkName ) {
24+ this . checkName = checkName ;
25+ this . errors = [ ] ;
26+ this . succeeded = true ;
27+ }
28+
29+ addError ( error ) {
30+ this . succeeded = false ;
31+ this . errors . push ( error ) ;
32+ }
33+
34+ toString ( ) {
35+ if ( this . succeeded ) {
36+ return `✓ ${ this . checkName } : Passed` ;
37+ } else {
38+ return [ `✗ ${ this . checkName } : Failed` , ...this . errors . map ( ( err ) => ` - ${ err } ` ) ] . join ( '\n' ) ;
39+ }
40+ }
41+ }
42+
43+ class PrecommitCheck {
44+ constructor ( name ) {
45+ this . name = name ;
46+ }
47+
48+ async execute ( ) {
49+ throw new Error ( 'execute() must be implemented by check class' ) ;
50+ }
51+
52+ async runSafely ( log , files , options ) {
53+ const result = new CheckResult ( this . name ) ;
54+ try {
55+ await this . execute ( log , files , options ) ;
56+ } catch ( error ) {
57+ if ( error . errors ) {
58+ error . errors . forEach ( ( err ) => result . addError ( err . message || err . toString ( ) ) ) ;
59+ } else {
60+ result . addError ( error . message || error . toString ( ) ) ;
61+ }
62+ }
63+ return result ;
64+ }
65+ }
66+
67+ class FileCasingCheck extends PrecommitCheck {
68+ constructor ( ) {
69+ super ( 'File Casing' ) ;
70+ }
71+
72+ async execute ( log , files ) {
73+ await checkFileCasing ( log , files ) ;
74+ }
75+ }
76+
77+ class LinterCheck extends PrecommitCheck {
78+ constructor ( name , linter ) {
79+ super ( name ) ;
80+ this . linter = linter ;
81+ }
82+
83+ async execute ( log , files , options ) {
84+ const filesToLint = await this . linter . pickFilesToLint ( log , files ) ;
85+ if ( filesToLint . length > 0 ) {
86+ await this . linter . lintFiles ( log , filesToLint , {
87+ fix : options . fix ,
88+ } ) ;
89+
90+ if ( options . fix && options . stage ) {
91+ const simpleGit = new SimpleGit ( REPO_ROOT ) ;
92+ await simpleGit . add ( filesToLint ) ;
93+ }
94+ }
95+ }
96+ }
97+
98+ class YamlLintCheck extends PrecommitCheck {
99+ constructor ( ) {
100+ super ( 'YAML Lint' ) ;
101+ }
102+
103+ isYamlFile ( filePath ) {
104+ const ext = extname ( filePath ) . toLowerCase ( ) ;
105+ return ext === '.yml' || ext === '.yaml' ;
106+ }
107+
108+ async execute ( log , files ) {
109+ const yamlFiles = files . filter ( ( file ) => this . isYamlFile ( file . getRelativePath ( ) ) ) ;
110+
111+ if ( yamlFiles . length === 0 ) {
112+ log . verbose ( 'No YAML files to check' ) ;
113+ return ;
114+ }
115+
116+ log . verbose ( `Checking ${ yamlFiles . length } YAML files for syntax errors` ) ;
117+
118+ const errors = [ ] ;
119+ for ( const file of yamlFiles ) {
120+ try {
121+ const content = await readFile ( file . getAbsolutePath ( ) , 'utf8' ) ;
122+ yamlLoad ( content , {
123+ filename : file . getRelativePath ( ) ,
124+ } ) ;
125+ } catch ( error ) {
126+ errors . push ( `Error in ${ file . getRelativePath ( ) } :\n${ error . message } ` ) ;
127+ }
128+ }
129+
130+ if ( errors . length > 0 ) {
131+ throw new Error ( errors . join ( '\n\n' ) ) ;
132+ }
133+ }
134+ }
135+
136+ const PRECOMMIT_CHECKS = [
137+ new FileCasingCheck ( ) ,
138+ new LinterCheck ( 'ESLint' , Eslint ) ,
139+ new LinterCheck ( 'StyleLint' , Stylelint ) ,
140+ new YamlLintCheck ( ) ,
141+ ] ;
18142
19143run (
20144 async ( { log, flags } ) => {
21145 process . env . IS_KIBANA_PRECOMIT_HOOK = 'true' ;
22146
23147 const files = await getFilesForCommit ( flags . ref ) ;
24- const errors = [ ] ;
25148
26149 const maxFilesCount = flags [ 'max-files' ]
27150 ? Number . parseInt ( String ( flags [ 'max-files' ] ) , 10 )
@@ -37,33 +160,33 @@ run(
37160 return ;
38161 }
39162
40- try {
41- await checkFileCasing ( log , files ) ;
42- } catch ( error ) {
43- errors . push ( error ) ;
44- }
163+ log . verbose ( 'Running pre-commit checks...' ) ;
164+ const results = await Promise . all (
165+ PRECOMMIT_CHECKS . map ( async ( check ) => {
166+ const startTime = Date . now ( ) ;
167+ const result = await check . runSafely ( log , files , {
168+ fix : flags . fix ,
169+ stage : flags . stage ,
170+ } ) ;
171+ const duration = Date . now ( ) - startTime ;
172+ log . verbose ( `${ check . name } completed in ${ duration } ms` ) ;
173+ return result ;
174+ } )
175+ ) ;
45176
46- for ( const Linter of [ Eslint , Stylelint ] ) {
47- const filesToLint = await Linter . pickFilesToLint ( log , files ) ;
48- if ( filesToLint . length > 0 ) {
49- try {
50- await Linter . lintFiles ( log , filesToLint , {
51- fix : flags . fix ,
52- } ) ;
53-
54- if ( flags . fix && flags . stage ) {
55- const simpleGit = new SimpleGit ( REPO_ROOT ) ;
56- await simpleGit . add ( filesToLint ) ;
57- }
58- } catch ( error ) {
59- errors . push ( error ) ;
60- }
61- }
62- }
177+ const failedChecks = results . filter ( ( result ) => ! result . succeeded ) ;
178+
179+ if ( failedChecks . length > 0 ) {
180+ const errorReport = [
181+ '\nPre-commit checks failed:' ,
182+ ...results . map ( ( result ) => result . toString ( ) ) ,
183+ '\nPlease fix the above issues before committing.' ,
184+ ] . join ( '\n' ) ;
63185
64- if ( errors . length ) {
65- throw combineErrors ( errors ) ;
186+ throw new Error ( errorReport ) ;
66187 }
188+
189+ log . success ( 'All pre-commit checks passed!' ) ;
67190 } ,
68191 {
69192 description : `
77200 stage : true ,
78201 } ,
79202 help : `
80- --fix Execute eslint in --fix mode
203+ --fix Execute checks with possible fixes
81204 --max-files Max files number to check against. If exceeded the script will skip the execution
82205 --ref Run checks against any git ref files (example HEAD or <commit_sha>) instead of running against staged ones
83206 --no-stage By default when using --fix the changes are staged, use --no-stage to disable that behavior
0 commit comments