Skip to content

Instantly share code, notes, and snippets.

@arisris
Created July 7, 2024 16:06
Show Gist options
  • Select an option

  • Save arisris/11fa812717b8f96d50bd4e1eb2be27f3 to your computer and use it in GitHub Desktop.

Select an option

Save arisris/11fa812717b8f96d50bd4e1eb2be27f3 to your computer and use it in GitHub Desktop.
Vite plugin to develop fetch based web server like cloudflare workers & pages functions
import { type Connect, type Plugin } from "vite"
import { getRequestListener } from "@hono/node-server"
import { getPlatformProxy, type PlatformProxy } from "wrangler"
type HonoDevServerPlugin = {
entry?: string
export?: string
injectClientScript?: boolean
exclude?: RegExp[]
ignoreWatching?: RegExp[]
}
export default function honoDevServer(options: HonoDevServerPlugin = {}): Plugin {
const defaultOptions: Required<HonoDevServerPlugin> = {
entry: './src/index.ts',
export: 'default',
injectClientScript: true,
exclude: [
/.*\.css$/,
/.*\.ts$/,
/.*\.tsx$/,
/^\/@.+$/,
/\?t\=\d+$/,
/^\/favicon\.ico$/,
/^\/assets\/.+/,
/^\/node_modules\/.*/,
],
ignoreWatching: [/\.wrangler/, /\.mf/]
}
const encoder = new TextEncoder();
options = { ...defaultOptions, ...options }
let platformProxy: PlatformProxy
const injectStringToResponse = (response: Response, str: string): Response => {
if (!response.body) return response;
let injectDone = false
const transformedStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
async transform(chunk, controller) {
if (!injectDone && chunk.includes("</head>")) {
chunk = chunk.replace("</head>", `${str}</head>`)
injectDone = true
}
controller.enqueue(encoder.encode(chunk))
},
flush(controller) {
if (!injectDone) {
controller.enqueue(encoder.encode(str))
}
controller.terminate()
},
}))
return new Response(transformedStream, response)
}
return {
name: "hono:dev-server",
apply(_, env) {
return env.command === "serve"
},
config(config) {
return {
ssr: {
noExternal: true,
...config.ssr
}
}
},
async configureServer(server) {
let logger = server.config.logger
platformProxy = await getPlatformProxy()
const handler: Connect.NextHandleFunction = async (req, res, next) => {
if (options.exclude?.some((r) => r.test(req.url!))) {
return next()
}
let appModule: any;
try {
const ssrModule = await server.ssrLoadModule(options.entry!, { fixStacktrace: true })
appModule = ssrModule?.default
if (!appModule || typeof appModule.fetch !== "function") {
throw new Error("SSR Module is not hono app.")
}
} catch (e) {
return next(e)
}
return getRequestListener(async (req) => {
Object.assign(globalThis, { caches: platformProxy.caches })
Object.assign(req, { cf: platformProxy.cf })
let response = await appModule.fetch(req, platformProxy.env, platformProxy.ctx)
if (!(response instanceof Response)) {
throw response
}
if (options.injectClientScript && response.headers.get("content-type")?.includes("text/html")) {
response = injectStringToResponse(response, `<script>import("/@vite/client");</script>`)
}
return response
}, {
overrideGlobalObjects: false,
errorHandler: (e) => {
let err: Error
if (e instanceof Error) {
err = e
server.ssrFixStacktrace(err)
} else if (typeof e === 'string') {
err = new Error(`The response is not an instance of "Response", but: ${e}`)
} else {
err = new Error(`Unknown error: ${e}`)
}
next(err)
}
})(req, res)
}
server.middlewares.use(handler)
server.httpServer?.on("close", async () => {
await platformProxy.dispose()
logger.info("Workerd proxy restarted.", { timestamp: true })
})
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment