@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36
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/schemas/config.schema.json +194 -0
- package/schemas/figures.schema.json +56 -0
- package/schemas/layout.schema.json +5 -0
- package/schemas/objects.schema.json +62 -0
- package/schemas/publication.schema.json +140 -0
- package/schemas/references.schema.json +29 -0
- 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 +46 -37
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +402 -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 +26 -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 +176 -93
- package/src/lib/11ty/cli.js +77 -35
- 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 +90 -0
- package/src/lib/logger/debug.spec.js +130 -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
package/src/main.js
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Command, Argument, Option } from 'commander'
|
|
2
|
+
import {
|
|
3
|
+
arrayToArgument,
|
|
4
|
+
arrayToOption,
|
|
5
|
+
quietOption,
|
|
6
|
+
verboseOption,
|
|
7
|
+
debugOption
|
|
8
|
+
} from '#lib/commander/index.js'
|
|
2
9
|
import commands from '#src/commands/index.js'
|
|
3
10
|
import config from '#lib/conf/config.js'
|
|
11
|
+
import { handleError } from '#lib/error/handler.js'
|
|
12
|
+
import { docsUrl, DOCS_BASE } from '#helpers/docs-url.js'
|
|
4
13
|
import packageConfig from '#src/packageConfig.js'
|
|
14
|
+
import { enableDebug } from '#lib/logger/debug.js'
|
|
5
15
|
|
|
6
16
|
const { version } = packageConfig
|
|
7
17
|
|
|
18
|
+
const mainHelpText = `
|
|
19
|
+
Docs: ${DOCS_BASE}
|
|
20
|
+
|
|
21
|
+
Common Workflows:
|
|
22
|
+
New project quire new my-book && cd my-book && quire preview
|
|
23
|
+
Build for web quire build
|
|
24
|
+
Generate PDF quire pdf --build
|
|
25
|
+
Generate EPUB quire epub --build
|
|
26
|
+
|
|
27
|
+
Run 'quire help workflows' for detailed workflow documentation.
|
|
28
|
+
|
|
29
|
+
Output Modes:
|
|
30
|
+
-q, --quiet Suppress progress output (for CI/scripts)
|
|
31
|
+
-v, --verbose Show detailed progress (paths, timing, steps)
|
|
32
|
+
--debug Enable debug output for developers/troubleshooting
|
|
33
|
+
|
|
34
|
+
Set defaults: quire settings set verbose true
|
|
35
|
+
|
|
36
|
+
Environment Variables:
|
|
37
|
+
DEBUG=quire:* Enable debug output for all modules
|
|
38
|
+
DEBUG=quire:lib:pdf Enable debug output for PDF module only
|
|
39
|
+
DEBUG=quire:lib:* Enable debug output for all lib modules
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
$ quire build Build the publication
|
|
43
|
+
$ quire build --verbose Build with detailed progress
|
|
44
|
+
$ quire build --debug Build with debug output
|
|
45
|
+
$ DEBUG=quire:* quire pdf Generate PDF with debug output
|
|
46
|
+
`
|
|
47
|
+
|
|
8
48
|
/**
|
|
9
49
|
* Quire CLI implements the command pattern.
|
|
10
50
|
*
|
|
@@ -17,29 +57,67 @@ const program = new Command()
|
|
|
17
57
|
program
|
|
18
58
|
.name('quire')
|
|
19
59
|
.description('Quire command-line interface')
|
|
20
|
-
.version(version,
|
|
60
|
+
.version(version, '-V, --version', 'output quire version number')
|
|
61
|
+
.addOption(arrayToOption(quietOption))
|
|
62
|
+
.addOption(arrayToOption(verboseOption))
|
|
63
|
+
.addOption(arrayToOption(debugOption))
|
|
64
|
+
.addHelpText('after', mainHelpText)
|
|
21
65
|
.configureHelp({
|
|
22
66
|
helpWidth: 80,
|
|
23
67
|
sortOptions: false,
|
|
24
68
|
sortSubcommands: false,
|
|
69
|
+
styleOptionTerm: (term) => {
|
|
70
|
+
// Add spacing to long-only options to align with short-flag options
|
|
71
|
+
return /^-\w/.test(term) ? term : term.padStart(term.length + 4)
|
|
72
|
+
},
|
|
25
73
|
})
|
|
26
74
|
|
|
27
75
|
/**
|
|
28
|
-
*
|
|
76
|
+
* Handle global options before any command runs
|
|
29
77
|
*
|
|
30
|
-
*
|
|
78
|
+
* Output mode semantics:
|
|
79
|
+
* - --quiet: Suppress progress spinners (for CI/scripts)
|
|
80
|
+
* - --verbose: Show detailed progress (paths, timing, steps)
|
|
81
|
+
* - --debug: Enable DEBUG namespace + tool debug modes (for developers)
|
|
82
|
+
*
|
|
83
|
+
* These global options are passed through to commands via opts()
|
|
84
|
+
* and should be merged with command-level options.
|
|
85
|
+
*/
|
|
86
|
+
program.hook('preAction', (thisCommand) => {
|
|
87
|
+
const opts = thisCommand.opts()
|
|
88
|
+
|
|
89
|
+
// --debug or config.debug enables the quire:* DEBUG namespace for internal logging
|
|
90
|
+
// CLI flag takes precedence, then config setting
|
|
91
|
+
if (opts.debug ?? config.get('debug')) {
|
|
92
|
+
enableDebug('quire:*')
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register each command as a subcommand of this program
|
|
31
98
|
* @see https://github.com/tj/commander.js?tab=readme-ov-file#automated-help
|
|
32
99
|
*/
|
|
33
100
|
commands.forEach((command) => {
|
|
34
|
-
const { action, alias, aliases, args, description, name, options } = command
|
|
101
|
+
const { action, alias, aliases, args, description, docsLink, helpText, hidden, name, options, summary } = command
|
|
35
102
|
|
|
36
103
|
const subCommand = program
|
|
37
|
-
.command(name)
|
|
104
|
+
.command(name, { hidden })
|
|
38
105
|
.description(description)
|
|
106
|
+
.summary(summary || description)
|
|
39
107
|
.addHelpCommand()
|
|
40
108
|
.showHelpAfterError()
|
|
41
109
|
|
|
42
|
-
|
|
110
|
+
// Append docs link and/or custom help text after built-in help
|
|
111
|
+
const customHelpText = [
|
|
112
|
+
docsLink && `Docs: ${docsUrl(docsLink)}`,
|
|
113
|
+
helpText?.trim()
|
|
114
|
+
].filter(Boolean).join('\n\n')
|
|
115
|
+
|
|
116
|
+
if (customHelpText) {
|
|
117
|
+
subCommand.addHelpText('after', '\n' + customHelpText)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof alias === 'string') {
|
|
43
121
|
subCommand.alias(alias)
|
|
44
122
|
}
|
|
45
123
|
|
|
@@ -48,71 +126,80 @@ commands.forEach((command) => {
|
|
|
48
126
|
}
|
|
49
127
|
|
|
50
128
|
/**
|
|
51
|
-
*
|
|
129
|
+
* Register arguments with the subcommand
|
|
52
130
|
*/
|
|
53
131
|
if (Array.isArray(args)) {
|
|
54
|
-
args.forEach((
|
|
55
|
-
const argument =
|
|
56
|
-
if (configuration.choices) argument.choices(configuration.choices)
|
|
57
|
-
if (configuration.default) argument.default(configuration.default)
|
|
132
|
+
args.forEach((entry) => {
|
|
133
|
+
const argument = entry instanceof Argument ? entry : arrayToArgument(entry)
|
|
58
134
|
subCommand.addArgument(argument)
|
|
59
135
|
})
|
|
60
136
|
}
|
|
61
137
|
|
|
62
138
|
/**
|
|
63
|
-
*
|
|
139
|
+
* Register options with the subcommand
|
|
64
140
|
*/
|
|
65
141
|
if (Array.isArray(options)) {
|
|
66
142
|
options.forEach((entry) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// when only the short or the long flag is defined in the array
|
|
70
|
-
if (entry[0].startsWith('--')) entry.unshift('\u0020'.repeat(4))
|
|
71
|
-
if (entry[0].startsWith('-') && !entry[1].startsWith('--')) {
|
|
72
|
-
entry.splice(1, 0, '')
|
|
73
|
-
}
|
|
74
|
-
// assign attribute names to the array of option attributes
|
|
75
|
-
const [ short, long, description, defaultValue ] = entry
|
|
76
|
-
// join short and long flags as the option name attribute
|
|
77
|
-
const name = /^-\w/.test(short) && /^--\w/.test(long)
|
|
78
|
-
? [short, long].join(', ')
|
|
79
|
-
: [short, long].join('')
|
|
80
|
-
subCommand.option(name, description, defaultValue)
|
|
81
|
-
} else {
|
|
82
|
-
/**
|
|
83
|
-
* @todo allow options to be defined by a configuration object
|
|
84
|
-
* @see https://github.com/tj/commander.js/#more-configuration
|
|
85
|
-
*/
|
|
86
|
-
// const option = new Option(name, description)
|
|
87
|
-
// for (const property of configuration) {
|
|
88
|
-
// option[property](configuration[property])
|
|
89
|
-
// }
|
|
90
|
-
// subCommand.addOption(option)
|
|
91
|
-
console.error('@TODO please use an array to define option attributes')
|
|
92
|
-
}
|
|
143
|
+
const option = entry instanceof Option ? entry : arrayToOption(entry)
|
|
144
|
+
subCommand.addOption(option)
|
|
93
145
|
})
|
|
94
146
|
}
|
|
95
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Command lifecycle hook: called after action handler and nested subcommands
|
|
150
|
+
* @see https://github.com/tj/commander.js?tab=readme-ov-file#life-cycle-hooks
|
|
151
|
+
*/
|
|
96
152
|
if (command.postAction) {
|
|
97
|
-
subCommand.hook('postAction', (thisCommand, actionCommand) => {
|
|
98
|
-
|
|
153
|
+
subCommand.hook('postAction', async (thisCommand, actionCommand) => {
|
|
154
|
+
try {
|
|
155
|
+
await command.postAction.call(command, thisCommand, actionCommand)
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const { debug } = program.opts()
|
|
158
|
+
handleError(error, { debug })
|
|
159
|
+
}
|
|
99
160
|
})
|
|
100
161
|
}
|
|
101
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Command lifecycle hook: called before action handler and nested subcommands
|
|
165
|
+
* @see https://github.com/tj/commander.js?tab=readme-ov-file#life-cycle-hooks
|
|
166
|
+
*/
|
|
102
167
|
if (command.preAction) {
|
|
103
|
-
subCommand.hook('preAction', (thisCommand, actionCommand) => {
|
|
104
|
-
|
|
168
|
+
subCommand.hook('preAction', async (thisCommand, actionCommand) => {
|
|
169
|
+
try {
|
|
170
|
+
await command.preAction.call(command, thisCommand, actionCommand)
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const { debug } = program.opts()
|
|
173
|
+
handleError(error, { debug })
|
|
174
|
+
}
|
|
105
175
|
})
|
|
106
176
|
}
|
|
107
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Subcommand lifecyle hook: called before parsing direct subcommand
|
|
180
|
+
* @see https://github.com/tj/commander.js?tab=readme-ov-file#life-cycle-hooks
|
|
181
|
+
*/
|
|
108
182
|
if (command.preSubcommand) {
|
|
109
|
-
subCommand.hook('preSubcommand', (thisCommand, theSubcommand) => {
|
|
110
|
-
|
|
183
|
+
subCommand.hook('preSubcommand', async (thisCommand, theSubcommand) => {
|
|
184
|
+
try {
|
|
185
|
+
await command.preSubcommand.call(command, thisCommand, theSubcommand)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const { debug } = program.opts()
|
|
188
|
+
handleError(error, { debug })
|
|
189
|
+
}
|
|
111
190
|
})
|
|
112
191
|
}
|
|
113
192
|
|
|
114
|
-
//
|
|
115
|
-
|
|
193
|
+
// Wrap action in centralized error handler
|
|
194
|
+
// Using apply() preserves `this` context for `this.debug` and `this.logger`
|
|
195
|
+
subCommand.action(async (...args) => {
|
|
196
|
+
try {
|
|
197
|
+
await action.apply(command, args)
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const { debug } = program.opts()
|
|
200
|
+
handleError(error, { debug })
|
|
201
|
+
}
|
|
202
|
+
})
|
|
116
203
|
|
|
117
204
|
/**
|
|
118
205
|
* Inject the CLI configuration into commands
|
package/src/main.spec.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import program from './main.js'
|
|
3
|
+
import test from 'ava'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Main Program Unit Tests
|
|
7
|
+
*
|
|
8
|
+
* Tests the CLI program configuration and setup.
|
|
9
|
+
* Command registration is tested indirectly through command spec files.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test('program exports a Commander.js Command instance', (t) => {
|
|
13
|
+
t.truthy(program)
|
|
14
|
+
t.true(program instanceof Command, 'program should be a Commander.js Command')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('program has correct name and description', (t) => {
|
|
18
|
+
t.is(program.name(), 'quire')
|
|
19
|
+
t.truthy(program.description(), 'program should have a description')
|
|
20
|
+
t.regex(program.description(), /quire/i, 'description should mention Quire')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('program has version option with correct flags', (t) => {
|
|
24
|
+
// Commander stores _version and the version option in the options array
|
|
25
|
+
t.truthy(program._version, 'program should have version set')
|
|
26
|
+
|
|
27
|
+
// Find the version option
|
|
28
|
+
const versionOption = program.options.find((opt) => opt.long === '--version')
|
|
29
|
+
t.truthy(versionOption, '--version option should exist')
|
|
30
|
+
t.is(versionOption.short, '-v', 'version option should have -v short flag')
|
|
31
|
+
t.truthy(versionOption.description, 'version option should have description')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('program help is configured with correct width', (t) => {
|
|
35
|
+
// Access the help configuration
|
|
36
|
+
const helpConfig = program._helpConfiguration
|
|
37
|
+
|
|
38
|
+
t.truthy(helpConfig, 'help configuration should exist')
|
|
39
|
+
t.is(helpConfig.helpWidth, 80, 'help width should be 80')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('program registers commands from commands/index.js', (t) => {
|
|
43
|
+
// Verify commands are registered
|
|
44
|
+
t.true(program.commands.length > 0, 'program should have registered commands')
|
|
45
|
+
|
|
46
|
+
// Verify expected commands exist
|
|
47
|
+
const commandNames = program.commands.map((cmd) => cmd.name())
|
|
48
|
+
t.true(commandNames.includes('build'), 'build command should be registered')
|
|
49
|
+
t.true(commandNames.includes('new'), 'new command should be registered')
|
|
50
|
+
t.true(commandNames.includes('preview'), 'preview command should be registered')
|
|
51
|
+
t.true(commandNames.includes('info'), 'info command should be registered')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('program injects config into subcommands', (t) => {
|
|
55
|
+
// Pick a registered command and verify dependency injection of the config
|
|
56
|
+
const buildCommand = program.commands.find((cmd) => cmd.name() === 'build')
|
|
57
|
+
|
|
58
|
+
t.truthy(buildCommand, 'build command should exist')
|
|
59
|
+
t.truthy(buildCommand.config, 'subcommand should have config injected')
|
|
60
|
+
t.is(typeof buildCommand.config.get, 'function', 'config should have get method')
|
|
61
|
+
})
|
package/src/main.test.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import sinon from 'sinon'
|
|
3
|
+
import esmock from 'esmock'
|
|
4
|
+
import QuireError from '#src/errors/quire-error.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Main Program Integration Tests
|
|
8
|
+
*
|
|
9
|
+
* Tests the runtime behavior of the CLI program, including
|
|
10
|
+
* error handling in command lifecycle hooks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
test.beforeEach((t) => {
|
|
14
|
+
t.context.sandbox = sinon.createSandbox()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test.afterEach.always((t) => {
|
|
18
|
+
t.context.sandbox.restore()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Helper to create a mock command with configurable hooks
|
|
23
|
+
*/
|
|
24
|
+
function createMockCommand(overrides = {}) {
|
|
25
|
+
return {
|
|
26
|
+
name: 'mock-command',
|
|
27
|
+
description: 'Mock command for integration tests',
|
|
28
|
+
summary: 'mocked command',
|
|
29
|
+
action: async () => {},
|
|
30
|
+
...overrides
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('preAction hook errors are passed to handleError', async (t) => {
|
|
35
|
+
const { sandbox } = t.context
|
|
36
|
+
|
|
37
|
+
const testError = new QuireError('Test preAction error', {
|
|
38
|
+
code: 'TEST_ERROR',
|
|
39
|
+
exitCode: 42
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const mockHandleError = sandbox.stub()
|
|
43
|
+
|
|
44
|
+
const mockCommand = createMockCommand({
|
|
45
|
+
preAction: () => {
|
|
46
|
+
throw testError
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Use esmock to inject our mock command and handleError
|
|
51
|
+
await esmock('./main.js', {
|
|
52
|
+
'#src/commands/index.js': {
|
|
53
|
+
default: [mockCommand]
|
|
54
|
+
},
|
|
55
|
+
'#lib/error/handler.js': {
|
|
56
|
+
handleError: mockHandleError
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// The program is created at import time, so we need to trigger the hook
|
|
61
|
+
// by parsing a command. Import the mocked program.
|
|
62
|
+
const { default: program } = await esmock('./main.js', {
|
|
63
|
+
'#src/commands/index.js': {
|
|
64
|
+
default: [mockCommand]
|
|
65
|
+
},
|
|
66
|
+
'#lib/error/handler.js': {
|
|
67
|
+
handleError: mockHandleError
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Configure Commander to not exit on error during tests
|
|
72
|
+
program.exitOverride()
|
|
73
|
+
program.configureOutput({
|
|
74
|
+
writeErr: () => {},
|
|
75
|
+
writeOut: () => {}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Parse the test command to trigger preAction
|
|
79
|
+
try {
|
|
80
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
81
|
+
} catch {
|
|
82
|
+
// Commander may throw after our error is handled
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
t.true(mockHandleError.called, 'handleError should be called for preAction errors')
|
|
86
|
+
t.is(mockHandleError.firstCall.args[0], testError, 'handleError should receive the thrown error')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('postAction hook errors are passed to handleError', async (t) => {
|
|
90
|
+
const { sandbox } = t.context
|
|
91
|
+
|
|
92
|
+
const testError = new QuireError('Test postAction error', {
|
|
93
|
+
code: 'TEST_POST_ERROR',
|
|
94
|
+
exitCode: 43
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const mockHandleError = sandbox.stub()
|
|
98
|
+
|
|
99
|
+
const mockCommand = createMockCommand({
|
|
100
|
+
action: async () => {
|
|
101
|
+
// Action succeeds
|
|
102
|
+
},
|
|
103
|
+
postAction: () => {
|
|
104
|
+
throw testError
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const { default: program } = await esmock('./main.js', {
|
|
109
|
+
'#src/commands/index.js': {
|
|
110
|
+
default: [mockCommand]
|
|
111
|
+
},
|
|
112
|
+
'#lib/error/handler.js': {
|
|
113
|
+
handleError: mockHandleError
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
program.exitOverride()
|
|
118
|
+
program.configureOutput({
|
|
119
|
+
writeErr: () => {},
|
|
120
|
+
writeOut: () => {}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
125
|
+
} catch {
|
|
126
|
+
// Commander may throw after our error is handled
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
t.true(mockHandleError.called, 'handleError should be called for postAction errors')
|
|
130
|
+
t.is(mockHandleError.firstCall.args[0], testError, 'handleError should receive the thrown error')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('action errors are passed to handleError', async (t) => {
|
|
134
|
+
const { sandbox } = t.context
|
|
135
|
+
|
|
136
|
+
const testError = new QuireError('Test action error', {
|
|
137
|
+
code: 'TEST_ACTION_ERROR',
|
|
138
|
+
exitCode: 44
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const mockHandleError = sandbox.stub()
|
|
142
|
+
|
|
143
|
+
const mockCommand = createMockCommand({
|
|
144
|
+
action: async () => {
|
|
145
|
+
throw testError
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const { default: program } = await esmock('./main.js', {
|
|
150
|
+
'#src/commands/index.js': {
|
|
151
|
+
default: [mockCommand]
|
|
152
|
+
},
|
|
153
|
+
'#lib/error/handler.js': {
|
|
154
|
+
handleError: mockHandleError
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
program.exitOverride()
|
|
159
|
+
program.configureOutput({
|
|
160
|
+
writeErr: () => {},
|
|
161
|
+
writeOut: () => {}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
166
|
+
} catch {
|
|
167
|
+
// Commander may throw after our error is handled
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
t.true(mockHandleError.called, 'handleError should be called for action errors')
|
|
171
|
+
t.is(mockHandleError.firstCall.args[0], testError, 'handleError should receive the thrown error')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('preAction hook preserves this context for command methods', async (t) => {
|
|
175
|
+
const { sandbox } = t.context
|
|
176
|
+
|
|
177
|
+
const debugSpy = sandbox.spy()
|
|
178
|
+
let thisContext = null
|
|
179
|
+
|
|
180
|
+
const mockCommand = createMockCommand({
|
|
181
|
+
debug: debugSpy,
|
|
182
|
+
preAction: function () {
|
|
183
|
+
thisContext = this
|
|
184
|
+
this.debug('preAction called')
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const { default: program } = await esmock('./main.js', {
|
|
189
|
+
'#src/commands/index.js': {
|
|
190
|
+
default: [mockCommand]
|
|
191
|
+
},
|
|
192
|
+
'#lib/error/handler.js': {
|
|
193
|
+
handleError: sandbox.stub()
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
program.exitOverride()
|
|
198
|
+
program.configureOutput({
|
|
199
|
+
writeErr: () => {},
|
|
200
|
+
writeOut: () => {}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
205
|
+
} catch {
|
|
206
|
+
// Ignore Commander errors
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
t.is(thisContext, mockCommand, 'this should be bound to the command instance')
|
|
210
|
+
t.true(debugSpy.called, 'this.debug should be callable')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('postAction hook preserves this context for command methods', async (t) => {
|
|
214
|
+
const { sandbox } = t.context
|
|
215
|
+
|
|
216
|
+
const debugSpy = sandbox.spy()
|
|
217
|
+
let thisContext = null
|
|
218
|
+
|
|
219
|
+
const mockCommand = createMockCommand({
|
|
220
|
+
debug: debugSpy,
|
|
221
|
+
action: async () => {},
|
|
222
|
+
postAction: function () {
|
|
223
|
+
thisContext = this
|
|
224
|
+
this.debug('postAction called')
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const { default: program } = await esmock('./main.js', {
|
|
229
|
+
'#src/commands/index.js': {
|
|
230
|
+
default: [mockCommand]
|
|
231
|
+
},
|
|
232
|
+
'#lib/error/handler.js': {
|
|
233
|
+
handleError: sandbox.stub()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
program.exitOverride()
|
|
238
|
+
program.configureOutput({
|
|
239
|
+
writeErr: () => {},
|
|
240
|
+
writeOut: () => {}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore Commander errors
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
t.is(thisContext, mockCommand, 'this should be bound to the command instance')
|
|
250
|
+
t.true(debugSpy.called, 'this.debug should be callable')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('async preAction hooks are awaited before action runs', async (t) => {
|
|
254
|
+
const { sandbox } = t.context
|
|
255
|
+
|
|
256
|
+
const callOrder = []
|
|
257
|
+
|
|
258
|
+
const mockCommand = createMockCommand({
|
|
259
|
+
preAction: async () => {
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
261
|
+
callOrder.push('preAction')
|
|
262
|
+
},
|
|
263
|
+
action: async () => {
|
|
264
|
+
callOrder.push('action')
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const { default: program } = await esmock('./main.js', {
|
|
269
|
+
'#src/commands/index.js': {
|
|
270
|
+
default: [mockCommand]
|
|
271
|
+
},
|
|
272
|
+
'#lib/error/handler.js': {
|
|
273
|
+
handleError: sandbox.stub()
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
program.exitOverride()
|
|
278
|
+
program.configureOutput({
|
|
279
|
+
writeErr: () => {},
|
|
280
|
+
writeOut: () => {}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
285
|
+
} catch {
|
|
286
|
+
// Ignore Commander errors
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
t.deepEqual(callOrder, ['preAction', 'action'], 'preAction should complete before action starts')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('async postAction hooks are awaited after action completes', async (t) => {
|
|
293
|
+
const { sandbox } = t.context
|
|
294
|
+
|
|
295
|
+
const callOrder = []
|
|
296
|
+
|
|
297
|
+
const mockCommand = createMockCommand({
|
|
298
|
+
action: async () => {
|
|
299
|
+
callOrder.push('action')
|
|
300
|
+
},
|
|
301
|
+
postAction: async () => {
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
303
|
+
callOrder.push('postAction')
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const { default: program } = await esmock('./main.js', {
|
|
308
|
+
'#src/commands/index.js': {
|
|
309
|
+
default: [mockCommand]
|
|
310
|
+
},
|
|
311
|
+
'#lib/error/handler.js': {
|
|
312
|
+
handleError: sandbox.stub()
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
program.exitOverride()
|
|
317
|
+
program.configureOutput({
|
|
318
|
+
writeErr: () => {},
|
|
319
|
+
writeOut: () => {}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await program.parseAsync(['node', 'quire', 'mock-command'])
|
|
324
|
+
} catch {
|
|
325
|
+
// Ignore Commander errors
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
t.deepEqual(callOrder, ['action', 'postAction'], 'postAction should run after action completes')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Nota bene: preSubcommand hook tests are skipped because:
|
|
333
|
+
* 1. `main` does not process a `subcommands` array in command definitions
|
|
334
|
+
* 2. No commands in the CLI currently use nested subcommands
|
|
335
|
+
* 3. The wrapped error handler pattern is identical to preAction/postAction
|
|
336
|
+
*
|
|
337
|
+
* @todo When nested subcommands are added, these tests should be implemented.
|
|
338
|
+
*/
|
|
339
|
+
test.skip('preSubcommand hook errors are passed to handleError', async (t) => {
|
|
340
|
+
// Requires main.js to support nested subcommands in command definitions
|
|
341
|
+
t.pass()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test.skip('preSubcommand hook preserves this context for command methods', async (t) => {
|
|
345
|
+
// Requires main.js to support nested subcommands in command definitions
|
|
346
|
+
t.pass()
|
|
347
|
+
})
|
package/src/validators/utils.js
CHANGED
|
@@ -2,7 +2,7 @@ import YamlDuplicateIdError from '../errors/validation/yaml-duplicate-error.js'
|
|
|
2
2
|
import { fileURLToPath } from 'url'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import {
|
|
5
|
+
import { paths as eleventyPaths } from '#lib/11ty/index.js'
|
|
6
6
|
|
|
7
7
|
const IMAGE_KEYS = new Set(['src', 'image', 'logo'])
|
|
8
8
|
|
|
@@ -37,6 +37,7 @@ function collectImagePaths(node, paths=[]) {
|
|
|
37
37
|
function validateImage(label, src) {
|
|
38
38
|
if(!src) return
|
|
39
39
|
|
|
40
|
+
const projectRoot = eleventyPaths.getProjectRoot()
|
|
40
41
|
let assetPath = ''
|
|
41
42
|
if(src.endsWith('.html')) {
|
|
42
43
|
assetPath = path.join(projectRoot, 'content', '_assets', src)
|