From 5300f628a4407817e4ae52c06b0b82485fa6d4b4 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Sat, 21 Mar 2026 04:23:48 +0530 Subject: [PATCH] node modules symlink to avoid reinstallation of npm package --- .../languages/javascript/mocha_runner.py | 15 ++++++-- codeflash/languages/javascript/support.py | 7 ++++ codeflash/languages/javascript/test_runner.py | 15 ++++++-- .../languages/javascript/vitest_runner.py | 38 +++++++++++++++++-- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/codeflash/languages/javascript/mocha_runner.py b/codeflash/languages/javascript/mocha_runner.py index d904bac6d..59a6f7067 100644 --- a/codeflash/languages/javascript/mocha_runner.py +++ b/codeflash/languages/javascript/mocha_runner.py @@ -65,14 +65,21 @@ def _ensure_runtime_files(project_root: Path) -> None: Installs codeflash package if not already present. The package provides all runtime files needed for test instrumentation. + In monorepos, node_modules may be hoisted to the repo root, so we walk + upward from project_root to find an existing codeflash installation. + Args: project_root: The project root directory. """ - node_modules_pkg = project_root / "node_modules" / "codeflash" - if node_modules_pkg.exists(): - logger.debug("codeflash already installed") - return + # Walk upward to find codeflash in any ancestor node_modules (monorepo hoisting) + current = project_root + while current != current.parent: + node_modules_pkg = current / "node_modules" / "codeflash" + if node_modules_pkg.exists(): + logger.debug(f"codeflash already installed at {node_modules_pkg}") + return + current = current.parent install_cmd = get_package_install_command(project_root, "codeflash", dev=True) try: diff --git a/codeflash/languages/javascript/support.py b/codeflash/languages/javascript/support.py index 63690c7d9..039d1ce98 100644 --- a/codeflash/languages/javascript/support.py +++ b/codeflash/languages/javascript/support.py @@ -1951,6 +1951,13 @@ def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_workt ) if original_node_modules.exists() and not worktree_node_modules.exists(): worktree_node_modules.symlink_to(original_node_modules) + # In monorepos, node_modules lives at the repo root, not the package level. + # Symlink the root-level node_modules into the worktree so Vitest/npx can resolve deps. + if not worktree_node_modules.exists(): + worktree_root_node_modules = current_worktree / "node_modules" + original_root_node_modules = original_js_root / "node_modules" + if original_root_node_modules.exists() and not worktree_root_node_modules.exists(): + worktree_root_node_modules.symlink_to(original_root_node_modules) verify_js_requirements(test_cfg) def adjust_test_config_for_discovery(self, test_cfg: TestConfig) -> None: diff --git a/codeflash/languages/javascript/test_runner.py b/codeflash/languages/javascript/test_runner.py index fedab24e2..b32b5c52e 100644 --- a/codeflash/languages/javascript/test_runner.py +++ b/codeflash/languages/javascript/test_runner.py @@ -699,14 +699,21 @@ def _ensure_runtime_files(project_root: Path) -> None: The package provides all runtime files needed for test instrumentation. Uses the project's detected package manager (npm, pnpm, yarn, or bun). + In monorepos, node_modules may be hoisted to the repo root, so we walk + upward from project_root to find an existing codeflash installation. + Args: project_root: The project root directory. """ - node_modules_pkg = project_root / "node_modules" / "codeflash" - if node_modules_pkg.exists(): - logger.debug("codeflash already installed") - return + # Walk upward to find codeflash in any ancestor node_modules (monorepo hoisting) + current = project_root + while current != current.parent: + node_modules_pkg = current / "node_modules" / "codeflash" + if node_modules_pkg.exists(): + logger.debug(f"codeflash already installed at {node_modules_pkg}") + return + current = current.parent install_cmd = get_package_install_command(project_root, "codeflash", dev=True) try: diff --git a/codeflash/languages/javascript/vitest_runner.py b/codeflash/languages/javascript/vitest_runner.py index ef8e5bc11..1e1113162 100644 --- a/codeflash/languages/javascript/vitest_runner.py +++ b/codeflash/languages/javascript/vitest_runner.py @@ -6,6 +6,7 @@ from __future__ import annotations +import os import subprocess import time from pathlib import Path @@ -95,14 +96,21 @@ def _ensure_runtime_files(project_root: Path) -> None: The package provides all runtime files needed for test instrumentation. Uses the project's detected package manager (npm, pnpm, yarn, or bun). + In monorepos, node_modules may be hoisted to the repo root, so we walk + upward from project_root to find an existing codeflash installation. + Args: project_root: The project root directory. """ - node_modules_pkg = project_root / "node_modules" / "codeflash" - if node_modules_pkg.exists(): - logger.debug("codeflash already installed") - return + # Walk upward to find codeflash in any ancestor node_modules (monorepo hoisting) + current = project_root + while current != current.parent: + node_modules_pkg = current / "node_modules" / "codeflash" + if node_modules_pkg.exists(): + logger.debug(f"codeflash already installed at {node_modules_pkg}") + return + current = current.parent install_cmd = get_package_install_command(project_root, "codeflash", dev=True) try: @@ -295,6 +303,18 @@ def _build_vitest_behavioral_command( if codeflash_vitest_config: cmd.append(f"--config={codeflash_vitest_config}") + # In monorepos, test files may be outside the project root (e.g., tests/ at repo root + # while project_root is packages/features/). Vitest only discovers files under its root + # directory, so we use --dir to widen the scan to the common ancestor. + if project_root and test_files: + resolved_root = project_root.resolve() + test_dirs = {f.resolve().parent for f in test_files} + if any(not d.is_relative_to(resolved_root) for d in test_dirs): + all_paths = [str(resolved_root)] + [str(d) for d in test_dirs] + common_ancestor = Path(os.path.commonpath(all_paths)) + cmd.append(f"--dir={common_ancestor}") + logger.debug(f"Test files outside project root, using --dir={common_ancestor}") + if output_file: # Use dot notation for junit reporter output file when multiple reporters are used # Format: --outputFile.junit=/path/to/file.xml @@ -343,6 +363,16 @@ def _build_vitest_benchmarking_command( if codeflash_vitest_config: cmd.append(f"--config={codeflash_vitest_config}") + # In monorepos, test files may be outside the project root. Widen the scan directory. + if project_root and test_files: + resolved_root = project_root.resolve() + test_dirs = {f.resolve().parent for f in test_files} + if any(not d.is_relative_to(resolved_root) for d in test_dirs): + all_paths = [str(resolved_root)] + [str(d) for d in test_dirs] + common_ancestor = Path(os.path.commonpath(all_paths)) + cmd.append(f"--dir={common_ancestor}") + logger.debug(f"Test files outside project root, using --dir={common_ancestor}") + if output_file: # Use dot notation for junit reporter output file when multiple reporters are used cmd.append(f"--outputFile.junit={output_file}")