generate.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. 'use strict';
  2. const fs = require('hexo-fs');
  3. const pathFn = require('path');
  4. const Promise = require('bluebird');
  5. const prettyHrtime = require('pretty-hrtime');
  6. const chalk = require('chalk');
  7. const tildify = require('tildify');
  8. const Transform = require('stream').Transform;
  9. const PassThrough = require('stream').PassThrough;
  10. const util = require('hexo-util');
  11. const join = pathFn.join;
  12. function generateConsole(args = {}) {
  13. const force = args.f || args.force;
  14. const bail = args.b || args.bail;
  15. const route = this.route;
  16. const publicDir = this.public_dir;
  17. const log = this.log;
  18. const self = this;
  19. let start = process.hrtime();
  20. const Cache = this.model('Cache');
  21. const generatingFiles = {};
  22. function generateFile(path) {
  23. // Skip if the file is generating
  24. if (generatingFiles[path]) return Promise.resolve();
  25. // Lock the file
  26. generatingFiles[path] = true;
  27. const dest = join(publicDir, path);
  28. return fs.exists(dest).then(exist => {
  29. if (force || !exist) return writeFile(path, true);
  30. if (route.isModified(path)) return writeFile(path);
  31. }).finally(() => {
  32. // Unlock the file
  33. generatingFiles[path] = false;
  34. });
  35. }
  36. function writeFile(path, force) {
  37. const dest = join(publicDir, path);
  38. const cacheId = `public/${path}`;
  39. const dataStream = wrapDataStream(route.get(path), {bail});
  40. const cacheStream = new CacheStream();
  41. const hashStream = new util.HashStream();
  42. // Get data => Cache data => Calculate hash
  43. return pipeStream(dataStream, cacheStream, hashStream).then(() => {
  44. const cache = Cache.findById(cacheId);
  45. const hash = hashStream.read().toString('hex');
  46. // Skip generating if hash is unchanged
  47. if (!force && cache && cache.hash === hash) {
  48. return;
  49. }
  50. // Save new hash to cache
  51. return Cache.save({
  52. _id: cacheId,
  53. hash
  54. }).then(() => // Write cache data to public folder
  55. fs.writeFile(dest, cacheStream.getCache())).then(() => {
  56. log.info('Generated: %s', chalk.magenta(path));
  57. return true;
  58. });
  59. }).finally(() => {
  60. // Destroy cache
  61. cacheStream.destroy();
  62. });
  63. }
  64. function deleteFile(path) {
  65. const dest = join(publicDir, path);
  66. return fs.unlink(dest).then(() => {
  67. log.info('Deleted: %s', chalk.magenta(path));
  68. }, err => {
  69. // Skip ENOENT errors (file was deleted)
  70. if (err.cause && err.cause.code === 'ENOENT') return;
  71. throw err;
  72. });
  73. }
  74. function wrapDataStream(dataStream, options) {
  75. const bail = options && options.bail;
  76. // Pass original stream with all data and errors
  77. if (bail === true) {
  78. return dataStream;
  79. }
  80. // Pass all data, but don't populate errors
  81. dataStream.on('error', err => {
  82. log.error(err);
  83. });
  84. return dataStream.pipe(new PassThrough());
  85. }
  86. function firstGenerate() {
  87. // Show the loading time
  88. const interval = prettyHrtime(process.hrtime(start));
  89. log.info('Files loaded in %s', chalk.cyan(interval));
  90. // Reset the timer for later usage
  91. start = process.hrtime();
  92. // Check the public folder
  93. return fs.stat(publicDir).then(stats => {
  94. if (!stats.isDirectory()) {
  95. throw new Error('%s is not a directory', chalk.magenta(tildify(publicDir)));
  96. }
  97. }).catch(err => {
  98. // Create public folder if not exists
  99. if (err.cause && err.cause.code === 'ENOENT') {
  100. return fs.mkdirs(publicDir);
  101. }
  102. throw err;
  103. }).then(() => {
  104. const routeList = route.list();
  105. const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7));
  106. return Promise.all([
  107. // Generate files
  108. Promise.map(routeList, generateFile),
  109. // Clean files
  110. Promise.filter(publicFiles, path => !routeList.includes(path)).map(deleteFile)
  111. ]);
  112. }).spread(result => {
  113. const interval = prettyHrtime(process.hrtime(start));
  114. const count = result.filter(Boolean).length;
  115. log.info('%d files generated in %s', count, chalk.cyan(interval));
  116. });
  117. }
  118. if (args.w || args.watch) {
  119. return this.watch().then(firstGenerate).then(() => {
  120. log.info('Hexo is watching for file changes. Press Ctrl+C to exit.');
  121. // Watch changes of the route
  122. route.on('update', path => {
  123. const modified = route.isModified(path);
  124. if (!modified) return;
  125. generateFile(path);
  126. }).on('remove', path => {
  127. deleteFile(path);
  128. });
  129. });
  130. }
  131. return this.load().then(firstGenerate).then(() => {
  132. if (args.d || args.deploy) {
  133. return self.call('deploy', args);
  134. }
  135. });
  136. }
  137. // Pipe a stream from one to another
  138. function pipeStream(...args) {
  139. const src = args.shift();
  140. return new Promise((resolve, reject) => {
  141. let stream = src.on('error', reject);
  142. let target;
  143. while ((target = args.shift()) != null) {
  144. stream = stream.pipe(target).on('error', reject);
  145. }
  146. stream.on('finish', resolve);
  147. stream.on('end', resolve);
  148. stream.on('close', resolve);
  149. });
  150. }
  151. function CacheStream() {
  152. Transform.call(this);
  153. this._cache = [];
  154. }
  155. require('util').inherits(CacheStream, Transform);
  156. CacheStream.prototype._transform = function(chunk, enc, callback) {
  157. const buf = chunk instanceof Buffer ? chunk : Buffer.from(chunk, enc);
  158. this._cache.push(buf);
  159. this.push(buf);
  160. callback();
  161. };
  162. CacheStream.prototype.destroy = function() {
  163. this._cache.length = 0;
  164. };
  165. CacheStream.prototype.getCache = function() {
  166. return Buffer.concat(this._cache);
  167. };
  168. module.exports = generateConsole;