Skip to content

Commit

Permalink
Vite: Cloudflare Proxy plugin (#8749)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori authored Feb 15, 2024
1 parent 291e729 commit e7797e9
Show file tree
Hide file tree
Showing 18 changed files with 416 additions and 274 deletions.
44 changes: 44 additions & 0 deletions .changeset/khaki-starfishes-rest.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 18 additions & 26 deletions docs/future/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
121 changes: 53 additions & 68 deletions docs/future/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,126 +109,113 @@ 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 });
}
```

<docs-info>

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`)

</docs-info>

#### 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<PlatformProxy<Env>, "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<AppLoadContext> => {
export const getLoadContext: GetLoadContext = ({
context,
}) => {
return {
...context,
extra: "stuff",
};
};
```

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`:
<docs-warning>The Cloudflare Proxy plugin's `getLoadContext` **only augments the load context within Vite's dev server**, not within Wrangler nor in Cloudflare Pages deployments.</docs-warning>

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";
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -197,6 +222,10 @@ type Fixtures = {
port: number;
cwd: string;
}>;
wranglerPagesDev: (files: Files) => Promise<{
port: number;
cwd: string;
}>;
};

export const test = base.extend<Fixtures>({
Expand Down Expand Up @@ -240,6 +269,19 @@ export const test = base.extend<Fixtures>({
});
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(
Expand Down
2 changes: 1 addition & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading

0 comments on commit e7797e9

Please sign in to comment.