@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/commands/preview.js
CHANGED
|
@@ -2,6 +2,7 @@ import Command from '#src/Command.js'
|
|
|
2
2
|
import { Option } from 'commander'
|
|
3
3
|
import { withOutputModes } from '#lib/commander/index.js'
|
|
4
4
|
import { api, cli } from '#lib/11ty/index.js'
|
|
5
|
+
import reporter from '#lib/reporter/index.js'
|
|
5
6
|
import testcwd from '#helpers/test-cwd.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -22,11 +23,13 @@ export default class PreviewCommand extends Command {
|
|
|
22
23
|
Examples:
|
|
23
24
|
quire preview Start preview server on default port
|
|
24
25
|
quire preview --port 3000 Run on custom port
|
|
26
|
+
quire preview --open Start server and open in browser
|
|
25
27
|
quire preview --verbose Start with detailed progress
|
|
26
28
|
`,
|
|
27
29
|
version: '1.1.0',
|
|
28
30
|
options: [
|
|
29
31
|
[ '-p, --port <port>', 'configure development server port', 8080 ],
|
|
32
|
+
[ '--open', 'open in default browser when server starts' ],
|
|
30
33
|
// Use Option object syntax to configure this as a hidden option
|
|
31
34
|
new Option('--11ty <module>', 'use the specified 11ty module')
|
|
32
35
|
.choices(['api', 'cli']).default('api').hideHelp(),
|
|
@@ -40,12 +43,15 @@ Examples:
|
|
|
40
43
|
async action(options, command) {
|
|
41
44
|
this.debug('called with options %O', options)
|
|
42
45
|
|
|
46
|
+
// Configure reporter for this command
|
|
47
|
+
reporter.configure({ quiet: options.quiet, verbose: options.verbose })
|
|
48
|
+
|
|
43
49
|
if (options['11ty'] === 'api') {
|
|
44
50
|
this.debug('running eleventy using lib/11ty api')
|
|
45
|
-
api.serve(options)
|
|
51
|
+
await api.serve(options)
|
|
46
52
|
} else {
|
|
47
53
|
this.debug('running eleventy using lib/11ty cli')
|
|
48
|
-
cli.serve(options)
|
|
54
|
+
await cli.serve(options)
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
package/src/commands/validate.js
CHANGED
|
@@ -25,11 +25,14 @@ export default class ValidateCommand extends Command {
|
|
|
25
25
|
Examples:
|
|
26
26
|
quire validate Check YAML syntax in content/_data/
|
|
27
27
|
quire validate --verbose Validate with detailed file listing
|
|
28
|
+
quire validate --json Output validation results as JSON
|
|
28
29
|
|
|
29
30
|
Validates YAML files in content/_data/ directory.
|
|
30
31
|
`,
|
|
31
32
|
version: '1.0.0',
|
|
32
|
-
options: [
|
|
33
|
+
options: [
|
|
34
|
+
['--json', 'output validation results as JSON'],
|
|
35
|
+
],
|
|
33
36
|
})
|
|
34
37
|
|
|
35
38
|
constructor() {
|
|
@@ -45,21 +48,50 @@ Validates YAML files in content/_data/ directory.
|
|
|
45
48
|
.map(file => path.join(dataPath, file)
|
|
46
49
|
)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const results = []
|
|
52
|
+
if (!options.json) {
|
|
53
|
+
this.logger.info('Validating YAML files..')
|
|
54
|
+
}
|
|
50
55
|
|
|
51
56
|
for (const file of files) {
|
|
52
57
|
try {
|
|
53
58
|
yamlValidation(file)
|
|
59
|
+
results.push({ file, status: 'passed' })
|
|
54
60
|
} catch (error){
|
|
55
|
-
|
|
61
|
+
results.push({ file, status: 'failed', error: error.reason })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const errors = results.filter((r) => r.status === 'failed')
|
|
66
|
+
|
|
67
|
+
if (options.json) {
|
|
68
|
+
const output = {
|
|
69
|
+
summary: {
|
|
70
|
+
files: results.length,
|
|
71
|
+
passed: results.length - errors.length,
|
|
72
|
+
failed: errors.length,
|
|
73
|
+
},
|
|
74
|
+
files: results,
|
|
75
|
+
}
|
|
76
|
+
console.log(JSON.stringify(output, null, 2))
|
|
77
|
+
|
|
78
|
+
// Still throw so exit code reflects failure
|
|
79
|
+
if (errors.length > 0) {
|
|
80
|
+
throw new ValidationError(
|
|
81
|
+
`Validation failed with ${errors.length} error(s)`,
|
|
82
|
+
{
|
|
83
|
+
code: 'VALIDATION_FAILED',
|
|
84
|
+
suggestion: 'Fix the errors listed above and run validation again'
|
|
85
|
+
}
|
|
86
|
+
)
|
|
56
87
|
}
|
|
88
|
+
return
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
if(
|
|
60
|
-
|
|
91
|
+
if (errors.length > 0) {
|
|
92
|
+
errors.forEach((r) => { this.logger.error(`${r.error}`) })
|
|
61
93
|
throw new ValidationError(
|
|
62
|
-
`Validation failed with ${
|
|
94
|
+
`Validation failed with ${errors.length} error(s)`,
|
|
63
95
|
{
|
|
64
96
|
code: 'VALIDATION_FAILED',
|
|
65
97
|
suggestion: 'Fix the errors listed above and run validation again'
|
|
@@ -40,21 +40,28 @@ test('registered command has correct options', (t) => {
|
|
|
40
40
|
const { command } = t.context
|
|
41
41
|
|
|
42
42
|
// Get all options
|
|
43
|
+
const jsonOption = command.options.find((opt) => opt.long === '--json')
|
|
43
44
|
const quietOption = command.options.find((opt) => opt.long === '--quiet')
|
|
44
45
|
const verboseOption = command.options.find((opt) => opt.long === '--verbose')
|
|
45
46
|
const debugOption = command.options.find((opt) => opt.long === '--debug')
|
|
46
47
|
|
|
47
48
|
// Verify all options exist
|
|
49
|
+
t.truthy(jsonOption, '--json option should exist')
|
|
48
50
|
t.truthy(quietOption, '--quiet option should exist')
|
|
49
51
|
t.truthy(verboseOption, '--verbose option should exist')
|
|
50
52
|
t.truthy(debugOption, '--debug option should exist')
|
|
51
53
|
|
|
52
54
|
// Verify they are Option instances
|
|
55
|
+
t.true(jsonOption instanceof Option, '--json should be Option instance')
|
|
53
56
|
t.true(quietOption instanceof Option, '--quiet should be Option instance')
|
|
54
57
|
t.true(verboseOption instanceof Option, '--verbose should be Option instance')
|
|
55
58
|
t.true(debugOption instanceof Option, '--debug should be Option instance')
|
|
56
59
|
|
|
57
60
|
// Verify option properties
|
|
61
|
+
t.is(jsonOption.long, '--json')
|
|
62
|
+
t.truthy(jsonOption.description)
|
|
63
|
+
t.false(jsonOption.required, '--json should not require a value')
|
|
64
|
+
|
|
58
65
|
t.is(quietOption.long, '--quiet')
|
|
59
66
|
t.is(quietOption.short, '-q')
|
|
60
67
|
t.truthy(quietOption.description)
|
|
@@ -76,6 +83,7 @@ test('command options are accessible via public API', (t) => {
|
|
|
76
83
|
// Test that options can be accessed the way Commander.js does
|
|
77
84
|
const optionNames = command.options.map((opt) => opt.long)
|
|
78
85
|
|
|
86
|
+
t.true(optionNames.includes('--json'))
|
|
79
87
|
t.true(optionNames.includes('--quiet'))
|
|
80
88
|
t.true(optionNames.includes('--verbose'))
|
|
81
89
|
t.true(optionNames.includes('--debug'))
|
|
@@ -232,3 +232,147 @@ test('validate command should pass debug option through', async (t) => {
|
|
|
232
232
|
t.true(mockYamlValidation.called, 'validation should run')
|
|
233
233
|
// Debug output should be logged (verified through console.debug stub)
|
|
234
234
|
})
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// JSON output tests
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
test.serial('validate --json should output valid JSON when all files pass', async (t) => {
|
|
241
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
242
|
+
const consoleLogStub = sandbox.stub(console, 'log')
|
|
243
|
+
|
|
244
|
+
const mockYamlValidation = sandbox.stub()
|
|
245
|
+
const mockTestcwd = sandbox.stub()
|
|
246
|
+
|
|
247
|
+
const ValidateCommand = await esmock('./validate.js', {
|
|
248
|
+
'../validators/validate-yaml.js': {
|
|
249
|
+
default: mockYamlValidation
|
|
250
|
+
},
|
|
251
|
+
'#lib/project/index.js': {
|
|
252
|
+
default: { getProjectRoot: () => '/project' }
|
|
253
|
+
},
|
|
254
|
+
'#helpers/test-cwd.js': {
|
|
255
|
+
default: mockTestcwd
|
|
256
|
+
},
|
|
257
|
+
'#lib/logger/index.js': {
|
|
258
|
+
logger: mockLogger
|
|
259
|
+
},
|
|
260
|
+
'fs-extra': fs
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const command = new ValidateCommand()
|
|
264
|
+
command.name = sandbox.stub().returns('validate')
|
|
265
|
+
|
|
266
|
+
command.action({ json: true }, command)
|
|
267
|
+
|
|
268
|
+
// Should output via console.log, not logger.info
|
|
269
|
+
t.true(consoleLogStub.calledOnce, 'console.log should be called once')
|
|
270
|
+
t.false(mockLogger.info.called, 'logger.info should not be called in JSON mode')
|
|
271
|
+
|
|
272
|
+
const output = JSON.parse(consoleLogStub.firstCall.args[0])
|
|
273
|
+
|
|
274
|
+
// Verify JSON structure
|
|
275
|
+
t.truthy(output.summary, 'JSON should have summary section')
|
|
276
|
+
t.truthy(output.files, 'JSON should have files section')
|
|
277
|
+
t.is(output.summary.files, 2, 'should report 2 YAML files')
|
|
278
|
+
t.is(output.summary.passed, 2, 'both files should pass')
|
|
279
|
+
t.is(output.summary.failed, 0, 'no files should fail')
|
|
280
|
+
|
|
281
|
+
// Verify each file result
|
|
282
|
+
t.true(output.files.every((f) => f.status === 'passed'), 'all files should have passed status')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test.serial('validate --json should output valid JSON with errors and throw', async (t) => {
|
|
286
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
287
|
+
const consoleLogStub = sandbox.stub(console, 'log')
|
|
288
|
+
|
|
289
|
+
const validationError = new Error('Invalid YAML')
|
|
290
|
+
validationError.reason = 'Invalid YAML: missing required field'
|
|
291
|
+
const mockYamlValidation = sandbox.stub().throws(validationError)
|
|
292
|
+
const mockTestcwd = sandbox.stub()
|
|
293
|
+
|
|
294
|
+
const ValidateCommand = await esmock('./validate.js', {
|
|
295
|
+
'../validators/validate-yaml.js': {
|
|
296
|
+
default: mockYamlValidation
|
|
297
|
+
},
|
|
298
|
+
'#lib/project/index.js': {
|
|
299
|
+
default: { getProjectRoot: () => '/project' }
|
|
300
|
+
},
|
|
301
|
+
'#helpers/test-cwd.js': {
|
|
302
|
+
default: mockTestcwd
|
|
303
|
+
},
|
|
304
|
+
'#lib/logger/index.js': {
|
|
305
|
+
logger: mockLogger
|
|
306
|
+
},
|
|
307
|
+
'fs-extra': fs
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const command = new ValidateCommand()
|
|
311
|
+
command.name = sandbox.stub().returns('validate')
|
|
312
|
+
command.logger = mockLogger
|
|
313
|
+
|
|
314
|
+
// Should still throw ValidationError for exit code
|
|
315
|
+
const error = t.throws(() => command.action({ json: true }, command), { instanceOf: ValidationError })
|
|
316
|
+
t.is(error.code, 'VALIDATION_FAILED')
|
|
317
|
+
|
|
318
|
+
// Should output JSON before throwing
|
|
319
|
+
t.true(consoleLogStub.calledOnce, 'console.log should be called once')
|
|
320
|
+
|
|
321
|
+
const output = JSON.parse(consoleLogStub.firstCall.args[0])
|
|
322
|
+
|
|
323
|
+
t.is(output.summary.files, 2)
|
|
324
|
+
t.is(output.summary.failed, 2, 'both files should fail')
|
|
325
|
+
t.is(output.summary.passed, 0)
|
|
326
|
+
|
|
327
|
+
// Verify error details in file results
|
|
328
|
+
t.true(output.files.every((f) => f.status === 'failed'), 'all files should have failed status')
|
|
329
|
+
t.true(output.files.every((f) => f.error), 'all failed files should have error details')
|
|
330
|
+
|
|
331
|
+
// logger.error should NOT be called in JSON mode
|
|
332
|
+
t.false(mockLogger.error.called, 'logger.error should not be called in JSON mode')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test.serial('validate --json should output mixed results', async (t) => {
|
|
336
|
+
const { sandbox, fs, mockLogger } = t.context
|
|
337
|
+
const consoleLogStub = sandbox.stub(console, 'log')
|
|
338
|
+
|
|
339
|
+
// First file passes, second file fails
|
|
340
|
+
const validationError = new Error('Invalid YAML')
|
|
341
|
+
validationError.reason = 'duplicated mapping key at line 5'
|
|
342
|
+
const mockYamlValidation = sandbox.stub()
|
|
343
|
+
mockYamlValidation.onFirstCall().returns(undefined)
|
|
344
|
+
mockYamlValidation.onSecondCall().throws(validationError)
|
|
345
|
+
const mockTestcwd = sandbox.stub()
|
|
346
|
+
|
|
347
|
+
const ValidateCommand = await esmock('./validate.js', {
|
|
348
|
+
'../validators/validate-yaml.js': {
|
|
349
|
+
default: mockYamlValidation
|
|
350
|
+
},
|
|
351
|
+
'#lib/project/index.js': {
|
|
352
|
+
default: { getProjectRoot: () => '/project' }
|
|
353
|
+
},
|
|
354
|
+
'#helpers/test-cwd.js': {
|
|
355
|
+
default: mockTestcwd
|
|
356
|
+
},
|
|
357
|
+
'#lib/logger/index.js': {
|
|
358
|
+
logger: mockLogger
|
|
359
|
+
},
|
|
360
|
+
'fs-extra': fs
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const command = new ValidateCommand()
|
|
364
|
+
command.name = sandbox.stub().returns('validate')
|
|
365
|
+
command.logger = mockLogger
|
|
366
|
+
|
|
367
|
+
// Should throw because there are errors
|
|
368
|
+
t.throws(() => command.action({ json: true }, command), { instanceOf: ValidationError })
|
|
369
|
+
|
|
370
|
+
const output = JSON.parse(consoleLogStub.firstCall.args[0])
|
|
371
|
+
|
|
372
|
+
t.is(output.summary.files, 2)
|
|
373
|
+
t.is(output.summary.passed, 1)
|
|
374
|
+
t.is(output.summary.failed, 1)
|
|
375
|
+
t.is(output.files[0].status, 'passed')
|
|
376
|
+
t.is(output.files[1].status, 'failed')
|
|
377
|
+
t.is(output.files[1].error, 'duplicated mapping key at line 5')
|
|
378
|
+
})
|
package/src/lib/11ty/api.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { dynamicImport } from '#helpers/os-utils.js'
|
|
34
34
|
import path from 'node:path'
|
|
35
35
|
import paths from '#lib/project/index.js'
|
|
36
|
-
import
|
|
36
|
+
import reporter from '#lib/reporter/index.js'
|
|
37
37
|
import createDebug from '#debug'
|
|
38
38
|
|
|
39
39
|
const debug = createDebug('lib:11ty:api')
|
|
@@ -172,15 +172,21 @@ class Quire11ty {
|
|
|
172
172
|
const projectRoot = this.paths.getProjectRoot()
|
|
173
173
|
process.chdir(projectRoot)
|
|
174
174
|
|
|
175
|
-
logger.info('Building site...')
|
|
176
|
-
|
|
177
175
|
configureEleventyEnv({ mode: 'production', debug: options.debug })
|
|
178
176
|
|
|
177
|
+
reporter.start('Building site...', { showElapsed: true })
|
|
178
|
+
|
|
179
179
|
const eleventy = await createEleventyInstance(options)
|
|
180
180
|
|
|
181
181
|
eleventy.setDryRun(options.dryRun)
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
try {
|
|
184
|
+
await eleventy.write()
|
|
185
|
+
reporter.succeed('Build complete')
|
|
186
|
+
} catch (error) {
|
|
187
|
+
reporter.fail('Build failed')
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
/**
|
|
@@ -196,10 +202,10 @@ class Quire11ty {
|
|
|
196
202
|
const projectRoot = this.paths.getProjectRoot()
|
|
197
203
|
process.chdir(projectRoot)
|
|
198
204
|
|
|
199
|
-
logger.info('Starting development server...')
|
|
200
|
-
|
|
201
205
|
configureEleventyEnv({ mode: 'development', debug: options.debug })
|
|
202
206
|
|
|
207
|
+
reporter.start('Starting development server...')
|
|
208
|
+
|
|
203
209
|
const eleventy =
|
|
204
210
|
await createEleventyInstance({ ...options, runMode: 'serve' })
|
|
205
211
|
|
|
@@ -209,6 +215,19 @@ class Quire11ty {
|
|
|
209
215
|
// Initialize Eleventy before serving (required for eleventyServe)
|
|
210
216
|
await eleventy.init()
|
|
211
217
|
|
|
218
|
+
// Register a ready callback to resolve the spinner when the server is listening
|
|
219
|
+
// @see https://www.11ty.dev/docs/dev-server/#options
|
|
220
|
+
eleventy.eleventyServe.config.serverOptions = {
|
|
221
|
+
...eleventy.eleventyServe.config.serverOptions,
|
|
222
|
+
ready: (server) => {
|
|
223
|
+
const url = server.getServerUrl('localhost')
|
|
224
|
+
reporter.succeed(`Server running at ${url}`)
|
|
225
|
+
if (options.open) {
|
|
226
|
+
import('open').then(({ default: open }) => open(url))
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
|
|
212
231
|
await eleventy.serve(options.port)
|
|
213
232
|
}
|
|
214
233
|
}
|
package/src/lib/11ty/cli.js
CHANGED
|
@@ -3,8 +3,8 @@ import fs from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import paths from '#lib/project/index.js'
|
|
5
5
|
import processManager from '#lib/process/manager.js'
|
|
6
|
+
import reporter from '#lib/reporter/index.js'
|
|
6
7
|
import { BuildFailedError } from '#src/errors/index.js'
|
|
7
|
-
import { logger } from '#lib/logger/index.js'
|
|
8
8
|
import createDebug from '#debug'
|
|
9
9
|
|
|
10
10
|
const debug = createDebug('lib:11ty')
|
|
@@ -108,14 +108,14 @@ export default {
|
|
|
108
108
|
* @param {Object} options - Build options
|
|
109
109
|
*/
|
|
110
110
|
build: async (options = {}) => {
|
|
111
|
-
logger.info('Building site...')
|
|
112
|
-
|
|
113
111
|
const { command, env, projectRoot } = factory(options)
|
|
114
112
|
|
|
115
113
|
if (options.dryRun) command.push('--dryrun')
|
|
116
114
|
|
|
117
115
|
env.ELEVENTY_ENV = 'production'
|
|
118
116
|
|
|
117
|
+
reporter.start('Building site...', { showElapsed: true })
|
|
118
|
+
|
|
119
119
|
try {
|
|
120
120
|
const build = spawn(command, { cwd: projectRoot, env })
|
|
121
121
|
await build
|
|
@@ -123,11 +123,13 @@ export default {
|
|
|
123
123
|
if (build.exitCode !== 0) {
|
|
124
124
|
throw new BuildFailedError(`Eleventy exited with code ${build.exitCode}`)
|
|
125
125
|
}
|
|
126
|
+
reporter.succeed('Build complete')
|
|
126
127
|
} catch (error) {
|
|
127
128
|
if (error.isCanceled) {
|
|
128
129
|
debug('build cancelled')
|
|
129
130
|
return
|
|
130
131
|
}
|
|
132
|
+
reporter.fail('Build failed')
|
|
131
133
|
throw error
|
|
132
134
|
}
|
|
133
135
|
},
|
|
@@ -137,16 +139,26 @@ export default {
|
|
|
137
139
|
* @param {Object} options - Serve options
|
|
138
140
|
*/
|
|
139
141
|
serve: async (options = {}) => {
|
|
140
|
-
logger.info('Starting development server...')
|
|
141
|
-
|
|
142
142
|
const { command, env, projectRoot } = factory(options)
|
|
143
143
|
|
|
144
144
|
command.push('--serve')
|
|
145
145
|
|
|
146
|
+
const port = options.port || 8080
|
|
146
147
|
if (options.port) command.push(`--port=${options.port}`)
|
|
147
148
|
|
|
148
149
|
env.ELEVENTY_ENV = 'development'
|
|
149
150
|
|
|
151
|
+
reporter.start('Starting development server...')
|
|
152
|
+
|
|
153
|
+
// Resolve the spinner before the subprocess takes over stdout
|
|
154
|
+
const url = `http://localhost:${port}`
|
|
155
|
+
reporter.succeed(`Server running at ${url}`)
|
|
156
|
+
|
|
157
|
+
if (options.open) {
|
|
158
|
+
const { default: open } = await import('open')
|
|
159
|
+
open(url)
|
|
160
|
+
}
|
|
161
|
+
|
|
150
162
|
try {
|
|
151
163
|
await spawn(command, { cwd: projectRoot, env })
|
|
152
164
|
} catch (error) {
|
package/src/lib/README.md
CHANGED
|
@@ -5,28 +5,28 @@ This directory contains domain-specific modules that encapsulate the core functi
|
|
|
5
5
|
## Architecture Overview
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
│
|
|
19
|
-
│ ┌────────────┐ ┌──────────┐ ┌────────────┐
|
|
20
|
-
│ │ project/ │ │ 11ty/ │ │ pdf/ │ │
|
|
21
|
-
│ │ │ │ │ │ │ │
|
|
22
|
-
│ │ - paths │ │ - api │ │ - pagedjs │ │ - epubjs
|
|
23
|
-
│ │ - config │ │ - cli │ │ - prince │ │ - pandoc
|
|
24
|
-
│ │ - detect │ │ │ │ │ │
|
|
25
|
-
│ │ - version │ │ │ │ │ │
|
|
26
|
-
│ └────┬───────┘ └────┬─────┘ └────┬───────┘
|
|
27
|
-
|
|
28
|
-
│ │ │
|
|
29
|
-
│
|
|
8
|
+
Commands Layer
|
|
9
|
+
┌───────────────────────────────────────────────────┐
|
|
10
|
+
│ build preview new pdf epub info doctor │
|
|
11
|
+
└────────────────────────┬──────────────────────────┘
|
|
12
|
+
│
|
|
13
|
+
┌────────────────────────┴──────────────────────────┐
|
|
14
|
+
│ lib/ modules │
|
|
15
|
+
└───────────────────────────────────────────────────┘
|
|
16
|
+
|
|
17
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ Domain Modules │
|
|
19
|
+
│ ┌────────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌────────┐ │
|
|
20
|
+
│ │ project/ │ │ 11ty/ │ │ pdf/ │ │ epub/ │ │doctor/ │ │
|
|
21
|
+
│ │ │ │ │ │ │ │ │ │ │ │
|
|
22
|
+
│ │ - paths │ │ - api │ │ - pagedjs │ │ - epubjs │ │-checks │ │
|
|
23
|
+
│ │ - config │ │ - cli │ │ - prince │ │ - pandoc │ │-format │ │
|
|
24
|
+
│ │ - detect │ │ │ │ │ │ │ │ │ │
|
|
25
|
+
│ │ - version │ │ │ │ │ │ │ │ │ │
|
|
26
|
+
│ └────┬───────┘ └────┬─────┘ └────┬───────┘ └────┬─────┘ └───┬────┘ │
|
|
27
|
+
└───────┼───────────────┼─────────────┼───────────────┼─────────────┼──────┘
|
|
28
|
+
│ │ │ │ │
|
|
29
|
+
│ ┌────────┴─────────────┴───────────────┴─────────────┘
|
|
30
30
|
│ │
|
|
31
31
|
┌───────┴──────┴──────────────────────────────────────────────────┐
|
|
32
32
|
│ Installation Module │
|
|
@@ -65,6 +65,10 @@ These modules encapsulate specific business domains of Quire.
|
|
|
65
65
|
|--------|-------------|
|
|
66
66
|
| `paths` (default) | Singleton for path resolution |
|
|
67
67
|
| `Paths` | Class for custom path instances |
|
|
68
|
+
| `DATA_DIR` | Path to data files directory (`content/_data`) |
|
|
69
|
+
| `PROJECT_MARKERS` | Files that identify a Quire project |
|
|
70
|
+
| `REQUIRED_DATA_FILES` | Required data files (`publication.yaml`) |
|
|
71
|
+
| `SOURCE_DIRECTORIES` | Directories monitored for changes |
|
|
68
72
|
| `detect(dirpath)` | Check if directory is a Quire project |
|
|
69
73
|
| `loadProjectConfig(projectRoot?)` | Load and validate project config |
|
|
70
74
|
| `getVersion(projectPath?)` | Read `quire-11ty` version from project |
|
|
@@ -107,6 +111,57 @@ These modules encapsulate specific business domains of Quire.
|
|
|
107
111
|
|
|
108
112
|
**Dependencies:** `project/`
|
|
109
113
|
|
|
114
|
+
#### `doctor/`
|
|
115
|
+
**Purpose:** Diagnostic checks for Quire environment and project health.
|
|
116
|
+
|
|
117
|
+
| Export | Description |
|
|
118
|
+
|--------|-------------|
|
|
119
|
+
| `default` | Object with all check functions and arrays |
|
|
120
|
+
| `checks` | Flat array of all diagnostic checks |
|
|
121
|
+
| `checkSections` | Checks organized by section (Environment, Project) |
|
|
122
|
+
| `runAllChecks()` | Run all checks, return flat results array |
|
|
123
|
+
| `runAllChecksWithSections()` | Run all checks, return results by section |
|
|
124
|
+
| `checkNodeVersion()` | Verify Node.js >= 22 |
|
|
125
|
+
| `checkNpmAvailable()` | Verify npm in PATH |
|
|
126
|
+
| `checkGitAvailable()` | Verify git in PATH |
|
|
127
|
+
| `checkQuireProject()` | Detect project marker files |
|
|
128
|
+
| `checkDependencies()` | Verify node_modules exists |
|
|
129
|
+
| `checkDataFiles()` | Validate YAML files in content/_data/ |
|
|
130
|
+
| `checkStaleBuild()` | Compare source vs build timestamps |
|
|
131
|
+
|
|
132
|
+
**Sub-modules:**
|
|
133
|
+
|
|
134
|
+
| File | Description |
|
|
135
|
+
|------|-------------|
|
|
136
|
+
| `formatDuration.js` | Human-readable time duration formatting |
|
|
137
|
+
|
|
138
|
+
**Duration Formatting:**
|
|
139
|
+
|
|
140
|
+
The `formatDuration` function converts milliseconds to the most appropriate time unit:
|
|
141
|
+
|
|
142
|
+
| Duration | Example Output |
|
|
143
|
+
|----------------|-----------------|
|
|
144
|
+
| < 1 minute | "45 seconds" |
|
|
145
|
+
| < 1 hour | "30 minutes" |
|
|
146
|
+
| < 1 day | "5 hours" |
|
|
147
|
+
| < 1 week | "3 days" |
|
|
148
|
+
| < 1 month | "2 weeks" |
|
|
149
|
+
| < 1 year | "3 months" |
|
|
150
|
+
| >= 1 year | "2 years" |
|
|
151
|
+
|
|
152
|
+
**Data Files Validation:**
|
|
153
|
+
|
|
154
|
+
The `checkDataFiles` function validates YAML files in `content/_data/`:
|
|
155
|
+
|
|
156
|
+
| Validation | Description |
|
|
157
|
+
|------------|-------------|
|
|
158
|
+
| Required files | Checks `publication.yaml` exists |
|
|
159
|
+
| YAML syntax | Parses each file and reports syntax errors |
|
|
160
|
+
| Schema validation | Validates against JSON schemas in `schemas/` |
|
|
161
|
+
| Duplicate IDs | Detects duplicate `id` values in arrays |
|
|
162
|
+
|
|
163
|
+
**Dependencies:** `project/`, `npm/`, `git/`, `validators/validate-data-files`
|
|
164
|
+
|
|
110
165
|
#### `installer/`
|
|
111
166
|
**Purpose:** Installation of `@thegetty/quire-11ty` into Quire projects.
|
|
112
167
|
|
package/src/lib/conf/README.md
CHANGED
|
@@ -93,6 +93,12 @@ quire settings get <key> --json # Output single value as JSON
|
|
|
93
93
|
quire settings --json --debug # Include __internal__ keys in JSON output
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
`staleThreshold` How stale an output must be before `quire doctor` warns; default `'HOURLY'`. Options: `'ZERO'` (0 min), `'SHORT'` (5 min), `'HOURLY'` (60 min), `'DAILY'` (12 hours), `'NEVER'` (disabled).
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
❯ quire config set staleThreshold DAILY
|
|
100
|
+
```
|
|
101
|
+
|
|
96
102
|
The `--json` flag outputs raw JSON to stdout (bypassing the logger), suitable for piping to `jq` or other tools. `__internal__` keys are excluded from JSON output unless `--debug` is also passed, consistent with the plain-text display.
|
|
97
103
|
|
|
98
104
|
| Key | Type | Default | Description |
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project build status persistence
|
|
3
|
+
*
|
|
4
|
+
* Records command outcomes (ok/failed) for build, pdf, and epub commands
|
|
5
|
+
* so that `quire doctor` can distinguish "never ran" from "ran and failed"
|
|
6
|
+
* when no output files exist on disk.
|
|
7
|
+
*
|
|
8
|
+
* Status is stored in the global CLI config under the `projects` key,
|
|
9
|
+
* keyed by a SHA-256 hash of the project's absolute path to avoid
|
|
10
|
+
* dot-notation traversal issues.
|
|
11
|
+
*
|
|
12
|
+
* @module lib/conf/build-status
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import config from '#lib/conf/config.js'
|
|
16
|
+
import createDebug from '#debug'
|
|
17
|
+
|
|
18
|
+
const debug = createDebug('lib:conf:build-status')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Commands whose status is tracked
|
|
22
|
+
* @type {readonly string[]}
|
|
23
|
+
*/
|
|
24
|
+
export const TRACKED_COMMANDS = Object.freeze(['build', 'pdf', 'epub'])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Derive a fixed-length, dot-safe config key from a project path
|
|
28
|
+
*
|
|
29
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
30
|
+
* @returns {string} First 12 hex characters of the SHA-256 hash
|
|
31
|
+
*/
|
|
32
|
+
export function projectKey(projectPath) {
|
|
33
|
+
return createHash('sha256').update(projectPath).digest('hex').slice(0, 12)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Record the outcome of a command for a project
|
|
38
|
+
*
|
|
39
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
40
|
+
* @param {string} command - Command name ('build', 'pdf', or 'epub')
|
|
41
|
+
* @param {'ok'|'failed'} status - Outcome of the command
|
|
42
|
+
*/
|
|
43
|
+
export function recordStatus(projectPath, command, status) {
|
|
44
|
+
if (!TRACKED_COMMANDS.includes(command)) {
|
|
45
|
+
debug('ignoring untracked command: %s', command)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const key = projectKey(projectPath)
|
|
50
|
+
const projects = config.get('projects') || {}
|
|
51
|
+
|
|
52
|
+
const entry = projects[key] || { projectPath }
|
|
53
|
+
entry.projectPath = projectPath
|
|
54
|
+
entry.buildStatus = {
|
|
55
|
+
...entry.buildStatus,
|
|
56
|
+
[command]: {
|
|
57
|
+
status,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
projects[key] = entry
|
|
62
|
+
|
|
63
|
+
config.set('projects', projects)
|
|
64
|
+
debug('recorded %s=%s for %s (key=%s)', command, status, projectPath, key)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retrieve the stored status for a command in a project
|
|
69
|
+
*
|
|
70
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
71
|
+
* @param {string} command - Command name ('build', 'pdf', or 'epub')
|
|
72
|
+
* @returns {{ status: 'ok'|'failed', timestamp: number } | undefined}
|
|
73
|
+
*/
|
|
74
|
+
export function getStatus(projectPath, command) {
|
|
75
|
+
const key = projectKey(projectPath)
|
|
76
|
+
const projects = config.get('projects') || {}
|
|
77
|
+
const entry = projects[key]
|
|
78
|
+
|
|
79
|
+
if (!entry || !entry.buildStatus) {
|
|
80
|
+
debug('no status entry for key=%s', key)
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return entry.buildStatus[command]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear all stored status for a project
|
|
89
|
+
*
|
|
90
|
+
* @param {string} projectPath - Absolute path to the project directory
|
|
91
|
+
*/
|
|
92
|
+
export function clearStatus(projectPath) {
|
|
93
|
+
const key = projectKey(projectPath)
|
|
94
|
+
const projects = config.get('projects') || {}
|
|
95
|
+
const entry = projects[key]
|
|
96
|
+
|
|
97
|
+
if (entry && entry.buildStatus) {
|
|
98
|
+
delete entry.buildStatus
|
|
99
|
+
projects[key] = entry
|
|
100
|
+
config.set('projects', projects)
|
|
101
|
+
debug('cleared build status for %s (key=%s)', projectPath, key)
|
|
102
|
+
}
|
|
103
|
+
}
|