index.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = loader;
  6. exports.pitch = pitch;
  7. exports.raw = void 0;
  8. /* eslint-disable
  9. import/order
  10. */
  11. const fs = require('fs');
  12. const os = require('os');
  13. const path = require('path');
  14. const async = require('neo-async');
  15. const crypto = require('crypto');
  16. const mkdirp = require('mkdirp');
  17. const findCacheDir = require('find-cache-dir');
  18. const BJSON = require('buffer-json');
  19. const {
  20. getOptions
  21. } = require('loader-utils');
  22. const validateOptions = require('schema-utils');
  23. const pkg = require('../package.json');
  24. const env = process.env.NODE_ENV || 'development';
  25. const schema = require('./options.json');
  26. const defaults = {
  27. cacheContext: '',
  28. cacheDirectory: findCacheDir({
  29. name: 'cache-loader'
  30. }) || os.tmpdir(),
  31. cacheIdentifier: `cache-loader:${pkg.version} ${env}`,
  32. cacheKey,
  33. compare,
  34. precision: 0,
  35. read,
  36. readOnly: false,
  37. write
  38. };
  39. function pathWithCacheContext(cacheContext, originalPath) {
  40. if (!cacheContext) {
  41. return originalPath;
  42. }
  43. if (originalPath.includes(cacheContext)) {
  44. return originalPath.split('!').map(subPath => path.relative(cacheContext, subPath)).join('!');
  45. }
  46. return originalPath.split('!').map(subPath => path.resolve(cacheContext, subPath)).join('!');
  47. }
  48. function roundMs(mtime, precision) {
  49. return Math.floor(mtime / precision) * precision;
  50. } // NOTE: We should only apply `pathWithCacheContext` transformations
  51. // right before writing. Every other internal steps with the paths
  52. // should be accomplish over absolute paths. Otherwise we have the risk
  53. // to break watchpack -> chokidar watch logic over webpack@4 --watch
  54. function loader(...args) {
  55. const options = Object.assign({}, defaults, getOptions(this));
  56. validateOptions(schema, options, {
  57. name: 'Cache Loader',
  58. baseDataPath: 'options'
  59. });
  60. const {
  61. readOnly,
  62. write: writeFn
  63. } = options; // In case we are under a readOnly mode on cache-loader
  64. // we don't want to write or update any cache file
  65. if (readOnly) {
  66. this.callback(null, ...args);
  67. return;
  68. }
  69. const callback = this.async();
  70. const {
  71. data
  72. } = this;
  73. const dependencies = this.getDependencies().concat(this.loaders.map(l => l.path));
  74. const contextDependencies = this.getContextDependencies(); // Should the file get cached?
  75. let cache = true; // this.fs can be undefined
  76. // e.g when using the thread-loader
  77. // fallback to the fs module
  78. const FS = this.fs || fs;
  79. const toDepDetails = (dep, mapCallback) => {
  80. FS.stat(dep, (err, stats) => {
  81. if (err) {
  82. mapCallback(err);
  83. return;
  84. }
  85. const mtime = stats.mtime.getTime();
  86. if (mtime / 1000 >= Math.floor(data.startTime / 1000)) {
  87. // Don't trust mtime.
  88. // File was changed while compiling
  89. // or it could be an inaccurate filesystem.
  90. cache = false;
  91. }
  92. mapCallback(null, {
  93. path: pathWithCacheContext(options.cacheContext, dep),
  94. mtime
  95. });
  96. });
  97. };
  98. async.parallel([cb => async.mapLimit(dependencies, 20, toDepDetails, cb), cb => async.mapLimit(contextDependencies, 20, toDepDetails, cb)], (err, taskResults) => {
  99. if (err) {
  100. callback(null, ...args);
  101. return;
  102. }
  103. if (!cache) {
  104. callback(null, ...args);
  105. return;
  106. }
  107. const [deps, contextDeps] = taskResults;
  108. writeFn(data.cacheKey, {
  109. remainingRequest: pathWithCacheContext(options.cacheContext, data.remainingRequest),
  110. dependencies: deps,
  111. contextDependencies: contextDeps,
  112. result: args
  113. }, () => {
  114. // ignore errors here
  115. callback(null, ...args);
  116. });
  117. });
  118. } // NOTE: We should apply `pathWithCacheContext` transformations
  119. // right after reading. Every other internal steps with the paths
  120. // should be accomplish over absolute paths. Otherwise we have the risk
  121. // to break watchpack -> chokidar watch logic over webpack@4 --watch
  122. function pitch(remainingRequest, prevRequest, dataInput) {
  123. const options = Object.assign({}, defaults, getOptions(this));
  124. validateOptions(schema, options, {
  125. name: 'Cache Loader (Pitch)',
  126. baseDataPath: 'options'
  127. });
  128. const {
  129. cacheContext,
  130. cacheKey: cacheKeyFn,
  131. compare: compareFn,
  132. read: readFn,
  133. readOnly,
  134. precision
  135. } = options;
  136. const callback = this.async();
  137. const data = dataInput;
  138. data.remainingRequest = remainingRequest;
  139. data.cacheKey = cacheKeyFn(options, data.remainingRequest);
  140. readFn(data.cacheKey, (readErr, cacheData) => {
  141. if (readErr) {
  142. callback();
  143. return;
  144. } // We need to patch every path within data on cache with the cacheContext,
  145. // or it would cause problems when watching
  146. if (pathWithCacheContext(options.cacheContext, cacheData.remainingRequest) !== data.remainingRequest) {
  147. // in case of a hash conflict
  148. callback();
  149. return;
  150. }
  151. const FS = this.fs || fs;
  152. async.each(cacheData.dependencies.concat(cacheData.contextDependencies), (dep, eachCallback) => {
  153. // Applying reverse path transformation, in case they are relatives, when
  154. // reading from cache
  155. const contextDep = { ...dep,
  156. path: pathWithCacheContext(options.cacheContext, dep.path)
  157. };
  158. FS.stat(contextDep.path, (statErr, stats) => {
  159. if (statErr) {
  160. eachCallback(statErr);
  161. return;
  162. } // When we are under a readOnly config on cache-loader
  163. // we don't want to emit any other error than a
  164. // file stat error
  165. if (readOnly) {
  166. eachCallback();
  167. return;
  168. }
  169. const compStats = stats;
  170. const compDep = contextDep;
  171. if (precision > 1) {
  172. ['atime', 'mtime', 'ctime', 'birthtime'].forEach(key => {
  173. const msKey = `${key}Ms`;
  174. const ms = roundMs(stats[msKey], precision);
  175. compStats[msKey] = ms;
  176. compStats[key] = new Date(ms);
  177. });
  178. compDep.mtime = roundMs(dep.mtime, precision);
  179. } // If the compare function returns false
  180. // we not read from cache
  181. if (compareFn(compStats, compDep) !== true) {
  182. eachCallback(true);
  183. return;
  184. }
  185. eachCallback();
  186. });
  187. }, err => {
  188. if (err) {
  189. data.startTime = Date.now();
  190. callback();
  191. return;
  192. }
  193. cacheData.dependencies.forEach(dep => this.addDependency(pathWithCacheContext(cacheContext, dep.path)));
  194. cacheData.contextDependencies.forEach(dep => this.addContextDependency(pathWithCacheContext(cacheContext, dep.path)));
  195. callback(null, ...cacheData.result);
  196. });
  197. });
  198. }
  199. function digest(str) {
  200. return crypto.createHash('md5').update(str).digest('hex');
  201. }
  202. const directories = new Set();
  203. function write(key, data, callback) {
  204. const dirname = path.dirname(key);
  205. const content = BJSON.stringify(data);
  206. if (directories.has(dirname)) {
  207. // for performance skip creating directory
  208. fs.writeFile(key, content, 'utf-8', callback);
  209. } else {
  210. mkdirp(dirname, mkdirErr => {
  211. if (mkdirErr) {
  212. callback(mkdirErr);
  213. return;
  214. }
  215. directories.add(dirname);
  216. fs.writeFile(key, content, 'utf-8', callback);
  217. });
  218. }
  219. }
  220. function read(key, callback) {
  221. fs.readFile(key, 'utf-8', (err, content) => {
  222. if (err) {
  223. callback(err);
  224. return;
  225. }
  226. try {
  227. const data = BJSON.parse(content);
  228. callback(null, data);
  229. } catch (e) {
  230. callback(e);
  231. }
  232. });
  233. }
  234. function cacheKey(options, request) {
  235. const {
  236. cacheIdentifier,
  237. cacheDirectory
  238. } = options;
  239. const hash = digest(`${cacheIdentifier}\n${request}`);
  240. return path.join(cacheDirectory, `${hash}.json`);
  241. }
  242. function compare(stats, dep) {
  243. return stats.mtime.getTime() === dep.mtime;
  244. }
  245. const raw = true;
  246. exports.raw = raw;