try-path.ts 3.2 KB
import * as path from "path";
import { MappingEntry } from "./mapping-entry";
import { dirname } from "path";
import { removeExtension } from "./filesystem";

export interface TryPath {
  readonly type: "file" | "extension" | "index" | "package";
  readonly path: string;
}

/**
 * Builds a list of all physical paths to try by:
 * 1. Check for file named exactly as request.
 * 2. Check for files named as request ending in any of the extensions.
 * 3. Check for file specified in package.json's main property.
 * 4. Check for files named as request ending in "index" with any of the extensions.
 */
export function getPathsToTry(
  extensions: ReadonlyArray<string>,
  absolutePathMappings: ReadonlyArray<MappingEntry>,
  requestedModule: string
): ReadonlyArray<TryPath> | undefined {
  if (!absolutePathMappings || !requestedModule || requestedModule[0] === ".") {
    return undefined;
  }

  const pathsToTry: Array<TryPath> = [];
  for (const entry of absolutePathMappings) {
    const starMatch =
      entry.pattern === requestedModule
        ? ""
        : matchStar(entry.pattern, requestedModule);
    if (starMatch !== undefined) {
      for (const physicalPathPattern of entry.paths) {
        const physicalPath = physicalPathPattern.replace("*", starMatch);
        pathsToTry.push({ type: "file", path: physicalPath });
        pathsToTry.push(
          ...extensions.map(
            (e) => ({ type: "extension", path: physicalPath + e } as TryPath)
          )
        );
        pathsToTry.push({
          type: "package",
          path: path.join(physicalPath, "/package.json"),
        });
        const indexPath = path.join(physicalPath, "/index");
        pathsToTry.push(
          ...extensions.map(
            (e) => ({ type: "index", path: indexPath + e } as TryPath)
          )
        );
      }
    }
  }
  return pathsToTry.length === 0 ? undefined : pathsToTry;
}

// Not sure why we don't just return the full found path?
export function getStrippedPath(tryPath: TryPath): string {
  return tryPath.type === "index"
    ? dirname(tryPath.path)
    : tryPath.type === "file"
    ? tryPath.path
    : tryPath.type === "extension"
    ? removeExtension(tryPath.path)
    : tryPath.type === "package"
    ? tryPath.path
    : exhaustiveTypeException(tryPath.type);
}

export function exhaustiveTypeException(check: never): never {
  throw new Error(`Unknown type ${check}`);
}

/**
 * Matches pattern with a single star against search.
 * Star must match at least one character to be considered a match.
 * @param patttern for example "foo*"
 * @param search for example "fooawesomebar"
 * @returns the part of search that * matches, or undefined if no match.
 */
function matchStar(pattern: string, search: string): string | undefined {
  if (search.length < pattern.length) {
    return undefined;
  }
  if (pattern === "*") {
    return search;
  }
  const star = pattern.indexOf("*");
  if (star === -1) {
    return undefined;
  }
  const part1 = pattern.substring(0, star);
  const part2 = pattern.substring(star + 1);
  if (search.substr(0, star) !== part1) {
    return undefined;
  }
  if (search.substr(search.length - part2.length) !== part2) {
    return undefined;
  }
  return search.substr(star, search.length - part2.length);
}