1 | import fs from 'fs'
|
2 | import path from 'path'
|
3 | import http from 'http'
|
4 | import getPort from 'get-port'
|
5 | import sirv from 'sirv'
|
6 | import chokidar from 'chokidar'
|
7 | import mime from 'mime-types'
|
8 | import toRegExp from 'regexparam'
|
9 |
|
10 | import { timer } from './timer'
|
11 | import * as logger from './log'
|
12 | import { default404 } from './default404'
|
13 | import { requestToEvent } from './requestToEvent'
|
14 | import { sendServerlessResponse } from './sendServerlessResponse'
|
15 | import { AWS, Presta } from './types'
|
16 |
|
17 | const 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 |
|
33 | const devServerIcon = `
|
34 | <div style="${style.join(';')}">~</div>
|
35 | `
|
36 |
|
37 | function resolveHTML(dir: string, url: string) {
|
38 | let file = path.join(dir, url)
|
39 |
|
40 |
|
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 |
|
50 | function 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 |
|
81 | export 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 |
|
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 |
|
101 |
|
102 |
|
103 |
|
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 |
|
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 |
|
146 | if (!/ENOENT|EISDIR/.test((e as Error).message)) {
|
147 | console.error(e)
|
148 | }
|
149 |
|
150 | try {
|
151 | |
152 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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,
|
260 | })
|
261 | }
|
262 | }
|
263 | }
|
264 | }
|
265 | }
|
266 |
|
267 | export 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 | }
|