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"