mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2025-12-13 12:36:10 -06:00
* Remove code formatting workflows and related configuration files * Remove formatting instructions from README * Add plugin validator * Add workflow * Limit workflow to plugins/themes directories
163 lines
5.3 KiB
JavaScript
163 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const safeRequire = (name) => {
|
|
try {
|
|
return require(name);
|
|
} catch (error) {
|
|
if (error && error.code === 'MODULE_NOT_FOUND') {
|
|
console.log(`Error: Cannot find module '${name}', have you installed the dependencies?`);
|
|
process.exit(1);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const Ajv = safeRequire('ajv').default;
|
|
const betterAjvErrors = safeRequire('better-ajv-errors').default;
|
|
const chalk = safeRequire('chalk');
|
|
const YAML = safeRequire('yaml');
|
|
const addFormats = safeRequire('ajv-formats');
|
|
|
|
// https://www.peterbe.com/plog/nodejs-fs-walk-or-glob-or-fast-glob
|
|
function walk(directory, ext, filepaths = []) {
|
|
const files = fs.readdirSync(directory);
|
|
for (const filename of files) {
|
|
const filepath = path.join(directory, filename);
|
|
if (fs.statSync(filepath).isDirectory()) {
|
|
walk(filepath, ext, filepaths);
|
|
} else if (path.extname(filename) === ext && !filename.includes('config')) {
|
|
filepaths.push(filepath);
|
|
}
|
|
}
|
|
return filepaths;
|
|
}
|
|
|
|
// https://stackoverflow.com/a/53833620
|
|
const isSorted = arr => arr.every((v,i,a) => !i || a[i-1] <= v);
|
|
|
|
class Validator {
|
|
constructor(flags) {
|
|
this.allowDeprecations = flags.includes('-d');
|
|
this.stopOnError = !flags.includes('-a');
|
|
this.sortedURLs = flags.includes('-s');
|
|
this.verbose = flags.includes('-v');
|
|
|
|
const schemaPath = path.resolve(__dirname, './plugin.schema.json');
|
|
this.schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
this.ajv = new Ajv({
|
|
// allErrors: true,
|
|
allowUnionTypes: true, // Use allowUnionTypes instead of ignoreKeywordsWithRef
|
|
strict: true,
|
|
allowMatchingProperties: true, // Allow properties that match a pattern
|
|
});
|
|
addFormats(this.ajv);
|
|
}
|
|
|
|
run(files) {
|
|
let plugins;
|
|
|
|
if (files && Array.isArray(files) && files.length > 0) {
|
|
plugins = files.map(file => path.resolve(file));
|
|
} else {
|
|
const pluginsDir = path.resolve(__dirname, '../plugins');
|
|
const themesDir = path.resolve(__dirname, '../themes');
|
|
plugins = walk(pluginsDir, '.yml').concat(walk(themesDir, '.yml'));
|
|
}
|
|
|
|
let result = true;
|
|
const validate = this.ajv.compile(this.schema);
|
|
|
|
for (const file of plugins) {
|
|
const relPath = path.relative(process.cwd(), file);
|
|
let contents, data;
|
|
try {
|
|
contents = fs.readFileSync(file, 'utf8');
|
|
data = YAML.parse(contents);
|
|
} catch (error) {
|
|
console.error(`${chalk.red(chalk.bold('ERROR'))} in: ${relPath}:`);
|
|
error.stack = null;
|
|
console.error(error);
|
|
result = result && false;
|
|
if (this.stopOnError) break;
|
|
else continue;
|
|
}
|
|
|
|
let valid = validate(data);
|
|
|
|
// Output validation errors
|
|
if (!valid) {
|
|
const output = betterAjvErrors(this.schema, data, validate.errors, { indent: 2 });
|
|
console.log(output);
|
|
|
|
// Detailed error checks
|
|
validate.errors.forEach(err => {
|
|
switch (err.keyword) {
|
|
case 'required':
|
|
console.error(`${chalk.red('Missing Required Property:')} ${err.params.missingProperty}`);
|
|
break;
|
|
case 'type':
|
|
console.error(`${chalk.red('Type Mismatch:')} ${err.dataPath} should be ${err.params.type}`);
|
|
break;
|
|
case 'pattern':
|
|
console.error(`${chalk.red('Pattern Mismatch:')} ${err.dataPath} should match pattern ${err.params.pattern}`);
|
|
break;
|
|
case 'enum':
|
|
console.error(`${chalk.red('Enum Violation:')} ${err.dataPath} should be one of ${err.params.allowedValues.join(', ')}`);
|
|
break;
|
|
case 'additionalProperties':
|
|
console.error(`${chalk.red('Additional Properties:')} ${err.params.additionalProperty} is not allowed`);
|
|
break;
|
|
case '$ref':
|
|
console.error(`${chalk.red('Invalid Reference:')} ${err.dataPath} ${err.message}`);
|
|
break;
|
|
case 'items':
|
|
console.error(`${chalk.red('Array Item Type Mismatch:')} ${err.dataPath} ${err.message}`);
|
|
break;
|
|
case 'format':
|
|
console.error(`${chalk.red('Invalid Format:')} ${err.dataPath} should match format ${err.params.format}`);
|
|
break;
|
|
default:
|
|
console.error(`${chalk.red('Validation Error:')} ${err.dataPath} ${err.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.verbose || !valid) {
|
|
const validColor = valid ? chalk.green : chalk.red;
|
|
console.log(`${relPath} Valid: ${validColor(valid)}`);
|
|
}
|
|
|
|
result = result && valid;
|
|
|
|
if (!valid && this.stopOnError) break;
|
|
}
|
|
|
|
if (!this.verbose && result) {
|
|
console.log(chalk.green('Validation passed!'));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function main(flags, files) {
|
|
const args = process.argv.slice(2)
|
|
flags = (flags === undefined) ? args.filter(arg => arg.startsWith('-')) : flags;
|
|
files = (files === undefined) ? args.filter(arg => !arg.startsWith('-')) : files;
|
|
const validator = new Validator(flags);
|
|
const result = validator.run(files);
|
|
if (flags.includes('--ci')) {
|
|
process.exit(result ? 0 : 1);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = main;
|
|
module.exports.Validator = Validator; |