diff --git a/.changeset/khaki-starfishes-rest.md b/.changeset/khaki-starfishes-rest.md new file mode 100644 index 00000000000..aa0d8ba41d4 --- /dev/null +++ b/.changeset/khaki-starfishes-rest.md @@ -0,0 +1,44 @@ +--- +"@remix-run/cloudflare-pages": minor +"@remix-run/dev": minor +--- + +Vite: Cloudflare Proxy as a Vite plugin + +**This is a breaking change for projects relying on Cloudflare support from the unstable Vite plugin** + +The Cloudflare preset (`unstable_cloudflarePreset`) as been removed and replaced with a new Vite plugin: + +```diff + import { + unstable_vitePlugin as remix, +- unstable_cloudflarePreset as cloudflare, ++ cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, + } from "@remix-run/dev"; + import { defineConfig } from "vite"; + + export default defineConfig({ + plugins: [ ++ remixCloudflareDevProxy(), ++ remix(), +- remix({ +- presets: [cloudflare()], +- }), + ], +- ssr: { +- resolve: { +- externalConditions: ["workerd", "worker"], +- }, +- }, + }); +``` + +`remixCloudflareDevProxy` must come _before_ the `remix` plugin so that it can override Vite's dev server middleware to be compatible with Cloudflare's proxied environment. + +Because it is a Vite plugin, `remixCloudflareDevProxy` can set `ssr.resolve.externalConditions` to be `workerd`-compatible for you. + +`remixCloudflareDevProxy` accepts a `getLoadContext` function that replaces the old `getRemixDevLoadContext`. +If you were using a `nightly` version that required `getBindingsProxy` or `getPlatformProxy`, that is no longer required. +Any options you were passing to `getBindingsProxy` or `getPlatformProxy` should now be passed to `remixCloudflareDevProxy` instead. + +This API also better aligns with future plans to support Cloudflare with a framework-agnostic Vite plugin that makes use of Vite's (experimental) Runtime API. diff --git a/docs/future/presets.md b/docs/future/presets.md index 7a5a4a08636..158a0873f98 100644 --- a/docs/future/presets.md +++ b/docs/future/presets.md @@ -13,32 +13,6 @@ Presets can only do two things: The config returned by each preset is merged in the order they were defined. Any config directly passed to the Remix Vite plugin will be merged last. This means that user config will always take precedence over any presets. -## Using a preset - -Presets are designed to be published to npm and used within your Vite config. For example, Remix ships with a preset for Cloudflare: - -```ts filename=vite.config.ts lines=[3,11] -import { - vitePlugin as remix, - cloudflarePreset as cloudflare, -} from "@remix-run/dev"; -import { defineConfig } from "vite"; -import { getBindingsProxy } from "wrangler"; - -export default defineConfig({ - plugins: [ - remix({ - presets: [cloudflare(getBindingsProxy)], - }), - ], - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, -}); -``` - ## Creating a preset Presets conform to the following `Preset` type: @@ -121,5 +95,23 @@ export function myCoolPreset(): Preset { The `remixConfigResolved` hook should only be used in cases where it would be an error to merge or override your preset's config. +## Using a preset + +Presets are designed to be published to npm and used within your Vite config. + +```ts filename=vite.config.ts lines=[3,8] +import { vitePlugin as remix } from "@remix-run/dev"; +import { myCoolPreset } from "remix-preset-cool"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + remix({ + presets: [myCoolPreset()], + }), + ], +}); +``` + [remix-vite]: ./vite [server-bundles]: ./server-bundles diff --git a/docs/future/vite.md b/docs/future/vite.md index d7d9d0b7ddf..b1dfe291196 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -109,85 +109,83 @@ wrangler pages dev ./build/client While Vite provides a better development experience, Wrangler provides closer emulation of the Cloudflare environment by running your server code in [Cloudflare's `workerd` runtime][cloudflare-workerd] instead of Node. -#### Bindings - -To simulate the Cloudflare environment in Vite, Wrangler provides [Node proxies for resource bindings][wrangler-getbindingsproxy]. -Bindings for Cloudflare resources can be configured [within `wrangler.toml` for local development][wrangler-toml-bindings] or within the [Cloudflare dashboard for deployments][cloudflare-pages-bindings]. +#### Cloudflare Proxy -Remix's Cloudflare preset accepts Wrangler's `getBindingsProxy` function to simulate resource bindings within Vite's dev server: +To simulate the Cloudflare environment in Vite, Wrangler provides [Node proxies to local `workerd` bindings][wrangler-getplatformproxy]. +Remix's Cloudflare Proxy plugin sets up these proxies for you: -```ts filename=vite.config.ts lines=[6,11] +```ts filename=vite.config.ts lines=[3,8] import { vitePlugin as remix, - cloudflarePreset as cloudflare, + cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, } from "@remix-run/dev"; import { defineConfig } from "vite"; -import { getBindingsProxy } from "wrangler"; export default defineConfig({ - plugins: [ - remix({ - presets: [cloudflare(getBindingsProxy)], - }), - ], - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, + plugins: [remixCloudflareDevProxy(), remix()], }); ``` -Then, you can access your bindings via `context.env`. +The proxies are then available within `context.cloudflare` in your `loader` or `action` functions: + +```ts +export const loader = ({ context }: LoaderFunctionArgs) => { + const { env, cf, ctx } = context.cloudflare; + // ... more loader code here... +}; +``` + +Check out [Cloudflare's `getPlatformProxy` docs][wrangler-getplatformproxy-return] for more information on each of these proxies. + +#### Bindings + +To configure bindings for Cloudflare resources: + +- For local development with Vite or Wrangler, use [wrangler.toml][wrangler-toml-bindings] +- For deployments, use the [Cloudflare dashboard][cloudflare-pages-bindings] + +Whenever you change your `wrangler.toml` file, you'll need to run `wrangler types` to regenerate your bindings. + +Then, you can access your bindings via `context.cloudflare.env`. For example, with a [KV namespace][cloudflare-kv] bound as `MY_KV`: ```ts filename=app/routes/_index.tsx -export async function loader({ context }) { - const { MY_KV } = context.env; +export async function loader({ + context, +}: LoaderFunctionArgs) { + const { MY_KV } = context.cloudflare.env; const value = await MY_KV.get("my-key"); return json({ value }); } ``` - - -The Cloudflare team is working to improve their Node proxies to support: - -- [Cloudflare request][cloudflare-proxy-cf] (`cf`) -- [Context][cloudflare-proxy-ctx] (`ctx`) -- [Cache][cloudflare-proxy-caches] (`caches`) - - - #### Augmenting Cloudflare load context If you'd like to add additional properties to the load context, -you can export a `getLoadContext` function from `load-context.ts` that you can wire up to Vite and Cloudflare Pages: +you can export a `getLoadContext` function from a shared module that you can wire up to Vite and Cloudflare Pages: -```ts filename=load-context.ts lines=[2,14,18-28] -import { type KVNamespace } from "@cloudflare/workers-types"; +```ts filename=load-context.ts lines=[2,10,14-27] import { type AppLoadContext } from "@remix-run/cloudflare"; +import { type PlatformProxy } from "wrangler"; -// In the future, types for bindings will be generated by `wrangler types` -// See https://github.com/cloudflare/workers-sdk/pull/4931 -type Bindings = { - // Add types for bindings configured in `wrangler.toml` - MY_KV: KVNamespace; -}; +type Cloudflare = Omit, "dispose">; declare module "@remix-run/cloudflare" { interface AppLoadContext { - env: Bindings; + cloudflare: Cloudflare; extra: string; } } -type Context = { request: Request; env: Bindings }; +type GetLoadContext = (args: { + request: Request; + context: { cloudflare: Cloudflare }; +}) => AppLoadContext; // Shared implementation compatible with Vite, Wrangler, and Cloudflare Pages -export const getLoadContext = async ( - context: Context -): Promise => { +export const getLoadContext: GetLoadContext = ({ + context, +}) => { return { ...context, extra: "stuff", @@ -195,40 +193,29 @@ export const getLoadContext = async ( }; ``` -The Cloudflare preset accepts a `getRemixDevLoadContext` function whose return value is merged into the load context for each request in development: +For local development with Vite, you can then pass this `getLoadContext` function to the Cloudflare Proxy plugin in your Vite config: -```ts filename=vite.config.ts lines=[9,16] +```ts filename=vite.config.ts lines=[8,12] import { vitePlugin as remix, - cloudflarePreset as cloudflare, + cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import { getBindingsProxy } from "wrangler"; import { getLoadContext } from "./load-context"; export default defineConfig({ plugins: [ - remix({ - presets: [ - cloudflare(getBindingsProxy, { - getRemixDevLoadContext: getLoadContext, - }), - ], - }), - tsconfigPaths(), + remixCloudflareDevProxy({ getLoadContext }), + remix(), ], - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, }); ``` -As the name implies, `getRemixDevLoadContext` **only augments the load context within Vite's dev server**, not within Wrangler nor in Cloudflare Pages deployments. -To wire up Wrangler and deployments, you'll need to add `getLoadContext` to `functions/[[path]].ts`: +The Cloudflare Proxy plugin's `getLoadContext` **only augments the load context within Vite's dev server**, not within Wrangler nor in Cloudflare Pages deployments. + +To wire up Wrangler and deployments, you'll also need to add `getLoadContext` to `functions/[[path]].ts`: ```ts filename=functions/[[path]].ts lines=[5,9] import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; @@ -1276,12 +1263,10 @@ We're definitely late to the Vite party, but we're excited to be here now! [cloudflare-pages-bindings]: https://developers.cloudflare.com/pages/functions/bindings/ [cloudflare-kv]: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces [cloudflare-workerd]: https://blog.cloudflare.com/workerd-open-source-workers-runtime -[wrangler-getbindingsproxy]: https://developers.cloudflare.com/workers/wrangler/api/#getbindingsproxy +[wrangler-getplatformproxy]: https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy +[wrangler-getplatformproxy-return]: https://developers.cloudflare.com/workers/wrangler/api/#return-type-1 [remix-config-server]: https://remix.run/docs/en/main/file-conventions/remix-config#server [cloudflare-vite-and-wrangler]: #vite--wrangler -[cloudflare-proxy-cf]: https://github.com/cloudflare/workers-sdk/issues/4875 -[cloudflare-proxy-ctx]: https://github.com/cloudflare/workers-sdk/issues/4876 -[cloudflare-proxy-caches]: https://github.com/cloudflare/workers-sdk/issues/4879 [rr-basename]: https://reactrouter.com/routers/create-browser-router#basename [vite-public-base-path]: https://vitejs.dev/config/shared-options.html#base [vite-base]: https://vitejs.dev/config/shared-options.html#base diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 2ec4670abbb..bdb65f536a1 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -148,6 +148,31 @@ export const viteRemixServe = async ({ return () => serveProc.kill(); }; +export const wranglerPagesDev = async ({ + cwd, + port, +}: { + cwd: string; + port: number; +}) => { + let nodeBin = process.argv[0]; + + // grab wrangler bin from remix-run/remix root node_modules since its not copied into integration project's node_modules + let wranglerBin = path.resolve("node_modules/wrangler/bin/wrangler.js"); + + let proc = spawn( + nodeBin, + [wranglerBin, "pages", "dev", "./build/client", "--port", String(port)], + { + cwd, + stdio: "pipe", + env: { NODE_ENV: "production" }, + } + ); + await waitForServer(proc, { port }); + return () => proc.kill(); +}; + type ServerArgs = { cwd: string; port: number; @@ -197,6 +222,10 @@ type Fixtures = { port: number; cwd: string; }>; + wranglerPagesDev: (files: Files) => Promise<{ + port: number; + cwd: string; + }>; }; export const test = base.extend({ @@ -240,6 +269,19 @@ export const test = base.extend({ }); stop?.(); }, + // eslint-disable-next-line no-empty-pattern + wranglerPagesDev: async ({}, use) => { + let stop: (() => unknown) | undefined; + await use(async (files) => { + let port = await getPort(); + let cwd = await createProject(await files({ port })); + let { status } = viteBuild({ cwd }); + expect(status).toBe(0); + stop = await wranglerPagesDev({ cwd, port }); + return { port, cwd }; + }); + stop?.(); + }, }); function node( diff --git a/integration/package.json b/integration/package.json index 50d6b69d704..4ee01d7b1ad 100644 --- a/integration/package.json +++ b/integration/package.json @@ -37,6 +37,6 @@ "typescript": "^5.1.0", "vite-env-only": "^2.0.0", "vite-tsconfig-paths": "^4.2.2", - "wrangler": "^3.24.0" + "wrangler": "^3.28.2" } } diff --git a/integration/vite-cloudflare-test.ts b/integration/vite-cloudflare-test.ts index 7695a93739a..0c48c7b31d2 100644 --- a/integration/vite-cloudflare-test.ts +++ b/integration/vite-cloudflare-test.ts @@ -1,3 +1,4 @@ +import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; import dedent from "dedent"; @@ -35,7 +36,7 @@ const files: Files = async ({ port }) => ({ typescript: "^5.1.6", vite: "^5.1.0", "vite-tsconfig-paths": "^4.2.1", - wrangler: "^3.24.0", + wrangler: "^3.28.2", }, engines: { node: ">=18.0.0", @@ -47,47 +48,44 @@ const files: Files = async ({ port }) => ({ "vite.config.ts": dedent` import { vitePlugin as remix, - cloudflarePreset as cloudflare, + cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, } from "@remix-run/dev"; - import { getBindingsProxy } from "wrangler"; - import { getLoadContext } from "./get-load-context"; + import { getLoadContext } from "./load-context"; export default { ${await viteConfig.server({ port })} - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, plugins: [ - remix({ - presets: [ - cloudflare(getBindingsProxy, { - getRemixDevLoadContext: getLoadContext, - }) - ] - }) + remixCloudflareDevProxy({ getLoadContext }), + remix(), ], } `, - "get-load-context.ts": ` - import { type KVNamespace } from "@cloudflare/workers-types"; + "load-context.ts": ` import { type AppLoadContext } from "@remix-run/cloudflare"; + import { type PlatformProxy } from "wrangler"; + + type Env = { + MY_KV: KVNamespace; + } + type Cloudflare = Omit, 'dispose'>; declare module "@remix-run/cloudflare" { - export interface AppLoadContext { - env: { - MY_KV: KVNamespace; - }; + interface AppLoadContext { + cloudflare: Cloudflare; + env2: Cloudflare["env"]; extra: string; } } - type Context = { request: Request; env: AppLoadContext["env"] }; - export const getLoadContext = (context: Context): AppLoadContext => { + type GetLoadContext = (args: { + request: Request; + context: { cloudflare: Cloudflare }; + }) => AppLoadContext; + + export const getLoadContext: GetLoadContext = ({ context }) => { return { ...context, - env2: context.env, + env2: context.cloudflare.env, extra: "stuff", }; }; @@ -97,7 +95,7 @@ const files: Files = async ({ port }) => ({ // @ts-ignore - the server build file is generated by \`remix vite:build\` import * as build from "../build/server"; - import { getLoadContext } from "../get-load-context"; + import { getLoadContext } from "../load-context"; export const onRequest = createPagesFunctionHandler({ build, @@ -120,23 +118,23 @@ const files: Files = async ({ port }) => ({ const key = "__my-key__"; export async function loader({ context }: LoaderFunctionArgs) { - const { MY_KV } = context.env; + const { MY_KV } = context.cloudflare.env; const value = await MY_KV.get(key); return json({ value, extra: context.extra }); } export async function action({ request, context }: ActionFunctionArgs) { - const { MY_KV: myKv } = context.env2; + const { MY_KV } = context.env2; if (request.method === "POST") { const formData = await request.formData(); const value = formData.get("value") as string; - await myKv.put(key, value); + await MY_KV.put(key, value); return null; } if (request.method === "DELETE") { - await myKv.delete(key); + await MY_KV.delete(key); return null; } @@ -175,6 +173,15 @@ const files: Files = async ({ port }) => ({ test("vite dev", async ({ page, viteDev }) => { let { port } = await viteDev(files); + await workflow({ page, port }); +}); + +test("wrangler", async ({ page, wranglerPagesDev }) => { + let { port } = await wranglerPagesDev(files); + await workflow({ page, port }); +}); + +async function workflow({ page, port }: { page: Page; port: number }) { await page.goto(`http://localhost:${port}/`, { waitUntil: "networkidle", }); @@ -184,6 +191,5 @@ test("vite dev", async ({ page, viteDev }) => { await page.getByLabel("Set value:").fill("my-value"); await page.getByRole("button").click(); await expect(page.locator("[data-text]")).toHaveText("Value: my-value"); - expect(page.errors).toEqual([]); -}); +} diff --git a/packages/remix-cloudflare-pages/worker.ts b/packages/remix-cloudflare-pages/worker.ts index 0fc0bfbf9e8..2d251aa3963 100644 --- a/packages/remix-cloudflare-pages/worker.ts +++ b/packages/remix-cloudflare-pages/worker.ts @@ -1,5 +1,6 @@ import type { AppLoadContext, ServerBuild } from "@remix-run/cloudflare"; import { createRequestHandler as createRemixRequestHandler } from "@remix-run/cloudflare"; +import { type CacheStorage } from "@cloudflare/workers-types"; /** * A function that returns the value to use as `context` in route `loader` and @@ -12,9 +13,23 @@ export type GetLoadContextFunction< Env = unknown, Params extends string = any, Data extends Record = Record -> = ( - context: EventContext -) => Promise | AppLoadContext; +> = (args: { + request: Request; + context: { + cloudflare: EventContext & { + cf: EventContext["request"]["cf"]; + ctx: { + waitUntil: EventContext["waitUntil"]; + passThroughOnException: EventContext< + Env, + Params, + Data + >["passThroughOnException"]; + }; + caches: CacheStorage; + }; + }; +}) => AppLoadContext | Promise; export type RequestHandler = PagesFunction; @@ -27,14 +42,33 @@ export interface createPagesFunctionHandlerParams { export function createRequestHandler({ build, mode, - getLoadContext = (context) => ({ env: context.env }), + getLoadContext = ({ context }) => ({ + ...context, + cloudflare: { + ...context.cloudflare, + cf: context.cloudflare.request.cf, + }, + }), }: createPagesFunctionHandlerParams): RequestHandler { let handleRequest = createRemixRequestHandler(build, mode); - return async (context) => { - let loadContext = await getLoadContext(context); + return async (cloudflare) => { + let loadContext = await getLoadContext({ + request: cloudflare.request, + context: { + cloudflare: { + ...cloudflare, + cf: cloudflare.request.cf!, + ctx: { + waitUntil: cloudflare.waitUntil, + passThroughOnException: cloudflare.passThroughOnException, + }, + caches, + }, + }, + }); - return handleRequest(context.request, loadContext); + return handleRequest(cloudflare.request, loadContext); }; } diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index 18e02eea342..1c28706e7c2 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -12,4 +12,4 @@ export type { ServerBundlesFunction, VitePluginConfig, } from "./vite"; -export { vitePlugin, cloudflarePreset } from "./vite"; +export { vitePlugin, cloudflareDevProxyVitePlugin } from "./vite"; diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index d94f3424c6e..76c59f8fa9f 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -91,12 +91,14 @@ "msw": "^1.2.3", "strip-ansi": "^6.0.1", "tiny-invariant": "^1.2.0", - "vite": "5.1.0" + "vite": "5.1.0", + "wrangler": "^3.28.2" }, "peerDependencies": { "@remix-run/serve": "^2.6.0", "typescript": "^5.1.0", - "vite": "^5.1.0" + "vite": "^5.1.0", + "wrangler": "^3.28.2" }, "peerDependenciesMeta": { "@remix-run/serve": { @@ -107,6 +109,9 @@ }, "vite": { "optional": true + }, + "wrangler": { + "optional": true } }, "engines": { diff --git a/packages/remix-dev/vite/cloudflare-proxy-plugin.ts b/packages/remix-dev/vite/cloudflare-proxy-plugin.ts new file mode 100644 index 00000000000..314c94cf33e --- /dev/null +++ b/packages/remix-dev/vite/cloudflare-proxy-plugin.ts @@ -0,0 +1,87 @@ +import { createRequestHandler } from "@remix-run/server-runtime"; +import { + type AppLoadContext, + type ServerBuild, +} from "@remix-run/server-runtime"; +import { type Plugin } from "vite"; +import { type GetPlatformProxyOptions, type PlatformProxy } from "wrangler"; + +import { fromNodeRequest, toNodeRequest } from "./node-adapter"; + +let serverBuildId = "virtual:remix/server-build"; + +type CfProperties = Record; + +type LoadContext = { + cloudflare: Omit, "dispose">; +}; + +type GetLoadContext = (args: { + request: Request; + context: LoadContext; +}) => AppLoadContext | Promise; + +function importWrangler() { + try { + return import("wrangler"); + } catch (_) { + throw Error("Could not import `wrangler`. Do you have it installed?"); + } +} + +const NAME = "vite-plugin-remix-cloudflare-proxy"; + +export const cloudflareDevProxyVitePlugin = ({ + getLoadContext, + ...options +}: { + getLoadContext?: GetLoadContext; +} & GetPlatformProxyOptions = {}): Plugin => { + return { + name: NAME, + config: () => ({ + ssr: { + resolve: { + externalConditions: ["workerd", "worker"], + }, + }, + }), + configResolved: (viteConfig) => { + let pluginIndex = (name: string) => + viteConfig.plugins.findIndex((plugin) => plugin.name === name); + let remixIndex = pluginIndex("remix"); + if (remixIndex >= 0 && remixIndex < pluginIndex(NAME)) { + throw new Error( + `The "${NAME}" plugin should be placed before the Remix plugin in your Vite config file` + ); + } + }, + configureServer: async (viteDevServer) => { + let { getPlatformProxy } = await importWrangler(); + // Do not include `dispose` in Cloudflare context + let { dispose, ...cloudflare } = await getPlatformProxy(options); + let context = { cloudflare }; + return () => { + if (!viteDevServer.config.server.middlewareMode) { + viteDevServer.middlewares.use(async (nodeReq, nodeRes, next) => { + try { + let build = (await viteDevServer.ssrLoadModule( + serverBuildId + )) as ServerBuild; + + let handler = createRequestHandler(build, "development"); + let req = fromNodeRequest(nodeReq); + let loadContext = getLoadContext + ? await getLoadContext({ request: req, context }) + : context; + let res = await handler(req, loadContext); + await toNodeRequest(res, nodeRes); + } catch (error) { + next(error); + } + }); + } + }; + }, + }; +}; diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts index f966bc2c94a..f563856b381 100644 --- a/packages/remix-dev/vite/index.ts +++ b/packages/remix-dev/vite/index.ts @@ -15,4 +15,4 @@ export const vitePlugin: RemixVitePlugin = (...args) => { return remixVitePlugin(...args); }; -export { cloudflarePreset } from "./presets/cloudflare"; +export { cloudflareDevProxyVitePlugin } from "./cloudflare-proxy-plugin"; diff --git a/packages/remix-dev/vite/presets/cloudflare.ts b/packages/remix-dev/vite/presets/cloudflare.ts deleted file mode 100644 index aab0e75c8d1..00000000000 --- a/packages/remix-dev/vite/presets/cloudflare.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { type AppLoadContext } from "@remix-run/server-runtime"; - -import { type Preset, setRemixDevLoadContext } from "../plugin"; - -type MaybePromise = T | Promise; - -type GetRemixDevLoadContext = (args: { - request: Request; - env: AppLoadContext["env"]; -}) => MaybePromise>; - -type GetLoadContext = ( - request: Request -) => MaybePromise>; - -type GetBindingsProxy = () => Promise<{ bindings: Record }>; - -/** - * @param options.getRemixDevLoadContext - Augment the load context. - */ -export const cloudflarePreset = ( - getBindingsProxy: GetBindingsProxy, - options: { - getRemixDevLoadContext?: GetRemixDevLoadContext; - } = {} -): Preset => ({ - name: "cloudflare", - remixConfig: async () => { - let getLoadContext: GetLoadContext = async () => { - let { bindings } = await getBindingsProxy(); - return { env: bindings }; - }; - - // eslint-disable-next-line prefer-let/prefer-let - const { getRemixDevLoadContext } = options; - if (getRemixDevLoadContext) { - getLoadContext = async (request: Request) => { - let { bindings } = await getBindingsProxy(); - let loadContext = await getRemixDevLoadContext({ - env: bindings, - request, - }); - return loadContext; - }; - } - - setRemixDevLoadContext(getLoadContext); - return {}; - }, -}); diff --git a/templates/vite-cloudflare/README.md b/templates/vite-cloudflare/README.md index c05e097d923..b0b49f9ab41 100644 --- a/templates/vite-cloudflare/README.md +++ b/templates/vite-cloudflare/README.md @@ -2,14 +2,32 @@ 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. +## Typegen + +Generate types for your Cloudflare bindings in `wrangler.toml`: + +```sh +npm run typegen +``` + +This should have been done for you initially via the `postinstall` script, +but you will need to rerun typegen whenever you make changes to `wrangler.toml`. + ## Development Run the Vite dev server: -```shellscript +```sh npm run dev ``` +To run Wrangler: + +```sh +npm run build +npm run start +``` + ## Deployment First, build your app for production: @@ -18,19 +36,12 @@ First, build your app for production: npm run build ``` -Then run the app in production mode: +Then, deploy your app to Cloudflare Pages: ```sh -npm start +npm run deploy ``` -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` +> [!WARNING] +> Cloudflare does _not_ use `wrangler.toml` to configure deployment bindings. +> You **MUST** configure deployment bindings manually in the Cloudflare dashboard. diff --git a/templates/vite-cloudflare/app/routes/_index.tsx b/templates/vite-cloudflare/app/routes/_index.tsx index cad20670fcd..fe86669b534 100644 --- a/templates/vite-cloudflare/app/routes/_index.tsx +++ b/templates/vite-cloudflare/app/routes/_index.tsx @@ -8,13 +8,13 @@ import { Form, useLoaderData } from "@remix-run/react"; const key = "__my-key__"; export async function loader({ context }: LoaderFunctionArgs) { - const { MY_KV } = context.env; + const { MY_KV } = context.cloudflare.env; const value = await MY_KV.get(key); return json({ value }); } export async function action({ request, context }: ActionFunctionArgs) { - const { MY_KV: myKv } = context.env; + const { MY_KV: myKv } = context.cloudflare.env; if (request.method === "POST") { const formData = await request.formData(); diff --git a/templates/vite-cloudflare/load-context.ts b/templates/vite-cloudflare/load-context.ts index 5ebc9a6281a..fcf3a3ce659 100644 --- a/templates/vite-cloudflare/load-context.ts +++ b/templates/vite-cloudflare/load-context.ts @@ -1,14 +1,9 @@ -import { type KVNamespace } from "@cloudflare/workers-types"; +import { type PlatformProxy } from "wrangler"; -// In the future, types for bindings will be generated by `wrangler types` -// See https://github.com/cloudflare/workers-sdk/pull/4931 -type Bindings = { - // Add types for bindings configured in `wrangler.toml` - MY_KV: KVNamespace; -}; +type Cloudflare = Omit, "dispose">; declare module "@remix-run/cloudflare" { interface AppLoadContext { - env: Bindings; + cloudflare: Cloudflare; } } diff --git a/templates/vite-cloudflare/package.json b/templates/vite-cloudflare/package.json index 9e7e301ba02..6715b3041b0 100644 --- a/templates/vite-cloudflare/package.json +++ b/templates/vite-cloudflare/package.json @@ -3,12 +3,14 @@ "sideEffects": false, "type": "module", "scripts": { + "postinstall": "npm run typegen", "dev": "remix vite:dev", "build": "remix vite:build", "start": "wrangler pages dev ./build/client", "deploy": "wrangler pages deploy ./build/client", "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "typecheck": "tsc" + "typecheck": "tsc", + "typegen": "wrangler types" }, "dependencies": { "@remix-run/cloudflare": "*", diff --git a/templates/vite-cloudflare/vite.config.ts b/templates/vite-cloudflare/vite.config.ts index 4842ad88f18..37b93b6d033 100644 --- a/templates/vite-cloudflare/vite.config.ts +++ b/templates/vite-cloudflare/vite.config.ts @@ -1,21 +1,10 @@ import { vitePlugin as remix, - cloudflarePreset as cloudflare, + cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import { getBindingsProxy } from "wrangler"; export default defineConfig({ - plugins: [ - remix({ - presets: [cloudflare(getBindingsProxy)], - }), - tsconfigPaths(), - ], - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, + plugins: [remixCloudflareDevProxy(), remix(), tsconfigPaths()], }); diff --git a/yarn.lock b/yarn.lock index 4d1e55dd8e2..f915e347b43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,13 @@ human-id "^1.0.2" prettier "^2.7.1" +"@cloudflare/kv-asset-handler@0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.1.tgz#9b86167e58dbc419943c8d3ddcd8e2823f5db300" + integrity sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA== + dependencies: + mime "^3.0.0" + "@cloudflare/kv-asset-handler@^0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.1.3.tgz" @@ -1258,13 +1265,6 @@ dependencies: mime "^2.5.2" -"@cloudflare/kv-asset-handler@^0.2.0": - version "0.2.0" - resolved "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz#c9959bbd7a1c40bd7c674adae98aa8c8d0e5ca68" - integrity sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A== - dependencies: - mime "^3.0.0" - "@cloudflare/kv-asset-handler@^0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.0.tgz#11f0af0749a400ddadcca16dcd6f4696d7036991" @@ -1272,30 +1272,30 @@ dependencies: mime "^3.0.0" -"@cloudflare/workerd-darwin-64@1.20231218.0": - version "1.20231218.0" - resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20231218.0.tgz#e887296a6bfa707b2e02dbf5168582cd3afb800c" - integrity sha512-547gOmTIVmRdDy7HNAGJUPELa+fSDm2Y0OCxqAtQOz0GLTDu1vX61xYmsb2rn91+v3xW6eMttEIpbYokKjtfJA== +"@cloudflare/workerd-darwin-64@1.20240129.0": + version "1.20240129.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240129.0.tgz#b6db9c944fcb1a49b15be646383c937ffa175978" + integrity sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg== -"@cloudflare/workerd-darwin-arm64@1.20231218.0": - version "1.20231218.0" - resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20231218.0.tgz#9346de61b74324b09e3ef83e1666ffc84f1c4559" - integrity sha512-b39qrU1bKolCfmKFDAnX4vXcqzISkEUVE/V8sMBsFzxrIpNAbcUHBZAQPYmS/OHIGB94KjOVokvDi7J6UNurPw== +"@cloudflare/workerd-darwin-arm64@1.20240129.0": + version "1.20240129.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240129.0.tgz#1e217bae20c8407ed0225b3eb60b6b2c4ab1a5ed" + integrity sha512-t0q8ABkmumG1zRM/MZ/vIv/Ysx0vTAXnQAPy/JW5aeQi/tqrypXkO9/NhPc0jbF/g/hIPrWEqpDgEp3CB7Da7Q== -"@cloudflare/workerd-linux-64@1.20231218.0": - version "1.20231218.0" - resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20231218.0.tgz#7d21aaa0b4a97f9d7769fa6af2e484538f7e3713" - integrity sha512-dMUF1wA+0mybm6hHNOCgY/WMNMwomPPs4I7vvYCgwHSkch0Q2Wb7TnxQZSt8d1PK/myibaBwadrlIxpjxmpz3w== +"@cloudflare/workerd-linux-64@1.20240129.0": + version "1.20240129.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240129.0.tgz#d0e46297c79982b47495cbfb73623d621aa49335" + integrity sha512-sFV1uobHgDI+6CKBS/ZshQvOvajgwl6BtiYaH4PSFSpvXTmRx+A9bcug+6BnD+V4WgwxTiEO2iR97E1XuwDAVw== -"@cloudflare/workerd-linux-arm64@1.20231218.0": - version "1.20231218.0" - resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20231218.0.tgz#e8280275379aca868886db7d2491517be3f473f4" - integrity sha512-2s5uc8IHt0QmWyKxAr1Fy+4b8Xy0b/oUtlPnm5MrKi2gDRlZzR7JvxENPJCpCnYENydS8lzvkMiAFECPBccmyQ== +"@cloudflare/workerd-linux-arm64@1.20240129.0": + version "1.20240129.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240129.0.tgz#e5d02fafcad1536e0515ee5feb2e713e487b1f2a" + integrity sha512-O7q7htHaFRp8PgTqNJx1/fYc3+LnvAo6kWWB9a14C5OWak6AAZk42PNpKPx+DXTmGvI+8S1+futBGUeJ8NPDXg== -"@cloudflare/workerd-windows-64@1.20231218.0": - version "1.20231218.0" - resolved "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20231218.0.tgz#85fc18f18f7c6593b427c58bf58224850f706d20" - integrity sha512-oN5hz6TXUDB5YKUN5N3QWAv6cYz9JjTZ9g16HVyoegVFEL6/zXU3tV19MBX2IvlE11ab/mRogEv9KXVIrHfKmA== +"@cloudflare/workerd-windows-64@1.20240129.0": + version "1.20240129.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240129.0.tgz#99f456b636413e66d860deb2b803d04cc5b47d75" + integrity sha512-YqGno0XSqqqkDmNoGEX6M8kJlI2lEfWntbTPVtHaZlaXVR9sWfoD7TEno0NKC95cXFz+ioyFLbgbOdnfWwmVAA== "@cloudflare/workers-types@^4.20230518.0": version "4.20230628.0" @@ -9924,10 +9924,10 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -miniflare@3.20231218.3: - version "3.20231218.3" - resolved "https://registry.npmjs.org/miniflare/-/miniflare-3.20231218.3.tgz#31aa7f9165970ae53d3048eeb24ffb13ee84cf1e" - integrity sha512-OrPBYWO0WnFv6DrxZ7hF8f5agZ4+xo/2qSLE0wwCJSqlFhr91dfSJautxfCOBD896nAA7Jqr5LBPEnqq3/k/JQ== +miniflare@3.20240129.2: + version "3.20240129.2" + resolved "https://registry.npmjs.org/miniflare/-/miniflare-3.20240129.2.tgz#9fdfe5f4f2ade629996f2f7788a1500f765bcf67" + integrity sha512-BPUg8HsPmWQlRFUeiQk274i8M9L0gOvzbkjryuTvCX+M53EwBpP0gM2wyrRr/HokQoJcxWGh3InBu6L8+0bbPw== dependencies: "@cspotcode/source-map-support" "0.8.1" acorn "^8.8.0" @@ -9937,7 +9937,7 @@ miniflare@3.20231218.3: glob-to-regexp "^0.4.1" stoppable "^1.1.0" undici "^5.28.2" - workerd "1.20231218.0" + workerd "1.20240129.0" ws "^8.11.0" youch "^3.2.2" zod "^3.20.6" @@ -13670,29 +13670,29 @@ word-wrap@^1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workerd@1.20231218.0: - version "1.20231218.0" - resolved "https://registry.npmjs.org/workerd/-/workerd-1.20231218.0.tgz#a00403af346f654c1d73f4805c07b9ef3a6d2142" - integrity sha512-AGIsDvqCrcwhoA9kb1hxOhVAe53/xJeaGZxL4FbYI9FvO17DZwrnqGq+6eqItJ6Cfw1ZLmf3BM+QdMWaL2bFWQ== +workerd@1.20240129.0: + version "1.20240129.0" + resolved "https://registry.npmjs.org/workerd/-/workerd-1.20240129.0.tgz#123a84331ec18a1af7172fcd7b764070cfb951a9" + integrity sha512-t4pnsmjjk/u+GdVDgH2M1AFmJaBUABshYK/vT/HNrAXsHSwN6VR8Yqw0JQ845OokO34VLkuUtYQYyxHHKpdtsw== optionalDependencies: - "@cloudflare/workerd-darwin-64" "1.20231218.0" - "@cloudflare/workerd-darwin-arm64" "1.20231218.0" - "@cloudflare/workerd-linux-64" "1.20231218.0" - "@cloudflare/workerd-linux-arm64" "1.20231218.0" - "@cloudflare/workerd-windows-64" "1.20231218.0" - -wrangler@^3.24.0: - version "3.24.0" - resolved "https://registry.npmjs.org/wrangler/-/wrangler-3.24.0.tgz#a8ca60eaec280fecee7293189604a8b75150fa9a" - integrity sha512-jEnqpY+9/J4VPjtuEnS2lhCPXkvbDClnMalSWaRxSx+1tiTWMJhMjtK9oyXLdO+ZUf9Q4LvFTYSPm8O1uwmnxQ== - dependencies: - "@cloudflare/kv-asset-handler" "^0.2.0" + "@cloudflare/workerd-darwin-64" "1.20240129.0" + "@cloudflare/workerd-darwin-arm64" "1.20240129.0" + "@cloudflare/workerd-linux-64" "1.20240129.0" + "@cloudflare/workerd-linux-arm64" "1.20240129.0" + "@cloudflare/workerd-windows-64" "1.20240129.0" + +wrangler@^3.28.2: + version "3.28.2" + resolved "https://registry.npmjs.org/wrangler/-/wrangler-3.28.2.tgz#32a16dc150e31e9eb3f9f8076a8a71db9c8202e4" + integrity sha512-hlD4f2avBZuR1+qo9Um6D1prdWrSRtGTo9h6o/AKce+bHQEJWoJgJKHeLmrpZlLtHg/gGR1Xa1xzrexhuIzeJw== + dependencies: + "@cloudflare/kv-asset-handler" "0.3.1" "@esbuild-plugins/node-globals-polyfill" "^0.2.3" "@esbuild-plugins/node-modules-polyfill" "^0.2.2" blake3-wasm "^2.1.5" chokidar "^3.5.3" esbuild "0.17.19" - miniflare "3.20231218.3" + miniflare "3.20240129.2" nanoid "^3.3.3" path-to-regexp "^6.2.0" resolve "^1.22.8"