@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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { join, dirname, basename, extname, resolve } from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { parse } from './parser.ts'
|
|
6
|
+
import { render } from './renderer.ts'
|
|
7
|
+
import { typecheck } from './typecheck.ts'
|
|
8
|
+
import { stripTypesFlag, watchFilesAndWait } from './shell.ts'
|
|
9
|
+
import { resolveOutputFiles } from './resolver.ts'
|
|
10
|
+
import { resolveDescribeFormat, resetDescribeFormat } from './describe-format.ts'
|
|
11
|
+
|
|
12
|
+
// --- Argument parsing ---
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2)
|
|
15
|
+
|
|
16
|
+
function extractFlag(flag: string): boolean {
|
|
17
|
+
const idx = args.indexOf(flag)
|
|
18
|
+
if (idx === -1) return false
|
|
19
|
+
args.splice(idx, 1)
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractFlagValue(flag: string): string | undefined {
|
|
24
|
+
const idx = args.indexOf(flag)
|
|
25
|
+
if (idx === -1) {
|
|
26
|
+
// Check for --flag=value format
|
|
27
|
+
const eqIdx = args.findIndex(arg => arg.startsWith(flag + '='))
|
|
28
|
+
if (eqIdx === -1) return undefined
|
|
29
|
+
const value = args[eqIdx]!.slice(flag.length + 1)
|
|
30
|
+
args.splice(eqIdx, 1)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
const value = args[idx + 1]
|
|
34
|
+
args.splice(idx, 2)
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const showHelp = extractFlag('--help') || extractFlag('-h')
|
|
39
|
+
const dryrun = extractFlag('--dryrun')
|
|
40
|
+
const runTests = extractFlag('--test')
|
|
41
|
+
const runTypecheck = extractFlag('--typecheck')
|
|
42
|
+
const updateSnapshots = extractFlag('--update-snapshots') || extractFlag('-u')
|
|
43
|
+
const wait = extractFlag('--wait')
|
|
44
|
+
const outFlag = extractFlagValue('--out')
|
|
45
|
+
const outputDir = extractFlagValue('--outDir')
|
|
46
|
+
const describeFormat = extractFlagValue('--describe') || '##'
|
|
47
|
+
|
|
48
|
+
// Helper to parse test summary from output
|
|
49
|
+
function parseTestSummary(output: string): { passed: number; failed: number; total: number; hasFailed: boolean } {
|
|
50
|
+
const lines = output.split('\n')
|
|
51
|
+
let stats = { passed: 0, failed: 0, total: 0, hasFailed: false }
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line.includes('ℹ pass')) {
|
|
55
|
+
const match = line.match(/pass\s+(\d+)/)
|
|
56
|
+
if (match) stats.passed = parseInt(match[1])
|
|
57
|
+
}
|
|
58
|
+
if (line.includes('ℹ fail')) {
|
|
59
|
+
const match = line.match(/fail\s+(\d+)/)
|
|
60
|
+
if (match) stats.failed = parseInt(match[1])
|
|
61
|
+
}
|
|
62
|
+
if (line.includes('ℹ tests')) {
|
|
63
|
+
const match = line.match(/tests\s+(\d+)/)
|
|
64
|
+
if (match) stats.total = parseInt(match[1])
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stats.hasFailed = stats.failed > 0
|
|
69
|
+
return stats
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for unknown options
|
|
73
|
+
const unknownOptions = args.filter(a => a.startsWith('--') || (a.startsWith('-') && a.length > 1 && a !== '-'))
|
|
74
|
+
if (unknownOptions.length > 0) {
|
|
75
|
+
console.error(`error: unknown option${unknownOptions.length > 1 ? 's' : ''}: ${unknownOptions.join(', ')}`)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const inputPaths = args.filter(a => !a.startsWith('--'))
|
|
80
|
+
|
|
81
|
+
// --- File name rewriting helper ---
|
|
82
|
+
|
|
83
|
+
function getOutputFileName(inputPath: string): string {
|
|
84
|
+
const base = basename(inputPath)
|
|
85
|
+
// Check if file ends with .lit-md.ts or .lit-md.js pattern
|
|
86
|
+
if (base.endsWith('.lit-md.ts') || base.endsWith('.lit-md.js')) {
|
|
87
|
+
// Remove the entire .lit-md.ts or .lit-md.js extension
|
|
88
|
+
return base.slice(0, -(base.endsWith('.lit-md.ts') ? '.lit-md.ts'.length : '.lit-md.js'.length)) + '.md'
|
|
89
|
+
}
|
|
90
|
+
// Otherwise, remove the final extension (.ts, .js, etc.) and add .md
|
|
91
|
+
return basename(inputPath, extname(inputPath)) + '.md'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Help ---
|
|
95
|
+
|
|
96
|
+
if (showHelp) {
|
|
97
|
+
console.log(`lit-md - Generate markdown documentation from test files
|
|
98
|
+
|
|
99
|
+
Usage: lit-md [options] <file.ts|js> [file2 ...]
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--help, -h Show this help message
|
|
103
|
+
--test Run tests before generating markdown
|
|
104
|
+
--typecheck Run type checking before generating markdown
|
|
105
|
+
--dryrun Show what would be written without writing files
|
|
106
|
+
-u, --update-snapshots Update snapshot files instead of generating markdown
|
|
107
|
+
--wait After generating, keep the process alive and watch for file
|
|
108
|
+
changes. Press space to manually regenerate, Ctrl+C to exit.
|
|
109
|
+
Works with --test and --typecheck (reruns on each change).
|
|
110
|
+
--out <output.md> Write to a specific output file (requires single input)
|
|
111
|
+
--outDir <dir> Write generated markdown files to this directory
|
|
112
|
+
--describe <format> Control describe() block rendering (default: ##)
|
|
113
|
+
Formats:
|
|
114
|
+
hidden - Omit describes
|
|
115
|
+
# - Render as h1 headers, nested as h2, h3, etc.
|
|
116
|
+
## - Render as h2 headers, nested as h3, h4, etc. (default)
|
|
117
|
+
### - Render as h3 headers, nested as h4, h5, etc.
|
|
118
|
+
#### - Render as h4 headers, nested as h5, h6, etc.
|
|
119
|
+
auto - Dynamically determine level based on document structure
|
|
120
|
+
(h1 if no headers exist, else one level deeper than last header)
|
|
121
|
+
|
|
122
|
+
By default, output is written to stdout. Use --out or --outDir to write to files.
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
lit-md README.md.test.ts # outputs to stdout
|
|
126
|
+
lit-md --test --typecheck README.md.test.ts # outputs to stdout after testing
|
|
127
|
+
lit-md --wait README.md.test.ts # outputs to stdout, then waits for changes
|
|
128
|
+
lit-md --out /tmp/docs.md README.md.test.ts # writes to file
|
|
129
|
+
lit-md --outDir ./docs src/**/*.md.test.ts # writes to directory
|
|
130
|
+
lit-md --describe="#" README.md.test.ts # outputs to stdout with custom format
|
|
131
|
+
lit-md --describe="auto" README.md.test.ts # outputs to stdout with auto format
|
|
132
|
+
`)
|
|
133
|
+
process.exit(0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Validation ---
|
|
137
|
+
|
|
138
|
+
if (!inputPaths.length) {
|
|
139
|
+
console.error('Usage: lit-md [--test] [--typecheck] [--dryrun] [-u|--update-snapshots] [--out <output.md>] [--outDir <dir>] <file.ts|js> [file2 ...]')
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const validDescribeFormats = ['hidden', 'auto', '#', '##', '###', '####']
|
|
144
|
+
if (!validDescribeFormats.includes(describeFormat)) {
|
|
145
|
+
console.error(`error: invalid --describe format: ${describeFormat}. Valid formats: ${validDescribeFormats.join(', ')}`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (outFlag && outputDir) {
|
|
150
|
+
console.error('error: --out and --outDir are mutually exclusive')
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (outFlag && inputPaths.length > 1) {
|
|
155
|
+
console.error('error: --out can only be used with a single input file')
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (runTypecheck) {
|
|
160
|
+
const jsFiles = inputPaths.filter(f => extname(f) === '.js')
|
|
161
|
+
if (jsFiles.length) {
|
|
162
|
+
console.error(`error: --typecheck requires .ts files; received: ${jsFiles.join(', ')}`)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Typecheck ---
|
|
168
|
+
// NOTE: Moved into executeTasks() to run on each regeneration when --wait is used
|
|
169
|
+
|
|
170
|
+
// --- Run tests ---
|
|
171
|
+
// NOTE: Moved into executeTasks() to run on each regeneration when --wait is used
|
|
172
|
+
|
|
173
|
+
// --- Generate markdown ---
|
|
174
|
+
|
|
175
|
+
async function generateMarkdown(): Promise<void> {
|
|
176
|
+
let filesGenerated = 0
|
|
177
|
+
|
|
178
|
+
for (const inputPath of inputPaths) {
|
|
179
|
+
// Reset the describe format override before processing each file
|
|
180
|
+
resetDescribeFormat()
|
|
181
|
+
|
|
182
|
+
// Import the file to allow module-level setup (like setDescribeFormat calls)
|
|
183
|
+
const absolutePath = resolve(inputPath)
|
|
184
|
+
try {
|
|
185
|
+
// Suppress test output during import and test execution
|
|
186
|
+
const origStdoutWrite = process.stdout.write
|
|
187
|
+
const origStderrWrite = process.stderr.write
|
|
188
|
+
const origLog = console.log
|
|
189
|
+
const origInfo = console.info
|
|
190
|
+
const origWarn = console.warn
|
|
191
|
+
try {
|
|
192
|
+
process.stdout.write = () => true as any
|
|
193
|
+
process.stderr.write = () => true as any
|
|
194
|
+
console.log = () => {}
|
|
195
|
+
console.info = () => {}
|
|
196
|
+
console.warn = () => {}
|
|
197
|
+
await import(absolutePath)
|
|
198
|
+
// Wait for deferred test execution to complete while output is suppressed
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
200
|
+
} finally {
|
|
201
|
+
process.stdout.write = origStdoutWrite
|
|
202
|
+
process.stderr.write = origStderrWrite
|
|
203
|
+
console.log = origLog
|
|
204
|
+
console.info = origInfo
|
|
205
|
+
console.warn = origWarn
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// File might not be valid JavaScript/TypeScript module, continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const src = readFileSync(inputPath, 'utf8')
|
|
212
|
+
const lang = extname(inputPath) === '.js' ? 'javascript' : 'typescript'
|
|
213
|
+
let nodes = parse(src, lang)
|
|
214
|
+
if (!dryrun) {
|
|
215
|
+
nodes = resolveOutputFiles(nodes)
|
|
216
|
+
}
|
|
217
|
+
// Use resolved format (CLI value + file override)
|
|
218
|
+
const finalDescribeFormat = resolveDescribeFormat(describeFormat)
|
|
219
|
+
const md = render(nodes, finalDescribeFormat)
|
|
220
|
+
|
|
221
|
+
let outPath: string | null = null
|
|
222
|
+
let isStdout = false
|
|
223
|
+
if (updateSnapshots) {
|
|
224
|
+
const outputFileName = getOutputFileName(inputPath)
|
|
225
|
+
const fileNameWithoutMd = outputFileName.slice(0, -3) // Remove .md
|
|
226
|
+
outPath = join(dirname(resolve(inputPath)), `${fileNameWithoutMd}.snapshot.md`)
|
|
227
|
+
} else if (outFlag) {
|
|
228
|
+
outPath = outFlag
|
|
229
|
+
} else if (outputDir) {
|
|
230
|
+
mkdirSync(outputDir, { recursive: true })
|
|
231
|
+
outPath = join(outputDir, getOutputFileName(inputPath))
|
|
232
|
+
} else {
|
|
233
|
+
isStdout = true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (dryrun) {
|
|
237
|
+
if (isStdout) {
|
|
238
|
+
console.error(`dry run: would write to stdout`)
|
|
239
|
+
} else {
|
|
240
|
+
console.error(`dry run: would write ${outPath}`)
|
|
241
|
+
}
|
|
242
|
+
} else if (isStdout) {
|
|
243
|
+
process.stdout.write(md + '\n')
|
|
244
|
+
} else {
|
|
245
|
+
writeFileSync(outPath!, md + '\n', 'utf8')
|
|
246
|
+
filesGenerated++
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Show summary if files were written
|
|
251
|
+
if (!dryrun && filesGenerated > 0) {
|
|
252
|
+
const fileWord = filesGenerated === 1 ? 'file' : 'files'
|
|
253
|
+
console.error(`✅ Generated ${filesGenerated} ${fileWord}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function executeTasks(): Promise<void> {
|
|
258
|
+
// Run typecheck before generation (if enabled)
|
|
259
|
+
if (runTypecheck) {
|
|
260
|
+
const result = typecheck(inputPaths.map(p => resolve(p)))
|
|
261
|
+
if (!result.ok) {
|
|
262
|
+
for (const msg of result.messages) console.error(msg)
|
|
263
|
+
console.error('❌ Typecheck failed')
|
|
264
|
+
// In wait mode, report error but continue; in normal mode, exit
|
|
265
|
+
if (!wait) process.exit(1)
|
|
266
|
+
// Continue to markdown generation even if typecheck failed
|
|
267
|
+
} else {
|
|
268
|
+
console.error('✅ Typecheck passed')
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run tests before generation (if enabled)
|
|
273
|
+
if (runTests) {
|
|
274
|
+
const stripFlag = stripTypesFlag()
|
|
275
|
+
const nodeArgs = ['--test', ...(stripFlag ? [stripFlag] : []), ...inputPaths.map(p => resolve(p))]
|
|
276
|
+
|
|
277
|
+
// In wait mode, capture output for summary display; otherwise inherit (show full output)
|
|
278
|
+
const spawnOptions = wait ? { encoding: 'utf-8' as const } : { stdio: 'inherit' as const, env: process.env }
|
|
279
|
+
|
|
280
|
+
const result = spawnSync(process.execPath, nodeArgs, spawnOptions)
|
|
281
|
+
|
|
282
|
+
// Handle output based on mode
|
|
283
|
+
if (wait && result.stdout) {
|
|
284
|
+
// In wait mode: capture output and show condensed summary
|
|
285
|
+
const output = result.stdout.toString()
|
|
286
|
+
const stats = parseTestSummary(output)
|
|
287
|
+
|
|
288
|
+
if (stats.hasFailed) {
|
|
289
|
+
// Extract and show failures section
|
|
290
|
+
const failureStart = output.indexOf('✖ failing tests')
|
|
291
|
+
if (failureStart !== -1) {
|
|
292
|
+
const failureSection = output.substring(failureStart)
|
|
293
|
+
console.error(failureSection)
|
|
294
|
+
}
|
|
295
|
+
console.error(`\n❌ Tests failed: ${stats.failed}/${stats.total} failed`)
|
|
296
|
+
} else {
|
|
297
|
+
// All passed: show one-line summary
|
|
298
|
+
console.log(`✅ Tests passed: ${stats.passed} passed`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (result.status !== 0) {
|
|
303
|
+
// In wait mode, report error but continue; in normal mode, exit
|
|
304
|
+
if (!wait) process.exit(result.status ?? 1)
|
|
305
|
+
// Continue to markdown generation even if tests failed
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Generate markdown (always do this, even if tests/typecheck failed)
|
|
310
|
+
await generateMarkdown()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
;(async () => {
|
|
314
|
+
await executeTasks()
|
|
315
|
+
|
|
316
|
+
// If --wait flag is set and we're in an interactive terminal, enter the watch loop
|
|
317
|
+
if (wait && process.stdin.isTTY) {
|
|
318
|
+
while (true) {
|
|
319
|
+
const trigger = await watchFilesAndWait(inputPaths)
|
|
320
|
+
// On spacebar or file change, regenerate
|
|
321
|
+
await executeTasks()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type DescribeFormatType = 'hidden' | '#' | '##' | '###' | '####' | 'auto'
|
|
2
|
+
|
|
3
|
+
let overrideFormat: DescribeFormatType | undefined = undefined
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Set the describe format for the current file, overriding the CLI --describe option.
|
|
7
|
+
* This must be called at the top of the file, before any examples or describes.
|
|
8
|
+
*
|
|
9
|
+
* @param format - The format to use for rendering describe() block names:
|
|
10
|
+
* - 'hidden': omit describe names (default)
|
|
11
|
+
* - '#', '##', '###', '####': render as H1-H4 headers with nesting support
|
|
12
|
+
* - 'auto': dynamically determine level based on document structure (h1 if no headers exist, else one level deeper than last header)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { setDescribeFormat } from '@ndp-software/lit-md'
|
|
17
|
+
* setDescribeFormat('##')
|
|
18
|
+
* // All describe() blocks in this file will be rendered as H2+ headers
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function setDescribeFormat(format: DescribeFormatType): void {
|
|
22
|
+
if (overrideFormat !== undefined && overrideFormat !== format) {
|
|
23
|
+
console.warn(`setDescribeFormat called multiple times with different formats: ${overrideFormat} -> ${format}. Only the last call will be used.`)
|
|
24
|
+
}
|
|
25
|
+
overrideFormat = format
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current describe format override (if any).
|
|
30
|
+
* Returns undefined if no override has been set.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function getDescribeFormatOverride(): DescribeFormatType | undefined {
|
|
34
|
+
return overrideFormat
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reset the describe format override.
|
|
39
|
+
* Mainly useful for testing.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function resetDescribeFormat(): void {
|
|
43
|
+
overrideFormat = undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the final describe format to use.
|
|
48
|
+
* Takes the CLI format and applies the override if set.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export function resolveDescribeFormat(cliFormat: string): string {
|
|
52
|
+
return overrideFormat ?? cliFormat
|
|
53
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
example,
|
|
4
|
+
shellExample,
|
|
5
|
+
metaExample,
|
|
6
|
+
alias,
|
|
7
|
+
stripTypesFlag,
|
|
8
|
+
setDescribeFormat
|
|
9
|
+
} from '../index.ts'
|
|
10
|
+
import assert from 'node:assert/strict'
|
|
11
|
+
|
|
12
|
+
const _flag = stripTypesFlag()
|
|
13
|
+
alias('lit-md', ['node', _flag, './src/cli.ts'].filter(Boolean).join(' '))
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
# @ndp-software/lit-md
|
|
17
|
+
|
|
18
|
+
Literate test files that generate `README.md`s.
|
|
19
|
+
|
|
20
|
+
## Introduction
|
|
21
|
+
|
|
22
|
+
Some projects, especially libraries, require numerous and
|
|
23
|
+
detailed code examples. For those responsible for updating
|
|
24
|
+
a README, or other documentation, this can be burdensome
|
|
25
|
+
and error-prone.
|
|
26
|
+
|
|
27
|
+
With lit-md, you write your documentation with embedded code samples
|
|
28
|
+
that are automatically type-checked and run through node to
|
|
29
|
+
verify them. Every example actually works!
|
|
30
|
+
|
|
31
|
+
Key features:
|
|
32
|
+
- works with Typescript or Javascript
|
|
33
|
+
- provides utilities to include shell commands and their outputs
|
|
34
|
+
as part of your documentation. This is important if you tool has
|
|
35
|
+
a CLI, or you just need to show how it works in the terminal.
|
|
36
|
+
- supports flexible assertion methods for both code examples and shell commands,
|
|
37
|
+
with options to display actual outputs in the generated markdown
|
|
38
|
+
- fully tested with its own test suite, which also serves as
|
|
39
|
+
documentation and examples for users
|
|
40
|
+
|
|
41
|
+
There _are_ other tools with the same aims (e.g. [TwoSlash](https://github.com/microsoft/TypeScript-Website/tree/v2/packages/ts-twoslasher)),
|
|
42
|
+
but this follows in the Literate programming tradition but updated
|
|
43
|
+
for the Typescript and TDD era. I have aimed to provide a great DX
|
|
44
|
+
for writing documentation, with a simple syntax and powerful features
|
|
45
|
+
that work well with the node ecosystem.
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# You can "run" your documentation as a test:
|
|
49
|
+
node --test README.lit-md.ts
|
|
50
|
+
# You can also typecheck:
|
|
51
|
+
tsc README.lit-md.ts
|
|
52
|
+
# An it can be converted from Typescript to a plain old markdown
|
|
53
|
+
# README file with `lit-md`:
|
|
54
|
+
lit-md README.lit-md.ts # generates README.md
|
|
55
|
+
|
|
56
|
+
# Or, you can do it all in one step with:
|
|
57
|
+
lit-md --test --typecheck README.lit-md.ts # all-in-one!
|
|
58
|
+
```
|
|
59
|
+
## How it Works
|
|
60
|
+
A **lit-md** file contains prose in comments and examples in test bodies.
|
|
61
|
+
At a basic level, a file is processed and
|
|
62
|
+
- comments are directly transferred into markdown, and
|
|
63
|
+
- example (or `test`, `it`, `spec`) bodies become fenced code blocks.
|
|
64
|
+
To make this work well, there are quite a few nuances and features to control
|
|
65
|
+
what appears in the output and how it looks.
|
|
66
|
+
|
|
67
|
+
# A simple example
|
|
68
|
+
*/
|
|
69
|
+
shellExample(`lit-md tmp.ts --out out.md`, {
|
|
70
|
+
inputFiles: [{
|
|
71
|
+
path: 'tmp.ts',
|
|
72
|
+
displayPath: false,
|
|
73
|
+
content: `
|
|
74
|
+
describe('My Project README.', () => {
|
|
75
|
+
/*
|
|
76
|
+
This is a really great project!
|
|
77
|
+
Adding numbers is as simple as using the "+" operator:
|
|
78
|
+
*/
|
|
79
|
+
example('add example', () => {
|
|
80
|
+
const a = 1
|
|
81
|
+
const b = 2
|
|
82
|
+
console.log(a + b) // => 3
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Also supported is multiplication:
|
|
86
|
+
example('multiply example', () => {
|
|
87
|
+
const x = 3
|
|
88
|
+
const y = 4
|
|
89
|
+
console.log(x * y) // => 12
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
`
|
|
93
|
+
}],
|
|
94
|
+
outputFiles: [{
|
|
95
|
+
path: "out.md"
|
|
96
|
+
}]
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
For more information on the CLI, see [CLI documentation](./docs/cli.md).
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
describe('Shell examples', () => {
|
|
104
|
+
/*
|
|
105
|
+
Use `shellExample` to include executable shell commands in the README.
|
|
106
|
+
It verifies a 0 return code and provides flexible assertion and display options.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
describe('shellExample function', () => {
|
|
110
|
+
shellExample('echo "hello world"', { meta: true, stdout: { display: true } })
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
For more information on the `shellExample`, see [shellCommand documentation](./docs/shell-examples.md).
|
|
115
|
+
*/
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
## CREDITS
|
|
120
|
+
|
|
121
|
+
Built by Andrew J. Peterson, although there was some manual code changes,
|
|
122
|
+
most of the code was Github Copilot CLI, using mostly Claude Haiku 4.5
|
|
123
|
+
and some Claude Sonnet 4.6.
|
|
124
|
+
Most tasks used a plan-autopilot loop, but other approaches were used as well.
|
|
125
|
+
*/
|
|
126
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {alias, describe, stripTypesFlag, shellExample} from '../index.ts'
|
|
2
|
+
|
|
3
|
+
const _flag = stripTypesFlag()
|
|
4
|
+
alias('lit-md', ['node', _flag, './src/cli.ts'].filter(Boolean).join(' '))
|
|
5
|
+
|
|
6
|
+
describe('CLI', () => {
|
|
7
|
+
// By default, output is written to stdout.
|
|
8
|
+
shellExample('lit-md tmp.ts', {
|
|
9
|
+
displayCommand: true,
|
|
10
|
+
inputFiles: [{
|
|
11
|
+
path: 'tmp.ts',
|
|
12
|
+
content: `// # My Document\nimport { example } from 'node:test'\nexample('test', () => {})`
|
|
13
|
+
}],
|
|
14
|
+
stdout: {
|
|
15
|
+
contains: '# My Document',
|
|
16
|
+
display: true
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('Custom output path', () => {
|
|
21
|
+
// Use --out to write to a different location.
|
|
22
|
+
shellExample('lit-md tmp.ts --out /tmp/docs.md', {
|
|
23
|
+
displayCommand: true,
|
|
24
|
+
inputFiles: [{
|
|
25
|
+
path: 'tmp.ts',
|
|
26
|
+
content: `// # Documentation\nimport { example } from 'node:test'`
|
|
27
|
+
}],
|
|
28
|
+
outputFiles: [{
|
|
29
|
+
path: '/tmp/docs.md',
|
|
30
|
+
contains: '# Documentation'
|
|
31
|
+
}]
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('Help', () => {
|
|
36
|
+
// Use `lit-md --help` for options.
|
|
37
|
+
shellExample('lit-md --help', {
|
|
38
|
+
displayCommand: true,
|
|
39
|
+
stdout: {
|
|
40
|
+
display: true
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|