UNPKG

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