@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
package/src/commands/README.md
CHANGED
|
@@ -1,201 +1,292 @@
|
|
|
1
|
-
|
|
1
|
+
# Quire CLI Commands
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory contains the Quire CLI command implementations and their corresponding tests.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Command Class Pattern
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
quire build
|
|
9
|
-
```
|
|
7
|
+
All Quire CLI commands extend the base `Command` class and follow a consistent pattern:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
### Command Structure
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
```javascript
|
|
12
|
+
import Command from '#src/Command.js'
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export default class MyCommand extends Command {
|
|
15
|
+
static definition = {
|
|
16
|
+
name: 'my-command',
|
|
17
|
+
description: 'Description of what the command does',
|
|
18
|
+
summary: 'short summary for help text',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
args: [
|
|
21
|
+
['<required-arg>', 'description of required argument'],
|
|
22
|
+
['[optional-arg]', 'description of optional argument', 'default-value']
|
|
23
|
+
],
|
|
24
|
+
options: [
|
|
25
|
+
['-f', '--flag', 'description of flag'],
|
|
26
|
+
['-o', '--option <value>', 'description of option with value', 'default']
|
|
27
|
+
]
|
|
28
|
+
}
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
constructor() {
|
|
31
|
+
super(MyCommand.definition)
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
async action(args, options, command) {
|
|
35
|
+
// Use this.debug for developer debugging (controlled by DEBUG env var)
|
|
36
|
+
this.debug('called with options %O', options)
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
38
|
+
// Use this.logger for user-facing output
|
|
39
|
+
this.logger.info('Starting operation...')
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
// Command implementation
|
|
42
|
+
}
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
preAction(command) {
|
|
45
|
+
// Optional: runs before action()
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
postAction(command) {
|
|
49
|
+
// Optional: runs after action()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
```
|
|
34
53
|
|
|
35
|
-
|
|
54
|
+
### Inherited Properties
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
The base `Command` class provides these properties to all commands:
|
|
38
57
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
| Property | Type | Description |
|
|
59
|
+
|----------|------|-------------|
|
|
60
|
+
| `this.logger` | Logger | User-facing output with prefix `commands:{name}` |
|
|
61
|
+
| `this.debug` | Function | Developer debugging via `DEBUG` env var |
|
|
62
|
+
| `this.config` | Conf | CLI configuration store |
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
#### Using Logger vs Debug
|
|
44
65
|
|
|
45
|
-
|
|
66
|
+
```javascript
|
|
67
|
+
// Developer debugging - only shown when DEBUG=quire:commands:* is set
|
|
68
|
+
this.debug('processing file: %s', filename)
|
|
69
|
+
this.debug('options: %O', options)
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
// User-facing output - always shown (unless silenced by log level)
|
|
72
|
+
this.logger.info('Building PDF...')
|
|
73
|
+
this.logger.warn('Deprecated option used')
|
|
74
|
+
this.logger.error('Build failed: %s', error.message)
|
|
51
75
|
```
|
|
52
76
|
|
|
53
|
-
###
|
|
77
|
+
### File Organization
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
For each command, you'll find up to three types of files:
|
|
56
80
|
|
|
57
|
-
```
|
|
58
|
-
|
|
81
|
+
```
|
|
82
|
+
src/commands/
|
|
83
|
+
├── my-command.js # Command implementation
|
|
84
|
+
├── my-command.spec.js # Unit tests (structure, options, definitions)
|
|
85
|
+
└── my-command.test.js # Integration tests (functionality with mocked deps)
|
|
59
86
|
```
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
## Testing Strategy
|
|
62
89
|
|
|
63
|
-
|
|
90
|
+
The Quire CLI uses a comprehensive three-tier testing strategy to ensure reliable command functionality:
|
|
64
91
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
| Test Type | Pattern | Purpose | Location | Speed |
|
|
93
|
+
|-----------|---------|---------|----------|-------|
|
|
94
|
+
| **Unit** | `*.spec.js` | Command structure, options, definitions | `src/commands/` | Very Fast (seconds) |
|
|
95
|
+
| **Integration** | `*.test.js` | Command functionality with mocked dependencies | `src/commands/` | Fast (seconds) |
|
|
96
|
+
| **E2E** | `*.e2e.js` | Complete workflows with real dependencies | `test/e2e/` | Slow (minutes) |
|
|
68
97
|
|
|
69
|
-
### `
|
|
98
|
+
### Unit Tests (`*.spec.js`)
|
|
70
99
|
|
|
71
|
-
|
|
100
|
+
Unit tests verify the command structure and configuration without executing any logic.
|
|
72
101
|
|
|
73
|
-
|
|
102
|
+
**What they test:**
|
|
103
|
+
- Command is properly defined: name, description, and version
|
|
104
|
+
- Command is properly instantiated
|
|
105
|
+
- Arguments and options are properly defined
|
|
106
|
+
- Method existence (action, preAction)
|
|
107
|
+
- No actual functionality testing
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
**Example:**
|
|
110
|
+
```javascript
|
|
111
|
+
test('build command should have a dry-run option', (t) => {
|
|
112
|
+
const { command } = t.context
|
|
113
|
+
const dryRunOption = command.options.find((opt) => opt.flags.includes('--dry-run'))
|
|
114
|
+
t.truthy(dryRunOption)
|
|
115
|
+
})
|
|
77
116
|
```
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
### Integration Tests (`*.test.js`)
|
|
80
119
|
|
|
81
|
-
|
|
82
|
-
quire new <path>
|
|
83
|
-
```
|
|
120
|
+
Integration tests verify command functionality with all external dependencies mocked.
|
|
84
121
|
|
|
85
|
-
|
|
122
|
+
**What they test:**
|
|
123
|
+
- Command execution flow
|
|
124
|
+
- Interaction with mocked dependencies
|
|
125
|
+
- Option handling
|
|
126
|
+
- Error scenarios
|
|
86
127
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
Test command functionality with mocked dependencies:
|
|
129
|
+
- Uses `esmock` for ES module mocking
|
|
130
|
+
- Uses `memfs` for in-memory file system
|
|
131
|
+
- Uses `sinon` for function stubs
|
|
90
132
|
|
|
91
|
-
|
|
133
|
+
**Example:**
|
|
134
|
+
```javascript
|
|
135
|
+
test('build command should call eleventy CLI with default options', async (t) => {
|
|
136
|
+
const { sandbox, fs } = t.context
|
|
92
137
|
|
|
93
|
-
|
|
138
|
+
const mockEleventyCli = {
|
|
139
|
+
build: sandbox.stub().resolves({ exitCode: 0 })
|
|
140
|
+
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
const BuildCommand = await esmock('./build.js', {
|
|
143
|
+
'#lib/11ty/index.js': {
|
|
144
|
+
cli: mockEleventyCli,
|
|
145
|
+
paths: { output: '_site' },
|
|
146
|
+
projectRoot: '/project'
|
|
147
|
+
},
|
|
148
|
+
'fs-extra': fs
|
|
149
|
+
})
|
|
100
150
|
|
|
101
|
-
|
|
151
|
+
const command = new BuildCommand()
|
|
152
|
+
await command.action({ '11ty': 'cli' }, command)
|
|
102
153
|
|
|
103
|
-
|
|
104
|
-
|
|
154
|
+
t.true(mockEleventyCli.build.called)
|
|
155
|
+
})
|
|
105
156
|
```
|
|
106
157
|
|
|
107
|
-
|
|
108
|
-
quire new ./blargh --quire latest
|
|
109
|
-
```
|
|
158
|
+
### E2E Tests (`*.e2e.js`)
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
E2E tests verify complete workflows using real dependencies.
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
**What they test:**
|
|
163
|
+
- Complete command workflows
|
|
164
|
+
- Cross-platform compatibility
|
|
165
|
+
- Real file system operations
|
|
166
|
+
- Real external processes
|
|
167
|
+
- Actual PDF/EPUB generation
|
|
114
168
|
|
|
115
|
-
|
|
116
|
-
|
|
169
|
+
**Example:**
|
|
170
|
+
```javascript
|
|
171
|
+
test('build command should create _site directory', async (t) => {
|
|
172
|
+
await exec('quire build')
|
|
173
|
+
t.true(fs.existsSync('./_site'))
|
|
174
|
+
})
|
|
117
175
|
```
|
|
118
176
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Preview the Quire publication epub in the default application.
|
|
177
|
+
## Running Tests
|
|
122
178
|
|
|
123
179
|
```sh
|
|
124
|
-
|
|
125
|
-
|
|
180
|
+
# Run all tests (unit + integration + e2e)
|
|
181
|
+
npm test
|
|
126
182
|
|
|
127
|
-
|
|
183
|
+
# Run only unit tests (fast)
|
|
184
|
+
npm run test:unit
|
|
128
185
|
|
|
129
|
-
|
|
186
|
+
# Run only integration tests (fast, mocked)
|
|
187
|
+
npm run test:integration
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
|
|
189
|
+
# Run only E2E tests (slow, real dependencies)
|
|
190
|
+
npm run test:e2e
|
|
191
|
+
|
|
192
|
+
# Watch mode for development
|
|
193
|
+
npm run test:watch
|
|
194
|
+
|
|
195
|
+
# Generate coverage report
|
|
196
|
+
npm run test:coverage
|
|
133
197
|
```
|
|
134
198
|
|
|
135
|
-
|
|
199
|
+
## Writing Tests
|
|
136
200
|
|
|
137
|
-
|
|
201
|
+
### When to Write Each Type
|
|
138
202
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
✅ **Always write `*.spec.js`** - Unit tests for every command
|
|
204
|
+
✅ **Always write `*.test.js`** - Integration tests for command logic
|
|
205
|
+
⚠️ **Optionally write `*.e2e.js`** - E2E tests for critical workflows
|
|
142
206
|
|
|
143
|
-
###
|
|
207
|
+
### Integration Test Pattern
|
|
144
208
|
|
|
145
|
-
|
|
209
|
+
```javascript
|
|
210
|
+
import test from 'ava'
|
|
211
|
+
import { Volume, createFsFromVolume } from 'memfs'
|
|
212
|
+
import sinon from 'sinon'
|
|
213
|
+
import esmock from 'esmock'
|
|
146
214
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
215
|
+
test.beforeEach((t) => {
|
|
216
|
+
// Create sinon sandbox for mocking
|
|
217
|
+
t.context.sandbox = sinon.createSandbox()
|
|
150
218
|
|
|
151
|
-
|
|
219
|
+
// Create in-memory file system
|
|
220
|
+
t.context.vol = new Volume()
|
|
221
|
+
t.context.fs = createFsFromVolume(t.context.vol)
|
|
152
222
|
|
|
153
|
-
|
|
223
|
+
// Setup mock directory structure
|
|
224
|
+
t.context.vol.fromJSON({
|
|
225
|
+
'/project/content/_data/config.yaml': 'title: Test Project',
|
|
226
|
+
'/project/package.json': JSON.stringify({ name: 'test-project' })
|
|
227
|
+
})
|
|
154
228
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
```
|
|
229
|
+
t.context.projectRoot = '/project'
|
|
230
|
+
})
|
|
158
231
|
|
|
159
|
-
|
|
232
|
+
test.afterEach.always((t) => {
|
|
233
|
+
// Restore all mocks
|
|
234
|
+
t.context.sandbox.restore()
|
|
160
235
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
236
|
+
// Clear in-memory file system
|
|
237
|
+
t.context.vol.reset()
|
|
238
|
+
})
|
|
164
239
|
|
|
165
|
-
|
|
240
|
+
test('command should perform expected operation', async (t) => {
|
|
241
|
+
const { sandbox, fs } = t.context
|
|
166
242
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```
|
|
243
|
+
// Mock dependencies
|
|
244
|
+
const mockDependency = sandbox.stub().resolves()
|
|
170
245
|
|
|
171
|
-
|
|
246
|
+
// Use esmock to load command with mocked dependencies
|
|
247
|
+
const MyCommand = await esmock('./mycommand.js', {
|
|
248
|
+
'#lib/dependency/index.js': mockDependency,
|
|
249
|
+
'fs-extra': fs
|
|
250
|
+
})
|
|
172
251
|
|
|
173
|
-
|
|
174
|
-
|
|
252
|
+
const command = new MyCommand()
|
|
253
|
+
await command.action({}, command)
|
|
254
|
+
|
|
255
|
+
t.true(mockDependency.called)
|
|
256
|
+
})
|
|
175
257
|
```
|
|
176
258
|
|
|
177
|
-
|
|
259
|
+
## Testing Dependencies
|
|
178
260
|
|
|
179
|
-
|
|
261
|
+
All testing dependencies are managed in [package.json](../../package.json):
|
|
180
262
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
263
|
+
- **ava** - Test runner with ES module support
|
|
264
|
+
- **esmock** - ES module mocking for imports
|
|
265
|
+
- **memfs** - In-memory file system for fast, isolated tests
|
|
266
|
+
- **sinon** - Stubbing and mocking for function calls
|
|
184
267
|
|
|
185
|
-
|
|
268
|
+
## Test Quality Standards
|
|
186
269
|
|
|
187
|
-
|
|
270
|
+
- Each command must have integration test scenarios
|
|
271
|
+
- Critical workflows must have end-to-end tests
|
|
272
|
+
- All error paths must be tested
|
|
273
|
+
- Test execution time should be < 5 minutes total (with mocking: < 30 seconds)
|
|
274
|
+
- Code coverage target: 80% for integration paths
|
|
188
275
|
|
|
189
|
-
|
|
190
|
-
quire version prune
|
|
191
|
-
```
|
|
276
|
+
## Creating a New Command
|
|
192
277
|
|
|
193
|
-
|
|
278
|
+
1. **Create the command file** (`src/commands/my-command.js`)
|
|
279
|
+
- Extend the `Command` class
|
|
280
|
+
- Define static `definition` object
|
|
281
|
+
- Implement `action()` method
|
|
194
282
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
2. **Create integration tests** (`src/commands/my-command.test.js`)
|
|
284
|
+
- Test main functionality with mocked dependencies
|
|
285
|
+
- Test error handling
|
|
286
|
+
- Test option passing
|
|
198
287
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
288
|
+
3. **Create unit tests** (optional, `src/commands/my-command.spec.js`)
|
|
289
|
+
- Validate command structure and configuration
|
|
290
|
+
|
|
291
|
+
4. **Create e2e tests** (optional, `test/e2e/my-command.e2e.js`)
|
|
292
|
+
- Test critical workflows with real dependencies
|
package/src/commands/build.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import Command from '#src/Command.js'
|
|
2
|
-
import {
|
|
2
|
+
import { Option } from 'commander'
|
|
3
|
+
import { withOutputModes } from '#lib/commander/index.js'
|
|
4
|
+
import { api, cli } from '#lib/11ty/index.js'
|
|
5
|
+
import paths from '#lib/project/index.js'
|
|
3
6
|
import { clean } from '#helpers/clean.js'
|
|
7
|
+
import reporter from '#lib/reporter/index.js'
|
|
4
8
|
import testcwd from '#helpers/test-cwd.js'
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -12,56 +16,61 @@ import testcwd from '#helpers/test-cwd.js'
|
|
|
12
16
|
* @extends {Command}
|
|
13
17
|
*/
|
|
14
18
|
export default class BuildCommand extends Command {
|
|
15
|
-
static definition = {
|
|
19
|
+
static definition = withOutputModes({
|
|
16
20
|
name: 'build',
|
|
17
21
|
description: 'Generate publication outputs',
|
|
18
|
-
summary: '
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
summary: 'generate HTML site files',
|
|
23
|
+
docsLink: 'quire-commands/#output-files',
|
|
24
|
+
helpText: `
|
|
25
|
+
Examples:
|
|
26
|
+
quire build Build the site
|
|
27
|
+
quire build --verbose Build with detailed progress
|
|
28
|
+
quire build --debug Build with debug output
|
|
29
|
+
|
|
30
|
+
Note: Run before "quire pdf" or "quire epub" commands.
|
|
31
|
+
`,
|
|
32
|
+
version: '1.1.0',
|
|
28
33
|
options: [
|
|
29
34
|
[ '-d', '--dry-run', 'run build without writing files' ],
|
|
30
|
-
[ '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'
|
|
34
|
-
// { choices: ['api', 'cli'], default: 'cli' }
|
|
35
|
-
],
|
|
36
|
-
[ '--debug', 'run build with debug output to console' ],
|
|
35
|
+
[ '--dryrun', 'alias for --dry-run', { hidden: true, implies: { dryRun: true } } ],
|
|
36
|
+
// Use Option object syntax to configure this as a hidden option
|
|
37
|
+
new Option('--11ty <module>', 'use the specified 11ty module')
|
|
38
|
+
.choices(['api', 'cli']).default('api').hideHelp(),
|
|
37
39
|
],
|
|
38
|
-
}
|
|
40
|
+
})
|
|
39
41
|
|
|
40
42
|
constructor() {
|
|
41
43
|
super(BuildCommand.definition)
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
action(options, command) {
|
|
45
|
-
|
|
46
|
-
console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
|
|
47
|
-
}
|
|
46
|
+
async action(options, command) {
|
|
47
|
+
this.debug('called with options %O', options)
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Configure reporter for this command
|
|
50
|
+
reporter.configure({ quiet: options.quiet, verbose: options.verbose })
|
|
51
|
+
|
|
52
|
+
reporter.start('Building site...', { showElapsed: true })
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
if (options['11ty'] === 'api') {
|
|
56
|
+
this.debug('running eleventy using lib/11ty api')
|
|
57
|
+
await api.build(options)
|
|
58
|
+
} else {
|
|
59
|
+
this.debug('running eleventy using lib/11ty cli')
|
|
60
|
+
await cli.build(options)
|
|
61
|
+
}
|
|
62
|
+
reporter.succeed('Build complete')
|
|
63
|
+
} catch (error) {
|
|
64
|
+
reporter.fail('Build failed')
|
|
65
|
+
throw error
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
preAction(
|
|
59
|
-
testcwd(
|
|
69
|
+
preAction(thisCommand, actionCommand) {
|
|
70
|
+
testcwd(thisCommand)
|
|
60
71
|
|
|
61
|
-
const options =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
clean(projectRoot, paths, options)
|
|
72
|
+
const options = thisCommand.opts()
|
|
73
|
+
this.debug('pre-action with options %O', options)
|
|
74
|
+
clean(paths.getProjectRoot(), paths.toObject(), options)
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Command, Option } from 'commander'
|
|
2
|
+
import program from '#src/main.js'
|
|
3
|
+
import test from 'ava'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Command Contract/Interface Tests
|
|
7
|
+
*
|
|
8
|
+
* Verifies the command's public API and Commander.js integration.
|
|
9
|
+
* @see docs/testing-commands.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
test.before((t) => {
|
|
13
|
+
// Get the registered command (from program.commands) once and share across all tests
|
|
14
|
+
t.context.command = program.commands.find((cmd) => cmd.name() === 'build')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('command is registered in CLI program', (t) => {
|
|
18
|
+
const { command } = t.context
|
|
19
|
+
|
|
20
|
+
t.truthy(command, 'command "build" should be registered in program')
|
|
21
|
+
t.true(command instanceof Command, 'registered command should be Commander.js Command instance')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('registered command has correct metadata', (t) => {
|
|
25
|
+
const { command } = t.context
|
|
26
|
+
|
|
27
|
+
t.is(command.name(), 'build')
|
|
28
|
+
t.truthy(command.description())
|
|
29
|
+
t.is(typeof command._actionHandler, 'function', 'command should have action handler')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('registered command has no arguments', (t) => {
|
|
33
|
+
const { command } = t.context
|
|
34
|
+
const registeredArguments = command.registeredArguments
|
|
35
|
+
|
|
36
|
+
t.is(registeredArguments.length, 0, 'build command should have no arguments')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('registered command has correct options', (t) => {
|
|
40
|
+
const { command } = t.context
|
|
41
|
+
|
|
42
|
+
// Get all options
|
|
43
|
+
const dryRunOption = command.options.find((opt) => opt.long === '--dry-run')
|
|
44
|
+
const dryrunAlias = command.options.find((opt) => opt.long === '--dryrun')
|
|
45
|
+
const quietOption = command.options.find((opt) => opt.long === '--quiet')
|
|
46
|
+
const verboseOption = command.options.find((opt) => opt.long === '--verbose')
|
|
47
|
+
const progressOption = command.options.find((opt) => opt.long === '--progress')
|
|
48
|
+
const eleventyOption = command.options.find((opt) => opt.long === '--11ty')
|
|
49
|
+
const debugOption = command.options.find((opt) => opt.long === '--debug')
|
|
50
|
+
|
|
51
|
+
// Verify all options exist
|
|
52
|
+
t.truthy(dryRunOption, '--dry-run option should exist')
|
|
53
|
+
t.truthy(dryrunAlias, '--dryrun alias should exist')
|
|
54
|
+
t.truthy(quietOption, '--quiet option should exist')
|
|
55
|
+
t.truthy(verboseOption, '--verbose option should exist')
|
|
56
|
+
t.truthy(progressOption, '--progress option should exist')
|
|
57
|
+
t.truthy(eleventyOption, '--11ty option should exist')
|
|
58
|
+
t.truthy(debugOption, '--debug option should exist')
|
|
59
|
+
|
|
60
|
+
// Verify they are Option instances
|
|
61
|
+
t.true(dryRunOption instanceof Option, '--dry-run should be Option instance')
|
|
62
|
+
t.true(dryrunAlias instanceof Option, '--dryrun should be Option instance')
|
|
63
|
+
t.true(quietOption instanceof Option, '--quiet should be Option instance')
|
|
64
|
+
t.true(verboseOption instanceof Option, '--verbose should be Option instance')
|
|
65
|
+
t.true(progressOption instanceof Option, '--progress should be Option instance')
|
|
66
|
+
t.true(eleventyOption instanceof Option, '--11ty should be Option instance')
|
|
67
|
+
t.true(debugOption instanceof Option, '--debug should be Option instance')
|
|
68
|
+
|
|
69
|
+
// Verify option properties
|
|
70
|
+
t.is(dryRunOption.long, '--dry-run')
|
|
71
|
+
t.is(dryRunOption.short, '-d')
|
|
72
|
+
t.truthy(dryRunOption.description)
|
|
73
|
+
|
|
74
|
+
// --dryrun is a hidden alias for --dry-run
|
|
75
|
+
t.is(dryrunAlias.long, '--dryrun')
|
|
76
|
+
t.true(dryrunAlias.hidden, '--dryrun should be hidden from help')
|
|
77
|
+
|
|
78
|
+
t.is(quietOption.long, '--quiet')
|
|
79
|
+
t.is(quietOption.short, '-q')
|
|
80
|
+
t.truthy(quietOption.description)
|
|
81
|
+
|
|
82
|
+
t.is(verboseOption.long, '--verbose')
|
|
83
|
+
t.is(verboseOption.short, '-v')
|
|
84
|
+
t.truthy(verboseOption.description)
|
|
85
|
+
|
|
86
|
+
// --progress is a hidden alias for --verbose
|
|
87
|
+
t.is(progressOption.long, '--progress')
|
|
88
|
+
t.truthy(progressOption.description)
|
|
89
|
+
t.true(progressOption.hidden, '--progress should be hidden from help')
|
|
90
|
+
|
|
91
|
+
t.is(eleventyOption.long, '--11ty')
|
|
92
|
+
t.truthy(eleventyOption.description)
|
|
93
|
+
t.true(eleventyOption.required, '--11ty should require a value')
|
|
94
|
+
|
|
95
|
+
t.is(debugOption.long, '--debug')
|
|
96
|
+
t.truthy(debugOption.description)
|
|
97
|
+
t.false(debugOption.required, '--debug should not require a value')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('command options are accessible via public API', (t) => {
|
|
101
|
+
const { command } = t.context
|
|
102
|
+
|
|
103
|
+
// Test that options can be accessed the way Commander.js does
|
|
104
|
+
const optionNames = command.options.map((opt) => opt.long)
|
|
105
|
+
|
|
106
|
+
t.true(optionNames.includes('--dry-run'))
|
|
107
|
+
t.true(optionNames.includes('--dryrun'))
|
|
108
|
+
t.true(optionNames.includes('--quiet'))
|
|
109
|
+
t.true(optionNames.includes('--verbose'))
|
|
110
|
+
t.true(optionNames.includes('--progress'))
|
|
111
|
+
t.true(optionNames.includes('--11ty'))
|
|
112
|
+
t.true(optionNames.includes('--debug'))
|
|
113
|
+
})
|