From 8d9881c2e784a97ec764e55bbd29dc903f55840e Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Sat, 10 Feb 2024 22:40:37 +0100 Subject: [PATCH] feat: upgrade to remix v2 with vite setup --- .gitignore | 21 ++---- app/entry.client.tsx | 22 ------ app/entry.server.tsx | 34 --------- app/root.tsx | 102 +++++++++++++-------------- app/routes/{index.tsx => _index.tsx} | 26 +++---- app/styles.css | 3 + env.d.ts | 14 ++++ functions/[[path]].ts | 16 +++++ package.json | 60 ++++++++-------- postcss.config.js | 6 ++ public/_headers | 2 + public/_routes.json | 5 ++ remix.config.mjs | 8 --- tsconfig.json | 42 +++++------ vite.config.ts | 26 +++++++ worker.env.d.ts | 21 ------ worker/adapter.ts | 92 ------------------------ worker/index.ts | 31 -------- 18 files changed, 186 insertions(+), 345 deletions(-) delete mode 100644 app/entry.client.tsx delete mode 100644 app/entry.server.tsx rename app/routes/{index.tsx => _index.tsx} (60%) create mode 100644 app/styles.css create mode 100644 env.d.ts create mode 100644 functions/[[path]].ts create mode 100644 postcss.config.js create mode 100644 public/_headers create mode 100644 public/_routes.json delete mode 100644 remix.config.mjs create mode 100644 vite.config.ts delete mode 100644 worker.env.d.ts delete mode 100644 worker/adapter.ts delete mode 100644 worker/index.ts diff --git a/.gitignore b/.gitignore index cef697a..c731f12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,12 @@ node_modules +.env # Remix /.cache /build -/public/build -# Remix stacks -/package-lock.json - -# Custom Build -/dist - -# Tailwind -/app/styles/tailwind.css +# Cloudflare +.wrangler -# Cypress -/cypress/videos -/cypress/screenshots - -# Miniflare -/.mf -/.env +# Remix stacks +/package-lock.json \ No newline at end of file diff --git a/app/entry.client.tsx b/app/entry.client.tsx deleted file mode 100644 index ea51652..0000000 --- a/app/entry.client.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { RemixBrowser } from '@remix-run/react'; -import { startTransition, StrictMode } from 'react'; -import { hydrateRoot } from 'react-dom/client'; - -function hydrate() { - startTransition(() => { - hydrateRoot( - document, - - - - ); - }); -} - -if (window.requestIdleCallback) { - window.requestIdleCallback(hydrate); -} else { - // Safari doesn't support requestIdleCallback - // https://caniuse.com/requestidlecallback - window.setTimeout(hydrate, 1); -} diff --git a/app/entry.server.tsx b/app/entry.server.tsx deleted file mode 100644 index b845f39..0000000 --- a/app/entry.server.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { type 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 -) { - const body = await renderToReadableStream( - , - { - onError: (error) => { - responseStatusCode = 500; - console.error(error); - }, - signal: request.signal, - } - ); - - if (isbot(request.headers.get('User-Agent'))) { - await body.allReady; - } - - const headers = new Headers(responseHeaders); - headers.set('Content-Type', 'text/html'); - - return new Response(body, { - status: responseStatusCode, - headers, - }); -} diff --git a/app/root.tsx b/app/root.tsx index 394bfa9..848b933 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -7,35 +7,30 @@ import * as React from 'react'; import { Link, Links, - LiveReload, Meta, Outlet, Scripts, ScrollRestoration, - useCatch, + isRouteErrorResponse, + useRouteError, } from '@remix-run/react'; +import stylesUrl from '~/styles.css?url'; -import stylesUrl from '~/styles/tailwind.css'; - -/** - * The `links` export is a function that returns an array of objects that map to - * the attributes for an HTML `` element. These will load `` tags on - * every route in the app, but individual routes can include their own links - * that are automatically unloaded when a user navigates away from the route. - * - * https://remix.run/api/app#links - */ -export let links: LinksFunction = () => { +export const links: LinksFunction = () => { return [{ rel: 'stylesheet', href: stylesUrl }]; }; -export let meta: MetaFunction = () => { - return { - viewport: 'width=device-width, initial-scale=1', - }; +export const meta: MetaFunction = () => { + return [ + { + charset: 'utf-8', + // title: 'Conform Playground', + viewport: 'width=device-width,initial-scale=1', + }, + ]; }; -export let loader: LoaderFunction = async () => { +export const loader: LoaderFunction = async () => { return { date: new Date() }; }; @@ -68,7 +63,6 @@ function Document({ {children} - {process.env.NODE_ENV === 'development' && } ); @@ -98,49 +92,51 @@ function Layout({ children }: React.PropsWithChildren<{}>) { ); } -export function CatchBoundary() { - let caught = useCatch(); +export function ErrorBoundary() { + const error = useRouteError(); - let message; - switch (caught.status) { - case 401: - message = ( -

- Oops! Looks like you tried to visit a page that you do not have access - to. -

- ); - break; - case 404: - message = ( -

Oops! Looks like you tried to visit a page that does not exist.

- ); - break; + // Log the error to the console + console.error(error); - default: - throw new Error(caught.data || caught.statusText); - } + if (isRouteErrorResponse(error)) { + let message; + switch (error.status) { + case 401: + message = ( +

+ Oops! Looks like you tried to visit a page that you do not have + access to. +

+ ); + break; + case 404: + message = ( +

Oops! Looks like you tried to visit a page that does not exist.

+ ); + break; - return ( - - -

- {caught.status}: {caught.statusText} -

- {message} -
-
- ); -} + default: + throw new Error(error.data || error.statusText); + } + + return ( + + +

+ {error.status}: {error.statusText} +

+ {message} +
+
+ ); + } -export function ErrorBoundary({ error }: { error: Error }) { - console.error(error); return (

There was an error

-

{error.message}

+

{`${error}`}


Hey, developer, you should replace this with what you want your diff --git a/app/routes/index.tsx b/app/routes/_index.tsx similarity index 60% rename from app/routes/index.tsx rename to app/routes/_index.tsx index 396858a..42ac1ea 100644 --- a/app/routes/index.tsx +++ b/app/routes/_index.tsx @@ -1,29 +1,21 @@ -import type { - MetaFunction, - LinksFunction, - LoaderFunction, -} from '@remix-run/cloudflare'; +import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/cloudflare'; import { useLoaderData } from '@remix-run/react'; -export let meta: MetaFunction = () => { - return { - title: 'remix-worker-template', - description: 'All-in-one remix starter template for Cloudflare Workers', - }; +export const meta: MetaFunction = () => { + return [ + { title: 'remix-worker-template' }, + { description: 'All-in-one remix starter template for Cloudflare Workers' }, + ]; }; -export let links: LinksFunction = () => { - return []; -}; - -export let loader: LoaderFunction = async ({ request }) => { +export function loader({ request }: LoaderFunctionArgs) { return { title: 'remix-worker-template', }; -}; +} export default function Index() { - let { title } = useLoaderData(); + const { title } = useLoaderData(); return (

diff --git a/app/styles.css b/app/styles.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/app/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..009c0ed --- /dev/null +++ b/env.d.ts @@ -0,0 +1,14 @@ +import 'vite/client'; +import '@remix-run/cloudflare'; +import '@cloudflare/workers-types'; + +interface Env { + ENVIRONMENT?: 'development'; +} + +declare module '@remix-run/cloudflare' { + export interface AppLoadContext { + env: Env; + waitUntil(promise: Promise): void; + } +} diff --git a/functions/[[path]].ts b/functions/[[path]].ts new file mode 100644 index 0000000..d7db3b9 --- /dev/null +++ b/functions/[[path]].ts @@ -0,0 +1,16 @@ +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'; + +export const onRequest = createPagesFunctionHandler({ + build, + getLoadContext: (context) => ({ + env: context.env, + waitUntil(promise: Promise) { + context.waitUntil(promise); + }, + }), +}); diff --git a/package.json b/package.json index 3491e3c..6b00570 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,52 @@ { "private": true, "name": "remix-worker-template", - "description": "All-in-one remix starter template for Cloudflare Workers", + "description": "All-in-one remix starter template for Cloudflare Pages", "module": "./dist/worker.mjs", "scripts": { - "cleanup": "rimraf .cache ./dist ./build ./public/build ./app/styles/tailwind.css", - "build:remix": "remix build", - "build:style": "cross-env NODE_ENV=production tailwindcss -o ./app/styles/tailwind.css --minify", - "build:worker": "cross-env NODE_ENV=production node ./scripts/build.mjs", - "build": "npm run build:style && npm run build:remix && npm run build:worker", - "dev:miniflare": "miniflare --modules --build-command \"node ./scripts/build.mjs\" --build-watch-path ./worker --build-watch-path ./build/index.js --no-cache --watch --open", - "dev:remix": "remix watch", - "dev:style": "tailwindcss -o ./app/styles/tailwind.css --watch", - "dev": "concurrently \"npm:dev:*\"", - "prebuild": "npm run cleanup", + "dev": "remix vite:dev", "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" playwright test", - "start": "miniflare --modules", + "start": "wrangler pages dev ./build/client", + "build": "remix vite:build", + "cleanup": "rimraf .cache ./build ./public/build", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "typecheck": "tsc", "prepare": "husky install" }, "dependencies": { - "@cloudflare/kv-asset-handler": "^0.2.0", - "@remix-run/cloudflare-workers": "*", + "@remix-run/cloudflare": "*", + "@remix-run/cloudflare-pages": "*", "@remix-run/react": "*", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@cloudflare/workers-types": "^3.18.0", - "@playwright/test": "^1.28.1", + "@cloudflare/workers-types": "^4.20240208.0", + "@playwright/test": "^1.41.2", "@remix-run/dev": "*", "@remix-run/eslint-config": "*", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", - "concurrently": "^7.6.0", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "autoprefixer": "^10.4.17", + "concurrently": "^8.2.2", "cross-env": "^7.0.3", - "esbuild": "^0.15.15", - "eslint": "^8.28.0", - "eslint-config-prettier": "^8.5.0", - "husky": "^8.0.2", - "lint-staged": "^13.0.4", - "miniflare": "^2.11.0", - "prettier": "^2.8.0", - "rimraf": "^3.0.2", - "tailwindcss": "^3.2.4", - "typescript": "^4.9.3", - "wrangler": "^2.4.4" + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "husky": "^9.0.10", + "lint-staged": "^15.2.2", + "miniflare": "^3.20240129.1", + "postcss": "^8.4.35", + "prettier": "^3.2.5", + "rimraf": "^5.0.5", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.1.1", + "vite-tsconfig-paths": "^4.3.1", + "wrangler": "^3.28.1" }, "engines": { - "node": ">=16.7" + "node": ">=18" }, "sideEffects": false, "lint-staged": { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..ae0670c --- /dev/null +++ b/public/_headers @@ -0,0 +1,2 @@ +/assets/* + Cache-Control: public, max-age=31536000, immutable diff --git a/public/_routes.json b/public/_routes.json new file mode 100644 index 0000000..544b2de --- /dev/null +++ b/public/_routes.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "include": ["/*"], + "exclude": ["/assets/*", "/favicon.ico"] +} diff --git a/remix.config.mjs b/remix.config.mjs deleted file mode 100644 index 31a0e26..0000000 --- a/remix.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @type {import('@remix-run/dev').AppConfig} - */ -export default { - serverModuleFormat: 'esm', - devServerBroadcastDelay: 1000, - ignoredRouteFiles: ['.*'], -}; diff --git a/tsconfig.json b/tsconfig.json index 4c087a2..5de314c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,24 @@ { - "include": ["worker.env.d.ts", "**/*.ts", "**/*.tsx"], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "esModuleInterop": true, - "resolveJsonModule": true, - "jsx": "react-jsx", - "module": "CommonJS", - "moduleResolution": "node", - "target": "ESNext", - "strict": true, - "types": ["@cloudflare/workers-types"], - "paths": { - "~/*": ["./app/*"] - }, - "noEmit": true, - "baseUrl": ".", - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true - } + "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..68aa6c4 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,26 @@ +import { + unstable_vitePlugin as remix, + unstable_cloudflarePreset as cloudflare, +} from '@remix-run/dev'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [ + remix({ + presets: [ + cloudflare({ + getRemixDevLoadContext(context) { + return context; + }, + }), + ], + }), + tsconfigPaths(), + ], + ssr: { + resolve: { + externalConditions: ['workerd', 'worker'], + }, + }, +}); diff --git a/worker.env.d.ts b/worker.env.d.ts deleted file mode 100644 index c230421..0000000 --- a/worker.env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/// -/// -/// - -// Required by the worker adapter -declare module '__STATIC_CONTENT_MANIFEST' { - const value: string; - export default value; -} - -interface Env { - // Required by the worker adapter - __STATIC_CONTENT: string; -} - -declare module '@remix-run/server-runtime' { - export interface AppLoadContext { - env: Env; - ctx: ExecutionContext; - } -} diff --git a/worker/adapter.ts b/worker/adapter.ts deleted file mode 100644 index 6777a82..0000000 --- a/worker/adapter.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { AppLoadContext } from '@remix-run/cloudflare'; -import { createRequestHandler as createRemixRequestHandler } from '@remix-run/cloudflare'; -import { - getAssetFromKV, - MethodNotAllowedError, - NotFoundError, -} from '@cloudflare/kv-asset-handler'; - -import manifest from '__STATIC_CONTENT_MANIFEST'; - -const assetManifest = JSON.parse(manifest); - -export interface GetLoadContextFunction { - (request: Request, env: Env, ctx: ExecutionContext): AppLoadContext; -} - -export type RequestHandler = ReturnType; - -export function createRequestHandler({ - /** - * Remix build files - */ - build, - - /** - * Optional: Context to be available on `loader` or `action`, default to `undefined` if not defined - * @param request Request - * @param env Variables defined for the environment - * @param ctx Exectuion context, i.e. ctx.waitUntil() or ctx.passThroughOnException(); - * @returns Context - */ - getLoadContext, -}: { - build: any; // ServerBuild - getLoadContext?: GetLoadContextFunction; -}): ExportedHandlerFetchHandler { - let handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV); - - return (request: Request, env: Env, ctx: ExecutionContext) => { - let loadContext = - typeof getLoadContext === 'function' - ? getLoadContext(request, env, ctx) - : undefined; - - return handleRequest(request, loadContext); - }; -} - -export async function handleAsset( - request: Request, - env: Env, - ctx: ExecutionContext -): Promise { - try { - return await getAssetFromKV( - { - request, - waitUntil(promise: Promise) { - return ctx.waitUntil(promise); - }, - }, - { - cacheControl(request) { - const url = new URL(request.url); - - if (url.pathname.startsWith('/build')) { - return { - browserTTL: 60 * 60 * 24 * 365, - edgeTTL: 60 * 60 * 24 * 365, - }; - } - - return { - browserTTL: 60 * 10, - edgeTTL: 60 * 10, - }; - }, - ASSET_NAMESPACE: env.__STATIC_CONTENT, - ASSET_MANIFEST: assetManifest, - } - ); - } catch (error) { - if ( - error instanceof MethodNotAllowedError || - error instanceof NotFoundError - ) { - return new Response('Not Found', { status: 404 }); - } - - throw error; - } -} diff --git a/worker/index.ts b/worker/index.ts deleted file mode 100644 index 985bc7a..0000000 --- a/worker/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as build from '../build/index.js'; -import { createRequestHandler, handleAsset } from './adapter'; - -const handleRequest = createRequestHandler({ - build, - getLoadContext(request, env, ctx) { - return { env, ctx }; - }, -}); - -const worker: ExportedHandler = { - async fetch(request, env, ctx): Promise { - try { - let response = await handleAsset(request, env, ctx); - - if (response.status === 404) { - response = await handleRequest(request, env, ctx); - } - - return response; - } catch (exception) { - if (process.env.NODE_ENV === 'development') { - return new Response(`${exception}`, { status: 500 }); - } - - return new Response('Internal Server Error', { status: 500 }); - } - }, -}; - -export default worker;