run-js-doc-tests.ts
1 const LORO_VERSION = "1.3.5"; 2 3 export interface CodeBlock { 4 filename: string; 5 filePath: string; 6 lineNumber: number; 7 lang: string; 8 content: string; 9 } 10 11 export function extractCodeBlocks( 12 fileContent: string, 13 codeBlocks: CodeBlock[], 14 name: string, 15 path: string, 16 ) { 17 // Regular expression to detect TypeScript code blocks 18 const codeBlockRegex = /```(typescript|ts|js|javascript)\n([\s\S]*?)```/g; 19 let match; 20 while ((match = codeBlockRegex.exec(fileContent)) !== null) { 21 const startLine = 22 fileContent.substring(0, match.index).split("\n").length; 23 let content = match[2]; 24 content = content.replace(/^\s*\*/g, ""); 25 content = content.replace(/\n\s*\*/g, "\n"); 26 content = content.replace(/^\s*\/\/\//g, ""); 27 content = content.replace(/\n\s*\/\/\//g, "\n"); 28 content = replaceImportVersion(content, LORO_VERSION); 29 if (!content.includes("loro-crdt")) { 30 content = IMPORTS + content; 31 } 32 codeBlocks.push({ 33 filename: name, 34 filePath: path, 35 lineNumber: startLine, 36 content, 37 lang: match[1], 38 }); 39 } 40 } 41 42 function replaceImportVersion(input: string, targetVersion: string): string { 43 const regex = /from "loro-crdt"/g; 44 const replacement = `from "npm:loro-crdt@${targetVersion}"`; 45 return input.replace(regex, replacement); 46 } 47 48 const IMPORTS = 49 `import { Loro, LoroDoc, LoroMap, LoroText, LoroList, Delta, UndoManager, getType, isContainer } from "npm:loro-crdt@${LORO_VERSION}"; 50 import { expect } from "npm:expect@29.7.0";\n 51 `; 52 53 Deno.test("extract doc tests", async () => { 54 const filePath = "./doc-tests-tests/example.txt"; 55 const fileContent = await Deno.readTextFile(filePath); 56 const codeBlocks: CodeBlock[] = []; 57 extractCodeBlocks(fileContent, codeBlocks, "example.txt", filePath); 58 for (const block of codeBlocks) { 59 console.log(block.content); 60 console.log("=============================="); 61 } 62 await runCodeBlocks(codeBlocks); 63 }); 64 65 export async function runDocTests(paths: string[]) { 66 const codeBlocks: CodeBlock[] = []; 67 for (const path of paths) { 68 const fileContent = await Deno.readTextFile(path); 69 extractCodeBlocks(fileContent, codeBlocks, path, path); 70 } 71 72 await runCodeBlocks(codeBlocks); 73 } 74 75 async function runCodeBlocks(codeBlocks: CodeBlock[]) { 76 let testCases = 0; 77 let passed = 0; 78 let failed = 0; 79 for (const block of codeBlocks) { 80 try { 81 const command = new Deno.Command("deno", { 82 args: ["eval", "--ext=ts", block.content], 83 stdout: "null", 84 stderr: "inherit", 85 }); 86 const process = command.spawn(); 87 const status = await process.status; 88 testCases += 1; 89 if (status.success) { 90 passed += 1; 91 } else { 92 console.log("----------------"); 93 console.log(block.content); 94 console.log("-----------------"); 95 console.error( 96 `\x1b[31;1mError in \x1b[4m${block.filePath}:${block.lineNumber}\x1b[0m\n\n\n\n\n`, 97 ); 98 failed += 1; 99 } 100 } catch (error) { 101 console.error("Error:", error); 102 } 103 await Deno.stdout.write( 104 new TextEncoder().encode( 105 `\r🧪 ${testCases} tests, ✅ ${passed} passed,${failed > 0 ? " ❌" : "" 106 } ${failed} failed`, 107 ), 108 ); 109 } 110 } 111 112 if (Deno.args.length > 0) { 113 await runDocTests(Deno.args); 114 } else { 115 console.log("No paths provided. Please provide paths as arguments."); 116 }