Skip to content

Commit 822f039

Browse files
committed
feat(web): cf deployment setup
1 parent c36acbd commit 822f039

File tree

17 files changed

+1247
-283
lines changed

17 files changed

+1247
-283
lines changed

.github/workflows/deploy-web.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Deploy
2+
3+
on:
4+
push:
5+
branches:
6+
- dev
7+
- 'feat/nav-bar'
8+
9+
permissions:
10+
contents: read
11+
deployments: write
12+
13+
jobs:
14+
deploy:
15+
runs-on: ubuntu-latest
16+
name: Deploy
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: ./.github/actions/provision
20+
21+
- name: Build web app
22+
run: pnpm turbo run build --filter="@leather.io/web"
23+
24+
- run: ls -R apps/web/build
25+
26+
- name: Deploy
27+
uses: cloudflare/wrangler-action@v3
28+
with:
29+
workingDirectory: apps/web
30+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
31+
accountId: ${{ secrets.CLOUDFLARE_LEATHER_ACCOUNT_ID }}
32+
gitHubToken: ${{ secrets.LEATHER_BOT }}

apps/web/app/entry.server.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { renderToReadableStream } from 'react-dom/server';
2+
import { type AppLoadContext, type EntryContext, ServerRouter } from 'react-router';
3+
4+
import { isbot } from 'isbot';
5+
6+
export default async function handleRequest(
7+
request: Request,
8+
responseStatusCode: number,
9+
responseHeaders: Headers,
10+
routerContext: EntryContext,
11+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
12+
_loadContext: AppLoadContext
13+
) {
14+
let shellRendered = false;
15+
const userAgent = request.headers.get('user-agent');
16+
17+
const body = await renderToReadableStream(
18+
<ServerRouter context={routerContext} url={request.url} />,
19+
{
20+
onError(error: unknown) {
21+
responseStatusCode = 500;
22+
// Log streaming rendering errors from inside the shell. Don't log
23+
// errors encountered during initial shell rendering since they'll
24+
// reject and get logged in handleDocumentRequest.
25+
if (shellRendered) {
26+
// eslint-disable-next-line no-console
27+
console.error(error);
28+
}
29+
},
30+
}
31+
);
32+
shellRendered = true;
33+
34+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
35+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
36+
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
37+
await body.allReady;
38+
}
39+
40+
responseHeaders.set('Content-Type', 'text/html');
41+
return new Response(body, {
42+
headers: responseHeaders,
43+
status: responseStatusCode,
44+
});
45+
}

apps/web/app/features/home/home.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function Home({ latestArticles }: HomeProps) {
2525
<Button
2626
onClick={async () => {
2727
const result = await leather.getAddresses();
28+
// eslint-disable-next-line no-console
2829
console.log(result);
2930
}}
3031
>

apps/web/package.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,40 @@
33
"type": "module",
44
"scripts": {
55
"build": "react-router build",
6-
"dev": "react-router dev",
76
"deploy": "pnpm build && wrangler deploy",
7+
"dev": "react-router dev",
88
"prepare": "panda codegen",
99
"start": "react-router-serve ./build/server/index.js",
1010
"typecheck": "react-router typegen && tsc"
1111
},
1212
"dependencies": {
1313
"@leather.io/panda-preset": "workspace:*",
14+
"@leather.io/sdk": "workspace:*",
1415
"@leather.io/ui": "workspace:*",
1516
"@leather.io/utils": "workspace:*",
1617
"@react-router/node": "7.2.0",
1718
"@react-router/serve": "7.2.0",
1819
"isbot": "5.1.17",
1920
"react": "19.0.0",
2021
"react-dom": "19.0.0",
21-
"react-router": "7.2.0"
22+
"react-router": "7.2.0",
23+
"react-router-dom": "7.2.0"
2224
},
2325
"devDependencies": {
26+
"@cloudflare/workers-types": "4.20250303.0",
2427
"@originjs/vite-plugin-commonjs": "1.0.3",
25-
"@pandacss/dev": "0.53.0",
28+
"@pandacss/dev": "0.53.1",
2629
"@playwright/test": "1.50.1",
30+
"@react-router/cloudflare": "7.2.0",
2731
"@react-router/dev": "7.2.0",
2832
"@types/node": "22.13.5",
2933
"@types/react": "19.0.10",
3034
"@types/react-dom": "19.0.4",
31-
"react-router-devtools": "1.1.5",
35+
"react-router-devtools": "1.1.6",
3236
"typescript": "5.7.3",
3337
"vite": "6.2.0",
34-
"vite-plugin-static-copy": "2.2.0",
35-
"vite-tsconfig-paths": "5.1.4"
38+
"vite-plugin-static-copy": "2.3.0",
39+
"vite-tsconfig-paths": "5.1.4",
40+
"wrangler": "3.114.0"
3641
}
3742
}

apps/web/tsconfig.cloudflare.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": [
4+
".react-router/types/**/*",
5+
"app/**/*",
6+
"app/**/.server/**/*",
7+
"app/**/.client/**/*",
8+
"workers/**/*",
9+
"worker-configuration.d.ts",
10+
"./leather-styles/**/*"
11+
],
12+
"compilerOptions": {
13+
"composite": true,
14+
"strict": true,
15+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
16+
"types": ["@cloudflare/workers-types", "vite/client"],
17+
"target": "ES2022",
18+
"module": "ES2022",
19+
"moduleResolution": "bundler",
20+
"jsx": "react-jsx",
21+
"baseUrl": ".",
22+
"rootDirs": [".", "./.react-router/types"],
23+
"paths": {
24+
"~/*": ["./app/*"],
25+
"react": ["./node_modules/@types/react"]
26+
},
27+
"esModuleInterop": true,
28+
"resolveJsonModule": true
29+
}
30+
}

apps/web/tsconfig.json

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
11
{
2-
"extends": "../../packages/tsconfig-config/tsconfig.base",
3-
"include": [
4-
"app/**/*",
5-
"app/**/.server/**/*",
6-
"app/**/.client/**/*",
7-
".react-router/types/**/*",
8-
"./leather-styles/**/*"
2+
"files": [],
3+
"references": [
4+
{
5+
"path": "./tsconfig.node.json"
6+
},
7+
{ "path": "./tsconfig.cloudflare.json" }
98
],
10-
"exclude": ["node_modules/@leather.io/ui/dist-native"],
119
"compilerOptions": {
12-
"types": ["node", "vite/client"],
13-
"jsx": "react-jsx",
14-
"rootDirs": [".", "./.react-router/types"],
15-
"baseUrl": ".",
16-
"paths": {
17-
"~/*": ["app/*"],
18-
"react": ["./node_modules/@types/react"]
19-
},
10+
"checkJs": true,
2011
"verbatimModuleSyntax": true,
21-
"resolveJsonModule": true,
22-
"noUncheckedIndexedAccess": true,
23-
"exactOptionalPropertyTypes": true
12+
"skipLibCheck": true,
13+
"strict": true,
14+
"noEmit": true
2415
}
2516
}

apps/web/tsconfig.node.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["vite.config.ts", "panda.config.ts", "playwright.config.ts"],
4+
"compilerOptions": {
5+
"composite": true,
6+
"strict": true,
7+
"types": ["node"],
8+
"lib": ["ES2022"],
9+
"target": "ES2022",
10+
"module": "ES2022",
11+
"moduleResolution": "bundler"
12+
}
13+
}

apps/web/worker-configuration.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Generated by Wrangler by running `wrangler types`
2+
3+
interface Env {
4+
VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare";
5+
}

apps/web/workers/app.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createRequestHandler } from 'react-router';
2+
3+
declare global {
4+
type CloudflareEnvironment = Env;
5+
}
6+
7+
declare module 'react-router' {
8+
export interface AppLoadContext {
9+
cloudflare: {
10+
env: CloudflareEnvironment;
11+
ctx: ExecutionContext;
12+
};
13+
}
14+
}
15+
16+
const requestHandler = createRequestHandler(
17+
// @ts-expect-error - virtual module provided by React Router at build time
18+
() => import('virtual:react-router/server-build'),
19+
import.meta.env.MODE
20+
);
21+
22+
export default {
23+
async fetch(request, env, ctx) {
24+
//
25+
// Demo implementation of a proxy
26+
if (request.url.includes('/blog')) {
27+
const url = new URL(request.url);
28+
url.hostname = 'leather.io';
29+
url.pathname = '/blog' + url.pathname.replace('/blog', '');
30+
31+
const modifiedRequest = new Request(url, request);
32+
const resp = await fetch(modifiedRequest);
33+
return new Response(resp.body, resp);
34+
}
35+
//
36+
// End demo
37+
return requestHandler(request, {
38+
cloudflare: { env, ctx },
39+
});
40+
},
41+
} satisfies ExportedHandler<CloudflareEnvironment>;

apps/web/wrangler.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
workers_dev = true
2+
name = "leather-web"
3+
compatibility_date = "2024-11-18"
4+
main = "./build/server/index.js"
5+
assets = { directory = "./build/client/" }
6+
send_metrics = false
7+
8+
[vars]
9+
VALUE_FROM_CLOUDFLARE = "Hello from Cloudflare"
10+
11+
[observability]
12+
enabled = true
13+
head_sampling_rate = 1

0 commit comments

Comments
 (0)