@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,477 @@
1
+ import esmock from 'esmock'
2
+ import sinon from 'sinon'
3
+ import test from 'ava'
4
+ import createLogger, { logger, LOG_LEVELS, LOG_LEVEL_ENV_VAR } from './index.js'
5
+
6
+ /**
7
+ * Logger Module Integration Tests
8
+ *
9
+ * Tests the actual logging behavior including output formatting and level filtering.
10
+ *
11
+ * Note: loglevel captures console methods at module load time, so we can't stub
12
+ * console.log directly. Instead, we test the logger's behavior through its API
13
+ * and verify level filtering works correctly.
14
+ */
15
+
16
+ test.beforeEach((t) => {
17
+ t.context.sandbox = sinon.createSandbox()
18
+ })
19
+
20
+ test.afterEach.always((t) => {
21
+ t.context.sandbox.restore()
22
+ })
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Log method invocation tests
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ test('logger methods are callable without error', (t) => {
29
+ const log = createLogger('test:callable', 'silent') // Silent to avoid output
30
+
31
+ t.notThrows(() => log.trace('trace message'))
32
+ t.notThrows(() => log.debug('debug message'))
33
+ t.notThrows(() => log.info('info message'))
34
+ t.notThrows(() => log.warn('warn message'))
35
+ t.notThrows(() => log.error('error message'))
36
+ })
37
+
38
+ test('logger methods accept multiple arguments', (t) => {
39
+ const log = createLogger('test:multiarg', 'silent')
40
+
41
+ t.notThrows(() => log.info('message', 'arg1', 'arg2'))
42
+ t.notThrows(() => log.info('message', { key: 'value' }))
43
+ t.notThrows(() => log.info('message', 123, true, null))
44
+ })
45
+
46
+ test('logger methods accept no arguments', (t) => {
47
+ const log = createLogger('test:noarg', 'silent')
48
+
49
+ t.notThrows(() => log.info())
50
+ t.notThrows(() => log.error())
51
+ })
52
+
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Level filtering behavior tests
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+
57
+ test('logger at trace level has getLevel() return 0', (t) => {
58
+ const log = createLogger('test:trace', 'trace')
59
+ t.is(log.getLevel(), LOG_LEVELS.trace)
60
+ })
61
+
62
+ test('logger at debug level has getLevel() return 1', (t) => {
63
+ const log = createLogger('test:debug', 'debug')
64
+ t.is(log.getLevel(), LOG_LEVELS.debug)
65
+ })
66
+
67
+ test('logger at info level has getLevel() return 2', (t) => {
68
+ const log = createLogger('test:info', 'info')
69
+ t.is(log.getLevel(), LOG_LEVELS.info)
70
+ })
71
+
72
+ test('logger at warn level has getLevel() return 3', (t) => {
73
+ const log = createLogger('test:warn', 'warn')
74
+ t.is(log.getLevel(), LOG_LEVELS.warn)
75
+ })
76
+
77
+ test('logger at error level has getLevel() return 4', (t) => {
78
+ const log = createLogger('test:error', 'error')
79
+ t.is(log.getLevel(), LOG_LEVELS.error)
80
+ })
81
+
82
+ test('logger at silent level has getLevel() return 5', (t) => {
83
+ const log = createLogger('test:silent', 'silent')
84
+ t.is(log.getLevel(), LOG_LEVELS.silent)
85
+ })
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+ // Runtime level change tests
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+
91
+ test('setLevel changes getLevel() return value', (t) => {
92
+ const log = createLogger('test:setlevel', 'info')
93
+ t.is(log.getLevel(), LOG_LEVELS.info)
94
+
95
+ log.setLevel('debug')
96
+ t.is(log.getLevel(), LOG_LEVELS.debug)
97
+
98
+ log.setLevel('error')
99
+ t.is(log.getLevel(), LOG_LEVELS.error)
100
+
101
+ log.setLevel('silent')
102
+ t.is(log.getLevel(), LOG_LEVELS.silent)
103
+ })
104
+
105
+ test('setLevel accepts numeric values', (t) => {
106
+ const log = createLogger('test:numeric', 'info')
107
+
108
+ log.setLevel(0)
109
+ t.is(log.getLevel(), 0)
110
+
111
+ log.setLevel(4)
112
+ t.is(log.getLevel(), 4)
113
+ })
114
+
115
+ test('setLevel with invalid string defaults to info', (t) => {
116
+ const log = createLogger('test:invalid', 'debug')
117
+ t.is(log.getLevel(), LOG_LEVELS.debug)
118
+
119
+ log.setLevel('not-a-level')
120
+ t.is(log.getLevel(), LOG_LEVELS.info)
121
+ })
122
+
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+ // Independent instance tests
125
+ // ─────────────────────────────────────────────────────────────────────────────
126
+
127
+ test('multiple loggers maintain independent levels', (t) => {
128
+ const log1 = createLogger('independent:one', 'debug')
129
+ const log2 = createLogger('independent:two', 'error')
130
+
131
+ t.is(log1.getLevel(), LOG_LEVELS.debug)
132
+ t.is(log2.getLevel(), LOG_LEVELS.error)
133
+
134
+ log1.setLevel('warn')
135
+
136
+ t.is(log1.getLevel(), LOG_LEVELS.warn)
137
+ t.is(log2.getLevel(), LOG_LEVELS.error) // Unchanged
138
+ })
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Singleton behavior tests
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+
144
+ test('default singleton is functional', (t) => {
145
+ t.notThrows(() => logger.info('singleton test'))
146
+ t.is(typeof logger.getLevel(), 'number')
147
+ })
148
+
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+ // Console output tests with level labels (logShowLevel: true)
151
+ // ─────────────────────────────────────────────────────────────────────────────
152
+
153
+ test.serial('error level outputs to console.error with level label when configured', async (t) => {
154
+ const { sandbox } = t.context
155
+ const consoleErrorStub = sandbox.stub(console, 'error')
156
+
157
+ // Mock config to show level labels
158
+ const mockConfig = {
159
+ get: sandbox.stub().callsFake((key) => ({
160
+ logPrefix: 'quire',
161
+ logPrefixStyle: 'bracket',
162
+ logShowLevel: true,
163
+ logUseColor: false
164
+ })[key])
165
+ }
166
+
167
+ const { default: createLoggerMocked } = await esmock('./index.js', {
168
+ '#lib/conf/config.js': { default: mockConfig }
169
+ })
170
+
171
+ const log = createLoggerMocked('test:error:output', 'error')
172
+ log.error('Error message')
173
+
174
+ t.true(consoleErrorStub.calledOnce)
175
+ const output = consoleErrorStub.firstCall.args.join(' ')
176
+ t.true(output.includes('[quire]'))
177
+ t.true(output.includes('ERROR'))
178
+ t.true(output.includes('Error message'))
179
+ })
180
+
181
+ test.serial('warn level outputs to console.warn with level label when configured', async (t) => {
182
+ const { sandbox } = t.context
183
+ const consoleWarnStub = sandbox.stub(console, 'warn')
184
+
185
+ // Mock config to show level labels
186
+ const mockConfig = {
187
+ get: sandbox.stub().callsFake((key) => ({
188
+ logPrefix: 'quire',
189
+ logPrefixStyle: 'bracket',
190
+ logShowLevel: true,
191
+ logUseColor: false
192
+ })[key])
193
+ }
194
+
195
+ const { default: createLoggerMocked } = await esmock('./index.js', {
196
+ '#lib/conf/config.js': { default: mockConfig }
197
+ })
198
+
199
+ const log = createLoggerMocked('test:warn:output', 'warn')
200
+ log.warn('Warning message')
201
+
202
+ t.true(consoleWarnStub.calledOnce)
203
+ const output = consoleWarnStub.firstCall.args.join(' ')
204
+ t.true(output.includes('[quire]'))
205
+ t.true(output.includes('WARN'))
206
+ t.true(output.includes('Warning message'))
207
+ })
208
+
209
+ test.serial('trace level outputs to console.trace with level label when configured', async (t) => {
210
+ const { sandbox } = t.context
211
+ const consoleTraceStub = sandbox.stub(console, 'trace')
212
+
213
+ // Mock config to show level labels
214
+ const mockConfig = {
215
+ get: sandbox.stub().callsFake((key) => ({
216
+ logPrefix: 'quire',
217
+ logPrefixStyle: 'bracket',
218
+ logShowLevel: true,
219
+ logUseColor: false
220
+ })[key])
221
+ }
222
+
223
+ const { default: createLoggerMocked } = await esmock('./index.js', {
224
+ '#lib/conf/config.js': { default: mockConfig }
225
+ })
226
+
227
+ const log = createLoggerMocked('test:trace:output', 'trace')
228
+ log.trace('Trace message')
229
+
230
+ t.true(consoleTraceStub.calledOnce)
231
+ const output = consoleTraceStub.firstCall.args.join(' ')
232
+ t.true(output.includes('[quire]'))
233
+ t.true(output.includes('TRACE'))
234
+ t.true(output.includes('Trace message'))
235
+ })
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Prefix configuration tests
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ test('different prefixes create distinct loggers', (t) => {
242
+ const log1 = createLogger('prefix:alpha', 'debug')
243
+ const log2 = createLogger('prefix:beta', 'warn')
244
+
245
+ // Different levels confirm different instances
246
+ t.not(log1.getLevel(), log2.getLevel())
247
+ })
248
+
249
+ test('same prefix returns same underlying loglevel instance', (t) => {
250
+ const log1 = createLogger('same:prefix', 'debug')
251
+ const log2 = createLogger('same:prefix', 'warn')
252
+
253
+ // Setting level on one affects the other (same loglevel instance)
254
+ log1.setLevel('error')
255
+ t.is(log1.getLevel(), LOG_LEVELS.error)
256
+ t.is(log2.getLevel(), LOG_LEVELS.error)
257
+ })
258
+
259
+ // ─────────────────────────────────────────────────────────────────────────────
260
+ // Edge case tests
261
+ // ─────────────────────────────────────────────────────────────────────────────
262
+
263
+ test('empty prefix throws error (loglevel requirement)', (t) => {
264
+ t.throws(() => createLogger('', 'info'), {
265
+ instanceOf: TypeError,
266
+ message: /supply a name/
267
+ })
268
+ })
269
+
270
+ test('special characters in prefix are allowed', (t) => {
271
+ const log = createLogger('lib:pdf/paged.js', 'info')
272
+ t.notThrows(() => log.info('message'))
273
+ })
274
+
275
+ test('undefined level defaults to info', (t) => {
276
+ const log = createLogger('test:undefined')
277
+ t.is(log.getLevel(), LOG_LEVELS.info)
278
+ })
279
+
280
+ // ─────────────────────────────────────────────────────────────────────────────
281
+ // Environment variable tests
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+
284
+ test('LOG_LEVEL_ENV_VAR constant is exported', (t) => {
285
+ t.is(LOG_LEVEL_ENV_VAR, 'QUIRE_LOG_LEVEL')
286
+ })
287
+
288
+ test.serial('logger reads level from QUIRE_LOG_LEVEL env var', async (t) => {
289
+ const originalEnv = process.env[LOG_LEVEL_ENV_VAR]
290
+
291
+ try {
292
+ // Set env var to debug
293
+ process.env[LOG_LEVEL_ENV_VAR] = 'debug'
294
+
295
+ // Re-import to get fresh module that reads env var
296
+ // Note: We need to use a unique prefix so loglevel creates a new instance
297
+ const { default: freshCreateLogger } = await import('./index.js?env-debug')
298
+ const log = freshCreateLogger('env:debug:test')
299
+
300
+ t.is(log.getLevel(), LOG_LEVELS.debug)
301
+ } finally {
302
+ // Restore original env
303
+ if (originalEnv === undefined) {
304
+ delete process.env[LOG_LEVEL_ENV_VAR]
305
+ } else {
306
+ process.env[LOG_LEVEL_ENV_VAR] = originalEnv
307
+ }
308
+ }
309
+ })
310
+
311
+ test.serial('logger reads error level from QUIRE_LOG_LEVEL env var', async (t) => {
312
+ const originalEnv = process.env[LOG_LEVEL_ENV_VAR]
313
+
314
+ try {
315
+ process.env[LOG_LEVEL_ENV_VAR] = 'error'
316
+
317
+ const { default: freshCreateLogger } = await import('./index.js?env-error')
318
+ const log = freshCreateLogger('env:error:test')
319
+
320
+ t.is(log.getLevel(), LOG_LEVELS.error)
321
+ } finally {
322
+ if (originalEnv === undefined) {
323
+ delete process.env[LOG_LEVEL_ENV_VAR]
324
+ } else {
325
+ process.env[LOG_LEVEL_ENV_VAR] = originalEnv
326
+ }
327
+ }
328
+ })
329
+
330
+ test.serial('explicit level parameter overrides env var', async (t) => {
331
+ const originalEnv = process.env[LOG_LEVEL_ENV_VAR]
332
+
333
+ try {
334
+ process.env[LOG_LEVEL_ENV_VAR] = 'error'
335
+
336
+ const { default: freshCreateLogger } = await import('./index.js?env-override')
337
+ // Explicitly pass 'debug' which should override env var 'error'
338
+ const log = freshCreateLogger('env:override:test', 'debug')
339
+
340
+ t.is(log.getLevel(), LOG_LEVELS.debug)
341
+ } finally {
342
+ if (originalEnv === undefined) {
343
+ delete process.env[LOG_LEVEL_ENV_VAR]
344
+ } else {
345
+ process.env[LOG_LEVEL_ENV_VAR] = originalEnv
346
+ }
347
+ }
348
+ })
349
+
350
+ // ─────────────────────────────────────────────────────────────────────────────
351
+ // Printf-style string substitution tests
352
+ // ─────────────────────────────────────────────────────────────────────────────
353
+
354
+ test.serial('logger supports %s string substitution', async (t) => {
355
+ const { sandbox } = t.context
356
+ const consoleInfoStub = sandbox.stub(console, 'info')
357
+
358
+ const mockConfig = {
359
+ get: sandbox.stub().callsFake((key) => ({
360
+ logPrefix: 'quire',
361
+ logPrefixStyle: 'none',
362
+ logShowLevel: false,
363
+ logUseColor: false
364
+ })[key])
365
+ }
366
+
367
+ const { default: createLoggerMocked } = await esmock('./index.js', {
368
+ '#lib/conf/config.js': { default: mockConfig }
369
+ })
370
+
371
+ const log = createLoggerMocked('test:printf:string', 'info')
372
+ log.info('Hello %s', 'world')
373
+
374
+ t.true(consoleInfoStub.calledOnce)
375
+ const output = consoleInfoStub.firstCall.args.join(' ')
376
+ t.true(output.includes('Hello world'))
377
+ t.false(output.includes('%s'))
378
+ })
379
+
380
+ test.serial('logger supports %d number substitution', async (t) => {
381
+ const { sandbox } = t.context
382
+ const consoleInfoStub = sandbox.stub(console, 'info')
383
+
384
+ const mockConfig = {
385
+ get: sandbox.stub().callsFake((key) => ({
386
+ logPrefix: 'quire',
387
+ logPrefixStyle: 'none',
388
+ logShowLevel: false,
389
+ logUseColor: false
390
+ })[key])
391
+ }
392
+
393
+ const { default: createLoggerMocked } = await esmock('./index.js', {
394
+ '#lib/conf/config.js': { default: mockConfig }
395
+ })
396
+
397
+ const log = createLoggerMocked('test:printf:number', 'info')
398
+ log.info('Count: %d', 42)
399
+
400
+ t.true(consoleInfoStub.calledOnce)
401
+ const output = consoleInfoStub.firstCall.args.join(' ')
402
+ t.true(output.includes('Count: 42'))
403
+ t.false(output.includes('%d'))
404
+ })
405
+
406
+ test.serial('logger supports %O object substitution', async (t) => {
407
+ const { sandbox } = t.context
408
+ const consoleInfoStub = sandbox.stub(console, 'info')
409
+
410
+ const mockConfig = {
411
+ get: sandbox.stub().callsFake((key) => ({
412
+ logPrefix: 'quire',
413
+ logPrefixStyle: 'none',
414
+ logShowLevel: false,
415
+ logUseColor: false
416
+ })[key])
417
+ }
418
+
419
+ const { default: createLoggerMocked } = await esmock('./index.js', {
420
+ '#lib/conf/config.js': { default: mockConfig }
421
+ })
422
+
423
+ const log = createLoggerMocked('test:printf:object', 'info')
424
+ log.info('Data: %O', { key: 'value' })
425
+
426
+ t.true(consoleInfoStub.calledOnce)
427
+ const output = consoleInfoStub.firstCall.args.join(' ')
428
+ t.true(output.includes('key'))
429
+ t.true(output.includes('value'))
430
+ t.false(output.includes('%O'))
431
+ })
432
+
433
+ test.serial('logger supports multiple substitutions', async (t) => {
434
+ const { sandbox } = t.context
435
+ const consoleInfoStub = sandbox.stub(console, 'info')
436
+
437
+ const mockConfig = {
438
+ get: sandbox.stub().callsFake((key) => ({
439
+ logPrefix: 'quire',
440
+ logPrefixStyle: 'none',
441
+ logShowLevel: false,
442
+ logUseColor: false
443
+ })[key])
444
+ }
445
+
446
+ const { default: createLoggerMocked } = await esmock('./index.js', {
447
+ '#lib/conf/config.js': { default: mockConfig }
448
+ })
449
+
450
+ const log = createLoggerMocked('test:printf:multi', 'info')
451
+ log.info('%s is %d years old', 'Alice', 30)
452
+
453
+ t.true(consoleInfoStub.calledOnce)
454
+ const output = consoleInfoStub.firstCall.args.join(' ')
455
+ t.true(output.includes('Alice is 30 years old'))
456
+ t.false(output.includes('%s'))
457
+ t.false(output.includes('%d'))
458
+ })
459
+
460
+ test.serial('invalid env var value falls back to info', async (t) => {
461
+ const originalEnv = process.env[LOG_LEVEL_ENV_VAR]
462
+
463
+ try {
464
+ process.env[LOG_LEVEL_ENV_VAR] = 'not-a-valid-level'
465
+
466
+ const { default: freshCreateLogger } = await import('./index.js?env-invalid')
467
+ const log = freshCreateLogger('env:invalid:test')
468
+
469
+ t.is(log.getLevel(), LOG_LEVELS.info)
470
+ } finally {
471
+ if (originalEnv === undefined) {
472
+ delete process.env[LOG_LEVEL_ENV_VAR]
473
+ } else {
474
+ process.env[LOG_LEVEL_ENV_VAR] = originalEnv
475
+ }
476
+ }
477
+ })
@@ -0,0 +1,127 @@
1
+ # NPM Façade
2
+
3
+ A façade module that abstracts npm command-line operations for the Quire CLI.
4
+
5
+ ## Purpose
6
+
7
+ This module provides a unified interface for npm operations with:
8
+
9
+ - Encapsulation of npm implementation details
10
+ - Consistent logging prefixed with `[CLI:lib/npm]`
11
+ - Unified error handling
12
+ - Easy mockability for testing
13
+
14
+ ## Usage
15
+
16
+ ```javascript
17
+ import npm from '#lib/npm/index.js'
18
+
19
+ // Check npm availability
20
+ if (!npm.isAvailable()) {
21
+ console.error('npm is not installed')
22
+ }
23
+
24
+ // Get npm version
25
+ // @see https://docs.npmjs.com/cli/commands/npm-version
26
+ const version = await npm.version()
27
+
28
+ // Initialize package.json
29
+ // @see https://docs.npmjs.com/cli/commands/npm-init
30
+ await npm.init('/path/to/project', { yes: true })
31
+
32
+ // Install dependencies
33
+ // @see https://docs.npmjs.com/cli/commands/npm-install
34
+ await npm.install('/path/to/project', {
35
+ saveDev: true,
36
+ preferOffline: true
37
+ })
38
+
39
+ // Pack a package
40
+ // @see https://docs.npmjs.com/cli/commands/npm-pack
41
+ await npm.pack('@thegetty/quire-11ty@1.0.0', '/path/to/destination', {
42
+ quiet: true
43
+ })
44
+
45
+ // Clean cache
46
+ // @see https://docs.npmjs.com/cli/commands/npm-cache
47
+ await npm.cacheClean()
48
+
49
+ // View package version from registry
50
+ // @see https://docs.npmjs.com/cli/commands/npm-view
51
+ const latestVersion = await npm.view('@thegetty/quire-11ty', 'version')
52
+
53
+ // Get compatible version for a range
54
+ // @see https://docs.npmjs.com/about-semantic-versioning
55
+ const compatible = await npm.getCompatibleVersion('@thegetty/quire-11ty', '^1.0.0')
56
+ ```
57
+
58
+ ## API
59
+
60
+ ### `isAvailable()`
61
+
62
+ Check if npm is available in PATH.
63
+
64
+ ### `version()`
65
+
66
+ Get the installed npm version.
67
+
68
+ ### `init(cwd, options)`
69
+
70
+ Initialize a new package.json in the specified directory.
71
+
72
+ **Options:**
73
+ - `yes` (boolean, default: `true`) - Auto-accept defaults
74
+
75
+ ### `install(cwd, options)`
76
+
77
+ Install dependencies in the specified directory.
78
+
79
+ **Options:**
80
+ - `preferOffline` (boolean, default: `false`) - Prefer cached packages
81
+ - `saveDev` (boolean, default: `false`) - Install as devDependencies
82
+
83
+ ### `pack(packageSpec, destination, options)`
84
+
85
+ Download and pack a package to a tarball.
86
+
87
+ **Options:**
88
+ - `debug` (boolean, default: `false`) - Enable debug output
89
+ - `quiet` (boolean, default: `true`) - Suppress output
90
+
91
+ ### `cacheClean(cwd)`
92
+
93
+ Force clean the npm cache.
94
+
95
+ ### `view(packageName, field)`
96
+
97
+ Query package information from the npm registry using `npm view`.
98
+
99
+ ### `show(packageName, field)`
100
+
101
+ Query package information from the npm registry using `npm show`.
102
+
103
+ ### `fetchFromRegistry(packageName)`
104
+
105
+ Fetch package metadata directly from the npm registry API.
106
+
107
+ ### `getCompatibleVersion(packageName, range)`
108
+
109
+ Find the latest version compatible with a semver range.
110
+
111
+ ## Testing
112
+
113
+ The module exports a singleton instance which can be mocked in tests:
114
+
115
+ ```javascript
116
+ import esmock from 'esmock'
117
+
118
+ const mockNpm = {
119
+ isAvailable: sandbox.stub().returns(true),
120
+ install: sandbox.stub().resolves(),
121
+ version: sandbox.stub().resolves('10.2.4')
122
+ }
123
+
124
+ const MyCommand = await esmock('./mycommand.js', {
125
+ '#lib/npm/index.js': { default: mockNpm }
126
+ })
127
+ ```