@doccident/doccident 0.0.1 → 0.0.3
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 +94 -126
- package/bin/cmd.js +25 -24
- package/dist/doctest.js +57 -97
- package/dist/parse-code-snippets-from-markdown.js +18 -26
- package/dist/reporter.js +78 -0
- package/dist/types.js +2 -0
- package/dist/utils.js +6 -0
- package/package.json +17 -23
package/README.md
CHANGED
|
@@ -1,179 +1,147 @@
|
|
|
1
|
-
[](http://badge.fury.io/js/doccident)
|
|
2
|
-
[](https://travis-ci.org/Widdershin/doccident)
|
|
3
|
-
[](https://greenkeeper.io/)
|
|
4
|
-
|
|
5
|
-
* * *
|
|
6
|
-
|
|
7
1
|
# doccident
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Why on earth?
|
|
12
|
-
---
|
|
13
|
-
|
|
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*.
|
|
3
|
+
[](http://badge.fury.io/js/doccident)
|
|
15
4
|
|
|
16
|
-
|
|
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.
|
|
5
|
+
**Test the code examples in your Markdown documentation.**
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
---
|
|
7
|
+
## Overview
|
|
22
8
|
|
|
23
|
-
|
|
9
|
+
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 JavaScript and TypeScript code blocks, and executes them in a sandboxed environment to verify they run without errors.
|
|
24
10
|
|
|
25
|
-
|
|
26
|
-
var a = 5;
|
|
11
|
+
> **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.
|
|
27
12
|
|
|
28
|
-
|
|
13
|
+
## Installation
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install --save-dev @doccident/doccident
|
|
31
17
|
```
|
|
32
18
|
|
|
33
|
-
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Run `doccident` in your project root. By default, it recursively checks all `.md` and `.markdown` files (excluding `node_modules`).
|
|
34
22
|
|
|
35
23
|
```bash
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
npx doccident
|
|
25
|
+
```
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
evalmachine.<anonymous>:7
|
|
41
|
-
console.log(a + c);
|
|
42
|
-
^
|
|
27
|
+
You can also target specific files or directories:
|
|
43
28
|
|
|
44
|
-
|
|
29
|
+
```bash
|
|
30
|
+
npx doccident docs/**/*.md
|
|
45
31
|
```
|
|
46
32
|
|
|
47
|
-
|
|
33
|
+
### Language Support & Recipes
|
|
48
34
|
|
|
49
|
-
|
|
35
|
+
`doccident` executes code inside `js`, `javascript`, `es6`, `ts`, or `typescript` fenced code blocks. It automatically transforms modern JavaScript and TypeScript using **esbuild** before execution.
|
|
50
36
|
|
|
51
|
-
|
|
37
|
+
#### JavaScript
|
|
52
38
|
|
|
53
|
-
`
|
|
39
|
+
Use `js`, `javascript`, or `es6` for JavaScript examples.
|
|
54
40
|
|
|
55
|
-
|
|
56
|
-
---
|
|
41
|
+
**Recipe:**
|
|
57
42
|
|
|
58
|
-
|
|
43
|
+
1. Use `js` fenced code blocks.
|
|
44
|
+
2. Write standard JavaScript (ES6+ supported).
|
|
45
|
+
3. Use `require` to load dependencies defined in your configuration.
|
|
59
46
|
|
|
60
47
|
```js
|
|
61
|
-
|
|
48
|
+
const { sum } = require('./math-utils');
|
|
49
|
+
const result = sum(1, 2);
|
|
62
50
|
```
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
#### TypeScript
|
|
53
|
+
|
|
54
|
+
Use `ts` or `typescript` for TypeScript examples.
|
|
55
|
+
|
|
56
|
+
**Recipe:**
|
|
57
|
+
|
|
58
|
+
1. Use `ts` fenced code blocks.
|
|
59
|
+
2. Include type annotations to demonstrate correct usage.
|
|
60
|
+
3. `doccident` strips types during execution, so your examples serve as both documentation and functional tests.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
interface User {
|
|
64
|
+
id: number;
|
|
65
|
+
name: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const user: User = { id: 1, name: 'Doccident' };
|
|
69
|
+
console.log(user.name);
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
### Skipping Examples
|
|
73
|
+
|
|
74
|
+
To skip a specific code block, add the `<!-- skip-example -->` comment immediately before it:
|
|
72
75
|
|
|
73
76
|
<!-- skip-example -->
|
|
74
77
|
```js
|
|
75
|
-
// not
|
|
76
|
-
|
|
77
|
-
var foo = download(...);
|
|
78
|
+
// This code will not be executed
|
|
79
|
+
fetch('https://example.com');
|
|
78
80
|
```
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Create a `.doccident-setup.js` file in your project root to configure the test environment.
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
### Injecting Dependencies (`require`)
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
If your examples use external libraries, provide them here. This allows your examples to `require` modules just like users would:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
87
91
|
// .doccident-setup.js
|
|
88
92
|
module.exports = {
|
|
89
93
|
require: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
globals: {
|
|
94
|
-
$: require('jquery')
|
|
94
|
+
// Make 'my-library' available when examples call require('my-library')
|
|
95
|
+
'my-library': require('./index.js'),
|
|
96
|
+
'lodash': require('lodash')
|
|
95
97
|
}
|
|
96
|
-
}
|
|
98
|
+
};
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
You must explicitly configure all of the dependencies used in your examples.
|
|
101
|
+
### Global Variables
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
Define global variables available to all snippets:
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<!-- skip-example -->
|
|
107
|
-
```js
|
|
108
|
-
// .doccident-setup.js
|
|
105
|
+
```javascript
|
|
109
106
|
module.exports = {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
regexRequire: {
|
|
115
|
-
'rx/(.*)': function (fullPath, matchedModuleName) {
|
|
116
|
-
return require('./dist/' + matchedModuleName);
|
|
117
|
-
}
|
|
107
|
+
globals: {
|
|
108
|
+
$: require('jquery'),
|
|
109
|
+
window: {}
|
|
118
110
|
}
|
|
119
|
-
}
|
|
111
|
+
};
|
|
120
112
|
```
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
---
|
|
114
|
+
### Advanced Configuration
|
|
124
115
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
* **`regexRequire`**: Handle dynamic requires that match a pattern.
|
|
117
|
+
* **`beforeEach`**: Function to run before each snippet (e.g., to reset global state).
|
|
118
|
+
* **`transformCode`**: Pre-process code before execution (e.g., to strip out display-only syntax).
|
|
128
119
|
|
|
129
|
-
|
|
130
|
-
```js
|
|
131
|
-
//.doccident-setup.js
|
|
132
|
-
module.exports = {
|
|
133
|
-
babel: false
|
|
134
|
-
}
|
|
135
|
-
```
|
|
120
|
+
## Architecture and Approach
|
|
136
121
|
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
<!-- skip-example -->
|
|
140
|
-
```js
|
|
141
|
-
//.doccident-setup.js
|
|
142
|
-
module.exports = {
|
|
143
|
-
beforeEach: function () {
|
|
144
|
-
// reset your awesome global state
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
```
|
|
122
|
+
`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.
|
|
148
123
|
|
|
149
|
-
|
|
124
|
+
### Core Modules
|
|
150
125
|
|
|
151
|
-
|
|
152
|
-
|
|
126
|
+
1. **Parser (`src/parse-code-snippets-from-markdown.ts`)**
|
|
127
|
+
* Reads Markdown content line-by-line.
|
|
128
|
+
* Uses a robust state machine to identify code fences and control comments (like `skip-example`).
|
|
129
|
+
* Extracts valid snippets into structured objects containing code, file paths, and line numbers.
|
|
130
|
+
* **TypeScript Support**: Now recognizes `ts` and `typescript` blocks in addition to JS.
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
module.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
132
|
+
2. **Test Runner (`src/doctest.ts`)**
|
|
133
|
+
* The orchestrator of the application.
|
|
134
|
+
* Iterates through parsed snippets and manages the execution lifecycle.
|
|
135
|
+
* **Sandboxing**: Uses Node.js's `vm` module (`runInNewContext`) to execute code in isolation. This prevents examples from polluting the global scope of the runner itself, while allowing controlled injection of dependencies via the configuration.
|
|
136
|
+
* **Transformation**: Uses **esbuild** to compile modern JavaScript and TypeScript code down to a compatible format before execution. This ensures that modern syntax and type annotations run correctly in all environments, and is significantly faster than the previous Babel implementation.
|
|
137
|
+
|
|
138
|
+
3. **Reporter (`src/reporter.ts`)**
|
|
139
|
+
* Collects execution results (pass, fail, skip).
|
|
140
|
+
* Formats output using `chalk` for readability.
|
|
141
|
+
* **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.
|
|
142
|
+
|
|
143
|
+
4. **Types & Utils**
|
|
144
|
+
* Shared interfaces (`src/types.ts`) ensure type safety across the application.
|
|
145
|
+
* Utility functions (`src/utils.ts`) provide common helpers.
|
|
164
146
|
|
|
165
|
-
|
|
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)
|
|
147
|
+
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
|
|
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 (
|
|
41
|
-
const configPath = path.resolve(
|
|
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 (
|
|
57
|
+
if (options.testOutput) {
|
|
55
58
|
config.testOutput = true;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
// Resolve files
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
70
|
+
// Run tests
|
|
71
|
+
const results = doctest.runTests(files, config);
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
console.log('\n');
|
|
74
|
+
doctest.printResults(results);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
})();
|
package/dist/doctest.js
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
3
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printResults = void 0;
|
|
4
7
|
exports.runTests = runTests;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
var parse_code_snippets_from_markdown_1 = require("./parse-code-snippets-from-markdown");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const vm_1 = require("vm");
|
|
10
|
+
const esbuild_1 = require("esbuild");
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
const parse_code_snippets_from_markdown_1 = __importDefault(require("./parse-code-snippets-from-markdown"));
|
|
14
|
+
const reporter_1 = require("./reporter");
|
|
15
|
+
Object.defineProperty(exports, "printResults", { enumerable: true, get: function () { return reporter_1.printResults; } });
|
|
15
16
|
function runTests(files, config) {
|
|
16
|
-
|
|
17
|
+
const results = files
|
|
17
18
|
.map(read)
|
|
18
19
|
.map(parse_code_snippets_from_markdown_1.default)
|
|
19
20
|
.map(testFile(config));
|
|
20
|
-
return flatten(results);
|
|
21
|
+
return (0, utils_1.flatten)(results);
|
|
21
22
|
}
|
|
22
23
|
function read(fileName) {
|
|
23
|
-
return { contents: (0, fs_1.readFileSync)(fileName, "utf8"), fileName
|
|
24
|
+
return { contents: (0, fs_1.readFileSync)(fileName, "utf8"), fileName };
|
|
24
25
|
}
|
|
25
26
|
function makeTestSandbox(config) {
|
|
26
27
|
function sandboxRequire(moduleName) {
|
|
27
|
-
for (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
for (const regexRequire in config.regexRequire) {
|
|
29
|
+
const regex = new RegExp(regexRequire);
|
|
30
|
+
const match = regex.exec(moduleName);
|
|
31
|
+
const handler = config.regexRequire[regexRequire];
|
|
31
32
|
if (match) {
|
|
32
|
-
return handler
|
|
33
|
+
return handler(...match);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
if (config.require[moduleName] === undefined) {
|
|
36
|
+
if (config.require === undefined || config.require[moduleName] === undefined) {
|
|
36
37
|
throw moduleNotFoundError(moduleName);
|
|
37
38
|
}
|
|
38
39
|
return config.require[moduleName];
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
log:
|
|
41
|
+
const sandboxConsole = {
|
|
42
|
+
log: () => null,
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const sandboxGlobals = { require: sandboxRequire, console: config.testOutput ? console : sandboxConsole };
|
|
45
|
+
const sandbox = Object.assign({}, sandboxGlobals, config.globals);
|
|
45
46
|
return sandbox;
|
|
46
47
|
}
|
|
47
48
|
function testFile(config) {
|
|
48
49
|
return function testFileWithConfig(args) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const codeSnippets = args.codeSnippets;
|
|
51
|
+
const fileName = args.fileName;
|
|
52
|
+
const shareCodeInFile = args.shareCodeInFile;
|
|
53
|
+
let results;
|
|
53
54
|
if (shareCodeInFile) {
|
|
54
|
-
|
|
55
|
+
const sandbox = makeTestSandbox(config);
|
|
55
56
|
results = codeSnippets.map(test(config, fileName, sandbox));
|
|
56
57
|
}
|
|
57
58
|
else {
|
|
@@ -60,99 +61,58 @@ function testFile(config) {
|
|
|
60
61
|
return results;
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
|
-
function test(config,
|
|
64
|
-
return
|
|
64
|
+
function test(config, _filename, sandbox) {
|
|
65
|
+
return (codeSnippet) => {
|
|
65
66
|
if (codeSnippet.skip) {
|
|
66
|
-
return { status: "skip", codeSnippet
|
|
67
|
+
return { status: "skip", codeSnippet, stack: "" };
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
let success = false;
|
|
70
|
+
let stack = "";
|
|
71
|
+
let code = codeSnippet.code;
|
|
71
72
|
if (config.transformCode) {
|
|
72
73
|
try {
|
|
73
74
|
code = config.transformCode(code);
|
|
74
75
|
}
|
|
75
76
|
catch (e) {
|
|
76
|
-
return { status: "fail", codeSnippet
|
|
77
|
+
return { status: "fail", codeSnippet, stack: "Encountered an error while transforming snippet: \n" + e.stack };
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
let perSnippetSandbox;
|
|
80
81
|
if (sandbox === undefined) {
|
|
81
82
|
perSnippetSandbox = makeTestSandbox(config);
|
|
82
83
|
}
|
|
83
84
|
if (config.beforeEach) {
|
|
84
85
|
config.beforeEach();
|
|
85
86
|
}
|
|
86
|
-
var options = {
|
|
87
|
-
presets: [preset_env_1.default],
|
|
88
|
-
};
|
|
89
87
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
const result = (0, esbuild_1.transformSync)(code, {
|
|
89
|
+
loader: 'ts',
|
|
90
|
+
format: 'cjs',
|
|
91
|
+
target: 'node12'
|
|
92
|
+
});
|
|
93
|
+
code = result.code || "";
|
|
93
94
|
(0, vm_1.runInNewContext)(code, perSnippetSandbox || sandbox);
|
|
94
95
|
success = true;
|
|
95
96
|
}
|
|
96
97
|
catch (e) {
|
|
97
98
|
stack = e.stack || "";
|
|
98
99
|
}
|
|
99
|
-
|
|
100
|
+
const status = success ? "pass" : "fail";
|
|
100
101
|
process.stdout.write(success ? chalk_1.default.green(".") : chalk_1.default.red("x"));
|
|
101
|
-
return { status
|
|
102
|
+
return { status, codeSnippet, stack };
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
|
-
function printResults(results) {
|
|
105
|
-
results.filter(function (result) { return result.status === "fail"; }).forEach(printFailure);
|
|
106
|
-
var passingCount = results.filter(function (result) { return result.status === "pass"; })
|
|
107
|
-
.length;
|
|
108
|
-
var failingCount = results.filter(function (result) { return result.status === "fail"; })
|
|
109
|
-
.length;
|
|
110
|
-
var skippingCount = results.filter(function (result) { return result.status === "skip"; })
|
|
111
|
-
.length;
|
|
112
|
-
function successfulRun() {
|
|
113
|
-
return failingCount === 0;
|
|
114
|
-
}
|
|
115
|
-
console.log(chalk_1.default.green("Passed: " + passingCount));
|
|
116
|
-
if (skippingCount > 0) {
|
|
117
|
-
console.log(chalk_1.default.yellow("Skipped: " + skippingCount));
|
|
118
|
-
}
|
|
119
|
-
if (successfulRun()) {
|
|
120
|
-
console.log(chalk_1.default.green("\nSuccess!"));
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
console.log(chalk_1.default.red("Failed: " + failingCount));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function printFailure(result) {
|
|
127
|
-
console.log(chalk_1.default.red("Failed - ".concat(markDownErrorLocation(result))));
|
|
128
|
-
var stackDetails = relevantStackDetails(result.stack);
|
|
129
|
-
console.log(stackDetails);
|
|
130
|
-
var variableNotDefined = stackDetails.match(/(\w+) is not defined/);
|
|
131
|
-
if (variableNotDefined) {
|
|
132
|
-
var variableName = variableNotDefined[1];
|
|
133
|
-
console.log("You can declare ".concat(chalk_1.default.blue(variableName), " in the ").concat(chalk_1.default.blue("globals"), " section in ").concat(chalk_1.default.grey(".doccident-setup.js")));
|
|
134
|
-
console.log("\nFor example:\n".concat(chalk_1.default.grey("// .doccident-setup.js"), "\nmodule.exports = {\n globals: {\n ").concat(chalk_1.default.blue(variableName), ": ...\n }\n}\n "));
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function relevantStackDetails(stack) {
|
|
138
|
-
var match = stack.match(/([\w\W]*?)at eval/) ||
|
|
139
|
-
// eslint-disable-next-line no-useless-escape
|
|
140
|
-
stack.match(/([\w\W]*)at [\w*\/]*?doctest.js/);
|
|
141
|
-
if (match !== null) {
|
|
142
|
-
return match[1];
|
|
143
|
-
}
|
|
144
|
-
return stack;
|
|
145
|
-
}
|
|
146
105
|
function moduleNotFoundError(moduleName) {
|
|
147
|
-
return new Error(
|
|
106
|
+
return new Error(`
|
|
107
|
+
Attempted to require '${chalk_1.default.blue(moduleName)}' but was not found in config.
|
|
108
|
+
You need to include it in the require section of your ${chalk_1.default.grey(".doccident-setup.js")} file.
|
|
109
|
+
|
|
110
|
+
For example:
|
|
111
|
+
${chalk_1.default.grey("// .doccident-setup.js")}
|
|
112
|
+
module.exports = {
|
|
113
|
+
require: {
|
|
114
|
+
${chalk_1.default.blue(`'${moduleName}': require('${moduleName}')`)}
|
|
115
|
+
}
|
|
148
116
|
}
|
|
149
|
-
|
|
150
|
-
var match = result.stack.match(/eval.*<.*>:(\d+):(\d+)/);
|
|
151
|
-
if (match) {
|
|
152
|
-
var mdLineNumber = parseInt(match[1], 10);
|
|
153
|
-
var columnNumber = parseInt(match[2], 10);
|
|
154
|
-
var lineNumber = result.codeSnippet.lineNumber + mdLineNumber;
|
|
155
|
-
return "".concat(result.codeSnippet.fileName, ":").concat(lineNumber, ":").concat(columnNumber);
|
|
156
|
-
}
|
|
157
|
-
return "".concat(result.codeSnippet.fileName, ":").concat(result.codeSnippet.lineNumber);
|
|
117
|
+
`);
|
|
158
118
|
}
|
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var isSkip = function (line) { return line.trim() === "<!-- skip-example -->"; };
|
|
8
|
-
var isCodeSharedInFile = function (line) {
|
|
9
|
-
return line.trim() === "<!-- share-code-between-examples -->";
|
|
10
|
-
};
|
|
3
|
+
const isStartOfSnippet = (line) => line.trim().match(/```\W*(JavaScript|js|es6|ts|typescript)\s?$/i);
|
|
4
|
+
const isEndOfSnippet = (line) => line.trim() === "```";
|
|
5
|
+
const isSkip = (line) => line.trim() === "<!-- skip-example -->";
|
|
6
|
+
const isCodeSharedInFile = (line) => line.trim() === "<!-- share-code-between-examples -->";
|
|
11
7
|
function startNewSnippet(snippets, fileName, lineNumber) {
|
|
12
|
-
|
|
8
|
+
const skip = snippets.skip;
|
|
13
9
|
snippets.skip = false;
|
|
14
10
|
return Object.assign(snippets, {
|
|
15
11
|
snippets: snippets.snippets.concat([
|
|
16
|
-
{ code: "", fileName
|
|
12
|
+
{ code: "", fileName, lineNumber, complete: false, skip: skip ?? false }
|
|
17
13
|
])
|
|
18
14
|
});
|
|
19
15
|
}
|
|
20
16
|
function addLineToLastSnippet(line) {
|
|
21
17
|
return function addLine(snippets) {
|
|
22
|
-
|
|
18
|
+
const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
|
|
23
19
|
if (lastSnippet && !lastSnippet.complete) {
|
|
24
20
|
lastSnippet.code += line + "\n";
|
|
25
21
|
}
|
|
26
22
|
return snippets;
|
|
27
23
|
};
|
|
28
24
|
}
|
|
29
|
-
function endSnippet(snippets,
|
|
30
|
-
|
|
31
|
-
console.log("endSnippet", snippets, fileName, lineNumber);
|
|
32
|
-
var lastSnippet = snippets.snippets[snippets.snippets.length - 1];
|
|
25
|
+
function endSnippet(snippets, _fileName, _lineNumber) {
|
|
26
|
+
const lastSnippet = snippets.snippets[snippets.snippets.length - 1];
|
|
33
27
|
if (lastSnippet) {
|
|
34
28
|
lastSnippet.complete = true;
|
|
35
29
|
}
|
|
@@ -59,27 +53,25 @@ function parseLine(line) {
|
|
|
59
53
|
return addLineToLastSnippet(line);
|
|
60
54
|
}
|
|
61
55
|
function parseCodeSnippets(args) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
const contents = args.contents;
|
|
57
|
+
const fileName = args.fileName;
|
|
58
|
+
const initialState = {
|
|
65
59
|
snippets: [],
|
|
66
60
|
shareCodeInFile: false,
|
|
67
61
|
complete: false
|
|
68
62
|
};
|
|
69
|
-
|
|
63
|
+
const results = contents
|
|
70
64
|
.split("\n")
|
|
71
65
|
.map(parseLine)
|
|
72
|
-
.reduce(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
var codeSnippets = results.snippets;
|
|
76
|
-
var lastSnippet = codeSnippets[codeSnippets.length - 1];
|
|
66
|
+
.reduce((snippets, lineAction, index) => lineAction(snippets, fileName, index + 1), initialState);
|
|
67
|
+
const codeSnippets = results.snippets;
|
|
68
|
+
const lastSnippet = codeSnippets[codeSnippets.length - 1];
|
|
77
69
|
if (lastSnippet && !lastSnippet.complete) {
|
|
78
70
|
throw new Error("Snippet parsing was incomplete");
|
|
79
71
|
}
|
|
80
72
|
return {
|
|
81
|
-
fileName
|
|
82
|
-
codeSnippets
|
|
73
|
+
fileName,
|
|
74
|
+
codeSnippets,
|
|
83
75
|
shareCodeInFile: results.shareCodeInFile
|
|
84
76
|
};
|
|
85
77
|
}
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printResults = printResults;
|
|
7
|
+
/* eslint-disable no-console */
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
function printResults(results) {
|
|
10
|
+
results.filter((result) => result.status === "fail").forEach(printFailure);
|
|
11
|
+
const passingCount = results.filter((result) => result.status === "pass")
|
|
12
|
+
.length;
|
|
13
|
+
const failingCount = results.filter((result) => result.status === "fail")
|
|
14
|
+
.length;
|
|
15
|
+
const skippingCount = results.filter((result) => result.status === "skip")
|
|
16
|
+
.length;
|
|
17
|
+
function successfulRun() {
|
|
18
|
+
return failingCount === 0;
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk_1.default.green("Passed: " + passingCount));
|
|
21
|
+
if (skippingCount > 0) {
|
|
22
|
+
console.log(chalk_1.default.yellow("Skipped: " + skippingCount));
|
|
23
|
+
}
|
|
24
|
+
if (successfulRun()) {
|
|
25
|
+
console.log(chalk_1.default.green("\nSuccess!"));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(chalk_1.default.red("Failed: " + failingCount));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function printFailure(result) {
|
|
32
|
+
console.log(chalk_1.default.red(`Failed - ${markDownErrorLocation(result)}`));
|
|
33
|
+
const stackDetails = relevantStackDetails(result.stack);
|
|
34
|
+
console.log(stackDetails);
|
|
35
|
+
const variableNotDefined = stackDetails.match(/(\w{1,256}) is not defined/);
|
|
36
|
+
if (variableNotDefined) {
|
|
37
|
+
const variableName = variableNotDefined[1];
|
|
38
|
+
console.log(`You can declare ${chalk_1.default.blue(variableName)} in the ${chalk_1.default.blue("globals")} section in ${chalk_1.default.grey(".doccident-setup.js")}`);
|
|
39
|
+
console.log(`
|
|
40
|
+
For example:
|
|
41
|
+
${chalk_1.default.grey("// .doccident-setup.js")}
|
|
42
|
+
module.exports = {
|
|
43
|
+
globals: {
|
|
44
|
+
${chalk_1.default.blue(variableName)}: ...
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function relevantStackDetails(stack) {
|
|
51
|
+
const evalIndex = stack.indexOf("at eval");
|
|
52
|
+
if (evalIndex !== -1) {
|
|
53
|
+
return stack.substring(0, evalIndex);
|
|
54
|
+
}
|
|
55
|
+
const lines = stack.split("\n");
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
const line = lines[i];
|
|
58
|
+
if (line.includes("doctest.js") && line.trim().startsWith("at ")) {
|
|
59
|
+
return lines.slice(0, i).join("\n") + "\n";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return stack;
|
|
63
|
+
}
|
|
64
|
+
function markDownErrorLocation(result) {
|
|
65
|
+
const lines = result.stack.split("\n");
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (line.includes("eval")) {
|
|
68
|
+
const match = line.match(/<([^><]+)>:(\d+):(\d+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const mdLineNumber = parseInt(match[2], 10);
|
|
71
|
+
const columnNumber = parseInt(match[3], 10);
|
|
72
|
+
const lineNumber = result.codeSnippet.lineNumber + mdLineNumber;
|
|
73
|
+
return `${result.codeSnippet.fileName}:${lineNumber}:${columnNumber}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return `${result.codeSnippet.fileName}:${result.codeSnippet.lineNumber}`;
|
|
78
|
+
}
|
package/dist/types.js
ADDED
package/dist/utils.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccident/doccident",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Test all the code in your markdown docs!",
|
|
5
5
|
"main": "dist/doctest.js",
|
|
6
6
|
"files": [
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
"bin": {
|
|
11
11
|
"doccident": "bin/cmd.js"
|
|
12
12
|
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"precommit": "npm run lint && npm run test",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
20
|
+
},
|
|
13
21
|
"repository": {
|
|
14
22
|
"type": "git",
|
|
15
23
|
"url": "https://github.com/BillaudCipher/doccident"
|
|
@@ -29,36 +37,22 @@
|
|
|
29
37
|
},
|
|
30
38
|
"homepage": "https://github.com/BillaudCipher/doccident",
|
|
31
39
|
"devDependencies": {
|
|
32
|
-
"@babel/preset-typescript": "^7.27.1",
|
|
33
40
|
"@eslint/eslintrc": "^3.3.1",
|
|
34
41
|
"@eslint/js": "^9.23.0",
|
|
35
42
|
"@types/node": "^22.15.17",
|
|
36
43
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
|
37
44
|
"@typescript-eslint/parser": "^8.32.0",
|
|
45
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
38
46
|
"eslint": "^9.24.0",
|
|
39
47
|
"eslint-plugin-import": "^2.31.0",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
48
|
+
"typescript": "^5.8.3",
|
|
49
|
+
"vitest": "^4.0.16"
|
|
42
50
|
},
|
|
43
51
|
"dependencies": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"glob": "^7.2.3",
|
|
52
|
+
"chalk": "^4.1.2",
|
|
53
|
+
"commander": "^14.0.2",
|
|
54
|
+
"esbuild": "^0.27.2",
|
|
55
|
+
"fast-glob": "^3.3.3",
|
|
49
56
|
"globals": "^16.1.0"
|
|
50
|
-
},
|
|
51
|
-
"babel": {
|
|
52
|
-
"presets": [
|
|
53
|
-
"@babel/preset-env",
|
|
54
|
-
"@babel/preset-typescript"
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "tsc",
|
|
59
|
-
"lint": "eslint . --ext .ts",
|
|
60
|
-
"test": "jest",
|
|
61
|
-
"prepublish": "pnpm run build",
|
|
62
|
-
"clean": "rm -rf dist"
|
|
63
57
|
}
|
|
64
|
-
}
|
|
58
|
+
}
|