You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

322 lines
9.4 KiB

4 years ago
  1. //
  2. 'use strict';
  3. const path = require('path');
  4. const loaders = require('./loaders');
  5. const readFile = require('./readFile');
  6. const cacheWrapper = require('./cacheWrapper');
  7. const getDirectory = require('./getDirectory');
  8. const getPropertyByPath = require('./getPropertyByPath');
  9. const MODE_SYNC = 'sync';
  10. // An object value represents a config object.
  11. // null represents that the loader did not find anything relevant.
  12. // undefined represents that the loader found something relevant
  13. // but it was empty.
  14. class Explorer {
  15. constructor(options ) {
  16. this.loadCache = options.cache ? new Map() : null;
  17. this.loadSyncCache = options.cache ? new Map() : null;
  18. this.searchCache = options.cache ? new Map() : null;
  19. this.searchSyncCache = options.cache ? new Map() : null;
  20. this.config = options;
  21. this.validateConfig();
  22. }
  23. clearLoadCache() {
  24. if (this.loadCache) {
  25. this.loadCache.clear();
  26. }
  27. if (this.loadSyncCache) {
  28. this.loadSyncCache.clear();
  29. }
  30. }
  31. clearSearchCache() {
  32. if (this.searchCache) {
  33. this.searchCache.clear();
  34. }
  35. if (this.searchSyncCache) {
  36. this.searchSyncCache.clear();
  37. }
  38. }
  39. clearCaches() {
  40. this.clearLoadCache();
  41. this.clearSearchCache();
  42. }
  43. validateConfig() {
  44. const config = this.config;
  45. config.searchPlaces.forEach(place => {
  46. const loaderKey = path.extname(place) || 'noExt';
  47. const loader = config.loaders[loaderKey];
  48. if (!loader) {
  49. throw new Error(
  50. `No loader specified for ${getExtensionDescription(
  51. place
  52. )}, so searchPlaces item "${place}" is invalid`
  53. );
  54. }
  55. });
  56. }
  57. search(searchFrom ) {
  58. searchFrom = searchFrom || process.cwd();
  59. return getDirectory(searchFrom).then(dir => {
  60. return this.searchFromDirectory(dir);
  61. });
  62. }
  63. searchFromDirectory(dir ) {
  64. const absoluteDir = path.resolve(process.cwd(), dir);
  65. const run = () => {
  66. return this.searchDirectory(absoluteDir).then(result => {
  67. const nextDir = this.nextDirectoryToSearch(absoluteDir, result);
  68. if (nextDir) {
  69. return this.searchFromDirectory(nextDir);
  70. }
  71. return this.config.transform(result);
  72. });
  73. };
  74. if (this.searchCache) {
  75. return cacheWrapper(this.searchCache, absoluteDir, run);
  76. }
  77. return run();
  78. }
  79. searchSync(searchFrom ) {
  80. searchFrom = searchFrom || process.cwd();
  81. const dir = getDirectory.sync(searchFrom);
  82. return this.searchFromDirectorySync(dir);
  83. }
  84. searchFromDirectorySync(dir ) {
  85. const absoluteDir = path.resolve(process.cwd(), dir);
  86. const run = () => {
  87. const result = this.searchDirectorySync(absoluteDir);
  88. const nextDir = this.nextDirectoryToSearch(absoluteDir, result);
  89. if (nextDir) {
  90. return this.searchFromDirectorySync(nextDir);
  91. }
  92. return this.config.transform(result);
  93. };
  94. if (this.searchSyncCache) {
  95. return cacheWrapper(this.searchSyncCache, absoluteDir, run);
  96. }
  97. return run();
  98. }
  99. searchDirectory(dir ) {
  100. return this.config.searchPlaces.reduce((prevResultPromise, place) => {
  101. return prevResultPromise.then(prevResult => {
  102. if (this.shouldSearchStopWithResult(prevResult)) {
  103. return prevResult;
  104. }
  105. return this.loadSearchPlace(dir, place);
  106. });
  107. }, Promise.resolve(null));
  108. }
  109. searchDirectorySync(dir ) {
  110. let result = null;
  111. for (const place of this.config.searchPlaces) {
  112. result = this.loadSearchPlaceSync(dir, place);
  113. if (this.shouldSearchStopWithResult(result)) break;
  114. }
  115. return result;
  116. }
  117. shouldSearchStopWithResult(result ) {
  118. if (result === null) return false;
  119. if (result.isEmpty && this.config.ignoreEmptySearchPlaces) return false;
  120. return true;
  121. }
  122. loadSearchPlace(dir , place ) {
  123. const filepath = path.join(dir, place);
  124. return readFile(filepath).then(content => {
  125. return this.createCosmiconfigResult(filepath, content);
  126. });
  127. }
  128. loadSearchPlaceSync(dir , place ) {
  129. const filepath = path.join(dir, place);
  130. const content = readFile.sync(filepath);
  131. return this.createCosmiconfigResultSync(filepath, content);
  132. }
  133. nextDirectoryToSearch(
  134. currentDir ,
  135. currentResult
  136. ) {
  137. if (this.shouldSearchStopWithResult(currentResult)) {
  138. return null;
  139. }
  140. const nextDir = nextDirUp(currentDir);
  141. if (nextDir === currentDir || currentDir === this.config.stopDir) {
  142. return null;
  143. }
  144. return nextDir;
  145. }
  146. loadPackageProp(filepath , content ) {
  147. const parsedContent = loaders.loadJson(filepath, content);
  148. const packagePropValue = getPropertyByPath(
  149. parsedContent,
  150. this.config.packageProp
  151. );
  152. return packagePropValue || null;
  153. }
  154. getLoaderEntryForFile(filepath ) {
  155. if (path.basename(filepath) === 'package.json') {
  156. const loader = this.loadPackageProp.bind(this);
  157. return { sync: loader, async: loader };
  158. }
  159. const loaderKey = path.extname(filepath) || 'noExt';
  160. return this.config.loaders[loaderKey] || {};
  161. }
  162. getSyncLoaderForFile(filepath ) {
  163. const entry = this.getLoaderEntryForFile(filepath);
  164. if (!entry.sync) {
  165. throw new Error(
  166. `No sync loader specified for ${getExtensionDescription(filepath)}`
  167. );
  168. }
  169. return entry.sync;
  170. }
  171. getAsyncLoaderForFile(filepath ) {
  172. const entry = this.getLoaderEntryForFile(filepath);
  173. const loader = entry.async || entry.sync;
  174. if (!loader) {
  175. throw new Error(
  176. `No async loader specified for ${getExtensionDescription(filepath)}`
  177. );
  178. }
  179. return loader;
  180. }
  181. loadFileContent(
  182. mode ,
  183. filepath ,
  184. content
  185. ) {
  186. if (content === null) {
  187. return null;
  188. }
  189. if (content.trim() === '') {
  190. return undefined;
  191. }
  192. const loader =
  193. mode === MODE_SYNC
  194. ? this.getSyncLoaderForFile(filepath)
  195. : this.getAsyncLoaderForFile(filepath);
  196. return loader(filepath, content);
  197. }
  198. loadedContentToCosmiconfigResult(
  199. filepath ,
  200. loadedContent
  201. ) {
  202. if (loadedContent === null) {
  203. return null;
  204. }
  205. if (loadedContent === undefined) {
  206. return { filepath, config: undefined, isEmpty: true };
  207. }
  208. return { config: loadedContent, filepath };
  209. }
  210. createCosmiconfigResult(
  211. filepath ,
  212. content
  213. ) {
  214. return Promise.resolve()
  215. .then(() => {
  216. return this.loadFileContent('async', filepath, content);
  217. })
  218. .then(loaderResult => {
  219. return this.loadedContentToCosmiconfigResult(filepath, loaderResult);
  220. });
  221. }
  222. createCosmiconfigResultSync(
  223. filepath ,
  224. content
  225. ) {
  226. const loaderResult = this.loadFileContent('sync', filepath, content);
  227. return this.loadedContentToCosmiconfigResult(filepath, loaderResult);
  228. }
  229. validateFilePath(filepath ) {
  230. if (!filepath) {
  231. throw new Error('load and loadSync must pass a non-empty string');
  232. }
  233. }
  234. load(filepath ) {
  235. return Promise.resolve().then(() => {
  236. this.validateFilePath(filepath);
  237. const absoluteFilePath = path.resolve(process.cwd(), filepath);
  238. return cacheWrapper(this.loadCache, absoluteFilePath, () => {
  239. return readFile(absoluteFilePath, { throwNotFound: true })
  240. .then(content => {
  241. return this.createCosmiconfigResult(absoluteFilePath, content);
  242. })
  243. .then(this.config.transform);
  244. });
  245. });
  246. }
  247. loadSync(filepath ) {
  248. this.validateFilePath(filepath);
  249. const absoluteFilePath = path.resolve(process.cwd(), filepath);
  250. return cacheWrapper(this.loadSyncCache, absoluteFilePath, () => {
  251. const content = readFile.sync(absoluteFilePath, { throwNotFound: true });
  252. const result = this.createCosmiconfigResultSync(
  253. absoluteFilePath,
  254. content
  255. );
  256. return this.config.transform(result);
  257. });
  258. }
  259. }
  260. module.exports = function createExplorer(options ) {
  261. const explorer = new Explorer(options);
  262. return {
  263. search: explorer.search.bind(explorer),
  264. searchSync: explorer.searchSync.bind(explorer),
  265. load: explorer.load.bind(explorer),
  266. loadSync: explorer.loadSync.bind(explorer),
  267. clearLoadCache: explorer.clearLoadCache.bind(explorer),
  268. clearSearchCache: explorer.clearSearchCache.bind(explorer),
  269. clearCaches: explorer.clearCaches.bind(explorer),
  270. };
  271. };
  272. function nextDirUp(dir ) {
  273. return path.dirname(dir);
  274. }
  275. function getExtensionDescription(filepath ) {
  276. const ext = path.extname(filepath);
  277. return ext ? `extension "${ext}"` : 'files without extensions';
  278. }