All files match-path.ts

97.78% Statements 44/45
92.11% Branches 35/38
91.67% Functions 11/12
100% Lines 40/40
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 1331x 1x 1x 1x                           1x             11x 11x     14x         11x 12x                           1x       12x 12x 19x     12x           14x 14x 14x 15x 15x 15x 15x 11x           1x                                         15x 1x     49x 3x     11x   11x 3x 3x 3x 3x     8x 28x               12x   11x       22x 22x    
import { readPackage } from './package-reader';
import * as fs from "fs";
import * as path from "path";
import { matchStar } from "./match-star";
 
export type MatchPath = (
  absoluteSourceFileName: string,
  requestedModule: string,
  readPackageJson?: (packageJsonPath: string) => any,
  fileExists?: any, extensions?: Array<string>) => string | undefined;
 
/**
 * Creates a function that can resolve paths according to tsconfig paths property.
 * @param tsConfigPath The paths where tsconfig.json is located.
 * @param baseUrl The baseUrl specified in tsconfig.
 * @param paths The paths specified in tsconfig.
 */
export function createMatchPath(
  absoluteBaseUrl: string,
  paths: { [key: string]: Array<string> }
): MatchPath {
 
  // Resolve all paths to absolute form once here, this saves time on each request later.
  // We also add the baseUrl as a base which will be replace if specified in paths. This is how typescript works
  const absolutePaths: { [key: string]: Array<string> } = Object.keys(paths)
    .reduce((soFar, key) => ({
      ...soFar,
      [key]: paths[key]
        .map((pathToResolve) => path.join(absoluteBaseUrl, pathToResolve))
    }), {
      "*": [`${absoluteBaseUrl.replace(/\/$/, "")}/*`],
    });
 
  return (sourceFileName: string, requestedModule: string, readPackageJson: (packageJsonPath: string) => any, fileExists: any, extensions?: Array<string>) =>
    matchFromAbsolutePaths(absolutePaths, sourceFileName, requestedModule, readPackageJson, fileExists, extensions);
 
}
 
/**
 * Finds a path from tsconfig that matches a module load request.
 * @param absolutePathMappings The paths to try as specified in tsconfig but resolved to absolute form.
 * @param absoluteSourceFileName Absolute path to the file that requested the module.
 * @param requestedModule The required module name.
 * @param readPackageJson Function that returns parsed package.json if exists or undefined(useful for testing).
 * @param fileExists Function that checks for existance of a file (useful for testing).
 * @param extensions File extensions to probe for (useful for testing).
 * @returns the found path, or undefined if no path was found.
 */
export function matchFromAbsolutePaths(
  absolutePathMappings: { [key: string]: Array<string> },
  absoluteSourceFileName: string,
  requestedModule: string,
  IreadPackageJson: (packageJsonPath: string) => any = (packageJsonPath: string) => readPackage(packageJsonPath),
  IfileExists = fs.existsSync,
  extensions = Object.keys(require.extensions)
): string | undefined {
 
  Eif (requestedModule[0] !== '.'
    && requestedModule[0] !== path.sep
    && absolutePathMappings
    && absoluteSourceFileName
    && requestedModule
    && fileExists) {
    for (const virtualPathPattern of sortByLongestPrefix(Object.keys(absolutePathMappings))) {
      const starMatch = virtualPathPattern === requestedModule ? '' : matchStar(virtualPathPattern, requestedModule);
      if (starMatch !== undefined) {
        for (const physicalPathPattern of absolutePathMappings[virtualPathPattern]) {
          const physicalPath = physicalPathPattern.replace('*', starMatch);
          const resolved = tryResolve(physicalPath, fileExists, readPackageJson, extensions);
          if (resolved) {
            return resolved;
          }
        }
      }
    }
  }
  return undefined;
 
}
 
/**
 * Tries to resolve a physical path by:
 * 1. Check for files named as last part of request and ending in any of the extensions.
 * 2. Check for file specified in package.json's main property.
 * 3. Check for a file named index ending in any of the extensions.
 * @param physicalPath The path to check.
 * @param fileExists Function that checks for existance of a file (useful for testing).
 * @param readPackageJson Function that returns parsed package.json if exists or undefined(useful for testing).
 * @param extensions File extensions to probe for (useful for testing).
 * @returns {string}
 */
function tryResolve(
  physicalPath: string,
  fileExists: any,
  readPackageJson: (packageJsonPath: string) => any, extensions: Array<string>
): string | undefined {
 
  if (path.extname(path.basename(physicalPath)).length > 0 && fileExists(physicalPath)) {
    return physicalPath;
  }
 
  if (extensions.reduce((prev, curr) => prev || fileExists(physicalPath + curr), false)) {
    return physicalPath;
  }
 
  const packageJson = readPackageJson(path.join(physicalPath, "/package.json"));
 
  if (packageJson && packageJson.main && fileExists(path.join(physicalPath, packageJson.main))) {
    const file = path.join(physicalPath, packageJson.main);
    const fileExtension = path.extname(file).replace(/^\./, "");
    const fileExtensionRegex = new RegExp(`\.${fileExtension}$`);
    return fileExtension ? file.replace(fileExtensionRegex, "") : file;
  }
 
  const indexPath = path.join(physicalPath, "/index");
  return extensions.reduce((prev, curr) => prev || fileExists(indexPath + curr) && physicalPath, "");
}
 
/**
 * Sort path patterns.
 * If module name can be matches with multiple patterns then pattern with the longest prefix will be picked.
 */
function sortByLongestPrefix(arr: Array<string>): Array<string> {
  return arr
    .concat()
    .sort((a: string, b: string) => getPrefixLength(b) - getPrefixLength(a));
}
 
function getPrefixLength(pattern: string): number {
  const prefixLength = pattern.indexOf("*");
  return pattern.substr(0, prefixLength).length;
}