Skip to content

Commit 4973775

Browse files
committed
feat: add Cloudflare Workers adapter
1 parent 36e4b13 commit 4973775

File tree

22 files changed

+3670
-2138
lines changed

22 files changed

+3670
-2138
lines changed

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+
}

examples/cloudflare-workers/public/.assetsignore

Whitespace-only changes.

0 commit comments

Comments
 (0)