@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.
Files changed (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/schemas/config.schema.json +194 -0
  6. package/schemas/figures.schema.json +56 -0
  7. package/schemas/layout.schema.json +5 -0
  8. package/schemas/objects.schema.json +62 -0
  9. package/schemas/publication.schema.json +140 -0
  10. package/schemas/references.schema.json +29 -0
  11. package/src/Command.js +26 -6
  12. package/src/Command.spec.js +99 -0
  13. package/src/commands/README.md +213 -122
  14. package/src/commands/build.js +46 -37
  15. package/src/commands/build.spec.js +113 -0
  16. package/src/commands/build.test.js +402 -0
  17. package/src/commands/clean.js +25 -19
  18. package/src/commands/clean.spec.js +108 -0
  19. package/src/commands/clean.test.js +260 -0
  20. package/src/commands/config.js +251 -0
  21. package/src/commands/config.spec.js +107 -0
  22. package/src/commands/config.test.js +715 -0
  23. package/src/commands/create.js +42 -14
  24. package/src/commands/create.spec.js +112 -0
  25. package/src/commands/create.test.js +415 -0
  26. package/src/commands/epub.js +59 -30
  27. package/src/commands/epub.spec.js +114 -0
  28. package/src/commands/epub.test.js +503 -0
  29. package/src/commands/index.js +9 -2
  30. package/src/commands/info.js +18 -24
  31. package/src/commands/info.spec.js +64 -0
  32. package/src/commands/info.test.js +415 -0
  33. package/src/commands/pdf.js +58 -84
  34. package/src/commands/pdf.spec.js +114 -0
  35. package/src/commands/pdf.test.js +464 -0
  36. package/src/commands/preview.js +26 -31
  37. package/src/commands/preview.spec.js +97 -0
  38. package/src/commands/preview.test.js +250 -0
  39. package/src/commands/use.js +56 -0
  40. package/src/commands/use.spec.js +61 -0
  41. package/src/commands/use.test.js +280 -0
  42. package/src/commands/validate.js +31 -19
  43. package/src/commands/validate.spec.js +82 -0
  44. package/src/commands/validate.test.js +234 -0
  45. package/src/commands/workflows.js +70 -0
  46. package/src/errors/build/build-failed-error.js +19 -0
  47. package/src/errors/build/config-field-missing-error.js +20 -0
  48. package/src/errors/build/config-file-not-found-error.js +20 -0
  49. package/src/errors/build/index.js +11 -0
  50. package/src/errors/index.js +48 -0
  51. package/src/errors/install/dependency-install-error.js +19 -0
  52. package/src/errors/install/directory-not-empty-error.js +25 -0
  53. package/src/errors/install/index.js +12 -0
  54. package/src/errors/install/invalid-path-error.js +31 -0
  55. package/src/errors/install/invalid-starter-error.js +27 -0
  56. package/src/errors/install/version-not-found-error.js +35 -0
  57. package/src/errors/output/epub-generation-error.js +19 -0
  58. package/src/errors/output/index.js +14 -0
  59. package/src/errors/output/invalid-epub-library-error.js +20 -0
  60. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  61. package/src/errors/output/missing-build-output-error.js +21 -0
  62. package/src/errors/output/pdf-generation-error.js +24 -0
  63. package/src/errors/output/tool-not-found-error.js +37 -0
  64. package/src/errors/project/index.js +10 -0
  65. package/src/errors/project/not-in-project-error.js +20 -0
  66. package/src/errors/project/project-create-error.js +21 -0
  67. package/src/errors/quire-error.js +27 -0
  68. package/src/errors/validation/validation-error.js +20 -11
  69. package/src/helpers/clean.js +1 -1
  70. package/src/helpers/docs-url.js +32 -0
  71. package/src/helpers/test-cwd.js +5 -6
  72. package/src/helpers/test-cwd.test.js +192 -0
  73. package/src/helpers/which.js +10 -4
  74. package/src/lib/11ty/README.md +135 -19
  75. package/src/lib/11ty/api.js +176 -93
  76. package/src/lib/11ty/cli.js +77 -35
  77. package/src/lib/11ty/index.js +64 -5
  78. package/src/lib/11ty/index.test.js +655 -0
  79. package/src/lib/README.md +275 -0
  80. package/src/lib/commander/index.js +100 -0
  81. package/src/lib/commander/index.test.js +86 -0
  82. package/src/lib/commander/options.js +195 -0
  83. package/src/lib/commander/options.test.js +109 -0
  84. package/src/lib/conf/README.md +84 -73
  85. package/src/lib/conf/config.js +5 -3
  86. package/src/lib/conf/config.test.js +281 -0
  87. package/src/lib/conf/defaults.js +44 -0
  88. package/src/lib/conf/format.js +60 -0
  89. package/src/lib/conf/format.test.js +106 -0
  90. package/src/lib/conf/helpers.js +91 -0
  91. package/src/lib/conf/helpers.test.js +136 -0
  92. package/src/lib/conf/index.js +22 -0
  93. package/src/lib/conf/schema.js +53 -8
  94. package/src/lib/epub/README.md +133 -2
  95. package/src/lib/epub/engines.js +46 -0
  96. package/src/lib/epub/epub.js +36 -11
  97. package/src/lib/epub/index.js +111 -21
  98. package/src/lib/epub/index.test.js +518 -0
  99. package/src/lib/epub/pandoc.js +33 -4
  100. package/src/lib/epub/pandoc.test.js +122 -0
  101. package/src/lib/epub/schema.js +21 -0
  102. package/src/lib/error/README.md +170 -0
  103. package/src/lib/error/handler.js +107 -0
  104. package/src/lib/git/README.md +151 -2
  105. package/src/lib/git/index.js +217 -13
  106. package/src/lib/git/index.spec.js +80 -0
  107. package/src/lib/git/index.test.js +453 -0
  108. package/src/lib/installer/index.js +309 -0
  109. package/src/lib/installer/index.spec.js +83 -0
  110. package/src/lib/installer/index.test.js +545 -0
  111. package/src/lib/logger/README.md +424 -0
  112. package/src/lib/logger/debug.js +90 -0
  113. package/src/lib/logger/debug.spec.js +130 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,415 @@
1
+ import test from 'ava'
2
+ import sinon from 'sinon'
3
+ import esmock from 'esmock'
4
+
5
+ test.beforeEach((t) => {
6
+ // Create sinon sandbox for mocking
7
+ t.context.sandbox = sinon.createSandbox()
8
+
9
+ // Create mock logger
10
+ t.context.mockLogger = {
11
+ info: t.context.sandbox.stub(),
12
+ error: t.context.sandbox.stub(),
13
+ debug: t.context.sandbox.stub(),
14
+ log: t.context.sandbox.stub(),
15
+ warn: t.context.sandbox.stub()
16
+ }
17
+
18
+ // Mock config
19
+ t.context.mockConfig = {
20
+ get: t.context.sandbox.stub().returns('./.quire-version')
21
+ }
22
+
23
+ // Mock testcwd helper
24
+ t.context.mockTestcwd = t.context.sandbox.stub()
25
+
26
+ // Mock execaCommand for external commands
27
+ t.context.mockExecaCommand = t.context.sandbox.stub()
28
+ t.context.mockExecaCommand.withArgs('quire --version').resolves({ stdout: '1.0.0-rc.33' })
29
+ t.context.mockExecaCommand.withArgs('npm --version').resolves({ stdout: '10.2.4' })
30
+
31
+ // Mock os module
32
+ t.context.mockOs = {
33
+ type: t.context.sandbox.stub().returns('Darwin'),
34
+ release: t.context.sandbox.stub().returns('23.0.0')
35
+ }
36
+
37
+ // Mock fs module
38
+ t.context.mockFs = {
39
+ readFileSync: t.context.sandbox.stub(),
40
+ writeFileSync: t.context.sandbox.stub(),
41
+ existsSync: t.context.sandbox.stub().returns(true)
42
+ }
43
+
44
+ // Default version file content
45
+ t.context.versionFileContent = JSON.stringify({
46
+ cli: '1.0.0-rc.33',
47
+ starter: 'https://github.com/thegetty/quire-starter-default'
48
+ })
49
+
50
+ // Default package.json content
51
+ t.context.packageJsonContent = JSON.stringify({
52
+ name: 'test-project',
53
+ version: '1.0.0'
54
+ })
55
+
56
+ // Setup default readFileSync behavior - stub returns different content based on file path
57
+ t.context.mockFs.readFileSync.callsFake((filePath, options) => {
58
+ if (filePath === './.quire-version') {
59
+ return t.context.versionFileContent
60
+ }
61
+ if (filePath === './package.json') {
62
+ return t.context.packageJsonContent
63
+ }
64
+ // For any other file, return empty string
65
+ return ''
66
+ })
67
+ })
68
+
69
+ test.afterEach.always((t) => {
70
+ // Restore all mocks
71
+ t.context.sandbox.restore()
72
+ })
73
+
74
+ test('info command should validate Quire project in preAction', async (t) => {
75
+ const { sandbox, mockLogger, mockTestcwd, mockConfig, mockExecaCommand, mockOs, mockFs } = t.context
76
+
77
+ const { default: InfoCommand } = await esmock('./info.js', {
78
+ '#helpers/test-cwd.js': {
79
+ default: mockTestcwd
80
+ },
81
+ 'execa': {
82
+ execaCommand: mockExecaCommand
83
+ },
84
+ 'node:os': mockOs,
85
+ 'node:fs': mockFs
86
+ }, {
87
+ '#lib/logger/index.js': {
88
+ default: () => mockLogger
89
+ }
90
+ })
91
+
92
+ const command = new InfoCommand()
93
+ command.config = mockConfig
94
+ command.logger = mockLogger
95
+ command.debug = sandbox.stub()
96
+
97
+ // Mock Commander.js command object
98
+ const mockCommand = {
99
+ name: () => 'info',
100
+ opts: () => ({})
101
+ }
102
+
103
+ // Call preAction
104
+ command.preAction(mockCommand)
105
+
106
+ t.true(mockTestcwd.called, 'testcwd should be called in preAction')
107
+ t.true(mockTestcwd.calledWith(mockCommand), 'testcwd should be called with command')
108
+ })
109
+
110
+ test('info command should display project version information', async (t) => {
111
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
112
+
113
+ const { default: InfoCommand } = await esmock('./info.js', {
114
+ '#helpers/test-cwd.js': {
115
+ default: mockTestcwd
116
+ },
117
+ 'execa': {
118
+ execaCommand: mockExecaCommand
119
+ },
120
+ 'node:os': mockOs,
121
+ 'node:fs': mockFs
122
+ }, {
123
+ '#lib/logger/index.js': {
124
+ default: () => mockLogger
125
+ }
126
+ })
127
+
128
+ const command = new InfoCommand()
129
+ command.config = mockConfig
130
+ command.logger = mockLogger
131
+ command.debug = sandbox.stub()
132
+
133
+ await command.action({}, {})
134
+
135
+ // Verify logger.info was called with project information
136
+ t.true(mockLogger.info.called, 'logger.info should be called')
137
+
138
+ // Check for project section header (uses current directory name)
139
+ const calls = mockLogger.info.getCalls()
140
+ const hasProjectInfo = calls.some((call) =>
141
+ call.args[0] && call.args[0].includes('[') && call.args[0].includes(']')
142
+ )
143
+ t.true(hasProjectInfo, 'should display project section')
144
+
145
+ // Check for quire-cli version in output
146
+ const hasCliVersion = calls.some((call) =>
147
+ call.args[0] && call.args[0].includes('quire-cli') && call.args[0].includes('1.0.0-rc.33')
148
+ )
149
+ t.true(hasCliVersion, 'should display quire-cli version')
150
+ })
151
+
152
+ test('info command should display system information with debug flag', async (t) => {
153
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
154
+
155
+ const { default: InfoCommand } = await esmock('./info.js', {
156
+ '#helpers/test-cwd.js': {
157
+ default: mockTestcwd
158
+ },
159
+ 'execa': {
160
+ execaCommand: mockExecaCommand
161
+ },
162
+ 'node:os': mockOs,
163
+ 'node:fs': mockFs
164
+ }, {
165
+ '#lib/logger/index.js': {
166
+ default: () => mockLogger
167
+ }
168
+ })
169
+
170
+ const command = new InfoCommand()
171
+ command.config = mockConfig
172
+ command.logger = mockLogger
173
+ command.debug = sandbox.stub()
174
+
175
+ await command.action({ debug: true }, {})
176
+
177
+ // Verify logger.info was called
178
+ t.true(mockLogger.info.called, 'logger.info should be called')
179
+
180
+ // Check for system section header
181
+ const calls = mockLogger.info.getCalls()
182
+ const hasSystemInfo = calls.some((call) =>
183
+ call.args[0] && call.args[0].includes('[System]')
184
+ )
185
+ t.true(hasSystemInfo, 'should display system section with debug flag')
186
+
187
+ // Check for node version in output (only shown with debug)
188
+ const hasNodeVersion = calls.some((call) =>
189
+ call.args[0] && call.args[0].includes('node')
190
+ )
191
+ t.true(hasNodeVersion, 'should display node version with debug flag')
192
+
193
+ // Check for npm version in output (only shown with debug)
194
+ const hasNpmVersion = calls.some((call) =>
195
+ call.args[0] && call.args[0].includes('npm')
196
+ )
197
+ t.true(hasNpmVersion, 'should display npm version with debug flag')
198
+ })
199
+
200
+ test('info command should hide system details without debug flag', async (t) => {
201
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
202
+
203
+ const { default: InfoCommand } = await esmock('./info.js', {
204
+ '#helpers/test-cwd.js': {
205
+ default: mockTestcwd
206
+ },
207
+ 'execa': {
208
+ execaCommand: mockExecaCommand
209
+ },
210
+ 'node:os': mockOs,
211
+ 'node:fs': mockFs
212
+ }, {
213
+ '#lib/logger/index.js': {
214
+ default: () => mockLogger
215
+ }
216
+ })
217
+
218
+ const command = new InfoCommand()
219
+ command.config = mockConfig
220
+ command.logger = mockLogger
221
+ command.debug = sandbox.stub()
222
+
223
+ await command.action({}, {})
224
+
225
+ // Verify logger.info was called
226
+ t.true(mockLogger.info.called, 'logger.info should be called')
227
+
228
+ // Check that system details are NOT shown without debug flag
229
+ const calls = mockLogger.info.getCalls()
230
+
231
+ // Node version should not appear in non-debug output
232
+ const hasNodeVersion = calls.some((call) =>
233
+ call.args[0] && call.args[0].toLowerCase().includes('node') && !call.args[0].includes('quire')
234
+ )
235
+ t.false(hasNodeVersion, 'should not display node version without debug flag')
236
+ })
237
+
238
+ test('info command should handle missing version file', async (t) => {
239
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
240
+
241
+ // Setup mockFs to throw error for missing version file (simulating file not found)
242
+ mockFs.readFileSync.callsFake((filePath, options) => {
243
+ if (filePath === './.quire-version') {
244
+ throw new Error('ENOENT: no such file')
245
+ }
246
+ if (filePath === './package.json') {
247
+ return t.context.packageJsonContent
248
+ }
249
+ return ''
250
+ })
251
+
252
+ const { default: InfoCommand } = await esmock('./info.js', {
253
+ '#helpers/test-cwd.js': {
254
+ default: mockTestcwd
255
+ },
256
+ 'execa': {
257
+ execaCommand: mockExecaCommand
258
+ },
259
+ 'node:os': mockOs,
260
+ 'node:fs': mockFs
261
+ }, {
262
+ '#lib/logger/index.js': {
263
+ default: () => mockLogger
264
+ }
265
+ })
266
+
267
+ const command = new InfoCommand()
268
+ command.config = mockConfig
269
+ command.logger = mockLogger
270
+ command.debug = sandbox.stub()
271
+
272
+ await command.action({}, {})
273
+
274
+ // Verify warning was logged about old version
275
+ t.true(mockLogger.warn.called, 'logger.warn should be called for missing version file')
276
+ t.true(
277
+ mockLogger.warn.calledWith(sinon.match(/prior to version 1.0.0.rc-8/)),
278
+ 'should warn about old project version'
279
+ )
280
+
281
+ // Verify version file was created
282
+ t.true(mockFs.writeFileSync.called, 'writeFileSync should be called to create version file')
283
+ })
284
+
285
+ test('info command should handle malformed version file', async (t) => {
286
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
287
+
288
+ // Setup mockFs to return malformed JSON (simulating corrupted file)
289
+ mockFs.readFileSync.callsFake((filePath, options) => {
290
+ if (filePath === './.quire-version') {
291
+ return 'invalid json{'
292
+ }
293
+ if (filePath === './package.json') {
294
+ return t.context.packageJsonContent
295
+ }
296
+ return ''
297
+ })
298
+
299
+ const { default: InfoCommand } = await esmock('./info.js', {
300
+ '#helpers/test-cwd.js': {
301
+ default: mockTestcwd
302
+ },
303
+ 'execa': {
304
+ execaCommand: mockExecaCommand
305
+ },
306
+ 'node:os': mockOs,
307
+ 'node:fs': mockFs
308
+ }, {
309
+ '#lib/logger/index.js': {
310
+ default: () => mockLogger
311
+ }
312
+ })
313
+
314
+ const command = new InfoCommand()
315
+ command.config = mockConfig
316
+ command.logger = mockLogger
317
+ command.debug = sandbox.stub()
318
+
319
+ await command.action({}, {})
320
+
321
+ // Verify warning was logged
322
+ t.true(mockLogger.warn.called, 'logger.warn should be called for malformed version file')
323
+
324
+ // Verify version file was overwritten with valid JSON
325
+ t.true(mockFs.writeFileSync.called, 'writeFileSync should be called to fix malformed version file')
326
+
327
+ // Check that the content written is valid JSON
328
+ const writeCall = mockFs.writeFileSync.getCall(0)
329
+ if (writeCall) {
330
+ const writtenContent = writeCall.args[1]
331
+ t.notThrows(() => JSON.parse(writtenContent), 'written content should be valid JSON')
332
+ }
333
+ })
334
+
335
+ test('info command should read quire-11ty version from package.json', async (t) => {
336
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
337
+
338
+ // Override package.json to have specific version
339
+ mockFs.readFileSync.callsFake((filePath, options) => {
340
+ if (filePath === './.quire-version') {
341
+ return t.context.versionFileContent
342
+ }
343
+ if (filePath === './package.json') {
344
+ return JSON.stringify({
345
+ name: 'test-project',
346
+ version: '2.5.3'
347
+ })
348
+ }
349
+ return ''
350
+ })
351
+
352
+ const { default: InfoCommand } = await esmock('./info.js', {
353
+ '#helpers/test-cwd.js': {
354
+ default: mockTestcwd
355
+ },
356
+ 'execa': {
357
+ execaCommand: mockExecaCommand
358
+ },
359
+ 'node:os': mockOs,
360
+ 'node:fs': mockFs
361
+ }, {
362
+ '#lib/logger/index.js': {
363
+ default: () => mockLogger
364
+ }
365
+ })
366
+
367
+ const command = new InfoCommand()
368
+ command.config = mockConfig
369
+ command.logger = mockLogger
370
+ command.debug = sandbox.stub()
371
+
372
+ await command.action({}, {})
373
+
374
+ // Verify logger.info was called
375
+ t.true(mockLogger.info.called, 'logger.info should be called')
376
+
377
+ // Check for quire-11ty version from package.json
378
+ const calls = mockLogger.info.getCalls()
379
+ const hasEleventyVersion = calls.some((call) =>
380
+ call.args[0] && call.args[0].includes('quire-11ty') && call.args[0].includes('2.5.3')
381
+ )
382
+ t.true(hasEleventyVersion, 'should display quire-11ty version from package.json')
383
+ })
384
+
385
+ test('info command should log debug information when debug flag is set', async (t) => {
386
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
387
+
388
+ const mockDebug = sandbox.stub()
389
+
390
+ const { default: InfoCommand } = await esmock('./info.js', {
391
+ '#helpers/test-cwd.js': {
392
+ default: mockTestcwd
393
+ },
394
+ 'execa': {
395
+ execaCommand: mockExecaCommand
396
+ },
397
+ 'node:os': mockOs,
398
+ 'node:fs': mockFs
399
+ }, {
400
+ '#lib/logger/index.js': {
401
+ default: () => mockLogger
402
+ }
403
+ })
404
+
405
+ const command = new InfoCommand()
406
+ command.config = mockConfig
407
+ command.logger = mockLogger
408
+ command.debug = mockDebug
409
+
410
+ await command.action({ debug: true }, {})
411
+
412
+ // Verify debug was called
413
+ t.true(mockDebug.called, 'debug should be called with debug flag')
414
+ t.true(mockDebug.calledWith('called with options %O', { debug: true }))
415
+ })
@@ -1,50 +1,13 @@
1
- import { paths, projectRoot } from '#lib/11ty/index.js'
2
1
  import Command from '#src/Command.js'
3
- import fs from 'fs-extra'
4
- import libPdf from '#lib/pdf/index.js'
2
+ import { withOutputModes } from '#lib/commander/index.js'
3
+ import paths, { hasSiteOutput } from '#lib/project/index.js'
4
+ import eleventy from '#lib/11ty/index.js'
5
+ import generatePdf, { ENGINES } from '#lib/pdf/index.js'
5
6
  import open from 'open'
6
7
  import path from 'node:path'
7
- import { pathToFileURL } from 'node:url'
8
- import yaml from 'js-yaml'
9
-
10
- /**
11
- * @function loadConfig(path) - loads and validates quire config, returns an empty object if not found
12
- *
13
- * @param {path} string - file path to config file
14
- *
15
- * @return {Object|undefined} User configuration object or undefined
16
- *
17
- * @todo consider hardcoding a version check against .quire / project's package.json ver
18
- */
19
- async function loadConfig(configPath) {
20
- if (!fs.existsSync(configPath)) {
21
- return undefined
22
- }
23
-
24
- const data = fs.readFileSync(configPath)
25
- let config = yaml.load(data)
26
-
27
- // NB: Schemas and validators are specific to the 11ty version of the project being built
28
- const schemaPath = path.join(projectRoot,'_plugins','schemas','config.json')
29
- const validatorPath = path.join(projectRoot, '_plugins', 'globalData', 'validator.js')
30
-
31
- if (fs.existsSync(schemaPath) && fs.existsSync(validatorPath)) {
32
-
33
- const { validateUserConfig } = await import(pathToFileURL(validatorPath))
34
-
35
- const schemaJSON = fs.readFileSync(schemaPath)
36
- const schema = JSON.parse(schemaJSON)
37
-
38
- try {
39
- config = validateUserConfig('config', config, { config: schema })
40
- } catch (error) {
41
- console.error(error)
42
- process.exit(1)
43
- }
44
- }
45
-
46
- return config
47
- }
8
+ import reporter from '#lib/reporter/index.js'
9
+ import testcwd from '#helpers/test-cwd.js'
10
+ import { MissingBuildOutputError } from '#src/errors/index.js'
48
11
 
49
12
  /**
50
13
  * Quire CLI `pdf` Command
@@ -55,66 +18,77 @@ async function loadConfig(configPath) {
55
18
  * @extends {Command}
56
19
  */
57
20
  export default class PDFCommand extends Command {
58
- static definition = {
21
+ static definition = withOutputModes({
59
22
  name: 'pdf',
60
23
  description: 'Generate publication PDF',
61
- summary: 'run build pdf',
24
+ summary: 'generate print-ready PDF',
25
+ docsLink: 'quire-commands/#output-files',
26
+ helpText: `
27
+ Examples:
28
+ quire pdf Generate PDF using default engine
29
+ quire pdf --engine prince Generate PDF using PrinceXML
30
+ quire pdf --build Build site first, then generate PDF
31
+ quire pdf --output my-book.pdf Generate PDF with custom output path
32
+ quire pdf --verbose Generate with detailed progress
33
+ `,
62
34
  version: '1.0.0',
63
- args: [
64
- ],
65
35
  options: [
36
+ [ '--build', 'run build first if output is missing' ],
37
+ [ '--open', 'open PDF in default application' ],
38
+ [ '-o, --output <path>', 'output file path (default: from project config)' ],
66
39
  [
67
- '--lib <module>', 'use the specified pdf module', 'pagedjs',
68
- { choices: ['pagedjs', 'prince'], default: 'pagedjs' }
40
+ '--engine <name>', 'PDF engine to use (default: from config or pagedjs)',
41
+ { choices: ENGINES }
42
+ ],
43
+ [
44
+ '--lib <name>', 'deprecated alias for --engine option',
45
+ { hidden: true, choices: ENGINES, conflicts: 'engine' }
69
46
  ],
70
- [ '--open', 'open PDF in default application' ],
71
- [ '--debug', 'run build with debug output to console' ],
72
47
  ],
73
- }
48
+ })
74
49
 
75
50
  constructor() {
76
51
  super(PDFCommand.definition)
77
52
  }
78
53
 
79
54
  async action(options, command) {
80
- if (options.debug) {
81
- console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
82
- }
83
-
84
- const publicationInput = path.join(projectRoot, paths.output, 'pdf.html')
85
- const coversInput = path.join(projectRoot, paths.output, 'pdf-covers.html')
86
-
87
- const quireConfig = await loadConfig(path.join(projectRoot,'content','_data','config.yaml'))
88
-
89
- if (quireConfig === undefined) {
90
- console.error(`[quire pdf]: ERROR Unable to find a configuration file at ${path.join(projectRoot,'content','_data','config.yaml')}\nIs the command being run in a quire project?`)
91
- process.exit(1)
55
+ this.debug('called with options %O', options)
56
+ /**
57
+ * Configure reporter for this command
58
+ * reporter lifecycle (start/succeed/fail) is handled by the façade,
59
+ * not by the command.
60
+ */
61
+ reporter.configure({ quiet: options.quiet, verbose: options.verbose })
62
+
63
+ // Resolve engine: CLI --engine > deprecated --lib > config pdfEngine > default
64
+ if (!options.engine) {
65
+ if (options.lib) {
66
+ // Support deprecated --lib option
67
+ options.engine = options.lib
68
+ } else {
69
+ // Use config setting or fallback to default
70
+ options.engine = this.config.get('pdfEngine') || 'pagedjs'
71
+ }
92
72
  }
93
73
 
94
- if (!fs.existsSync(publicationInput)) {
95
- console.error(`[quire pdf]: ERROR Unable to find PDF input at ${publicationInput}\nPlease first run the 'quire build' command.`)
96
- process.exit(1)
74
+ // Run build first if --build flag is set and output is missing
75
+ if (options.build && !hasSiteOutput()) {
76
+ this.debug('running build before pdf generation')
77
+ reporter.start('Building site...', { showElapsed: true })
78
+ await eleventy.build({ debug: options.debug })
79
+ reporter.succeed('Build complete')
97
80
  }
98
81
 
99
- const output = quireConfig.pdf !== undefined
100
- ? path.join(paths.output, quireConfig.pdf.outputDir, `${quireConfig.pdf.filename}.pdf`)
101
- : path.join(projectRoot, `${options.lib}.pdf`)
102
-
103
- const pdfLib = await libPdf(options.lib, { ...options, pdfConfig: quireConfig.pdf })
104
- await pdfLib(publicationInput, coversInput, output)
82
+ // Generate PDF - façade handles validation, progress, and errors
83
+ const pdfOptions = { ...options, lib: options.engine }
84
+ const output = await generatePdf(pdfOptions)
105
85
 
106
- try {
107
- if (fs.existsSync(output) && options.open) open(output)
108
- } catch (error) {
109
- console.error(`[quire pdf]: ERROR`,error)
110
- process.exit(1)
86
+ if (options.open) {
87
+ open(output)
111
88
  }
112
89
  }
113
90
 
114
- /**
115
- * @todo test if build has already be run and output can be reused
116
- */
117
- preAction(command) {
118
- // testcwd(command)
91
+ preAction(thisCommand, actionCommand) {
92
+ testcwd(thisCommand)
119
93
  }
120
94
  }