@thegetty/quire-cli 1.0.0-rc.41 → 1.0.0-rc.43
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/bin/cli.js +14 -0
- package/package.json +1 -1
- package/src/lib/11ty/api.js +21 -5
- package/src/lib/commander/index.js +1 -0
- package/src/lib/commander/options.js +18 -0
- package/src/lib/commander/options.test.js +52 -1
- package/src/lib/conf/defaults.js +7 -0
- package/src/lib/conf/schema.js +4 -0
- package/src/lib/reporter/index.js +96 -1
- package/src/lib/reporter/index.test.js +269 -0
- package/src/main.js +24 -4
package/bin/cli.js
CHANGED
|
@@ -19,6 +19,20 @@ process.removeAllListeners('warning')
|
|
|
19
19
|
*/
|
|
20
20
|
process.env.QUIRE_LOG_LEVEL = config.get('logLevel') || 'info'
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Set REDUCED_MOTION env var from config before importing CLI modules
|
|
24
|
+
*
|
|
25
|
+
* The reporter reads REDUCED_MOTION to decide whether to use animated
|
|
26
|
+
* spinners or static text output. Setting it here ensures the reporter
|
|
27
|
+
* picks up the config value at module load time.
|
|
28
|
+
* If REDUCED_MOTION is already set in the shell environment, we do not override it.
|
|
29
|
+
*
|
|
30
|
+
* @see lib/reporter/index.js
|
|
31
|
+
*/
|
|
32
|
+
if (process.env.REDUCED_MOTION === undefined && config.get('reducedMotion') === true) {
|
|
33
|
+
process.env.REDUCED_MOTION = '1'
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/**
|
|
23
37
|
* Set NO_COLOR env var from config before importing CLI modules
|
|
24
38
|
*
|
package/package.json
CHANGED
package/src/lib/11ty/api.js
CHANGED
|
@@ -174,12 +174,15 @@ class Quire11ty {
|
|
|
174
174
|
|
|
175
175
|
configureEleventyEnv({ mode: 'production', debug: options.debug })
|
|
176
176
|
|
|
177
|
-
reporter.start('Building site...', { showElapsed: true })
|
|
178
|
-
|
|
179
177
|
const eleventy = await createEleventyInstance(options)
|
|
180
178
|
|
|
181
179
|
eleventy.setDryRun(options.dryRun)
|
|
182
180
|
|
|
181
|
+
// Print a static info line before Eleventy's build output begins.
|
|
182
|
+
// A spinner is not used here because write() writes directly to stdout
|
|
183
|
+
// and would overwrite the spinner line.
|
|
184
|
+
reporter.info('Building site...')
|
|
185
|
+
|
|
183
186
|
try {
|
|
184
187
|
await eleventy.write()
|
|
185
188
|
reporter.succeed('Build complete')
|
|
@@ -204,17 +207,29 @@ class Quire11ty {
|
|
|
204
207
|
|
|
205
208
|
configureEleventyEnv({ mode: 'development', debug: options.debug })
|
|
206
209
|
|
|
207
|
-
reporter.start('Starting development server...')
|
|
208
|
-
|
|
209
210
|
const eleventy =
|
|
210
211
|
await createEleventyInstance({ ...options, runMode: 'serve' })
|
|
211
212
|
|
|
212
213
|
// Store reference for lifecycle management (graceful shutdown)
|
|
213
214
|
this.activeInstance = eleventy
|
|
214
215
|
|
|
215
|
-
// Initialize Eleventy
|
|
216
|
+
// Initialize Eleventy (required before watch/serve)
|
|
216
217
|
await eleventy.init()
|
|
217
218
|
|
|
219
|
+
// Print a static info line before Eleventy's build output begins.
|
|
220
|
+
// A spinner is not used here because watch() writes directly to stdout
|
|
221
|
+
// and would overwrite the spinner line.
|
|
222
|
+
reporter.info('Building site...')
|
|
223
|
+
|
|
224
|
+
// Build the site and start file watchers.
|
|
225
|
+
// watch() performs the initial build via write(), then sets up chokidar
|
|
226
|
+
// file watchers for incremental rebuilds on file changes.
|
|
227
|
+
// This matches the Eleventy CLI sequence: init() → watch() → serve()
|
|
228
|
+
// @see https://github.com/11ty/eleventy/blob/main/cmd.cjs
|
|
229
|
+
await eleventy.watch()
|
|
230
|
+
|
|
231
|
+
reporter.start('Starting development server...')
|
|
232
|
+
|
|
218
233
|
// Register a ready callback to resolve the spinner when the server is listening
|
|
219
234
|
// @see https://www.11ty.dev/docs/dev-server/#options
|
|
220
235
|
eleventy.eleventyServe.config.serverOptions = {
|
|
@@ -228,6 +243,7 @@ class Quire11ty {
|
|
|
228
243
|
},
|
|
229
244
|
}
|
|
230
245
|
|
|
246
|
+
// Start the HTTP dev server (serves the built _site/ directory)
|
|
231
247
|
await eleventy.serve(options.port)
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -124,6 +124,24 @@ export const debugOption = [
|
|
|
124
124
|
{ conflicts: ['quiet'] }
|
|
125
125
|
]
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Reduced motion option - disable spinner animation and line overwriting
|
|
129
|
+
*
|
|
130
|
+
* When enabled, the reporter outputs static text on new lines instead of
|
|
131
|
+
* animated spinners that overwrite the current line. This is useful for:
|
|
132
|
+
* - Screen reader users (animated overwriting disrupts reading flow)
|
|
133
|
+
* - Users who prefer reduced motion
|
|
134
|
+
* - Environments where terminal animation is problematic
|
|
135
|
+
*
|
|
136
|
+
* Respects config default: `quire settings set reducedMotion true`
|
|
137
|
+
* Also respects REDUCED_MOTION environment variable.
|
|
138
|
+
*
|
|
139
|
+
* @type {Array}
|
|
140
|
+
*/
|
|
141
|
+
export const reducedMotionOption = [
|
|
142
|
+
'--reduced-motion', 'disable spinner animation and line overwriting',
|
|
143
|
+
]
|
|
144
|
+
|
|
127
145
|
/**
|
|
128
146
|
* Color options - enable/disable colored output
|
|
129
147
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from 'ava'
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import { arrayToOption } from './index.js'
|
|
4
|
-
import { colorOption, noColorOption, quietOption, verboseOption, debugOption } from './options.js'
|
|
4
|
+
import { colorOption, noColorOption, quietOption, verboseOption, debugOption, reducedMotionOption } from './options.js'
|
|
5
5
|
|
|
6
6
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
// Integration tests for option conflicts
|
|
@@ -21,6 +21,7 @@ function createTestProgram() {
|
|
|
21
21
|
.addOption(arrayToOption(quietOption))
|
|
22
22
|
.addOption(arrayToOption(verboseOption))
|
|
23
23
|
.addOption(arrayToOption(debugOption))
|
|
24
|
+
.addOption(arrayToOption(reducedMotionOption))
|
|
24
25
|
.action(() => {})
|
|
25
26
|
.exitOverride() // Throw instead of process.exit
|
|
26
27
|
.configureOutput({
|
|
@@ -110,6 +111,56 @@ test('options order does not affect conflict detection', (t) => {
|
|
|
110
111
|
t.throws(() => program2.parse(['node', 'test', '--verbose', '--quiet']))
|
|
111
112
|
})
|
|
112
113
|
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Reduced motion option tests
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
test('--reduced-motion alone is valid', (t) => {
|
|
119
|
+
const program = createTestProgram()
|
|
120
|
+
|
|
121
|
+
t.notThrows(() => {
|
|
122
|
+
program.parse(['node', 'test', '--reduced-motion'])
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('--reduced-motion sets opts.reducedMotion to true', (t) => {
|
|
127
|
+
const program = createTestProgram()
|
|
128
|
+
program.parse(['node', 'test', '--reduced-motion'])
|
|
129
|
+
|
|
130
|
+
t.is(program.opts().reducedMotion, true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('no flag leaves opts.reducedMotion as undefined', (t) => {
|
|
134
|
+
const program = createTestProgram()
|
|
135
|
+
program.parse(['node', 'test'])
|
|
136
|
+
|
|
137
|
+
t.is(program.opts().reducedMotion, undefined)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('--reduced-motion combines with --verbose without conflict', (t) => {
|
|
141
|
+
const program = createTestProgram()
|
|
142
|
+
|
|
143
|
+
t.notThrows(() => {
|
|
144
|
+
program.parse(['node', 'test', '--reduced-motion', '--verbose'])
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('--reduced-motion combines with --quiet without conflict', (t) => {
|
|
149
|
+
const program = createTestProgram()
|
|
150
|
+
|
|
151
|
+
t.notThrows(() => {
|
|
152
|
+
program.parse(['node', 'test', '--reduced-motion', '--quiet'])
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('--reduced-motion combines with --debug without conflict', (t) => {
|
|
157
|
+
const program = createTestProgram()
|
|
158
|
+
|
|
159
|
+
t.notThrows(() => {
|
|
160
|
+
program.parse(['node', 'test', '--reduced-motion', '--debug'])
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
113
164
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
165
|
// Color option tests
|
|
115
166
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/lib/conf/defaults.js
CHANGED
|
@@ -48,6 +48,13 @@ export default {
|
|
|
48
48
|
* Default PDF engine for quire pdf command.
|
|
49
49
|
*/
|
|
50
50
|
pdfEngine: 'pagedjs',
|
|
51
|
+
/**
|
|
52
|
+
* Disable spinner animation and line overwriting.
|
|
53
|
+
* When enabled, progress output uses static text instead of animated spinners,
|
|
54
|
+
* and each stage prints on a new line rather than overwriting the current line.
|
|
55
|
+
* Can be overridden per-command with --reduced-motion.
|
|
56
|
+
*/
|
|
57
|
+
reducedMotion: false,
|
|
51
58
|
/**
|
|
52
59
|
* Project starter template to use when creating new projects.
|
|
53
60
|
*/
|
package/src/lib/conf/schema.js
CHANGED
|
@@ -41,6 +41,10 @@ export default {
|
|
|
41
41
|
type: 'boolean',
|
|
42
42
|
description: 'Color message text by log level (e.g., red for errors). Requires logUseColor'
|
|
43
43
|
},
|
|
44
|
+
reducedMotion: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
description: 'Disable spinner animation and line overwriting (for screen readers and reduced-motion preferences)'
|
|
47
|
+
},
|
|
44
48
|
projectTemplate: {
|
|
45
49
|
type: 'string',
|
|
46
50
|
format: 'uri',
|
|
@@ -14,14 +14,27 @@
|
|
|
14
14
|
* | Default | (none) | Show spinner with basic status |
|
|
15
15
|
* | Quiet | `-q, --quiet` | Suppress all output (for CI/scripts) |
|
|
16
16
|
* | Verbose | `-v, --verbose` | Show detailed progress with paths, timing |
|
|
17
|
+
* | Reduced Motion | `--reduced-motion` | Static text, no animation, no line overwriting |
|
|
17
18
|
*
|
|
18
19
|
* Note: `--debug` is handled separately and enables DEBUG namespace logging,
|
|
19
20
|
* not reporter output. Use verbose mode for detailed user-facing output.
|
|
20
21
|
*
|
|
22
|
+
* ## Reduced Motion
|
|
23
|
+
*
|
|
24
|
+
* When reduced motion is enabled (via `--reduced-motion` flag, `REDUCED_MOTION`
|
|
25
|
+
* environment variable, or `reducedMotion` config setting), the reporter:
|
|
26
|
+
* - Prints static text instead of animated spinners
|
|
27
|
+
* - Outputs each stage on a new line (no line overwriting)
|
|
28
|
+
* - Still shows status symbols (✔ ✖ ⚠ ℹ) for completion states
|
|
29
|
+
*
|
|
30
|
+
* This mode is designed for screen reader users and environments where
|
|
31
|
+
* terminal animation is problematic.
|
|
32
|
+
*
|
|
21
33
|
* ## Config Defaults
|
|
22
34
|
*
|
|
23
35
|
* Users can set default values via `quire settings`:
|
|
24
36
|
* - `quire settings set verbose true` - Always run in verbose mode
|
|
37
|
+
* - `quire settings set reducedMotion true` - Disable spinner animation
|
|
25
38
|
*
|
|
26
39
|
* CLI flags override config settings. Use `--no-verbose` to disable verbose
|
|
27
40
|
* mode even when enabled in config.
|
|
@@ -81,11 +94,27 @@ import config from '#lib/conf/config.js'
|
|
|
81
94
|
|
|
82
95
|
const debug = createDebug('lib:reporter')
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Status symbols for reduced motion output
|
|
99
|
+
*
|
|
100
|
+
* These match the symbols used by ora for visual consistency,
|
|
101
|
+
* ensuring the same information is conveyed with or without animation.
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
const STATUS_SYMBOLS = {
|
|
105
|
+
start: '–',
|
|
106
|
+
succeed: '✔',
|
|
107
|
+
fail: '✖',
|
|
108
|
+
warn: '⚠',
|
|
109
|
+
info: 'ℹ',
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
/**
|
|
85
113
|
* Reporter class for CLI progress feedback
|
|
86
114
|
*
|
|
87
115
|
* Wraps ora spinner with additional features:
|
|
88
116
|
* - Respects --quiet and --json flags
|
|
117
|
+
* - Supports reduced motion (static text, no line overwriting)
|
|
89
118
|
* - Supports elapsed time display
|
|
90
119
|
* - Provides consistent API across commands
|
|
91
120
|
*/
|
|
@@ -111,6 +140,22 @@ class Reporter {
|
|
|
111
140
|
/** @type {string} */
|
|
112
141
|
#baseText = ''
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Check if reduced motion mode is active
|
|
145
|
+
*
|
|
146
|
+
* Reduced motion disables spinner animation and line overwriting.
|
|
147
|
+
* Reads from the REDUCED_MOTION environment variable, which is set by:
|
|
148
|
+
* - `--reduced-motion` CLI flag (via preAction hook)
|
|
149
|
+
* - `REDUCED_MOTION=1` shell environment variable
|
|
150
|
+
* - `reducedMotion: true` config setting (via bin/cli.js)
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
#isReducedMotion() {
|
|
156
|
+
return Boolean(process.env.REDUCED_MOTION)
|
|
157
|
+
}
|
|
158
|
+
|
|
114
159
|
/**
|
|
115
160
|
* Configure reporter for current command context
|
|
116
161
|
*
|
|
@@ -134,7 +179,8 @@ class Reporter {
|
|
|
134
179
|
this.#json = options.json || false
|
|
135
180
|
// CLI flag takes precedence, then config setting, then default false
|
|
136
181
|
this.#verbose = options.verbose ?? config.get('verbose') ?? false
|
|
137
|
-
debug('configured: quiet=%s, json=%s, verbose=%s
|
|
182
|
+
debug('configured: quiet=%s, json=%s, verbose=%s, reducedMotion=%s',
|
|
183
|
+
this.#quiet, this.#json, this.#verbose, this.#isReducedMotion())
|
|
138
184
|
return this
|
|
139
185
|
}
|
|
140
186
|
|
|
@@ -175,6 +221,13 @@ class Reporter {
|
|
|
175
221
|
return this
|
|
176
222
|
}
|
|
177
223
|
|
|
224
|
+
// Reduced motion: print static text on a new line, no spinner
|
|
225
|
+
if (this.#isReducedMotion()) {
|
|
226
|
+
debug('spinner start (reduced motion): %s', text)
|
|
227
|
+
console.log(`${STATUS_SYMBOLS.start} ${text}`)
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
178
231
|
debug('spinner start: %s', text)
|
|
179
232
|
this.#spinner = ora({
|
|
180
233
|
text,
|
|
@@ -235,6 +288,13 @@ class Reporter {
|
|
|
235
288
|
return this
|
|
236
289
|
}
|
|
237
290
|
|
|
291
|
+
// Reduced motion: print new line instead of overwriting
|
|
292
|
+
if (this.#isReducedMotion()) {
|
|
293
|
+
debug('spinner update (reduced motion): %s', text)
|
|
294
|
+
console.log(`${STATUS_SYMBOLS.start} ${text}`)
|
|
295
|
+
return this
|
|
296
|
+
}
|
|
297
|
+
|
|
238
298
|
if (this.#spinner) {
|
|
239
299
|
debug('spinner update: %s', text)
|
|
240
300
|
this.#spinner.text = text
|
|
@@ -273,6 +333,13 @@ class Reporter {
|
|
|
273
333
|
return this
|
|
274
334
|
}
|
|
275
335
|
|
|
336
|
+
// Reduced motion: print static success line
|
|
337
|
+
if (this.#isReducedMotion()) {
|
|
338
|
+
debug('spinner succeed (reduced motion): %s', message)
|
|
339
|
+
console.log(`${STATUS_SYMBOLS.succeed} ${message}`)
|
|
340
|
+
return this
|
|
341
|
+
}
|
|
342
|
+
|
|
276
343
|
if (this.#spinner) {
|
|
277
344
|
debug('spinner succeed: %s', message)
|
|
278
345
|
this.#spinner.succeed(message)
|
|
@@ -309,6 +376,13 @@ class Reporter {
|
|
|
309
376
|
return this
|
|
310
377
|
}
|
|
311
378
|
|
|
379
|
+
// Reduced motion: print static failure line
|
|
380
|
+
if (this.#isReducedMotion()) {
|
|
381
|
+
debug('spinner fail (reduced motion): %s', message)
|
|
382
|
+
console.log(`${STATUS_SYMBOLS.fail} ${message}`)
|
|
383
|
+
return this
|
|
384
|
+
}
|
|
385
|
+
|
|
312
386
|
if (this.#spinner) {
|
|
313
387
|
debug('spinner fail: %s', message)
|
|
314
388
|
this.#spinner.fail(message)
|
|
@@ -343,6 +417,13 @@ class Reporter {
|
|
|
343
417
|
return this
|
|
344
418
|
}
|
|
345
419
|
|
|
420
|
+
// Reduced motion: print static warning line
|
|
421
|
+
if (this.#isReducedMotion()) {
|
|
422
|
+
debug('spinner warn (reduced motion): %s', message)
|
|
423
|
+
console.log(`${STATUS_SYMBOLS.warn} ${message}`)
|
|
424
|
+
return this
|
|
425
|
+
}
|
|
426
|
+
|
|
346
427
|
if (this.#spinner) {
|
|
347
428
|
debug('spinner warn: %s', message)
|
|
348
429
|
this.#spinner.warn(message)
|
|
@@ -370,6 +451,13 @@ class Reporter {
|
|
|
370
451
|
return this
|
|
371
452
|
}
|
|
372
453
|
|
|
454
|
+
// Reduced motion: print static info line
|
|
455
|
+
if (this.#isReducedMotion()) {
|
|
456
|
+
debug('spinner info (reduced motion): %s', text)
|
|
457
|
+
console.log(`${STATUS_SYMBOLS.info} ${text}`)
|
|
458
|
+
return this
|
|
459
|
+
}
|
|
460
|
+
|
|
373
461
|
if (this.#spinner) {
|
|
374
462
|
debug('spinner info: %s', text)
|
|
375
463
|
this.#spinner.info(text)
|
|
@@ -405,6 +493,13 @@ class Reporter {
|
|
|
405
493
|
return this
|
|
406
494
|
}
|
|
407
495
|
|
|
496
|
+
// Reduced motion: print detail directly (no spinner to stop/restart)
|
|
497
|
+
if (this.#isReducedMotion()) {
|
|
498
|
+
console.log(` ${text}`)
|
|
499
|
+
debug('detail (reduced motion): %s', text)
|
|
500
|
+
return this
|
|
501
|
+
}
|
|
502
|
+
|
|
408
503
|
// Print detail with indentation below spinner
|
|
409
504
|
if (this.#spinner) {
|
|
410
505
|
// Temporarily stop spinner to print detail
|
|
@@ -591,3 +591,272 @@ test('multi-phase workflow with different outcomes', (t) => {
|
|
|
591
591
|
|
|
592
592
|
t.pass()
|
|
593
593
|
})
|
|
594
|
+
|
|
595
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
// Reduced motion tests
|
|
597
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
test.serial('reduced motion: start() does not create ora spinner', async (t) => {
|
|
600
|
+
const { sandbox } = t.context
|
|
601
|
+
const mockOra = sandbox.stub()
|
|
602
|
+
|
|
603
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
604
|
+
ora: { default: mockOra },
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const originalIsTTY = process.stdout.isTTY
|
|
608
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
609
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
613
|
+
process.env.REDUCED_MOTION = '1'
|
|
614
|
+
|
|
615
|
+
const rep = new MockedReporter()
|
|
616
|
+
rep.start('Building...')
|
|
617
|
+
|
|
618
|
+
t.false(mockOra.called, 'ora should not be called in reduced motion mode')
|
|
619
|
+
t.true(consoleLog.calledOnce, 'console.log should be called once')
|
|
620
|
+
t.true(consoleLog.firstCall.args[0].includes('Building...'),
|
|
621
|
+
'output should include the text')
|
|
622
|
+
} finally {
|
|
623
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
624
|
+
if (originalReduceMotion === undefined) {
|
|
625
|
+
delete process.env.REDUCED_MOTION
|
|
626
|
+
} else {
|
|
627
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test.serial('reduced motion: update() prints new line instead of overwriting', async (t) => {
|
|
633
|
+
const { sandbox } = t.context
|
|
634
|
+
const mockOra = sandbox.stub()
|
|
635
|
+
|
|
636
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
637
|
+
ora: { default: mockOra },
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
const originalIsTTY = process.stdout.isTTY
|
|
641
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
642
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
646
|
+
process.env.REDUCED_MOTION = '1'
|
|
647
|
+
|
|
648
|
+
const rep = new MockedReporter()
|
|
649
|
+
rep.start('Step 1...')
|
|
650
|
+
rep.update('Step 2...')
|
|
651
|
+
|
|
652
|
+
t.is(consoleLog.callCount, 2, 'should print two separate lines')
|
|
653
|
+
t.true(consoleLog.firstCall.args[0].includes('Step 1...'))
|
|
654
|
+
t.true(consoleLog.secondCall.args[0].includes('Step 2...'))
|
|
655
|
+
} finally {
|
|
656
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
657
|
+
if (originalReduceMotion === undefined) {
|
|
658
|
+
delete process.env.REDUCED_MOTION
|
|
659
|
+
} else {
|
|
660
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
test.serial('reduced motion: succeed() prints static success line', async (t) => {
|
|
666
|
+
const { sandbox } = t.context
|
|
667
|
+
const mockOra = sandbox.stub()
|
|
668
|
+
|
|
669
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
670
|
+
ora: { default: mockOra },
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
const originalIsTTY = process.stdout.isTTY
|
|
674
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
675
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
679
|
+
process.env.REDUCED_MOTION = '1'
|
|
680
|
+
|
|
681
|
+
const rep = new MockedReporter()
|
|
682
|
+
rep.start('Building...')
|
|
683
|
+
rep.succeed('Build complete')
|
|
684
|
+
|
|
685
|
+
t.is(consoleLog.callCount, 2)
|
|
686
|
+
// Second call is succeed
|
|
687
|
+
t.true(consoleLog.secondCall.args[0].includes('✔'))
|
|
688
|
+
t.true(consoleLog.secondCall.args[0].includes('Build complete'))
|
|
689
|
+
} finally {
|
|
690
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
691
|
+
if (originalReduceMotion === undefined) {
|
|
692
|
+
delete process.env.REDUCED_MOTION
|
|
693
|
+
} else {
|
|
694
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
test.serial('reduced motion: fail() prints static failure line', async (t) => {
|
|
700
|
+
const { sandbox } = t.context
|
|
701
|
+
const mockOra = sandbox.stub()
|
|
702
|
+
|
|
703
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
704
|
+
ora: { default: mockOra },
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
const originalIsTTY = process.stdout.isTTY
|
|
708
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
709
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
713
|
+
process.env.REDUCED_MOTION = '1'
|
|
714
|
+
|
|
715
|
+
const rep = new MockedReporter()
|
|
716
|
+
rep.start('Generating PDF...')
|
|
717
|
+
rep.fail('PDF generation failed')
|
|
718
|
+
|
|
719
|
+
t.is(consoleLog.callCount, 2)
|
|
720
|
+
t.true(consoleLog.secondCall.args[0].includes('✖'))
|
|
721
|
+
t.true(consoleLog.secondCall.args[0].includes('PDF generation failed'))
|
|
722
|
+
} finally {
|
|
723
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
724
|
+
if (originalReduceMotion === undefined) {
|
|
725
|
+
delete process.env.REDUCED_MOTION
|
|
726
|
+
} else {
|
|
727
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test.serial('reduced motion: warn() prints static warning line', async (t) => {
|
|
733
|
+
const { sandbox } = t.context
|
|
734
|
+
const mockOra = sandbox.stub()
|
|
735
|
+
|
|
736
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
737
|
+
ora: { default: mockOra },
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
const originalIsTTY = process.stdout.isTTY
|
|
741
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
742
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
746
|
+
process.env.REDUCED_MOTION = '1'
|
|
747
|
+
|
|
748
|
+
const rep = new MockedReporter()
|
|
749
|
+
rep.start('Checking...')
|
|
750
|
+
rep.warn('Dependencies outdated')
|
|
751
|
+
|
|
752
|
+
t.is(consoleLog.callCount, 2)
|
|
753
|
+
t.true(consoleLog.secondCall.args[0].includes('⚠'))
|
|
754
|
+
t.true(consoleLog.secondCall.args[0].includes('Dependencies outdated'))
|
|
755
|
+
} finally {
|
|
756
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
757
|
+
if (originalReduceMotion === undefined) {
|
|
758
|
+
delete process.env.REDUCED_MOTION
|
|
759
|
+
} else {
|
|
760
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
test.serial('reduced motion: info() prints static info line', async (t) => {
|
|
766
|
+
const { sandbox } = t.context
|
|
767
|
+
const mockOra = sandbox.stub()
|
|
768
|
+
|
|
769
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
770
|
+
ora: { default: mockOra },
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const originalIsTTY = process.stdout.isTTY
|
|
774
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
775
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
779
|
+
process.env.REDUCED_MOTION = '1'
|
|
780
|
+
|
|
781
|
+
const rep = new MockedReporter()
|
|
782
|
+
rep.info('Using cached dependencies')
|
|
783
|
+
|
|
784
|
+
t.true(consoleLog.calledOnce)
|
|
785
|
+
t.true(consoleLog.firstCall.args[0].includes('ℹ'))
|
|
786
|
+
t.true(consoleLog.firstCall.args[0].includes('Using cached dependencies'))
|
|
787
|
+
} finally {
|
|
788
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
789
|
+
if (originalReduceMotion === undefined) {
|
|
790
|
+
delete process.env.REDUCED_MOTION
|
|
791
|
+
} else {
|
|
792
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test.serial('reduced motion: multi-phase workflow outputs each phase on new line', async (t) => {
|
|
798
|
+
const { sandbox } = t.context
|
|
799
|
+
const mockOra = sandbox.stub()
|
|
800
|
+
|
|
801
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
802
|
+
ora: { default: mockOra },
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
const originalIsTTY = process.stdout.isTTY
|
|
806
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
807
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
811
|
+
process.env.REDUCED_MOTION = '1'
|
|
812
|
+
|
|
813
|
+
const rep = new MockedReporter()
|
|
814
|
+
rep.start('Cloning starter...')
|
|
815
|
+
rep.update('Installing dependencies...')
|
|
816
|
+
rep.succeed('Project created')
|
|
817
|
+
|
|
818
|
+
t.is(consoleLog.callCount, 3, 'each stage should print on a new line')
|
|
819
|
+
t.true(consoleLog.getCall(0).args[0].includes('Cloning starter...'))
|
|
820
|
+
t.true(consoleLog.getCall(1).args[0].includes('Installing dependencies...'))
|
|
821
|
+
t.true(consoleLog.getCall(2).args[0].includes('✔'))
|
|
822
|
+
t.true(consoleLog.getCall(2).args[0].includes('Project created'))
|
|
823
|
+
t.false(mockOra.called, 'ora should never be called')
|
|
824
|
+
} finally {
|
|
825
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
826
|
+
if (originalReduceMotion === undefined) {
|
|
827
|
+
delete process.env.REDUCED_MOTION
|
|
828
|
+
} else {
|
|
829
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
test.serial('reduced motion: quiet mode takes precedence over reduced motion', async (t) => {
|
|
835
|
+
const { sandbox } = t.context
|
|
836
|
+
const mockOra = sandbox.stub()
|
|
837
|
+
|
|
838
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
839
|
+
ora: { default: mockOra },
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
843
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
process.env.REDUCED_MOTION = '1'
|
|
847
|
+
|
|
848
|
+
const rep = new MockedReporter()
|
|
849
|
+
rep.configure({ quiet: true })
|
|
850
|
+
rep.start('Building...')
|
|
851
|
+
rep.succeed('Build complete')
|
|
852
|
+
|
|
853
|
+
t.false(consoleLog.called, 'quiet mode should suppress all output')
|
|
854
|
+
t.false(mockOra.called, 'ora should not be called')
|
|
855
|
+
} finally {
|
|
856
|
+
if (originalReduceMotion === undefined) {
|
|
857
|
+
delete process.env.REDUCED_MOTION
|
|
858
|
+
} else {
|
|
859
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
})
|
package/src/main.js
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
noColorOption,
|
|
7
7
|
quietOption,
|
|
8
8
|
verboseOption,
|
|
9
|
-
debugOption
|
|
9
|
+
debugOption,
|
|
10
|
+
reducedMotionOption,
|
|
10
11
|
} from '#lib/commander/index.js'
|
|
11
12
|
import commands from '#src/commands/index.js'
|
|
12
13
|
import config from '#lib/conf/config.js'
|
|
@@ -30,12 +31,20 @@ Common Workflows:
|
|
|
30
31
|
Run 'quire help workflows' for detailed workflow documentation.
|
|
31
32
|
|
|
32
33
|
Output Modes:
|
|
33
|
-
-q, --quiet
|
|
34
|
-
-v, --verbose
|
|
35
|
-
--debug
|
|
34
|
+
-q, --quiet Suppress progress output (for CI/scripts)
|
|
35
|
+
-v, --verbose Show detailed progress (paths, timing, steps)
|
|
36
|
+
--debug Enable debug output for developers/troubleshooting
|
|
37
|
+
--reduced-motion Disable spinner animation and line overwriting
|
|
36
38
|
|
|
37
39
|
Set defaults: quire settings set verbose true
|
|
38
40
|
|
|
41
|
+
Accessibility:
|
|
42
|
+
--reduced-motion disables animated spinners and line overwriting.
|
|
43
|
+
Each stage prints on a new line as static text, making output
|
|
44
|
+
compatible with screen readers and reduced-motion preferences.
|
|
45
|
+
|
|
46
|
+
Set default: quire settings set reducedMotion true
|
|
47
|
+
|
|
39
48
|
Color Output:
|
|
40
49
|
--no-color Disable colored output
|
|
41
50
|
--color Force colored output (overrides NO_COLOR env var)
|
|
@@ -49,6 +58,7 @@ Paging:
|
|
|
49
58
|
PAGER=cat Traditional Unix alternative (passes output through)
|
|
50
59
|
|
|
51
60
|
Environment Variables:
|
|
61
|
+
REDUCED_MOTION Disable spinner animation and line overwriting
|
|
52
62
|
NO_COLOR Disable colored output (https://no-color.org/)
|
|
53
63
|
NO_PAGER=1 Disable paging for long output
|
|
54
64
|
PAGER=<program> Set pager program (default: less). Use PAGER=cat to disable
|
|
@@ -60,6 +70,8 @@ Examples:
|
|
|
60
70
|
$ quire build Build the publication
|
|
61
71
|
$ quire build --verbose Build with detailed progress
|
|
62
72
|
$ quire build --debug Build with debug output
|
|
73
|
+
$ quire build --reduced-motion Build without animated spinners
|
|
74
|
+
$ REDUCED_MOTION=1 quire build Build without animated spinners
|
|
63
75
|
$ quire build --no-color Build without colored output
|
|
64
76
|
$ NO_COLOR=1 quire build Build without colored output
|
|
65
77
|
$ DEBUG=quire:* quire pdf Generate PDF with debug output
|
|
@@ -83,6 +95,7 @@ program
|
|
|
83
95
|
.addOption(arrayToOption(quietOption))
|
|
84
96
|
.addOption(arrayToOption(verboseOption))
|
|
85
97
|
.addOption(arrayToOption(debugOption))
|
|
98
|
+
.addOption(arrayToOption(reducedMotionOption))
|
|
86
99
|
.option('--no-pager', 'disable paging for long output')
|
|
87
100
|
.addHelpText('after', mainHelpText)
|
|
88
101
|
.configureHelp({
|
|
@@ -102,6 +115,7 @@ program
|
|
|
102
115
|
* - --quiet: Suppress progress spinners (for CI/scripts)
|
|
103
116
|
* - --verbose: Show detailed progress (paths, timing, steps)
|
|
104
117
|
* - --debug: Enable DEBUG namespace + tool debug modes (for developers)
|
|
118
|
+
* - --reduced-motion: Disable animated spinners, use static text on new lines
|
|
105
119
|
* - --no-color: Disable colored output (sets NO_COLOR env var)
|
|
106
120
|
* - --color: Force colored output (overrides NO_COLOR env var)
|
|
107
121
|
*
|
|
@@ -127,6 +141,12 @@ program.hook('preAction', (thisCommand) => {
|
|
|
127
141
|
enableDebug('quire:*')
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
// --reduced-motion sets REDUCED_MOTION env var for reporter to read
|
|
145
|
+
// CLI flag takes precedence over env var and config setting
|
|
146
|
+
if (opts.reducedMotion) {
|
|
147
|
+
process.env.REDUCED_MOTION = '1'
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
// --no-pager sets pager to false; propagate via env var for pager utility
|
|
131
151
|
if (opts.pager === false) {
|
|
132
152
|
process.env.NO_PAGER = '1'
|