Merge branch 'main' into fix/js-jest30-loop-runner

This commit is contained in:
mohammed ahmed 2026-02-03 19:18:20 +02:00 committed by GitHub
commit 4c61d08ef9
18 changed files with 588 additions and 240 deletions

View file

@ -359,11 +359,13 @@ def _handle_show_config() -> None:
detected = detect_project(project_root)
# Check if config exists or is auto-detected
config_exists, _ = has_existing_config(project_root)
config_exists, config_file = has_existing_config(project_root)
status = "Saved config" if config_exists else "Auto-detected (not saved)"
console.print()
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
if config_exists and config_file:
console.print(f"[dim]Config file: {project_root / config_file}[/dim]")
console.print()
table = Table(show_header=True, header_style="bold cyan")

View file

@ -6,6 +6,8 @@ import json
from pathlib import Path
from typing import Any
from codeflash.setup.detector import is_build_output_dir
PACKAGE_JSON_CACHE: dict[Path, Path] = {}
PACKAGE_JSON_DATA_CACHE: dict[Path, dict[str, Any]] = {}
@ -50,12 +52,15 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
"""Detect module root from package.json fields or directory conventions.
Detection order:
1. package.json "exports" field (extract directory from main export)
2. package.json "module" field (ESM entry point)
3. package.json "main" field (CJS entry point)
4. "src/" directory if it exists
1. src/, lib/, source/ directories (common source directories)
2. package.json "exports" field (if not in build output directory)
3. package.json "module" field (ESM, if not in build output directory)
4. package.json "main" field (CJS, if not in build output directory)
5. Fall back to "." (project root)
Build output directories (build/, dist/, out/) are skipped since they contain
compiled code, not source files.
Args:
project_root: Root directory of the project.
package_data: Parsed package.json data.
@ -64,6 +69,11 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
Detected module root path (relative to project root).
"""
# Check for common source directories first - these are always preferred
for src_dir in ["src", "lib", "source"]:
if (project_root / src_dir).is_dir():
return src_dir
# Check exports field (modern Node.js)
exports = package_data.get("exports")
if exports:
@ -80,27 +90,38 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
if entry_path and isinstance(entry_path, str):
parent = Path(entry_path).parent
if parent != Path() and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return parent.as_posix()
# Check module field (ESM)
module_field = package_data.get("module")
if module_field and isinstance(module_field, str):
parent = Path(module_field).parent
if parent != Path() and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return parent.as_posix()
# Check main field (CJS)
main_field = package_data.get("main")
if main_field and isinstance(main_field, str):
parent = Path(main_field).parent
if parent != Path() and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return parent.as_posix()
# Check for src/ directory convention
if (project_root / "src").is_dir():
return "src"
# Default to project root
return "."

View file

@ -222,6 +222,7 @@ def _convert_destructuring_to_imports(names_str: str) -> str:
Returns:
Import names string with aliases using 'as' syntax
"""
# Split by commas and process each name
parts = []

View file

@ -296,6 +296,33 @@ def _find_node_project_root(file_path: Path) -> Path | None:
return None
def _find_monorepo_root(start_path: Path) -> Path | None:
"""Find the monorepo workspace root by looking for workspace markers.
Traverses up from the given path to find a directory containing
monorepo workspace markers like yarn.lock, pnpm-workspace.yaml, etc.
Args:
start_path: A path within the monorepo.
Returns:
The monorepo root directory, or None if not found.
"""
monorepo_markers = ["yarn.lock", "pnpm-workspace.yaml", "lerna.json", "package-lock.json"]
current = start_path if start_path.is_dir() else start_path.parent
while current != current.parent:
# Check for monorepo markers
if any((current / marker).exists() for marker in monorepo_markers):
# Verify it has node_modules (it's the workspace root)
if (current / "node_modules").exists():
return current
current = current.parent
return None
def _find_jest_config(project_root: Path) -> Path | None:
"""Find Jest configuration file in the project.
@ -797,6 +824,12 @@ def run_jest_benchmarking_tests(
jest_env["JEST_JUNIT_SUITE_NAME"] = "{filepath}"
jest_env["JEST_JUNIT_ADD_FILE_ATTRIBUTE"] = "true"
jest_env["JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT"] = "true"
# Pass monorepo root to loop-runner for jest-runner resolution
monorepo_root = _find_monorepo_root(effective_cwd)
if monorepo_root:
jest_env["CODEFLASH_MONOREPO_ROOT"] = str(monorepo_root)
logger.debug(f"Detected monorepo root: {monorepo_root}")
codeflash_sqlite_file = get_run_tmp_file(Path("test_return_values_0.sqlite"))
jest_env["CODEFLASH_OUTPUT_FILE"] = str(codeflash_sqlite_file)
jest_env["CODEFLASH_TEST_ITERATION"] = "0"

View file

@ -21,6 +21,8 @@ from typing import Any
import tomlkit
_BUILD_DIRS = frozenset({"build", "dist", "out", ".next", ".nuxt"})
@dataclass
class DetectedProject:
@ -310,14 +312,21 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
"""Detect JavaScript/TypeScript module root.
Priority:
1. package.json "exports" field
2. package.json "module" field (ESM)
3. package.json "main" field (CJS)
4. src/ directory
5. lib/ directory
6. Project root
1. src/, lib/, source/ directories (common source directories)
2. package.json "exports" field (if not in build output directory)
3. package.json "module" field (ESM, if not in build output directory)
4. package.json "main" field (CJS, if not in build output directory)
5. Project root
Build output directories (build/, dist/, out/) are skipped since they contain
compiled code, not source files.
"""
# Check for common source directories first - these are always preferred
for src_dir in ["src", "lib", "source"]:
if (project_root / src_dir).is_dir():
return project_root / src_dir, f"{src_dir}/ directory"
package_json_path = project_root / "package.json"
package_data: dict[str, Any] = {}
@ -334,32 +343,52 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
entry_path = _extract_entry_path(exports)
if entry_path:
parent = Path(entry_path).parent
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return project_root / parent, f'{parent.as_posix()}/ (from package.json "exports")'
# Check module field (ESM)
module_field = package_data.get("module")
if module_field and isinstance(module_field, str):
parent = Path(module_field).parent
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return project_root / parent, f'{parent.as_posix()}/ (from package.json "module")'
# Check main field (CJS)
main_field = package_data.get("main")
if main_field and isinstance(main_field, str):
parent = Path(main_field).parent
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
if (
parent != Path()
and parent.as_posix() != "."
and (project_root / parent).is_dir()
and not is_build_output_dir(parent)
):
return project_root / parent, f'{parent.as_posix()}/ (from package.json "main")'
# Check for common source directories
for src_dir in ["src", "lib", "source"]:
if (project_root / src_dir).is_dir():
return project_root / src_dir, f"{src_dir}/ directory"
# Default to project root
return project_root, "project root"
def is_build_output_dir(path: Path) -> bool:
"""Check if a path is within a common build output directory.
Build output directories contain compiled code and should be skipped
in favor of source directories.
"""
return not _BUILD_DIRS.isdisjoint(path.parts)
def _extract_entry_path(exports: Any) -> str | None:
"""Extract entry path from package.json exports field."""
if isinstance(exports, str):

View file

@ -150,19 +150,31 @@ def resolve_test_file_from_class_path(test_class_path: str, base_dir: Path) -> P
# Handle file paths (contain slashes and extensions like .js/.ts)
if "/" in test_class_path or "\\" in test_class_path:
# This is a file path, not a Python module path
# Try the path as-is if it's absolute
potential_path = Path(test_class_path)
if potential_path.is_absolute() and potential_path.exists():
return potential_path
# Try to resolve relative to base_dir's parent (project root)
project_root = base_dir.parent
potential_path = project_root / test_class_path
if potential_path.exists():
return potential_path
# Normalize to resolve .. and . components
try:
potential_path = potential_path.resolve()
if potential_path.exists():
return potential_path
except (OSError, RuntimeError):
pass
# Also try relative to base_dir itself
potential_path = base_dir / test_class_path
if potential_path.exists():
return potential_path
# Try the path as-is if it's absolute
potential_path = Path(test_class_path)
if potential_path.exists():
return potential_path
try:
potential_path = potential_path.resolve()
if potential_path.exists():
return potential_path
except (OSError, RuntimeError):
pass
return None
# First try the full path (Python module path)
@ -731,16 +743,25 @@ def parse_jest_test_xml(
if not test_file_path.exists():
test_file_path = base_dir / test_file_name
if test_file_path is None or not test_file_path.exists():
# For Jest tests in monorepos, test files may not exist after cleanup
# but we can still parse results and infer test type from the path
if test_file_path is None:
logger.warning(f"Could not resolve test file for Jest test: {test_class_path}")
continue
# Get test type if not already set from lookup
if test_type is None:
if test_type is None and test_file_path.exists():
test_type = test_files.get_test_type_by_instrumented_file_path(test_file_path)
if test_type is None:
# Default to GENERATED_REGRESSION for Jest tests
test_type = TestType.GENERATED_REGRESSION
# Infer test type from filename pattern
filename = test_file_path.name
if "__perf_test_" in filename or "_perf_test_" in filename:
test_type = TestType.GENERATED_PERFORMANCE
elif "__unit_test_" in filename or "_unit_test_" in filename:
test_type = TestType.GENERATED_REGRESSION
else:
# Default to GENERATED_REGRESSION for Jest tests
test_type = TestType.GENERATED_REGRESSION
# For Jest tests, keep the relative file path with extension intact
# (Python uses module_name_from_file_path which strips extensions)

View file

@ -1,60 +0,0 @@
# codeflash
AI-powered code performance optimization for JavaScript and TypeScript.
## Installation
```bash
npm install -g codeflash
# or
npx codeflash
```
## Quick Start
1. Get your API key from [codeflash.ai](https://codeflash.ai)
2. Set your API key:
```bash
export CODEFLASH_API_KEY=your-api-key
```
3. Optimize a function:
```bash
codeflash --file src/utils.ts --function slowFunction
```
## Usage
```bash
# Optimize a specific function
codeflash --file <path> --function <name>
# Optimize all functions in a directory
codeflash --all src/
# Initialize GitHub Actions workflow
codeflash init-actions
# Verify setup
codeflash --verify-setup
```
## Requirements
- Node.js >= 16.0.0
- A codeflash API key
## Supported Platforms
- Linux (x64, arm64)
- macOS (x64, arm64)
- Windows (x64)
## Documentation
See [codeflash.ai/docs](https://codeflash.ai/docs) for full documentation.
## License
BSL-1.1

View file

@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* Wrapper script for codeflash CLI.
* Invokes the downloaded binary with all passed arguments.
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
function getBinaryPath() {
const binDir = __dirname;
const isWindows = process.platform === 'win32';
return path.join(binDir, isWindows ? 'codeflash.exe' : 'codeflash-binary');
}
function main() {
const binaryPath = getBinaryPath();
if (!fs.existsSync(binaryPath)) {
console.error('\x1b[31mError: codeflash binary not found.\x1b[0m');
console.error('Try reinstalling: npm install codeflash');
process.exit(1);
}
// Pass all arguments to the binary
const args = process.argv.slice(2);
const child = spawn(binaryPath, args, {
stdio: 'inherit',
env: process.env,
});
child.on('error', (error) => {
console.error(`\x1b[31mError running codeflash: ${error.message}\x1b[0m`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.exit(1);
}
process.exit(code || 0);
});
}
main();

View file

@ -1,47 +0,0 @@
{
"name": "codeflash",
"version": "0.0.0",
"description": "AI-powered code performance optimization - automatically find and fix slow code",
"keywords": [
"codeflash",
"performance",
"optimization",
"ai",
"code",
"profiler",
"typescript",
"javascript"
],
"author": "CodeFlash Inc. <contact@codeflash.ai>",
"license": "BSL-1.1",
"homepage": "https://codeflash.ai",
"repository": {
"type": "git",
"url": "git+https://github.com/codeflash-ai/codeflash.git"
},
"bugs": {
"url": "https://github.com/codeflash-ai/codeflash/issues"
},
"bin": {
"codeflash": "./bin/codeflash"
},
"scripts": {
"postinstall": "node lib/install.js"
},
"engines": {
"node": ">=16.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"files": [
"bin/",
"lib/"
]
}

View file

@ -1,12 +1,12 @@
{
"name": "codeflash",
"version": "0.5.0",
"version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeflash",
"version": "0.5.0",
"version": "0.7.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "codeflash",
"version": "0.5.0",
"version": "0.7.0",
"description": "Codeflash - AI-powered code optimization for JavaScript and TypeScript",
"main": "runtime/index.js",
"types": "runtime/index.d.ts",

View file

@ -0,0 +1,73 @@
/**
* Test: Dynamic environment variable reading
*
* This test verifies that the performance configuration functions read
* environment variables at runtime rather than at module load time.
*
* This is critical for Vitest compatibility, where modules may be cached
* and loaded before environment variables are set.
*
* Run with: node __tests__/dynamic-env-vars.test.js
*/
const assert = require('assert');
// Clear any existing env vars before loading the module
delete process.env.CODEFLASH_PERF_LOOP_COUNT;
delete process.env.CODEFLASH_PERF_MIN_LOOPS;
delete process.env.CODEFLASH_PERF_TARGET_DURATION_MS;
delete process.env.CODEFLASH_PERF_BATCH_SIZE;
delete process.env.CODEFLASH_PERF_STABILITY_CHECK;
delete process.env.CODEFLASH_PERF_CURRENT_BATCH;
// Now load the module - at this point env vars are not set
const capture = require('../capture');
console.log('Testing dynamic environment variable reading...\n');
// Test 1: Default values when env vars are not set
console.log('Test 1: Default values');
assert.strictEqual(capture.getPerfLoopCount(), 1, 'getPerfLoopCount default should be 1');
assert.strictEqual(capture.getPerfMinLoops(), 5, 'getPerfMinLoops default should be 5');
assert.strictEqual(capture.getPerfTargetDurationMs(), 10000, 'getPerfTargetDurationMs default should be 10000');
assert.strictEqual(capture.getPerfBatchSize(), 10, 'getPerfBatchSize default should be 10');
assert.strictEqual(capture.getPerfStabilityCheck(), false, 'getPerfStabilityCheck default should be false');
assert.strictEqual(capture.getPerfCurrentBatch(), 0, 'getPerfCurrentBatch default should be 0');
console.log(' PASS: All defaults correct\n');
// Test 2: Values change when env vars are set AFTER module load
// This is the critical test - if these were constants, they would still return defaults
console.log('Test 2: Dynamic reading after module load');
process.env.CODEFLASH_PERF_LOOP_COUNT = '100';
process.env.CODEFLASH_PERF_MIN_LOOPS = '10';
process.env.CODEFLASH_PERF_TARGET_DURATION_MS = '5000';
process.env.CODEFLASH_PERF_BATCH_SIZE = '20';
process.env.CODEFLASH_PERF_STABILITY_CHECK = 'true';
process.env.CODEFLASH_PERF_CURRENT_BATCH = '5';
assert.strictEqual(capture.getPerfLoopCount(), 100, 'getPerfLoopCount should read 100 from env');
assert.strictEqual(capture.getPerfMinLoops(), 10, 'getPerfMinLoops should read 10 from env');
assert.strictEqual(capture.getPerfTargetDurationMs(), 5000, 'getPerfTargetDurationMs should read 5000 from env');
assert.strictEqual(capture.getPerfBatchSize(), 20, 'getPerfBatchSize should read 20 from env');
assert.strictEqual(capture.getPerfStabilityCheck(), true, 'getPerfStabilityCheck should read true from env');
assert.strictEqual(capture.getPerfCurrentBatch(), 5, 'getPerfCurrentBatch should read 5 from env');
console.log(' PASS: Dynamic reading works correctly\n');
// Test 3: Values change again when env vars are modified
console.log('Test 3: Values update when env vars change');
process.env.CODEFLASH_PERF_LOOP_COUNT = '500';
process.env.CODEFLASH_PERF_BATCH_SIZE = '50';
assert.strictEqual(capture.getPerfLoopCount(), 500, 'getPerfLoopCount should update to 500');
assert.strictEqual(capture.getPerfBatchSize(), 50, 'getPerfBatchSize should update to 50');
console.log(' PASS: Values update correctly\n');
// Cleanup
delete process.env.CODEFLASH_PERF_LOOP_COUNT;
delete process.env.CODEFLASH_PERF_MIN_LOOPS;
delete process.env.CODEFLASH_PERF_TARGET_DURATION_MS;
delete process.env.CODEFLASH_PERF_BATCH_SIZE;
delete process.env.CODEFLASH_PERF_STABILITY_CHECK;
delete process.env.CODEFLASH_PERF_CURRENT_BATCH;
console.log('All tests passed!');

View file

@ -47,14 +47,30 @@ const TEST_MODULE = process.env.CODEFLASH_TEST_MODULE;
// Batch 1: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
// Batch 2: Test1(5 loops) → Test2(5 loops) → Test3(5 loops)
// ...until time budget exhausted
const PERF_LOOP_COUNT = parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '1', 10);
const PERF_MIN_LOOPS = parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
const PERF_TARGET_DURATION_MS = parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
const PERF_BATCH_SIZE = parseInt(process.env.CODEFLASH_PERF_BATCH_SIZE || '10', 10);
const PERF_STABILITY_CHECK = (process.env.CODEFLASH_PERF_STABILITY_CHECK || 'false').toLowerCase() === 'true';
//
// IMPORTANT: These are getter functions, NOT constants!
// Vitest caches modules and may load this file before env vars are set.
// Using getter functions ensures we read the env vars at runtime when they're actually needed.
function getPerfLoopCount() {
return parseInt(process.env.CODEFLASH_PERF_LOOP_COUNT || '1', 10);
}
function getPerfMinLoops() {
return parseInt(process.env.CODEFLASH_PERF_MIN_LOOPS || '5', 10);
}
function getPerfTargetDurationMs() {
return parseInt(process.env.CODEFLASH_PERF_TARGET_DURATION_MS || '10000', 10);
}
function getPerfBatchSize() {
return parseInt(process.env.CODEFLASH_PERF_BATCH_SIZE || '10', 10);
}
function getPerfStabilityCheck() {
return (process.env.CODEFLASH_PERF_STABILITY_CHECK || 'false').toLowerCase() === 'true';
}
// Current batch number - set by loop-runner before each batch
// This allows continuous loop indices even when Jest resets module state
const PERF_CURRENT_BATCH = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '0', 10);
function getPerfCurrentBatch() {
return parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '0', 10);
}
// Stability constants (matching Python's config_consts.py)
const STABILITY_WINDOW_SIZE = 0.35;
@ -88,7 +104,7 @@ function checkSharedTimeLimit() {
return false;
}
const elapsed = Date.now() - sharedPerfState.startTime;
if (elapsed >= PERF_TARGET_DURATION_MS && sharedPerfState.totalLoopsCompleted >= PERF_MIN_LOOPS) {
if (elapsed >= getPerfTargetDurationMs() && sharedPerfState.totalLoopsCompleted >= getPerfMinLoops()) {
sharedPerfState.shouldStop = true;
return true;
}
@ -113,7 +129,7 @@ function getInvocationLoopIndex(invocationKey) {
// Calculate global loop index using batch number from environment
// PERF_CURRENT_BATCH is 1-based (set by loop-runner before each batch)
const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
const globalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + localIndex;
const globalIndex = (currentBatch - 1) * getPerfBatchSize() + localIndex;
return globalIndex;
}
@ -608,7 +624,7 @@ function capture(funcName, lineId, fn, ...args) {
*/
function capturePerf(funcName, lineId, fn, ...args) {
// Check if we should skip looping entirely (shared time budget exceeded)
const shouldLoop = PERF_LOOP_COUNT > 1 && !checkSharedTimeLimit();
const shouldLoop = getPerfLoopCount() > 1 && !checkSharedTimeLimit();
// Get test context (computed once, reused across batch)
let testModulePath;
@ -638,9 +654,9 @@ function capturePerf(funcName, lineId, fn, ...args) {
// If so, just execute the function once without timing (for test assertions)
const peekLoopIndex = (sharedPerfState.invocationLoopCounts[invocationKey] || 0);
const currentBatch = parseInt(process.env.CODEFLASH_PERF_CURRENT_BATCH || '1', 10);
const nextGlobalIndex = (currentBatch - 1) * PERF_BATCH_SIZE + peekLoopIndex + 1;
const nextGlobalIndex = (currentBatch - 1) * getPerfBatchSize() + peekLoopIndex + 1;
if (shouldLoop && nextGlobalIndex > PERF_LOOP_COUNT) {
if (shouldLoop && nextGlobalIndex > getPerfLoopCount()) {
// All loops completed, just execute once for test assertion
return fn(...args);
}
@ -656,7 +672,7 @@ function capturePerf(funcName, lineId, fn, ...args) {
// Batched looping: run BATCH_SIZE loops per capturePerf call when using loop-runner
// For Vitest (no loop-runner), do all loops internally in a single call
const batchSize = shouldLoop
? (hasExternalLoopRunner ? PERF_BATCH_SIZE : PERF_LOOP_COUNT)
? (hasExternalLoopRunner ? getPerfBatchSize() : getPerfLoopCount())
: 1;
// Initialize runtime tracking for this invocation if needed
@ -683,7 +699,7 @@ function capturePerf(funcName, lineId, fn, ...args) {
const loopIndex = getInvocationLoopIndex(invocationKey);
// Check if we've exceeded max loops for this invocation
if (loopIndex > PERF_LOOP_COUNT) {
if (loopIndex > getPerfLoopCount()) {
break;
}
@ -991,7 +1007,11 @@ module.exports = {
LOOP_INDEX,
OUTPUT_FILE,
TEST_ITERATION,
// Batch configuration
PERF_BATCH_SIZE,
PERF_LOOP_COUNT,
// Batch configuration (getter functions for dynamic env var reading)
getPerfBatchSize,
getPerfLoopCount,
getPerfMinLoops,
getPerfTargetDurationMs,
getPerfStabilityCheck,
getPerfCurrentBatch,
};

View file

@ -32,6 +32,67 @@
const { createRequire } = require('module');
const path = require('path');
const fs = require('fs');
/**
* Resolve jest-runner with monorepo support.
* Uses CODEFLASH_MONOREPO_ROOT environment variable if available,
* otherwise walks up the directory tree looking for node_modules/jest-runner.
*/
function resolveJestRunner() {
// Try standard resolution first (works in simple projects)
try {
return require.resolve('jest-runner');
} catch (e) {
// Standard resolution failed - try monorepo-aware resolution
}
// If Python detected a monorepo root, check there first
const monorepoRoot = process.env.CODEFLASH_MONOREPO_ROOT;
if (monorepoRoot) {
const jestRunnerPath = path.join(monorepoRoot, 'node_modules', 'jest-runner');
if (fs.existsSync(jestRunnerPath)) {
const packageJsonPath = path.join(jestRunnerPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return jestRunnerPath;
}
}
}
// Fallback: Walk up from cwd looking for node_modules/jest-runner
const monorepoMarkers = ['yarn.lock', 'pnpm-workspace.yaml', 'lerna.json', 'package-lock.json'];
let currentDir = process.cwd();
const visitedDirs = new Set();
while (currentDir !== path.dirname(currentDir)) {
// Avoid infinite loops
if (visitedDirs.has(currentDir)) break;
visitedDirs.add(currentDir);
// Try node_modules/jest-runner at this level
const jestRunnerPath = path.join(currentDir, 'node_modules', 'jest-runner');
if (fs.existsSync(jestRunnerPath)) {
const packageJsonPath = path.join(jestRunnerPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
return jestRunnerPath;
}
}
// Check if this is a workspace root (has monorepo markers)
const isWorkspaceRoot = monorepoMarkers.some(marker =>
fs.existsSync(path.join(currentDir, marker))
);
if (isWorkspaceRoot) {
// Found workspace root but no jest-runner - stop searching
break;
}
currentDir = path.dirname(currentDir);
}
throw new Error('jest-runner not found');
}
// Try to load jest-runner from the PROJECT's node_modules, not from codeflash package
// This ensures we use the same version of jest-runner that the project uses
@ -41,20 +102,7 @@ let jestRunnerAvailable = false;
let jestVersion = 0;
try {
// Resolve jest-runner from the current working directory (project root)
// This is important because the codeflash package may bundle a different version
const projectRoot = process.cwd();
const projectRequire = createRequire(path.join(projectRoot, 'node_modules', 'package.json'));
let jestRunnerPath;
try {
// First try to resolve from project's node_modules
jestRunnerPath = projectRequire.resolve('jest-runner');
} catch (e) {
// Fall back to default resolution (codeflash's bundled version)
jestRunnerPath = require.resolve('jest-runner');
}
const jestRunnerPath = resolveJestRunner();
const internalRequire = createRequire(jestRunnerPath);
// Try to get the TestRunner class (Jest 30+)

View file

@ -127,13 +127,14 @@ class TestDetectModuleRoot:
assert result == "lib"
def test_detects_from_exports_object_dot(self, tmp_path: Path) -> None:
"""Should detect module root from exports object with '.' key."""
"""Should skip build output dirs and return '.' when no src dir exists."""
(tmp_path / "dist").mkdir()
package_data = {"exports": {".": "./dist/index.js"}}
result = detect_module_root(tmp_path, package_data)
assert result == "dist"
# dist is a build output directory, so it's skipped
assert result == "."
def test_detects_from_exports_object_nested(self, tmp_path: Path) -> None:
"""Should detect module root from nested exports object."""
@ -227,13 +228,14 @@ class TestDetectModuleRoot:
assert result == "src"
def test_handles_deeply_nested_exports(self, tmp_path: Path) -> None:
"""Should handle deeply nested export paths."""
"""Should handle deeply nested export paths but skip build output dirs."""
(tmp_path / "packages" / "core" / "dist").mkdir(parents=True)
package_data = {"exports": {".": {"import": "./packages/core/dist/index.mjs"}}}
result = detect_module_root(tmp_path, package_data)
assert result == "packages/core/dist"
# dist is a build output directory, so it's skipped even when nested
assert result == "."
def test_handles_empty_exports(self, tmp_path: Path) -> None:
"""Should handle empty exports gracefully."""
@ -756,7 +758,7 @@ class TestRealWorldPackageJsonExamples:
assert config["formatter_cmds"] == ["npx eslint --fix $file"]
def test_library_with_exports(self, tmp_path: Path) -> None:
"""Should handle library with modern exports field."""
"""Should handle library with modern exports field, skipping build output dirs."""
(tmp_path / "dist").mkdir()
package_json = tmp_path / "package.json"
package_json.write_text(
@ -773,7 +775,8 @@ class TestRealWorldPackageJsonExamples:
assert result is not None
config, _ = result
assert config["module_root"] == str((tmp_path / "dist").resolve())
# dist is a build output directory, so it's skipped and falls back to project root
assert config["module_root"] == str(tmp_path.resolve())
def test_monorepo_package(self, tmp_path: Path) -> None:
"""Should handle monorepo package configuration."""

View file

@ -1822,15 +1822,15 @@ export const sendSlackMessage = async (
target_func = "sendSlackMessage"
functions = ts_support.discover_functions(file_path)
func_info = next(f for f in functions if f.name == target_func)
func_info = next(f for f in functions if f.function_name == target_func)
fto = FunctionToOptimize(
function_name=target_func,
file_path=file_path,
parents=func_info.parents,
starting_line=func_info.start_line,
ending_line=func_info.end_line,
starting_col=func_info.start_col,
ending_col=func_info.end_col,
starting_line=func_info.starting_line,
ending_line=func_info.ending_line,
starting_col=func_info.starting_col,
ending_col=func_info.ending_col,
is_async=func_info.is_async,
language="typescript",
)

View file

@ -14,6 +14,7 @@ from codeflash.setup.detector import (
_find_project_root,
detect_project,
has_existing_config,
is_build_output_dir,
)
@ -139,15 +140,15 @@ class TestDetectModuleRoot:
assert "pyproject.toml" in detail
def test_js_detects_from_exports(self, tmp_path):
"""Should detect module root from package.json exports."""
"""Should detect module root from package.json exports when no common src dir exists."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"exports": {".": "./src/index.js"}
"exports": {".": "./packages/core/index.js"}
}))
(tmp_path / "src").mkdir()
(tmp_path / "packages" / "core").mkdir(parents=True)
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "src"
assert module_root == tmp_path / "packages" / "core"
assert "exports" in detail
def test_js_detects_src_convention(self, tmp_path):
@ -158,6 +159,214 @@ class TestDetectModuleRoot:
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "src"
def test_js_prefers_src_over_build_src(self, tmp_path):
"""Should prefer src/ over build/src/ even when package.json points to build/."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "build/src/index.js",
"module": "build/src/index.js"
}))
(tmp_path / "src").mkdir()
(tmp_path / "build" / "src").mkdir(parents=True)
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "src"
assert "src/ directory" in detail
def test_js_skips_build_dir_from_main(self, tmp_path):
"""Should skip build output directories from package.json main field."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "build/index.js"
}))
(tmp_path / "build").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path
assert "project root" in detail
def test_js_skips_dist_dir_from_exports(self, tmp_path):
"""Should skip dist output directories from package.json exports field."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"exports": {".": "./dist/index.js"}
}))
(tmp_path / "dist").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path
assert "project root" in detail
def test_js_skips_out_dir_from_module(self, tmp_path):
"""Should skip out output directories from package.json module field."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"module": "out/esm/index.js"
}))
(tmp_path / "out" / "esm").mkdir(parents=True)
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path
assert "project root" in detail
def test_js_prefers_lib_over_build_dir(self, tmp_path):
"""Should prefer lib/ over build output directories."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "dist/index.js"
}))
(tmp_path / "lib").mkdir()
(tmp_path / "dist").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "lib"
assert "lib/ directory" in detail
def test_js_prefers_source_over_build_dir(self, tmp_path):
"""Should prefer source/ over build output directories."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "build/index.js"
}))
(tmp_path / "source").mkdir()
(tmp_path / "build").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "source"
assert "source/ directory" in detail
def test_js_falls_back_to_valid_exports_path(self, tmp_path):
"""Should use exports path when no common source dirs exist and path is not build output."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"exports": {".": "./packages/core/index.js"}
}))
(tmp_path / "packages" / "core").mkdir(parents=True)
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "packages" / "core"
assert "exports" in detail
def test_js_falls_back_to_valid_main_path(self, tmp_path):
"""Should use main path when no common source dirs exist and path is not build output."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "packages/main/index.js"
}))
(tmp_path / "packages" / "main").mkdir(parents=True)
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "packages" / "main"
assert "main" in detail
def test_js_falls_back_to_valid_module_path(self, tmp_path):
"""Should use module path when no common source dirs exist and path is not build output."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"module": "esm/index.js"
}))
(tmp_path / "esm").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path / "esm"
assert "module" in detail
def test_js_returns_project_root_when_all_paths_are_build_output(self, tmp_path):
"""Should return project root when all package.json paths point to build outputs."""
(tmp_path / "package.json").write_text(json.dumps({
"name": "test",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {".": "./build/index.js"}
}))
(tmp_path / "dist" / "cjs").mkdir(parents=True)
(tmp_path / "dist" / "esm").mkdir(parents=True)
(tmp_path / "build").mkdir()
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path
assert "project root" in detail
def test_js_handles_malformed_package_json(self, tmp_path):
"""Should handle malformed package.json gracefully."""
(tmp_path / "package.json").write_text("{ invalid json }")
module_root, detail = _detect_js_module_root(tmp_path)
assert module_root == tmp_path
assert "project root" in detail
class TestIsBuildOutputDir:
"""Tests for is_build_output_dir function."""
def test_detects_build_dir(self):
"""Should detect build/ as build output."""
from pathlib import Path
assert is_build_output_dir(Path("build"))
assert is_build_output_dir(Path("build/src"))
assert is_build_output_dir(Path("build/src/index.js"))
def test_detects_dist_dir(self):
"""Should detect dist/ as build output."""
from pathlib import Path
assert is_build_output_dir(Path("dist"))
assert is_build_output_dir(Path("dist/esm"))
assert is_build_output_dir(Path("dist/cjs/index.js"))
def test_detects_out_dir(self):
"""Should detect out/ as build output."""
from pathlib import Path
assert is_build_output_dir(Path("out"))
assert is_build_output_dir(Path("out/src"))
def test_detects_next_dir(self):
"""Should detect .next/ as build output."""
from pathlib import Path
assert is_build_output_dir(Path(".next"))
assert is_build_output_dir(Path(".next/static"))
def test_detects_nuxt_dir(self):
"""Should detect .nuxt/ as build output."""
from pathlib import Path
assert is_build_output_dir(Path(".nuxt"))
assert is_build_output_dir(Path(".nuxt/dist"))
def test_detects_nested_build_dir(self):
"""Should detect build dir nested in path."""
from pathlib import Path
assert is_build_output_dir(Path("packages/build/index.js"))
assert is_build_output_dir(Path("foo/dist/bar"))
def test_does_not_detect_src(self):
"""Should not detect src/ as build output."""
from pathlib import Path
assert not is_build_output_dir(Path("src"))
assert not is_build_output_dir(Path("src/index.js"))
def test_does_not_detect_lib(self):
"""Should not detect lib/ as build output."""
from pathlib import Path
assert not is_build_output_dir(Path("lib"))
assert not is_build_output_dir(Path("lib/utils"))
def test_does_not_detect_source(self):
"""Should not detect source/ as build output."""
from pathlib import Path
assert not is_build_output_dir(Path("source"))
def test_does_not_detect_packages(self):
"""Should not detect packages/ as build output."""
from pathlib import Path
assert not is_build_output_dir(Path("packages"))
assert not is_build_output_dir(Path("packages/core"))
def test_does_not_detect_similar_names(self):
"""Should not detect directories with similar but different names."""
from pathlib import Path
assert not is_build_output_dir(Path("builder"))
assert not is_build_output_dir(Path("distribution"))
assert not is_build_output_dir(Path("output"))
class TestDetectTestsRoot:
"""Tests for tests root detection."""

View file

@ -857,6 +857,48 @@ class TestE2ECLIFlags:
# Should complete without error
_handle_show_config()
def test_show_config_displays_config_path_when_saved(self, project_with_existing_config, monkeypatch):
"""Should display config file path when saved config exists."""
monkeypatch.chdir(project_with_existing_config)
# Track what gets printed
printed_messages = []
def mock_print(msg="", *args, **kwargs):
printed_messages.append(str(msg))
from codeflash.cli_cmds import console
monkeypatch.setattr(console.console, "print", mock_print)
from codeflash.cli_cmds.cli import _handle_show_config
_handle_show_config()
# Verify config path is displayed
all_output = "\n".join(printed_messages)
assert "pyproject.toml" in all_output
assert "Config file:" in all_output
def test_show_config_no_path_when_auto_detected(self, python_src_layout, monkeypatch):
"""Should not display config file path when config is auto-detected."""
monkeypatch.chdir(python_src_layout)
# Track what gets printed
printed_messages = []
def mock_print(msg="", *args, **kwargs):
printed_messages.append(str(msg))
from codeflash.cli_cmds import console
monkeypatch.setattr(console.console, "print", mock_print)
from codeflash.cli_cmds.cli import _handle_show_config
_handle_show_config()
# Verify no config path line is displayed
all_output = "\n".join(printed_messages)
assert "Config file:" not in all_output
assert "Auto-detected" in all_output
def test_reset_config_removes_from_pyproject(self, project_with_existing_config, monkeypatch):
"""Should remove codeflash config from pyproject.toml."""
monkeypatch.chdir(project_with_existing_config)