match-path-async.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import * as path from "path";
  2. import * as TryPath from "./try-path";
  3. import * as MappingEntry from "./mapping-entry";
  4. import * as Filesystem from "./filesystem";
  5. /**
  6. * Function that can match a path async
  7. */
  8. export interface MatchPathAsync {
  9. (
  10. requestedModule: string,
  11. readJson: Filesystem.ReadJsonAsync | undefined,
  12. fileExists: Filesystem.FileExistsAsync | undefined,
  13. extensions: ReadonlyArray<string> | undefined,
  14. callback: MatchPathAsyncCallback
  15. ): void;
  16. }
  17. export interface MatchPathAsyncCallback {
  18. (err?: Error, path?: string): void;
  19. }
  20. /**
  21. * See the sync version for docs.
  22. */
  23. export function createMatchPathAsync(
  24. absoluteBaseUrl: string,
  25. paths: { [key: string]: Array<string> },
  26. mainFields: string[] = ["main"],
  27. addMatchAll: boolean = true
  28. ): MatchPathAsync {
  29. const absolutePaths = MappingEntry.getAbsoluteMappingEntries(
  30. absoluteBaseUrl,
  31. paths,
  32. addMatchAll
  33. );
  34. return (
  35. requestedModule: string,
  36. readJson: Filesystem.ReadJsonAsync | undefined,
  37. fileExists: Filesystem.FileExistsAsync | undefined,
  38. extensions: ReadonlyArray<string> | undefined,
  39. callback: MatchPathAsyncCallback
  40. ) =>
  41. matchFromAbsolutePathsAsync(
  42. absolutePaths,
  43. requestedModule,
  44. readJson,
  45. fileExists,
  46. extensions,
  47. callback,
  48. mainFields
  49. );
  50. }
  51. /**
  52. * See the sync version for docs.
  53. */
  54. export function matchFromAbsolutePathsAsync(
  55. absolutePathMappings: ReadonlyArray<MappingEntry.MappingEntry>,
  56. requestedModule: string,
  57. readJson: Filesystem.ReadJsonAsync = Filesystem.readJsonFromDiskAsync,
  58. fileExists: Filesystem.FileExistsAsync = Filesystem.fileExistsAsync,
  59. extensions: ReadonlyArray<string> = Object.keys(require.extensions),
  60. callback: MatchPathAsyncCallback,
  61. mainFields: string[] = ["main"]
  62. ): void {
  63. const tryPaths = TryPath.getPathsToTry(
  64. extensions,
  65. absolutePathMappings,
  66. requestedModule
  67. );
  68. if (!tryPaths) {
  69. return callback();
  70. }
  71. findFirstExistingPath(
  72. tryPaths,
  73. readJson,
  74. fileExists,
  75. callback,
  76. 0,
  77. mainFields
  78. );
  79. }
  80. function findFirstExistingMainFieldMappedFile(
  81. packageJson: Filesystem.PackageJson,
  82. mainFields: string[],
  83. packageJsonPath: string,
  84. fileExistsAsync: Filesystem.FileExistsAsync,
  85. doneCallback: (err?: Error, filepath?: string) => void,
  86. index: number = 0
  87. ): void {
  88. if (index >= mainFields.length) {
  89. return doneCallback(undefined, undefined);
  90. }
  91. const tryNext = () =>
  92. findFirstExistingMainFieldMappedFile(
  93. packageJson,
  94. mainFields,
  95. packageJsonPath,
  96. fileExistsAsync,
  97. doneCallback,
  98. index + 1
  99. );
  100. const mainFieldMapping = packageJson[mainFields[index]];
  101. if (typeof mainFieldMapping !== "string") {
  102. // Skip mappings that are not pointers to replacement files
  103. return tryNext();
  104. }
  105. const mappedFilePath = path.join(
  106. path.dirname(packageJsonPath),
  107. mainFieldMapping
  108. );
  109. fileExistsAsync(mappedFilePath, (err?: Error, exists?: boolean) => {
  110. if (err) {
  111. return doneCallback(err);
  112. }
  113. if (exists) {
  114. return doneCallback(undefined, mappedFilePath);
  115. }
  116. return tryNext();
  117. });
  118. }
  119. // Recursive loop to probe for physical files
  120. function findFirstExistingPath(
  121. tryPaths: ReadonlyArray<TryPath.TryPath>,
  122. readJson: Filesystem.ReadJsonAsync,
  123. fileExists: Filesystem.FileExistsAsync,
  124. doneCallback: MatchPathAsyncCallback,
  125. index: number = 0,
  126. mainFields: string[] = ["main"]
  127. ): void {
  128. const tryPath = tryPaths[index];
  129. if (
  130. tryPath.type === "file" ||
  131. tryPath.type === "extension" ||
  132. tryPath.type === "index"
  133. ) {
  134. fileExists(tryPath.path, (err: Error, exists: boolean) => {
  135. if (err) {
  136. return doneCallback(err);
  137. }
  138. if (exists) {
  139. return doneCallback(undefined, TryPath.getStrippedPath(tryPath));
  140. }
  141. if (index === tryPaths.length - 1) {
  142. return doneCallback();
  143. }
  144. // Continue with the next path
  145. return findFirstExistingPath(
  146. tryPaths,
  147. readJson,
  148. fileExists,
  149. doneCallback,
  150. index + 1,
  151. mainFields
  152. );
  153. });
  154. } else if (tryPath.type === "package") {
  155. readJson(tryPath.path, (err, packageJson) => {
  156. if (err) {
  157. return doneCallback(err);
  158. }
  159. if (packageJson) {
  160. return findFirstExistingMainFieldMappedFile(
  161. packageJson,
  162. mainFields,
  163. tryPath.path,
  164. fileExists,
  165. (mainFieldErr?: Error, mainFieldMappedFile?: string) => {
  166. if (mainFieldErr) {
  167. return doneCallback(mainFieldErr);
  168. }
  169. if (mainFieldMappedFile) {
  170. return doneCallback(undefined, mainFieldMappedFile);
  171. }
  172. // No field in package json was a valid option. Continue with the next path.
  173. return findFirstExistingPath(
  174. tryPaths,
  175. readJson,
  176. fileExists,
  177. doneCallback,
  178. index + 1,
  179. mainFields
  180. );
  181. }
  182. );
  183. }
  184. // This is async code, we need to return unconditionally, otherwise the code still falls
  185. // through and keeps recursing. While this might work in general, libraries that use neo-async
  186. // like Webpack will actually not allow you to call the same callback twice.
  187. //
  188. // An example of where this caused issues:
  189. // https://github.com/dividab/tsconfig-paths-webpack-plugin/issues/11
  190. //
  191. // Continue with the next path
  192. return findFirstExistingPath(
  193. tryPaths,
  194. readJson,
  195. fileExists,
  196. doneCallback,
  197. index + 1,
  198. mainFields
  199. );
  200. });
  201. } else {
  202. TryPath.exhaustiveTypeException(tryPath.type);
  203. }
  204. }