generate.js 5.8 KB

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