123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 'use strict';
- const { exists, writeFile, unlink, stat, mkdirs } = require('hexo-fs');
- const { join } = require('path');
- const Promise = require('bluebird');
- const prettyHrtime = require('pretty-hrtime');
- const { cyan, magenta } = require('chalk');
- const tildify = require('tildify');
- const { PassThrough } = require('stream');
- const { createSha1Hash } = require('hexo-util');
- class Generater {
- constructor(ctx, args) {
- this.context = ctx;
- this.force = args.f || args.force;
- this.bail = args.b || args.bail;
- this.concurrency = args.c || args.concurrency;
- this.watch = args.w || args.watch;
- this.deploy = args.d || args.deploy;
- this.generatingFiles = new Set();
- this.start = process.hrtime();
- this.args = args;
- }
- generateFile(path) {
- const publicDir = this.context.public_dir;
- const { generatingFiles } = this;
- const { route } = this.context;
- // Skip if the file is generating
- if (generatingFiles.has(path)) return Promise.resolve();
- // Lock the file
- generatingFiles.add(path);
- let promise;
- if (this.force) {
- promise = this.writeFile(path, true);
- } else {
- const dest = join(publicDir, path);
- promise = exists(dest).then(exist => {
- if (!exist) return this.writeFile(path, true);
- if (route.isModified(path)) return this.writeFile(path);
- });
- }
- return promise.finally(() => {
- // Unlock the file
- generatingFiles.delete(path);
- });
- }
- writeFile(path, force) {
- const { route, log } = this.context;
- const publicDir = this.context.public_dir;
- const Cache = this.context.model('Cache');
- const dataStream = this.wrapDataStream(route.get(path));
- const buffers = [];
- const hasher = createSha1Hash();
- const finishedPromise = new Promise((resolve, reject) => {
- dataStream.once('error', reject);
- dataStream.once('end', resolve);
- });
- // Get data => Cache data => Calculate hash
- dataStream.on('data', chunk => {
- buffers.push(chunk);
- hasher.update(chunk);
- });
- return finishedPromise.then(() => {
- const dest = join(publicDir, path);
- const cacheId = `public/${path}`;
- const cache = Cache.findById(cacheId);
- const hash = hasher.digest('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
- writeFile(dest, Buffer.concat(buffers))).then(() => {
- log.info('Generated: %s', magenta(path));
- return true;
- });
- });
- }
- deleteFile(path) {
- const { log } = this.context;
- const publicDir = this.context.public_dir;
- const dest = join(publicDir, path);
- return unlink(dest).then(() => {
- log.info('Deleted: %s', magenta(path));
- }, err => {
- // Skip ENOENT errors (file was deleted)
- if (err && err.code === 'ENOENT') return;
- throw err;
- });
- }
- wrapDataStream(dataStream) {
- const { log } = this.context;
- // Pass original stream with all data and errors
- if (this.bail) {
- return dataStream;
- }
- // Pass all data, but don't populate errors
- dataStream.on('error', err => {
- log.error(err);
- });
- return dataStream.pipe(new PassThrough());
- }
- firstGenerate() {
- const { concurrency } = this;
- const { route, log } = this.context;
- const publicDir = this.context.public_dir;
- const Cache = this.context.model('Cache');
- // Show the loading time
- const interval = prettyHrtime(process.hrtime(this.start));
- log.info('Files loaded in %s', cyan(interval));
- // Reset the timer for later usage
- this.start = process.hrtime();
- // Check the public folder
- return stat(publicDir).then(stats => {
- if (!stats.isDirectory()) {
- throw new Error('%s is not a directory', magenta(tildify(publicDir)));
- }
- }).catch(err => {
- // Create public folder if not exists
- if (err && err.code === 'ENOENT') {
- return mkdirs(publicDir);
- }
- throw err;
- }).then(() => {
- const task = (fn, path) => () => fn.call(this, path);
- const doTask = fn => fn();
- const routeList = route.list();
- const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7));
- const tasks = publicFiles.filter(path => !routeList.includes(path))
- // Clean files
- .map(path => task(this.deleteFile, path))
- // Generate files
- .concat(routeList.map(path => task(this.generateFile, path)));
- return Promise.all(Promise.map(tasks, doTask, { concurrency: parseFloat(concurrency || 'Infinity') }));
- }).then(result => {
- const interval = prettyHrtime(process.hrtime(this.start));
- const count = result.filter(Boolean).length;
- log.info('%d files generated in %s', count, cyan(interval));
- });
- }
- execWatch() {
- const { route, log } = this.context;
- return this.context.watch().then(() => this.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;
- this.generateFile(path);
- }).on('remove', path => {
- this.deleteFile(path);
- });
- });
- }
- execDeploy() {
- return this.context.call('deploy', this.args);
- }
- }
- function generateConsole(args = {}) {
- const generator = new Generater(this, args);
- if (generator.watch) {
- return generator.execWatch();
- }
- return this.load().then(() => generator.firstGenerate()).then(() => {
- if (generator.deploy) {
- return generator.execDeploy();
- }
- });
- }
- module.exports = generateConsole;
|