Skip to content

Instantly share code, notes, and snippets.

@arisris
Created June 15, 2024 06:29
Show Gist options
  • Select an option

  • Save arisris/5b035155822849143358f9f133bbc00a to your computer and use it in GitHub Desktop.

Select an option

Save arisris/5b035155822849143358f9f133bbc00a to your computer and use it in GitHub Desktop.
fetch based dev server middleware for vite.
import { Connect, type Plugin } from "vite"
type DevSeverOptions = {
/** relative path to appDir without trailing slash */
appDir?: string;
}
export const getHeaders = (req: Connect.IncomingMessage) => {
const h = new Headers()
for (const [key, value] of Object.entries(req.headers)) {
if (value === undefined) continue
if (Array.isArray(value)) {
for (const v of value) {
h.append(key, v)
}
} else {
h.append(key, value)
}
}
return h
}
export const getBody = async (req: Connect.IncomingMessage) => {
let body: BodyInit | undefined
if (req.method !== "GET") {
body = await new Promise((resolve) => {
let b = ""
req.on("data", (chunk) => {
b += chunk
})
req.on("end", () => {
resolve(b)
})
})
}
return body
}
export function devServer({
appDir = "app"
}: DevSeverOptions = {}): Plugin {
if (!("Bun" in globalThis)) {
throw new Error(`This Plugin Only Works in Bun`);
}
const serverEntry = appDir + "/server.ts", clientEntry = appDir + "/client.ts";
let isSsrBuild = false, isServeCommand = false;
return {
name: "bun-vite:dev-server",
async config(config, env) {
if (env.command === "serve") {
isServeCommand = true
}
if (env.isSsrBuild) {
isSsrBuild = true
}
if (isServeCommand || isSsrBuild) {
return {
...config,
build: {
emptyOutDir: false,
ssr: true,
rollupOptions: {
...config.build?.rollupOptions,
input: [serverEntry]
}
},
ssr: {
noExternal: true
}
}
} else {
return {
...config,
build: {
...config.build,
manifest: true,
emptyOutDir: true,
rollupOptions: {
...config.build?.rollupOptions,
input: [clientEntry],
output: {
entryFileNames: "static/[name]-[hash].js",
chunkFileNames: "static/[name]-[hash].js",
assetFileNames: "static/[name]-[hash].[ext]"
}
}
}
}
}
},
async configureServer(server) {
if (isServeCommand) {
const excluded = [
/.*\.css$/,
/.*\.ts$/,
/.*\.tsx$/,
/^\/@.+$/,
/\?t\=\d+$/,
/^\/favicon\.ico$/,
/^\/static\/.+/,
/^\/node_modules\/.*/,
/^\/app\/.*/,
]
server.middlewares.use(async (req, res, next) => {
if (excluded.some((r) => r.test(req.url!))) {
return next()
}
const ssrModule = await server.ssrLoadModule(serverEntry, { fixStacktrace: true })
if (typeof ssrModule.default?.fetch !== "function") {
res.setHeader("Content-Type", "text/html")
return next(new Error("Server entry is not hono app"))
}
const response = await (ssrModule.default?.fetch as typeof fetch)(new Request(new URL(req.url!, `http://${req.headers.host}`), {
method: req.method,
headers: getHeaders(req),
body: await getBody(req),
}))
response.headers.forEach((value, key) => {
res.setHeader(key, value)
})
res.statusCode = response.status
res.statusMessage = response.statusText
const reader = response.body?.getReader()
if (!reader) {
res.end()
return
}
const decoder = new TextDecoder()
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunkValue = decoder.decode(value)
res.write(chunkValue)
}
res.end()
return
})
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment