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 { createLiveReloadScript } from './liveReloadScript'
|
16 | import { AWS, Presta } from './types'
|
17 |
|
18 | function resolveHTML(dir: string, url: string) {
|
19 | let file = path.join(dir, url)
|
20 |
|
21 |
|
22 | if (!path.extname(url)) {
|
23 | try {
|
24 | return fs.readFileSync(path.join(dir, url, 'index.html'), 'utf8')
|
25 | } catch (e) {}
|
26 | }
|
27 |
|
28 | return fs.readFileSync(file, 'utf8')
|
29 | }
|
30 |
|
31 | export function createServerHandler({ port, config }: { port: number; config: Presta }) {
|
32 | const devClient = createLiveReloadScript({ port })
|
33 | const staticDir = config.staticOutputDir
|
34 | const assetDir = config.assets
|
35 |
|
36 | return async function serveHandler(req: http.IncomingMessage, res: http.ServerResponse) {
|
37 | const time = timer()
|
38 | const url = req.url as string
|
39 |
|
40 | |
41 |
|
42 |
|
43 | logger.debug({
|
44 | label: 'debug',
|
45 | message: `attempting to serve user static asset ${url}`,
|
46 | })
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | sirv(assetDir, { dev: true })(req, res, () => {
|
55 | logger.debug({
|
56 | label: 'debug',
|
57 | message: `attempting to serve generated static asset ${url}`,
|
58 | })
|
59 |
|
60 | sirv(staticDir, { dev: true })(req, res, async () => {
|
61 | try {
|
62 | |
63 |
|
64 |
|
65 | delete require.cache[config.functionsManifest]
|
66 | const manifest = require(config.functionsManifest)
|
67 | const routes = Object.keys(manifest)
|
68 | const lambdaFilepath = routes
|
69 | .map((route) => ({
|
70 | matcher: toRegExp(route),
|
71 | route,
|
72 | }))
|
73 | .filter(({ matcher }) => {
|
74 | return matcher.pattern.test(url.split('?')[0])
|
75 | })
|
76 | .map(({ route }) => manifest[route])[0]
|
77 |
|
78 | |
79 |
|
80 |
|
81 | if (lambdaFilepath) {
|
82 | logger.debug({
|
83 | label: 'debug',
|
84 | message: `attempting to render lambda for ${url}`,
|
85 | })
|
86 |
|
87 | const { handler }: { handler: AWS['Handler'] } = require(lambdaFilepath)
|
88 | const event = await requestToEvent(req)
|
89 | const response = await handler(event, {})
|
90 | const headers = response.headers || {}
|
91 | const redir = response.statusCode > 299 && response.statusCode < 399
|
92 |
|
93 |
|
94 | const type = headers['Content-Type'] as string
|
95 | const ext = type ? mime.extension(type) : 'html'
|
96 |
|
97 | logger.info({
|
98 | label: 'serve',
|
99 | message: `${response.statusCode} ${redir ? headers.Location : url}`,
|
100 | duration: time(),
|
101 | })
|
102 |
|
103 | sendServerlessResponse(res, {
|
104 | statusCode: response.statusCode,
|
105 | headers: response.headers,
|
106 | multiValueHeaders: response.multiValueHeaders,
|
107 |
|
108 | body: ext === 'html' ? (response.body || '').split('</body>')[0] + devClient : response.body,
|
109 | })
|
110 | } else {
|
111 | logger.debug({
|
112 | label: 'debug',
|
113 | message: `attempting to render static 404.html page for ${url}`,
|
114 | })
|
115 |
|
116 | |
117 |
|
118 |
|
119 | try {
|
120 | const file = resolveHTML(staticDir, '404') + devClient
|
121 |
|
122 | logger.warn({
|
123 | label: 'serve',
|
124 | message: `404 ${url}`,
|
125 | duration: time(),
|
126 | })
|
127 |
|
128 | sendServerlessResponse(res, {
|
129 | statusCode: 404,
|
130 | body: file,
|
131 | })
|
132 | } catch (e) {
|
133 | if (!(e as Error).message.includes('ENOENT')) {
|
134 | console.error(e)
|
135 | }
|
136 |
|
137 | logger.debug({
|
138 | label: 'debug',
|
139 | message: `rendering default 404 HTML page for ${url}`,
|
140 | })
|
141 |
|
142 | logger.warn({
|
143 | label: 'serve',
|
144 | message: `404 ${url}`,
|
145 | duration: time(),
|
146 | })
|
147 |
|
148 | sendServerlessResponse(res, {
|
149 | statusCode: 404,
|
150 | body: default404 + devClient,
|
151 | })
|
152 | }
|
153 | }
|
154 | } catch (e) {
|
155 | logger.debug({
|
156 | label: 'debug',
|
157 | message: `rendering default 500 HTML page for ${url}`,
|
158 | })
|
159 |
|
160 | logger.error({
|
161 | label: 'serve',
|
162 | message: `500 ${url}`,
|
163 | error: e as Error,
|
164 | duration: time(),
|
165 | })
|
166 |
|
167 | sendServerlessResponse(res, {
|
168 | statusCode: 500,
|
169 | body: '' + devClient,
|
170 | })
|
171 | }
|
172 | })
|
173 | })
|
174 | }
|
175 | }
|
176 |
|
177 | export async function serve(config: Presta) {
|
178 | const port = await getPort({ port: config.port })
|
179 | const server = http.createServer(createServerHandler({ port, config })).listen(port)
|
180 | const socket = require('pocket.io')(server, { serveClient: false })
|
181 |
|
182 | config.hooks.onBrowserRefresh(() => {
|
183 | logger.debug({
|
184 | label: 'debug',
|
185 | message: `refresh event received`,
|
186 | })
|
187 |
|
188 | socket.emit('refresh')
|
189 | })
|
190 |
|
191 | chokidar.watch(config.assets, { ignoreInitial: true }).on('all', () => {
|
192 | config.hooks.emitBrowserRefresh()
|
193 | })
|
194 |
|
195 | return { port }
|
196 | }
|