@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.37
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/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/bin/cli.js +19 -1
- package/package.json +21 -8
- package/patches/README.md +19 -0
- package/patches/install-npm-version+1.0.9.patch +12119 -0
- package/schemas/objects.schema.json +12 -38
- package/schemas/publication.schema.json +0 -22
- package/schemas/references.schema.json +0 -1
- package/src/Command.js +26 -6
- package/src/Command.spec.js +99 -0
- package/src/commands/README.md +213 -122
- package/src/commands/build.js +37 -36
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +397 -0
- package/src/commands/clean.js +25 -19
- package/src/commands/clean.spec.js +108 -0
- package/src/commands/clean.test.js +260 -0
- package/src/commands/config.js +251 -0
- package/src/commands/config.spec.js +107 -0
- package/src/commands/config.test.js +715 -0
- package/src/commands/create.js +42 -14
- package/src/commands/create.spec.js +112 -0
- package/src/commands/create.test.js +415 -0
- package/src/commands/epub.js +59 -30
- package/src/commands/epub.spec.js +114 -0
- package/src/commands/epub.test.js +503 -0
- package/src/commands/index.js +9 -2
- package/src/commands/info.js +18 -24
- package/src/commands/info.spec.js +64 -0
- package/src/commands/info.test.js +415 -0
- package/src/commands/pdf.js +58 -84
- package/src/commands/pdf.spec.js +114 -0
- package/src/commands/pdf.test.js +464 -0
- package/src/commands/preview.js +32 -31
- package/src/commands/preview.spec.js +97 -0
- package/src/commands/preview.test.js +250 -0
- package/src/commands/use.js +56 -0
- package/src/commands/use.spec.js +61 -0
- package/src/commands/use.test.js +280 -0
- package/src/commands/validate.js +31 -19
- package/src/commands/validate.spec.js +82 -0
- package/src/commands/validate.test.js +234 -0
- package/src/commands/workflows.js +70 -0
- package/src/errors/build/build-failed-error.js +19 -0
- package/src/errors/build/config-field-missing-error.js +20 -0
- package/src/errors/build/config-file-not-found-error.js +20 -0
- package/src/errors/build/index.js +11 -0
- package/src/errors/index.js +48 -0
- package/src/errors/install/dependency-install-error.js +19 -0
- package/src/errors/install/directory-not-empty-error.js +25 -0
- package/src/errors/install/index.js +12 -0
- package/src/errors/install/invalid-path-error.js +31 -0
- package/src/errors/install/invalid-starter-error.js +27 -0
- package/src/errors/install/version-not-found-error.js +35 -0
- package/src/errors/output/epub-generation-error.js +19 -0
- package/src/errors/output/index.js +14 -0
- package/src/errors/output/invalid-epub-library-error.js +20 -0
- package/src/errors/output/invalid-pdf-library-error.js +20 -0
- package/src/errors/output/missing-build-output-error.js +21 -0
- package/src/errors/output/pdf-generation-error.js +24 -0
- package/src/errors/output/tool-not-found-error.js +37 -0
- package/src/errors/project/index.js +10 -0
- package/src/errors/project/not-in-project-error.js +20 -0
- package/src/errors/project/project-create-error.js +21 -0
- package/src/errors/quire-error.js +27 -0
- package/src/errors/validation/validation-error.js +20 -11
- package/src/helpers/clean.js +1 -1
- package/src/helpers/docs-url.js +32 -0
- package/src/helpers/test-cwd.js +5 -6
- package/src/helpers/test-cwd.test.js +192 -0
- package/src/helpers/which.js +10 -4
- package/src/lib/11ty/README.md +135 -19
- package/src/lib/11ty/api.js +196 -94
- package/src/lib/11ty/cli.js +91 -37
- package/src/lib/11ty/index.js +64 -5
- package/src/lib/11ty/index.test.js +655 -0
- package/src/lib/README.md +275 -0
- package/src/lib/commander/index.js +100 -0
- package/src/lib/commander/index.test.js +86 -0
- package/src/lib/commander/options.js +195 -0
- package/src/lib/commander/options.test.js +109 -0
- package/src/lib/conf/README.md +84 -73
- package/src/lib/conf/config.js +5 -3
- package/src/lib/conf/config.test.js +281 -0
- package/src/lib/conf/defaults.js +44 -0
- package/src/lib/conf/format.js +60 -0
- package/src/lib/conf/format.test.js +106 -0
- package/src/lib/conf/helpers.js +91 -0
- package/src/lib/conf/helpers.test.js +136 -0
- package/src/lib/conf/index.js +22 -0
- package/src/lib/conf/schema.js +53 -8
- package/src/lib/epub/README.md +133 -2
- package/src/lib/epub/engines.js +46 -0
- package/src/lib/epub/epub.js +36 -11
- package/src/lib/epub/index.js +111 -21
- package/src/lib/epub/index.test.js +518 -0
- package/src/lib/epub/pandoc.js +33 -4
- package/src/lib/epub/pandoc.test.js +122 -0
- package/src/lib/epub/schema.js +21 -0
- package/src/lib/error/README.md +170 -0
- package/src/lib/error/handler.js +107 -0
- package/src/lib/git/README.md +151 -2
- package/src/lib/git/index.js +217 -13
- package/src/lib/git/index.spec.js +80 -0
- package/src/lib/git/index.test.js +453 -0
- package/src/lib/installer/index.js +309 -0
- package/src/lib/installer/index.spec.js +83 -0
- package/src/lib/installer/index.test.js +545 -0
- package/src/lib/logger/README.md +424 -0
- package/src/lib/logger/debug.js +132 -0
- package/src/lib/logger/debug.spec.js +130 -0
- package/src/lib/logger/debug.test.js +128 -0
- package/src/lib/logger/index.js +228 -0
- package/src/lib/logger/index.spec.js +131 -0
- package/src/lib/logger/index.test.js +477 -0
- package/src/lib/npm/README.md +127 -0
- package/src/lib/npm/index.js +198 -0
- package/src/lib/npm/index.spec.js +60 -0
- package/src/lib/npm/index.test.js +355 -0
- package/src/lib/pdf/README.md +131 -0
- package/src/lib/pdf/engines.js +46 -0
- package/src/lib/pdf/index.js +124 -21
- package/src/lib/pdf/index.test.js +708 -0
- package/src/lib/pdf/paged.js +100 -52
- package/src/lib/pdf/paged.test.js +366 -0
- package/src/lib/pdf/prince.js +115 -37
- package/src/lib/pdf/prince.test.js +202 -0
- package/src/lib/pdf/schema.js +21 -0
- package/src/lib/pdf/split.js +61 -33
- package/src/lib/pdf/split.test.js +445 -0
- package/src/lib/process/manager.js +110 -0
- package/src/lib/process/manager.test.js +55 -0
- package/src/lib/project/build.js +143 -0
- package/src/lib/project/build.test.js +253 -0
- package/src/lib/project/config.js +48 -0
- package/src/lib/project/config.test.js +134 -0
- package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
- package/src/lib/project/detect.test.js +157 -0
- package/src/lib/project/index.js +40 -0
- package/src/lib/project/paths.js +224 -0
- package/src/lib/project/version.js +110 -0
- package/src/lib/project/version.test.js +350 -0
- package/src/lib/reporter/README.md +211 -2
- package/src/lib/reporter/index.js +512 -0
- package/src/lib/reporter/index.test.js +593 -0
- package/src/main.js +134 -47
- package/src/main.spec.js +61 -0
- package/src/main.test.js +347 -0
- package/src/validators/utils.js +2 -1
- package/src/commands/conf.js +0 -43
- package/src/commands/version.js +0 -43
- package/src/lib/11ty/paths.js +0 -103
- package/src/lib/i18n/README.md +0 -3
- package/src/lib/i18n/config.js +0 -53
- package/src/lib/i18n/index.js +0 -43
- package/src/lib/i18n/localeService.js +0 -58
- package/src/lib/quire/README.md +0 -19
- package/src/lib/quire/index.js +0 -350
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build detection module
|
|
3
|
+
*
|
|
4
|
+
* Detects if build output exists and provides build status information.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/project/build
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import paths from './paths.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get build output information
|
|
14
|
+
*
|
|
15
|
+
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
16
|
+
* @returns {Object} Build status information
|
|
17
|
+
*/
|
|
18
|
+
export function getBuildInfo(projectRoot = paths.getProjectRoot()) {
|
|
19
|
+
const sitePath = path.join(projectRoot, '_site')
|
|
20
|
+
const epubPath = path.join(projectRoot, '_epub')
|
|
21
|
+
const pdfFiles = ['pagedjs.pdf', 'prince.pdf']
|
|
22
|
+
|
|
23
|
+
const info = {
|
|
24
|
+
site: {
|
|
25
|
+
exists: fs.existsSync(sitePath),
|
|
26
|
+
path: sitePath,
|
|
27
|
+
mtime: null,
|
|
28
|
+
},
|
|
29
|
+
epub: {
|
|
30
|
+
exists: fs.existsSync(epubPath),
|
|
31
|
+
path: epubPath,
|
|
32
|
+
mtime: null,
|
|
33
|
+
},
|
|
34
|
+
pdf: {
|
|
35
|
+
exists: false,
|
|
36
|
+
paths: [],
|
|
37
|
+
mtime: null,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get modification times if directories exist
|
|
42
|
+
if (info.site.exists) {
|
|
43
|
+
info.site.mtime = fs.statSync(sitePath).mtime
|
|
44
|
+
}
|
|
45
|
+
if (info.epub.exists) {
|
|
46
|
+
info.epub.mtime = fs.statSync(epubPath).mtime
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for PDF files
|
|
50
|
+
for (const file of pdfFiles) {
|
|
51
|
+
const pdfPath = path.join(projectRoot, file)
|
|
52
|
+
if (fs.existsSync(pdfPath)) {
|
|
53
|
+
info.pdf.exists = true
|
|
54
|
+
info.pdf.paths.push(pdfPath)
|
|
55
|
+
// Use most recent mtime if multiple PDFs exist
|
|
56
|
+
const mtime = fs.statSync(pdfPath).mtime
|
|
57
|
+
if (!info.pdf.mtime || mtime > info.pdf.mtime) {
|
|
58
|
+
info.pdf.mtime = mtime
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return info
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if site build output exists
|
|
68
|
+
*
|
|
69
|
+
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
70
|
+
* @returns {boolean} True if _site directory exists
|
|
71
|
+
*/
|
|
72
|
+
export function hasSiteOutput(projectRoot = paths.getProjectRoot()) {
|
|
73
|
+
const sitePath = path.join(projectRoot, '_site')
|
|
74
|
+
return fs.existsSync(sitePath)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if epub build output exists
|
|
79
|
+
*
|
|
80
|
+
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
81
|
+
* @returns {boolean} True if _epub directory exists
|
|
82
|
+
*/
|
|
83
|
+
export function hasEpubOutput(projectRoot = paths.getProjectRoot()) {
|
|
84
|
+
const epubPath = path.join(projectRoot, '_epub')
|
|
85
|
+
return fs.existsSync(epubPath)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if PDF output exists
|
|
90
|
+
*
|
|
91
|
+
* Checks for PDF output at common locations:
|
|
92
|
+
* - Default library outputs: pagedjs.pdf, prince.pdf
|
|
93
|
+
* - Specific library output when lib option provided
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} [options]
|
|
96
|
+
* @param {string} [options.lib] - Specific library to check ('pagedjs' or 'prince')
|
|
97
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
98
|
+
* @returns {boolean} True if PDF output exists
|
|
99
|
+
*/
|
|
100
|
+
export function hasPdfOutput(options = {}) {
|
|
101
|
+
const { lib, projectRoot = paths.getProjectRoot() } = options
|
|
102
|
+
|
|
103
|
+
if (lib) {
|
|
104
|
+
const pdfPath = path.join(projectRoot, `${lib}.pdf`)
|
|
105
|
+
return fs.existsSync(pdfPath)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for any common PDF output
|
|
109
|
+
const pdfFiles = ['pagedjs.pdf', 'prince.pdf']
|
|
110
|
+
return pdfFiles.some((file) => fs.existsSync(path.join(projectRoot, file)))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Require build output to exist, throwing an error if missing
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} options
|
|
117
|
+
* @param {string} [options.type='site'] - Output type to check ('site' or 'epub')
|
|
118
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
119
|
+
* @throws {Error} If required build output does not exist
|
|
120
|
+
*/
|
|
121
|
+
export function requireBuildOutput(options = {}) {
|
|
122
|
+
const { type = 'site', projectRoot = paths.getProjectRoot() } = options
|
|
123
|
+
|
|
124
|
+
if (type === 'site' && !hasSiteOutput(projectRoot)) {
|
|
125
|
+
const error = new Error('Build output not found. Run "quire build" first.')
|
|
126
|
+
error.code = 'ENOBUILD'
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (type === 'epub' && !hasEpubOutput(projectRoot)) {
|
|
131
|
+
const error = new Error('EPUB output not found. Run "quire build" first.')
|
|
132
|
+
error.code = 'ENOBUILD'
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default {
|
|
138
|
+
getBuildInfo,
|
|
139
|
+
hasEpubOutput,
|
|
140
|
+
hasPdfOutput,
|
|
141
|
+
hasSiteOutput,
|
|
142
|
+
requireBuildOutput,
|
|
143
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
4
|
+
import esmock from 'esmock'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper to create cross-platform paths for memfs
|
|
8
|
+
* memfs uses forward slashes internally, but we need to pass
|
|
9
|
+
* platform-native paths to the functions under test
|
|
10
|
+
*/
|
|
11
|
+
const testRoot = '/test-project'
|
|
12
|
+
const nativePath = (...segments) => path.join(testRoot, ...segments)
|
|
13
|
+
const memfsPath = (...segments) => [testRoot, ...segments].join('/')
|
|
14
|
+
|
|
15
|
+
test.beforeEach((t) => {
|
|
16
|
+
// Create in-memory file system
|
|
17
|
+
t.context.vol = new Volume()
|
|
18
|
+
t.context.fs = createFsFromVolume(t.context.vol)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test.afterEach.always((t) => {
|
|
22
|
+
t.context.vol.reset()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('hasSiteOutput returns true when _site exists', async (t) => {
|
|
26
|
+
const { vol } = t.context
|
|
27
|
+
|
|
28
|
+
vol.fromJSON({
|
|
29
|
+
[memfsPath('_site', 'index.html')]: '<html></html>',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const { hasSiteOutput } = await esmock('./build.js', {
|
|
33
|
+
'node:fs': createFsFromVolume(vol),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
t.true(hasSiteOutput(testRoot))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('hasSiteOutput returns false when _site does not exist', async (t) => {
|
|
40
|
+
const { vol } = t.context
|
|
41
|
+
|
|
42
|
+
vol.fromJSON({
|
|
43
|
+
[memfsPath('.quire')]: '',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const { hasSiteOutput } = await esmock('./build.js', {
|
|
47
|
+
'node:fs': createFsFromVolume(vol),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
t.false(hasSiteOutput(testRoot))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('hasEpubOutput returns true when _epub exists', async (t) => {
|
|
54
|
+
const { vol } = t.context
|
|
55
|
+
|
|
56
|
+
vol.fromJSON({
|
|
57
|
+
[memfsPath('_epub', 'content.opf')]: '<package></package>',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const { hasEpubOutput } = await esmock('./build.js', {
|
|
61
|
+
'node:fs': createFsFromVolume(vol),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
t.true(hasEpubOutput(testRoot))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('hasEpubOutput returns false when _epub does not exist', async (t) => {
|
|
68
|
+
const { vol } = t.context
|
|
69
|
+
|
|
70
|
+
vol.fromJSON({
|
|
71
|
+
[memfsPath('.quire')]: '',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const { hasEpubOutput } = await esmock('./build.js', {
|
|
75
|
+
'node:fs': createFsFromVolume(vol),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
t.false(hasEpubOutput(testRoot))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('getBuildInfo returns correct status for existing builds', async (t) => {
|
|
82
|
+
const { vol } = t.context
|
|
83
|
+
|
|
84
|
+
vol.fromJSON({
|
|
85
|
+
[memfsPath('_site', 'index.html')]: '<html></html>',
|
|
86
|
+
[memfsPath('_epub', 'content.opf')]: '<package></package>',
|
|
87
|
+
[memfsPath('pagedjs.pdf')]: '%PDF-1.4',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const { getBuildInfo } = await esmock('./build.js', {
|
|
91
|
+
'node:fs': createFsFromVolume(vol),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const info = getBuildInfo(testRoot)
|
|
95
|
+
|
|
96
|
+
t.true(info.site.exists)
|
|
97
|
+
t.is(info.site.path, nativePath('_site'))
|
|
98
|
+
t.truthy(info.site.mtime)
|
|
99
|
+
|
|
100
|
+
t.true(info.epub.exists)
|
|
101
|
+
t.is(info.epub.path, nativePath('_epub'))
|
|
102
|
+
t.truthy(info.epub.mtime)
|
|
103
|
+
|
|
104
|
+
t.true(info.pdf.exists)
|
|
105
|
+
t.deepEqual(info.pdf.paths, [nativePath('pagedjs.pdf')])
|
|
106
|
+
t.truthy(info.pdf.mtime)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('getBuildInfo returns correct status for missing builds', async (t) => {
|
|
110
|
+
const { vol } = t.context
|
|
111
|
+
|
|
112
|
+
vol.fromJSON({
|
|
113
|
+
[memfsPath('.quire')]: '',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const { getBuildInfo } = await esmock('./build.js', {
|
|
117
|
+
'node:fs': createFsFromVolume(vol),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const info = getBuildInfo(testRoot)
|
|
121
|
+
|
|
122
|
+
t.false(info.site.exists)
|
|
123
|
+
t.is(info.site.mtime, null)
|
|
124
|
+
|
|
125
|
+
t.false(info.epub.exists)
|
|
126
|
+
t.is(info.epub.mtime, null)
|
|
127
|
+
|
|
128
|
+
t.false(info.pdf.exists)
|
|
129
|
+
t.deepEqual(info.pdf.paths, [])
|
|
130
|
+
t.is(info.pdf.mtime, null)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('getBuildInfo returns multiple PDF paths when both exist', async (t) => {
|
|
134
|
+
const { vol } = t.context
|
|
135
|
+
|
|
136
|
+
vol.fromJSON({
|
|
137
|
+
[memfsPath('pagedjs.pdf')]: '%PDF-1.4',
|
|
138
|
+
[memfsPath('prince.pdf')]: '%PDF-1.4',
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const { getBuildInfo } = await esmock('./build.js', {
|
|
142
|
+
'node:fs': createFsFromVolume(vol),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const info = getBuildInfo(testRoot)
|
|
146
|
+
|
|
147
|
+
t.true(info.pdf.exists)
|
|
148
|
+
t.is(info.pdf.paths.length, 2)
|
|
149
|
+
t.true(info.pdf.paths.includes(nativePath('pagedjs.pdf')))
|
|
150
|
+
t.true(info.pdf.paths.includes(nativePath('prince.pdf')))
|
|
151
|
+
t.truthy(info.pdf.mtime)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('requireBuildOutput throws when site output missing', async (t) => {
|
|
155
|
+
const { vol } = t.context
|
|
156
|
+
|
|
157
|
+
vol.fromJSON({
|
|
158
|
+
[memfsPath('.quire')]: '',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const { requireBuildOutput } = await esmock('./build.js', {
|
|
162
|
+
'node:fs': createFsFromVolume(vol),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const error = t.throws(() => {
|
|
166
|
+
requireBuildOutput({ type: 'site', projectRoot: testRoot })
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
t.is(error.code, 'ENOBUILD')
|
|
170
|
+
t.regex(error.message, /quire build/)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('requireBuildOutput throws when epub output missing', async (t) => {
|
|
174
|
+
const { vol } = t.context
|
|
175
|
+
|
|
176
|
+
vol.fromJSON({
|
|
177
|
+
[memfsPath('.quire')]: '',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const { requireBuildOutput } = await esmock('./build.js', {
|
|
181
|
+
'node:fs': createFsFromVolume(vol),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const error = t.throws(() => {
|
|
185
|
+
requireBuildOutput({ type: 'epub', projectRoot: testRoot })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
t.is(error.code, 'ENOBUILD')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('requireBuildOutput does not throw when output exists', async (t) => {
|
|
192
|
+
const { vol } = t.context
|
|
193
|
+
|
|
194
|
+
vol.fromJSON({
|
|
195
|
+
[memfsPath('_site', 'index.html')]: '<html></html>',
|
|
196
|
+
[memfsPath('_epub', 'content.opf')]: '<package></package>',
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const { requireBuildOutput } = await esmock('./build.js', {
|
|
200
|
+
'node:fs': createFsFromVolume(vol),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
t.notThrows(() => {
|
|
204
|
+
requireBuildOutput({ type: 'site', projectRoot: testRoot })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
t.notThrows(() => {
|
|
208
|
+
requireBuildOutput({ type: 'epub', projectRoot: testRoot })
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('hasPdfOutput returns true when specific lib PDF exists', async (t) => {
|
|
213
|
+
const { vol } = t.context
|
|
214
|
+
|
|
215
|
+
vol.fromJSON({
|
|
216
|
+
[memfsPath('pagedjs.pdf')]: '%PDF-1.4',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const { hasPdfOutput } = await esmock('./build.js', {
|
|
220
|
+
'node:fs': createFsFromVolume(vol),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
t.true(hasPdfOutput({ lib: 'pagedjs', projectRoot: testRoot }))
|
|
224
|
+
t.false(hasPdfOutput({ lib: 'prince', projectRoot: testRoot }))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('hasPdfOutput returns false when no PDF exists', async (t) => {
|
|
228
|
+
const { vol } = t.context
|
|
229
|
+
|
|
230
|
+
vol.fromJSON({
|
|
231
|
+
[memfsPath('.quire')]: '',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const { hasPdfOutput } = await esmock('./build.js', {
|
|
235
|
+
'node:fs': createFsFromVolume(vol),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
t.false(hasPdfOutput({ projectRoot: testRoot }))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('hasPdfOutput returns true when any common PDF exists', async (t) => {
|
|
242
|
+
const { vol } = t.context
|
|
243
|
+
|
|
244
|
+
vol.fromJSON({
|
|
245
|
+
[memfsPath('prince.pdf')]: '%PDF-1.4',
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const { hasPdfOutput } = await esmock('./build.js', {
|
|
249
|
+
'node:fs': createFsFromVolume(vol),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
t.true(hasPdfOutput({ projectRoot: testRoot }))
|
|
253
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project configuration module
|
|
3
|
+
*
|
|
4
|
+
* Loads and validates Quire project configuration files.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/project/config
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs-extra'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { pathToFileURL } from 'node:url'
|
|
11
|
+
import yaml from 'js-yaml'
|
|
12
|
+
import paths from './paths.js'
|
|
13
|
+
import { ConfigFileNotFoundError } from '#src/errors/index.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load and validate Quire project configuration
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [projectRoot] - project root directory (defaults to cwd)
|
|
19
|
+
* @returns {Promise<Object>} validated configuration object
|
|
20
|
+
*
|
|
21
|
+
* @todo consider hardcoding a version check against .quire / project's package.json ver
|
|
22
|
+
*/
|
|
23
|
+
export async function loadProjectConfig(projectRoot) {
|
|
24
|
+
const root = projectRoot || paths.getProjectRoot()
|
|
25
|
+
const configPath = path.join(root, 'content', '_data', 'config.yaml')
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(configPath)) {
|
|
28
|
+
throw new ConfigFileNotFoundError(configPath)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const data = fs.readFileSync(configPath)
|
|
32
|
+
let config = yaml.load(data)
|
|
33
|
+
|
|
34
|
+
// NB: Schemas and validators are specific to the 11ty version of the project being built
|
|
35
|
+
const schemaPath = path.join(root, '_plugins', 'schemas', 'config.json')
|
|
36
|
+
const validatorPath = path.join(root, '_plugins', 'globalData', 'validator.js')
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(schemaPath) && fs.existsSync(validatorPath)) {
|
|
39
|
+
const { validateUserConfig } = await import(pathToFileURL(validatorPath))
|
|
40
|
+
|
|
41
|
+
const schemaJSON = fs.readFileSync(schemaPath)
|
|
42
|
+
const schema = JSON.parse(schemaJSON)
|
|
43
|
+
|
|
44
|
+
config = validateUserConfig('config', config, { config: schema })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return config
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
3
|
+
import sinon from 'sinon'
|
|
4
|
+
import esmock from 'esmock'
|
|
5
|
+
|
|
6
|
+
test.beforeEach((t) => {
|
|
7
|
+
// Create sinon sandbox for mocking
|
|
8
|
+
t.context.sandbox = sinon.createSandbox()
|
|
9
|
+
|
|
10
|
+
// Create in-memory file system
|
|
11
|
+
t.context.vol = new Volume()
|
|
12
|
+
t.context.fs = createFsFromVolume(t.context.vol)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test.afterEach.always((t) => {
|
|
16
|
+
// Restore all mocks
|
|
17
|
+
if (t.context.sandbox) {
|
|
18
|
+
t.context.sandbox.restore()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Clear in-memory file system
|
|
22
|
+
t.context.vol.reset()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('loadProjectConfig should throw ConfigFileNotFoundError when config file does not exist', async (t) => {
|
|
26
|
+
const { fs, vol } = t.context
|
|
27
|
+
|
|
28
|
+
// Setup empty directory
|
|
29
|
+
vol.fromJSON({
|
|
30
|
+
'/project/content/_data/.gitkeep': ''
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const { loadProjectConfig } = await esmock('./config.js', {
|
|
34
|
+
'fs-extra': {
|
|
35
|
+
existsSync: (p) => fs.existsSync(p),
|
|
36
|
+
readFileSync: (p) => fs.readFileSync(p)
|
|
37
|
+
},
|
|
38
|
+
'./paths.js': {
|
|
39
|
+
default: {
|
|
40
|
+
getProjectRoot: () => '/project'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const error = await t.throwsAsync(() => loadProjectConfig('/project'))
|
|
46
|
+
|
|
47
|
+
t.is(error.code, 'CONFIG_FILE_NOT_FOUND')
|
|
48
|
+
t.true(error.message.includes('config.yaml'), 'error should include config path')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('loadProjectConfig should load and parse YAML config file', async (t) => {
|
|
52
|
+
const { fs, vol } = t.context
|
|
53
|
+
|
|
54
|
+
// Setup config file
|
|
55
|
+
vol.fromJSON({
|
|
56
|
+
'/project/content/_data/config.yaml': 'title: Test Project\nauthor: Test Author'
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Mock fs-extra to use memfs
|
|
60
|
+
const { loadProjectConfig } = await esmock('./config.js', {
|
|
61
|
+
'fs-extra': {
|
|
62
|
+
existsSync: (p) => fs.existsSync(p),
|
|
63
|
+
readFileSync: (p) => fs.readFileSync(p)
|
|
64
|
+
},
|
|
65
|
+
'./paths.js': {
|
|
66
|
+
default: {
|
|
67
|
+
getProjectRoot: () => '/project'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const result = await loadProjectConfig('/project')
|
|
73
|
+
|
|
74
|
+
t.truthy(result, 'should return config object')
|
|
75
|
+
t.is(result.title, 'Test Project', 'should parse title')
|
|
76
|
+
t.is(result.author, 'Test Author', 'should parse author')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('loadProjectConfig should load PDF configuration', async (t) => {
|
|
80
|
+
const { fs, vol } = t.context
|
|
81
|
+
|
|
82
|
+
// Setup config file with PDF settings
|
|
83
|
+
vol.fromJSON({
|
|
84
|
+
'/project/content/_data/config.yaml': `title: Test Project
|
|
85
|
+
pdf:
|
|
86
|
+
filename: test-book
|
|
87
|
+
outputDir: _pdf`
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Mock fs-extra to use memfs
|
|
91
|
+
const { loadProjectConfig } = await esmock('./config.js', {
|
|
92
|
+
'fs-extra': {
|
|
93
|
+
existsSync: (p) => fs.existsSync(p),
|
|
94
|
+
readFileSync: (p) => fs.readFileSync(p)
|
|
95
|
+
},
|
|
96
|
+
'./paths.js': {
|
|
97
|
+
default: {
|
|
98
|
+
getProjectRoot: () => '/project'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const result = await loadProjectConfig('/project')
|
|
104
|
+
|
|
105
|
+
t.truthy(result.pdf, 'should have pdf config')
|
|
106
|
+
t.is(result.pdf.filename, 'test-book', 'should parse pdf filename')
|
|
107
|
+
t.is(result.pdf.outputDir, '_pdf', 'should parse pdf outputDir')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('loadProjectConfig should use default project root when not provided', async (t) => {
|
|
111
|
+
const { fs, vol } = t.context
|
|
112
|
+
|
|
113
|
+
// Setup config file
|
|
114
|
+
vol.fromJSON({
|
|
115
|
+
'/default/project/content/_data/config.yaml': 'title: Default Project'
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Mock fs-extra to use memfs
|
|
119
|
+
const { loadProjectConfig } = await esmock('./config.js', {
|
|
120
|
+
'fs-extra': {
|
|
121
|
+
existsSync: (p) => fs.existsSync(p),
|
|
122
|
+
readFileSync: (p) => fs.readFileSync(p)
|
|
123
|
+
},
|
|
124
|
+
'./paths.js': {
|
|
125
|
+
default: {
|
|
126
|
+
getProjectRoot: () => '/default/project'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = await loadProjectConfig()
|
|
132
|
+
|
|
133
|
+
t.is(result.title, 'Default Project', 'should use default project root')
|
|
134
|
+
})
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Project detection module
|
|
3
|
+
*
|
|
4
|
+
* Detects if a directory is a Quire project root by checking for marker files.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/project/detect
|
|
4
7
|
*/
|
|
5
8
|
import fs from 'node:fs'
|
|
6
9
|
|
|
@@ -28,4 +31,3 @@ export default function (dirpath) {
|
|
|
28
31
|
return fs.readdirSync(dirpath)
|
|
29
32
|
.find((entry) => QUIRE_DOT_FILES.includes(entry))
|
|
30
33
|
}
|
|
31
|
-
|