@ndp-software/lit-md 0.3.0
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/LICENSE.md +3 -0
- package/README.md +125 -0
- package/dist/cli.js +1273 -0
- package/dist/cli.js.map +7 -0
- package/docs/cli.md +74 -0
- package/docs/how-wait-mode-works.md +381 -0
- package/docs/shell-examples.md +254 -0
- package/package.json +58 -0
- package/src/cli.ts +327 -0
- package/src/describe-format.ts +53 -0
- package/src/docs/README.lit-md.ts +126 -0
- package/src/docs/cli.lit-md.ts +44 -0
- package/src/docs/shell-examples.lit-md.ts +243 -0
- package/src/index.ts +7 -0
- package/src/parser.ts +970 -0
- package/src/renderer.ts +130 -0
- package/src/resolver.ts +65 -0
- package/src/shell.ts +362 -0
- package/src/typecheck.ts +51 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# How --wait Mode Works
|
|
2
|
+
|
|
3
|
+
A detailed walkthrough of the `--wait` mode implementation in lit-md, explaining every piece of the code.
|
|
4
|
+
|
|
5
|
+
## 1. Startup Phase
|
|
6
|
+
|
|
7
|
+
**File: `cli.ts` lines 38-43**
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const wait = extractFlag('--wait')
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When a user runs:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
lit-md --wait --test file.lit-md.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `extractFlag()` function checks if `--wait` is in the argument list. If found, it removes it from `args` and returns `true`. The `wait` variable is then used throughout the program to change behavior.
|
|
20
|
+
|
|
21
|
+
## 2. Entry Point - Main Async Function
|
|
22
|
+
|
|
23
|
+
**File: `cli.ts` lines 302-313**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
;(async () => {
|
|
27
|
+
await executeTasks() // ← Run ONCE on startup
|
|
28
|
+
|
|
29
|
+
if (wait && process.stdin.isTTY) { // ← If --wait AND interactive terminal
|
|
30
|
+
while (true) { // ← INFINITE LOOP
|
|
31
|
+
const trigger = await watchFilesAndWait(inputPaths)
|
|
32
|
+
await executeTasks() // ← Run again on change/spacebar
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is where the main logic happens:
|
|
39
|
+
|
|
40
|
+
1. **First run**: `executeTasks()` is called once immediately
|
|
41
|
+
2. **Check conditions**:
|
|
42
|
+
- `wait` = user passed `--wait` flag
|
|
43
|
+
- `process.stdin.isTTY` = terminal is interactive (not piped/redirected)
|
|
44
|
+
3. **If both true**: Enter infinite loop that:
|
|
45
|
+
- Waits for file changes or spacebar
|
|
46
|
+
- Runs `executeTasks()` again
|
|
47
|
+
- Loops back to wait
|
|
48
|
+
|
|
49
|
+
If either condition is false, the program ends after the first run.
|
|
50
|
+
|
|
51
|
+
## 3. First Run: executeTasks()
|
|
52
|
+
|
|
53
|
+
**File: `cli.ts` lines 249-300**
|
|
54
|
+
|
|
55
|
+
This function runs on startup AND every time through the watch loop. It performs three tasks in order:
|
|
56
|
+
|
|
57
|
+
### 3a) Typecheck (if `--typecheck` flag used)
|
|
58
|
+
|
|
59
|
+
**Lines 250-259**
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
if (runTypecheck) {
|
|
63
|
+
const result = typecheck(inputPaths.map(p => resolve(p)))
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
for (const msg of result.messages) console.error(msg)
|
|
66
|
+
// In wait mode, report error but continue; in normal mode, exit
|
|
67
|
+
if (!wait) process.exit(1)
|
|
68
|
+
// Continue to markdown generation even if typecheck failed
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Key difference in wait mode**: If typecheck fails, the program prints the error but **doesn't exit**. It continues to the next step. In normal mode, it would call `process.exit(1)` and stop.
|
|
74
|
+
|
|
75
|
+
### 3b) Run Tests (if `--test` flag used)
|
|
76
|
+
|
|
77
|
+
**Lines 262-296**
|
|
78
|
+
|
|
79
|
+
This is where the most significant difference between wait and normal mode occurs:
|
|
80
|
+
|
|
81
|
+
#### Normal Mode Output
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const spawnOptions = { stdio: 'inherit', env: process.env }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `stdio: 'inherit'` means test output goes **directly to the console** in real-time
|
|
88
|
+
- User sees every test name, timing, and full output
|
|
89
|
+
- This is useful for initial verification
|
|
90
|
+
|
|
91
|
+
#### Wait Mode Output
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const spawnOptions = wait ? { encoding: 'utf-8' as const } : { stdio: 'inherit' as const, env: process.env }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `encoding: 'utf-8'` captures **all output into memory** in `result.stdout`
|
|
98
|
+
- We can then process and summarize it before displaying
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
if (wait && result.stdout) {
|
|
102
|
+
const output = result.stdout.toString()
|
|
103
|
+
const stats = parseTestSummary(output) // Extract pass/fail counts
|
|
104
|
+
|
|
105
|
+
if (stats.hasFailed) {
|
|
106
|
+
// Extract and show failures section
|
|
107
|
+
const failureStart = output.indexOf('✖ failing tests')
|
|
108
|
+
if (failureStart !== -1) {
|
|
109
|
+
const failureSection = output.substring(failureStart)
|
|
110
|
+
console.error(failureSection) // ← Show ONLY failures
|
|
111
|
+
}
|
|
112
|
+
console.error(`\n❌ Tests failed: ${stats.failed}/${stats.total} failed`)
|
|
113
|
+
} else {
|
|
114
|
+
// All passed: show one-line summary
|
|
115
|
+
console.log(`✅ Tests passed: ${stats.passed} passed`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**In wait mode:**
|
|
121
|
+
- If all tests pass: `✅ Tests passed: 5 passed` (one line)
|
|
122
|
+
- If tests fail: Show failures section + count
|
|
123
|
+
|
|
124
|
+
**Error handling:**
|
|
125
|
+
- Tests failed in wait mode? Continue to markdown generation anyway
|
|
126
|
+
- Tests failed in normal mode? Call `process.exit()` and stop
|
|
127
|
+
|
|
128
|
+
### 3c) Generate Markdown
|
|
129
|
+
|
|
130
|
+
**Line 299**
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
await generateMarkdown()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This always happens, even if typecheck or tests failed. This is crucial for wait mode - we want to regenerate the documentation on every change, even if there are errors.
|
|
137
|
+
|
|
138
|
+
## 4. Watch Loop: watchFilesAndWait()
|
|
139
|
+
|
|
140
|
+
**File: `shell.ts` lines 275-353**
|
|
141
|
+
|
|
142
|
+
This is the heart of the watch mechanism. It waits for either:
|
|
143
|
+
1. A file to change, or
|
|
144
|
+
2. The user to press spacebar
|
|
145
|
+
|
|
146
|
+
And returns a Promise that resolves when one of those happens.
|
|
147
|
+
|
|
148
|
+
### 4a) File Watching - Dependency Collection
|
|
149
|
+
|
|
150
|
+
**Lines 289-300**
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const filesToWatch = collectAllDependencies(inputPaths)
|
|
154
|
+
|
|
155
|
+
const watchers = Array.from(filesToWatch).map(filePath => {
|
|
156
|
+
return watch(filePath, (eventType) => {
|
|
157
|
+
const now = Date.now()
|
|
158
|
+
// Debounce: only consider changes if enough time has passed
|
|
159
|
+
if (now - lastChangeTime >= DEBOUNCE_MS) {
|
|
160
|
+
lastChangeTime = now
|
|
161
|
+
changeDetected = true
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Key features:**
|
|
168
|
+
|
|
169
|
+
- **Collects all dependencies**: Not just the input file, but also all files it imports. For example:
|
|
170
|
+
- Input: `file.lit-md.ts`
|
|
171
|
+
- Imports: `./utils.ts`
|
|
172
|
+
- `utils.ts` imports: `./parser.ts`
|
|
173
|
+
- Result: Watch all three files
|
|
174
|
+
|
|
175
|
+
- **Debouncing**: If a file changes rapidly (like during a save operation that triggers multiple write events), debounce for 300ms to avoid redundant runs
|
|
176
|
+
|
|
177
|
+
- **Sets flag**: When a change is detected, set `changeDetected = true`
|
|
178
|
+
|
|
179
|
+
### 4b) Keyboard Listening - Raw Mode
|
|
180
|
+
|
|
181
|
+
**Lines 316-317**
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
console.error('Press space to regenerate, Ctrl+C to exit...')
|
|
185
|
+
|
|
186
|
+
process.stdin.setRawMode(true) // ← Detect individual keypress
|
|
187
|
+
process.stdin.resume()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `setRawMode(true)` puts the terminal in "raw mode", allowing detection of individual key presses (not line-buffered)
|
|
191
|
+
- This lets us respond immediately to spacebar
|
|
192
|
+
|
|
193
|
+
### 4c) Return a Promise
|
|
194
|
+
|
|
195
|
+
**Lines 319-352**
|
|
196
|
+
|
|
197
|
+
The function returns a Promise that resolves when one of two things happens:
|
|
198
|
+
|
|
199
|
+
#### Handler 1: Keyboard Input
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const onData = (data: Buffer) => {
|
|
203
|
+
const char = data[0]
|
|
204
|
+
|
|
205
|
+
if (char === 0x20) { // Spacebar (ASCII 0x20)
|
|
206
|
+
cleanup()
|
|
207
|
+
resolve('spacebar')
|
|
208
|
+
} else if (char === 0x03) { // Ctrl+C (ASCII 0x03)
|
|
209
|
+
cleanup()
|
|
210
|
+
process.exit(0)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.stdin.on('data', onData)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- **Spacebar (0x20)**: Clean up and return `'spacebar'`
|
|
218
|
+
- **Ctrl+C (0x03)**: Close watchers and exit the process
|
|
219
|
+
|
|
220
|
+
#### Handler 2: File Changes
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const checkForChanges = setInterval(() => {
|
|
224
|
+
if (changeDetected) {
|
|
225
|
+
cleanup()
|
|
226
|
+
console.error('Files changed, regenerating...')
|
|
227
|
+
resolve('filechange')
|
|
228
|
+
}
|
|
229
|
+
}, 50)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- Checks every 50ms if `changeDetected` flag is set
|
|
233
|
+
- If yes, clean up and return `'filechange'`
|
|
234
|
+
|
|
235
|
+
#### Cleanup Function
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const cleanup = () => {
|
|
239
|
+
process.stdin.off('data', onData)
|
|
240
|
+
clearInterval(checkForChanges)
|
|
241
|
+
process.stdin.setRawMode(false) // ← Restore normal terminal
|
|
242
|
+
process.stdin.pause()
|
|
243
|
+
process.removeListener('SIGINT', exitHandler)
|
|
244
|
+
process.removeListener('SIGTERM', exitHandler)
|
|
245
|
+
watchers.forEach(w => w.close()) // ← Close file watchers
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Called by either handler to properly clean up before resolving the Promise.
|
|
250
|
+
|
|
251
|
+
## 5. The Loop Continues
|
|
252
|
+
|
|
253
|
+
**Back in `cli.ts` lines 307-311**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
while (true) {
|
|
257
|
+
const trigger = await watchFilesAndWait(inputPaths) // ← Waits here
|
|
258
|
+
// trigger = 'spacebar' OR 'filechange'
|
|
259
|
+
|
|
260
|
+
await executeTasks() // ← Run again!
|
|
261
|
+
// Loop back to watchFilesAndWait()
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
After `watchFilesAndWait()` returns, `executeTasks()` is called again, which:
|
|
266
|
+
1. Runs typecheck (if enabled)
|
|
267
|
+
2. Runs tests with **condensed output** (in wait mode)
|
|
268
|
+
3. Generates markdown
|
|
269
|
+
|
|
270
|
+
Then the loop goes back to waiting for the next change or spacebar press.
|
|
271
|
+
|
|
272
|
+
## 6. Key Differences: Wait Mode vs Normal Mode
|
|
273
|
+
|
|
274
|
+
| Aspect | Normal Mode | Wait Mode |
|
|
275
|
+
|--------|------------|-----------|
|
|
276
|
+
| **Execution** | Run once and exit | Run, then enter watch loop |
|
|
277
|
+
| **Test Output** | Show full output | Show condensed summary |
|
|
278
|
+
| **On Typecheck Fail** | Exit with error | Print error, continue |
|
|
279
|
+
| **On Tests Fail** | Exit with error | Show summary, continue |
|
|
280
|
+
| **Target Environment** | CI/CD, one-time runs | Development (iterative) |
|
|
281
|
+
|
|
282
|
+
## 7. Error Handling in Wait Mode
|
|
283
|
+
|
|
284
|
+
Unlike normal mode, **errors don't stop the process**:
|
|
285
|
+
|
|
286
|
+
- **Typecheck fails**: Print error → continue to tests → generate markdown
|
|
287
|
+
- **Tests fail**: Print summary + failures → generate markdown
|
|
288
|
+
|
|
289
|
+
This is by design. In development, you want to keep the process running so you can make edits and regenerate without restarting. The errors are reported, but they don't halt the workflow.
|
|
290
|
+
|
|
291
|
+
## 8. Test Output Parsing
|
|
292
|
+
|
|
293
|
+
**`cli.ts` lines 49-70**
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
function parseTestSummary(output: string): { passed: number; failed: number; total: number; hasFailed: boolean } {
|
|
297
|
+
const lines = output.split('\n')
|
|
298
|
+
let stats = { passed: 0, failed: 0, total: 0, hasFailed: false }
|
|
299
|
+
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
if (line.includes('ℹ pass')) {
|
|
302
|
+
const match = line.match(/pass\s+(\d+)/)
|
|
303
|
+
if (match) stats.passed = parseInt(match[1])
|
|
304
|
+
}
|
|
305
|
+
if (line.includes('ℹ fail')) {
|
|
306
|
+
const match = line.match(/fail\s+(\d+)/)
|
|
307
|
+
if (match) stats.failed = parseInt(match[1])
|
|
308
|
+
}
|
|
309
|
+
if (line.includes('ℹ tests')) {
|
|
310
|
+
const match = line.match(/tests\s+(\d+)/)
|
|
311
|
+
if (match) stats.total = parseInt(match[1])
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
stats.hasFailed = stats.failed > 0
|
|
316
|
+
return stats
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
This function looks for summary lines in the test output like:
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
ℹ tests 12
|
|
324
|
+
ℹ pass 12
|
|
325
|
+
ℹ fail 0
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
It extracts the numbers and returns an object with:
|
|
329
|
+
- `passed`: Number of passing tests
|
|
330
|
+
- `failed`: Number of failing tests
|
|
331
|
+
- `total`: Total test count
|
|
332
|
+
- `hasFailed`: Boolean to quickly check if there were failures
|
|
333
|
+
|
|
334
|
+
This is used to decide whether to show a simple summary or the failures section.
|
|
335
|
+
|
|
336
|
+
## 9. Execution Timeline
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
Start: lit-md --wait --test file.lit-md.ts
|
|
340
|
+
│
|
|
341
|
+
├─ Extract --wait flag → wait = true
|
|
342
|
+
│
|
|
343
|
+
├─ First executeTasks() call
|
|
344
|
+
│ ├─ Run tests → Show FULL output (stdio: 'inherit')
|
|
345
|
+
│ └─ Generate markdown
|
|
346
|
+
│
|
|
347
|
+
└─ Check: wait && process.stdin.isTTY? → YES
|
|
348
|
+
│
|
|
349
|
+
└─ Enter watch loop (infinite)
|
|
350
|
+
│
|
|
351
|
+
├─ await watchFilesAndWait() ← Waits here
|
|
352
|
+
│ │
|
|
353
|
+
│ ├─ (User presses spacebar)
|
|
354
|
+
│ │ └─ resolve('spacebar')
|
|
355
|
+
│ │
|
|
356
|
+
│ └─ (Or: User edits file.lit-md.ts)
|
|
357
|
+
│ └─ Detect change → resolve('filechange')
|
|
358
|
+
│ Show: "Files changed, regenerating..."
|
|
359
|
+
│
|
|
360
|
+
├─ Second executeTasks() call
|
|
361
|
+
│ ├─ Run tests → Show CONDENSED output (capture and parse)
|
|
362
|
+
│ │ If all passed: ✅ Tests passed: 5 passed
|
|
363
|
+
│ │ If failed: ❌ Tests failed: 1/5 failed + failures section
|
|
364
|
+
│ └─ Generate markdown
|
|
365
|
+
│
|
|
366
|
+
└─ Loop back to watchFilesAndWait()
|
|
367
|
+
(repeat forever until Ctrl+C)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Summary
|
|
371
|
+
|
|
372
|
+
The `--wait` mode creates a development-friendly loop:
|
|
373
|
+
|
|
374
|
+
1. **Initial run**: Execute all tasks with full output for debugging
|
|
375
|
+
2. **Watch and wait**: Monitor input files and their dependencies
|
|
376
|
+
3. **Responsive to input**: Either file changes (auto-regenerate) or spacebar (manual trigger)
|
|
377
|
+
4. **Condensed feedback**: Show test summaries instead of full output during iterations
|
|
378
|
+
5. **Resilient to errors**: Keep running even if tests or typecheck fail
|
|
379
|
+
6. **Clean exit**: Ctrl+C properly closes watchers and restores terminal
|
|
380
|
+
|
|
381
|
+
This makes `--wait` ideal for iterative development where you're editing, testing, and regenerating documentation repeatedly.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
## shellExample
|
|
2
|
+
|
|
3
|
+
`shellExample` provides a more structured way to include shell commands,
|
|
4
|
+
with support for
|
|
5
|
+
- input file generation and
|
|
6
|
+
- output file assertions, and
|
|
7
|
+
- more detailed stdout assertions.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
shellExample('echo "hello world"', { stdout: {display: true}})
|
|
11
|
+
```
|
|
12
|
+
becomes
|
|
13
|
+
```sh
|
|
14
|
+
$ echo "hello world"
|
|
15
|
+
hello world
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Can contain assertions on stdout, which appear as comments in the emitted markdown.
|
|
19
|
+
Assertions can use `contains` or `matches`, with either strings or regex patterns.
|
|
20
|
+
```ts
|
|
21
|
+
shellExample('echo "ok"', {stdout: {contains: 'ok'}})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
stdout.matches provides an alternative assertion method:
|
|
25
|
+
```ts
|
|
26
|
+
shellExample('echo "version 1.0.0"', {stdout: {matches: /version \d+\.\d+\.\d+/}})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Can provide input files that are created before the command runs,
|
|
30
|
+
and output file assertions that check for files created by the command and their contents.
|
|
31
|
+
```ts
|
|
32
|
+
shellExample('cp input.txt output.txt', {
|
|
33
|
+
inputFiles: [{path: 'input.txt', content: 'hello world'}],
|
|
34
|
+
outputFiles: [{path: 'output.txt', contains: 'hello world'}]
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output file assertions can check contents with `contains` (substring or regex) or `matches` (regex or string).
|
|
39
|
+
Both properties are optional — you can specify just one, or display file contents without assertions.
|
|
40
|
+
```ts
|
|
41
|
+
shellExample('cp input.txt output.txt', {
|
|
42
|
+
inputFiles: [{path: 'input.txt', content: 'first line\nsecond line'}],
|
|
43
|
+
outputFiles: [{path: 'output.txt', matches: /^first/}]
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
File assertions can also use regex in contains or strings in matches:
|
|
48
|
+
```ts
|
|
49
|
+
shellExample('cp input.txt output.txt', {
|
|
50
|
+
inputFiles: [{path: 'input.txt', content: 'data.json'}],
|
|
51
|
+
outputFiles: [{path: 'output.txt', contains: /\.json/}]
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
You can even output the output file contents, or the stdout:
|
|
56
|
+
```ts
|
|
57
|
+
shellExample('echo "Hello, World!" | tee greeting.txt', {
|
|
58
|
+
stdout: {
|
|
59
|
+
contains: "Hello",
|
|
60
|
+
display: true /* outputs standard out after the command */
|
|
61
|
+
},
|
|
62
|
+
outputFiles: [{
|
|
63
|
+
contains: 'Hello',
|
|
64
|
+
path: 'greeting.txt',
|
|
65
|
+
// display: true, /* by default display, but suppress with `display: false` */
|
|
66
|
+
summary: true
|
|
67
|
+
}]
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
$ echo "Hello, World!" | tee greeting.txt
|
|
73
|
+
Hello, World!
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Output file `greeting.txt` contains `Hello`:
|
|
77
|
+
```
|
|
78
|
+
Hello, World!
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can also display the `shellExample` call itself in the output using the `meta` option:
|
|
82
|
+
```ts
|
|
83
|
+
shellExample('echo "Hello, World!"', {
|
|
84
|
+
meta: true,
|
|
85
|
+
stdout: {}
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
shellExample('echo "Hello, World!"', {
|
|
91
|
+
stdout: {}
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
becomes
|
|
95
|
+
```sh
|
|
96
|
+
$ echo "Hello, World!"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Input and output files can display their contents:
|
|
100
|
+
```ts
|
|
101
|
+
shellExample('cat input.txt', {
|
|
102
|
+
inputFiles: [{
|
|
103
|
+
path: 'input.txt',
|
|
104
|
+
content: 'File contents to display',
|
|
105
|
+
display: true, // Show file contents in output
|
|
106
|
+
displayPath: true,
|
|
107
|
+
summary: true
|
|
108
|
+
}],
|
|
109
|
+
stdout: {display: true}
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Advanced Usage
|
|
114
|
+
|
|
115
|
+
shellExample provides more control and structured options for shell command examples. Use when you need to:
|
|
116
|
+
|
|
117
|
+
- Capture and display stdout dynamically
|
|
118
|
+
- Create input files before running
|
|
119
|
+
- Assert output files match patterns
|
|
120
|
+
- Hide/customize what's displayed
|
|
121
|
+
- Show the function call itself with `meta: true`
|
|
122
|
+
```ts
|
|
123
|
+
shellExample('echo "hello world"')
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
With Assertions
|
|
127
|
+
```ts
|
|
128
|
+
shellExample('echo "ok"', {
|
|
129
|
+
stdout: {contains: 'ok'}
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Exit Code Assertions
|
|
134
|
+
```ts
|
|
135
|
+
shellExample('echo "success"', {
|
|
136
|
+
exitCode: 0,
|
|
137
|
+
stdout: {display: true}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
shellExample('false', {
|
|
141
|
+
exitCode: 1
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
shellExample('true', {
|
|
145
|
+
exitCode: 0,
|
|
146
|
+
stdout: {display: true}
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Input and Output Files
|
|
151
|
+
```ts
|
|
152
|
+
shellExample('cat input.txt > output.txt', {
|
|
153
|
+
inputFiles: [
|
|
154
|
+
{path: 'input.txt', content: 'Hello'}
|
|
155
|
+
],
|
|
156
|
+
outputFiles: [
|
|
157
|
+
{path: 'output.txt', matches: /Hello/}
|
|
158
|
+
]
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Timeout Configuration
|
|
163
|
+
```ts
|
|
164
|
+
shellExample('echo "quick"', {
|
|
165
|
+
timeout: 3000,
|
|
166
|
+
stdout: {display: true}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
shellExample('echo "instant"', {
|
|
170
|
+
timeout: 500,
|
|
171
|
+
stdout: {display: true}
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Options Reference
|
|
176
|
+
|
|
177
|
+
#### displayCommand
|
|
178
|
+
|
|
179
|
+
- Type: `boolean` | `'hidden'`
|
|
180
|
+
- When 'hidden' or false, command is executed but not shown in output
|
|
181
|
+
- Default: true (command is shown)
|
|
182
|
+
|
|
183
|
+
#### stdout
|
|
184
|
+
|
|
185
|
+
- Type: { contains?: string | RegExp; matches?: string | RegExp; display?: boolean }
|
|
186
|
+
- `contains`: Assert output contains this string or matches regex pattern (optional)
|
|
187
|
+
- `matches`: Assert output matches this string (substring) or regex pattern (optional)
|
|
188
|
+
- `display`: When true, dynamically execute and show actual stdout (default: false)
|
|
189
|
+
- At least one of contains/matches is typically specified, but both are optional
|
|
190
|
+
|
|
191
|
+
#### outputFiles
|
|
192
|
+
|
|
193
|
+
Array of output file assertions:
|
|
194
|
+
|
|
195
|
+
- `path`: File path (relative to temp directory)
|
|
196
|
+
- `contains`: String or regex to check if file contains this value (optional)
|
|
197
|
+
- `matches`: String or regex to check if file matches this value (optional)
|
|
198
|
+
- `displayPath`: Show the filename (default: true)
|
|
199
|
+
- `summary`: Show summary line before contents (default: true)
|
|
200
|
+
|
|
201
|
+
#### inputFiles
|
|
202
|
+
|
|
203
|
+
Array of input files to create:
|
|
204
|
+
|
|
205
|
+
- `path`: File path
|
|
206
|
+
- `content`: File contents
|
|
207
|
+
- `displayPath`: Show the filename (default: true)
|
|
208
|
+
- `display`: Show the file contents (default: true)
|
|
209
|
+
- `summary`: Show summary line (default: true)
|
|
210
|
+
|
|
211
|
+
#### exitCode
|
|
212
|
+
|
|
213
|
+
- Type: `number`
|
|
214
|
+
- Asserts the command exits with this specific code
|
|
215
|
+
- When not specified, expects exit code 0 (success)
|
|
216
|
+
- Useful for testing expected failures (e.g., exitCode: 1)
|
|
217
|
+
- If actual exit code doesn't match, command fails with detailed error message
|
|
218
|
+
|
|
219
|
+
#### timeout
|
|
220
|
+
|
|
221
|
+
- Type: `number`
|
|
222
|
+
- Command timeout in milliseconds
|
|
223
|
+
- Default: 3000 (3 seconds)
|
|
224
|
+
- If command runs longer than timeout, throws ETIMEDOUT error
|
|
225
|
+
- Useful for preventing infinite loops or very long-running commands
|
|
226
|
+
|
|
227
|
+
#### meta
|
|
228
|
+
|
|
229
|
+
- Type: `boolean`
|
|
230
|
+
- When true, outputs a fenced code block showing the `shellExample` call itself before the command output
|
|
231
|
+
- The `meta: true` option is removed from the reconstructed call for cleaner documentation
|
|
232
|
+
- Default: false (only shows the command and its output)
|
|
233
|
+
- Useful for showing both the code and its result in documentation
|
|
234
|
+
|
|
235
|
+
#### Example with All Options
|
|
236
|
+
```ts
|
|
237
|
+
shellExample(
|
|
238
|
+
'cat input.txt && echo "Done" | tee result.log', {
|
|
239
|
+
displayCommand: true,
|
|
240
|
+
inputFiles: [
|
|
241
|
+
{path: 'input.txt', content: 'Config data', displayPath: true, display: true, summary: true}
|
|
242
|
+
],
|
|
243
|
+
stdout: {
|
|
244
|
+
contains: 'Done',
|
|
245
|
+
display: true, // Show actual output
|
|
246
|
+
matches: /Done/ // Both contains and matches are optional
|
|
247
|
+
},
|
|
248
|
+
outputFiles: [
|
|
249
|
+
{path: 'result.log', matches: /Done/, displayPath: true, summary: true}
|
|
250
|
+
],
|
|
251
|
+
exitCode: 0, // Assert successful exit
|
|
252
|
+
timeout: 5000 // Set 5 second timeout
|
|
253
|
+
})
|
|
254
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ndp-software/lit-md",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Literate test files that generate README.md",
|
|
5
|
+
"license": "UNLICENSED -- All rights reserved. See LICENSE file.",
|
|
6
|
+
"author": "Andy Peterson <andy@ndpsoftware.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": ""
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"literate programming",
|
|
14
|
+
"markdown",
|
|
15
|
+
"testing",
|
|
16
|
+
"readme",
|
|
17
|
+
"documentation",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"bin": {
|
|
25
|
+
"lit-md": "dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"README.md",
|
|
29
|
+
"dist/**/*",
|
|
30
|
+
"docs/**/*",
|
|
31
|
+
"src/**/*"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prepublishOnly": "npm run verify && npm run build",
|
|
39
|
+
"readme": "node ./src/cli.ts --test --typecheck --out ./README.md src/docs/README.lit-md.ts",
|
|
40
|
+
"test": "node --test './test/**/*.test.ts'",
|
|
41
|
+
"test:watch": "node --test --watch './test/**/*.test.ts'",
|
|
42
|
+
"test:acceptance": "node --test test/acceptance.ts",
|
|
43
|
+
"test:update": "node src/cli.ts -u test/acceptance/*.ts test/acceptance/*.js",
|
|
44
|
+
"typecheck": "tsc -p ./tsconfig.check.json",
|
|
45
|
+
"verify": "npm run typecheck && npm run test && node ./src/cli.ts --test --typecheck --dryrun src/docs/README.lit-md.ts",
|
|
46
|
+
"build": "npm run build:types && npm run build:js",
|
|
47
|
+
"build:all": "npm run clean && npm run readme && npm run build",
|
|
48
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
49
|
+
"build:js": "esbuild ./src/cli.ts --bundle --format=esm --platform=node --target=es2022 --sourcemap --external:typescript --outfile=./dist/cli.js",
|
|
50
|
+
"build:docs": "node ./src/cli.ts --test --typecheck --out ./docs/cli.md ./src/docs/cli.lit-md.ts && node ./src/cli.ts --test --typecheck --out ./docs/shell-examples.md ./src/docs/shell-examples.lit-md.ts",
|
|
51
|
+
"clean": "rm -rf dist"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^25",
|
|
55
|
+
"esbuild": "^0.27.3",
|
|
56
|
+
"typescript": "^5"
|
|
57
|
+
}
|
|
58
|
+
}
|