Skip to content

Commit c8a32e9

Browse files
authored
feat: add Cloudflare Workers adapter (#18)
* feat: add Cloudflare Workers adapter * update README * rename `adapters` to `handlers`
1 parent 36e4b13 commit c8a32e9

23 files changed

+3710
-2144
lines changed

README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# hono-remix-adapter
22

3-
`hono-remix-adapter` is a set of tools for adapting between Hono and Remix. It is composed of a Vite plugin and handlers that enable it to support platforms like Cloudflare Pages. You can create an Hono app, and it will be applied to your Remix app.
3+
`hono-remix-adapter` is a set of tools for adapting between Hono and Remix. It is composed of a Vite plugin and handlers that enable it to support platforms like Cloudflare Workers. You can create an Hono app, and it will be applied to your Remix app.
44

55
```ts
66
// server/index.ts
@@ -66,9 +66,9 @@ const app = new Hono()
6666
export default app
6767
```
6868

69-
## Cloudflare Pages
69+
## Cloudflare Workers
7070

71-
To support Cloudflare Pages, add the adapter in `@hono/vite-dev-server` for development.
71+
To support Cloudflare Workers and Cloudflare Pages, add the adapter in `@hono/vite-dev-server` for development.
7272

7373
```ts
7474
// vite.config.ts
@@ -87,7 +87,29 @@ export default defineConfig({
8787
})
8888
```
8989

90-
To deploy it, you can write the following handler on `functions/[[path]].ts`:
90+
To deploy your app to Cloudflare Workers, you can write the following handler on `worker.ts`:
91+
92+
```ts
93+
// worker.ts
94+
import handle from 'hono-remix-adapter/cloudflare-workers'
95+
import * as build from './build/server'
96+
import app from './server'
97+
98+
export default handle(build, app)
99+
```
100+
101+
Specify `worker.ts` in your `wrangler.toml`:
102+
103+
```toml
104+
name = "example-cloudflare-workers"
105+
compatibility_date = "2024-11-06"
106+
main = "./worker.ts"
107+
assets = { directory = "./build/client" }
108+
```
109+
110+
## Cloudflare Pages
111+
112+
To deploy your app to Cloudflare Pages, you can write the following handler on `functions/[[path]].ts`:
91113

92114
```ts
93115
// functions/[[path]].ts
@@ -169,7 +191,19 @@ export default defineConfig({
169191
})
170192
```
171193

172-
For Cloudflare Pages, you can add it to the `handle` function:
194+
For Cloudflare Workers, you can add it to the `handler` function:
195+
196+
```ts
197+
// worker.ts
198+
import handle from 'hono-remix-adapter/cloudflare-workers'
199+
import * as build from './build/server'
200+
import { getLoadContext } from './load-context'
201+
import app from './server'
202+
203+
export default handle(build, app, { getLoadContext })
204+
```
205+
206+
You can also add it for Cloudflare Pages:
173207

174208
```ts
175209
// functions/[[path]].ts

examples/cloudflare-pages/package.json

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@
1313
"test:e2e": "playwright test -- -c playwright.config.ts e2e.test.ts"
1414
},
1515
"dependencies": {
16-
"@remix-run/cloudflare": "^2.11.1",
17-
"@remix-run/cloudflare-pages": "^2.11.1",
18-
"@remix-run/react": "^2.11.1",
16+
"@remix-run/cloudflare": "^2.14.0",
17+
"@remix-run/react": "^1.19.3",
1918
"hono": "^4.5.11",
2019
"isbot": "^4.1.0",
2120
"react": "^18.3.1",
22-
"react-dom": "^18.3.1"
21+
"react-dom": "^18.3.1",
22+
"remix-hono": "^0.0.16"
2323
},
2424
"devDependencies": {
25-
"@cloudflare/workers-types": "^4.20240903.0",
2625
"@hono/vite-dev-server": "^0.16.0",
27-
"@playwright/test": "^1.47.0",
28-
"@remix-run/dev": "^2.11.1",
26+
"@playwright/test": "^1.48.2",
27+
"@remix-run/dev": "^2.14.0",
2928
"@types/react": "^18.2.20",
3029
"@types/react-dom": "^18.2.7",
3130
"playwright": "^1.47.0",
@@ -37,4 +36,4 @@
3736
"engines": {
3837
"node": ">=20.0.0"
3938
}
40-
}
39+
}

examples/cloudflare-pages/wrangler.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#:schema node_modules/wrangler/config-schema.json
2-
name = "example"
2+
name = "example-cloudflare-pages"
33
compatibility_date = "2024-09-03"
44
pages_build_output_dir = "./build/client"
55

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build
2+
.env
3+
.dev.vars
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { RemixBrowser } from '@remix-run/react'
8+
import { startTransition, StrictMode } from 'react'
9+
import { hydrateRoot } from 'react-dom/client'
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<RemixBrowser />
16+
</StrictMode>
17+
)
18+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* By default, Remix will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.server
5+
*/
6+
7+
import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'
8+
import { RemixServer } from '@remix-run/react'
9+
import { isbot } from 'isbot'
10+
import { renderToReadableStream } from 'react-dom/server'
11+
12+
const ABORT_DELAY = 5000
13+
14+
export default async function handleRequest(
15+
request: Request,
16+
responseStatusCode: number,
17+
responseHeaders: Headers,
18+
remixContext: EntryContext,
19+
// This is ignored so we can keep it in the template for visibility. Feel
20+
// free to delete this parameter in your app if you're not using it!
21+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
22+
loadContext: AppLoadContext
23+
) {
24+
const controller = new AbortController()
25+
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY)
26+
27+
const body = await renderToReadableStream(
28+
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
29+
{
30+
signal: controller.signal,
31+
onError(error: unknown) {
32+
if (!controller.signal.aborted) {
33+
// Log streaming rendering errors from inside the shell
34+
console.error(error)
35+
}
36+
responseStatusCode = 500
37+
},
38+
}
39+
)
40+
41+
body.allReady.then(() => clearTimeout(timeoutId))
42+
43+
if (isbot(request.headers.get('user-agent') || '')) {
44+
await body.allReady
45+
}
46+
47+
responseHeaders.set('Content-Type', 'text/html')
48+
return new Response(body, {
49+
headers: responseHeaders,
50+
status: responseStatusCode,
51+
})
52+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Outlet, Scripts } from '@remix-run/react'
2+
3+
export function Layout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<html lang='en'>
6+
<head>
7+
<meta charSet='utf-8' />
8+
<meta name='viewport' content='width=device-width, initial-scale=1' />
9+
</head>
10+
<body>
11+
{children}
12+
<Scripts />
13+
</body>
14+
</html>
15+
)
16+
}
17+
18+
export default function App() {
19+
return <Outlet />
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { LoaderFunctionArgs } from '@remix-run/cloudflare'
2+
import { useLoaderData } from '@remix-run/react'
3+
4+
export const loader = (args: LoaderFunctionArgs) => {
5+
const extra = args.context.extra
6+
const cloudflare = args.context.cloudflare
7+
return { cloudflare, extra }
8+
}
9+
10+
export default function Index() {
11+
const { cloudflare, extra } = useLoaderData<typeof loader>()
12+
return (
13+
<div>
14+
<h1>Remix and Hono</h1>
15+
<h2>Var is {cloudflare.env.MY_VAR}</h2>
16+
<h3>
17+
{cloudflare.cf ? 'cf,' : ''}
18+
{cloudflare.ctx ? 'ctx,' : ''}
19+
{cloudflare.caches ? 'caches are available' : ''}
20+
</h3>
21+
<h4>Extra is {extra}</h4>
22+
</div>
23+
)
24+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { PlatformProxy } from 'wrangler'
2+
3+
interface Env {
4+
MY_VAR: string
5+
}
6+
7+
type GetLoadContextArgs = {
8+
request: Request
9+
context: {
10+
cloudflare: Omit<PlatformProxy<Env>, 'dispose' | 'caches' | 'cf'> & {
11+
caches: PlatformProxy<Env>['caches'] | CacheStorage
12+
cf: Request['cf']
13+
}
14+
}
15+
}
16+
17+
declare module '@remix-run/cloudflare' {
18+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
19+
interface AppLoadContext extends ReturnType<typeof getLoadContext> {
20+
// This will merge the result of `getLoadContext` into the `AppLoadContext`
21+
extra: string
22+
}
23+
}
24+
25+
export function getLoadContext({ context }: GetLoadContextArgs) {
26+
return {
27+
...context,
28+
extra: 'stuff',
29+
}
30+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "example-cloudflare-workers",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"build": "remix vite:build",
8+
"deploy": "npm run build && wrangler deploy",
9+
"dev": "remix vite:dev",
10+
"start": "wrangler dev",
11+
"typecheck": "tsc",
12+
"preview": "npm run build && wrangler dev"
13+
},
14+
"dependencies": {
15+
"@remix-run/cloudflare": "^2.14.0",
16+
"@remix-run/react": "^1.19.3",
17+
"hono": "^4.6.9",
18+
"isbot": "^4.1.0",
19+
"react": "^18.2.0",
20+
"react-dom": "^18.2.0",
21+
"remix-hono": "^0.0.16"
22+
},
23+
"devDependencies": {
24+
"@hono/vite-dev-server": "^0.16.0",
25+
"@remix-run/dev": "^2.14.0",
26+
"@types/react": "^18.2.20",
27+
"@types/react-dom": "^18.2.7",
28+
"typescript": "^5.1.6",
29+
"vite": "^5.1.0",
30+
"vite-tsconfig-paths": "^4.2.1",
31+
"wrangler": "^3.86.0"
32+
},
33+
"engines": {
34+
"node": ">=20.0.0"
35+
}
36+
}

0 commit comments

Comments
 (0)