diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..e33623f
--- /dev/null
+++ b/.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,
+ },
+ },
+ ],
+};
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 4ffeeca..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": ["@remix-run", "prettier"]
-}
diff --git a/LICENSE b/.github/LICENSE
similarity index 97%
rename from LICENSE
rename to .github/LICENSE
index db6f055..262fdda 100644
--- a/LICENSE
+++ b/.github/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2021 Edmund Hung
+Copyright (c) 2024 Edmund Hung
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c13b90f..f6fd1e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
- node-version: 18
+ node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
@@ -21,7 +21,7 @@ jobs:
- name: 📦 Prepare the environment
run: cp .dev.vars.example .dev.vars
- name: 💣 Run some tests
- run: npx playwright test
+ run: npm run test
lint:
name: ⬣ Linting
@@ -32,12 +32,12 @@ jobs:
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
- node-version: 18
+ node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
useLockFile: false
- name: ✨ Code format check
- run: npx prettier --check .
+ run: npm run format -- --check .
- name: ✅ Code linting
- run: npx eslint . --ext .js,.mjs,.ts,.tsx
+ run: npm run lint
diff --git a/.node-version b/.node-version
deleted file mode 100644
index 3c03207..0000000
--- a/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-18
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..2edeafb
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20
\ No newline at end of file
diff --git a/README.md b/README.md
index f1bb0ac..405ed3f 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,12 @@ npx create-remix@latest --template edmundhung/remix-cloudflare-template
What's included?
- Development with [Vite](https://vitejs.dev)
-- [Github Actions](https://github.com/features/actions) for CI/CD
+- Hosting on [Cloudflare Workers](https://developers.cloudflare.com/workers/)
+ with [Static Assets](https://developers.cloudflare.com/workers/static-assets/)
+- [Github Actions](https://github.com/features/actions) for continuous
+ integration
+- Automatic builds and deployments with
+ [Workers Build](https://developers.cloudflare.com/workers/ci-cd/builds/)
- [Markdoc](https://markdoc.dev) for rendering markdown
- Styling with [Tailwind](https://tailwindcss.com/)
- End-to-end testing with [Playwright](https://playwright.dev/)
@@ -86,7 +91,7 @@ You can generate the types of the `env` object based on `wrangler.toml` and
`.dev.vars` with:
```sh
-npx wrangler types
+npm run typegen
```
## Deployment
diff --git a/app/layout.tsx b/app/layout.tsx
index 224b0d8..c1f3abb 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -105,7 +105,7 @@ export function MainNavigation({ menus }: { menus: Menu[] }) {
{menus.map(menu => (
-
+
{menu.title}
diff --git a/functions/[[path]].ts b/functions/[[path]].ts
deleted file mode 100644
index f5a851f..0000000
--- a/functions/[[path]].ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore - the server build file is generated by `remix vite:build`
-// eslint-disable-next-line import/no-unresolved
-import * as build from '../build/server';
-import { getLoadContext } from '../load-context';
-
-export const onRequest = createPagesFunctionHandler({
- build,
- getLoadContext,
-});
diff --git a/load-context.ts b/load-context.ts
index 0b3a9aa..db66282 100644
--- a/load-context.ts
+++ b/load-context.ts
@@ -1,18 +1,9 @@
-import type { PlatformProxy } from 'wrangler';
-
-// You can generate the ENV type based on `wrangler.toml` and `.dev.vars`
-// by running `npm run typegen`
-type Cloudflare = Omit
, 'dispose'>;
-type LoadContext = {
- cloudflare: Cloudflare;
-};
+import { type PlatformProxy } from 'wrangler';
declare module '@remix-run/cloudflare' {
- interface AppLoadContext {
- env: Cloudflare['env'];
- cf: Cloudflare['cf'];
- ctx: Cloudflare['ctx'];
- cache: Cloudflare['caches'];
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
+ interface AppLoadContext extends ReturnType {
+ // This will merge the result of `getLoadContext` into the `AppLoadContext`
}
}
@@ -20,7 +11,16 @@ export function getLoadContext({
context,
}: {
request: Request;
- context: LoadContext;
+ context: {
+ cloudflare: Omit<
+ PlatformProxy,
+ 'dispose' | 'caches'
+ > & {
+ caches:
+ | PlatformProxy['caches']
+ | CacheStorage;
+ };
+ };
}) {
return {
env: context.cloudflare.env,
diff --git a/package.json b/package.json
index b3c8efe..32fa86c 100644
--- a/package.json
+++ b/package.json
@@ -4,53 +4,61 @@
"type": "module",
"description": "All-in-one remix starter template for Cloudflare Pages",
"scripts": {
+ "start": "wrangler dev",
"dev": "remix vite:dev",
"test": "playwright test --ui",
- "start": "wrangler pages dev ./build/client",
+ "deploy": "wrangler deploy",
"build": "remix vite:build",
"cleanup": "rimraf .cache ./build",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+ "format": "prettier --write .",
"typecheck": "wrangler types && tsc",
+ "typegen": "wrangler types",
"prepare": "husky"
},
"dependencies": {
"@markdoc/markdoc": "^0.4.0",
- "@remix-run/cloudflare": "^2.8.1",
- "@remix-run/cloudflare-pages": "^2.8.1",
- "@remix-run/react": "^2.8.1",
- "isbot": "^3.6.5",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "@remix-run/cloudflare": "^2.12.1",
+ "@remix-run/cloudflare-pages": "^2.12.1",
+ "@remix-run/react": "^2.12.1",
+ "isbot": "^4.1.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
},
"devDependencies": {
- "@cloudflare/workers-types": "^4.20240222.0",
- "@octokit/types": "^12.6.0",
- "@playwright/test": "^1.42.1",
- "@remix-run/dev": "^2.8.1",
- "@remix-run/eslint-config": "^2.8.1",
- "@tailwindcss/typography": "^0.5.10",
- "@types/react": "^18.2.64",
- "@types/react-dom": "^18.2.21",
- "autoprefixer": "^10.4.18",
- "concurrently": "^8.2.2",
+ "@cloudflare/workers-types": "^4.20240925.0",
+ "@octokit/types": "^13.6.0",
+ "@playwright/test": "^1.47.2",
+ "@remix-run/dev": "^2.12.1",
+ "@remix-run/eslint-config": "^2.12.1",
+ "@tailwindcss/typography": "^0.5.15",
+ "@types/react": "^18.3.10",
+ "@types/react-dom": "^18.3.0",
+ "autoprefixer": "^10.4.20",
+ "concurrently": "^9.0.1",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
- "husky": "^9.0.11",
- "lint-staged": "^15.2.2",
- "msw": "^2.2.3",
- "postcss": "^8.4.35",
- "prettier": "^3.2.5",
- "prettier-plugin-tailwindcss": "^0.5.12",
+ "eslint-import-resolver-typescript": "^3.6.3",
+ "eslint-plugin-import": "^2.30.0",
+ "eslint-plugin-jsx-a11y": "^6.10.0",
+ "eslint-plugin-react": "^7.37.0",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "husky": "^9.1.6",
+ "lint-staged": "^15.2.10",
+ "msw": "^2.4.9",
+ "postcss": "^8.4.47",
+ "prettier": "^3.3.3",
+ "prettier-plugin-tailwindcss": "^0.6.8",
"rimraf": "^5.0.5",
- "tailwindcss": "^3.4.1",
- "typescript": "^5.4.2",
- "vite": "^5.1.5",
- "vite-tsconfig-paths": "^4.3.1",
- "wrangler": "^3.32.0"
+ "tailwindcss": "^3.4.13",
+ "typescript": "^5.6.2",
+ "vite": "^5.4.8",
+ "vite-tsconfig-paths": "^4.3.2",
+ "wrangler": "^3.78.12"
},
"engines": {
- "node": ">=18"
+ "node": ">=20"
},
"sideEffects": false,
"lint-staged": {
diff --git a/public/_headers b/public/_headers
deleted file mode 100644
index ae0670c..0000000
--- a/public/_headers
+++ /dev/null
@@ -1,2 +0,0 @@
-/assets/*
- Cache-Control: public, max-age=31536000, immutable
diff --git a/public/_routes.json b/public/_routes.json
deleted file mode 100644
index 544b2de..0000000
--- a/public/_routes.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "version": 1,
- "include": ["/*"],
- "exclude": ["/assets/*", "/favicon.ico"]
-}
diff --git a/server.ts b/server.ts
new file mode 100644
index 0000000..caff8ef
--- /dev/null
+++ b/server.ts
@@ -0,0 +1,37 @@
+import { createRequestHandler, type ServerBuild } from '@remix-run/cloudflare';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore This file won’t exist if it hasn’t yet been built
+import * as build from './build/server'; // eslint-disable-line import/no-unresolved
+import { getLoadContext } from './load-context';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const handleRemixRequest = createRequestHandler(build as any as ServerBuild);
+
+export default {
+ async fetch(request, env, ctx) {
+ try {
+ const loadContext = getLoadContext({
+ request,
+ context: {
+ 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.bind(ctx),
+ passThroughOnException: ctx.passThroughOnException.bind(ctx),
+ },
+ caches,
+ env,
+ },
+ },
+ });
+ return await handleRemixRequest(request, loadContext);
+ } catch (error) {
+ console.log(error);
+ return new Response('An unexpected error occurred', { status: 500 });
+ }
+ },
+} satisfies ExportedHandler;
diff --git a/vite.config.ts b/vite.config.ts
index f1923ef..1bdf817 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,15 +1,34 @@
+import { defineConfig } from 'vite';
import {
vitePlugin as remix,
- cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
+ cloudflareDevProxyVitePlugin,
} from '@remix-run/dev';
-import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { getLoadContext } from './load-context';
export default defineConfig({
plugins: [
- remixCloudflareDevProxy({ getLoadContext }),
- remix(),
+ cloudflareDevProxyVitePlugin({
+ getLoadContext,
+ }),
+ remix({
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ },
+ }),
tsconfigPaths(),
],
+ ssr: {
+ resolve: {
+ conditions: ['workerd', 'worker', 'browser'],
+ },
+ },
+ resolve: {
+ mainFields: ['browser', 'module', 'main'],
+ },
+ build: {
+ minify: true,
+ },
});
diff --git a/wrangler.toml b/wrangler.toml
index 24607dd..5fcba90 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -1,8 +1,19 @@
-name = "remix-cloudflare-template"
-compatibility_date = "2024-02-11"
-kv_namespaces = [
- { binding = "cache", id = "cache" }
-]
+#:schema node_modules/wrangler/config-schema.json
+name = "template"
+main = "./server.ts"
+workers_dev = true
+
+# https://developers.cloudflare.com/workers/platform/compatibility-dates
+compatibility_date = "2024-09-26"
+
+kv_namespaces = [{ binding = "cache", id = "cache" }]
+
+[assets]
+# https://developers.cloudflare.com/workers/static-assets/binding/
+directory = "./build/client"
+
+[build]
+command = "npm run build"
[vars]
GITHUB_OWNER = "edmundhung"