Skip to content

Commit

Permalink
refactor(console/generate): class & destructure assign (#4338)
Browse files Browse the repository at this point in the history
Co-authored-by: segayuu <segayuu@gmail.com>
  • Loading branch information
SukkaW and segayuu committed May 30, 2020
1 parent 936bc07 commit 1f6c087
Showing 1 changed file with 93 additions and 77 deletions.
170 changes: 93 additions & 77 deletions lib/plugins/console/generate.js
@@ -1,53 +1,77 @@
'use strict';

const fs = require('hexo-fs');
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 { CacheStream, createSha1Hash } = require('hexo-util');

function generateConsole(args = {}) {
const force = args.f || args.force;
const bail = args.b || args.bail;
const concurrency = args.c || args.concurrency;
const { route, log } = this;
const publicDir = this.public_dir;
let start = process.hrtime();
const Cache = this.model('Cache');
const generatingFiles = {};

function generateFile(path) {
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[path]) return Promise.resolve();
if (generatingFiles.has(path)) return Promise.resolve();

// Lock the file
generatingFiles[path] = true;
generatingFiles.add(path);

const dest = join(publicDir, 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 fs.exists(dest).then(exist => {
if (force || !exist) return writeFile(path, true);
if (route.isModified(path)) return writeFile(path);
}).finally(() => {
return promise.finally(() => {
// Unlock the file
generatingFiles[path] = false;
generatingFiles.delete(path);
});
}

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 = createSha1Hash();
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
return pipeStream(dataStream, cacheStream, hashStream).then(hash_buf => {
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 = hash_buf.toString('hex');
const hash = hasher.digest('hex');

// Skip generating if hash is unchanged
if (!force && cache && cache.hash === hash) {
Expand All @@ -59,31 +83,29 @@ function generateConsole(args = {}) {
_id: cacheId,
hash
}).then(() => // Write cache data to public folder
fs.writeFile(dest, cacheStream.getCache())).then(() => {
writeFile(dest, Buffer.concat(buffers))).then(() => {
log.info('Generated: %s', magenta(path));
return true;
});
}).finally(() => {
// Destroy cache
cacheStream.destroy();
});
}

function deleteFile(path) {
deleteFile(path) {
const { log } = this.context;
const publicDir = this.context.public_dir;
const dest = join(publicDir, path);

return fs.unlink(dest).then(() => {
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;
});
}

function wrapDataStream(dataStream, bail) {
wrapDataStream(dataStream) {
const { log } = this.context;
// Pass original stream with all data and errors
if (bail) {
if (this.bail) {
return dataStream;
}

Expand All @@ -94,89 +116,83 @@ function generateConsole(args = {}) {

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');

function firstGenerate() {
// Show the loading time
const interval = prettyHrtime(process.hrtime(start));
const interval = prettyHrtime(process.hrtime(this.start));
log.info('Files loaded in %s', cyan(interval));

// Reset the timer for later usage
start = process.hrtime();
this.start = process.hrtime();


// Check the public folder
return fs.stat(publicDir).then(stats => {
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 fs.mkdirs(publicDir);
return mkdirs(publicDir);
}

throw err;
}).then(() => {
const task = (fn, path) => () => fn(path);
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(deleteFile, path))
.map(path => task(this.deleteFile, path))
// Generate files
.concat(routeList.map(path => task(generateFile, path)));
.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(start));
const interval = prettyHrtime(process.hrtime(this.start));
const count = result.filter(Boolean).length;

log.info('%d files generated in %s', count, cyan(interval));
});
}

if (args.w || args.watch) {
return this.watch().then(firstGenerate).then(() => {
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;

generateFile(path);
this.generateFile(path);
}).on('remove', path => {
deleteFile(path);
this.deleteFile(path);
});
});
}

return this.load().then(firstGenerate).then(() => {
if (args.d || args.deploy) {
return this.call('deploy', args);
}
});
execDeploy() {
return this.context.call('deploy', this.args);
}
}

// Pipe a stream from one to another
function pipeStream(...args) {
const src = args.shift();
function generateConsole(args = {}) {
const generator = new Generater(this, args);

return new Promise((resolve, reject) => {
let stream = src.on('error', reject);
let target,
result;
if (generator.watch) {
return generator.execWatch();
}

while ((target = args.shift()) != null) {
stream = stream.pipe(target).on('error', reject);
return this.load().then(() => generator.firstGenerate()).then(() => {
if (generator.deploy) {
return generator.execDeploy();
}

stream.on('data', chunk => {
result = chunk;
});
stream.on('end', () => {
resolve(result);
});
});
}

Expand Down

0 comments on commit 1f6c087

Please sign in to comment.