All files requirefs.ts

100% Statements 66/66
100% Branches 26/26
100% Functions 15/15
100% Lines 63/63

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 1711x 1x 1x                 1x         1x           2x 2x 2x 2x           1x 4x 2x   2x           1x 44x             66x 66x 2x     64x 62x 12x       50x 50x         50x       50x         50x     50x 24x 24x 2x   22x       50x 50x 2x   48x       48x       6x         50x   50x             1x 24x 24x           1x 168x         1x 66x   1x         1x 1x 1x   84x     33x 33x 1x   32x               1x   1x 1x   84x     33x 33x 1x   32x        
import * as path from "./path"
import { Resolver } from "./resolver"
import { Tar } from "./tar"
 
type Module = any // eslint-disable-line @typescript-eslint/no-explicit-any
 
export interface FileReader {
  exists(filePath: string): boolean
  read(filePath: string, encoding?: "utf8"): string | Uint8Array
}
 
const originalExports = Symbol("originalExports")
 
/**
 * Allow requiring modules and files from a provided file system.
 */
export class RequireFS extends Resolver {
  private readonly reader: FileReader
  private readonly customModules: Map<string, Module>
  private readonly requireCache: Map<string, Module>
 
  public constructor(reader: FileReader) {
    super()
    this.reader = reader
    this.customModules = new Map()
    this.requireCache = new Map()
  }
 
  /**
   * Provide a custom module.
   */
  public provide(moduleName: string, value: Module): void {
    if (this.customModules.has(moduleName)) {
      throw new Error("custom module has already been registered with this name")
    }
    this.customModules.set(moduleName, value)
  }
 
  /**
   * Require a path relative to the root.
   */
  public require(target: string): Module {
    return this.doRequire(`./${path.normalize(target)}`, "./")
  }
 
  /**
   * Attempt to require the provided path. If the path requires another path, it
   * will recursively follow the dependency tree and return any exported data.
   */
  private doRequire(importPath: string, directoryPath: string): Module {
    if (this.customModules.has(importPath)) {
      return this.customModules.get(importPath)
    }
 
    const resolvedPath = this.resolvePath(importPath, directoryPath)
    if (this.requireCache.has(resolvedPath)) {
      return this.requireCache.get(resolvedPath).exports
    }
 
    // Provide globals that can be referenced in the `eval`.
    let exports = {}
    const module = { exports, [originalExports]: exports }
 
    // We must do this immediately in case of circular imports. This means that
    // a circular import can't catch reassigned values but it's better than
    // failing.
    this.requireCache.set(resolvedPath, { exports })
 
    /* eslint-disable @typescript-eslint/no-unused-vars */
    // @ts-ignore
    const __dirname = path.dirname(resolvedPath)
 
    // Some modules will try to use `define` if it exists (lodash) and we only
    // want modules using our custom-provided `require` function.
    // @ts-ignore
    const define = undefined
 
    // @ts-ignore
    const require = (target: string): Module => {
      const nativeModule = this.tryNativeRequire(target)
      if (typeof nativeModule !== "undefined" && nativeModule !== null) {
        return nativeModule
      }
      return this.doRequire(target, path.dirname(resolvedPath))
    }
    /* eslint-enable @typescript-eslint/no-unused-vars */
 
    const content = this.readFile(resolvedPath, "utf8")
    if (/\.json$/.test(resolvedPath)) {
      exports = JSON.parse(content)
    } else {
      eval(`'use strict'; ${content}`)
      // It's possible that `exports` or `module.exports` have been reassigned.
      // In that case use the reassigned version, giving preference to
      // `exports`.
      if (
        exports === module[originalExports] && // exports not reassigned
        module.exports !== module[originalExports] // module.exports reassigned
      ) {
        exports = module.exports
      }
    }
 
    // Set it again in case it was reassigned.
    this.requireCache.set(resolvedPath, { exports })
 
    return exports
  }
 
  /**
   * Require a module using NodeJS's `module.require`. Note that script runners
   * (e.g. Jest, which uses `resolve.Sync` under the hood) may interfere.
   */
  private tryNativeRequire(modulePath: string): Module {
    try {
      return require(modulePath)
    } catch (e) {
      // Don't throw here. The module may be retrievable in another way.
    }
  }
 
  protected isFile(filePath: string): boolean {
    return this.reader.exists(filePath)
  }
 
  public readFile(filePath: string): Uint8Array
  public readFile(filePath: string, encoding: "utf8"): string
  public readFile(filePath: string, encoding?: "utf8"): string | Uint8Array {
    return this.reader.read(filePath, encoding)
  }
}
 
/**
 * Return a readable and requirable file system from a tar.
 */
export const fromTar = (content: Uint8Array): RequireFS => {
  const tar = Tar.fromUint8Array(content)
  return new RequireFS({
    exists: (filePath: string): boolean => {
      return !!tar.getFile(filePath)
    },
    read: (filePath: string, encoding?: "utf8"): string | Uint8Array => {
      const file = tar.getFile(filePath)
      if (!file) {
        throw new Error(`"${filePath}" does not exist`)
      }
      return encoding ? file.read(encoding) : file.read()
    },
  })
}
 
/**
 * Return a readable and requirable file system from a zip.
 */
export const fromZip = (content: Uint8Array): RequireFS => {
  // @ts-ignore
  const zip = new (require("jszip") as typeof import("jszip"))(content)
  return new RequireFS({
    exists: (filePath: string): boolean => {
      return !!zip.file(filePath)
    },
    read: (filePath: string, encoding?: "utf8"): string | Uint8Array => {
      const file = zip.file(filePath)
      if (!file) {
        throw new Error(`"${filePath}" does not exist`)
      }
      return encoding ? zip.file(filePath).asText() : zip.file(filePath).asUint8Array()
    },
  })
}