@forge-ts/doctest 0.2.0 → 0.2.1

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 ADDED
@@ -0,0 +1,42 @@
1
+ # @forge-ts/doctest
2
+
3
+ Executable documentation testing for the [forge-ts](https://github.com/kryptobaseddev/forge-ts) toolchain. Extracts `@example` code blocks from TSDoc comments and runs them as tests.
4
+
5
+ ## When to use this package
6
+
7
+ **Most users should install `@forge-ts/cli` instead** and use `npx forge-ts test`. This package is for programmatic use.
8
+
9
+ ```bash
10
+ npm install @forge-ts/doctest
11
+ ```
12
+
13
+ ## How it works
14
+
15
+ 1. Walks your TypeScript AST to find `@example` blocks in TSDoc comments
16
+ 2. Generates virtual test files with auto-injected imports
17
+ 3. Adds inline source maps pointing back to your original source
18
+ 4. Converts `// => value` comments into assertions
19
+ 5. Runs tests via Node 24's built-in `node:test` runner
20
+
21
+ ## Example
22
+
23
+ ```typescript
24
+ import { loadConfig } from "@forge-ts/core";
25
+ import { doctest } from "@forge-ts/doctest";
26
+
27
+ const config = await loadConfig();
28
+ const result = await doctest(config);
29
+
30
+ if (!result.success) {
31
+ console.error(`${result.errors.length} doctest(s) failed`);
32
+ process.exit(1);
33
+ }
34
+ ```
35
+
36
+ ## Part of forge-ts
37
+
38
+ See the [main repo](https://github.com/kryptobaseddev/forge-ts) for full documentation.
39
+
40
+ ## License
41
+
42
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ForgeSymbol, ForgeConfig, ForgeResult } from '@codluv/forge-core';
1
+ import { ForgeSymbol, ForgeConfig, ForgeResult } from '@forge-ts/core';
2
2
 
3
3
  /**
4
4
  * A single extracted `@example` block ready for test generation.
@@ -99,7 +99,7 @@ interface TestCaseResult {
99
99
  declare function runTests(files: VirtualTestFile[]): Promise<RunResult>;
100
100
 
101
101
  /**
102
- * @codluv/forge-doctest — TSDoc `@example` block extractor and test runner.
102
+ * @forge-ts/doctest — TSDoc `@example` block extractor and test runner.
103
103
  *
104
104
  * Extracts fenced code blocks from `@example` tags in TSDoc comments,
105
105
  * generates virtual `node:test` test files, and executes them.
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ function generateTestFiles(examples, options) {
88
88
  const symbolNames = [...new Set(exs.map((ex) => ex.symbolName))];
89
89
  const lineMap = [];
90
90
  const lines = [];
91
- lines.push(`// Auto-generated by @codluv/forge-doctest \u2014 do not edit`);
91
+ lines.push(`// Auto-generated by @forge-ts/doctest \u2014 do not edit`);
92
92
  lines.push(`// Source: ${sourcePath}`);
93
93
  lines.push(`import { describe, it } from "node:test";`);
94
94
  lines.push(`import assert from "node:assert/strict";`);
@@ -196,7 +196,7 @@ async function runTests(files) {
196
196
  }
197
197
 
198
198
  // src/index.ts
199
- import { createWalker } from "@codluv/forge-core";
199
+ import { createWalker } from "@forge-ts/core";
200
200
  async function doctest(config) {
201
201
  const start = Date.now();
202
202
  const walker = createWalker(config);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/extractor.ts","../src/generator.ts","../src/runner.ts","../src/index.ts"],"sourcesContent":["import type { ForgeSymbol } from \"@codluv/forge-core\";\n\n/**\n * A single extracted `@example` block ready for test generation.\n * @public\n */\nexport interface ExtractedExample {\n\t/** The symbol this example belongs to. */\n\tsymbolName: string;\n\t/** Absolute path to the source file. */\n\tfilePath: string;\n\t/** 1-based line number of the `@example` tag. */\n\tline: number;\n\t/** The raw code inside the fenced block. */\n\tcode: string;\n\t/** The language identifier (e.g. `\"typescript\"`). */\n\tlanguage: string;\n\t/** Sequential index among examples for this symbol. */\n\tindex: number;\n}\n\n/**\n * Extracts all `@example` blocks from a list of {@link ForgeSymbol} objects.\n *\n * @param symbols - The symbols produced by the core AST walker.\n * @returns A flat array of {@link ExtractedExample} objects, one per code block.\n * @public\n */\nexport function extractExamples(symbols: ForgeSymbol[]): ExtractedExample[] {\n\tconst results: ExtractedExample[] = [];\n\n\tfor (const symbol of symbols) {\n\t\tconst examples = symbol.documentation?.examples ?? [];\n\t\tfor (let i = 0; i < examples.length; i++) {\n\t\t\tconst ex = examples[i];\n\t\t\tresults.push({\n\t\t\t\tsymbolName: symbol.name,\n\t\t\t\tfilePath: symbol.filePath,\n\t\t\t\tline: ex.line,\n\t\t\t\tcode: ex.code,\n\t\t\t\tlanguage: ex.language,\n\t\t\t\tindex: i,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn results;\n}\n","import { basename, relative } from \"node:path\";\nimport type { ExtractedExample } from \"./extractor.js\";\n\n/**\n * Options for virtual test file generation.\n * @public\n */\nexport interface GeneratorOptions {\n\t/** Directory where virtual test files will be written. */\n\tcacheDir: string;\n}\n\n/**\n * A generated virtual test file.\n * @public\n */\nexport interface VirtualTestFile {\n\t/** Absolute path where the file will be written. */\n\tpath: string;\n\t/** File contents (valid TypeScript). */\n\tcontent: string;\n}\n\n/**\n * Converts `// => value` comment patterns in example code to `assert.strictEqual` calls.\n *\n * @param code - Raw example code from the TSDoc block.\n * @returns Code with assertion comments replaced by actual assertion calls.\n * @internal\n */\nfunction processAssertions(code: string): string {\n\treturn code\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\t// Match: expression // => expected\n\t\t\tconst arrowMatch = line.match(/^(\\s*)(.+?)\\s*\\/\\/\\s*=>\\s*(.+)$/);\n\t\t\tif (arrowMatch) {\n\t\t\t\tconst [, indent, expr, expected] = arrowMatch;\n\t\t\t\treturn `${indent}assert.strictEqual(${expr.trim()}, ${expected.trim()});`;\n\t\t\t}\n\t\t\treturn line;\n\t\t})\n\t\t.join(\"\\n\");\n}\n\n/**\n * Builds a base64-encoded inline source map that maps generated test file lines\n * back to the original TSDoc `@example` block in the source file.\n *\n * @param generatedFile - Absolute path of the generated test file.\n * @param examples - Examples contained in this file, each carrying its source location.\n * @param lineMap - Array mapping each generated line index (0-based) to its original line.\n * @returns A `//# sourceMappingURL=data:...` comment string.\n * @internal\n */\nfunction buildInlineSourceMap(\n\tgeneratedFile: string,\n\texamples: ExtractedExample[],\n\tlineMap: Array<{ generatedLine: number; originalLine: number; sourceFile: string }>,\n): string {\n\t// Collect unique source files\n\tconst sources = [...new Set(examples.map((e) => e.filePath))];\n\n\t// Build mappings: each entry is [generatedLine, sourceIndex, originalLine, 0] (0-based)\n\t// VLQ encoding is complex; we generate a minimal valid source map with explicit mappings\n\tconst mappings: string[] = [];\n\tlet lastGenLine = 0;\n\n\tfor (const entry of lineMap) {\n\t\t// Fill gaps with empty mappings\n\t\twhile (lastGenLine < entry.generatedLine) {\n\t\t\tmappings.push(\";\");\n\t\t\tlastGenLine++;\n\t\t}\n\t\tconst sourceIdx = sources.indexOf(entry.sourceFile);\n\t\t// Each mapping segment: [genCol=0, srcIdx, srcLine(0-based), srcCol=0]\n\t\t// Encode as VLQ - use a simple approach with base64 VLQ\n\t\tconst seg = encodeVlqSegment(0, sourceIdx, entry.originalLine - 1, 0);\n\t\tmappings.push(seg);\n\t\tlastGenLine++;\n\t}\n\n\tconst sourceMap = {\n\t\tversion: 3,\n\t\tfile: basename(generatedFile),\n\t\tsources,\n\t\tsourcesContent: null,\n\t\tnames: [],\n\t\tmappings: mappings.join(\";\"),\n\t};\n\n\tconst encoded = Buffer.from(JSON.stringify(sourceMap)).toString(\"base64\");\n\treturn `//# sourceMappingURL=data:application/json;base64,${encoded}`;\n}\n\n/**\n * Encodes a single source map segment using Base64 VLQ encoding.\n * Each field is relative to the previous segment's value.\n * @internal\n */\nfunction encodeVlqSegment(...fields: number[]): string {\n\treturn fields.map(encodeVlq).join(\"\");\n}\n\n/** VLQ Base64 alphabet. @internal */\nconst VLQ_BASE64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/**\n * Encodes a single signed integer as Base64 VLQ.\n * @internal\n */\nfunction encodeVlq(value: number): string {\n\t// Convert to unsigned VLQ (sign bit in LSB)\n\tlet vlq = value < 0 ? (-value << 1) | 1 : value << 1;\n\tlet result = \"\";\n\tdo {\n\t\tlet digit = vlq & 0x1f;\n\t\tvlq >>>= 5;\n\t\tif (vlq > 0) {\n\t\t\tdigit |= 0x20; // continuation bit\n\t\t}\n\t\tresult += VLQ_BASE64[digit];\n\t} while (vlq > 0);\n\treturn result;\n}\n\n/**\n * Generates a virtual test file for a set of extracted examples.\n *\n * Each example is wrapped in an `it()` block using the Node built-in\n * `node:test` runner so that no additional test framework is required.\n * Auto-imports the tested symbol from the source file, processes `// =>`\n * assertion patterns, and appends an inline source map.\n *\n * @param examples - Examples to include in the generated file.\n * @param options - Output configuration.\n * @returns An array of {@link VirtualTestFile} objects (one per source file).\n * @public\n */\nexport function generateTestFiles(\n\texamples: ExtractedExample[],\n\toptions: GeneratorOptions,\n): VirtualTestFile[] {\n\t// Group by source file\n\tconst byFile = new Map<string, ExtractedExample[]>();\n\tfor (const ex of examples) {\n\t\tconst group = byFile.get(ex.filePath) ?? [];\n\t\tgroup.push(ex);\n\t\tbyFile.set(ex.filePath, group);\n\t}\n\n\tconst files: VirtualTestFile[] = [];\n\n\tfor (const [sourcePath, exs] of byFile) {\n\t\tconst slug = sourcePath\n\t\t\t.replace(/[^a-zA-Z0-9]/g, \"_\")\n\t\t\t.replace(/_+/g, \"_\")\n\t\t\t.toLowerCase();\n\t\tconst testFilePath = `${options.cacheDir}/${slug}.test.ts`;\n\n\t\t// Compute relative import path from cacheDir to sourcePath,\n\t\t// replacing the extension with .js for ESM compatibility\n\t\tconst relPath = relative(options.cacheDir, sourcePath).replace(/\\.tsx?$/, \".js\");\n\t\tconst importPath = relPath.startsWith(\".\") ? relPath : `./${relPath}`;\n\n\t\t// Collect unique symbol names for the import\n\t\tconst symbolNames = [...new Set(exs.map((ex) => ex.symbolName))];\n\n\t\t// Track line numbers for source map\n\t\tconst lineMap: Array<{ generatedLine: number; originalLine: number; sourceFile: string }> = [];\n\n\t\t// Build it-blocks and track line positions\n\t\tconst lines: string[] = [];\n\n\t\t// Header lines (0-based index):\n\t\t// 0: // Auto-generated...\n\t\t// 1: // Source: ...\n\t\t// 2: import { describe, it } from \"node:test\";\n\t\t// 3: import assert from \"node:assert/strict\";\n\t\t// 4: import { symbolNames } from \"...\";\n\t\t// 5: (empty)\n\t\t// 6: describe(...) {\n\t\tlines.push(`// Auto-generated by @codluv/forge-doctest — do not edit`);\n\t\tlines.push(`// Source: ${sourcePath}`);\n\t\tlines.push(`import { describe, it } from \"node:test\";`);\n\t\tlines.push(`import assert from \"node:assert/strict\";`);\n\t\tlines.push(`import { ${symbolNames.join(\", \")} } from \"${importPath}\";`);\n\t\tlines.push(``);\n\t\tlines.push(`describe(\"doctest: ${basename(sourcePath)}\", () => {`);\n\n\t\tfor (const ex of exs) {\n\t\t\tconst itLine = lines.length; // 0-based line index of the it() call\n\t\t\tlineMap.push({ generatedLine: itLine, originalLine: ex.line, sourceFile: ex.filePath });\n\n\t\t\tlines.push(\n\t\t\t\t`\\tit(\"${ex.symbolName} example ${ex.index + 1} (line ${ex.line})\", async () => {`,\n\t\t\t);\n\n\t\t\tconst processedCode = processAssertions(ex.code);\n\t\t\tfor (const codeLine of processedCode.split(\"\\n\")) {\n\t\t\t\tlines.push(`\\t\\t${codeLine}`);\n\t\t\t}\n\t\t\tlines.push(`\\t});`);\n\t\t}\n\n\t\tlines.push(`});`);\n\t\tlines.push(``);\n\n\t\t// Append inline source map\n\t\tconst sourceMapComment = buildInlineSourceMap(testFilePath, exs, lineMap);\n\t\tlines.push(sourceMapComment);\n\t\tlines.push(``);\n\n\t\tconst content = lines.join(\"\\n\");\n\t\tfiles.push({ path: testFilePath, content });\n\t}\n\n\treturn files;\n}\n","import { spawn } from \"node:child_process\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { basename, dirname } from \"node:path\";\nimport type { VirtualTestFile } from \"./generator.js\";\n\n/**\n * Result of running the generated test files.\n * @public\n */\nexport interface RunResult {\n\t/** Whether all tests passed. */\n\tsuccess: boolean;\n\t/** Number of tests that passed. */\n\tpassed: number;\n\t/** Number of tests that failed. */\n\tfailed: number;\n\t/** Combined stdout + stderr output from the test runner. */\n\toutput: string;\n\t/** Individual test results with name and status. */\n\ttests: TestCaseResult[];\n}\n\n/**\n * The result of a single test case.\n * @public\n */\nexport interface TestCaseResult {\n\t/** The full test name as reported by the runner. */\n\tname: string;\n\t/** Whether this test passed. */\n\tpassed: boolean;\n\t/** The source file this test was generated from, if determinable. */\n\tsourceFile?: string;\n}\n\n/**\n * Parses TAP output from `node --test` into structured results.\n *\n * @param output - The raw TAP text from the runner.\n * @returns An object with pass/fail counts and per-test results.\n * @internal\n */\nfunction parseTapOutput(output: string): {\n\tpassed: number;\n\tfailed: number;\n\ttests: TestCaseResult[];\n} {\n\tconst tests: TestCaseResult[] = [];\n\tlet passed = 0;\n\tlet failed = 0;\n\n\tfor (const line of output.split(\"\\n\")) {\n\t\t// TAP ok / not ok lines: \"ok 1 - test name\" or \"not ok 1 - test name\"\n\t\tconst okMatch = line.match(/^(?: {4})?ok \\d+ - (.+)$/);\n\t\tconst notOkMatch = line.match(/^(?: {4})?not ok \\d+ - (.+)$/);\n\n\t\tif (okMatch) {\n\t\t\tconst name = okMatch[1].trim();\n\t\t\t// Skip subtests that are suite-level (no leading spaces means top-level pass)\n\t\t\tif (!line.startsWith(\" \")) {\n\t\t\t\tpassed++;\n\t\t\t\ttests.push({ name, passed: true });\n\t\t\t}\n\t\t} else if (notOkMatch) {\n\t\t\tconst name = notOkMatch[1].trim();\n\t\t\tif (!line.startsWith(\" \")) {\n\t\t\t\tfailed++;\n\t\t\t\ttests.push({ name, passed: false });\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: use summary lines if no individual results parsed\n\tif (tests.length === 0) {\n\t\tconst passMatch = output.match(/# pass\\s+(\\d+)/);\n\t\tconst failMatch = output.match(/# fail\\s+(\\d+)/);\n\t\tpassed = passMatch ? parseInt(passMatch[1], 10) : 0;\n\t\tfailed = failMatch ? parseInt(failMatch[1], 10) : 0;\n\t}\n\n\treturn { passed, failed, tests };\n}\n\n/**\n * Writes virtual test files to disk and executes them with Node 24 native\n * TypeScript support (`--experimental-strip-types --test`).\n *\n * @param files - The virtual test files to write and run.\n * @returns A {@link RunResult} summarising the test outcome.\n * @public\n */\nexport async function runTests(files: VirtualTestFile[]): Promise<RunResult> {\n\tif (files.length === 0) {\n\t\treturn { success: true, passed: 0, failed: 0, output: \"\", tests: [] };\n\t}\n\n\t// Write all files to disk\n\tfor (const file of files) {\n\t\tawait mkdir(dirname(file.path), { recursive: true });\n\t\tawait writeFile(file.path, file.content, \"utf8\");\n\t}\n\n\tconst paths = files.map((f) => f.path);\n\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(process.execPath, [\"--experimental-strip-types\", \"--test\", ...paths], {\n\t\t\tstdio: \"pipe\",\n\t\t});\n\n\t\tlet output = \"\";\n\t\tproc.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\toutput += chunk.toString();\n\t\t});\n\t\tproc.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\toutput += chunk.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tconst { passed, failed, tests } = parseTapOutput(output);\n\n\t\t\t// Annotate tests with source file from filename slug\n\t\t\tconst annotated = tests.map((t) => {\n\t\t\t\t// Test names like \"doctest: filename.ts > symbolName example 1 (line N)\"\n\t\t\t\tconst srcMatch = t.name.match(/doctest:\\s*(.+?)(?:\\s*>|$)/);\n\t\t\t\treturn srcMatch ? { ...t, sourceFile: srcMatch[1].trim() } : t;\n\t\t\t});\n\n\t\t\t// Enrich failure output with file locations\n\t\t\tconst enrichedOutput = output;\n\t\t\tfor (const file of files) {\n\t\t\t\tconst fileBase = basename(file.path);\n\t\t\t\tif (enrichedOutput.includes(fileBase)) {\n\t\t\t\t\t// Already references the file; no additional enrichment needed\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tresolve({\n\t\t\t\tsuccess: code === 0,\n\t\t\t\tpassed,\n\t\t\t\tfailed,\n\t\t\t\toutput: enrichedOutput,\n\t\t\t\ttests: annotated,\n\t\t\t});\n\t\t});\n\t});\n}\n","/**\n * @codluv/forge-doctest — TSDoc `@example` block extractor and test runner.\n *\n * Extracts fenced code blocks from `@example` tags in TSDoc comments,\n * generates virtual `node:test` test files, and executes them.\n *\n * @packageDocumentation\n * @public\n */\n\nexport { type ExtractedExample, extractExamples } from \"./extractor.js\";\nexport {\n\ttype GeneratorOptions,\n\tgenerateTestFiles,\n\ttype VirtualTestFile,\n} from \"./generator.js\";\nexport { type RunResult, runTests, type TestCaseResult } from \"./runner.js\";\n\nimport { createWalker, type ForgeConfig, type ForgeResult } from \"@codluv/forge-core\";\nimport { extractExamples } from \"./extractor.js\";\nimport { generateTestFiles } from \"./generator.js\";\nimport { runTests } from \"./runner.js\";\n\n/**\n * Runs the full doctest pipeline: extract → generate → run.\n *\n * @param config - The resolved {@link ForgeConfig} for the project.\n * @returns A {@link ForgeResult} with success/failure and any diagnostics.\n * @public\n */\nexport async function doctest(config: ForgeConfig): Promise<ForgeResult> {\n\tconst start = Date.now();\n\n\tconst walker = createWalker(config);\n\tconst symbols = walker.walk();\n\n\tconst examples = extractExamples(symbols);\n\tconst files = generateTestFiles(examples, { cacheDir: config.doctest.cacheDir });\n\tconst runResult = await runTests(files);\n\n\treturn {\n\t\tsuccess: runResult.success,\n\t\tsymbols,\n\t\terrors:\n\t\t\trunResult.failed > 0\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcode: \"D001\",\n\t\t\t\t\t\t\tmessage: `${runResult.failed} doctest(s) failed. See output for details.`,\n\t\t\t\t\t\t\tfilePath: \"\",\n\t\t\t\t\t\t\tline: 0,\n\t\t\t\t\t\t\tcolumn: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [],\n\t\twarnings: [],\n\t\tduration: Date.now() - start,\n\t};\n}\n"],"mappings":"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","names":["basename"]}
1
+ {"version":3,"sources":["../src/extractor.ts","../src/generator.ts","../src/runner.ts","../src/index.ts"],"sourcesContent":["import type { ForgeSymbol } from \"@forge-ts/core\";\n\n/**\n * A single extracted `@example` block ready for test generation.\n * @public\n */\nexport interface ExtractedExample {\n\t/** The symbol this example belongs to. */\n\tsymbolName: string;\n\t/** Absolute path to the source file. */\n\tfilePath: string;\n\t/** 1-based line number of the `@example` tag. */\n\tline: number;\n\t/** The raw code inside the fenced block. */\n\tcode: string;\n\t/** The language identifier (e.g. `\"typescript\"`). */\n\tlanguage: string;\n\t/** Sequential index among examples for this symbol. */\n\tindex: number;\n}\n\n/**\n * Extracts all `@example` blocks from a list of {@link ForgeSymbol} objects.\n *\n * @param symbols - The symbols produced by the core AST walker.\n * @returns A flat array of {@link ExtractedExample} objects, one per code block.\n * @public\n */\nexport function extractExamples(symbols: ForgeSymbol[]): ExtractedExample[] {\n\tconst results: ExtractedExample[] = [];\n\n\tfor (const symbol of symbols) {\n\t\tconst examples = symbol.documentation?.examples ?? [];\n\t\tfor (let i = 0; i < examples.length; i++) {\n\t\t\tconst ex = examples[i];\n\t\t\tresults.push({\n\t\t\t\tsymbolName: symbol.name,\n\t\t\t\tfilePath: symbol.filePath,\n\t\t\t\tline: ex.line,\n\t\t\t\tcode: ex.code,\n\t\t\t\tlanguage: ex.language,\n\t\t\t\tindex: i,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn results;\n}\n","import { basename, relative } from \"node:path\";\nimport type { ExtractedExample } from \"./extractor.js\";\n\n/**\n * Options for virtual test file generation.\n * @public\n */\nexport interface GeneratorOptions {\n\t/** Directory where virtual test files will be written. */\n\tcacheDir: string;\n}\n\n/**\n * A generated virtual test file.\n * @public\n */\nexport interface VirtualTestFile {\n\t/** Absolute path where the file will be written. */\n\tpath: string;\n\t/** File contents (valid TypeScript). */\n\tcontent: string;\n}\n\n/**\n * Converts `// => value` comment patterns in example code to `assert.strictEqual` calls.\n *\n * @param code - Raw example code from the TSDoc block.\n * @returns Code with assertion comments replaced by actual assertion calls.\n * @internal\n */\nfunction processAssertions(code: string): string {\n\treturn code\n\t\t.split(\"\\n\")\n\t\t.map((line) => {\n\t\t\t// Match: expression // => expected\n\t\t\tconst arrowMatch = line.match(/^(\\s*)(.+?)\\s*\\/\\/\\s*=>\\s*(.+)$/);\n\t\t\tif (arrowMatch) {\n\t\t\t\tconst [, indent, expr, expected] = arrowMatch;\n\t\t\t\treturn `${indent}assert.strictEqual(${expr.trim()}, ${expected.trim()});`;\n\t\t\t}\n\t\t\treturn line;\n\t\t})\n\t\t.join(\"\\n\");\n}\n\n/**\n * Builds a base64-encoded inline source map that maps generated test file lines\n * back to the original TSDoc `@example` block in the source file.\n *\n * @param generatedFile - Absolute path of the generated test file.\n * @param examples - Examples contained in this file, each carrying its source location.\n * @param lineMap - Array mapping each generated line index (0-based) to its original line.\n * @returns A `//# sourceMappingURL=data:...` comment string.\n * @internal\n */\nfunction buildInlineSourceMap(\n\tgeneratedFile: string,\n\texamples: ExtractedExample[],\n\tlineMap: Array<{ generatedLine: number; originalLine: number; sourceFile: string }>,\n): string {\n\t// Collect unique source files\n\tconst sources = [...new Set(examples.map((e) => e.filePath))];\n\n\t// Build mappings: each entry is [generatedLine, sourceIndex, originalLine, 0] (0-based)\n\t// VLQ encoding is complex; we generate a minimal valid source map with explicit mappings\n\tconst mappings: string[] = [];\n\tlet lastGenLine = 0;\n\n\tfor (const entry of lineMap) {\n\t\t// Fill gaps with empty mappings\n\t\twhile (lastGenLine < entry.generatedLine) {\n\t\t\tmappings.push(\";\");\n\t\t\tlastGenLine++;\n\t\t}\n\t\tconst sourceIdx = sources.indexOf(entry.sourceFile);\n\t\t// Each mapping segment: [genCol=0, srcIdx, srcLine(0-based), srcCol=0]\n\t\t// Encode as VLQ - use a simple approach with base64 VLQ\n\t\tconst seg = encodeVlqSegment(0, sourceIdx, entry.originalLine - 1, 0);\n\t\tmappings.push(seg);\n\t\tlastGenLine++;\n\t}\n\n\tconst sourceMap = {\n\t\tversion: 3,\n\t\tfile: basename(generatedFile),\n\t\tsources,\n\t\tsourcesContent: null,\n\t\tnames: [],\n\t\tmappings: mappings.join(\";\"),\n\t};\n\n\tconst encoded = Buffer.from(JSON.stringify(sourceMap)).toString(\"base64\");\n\treturn `//# sourceMappingURL=data:application/json;base64,${encoded}`;\n}\n\n/**\n * Encodes a single source map segment using Base64 VLQ encoding.\n * Each field is relative to the previous segment's value.\n * @internal\n */\nfunction encodeVlqSegment(...fields: number[]): string {\n\treturn fields.map(encodeVlq).join(\"\");\n}\n\n/** VLQ Base64 alphabet. @internal */\nconst VLQ_BASE64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n/**\n * Encodes a single signed integer as Base64 VLQ.\n * @internal\n */\nfunction encodeVlq(value: number): string {\n\t// Convert to unsigned VLQ (sign bit in LSB)\n\tlet vlq = value < 0 ? (-value << 1) | 1 : value << 1;\n\tlet result = \"\";\n\tdo {\n\t\tlet digit = vlq & 0x1f;\n\t\tvlq >>>= 5;\n\t\tif (vlq > 0) {\n\t\t\tdigit |= 0x20; // continuation bit\n\t\t}\n\t\tresult += VLQ_BASE64[digit];\n\t} while (vlq > 0);\n\treturn result;\n}\n\n/**\n * Generates a virtual test file for a set of extracted examples.\n *\n * Each example is wrapped in an `it()` block using the Node built-in\n * `node:test` runner so that no additional test framework is required.\n * Auto-imports the tested symbol from the source file, processes `// =>`\n * assertion patterns, and appends an inline source map.\n *\n * @param examples - Examples to include in the generated file.\n * @param options - Output configuration.\n * @returns An array of {@link VirtualTestFile} objects (one per source file).\n * @public\n */\nexport function generateTestFiles(\n\texamples: ExtractedExample[],\n\toptions: GeneratorOptions,\n): VirtualTestFile[] {\n\t// Group by source file\n\tconst byFile = new Map<string, ExtractedExample[]>();\n\tfor (const ex of examples) {\n\t\tconst group = byFile.get(ex.filePath) ?? [];\n\t\tgroup.push(ex);\n\t\tbyFile.set(ex.filePath, group);\n\t}\n\n\tconst files: VirtualTestFile[] = [];\n\n\tfor (const [sourcePath, exs] of byFile) {\n\t\tconst slug = sourcePath\n\t\t\t.replace(/[^a-zA-Z0-9]/g, \"_\")\n\t\t\t.replace(/_+/g, \"_\")\n\t\t\t.toLowerCase();\n\t\tconst testFilePath = `${options.cacheDir}/${slug}.test.ts`;\n\n\t\t// Compute relative import path from cacheDir to sourcePath,\n\t\t// replacing the extension with .js for ESM compatibility\n\t\tconst relPath = relative(options.cacheDir, sourcePath).replace(/\\.tsx?$/, \".js\");\n\t\tconst importPath = relPath.startsWith(\".\") ? relPath : `./${relPath}`;\n\n\t\t// Collect unique symbol names for the import\n\t\tconst symbolNames = [...new Set(exs.map((ex) => ex.symbolName))];\n\n\t\t// Track line numbers for source map\n\t\tconst lineMap: Array<{ generatedLine: number; originalLine: number; sourceFile: string }> = [];\n\n\t\t// Build it-blocks and track line positions\n\t\tconst lines: string[] = [];\n\n\t\t// Header lines (0-based index):\n\t\t// 0: // Auto-generated...\n\t\t// 1: // Source: ...\n\t\t// 2: import { describe, it } from \"node:test\";\n\t\t// 3: import assert from \"node:assert/strict\";\n\t\t// 4: import { symbolNames } from \"...\";\n\t\t// 5: (empty)\n\t\t// 6: describe(...) {\n\t\tlines.push(`// Auto-generated by @forge-ts/doctest — do not edit`);\n\t\tlines.push(`// Source: ${sourcePath}`);\n\t\tlines.push(`import { describe, it } from \"node:test\";`);\n\t\tlines.push(`import assert from \"node:assert/strict\";`);\n\t\tlines.push(`import { ${symbolNames.join(\", \")} } from \"${importPath}\";`);\n\t\tlines.push(``);\n\t\tlines.push(`describe(\"doctest: ${basename(sourcePath)}\", () => {`);\n\n\t\tfor (const ex of exs) {\n\t\t\tconst itLine = lines.length; // 0-based line index of the it() call\n\t\t\tlineMap.push({ generatedLine: itLine, originalLine: ex.line, sourceFile: ex.filePath });\n\n\t\t\tlines.push(\n\t\t\t\t`\\tit(\"${ex.symbolName} example ${ex.index + 1} (line ${ex.line})\", async () => {`,\n\t\t\t);\n\n\t\t\tconst processedCode = processAssertions(ex.code);\n\t\t\tfor (const codeLine of processedCode.split(\"\\n\")) {\n\t\t\t\tlines.push(`\\t\\t${codeLine}`);\n\t\t\t}\n\t\t\tlines.push(`\\t});`);\n\t\t}\n\n\t\tlines.push(`});`);\n\t\tlines.push(``);\n\n\t\t// Append inline source map\n\t\tconst sourceMapComment = buildInlineSourceMap(testFilePath, exs, lineMap);\n\t\tlines.push(sourceMapComment);\n\t\tlines.push(``);\n\n\t\tconst content = lines.join(\"\\n\");\n\t\tfiles.push({ path: testFilePath, content });\n\t}\n\n\treturn files;\n}\n","import { spawn } from \"node:child_process\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { basename, dirname } from \"node:path\";\nimport type { VirtualTestFile } from \"./generator.js\";\n\n/**\n * Result of running the generated test files.\n * @public\n */\nexport interface RunResult {\n\t/** Whether all tests passed. */\n\tsuccess: boolean;\n\t/** Number of tests that passed. */\n\tpassed: number;\n\t/** Number of tests that failed. */\n\tfailed: number;\n\t/** Combined stdout + stderr output from the test runner. */\n\toutput: string;\n\t/** Individual test results with name and status. */\n\ttests: TestCaseResult[];\n}\n\n/**\n * The result of a single test case.\n * @public\n */\nexport interface TestCaseResult {\n\t/** The full test name as reported by the runner. */\n\tname: string;\n\t/** Whether this test passed. */\n\tpassed: boolean;\n\t/** The source file this test was generated from, if determinable. */\n\tsourceFile?: string;\n}\n\n/**\n * Parses TAP output from `node --test` into structured results.\n *\n * @param output - The raw TAP text from the runner.\n * @returns An object with pass/fail counts and per-test results.\n * @internal\n */\nfunction parseTapOutput(output: string): {\n\tpassed: number;\n\tfailed: number;\n\ttests: TestCaseResult[];\n} {\n\tconst tests: TestCaseResult[] = [];\n\tlet passed = 0;\n\tlet failed = 0;\n\n\tfor (const line of output.split(\"\\n\")) {\n\t\t// TAP ok / not ok lines: \"ok 1 - test name\" or \"not ok 1 - test name\"\n\t\tconst okMatch = line.match(/^(?: {4})?ok \\d+ - (.+)$/);\n\t\tconst notOkMatch = line.match(/^(?: {4})?not ok \\d+ - (.+)$/);\n\n\t\tif (okMatch) {\n\t\t\tconst name = okMatch[1].trim();\n\t\t\t// Skip subtests that are suite-level (no leading spaces means top-level pass)\n\t\t\tif (!line.startsWith(\" \")) {\n\t\t\t\tpassed++;\n\t\t\t\ttests.push({ name, passed: true });\n\t\t\t}\n\t\t} else if (notOkMatch) {\n\t\t\tconst name = notOkMatch[1].trim();\n\t\t\tif (!line.startsWith(\" \")) {\n\t\t\t\tfailed++;\n\t\t\t\ttests.push({ name, passed: false });\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: use summary lines if no individual results parsed\n\tif (tests.length === 0) {\n\t\tconst passMatch = output.match(/# pass\\s+(\\d+)/);\n\t\tconst failMatch = output.match(/# fail\\s+(\\d+)/);\n\t\tpassed = passMatch ? parseInt(passMatch[1], 10) : 0;\n\t\tfailed = failMatch ? parseInt(failMatch[1], 10) : 0;\n\t}\n\n\treturn { passed, failed, tests };\n}\n\n/**\n * Writes virtual test files to disk and executes them with Node 24 native\n * TypeScript support (`--experimental-strip-types --test`).\n *\n * @param files - The virtual test files to write and run.\n * @returns A {@link RunResult} summarising the test outcome.\n * @public\n */\nexport async function runTests(files: VirtualTestFile[]): Promise<RunResult> {\n\tif (files.length === 0) {\n\t\treturn { success: true, passed: 0, failed: 0, output: \"\", tests: [] };\n\t}\n\n\t// Write all files to disk\n\tfor (const file of files) {\n\t\tawait mkdir(dirname(file.path), { recursive: true });\n\t\tawait writeFile(file.path, file.content, \"utf8\");\n\t}\n\n\tconst paths = files.map((f) => f.path);\n\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(process.execPath, [\"--experimental-strip-types\", \"--test\", ...paths], {\n\t\t\tstdio: \"pipe\",\n\t\t});\n\n\t\tlet output = \"\";\n\t\tproc.stdout?.on(\"data\", (chunk: Buffer) => {\n\t\t\toutput += chunk.toString();\n\t\t});\n\t\tproc.stderr?.on(\"data\", (chunk: Buffer) => {\n\t\t\toutput += chunk.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tconst { passed, failed, tests } = parseTapOutput(output);\n\n\t\t\t// Annotate tests with source file from filename slug\n\t\t\tconst annotated = tests.map((t) => {\n\t\t\t\t// Test names like \"doctest: filename.ts > symbolName example 1 (line N)\"\n\t\t\t\tconst srcMatch = t.name.match(/doctest:\\s*(.+?)(?:\\s*>|$)/);\n\t\t\t\treturn srcMatch ? { ...t, sourceFile: srcMatch[1].trim() } : t;\n\t\t\t});\n\n\t\t\t// Enrich failure output with file locations\n\t\t\tconst enrichedOutput = output;\n\t\t\tfor (const file of files) {\n\t\t\t\tconst fileBase = basename(file.path);\n\t\t\t\tif (enrichedOutput.includes(fileBase)) {\n\t\t\t\t\t// Already references the file; no additional enrichment needed\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tresolve({\n\t\t\t\tsuccess: code === 0,\n\t\t\t\tpassed,\n\t\t\t\tfailed,\n\t\t\t\toutput: enrichedOutput,\n\t\t\t\ttests: annotated,\n\t\t\t});\n\t\t});\n\t});\n}\n","/**\n * @forge-ts/doctest — TSDoc `@example` block extractor and test runner.\n *\n * Extracts fenced code blocks from `@example` tags in TSDoc comments,\n * generates virtual `node:test` test files, and executes them.\n *\n * @packageDocumentation\n * @public\n */\n\nexport { type ExtractedExample, extractExamples } from \"./extractor.js\";\nexport {\n\ttype GeneratorOptions,\n\tgenerateTestFiles,\n\ttype VirtualTestFile,\n} from \"./generator.js\";\nexport { type RunResult, runTests, type TestCaseResult } from \"./runner.js\";\n\nimport { createWalker, type ForgeConfig, type ForgeResult } from \"@forge-ts/core\";\nimport { extractExamples } from \"./extractor.js\";\nimport { generateTestFiles } from \"./generator.js\";\nimport { runTests } from \"./runner.js\";\n\n/**\n * Runs the full doctest pipeline: extract → generate → run.\n *\n * @param config - The resolved {@link ForgeConfig} for the project.\n * @returns A {@link ForgeResult} with success/failure and any diagnostics.\n * @public\n */\nexport async function doctest(config: ForgeConfig): Promise<ForgeResult> {\n\tconst start = Date.now();\n\n\tconst walker = createWalker(config);\n\tconst symbols = walker.walk();\n\n\tconst examples = extractExamples(symbols);\n\tconst files = generateTestFiles(examples, { cacheDir: config.doctest.cacheDir });\n\tconst runResult = await runTests(files);\n\n\treturn {\n\t\tsuccess: runResult.success,\n\t\tsymbols,\n\t\terrors:\n\t\t\trunResult.failed > 0\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcode: \"D001\",\n\t\t\t\t\t\t\tmessage: `${runResult.failed} doctest(s) failed. See output for details.`,\n\t\t\t\t\t\t\tfilePath: \"\",\n\t\t\t\t\t\t\tline: 0,\n\t\t\t\t\t\t\tcolumn: 0,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [],\n\t\twarnings: [],\n\t\tduration: Date.now() - start,\n\t};\n}\n"],"mappings":"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","names":["basename"]}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@forge-ts/doctest",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "TSDoc @example block extractor and test runner for forge-ts",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/codluv/forge-ts"
9
+ "url": "https://github.com/kryptobaseddev/forge-ts"
10
10
  },
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -24,13 +24,13 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@codluv/forge-core": "npm:@forge-ts/core@0.2.0"
27
+ "@forge-ts/core": "0.2.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.3.5",
31
31
  "typescript": "^5.8.2",
32
32
  "vitest": "^4.1.0",
33
- "@codluv/forge-core": "npm:@forge-ts/core@0.2.0"
33
+ "@forge-ts/core": "0.2.1"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsup",