Skip to content

Commit 5120f90

Browse files
committed
add example for node
1 parent 91acc5a commit 5120f90

File tree

13 files changed

+907
-189
lines changed

13 files changed

+907
-189
lines changed

examples/node/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env

examples/node/app/entry.client.tsx

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

examples/node/app/entry.server.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 { PassThrough } from "node:stream";
8+
9+
import type { AppLoadContext, EntryContext } from "@remix-run/node";
10+
import { createReadableStreamFromReadable } from "@remix-run/node";
11+
import { RemixServer } from "@remix-run/react";
12+
import { isbot } from "isbot";
13+
import { renderToPipeableStream } from "react-dom/server";
14+
15+
const ABORT_DELAY = 5_000;
16+
17+
export default function handleRequest(
18+
request: Request,
19+
responseStatusCode: number,
20+
responseHeaders: Headers,
21+
remixContext: EntryContext,
22+
// This is ignored so we can keep it in the template for visibility. Feel
23+
// free to delete this parameter in your app if you're not using it!
24+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
25+
loadContext: AppLoadContext
26+
) {
27+
return isbot(request.headers.get("user-agent") || "")
28+
? handleBotRequest(
29+
request,
30+
responseStatusCode,
31+
responseHeaders,
32+
remixContext
33+
)
34+
: handleBrowserRequest(
35+
request,
36+
responseStatusCode,
37+
responseHeaders,
38+
remixContext
39+
);
40+
}
41+
42+
function handleBotRequest(
43+
request: Request,
44+
responseStatusCode: number,
45+
responseHeaders: Headers,
46+
remixContext: EntryContext
47+
) {
48+
return new Promise((resolve, reject) => {
49+
let shellRendered = false;
50+
const { pipe, abort } = renderToPipeableStream(
51+
<RemixServer
52+
context={remixContext}
53+
url={request.url}
54+
abortDelay={ABORT_DELAY}
55+
/>,
56+
{
57+
onAllReady() {
58+
shellRendered = true;
59+
const body = new PassThrough();
60+
const stream = createReadableStreamFromReadable(body);
61+
62+
responseHeaders.set("Content-Type", "text/html");
63+
64+
resolve(
65+
new Response(stream, {
66+
headers: responseHeaders,
67+
status: responseStatusCode,
68+
})
69+
);
70+
71+
pipe(body);
72+
},
73+
onShellError(error: unknown) {
74+
reject(error);
75+
},
76+
onError(error: unknown) {
77+
responseStatusCode = 500;
78+
// Log streaming rendering errors from inside the shell. Don't log
79+
// errors encountered during initial shell rendering since they'll
80+
// reject and get logged in handleDocumentRequest.
81+
if (shellRendered) {
82+
console.error(error);
83+
}
84+
},
85+
}
86+
);
87+
88+
setTimeout(abort, ABORT_DELAY);
89+
});
90+
}
91+
92+
function handleBrowserRequest(
93+
request: Request,
94+
responseStatusCode: number,
95+
responseHeaders: Headers,
96+
remixContext: EntryContext
97+
) {
98+
return new Promise((resolve, reject) => {
99+
let shellRendered = false;
100+
const { pipe, abort } = renderToPipeableStream(
101+
<RemixServer
102+
context={remixContext}
103+
url={request.url}
104+
abortDelay={ABORT_DELAY}
105+
/>,
106+
{
107+
onShellReady() {
108+
shellRendered = true;
109+
const body = new PassThrough();
110+
const stream = createReadableStreamFromReadable(body);
111+
112+
responseHeaders.set("Content-Type", "text/html");
113+
114+
resolve(
115+
new Response(stream, {
116+
headers: responseHeaders,
117+
status: responseStatusCode,
118+
})
119+
);
120+
121+
pipe(body);
122+
},
123+
onShellError(error: unknown) {
124+
reject(error);
125+
},
126+
onError(error: unknown) {
127+
responseStatusCode = 500;
128+
// Log streaming rendering errors from inside the shell. Don't log
129+
// errors encountered during initial shell rendering since they'll
130+
// reject and get logged in handleDocumentRequest.
131+
if (shellRendered) {
132+
console.error(error);
133+
}
134+
},
135+
}
136+
);
137+
138+
setTimeout(abort, ABORT_DELAY);
139+
});
140+
}

examples/node/app/root.tsx

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Index() {
2+
return (
3+
<div>
4+
<h1>Remix and Hono</h1>
5+
</div>
6+
)
7+
}

examples/node/main.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// main.ts
2+
import { serve } from '@hono/node-server'
3+
import { serveStatic } from '@hono/node-server/serve-static'
4+
import handle from 'hono-remix-adapter/node'
5+
import * as build from './build/server'
6+
import server from './server'
7+
8+
server.use(
9+
serveStatic({
10+
root: './build/client',
11+
})
12+
)
13+
14+
serve(handle(build, server))

examples/node/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "example-node",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"build": "remix vite:build",
8+
"dev": "remix vite:dev",
9+
"start": "remix-serve ./build/server/index.js",
10+
"start-with-adapter": "tsx main.ts",
11+
"typecheck": "tsc"
12+
},
13+
"dependencies": {
14+
"@hono/node-server": "^1.13.7",
15+
"@remix-run/node": "^2.14.0",
16+
"@remix-run/react": "^2.14.0",
17+
"@remix-run/serve": "^2.14.0",
18+
"hono": "^4.6.11",
19+
"isbot": "^4.1.0",
20+
"react": "^18.2.0",
21+
"react-dom": "^18.2.0"
22+
},
23+
"devDependencies": {
24+
"@remix-run/dev": "^2.14.0",
25+
"@types/react": "^18.2.20",
26+
"@types/react-dom": "^18.2.7",
27+
"autoprefixer": "^10.4.19",
28+
"tsx": "^4.19.2",
29+
"typescript": "^5.1.6",
30+
"vite": "^5.1.0",
31+
"vite-tsconfig-paths": "^4.2.1"
32+
},
33+
"engines": {
34+
"node": ">=20.0.0"
35+
}
36+
}

examples/node/public/favicon.ico

16.6 KB
Binary file not shown.

examples/node/server/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// server/index.ts
2+
import { Hono } from 'hono'
3+
4+
const app = new Hono()
5+
6+
app.use(async (c, next) => {
7+
await next()
8+
c.header('X-Powered-By', 'Remix and Hono')
9+
})
10+
11+
app.get('/api', (c) => {
12+
return c.json({
13+
message: 'Hello',
14+
})
15+
})
16+
17+
export default app

examples/node/tsconfig.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"include": [
3+
"**/*.ts",
4+
"**/*.tsx",
5+
"**/.server/**/*.ts",
6+
"**/.server/**/*.tsx",
7+
"**/.client/**/*.ts",
8+
"**/.client/**/*.tsx"
9+
],
10+
"compilerOptions": {
11+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
12+
"types": ["@remix-run/node", "vite/client"],
13+
"isolatedModules": true,
14+
"esModuleInterop": true,
15+
"jsx": "react-jsx",
16+
"module": "ESNext",
17+
"moduleResolution": "Bundler",
18+
"resolveJsonModule": true,
19+
"target": "ES2022",
20+
"strict": true,
21+
"allowJs": true,
22+
"skipLibCheck": true,
23+
"forceConsistentCasingInFileNames": true,
24+
"baseUrl": ".",
25+
"paths": {
26+
"~/*": ["./app/*"]
27+
},
28+
29+
// Vite takes care of building everything, not tsc.
30+
"noEmit": true
31+
}
32+
}

0 commit comments

Comments
 (0)