'use strict'; const fs = require('hexo-fs'); const pathFn = require('path'); const Promise = require('bluebird'); const prettyHrtime = require('pretty-hrtime'); const chalk = require('chalk'); const tildify = require('tildify'); const Transform = require('stream').Transform; const PassThrough = require('stream').PassThrough; const util = require('hexo-util'); const join = pathFn.join; function generateConsole(args = {}) { const force = args.f || args.force; const bail = args.b || args.bail; const route = this.route; const publicDir = this.public_dir; const log = this.log; const self = this; let start = process.hrtime(); const Cache = this.model('Cache'); const generatingFiles = {}; function generateFile(path) { // Skip if the file is generating if (generatingFiles[path]) return Promise.resolve(); // Lock the file generatingFiles[path] = true; const dest = join(publicDir, path); return fs.exists(dest).then(exist => { if (force || !exist) return writeFile(path, true); if (route.isModified(path)) return writeFile(path); }).finally(() => { // Unlock the file generatingFiles[path] = false; }); } function writeFile(path, force) { const dest = join(publicDir, path); const cacheId = `public/${path}`; const dataStream = wrapDataStream(route.get(path), {bail}); const cacheStream = new CacheStream(); const hashStream = new util.HashStream(); // Get data => Cache data => Calculate hash return pipeStream(dataStream, cacheStream, hashStream).then(() => { const cache = Cache.findById(cacheId); const hash = hashStream.read().toString('hex'); // Skip generating if hash is unchanged if (!force && cache && cache.hash === hash) { return; } // Save new hash to cache return Cache.save({ _id: cacheId, hash }).then(() => // Write cache data to public folder fs.writeFile(dest, cacheStream.getCache())).then(() => { log.info('Generated: %s', chalk.magenta(path)); return true; }); }).finally(() => { // Destroy cache cacheStream.destroy(); }); } function deleteFile(path) { const dest = join(publicDir, path); return fs.unlink(dest).then(() => { log.info('Deleted: %s', chalk.magenta(path)); }, err => { // Skip ENOENT errors (file was deleted) if (err.cause && err.cause.code === 'ENOENT') return; throw err; }); } function wrapDataStream(dataStream, options) { const bail = options && options.bail; // Pass original stream with all data and errors if (bail === true) { return dataStream; } // Pass all data, but don't populate errors dataStream.on('error', err => { log.error(err); }); return dataStream.pipe(new PassThrough()); } function firstGenerate() { // Show the loading time const interval = prettyHrtime(process.hrtime(start)); log.info('Files loaded in %s', chalk.cyan(interval)); // Reset the timer for later usage start = process.hrtime(); // Check the public folder return fs.stat(publicDir).then(stats => { if (!stats.isDirectory()) { throw new Error('%s is not a directory', chalk.magenta(tildify(publicDir))); } }).catch(err => { // Create public folder if not exists if (err.cause && err.cause.code === 'ENOENT') { return fs.mkdirs(publicDir); } throw err; }).then(() => { const routeList = route.list(); const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7)); return Promise.all([ // Generate files Promise.map(routeList, generateFile), // Clean files Promise.filter(publicFiles, path => !routeList.includes(path)).map(deleteFile) ]); }).spread(result => { const interval = prettyHrtime(process.hrtime(start)); const count = result.filter(Boolean).length; log.info('%d files generated in %s', count, chalk.cyan(interval)); }); } if (args.w || args.watch) { return this.watch().then(firstGenerate).then(() => { log.info('Hexo is watching for file changes. Press Ctrl+C to exit.'); // Watch changes of the route route.on('update', path => { const modified = route.isModified(path); if (!modified) return; generateFile(path); }).on('remove', path => { deleteFile(path); }); }); } return this.load().then(firstGenerate).then(() => { if (args.d || args.deploy) { return self.call('deploy', args); } }); } // Pipe a stream from one to another function pipeStream(...args) { const src = args.shift(); return new Promise((resolve, reject) => { let stream = src.on('error', reject); let target; while ((target = args.shift()) != null) { stream = stream.pipe(target).on('error', reject); } stream.on('finish', resolve); stream.on('end', resolve); stream.on('close', resolve); }); } function CacheStream() { Transform.call(this); this._cache = []; } require('util').inherits(CacheStream, Transform); CacheStream.prototype._transform = function(chunk, enc, callback) { const buf = chunk instanceof Buffer ? chunk : Buffer.from(chunk, enc); this._cache.push(buf); this.push(buf); callback(); }; CacheStream.prototype.destroy = function() { this._cache.length = 0; }; CacheStream.prototype.getCache = function() { return Buffer.concat(this._cache); }; module.exports = generateConsole;