1 | 'use-strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const { relative, extname, join, resolve } = require('path');
|
5 | const { stat } = require('fs');
|
6 | const parseUrl = require('parseurl');
|
7 | const http2 = require('http2');
|
8 |
|
9 | const 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 |
|
17 | const renderCache = {};
|
18 | const renderTimes = {};
|
19 |
|
20 | function replaceExt(filepath, ext) {
|
21 | return filepath.trim().replace(new RegExp(`${extname(filepath)}$`), ext);
|
22 | }
|
23 |
|
24 |
|
25 |
|
26 | const serveFavicon = require('serve-favicon')(resolve(__dirname, './favicon.ico'));
|
27 |
|
28 | const serveStaticOptions = { extensions: ['html'] };
|
29 |
|
30 |
|
31 | const loggerFn = require('eazy-logger').Logger({
|
32 | prefix: '[{blue:penny}] ',
|
33 | useLevelPrefixes: true
|
34 | });
|
35 |
|
36 | module.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 |
|
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 |
|
60 | open: false,
|
61 | server: { baseDir, serveStaticOptions },
|
62 | https: true,
|
63 | httpModule: http2,
|
64 | logLevel,
|
65 | logPrefix: 'penny',
|
66 | minify: !isDev,
|
67 | middleware: [ serveFavicon, logger, srcController ],
|
68 | },
|
69 | function () {
|
70 | let ready = false;
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | bsync.watch(
|
82 |
|
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 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
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);
|
130 | stat(srcFile, (err, stats) => {
|
131 | if (err || !stats.isFile()) return next();
|
132 | res.setHeader('Cache-Control', isDev?'no-cache':'public');
|
133 | res.setHeader('Content-Type', srcContentTypes[srcExt]);
|
134 | if (!(srcFile in renderCache) || renderTimes[srcFile] < changeTimes[srcExt]) {
|
135 | renderTimes && (renderTimes[srcFile] = Date.now());
|
136 | renderCache[srcFile] = renderFn(srcFile);
|
137 | }
|
138 | renderCache[srcFile].then(data => {
|
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 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | function srcController(req, res, next) {
|
157 | let { pathname } = parseUrl(req),
|
158 | ext = extname(pathname);
|
159 | |
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | if (!ext) { ext = '.html'; pathname = pathname.replace(/\/$/, '/index') + ext; }
|
167 |
|
168 | if (!~Object.keys(reqSrcExt).indexOf(ext)) { return next(); }
|
169 |
|
170 | else {
|
171 | const reqFile = join(srcDir, pathname);
|
172 | return srcServerFns[reqSrcExt[ext]](reqFile, res, next);
|
173 | }
|
174 | }
|
175 | };
|