/
server.ts
141 lines (120 loc) · 3.9 KB
/
server.ts
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
import type { ServerResponse } from 'http'
import { promises as fs } from 'fs'
import path from 'path'
import connect, { NextHandleFunction } from 'connect'
import {
createServer as createViteServer,
InlineConfig,
ViteDevServer,
} from 'vite'
import { getEntryPoint } from '../config'
import { buildHtmlDocument } from '../build/utils'
function fixEntryPoint(vite: ViteDevServer) {
// The plugin is redirecting to the entry-client for the SPA,
// but we need to reach the entry-server here. This trick
// replaces the plugin behavior in the config and seems
// to keep the entry-client for the SPA.
for (const alias of vite.config.resolve.alias || []) {
// @ts-ignore
if (alias._viteSSR === true) {
alias.replacement = alias.replacement.replace('client', 'server')
}
}
}
export type SsrOptions = {
plugin?: string
ssr?: string
getRenderContext?: (params: {
url: string
request: connect.IncomingMessage
response: ServerResponse
resolvedEntryPoint: Record<string, any>
}) => Promise<any>
}
export const createSSRDevHandler = (
server: ViteDevServer,
options: SsrOptions = {}
) => {
options = {
...server.config.inlineConfig, // CLI flags
...options,
}
const resolve = (p: string) => path.resolve(server.config.root, p)
async function getIndexTemplate(url: string) {
// Template should be fresh in every request
const indexHtml = await fs.readFile(resolve('index.html'), 'utf-8')
return await server.transformIndexHtml(url, indexHtml)
}
const handleSsrRequest: NextHandleFunction = async (
request,
response,
next
) => {
if (request.method !== 'GET' || request.originalUrl === '/favicon.ico') {
return next()
}
fixEntryPoint(server)
try {
const template = await getIndexTemplate(request.originalUrl as string)
const entryPoint =
options.ssr || (await getEntryPoint(server.config.root, template))
let resolvedEntryPoint = await server.ssrLoadModule(resolve(entryPoint))
resolvedEntryPoint = resolvedEntryPoint.default || resolvedEntryPoint
const render = resolvedEntryPoint.render || resolvedEntryPoint
const protocol =
// @ts-ignore
request.protocol ||
(request.headers.referer || '').split(':')[0] ||
'http'
const url = protocol + '://' + request.headers.host + request.originalUrl
// This context might contain initialState provided by other plugins
const context = options.getRenderContext
? await options.getRenderContext({
url,
request,
response,
resolvedEntryPoint,
})
: {}
if (context && context.status) {
// If response-like is provided, just return the response
for (const [key, value] of Object.entries(context.headers || {})) {
response.setHeader(key, value as string)
}
response.statusCode = context.status
response.statusMessage = context.statusText
return response.end(context.body)
}
const htmlParts = await render(url, { request, response, ...context })
const html = buildHtmlDocument(template, htmlParts)
response.setHeader('Content-Type', 'text/html')
response.end(html)
} catch (e) {
server.ssrFixStacktrace(e)
console.log(e.stack)
next(e)
}
}
return handleSsrRequest
}
export default async function createSsrServer(
options: SsrOptions & InlineConfig = {}
) {
// Enable SSR in the plugin
process.env.__DEV_MODE_SSR = 'true'
const viteServer = await createViteServer({
...options,
server: options,
})
return {
async listen(port?: number) {
if (!globalThis.fetch) {
const fetch = await import('node-fetch')
// @ts-ignore
globalThis.fetch = fetch.default || fetch
}
await viteServer.listen(port)
viteServer.config.logger.info('\n -- SSR mode\n')
},
}
}