@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/bin/cli.js +19 -1
- package/package.json +21 -8
- package/patches/README.md +19 -0
- package/patches/install-npm-version+1.0.9.patch +12119 -0
- package/schemas/objects.schema.json +12 -38
- package/schemas/publication.schema.json +0 -22
- package/schemas/references.schema.json +0 -1
- package/src/Command.js +26 -6
- package/src/Command.spec.js +99 -0
- package/src/commands/README.md +213 -122
- package/src/commands/build.js +46 -37
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +402 -0
- package/src/commands/clean.js +25 -19
- package/src/commands/clean.spec.js +108 -0
- package/src/commands/clean.test.js +260 -0
- package/src/commands/config.js +251 -0
- package/src/commands/config.spec.js +107 -0
- package/src/commands/config.test.js +715 -0
- package/src/commands/create.js +42 -14
- package/src/commands/create.spec.js +112 -0
- package/src/commands/create.test.js +415 -0
- package/src/commands/epub.js +59 -30
- package/src/commands/epub.spec.js +114 -0
- package/src/commands/epub.test.js +503 -0
- package/src/commands/index.js +9 -2
- package/src/commands/info.js +18 -24
- package/src/commands/info.spec.js +64 -0
- package/src/commands/info.test.js +415 -0
- package/src/commands/pdf.js +58 -84
- package/src/commands/pdf.spec.js +114 -0
- package/src/commands/pdf.test.js +464 -0
- package/src/commands/preview.js +26 -31
- package/src/commands/preview.spec.js +97 -0
- package/src/commands/preview.test.js +250 -0
- package/src/commands/use.js +56 -0
- package/src/commands/use.spec.js +61 -0
- package/src/commands/use.test.js +280 -0
- package/src/commands/validate.js +31 -19
- package/src/commands/validate.spec.js +82 -0
- package/src/commands/validate.test.js +234 -0
- package/src/commands/workflows.js +70 -0
- package/src/errors/build/build-failed-error.js +19 -0
- package/src/errors/build/config-field-missing-error.js +20 -0
- package/src/errors/build/config-file-not-found-error.js +20 -0
- package/src/errors/build/index.js +11 -0
- package/src/errors/index.js +48 -0
- package/src/errors/install/dependency-install-error.js +19 -0
- package/src/errors/install/directory-not-empty-error.js +25 -0
- package/src/errors/install/index.js +12 -0
- package/src/errors/install/invalid-path-error.js +31 -0
- package/src/errors/install/invalid-starter-error.js +27 -0
- package/src/errors/install/version-not-found-error.js +35 -0
- package/src/errors/output/epub-generation-error.js +19 -0
- package/src/errors/output/index.js +14 -0
- package/src/errors/output/invalid-epub-library-error.js +20 -0
- package/src/errors/output/invalid-pdf-library-error.js +20 -0
- package/src/errors/output/missing-build-output-error.js +21 -0
- package/src/errors/output/pdf-generation-error.js +24 -0
- package/src/errors/output/tool-not-found-error.js +37 -0
- package/src/errors/project/index.js +10 -0
- package/src/errors/project/not-in-project-error.js +20 -0
- package/src/errors/project/project-create-error.js +21 -0
- package/src/errors/quire-error.js +27 -0
- package/src/errors/validation/validation-error.js +20 -11
- package/src/helpers/clean.js +1 -1
- package/src/helpers/docs-url.js +32 -0
- package/src/helpers/test-cwd.js +5 -6
- package/src/helpers/test-cwd.test.js +192 -0
- package/src/helpers/which.js +10 -4
- package/src/lib/11ty/README.md +135 -19
- package/src/lib/11ty/api.js +176 -93
- package/src/lib/11ty/cli.js +77 -35
- package/src/lib/11ty/index.js +64 -5
- package/src/lib/11ty/index.test.js +655 -0
- package/src/lib/README.md +275 -0
- package/src/lib/commander/index.js +100 -0
- package/src/lib/commander/index.test.js +86 -0
- package/src/lib/commander/options.js +195 -0
- package/src/lib/commander/options.test.js +109 -0
- package/src/lib/conf/README.md +84 -73
- package/src/lib/conf/config.js +5 -3
- package/src/lib/conf/config.test.js +281 -0
- package/src/lib/conf/defaults.js +44 -0
- package/src/lib/conf/format.js +60 -0
- package/src/lib/conf/format.test.js +106 -0
- package/src/lib/conf/helpers.js +91 -0
- package/src/lib/conf/helpers.test.js +136 -0
- package/src/lib/conf/index.js +22 -0
- package/src/lib/conf/schema.js +53 -8
- package/src/lib/epub/README.md +133 -2
- package/src/lib/epub/engines.js +46 -0
- package/src/lib/epub/epub.js +36 -11
- package/src/lib/epub/index.js +111 -21
- package/src/lib/epub/index.test.js +518 -0
- package/src/lib/epub/pandoc.js +33 -4
- package/src/lib/epub/pandoc.test.js +122 -0
- package/src/lib/epub/schema.js +21 -0
- package/src/lib/error/README.md +170 -0
- package/src/lib/error/handler.js +107 -0
- package/src/lib/git/README.md +151 -2
- package/src/lib/git/index.js +217 -13
- package/src/lib/git/index.spec.js +80 -0
- package/src/lib/git/index.test.js +453 -0
- package/src/lib/installer/index.js +309 -0
- package/src/lib/installer/index.spec.js +83 -0
- package/src/lib/installer/index.test.js +545 -0
- package/src/lib/logger/README.md +424 -0
- package/src/lib/logger/debug.js +90 -0
- package/src/lib/logger/debug.spec.js +130 -0
- package/src/lib/logger/index.js +228 -0
- package/src/lib/logger/index.spec.js +131 -0
- package/src/lib/logger/index.test.js +477 -0
- package/src/lib/npm/README.md +127 -0
- package/src/lib/npm/index.js +198 -0
- package/src/lib/npm/index.spec.js +60 -0
- package/src/lib/npm/index.test.js +355 -0
- package/src/lib/pdf/README.md +131 -0
- package/src/lib/pdf/engines.js +46 -0
- package/src/lib/pdf/index.js +124 -21
- package/src/lib/pdf/index.test.js +708 -0
- package/src/lib/pdf/paged.js +100 -52
- package/src/lib/pdf/paged.test.js +366 -0
- package/src/lib/pdf/prince.js +115 -37
- package/src/lib/pdf/prince.test.js +202 -0
- package/src/lib/pdf/schema.js +21 -0
- package/src/lib/pdf/split.js +61 -33
- package/src/lib/pdf/split.test.js +445 -0
- package/src/lib/process/manager.js +110 -0
- package/src/lib/process/manager.test.js +55 -0
- package/src/lib/project/build.js +143 -0
- package/src/lib/project/build.test.js +253 -0
- package/src/lib/project/config.js +48 -0
- package/src/lib/project/config.test.js +134 -0
- package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
- package/src/lib/project/detect.test.js +157 -0
- package/src/lib/project/index.js +40 -0
- package/src/lib/project/paths.js +224 -0
- package/src/lib/project/version.js +110 -0
- package/src/lib/project/version.test.js +350 -0
- package/src/lib/reporter/README.md +211 -2
- package/src/lib/reporter/index.js +512 -0
- package/src/lib/reporter/index.test.js +593 -0
- package/src/main.js +134 -47
- package/src/main.spec.js +61 -0
- package/src/main.test.js +347 -0
- package/src/validators/utils.js +2 -1
- package/src/commands/conf.js +0 -43
- package/src/commands/version.js +0 -43
- package/src/lib/11ty/paths.js +0 -103
- package/src/lib/i18n/README.md +0 -3
- package/src/lib/i18n/config.js +0 -53
- package/src/lib/i18n/index.js +0 -43
- package/src/lib/i18n/localeService.js +0 -58
- package/src/lib/quire/README.md +0 -19
- package/src/lib/quire/index.js +0 -350
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
3
|
+
import esmock from 'esmock'
|
|
4
|
+
|
|
5
|
+
test.beforeEach((t) => {
|
|
6
|
+
// Create in-memory file system
|
|
7
|
+
t.context.vol = new Volume()
|
|
8
|
+
t.context.fs = createFsFromVolume(t.context.vol)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test.afterEach.always((t) => {
|
|
12
|
+
// Clear in-memory file system
|
|
13
|
+
t.context.vol.reset()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// getVersion tests
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
test('getVersion should read version from project version file', async (t) => {
|
|
21
|
+
const { fs, vol } = t.context
|
|
22
|
+
|
|
23
|
+
vol.fromJSON({
|
|
24
|
+
'/project/.quire': '1.0.0'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const { getVersion } = await esmock('./version.js', {
|
|
28
|
+
'fs-extra': {
|
|
29
|
+
existsSync: (p) => fs.existsSync(p),
|
|
30
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
31
|
+
},
|
|
32
|
+
'./paths.js': {
|
|
33
|
+
default: { getProjectRoot: () => '/project' }
|
|
34
|
+
},
|
|
35
|
+
'#lib/conf/config.js': {
|
|
36
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const result = getVersion('/project')
|
|
41
|
+
|
|
42
|
+
t.is(result, '1.0.0', 'should return version string')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('getVersion should use default project root when not provided', async (t) => {
|
|
46
|
+
const { fs, vol } = t.context
|
|
47
|
+
|
|
48
|
+
vol.fromJSON({
|
|
49
|
+
'/default/project/.quire': '2.0.0'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const { getVersion } = await esmock('./version.js', {
|
|
53
|
+
'fs-extra': {
|
|
54
|
+
existsSync: (p) => fs.existsSync(p),
|
|
55
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
56
|
+
},
|
|
57
|
+
'./paths.js': {
|
|
58
|
+
default: { getProjectRoot: () => '/default/project' }
|
|
59
|
+
},
|
|
60
|
+
'#lib/conf/config.js': {
|
|
61
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const result = getVersion()
|
|
66
|
+
|
|
67
|
+
t.is(result, '2.0.0', 'should use default project root')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
// setVersion tests
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
test('setVersion should create version file with quire11ty field', async (t) => {
|
|
75
|
+
const { fs, vol } = t.context
|
|
76
|
+
|
|
77
|
+
vol.fromJSON({
|
|
78
|
+
'/project/.gitkeep': ''
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const { setVersion } = await esmock('./version.js', {
|
|
82
|
+
'fs-extra': {
|
|
83
|
+
existsSync: (p) => fs.existsSync(p),
|
|
84
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts),
|
|
85
|
+
writeFileSync: (p, data) => fs.writeFileSync(p, data)
|
|
86
|
+
},
|
|
87
|
+
'./paths.js': {
|
|
88
|
+
default: { getProjectRoot: () => '/project' }
|
|
89
|
+
},
|
|
90
|
+
'#lib/conf/config.js': {
|
|
91
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
setVersion('1.5.0', '/project')
|
|
96
|
+
|
|
97
|
+
const written = fs.readFileSync('/project/.quire', 'utf8')
|
|
98
|
+
const parsed = JSON.parse(written)
|
|
99
|
+
|
|
100
|
+
t.is(parsed.quire11ty, '1.5.0', 'should write version to quire11ty field')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('setVersion should preserve existing version file metadata', async (t) => {
|
|
104
|
+
const { fs, vol } = t.context
|
|
105
|
+
|
|
106
|
+
const existingData = JSON.stringify({
|
|
107
|
+
cli: '1.0.0',
|
|
108
|
+
starter: 'https://example.com/starter@1.0.0',
|
|
109
|
+
quire11ty: '1.0.0'
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
vol.fromJSON({
|
|
113
|
+
'/project/.quire': existingData
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const { setVersion } = await esmock('./version.js', {
|
|
117
|
+
'fs-extra': {
|
|
118
|
+
existsSync: (p) => fs.existsSync(p),
|
|
119
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts),
|
|
120
|
+
writeFileSync: (p, data) => fs.writeFileSync(p, data)
|
|
121
|
+
},
|
|
122
|
+
'./paths.js': {
|
|
123
|
+
default: { getProjectRoot: () => '/project' }
|
|
124
|
+
},
|
|
125
|
+
'#lib/conf/config.js': {
|
|
126
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
setVersion('2.0.0', '/project')
|
|
131
|
+
|
|
132
|
+
const written = fs.readFileSync('/project/.quire', 'utf8')
|
|
133
|
+
const parsed = JSON.parse(written)
|
|
134
|
+
|
|
135
|
+
t.is(parsed.quire11ty, '2.0.0', 'should update quire11ty version')
|
|
136
|
+
t.is(parsed.cli, '1.0.0', 'should preserve cli version')
|
|
137
|
+
t.is(parsed.starter, 'https://example.com/starter@1.0.0', 'should preserve starter info')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('setVersion should handle legacy plain text version file', async (t) => {
|
|
141
|
+
const { fs, vol } = t.context
|
|
142
|
+
|
|
143
|
+
// Legacy format: just a version string, not JSON
|
|
144
|
+
vol.fromJSON({
|
|
145
|
+
'/project/.quire': '1.0.0-legacy'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const { setVersion } = await esmock('./version.js', {
|
|
149
|
+
'fs-extra': {
|
|
150
|
+
existsSync: (p) => fs.existsSync(p),
|
|
151
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts),
|
|
152
|
+
writeFileSync: (p, data) => fs.writeFileSync(p, data)
|
|
153
|
+
},
|
|
154
|
+
'./paths.js': {
|
|
155
|
+
default: { getProjectRoot: () => '/project' }
|
|
156
|
+
},
|
|
157
|
+
'#lib/conf/config.js': {
|
|
158
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
setVersion('2.0.0', '/project')
|
|
163
|
+
|
|
164
|
+
const written = fs.readFileSync('/project/.quire', 'utf8')
|
|
165
|
+
const parsed = JSON.parse(written)
|
|
166
|
+
|
|
167
|
+
t.is(parsed.quire11ty, '2.0.0', 'should migrate to new JSON format')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('setVersion should use default project root when not provided', async (t) => {
|
|
171
|
+
const { fs, vol } = t.context
|
|
172
|
+
|
|
173
|
+
vol.fromJSON({
|
|
174
|
+
'/default/project/.gitkeep': ''
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const { setVersion } = await esmock('./version.js', {
|
|
178
|
+
'fs-extra': {
|
|
179
|
+
existsSync: (p) => fs.existsSync(p),
|
|
180
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts),
|
|
181
|
+
writeFileSync: (p, data) => fs.writeFileSync(p, data)
|
|
182
|
+
},
|
|
183
|
+
'./paths.js': {
|
|
184
|
+
default: { getProjectRoot: () => '/default/project' }
|
|
185
|
+
},
|
|
186
|
+
'#lib/conf/config.js': {
|
|
187
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
setVersion('3.0.0')
|
|
192
|
+
|
|
193
|
+
const written = fs.readFileSync('/default/project/.quire', 'utf8')
|
|
194
|
+
const parsed = JSON.parse(written)
|
|
195
|
+
|
|
196
|
+
t.is(parsed.quire11ty, '3.0.0', 'should use default project root')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// getVersionsFromStarter tests
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
test('getVersionsFromStarter should read versions from package.json', async (t) => {
|
|
204
|
+
const { fs, vol } = t.context
|
|
205
|
+
|
|
206
|
+
const packageJson = JSON.stringify({
|
|
207
|
+
name: 'quire-starter-default',
|
|
208
|
+
version: '1.2.3',
|
|
209
|
+
peerDependencies: {
|
|
210
|
+
'@thegetty/quire-11ty': '^1.0.0'
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
vol.fromJSON({
|
|
215
|
+
'/project/package.json': packageJson
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const { getVersionsFromStarter } = await esmock('./version.js', {
|
|
219
|
+
'fs-extra': {
|
|
220
|
+
existsSync: (p) => fs.existsSync(p),
|
|
221
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
222
|
+
},
|
|
223
|
+
'./paths.js': {
|
|
224
|
+
default: { getProjectRoot: () => '/project' }
|
|
225
|
+
},
|
|
226
|
+
'#lib/conf/config.js': {
|
|
227
|
+
default: { get: () => '.quire' }
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const result = await getVersionsFromStarter('/project')
|
|
232
|
+
|
|
233
|
+
t.is(result.quire11tyVersion, '^1.0.0', 'should return quire-11ty version range')
|
|
234
|
+
t.is(result.starterVersion, '1.2.3', 'should return starter version')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// writeVersionFile tests
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
test('writeVersionFile should write JSON to version file', async (t) => {
|
|
242
|
+
const { fs, vol } = t.context
|
|
243
|
+
|
|
244
|
+
vol.fromJSON({
|
|
245
|
+
'/project/.gitkeep': ''
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const { writeVersionFile } = await esmock('./version.js', {
|
|
249
|
+
'fs-extra': {
|
|
250
|
+
existsSync: (p) => fs.existsSync(p),
|
|
251
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts),
|
|
252
|
+
writeFileSync: (p, data) => fs.writeFileSync(p, data)
|
|
253
|
+
},
|
|
254
|
+
'./paths.js': {
|
|
255
|
+
default: { getProjectRoot: () => '/project' }
|
|
256
|
+
},
|
|
257
|
+
'#lib/conf/config.js': {
|
|
258
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const versionInfo = { cli: '1.0.0', starter: 'test@1.0.0' }
|
|
263
|
+
writeVersionFile('/project', versionInfo)
|
|
264
|
+
|
|
265
|
+
const written = fs.readFileSync('/project/.quire', 'utf8')
|
|
266
|
+
const parsed = JSON.parse(written)
|
|
267
|
+
|
|
268
|
+
t.deepEqual(parsed, versionInfo, 'should write version info as JSON')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
272
|
+
// readVersionFile tests
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
test('readVersionFile should return null when file does not exist', async (t) => {
|
|
276
|
+
const { fs, vol } = t.context
|
|
277
|
+
|
|
278
|
+
vol.fromJSON({
|
|
279
|
+
'/project/.gitkeep': ''
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const { readVersionFile } = await esmock('./version.js', {
|
|
283
|
+
'fs-extra': {
|
|
284
|
+
existsSync: (p) => fs.existsSync(p),
|
|
285
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
286
|
+
},
|
|
287
|
+
'./paths.js': {
|
|
288
|
+
default: { getProjectRoot: () => '/project' }
|
|
289
|
+
},
|
|
290
|
+
'#lib/conf/config.js': {
|
|
291
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const result = readVersionFile('/project')
|
|
296
|
+
|
|
297
|
+
t.is(result, null, 'should return null when file does not exist')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('readVersionFile should parse JSON version file', async (t) => {
|
|
301
|
+
const { fs, vol } = t.context
|
|
302
|
+
|
|
303
|
+
const versionInfo = { cli: '1.0.0', quire11ty: '1.5.0' }
|
|
304
|
+
vol.fromJSON({
|
|
305
|
+
'/project/.quire': JSON.stringify(versionInfo)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const { readVersionFile } = await esmock('./version.js', {
|
|
309
|
+
'fs-extra': {
|
|
310
|
+
existsSync: (p) => fs.existsSync(p),
|
|
311
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
312
|
+
},
|
|
313
|
+
'./paths.js': {
|
|
314
|
+
default: { getProjectRoot: () => '/project' }
|
|
315
|
+
},
|
|
316
|
+
'#lib/conf/config.js': {
|
|
317
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const result = readVersionFile('/project')
|
|
322
|
+
|
|
323
|
+
t.deepEqual(result, versionInfo, 'should parse and return version info')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('readVersionFile should handle legacy plain text format', async (t) => {
|
|
327
|
+
const { fs, vol } = t.context
|
|
328
|
+
|
|
329
|
+
// Legacy format: plain version string
|
|
330
|
+
vol.fromJSON({
|
|
331
|
+
'/project/.quire': '1.0.0-legacy'
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const { readVersionFile } = await esmock('./version.js', {
|
|
335
|
+
'fs-extra': {
|
|
336
|
+
existsSync: (p) => fs.existsSync(p),
|
|
337
|
+
readFileSync: (p, opts) => fs.readFileSync(p, opts)
|
|
338
|
+
},
|
|
339
|
+
'./paths.js': {
|
|
340
|
+
default: { getProjectRoot: () => '/project' }
|
|
341
|
+
},
|
|
342
|
+
'#lib/conf/config.js': {
|
|
343
|
+
default: { get: (key) => key === 'versionFile' ? '.quire' : null }
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const result = readVersionFile('/project')
|
|
348
|
+
|
|
349
|
+
t.deepEqual(result, { version: '1.0.0-legacy' }, 'should return legacy format as object')
|
|
350
|
+
})
|
|
@@ -1,3 +1,212 @@
|
|
|
1
|
-
|
|
1
|
+
# Reporter Module
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Reporter module provides CLI progress feedback through spinners and status indicators.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This module wraps [ora](https://github.com/sindresorhus/ora) to provide:
|
|
8
|
+
|
|
9
|
+
- Consistent progress indicators across all CLI commands
|
|
10
|
+
- Automatic suppression in quiet mode, JSON mode, and non-TTY environments
|
|
11
|
+
- Elapsed time display for long-running operations
|
|
12
|
+
- Multi-phase operation support
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Basic Usage
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import reporter from '#lib/reporter/index.js'
|
|
20
|
+
|
|
21
|
+
// Start a spinner
|
|
22
|
+
reporter.start('Building site...')
|
|
23
|
+
|
|
24
|
+
// Do work...
|
|
25
|
+
await build()
|
|
26
|
+
|
|
27
|
+
// Mark as complete
|
|
28
|
+
reporter.succeed('Build complete')
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Multi-Phase Operations
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
reporter.start('Cloning starter project...')
|
|
35
|
+
await git.clone(starter, '.')
|
|
36
|
+
|
|
37
|
+
reporter.update('Initializing repository...')
|
|
38
|
+
await git.init()
|
|
39
|
+
|
|
40
|
+
reporter.update('Installing dependencies...')
|
|
41
|
+
await npm.install()
|
|
42
|
+
|
|
43
|
+
reporter.succeed('Project created')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Error Handling
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
reporter.start('Generating PDF...')
|
|
50
|
+
try {
|
|
51
|
+
await generatePdf()
|
|
52
|
+
reporter.succeed('PDF generated')
|
|
53
|
+
} catch (error) {
|
|
54
|
+
reporter.fail('PDF generation failed')
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Elapsed Time Display
|
|
60
|
+
|
|
61
|
+
For operations with unpredictable duration:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
reporter.start('Building site...', { showElapsed: true })
|
|
65
|
+
// Shows: ⠋ Building site... (12s)
|
|
66
|
+
await build()
|
|
67
|
+
reporter.succeed('Build complete')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Configuration
|
|
71
|
+
|
|
72
|
+
Configure the reporter to respect command options:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
async action(options) {
|
|
76
|
+
// Call at start of action() to respect --quiet, --json flags
|
|
77
|
+
reporter.configure(options)
|
|
78
|
+
|
|
79
|
+
reporter.start('Working...')
|
|
80
|
+
// ...
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Option | Effect |
|
|
85
|
+
|--------|--------|
|
|
86
|
+
| `quiet: true` | Suppress all spinner output |
|
|
87
|
+
| `json: true` | Suppress spinner (JSON-only output) |
|
|
88
|
+
| `verbose: true` | (Reserved for future use) |
|
|
89
|
+
|
|
90
|
+
## API Reference
|
|
91
|
+
|
|
92
|
+
### `reporter.configure(options)`
|
|
93
|
+
|
|
94
|
+
Configure reporter for current command context.
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
reporter.configure({ quiet: true, json: false })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `reporter.start(text, options)`
|
|
101
|
+
|
|
102
|
+
Start a new spinner.
|
|
103
|
+
|
|
104
|
+
| Parameter | Type | Description |
|
|
105
|
+
|-----------|------|-------------|
|
|
106
|
+
| `text` | `string` | Spinner text |
|
|
107
|
+
| `options.showElapsed` | `boolean` | Show elapsed time (default: `false`) |
|
|
108
|
+
|
|
109
|
+
### `reporter.update(text)`
|
|
110
|
+
|
|
111
|
+
Update spinner text while running.
|
|
112
|
+
|
|
113
|
+
### `reporter.succeed(text)`
|
|
114
|
+
|
|
115
|
+
Mark operation as successful. Uses current text if `text` not provided.
|
|
116
|
+
|
|
117
|
+
### `reporter.fail(text)`
|
|
118
|
+
|
|
119
|
+
Mark operation as failed.
|
|
120
|
+
|
|
121
|
+
### `reporter.warn(text)`
|
|
122
|
+
|
|
123
|
+
Mark operation with a warning.
|
|
124
|
+
|
|
125
|
+
### `reporter.info(text)`
|
|
126
|
+
|
|
127
|
+
Display an info message.
|
|
128
|
+
|
|
129
|
+
### `reporter.stop()`
|
|
130
|
+
|
|
131
|
+
Stop spinner without status indicator.
|
|
132
|
+
|
|
133
|
+
### `reporter.isSpinning()`
|
|
134
|
+
|
|
135
|
+
Returns `true` if a spinner is currently running.
|
|
136
|
+
|
|
137
|
+
### `reporter.getElapsed()`
|
|
138
|
+
|
|
139
|
+
Returns elapsed time in milliseconds since `start()`, or `null` if not started.
|
|
140
|
+
|
|
141
|
+
### `reporter.reset()`
|
|
142
|
+
|
|
143
|
+
Reset all reporter state. Useful in tests.
|
|
144
|
+
|
|
145
|
+
## Testing
|
|
146
|
+
|
|
147
|
+
### Mocking in Tests
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
import esmock from 'esmock'
|
|
151
|
+
import sinon from 'sinon'
|
|
152
|
+
|
|
153
|
+
const mockReporter = {
|
|
154
|
+
start: sinon.stub().returnsThis(),
|
|
155
|
+
update: sinon.stub().returnsThis(),
|
|
156
|
+
succeed: sinon.stub().returnsThis(),
|
|
157
|
+
fail: sinon.stub().returnsThis(),
|
|
158
|
+
configure: sinon.stub().returnsThis(),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const MyCommand = await esmock('./mycommand.js', {
|
|
162
|
+
'#lib/reporter/index.js': { default: mockReporter }
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Verifying Calls
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
test('shows progress during build', async (t) => {
|
|
170
|
+
await command.action(options)
|
|
171
|
+
|
|
172
|
+
t.true(mockReporter.start.calledWith('Building...'))
|
|
173
|
+
t.true(mockReporter.succeed.calledWith('Build complete'))
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Design Decisions
|
|
178
|
+
|
|
179
|
+
### Why ora?
|
|
180
|
+
|
|
181
|
+
Automatic supression of output when the `--quiet` or `--json` flags are set or TTY is detected support the four primary user-groups/use-cases: non-technical, developer, maintainer, and programmatic use.
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
| Factor | ora | Alternatives |
|
|
185
|
+
|--------|-----|--------------|
|
|
186
|
+
| Bundle size | 30.4 kB | cli-progress: 62.2 kB |
|
|
187
|
+
| Maintenance | Active | progress: 7 years dormant |
|
|
188
|
+
| Complexity | Simple | listr2: over-engineered |
|
|
189
|
+
|
|
190
|
+
### Singleton Pattern
|
|
191
|
+
|
|
192
|
+
Follows the established pattern from `lib/logger`, `lib/git`, and `lib/npm`:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// Singleton export
|
|
196
|
+
export default new Reporter()
|
|
197
|
+
|
|
198
|
+
// Class export for testing
|
|
199
|
+
export { Reporter }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Automatic TTY Detection
|
|
203
|
+
|
|
204
|
+
Spinners are automatically suppressed when:
|
|
205
|
+
|
|
206
|
+
1. `--quiet` flag is set
|
|
207
|
+
2. `--json` flag is set
|
|
208
|
+
3. `process.stdout.isTTY` is `false` (CI environments, piped output)
|
|
209
|
+
|
|
210
|
+
## Related
|
|
211
|
+
|
|
212
|
+
- [lib/logger](../logger/) - Logging abstraction (different concern)
|