From 6246d27b85192dd0209ce451d161d0ac5618c91e Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Wed, 4 Feb 2026 05:04:28 -0500 Subject: [PATCH 1/8] feat(js): add JavaScript tracer runtime Add Babel-based function tracing for JavaScript/TypeScript: - tracer.js: Core tracer with SQLite storage and nanosecond timing - babel-tracer-plugin.js: AST transformation to wrap functions - trace-runner.js: Entry point for running traced code - replay.js: Utilities for replay test generation - Update package.json with exports and dependencies --- packages/codeflash/package.json | 21 +- .../codeflash/runtime/babel-tracer-plugin.js | 434 ++++++++++++++ packages/codeflash/runtime/index.js | 38 ++ packages/codeflash/runtime/replay.js | 454 ++++++++++++++ packages/codeflash/runtime/trace-runner.js | 395 +++++++++++++ packages/codeflash/runtime/tracer.js | 558 ++++++++++++++++++ 6 files changed, 1899 insertions(+), 1 deletion(-) create mode 100644 packages/codeflash/runtime/babel-tracer-plugin.js create mode 100644 packages/codeflash/runtime/replay.js create mode 100644 packages/codeflash/runtime/trace-runner.js create mode 100644 packages/codeflash/runtime/tracer.js diff --git a/packages/codeflash/package.json b/packages/codeflash/package.json index dfd3abdf1..8135db4a3 100644 --- a/packages/codeflash/package.json +++ b/packages/codeflash/package.json @@ -6,7 +6,8 @@ "types": "runtime/index.d.ts", "bin": { "codeflash": "./bin/codeflash.js", - "codeflash-setup": "./bin/codeflash-setup.js" + "codeflash-setup": "./bin/codeflash-setup.js", + "codeflash-trace": "./runtime/trace-runner.js" }, "publishConfig": { "access": "public" @@ -32,6 +33,18 @@ "./loop-runner": { "require": "./runtime/loop-runner.js", "import": "./runtime/loop-runner.js" + }, + "./tracer": { + "require": "./runtime/tracer.js", + "import": "./runtime/tracer.js" + }, + "./replay": { + "require": "./runtime/replay.js", + "import": "./runtime/replay.js" + }, + "./babel-tracer-plugin": { + "require": "./runtime/babel-tracer-plugin.js", + "import": "./runtime/babel-tracer-plugin.js" } }, "scripts": { @@ -88,5 +101,11 @@ "dependencies": { "better-sqlite3": "^12.0.0", "@msgpack/msgpack": "^3.0.0" + }, + "optionalDependencies": { + "@babel/core": "^7.24.0", + "@babel/register": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@babel/preset-typescript": "^7.24.0" } } diff --git a/packages/codeflash/runtime/babel-tracer-plugin.js b/packages/codeflash/runtime/babel-tracer-plugin.js new file mode 100644 index 000000000..558e0664f --- /dev/null +++ b/packages/codeflash/runtime/babel-tracer-plugin.js @@ -0,0 +1,434 @@ +/** + * Codeflash Babel Tracer Plugin + * + * A Babel plugin that instruments JavaScript/TypeScript functions for tracing. + * This plugin wraps functions with tracing calls to capture: + * - Function arguments + * - Return values + * - Execution time + * + * The plugin transforms: + * function foo(a, b) { return a + b; } + * + * Into: + * const __codeflash_tracer__ = require('codeflash/tracer'); + * function foo(a, b) { + * return __codeflash_tracer__.wrap(function foo(a, b) { return a + b; }, 'foo', '/path/file.js', 1) + * .apply(this, arguments); + * } + * + * Supported function types: + * - FunctionDeclaration: function foo() {} + * - FunctionExpression: const foo = function() {} + * - ArrowFunctionExpression: const foo = () => {} + * - ClassMethod: class Foo { bar() {} } + * - ObjectMethod: const obj = { foo() {} } + * + * Configuration (via plugin options or environment variables): + * - functions: Array of function names to trace (traces all if not set) + * - files: Array of file patterns to trace (traces all if not set) + * - exclude: Array of patterns to exclude from tracing + * + * Usage with @babel/register: + * require('@babel/register')({ + * plugins: [['codeflash/babel-tracer-plugin', { functions: ['myFunc'] }]], + * }); + * + * Environment Variables: + * CODEFLASH_FUNCTIONS - JSON array of functions to trace + * CODEFLASH_TRACE_FILES - JSON array of file patterns to trace + * CODEFLASH_TRACE_EXCLUDE - JSON array of patterns to exclude + */ + +'use strict'; + +const path = require('path'); + +// Parse environment variables for configuration +function getEnvConfig() { + const config = { + functions: null, + files: null, + exclude: null, + }; + + try { + if (process.env.CODEFLASH_FUNCTIONS) { + config.functions = JSON.parse(process.env.CODEFLASH_FUNCTIONS); + } + } catch (e) { + console.error('[codeflash-babel] Failed to parse CODEFLASH_FUNCTIONS:', e.message); + } + + try { + if (process.env.CODEFLASH_TRACE_FILES) { + config.files = JSON.parse(process.env.CODEFLASH_TRACE_FILES); + } + } catch (e) { + console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_FILES:', e.message); + } + + try { + if (process.env.CODEFLASH_TRACE_EXCLUDE) { + config.exclude = JSON.parse(process.env.CODEFLASH_TRACE_EXCLUDE); + } + } catch (e) { + console.error('[codeflash-babel] Failed to parse CODEFLASH_TRACE_EXCLUDE:', e.message); + } + + return config; +} + +/** + * Check if a function should be traced based on configuration. + * + * @param {string} funcName - Function name + * @param {string} fileName - File path + * @param {string|null} className - Class name (for methods) + * @param {Object} config - Plugin configuration + * @returns {boolean} - True if function should be traced + */ +function shouldTraceFunction(funcName, fileName, className, config) { + // Check exclude patterns first + if (config.exclude && config.exclude.length > 0) { + for (const pattern of config.exclude) { + if (typeof pattern === 'string') { + if (funcName === pattern || fileName.includes(pattern)) { + return false; + } + } else if (pattern instanceof RegExp) { + if (pattern.test(funcName) || pattern.test(fileName)) { + return false; + } + } + } + } + + // Check file patterns + if (config.files && config.files.length > 0) { + const matchesFile = config.files.some(pattern => { + if (typeof pattern === 'string') { + return fileName.includes(pattern); + } + if (pattern instanceof RegExp) { + return pattern.test(fileName); + } + return false; + }); + if (!matchesFile) return false; + } + + // Check function names + if (config.functions && config.functions.length > 0) { + const matchesName = config.functions.some(f => { + if (typeof f === 'string') { + return f === funcName || f === `${className}.${funcName}`; + } + // Support object format: { function: 'name', file: 'path', class: 'className' } + if (typeof f === 'object' && f !== null) { + if (f.function && f.function !== funcName) return false; + if (f.file && !fileName.includes(f.file)) return false; + if (f.class && f.class !== className) return false; + return true; + } + return false; + }); + if (!matchesName) return false; + } + + return true; +} + +/** + * Check if a path should be excluded from tracing (node_modules, etc.) + * + * @param {string} fileName - File path + * @returns {boolean} - True if file should be excluded + */ +function isExcludedPath(fileName) { + // Always exclude node_modules + if (fileName.includes('node_modules')) return true; + + // Exclude common test runner internals + if (fileName.includes('jest-runner') || fileName.includes('jest-jasmine')) return true; + if (fileName.includes('@vitest')) return true; + + // Exclude this plugin itself + if (fileName.includes('codeflash/runtime')) return true; + if (fileName.includes('babel-tracer-plugin')) return true; + + return false; +} + +/** + * Create the Babel plugin. + * + * @param {Object} babel - Babel object with types (t) + * @returns {Object} - Babel plugin configuration + */ +module.exports = function codeflashTracerPlugin(babel) { + const { types: t } = babel; + + // Merge environment config with plugin options + const envConfig = getEnvConfig(); + + return { + name: 'codeflash-tracer', + + visitor: { + Program: { + enter(programPath, state) { + // Merge options from plugin config and environment + state.codeflashConfig = { + ...envConfig, + ...(state.opts || {}), + }; + + // Track whether we've added the tracer import + state.tracerImportAdded = false; + + // Get file info + state.fileName = state.filename || state.file.opts.filename || 'unknown'; + + // Check if entire file should be excluded + if (isExcludedPath(state.fileName)) { + state.skipFile = true; + return; + } + + state.skipFile = false; + }, + + exit(programPath, state) { + // Add tracer import if we instrumented any functions + if (state.tracerImportAdded) { + const tracerRequire = t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('__codeflash_tracer__'), + t.callExpression( + t.identifier('require'), + [t.stringLiteral('codeflash/tracer')] + ) + ), + ]); + + // Add at the beginning of the program + programPath.unshiftContainer('body', tracerRequire); + } + }, + }, + + // Handle: function foo() {} + FunctionDeclaration(path, state) { + if (state.skipFile) return; + if (!path.node.id) return; // Skip anonymous functions + + const funcName = path.node.id.name; + const lineNumber = path.node.loc ? path.node.loc.start.line : 0; + + if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) { + return; + } + + // Transform the function body to wrap with tracing + wrapFunctionBody(t, path, funcName, state.fileName, lineNumber, null); + state.tracerImportAdded = true; + }, + + // Handle: const foo = function() {} or const foo = () => {} + VariableDeclarator(path, state) { + if (state.skipFile) return; + if (!t.isIdentifier(path.node.id)) return; + if (!path.node.init) return; + + const init = path.node.init; + if (!t.isFunctionExpression(init) && !t.isArrowFunctionExpression(init)) { + return; + } + + const funcName = path.node.id.name; + const lineNumber = path.node.loc ? path.node.loc.start.line : 0; + + if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) { + return; + } + + // Wrap the function expression with tracer.wrap() + path.node.init = createWrapperCall(t, init, funcName, state.fileName, lineNumber, null); + state.tracerImportAdded = true; + }, + + // Handle: class Foo { bar() {} } + ClassMethod(path, state) { + if (state.skipFile) return; + if (path.node.kind === 'constructor') return; // Skip constructors for now + + const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value)); + if (!funcName) return; + + // Get class name from parent + const classPath = path.findParent(p => t.isClassDeclaration(p) || t.isClassExpression(p)); + const className = classPath && classPath.node.id ? classPath.node.id.name : null; + + const lineNumber = path.node.loc ? path.node.loc.start.line : 0; + + if (!shouldTraceFunction(funcName, state.fileName, className, state.codeflashConfig)) { + return; + } + + // Wrap the method body + wrapMethodBody(t, path, funcName, state.fileName, lineNumber, className); + state.tracerImportAdded = true; + }, + + // Handle: const obj = { foo() {} } + ObjectMethod(path, state) { + if (state.skipFile) return; + + const funcName = path.node.key.name || (path.node.key.value && String(path.node.key.value)); + if (!funcName) return; + + const lineNumber = path.node.loc ? path.node.loc.start.line : 0; + + if (!shouldTraceFunction(funcName, state.fileName, null, state.codeflashConfig)) { + return; + } + + // Wrap the method body + wrapMethodBody(t, path, funcName, state.fileName, lineNumber, null); + state.tracerImportAdded = true; + }, + }, + }; +}; + +/** + * Create a __codeflash_tracer__.wrap() call expression. + * + * @param {Object} t - Babel types + * @param {Object} funcNode - The function AST node + * @param {string} funcName - Function name + * @param {string} fileName - File path + * @param {number} lineNumber - Line number + * @param {string|null} className - Class name + * @returns {Object} - Call expression AST node + */ +function createWrapperCall(t, funcNode, funcName, fileName, lineNumber, className) { + const args = [ + funcNode, + t.stringLiteral(funcName), + t.stringLiteral(fileName), + t.numericLiteral(lineNumber), + ]; + + if (className) { + args.push(t.stringLiteral(className)); + } else { + args.push(t.nullLiteral()); + } + + return t.callExpression( + t.memberExpression( + t.identifier('__codeflash_tracer__'), + t.identifier('wrap') + ), + args + ); +} + +/** + * Wrap a function declaration's body with tracing. + * Transforms: + * function foo(a, b) { return a + b; } + * Into: + * function foo(a, b) { + * const __original__ = function(a, b) { return a + b; }; + * return __codeflash_tracer__.wrap(__original__, 'foo', 'file.js', 1, null).apply(this, arguments); + * } + * + * @param {Object} t - Babel types + * @param {Object} path - Babel path + * @param {string} funcName - Function name + * @param {string} fileName - File path + * @param {number} lineNumber - Line number + * @param {string|null} className - Class name + */ +function wrapFunctionBody(t, path, funcName, fileName, lineNumber, className) { + const node = path.node; + const isAsync = node.async; + const isGenerator = node.generator; + + // Create a copy of the original function as an expression + const originalFunc = t.functionExpression( + null, // anonymous + node.params, + node.body, + isGenerator, + isAsync + ); + + // Create the wrapper call + const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className); + + // Create: return __codeflash_tracer__.wrap(...).apply(this, arguments) + const applyCall = t.callExpression( + t.memberExpression(wrapperCall, t.identifier('apply')), + [t.thisExpression(), t.identifier('arguments')] + ); + + const returnStatement = t.returnStatement(applyCall); + + // Replace the function body + node.body = t.blockStatement([returnStatement]); +} + +/** + * Wrap a method's body with tracing. + * Similar to wrapFunctionBody but preserves method semantics. + * + * @param {Object} t - Babel types + * @param {Object} path - Babel path + * @param {string} funcName - Function name + * @param {string} fileName - File path + * @param {number} lineNumber - Line number + * @param {string|null} className - Class name + */ +function wrapMethodBody(t, path, funcName, fileName, lineNumber, className) { + const node = path.node; + const isAsync = node.async; + const isGenerator = node.generator; + + // Create a copy of the original function as an expression + const originalFunc = t.functionExpression( + null, // anonymous + node.params, + node.body, + isGenerator, + isAsync + ); + + // Create the wrapper call + const wrapperCall = createWrapperCall(t, originalFunc, funcName, fileName, lineNumber, className); + + // Create: return __codeflash_tracer__.wrap(...).apply(this, arguments) + const applyCall = t.callExpression( + t.memberExpression(wrapperCall, t.identifier('apply')), + [t.thisExpression(), t.identifier('arguments')] + ); + + let returnStatement; + if (isAsync) { + // For async methods, we need to await the result + returnStatement = t.returnStatement(t.awaitExpression(applyCall)); + } else { + returnStatement = t.returnStatement(applyCall); + } + + // Replace the function body + node.body = t.blockStatement([returnStatement]); +} + +// Export helper functions for testing +module.exports.shouldTraceFunction = shouldTraceFunction; +module.exports.isExcludedPath = isExcludedPath; +module.exports.getEnvConfig = getEnvConfig; diff --git a/packages/codeflash/runtime/index.js b/packages/codeflash/runtime/index.js index 982912c24..864b03066 100644 --- a/packages/codeflash/runtime/index.js +++ b/packages/codeflash/runtime/index.js @@ -8,6 +8,8 @@ * - capturePerf: Capture performance metrics (timing only) * - serialize/deserialize: Value serialization for storage * - comparator: Deep equality comparison + * - tracer: Function tracing for replay test generation + * - replay: Replay test utilities * * Usage (CommonJS): * const { capture, capturePerf } = require('codeflash'); @@ -30,6 +32,22 @@ const comparator = require('./comparator'); // Result comparison (used by CLI) const compareResults = require('./compare-results'); +// Function tracing (for replay test generation) +let tracer = null; +try { + tracer = require('./tracer'); +} catch (e) { + // Tracer may not be available if better-sqlite3 is not installed +} + +// Replay test utilities +let replay = null; +try { + replay = require('./replay'); +} catch (e) { + // Replay may not be available +} + // Re-export all public APIs module.exports = { // === Main Instrumentation API === @@ -88,4 +106,24 @@ module.exports = { // === Feature Detection === hasV8: serializer.hasV8, hasMsgpack: serializer.hasMsgpack, + + // === Function Tracing (for replay test generation) === + tracer: tracer ? { + init: tracer.init, + wrap: tracer.wrap, + createWrapper: tracer.createWrapper, + disable: tracer.disable, + enable: tracer.enable, + getStats: tracer.getStats, + } : null, + + // === Replay Test Utilities === + replay: replay ? { + getNextArg: replay.getNextArg, + getTracesWithMetadata: replay.getTracesWithMetadata, + getTracedFunctions: replay.getTracedFunctions, + getTraceMetadata: replay.getTraceMetadata, + generateReplayTest: replay.generateReplayTest, + createReplayTestFromTrace: replay.createReplayTestFromTrace, + } : null, }; diff --git a/packages/codeflash/runtime/replay.js b/packages/codeflash/runtime/replay.js new file mode 100644 index 000000000..733fed41e --- /dev/null +++ b/packages/codeflash/runtime/replay.js @@ -0,0 +1,454 @@ +/** + * Codeflash Replay Test Utilities + * + * This module provides utilities for generating and running replay tests + * from traced function calls. Replay tests allow verifying that optimized + * code produces the same results as the original code. + * + * Usage: + * const { getNextArg, createReplayTest } = require('codeflash/replay'); + * + * // In a test file: + * describe('Replay tests', () => { + * test.each(getNextArg(traceFile, 'myFunction', '/path/file.js', 25)) + * ('myFunction replay %#', (args) => { + * myFunction(...args); + * }); + * }); + * + * The module supports both Jest and Vitest test frameworks. + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +// Load the codeflash serializer for argument deserialization +const serializer = require('./serializer'); + +// ============================================================================ +// DATABASE ACCESS +// ============================================================================ + +/** + * Open a SQLite database connection. + * + * @param {string} dbPath - Path to the SQLite database + * @returns {Object|null} - Database connection or null if failed + */ +function openDatabase(dbPath) { + try { + const Database = require('better-sqlite3'); + return new Database(dbPath, { readonly: true }); + } catch (e) { + console.error('[codeflash-replay] Failed to open database:', e.message); + return null; + } +} + +/** + * Get traced function calls from the database. + * + * @param {string} traceFile - Path to the trace SQLite database + * @param {string} functionName - Name of the function + * @param {string} fileName - Path to the source file + * @param {string|null} className - Class name (for methods) + * @param {number} limit - Maximum number of traces to retrieve + * @returns {Array} - Array of traced arguments + */ +function getNextArg(traceFile, functionName, fileName, limit = 25, className = null) { + const db = openDatabase(traceFile); + if (!db) { + return []; + } + + try { + let stmt; + let rows; + + if (className) { + stmt = db.prepare(` + SELECT args FROM function_calls + WHERE function = ? AND filename = ? AND classname = ? AND type = 'call' + ORDER BY time_ns ASC + LIMIT ? + `); + rows = stmt.all(functionName, fileName, className, limit); + } else { + stmt = db.prepare(` + SELECT args FROM function_calls + WHERE function = ? AND filename = ? AND type = 'call' + ORDER BY time_ns ASC + LIMIT ? + `); + rows = stmt.all(functionName, fileName, limit); + } + + db.close(); + + // Deserialize arguments + return rows.map((row, index) => { + try { + const args = serializer.deserialize(row.args); + return args; + } catch (e) { + console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message); + return []; + } + }); + } catch (e) { + console.error('[codeflash-replay] Database query failed:', e.message); + db.close(); + return []; + } +} + +/** + * Get traced function calls with full metadata. + * + * @param {string} traceFile - Path to the trace SQLite database + * @param {string} functionName - Name of the function + * @param {string} fileName - Path to the source file + * @param {string|null} className - Class name (for methods) + * @param {number} limit - Maximum number of traces to retrieve + * @returns {Array} - Array of trace objects with args and metadata + */ +function getTracesWithMetadata(traceFile, functionName, fileName, limit = 25, className = null) { + const db = openDatabase(traceFile); + if (!db) { + return []; + } + + try { + let stmt; + let rows; + + if (className) { + stmt = db.prepare(` + SELECT type, function, classname, filename, line_number, time_ns, args + FROM function_calls + WHERE function = ? AND filename = ? AND classname = ? AND type = 'call' + ORDER BY time_ns ASC + LIMIT ? + `); + rows = stmt.all(functionName, fileName, className, limit); + } else { + stmt = db.prepare(` + SELECT type, function, classname, filename, line_number, time_ns, args + FROM function_calls + WHERE function = ? AND filename = ? AND type = 'call' + ORDER BY time_ns ASC + LIMIT ? + `); + rows = stmt.all(functionName, fileName, limit); + } + + db.close(); + + // Deserialize arguments and return with metadata + return rows.map((row, index) => { + let args; + try { + args = serializer.deserialize(row.args); + } catch (e) { + console.warn(`[codeflash-replay] Failed to deserialize args at index ${index}:`, e.message); + args = []; + } + + return { + args, + function: row.function, + className: row.classname, + fileName: row.filename, + lineNumber: row.line_number, + timeNs: row.time_ns, + }; + }); + } catch (e) { + console.error('[codeflash-replay] Database query failed:', e.message); + db.close(); + return []; + } +} + +/** + * Get all traced functions from the database. + * + * @param {string} traceFile - Path to the trace SQLite database + * @returns {Array} - Array of { function, fileName, className, count } objects + */ +function getTracedFunctions(traceFile) { + const db = openDatabase(traceFile); + if (!db) { + return []; + } + + try { + const stmt = db.prepare(` + SELECT function, filename, classname, COUNT(*) as count + FROM function_calls + WHERE type = 'call' + GROUP BY function, filename, classname + ORDER BY count DESC + `); + const rows = stmt.all(); + db.close(); + + return rows.map(row => ({ + function: row.function, + fileName: row.filename, + className: row.classname, + count: row.count, + })); + } catch (e) { + console.error('[codeflash-replay] Failed to get traced functions:', e.message); + db.close(); + return []; + } +} + +/** + * Get metadata from the trace database. + * + * @param {string} traceFile - Path to the trace SQLite database + * @returns {Object} - Metadata key-value pairs + */ +function getTraceMetadata(traceFile) { + const db = openDatabase(traceFile); + if (!db) { + return {}; + } + + try { + const stmt = db.prepare('SELECT key, value FROM metadata'); + const rows = stmt.all(); + db.close(); + + const metadata = {}; + for (const row of rows) { + metadata[row.key] = row.value; + } + return metadata; + } catch (e) { + console.error('[codeflash-replay] Failed to get metadata:', e.message); + db.close(); + return {}; + } +} + +// ============================================================================ +// TEST GENERATION +// ============================================================================ + +/** + * Generate a Jest/Vitest replay test file. + * + * @param {string} traceFile - Path to the trace SQLite database + * @param {Array} functions - Array of { function, fileName, className, modulePath } to test + * @param {Object} options - Generation options + * @returns {string} - Generated test file content + */ +function generateReplayTest(traceFile, functions, options = {}) { + const { + framework = 'jest', // 'jest' or 'vitest' + maxRunCount = 100, + outputPath = null, + } = options; + + const isVitest = framework === 'vitest'; + + // Build imports section + const imports = []; + + if (isVitest) { + imports.push("import { describe, test } from 'vitest';"); + } + + imports.push("const { getNextArg } = require('codeflash/replay');"); + imports.push(''); + + // Build function imports + for (const func of functions) { + const alias = getFunctionAlias(func.modulePath, func.function, func.className); + + if (func.className) { + // Import class for method testing + imports.push(`const { ${func.className}: ${alias}_class } = require('${func.modulePath}');`); + } else { + // Import function directly + imports.push(`const { ${func.function}: ${alias} } = require('${func.modulePath}');`); + } + } + + imports.push(''); + + // Metadata + const metadata = [ + `const traceFilePath = '${traceFile}';`, + `const functions = ${JSON.stringify(functions.map(f => f.function))};`, + '', + ]; + + // Build test cases + const testCases = []; + + for (const func of functions) { + const alias = getFunctionAlias(func.modulePath, func.function, func.className); + const testName = func.className + ? `${func.className}.${func.function}` + : func.function; + + if (func.className) { + // Method test + testCases.push(` +describe('Replay: ${testName}', () => { + const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount}, '${func.className}'); + + test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => { + // For instance methods, first arg is 'this' context + const [thisArg, ...methodArgs] = args; + const instance = thisArg || new ${alias}_class(); + instance.${func.function}(...methodArgs); + }); +}); +`); + } else { + // Function test + testCases.push(` +describe('Replay: ${testName}', () => { + const traces = getNextArg(traceFilePath, '${func.function}', '${func.fileName}', ${maxRunCount}); + + test.each(traces.map((args, i) => [i, args]))('call %i', (index, args) => { + ${alias}(...args); + }); +}); +`); + } + } + + // Combine all parts + const content = [ + '// Auto-generated replay test by Codeflash', + '// Do not edit this file directly', + '', + ...imports, + ...metadata, + ...testCases, + ].join('\n'); + + // Write to file if outputPath provided + if (outputPath) { + const dir = path.dirname(outputPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(outputPath, content); + console.log(`[codeflash-replay] Generated test file: ${outputPath}`); + } + + return content; +} + +/** + * Create a function alias for imports to avoid naming conflicts. + * + * @param {string} modulePath - Module path + * @param {string} functionName - Function name + * @param {string|null} className - Class name + * @returns {string} - Alias name + */ +function getFunctionAlias(modulePath, functionName, className = null) { + // Normalize module path to valid identifier + const moduleAlias = modulePath + .replace(/[^a-zA-Z0-9]/g, '_') + .replace(/^_+|_+$/g, ''); + + if (className) { + return `${moduleAlias}_${className}_${functionName}`; + } + return `${moduleAlias}_${functionName}`; +} + +/** + * Create replay tests from a trace file. + * This is the main entry point for Python integration. + * + * @param {string} traceFile - Path to the trace SQLite database + * @param {string} outputPath - Path to write the test file + * @param {Object} options - Generation options + * @returns {Object} - { success, outputPath, functions } + */ +function createReplayTestFromTrace(traceFile, outputPath, options = {}) { + const { + framework = 'jest', + maxRunCount = 100, + projectRoot = process.cwd(), + } = options; + + // Get all traced functions + const tracedFunctions = getTracedFunctions(traceFile); + + if (tracedFunctions.length === 0) { + console.warn('[codeflash-replay] No traced functions found in database'); + return { success: false, outputPath: null, functions: [] }; + } + + // Convert to the format expected by generateReplayTest + const functions = tracedFunctions.map(tf => { + // Calculate module path from file name + let modulePath = tf.fileName; + + // Make relative to project root + if (path.isAbsolute(modulePath)) { + modulePath = path.relative(projectRoot, modulePath); + } + + // Convert to module path (remove .js extension, use forward slashes) + modulePath = './' + modulePath + .replace(/\\/g, '/') + .replace(/\.js$/, '') + .replace(/\.ts$/, ''); + + return { + function: tf.function, + fileName: tf.fileName, + className: tf.className, + modulePath, + }; + }); + + // Generate the test file + const testContent = generateReplayTest(traceFile, functions, { + framework, + maxRunCount, + outputPath, + }); + + return { + success: true, + outputPath, + functions: functions.map(f => f.function), + content: testContent, + }; +} + +// ============================================================================ +// EXPORTS +// ============================================================================ + +module.exports = { + // Core API + getNextArg, + getTracesWithMetadata, + getTracedFunctions, + getTraceMetadata, + + // Test generation + generateReplayTest, + createReplayTestFromTrace, + getFunctionAlias, + + // Database utilities + openDatabase, +}; diff --git a/packages/codeflash/runtime/trace-runner.js b/packages/codeflash/runtime/trace-runner.js new file mode 100644 index 000000000..2a14b924f --- /dev/null +++ b/packages/codeflash/runtime/trace-runner.js @@ -0,0 +1,395 @@ +#!/usr/bin/env node +/** + * Codeflash Trace Runner + * + * Entry point script that runs JavaScript/TypeScript code with function tracing enabled. + * This script: + * 1. Registers Babel with the tracer plugin for AST transformation + * 2. Sets up environment variables for tracing configuration + * 3. Runs the user's script, tests, or module + * + * Usage: + * # Run a script with tracing + * node trace-runner.js script.js + * + * # Run tests with tracing (Jest) + * node trace-runner.js --jest -- --testPathPattern=mytest + * + * # Run tests with tracing (Vitest) + * node trace-runner.js --vitest -- --run + * + * # Run with specific functions to trace + * node trace-runner.js --functions='["myFunc","otherFunc"]' script.js + * + * Environment Variables (also settable via command line): + * CODEFLASH_TRACE_DB - Path to SQLite database for storing traces + * CODEFLASH_PROJECT_ROOT - Project root for relative path calculation + * CODEFLASH_FUNCTIONS - JSON array of functions to trace + * CODEFLASH_MAX_FUNCTION_COUNT - Maximum traces per function (default: 256) + * CODEFLASH_TRACER_TIMEOUT - Timeout in seconds for tracing + * + * For ESM (ECMAScript modules), use the loader flag: + * node --loader ./esm-loader.mjs trace-runner.js script.mjs + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +// ============================================================================ +// ARGUMENT PARSING +// ============================================================================ + +function parseArgs(args) { + const config = { + traceDb: process.env.CODEFLASH_TRACE_DB || path.join(process.cwd(), 'codeflash.trace.sqlite'), + projectRoot: process.env.CODEFLASH_PROJECT_ROOT || process.cwd(), + functions: process.env.CODEFLASH_FUNCTIONS || null, + maxFunctionCount: process.env.CODEFLASH_MAX_FUNCTION_COUNT || '256', + tracerTimeout: process.env.CODEFLASH_TRACER_TIMEOUT || null, + traceFiles: process.env.CODEFLASH_TRACE_FILES || null, + traceExclude: process.env.CODEFLASH_TRACE_EXCLUDE || null, + jest: false, + vitest: false, + module: false, + script: null, + scriptArgs: [], + }; + + let i = 0; + while (i < args.length) { + const arg = args[i]; + + if (arg === '--trace-db') { + config.traceDb = args[++i]; + } else if (arg.startsWith('--trace-db=')) { + config.traceDb = arg.split('=')[1]; + } else if (arg === '--project-root') { + config.projectRoot = args[++i]; + } else if (arg.startsWith('--project-root=')) { + config.projectRoot = arg.split('=')[1]; + } else if (arg === '--functions') { + config.functions = args[++i]; + } else if (arg.startsWith('--functions=')) { + config.functions = arg.split('=')[1]; + } else if (arg === '--max-function-count') { + config.maxFunctionCount = args[++i]; + } else if (arg.startsWith('--max-function-count=')) { + config.maxFunctionCount = arg.split('=')[1]; + } else if (arg === '--timeout') { + config.tracerTimeout = args[++i]; + } else if (arg.startsWith('--timeout=')) { + config.tracerTimeout = arg.split('=')[1]; + } else if (arg === '--trace-files') { + config.traceFiles = args[++i]; + } else if (arg.startsWith('--trace-files=')) { + config.traceFiles = arg.split('=')[1]; + } else if (arg === '--trace-exclude') { + config.traceExclude = args[++i]; + } else if (arg.startsWith('--trace-exclude=')) { + config.traceExclude = arg.split('=')[1]; + } else if (arg === '--jest') { + config.jest = true; + } else if (arg === '--vitest') { + config.vitest = true; + } else if (arg === '-m' || arg === '--module') { + config.module = true; + } else if (arg === '--') { + // Everything after -- is passed to the script/test runner + config.scriptArgs = args.slice(i + 1); + break; + } else if (arg === '--help' || arg === '-h') { + printHelp(); + process.exit(0); + } else if (!arg.startsWith('-')) { + // First non-flag argument is the script + config.script = arg; + config.scriptArgs = args.slice(i + 1); + break; + } + + i++; + } + + return config; +} + +function printHelp() { + console.log(` +Codeflash Trace Runner - JavaScript Function Tracing + +Usage: + trace-runner [options]