#!/usr/bin/env node /** * Extract inline tests from OxJS documentation comments * and convert them to Vitest test files */ const fs = require('fs'); const path = require('path'); const { glob } = require('glob'); // Regular expressions from Ox.doc const re = { multiline: /\/\*@([\w\W]+?)@?\*\//g, singleline: /\/\/@\s*(.*?)\s*$/gm, test: /^\s*>\s+(.+)$/, expected: /^\s*([^>].*)$/, item: /^(.+?)\s+<(.+?)>\s+(.+?)$/, }; /** * Parse documentation comments from source code */ function parseDocComments(source, filename) { const docs = []; let match; // Parse multiline comments while ((match = re.multiline.exec(source)) !== null) { const content = match[1]; const doc = parseDocContent(content, filename); if (doc) docs.push(doc); } // Parse single line comments source.replace(re.singleline, (match, content) => { const doc = parseDocContent(content, filename); if (doc) docs.push(doc); return match; }); return docs; } /** * Parse documentation content */ function parseDocContent(content, filename) { const lines = content.split('\n'); // Find the first non-empty line that matches the item pattern let itemMatch = null; let itemName = 'Unknown'; for (const line of lines) { const trimmed = line.trim(); if (trimmed) { itemMatch = trimmed.match(re.item); if (itemMatch) { break; } } } if (!itemMatch) { // If no item match, still try to extract tests with a generic name // This handles cases where tests are in script blocks or without proper headers itemMatch = ['', filename.replace(/.*\//, '').replace('.js', ''), 'tests', '']; } const doc = { name: itemMatch[1], type: itemMatch[2], summary: itemMatch[3], file: filename, tests: [] }; // Extract tests let inTest = false; let currentTest = null; for (let i = 1; i < lines.length; i++) { const line = lines[i]; const testMatch = line.match(re.test); const expectedMatch = line.match(re.expected); if (testMatch) { if (currentTest) { doc.tests.push(currentTest); } currentTest = { statement: testMatch[1], expected: null }; inTest = true; } else if (inTest && expectedMatch) { if (currentTest) { currentTest.expected = expectedMatch[1].trim(); doc.tests.push(currentTest); currentTest = null; inTest = false; } } } if (currentTest) { doc.tests.push(currentTest); } return doc.tests.length > 0 ? doc : null; } /** * Convert extracted tests to Vitest format */ function generateVitestTest(docs, sourceFile) { if (docs.length === 0) return null; const testName = path.basename(sourceFile, '.js'); const tests = []; tests.push(`import { describe, it, expect, beforeAll } from 'vitest';`); tests.push(`import '../test-setup.js';\n`); tests.push(`// Tests extracted from ${sourceFile}\n`); tests.push(`describe('${testName}', () => {`); for (const doc of docs) { if (doc.tests.length === 0) continue; tests.push(` describe('${doc.name}', () => {`); for (const test of doc.tests) { if (!test.expected) continue; // Escape the test statement for use in test name const testName = test.statement.replace(/'/g, "\\'").substring(0, 60); tests.push(` it('${testName}...', async () => {`); tests.push(` const actual = await evaluateInContext(\`${test.statement.replace(/`/g, '\\`')}\`);`); tests.push(` const expected = ${test.expected};`); tests.push(` expect(actual).toEqual(expected);`); tests.push(` });\n`); } tests.push(` });\n`); } tests.push(`});`); return tests.join('\n'); } /** * Process a single source file */ async function processFile(filePath) { const source = fs.readFileSync(filePath, 'utf-8'); const docs = parseDocComments(source, filePath); if (docs.length === 0) return null; const testContent = generateVitestTest(docs, filePath); if (!testContent) return null; // Create test file path const relativePath = path.relative('source', filePath); const testPath = path.join('test/extracted', relativePath.replace('.js', '.test.js')); // Ensure directory exists fs.mkdirSync(path.dirname(testPath), { recursive: true }); // Write test file fs.writeFileSync(testPath, testContent); console.log(`āœ“ Extracted tests from ${relativePath} -> ${testPath}`); return testPath; } /** * Create test setup file */ function createTestSetup() { const setupContent = `/** * Test setup for running extracted OxJS inline tests */ // Load OxJS in test environment import '../source/Ox.js'; // Helper function to evaluate test statements in context global.evaluateInContext = async function(statement) { try { // This will need to be enhanced to handle async tests // For now, we'll use eval which isn't ideal but matches the original test system return eval(statement); } catch (error) { console.error('Error evaluating:', statement, error); throw error; } }; // Make Ox available globally for tests global.Ox = window.Ox || global.Ox; `; fs.mkdirSync('test', { recursive: true }); fs.writeFileSync('test/test-setup.js', setupContent); console.log('āœ“ Created test/test-setup.js'); } /** * Main function */ async function main() { console.log('Extracting inline tests from OxJS source files...\n'); // Create test setup createTestSetup(); // Find all JavaScript files in source const files = await glob('source/**/*.js', { ignore: ['**/node_modules/**', '**/min/**', '**/dev/**'] }); console.log(`Found ${files.length} source files to process\n`); const testFiles = []; for (const file of files) { const testFile = await processFile(file); if (testFile) { testFiles.push(testFile); } } console.log(`\nāœ… Extracted tests from ${testFiles.length} files`); console.log('\nRun "npm test" to execute the extracted tests'); } // Run if called directly if (require.main === module) { main().catch(console.error); } module.exports = { parseDocComments, generateVitestTest };