fs-cache.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. "use strict";
  2. /**
  3. * Filesystem cache
  4. *
  5. * Given a file and a transform function, cache the result into files
  6. * or retrieve the previously cached files if the given file is already known.
  7. *
  8. * @see https://github.com/babel/babel-loader/issues/34
  9. * @see https://github.com/babel/babel-loader/pull/41
  10. */
  11. var crypto = require("crypto");
  12. var mkdirp = require("mkdirp");
  13. var findCacheDir = require("find-cache-dir");
  14. var fs = require("fs");
  15. var os = require("os");
  16. var path = require("path");
  17. var zlib = require("zlib");
  18. var defaultCacheDirectory = null; // Lazily instantiated when needed
  19. /**
  20. * Read the contents from the compressed file.
  21. *
  22. * @async
  23. * @params {String} filename
  24. * @params {Function} callback
  25. */
  26. var read = function read(filename, callback) {
  27. return fs.readFile(filename, function (err, data) {
  28. if (err) return callback(err);
  29. return zlib.gunzip(data, function (err, content) {
  30. if (err) return callback(err);
  31. var result = {};
  32. try {
  33. result = JSON.parse(content);
  34. } catch (e) {
  35. return callback(e);
  36. }
  37. return callback(null, result);
  38. });
  39. });
  40. };
  41. /**
  42. * Write contents into a compressed file.
  43. *
  44. * @async
  45. * @params {String} filename
  46. * @params {String} result
  47. * @params {Function} callback
  48. */
  49. var write = function write(filename, result, callback) {
  50. var content = JSON.stringify(result);
  51. return zlib.gzip(content, function (err, data) {
  52. if (err) return callback(err);
  53. return fs.writeFile(filename, data, callback);
  54. });
  55. };
  56. /**
  57. * Build the filename for the cached file
  58. *
  59. * @params {String} source File source code
  60. * @params {Object} options Options used
  61. *
  62. * @return {String}
  63. */
  64. var filename = function filename(source, identifier, options) {
  65. var hash = crypto.createHash("md4");
  66. var contents = JSON.stringify({
  67. source: source,
  68. options: options,
  69. identifier: identifier
  70. });
  71. hash.update(contents);
  72. return hash.digest("hex") + ".json.gz";
  73. };
  74. /**
  75. * Handle the cache
  76. *
  77. * @params {String} directory
  78. * @params {Object} params
  79. * @params {Function} callback
  80. */
  81. var handleCache = function handleCache(directory, params, callback) {
  82. var source = params.source;
  83. var options = params.options || {};
  84. var transform = params.transform;
  85. var identifier = params.identifier;
  86. var shouldFallback = typeof params.directory !== "string" && directory !== os.tmpdir();
  87. // Make sure the directory exists.
  88. mkdirp(directory, function (err) {
  89. // Fallback to tmpdir if node_modules folder not writable
  90. if (err) return shouldFallback ? handleCache(os.tmpdir(), params, callback) : callback(err);
  91. var file = path.join(directory, filename(source, identifier, options));
  92. return read(file, function (err, content) {
  93. var result = {};
  94. // No errors mean that the file was previously cached
  95. // we just need to return it
  96. if (!err) return callback(null, content);
  97. // Otherwise just transform the file
  98. // return it to the user asap and write it in cache
  99. try {
  100. result = transform(source, options);
  101. } catch (error) {
  102. return callback(error);
  103. }
  104. return write(file, result, function (err) {
  105. // Fallback to tmpdir if node_modules folder not writable
  106. if (err) return shouldFallback ? handleCache(os.tmpdir(), params, callback) : callback(err);
  107. callback(null, result);
  108. });
  109. });
  110. });
  111. };
  112. /**
  113. * Retrieve file from cache, or create a new one for future reads
  114. *
  115. * @async
  116. * @param {Object} params
  117. * @param {String} params.directory Directory to store cached files
  118. * @param {String} params.identifier Unique identifier to bust cache
  119. * @param {String} params.source Original contents of the file to be cached
  120. * @param {Object} params.options Options to be given to the transform fn
  121. * @param {Function} params.transform Function that will transform the
  122. * original file and whose result will be
  123. * cached
  124. *
  125. * @param {Function<err, result>} callback
  126. *
  127. * @example
  128. *
  129. * cache({
  130. * directory: '.tmp/cache',
  131. * identifier: 'babel-loader-cachefile',
  132. * source: *source code from file*,
  133. * options: {
  134. * experimental: true,
  135. * runtime: true
  136. * },
  137. * transform: function(source, options) {
  138. * var content = *do what you need with the source*
  139. * return content;
  140. * }
  141. * }, function(err, result) {
  142. *
  143. * });
  144. */
  145. module.exports = function (params, callback) {
  146. var directory = void 0;
  147. if (typeof params.directory === "string") {
  148. directory = params.directory;
  149. } else {
  150. if (defaultCacheDirectory === null) {
  151. defaultCacheDirectory = findCacheDir({ name: "babel-loader" }) || os.tmpdir();
  152. }
  153. directory = defaultCacheDirectory;
  154. }
  155. handleCache(directory, params, callback);
  156. };