@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.
Files changed (85) hide show
  1. package/package.json +3 -2
  2. package/src/commands/build.js +13 -6
  3. package/src/commands/clean.js +10 -2
  4. package/src/commands/clean.spec.js +10 -0
  5. package/src/commands/clean.test.js +122 -0
  6. package/src/commands/config.js +3 -1
  7. package/src/commands/config.test.js +42 -1
  8. package/src/commands/doctor.js +251 -0
  9. package/src/commands/doctor.spec.js +115 -0
  10. package/src/commands/doctor.test.js +1409 -0
  11. package/src/commands/epub.js +9 -2
  12. package/src/commands/info.js +79 -71
  13. package/src/commands/info.spec.js +8 -0
  14. package/src/commands/info.test.js +173 -76
  15. package/src/commands/pdf.js +10 -3
  16. package/src/commands/validate.js +39 -7
  17. package/src/commands/validate.spec.js +8 -0
  18. package/src/commands/validate.test.js +144 -0
  19. package/src/lib/README.md +77 -22
  20. package/src/lib/conf/README.md +6 -0
  21. package/src/lib/conf/build-status.js +103 -0
  22. package/src/lib/conf/build-status.test.js +247 -0
  23. package/src/lib/conf/defaults.js +14 -0
  24. package/src/lib/conf/format.js +1 -1
  25. package/src/lib/conf/schema.js +46 -1
  26. package/src/lib/constants.js +28 -0
  27. package/src/lib/doctor/README.md +667 -0
  28. package/src/lib/doctor/checks/environment/cli-version.js +62 -0
  29. package/src/lib/doctor/checks/environment/cli-version.test.js +132 -0
  30. package/src/lib/doctor/checks/environment/git-available.js +77 -0
  31. package/src/lib/doctor/checks/environment/git-available.test.js +52 -0
  32. package/src/lib/doctor/checks/environment/index.js +13 -0
  33. package/src/lib/doctor/checks/environment/node-version.js +66 -0
  34. package/src/lib/doctor/checks/environment/node-version.test.js +17 -0
  35. package/src/lib/doctor/checks/environment/npm-available.js +66 -0
  36. package/src/lib/doctor/checks/environment/npm-available.test.js +52 -0
  37. package/src/lib/doctor/checks/environment/os-info.js +48 -0
  38. package/src/lib/doctor/checks/environment/os-info.test.js +81 -0
  39. package/src/lib/doctor/checks/environment/runtime-info.js +51 -0
  40. package/src/lib/doctor/checks/environment/runtime-info.test.js +132 -0
  41. package/src/lib/doctor/checks/outputs/epub-output.js +119 -0
  42. package/src/lib/doctor/checks/outputs/epub-output.test.js +277 -0
  43. package/src/lib/doctor/checks/outputs/index.js +10 -0
  44. package/src/lib/doctor/checks/outputs/pdf-output.js +144 -0
  45. package/src/lib/doctor/checks/outputs/pdf-output.test.js +377 -0
  46. package/src/lib/doctor/checks/outputs/stale-build.js +122 -0
  47. package/src/lib/doctor/checks/outputs/stale-build.test.js +282 -0
  48. package/src/lib/doctor/checks/project/data-files.js +56 -0
  49. package/src/lib/doctor/checks/project/data-files.test.js +125 -0
  50. package/src/lib/doctor/checks/project/dependencies.js +53 -0
  51. package/src/lib/doctor/checks/project/dependencies.test.js +71 -0
  52. package/src/lib/doctor/checks/project/index.js +11 -0
  53. package/src/lib/doctor/checks/project/quire-11ty.js +98 -0
  54. package/src/lib/doctor/checks/project/quire-11ty.test.js +170 -0
  55. package/src/lib/doctor/checks/project/quire-project.js +38 -0
  56. package/src/lib/doctor/checks/project/quire-project.test.js +47 -0
  57. package/src/lib/doctor/checks/tools/index.js +10 -0
  58. package/src/lib/doctor/checks/tools/pandoc-available.js +82 -0
  59. package/src/lib/doctor/checks/tools/pandoc-available.test.js +73 -0
  60. package/src/lib/doctor/checks/tools/prince-available.js +81 -0
  61. package/src/lib/doctor/checks/tools/prince-available.test.js +73 -0
  62. package/src/lib/doctor/constants.js +39 -0
  63. package/src/lib/doctor/formatDuration.js +108 -0
  64. package/src/lib/doctor/formatDuration.test.js +76 -0
  65. package/src/lib/doctor/formatters/human.js +257 -0
  66. package/src/lib/doctor/formatters/human.test.js +463 -0
  67. package/src/lib/doctor/formatters/index.js +8 -0
  68. package/src/lib/doctor/formatters/json.js +78 -0
  69. package/src/lib/doctor/formatters/json.test.js +174 -0
  70. package/src/lib/doctor/formatters/shared.js +129 -0
  71. package/src/lib/doctor/formatters/shared.test.js +194 -0
  72. package/src/lib/doctor/index.js +271 -0
  73. package/src/lib/doctor/index.test.js +797 -0
  74. package/src/lib/platform.js +95 -0
  75. package/src/lib/project/build.js +44 -25
  76. package/src/lib/project/build.test.js +30 -8
  77. package/src/lib/project/detect.js +16 -2
  78. package/src/lib/project/index.js +18 -2
  79. package/src/lib/project/output-paths.js +87 -0
  80. package/src/lib/project/output-paths.test.js +66 -0
  81. package/src/lib/project/paths.js +48 -0
  82. package/src/main.js +24 -2
  83. package/src/packageConfig.js +17 -0
  84. package/src/validators/validate-data-files.js +154 -0
  85. 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
+ })