All files file-module.ts

95% Statements 76/80
82.35% Branches 14/17
100% Functions 2/2
95% Lines 76/80

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 991x 1x 1x 1x 1x                     23x 23x 23x 23x 23x 18x 18x 18x 18x     18x 13x 13x 13x 13x 11x 13x   13x 13x 13x 13x     18x 30x 17x 17x 30x   18x 18x   10x 10x 10x 10x 10x 10x 10x 10x 10x       10x 10x   10x 25x 25x   25x 5x 5x 5x 5x 5x 5x 5x 5x 25x 20x 20x 4x 4x 4x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 25x 10x 10x  
import fs from "node:fs/promises";
import { readdir } from "node:fs/promises";
import path from "node:path";
import { getLogger } from "./log";
import { directoryExists, fileExists } from "./plainstack-fs";
 
type FileModule<T> = {
  defaultExport?: T;
  namedExports: Record<string, T>;
  filename: string;
  extension: string;
  absolutePath: string;
  relativePath: string;
};
 
export async function loadModule<T>(
  filePath: string,
  load: (module: unknown) => Promise<T>,
): Promise<Pick<FileModule<T>, "defaultExport" | "namedExports"> | undefined> {
  if (!(await fileExists(filePath))) return undefined;
  const module = await import(filePath);
  const result: { defaultExport?: T; namedExports: Record<string, T> } = {
    namedExports: {},
  };
 
  // handle default export
  if ("default" in module) {
    const defaultExport = module.default;
    if (
      defaultExport &&
      typeof defaultExport === "object" &&
      "default" in defaultExport
    ) {
      result.defaultExport = await load(defaultExport.default);
    } else {
      result.defaultExport = await load(defaultExport);
    }
  }
 
  // handle named exports
  for (const [key, value] of Object.entries(module)) {
    if (key !== "default") {
      result.namedExports[key] = await load(value);
    }
  }
 
  return result;
}
 
export async function loadModulesfromDir<T>(
  baseDir: string,
  load: (module: unknown) => Promise<T>,
  extensions: string[] = [".ts", ".tsx"],
  currentDir: string = baseDir,
): Promise<FileModule<T>[]> {
  const modules: FileModule<T>[] = [];
  const log = getLogger("manifest");
  if (!(await directoryExists(currentDir))) {
    log.debug(`directory ${currentDir} does not exist`);
    return [];
  }
  const files = await readdir(currentDir);
  log.debug(`found ${files.length} files in ${currentDir}`);
 
  for (const file of files) {
    const absolutePath = path.join(currentDir, file);
    const stat = await fs.stat(absolutePath);
 
    if (stat.isDirectory()) {
      log.debug(`found directory: ${absolutePath}`);
      const subModules = await loadModulesfromDir(
        baseDir,
        load,
        extensions,
        absolutePath,
      );
      modules.push(...subModules);
    } else if (stat.isFile()) {
      log.debug(`found file: ${absolutePath}`);
      if (!extensions.includes(path.extname(file))) {
        log.debug(`skipping file ${absolutePath} with unsupported extension`);
        continue;
      }
      const relativePath = path.relative(baseDir, absolutePath);
      const result = await loadModule(absolutePath, load);
      if (!result) continue;
      modules.push({
        defaultExport: result.defaultExport,
        namedExports: result.namedExports,
        filename: path.parse(file).name,
        extension: path.extname(file),
        absolutePath,
        relativePath,
      });
    }
  }
  return modules;
}