All files / transpiler/data PathResolver.ts

95.08% Statements 58/61
88.88% Branches 40/45
100% Functions 6/6
95.08% Lines 58/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198                              14x                                               189x 189x                       117x 117x     117x 89x     28x     28x 23x       94x                 2x                     67x   67x 67x   8x 8x   8x 8x 2x     8x         59x 59x                       37x   37x 37x   6x 6x 6x   6x 6x 3x     6x         31x 22x 22x   22x 2x 2x 2x   2x 2x 2x     2x       20x 20x   20x       20x         9x 9x               8x 5x     3x 3x 3x     3x     3x 3x              
/**
 * PathResolver
 * Handles path calculations for output files.
 *
 * Consolidates path resolution logic used by Transpiler and CleanCommand,
 * including directory structure preservation and basePath stripping.
 */
 
import { join, basename, relative, dirname, resolve } from "node:path";
 
import IDiscoveredFile from "./types/IDiscoveredFile";
import IFileSystem from "../types/IFileSystem";
import NodeFileSystem from "../NodeFileSystem";
 
/** Default file system instance (singleton for performance) */
const defaultFs = NodeFileSystem.instance;
 
/**
 * Configuration for PathResolver
 */
interface IPathResolverConfig {
  /** Input files or directories */
  inputs: string[];
  /** Output directory for generated code */
  outDir: string;
  /** Optional separate output directory for headers */
  headerOutDir?: string;
  /** Optional base path to strip from header output paths */
  basePath?: string;
}
 
/**
 * Resolves output paths for transpiled files
 */
class PathResolver {
  private readonly config: IPathResolverConfig;
  private readonly fs: IFileSystem;
 
  constructor(config: IPathResolverConfig, fs: IFileSystem = defaultFs) {
    this.config = config;
    this.fs = fs;
  }
 
  /**
   * Get relative path from any input directory for a file.
   * Returns the relative path (e.g., "Display/Utils.cnx") or null if the file
   * is not under any input directory.
   *
   * This is the core utility used by getSourceRelativePath, getOutputPath,
   * and getHeaderOutputPath for directory structure preservation.
   */
  getRelativePathFromInputs(filePath: string): string | null {
    for (const input of this.config.inputs) {
      const resolvedInput = resolve(input);
 
      // Skip if input is a file (not a directory) - can't preserve structure
      if (this.fs.exists(resolvedInput) && this.fs.isFile(resolvedInput)) {
        continue;
      }
 
      const relativePath = relative(resolvedInput, filePath);
 
      // Check if file is under this input directory
      if (relativePath && !relativePath.startsWith("..")) {
        return relativePath;
      }
    }
 
    return null;
  }
 
  /**
   * Issue #339: Get relative path from input directory for self-include generation.
   * Returns the relative path (e.g., "Display/Utils.cnx") or just the basename
   * if the file is not in any input directory.
   */
  getSourceRelativePath(filePath: string): string {
    return this.getRelativePathFromInputs(filePath) ?? basename(filePath);
  }
 
  /**
   * Get output path for a transpiled file (.c or .cpp)
   *
   * @param file - The discovered file to get output path for
   * @param cppMode - If true, output .cpp; otherwise .c
   * @returns The full output path
   */
  getOutputPath(file: IDiscoveredFile, cppMode: boolean): string {
    const ext = cppMode ? ".cpp" : ".c";
 
    const relativePath = this.getRelativePathFromInputs(file.path);
    if (relativePath) {
      // File is under an input directory - preserve structure
      const outputRelative = relativePath.replace(/\.cnx$|\.cnext$/, ext);
      const outputPath = join(this.config.outDir, outputRelative);
 
      const outputDir = dirname(outputPath);
      if (!this.fs.exists(outputDir)) {
        this.fs.mkdir(outputDir, { recursive: true });
      }
 
      return outputPath;
    }
 
    // Fallback: output next to the source file (not in outDir)
    // This handles included files that aren't under any input directory
    const outputName = basename(file.path).replace(/\.cnx$|\.cnext$/, ext);
    return join(dirname(file.path), outputName);
  }
 
  /**
   * Get output path for a header file (.h)
   * Uses headerOutDir if specified, otherwise falls back to outDir
   *
   * @param file - The discovered file to get header path for
   * @returns The full header output path
   */
  getHeaderOutputPath(file: IDiscoveredFile): string {
    // Use headerOutDir if specified, otherwise fall back to outDir
    const headerDir = this.config.headerOutDir || this.config.outDir;
 
    const relativePath = this.getRelativePathFromInputs(file.path);
    if (relativePath) {
      // File is under an input directory - preserve structure (minus basePath)
      const strippedPath = this.stripBasePath(relativePath);
      const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ".h");
      const outputPath = join(headerDir, outputRelative);
 
      const outputDir = dirname(outputPath);
      if (!this.fs.exists(outputDir)) {
        this.fs.mkdir(outputDir, { recursive: true });
      }
 
      return outputPath;
    }
 
    // Issue #489: If headerOutDir is explicitly set, use it with relative path from CWD
    // This handles single-file inputs like "cnext src/AppConfig.cnx" with headerOut config
    if (this.config.headerOutDir) {
      const cwd = process.cwd();
      const relativeFromCwd = relative(cwd, file.path);
      // Only use CWD-relative path if file is under CWD (not starting with ..)
      if (relativeFromCwd && !relativeFromCwd.startsWith("..")) {
        const strippedPath = this.stripBasePath(relativeFromCwd);
        const outputRelative = strippedPath.replace(/\.cnx$|\.cnext$/, ".h");
        const outputPath = join(this.config.headerOutDir, outputRelative);
 
        const outputDir = dirname(outputPath);
        Eif (!this.fs.exists(outputDir)) {
          this.fs.mkdir(outputDir, { recursive: true });
        }
 
        return outputPath;
      }
 
      // File outside CWD: put in headerOutDir with just basename
      const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
      const outputPath = join(this.config.headerOutDir, headerName);
 
      Iif (!this.fs.exists(this.config.headerOutDir)) {
        this.fs.mkdir(this.config.headerOutDir, { recursive: true });
      }
 
      return outputPath;
    }
 
    // Fallback: output next to the source file (no headerDir specified)
    // This handles included files that aren't under any input directory
    const headerName = basename(file.path).replace(/\.cnx$|\.cnext$/, ".h");
    return join(dirname(file.path), headerName);
  }
 
  /**
   * Strip basePath prefix from a relative path
   * e.g., "src/AppConfig.cnx" with basePath "src" -> "AppConfig.cnx"
   */
  private stripBasePath(relPath: string): string {
    if (!this.config.basePath || !this.config.headerOutDir) {
      return relPath;
    }
    // Normalize basePath (remove trailing slashes) using string methods
    let base = this.config.basePath;
    while (base.endsWith("/") || base.endsWith("\\")) {
      base = base.slice(0, -1);
    }
    // Check if relPath starts with basePath (+ separator or exact match)
    Iif (relPath === base) {
      return "";
    }
    Eif (relPath.startsWith(base + "/") || relPath.startsWith(base + "\\")) {
      return relPath.slice(base.length + 1);
    }
    return relPath;
  }
}
 
export default PathResolver;