@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thegetty/quire-cli",
3
3
  "description": "Quire command-line interface",
4
- "version": "1.0.0-rc.33",
4
+ "version": "1.0.0-rc.34",
5
5
  "author": "Getty Digital",
6
6
  "license": "SEE LICENSE IN https://github.com/thegetty/quire/blob/main/LICENSE",
7
7
  "bugs": {
@@ -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
Binary file