@doccident/doccident 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -1,179 +1,371 @@
1
- [![npm version](https://badge.fury.io/js/doccident.svg)](http://badge.fury.io/js/doccident)
2
- [![Build Status](https://travis-ci.org/Widdershin/doccident.svg?branch=master)](https://travis-ci.org/Widdershin/doccident)
3
- [![Greenkeeper badge](https://badges.greenkeeper.io/Widdershin/doccident.svg)](https://greenkeeper.io/)
1
+ # doccident
4
2
 
5
- * * *
3
+ [![npm version](https://badge.fury.io/js/doccident.svg)](http://badge.fury.io/js/doccident)
4
+ [![Doccident Tested](https://img.shields.io/badge/doccident-tested-brightgreen.svg)](README.md)
6
5
 
7
- # doccident
6
+ **Test the code examples in your Markdown documentation.**
8
7
 
9
- Test all the code in your markdown docs!
8
+ ## Overview
10
9
 
11
- Why on earth?
12
- ---
10
+ As an open source developer, few things are more frustrating for users than encountering broken examples in a README. `doccident` ensures your documentation remains accurate by treating your examples as testable code. It parses your Markdown files, extracts code blocks for multiple languages, and executes them in a sandboxed environment to verify they run without errors.
13
11
 
14
- As an open source developer, there are few things more embarrassing than a user opening an issue to inform you that your README example is broken! With `doccident`, you can rest easy knowing that your example code is *actually runnable*.
12
+ **Supported Languages:**
13
+ * JavaScript (Node.js)
14
+ * TypeScript
15
+ * Python
16
+ * Shell (Bash, Sh, Zsh)
17
+ * Go
18
+ * Rust
19
+ * Fortran
20
+ * COBOL
21
+ * C
15
22
 
16
- Installation
17
- ---
18
- Just `npm install @doccident/doccident` and run `doccident`. It will run all of the Javascript code examples tucked away in your markdown, and let you know if any blow up.
23
+ > **Note**: `doccident` primarily verifies that your code *runs* without throwing exceptions. While you can add assertions to your examples to test correctness, its main goal is to ensure your documentation examples are valid and runnable.
19
24
 
20
- Okay, how do I use it?
21
- ---
25
+ ## Installation
22
26
 
23
- Let's try it on this repo!
27
+ <!-- skip-example -->
28
+ ```bash
29
+ npm install --save-dev @doccident/doccident
30
+ ```
24
31
 
25
- ```js
26
- var a = 5;
32
+ ## Usage
27
33
 
28
- var b = 10;
34
+ Run `doccident` in your project root. By default, it recursively checks all `.md` and `.markdown` files (excluding `node_modules`).
29
35
 
30
- console.log(a + c);
36
+ <!-- skip-example -->
37
+ ```bash
38
+ npx doccident
31
39
  ```
32
40
 
33
- There's a problem with that example. `doccident` finds it for us:
41
+ You can also target specific files or directories:
34
42
 
43
+ <!-- skip-example -->
35
44
  ```bash
36
- $ doccident
37
- x..
45
+ npx doccident docs/**/*.md
46
+ ```
38
47
 
39
- Failed - README.md:32:17
40
- evalmachine.<anonymous>:7
41
- console.log(a + c);
42
- ^
48
+ ### Language Support & Recipes
43
49
 
44
- ReferenceError: c is not defined
45
- ```
50
+ `doccident` executes code inside fenced code blocks for the following languages:
51
+
52
+ * **JavaScript**: `js`, `javascript`, `es6`
53
+ * **TypeScript**: `ts`, `typescript`
54
+ * **Python**: `py`, `python`
55
+ * **Shell**: `sh`, `bash`, `zsh`, `shell`
56
+ * **Go**: `go`
57
+ * **Rust**: `rust`, `rs`
58
+ * **Fortran**: `fortran`, `f90`, `f95`
59
+ * **COBOL**: `cobol`, `cob`
60
+ * **C**: `c`
46
61
 
47
- Awesome! No excuse for broken documentation ever again, right? :wink:
62
+ It automatically transforms modern JavaScript and TypeScript using **esbuild** before execution.
48
63
 
49
- We can also run specific files or folders by running `doccident` with a glob, like `doccident docs/**/*.md`. By default `doccident` will recursively run all the `.md` or `.markdown` files starting with the current directory, with the exception of the `node_modules` directory.
64
+ #### JavaScript
50
65
 
51
- Note: `doccident` doesn't actually attempt to provide any guarantee that your code worked, only that it didn't explode in a horrible fashion. If you would like to use `doccident` for actually testing the correctness of your code, you can add some `assert`s to your examples.
66
+ Use `js`, `javascript`, or `es6` for JavaScript examples.
52
67
 
53
- `doccident` is not a replacement for your test suite. It's designed to run with your CI build and give you peace of mind that all of your examples are at least vaguely runnable.
68
+ **Execution Model:**
69
+ Runs directly in a Node.js `vm` sandbox.
54
70
 
55
- So how do I write those examples?
56
- ---
71
+ **Recipe:**
57
72
 
58
- In your markdown files, anything inside of code blocks with 'js' or 'es6' will be run. E.g:
73
+ 1. Use `js` fenced code blocks.
74
+ 2. Write standard JavaScript (ES6+ supported).
75
+ 3. Use `require` to load dependencies defined in your configuration.
59
76
 
60
77
  ```js
61
- console.log("Yay, tests in my docs");
78
+ const { sum } = require('./math-utils');
79
+ const result = sum(1, 2);
80
+ ```
81
+
82
+ #### TypeScript
83
+
84
+ Use `ts` or `typescript` for TypeScript examples.
85
+
86
+ **Execution Model:**
87
+ Transpiled via `esbuild`, then runs in a Node.js `vm` sandbox.
88
+
89
+ **Recipe:**
90
+
91
+ 1. Use `ts` fenced code blocks.
92
+ 2. Include type annotations to demonstrate correct usage.
93
+ 3. `doccident` strips types during execution, so your examples serve as both documentation and functional tests.
94
+
95
+ ```ts
96
+ interface User {
97
+ id: number;
98
+ name: string;
99
+ }
100
+
101
+ const user: User = { id: 1, name: 'Doccident' };
102
+ console.log(user.name);
103
+ ```
104
+
105
+ #### Python
106
+
107
+ Use `py` or `python` for Python examples.
108
+
109
+ **Prerequisites:**
110
+ * `python3` must be installed and available in your system path.
111
+ * **macOS**: `brew install python`
112
+ * **Ubuntu/Debian**: `sudo apt-get install python3`
113
+
114
+ **Execution Model:**
115
+ Spawns a `python3` subprocess with the code piped to stdin.
116
+
117
+ **Recipe:**
118
+
119
+ 1. Use `python` fenced code blocks.
120
+ 2. Standard library imports work out of the box.
121
+ 3. State can be shared between blocks using `<!-- share-code-between-examples -->`.
122
+
123
+ ```python
124
+ import json
125
+ data = {"key": "value"}
126
+ assert json.loads('{"key": "value"}') == data
127
+ ```
128
+
129
+ #### Shell Scripts
130
+
131
+ Use `sh`, `bash`, `zsh` or `shell` (defaults to bash) for shell examples.
132
+
133
+ **Prerequisites:**
134
+ * The specified shell (`bash`, `sh`, or `zsh`) must be available in your system path.
135
+
136
+ **Execution Model:**
137
+ Spawns a subprocess using the specified shell, with the code piped to stdin.
138
+
139
+ **Recipe:**
140
+
141
+ 1. Use specific shell tags like `bash` or `zsh` if your script relies on shell-specific syntax.
142
+ 2. Use `sh` for POSIX-compliant scripts.
143
+ 3. State (variables) can be shared between blocks using `<!-- share-code-between-examples -->`.
144
+
145
+ ```bash
146
+ export MY_VAR="hello"
147
+ ```
148
+
149
+ ```bash
150
+ if [ "$MY_VAR" != "hello" ]; then exit 1; fi
151
+ ```
152
+
153
+ #### Go
154
+
155
+ Use `go` for Go examples.
156
+
157
+ **Prerequisites:**
158
+ * `go` must be installed and available in your system path.
159
+ * **macOS**: `brew install go`
160
+ * **Ubuntu/Debian**: `sudo apt-get install golang-go`
161
+
162
+ **Execution Model:**
163
+ Writes code to a temporary file and executes it via `go run`.
164
+
165
+ **Recipe:**
166
+
167
+ 1. Use `go` fenced code blocks.
168
+ 2. You can provide a full program (including `package main`) OR a simple snippet.
169
+ 3. Simple snippets (without `package main`) are automatically wrapped in a `main` function and include `import "fmt"`.
170
+ 4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Go.
171
+
172
+ **Simple Snippet:**
173
+ ```go
174
+ fmt.Println("Hello Go")
175
+ ```
176
+
177
+ #### Rust
178
+
179
+ Use `rust` or `rs` for Rust examples.
180
+
181
+ **Prerequisites:**
182
+ * `rustc` (Rust compiler) must be installed and available in your system path.
183
+ * **macOS**: `brew install rust`
184
+ * **Ubuntu/Debian**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` or `sudo apt-get install rustc`
185
+
186
+ **Execution Model:**
187
+ Compiles the code using `rustc` into a temporary binary, then executes the binary.
188
+
189
+ **Recipe:**
190
+
191
+ 1. Use `rust` fenced code blocks.
192
+ 2. You can provide a full program (including `fn main()`) OR a simple snippet.
193
+ 3. Simple snippets (without `fn main`) are automatically wrapped in a `main` function.
194
+ 4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Rust.
195
+
196
+ **Simple Snippet:**
197
+ ```rust
198
+ println!("Hello Rust");
199
+ let x = 5;
200
+ assert_eq!(x, 5);
62
201
  ```
63
202
 
64
- ```es6
65
- const a = 5;
66
- console.log({a, foo: 'test'});
203
+ #### Fortran
204
+
205
+ Use `fortran`, `f90`, or `f95` for Fortran examples.
206
+
207
+ **Prerequisites:**
208
+ * `gfortran` must be installed and available in your system path.
209
+ * **macOS**: `brew install gcc` (includes gfortran)
210
+ * **Ubuntu/Debian**: `sudo apt-get install gfortran`
211
+
212
+ **Execution Model:**
213
+ Compiles the code using `gfortran` into a temporary binary, then executes the binary.
214
+
215
+ **Recipe:**
216
+
217
+ 1. Use `fortran` fenced code blocks.
218
+ 2. You can provide a full program (starting with `program name`) OR a simple snippet.
219
+ 3. Simple snippets are automatically wrapped in a `program main ... end program main` block.
220
+ 4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for Fortran.
221
+
222
+ **Simple Snippet:**
223
+ ```fortran
224
+ print *, "Hello Fortran"
225
+ ```
226
+
227
+ #### COBOL
228
+
229
+ Use `cobol` or `cob` for COBOL examples.
230
+
231
+ **Prerequisites:**
232
+ * `cobc` (GnuCOBOL) must be installed and available in your system path.
233
+ * **macOS**: `brew install gnucobol`
234
+ * **Ubuntu/Debian**: `sudo apt-get install gnucobol`
235
+
236
+ **Execution Model:**
237
+ Compiles the code using `cobc -x -free` into a temporary executable, then runs it.
238
+
239
+ **Recipe:**
240
+
241
+ 1. Use `cobol` fenced code blocks.
242
+ 2. Provide a full COBOL program (including `IDENTIFICATION DIVISION`).
243
+ 3. The compiler is run in free-format mode (`-free`).
244
+ 4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for COBOL.
245
+
246
+ ```cobol
247
+ IDENTIFICATION DIVISION.
248
+ PROGRAM-ID. HELLO.
249
+ PROCEDURE DIVISION.
250
+ DISPLAY 'Hello COBOL'.
251
+ STOP RUN.
67
252
  ```
68
253
 
69
- I have a code example I don't want tested!
70
- ---
71
- You can tell `doccident` to skip examples by adding `<!-- skip-example -->` before the example. E.g:
254
+ #### C
255
+
256
+ Use `c` for C examples.
257
+
258
+ **Prerequisites:**
259
+ * `gcc` must be installed and available in your system path.
260
+ * **macOS**: `xcode-select --install` or `brew install gcc`
261
+ * **Ubuntu/Debian**: `sudo apt-get install build-essential`
262
+
263
+ **Execution Model:**
264
+ Compiles the code using `gcc` into a temporary binary, then executes the binary.
265
+
266
+ **Recipe:**
267
+
268
+ 1. Use `c` fenced code blocks.
269
+ 2. You can provide a full program (including `main()`) OR a simple snippet.
270
+ 3. Simple snippets are automatically wrapped in a `main` function and include `stdio.h`.
271
+ 4. **Note**: `<!-- share-code-between-examples -->` is **not** supported for C.
272
+
273
+ **Simple Snippet:**
274
+ ```c
275
+ printf("Hello C\n");
276
+ ```
277
+
278
+ ### Skipping Examples
279
+
280
+ To skip a specific code block, add the `<!-- skip-example -->` comment immediately before it:
72
281
 
73
282
  <!-- skip-example -->
74
283
  ```js
75
- // not a runnable example
76
-
77
- var foo = download(...);
284
+ // This code will not be executed
285
+ fetch('https://example.com');
78
286
  ```
79
287
 
80
- How do requires work? And other setup logic?
81
- ---
288
+ ### Sharing Code Between Examples
82
289
 
83
- You can `require` any needed modules or example helpers in `.doccident-setup.js`. E.g:
290
+ By default, each code block is executed in isolation. To share state (variables, functions, classes) between multiple code blocks in the same file, add the `<!-- share-code-between-examples -->` comment. This applies to the entire file.
84
291
 
85
- <!-- skip-example -->
86
- ```js
87
- // .doccident-setup.js
88
- module.exports = {
89
- require: {
90
- Rx: require('rx')
91
- },
292
+ > **Note**: This feature is currently supported for JavaScript, TypeScript, Python, and Shell, but **not** compiled languages like Go, Rust, Fortran, COBOL, and C.
92
293
 
93
- globals: {
94
- $: require('jquery')
95
- }
96
- }
97
- ```
294
+ <!-- share-code-between-examples -->
98
295
 
99
- Anything exported under `require` will then be used by any examples that `require` that key.
100
- You must explicitly configure all of the dependencies used in your examples.
296
+ ```python
297
+ x = 10
298
+ ```
101
299
 
102
- Anything exported under `globals` will be available globally across all examples.
300
+ ```python
301
+ # x is still available here
302
+ assert x == 10
303
+ ```
103
304
 
104
- You can also specify a regexRequire section to handle anything more complex than an exact string match!
305
+ ## Configuration
105
306
 
106
- <!-- skip-example -->
107
- ```js
307
+ Create a `.doccident-setup.js` file in your project root to configure the test environment.
308
+
309
+ ### Injecting Dependencies (`require`)
310
+
311
+ If your examples use external libraries, provide them here. This allows your examples to `require` modules just like users would:
312
+
313
+ ```javascript
108
314
  // .doccident-setup.js
109
315
  module.exports = {
110
316
  require: {
111
- Rx: require('rx')
112
- },
113
-
114
- regexRequire: {
115
- 'rx/(.*)': function (fullPath, matchedModuleName) {
116
- return require('./dist/' + matchedModuleName);
317
+ // Make 'my-library' available when examples call require('my-library')
318
+ 'my-library': require('./index.js'),
319
+ 'lodash': require('lodash')
117
320
  }
118
- }
119
- }
321
+ };
120
322
  ```
121
323
 
122
- Do I have to enable es6 support?
123
- ---
324
+ ### Global Variables
124
325
 
125
- Nope, ES6 support is on by default. You can disable `babel` support
126
- in your `.doccident-setup.js` file.
127
- This will speed things up drastically:
326
+ Define global variables available to all snippets:
128
327
 
129
- <!-- skip-example -->
130
- ```js
131
- //.doccident-setup.js
328
+ ```javascript
132
329
  module.exports = {
133
- babel: false
134
- }
135
- ```
136
-
137
- What if I have global state that needs to be reset after my examples run?
138
- ---
139
- <!-- skip-example -->
140
- ```js
141
- //.doccident-setup.js
142
- module.exports = {
143
- beforeEach: function () {
144
- // reset your awesome global state
330
+ globals: {
331
+ $: require('jquery'),
332
+ window: {}
145
333
  }
146
- }
334
+ };
147
335
  ```
148
336
 
149
- You can specify a function to be run before each example in your `.doccident-setup.js`.
337
+ ### Advanced Configuration
150
338
 
151
- What if I want to remove custom syntax from examples before processing?
152
- ---
339
+ * **`regexRequire`**: Handle dynamic requires that match a pattern.
340
+ * **`beforeEach`**: Function to run before each snippet (e.g., to reset global state).
341
+ * **`transformCode`**: Pre-process code before execution (e.g., to strip out display-only syntax).
153
342
 
154
- <!-- skip-example -->
155
- ```js
156
- //.doccident-setup.js
157
- module.exports = {
158
- transformCode(code) {
159
- // Remove ... from code syntax
160
- return code.replace(/\.\.\./g, "");
161
- }
162
- }
163
- ```
343
+ ## Architecture and Approach
344
+
345
+ `doccident` is designed to be a lightweight, flexible testing harness for documentation. The codebase is modularized to separate parsing, execution, and reporting, ensuring maintainability and extensibility.
346
+
347
+ ### Core Modules
348
+
349
+ 1. **Parser (`src/parse-code-snippets-from-markdown.ts`)**
350
+ * Reads Markdown content line-by-line.
351
+ * Uses a robust state machine to identify code fences and control comments (like `skip-example`).
352
+ * Extracts valid snippets into structured objects containing code, file paths, and line numbers.
353
+ * **Multi-Language Support**: Recognizes `js`, `ts`, `python`, `shell`, `go`, `rust`, `fortran`, `cobol`, and `c` blocks.
354
+
355
+ 2. **Test Runner (`src/doctest.ts`)**
356
+ * The orchestrator of the application.
357
+ * Iterates through parsed snippets and manages the execution lifecycle.
358
+ * **Sandboxing**: Uses Node.js's `vm` module (`runInNewContext`) to execute JS/TS code in isolation.
359
+ * **Subprocess Execution**: Spawns `python3`, `go`, `rustc`, `gfortran`, `cobc`, `gcc`, or shell subprocesses for non-JS languages.
360
+ * **Transformation**: Uses **esbuild** to compile modern JavaScript and TypeScript code down to a compatible format before execution.
361
+
362
+ 3. **Reporter (`src/reporter.ts`)**
363
+ * Collects execution results (pass, fail, skip).
364
+ * Formats output using `chalk` for readability.
365
+ * **Error Mapping**: Crucially, it maps execution errors back to the specific line number in the original Markdown file, making it easy to identify exactly which line in your documentation caused the failure.
366
+
367
+ 4. **Types & Utils**
368
+ * Shared interfaces (`src/types.ts`) ensure type safety across the application.
369
+ * Utility functions (`src/utils.ts`) provide common helpers.
164
370
 
165
- Who uses doccident?
166
- ---
167
-
168
- All of these projects either run `doccident` with `npm test` or as part of their CI process:
169
-
170
- * [lodash](https://github.com/lodash/lodash)
171
- * [Moment](https://github.com/moment/momentjs.com)
172
- * [RxJS](https://github.com/ReactiveX/RxJS)
173
- * [most](https://github.com/cujojs/most)
174
- * [xstream](https://github.com/staltz/xstream)
175
- * [cyclejs/time](https://github.com/cyclejs/time)
176
- * [rx.schedulers](https://github.com/Reactive-Extensions/rx.schedulers)
177
- * [rx.priorityqueue](https://github.com/Reactive-Extensions/rx.priorityqueue)
178
- * [rx.disposables](https://github.com/Reactive-Extensions/rx.disposables)
179
- * [rx-undoable](https://github.com/Widdershin/rx-undoable)
371
+ This separation of concerns allows `doccident` to be easily extended—for example, by adding new parsers for different documentation formats or custom reporters for CI environments.
package/bin/cmd.js CHANGED
@@ -5,9 +5,9 @@
5
5
 
6
6
  const { version } = require('../package');
7
7
  const doctest = require('..');
8
- const program = require('commander');
8
+ const { program } = require('commander');
9
9
  const path = require('path');
10
- const glob = require('glob');
10
+ const fg = require('fast-glob');
11
11
  const fs = require('fs');
12
12
  const { pathToFileURL } = require('url');
13
13
 
@@ -30,15 +30,18 @@ program
30
30
  .name('doccident')
31
31
  .description('Test all the code in your markdown docs!')
32
32
  .version(version, '-v, --version', 'output the current version')
33
+ .argument('[glob]', 'glob pattern for files to test')
33
34
  .helpOption('-h, --help', 'output usage informations')
34
35
  .option('-c, --config <path>', 'custom config location', path.join(process.cwd(), '/.doccident-setup.js'))
35
36
  .option('--test-output', 'output the test results to the console')
36
37
  .parse(process.argv);
37
38
 
39
+ const options = program.opts();
40
+
38
41
  // Parse config file
39
42
  (async () => {
40
- if (program.config) {
41
- const configPath = path.resolve(program.config);
43
+ if (options.config) {
44
+ const configPath = path.resolve(options.config);
42
45
 
43
46
  if (fs.existsSync(configPath)) {
44
47
  try {
@@ -51,33 +54,31 @@ program
51
54
  }
52
55
  }
53
56
 
54
- if (program.testOutput) {
57
+ if (options.testOutput) {
55
58
  config.testOutput = true;
56
59
  }
57
60
 
58
61
  // Resolve files
59
- glob(
60
- program.args[0] || DEFAULT_GLOB,
61
- {
62
- ignore: [...config.ignore, ...DEFAULT_IGNORE]
63
- },
64
- (err, files) => {
65
-
66
- if (err) {
67
- console.trace(err);
62
+ try {
63
+ const files = await fg(
64
+ program.args[0] || DEFAULT_GLOB,
65
+ {
66
+ ignore: [...config.ignore, ...DEFAULT_IGNORE]
68
67
  }
68
+ );
69
69
 
70
- // Run tests
71
- const results = doctest.runTests(files, config);
70
+ // Run tests
71
+ const results = doctest.runTests(files, config);
72
72
 
73
- console.log('\n');
74
- doctest.printResults(results);
73
+ console.log('\n');
74
+ doctest.printResults(results);
75
75
 
76
- // Exit with error-code if any test failed
77
- const failures = results.filter(result => result.status === 'fail');
78
- if (failures.length > 0) {
79
- process.exit(1);
80
- }
76
+ // Exit with error-code if any test failed
77
+ const failures = results.filter(result => result.status === 'fail');
78
+ if (failures.length > 0) {
79
+ process.exit(1);
81
80
  }
82
- );
81
+ } catch (err) {
82
+ console.trace(err);
83
+ }
83
84
  })();