UNPKG

5.85 kBJavaScriptView Raw
1'use-strict';
2
3const _ = require('lodash');
4const { relative, extname, join, resolve } = require('path');
5const { stat } = require('fs');
6const parseUrl = require('parseurl');
7const http2 = require('http2');
8
9const srcContentTypes = {
10 '.css': 'text/css; charset=utf-8',
11 '.scss': 'text/css; charset=utf-8',
12 '.html': 'text/html; charset=utf-8',
13 '.pug': 'text/html; charset=utf-8',
14 '.js': 'text/javascript; charset=utf-8'
15};
16
17const renderCache = {};
18const renderTimes = {};
19
20function replaceExt(filepath, ext) {
21 return filepath.trim().replace(new RegExp(`${extname(filepath)}$`), ext);
22}
23
24// http://www.xiconeditor.com/
25// https://realfavicongenerator.net/
26const serveFavicon = require('serve-favicon')(resolve(__dirname, './favicon.ico'));
27
28const serveStaticOptions = { extensions: ['html'] };
29
30// eazy-logger https://github.com/shakyShane/eazy-logger/blob/master/index.js
31const loggerFn = require('eazy-logger').Logger({
32 prefix: '[{blue:penny}] ',
33 useLevelPrefixes: true
34});
35
36module.exports = function(srcDir, options) {
37
38 const { reqSrcExt, isDev, logLevel } = options;
39 const srcOutExt = _.invert(reqSrcExt);
40 const srcExts = Object.keys(srcOutExt);
41 const changeTimes = _.mapValues(srcOutExt, Date.now);
42 const srcServerFns = _.mapValues(srcOutExt, (outExt, srcExt) => srcServer(srcExt));
43
44 loggerFn.setLevel(logLevel);
45 // loggerFn.setLevel('debug');
46
47 if (isDev) {
48
49 const bsync = require('browser-sync').create();
50 const logger = require('morgan')('dev', {
51 skip: function (req, res) {
52 return res.statusCode < 300;
53 }
54 });
55 const baseDir = srcDir;
56
57 bsync.init({
58 notify: false,
59 // online: false,
60 open: false,
61 server: { baseDir, serveStaticOptions },
62 https: true,
63 httpModule: http2,
64 logLevel, // info, warn, debug (ascending verbosity)
65 logPrefix: 'penny',
66 minify: !isDev,
67 middleware: [ serveFavicon, logger, srcController ],
68 },
69 function () {
70 let ready = false;
71
72 // capturing eazy-logger instance from bsync instance
73 // eazy-logger: https://github.com/shakyShane/eazy-logger
74 // tfunk: https://github.com/shakyshane/tfunk
75 // colors reference: https://github.com/chalk/chalk#colors
76 // bsync.instance.logger.error('{red:%s', 'test error');
77 // bsync.instance.logger.info('{cyan:%s', 'this info');
78 // bsync.instance.logger.warn('{yellow:%s', 'test warning');
79 // bsync.instance.logger.debug('{magenta:%s', 'test debug');
80
81 bsync.watch(
82 // TRYME: remove srcDir from path; add as `cwd` option to chokidar ${srcDir}/
83 srcExts.concat(['.json', '.yml', '.yaml']).map(srcExt => `**/*${srcExt}`), {
84 ignored: ['**/node_modules/**'],
85 ignoreInitial: true,
86 cwd: srcDir
87 },
88 (event, file) => {
89 if (ready) {
90 const srcExt = extname(file);
91 const outExt = srcOutExt[srcExt] || '';
92 changeTimes[srcExt] = Date.now();
93 bsync.reload(`*${outExt}`);
94 }
95 }
96 ).on('ready', () => (ready = true));
97 });
98 } else {
99
100 const http = require('http');
101 const connect = require('connect');
102 const serveStatic = require('serve-static');
103 const app = connect();
104
105 app.use(serveFavicon);
106 app.use(srcController);
107 app.use(serveStatic(srcDir, serveStaticOptions));
108 http.createServer(app).listen(3000);
109
110 loggerFn.info(`Serving files from: ${srcDir}`);
111 }
112
113 /*
114 SRC SERVER
115 0. convert reqFile to srcFile
116 1. check for srcFile;
117 2. (bail, if srcFile does not exist)
118 3a. set cache-control https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
119 3b. set header per srcType
120 4. if renderCache is invalid, re-render (renderFn also updates renderTime)
121 5. await renderCache, then...
122 5b. log change/render/serve stats
123 5c. serve data
124 */
125
126 function srcServer(srcExt) {
127 const renderFn = require(`./render-${srcExt.slice(1)}`)({srcDir, loggerFn: loggerFn, options});
128 return function(reqFile, res, next) {
129 const srcFile = replaceExt(reqFile, srcExt); // 0.
130 stat(srcFile, (err, stats) => { // 1.
131 if (err || !stats.isFile()) return next(); // 2.
132 res.setHeader('Cache-Control', isDev?'no-cache':'public'); // 3a.
133 res.setHeader('Content-Type', srcContentTypes[srcExt]); // 3b.
134 if (!(srcFile in renderCache) || renderTimes[srcFile] < changeTimes[srcExt]) { // 4.
135 renderTimes && (renderTimes[srcFile] = Date.now());
136 renderCache[srcFile] = renderFn(srcFile);
137 }
138 renderCache[srcFile].then(data => { // 5.-5c.
139 loggerFn.debug(`${relative(srcDir, srcFile)}\nchanged: ${changeTimes[srcExt]} \nrendered: ${renderTimes[srcFile]}\nserved: ${Date.now()}`);
140 res.end(data);
141 });
142 });
143 };
144 }
145
146 /*
147 SRC CONTROLLER
148 A. resolve extension if none present
149 - if: foo, seek foo.html
150 - if: foo/ seek foo/index.html
151 - else: fall-through, connect will redirect if necessary
152 B. fall through, if the file extension is still not one that we care about
153 C. else proceed to sourceWare
154 */
155
156 function srcController(req, res, next) {
157 let { pathname } = parseUrl(req),
158 ext = extname(pathname);
159 /*
160 catch bad paths here with the same mmatch patters as in build.js (should be disable-able with option)
161 res.statusCode(403)
162 res.statusMessage('Forbidden')
163 res.end('This request path contains an underscore-prefixed folder or file. These are not served or built by Penny.')
164 */
165 // A
166 if (!ext) { ext = '.html'; pathname = pathname.replace(/\/$/, '/index') + ext; }
167 // B
168 if (!~Object.keys(reqSrcExt).indexOf(ext)) { return next(); }
169 // C
170 else {
171 const reqFile = join(srcDir, pathname);
172 return srcServerFns[reqSrcExt[ext]](reqFile, res, next);
173 }
174 }
175};