forked from tale/headplane
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.mjs
executable file
·159 lines (131 loc) · 4.64 KB
/
server.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// This is a polyglot entrypoint for Headplane when running in production
// It doesn't use any dependencies aside from @remix-run/node and mime
// During build we bundle the used dependencies into the file so that
// we can only need this file and a Node.js installation to run the server.
// PREFIX is defined globally, see vite.config.ts
import { access, constants } from 'node:fs/promises'
import { createReadStream, existsSync, statSync } from 'node:fs'
import { createServer } from 'node:http'
import { join, resolve } from 'node:path'
import { env } from 'node:process'
function log(level, message) {
const date = new Date().toISOString()
console.log(`${date} (${level}) [SRVX] ${message}`)
}
log('INFO', `Running with Node.js ${process.versions.node}`)
try {
await access('./node_modules/@remix-run', constants.F_OK | constants.R_OK)
log('INFO', 'Found node_modules dependencies')
} catch (error) {
log('ERROR', 'No node_modules found. Please run `pnpm install` first')
log('ERROR', error)
process.exit(1)
}
try {
await access('./build/server', constants.F_OK | constants.R_OK)
log('INFO', 'Found build directory')
} catch (error) {
const date = new Date().toISOString()
log('ERROR', 'No build directory found. Please run `pnpm build` first')
log('ERROR', error)
process.exit(1)
}
const {
createRequestHandler: remixRequestHandler,
createReadableStreamFromReadable,
writeReadableStreamToWritable
} = await import('@remix-run/node')
const { default: mime } = await import('mime')
const port = env.PORT || 3000
const host = env.HOST || '0.0.0.0'
const buildPath = env.BUILD_PATH || './build'
// Because this is a dynamic import without an easily discernable path
// we gain the "deoptimization" we want so that Vite doesn't bundle this
const build = await import(resolve(join(buildPath, 'server', 'index.js')))
const baseDir = resolve(join(buildPath, 'client'))
const handler = remixRequestHandler(build, 'production')
const http = createServer(async (req, res) => {
const url = new URL(`http://${req.headers.host}${req.url}`)
if (!url.pathname.startsWith(PREFIX)) {
res.writeHead(404)
res.end()
return
}
// We need to handle an issue where say we are navigating to $PREFIX
// but Remix does not handle it without the trailing slash. This is
// because Remix uses the URL constructor to parse the URL and it
// will remove the trailing slash. We need to redirect to the correct
// URL so that Remix can handle it correctly.
if (url.pathname === PREFIX) {
res.writeHead(302, {
Location: `${PREFIX}/`
})
res.end()
return
}
// Before we pass any requests to our Remix handler we need to check
// if we can handle a raw file request. This is important for the
// Remix loader to work correctly.
//
// To optimize this, we send them as readable streams in the node
// response and we also set headers for aggressive caching.
if (url.pathname.startsWith(`${PREFIX}/assets/`)) {
const filePath = join(baseDir, url.pathname.replace(PREFIX, ''))
const exists = existsSync(filePath)
const stats = statSync(filePath)
if (exists && stats.isFile()) {
// Build assets are cache-bust friendly so we can cache them heavily
if (req.url.startsWith('/build')) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
// Send the file as a readable stream
const fileStream = createReadStream(filePath)
const type = mime.getType(filePath)
res.setHeader('Content-Length', stats.size)
res.setHeader('Content-Type', type)
fileStream.pipe(res)
return
}
}
// Handling the request
const controller = new AbortController()
res.on('close', () => controller.abort())
const headers = new Headers()
for (const [key, value] of Object.entries(req.headers)) {
if (!value) continue
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v)
}
continue
}
headers.append(key, value)
}
const remixReq = new Request(url.href, {
headers,
method: req.method,
signal: controller.signal,
// If we have a body we set a duplex and we load the body
...(req.method !== 'GET' && req.method !== 'HEAD' ? {
body: createReadableStreamFromReadable(req),
duplex: 'half'
} : {}
)
})
// Pass our request to the Remix handler and get a response
const response = await handler(remixReq, {}) // No context
// Handle our response and reply
res.statusCode = response.status
res.statusMessage = response.statusText
for (const [key, value] of response.headers.entries()) {
res.appendHeader(key, value)
}
if (response.body) {
await writeReadableStreamToWritable(response.body, res)
return
}
res.end()
})
http.listen(port, host, () => {
log('INFO', `Running on ${host}:${port}`)
})