Skip to content

Commit

Permalink
feat: add Cloudflare Workers adapter (#18)
Browse files Browse the repository at this point in the history
* feat: add Cloudflare Workers adapter

* update README

* rename `adapters` to `handlers`
  • Loading branch information
yusukebe authored Nov 11, 2024
1 parent 36e4b13 commit c8a32e9
Show file tree
Hide file tree
Showing 23 changed files with 3,710 additions and 2,144 deletions.
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# hono-remix-adapter

`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.
`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.

```ts
// server/index.ts
Expand Down Expand Up @@ -66,9 +66,9 @@ const app = new Hono()
export default app
```

## Cloudflare Pages
## Cloudflare Workers

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

```ts
// vite.config.ts
Expand All @@ -87,7 +87,29 @@ export default defineConfig({
})
```

To deploy it, you can write the following handler on `functions/[[path]].ts`:
To deploy your app to Cloudflare Workers, you can write the following handler on `worker.ts`:

```ts
// worker.ts
import handle from 'hono-remix-adapter/cloudflare-workers'
import * as build from './build/server'
import app from './server'

export default handle(build, app)
```

Specify `worker.ts` in your `wrangler.toml`:

```toml
name = "example-cloudflare-workers"
compatibility_date = "2024-11-06"
main = "./worker.ts"
assets = { directory = "./build/client" }
```

## Cloudflare Pages

To deploy your app to Cloudflare Pages, you can write the following handler on `functions/[[path]].ts`:

```ts
// functions/[[path]].ts
Expand Down Expand Up @@ -169,7 +191,19 @@ export default defineConfig({
})
```

For Cloudflare Pages, you can add it to the `handle` function:
For Cloudflare Workers, you can add it to the `handler` function:

```ts
// worker.ts
import handle from 'hono-remix-adapter/cloudflare-workers'
import * as build from './build/server'
import { getLoadContext } from './load-context'
import app from './server'

export default handle(build, app, { getLoadContext })
```

You can also add it for Cloudflare Pages:

```ts
// functions/[[path]].ts
Expand Down
15 changes: 7 additions & 8 deletions examples/cloudflare-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@
"test:e2e": "playwright test -- -c playwright.config.ts e2e.test.ts"
},
"dependencies": {
"@remix-run/cloudflare": "^2.11.1",
"@remix-run/cloudflare-pages": "^2.11.1",
"@remix-run/react": "^2.11.1",
"@remix-run/cloudflare": "^2.14.0",
"@remix-run/react": "^1.19.3",
"hono": "^4.5.11",
"isbot": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"remix-hono": "^0.0.16"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240903.0",
"@hono/vite-dev-server": "^0.16.0",
"@playwright/test": "^1.47.0",
"@remix-run/dev": "^2.11.1",
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.14.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"playwright": "^1.47.0",
Expand All @@ -37,4 +36,4 @@
"engines": {
"node": ">=20.0.0"
}
}
}
2 changes: 1 addition & 1 deletion examples/cloudflare-pages/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#:schema node_modules/wrangler/config-schema.json
name = "example"
name = "example-cloudflare-pages"
compatibility_date = "2024-09-03"
pages_build_output_dir = "./build/client"

Expand Down
3 changes: 3 additions & 0 deletions examples/cloudflare-workers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/build
.env
.dev.vars
18 changes: 18 additions & 0 deletions examples/cloudflare-workers/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* 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` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/

import { RemixBrowser } from '@remix-run/react'
import { startTransition, StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
)
})
52 changes: 52 additions & 0 deletions examples/cloudflare-workers/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* 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` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/

import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'
import { RemixServer } from '@remix-run/react'
import { isbot } from 'isbot'
import { renderToReadableStream } from 'react-dom/server'

const ABORT_DELAY = 5000

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext
) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY)

const body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
signal: controller.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
// Log streaming rendering errors from inside the shell
console.error(error)
}
responseStatusCode = 500
},
}
)

body.allReady.then(() => clearTimeout(timeoutId))

if (isbot(request.headers.get('user-agent') || '')) {
await body.allReady
}

responseHeaders.set('Content-Type', 'text/html')
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
})
}
20 changes: 20 additions & 0 deletions examples/cloudflare-workers/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Outlet, Scripts } from '@remix-run/react'

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<head>
<meta charSet='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}

export default function App() {
return <Outlet />
}
24 changes: 24 additions & 0 deletions examples/cloudflare-workers/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { LoaderFunctionArgs } from '@remix-run/cloudflare'
import { useLoaderData } from '@remix-run/react'

export const loader = (args: LoaderFunctionArgs) => {
const extra = args.context.extra
const cloudflare = args.context.cloudflare
return { cloudflare, extra }
}

export default function Index() {
const { cloudflare, extra } = useLoaderData<typeof loader>()
return (
<div>
<h1>Remix and Hono</h1>
<h2>Var is {cloudflare.env.MY_VAR}</h2>
<h3>
{cloudflare.cf ? 'cf,' : ''}
{cloudflare.ctx ? 'ctx,' : ''}
{cloudflare.caches ? 'caches are available' : ''}
</h3>
<h4>Extra is {extra}</h4>
</div>
)
}
30 changes: 30 additions & 0 deletions examples/cloudflare-workers/load-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PlatformProxy } from 'wrangler'

interface Env {
MY_VAR: string
}

type GetLoadContextArgs = {
request: Request
context: {
cloudflare: Omit<PlatformProxy<Env>, 'dispose' | 'caches' | 'cf'> & {
caches: PlatformProxy<Env>['caches'] | CacheStorage
cf: Request['cf']
}
}
}

declare module '@remix-run/cloudflare' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface AppLoadContext extends ReturnType<typeof getLoadContext> {
// This will merge the result of `getLoadContext` into the `AppLoadContext`
extra: string
}
}

export function getLoadContext({ context }: GetLoadContextArgs) {
return {
...context,
extra: 'stuff',
}
}
36 changes: 36 additions & 0 deletions examples/cloudflare-workers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "example-cloudflare-workers",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"deploy": "npm run build && wrangler deploy",
"dev": "remix vite:dev",
"start": "wrangler dev",
"typecheck": "tsc",
"preview": "npm run build && wrangler dev"
},
"dependencies": {
"@remix-run/cloudflare": "^2.14.0",
"@remix-run/react": "^1.19.3",
"hono": "^4.6.9",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remix-hono": "^0.0.16"
},
"devDependencies": {
"@hono/vite-dev-server": "^0.16.0",
"@remix-run/dev": "^2.14.0",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
"wrangler": "^3.86.0"
},
"engines": {
"node": ">=20.0.0"
}
}
Empty file.
Binary file added examples/cloudflare-workers/public/favicon.ico
Binary file not shown.
23 changes: 23 additions & 0 deletions examples/cloudflare-workers/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// server/index.ts
import { Hono } from 'hono'

const app = new Hono<{
Bindings: {
MY_VAR: string
}
}>()

app.use(async(c, next) => {
await next()
c.header('X-Powered-By', 'Remix and Hono')
})

app.get('/api', (c) => {
return c.json({
message: 'Hello',
var: c.env.MY_VAR
})
})


export default app
42 changes: 42 additions & 0 deletions examples/cloudflare-workers/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"include": [
"worker-configuration.d.ts",
"env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
"**/.client/**/*.tsx"
],
"compilerOptions": {
"lib": [
"DOM",
"DOM.Iterable",
"ES2022"
],
"types": [
"@remix-run/cloudflare",
"vite/client",
"@cloudflare/workers-types/2023-07-01"
],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./app/*"
]
},
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}
Loading

0 comments on commit c8a32e9

Please sign in to comment.