All files match-path.ts

97.67% Statements 42/43
91.18% Branches 31/34
91.67% Functions 11/12
100% Lines 38/38
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 1231x 1x 1x 1x                           1x           10x 10x     13x         10x 11x                           1x     11x 11x 18x   11x           13x 13x 13x 14x 14x 14x 14x 10x           1x                                   49x 3x     11x   11x 3x 3x 3x 3x     8x 28x               11x   10x       20x 20x    
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 (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;
}