index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = loader;
  6. exports.pitch = pitch;
  7. /* eslint-disable
  8. import/order
  9. */
  10. const fs = require('fs');
  11. const path = require('path');
  12. const normalizePath = require('normalize-path');
  13. const async = require('neo-async');
  14. const crypto = require('crypto');
  15. const mkdirp = require('mkdirp');
  16. const {
  17. getOptions
  18. } = require('loader-utils');
  19. const validateOptions = require('schema-utils');
  20. const pkg = require('../package.json');
  21. const env = process.env.NODE_ENV || 'development';
  22. const schema = require('./options.json');
  23. const defaults = {
  24. cacheContext: '',
  25. cacheDirectory: path.resolve('.cache-loader'),
  26. cacheIdentifier: `cache-loader:${pkg.version} ${env}`,
  27. cacheKey,
  28. read,
  29. write
  30. };
  31. function pathWithCacheContext(cacheContext, originalPath) {
  32. if (!cacheContext) {
  33. return originalPath;
  34. }
  35. if (originalPath.includes(cacheContext)) {
  36. return originalPath.split('!').map(subPath => normalizePath(path.relative(cacheContext, subPath))).join('!');
  37. }
  38. return originalPath.split('!').map(subPath => normalizePath(path.resolve(cacheContext, subPath))).join('!');
  39. }
  40. function loader(...args) {
  41. const options = Object.assign({}, defaults, getOptions(this));
  42. validateOptions(schema, options, 'Cache Loader');
  43. const {
  44. write: writeFn
  45. } = options;
  46. const callback = this.async();
  47. const {
  48. data
  49. } = this;
  50. const dependencies = this.getDependencies().concat(this.loaders.map(l => l.path));
  51. const contextDependencies = this.getContextDependencies(); // Should the file get cached?
  52. let cache = true; // this.fs can be undefined
  53. // e.g when using the thread-loader
  54. // fallback to the fs module
  55. const FS = this.fs || fs;
  56. const toDepDetails = (dep, mapCallback) => {
  57. FS.stat(dep, (err, stats) => {
  58. if (err) {
  59. mapCallback(err);
  60. return;
  61. }
  62. const mtime = stats.mtime.getTime();
  63. if (mtime / 1000 >= Math.floor(data.startTime / 1000)) {
  64. // Don't trust mtime.
  65. // File was changed while compiling
  66. // or it could be an inaccurate filesystem.
  67. cache = false;
  68. }
  69. mapCallback(null, {
  70. path: pathWithCacheContext(options.cacheContext, dep),
  71. mtime
  72. });
  73. });
  74. };
  75. async.parallel([cb => async.mapLimit(dependencies, 20, toDepDetails, cb), cb => async.mapLimit(contextDependencies, 20, toDepDetails, cb)], (err, taskResults) => {
  76. if (err) {
  77. callback(null, ...args);
  78. return;
  79. }
  80. if (!cache) {
  81. callback(null, ...args);
  82. return;
  83. }
  84. const [deps, contextDeps] = taskResults;
  85. writeFn(data.cacheKey, {
  86. remainingRequest: data.remainingRequest,
  87. dependencies: deps,
  88. contextDependencies: contextDeps,
  89. result: args
  90. }, () => {
  91. // ignore errors here
  92. callback(null, ...args);
  93. });
  94. });
  95. }
  96. function pitch(remainingRequest, prevRequest, dataInput) {
  97. const options = Object.assign({}, defaults, getOptions(this));
  98. validateOptions(schema, options, 'Cache Loader (Pitch)');
  99. const {
  100. read: readFn,
  101. cacheContext,
  102. cacheKey: cacheKeyFn
  103. } = options;
  104. const callback = this.async();
  105. const data = dataInput;
  106. data.remainingRequest = pathWithCacheContext(cacheContext, remainingRequest);
  107. data.cacheKey = cacheKeyFn(options, data.remainingRequest);
  108. readFn(data.cacheKey, (readErr, cacheData) => {
  109. if (readErr) {
  110. callback();
  111. return;
  112. }
  113. if (cacheData.remainingRequest !== data.remainingRequest) {
  114. // in case of a hash conflict
  115. callback();
  116. return;
  117. }
  118. const FS = this.fs || fs;
  119. async.each(cacheData.dependencies.concat(cacheData.contextDependencies), (dep, eachCallback) => {
  120. FS.stat(dep.path, (statErr, stats) => {
  121. if (statErr) {
  122. eachCallback(statErr);
  123. return;
  124. }
  125. if (stats.mtime.getTime() !== dep.mtime) {
  126. eachCallback(true);
  127. return;
  128. }
  129. eachCallback();
  130. });
  131. }, err => {
  132. if (err) {
  133. data.startTime = Date.now();
  134. callback();
  135. return;
  136. }
  137. cacheData.dependencies.forEach(dep => this.addDependency(pathWithCacheContext(cacheContext, dep.path)));
  138. cacheData.contextDependencies.forEach(dep => this.addContextDependency(pathWithCacheContext(cacheContext, dep.path)));
  139. callback(null, ...cacheData.result);
  140. });
  141. });
  142. }
  143. function digest(str) {
  144. return crypto.createHash('md5').update(str).digest('hex');
  145. }
  146. const directories = new Set();
  147. function write(key, data, callback) {
  148. const dirname = path.dirname(key);
  149. const content = JSON.stringify(data);
  150. if (directories.has(dirname)) {
  151. // for performance skip creating directory
  152. fs.writeFile(key, content, 'utf-8', callback);
  153. } else {
  154. mkdirp(dirname, mkdirErr => {
  155. if (mkdirErr) {
  156. callback(mkdirErr);
  157. return;
  158. }
  159. directories.add(dirname);
  160. fs.writeFile(key, content, 'utf-8', callback);
  161. });
  162. }
  163. }
  164. function read(key, callback) {
  165. fs.readFile(key, 'utf-8', (err, content) => {
  166. if (err) {
  167. callback(err);
  168. return;
  169. }
  170. try {
  171. const data = JSON.parse(content);
  172. callback(null, data);
  173. } catch (e) {
  174. callback(e);
  175. }
  176. });
  177. }
  178. function cacheKey(options, request) {
  179. const {
  180. cacheIdentifier,
  181. cacheDirectory
  182. } = options;
  183. const hash = digest(`${cacheIdentifier}\n${request}`);
  184. return path.join(cacheDirectory, `${hash}.json`);
  185. }