@thegetty/quire-cli 1.0.0-rc.33 → 1.0.0-rc.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/validate.js +64 -0
- package/src/errors/validation/validation-error.js +13 -0
- package/src/errors/validation/yaml-duplicate-error.js +11 -0
- package/src/errors/validation/yaml-parse-error.js +11 -0
- package/src/errors/validation/yaml-validation-error.js +11 -0
- package/src/validators/utils.js +98 -0
- package/src/validators/validate-yaml.js +38 -0
- package/patches/.DS_Store +0 -0
- package/src/.DS_Store +0 -0
- package/src/helpers/.DS_Store +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import Command from '#src/Command.js'
|
|
2
|
+
import { YAMLException } from 'js-yaml'
|
|
3
|
+
import YamlValidationError from '../errors/validation/yaml-validation-error.js'
|
|
4
|
+
import fs from 'fs-extra'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { projectRoot } from '#lib/11ty/index.js'
|
|
7
|
+
import testcwd from '../helpers/test-cwd.js'
|
|
8
|
+
import yamlValidation from '../validators/validate-yaml.js'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Quire CLI `validate` Command
|
|
13
|
+
*
|
|
14
|
+
* @class ValidateCommand
|
|
15
|
+
* @extends {Command}
|
|
16
|
+
*/
|
|
17
|
+
export default class ValidateCommand extends Command {
|
|
18
|
+
static definition = {
|
|
19
|
+
name: 'validate',
|
|
20
|
+
description: 'Validate configuration files',
|
|
21
|
+
summary: 'run validation',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
options: [
|
|
24
|
+
[ '--debug', 'run validate with debug output to console' ],
|
|
25
|
+
],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super(ValidateCommand.definition)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
action(options, command){
|
|
33
|
+
if(options.debug) {
|
|
34
|
+
console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dataPath = path.join(projectRoot, 'content', '_data')
|
|
38
|
+
const files = fs.readdirSync(dataPath)
|
|
39
|
+
.filter(file => file.endsWith('.yaml') || file.endsWith('.yml'))
|
|
40
|
+
.map(file => path.join(dataPath, file)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
let errorList = []
|
|
44
|
+
console.log('Validating YAML files..')
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
try {
|
|
48
|
+
yamlValidation(file)
|
|
49
|
+
} catch (error){
|
|
50
|
+
errorList.push(error)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if(errorList.length > 0) {
|
|
55
|
+
errorList.forEach(err => { console.error(`${err.reason}`) })
|
|
56
|
+
} else {
|
|
57
|
+
console.log('Validation complete.')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
preAction(options, command) {
|
|
62
|
+
testcwd(command)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default class ValidationError extends Error {
|
|
2
|
+
constructor(message, {filePath, reason, code} = {}) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.name = this.constructor.name
|
|
5
|
+
this.filePath = filePath
|
|
6
|
+
this.reason = reason
|
|
7
|
+
this.code = code
|
|
8
|
+
|
|
9
|
+
if ('captureStackTrace' in Error) {
|
|
10
|
+
Error.captureStackTrace(this, this.constructor)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ValidationError from './validation-error.js'
|
|
2
|
+
|
|
3
|
+
export default class YamlDuplicateIdError extends ValidationError {
|
|
4
|
+
constructor(filePath, reason) {
|
|
5
|
+
super('Duplicate ID found in YAML file', {
|
|
6
|
+
filePath,
|
|
7
|
+
reason: reason,
|
|
8
|
+
code: 'YAML_DUPLICATE_ID_ERROR',
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ValidationError from './validation-error.js'
|
|
2
|
+
|
|
3
|
+
export default class YamlParseError extends ValidationError {
|
|
4
|
+
constructor(filePath, reason) {
|
|
5
|
+
super('Error parsing YAML file', {
|
|
6
|
+
filePath,
|
|
7
|
+
reason: reason,
|
|
8
|
+
code: 'YAML_PARSE_ERROR',
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ValidationError from './validation-error.js'
|
|
2
|
+
|
|
3
|
+
export default class YamlValidationError extends ValidationError {
|
|
4
|
+
constructor(filePath, reason) {
|
|
5
|
+
super('Error validating YAML file', {
|
|
6
|
+
filePath,
|
|
7
|
+
reason: reason,
|
|
8
|
+
code: 'YAML_VALIDATION_ERROR',
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import YamlDuplicateIdError from '../errors/validation/yaml-duplicate-error.js'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { projectRoot } from '#lib/11ty/index.js'
|
|
6
|
+
|
|
7
|
+
const IMAGE_KEYS = new Set(['src', 'image', 'logo'])
|
|
8
|
+
|
|
9
|
+
export function getSchemaForDocument(file) {
|
|
10
|
+
const schemaName = path.basename(file, path.extname(file))
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const schemaPath = path.join(__dirname,'..','..','schemas', `${schemaName}.schema.json`)
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(fs.readFileSync(schemaPath, 'utf8'))
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.warn(`Warning: No schema found for document ${schemaName} at path: ${schemaPath}.`)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Recursive helper to check image paths in nested figure list structures
|
|
23
|
+
function collectImagePaths(node, paths=[]) {
|
|
24
|
+
if(!node || typeof node !== 'object') return
|
|
25
|
+
|
|
26
|
+
for (const key in node) {
|
|
27
|
+
const value = node[key]
|
|
28
|
+
if (IMAGE_KEYS.has(key) && typeof value === 'string') {
|
|
29
|
+
paths.push(value)
|
|
30
|
+
}
|
|
31
|
+
collectImagePaths(value, paths)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return paths
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateImage(label, src) {
|
|
38
|
+
if(!src) return
|
|
39
|
+
|
|
40
|
+
let assetPath = ''
|
|
41
|
+
if(src.endsWith('.html')) {
|
|
42
|
+
assetPath = path.join(projectRoot, 'content', '_assets', src)
|
|
43
|
+
} else {
|
|
44
|
+
assetPath = path.join(projectRoot, 'content', '_assets', 'images', src)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if(!fs.existsSync(assetPath)) {
|
|
48
|
+
console.warn(`Warning: ${label} source not found at path: ${assetPath}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateImagePaths(doc) {
|
|
53
|
+
validateImage('Cover image', doc?.epub?.defaultCoverImage)
|
|
54
|
+
validateImage('Promo image', doc?.promo_image)
|
|
55
|
+
|
|
56
|
+
for (const figure of doc?.figure_list || []) {
|
|
57
|
+
let paths = []
|
|
58
|
+
const imagePaths = collectImagePaths(figure, paths)
|
|
59
|
+
for (const imgPath of imagePaths) {
|
|
60
|
+
validateImage(`Figure id ${figure.id}`, imgPath)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const publisher of doc?.publisher || []) {
|
|
65
|
+
validateImage('Logo', publisher.logo)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const contributor of doc?.contributor || []) {
|
|
69
|
+
validateImage('Contributor', contributor.image)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lifted from packages/11ty/_plugins/globalData
|
|
75
|
+
* Throws an error if data contains duplicate ids
|
|
76
|
+
* @param {Object|Array} data
|
|
77
|
+
*/
|
|
78
|
+
export const checkForDuplicateIds = function (data, file) {
|
|
79
|
+
if (!data) return
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(data)) {
|
|
82
|
+
if (data.every((item) => Object.hasOwn(item, 'id'))) {
|
|
83
|
+
const duplicates = data.filter((a, index) => {
|
|
84
|
+
return index !== data.findIndex((b) => b.id === a.id)
|
|
85
|
+
})
|
|
86
|
+
if (duplicates.length) {
|
|
87
|
+
const ids = duplicates.map(({ id }) => id)
|
|
88
|
+
throw new YamlDuplicateIdError(file, `Error in ${file}: Duplicate IDs found: ${ids.join(', ')}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof data === 'object') {
|
|
94
|
+
Object.keys(data).forEach((key) => {
|
|
95
|
+
checkForDuplicateIds(data[key], file)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Ajv from 'ajv'
|
|
2
|
+
import addFormats from 'ajv-formats'
|
|
3
|
+
import { validateImagePaths ,getSchemaForDocument, checkForDuplicateIds } from './utils.js'
|
|
4
|
+
import YamlValidationError from '../errors/validation/yaml-validation-error.js'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import yaml from 'js-yaml'
|
|
7
|
+
|
|
8
|
+
export default function yamlValidation(file) {
|
|
9
|
+
|
|
10
|
+
const fileContent = fs.readFileSync(file, 'utf8')
|
|
11
|
+
|
|
12
|
+
let doc
|
|
13
|
+
try {
|
|
14
|
+
doc = yaml.load(fileContent)
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const message = `Error in ${file}: ${error.reason} at line ${error.mark.line} column ${error.mark.column}`
|
|
17
|
+
throw new YamlValidationError(file, `${message}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const schema = getSchemaForDocument(file)
|
|
21
|
+
if(!schema){ return }
|
|
22
|
+
|
|
23
|
+
const ajv = new Ajv({allErrors:true})
|
|
24
|
+
addFormats(ajv)
|
|
25
|
+
const validate = ajv.compile(schema)
|
|
26
|
+
const valid = validate(doc)
|
|
27
|
+
if(!valid) {
|
|
28
|
+
const messages = validate.errors
|
|
29
|
+
.map(err => `${err.instancePath || '(root)'} ${err.message}`)
|
|
30
|
+
.join('\n')
|
|
31
|
+
|
|
32
|
+
const fullMessage = `Error in ${file}:\n${messages}`
|
|
33
|
+
throw new YamlValidationError(file, fullMessage)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validateImagePaths(doc)
|
|
37
|
+
checkForDuplicateIds(doc, file)
|
|
38
|
+
}
|
package/patches/.DS_Store
DELETED
|
Binary file
|
package/src/.DS_Store
DELETED
|
Binary file
|
package/src/helpers/.DS_Store
DELETED
|
Binary file
|