diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..91670c1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +/.cache +/.github +/.husky +/build +/public +/node_modules +/dist +/app/styles/tailwind.css diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4ffeeca --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@remix-run", "prettier"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4cd5de9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI +on: + - push +jobs: + test: + name: 🔍 Testing + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: 📥 Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + - name: 🎭 Install Playwright + run: npx playwright install --with-deps + - name: 📦 Prepare the environment + run: cp wrangler.toml.example wrangler.toml + - name: 💣 Run some tests + run: npx playwright test + + lint: + name: ⬣ Linting + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: 📥 Download deps + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + - name: ✨ Code format check + run: npx prettier --check . + - name: ✅ Code linting + run: npx eslint . --ext .js,.mjs,.ts,.tsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2565be0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +node_modules +.env + +# Remix +/.cache +/build + +# Cloudflare +.wrangler +wrangler.toml + +# Playwright +/test-results +/playwright-report + +# Remix stacks +/package-lock.json \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..3c03207 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..91670c1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +/.cache +/.github +/.husky +/build +/public +/node_modules +/dist +/app/styles/tailwind.css diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..e554965 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,32 @@ +/** @type {import("prettier").Config} */ +const config = { + arrowParens: 'avoid', + bracketSameLine: false, + bracketSpacing: true, + embeddedLanguageFormatting: 'auto', + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxSingleQuote: false, + printWidth: 80, + proseWrap: 'always', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleAttributePerLine: false, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: true, + overrides: [ + { + files: ['**/*.json', '**/*.md'], + options: { + useTabs: false, + }, + }, + ], + plugins: ['prettier-plugin-tailwindcss'], +}; + +export default config; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db6f055 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..afd4cef --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# remix-cloudflare-template + +Learn more about [Remix Stacks](https://remix.run/stacks). + +``` +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 +- [Markdoc](https://markdoc.dev) for rendering markdown +- Styling with [Tailwind](https://tailwindcss.com/) +- End-to-end testing with [Playwright](playwright.dev/) +- Local third party request mocking with [MSW](https://mswjs.io/) +- Code formatting with [Prettier](https://prettier.io) +- Linting with [ESLint](https://eslint.org) +- Static Types with [TypeScript](https://typescriptlang.org) + +## Development + +Before start, copy `wrangler.toml.example` and name it `wrangler.toml`. This +file will be used to configure the development environment and define all the +environment variables that you need for your application. + +```sh +cp wrangler.toml.example wrangler.toml +``` + +To starts the vite dev server: + +```sh +npm run dev +``` + +You can also start the Playwright UI mode to test your application. You will +find all the tests defined in the [/tests/e2e](./tests/e2e/) directory. + +```sh +npm run test +``` + +To test your application on the workerd runtime, you can start the wrangler dev +server with: + +```sh +npm run build && npm run start +``` + +### New environment variable + +To add a new environment variable, you can update the **var** section on the +[wrangler.toml](./wrangler.toml) file with the new variable: + +```toml +[vars] +NEW_VARIABLE = "..." +``` + +The variable will be available from the `env` object in the context. + +### Setup a KV Namespace + +To setup a new KV namespace on the **development environment**, update +[wrangler.toml](./wrangler.toml) with another object similar to the cache +namespace as shown below: + +```toml +kv_namespaces = [ + { binding = "cache", id = "cache" }, + { binding = "new_namespace", id = "new_namespace" } +] +``` + +Note that the `id` has no effect on the dev environment. You can use the same +name for both `id` and `binding`. The namespace will be available form the `env` +object in the context. + +### Improving types + +You can improve the types of the `env` object by updating +[env.d.ts](./env.d.ts). + +## Deployment + +Before your first deployment, make sure all the environment variables and +bindings are set properly on the +[Cloudlfare Dashboard](https://dash.cloudflare.com/login). + +### Creating a new application + +To create a new application on the Cloudflare Dashboard, select **Workers and +Pages** from the menu and click on **Create Application**. You can then follow +the instructions based on your needs. + +### Setting up environment variables + +To set up environment variables, select **Workers and Pages** from the menu and +look for the application details. You will find the **environment variables** +section under the **Settings** tab. + +### Setting up KV namespaces + +To set up a new KV namespaces, you need to create a new namespace first through +the **KV** menu under **Workers and Pages** and click **Create a namespace**. + +After creating the namespace, you can bind the namespace to the application from +the application details page. You can find the setting from the **Functions** +section under the **Settings** tab. + +### Debugging + +If your application is not working properly, you can find the real-time logs in +the **Functions** tab from the deployment details page. diff --git a/app/components.tsx b/app/components.tsx new file mode 100644 index 0000000..3de3d41 --- /dev/null +++ b/app/components.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import markdoc, { type RenderableTreeNodes } from '@markdoc/markdoc'; + +export function RemixLogo(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + Remix Logo + + + + + + + ); +} + +export function Markdown({ content }: { content: RenderableTreeNodes }) { + return
{markdoc.renderers.react(content, React)}
; +} diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 0000000..0832532 --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,175 @@ +import type { MetaFunction } from '@remix-run/cloudflare'; +import * as React from 'react'; +import { + Link, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + isRouteErrorResponse, + json, + useLoaderData, + useRouteError, +} from '@remix-run/react'; +import '~/styles.css'; +import { metadata } from './services/github.server'; +import { RemixLogo } from './components'; + +// We will rollback to loading CSS through links when `.css?url` is supported +// export const links: LinksFunction = () => { +// return [{ rel: 'stylesheet', href: stylesUrl }]; +// }; + +export const meta: MetaFunction = () => { + return [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'remix-cloudlfare-template' }, + ]; +}; + +export function loader() { + return json({ + repo: metadata.repo, + owner: metadata.owner, + description: '📜 All-in-one remix starter template for Cloudflare Pages', + }); +} + +export default function App() { + const { repo, owner, description } = useLoaderData(); + return ( + + + + + + ); +} + +function Document({ + children, + title, +}: { + children: React.ReactNode; + title?: string; +}) { + return ( + + + + {title ? {title} : null} + + + + + {children} + + + + + ); +} + +function Layout({ + children, + title, + description, + actionText, + actionLink, +}: { + children?: React.ReactNode; + title?: string; + description?: string; + actionText?: string; + actionLink?: string; +}) { + return ( +
+
+
+
+
+
+ + + +
+
+

{title}

+

{description}

+ {actionText ? ( + + {actionText} + + ) : null} +
+ +
+
+
+ {children ? ( +
+
{children}
+
+ ) : null} +
+
+ ); +} + +export function ErrorBoundary() { + const error = useRouteError(); + + // Log the error to the console + console.error(error); + + if (isRouteErrorResponse(error)) { + const title = `${error.status} ${error.statusText}`; + + 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; + default: + message = JSON.stringify(error.data, null, 2); + break; + } + + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx new file mode 100644 index 0000000..f0e477e --- /dev/null +++ b/app/routes/_index.tsx @@ -0,0 +1,26 @@ +import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; +import { json, useLoaderData } from '@remix-run/react'; +import { Markdown } from '~/components'; +import { getFileContentWithCache } from '~/services/github.server'; +import { parse } from '~/services/markdoc.server'; + +export async function loader({ context }: LoaderFunctionArgs) { + const content = await getFileContentWithCache(context, 'README.md'); + + return json( + { + content: parse(content), + }, + { + headers: { + 'Cache-Control': 'public, max-age=3600', + }, + }, + ); +} + +export default function Index() { + const { content } = useLoaderData(); + + return ; +} diff --git a/app/services/github.server.ts b/app/services/github.server.ts new file mode 100644 index 0000000..bd23569 --- /dev/null +++ b/app/services/github.server.ts @@ -0,0 +1,77 @@ +import type { Endpoints } from '@octokit/types'; +import type { AppLoadContext } from '@remix-run/cloudflare'; + +export const metadata = { + repo: 'remix-cloudflare-template', + owner: 'edmundhung', +}; + +export function getHeaders(auth: string | undefined) { + const headers = new Headers({ + Accept: 'application/vnd.github+json', + 'User-Agent': 'Conform Guide', + }); + + if (auth) { + headers.set('Authorization', `Bearer ${auth}`); + } + + return headers; +} + +export async function getFileContent(options: { + auth?: string; + ref?: string; + path: string; + owner: string; + repo: string; +}): Promise { + const searchParams = new URLSearchParams(); + + if (options.ref) { + searchParams.set('ref', options.ref); + } + + const url = `https://api.github.com/repos/${options.owner}/${options.repo}/contents/${options.path}?${searchParams}`; + const resposne = await fetch(url, { + headers: getHeaders(options.auth), + }); + + if (resposne.status === 404) { + throw resposne; + } + + const file: Endpoints['GET /repos/{owner}/{repo}/contents/{path}']['response']['data'] = + await resposne.json(); + + if (Array.isArray(file) || file.type !== 'file') { + throw new Response('Not found', { status: 404 }); + } + + return atob(file.content); +} + +export async function getFileContentWithCache( + context: AppLoadContext, + path: string, +): Promise { + const key = `github/${path}`; + const cache = await context.env.cache.get(key); + + if (cache) { + return cache; + } + + const content = await getFileContent({ + auth: context.env.GITHUB_TOKEN, + owner: metadata.owner, + repo: metadata.repo, + path, + }); + + // Update the cache + // TODO: Use `waitUntil` to update the cache in the background + await context.env.cache.put(key, content, { expirationTtl: 60 * 60 }); + + return content; +} diff --git a/app/services/markdoc.server.tsx b/app/services/markdoc.server.tsx new file mode 100644 index 0000000..85a6526 --- /dev/null +++ b/app/services/markdoc.server.tsx @@ -0,0 +1,8 @@ +import markdoc from '@markdoc/markdoc'; + +export function parse(markdown: string) { + const ast = markdoc.parse(markdown); + const node = markdoc.transform(ast); + + return node; +} 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..5ec33de --- /dev/null +++ b/env.d.ts @@ -0,0 +1,15 @@ +import 'vite/client'; +import '@remix-run/cloudflare'; +import '@cloudflare/workers-types'; + +interface Env { + ENVIRONMENT?: 'development'; + GITHUB_TOKEN?: string; + cache: KVNamespace; +} + +declare module '@remix-run/cloudflare' { + export interface AppLoadContext { + env: Env; + } +} diff --git a/functions/[[path]].ts b/functions/[[path]].ts new file mode 100644 index 0000000..c95cf92 --- /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 new file mode 100644 index 0000000..11f8d06 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "private": true, + "name": "remix-cloudflare-template", + "type": "module", + "description": "All-in-one remix starter template for Cloudflare Pages", + "scripts": { + "dev": "remix vite:dev", + "test": "playwright test --ui", + "start": "wrangler pages dev ./build/client", + "build": "remix vite:build", + "cleanup": "rimraf .cache ./build", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "typecheck": "tsc", + "prepare": "husky" + }, + "dependencies": { + "@markdoc/markdoc": "^0.4.0", + "@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": "^4.20240208.0", + "@octokit/types": "^12.4.0", + "@playwright/test": "^1.41.2", + "@remix-run/dev": "*", + "@remix-run/eslint-config": "*", + "@tailwindcss/typography": "^0.5.10", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "autoprefixer": "^10.4.17", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "husky": "^9.0.10", + "lint-staged": "^15.2.2", + "msw": "^2.1.7", + "postcss": "^8.4.35", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.11", + "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": ">=18" + }, + "sideEffects": false, + "lint-staged": { + "*.{js,mjs,ts,tsx,css,md,yml}": "prettier --write" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..0026eb3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test'; + +const PORT = process.env.PORT || '5173'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 15 * 1000, + expect: { + timeout: 5 * 1000, + }, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: `http://localhost:${PORT}`, + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..7b75c83 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + 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/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..8830cf6 Binary files /dev/null and b/public/favicon.ico differ diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..3c4ff11 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,7 @@ +import typeography from '@tailwindcss/typography'; + +export default { + content: ['./app/**/*.tsx', './app/**/*.ts'], + theme: {}, + plugins: [typeography], +}; diff --git a/tests/e2e/index.test.ts b/tests/e2e/index.test.ts new file mode 100644 index 0000000..420c0b6 --- /dev/null +++ b/tests/e2e/index.test.ts @@ -0,0 +1,65 @@ +import { http } from 'msw'; +import { test, expect } from '../playwright'; + +/** + * You can interact with browser through the page instance + */ +test('shows the package name', async ({ page }) => { + await page.goto('/'); + + const title = page.getByRole('heading', { + name: 'remix-cloudflare-template', + level: 2, + }); + + await expect(title).toBeVisible(); +}); + +/** + * You can interact with the wrangler binding similar to the remix app + */ +test('cache the README in KV', async ({ page, wrangler }) => { + await wrangler.bindings.cache.put('github/README.md', '# cached-readme'); + await page.goto('/'); + + const title = page.getByRole('heading', { + name: 'cached-readme', + level: 1, + }); + + await expect(title).toBeVisible(); +}); + +/** + * You can also mock the requests with MSW + */ +test('fetch README from GitHub if not cached', async ({ + page, + wrangler, + msw, +}) => { + // Mock request + msw.use( + http.get( + 'https://api.github.com/repos/edmundhung/remix-cloudflare-template/contents/README.md', + () => { + return Response.json({ + type: 'file', + content: btoa('# testing'), + }); + }, + ), + ); + + // Clear cache + await wrangler.bindings.cache.delete('github/README.md'); + + await page.goto('/'); + + const title = page.getByRole('heading', { + name: 'testing', + level: 1, + }); + + await expect(title).toBeVisible(); +}); diff --git a/tests/playwright.ts b/tests/playwright.ts new file mode 100644 index 0000000..21899b4 --- /dev/null +++ b/tests/playwright.ts @@ -0,0 +1,90 @@ +import { test as baseTest, expect as baseExpect } from '@playwright/test'; +import type { Env } from 'env'; +import { type ViteDevServer, createServer } from 'vite'; +import { type SetupServer, setupServer } from 'msw/node'; +import { type BindingsProxy, getBindingsProxy } from 'wrangler'; + +interface TestFixtures {} + +interface WorkerFixtures { + port: number; + wrangler: BindingsProxy; + server: ViteDevServer; + msw: SetupServer; +} + +export async function clearKV(namespace: KVNamespace): Promise { + const result = await namespace.list(); + + await Promise.all(result.keys.map(key => namespace.delete(key.name))); +} + +export const expect = baseExpect.extend({}); + +export const test = baseTest.extend({ + // Assign a unique "port" for each worker process + port: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use, workerInfo) => { + await use(3515 + workerInfo.workerIndex); + }, + { scope: 'worker' }, + ], + + // Ensure visits works with relative path + baseURL: ({ port }, use) => { + use(`http://localhost:${port}`); + }, + + // Start a Vite dev server for each worker + // This allows MSW to intercept requests properly + server: [ + async ({ port }, use) => { + const server = await createServer({ + configFile: './vite.config.ts', + }); + + await server.listen(port); + + await use(server); + + await server.close(); + }, + { scope: 'worker', auto: true }, + ], + + msw: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + const server = setupServer(); + + server.listen(); + + await use(server); + + server.close(); + }, + { scope: 'worker', auto: true }, + ], + + // To access wrangler bindings similar to Remix / Vite + wrangler: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + const wrangler = await getBindingsProxy(); + + // To access bindings in the tests. + await use(wrangler); + + // Ensure all cachees are cleaned up + await clearKV(wrangler.bindings.cache); + + await wrangler.dispose(); + }, + { scope: 'worker', auto: true }, + ], +}); + +test.beforeEach(({ msw }) => { + msw.resetHandlers(); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..77291a9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "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..c79c48f --- /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/wrangler.toml.example b/wrangler.toml.example new file mode 100644 index 0000000..765717c --- /dev/null +++ b/wrangler.toml.example @@ -0,0 +1,8 @@ +name = "remix-cloudflare-template" +compatibility_date = "2024-02-11" +kv_namespaces = [ + { binding = "cache", id = "cache" } +] + +[vars] +GITHUB_TOKEN = ""