@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
package/src/lib/logger/debug.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import util from 'node:util'
|
|
1
2
|
import debug from 'debug'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,6 +7,10 @@ import debug from 'debug'
|
|
|
6
7
|
* Creates debug instances with the `quire:` namespace prefix.
|
|
7
8
|
* Uses the standard `debug` package for namespace-based filtering.
|
|
8
9
|
*
|
|
10
|
+
* Object formatting:
|
|
11
|
+
* %O Pretty-printed, indented on a new line (multi-line)
|
|
12
|
+
* %o Compact single-line output (built-in default)
|
|
13
|
+
*
|
|
9
14
|
* @example
|
|
10
15
|
* // In a module (short alias)
|
|
11
16
|
* import createDebug from '#debug'
|
|
@@ -22,6 +27,42 @@ import debug from 'debug'
|
|
|
22
27
|
* @see https://www.npmjs.com/package/debug
|
|
23
28
|
*/
|
|
24
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Override %O formatter to produce indented multi-line output
|
|
32
|
+
*
|
|
33
|
+
* Pretty-prints objects with each line indented by 2 spaces,
|
|
34
|
+
* starting on a new line after the label.
|
|
35
|
+
*
|
|
36
|
+
* Use %o (lowercase) for compact single-line output.
|
|
37
|
+
*/
|
|
38
|
+
debug.formatters.O = function (value) {
|
|
39
|
+
this.inspectOpts.colors = this.useColors
|
|
40
|
+
const string = util.inspect(value, this.inspectOpts)
|
|
41
|
+
return '\n' + string.split('\n').map((line) => ' ' + line).join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Override formatArgs to prevent per-line namespace prefixing
|
|
46
|
+
*
|
|
47
|
+
* The default debug formatArgs (in color mode) splits the formatted
|
|
48
|
+
* message on newlines and inserts the namespace prefix before every
|
|
49
|
+
* line. This makes multi-line %O output noisy. This override only
|
|
50
|
+
* prefixes the first line, letting continuation lines flow cleanly.
|
|
51
|
+
*/
|
|
52
|
+
const originalFormatArgs = debug.formatArgs
|
|
53
|
+
debug.formatArgs = function (args) {
|
|
54
|
+
if (this.useColors) {
|
|
55
|
+
const c = this.color
|
|
56
|
+
const colorCode = '\u001B[3' + (c < 8 ? c : '8;5;' + c)
|
|
57
|
+
const prefix = ` ${colorCode};1m${this.namespace} \u001B[0m`
|
|
58
|
+
|
|
59
|
+
args[0] = prefix + args[0]
|
|
60
|
+
args.push(colorCode + 'm+' + debug.humanize(this.diff) + '\u001B[0m')
|
|
61
|
+
} else {
|
|
62
|
+
originalFormatArgs.call(this, args)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
/**
|
|
26
67
|
* Root namespace for all Quire CLI debug output
|
|
27
68
|
*/
|
|
@@ -37,7 +78,8 @@ export const DEBUG_NAMESPACE = 'quire'
|
|
|
37
78
|
* const debug = createDebug('lib:pdf:paged')
|
|
38
79
|
* debug('printer options: %O', options)
|
|
39
80
|
* // Output (when DEBUG=quire:lib:pdf:paged):
|
|
40
|
-
* // quire:lib:pdf:paged printer options:
|
|
81
|
+
* // quire:lib:pdf:paged printer options:
|
|
82
|
+
* // { format: 'A4', landscape: false } +0ms
|
|
41
83
|
*/
|
|
42
84
|
export default function createDebug(namespace) {
|
|
43
85
|
return debug(`${DEBUG_NAMESPACE}:${namespace}`)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import debug from 'debug'
|
|
3
|
+
// Import to register formatter and formatArgs overrides
|
|
4
|
+
import './debug.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Debug formatter and output tests
|
|
8
|
+
*
|
|
9
|
+
* Verifies the custom %O formatter produces indented multi-line output,
|
|
10
|
+
* the %o formatter remains compact single-line, and the formatArgs
|
|
11
|
+
* override prevents per-line namespace prefixing in colored output.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to call a debug formatter with a value
|
|
16
|
+
*
|
|
17
|
+
* Formatters are called with `this` bound to a debugger-like context.
|
|
18
|
+
* We provide the minimal context the formatters expect.
|
|
19
|
+
*/
|
|
20
|
+
function callFormatter(specifier, value) {
|
|
21
|
+
const formatter = debug.formatters[specifier]
|
|
22
|
+
const context = {
|
|
23
|
+
useColors: false,
|
|
24
|
+
inspectOpts: { colors: false },
|
|
25
|
+
}
|
|
26
|
+
return formatter.call(context, value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// %O formatter (pretty-printed, indented)
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
test('%O output starts with a newline', (t) => {
|
|
34
|
+
const result = callFormatter('O', { a: 1 })
|
|
35
|
+
t.true(result.startsWith('\n'))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('%O output lines are indented with 2 spaces', (t) => {
|
|
39
|
+
const result = callFormatter('O', { a: 1, b: 2 })
|
|
40
|
+
const lines = result.split('\n').slice(1) // skip leading empty line
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
t.true(line.startsWith(' '), `Line not indented: "${line}"`)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('%O formats nested objects with indentation', (t) => {
|
|
47
|
+
const result = callFormatter('O', { outer: { inner: 'value' } })
|
|
48
|
+
t.true(result.includes('outer'))
|
|
49
|
+
t.true(result.includes('inner'))
|
|
50
|
+
// Every line after the leading newline should be indented
|
|
51
|
+
const lines = result.split('\n').slice(1)
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
t.true(line.startsWith(' '), `Line not indented: "${line}"`)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('%O handles null without error', (t) => {
|
|
58
|
+
const result = callFormatter('O', null)
|
|
59
|
+
t.true(result.includes('null'))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('%O handles undefined without error', (t) => {
|
|
63
|
+
const result = callFormatter('O', undefined)
|
|
64
|
+
t.true(result.includes('undefined'))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('%O handles primitive values', (t) => {
|
|
68
|
+
t.true(callFormatter('O', 42).includes('42'))
|
|
69
|
+
t.true(callFormatter('O', 'hello').includes('hello'))
|
|
70
|
+
t.true(callFormatter('O', true).includes('true'))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('%O handles arrays', (t) => {
|
|
74
|
+
const result = callFormatter('O', [1, 2, 3])
|
|
75
|
+
t.true(result.includes('1'))
|
|
76
|
+
t.true(result.includes('2'))
|
|
77
|
+
t.true(result.includes('3'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// %o formatter (compact single-line, built-in)
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
test('%o output is single-line', (t) => {
|
|
85
|
+
const result = callFormatter('o', { a: 1, b: 2, c: 3 })
|
|
86
|
+
t.false(result.includes('\n'))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('%o does not start with a newline', (t) => {
|
|
90
|
+
const result = callFormatter('o', { a: 1 })
|
|
91
|
+
t.false(result.startsWith('\n'))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// formatArgs override (prefix only on first line)
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Helper to call formatArgs and capture the result
|
|
100
|
+
*
|
|
101
|
+
* Creates a minimal debugger-like context to test formatArgs behavior.
|
|
102
|
+
* In color mode, the default debug formatArgs would split on newlines
|
|
103
|
+
* and insert the prefix before every line. Our override prevents this.
|
|
104
|
+
*/
|
|
105
|
+
function callFormatArgs(message, useColors = true) {
|
|
106
|
+
const args = [message]
|
|
107
|
+
const context = {
|
|
108
|
+
namespace: 'test',
|
|
109
|
+
useColors,
|
|
110
|
+
color: 5,
|
|
111
|
+
diff: 0,
|
|
112
|
+
}
|
|
113
|
+
debug.formatArgs.call(context, args)
|
|
114
|
+
return args[0]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('formatArgs prefixes the first line in color mode', (t) => {
|
|
118
|
+
const result = callFormatArgs('hello')
|
|
119
|
+
t.true(result.includes('test'))
|
|
120
|
+
t.true(result.includes('hello'))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('formatArgs does not duplicate prefix on multi-line output', (t) => {
|
|
124
|
+
const result = callFormatArgs('paths:\n { a: 1 }\n { b: 2 }')
|
|
125
|
+
// Namespace should appear exactly once
|
|
126
|
+
const matches = result.match(/test/g)
|
|
127
|
+
t.is(matches.length, 1)
|
|
128
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides OS detection and platform-specific helpers for the CLI.
|
|
5
|
+
*
|
|
6
|
+
* @module lib/platform
|
|
7
|
+
*/
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Platform identifiers
|
|
12
|
+
* @readonly
|
|
13
|
+
* @enum {string}
|
|
14
|
+
*/
|
|
15
|
+
export const Platform = {
|
|
16
|
+
MACOS: 'macos',
|
|
17
|
+
WINDOWS: 'windows',
|
|
18
|
+
LINUX: 'linux',
|
|
19
|
+
UNKNOWN: 'unknown',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the current platform identifier
|
|
24
|
+
* @returns {string} Platform identifier (macos, windows, linux, or unknown)
|
|
25
|
+
*/
|
|
26
|
+
export function getPlatform() {
|
|
27
|
+
switch (process.platform) {
|
|
28
|
+
case 'darwin':
|
|
29
|
+
return Platform.MACOS
|
|
30
|
+
case 'win32':
|
|
31
|
+
return Platform.WINDOWS
|
|
32
|
+
case 'linux':
|
|
33
|
+
return Platform.LINUX
|
|
34
|
+
default:
|
|
35
|
+
return Platform.UNKNOWN
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a human-readable platform name
|
|
41
|
+
* @returns {string} Human-readable OS name (e.g., "macOS 14.0", "Windows 11", "Linux")
|
|
42
|
+
*/
|
|
43
|
+
export function getPlatformName() {
|
|
44
|
+
const platform = getPlatform()
|
|
45
|
+
const release = os.release()
|
|
46
|
+
|
|
47
|
+
switch (platform) {
|
|
48
|
+
case Platform.MACOS: {
|
|
49
|
+
// Darwin kernel version to macOS version mapping (approximate)
|
|
50
|
+
const majorVersion = parseInt(release.split('.')[0], 10)
|
|
51
|
+
// Darwin 23.x = macOS 14 (Sonoma), 22.x = macOS 13, etc.
|
|
52
|
+
const macOSVersion = majorVersion >= 20 ? majorVersion - 9 : majorVersion - 4
|
|
53
|
+
return `macOS ${macOSVersion >= 14 ? macOSVersion : release}`
|
|
54
|
+
}
|
|
55
|
+
case Platform.WINDOWS:
|
|
56
|
+
return `Windows (${release})`
|
|
57
|
+
case Platform.LINUX:
|
|
58
|
+
return `Linux (${release})`
|
|
59
|
+
default:
|
|
60
|
+
return `${process.platform} (${release})`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if running on macOS
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function isMacOS() {
|
|
69
|
+
return process.platform === 'darwin'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if running on Windows
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isWindows() {
|
|
77
|
+
return process.platform === 'win32'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if running on Linux
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
export function isLinux() {
|
|
85
|
+
return process.platform === 'linux'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default {
|
|
89
|
+
Platform,
|
|
90
|
+
getPlatform,
|
|
91
|
+
getPlatformName,
|
|
92
|
+
isMacOS,
|
|
93
|
+
isWindows,
|
|
94
|
+
isLinux,
|
|
95
|
+
}
|
package/src/lib/project/build.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* Build detection module
|
|
3
3
|
*
|
|
4
4
|
* Detects if build output exists and provides build status information.
|
|
5
|
+
* Uses shared output path resolution to ensure consistency with the
|
|
6
|
+
* doctor command and generation commands.
|
|
5
7
|
*
|
|
6
8
|
* @module lib/project/build
|
|
7
9
|
*/
|
|
8
10
|
import fs from 'node:fs'
|
|
9
11
|
import path from 'node:path'
|
|
10
12
|
import paths from './paths.js'
|
|
13
|
+
import { getPdfOutputPaths, getEpubOutputPaths, getEpubBuildDir } from './output-paths.js'
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Get build output information
|
|
@@ -17,45 +20,60 @@ import paths from './paths.js'
|
|
|
17
20
|
*/
|
|
18
21
|
export function getBuildInfo(projectRoot = paths.getProjectRoot()) {
|
|
19
22
|
const sitePath = path.join(projectRoot, '_site')
|
|
20
|
-
const
|
|
21
|
-
const pdfFiles = ['pagedjs.pdf', 'prince.pdf']
|
|
23
|
+
const epubBuildPath = getEpubBuildDir({ projectRoot })
|
|
22
24
|
|
|
23
25
|
const info = {
|
|
24
26
|
site: {
|
|
25
27
|
exists: fs.existsSync(sitePath),
|
|
26
28
|
path: sitePath,
|
|
27
|
-
|
|
29
|
+
lastModified: null,
|
|
28
30
|
},
|
|
29
31
|
epub: {
|
|
30
|
-
exists:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
exists: false,
|
|
33
|
+
paths: [],
|
|
34
|
+
buildDir: epubBuildPath,
|
|
35
|
+
buildDirExists: fs.existsSync(epubBuildPath),
|
|
36
|
+
lastModified: null,
|
|
33
37
|
},
|
|
34
38
|
pdf: {
|
|
35
39
|
exists: false,
|
|
36
40
|
paths: [],
|
|
37
|
-
|
|
41
|
+
lastModified: null,
|
|
38
42
|
},
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
// Get
|
|
45
|
+
// Get last modified times if directories exist
|
|
42
46
|
if (info.site.exists) {
|
|
43
|
-
|
|
47
|
+
const { mtime: lastModified } = fs.statSync(sitePath)
|
|
48
|
+
info.site.lastModified = lastModified
|
|
44
49
|
}
|
|
45
|
-
if (info.epub.
|
|
46
|
-
|
|
50
|
+
if (info.epub.buildDirExists) {
|
|
51
|
+
const { mtime: lastModified } = fs.statSync(epubBuildPath)
|
|
52
|
+
info.epub.lastModified = lastModified
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
// Check for
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// Check for EPUB files using shared path resolution
|
|
56
|
+
const epubPaths = getEpubOutputPaths({ projectRoot })
|
|
57
|
+
for (const epubPath of epubPaths) {
|
|
58
|
+
if (fs.existsSync(epubPath)) {
|
|
59
|
+
info.epub.exists = true
|
|
60
|
+
info.epub.paths.push(epubPath)
|
|
61
|
+
const { mtime: lastModified } = fs.statSync(epubPath)
|
|
62
|
+
if (!info.epub.lastModified || lastModified > info.epub.lastModified) {
|
|
63
|
+
info.epub.lastModified = lastModified
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for PDF files using shared path resolution
|
|
69
|
+
const pdfPaths = getPdfOutputPaths({ projectRoot })
|
|
70
|
+
for (const pdfPath of pdfPaths) {
|
|
52
71
|
if (fs.existsSync(pdfPath)) {
|
|
53
72
|
info.pdf.exists = true
|
|
54
73
|
info.pdf.paths.push(pdfPath)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
info.pdf.mtime = mtime
|
|
74
|
+
const { mtime: lastModified } = fs.statSync(pdfPath)
|
|
75
|
+
if (!info.pdf.lastModified || lastModified > info.pdf.lastModified) {
|
|
76
|
+
info.pdf.lastModified = lastModified
|
|
59
77
|
}
|
|
60
78
|
}
|
|
61
79
|
}
|
|
@@ -77,20 +95,21 @@ export function hasSiteOutput(projectRoot = paths.getProjectRoot()) {
|
|
|
77
95
|
/**
|
|
78
96
|
* Check if epub build output exists
|
|
79
97
|
*
|
|
98
|
+
* Checks for the intermediate _epub build directory.
|
|
99
|
+
*
|
|
80
100
|
* @param {string} [projectRoot] - Project root directory (defaults to cwd)
|
|
81
101
|
* @returns {boolean} True if _epub directory exists
|
|
82
102
|
*/
|
|
83
103
|
export function hasEpubOutput(projectRoot = paths.getProjectRoot()) {
|
|
84
|
-
const epubPath =
|
|
104
|
+
const epubPath = getEpubBuildDir({ projectRoot })
|
|
85
105
|
return fs.existsSync(epubPath)
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
/**
|
|
89
109
|
* Check if PDF output exists
|
|
90
110
|
*
|
|
91
|
-
*
|
|
92
|
-
* -
|
|
93
|
-
* - Specific library output when lib option provided
|
|
111
|
+
* Uses shared path resolution to check all possible PDF output locations,
|
|
112
|
+
* including config-aware custom paths and default engine-named paths.
|
|
94
113
|
*
|
|
95
114
|
* @param {Object} [options]
|
|
96
115
|
* @param {string} [options.lib] - Specific library to check ('pagedjs' or 'prince')
|
|
@@ -105,9 +124,9 @@ export function hasPdfOutput(options = {}) {
|
|
|
105
124
|
return fs.existsSync(pdfPath)
|
|
106
125
|
}
|
|
107
126
|
|
|
108
|
-
// Check
|
|
109
|
-
const
|
|
110
|
-
return
|
|
127
|
+
// Check all possible PDF output locations
|
|
128
|
+
const pdfPaths = getPdfOutputPaths({ projectRoot })
|
|
129
|
+
return pdfPaths.some((pdfPath) => fs.existsSync(pdfPath))
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
/**
|
|
@@ -84,6 +84,7 @@ test('getBuildInfo returns correct status for existing builds', async (t) => {
|
|
|
84
84
|
vol.fromJSON({
|
|
85
85
|
[memfsPath('_site', 'index.html')]: '<html></html>',
|
|
86
86
|
[memfsPath('_epub', 'content.opf')]: '<package></package>',
|
|
87
|
+
[memfsPath('epubjs.epub')]: 'PK\x03\x04',
|
|
87
88
|
[memfsPath('pagedjs.pdf')]: '%PDF-1.4',
|
|
88
89
|
})
|
|
89
90
|
|
|
@@ -95,15 +96,17 @@ test('getBuildInfo returns correct status for existing builds', async (t) => {
|
|
|
95
96
|
|
|
96
97
|
t.true(info.site.exists)
|
|
97
98
|
t.is(info.site.path, nativePath('_site'))
|
|
98
|
-
t.truthy(info.site.
|
|
99
|
+
t.truthy(info.site.lastModified)
|
|
99
100
|
|
|
100
101
|
t.true(info.epub.exists)
|
|
101
|
-
t.
|
|
102
|
-
t.
|
|
102
|
+
t.true(info.epub.buildDirExists)
|
|
103
|
+
t.is(info.epub.buildDir, nativePath('_epub'))
|
|
104
|
+
t.true(info.epub.paths.includes(nativePath('epubjs.epub')))
|
|
105
|
+
t.truthy(info.epub.lastModified)
|
|
103
106
|
|
|
104
107
|
t.true(info.pdf.exists)
|
|
105
108
|
t.deepEqual(info.pdf.paths, [nativePath('pagedjs.pdf')])
|
|
106
|
-
t.truthy(info.pdf.
|
|
109
|
+
t.truthy(info.pdf.lastModified)
|
|
107
110
|
})
|
|
108
111
|
|
|
109
112
|
test('getBuildInfo returns correct status for missing builds', async (t) => {
|
|
@@ -120,14 +123,15 @@ test('getBuildInfo returns correct status for missing builds', async (t) => {
|
|
|
120
123
|
const info = getBuildInfo(testRoot)
|
|
121
124
|
|
|
122
125
|
t.false(info.site.exists)
|
|
123
|
-
t.is(info.site.
|
|
126
|
+
t.is(info.site.lastModified, null)
|
|
124
127
|
|
|
125
128
|
t.false(info.epub.exists)
|
|
126
|
-
t.
|
|
129
|
+
t.false(info.epub.buildDirExists)
|
|
130
|
+
t.is(info.epub.lastModified, null)
|
|
127
131
|
|
|
128
132
|
t.false(info.pdf.exists)
|
|
129
133
|
t.deepEqual(info.pdf.paths, [])
|
|
130
|
-
t.is(info.pdf.
|
|
134
|
+
t.is(info.pdf.lastModified, null)
|
|
131
135
|
})
|
|
132
136
|
|
|
133
137
|
test('getBuildInfo returns multiple PDF paths when both exist', async (t) => {
|
|
@@ -148,7 +152,25 @@ test('getBuildInfo returns multiple PDF paths when both exist', async (t) => {
|
|
|
148
152
|
t.is(info.pdf.paths.length, 2)
|
|
149
153
|
t.true(info.pdf.paths.includes(nativePath('pagedjs.pdf')))
|
|
150
154
|
t.true(info.pdf.paths.includes(nativePath('prince.pdf')))
|
|
151
|
-
t.truthy(info.pdf.
|
|
155
|
+
t.truthy(info.pdf.lastModified)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('getBuildInfo detects epub build dir without .epub files', async (t) => {
|
|
159
|
+
const { vol } = t.context
|
|
160
|
+
|
|
161
|
+
vol.fromJSON({
|
|
162
|
+
[memfsPath('_epub', 'content.opf')]: '<package></package>',
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const { getBuildInfo } = await esmock('./build.js', {
|
|
166
|
+
'node:fs': createFsFromVolume(vol),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const info = getBuildInfo(testRoot)
|
|
170
|
+
|
|
171
|
+
t.false(info.epub.exists, 'no .epub files should mean exists is false')
|
|
172
|
+
t.true(info.epub.buildDirExists, '_epub directory should be detected')
|
|
173
|
+
t.truthy(info.epub.lastModified, 'lastModified from _epub dir should be present')
|
|
152
174
|
})
|
|
153
175
|
|
|
154
176
|
test('requireBuildOutput throws when site output missing', async (t) => {
|
|
@@ -7,7 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Files that indicate a Quire project directory.
|
|
12
|
+
*
|
|
13
|
+
* These marker files are created by `quire new` or exist in Quire project templates.
|
|
14
|
+
* The presence of any one of these files indicates the directory is a Quire project root.
|
|
15
|
+
*
|
|
16
|
+
* @constant {ReadonlyArray<string>}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Check if a file is a project marker
|
|
20
|
+
* if (PROJECT_MARKERS.includes(filename)) {
|
|
21
|
+
* console.log('This is a Quire project root')
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export const PROJECT_MARKERS = Object.freeze([
|
|
11
25
|
'.eleventy.js',
|
|
12
26
|
'.quire',
|
|
13
27
|
'.quire-11ty',
|
|
@@ -29,5 +43,5 @@ const QUIRE_DOT_FILES = Object.freeze([
|
|
|
29
43
|
*/
|
|
30
44
|
export default function (dirpath) {
|
|
31
45
|
return fs.readdirSync(dirpath)
|
|
32
|
-
.find((entry) =>
|
|
46
|
+
.find((entry) => PROJECT_MARKERS.includes(entry))
|
|
33
47
|
}
|
package/src/lib/project/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Provides project-level concerns including paths, detection, configuration,
|
|
5
5
|
* build status, and version management.
|
|
6
6
|
*/
|
|
7
|
-
import paths, { Paths } from './paths.js'
|
|
8
|
-
import detect from './detect.js'
|
|
7
|
+
import paths, { DATA_DIR, Paths, REQUIRED_DATA_FILES, SOURCE_DIRECTORIES } from './paths.js'
|
|
8
|
+
import detect, { PROJECT_MARKERS } from './detect.js'
|
|
9
9
|
import { loadProjectConfig } from './config.js'
|
|
10
10
|
import {
|
|
11
11
|
getBuildInfo,
|
|
@@ -14,6 +14,13 @@ import {
|
|
|
14
14
|
hasSiteOutput,
|
|
15
15
|
requireBuildOutput,
|
|
16
16
|
} from './build.js'
|
|
17
|
+
import {
|
|
18
|
+
getPdfOutputPaths,
|
|
19
|
+
getEpubOutputPaths,
|
|
20
|
+
getEpubBuildDir,
|
|
21
|
+
PDF_ENGINES,
|
|
22
|
+
EPUB_ENGINES,
|
|
23
|
+
} from './output-paths.js'
|
|
17
24
|
import {
|
|
18
25
|
getVersion,
|
|
19
26
|
setVersion,
|
|
@@ -23,8 +30,17 @@ import {
|
|
|
23
30
|
} from './version.js'
|
|
24
31
|
|
|
25
32
|
export {
|
|
33
|
+
DATA_DIR,
|
|
34
|
+
EPUB_ENGINES,
|
|
35
|
+
PDF_ENGINES,
|
|
36
|
+
PROJECT_MARKERS,
|
|
37
|
+
REQUIRED_DATA_FILES,
|
|
38
|
+
SOURCE_DIRECTORIES,
|
|
26
39
|
detect,
|
|
27
40
|
getBuildInfo,
|
|
41
|
+
getEpubBuildDir,
|
|
42
|
+
getEpubOutputPaths,
|
|
43
|
+
getPdfOutputPaths,
|
|
28
44
|
hasEpubOutput,
|
|
29
45
|
hasPdfOutput,
|
|
30
46
|
hasSiteOutput,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output path resolution module
|
|
3
|
+
*
|
|
4
|
+
* Centralizes knowledge of where PDF and EPUB output files are located.
|
|
5
|
+
* Used by the doctor command and build detection to find output artifacts
|
|
6
|
+
* using the same logic as the generation commands.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/project/output-paths
|
|
9
|
+
*/
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { ENGINES as PDF_ENGINES } from '#lib/pdf/schema.js'
|
|
12
|
+
import { ENGINES as EPUB_ENGINES } from '#lib/epub/schema.js'
|
|
13
|
+
|
|
14
|
+
export { PDF_ENGINES, EPUB_ENGINES }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get all possible PDF output file paths for a project
|
|
18
|
+
*
|
|
19
|
+
* PDF output paths depend on configuration:
|
|
20
|
+
* - With config.pdf: `{outputDir}/{config.pdf.outputDir}/{config.pdf.filename}.pdf`
|
|
21
|
+
* - Without config (default): `{projectRoot}/{engine}.pdf`
|
|
22
|
+
*
|
|
23
|
+
* This reuses the same resolution logic as `lib/pdf/index.js#getOutputPath`.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} [options]
|
|
26
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root directory
|
|
27
|
+
* @param {string} [options.outputDir='_site'] - Build output directory (relative)
|
|
28
|
+
* @param {Object} [options.pdfConfig] - PDF config from config.yaml (config.pdf)
|
|
29
|
+
* @param {string} [options.pdfConfig.outputDir] - PDF output subdirectory
|
|
30
|
+
* @param {string} [options.pdfConfig.filename] - PDF filename (without extension)
|
|
31
|
+
* @returns {string[]} Array of absolute paths to check for PDF output
|
|
32
|
+
*/
|
|
33
|
+
export function getPdfOutputPaths(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
projectRoot = process.cwd(),
|
|
36
|
+
outputDir = '_site',
|
|
37
|
+
pdfConfig,
|
|
38
|
+
} = options
|
|
39
|
+
|
|
40
|
+
const paths = []
|
|
41
|
+
|
|
42
|
+
if (pdfConfig && pdfConfig.outputDir && pdfConfig.filename) {
|
|
43
|
+
// Config-aware path: matches lib/pdf/index.js getOutputPath()
|
|
44
|
+
paths.push(
|
|
45
|
+
path.join(projectRoot, outputDir, pdfConfig.outputDir, `${pdfConfig.filename}.pdf`)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Default engine-named paths (always check these as fallback)
|
|
50
|
+
for (const engine of PDF_ENGINES) {
|
|
51
|
+
paths.push(path.join(projectRoot, `${engine}.pdf`))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return paths
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all possible EPUB output file paths for a project
|
|
59
|
+
*
|
|
60
|
+
* EPUB output is at `{projectRoot}/{engine}.epub` for each supported engine.
|
|
61
|
+
* The `_epub` directory is an intermediate build artifact, not the final output.
|
|
62
|
+
*
|
|
63
|
+
* This reuses the same resolution logic as `commands/epub.js`.
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} [options]
|
|
66
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root directory
|
|
67
|
+
* @returns {string[]} Array of absolute paths to check for EPUB output
|
|
68
|
+
*/
|
|
69
|
+
export function getEpubOutputPaths(options = {}) {
|
|
70
|
+
const { projectRoot = process.cwd() } = options
|
|
71
|
+
|
|
72
|
+
return EPUB_ENGINES.map(
|
|
73
|
+
(engine) => path.join(projectRoot, `${engine}.epub`)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the intermediate EPUB build directory path
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} [options]
|
|
81
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root directory
|
|
82
|
+
* @returns {string} Absolute path to _epub directory
|
|
83
|
+
*/
|
|
84
|
+
export function getEpubBuildDir(options = {}) {
|
|
85
|
+
const { projectRoot = process.cwd() } = options
|
|
86
|
+
return path.join(projectRoot, '_epub')
|
|
87
|
+
}
|