@thegetty/quire-cli 1.0.0-rc.37 → 1.0.0-rc.38
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 +3 -2
- package/src/commands/build.js +13 -6
- package/src/commands/clean.js +10 -2
- package/src/commands/clean.spec.js +10 -0
- package/src/commands/clean.test.js +122 -0
- package/src/commands/config.js +3 -1
- package/src/commands/config.test.js +42 -1
- package/src/commands/doctor.js +251 -0
- package/src/commands/doctor.spec.js +115 -0
- package/src/commands/doctor.test.js +1409 -0
- package/src/commands/epub.js +9 -2
- package/src/commands/info.js +79 -71
- package/src/commands/info.spec.js +8 -0
- package/src/commands/info.test.js +173 -76
- package/src/commands/pdf.js +10 -3
- package/src/commands/validate.js +39 -7
- package/src/commands/validate.spec.js +8 -0
- package/src/commands/validate.test.js +144 -0
- package/src/lib/README.md +77 -22
- package/src/lib/conf/README.md +6 -0
- package/src/lib/conf/build-status.js +103 -0
- package/src/lib/conf/build-status.test.js +247 -0
- package/src/lib/conf/defaults.js +14 -0
- package/src/lib/conf/format.js +1 -1
- package/src/lib/conf/schema.js +46 -1
- package/src/lib/constants.js +28 -0
- package/src/lib/doctor/README.md +667 -0
- package/src/lib/doctor/checks/environment/cli-version.js +62 -0
- package/src/lib/doctor/checks/environment/cli-version.test.js +132 -0
- package/src/lib/doctor/checks/environment/git-available.js +77 -0
- package/src/lib/doctor/checks/environment/git-available.test.js +52 -0
- package/src/lib/doctor/checks/environment/index.js +13 -0
- package/src/lib/doctor/checks/environment/node-version.js +66 -0
- package/src/lib/doctor/checks/environment/node-version.test.js +17 -0
- package/src/lib/doctor/checks/environment/npm-available.js +66 -0
- package/src/lib/doctor/checks/environment/npm-available.test.js +52 -0
- package/src/lib/doctor/checks/environment/os-info.js +48 -0
- package/src/lib/doctor/checks/environment/os-info.test.js +81 -0
- package/src/lib/doctor/checks/environment/runtime-info.js +51 -0
- package/src/lib/doctor/checks/environment/runtime-info.test.js +132 -0
- package/src/lib/doctor/checks/outputs/epub-output.js +119 -0
- package/src/lib/doctor/checks/outputs/epub-output.test.js +277 -0
- package/src/lib/doctor/checks/outputs/index.js +10 -0
- package/src/lib/doctor/checks/outputs/pdf-output.js +144 -0
- package/src/lib/doctor/checks/outputs/pdf-output.test.js +377 -0
- package/src/lib/doctor/checks/outputs/stale-build.js +122 -0
- package/src/lib/doctor/checks/outputs/stale-build.test.js +282 -0
- package/src/lib/doctor/checks/project/data-files.js +56 -0
- package/src/lib/doctor/checks/project/data-files.test.js +125 -0
- package/src/lib/doctor/checks/project/dependencies.js +53 -0
- package/src/lib/doctor/checks/project/dependencies.test.js +71 -0
- package/src/lib/doctor/checks/project/index.js +11 -0
- package/src/lib/doctor/checks/project/quire-11ty.js +98 -0
- package/src/lib/doctor/checks/project/quire-11ty.test.js +170 -0
- package/src/lib/doctor/checks/project/quire-project.js +38 -0
- package/src/lib/doctor/checks/project/quire-project.test.js +47 -0
- package/src/lib/doctor/checks/tools/index.js +10 -0
- package/src/lib/doctor/checks/tools/pandoc-available.js +82 -0
- package/src/lib/doctor/checks/tools/pandoc-available.test.js +73 -0
- package/src/lib/doctor/checks/tools/prince-available.js +81 -0
- package/src/lib/doctor/checks/tools/prince-available.test.js +73 -0
- package/src/lib/doctor/constants.js +39 -0
- package/src/lib/doctor/formatDuration.js +108 -0
- package/src/lib/doctor/formatDuration.test.js +76 -0
- package/src/lib/doctor/formatters/human.js +257 -0
- package/src/lib/doctor/formatters/human.test.js +463 -0
- package/src/lib/doctor/formatters/index.js +8 -0
- package/src/lib/doctor/formatters/json.js +78 -0
- package/src/lib/doctor/formatters/json.test.js +174 -0
- package/src/lib/doctor/formatters/shared.js +129 -0
- package/src/lib/doctor/formatters/shared.test.js +194 -0
- package/src/lib/doctor/index.js +271 -0
- package/src/lib/doctor/index.test.js +797 -0
- package/src/lib/platform.js +95 -0
- package/src/lib/project/build.js +44 -25
- package/src/lib/project/build.test.js +30 -8
- package/src/lib/project/detect.js +16 -2
- package/src/lib/project/index.js +18 -2
- package/src/lib/project/output-paths.js +87 -0
- package/src/lib/project/output-paths.test.js +66 -0
- package/src/lib/project/paths.js +48 -0
- package/src/main.js +24 -2
- package/src/packageConfig.js +17 -0
- package/src/validators/validate-data-files.js +154 -0
- package/src/validators/validate-data-files.test.js +217 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data files validation for Quire projects
|
|
3
|
+
*
|
|
4
|
+
* Validates YAML files in content/_data/ directory:
|
|
5
|
+
* - Required files exist (publication.yaml)
|
|
6
|
+
* - YAML syntax is correct
|
|
7
|
+
* - Files conform to their JSON schemas (when schema exists)
|
|
8
|
+
* - No duplicate IDs in arrays
|
|
9
|
+
*
|
|
10
|
+
* @module validators/validate-data-files
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import yaml from 'js-yaml'
|
|
15
|
+
import Ajv from 'ajv'
|
|
16
|
+
import addFormats from 'ajv-formats'
|
|
17
|
+
import { DATA_DIR, REQUIRED_DATA_FILES } from '#lib/project/index.js'
|
|
18
|
+
import { getSchemaForDocument, checkForDuplicateIds } from './utils.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validation result for a single file
|
|
22
|
+
* @typedef {Object} FileValidationResult
|
|
23
|
+
* @property {string} file - Filename
|
|
24
|
+
* @property {boolean} valid - Whether the file is valid
|
|
25
|
+
* @property {string[]} errors - List of error messages
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validation result for all data files
|
|
30
|
+
* @typedef {Object} DataFilesValidationResult
|
|
31
|
+
* @property {boolean} valid - Whether all files are valid
|
|
32
|
+
* @property {string[]} errors - All error messages across all files
|
|
33
|
+
* @property {number} fileCount - Number of YAML files found
|
|
34
|
+
* @property {FileValidationResult[]} files - Per-file validation results
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate a single YAML file
|
|
39
|
+
*
|
|
40
|
+
* @param {string} filePath - Absolute path to the YAML file
|
|
41
|
+
* @returns {FileValidationResult} Validation result
|
|
42
|
+
*/
|
|
43
|
+
export function validateYamlFile(filePath) {
|
|
44
|
+
const errors = []
|
|
45
|
+
const filename = path.basename(filePath)
|
|
46
|
+
|
|
47
|
+
// Check file exists
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
return { file: filename, valid: false, errors: [`File not found: ${filename}`] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Read and parse YAML
|
|
53
|
+
let doc
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
56
|
+
doc = yaml.load(content)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const loc = error.mark ? ` at line ${error.mark.line + 1}` : ''
|
|
59
|
+
errors.push(`${filename}: YAML syntax error${loc} - ${error.reason || error.message}`)
|
|
60
|
+
return { file: filename, valid: false, errors }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate against schema if one exists
|
|
64
|
+
const schema = getSchemaForDocument(filePath)
|
|
65
|
+
if (schema) {
|
|
66
|
+
const ajv = new Ajv({ allErrors: true })
|
|
67
|
+
addFormats(ajv)
|
|
68
|
+
const validate = ajv.compile(schema)
|
|
69
|
+
const valid = validate(doc)
|
|
70
|
+
|
|
71
|
+
if (!valid) {
|
|
72
|
+
for (const err of validate.errors) {
|
|
73
|
+
const errPath = err.instancePath || '(root)'
|
|
74
|
+
errors.push(`${filename}: ${errPath} ${err.message}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for duplicate IDs using the existing utility
|
|
80
|
+
// Note: checkForDuplicateIds throws, so we need to catch
|
|
81
|
+
try {
|
|
82
|
+
checkForDuplicateIds(doc, filePath)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Extract the duplicate ID info from the error
|
|
85
|
+
const match = error.reason?.match(/Duplicate IDs found: (.+)/)
|
|
86
|
+
if (match) {
|
|
87
|
+
errors.push(`${filename}: duplicate IDs found: ${match[1]}`)
|
|
88
|
+
} else {
|
|
89
|
+
errors.push(`${filename}: ${error.reason || error.message}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { file: filename, valid: errors.length === 0, errors }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate all data files in content/_data/
|
|
98
|
+
*
|
|
99
|
+
* @returns {DataFilesValidationResult} Validation result
|
|
100
|
+
*/
|
|
101
|
+
export function validateDataFiles() {
|
|
102
|
+
const allErrors = []
|
|
103
|
+
const fileResults = []
|
|
104
|
+
|
|
105
|
+
// Check if data directory exists
|
|
106
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
107
|
+
return {
|
|
108
|
+
valid: true,
|
|
109
|
+
errors: [],
|
|
110
|
+
fileCount: 0,
|
|
111
|
+
files: [],
|
|
112
|
+
notInProject: true,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check required files exist
|
|
117
|
+
for (const required of REQUIRED_DATA_FILES) {
|
|
118
|
+
const filePath = path.join(DATA_DIR, required)
|
|
119
|
+
if (!fs.existsSync(filePath)) {
|
|
120
|
+
allErrors.push(`Required file missing: ${required}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Find all YAML files
|
|
125
|
+
let yamlFiles = []
|
|
126
|
+
try {
|
|
127
|
+
yamlFiles = fs.readdirSync(DATA_DIR)
|
|
128
|
+
.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'))
|
|
129
|
+
.map((file) => path.join(DATA_DIR, file))
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
errors: [`Cannot read ${DATA_DIR}: ${error.message}`],
|
|
134
|
+
fileCount: 0,
|
|
135
|
+
files: [],
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate each file
|
|
140
|
+
for (const filePath of yamlFiles) {
|
|
141
|
+
const result = validateYamlFile(filePath)
|
|
142
|
+
fileResults.push(result)
|
|
143
|
+
allErrors.push(...result.errors)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
valid: allErrors.length === 0,
|
|
148
|
+
errors: allErrors,
|
|
149
|
+
fileCount: yamlFiles.length,
|
|
150
|
+
files: fileResults,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default validateDataFiles
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import sinon from 'sinon'
|
|
3
|
+
import esmock from 'esmock'
|
|
4
|
+
|
|
5
|
+
test.beforeEach((t) => {
|
|
6
|
+
t.context.sandbox = sinon.createSandbox()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test.afterEach.always((t) => {
|
|
10
|
+
t.context.sandbox.restore()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('validateDataFiles returns notInProject when data dir does not exist', async (t) => {
|
|
14
|
+
const { sandbox } = t.context
|
|
15
|
+
|
|
16
|
+
const { validateDataFiles } = await esmock('./validate-data-files.js', {
|
|
17
|
+
'node:fs': {
|
|
18
|
+
default: {
|
|
19
|
+
existsSync: sandbox.stub().returns(false),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const result = validateDataFiles()
|
|
25
|
+
|
|
26
|
+
t.true(result.valid)
|
|
27
|
+
t.true(result.notInProject)
|
|
28
|
+
t.is(result.fileCount, 0)
|
|
29
|
+
t.deepEqual(result.errors, [])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('validateDataFiles returns error when required file is missing', async (t) => {
|
|
33
|
+
const { sandbox } = t.context
|
|
34
|
+
|
|
35
|
+
const existsStub = sandbox.stub()
|
|
36
|
+
existsStub.withArgs('content/_data').returns(true)
|
|
37
|
+
existsStub.withArgs('content/_data/publication.yaml').returns(false)
|
|
38
|
+
existsStub.returns(false)
|
|
39
|
+
|
|
40
|
+
const path = await import('node:path')
|
|
41
|
+
const { validateDataFiles } = await esmock('./validate-data-files.js', {
|
|
42
|
+
'node:fs': {
|
|
43
|
+
default: {
|
|
44
|
+
existsSync: existsStub,
|
|
45
|
+
readdirSync: sandbox.stub().returns([]),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
'node:path': {
|
|
49
|
+
default: path.default,
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const result = validateDataFiles()
|
|
54
|
+
|
|
55
|
+
t.false(result.valid)
|
|
56
|
+
t.true(result.errors.includes('Required file missing: publication.yaml'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('validateDataFiles validates all YAML files in directory', async (t) => {
|
|
60
|
+
const { sandbox } = t.context
|
|
61
|
+
|
|
62
|
+
const existsStub = sandbox.stub()
|
|
63
|
+
existsStub.withArgs('content/_data').returns(true)
|
|
64
|
+
existsStub.withArgs('content/_data/publication.yaml').returns(true)
|
|
65
|
+
existsStub.returns(true)
|
|
66
|
+
|
|
67
|
+
const readFileStub = sandbox.stub()
|
|
68
|
+
readFileStub.callsFake((filePath) => {
|
|
69
|
+
if (filePath.includes('publication.yaml')) {
|
|
70
|
+
return 'title: Test Publication'
|
|
71
|
+
}
|
|
72
|
+
if (filePath.includes('config.yaml')) {
|
|
73
|
+
return 'setting: value'
|
|
74
|
+
}
|
|
75
|
+
throw new Error('ENOENT')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const path = await import('node:path')
|
|
79
|
+
const { validateDataFiles } = await esmock('./validate-data-files.js', {
|
|
80
|
+
'node:fs': {
|
|
81
|
+
default: {
|
|
82
|
+
existsSync: existsStub,
|
|
83
|
+
readdirSync: sandbox.stub().returns(['publication.yaml', 'config.yaml']),
|
|
84
|
+
readFileSync: readFileStub,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
'node:path': {
|
|
88
|
+
default: path.default,
|
|
89
|
+
},
|
|
90
|
+
'./utils.js': {
|
|
91
|
+
getSchemaForDocument: sandbox.stub().returns(null),
|
|
92
|
+
checkForDuplicateIds: sandbox.stub(),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const result = validateDataFiles()
|
|
97
|
+
|
|
98
|
+
t.true(result.valid)
|
|
99
|
+
t.is(result.fileCount, 2)
|
|
100
|
+
t.is(result.files.length, 2)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('validateYamlFile returns error for invalid YAML syntax', async (t) => {
|
|
104
|
+
const { sandbox } = t.context
|
|
105
|
+
|
|
106
|
+
const path = await import('node:path')
|
|
107
|
+
const { validateYamlFile } = await esmock('./validate-data-files.js', {
|
|
108
|
+
'node:fs': {
|
|
109
|
+
default: {
|
|
110
|
+
existsSync: sandbox.stub().returns(true),
|
|
111
|
+
readFileSync: sandbox.stub().returns('title: [invalid yaml'),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
'node:path': {
|
|
115
|
+
default: path.default,
|
|
116
|
+
},
|
|
117
|
+
'./utils.js': {
|
|
118
|
+
getSchemaForDocument: sandbox.stub().returns(null),
|
|
119
|
+
checkForDuplicateIds: sandbox.stub(),
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const result = validateYamlFile('content/_data/test.yaml')
|
|
124
|
+
|
|
125
|
+
t.false(result.valid)
|
|
126
|
+
t.true(result.errors.some((e) => e.includes('YAML syntax error')))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('validateYamlFile returns error when file not found', async (t) => {
|
|
130
|
+
const { sandbox } = t.context
|
|
131
|
+
|
|
132
|
+
const path = await import('node:path')
|
|
133
|
+
const { validateYamlFile } = await esmock('./validate-data-files.js', {
|
|
134
|
+
'node:fs': {
|
|
135
|
+
default: {
|
|
136
|
+
existsSync: sandbox.stub().returns(false),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
'node:path': {
|
|
140
|
+
default: path.default,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const result = validateYamlFile('content/_data/missing.yaml')
|
|
145
|
+
|
|
146
|
+
t.false(result.valid)
|
|
147
|
+
t.true(result.errors.some((e) => e.includes('File not found')))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('validateYamlFile validates against schema when available', async (t) => {
|
|
151
|
+
const { sandbox } = t.context
|
|
152
|
+
|
|
153
|
+
const schema = {
|
|
154
|
+
type: 'object',
|
|
155
|
+
required: ['title'],
|
|
156
|
+
properties: {
|
|
157
|
+
title: { type: 'string' },
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const path = await import('node:path')
|
|
162
|
+
const { validateYamlFile } = await esmock('./validate-data-files.js', {
|
|
163
|
+
'node:fs': {
|
|
164
|
+
default: {
|
|
165
|
+
existsSync: sandbox.stub().returns(true),
|
|
166
|
+
readFileSync: sandbox.stub().returns('notitle: value'),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
'node:path': {
|
|
170
|
+
default: path.default,
|
|
171
|
+
},
|
|
172
|
+
'./utils.js': {
|
|
173
|
+
getSchemaForDocument: sandbox.stub().returns(schema),
|
|
174
|
+
checkForDuplicateIds: sandbox.stub(),
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const result = validateYamlFile('content/_data/test.yaml')
|
|
179
|
+
|
|
180
|
+
t.false(result.valid)
|
|
181
|
+
t.true(result.errors.some((e) => e.includes('title')))
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('validateYamlFile catches duplicate ID errors', async (t) => {
|
|
185
|
+
const { sandbox } = t.context
|
|
186
|
+
|
|
187
|
+
const path = await import('node:path')
|
|
188
|
+
const { validateYamlFile } = await esmock('./validate-data-files.js', {
|
|
189
|
+
'node:fs': {
|
|
190
|
+
default: {
|
|
191
|
+
existsSync: sandbox.stub().returns(true),
|
|
192
|
+
readFileSync: sandbox.stub().returns('title: Test'),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
'node:path': {
|
|
196
|
+
default: path.default,
|
|
197
|
+
},
|
|
198
|
+
'./utils.js': {
|
|
199
|
+
getSchemaForDocument: sandbox.stub().returns(null),
|
|
200
|
+
checkForDuplicateIds: sandbox.stub().throws({
|
|
201
|
+
reason: 'Duplicate IDs found: fig-1, fig-2',
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const result = validateYamlFile('content/_data/figures.yaml')
|
|
207
|
+
|
|
208
|
+
t.false(result.valid)
|
|
209
|
+
t.true(result.errors.some((e) => e.includes('duplicate IDs')))
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('default export is validateDataFiles', async (t) => {
|
|
213
|
+
const module = await import('./validate-data-files.js')
|
|
214
|
+
|
|
215
|
+
t.is(typeof module.default, 'function')
|
|
216
|
+
t.is(module.default, module.validateDataFiles)
|
|
217
|
+
})
|