CacheMd5.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. const crypto = require('crypto');
  2. const path = require('path');
  3. const lodash = require('lodash');
  4. const bulkFsTask = require('./util/bulk-fs-task');
  5. const pluginCompat = require('./util/plugin-compat');
  6. const promisify = require('./util/promisify');
  7. const values = require('./util/Object.values');
  8. const { parityCacheFromCache, pushParityWriteOps } = require('./util/parity');
  9. class Md5Cache {
  10. apply(compiler) {
  11. let md5Cache = {};
  12. let parityCache = {};
  13. const fileMd5s = {};
  14. const cachedMd5s = {};
  15. let fileTimestamps = {};
  16. const contextMd5s = {};
  17. let contextTimestamps = {};
  18. let md5CacheSerializer;
  19. let latestStats = {};
  20. let latestMd5s = {};
  21. let unbuildMd5s = {};
  22. let fileDependencies = [];
  23. let contextDependencies = [];
  24. let stat;
  25. let readdir;
  26. let readFile;
  27. let mtime;
  28. let md5;
  29. let fileStamp;
  30. let contextStamp;
  31. let contextStamps;
  32. function bindFS() {
  33. stat = promisify(compiler.inputFileSystem.stat, {
  34. context: compiler.inputFileSystem,
  35. });
  36. // stat = promisify(fs.stat, {context: fs});
  37. readdir = promisify(compiler.inputFileSystem.readdir, {
  38. context: compiler.inputFileSystem,
  39. });
  40. readFile = promisify(compiler.inputFileSystem.readFile, {
  41. context: compiler.inputFileSystem,
  42. });
  43. mtime = file =>
  44. stat(file)
  45. .then(stat => +stat.mtime)
  46. .catch(() => 0);
  47. md5 = file =>
  48. readFile(file)
  49. .then(contents =>
  50. crypto
  51. .createHash('md5')
  52. .update(contents, 'utf8')
  53. .digest('hex'),
  54. )
  55. .catch(() => '');
  56. fileStamp = (file, stats) => {
  57. if (compiler.__hardSource_fileTimestamps[file]) {
  58. return compiler.__hardSource_fileTimestamps[file];
  59. } else {
  60. if (!stats[file]) {
  61. stats[file] = stat(file);
  62. }
  63. return stats[file].then(stat => {
  64. const mtime = +stat.mtime;
  65. compiler.__hardSource_fileTimestamps[file] = mtime;
  66. return mtime;
  67. });
  68. }
  69. };
  70. contextStamp = (dir, stats) => {
  71. const context = {};
  72. let selfTime = 0;
  73. function walk(dir) {
  74. return readdir(dir)
  75. .then(items =>
  76. Promise.all(
  77. items.map(item => {
  78. const file = path.join(dir, item);
  79. if (!stats[file]) {
  80. stats[file] = stat(file);
  81. }
  82. return stats[file].then(
  83. stat => {
  84. if (stat.isDirectory()) {
  85. return walk(path.join(dir, item)).then(items2 =>
  86. items2.map(item2 => path.join(item, item2)),
  87. );
  88. }
  89. if (+stat.mtime > selfTime) {
  90. selfTime = +stat.mtime;
  91. }
  92. return item;
  93. },
  94. () => {
  95. return;
  96. },
  97. );
  98. }),
  99. ),
  100. )
  101. .catch(() => [])
  102. .then(items =>
  103. items
  104. .reduce((carry, item) => carry.concat(item), [])
  105. .filter(Boolean),
  106. );
  107. }
  108. return walk(dir).then(items => {
  109. items.sort();
  110. const selfHash = crypto.createHash('md5');
  111. items.forEach(item => {
  112. selfHash.update(item);
  113. });
  114. context.mtime = selfTime;
  115. context.hash = selfHash.digest('hex');
  116. return context;
  117. });
  118. };
  119. contextStamps = (contextDependencies, stats) => {
  120. stats = stats || {};
  121. const contexts = {};
  122. contextDependencies.forEach(context => {
  123. contexts[context] = { files: [], mtime: 0, hash: '' };
  124. });
  125. const compilerContextTs = compiler.contextTimestamps;
  126. contextDependencies.forEach(contextPath => {
  127. const _context = contextStamp(contextPath, stats);
  128. if (!_context.then) {
  129. contexts[contextPath] = _context;
  130. } else {
  131. contexts[contextPath] = _context.then(context => {
  132. contexts[contextPath] = context;
  133. return context;
  134. });
  135. }
  136. });
  137. return contexts;
  138. };
  139. }
  140. if (compiler.inputFileSystem) {
  141. bindFS();
  142. } else {
  143. pluginCompat.tap(
  144. compiler,
  145. 'afterEnvironment',
  146. 'HardSource - Md5Cache',
  147. bindFS,
  148. );
  149. }
  150. pluginCompat.tap(
  151. compiler,
  152. '_hardSourceCreateSerializer',
  153. 'HardSource - Md5Cache',
  154. (cacheSerializerFactory, cacheDirPath) => {
  155. md5CacheSerializer = cacheSerializerFactory.create({
  156. name: 'md5',
  157. type: 'data',
  158. autoParse: true,
  159. cacheDirPath,
  160. });
  161. },
  162. );
  163. pluginCompat.tap(
  164. compiler,
  165. '_hardSourceResetCache',
  166. 'HardSource - Md5Cache',
  167. () => {
  168. md5Cache = {};
  169. parityCache = {};
  170. fileTimestamps = {};
  171. contextTimestamps = {};
  172. },
  173. );
  174. pluginCompat.tapPromise(
  175. compiler,
  176. '_hardSourceReadCache',
  177. 'HardSource - Md5Cache',
  178. ({ contextKeys, contextNormalPath }) =>
  179. md5CacheSerializer
  180. .read()
  181. .then(_md5Cache => {
  182. Object.keys(_md5Cache).forEach(key => {
  183. if (key.startsWith('__hardSource_parityToken')) {
  184. parityCache[key] = _md5Cache[key];
  185. delete _md5Cache[key];
  186. }
  187. });
  188. return _md5Cache;
  189. })
  190. .then(contextKeys(compiler, contextNormalPath))
  191. .then(_md5Cache => {
  192. Object.keys(_md5Cache).forEach(key => {
  193. if (typeof _md5Cache[key] === 'string') {
  194. _md5Cache[key] = JSON.parse(_md5Cache[key]);
  195. }
  196. if (_md5Cache[key] && _md5Cache[key].hash) {
  197. cachedMd5s[key] = _md5Cache[key].hash;
  198. }
  199. });
  200. md5Cache = _md5Cache;
  201. })
  202. .then(() => {
  203. const dependencies = Object.keys(md5Cache);
  204. fileDependencies = dependencies.filter(
  205. file => md5Cache[file].isFile,
  206. );
  207. contextDependencies = dependencies.filter(
  208. file => md5Cache[file].isDirectory,
  209. );
  210. }),
  211. );
  212. pluginCompat.tap(
  213. compiler,
  214. '_hardSourceParityCache',
  215. 'HardSource - Md5Cache',
  216. parityRoot => {
  217. parityCacheFromCache('Md5', parityRoot, parityCache);
  218. },
  219. );
  220. pluginCompat.tapPromise(
  221. compiler,
  222. '_hardSourceVerifyCache',
  223. 'HardSource - Md5Cache',
  224. () => {
  225. latestStats = {};
  226. latestMd5s = {};
  227. unbuildMd5s = {};
  228. const stats = {};
  229. // var md5s = latestMd5s;
  230. // Prepare objects to mark md5s to delete if they are not used.
  231. for (const key in cachedMd5s) {
  232. unbuildMd5s[key] = null;
  233. }
  234. return Promise.all([
  235. (() => {
  236. const compilerFileTs = (compiler.__hardSource_fileTimestamps = {});
  237. const fileTs = (fileTimestamps = {});
  238. return bulkFsTask(fileDependencies, (file, task) => {
  239. if (compiler.__hardSource_fileTimestamps[file]) {
  240. return compiler.__hardSource_fileTimestamps[file];
  241. } else {
  242. compiler.inputFileSystem.stat(
  243. file,
  244. task((err, value) => {
  245. if (err) {
  246. return 0;
  247. }
  248. const mtime = +value.mtime;
  249. compiler.__hardSource_fileTimestamps[file] = mtime;
  250. return mtime;
  251. }),
  252. );
  253. }
  254. }).then(mtimes => {
  255. const bulk = lodash.zip(fileDependencies, mtimes);
  256. return bulkFsTask(bulk, (item, task) => {
  257. const file = item[0];
  258. const mtime = item[1];
  259. fileTs[file] = mtime || 0;
  260. if (!compiler.__hardSource_fileTimestamps[file]) {
  261. compiler.__hardSource_fileTimestamps[file] = mtime;
  262. }
  263. compiler.inputFileSystem.readFile(
  264. file,
  265. task(function(err, body) {
  266. if (err) {
  267. fileMd5s[file] = '';
  268. return;
  269. }
  270. const hash = crypto
  271. .createHash('md5')
  272. .update(body, 'utf8')
  273. .digest('hex');
  274. fileMd5s[file] = hash;
  275. }),
  276. );
  277. });
  278. });
  279. })(),
  280. (() => {
  281. compiler.contextTimestamps = compiler.contextTimestamps || {};
  282. const contextTs = (contextTimestamps = {});
  283. const contexts = contextStamps(contextDependencies, stats);
  284. return Promise.all(values(contexts)).then(function() {
  285. for (var contextPath in contexts) {
  286. var context = contexts[contextPath];
  287. if (!compiler.contextTimestamps[contextPath]) {
  288. compiler.contextTimestamps[contextPath] = context.mtime;
  289. }
  290. contextTimestamps[contextPath] = context.mtime;
  291. fileMd5s[contextPath] = context.hash;
  292. }
  293. });
  294. })(),
  295. ]);
  296. },
  297. );
  298. pluginCompat.tap(
  299. compiler,
  300. 'compilation',
  301. 'HardSource - Md5Cache',
  302. compilation => {
  303. compilation.__hardSourceFileMd5s = fileMd5s;
  304. compilation.__hardSourceCachedMd5s = cachedMd5s;
  305. compilation.__hardSourceFileTimestamps = fileTimestamps;
  306. },
  307. );
  308. pluginCompat.tapPromise(
  309. compiler,
  310. '_hardSourceWriteCache',
  311. 'HardSource - Md5Cache',
  312. (compilation, { relateNormalPath, contextNormalPath }) => {
  313. const moduleOps = [];
  314. const dataOps = [];
  315. const md5Ops = [];
  316. const assetOps = [];
  317. const moduleResolveOps = [];
  318. const missingOps = [];
  319. const resolverOps = [];
  320. let buildingMd5s = {};
  321. function buildMd5Ops(dependencies) {
  322. dependencies.forEach(file => {
  323. function updateMd5CacheItem(value) {
  324. if (
  325. !md5Cache[file] ||
  326. (md5Cache[file] && md5Cache[file].hash !== value.hash)
  327. ) {
  328. md5Cache[file] = value;
  329. cachedMd5s[file] = value.hash;
  330. md5Ops.push({
  331. key: relateNormalPath(compiler, file),
  332. value: value,
  333. });
  334. } else if (
  335. !value.mtime &&
  336. md5Cache[file] &&
  337. md5Cache[file].mtime !== value.mtime
  338. ) {
  339. md5Cache[file] = value;
  340. cachedMd5s[file] = value.hash;
  341. }
  342. }
  343. const building = buildingMd5s[file];
  344. if (!building.then) {
  345. updateMd5CacheItem(building);
  346. } else {
  347. buildingMd5s[file] = building.then(updateMd5CacheItem);
  348. }
  349. });
  350. }
  351. const fileDependencies = Array.from(compilation.fileDependencies).map(
  352. file => contextNormalPath(compiler, file),
  353. );
  354. const MD5_TIME_PRECISION_BUFFER = 2000;
  355. fileDependencies.forEach(file => {
  356. if (buildingMd5s[file]) {
  357. return;
  358. }
  359. delete unbuildMd5s[file];
  360. if (fileMd5s[file]) {
  361. buildingMd5s[file] = {
  362. // Subtract a small buffer from now for file systems that record
  363. // lower precision mtimes.
  364. mtime: Date.now() - MD5_TIME_PRECISION_BUFFER,
  365. hash: fileMd5s[file],
  366. isFile: true,
  367. isDirectory: false,
  368. };
  369. } else {
  370. buildingMd5s[file] = md5(file).then(hash => ({
  371. mtime: Date.now() - MD5_TIME_PRECISION_BUFFER,
  372. hash,
  373. isFile: true,
  374. isDirectory: false,
  375. }));
  376. }
  377. });
  378. buildMd5Ops(fileDependencies);
  379. const contextDependencies = Array.from(
  380. compilation.contextDependencies,
  381. ).map(file => contextNormalPath(compiler, file));
  382. const contexts = contextStamps(contextDependencies);
  383. contextDependencies.forEach(file => {
  384. if (buildingMd5s[file]) {
  385. return;
  386. }
  387. delete unbuildMd5s[file];
  388. let context = contexts[file];
  389. if (!context.then) {
  390. // Subtract a small buffer from now for file systems that record lower
  391. // precision mtimes.
  392. context.mtime = Date.now() - MD5_TIME_PRECISION_BUFFER;
  393. context.isFile = false;
  394. context.isDirectory = true;
  395. } else {
  396. context = context.then(context => {
  397. context.mtime = Date.now() - MD5_TIME_PRECISION_BUFFER;
  398. context.isFile = false;
  399. context.isDirectory = true;
  400. return context;
  401. });
  402. }
  403. buildingMd5s[file] = context;
  404. });
  405. buildMd5Ops(contextDependencies);
  406. const writeMd5Ops = Promise.all(
  407. Object.keys(buildingMd5s).map(key => buildingMd5s[key]),
  408. ).then(() => {
  409. if (!compilation.compiler.parentCompilation) {
  410. for (const key in unbuildMd5s) {
  411. md5Ops.push({
  412. key: relateNormalPath(compiler, key),
  413. value: unbuildMd5s[key],
  414. });
  415. }
  416. }
  417. pushParityWriteOps(compilation, md5Ops);
  418. });
  419. return writeMd5Ops.then(() => md5CacheSerializer.write(md5Ops));
  420. },
  421. );
  422. }
  423. }
  424. module.exports = Md5Cache;