Skip to content

Commit

Permalink
Add Vite-based cloudflare-workers template (#9345)
Browse files Browse the repository at this point in the history
  • Loading branch information
markdalgleish committed May 3, 2024
1 parent da96d35 commit b3f75da
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Expand Up @@ -15,3 +15,4 @@ templates/deno

packages/remix-dev/config/defaults
templates/remix-tutorial/app/data.ts
templates/cloudflare-workers/worker-configuration.d.ts
1 change: 1 addition & 0 deletions docs/guides/templates.md
Expand Up @@ -29,6 +29,7 @@ If you want more control over your server or wish to deploy to a non-node runtim

```shellscript nonumber
npx create-remix@latest --template remix-run/remix/templates/cloudflare
npx create-remix@latest --template remix-run/remix/templates/cloudflare-workers
npx create-remix@latest --template remix-run/remix/templates/express
npx create-remix@latest --template remix-run/remix/templates/remix
npx create-remix@latest --template remix-run/remix/templates/remix-javascript
Expand Down
3 changes: 3 additions & 0 deletions docs/guides/vite.md
Expand Up @@ -27,6 +27,9 @@ npx create-remix@latest --template remix-run/remix/templates/express
# Cloudflare:
npx create-remix@latest --template remix-run/remix/templates/cloudflare
# Cloudflare Workers:
npx create-remix@latest --template remix-run/remix/templates/cloudflare-workers
```

These templates include a `vite.config.ts` file which is where the Remix Vite plugin is configured.
Expand Down
83 changes: 83 additions & 0 deletions templates/cloudflare-workers/.eslintrc.cjs
@@ -0,0 +1,83 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/

/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},

// Base config
extends: ["eslint:recommended"],

overrides: [
// React
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: ["react", "jsx-a11y"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
],
settings: {
react: {
version: "detect",
},
formComponents: ["Form"],
linkComponents: [
{ name: "Link", linkAttribute: "to" },
{ name: "NavLink", linkAttribute: "to" },
],
"import/resolver": {
typescript: {},
},
},
},

// Typescript
{
files: ["**/*.{ts,tsx}"],
plugins: ["@typescript-eslint", "import"],
parser: "@typescript-eslint/parser",
settings: {
"import/internal-regex": "^~/",
"import/resolver": {
node: {
extensions: [".ts", ".tsx"],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
},

// Node
{
files: [".eslintrc.cjs"],
env: {
node: true,
},
},
],
};
5 changes: 5 additions & 0 deletions templates/cloudflare-workers/.gitignore
@@ -0,0 +1,5 @@
node_modules

/.wrangler
/build
.env
38 changes: 38 additions & 0 deletions templates/cloudflare-workers/README.md
@@ -0,0 +1,38 @@
# Welcome to Remix + Vite!

📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features.

## Typegen

Generate types for your Cloudflare bindings in `wrangler.toml`:

```sh
pnpm typegen
```

You will need to rerun typegen whenever you make changes to `wrangler.toml`.

## Development

Run the Vite dev server:

```sh
npm run dev
```

To run Wrangler:

```sh
npm run build
npm start
```

## Deployment

If you don't already have an account, then [create a cloudflare account here](https://dash.cloudflare.com/sign-up) and after verifying your email address with Cloudflare, go to your dashboard and set up your free custom Cloudflare Workers subdomain.

Once that's done, you should be able to deploy your app:

```sh
pnpm run deploy
```
18 changes: 18 additions & 0 deletions templates/cloudflare-workers/app/entry.client.tsx
@@ -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>
);
});
43 changes: 43 additions & 0 deletions templates/cloudflare-workers/app/entry.server.tsx
@@ -0,0 +1,43 @@
/**
* 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";

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 body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
// Log streaming rendering errors from inside the shell
console.error(error);
responseStatusCode = 500;
},
}
);

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

responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
29 changes: 29 additions & 0 deletions templates/cloudflare-workers/app/root.tsx
@@ -0,0 +1,29 @@
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} 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" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}
35 changes: 35 additions & 0 deletions templates/cloudflare-workers/app/routes/_index.tsx
@@ -0,0 +1,35 @@
import type { MetaFunction } from "@remix-run/cloudflare";

export const meta: MetaFunction = () => {
return [
{ title: "New Remix App" },
{
name: "description",
content: "Welcome to Remix! Using Vite and Cloudflare Workers!",
},
];
};

export default function Index() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
<h1>Welcome to Remix (with Vite and Cloudflare Workers)</h1>
<ul>
<li>
<a
target="_blank"
href="https://developers.cloudflare.com/workers/"
rel="noreferrer"
>
Cloudflare Workers Docs
</a>
</li>
<li>
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
Remix Docs
</a>
</li>
</ul>
</div>
);
}
4 changes: 4 additions & 0 deletions templates/cloudflare-workers/env.d.ts
@@ -0,0 +1,4 @@
declare module "__STATIC_CONTENT_MANIFEST" {
const manifest: string;
export default manifest;
}
7 changes: 7 additions & 0 deletions templates/cloudflare-workers/load-context.ts
@@ -0,0 +1,7 @@
import { type PlatformProxy } from "wrangler";

declare module "@remix-run/cloudflare" {
interface AppLoadContext {
cloudflare: Omit<PlatformProxy<Env>, "dispose">;
}
}
44 changes: 44 additions & 0 deletions templates/cloudflare-workers/package.json
@@ -0,0 +1,44 @@
{
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build",
"deploy": "wrangler deploy",
"dev": "remix vite:dev",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "wrangler dev ./server.js",
"typegen": "wrangler types",
"typecheck": "tsc"
},
"dependencies": {
"@cloudflare/kv-asset-handler": "^0.1.3",
"@remix-run/cloudflare": "*",
"@remix-run/react": "*",
"@remix-run/server-runtime": "*",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230518.0",
"@remix-run/dev": "*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"typescript": "^5.1.6",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.2.1",
"wrangler": "^3.24.0"
},
"engines": {
"node": ">=18.0.0"
}
}
Binary file added templates/cloudflare-workers/public/favicon.ico
Binary file not shown.
57 changes: 57 additions & 0 deletions templates/cloudflare-workers/server.js
@@ -0,0 +1,57 @@
import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
import { createRequestHandler } from "@remix-run/cloudflare";
import * as remixBuild from "./build/server";
// eslint-disable-next-line import/no-unresolved
import __STATIC_CONTENT_MANIFEST from "__STATIC_CONTENT_MANIFEST";

const MANIFEST = JSON.parse(__STATIC_CONTENT_MANIFEST);
const handleRemixRequest = createRequestHandler(remixBuild);

export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
const ttl = url.pathname.startsWith("/assets/")
? 60 * 60 * 24 * 365 // 1 year
: 60 * 5; // 5 minutes
return await getAssetFromKV(
{
request,
waitUntil: ctx.waitUntil.bind(ctx),
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: MANIFEST,
cacheControl: {
browserTTL: ttl,
edgeTTL: ttl,
},
}
);
} catch (error) {
// No-op
}

try {
const loadContext = {
cloudflare: {
// This object matches the return value from Wrangler's
// `getPlatformProxy` used during development via Remix's
// `cloudflareDevProxyVitePlugin`:
// https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy
cf: request.cf,
ctx: {
waitUntil: ctx.waitUntil,
passThroughOnException: ctx.passThroughOnException,
},
caches,
env,
},
};
return await handleRemixRequest(request, loadContext);
} catch (error) {
console.log(error);
return new Response("An unexpected error occurred", { status: 500 });
}
},
};

0 comments on commit b3f75da

Please sign in to comment.