readme-assert 6.0.2 → 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.2",
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.0.0",
44
- "@babel/node": "^7.0.0",
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,36 +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
- --babel Use babelrc when transpiling [boolean] [default: false]
25
22
  --file, -f readme.md file to read
26
- --main, -m Points to the entry point of the module [string]
27
- --print-code, -p Print the transformed code [boolean]
28
- --require, -r Require a given module [array]
29
- --version Show version number [boolean]
30
- -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
31
29
  ```
32
30
 
33
- 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`:
34
40
 
35
41
  ````
36
42
  ```javascript test
@@ -38,84 +44,98 @@ Write a test in the readme with the special code-block tag `test`
38
44
  ```
39
45
  ````
40
46
 
41
- Run the test in the same folder as your readme:
47
+ ### Assertion Comments
42
48
 
43
- ```
44
- $ readme-assert
49
+ Use `//=>` to assert the value of an expression:
50
+
51
+ ```javascript test
52
+ let a = 1;
53
+ a; //=> 1
45
54
  ```
46
55
 
47
- output:
56
+ The `// →` (unicode arrow) and `// ->` (ascii arrow) variants also work:
48
57
 
49
- ```
50
- TAP version 13
51
- ok 1
52
- # tests 1
53
- # pass 1
54
- # fail 0
58
+ ```javascript test
59
+ let b = 1;
60
+ b; // → 1
55
61
  ```
56
62
 
57
- Printing the evaluated code, can be useful when debugging:
63
+ ### throws
58
64
 
59
- ```
60
- $ 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/
61
72
  ```
62
73
 
63
- output:
74
+ ### console.log
64
75
 
65
- ```
66
- # /path/to/module/readme.md.js
67
- # 1 "use strict";
68
- # 2
69
- # 3 assert.deepEqual(1 + 1, 2);
70
- TAP version 13
71
- ok 1
72
- 1..1
73
- # tests 1
74
- # pass 1
75
- # 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 }
76
81
  ```
77
82
 
78
- ## Sample tests
83
+ ### Promises
79
84
 
80
- ### simple
85
+ Assert that a promise resolves to a value with `//=> resolves to`:
81
86
 
82
- ```javascript should equal 1
83
- let a = 1;
84
- a; //=> 1
87
+ ```javascript test
88
+ Promise.resolve(true) //=> resolves to true
85
89
  ```
86
90
 
87
- ### utf-8 arrow
91
+ Assert that a promise rejects with `// rejects`:
88
92
 
89
- ```javascript test utf8 arrow
90
- a; // 1
93
+ ```javascript test
94
+ Promise.reject(new Error("no")) // rejects /no/
91
95
  ```
92
96
 
93
- ### console.log
97
+ ### TypeScript
94
98
 
95
- ```javascript test console.log
96
- a = { a: 1 };
97
- console.log(a); //=> { 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
98
104
  ```
99
105
 
100
- ### throws
106
+ ### Grouping blocks
101
107
 
102
- ```javascript test throws
103
- const b = () => {
104
- throw new Error("fail");
105
- };
106
- b(); // throws /fail/
107
- ```
108
+ Each code block runs as its own file. To share variables across
109
+ blocks, give them the same group name with `test:groupname`:
108
110
 
109
- ### TypesScript
111
+ ````
112
+ ```javascript test:math
113
+ let x = 2;
114
+ ```
110
115
 
111
- ```typescript should add two numbers with typescript
112
- const sum: number = 1 + 1;
113
- sum; //=> 2
116
+ ```javascript test:math
117
+ x; //=> 2
114
118
  ```
119
+ ````
120
+
121
+ ### Auto-discover mode
122
+
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`
115
138
 
116
- ## Projects using readme-assert
139
+ ## License
117
140
 
118
- - [fen-chess-board](https://github.com/laat/fen-chess-board)
119
- - [babel-plugin-transform-comment-to-assert](https://github.com/laat/babel-plugin-transform-comment-to-assert)
120
- - [babel-plugin-transform-rename-import](https://github.com/laat/babel-plugin-transform-rename-import)
121
- - [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";