UNPKG

6 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 { createLiveReloadScript } from './liveReloadScript'
16import { AWS, Presta } from './types'
17
18function resolveHTML(dir: string, url: string) {
19 let file = path.join(dir, url)
20
21 // if no extension, it's probably intended to be an HTML file
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
31export 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 * If this is an asset other than HTML files, just serve it
42 */
43 logger.debug({
44 label: 'debug',
45 message: `attempting to serve user static asset ${url}`,
46 })
47
48 /*
49 * first check the vcs-tracked static folder,
50 * then check the presta-built static folder
51 *
52 * @see https://github.com/sure-thing/presta/issues/30
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 * No asset file, no static file, try dynamic
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 * If we have a serverless function, delegate to it, otherwise 404
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 // get mime type
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 // only html can be live-reloaded, duh
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 * Try to fall back to a static 404 page
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, // TODO default 500 screen
170 })
171 }
172 })
173 })
174 }
175}
176
177export 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}