UNPKG

7.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 logger.debug({
94 label: 'debug',
95 message: `attempting to serve user static asset ${url}`,
96 })
97
98 /*
99 * first check the vcs-tracked static folder,
100 * then check the presta-built static folder
101 *
102 * @see https://github.com/sure-thing/presta/issues/30
103 */
104 sirv(assetDir, { dev: true })(req, res, () => {
105 logger.debug({
106 label: 'debug',
107 message: `attempting to serve generated static asset ${url}`,
108 })
109
110 sirv(staticDir, { dev: true })(req, res, async () => {
111 try {
112 /*
113 * No asset file, no static file, try dynamic
114 */
115 delete require.cache[config.functionsManifest]
116 const manifest = require(config.functionsManifest)
117 const routes = Object.keys(manifest)
118 const lambdaFilepath = routes
119 .map((route) => ({
120 matcher: toRegExp(route),
121 route,
122 }))
123 .filter(({ matcher }) => {
124 return matcher.pattern.test(url.split('?')[0])
125 })
126 .map(({ route }) => manifest[route])[0]
127
128 /**
129 * If we have a serverless function, delegate to it, otherwise 404
130 */
131 if (lambdaFilepath) {
132 logger.debug({
133 label: 'debug',
134 message: `attempting to render lambda for ${url}`,
135 })
136
137 const { handler }: { handler: AWS['Handler'] } = require(lambdaFilepath)
138 const event = await requestToEvent(req)
139 const response = await handler(event, {})
140 const headers = response.headers || {}
141 const redir = response.statusCode > 299 && response.statusCode < 399
142
143 // get mime type
144 const type = headers['Content-Type'] as string
145 const ext = type ? mime.extension(type) : 'html'
146
147 logger.info({
148 label: 'serve',
149 message: `${response.statusCode} ${redir ? headers.Location : url}`,
150 duration: time(),
151 })
152
153 sendServerlessResponse(res, {
154 statusCode: response.statusCode,
155 headers: response.headers,
156 multiValueHeaders: response.multiValueHeaders,
157 // only html can be live-reloaded, duh
158 body:
159 ext === 'html' ? (response.body || '').split('</body>')[0] + devClient + devServerIcon : response.body,
160 })
161 } else {
162 logger.debug({
163 label: 'debug',
164 message: `attempting to render static 404.html page for ${url}`,
165 })
166
167 /*
168 * Try to fall back to a static 404 page
169 */
170 try {
171 const file = resolveHTML(staticDir, '404') + devClient + devServerIcon
172
173 logger.warn({
174 label: 'serve',
175 message: `404 ${url}`,
176 duration: time(),
177 })
178
179 sendServerlessResponse(res, {
180 statusCode: 404,
181 body: file,
182 })
183 } catch (e) {
184 if (!(e as Error).message.includes('ENOENT')) {
185 console.error(e)
186 }
187
188 logger.debug({
189 label: 'debug',
190 message: `rendering default 404 HTML page for ${url}`,
191 })
192
193 logger.warn({
194 label: 'serve',
195 message: `404 ${url}`,
196 duration: time(),
197 })
198
199 sendServerlessResponse(res, {
200 statusCode: 404,
201 body: default404 + devClient + devServerIcon,
202 })
203 }
204 }
205 } catch (e) {
206 logger.debug({
207 label: 'debug',
208 message: `rendering default 500 HTML page for ${url}`,
209 })
210
211 logger.error({
212 label: 'serve',
213 message: `500 ${url}`,
214 error: e as Error,
215 duration: time(),
216 })
217
218 sendServerlessResponse(res, {
219 statusCode: 500,
220 body: '' + devClient + devServerIcon, // TODO default 500 screen
221 })
222 }
223 })
224 })
225 }
226}
227
228export async function serve(config: Presta) {
229 const port = await getPort({ port: config.port })
230 const server = http.createServer(createServerHandler({ port, config })).listen(port)
231 const socket = require('pocket.io')(server, { serveClient: false })
232
233 config.hooks.onBrowserRefresh(() => {
234 logger.debug({
235 label: 'debug',
236 message: `refresh event received`,
237 })
238
239 socket.emit('refresh')
240 })
241
242 chokidar.watch(config.assets, { ignoreInitial: true }).on('all', () => {
243 config.hooks.emitBrowserRefresh()
244 })
245
246 return { port }
247}