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 | logger.debug({
|
94 | label: 'debug',
|
95 | message: `attempting to serve user static asset ${url}`,
|
96 | })
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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,
|
221 | })
|
222 | }
|
223 | })
|
224 | })
|
225 | }
|
226 | }
|
227 |
|
228 | export 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 | }
|