UNPKG

8.55 kBPlain TextView Raw
1import fs from 'fs'
2import path from 'path'
3import http from 'http'
4import getPort from 'get-port'
5import sirv from 'sirv'
6import chokidar from 'chokidar'
7import mime from 'mime-types'
8import toRegExp from 'regexparam'
9
10import { timer } from './timer'
11import * as logger from './log'
12import { default404 } from './default404'
13import { requestToEvent } from './requestToEvent'
14import { sendServerlessResponse } from './sendServerlessResponse'
15import { AWS, Presta } from './types'
16
17const style = [
18 'position: fixed',
19 'bottom: 24px',
20 'right: 24px',
21 'width: 32px',
22 'height: 32px',
23 'border-radius: 32px',
24 'background: white',
25 'color: #FF7A93',
26 'font-size: 20px',
27 'font-weight: bold',
28 'text-align: center',
29 'line-height: 31px',
30 'box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 24px 32px rgba(0, 0, 0, 0.04)',
31]
32
33const devServerIcon = `
34 <div style="${style.join(';')}">~</div>
35`
36
37function resolveHTML(dir: string, url: string) {
38 let file = path.join(dir, url)
39
40 // if no extension, it's probably intended to be an HTML file
41 if (!path.extname(url)) {
42 try {
43 return fs.readFileSync(path.join(dir, url, 'index.html'), 'utf8')
44 } catch (e) {}
45 }
46
47 return fs.readFileSync(file, 'utf8')
48}
49
50function createDevClient({ port }: { port: number }) {
51 return `
52 <script>
53 (function (global) {
54 try {
55 const socketio = document.createElement('script')
56 socketio.src = 'https://unpkg.com/pocket.io@0.1.4/min.js'
57 socketio.onload = function init () {
58 var disconnected = false
59 var socket = io('http://localhost:${port}', {
60 reconnectionAttempts: 3
61 })
62 socket.on('connect', function() { console.log('presta connected on port ${port}') })
63 socket.on('refresh', function() {
64 global.location.reload()
65 })
66 socket.on('disconnect', function() {
67 disconnected = true
68 })
69 socket.on('reconnect_failed', function(e) {
70 if (disconnected) return
71 console.error("presta - connection to server on :${port} failed")
72 })
73 }
74 document.head.appendChild(socketio)
75 } catch (e) {}
76 })(this);
77 </script>
78 `
79}
80
81export function createServerHandler({ port, config }: { port: number; config: Presta }) {
82 const devClient = createDevClient({ port })
83 const staticDir = config.staticOutputDir
84 const assetDir = config.assets
85
86 return async function serveHandler(req: http.IncomingMessage, res: http.ServerResponse) {
87 const time = timer()
88 const url = req.url as string
89
90 /*
91 * If this is an asset other than HTML files, just serve it
92 */
93 if (/^.+\..+$/.test(url) && !/\.html?$/.test(url)) {
94 logger.debug({
95 label: 'debug',
96 message: `attempting to serve static asset ${url}`,
97 })
98
99 /*
100 * first check the vcs-tracked static folder,
101 * then check the presta-built static folder
102 *
103 * @see https://github.com/sure-thing/presta/issues/30
104 */
105 sirv(assetDir, { dev: true })(req, res, () => {
106 sirv(staticDir, { dev: true })(req, res, () => {
107 logger.warn({
108 label: 'serve',
109 message: `404 ${url}`,
110 duration: time(),
111 })
112
113 sendServerlessResponse(res, {
114 statusCode: 404,
115 body: default404 + devClient + devServerIcon,
116 })
117 })
118 })
119 } else {
120 /*
121 * Try to resolve a static route normally
122 */
123 try {
124 logger.debug({
125 label: 'debug',
126 message: `attempting to render static HTML for ${url}`,
127 })
128
129 const file = resolveHTML(staticDir, url) + devClient + devServerIcon
130
131 logger.info({
132 label: 'serve',
133 message: `200 ${url}`,
134 duration: time(),
135 })
136
137 sendServerlessResponse(res, { body: file })
138 } catch (e) {
139 logger.debug({
140 label: 'debug',
141 message: `static route failed`,
142 error: e as Error,
143 })
144
145 // expect ENOENT, log everything else
146 if (!/ENOENT|EISDIR/.test((e as Error).message)) {
147 console.error(e)
148 }
149
150 try {
151 /*
152 * No asset file, no static file, try dynamic
153 */
154 delete require.cache[config.functionsManifest]
155 const manifest = require(config.functionsManifest)
156 const routes = Object.keys(manifest)
157 const lambdaFilepath = routes
158 .map((route) => ({
159 matcher: toRegExp(route),
160 route,
161 }))
162 .filter(({ matcher }) => {
163 return matcher.pattern.test(url.split('?')[0])
164 })
165 .map(({ route }) => manifest[route])[0]
166
167 /**
168 * If we have a serverless function, delegate to it, otherwise 404
169 */
170 if (lambdaFilepath) {
171 logger.debug({
172 label: 'debug',
173 message: `attempting to render lambda for ${url}`,
174 })
175
176 const { handler }: { handler: AWS['Handler'] } = require(lambdaFilepath)
177 const event = await requestToEvent(req)
178 const response = await handler(event, {})
179 const headers = response.headers || {}
180 const redir = response.statusCode > 299 && response.statusCode < 399
181
182 // get mime type
183 const type = headers['Content-Type'] as string
184 const ext = type ? mime.extension(type) : 'html'
185
186 logger.info({
187 label: 'serve',
188 message: `${response.statusCode} ${redir ? headers.Location : url}`,
189 duration: time(),
190 })
191
192 sendServerlessResponse(res, {
193 statusCode: response.statusCode,
194 headers: response.headers,
195 multiValueHeaders: response.multiValueHeaders,
196 // only html can be live-reloaded, duh
197 body:
198 ext === 'html' ? (response.body || '').split('</body>')[0] + devClient + devServerIcon : response.body,
199 })
200 } else {
201 logger.debug({
202 label: 'debug',
203 message: `attempting to render static 404.html page for ${url}`,
204 })
205
206 /*
207 * Try to fall back to a static 404 page
208 */
209 try {
210 const file = resolveHTML(staticDir, '404') + devClient + devServerIcon
211
212 logger.warn({
213 label: 'serve',
214 message: `404 ${url}`,
215 duration: time(),
216 })
217
218 sendServerlessResponse(res, {
219 statusCode: 404,
220 body: file,
221 })
222 } catch (e) {
223 if (!(e as Error).message.includes('ENOENT')) {
224 console.error(e)
225 }
226
227 logger.debug({
228 label: 'debug',
229 message: `rendering default 404 HTML page for ${url}`,
230 })
231
232 logger.warn({
233 label: 'serve',
234 message: `404 ${url}`,
235 duration: time(),
236 })
237
238 sendServerlessResponse(res, {
239 statusCode: 404,
240 body: default404 + devClient + devServerIcon,
241 })
242 }
243 }
244 } catch (e) {
245 logger.debug({
246 label: 'debug',
247 message: `rendering default 500 HTML page for ${url}`,
248 })
249
250 logger.error({
251 label: 'serve',
252 message: `500 ${url}`,
253 error: e as Error,
254 duration: time(),
255 })
256
257 sendServerlessResponse(res, {
258 statusCode: 500,
259 body: '' + devClient + devServerIcon, // TODO default 500 screen
260 })
261 }
262 }
263 }
264 }
265}
266
267export async function serve(config: Presta) {
268 const port = await getPort({ port: config.port })
269 const server = http.createServer(createServerHandler({ port, config })).listen(port)
270 const socket = require('pocket.io')(server, { serveClient: false })
271
272 config.hooks.onBrowserRefresh(() => {
273 logger.debug({
274 label: 'debug',
275 message: `refresh event received`,
276 })
277
278 socket.emit('refresh')
279 })
280
281 chokidar.watch(config.assets, { ignoreInitial: true }).on('all', () => {
282 config.hooks.emitBrowserRefresh()
283 })
284
285 return { port }
286}