index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. 'use strict';
  2. const Promise = require('bluebird');
  3. const { sep, join, dirname } = require('path');
  4. const tildify = require('tildify');
  5. const Database = require('warehouse');
  6. const { magenta, underline } = require('chalk');
  7. const { EventEmitter } = require('events');
  8. const { readFile } = require('hexo-fs');
  9. const Module = require('module');
  10. const { runInThisContext } = require('vm');
  11. const { version } = require('../../package.json');
  12. const logger = require('hexo-log');
  13. const { Console, Deployer, Filter, Generator, Helper, Injector, Migrator, Processor, Renderer, Tag } = require('../extend');
  14. const Render = require('./render');
  15. const registerModels = require('./register_models');
  16. const Post = require('./post');
  17. const Scaffold = require('./scaffold');
  18. const Source = require('./source');
  19. const Router = require('./router');
  20. const Theme = require('../theme');
  21. const Locals = require('./locals');
  22. const defaultConfig = require('./default_config');
  23. const loadDatabase = require('./load_database');
  24. const multiConfigPath = require('./multi_config_path');
  25. const { sync } = require('resolve');
  26. const { deepMerge, full_url_for } = require('hexo-util');
  27. const libDir = dirname(__dirname);
  28. const dbVersion = 1;
  29. const stopWatcher = box => { if (box.isWatching()) box.unwatch(); };
  30. const routeCache = new WeakMap();
  31. const castArray = obj => { return Array.isArray(obj) ? obj : [obj]; };
  32. const mergeCtxThemeConfig = ctx => {
  33. // Merge hexo.config.theme_config into hexo.theme.config before post rendering & generating
  34. // config.theme_config has "_config.[theme].yml" merged in load_theme_config.js
  35. if (ctx.config.theme_config) {
  36. ctx.theme.config = deepMerge(ctx.theme.config, ctx.config.theme_config);
  37. }
  38. };
  39. const createLoadThemeRoute = function(generatorResult, locals, ctx) {
  40. const { log, theme } = ctx;
  41. const { path, cache: useCache } = locals;
  42. const layout = [...new Set(castArray(generatorResult.layout))];
  43. const layoutLength = layout.length;
  44. // always use cache in fragment_cache
  45. locals.cache = true;
  46. return () => {
  47. if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult);
  48. for (let i = 0; i < layoutLength; i++) {
  49. const name = layout[i];
  50. const view = theme.getView(name);
  51. if (view) {
  52. log.debug(`Rendering HTML ${name}: ${magenta(path)}`);
  53. return view.render(locals)
  54. .then(result => ctx.extend.injector.exec(result, locals))
  55. .then(result => ctx.execFilter('_after_html_render', result, {
  56. context: ctx,
  57. args: [locals]
  58. }))
  59. .tap(result => {
  60. if (useCache) {
  61. routeCache.set(generatorResult, result);
  62. }
  63. }).tapCatch(err => {
  64. log.error({ err }, `Render HTML failed: ${magenta(path)}`);
  65. });
  66. }
  67. }
  68. log.warn(`No layout: ${magenta(path)}`);
  69. };
  70. };
  71. function debounce(func, wait) {
  72. let timeout;
  73. return function() {
  74. const context = this;
  75. const args = arguments;
  76. clearTimeout(timeout);
  77. timeout = setTimeout(() => {
  78. func.apply(context, args);
  79. }, wait);
  80. };
  81. }
  82. class Hexo extends EventEmitter {
  83. constructor(base = process.cwd(), args = {}) {
  84. super();
  85. this.base_dir = base + sep;
  86. this.public_dir = join(base, 'public') + sep;
  87. this.source_dir = join(base, 'source') + sep;
  88. this.plugin_dir = join(base, 'node_modules') + sep;
  89. this.script_dir = join(base, 'scripts') + sep;
  90. this.scaffold_dir = join(base, 'scaffolds') + sep;
  91. this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep;
  92. this.theme_script_dir = join(this.theme_dir, 'scripts') + sep;
  93. this.env = {
  94. args,
  95. debug: Boolean(args.debug),
  96. safe: Boolean(args.safe),
  97. silent: Boolean(args.silent),
  98. env: process.env.NODE_ENV || 'development',
  99. version,
  100. cmd: args._ ? args._[0] : '',
  101. init: false
  102. };
  103. this.extend = {
  104. console: new Console(),
  105. deployer: new Deployer(),
  106. filter: new Filter(),
  107. generator: new Generator(),
  108. helper: new Helper(),
  109. injector: new Injector(),
  110. migrator: new Migrator(),
  111. processor: new Processor(),
  112. renderer: new Renderer(),
  113. tag: new Tag()
  114. };
  115. this.config = { ...defaultConfig };
  116. this.log = logger(this.env);
  117. this.render = new Render(this);
  118. this.route = new Router();
  119. this.post = new Post(this);
  120. this.scaffold = new Scaffold(this);
  121. this._dbLoaded = false;
  122. this._isGenerating = false;
  123. // If `output` is provided, use that as the
  124. // root for saving the db. Otherwise default to `base`.
  125. const dbPath = args.output || base;
  126. if (/^(init|new|g|publish|s|deploy|render|migrate)/.test(this.env.cmd)) {
  127. this.log.d(`Writing database to ${dbPath}/db.json`);
  128. }
  129. this.database = new Database({
  130. version: dbVersion,
  131. path: join(dbPath, 'db.json')
  132. });
  133. const mcp = multiConfigPath(this);
  134. this.config_path = args.config ? mcp(base, args.config, args.output)
  135. : join(base, '_config.yml');
  136. registerModels(this);
  137. this.source = new Source(this);
  138. this.theme = new Theme(this);
  139. this.locals = new Locals(this);
  140. this._bindLocals();
  141. }
  142. _bindLocals() {
  143. const db = this.database;
  144. const { locals } = this;
  145. locals.set('posts', () => {
  146. const query = {};
  147. if (!this.config.future) {
  148. query.date = {$lte: Date.now()};
  149. }
  150. if (!this._showDrafts()) {
  151. query.published = true;
  152. }
  153. return db.model('Post').find(query);
  154. });
  155. locals.set('pages', () => {
  156. const query = {};
  157. if (!this.config.future) {
  158. query.date = {$lte: Date.now()};
  159. }
  160. return db.model('Page').find(query);
  161. });
  162. locals.set('categories', () => {
  163. // Ignore categories with zero posts
  164. return db.model('Category').filter(category => category.length);
  165. });
  166. locals.set('tags', () => {
  167. // Ignore tags with zero posts
  168. return db.model('Tag').filter(tag => tag.length);
  169. });
  170. locals.set('data', () => {
  171. const obj = {};
  172. db.model('Data').forEach(data => {
  173. obj[data._id] = data.data;
  174. });
  175. return obj;
  176. });
  177. }
  178. init() {
  179. this.log.debug('Hexo version: %s', magenta(this.version));
  180. this.log.debug('Working directory: %s', magenta(tildify(this.base_dir)));
  181. // Load internal plugins
  182. require('../plugins/console')(this);
  183. require('../plugins/filter')(this);
  184. require('../plugins/generator')(this);
  185. require('../plugins/helper')(this);
  186. require('../plugins/injector')(this);
  187. require('../plugins/processor')(this);
  188. require('../plugins/renderer')(this);
  189. require('../plugins/tag')(this);
  190. // Load config
  191. return Promise.each([
  192. 'update_package', // Update package.json
  193. 'load_config', // Load config
  194. 'load_theme_config', // Load alternate theme config
  195. 'load_plugins' // Load external plugins & scripts
  196. ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, {context: this})).then(() => {
  197. // Ready to go!
  198. this.emit('ready');
  199. });
  200. }
  201. call(name, args, callback) {
  202. if (!callback && typeof args === 'function') {
  203. callback = args;
  204. args = {};
  205. }
  206. return new Promise((resolve, reject) => {
  207. const c = this.extend.console.get(name);
  208. if (c) {
  209. Reflect.apply(c, this, [args]).then(resolve, reject);
  210. } else {
  211. reject(new Error(`Console \`${name}\` has not been registered yet!`));
  212. }
  213. }).asCallback(callback);
  214. }
  215. model(name, schema) {
  216. return this.database.model(name, schema);
  217. }
  218. resolvePlugin(name) {
  219. const baseDir = this.base_dir;
  220. try {
  221. // Try to resolve the plugin with the resolve.sync.
  222. return sync(name, { basedir: baseDir });
  223. } catch (err) {
  224. // There was an error (likely the plugin wasn't found), so return a possibly
  225. // non-existing path that a later part of the resolution process will check.
  226. return join(baseDir, 'node_modules', name);
  227. }
  228. }
  229. loadPlugin(path, callback) {
  230. return readFile(path).then(script => {
  231. // Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
  232. const module = new Module(path);
  233. module.filename = path;
  234. module.paths = Module._nodeModulePaths(path);
  235. function req(path) {
  236. return module.require(path);
  237. }
  238. req.resolve = request => Module._resolveFilename(request, module);
  239. req.main = require.main;
  240. req.extensions = Module._extensions;
  241. req.cache = Module._cache;
  242. script = `(function(exports, require, module, __filename, __dirname, hexo){${script}\n});`;
  243. const fn = runInThisContext(script, path);
  244. return fn(module.exports, req, module, path, dirname(path), this);
  245. }).asCallback(callback);
  246. }
  247. _showDrafts() {
  248. const { args } = this.env;
  249. return args.draft || args.drafts || this.config.render_drafts;
  250. }
  251. load(callback) {
  252. return loadDatabase(this).then(() => {
  253. this.log.info('Start processing');
  254. return Promise.all([
  255. this.source.process(),
  256. this.theme.process()
  257. ]);
  258. }).then(() => {
  259. mergeCtxThemeConfig(this);
  260. }).then(() => this._generate({cache: false})).asCallback(callback);
  261. }
  262. watch(callback) {
  263. let useCache = false;
  264. const { cache } = Object.assign({
  265. cache: false
  266. }, this.config.server);
  267. const { alias } = this.extend.console;
  268. if (alias[this.env.cmd] === 'server' && cache) {
  269. // enable cache when run hexo server
  270. useCache = true;
  271. }
  272. this._watchBox = debounce(() => this._generate({cache: useCache}), 100);
  273. return loadDatabase(this).then(() => {
  274. this.log.info('Start processing');
  275. return Promise.all([
  276. this.source.watch(),
  277. this.theme.watch()
  278. ]);
  279. }).then(() => {
  280. mergeCtxThemeConfig(this);
  281. }).then(() => {
  282. this.source.on('processAfter', this._watchBox);
  283. this.theme.on('processAfter', () => {
  284. this._watchBox();
  285. mergeCtxThemeConfig(this);
  286. });
  287. return this._generate({cache: useCache});
  288. }).asCallback(callback);
  289. }
  290. unwatch() {
  291. if (this._watchBox != null) {
  292. this.source.removeListener('processAfter', this._watchBox);
  293. this.theme.removeListener('processAfter', this._watchBox);
  294. this._watchBox = null;
  295. }
  296. stopWatcher(this.source);
  297. stopWatcher(this.theme);
  298. }
  299. _generateLocals() {
  300. const { config, env, theme, theme_dir } = this;
  301. const ctx = { config: { url: this.config.url } };
  302. const localsObj = this.locals.toObject();
  303. class Locals {
  304. constructor(path, locals) {
  305. this.page = { ...locals };
  306. if (this.page.path == null) this.page.path = path;
  307. this.path = path;
  308. this.url = full_url_for.call(ctx, path);
  309. this.config = config;
  310. this.theme = theme.config;
  311. this.layout = 'layout';
  312. this.env = env;
  313. this.view_dir = join(theme_dir, 'layout') + sep;
  314. this.site = localsObj;
  315. }
  316. }
  317. return Locals;
  318. }
  319. _runGenerators() {
  320. this.locals.invalidate();
  321. const siteLocals = this.locals.toObject();
  322. const generators = this.extend.generator.list();
  323. const { log } = this;
  324. // Run generators
  325. return Promise.map(Object.keys(generators), key => {
  326. const generator = generators[key];
  327. return Reflect.apply(generator, this, [siteLocals]).then(data => {
  328. log.debug('Generator: %s', magenta(key));
  329. return data;
  330. });
  331. }).reduce((result, data) => {
  332. return data ? result.concat(data) : result;
  333. }, []);
  334. }
  335. _routerReflesh(runningGenerators, useCache) {
  336. const { route } = this;
  337. const routeList = route.list();
  338. const Locals = this._generateLocals();
  339. Locals.prototype.cache = useCache;
  340. return runningGenerators.map(generatorResult => {
  341. if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined;
  342. // add Route
  343. const path = route.format(generatorResult.path);
  344. const { data, layout } = generatorResult;
  345. if (!layout) {
  346. route.set(path, data);
  347. return path;
  348. }
  349. return this.execFilter('template_locals', new Locals(path, data), {context: this})
  350. .then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
  351. .thenReturn(path);
  352. }).then(newRouteList => {
  353. // Remove old routes
  354. const removed = routeList.filter(item => !newRouteList.includes(item));
  355. for (let i = 0, len = removed.length; i < len; i++) {
  356. route.remove(removed[i]);
  357. }
  358. });
  359. }
  360. _generate(options = {}) {
  361. if (this._isGenerating) return;
  362. const useCache = options.cache;
  363. this._isGenerating = true;
  364. this.emit('generateBefore');
  365. // Run before_generate filters
  366. return this.execFilter('before_generate', this.locals.get('data'), {context: this})
  367. .then(() => this._routerReflesh(this._runGenerators(), useCache)).then(() => {
  368. this.emit('generateAfter');
  369. // Run after_generate filters
  370. return this.execFilter('after_generate', null, {context: this});
  371. }).finally(() => {
  372. this._isGenerating = false;
  373. });
  374. }
  375. exit(err) {
  376. if (err) {
  377. this.log.fatal(
  378. {err},
  379. 'Something\'s wrong. Maybe you can find the solution here: %s',
  380. underline('https://hexo.io/docs/troubleshooting.html')
  381. );
  382. }
  383. return this.execFilter('before_exit', null, {context: this}).then(() => {
  384. this.emit('exit', err);
  385. });
  386. }
  387. execFilter(type, data, options) {
  388. return this.extend.filter.exec(type, data, options);
  389. }
  390. execFilterSync(type, data, options) {
  391. return this.extend.filter.execSync(type, data, options);
  392. }
  393. }
  394. Hexo.lib_dir = libDir + sep;
  395. Hexo.prototype.lib_dir = Hexo.lib_dir;
  396. Hexo.core_dir = dirname(libDir) + sep;
  397. Hexo.prototype.core_dir = Hexo.core_dir;
  398. Hexo.version = version;
  399. Hexo.prototype.version = Hexo.version;
  400. module.exports = Hexo;