@thegetty/quire-cli 1.0.0-rc.36 → 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 +3 -4
- package/src/commands/build.test.js +2 -7
- 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/preview.js +8 -2
- 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/11ty/api.js +25 -6
- package/src/lib/11ty/cli.js +17 -5
- 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/logger/debug.js +43 -1
- package/src/lib/logger/debug.test.js +128 -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,66 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getPdfOutputPaths, getEpubOutputPaths, getEpubBuildDir } from './output-paths.js'
|
|
4
|
+
|
|
5
|
+
const projectRoot = '/test-project'
|
|
6
|
+
|
|
7
|
+
test('getPdfOutputPaths returns default engine paths', (t) => {
|
|
8
|
+
const paths = getPdfOutputPaths({ projectRoot })
|
|
9
|
+
|
|
10
|
+
t.true(paths.includes(path.join(projectRoot, 'pagedjs.pdf')))
|
|
11
|
+
t.true(paths.includes(path.join(projectRoot, 'prince.pdf')))
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('getPdfOutputPaths includes config-aware path when pdfConfig provided', (t) => {
|
|
15
|
+
const pdfConfig = { outputDir: 'pdf', filename: 'my-publication' }
|
|
16
|
+
const paths = getPdfOutputPaths({ projectRoot, outputDir: '_site', pdfConfig })
|
|
17
|
+
|
|
18
|
+
// Config path should be first
|
|
19
|
+
t.is(paths[0], path.join(projectRoot, '_site', 'pdf', 'my-publication.pdf'))
|
|
20
|
+
// Default paths should still be present
|
|
21
|
+
t.true(paths.includes(path.join(projectRoot, 'pagedjs.pdf')))
|
|
22
|
+
t.true(paths.includes(path.join(projectRoot, 'prince.pdf')))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('getPdfOutputPaths skips config path when pdfConfig is incomplete', (t) => {
|
|
26
|
+
// Missing filename
|
|
27
|
+
const paths = getPdfOutputPaths({ projectRoot, pdfConfig: { outputDir: 'pdf' } })
|
|
28
|
+
|
|
29
|
+
// Should only have default engine paths
|
|
30
|
+
t.is(paths.length, 2)
|
|
31
|
+
t.true(paths.includes(path.join(projectRoot, 'pagedjs.pdf')))
|
|
32
|
+
t.true(paths.includes(path.join(projectRoot, 'prince.pdf')))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('getPdfOutputPaths uses cwd when no projectRoot provided', (t) => {
|
|
36
|
+
const paths = getPdfOutputPaths()
|
|
37
|
+
|
|
38
|
+
t.true(paths.includes(path.join(process.cwd(), 'pagedjs.pdf')))
|
|
39
|
+
t.true(paths.includes(path.join(process.cwd(), 'prince.pdf')))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('getEpubOutputPaths returns default engine paths', (t) => {
|
|
43
|
+
const paths = getEpubOutputPaths({ projectRoot })
|
|
44
|
+
|
|
45
|
+
t.true(paths.includes(path.join(projectRoot, 'epubjs.epub')))
|
|
46
|
+
t.true(paths.includes(path.join(projectRoot, 'pandoc.epub')))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('getEpubOutputPaths uses cwd when no projectRoot provided', (t) => {
|
|
50
|
+
const paths = getEpubOutputPaths()
|
|
51
|
+
|
|
52
|
+
t.true(paths.includes(path.join(process.cwd(), 'epubjs.epub')))
|
|
53
|
+
t.true(paths.includes(path.join(process.cwd(), 'pandoc.epub')))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('getEpubBuildDir returns _epub path', (t) => {
|
|
57
|
+
const dir = getEpubBuildDir({ projectRoot })
|
|
58
|
+
|
|
59
|
+
t.is(dir, path.join(projectRoot, '_epub'))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('getEpubBuildDir uses cwd when no projectRoot provided', (t) => {
|
|
63
|
+
const dir = getEpubBuildDir()
|
|
64
|
+
|
|
65
|
+
t.is(dir, path.join(process.cwd(), '_epub'))
|
|
66
|
+
})
|
package/src/lib/project/paths.js
CHANGED
|
@@ -5,6 +5,54 @@ import path from 'node:path'
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url)
|
|
6
6
|
const __dirname = path.dirname(__filename)
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Source directories that contain content for building.
|
|
10
|
+
*
|
|
11
|
+
* These directories are monitored for changes during preview and are used
|
|
12
|
+
* to determine if a build is stale. Changes to files in these directories
|
|
13
|
+
* indicate the site needs to be rebuilt.
|
|
14
|
+
*
|
|
15
|
+
* @constant {ReadonlyArray<string>}
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Check all source directories for changes
|
|
19
|
+
* for (const dir of SOURCE_DIRECTORIES) {
|
|
20
|
+
* const mtime = getLatestMtime(dir)
|
|
21
|
+
* // ...
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export const SOURCE_DIRECTORIES = Object.freeze([
|
|
25
|
+
'_data',
|
|
26
|
+
'_includes',
|
|
27
|
+
'_layouts',
|
|
28
|
+
'content',
|
|
29
|
+
'static',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Path to data files directory (relative to project root)
|
|
34
|
+
*
|
|
35
|
+
* Contains YAML configuration files for the publication:
|
|
36
|
+
* - publication.yaml - Publication metadata
|
|
37
|
+
* - config.yaml - Site configuration
|
|
38
|
+
* - figures.yaml - Figure definitions
|
|
39
|
+
* - objects.yaml - Object catalog entries
|
|
40
|
+
* - references.yaml - Bibliography entries
|
|
41
|
+
*
|
|
42
|
+
* @constant {string}
|
|
43
|
+
*/
|
|
44
|
+
export const DATA_DIR = 'content/_data'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Required data files that must exist in a Quire project.
|
|
48
|
+
*
|
|
49
|
+
* These files are validated by the doctor command and the validate command.
|
|
50
|
+
* Missing required files will trigger a warning.
|
|
51
|
+
*
|
|
52
|
+
* @constant {ReadonlyArray<string>}
|
|
53
|
+
*/
|
|
54
|
+
export const REQUIRED_DATA_FILES = Object.freeze(['publication.yaml'])
|
|
55
|
+
|
|
8
56
|
/**
|
|
9
57
|
* Project path configuration
|
|
10
58
|
*
|
package/src/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import commands from '#src/commands/index.js'
|
|
10
10
|
import config from '#lib/conf/config.js'
|
|
11
11
|
import { handleError } from '#lib/error/handler.js'
|
|
12
|
+
import reporter from '#lib/reporter/index.js'
|
|
12
13
|
import { docsUrl, DOCS_BASE } from '#helpers/docs-url.js'
|
|
13
14
|
import packageConfig from '#src/packageConfig.js'
|
|
14
15
|
import { enableDebug } from '#lib/logger/debug.js'
|
|
@@ -93,6 +94,14 @@ program.hook('preAction', (thisCommand) => {
|
|
|
93
94
|
}
|
|
94
95
|
})
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Stop the reporter after every command to clear any active setInterval timers
|
|
99
|
+
* (e.g. elapsed time display) that would otherwise keep the event loop alive.
|
|
100
|
+
*/
|
|
101
|
+
program.hook('postAction', () => {
|
|
102
|
+
reporter.stop()
|
|
103
|
+
})
|
|
104
|
+
|
|
96
105
|
/**
|
|
97
106
|
* Register each command as a subcommand of this program
|
|
98
107
|
* @see https://github.com/tj/commander.js?tab=readme-ov-file#automated-help
|
|
@@ -190,12 +199,25 @@ commands.forEach((command) => {
|
|
|
190
199
|
})
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Wrap action in centralized error handler.
|
|
204
|
+
* Using apply() preserves `this` context for `this.debug` and `this.logger`.
|
|
205
|
+
*
|
|
206
|
+
* Nota bene: Commander passes subcommand-local opts as the options argument,
|
|
207
|
+
* but global options (--verbose, --quiet, --debug) defined on the parent
|
|
208
|
+
* program are only available via optsWithGlobals(). We replace the options
|
|
209
|
+
* argument with the merged set so action handlers see all options uniformly.
|
|
210
|
+
*/
|
|
195
211
|
subCommand.action(async (...args) => {
|
|
196
212
|
try {
|
|
213
|
+
// Commander Command instance is the last element in args array
|
|
214
|
+
// @see https://github.com/tj/commander.js#action-handler
|
|
215
|
+
const cmd = args[args.length - 1]
|
|
216
|
+
const mergedOpts = cmd.optsWithGlobals()
|
|
217
|
+
args[args.length - 2] = mergedOpts
|
|
197
218
|
await action.apply(command, args)
|
|
198
219
|
} catch (error) {
|
|
220
|
+
reporter.stop()
|
|
199
221
|
const { debug } = program.opts()
|
|
200
222
|
handleError(error, { debug })
|
|
201
223
|
}
|
package/src/packageConfig.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dirname } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { readPackageUpSync } from 'read-package-up'
|
|
4
|
+
import which from '#helpers/which.js'
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url)
|
|
6
7
|
const __dirname = dirname(__filename)
|
|
@@ -12,4 +13,20 @@ const __dirname = dirname(__filename)
|
|
|
12
13
|
*/
|
|
13
14
|
const { packageJson } = readPackageUpSync({ cwd: __dirname, normalize: true })
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the full filesystem path to the quire CLI executable
|
|
18
|
+
*
|
|
19
|
+
* Uses the `which` helper to find the first `quire` executable in PATH.
|
|
20
|
+
* Returns null if the executable is not found.
|
|
21
|
+
*
|
|
22
|
+
* @returns {string|null} Absolute path to the quire executable, or null
|
|
23
|
+
*/
|
|
24
|
+
export function binPath() {
|
|
25
|
+
try {
|
|
26
|
+
return which('quire')
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export default packageJson
|
|
@@ -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
|
+
})
|