readme-assert 6.0.3 → 7.0.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/package.json CHANGED
@@ -1,54 +1,34 @@
1
1
  {
2
2
  "name": "readme-assert",
3
- "version": "6.0.3",
4
- "engines": {
5
- "node": ">=4"
6
- },
7
- "description": "Run code blocks in your readme as test",
8
- "main": "lib/index.js",
9
- "jsnext:main": "src/index.js",
10
- "bin": "cli.js",
11
- "scripts": {
12
- "build": "babel src/. -d lib/. --ignore=spec.js",
13
- "test:readme": "babel-node src/cli.js -m ./src",
14
- "test": "npm-run-all test:*",
15
- "prettier": "prettier 'src/**/*'",
16
- "prepublish": "npm run build"
3
+ "version": "7.0.0",
4
+ "description": "Run code blocks in your readme as tests",
5
+ "type": "module",
6
+ "exports": "./src/index.js",
7
+ "bin": {
8
+ "readme-assert": "./src/cli.js"
17
9
  },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "license": "MIT",
18
14
  "author": {
19
15
  "name": "Sigurd Fosseng",
20
16
  "email": "sigurd@fosseng.net",
21
17
  "url": "http://laat.io"
22
18
  },
23
- "license": "MIT",
24
19
  "repository": {
25
20
  "type": "git",
26
21
  "url": "https://github.com/laat/readme-assert.git"
27
22
  },
28
- "dependencies": {
29
- "@babel/core": "^7.0.0",
30
- "@babel/plugin-syntax-typescript": "^7.3.3",
31
- "@babel/plugin-transform-typescript": "^7.4.5",
32
- "@babel/preset-env": "^7.0.0",
33
- "babel-plugin-transform-comment-to-assert": "^4.1.0",
34
- "babel-plugin-transform-rename-import": "^2.0.0",
35
- "gfm-code-blocks": "^1.0.0",
36
- "pkg-up": "^3.1.0",
37
- "source-map-support": "^0.5.12",
38
- "stack-utils": "^1.0.2",
39
- "tap-yaml": "^1.0.0",
40
- "yargs": "^13.2.4"
23
+ "engines": {
24
+ "node": ">=20.19.0 || >=22.12.0"
41
25
  },
42
- "devDependencies": {
43
- "@babel/cli": "7.4.4",
44
- "@babel/node": "7.4.5",
45
- "@babel/register": "7.4.4",
46
- "npm-run-all": "4.1.5",
47
- "prettier": "1.18.2",
48
- "run-tests": "1.0.4"
26
+ "dependencies": {
27
+ "esbuild": "^0.27.0",
28
+ "magic-string": "^0.30.0",
29
+ "oxc-parser": "^0.123.0"
49
30
  },
50
- "files": [
51
- "lib",
52
- "cli.js"
53
- ]
54
- }
31
+ "scripts": {
32
+ "test": "node --test test/*.test.js && pnpm -r test"
33
+ }
34
+ }
package/readme.md CHANGED
@@ -1,37 +1,42 @@
1
- # readme-assert [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url]
1
+ # readme-assert [![npm][npm-image]][npm-url]
2
2
 
3
- [travis-image]: https://travis-ci.org/laat/readme-assert.svg?branch=master
4
- [travis-url]: https://travis-ci.org/laat/readme-assert
5
3
  [npm-image]: https://img.shields.io/npm/v/readme-assert.svg?style=flat
6
4
  [npm-url]: https://npmjs.org/package/readme-assert
7
5
 
8
- > Run code blocks in your readme as test
6
+ `README.md` files often become outdated because the code examples are
7
+ not regularly tested. readme-assert extracts fenced code blocks from
8
+ your readme and runs them as tests, using special comments as assertions.
9
9
 
10
- `README.md` files often become outdated over time because the code
11
- examples are not regulary tested. By commenting `javascript`
12
- codeblocks the `README.md` file with special comments we can create
13
- simple tests that ensures that the readme is still correct.
10
+ ## Install
11
+
12
+ ```
13
+ npm install readme-assert
14
+ ```
14
15
 
15
16
  ## Usage
16
17
 
17
18
  ```
18
- Run readme as test
19
-
20
19
  Usage: readme-assert [options]
21
20
 
22
21
  Options:
23
- --auto, -a Auto discover test code block [boolean]
24
- --all, -l Run all supported code blocks [boolean]
25
- --babel Use babelrc when transpiling [boolean] [default: false]
26
22
  --file, -f readme.md file to read
27
- --main, -m Points to the entry point of the module [string]
28
- --print-code, -p Print the transformed code [boolean]
29
- --require, -r Require a given module [array]
30
- --version Show version number [boolean]
31
- -h, --help Show help [boolean]
23
+ --main, -m Entry point of the module
24
+ --auto, -a Auto discover test code blocks
25
+ --all, -l Run all supported code blocks
26
+ --print-code, -p Print the transformed code
27
+ --version, -v Show version number
28
+ -h, --help Show help
32
29
  ```
33
30
 
34
- Write a test in the readme with the special code-block tag `test`
31
+ Run in the same folder as your readme:
32
+
33
+ ```
34
+ $ readme-assert
35
+ ```
36
+
37
+ ## Writing Tests
38
+
39
+ Tag your fenced code blocks with `test` or `should`:
35
40
 
36
41
  ````
37
42
  ```javascript test
@@ -39,88 +44,98 @@ Write a test in the readme with the special code-block tag `test`
39
44
  ```
40
45
  ````
41
46
 
42
- Run the test in the same folder as your readme:
47
+ ### Assertion Comments
43
48
 
44
- ```
45
- $ readme-assert
49
+ Use `//=>` to assert the value of an expression:
50
+
51
+ ```javascript test
52
+ let a = 1;
53
+ a; //=> 1
46
54
  ```
47
55
 
48
- output:
56
+ The `// →` (unicode arrow) and `// ->` (ascii arrow) variants also work:
49
57
 
50
- ```
51
- TAP version 13
52
- ok 1
53
- # tests 1
54
- # pass 1
55
- # fail 0
58
+ ```javascript test
59
+ let b = 1;
60
+ b; // → 1
56
61
  ```
57
62
 
58
- Printing the evaluated code, can be useful when debugging:
63
+ ### throws
59
64
 
60
- ```
61
- $ readme-assert --print-code
65
+ Assert that an expression throws using `// throws` with a regex pattern:
66
+
67
+ ```javascript test
68
+ const b = () => {
69
+ throw new Error("fail");
70
+ };
71
+ b(); // throws /fail/
62
72
  ```
63
73
 
64
- output:
74
+ ### console.log
65
75
 
66
- ```
67
- # /path/to/module/readme.md.js
68
- # 1 "use strict";
69
- # 2
70
- # 3 assert.deepEqual(1 + 1, 2);
71
- TAP version 13
72
- ok 1
73
- 1..1
74
- # tests 1
75
- # pass 1
76
- # fail 0
76
+ Assert console output — the call is preserved and an assertion is added:
77
+
78
+ ```javascript test
79
+ let a = { a: 1 };
80
+ console.log(a); //=> { a: 1 }
77
81
  ```
78
82
 
79
- ## Examples
83
+ ### Promises
80
84
 
81
- To see some examples of use see the [examples folder](https://github.com/laat/readme-assert/tree/master/examples) in the repository
85
+ Assert that a promise resolves to a value with `//=> resolves to`:
82
86
 
83
- ## Sample tests
87
+ ```javascript test
88
+ Promise.resolve(true) //=> resolves to true
89
+ ```
84
90
 
85
- ### simple
91
+ Assert that a promise rejects with `// rejects`:
86
92
 
87
- ```javascript should equal 1
88
- let a = 1;
89
- a; //=> 1
93
+ ```javascript test
94
+ Promise.reject(new Error("no")) // rejects /no/
90
95
  ```
91
96
 
92
- ### utf-8 arrow
97
+ ### TypeScript
93
98
 
94
- ```javascript test utf8 arrow
95
- a; // → 1
99
+ TypeScript code blocks are supported natively:
100
+
101
+ ```typescript should add two numbers
102
+ const sum: number = 1 + 1;
103
+ sum; //=> 2
96
104
  ```
97
105
 
98
- ### console.log
106
+ ### Grouping blocks
99
107
 
100
- ```javascript test console.log
101
- a = { a: 1 };
102
- console.log(a); //=> { a: 1 }
103
- ```
108
+ Each code block runs as its own file. To share variables across
109
+ blocks, give them the same group name with `test:groupname`:
104
110
 
105
- ### throws
111
+ ````
112
+ ```javascript test:math
113
+ let x = 2;
114
+ ```
106
115
 
107
- ```javascript test throws
108
- const b = () => {
109
- throw new Error("fail");
110
- };
111
- b(); // throws /fail/
116
+ ```javascript test:math
117
+ x; //=> 2
112
118
  ```
119
+ ````
113
120
 
114
- ### TypesScript
121
+ ### Auto-discover mode
115
122
 
116
- ```typescript should add two numbers with typescript
117
- const sum: number = 1 + 1;
118
- sum; //=> 2
119
- ```
123
+ With `--auto`, any code block containing `//=>`, `// →`, or `// throws`
124
+ is treated as a test no `test` tag needed.
125
+
126
+ ### All mode
127
+
128
+ With `--all`, every JavaScript and TypeScript code block is executed,
129
+ regardless of tags.
130
+
131
+ ## How It Works
132
+
133
+ 1. Each fenced code block is extracted from the markdown
134
+ 2. Blocks with the same `test:group` name are merged; others run independently
135
+ 3. Assertion comments (`//=> value`) are transformed into `assert.deepEqual()` calls using [oxc-parser](https://oxc.rs) and [magic-string](https://github.com/rich-harris/magic-string)
136
+ 4. Imports of your package name are rewritten to point to your local source
137
+ 5. Each block is written to a temp file and executed with `node`
120
138
 
121
- ## Projects using readme-assert
139
+ ## License
122
140
 
123
- - [fen-chess-board](https://github.com/laat/fen-chess-board)
124
- - [babel-plugin-transform-comment-to-assert](https://github.com/laat/babel-plugin-transform-comment-to-assert)
125
- - [babel-plugin-transform-rename-import](https://github.com/laat/babel-plugin-transform-rename-import)
126
- - [escape-invisibles](https://github.com/laat/escape-invisibles)
141
+ MIT
package/src/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+
6
+ const { values: args } = parseArgs({
7
+ options: {
8
+ file: { type: "string", short: "f" },
9
+ main: { type: "string", short: "m" },
10
+ auto: { type: "boolean", short: "a", default: false },
11
+ all: { type: "boolean", short: "l", default: false },
12
+ require: { type: "string", short: "r", multiple: true },
13
+ import: { type: "string", short: "i", multiple: true },
14
+ "print-code": { type: "boolean", short: "p", default: false },
15
+ help: { type: "boolean", short: "h", default: false },
16
+ version: { type: "boolean", short: "v", default: false },
17
+ },
18
+ strict: false,
19
+ });
20
+
21
+ if (args.help) {
22
+ console.log(`
23
+ Run code blocks in your readme as tests
24
+
25
+ Usage: readme-assert [options]
26
+
27
+ Options:
28
+ --file, -f readme.md file to read
29
+ --main, -m Entry point of the module
30
+ --auto, -a Auto discover test code blocks
31
+ --all, -l Run all supported code blocks
32
+ --require, -r Require a module before running [array]
33
+ --import, -i Import a module before running [array]
34
+ --print-code, -p Print the transformed code
35
+ --version, -v Show version number
36
+ -h, --help Show help
37
+ `);
38
+ process.exit(0);
39
+ }
40
+
41
+ if (args.version) {
42
+ const pkgPath = new URL("../package.json", import.meta.url);
43
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
44
+ console.log(pkg.version);
45
+ process.exit(0);
46
+ }
47
+
48
+ function findReadme() {
49
+ for (const name of ["README.md", "readme.md"]) {
50
+ const p = path.resolve(name);
51
+ if (fs.existsSync(p)) return p;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ const filePath = args.file ? path.resolve(args.file) : findReadme();
57
+
58
+ if (!filePath) {
59
+ console.error("Could not locate readme.md");
60
+ process.exit(1);
61
+ }
62
+
63
+ const opts = {
64
+ auto: args.auto,
65
+ all: args.all,
66
+ main: args.main,
67
+ require: args.require,
68
+ import: args.import,
69
+ };
70
+
71
+ if (args["print-code"]) {
72
+ const { processMarkdown } = await import("./run.js");
73
+ const units = await processMarkdown(filePath, opts);
74
+ for (const unit of units) {
75
+ console.log(`# --- ${unit.name} ---`);
76
+ console.log(unit.code);
77
+ }
78
+ } else {
79
+ const { run } = await import("./run.js");
80
+ const { exitCode, stdout, stderr, results } = await run(filePath, opts);
81
+ if (stdout) process.stdout.write(stdout);
82
+ if (stderr) process.stderr.write(stderr);
83
+ if (exitCode === 0) {
84
+ console.log(`All assertions passed. (${results.length} blocks)`);
85
+ }
86
+ process.exitCode = exitCode;
87
+ }
@@ -0,0 +1,139 @@
1
+ import { parseSync } from "oxc-parser";
2
+ import MagicString from "magic-string";
3
+
4
+ /**
5
+ * Transform assertion comments into assert calls.
6
+ *
7
+ * expr //=> value → assert.deepEqual(expr, value)
8
+ * expr // → value → assert.deepEqual(expr, value)
9
+ * expr // throws /pat/ → assert.throws(() => { expr }, /pat/)
10
+ * console.log(x) //=> v → console.log(x); assert.deepEqual(x, v)
11
+ * expr //=> resolves to v → assert.deepEqual(await expr, v)
12
+ * expr // rejects /pat/ → assert.rejects(() => expr, /pat/)
13
+ *
14
+ * Uses oxc-parser for AST + comment extraction. Handles both JS and TS.
15
+ *
16
+ * @param {string} code - JavaScript or TypeScript source
17
+ * @param {{ filename?: string, typescript?: boolean }} options
18
+ * @returns {{ code: string, map: object }}
19
+ */
20
+ export function commentToAssert(code, { filename, typescript = false } = {}) {
21
+ const ext = typescript ? "test.ts" : "test.js";
22
+ const result = parseSync(ext, code);
23
+ const ast = result.program;
24
+ const comments = result.comments;
25
+
26
+ const s = new MagicString(code);
27
+ let changed = false;
28
+
29
+ for (const node of ast.body) {
30
+ if (node.type !== "ExpressionStatement") continue;
31
+
32
+ const comment = findTrailingComment(comments, node, code);
33
+ if (!comment) continue;
34
+
35
+ const match = comment.value.match(/^\s*(=>|→|->)\s*([\s\S]*)$/);
36
+ const throwsMatch = comment.value.match(/^\s*throws\s+([\s\S]*)$/);
37
+ const rejectsMatch = comment.value.match(/^\s*rejects\s+([\s\S]*)$/);
38
+
39
+ if (match) {
40
+ const rest = match[2].trim();
41
+ const resolvesMatch = rest.match(/^resolves\s+(?:to\s+)?([\s\S]*)$/);
42
+ changed = true;
43
+
44
+ if (resolvesMatch) {
45
+ // expr //=> resolves to value → assert.deepEqual(await expr, value)
46
+ const expected = resolvesMatch[1].trim();
47
+ const exprSource = code.slice(
48
+ node.expression.start,
49
+ node.expression.end,
50
+ );
51
+ s.overwrite(
52
+ node.start,
53
+ comment.end,
54
+ `assert.deepEqual(await ${exprSource}, ${expected});`,
55
+ );
56
+ } else if (isConsoleCall(node.expression)) {
57
+ // console.log(expr) //=> value → keep log, add assertion after
58
+ const arg = code.slice(
59
+ node.expression.arguments[0].start,
60
+ node.expression.arguments[0].end,
61
+ );
62
+ s.overwrite(
63
+ node.expression.end,
64
+ comment.end,
65
+ `;\nassert.deepEqual(${arg}, ${rest});`,
66
+ );
67
+ } else {
68
+ // expr //=> value → assert.deepEqual(expr, value)
69
+ const exprSource = code.slice(
70
+ node.expression.start,
71
+ node.expression.end,
72
+ );
73
+ s.overwrite(
74
+ node.start,
75
+ comment.end,
76
+ `assert.deepEqual(${exprSource}, ${rest});`,
77
+ );
78
+ }
79
+ } else if (throwsMatch) {
80
+ const pattern = throwsMatch[1].trim();
81
+ const exprSource = code.slice(
82
+ node.expression.start,
83
+ node.expression.end,
84
+ );
85
+ s.overwrite(
86
+ node.start,
87
+ comment.end,
88
+ `assert.throws(() => { ${exprSource}; }, ${pattern});`,
89
+ );
90
+ changed = true;
91
+ } else if (rejectsMatch) {
92
+ const pattern = rejectsMatch[1].trim();
93
+ const exprSource = code.slice(
94
+ node.expression.start,
95
+ node.expression.end,
96
+ );
97
+ s.overwrite(
98
+ node.start,
99
+ comment.end,
100
+ `await assert.rejects(() => ${exprSource}, ${pattern});`,
101
+ );
102
+ changed = true;
103
+ }
104
+ }
105
+
106
+ if (!changed) return { code, map: null };
107
+
108
+ return {
109
+ code: s.toString(),
110
+ map: s.generateMap({ source: filename, hires: true }),
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Find a trailing line comment for an expression statement.
116
+ * The comment must start after the expression and be on the same line.
117
+ */
118
+ function findTrailingComment(comments, node, code) {
119
+ for (const c of comments) {
120
+ if (c.type !== "Line") continue;
121
+ if (c.start < node.expression.end) continue;
122
+
123
+ // Must be on the same line as the expression (no newline between)
124
+ const between = code.slice(node.expression.end, c.start);
125
+ if (between.includes("\n")) continue;
126
+
127
+ return c;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function isConsoleCall(expr) {
133
+ return (
134
+ expr.type === "CallExpression" &&
135
+ expr.callee.type === "MemberExpression" &&
136
+ expr.callee.object.type === "Identifier" &&
137
+ expr.callee.object.name === "console"
138
+ );
139
+ }
package/src/extract.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Extract tagged code blocks from a markdown string.
3
+ *
4
+ * @param {string} markdown
5
+ * @param {{ auto?: boolean, all?: boolean }} options
6
+ * @returns {{ blocks: Block[], hasTypescript: boolean }}
7
+ *
8
+ * Block: { code, lang, tag, group, startLine, endLine }
9
+ *
10
+ * Tags: "test", "test:groupname", "should description", "should:groupname description"
11
+ * Blocks with the same group name are merged into a single execution unit.
12
+ * Blocks without a group name each run independently.
13
+ */
14
+ export function extractBlocks(markdown, { auto = false, all = false } = {}) {
15
+ // Based on gfm-code-block-regex — the backreference \2 ensures a 4-backtick
16
+ // fence only closes with 4 backticks, so nested display fences are skipped.
17
+ const fenceRe = /^(([ \t]*`{3,4})([^\n]*)([\s\S]+?)(^[ \t]*\2))/gm;
18
+ const supportedLangs = new Set(["javascript", "js", "typescript", "ts"]);
19
+ const tsLangs = new Set(["typescript", "ts"]);
20
+ const assertRe = /\/[/*]\s*(=>|→|throws)/;
21
+
22
+ let hasTypescript = false;
23
+ const blocks = [];
24
+ let match;
25
+
26
+ while ((match = fenceRe.exec(markdown)) !== null) {
27
+ const infoString = match[3].trim();
28
+ const code = match[4].replace(/^\n/, "");
29
+ const blockStart = match.index;
30
+
31
+ // Parse language and tag from info string (e.g. "javascript test" or "ts test:group")
32
+ const parts = infoString.split(/\s+/);
33
+ const lang = parts[0] || "";
34
+ const tag = parts.slice(1).join(" ");
35
+
36
+ if (!supportedLangs.has(lang)) continue;
37
+
38
+ // Filter by mode
39
+ if (!all) {
40
+ if (auto) {
41
+ if (!assertRe.test(code)) continue;
42
+ } else {
43
+ const firstWord = tag.split(/\s+/)[0] || "";
44
+ const keyword = firstWord.split(":")[0];
45
+ if (keyword !== "test" && keyword !== "should") continue;
46
+ }
47
+ }
48
+
49
+ // Count lines before this block to get startLine (1-based)
50
+ const linesBeforeBlock = markdown.slice(0, blockStart).split("\n").length;
51
+ const startLine = linesBeforeBlock + 1; // +1 for the fence line itself
52
+ const codeLines = code.split("\n").length;
53
+ const endLine = startLine + codeLines - 1;
54
+
55
+ if (tsLangs.has(lang)) hasTypescript = true;
56
+
57
+ // Parse group from tag: "test:mygroup ..." or "should:mygroup ..."
58
+ const firstTagWord = tag.split(/\s+/)[0] || "";
59
+ const colonIdx = firstTagWord.indexOf(":");
60
+ const group = colonIdx !== -1 ? firstTagWord.slice(colonIdx + 1) : null;
61
+
62
+ blocks.push({ code, lang, tag, group, startLine, endLine });
63
+ }
64
+
65
+ return { blocks, hasTypescript };
66
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Assemble extracted code blocks into runnable JS modules.
3
+ *
4
+ * Each block becomes its own module unless blocks share a group name,
5
+ * in which case they are merged into a single module.
6
+ *
7
+ * @param {{ blocks: Block[], hasTypescript: boolean }} extracted
8
+ * @returns {{ units: Array<{ code: string, name: string, hasTypescript: boolean }> }}
9
+ */
10
+ export function generate({ blocks }) {
11
+ if (blocks.length === 0) return { units: [] };
12
+
13
+ // Group blocks: blocks with a group name are merged, others are standalone
14
+ const groups = new Map();
15
+ const units = [];
16
+
17
+ for (const block of blocks) {
18
+ if (block.group) {
19
+ if (!groups.has(block.group)) {
20
+ const entry = { blocks: [], name: block.group };
21
+ groups.set(block.group, entry);
22
+ units.push(entry);
23
+ }
24
+ groups.get(block.group).blocks.push(block);
25
+ } else {
26
+ units.push({ blocks: [block], name: block.tag || `line ${block.startLine}` });
27
+ }
28
+ }
29
+
30
+ return {
31
+ units: units.map((unit) => ({
32
+ code: assembleUnit(unit.blocks),
33
+ name: unit.name,
34
+ hasTypescript: unit.blocks.some((b) => b.lang === "typescript" || b.lang === "ts"),
35
+ })),
36
+ };
37
+ }
38
+
39
+ function assembleUnit(blocks) {
40
+ // Find the last line number we need to cover
41
+ const maxLine = Math.max(...blocks.map((b) => b.endLine));
42
+
43
+ // Build a line array filled with empty strings
44
+ const lines = new Array(maxLine).fill("");
45
+
46
+ // Place each block's code at its source position
47
+ for (const block of blocks) {
48
+ const codeLines = block.code.replace(/\n$/, "").split("\n");
49
+ for (let i = 0; i < codeLines.length; i++) {
50
+ lines[block.startLine - 1 + i] = codeLines[i];
51
+ }
52
+ }
53
+
54
+ // Separate import/export lines from body lines
55
+ const imports = [];
56
+ const bodyLines = [];
57
+
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const trimmed = lines[i].trimStart();
60
+ if (isImportOrExport(trimmed)) {
61
+ imports.push(lines[i]);
62
+ bodyLines.push(""); // keep line padding
63
+ } else {
64
+ bodyLines.push(lines[i]);
65
+ }
66
+ }
67
+
68
+ const hasESM = imports.length > 0;
69
+ const hasCJS = /\brequire\s*\(/.test(bodyLines.join("\n"));
70
+
71
+ // Place assert import on line 0 (before markdown line 1) so line numbers
72
+ // in the generated code match the original markdown positions exactly.
73
+ let assertLine;
74
+ if (hasESM) {
75
+ assertLine = 'import assert from "node:assert/strict";';
76
+ } else if (hasCJS) {
77
+ assertLine = 'const assert = require("node:assert/strict");';
78
+ } else {
79
+ assertLine = 'const { default: assert } = await import("node:assert/strict");';
80
+ }
81
+
82
+ // imports go on line 0 too (they're already removed from bodyLines)
83
+ const header = [assertLine, ...imports].join("; ");
84
+ bodyLines[0] = header;
85
+ return bodyLines.join("\n") + "\n";
86
+ }
87
+
88
+ function isImportOrExport(line) {
89
+ return (
90
+ /^import\s/.test(line) ||
91
+ /^import\(/.test(line) ||
92
+ /^export\s/.test(line)
93
+ );
94
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { extractBlocks } from "./extract.js";
2
+ export { commentToAssert } from "./comment-to-assert.js";
3
+ export { generate } from "./generate.js";
4
+ export { processMarkdown, run } from "./run.js";