1 | 'use strict';
|
2 |
|
3 | const fs = require('hexo-fs');
|
4 | const pathFn = require('path');
|
5 | const Promise = require('bluebird');
|
6 | const prettyHrtime = require('pretty-hrtime');
|
7 | const chalk = require('chalk');
|
8 | const tildify = require('tildify');
|
9 | const Transform = require('stream').Transform;
|
10 | const PassThrough = require('stream').PassThrough;
|
11 | const util = require('hexo-util');
|
12 |
|
13 | const join = pathFn.join;
|
14 |
|
15 | function 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 |
|
28 | if (generatingFiles[path]) return Promise.resolve();
|
29 |
|
30 |
|
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 |
|
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 |
|
52 | return pipeStream(dataStream, cacheStream, hashStream).then(() => {
|
53 | const cache = Cache.findById(cacheId);
|
54 | const hash = hashStream.read().toString('hex');
|
55 |
|
56 |
|
57 | if (!force && cache && cache.hash === hash) {
|
58 | return;
|
59 | }
|
60 |
|
61 |
|
62 | return Cache.save({
|
63 | _id: cacheId,
|
64 | hash
|
65 | }).then(() =>
|
66 | fs.writeFile(dest, cacheStream.getCache())).then(() => {
|
67 | log.info('Generated: %s', chalk.magenta(path));
|
68 | return true;
|
69 | });
|
70 | }).finally(() => {
|
71 |
|
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 |
|
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 |
|
92 | if (bail === true) {
|
93 | return dataStream;
|
94 | }
|
95 |
|
96 |
|
97 | dataStream.on('error', err => {
|
98 | log.error(err);
|
99 | });
|
100 |
|
101 | return dataStream.pipe(new PassThrough());
|
102 | }
|
103 |
|
104 | function firstGenerate() {
|
105 |
|
106 | const interval = prettyHrtime(process.hrtime(start));
|
107 | log.info('Files loaded in %s', chalk.cyan(interval));
|
108 |
|
109 |
|
110 | start = process.hrtime();
|
111 |
|
112 |
|
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 |
|
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 |
|
130 | Promise.map(routeList, generateFile),
|
131 |
|
132 |
|
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 |
|
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 |
|
167 | function 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 |
|
184 | function CacheStream() {
|
185 | Transform.call(this);
|
186 |
|
187 | this._cache = [];
|
188 | }
|
189 |
|
190 | require('util').inherits(CacheStream, Transform);
|
191 |
|
192 | CacheStream.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 |
|
200 | CacheStream.prototype.destroy = function() {
|
201 | this._cache.length = 0;
|
202 | };
|
203 |
|
204 | CacheStream.prototype.getCache = function() {
|
205 | return Buffer.concat(this._cache);
|
206 | };
|
207 |
|
208 | module.exports = generateConsole;
|