Skip to content

Commit

Permalink
chore: Remix to React Router 7 (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
yusukebe authored Jan 4, 2025
1 parent fd2c34f commit 6743c53
Show file tree
Hide file tree
Showing 40 changed files with 93 additions and 138 deletions.
53 changes: 26 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# hono-remix-adapter
# hono-react-router-adapter

`hono-remix-adapter` is a set of tools for adapting between Hono and React Router. It is composed of a Vite plugin and handlers that enable it to support platforms like Cloudflare Workers and Node.js. You just create Hono app, and it will be applied to your React Router app.
`hono-react-router-adapter` is a set of tools for adapting between Hono and React Router. It is composed of a Vite plugin and handlers that enable it to support platforms like Cloudflare Workers and Node.js. You just create Hono app, and it will be applied to your React Router app.

```ts
// server/index.ts
Expand All @@ -10,7 +10,7 @@ const app = new Hono()

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

app.get('/api', (c) => {
Expand All @@ -26,12 +26,12 @@ This means you can create API routes with Hono's syntax and use a lot of Hono's

> [!WARNING]
>
> `hono-remix-adapter` is currently unstable. The API may be changed without announcement in the future.
> `hono-react-router-adapter` is currently unstable. The API may be changed without announcement in the future.
## Install

```bash
npm i hono-remix-adapter hono
npm i hono-react-router-adapter hono
```

## How to use
Expand All @@ -40,12 +40,12 @@ Edit your `vite.config.ts`:

```ts
// vite.config.ts
import serverAdapter from 'hono-remix-adapter/vite'
import serverAdapter from 'hono-react-router-adapter/vite'

export default defineConfig({
plugins: [
// ...
remix(),
reactRouter(),
serverAdapter({
entry: 'server/index.ts',
}),
Expand Down Expand Up @@ -73,12 +73,12 @@ To support Cloudflare Workers and Cloudflare Pages, add the adapter in `@hono/vi
```ts
// vite.config.ts
import adapter from '@hono/vite-dev-server/cloudflare'
import serverAdapter from 'hono-remix-adapter/vite'
import serverAdapter from 'hono-react-router-adapter/vite'

export default defineConfig({
plugins: [
// ...
remix(),
reactRouter(),
serverAdapter({
adapter, // Add Cloudflare adapter
entry: 'server/index.ts',
Expand All @@ -91,7 +91,7 @@ To deploy your app to Cloudflare Workers, you can write the following handler on

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

Expand All @@ -113,7 +113,7 @@ To deploy your app to Cloudflare Pages, you can write the following handler on `

```ts
// functions/[[path]].ts
import handle from 'hono-remix-adapter/cloudflare-pages'
import handle from 'hono-react-router-adapter/cloudflare-pages'
import * as build from '../build/server'
import server from '../server'

Expand All @@ -122,13 +122,13 @@ export const onRequest = handle(build, server)

## Node.js

If you want to run your app on Node.js, you can use `hono-remix-adapter/node`. Write `main.ts`:
If you want to run your app on Node.js, you can use `hono-react-router-adapter/node`. Write `main.ts`:

```ts
// main.ts
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import handle from 'hono-remix-adapter/node'
import handle from 'hono-react-router-adapter/node'
import * as build from './build/server'
import { getLoadContext } from './load-context'
import server from './server'
Expand Down Expand Up @@ -158,19 +158,18 @@ esbuild main.ts --bundle --outfile=main.mjs --platform=node --target=node16.8 --

## `getLoadContext`

If you want to add extra context values when you use Remix routes, like in the following use case:
If you want to add extra context values when you use React Router routes, like in the following use case:

```ts
// app/routes/_index.tsx
import type { LoaderFunctionArgs } from '@remix-run/cloudflare'
import { useLoaderData } from '@remix-run/react'
import type { Route } from './+types/_index'

export const loader = ({ context }) => {
export const loader = (args: Route.LoaderArgs) => {
return { extra: context.extra }
}

export default function Index() {
const { extra } = useLoaderData<typeof loader>()
export default function Index({ loaderData }: Route.ComponentProps) {
const { extra } = loaderData
return <h1>Extra is {extra}</h1>
}
```
Expand Down Expand Up @@ -210,7 +209,7 @@ Then import the `getLoadContext` and add it to the `serverAdapter` as an argumen
// vite.config.ts
import adapter from '@hono/vite-dev-server/cloudflare'
import { reactRouter } from '@react-router/dev'
import serverAdapter from 'hono-remix-adapter/vite'
import serverAdapter from 'hono-react-router-adapter/vite'
import { defineConfig } from 'vite'
import { getLoadContext } from './load-context'

Expand All @@ -231,7 +230,7 @@ For Cloudflare Workers, you can add it to the `handler` function:

```ts
// worker.ts
import handle from 'hono-remix-adapter/cloudflare-workers'
import handle from 'hono-react-router-adapter/cloudflare-workers'
import * as build from './build/server'
import { getLoadContext } from './load-context'
import app from './server'
Expand All @@ -243,7 +242,7 @@ You can also add it for Cloudflare Pages:

```ts
// functions/[[path]].ts
import handle from 'hono-remix-adapter/cloudflare-pages'
import handle from 'hono-react-router-adapter/cloudflare-pages'
import { getLoadContext } from 'load-context'
import * as build from '../build/server'
import server from '../server'
Expand All @@ -255,7 +254,7 @@ This way is almost the same as [Remix](https://remix.run/docs/en/main/guides/vit

### Getting Hono context

You can get the Hono context in Remix routes. For example, you can pass the value with `c.set()` from your Hono instance in the `server/index.ts`:
You can get the Hono context in React Router routes. For example, you can pass the value with `c.set()` from your Hono instance in the `server/index.ts`:

```ts
// server/index.ts
Expand All @@ -279,14 +278,14 @@ In the React Router route, you can get the context from `args.context.hono.conte

```ts
// app/routes/_index.tsx
import { Router } from "./types/_index"
import { Router } from './types/_index'

export const loader = ({ context }) => {
const message = args.context.hono.context.get('message')
return { message }
}

export default function Index({ loaderData }:Route.ComponentProps) {
export default function Index({ loaderData }: Route.ComponentProps) {
const { message } = loaderData
return <h1>Message is {message}</h1>
}
Expand Down Expand Up @@ -364,7 +363,7 @@ app.use(async (c, next) => {
export default app
```

You can retrieve and process the context saved in Hono from Remix as follows:
You can retrieve and process the context saved in Hono from React Router as follows:

```ts
// app/routes/_index.tsx
Expand All @@ -377,7 +376,7 @@ export const loader = () => {
}
```

## Auth middleware for Remix routes
## Auth middleware for React Router routes

If you want to add Auth Middleware, e.g. Basic Auth middleware, please be careful that users can access the protected pages with SPA tradition. To prevent this, add a `loader` to the page:

Expand Down
6 changes: 0 additions & 6 deletions examples/cloudflare-pages/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* 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 { StrictMode, startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'
Expand Down
6 changes: 0 additions & 6 deletions examples/cloudflare-pages/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* 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 { isbot } from 'isbot'
import { renderToReadableStream } from 'react-dom/server'
import type { AppLoadContext, EntryContext } from 'react-router'
Expand Down
6 changes: 3 additions & 3 deletions examples/cloudflare-pages/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Route } from './+types/_index'
import logoDark from '/logo-dark.png?inline'
import logoDark from '/logo.png?inline'

export const loader = (args: Route.LoaderArgs) => {
const extra = args.context.extra
Expand All @@ -13,7 +13,7 @@ export default function Index({ loaderData }: Route.ComponentProps) {
const { cloudflare, extra, myVarInVariables, isWaitUntilDefined } = loaderData
return (
<div>
<h1>Remix and Hono</h1>
<h1>React Router and Hono</h1>
<h2>Var is {cloudflare.env.MY_VAR}</h2>
<h3>
{cloudflare.cf ? 'cf,' : ''}
Expand All @@ -23,7 +23,7 @@ export default function Index({ loaderData }: Route.ComponentProps) {
<h4>Extra is {extra}</h4>
<h5>Var in Variables is {myVarInVariables}</h5>
<h6>waitUntil is {isWaitUntilDefined ? 'defined' : 'not defined'}</h6>
<img src={logoDark} alt='Remix' />
<img src={logoDark} alt='React Router' />
</div>
)
}
6 changes: 3 additions & 3 deletions examples/cloudflare-pages/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ test('Should return 200 response - /', async ({ page }) => {
expect(response?.status()).toBe(200)

const headers = response?.headers() ?? {}
expect(headers['x-powered-by']).toBe('Remix and Hono')
expect(headers['x-powered-by']).toBe('React Router and Hono')

const contentH1 = await page.textContent('h1')
expect(contentH1).toBe('Remix and Hono')
expect(contentH1).toBe('React Router and Hono')

const contentH2 = await page.textContent('h2')
expect(contentH2).toBe('Var is My Value')
Expand All @@ -25,7 +25,7 @@ test('Should return 200 response - /', async ({ page }) => {
const contentH6 = await page.textContent('h6')
expect(contentH6).toBe('waitUntil is defined')

const imageResponse = await page.goto('/logo-dark.png?inline')
const imageResponse = await page.goto('/logo.png?inline')
expect(imageResponse?.status()).toBe(200)
})

Expand Down
2 changes: 1 addition & 1 deletion examples/cloudflare-pages/functions/[[path]].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// functions/[[path]].ts
import handle from 'hono-remix-adapter/cloudflare-pages'
import handle from 'hono-react-router-adapter/cloudflare-pages'
import { getLoadContext } from 'load-context'
import * as build from '../build/server'
import server from '../server'
Expand Down
Binary file modified examples/cloudflare-pages/public/favicon.ico
Binary file not shown.
Binary file removed examples/cloudflare-pages/public/logo-dark.png
Binary file not shown.
Binary file added examples/cloudflare-pages/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/cloudflare-pages/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const app = new Hono<{
app.use(async (c, next) => {
c.set('MY_VAR_IN_VARIABLES', 'My variable set in c.set')
await next()
c.header('X-Powered-By', 'Remix and Hono')
c.header('X-Powered-By', 'React Router and Hono')
})

app.get('/api', (c) => {
Expand Down
6 changes: 3 additions & 3 deletions examples/cloudflare-pages/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// vite.config.ts
import adapter from '@hono/vite-dev-server/cloudflare'
import { reactRouter } from '@react-router/dev/vite'
import { cloudflareDevProxy as remixCloudflareDevProxy } from '@react-router/dev/vite/cloudflare'
import serverAdapter from 'hono-remix-adapter/vite'
import { cloudflareDevProxy } from '@react-router/dev/vite/cloudflare'
import serverAdapter from 'hono-react-router-adapter/vite'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { getLoadContext } from './load-context'

export default defineConfig({
plugins: [
remixCloudflareDevProxy(),
cloudflareDevProxy(),
reactRouter(),
serverAdapter({
adapter,
Expand Down
6 changes: 0 additions & 6 deletions examples/cloudflare-workers/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* 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 { startTransition, StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'
Expand Down
23 changes: 4 additions & 19 deletions examples/cloudflare-workers/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
/**
* 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 { isbot } from 'isbot'
import { renderToReadableStream } from 'react-dom/server'
import type { AppLoadContext, EntryContext } from 'react-router'
import { ServerRouter } from 'react-router'

const ABORT_DELAY = 5000

export default async function handleRequest(
request: Request,
responseStatusCode: number,
Expand All @@ -21,25 +13,18 @@ export default async function handleRequest(
// 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(
<ServerRouter context={reactRouterContext} url={request.url} abortDelay={ABORT_DELAY} />,
<ServerRouter context={reactRouterContext} url={request.url} />,
{
signal: controller.signal,
signal: request.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
// Log streaming rendering errors from inside the shell
console.error(error)
}
// 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
}
Expand Down
2 changes: 1 addition & 1 deletion examples/cloudflare-workers/app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Index({ loaderData }: Route.ComponentProps) {
const { cloudflare, extra, myVarInVariables, isWaitUntilDefined } = loaderData
return (
<div>
<h1>Remix and Hono</h1>
<h1>React Router and Hono</h1>
<h2>Var is {cloudflare.env.MY_VAR}</h2>
<h3>
{cloudflare.cf ? 'cf,' : ''}
Expand Down
4 changes: 2 additions & 2 deletions examples/cloudflare-workers/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ test('Should return 200 response - /', async ({ page }) => {
expect(response?.status()).toBe(200)

const headers = response?.headers() ?? {}
expect(headers['x-powered-by']).toBe('Remix and Hono')
expect(headers['x-powered-by']).toBe('React Router and Hono')

const contentH1 = await page.textContent('h1')
expect(contentH1).toBe('Remix and Hono')
expect(contentH1).toBe('React Router and Hono')

const contentH2 = await page.textContent('h2')
expect(contentH2).toBe('Var is My Value')
Expand Down
1 change: 0 additions & 1 deletion examples/cloudflare-workers/load-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ type GetLoadContextArgs = {
}

declare module 'react-router' {
// 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
Expand Down
Binary file modified examples/cloudflare-workers/public/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion examples/cloudflare-workers/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const app = new Hono<{
app.use(async (c, next) => {
c.set('MY_VAR_IN_VARIABLES', 'My variable set in c.set')
await next()
c.header('X-Powered-By', 'Remix and Hono')
c.header('X-Powered-By', 'React Router and Hono')
})

app.get('/api', (c) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/cloudflare-workers/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import adapter from '@hono/vite-dev-server/cloudflare'
import { reactRouter } from '@react-router/dev/vite'
import { cloudflareDevProxy as remixCloudflareDevProxy } from '@react-router/dev/vite/cloudflare'
import serverAdapter from 'hono-remix-adapter/vite'
import serverAdapter from 'hono-react-router-adapter/vite'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { getLoadContext } from './load-context'
Expand Down
Loading

0 comments on commit 6743c53

Please sign in to comment.