Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudflare support for Vite #8531

Merged
merged 28 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
66a89ab
wip: adapter works in dev
pcattori Jan 14, 2024
9f49258
pr feedback
pcattori Jan 18, 2024
a903006
pr feedback
pcattori Jan 18, 2024
b10321c
ignore .wrangler
pcattori Jan 18, 2024
ba404f8
add catch-all function for preview/prod
pcattori Jan 19, 2024
5e41f43
fix: configure external conditions for cf template (#8561)
jacob-ebey Jan 19, 2024
8793188
`getBindingsProxy` from wrangler beta
pcattori Jan 20, 2024
2a789a4
build ssr conditions
pcattori Jan 20, 2024
dadd18e
fix lint error
pcattori Jan 20, 2024
130f060
fix: derive default build mode from `build`
pcattori Jan 20, 2024
b383418
fix typecheck errors and lint errors
pcattori Jan 20, 2024
a18f939
adapter can set vite options
pcattori Jan 22, 2024
5f10d7f
wip: adapter from @remix-run/cloudflare pkg
pcattori Jan 22, 2024
31c2d15
adapter overrides vite config
pcattori Jan 23, 2024
d2cf14f
use vite plugin adapter from @remix-run/cloudflare
pcattori Jan 23, 2024
673dc64
cloudflare adapter as part of dev pkg
pcattori Jan 23, 2024
72954eb
add wrangler as dev dep for dev pkg
pcattori Jan 23, 2024
028ac1e
upgrade wrangler versions from beta to 3.24
pcattori Jan 23, 2024
66a2adc
docs: vite > cloudflare
pcattori Jan 24, 2024
eb9084b
changeset and better wording in docs
pcattori Jan 24, 2024
4022791
tweak docs
markdalgleish Jan 24, 2024
60ca0ab
Merge branch 'dev' into pedro/vite-cloudflare
pcattori Jan 25, 2024
c30f22e
tweak docs
pcattori Jan 25, 2024
6a78dae
tweak docs
pcattori Jan 25, 2024
8d534fe
simplify build import in template
pcattori Jan 25, 2024
92b44b6
tweak changeset
pcattori Jan 25, 2024
b24b14a
add tests for vite dev + cloudflare
pcattori Jan 25, 2024
132fc21
Merge branch 'dev' into pedro/vite-cloudflare
pcattori Jan 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/fluffy-dots-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@remix-run/cloudflare-pages": patch
"@remix-run/dev": patch
"@remix-run/server-runtime": patch
---

Vite: Cloudflare Pages support

To get started with Cloudflare, you can use the [`unstable-vite-cloudflare`][template-vite-cloudflare] template:

```shellscript nonumber
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare
```

Or read the new docs at [Future > Vite > Cloudflare](https://remix.run/docs/en/main/future/vite#cloudflare) and
[Future > Vite > Migrating > Migrating Cloudflare Functions](https://remix.run/docs/en/main/future/vite#migrating-cloudflare-functions).
161 changes: 151 additions & 10 deletions docs/future/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@ title: Vite (Unstable)

[Vite][vite] is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we now support Vite as an alternative compiler. In the future, Vite will become the default compiler for Remix.

<docs-warning>Note that Cloudflare is not yet supported when using Vite.</docs-warning>

Comment on lines -9 to -10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

## Getting started

To get started with a minimal server, you can use the [`unstable-vite`][template-vite] template:
We've got a few different Vite-based templates to get you started.

```shellscript nonumber
# Minimal server:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite
```

If you'd rather customize your server, you can use the [`unstable-vite-express`][template-vite-express] template:

```shellscript nonumber
# Express:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-express

# Cloudflare:
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare
```

These templates include a `vite.config.ts` file which is where the Remix Vite plugin is configured.
Expand Down Expand Up @@ -80,6 +79,62 @@ A function for assigning addressable routes to [server bundles][server-bundles].

You may also want to enable the `manifest` option since, when server bundles are enabled, it contains mappings between routes and server bundles.

## Cloudflare

To get started with Cloudflare, you can use the [`unstable-vite-cloudflare`][template-vite-cloudflare] template:

```shellscript nonumber
npx create-remix@latest --template remix-run/remix/templates/unstable-vite-cloudflare
```

#### Bindings

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].
Then, you can access your bindings via `context.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;
const value = await MY_KV.get("my-key");
return json({ value });
}
```

#### Vite & Wrangler

There are two ways to run your Cloudflare app locally:

```shellscript nonumber
# Vite
remix vite:dev

# Wrangler
remix vite:build # build app before running wrangler
wranger pages dev ./build/client
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be 'wrangler'?

Copy link
Contributor Author

@pcattori pcattori Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is using wrangler via wrangler pages dev, not sure I understand your question.

Copy link
Contributor

@reichhartd reichhartd Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo wranger instead of wrangler 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh now I see it 😅 yep you're right

```

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.
To simulate the Cloudflare environment in Vite, Wrangler provides [Node proxies for resource bindings][wrangler-getbindingsproxy] which are automatically available when using the Remix Cloudflare adapter:

```ts filename=vite.config.ts lines=[3,10]
import {
unstable_vitePlugin as remix,
unstable_vitePluginAdapterCloudflare as cloudflare,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [
remix({
adapter: cloudflare(),
}),
],
});
```

<docs-info>Vite will not use your Cloudflare Pages Functions (`functions/*`) in development as those are purely for Wrangler routing.</docs-info>

## Splitting up client and server code

Remix lets you write code that [runs on both the client and the server][server-vs-client].
Expand Down Expand Up @@ -263,7 +318,7 @@ export default defineConfig({
});
```

#### Migrating from a custom server
#### Migrating a custom server

If you were using a custom server in development, you'll need to edit your custom server to use Vite's `connect` middleware.
This will delegate asset requests and initial render requests to Vite during development, letting you benefit from Vite's excellent DX even with a custom server.
Expand Down Expand Up @@ -349,6 +404,66 @@ node --loader tsm ./server.ts

Just remember that there might be some noticeable slowdown for initial server startup if you do this.

#### Migrating Cloudflare Functions

<docs-warning>

The Remix Vite plugin only officially supports [Cloudflare Pages][cloudflare-pages] which is specifically designed for fullstack applications, unlike [Cloudflare Workers Sites][cloudflare-workers-sites]. If you're currently on Cloudflare Workers Sites, refer to the [Cloudflare Pages migration guide][cloudflare-pages-migration-guide].

</docs-warning>

👉 **Add the Cloudflare adapter to your Vite config**

```ts filename=vite.config.ts lines=[3,10]
import {
unstable_vitePlugin as remix,
unstable_vitePluginAdapterCloudflare as cloudflare,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [
remix({
adapter: cloudflare(),
}),
],
});
```

Your Cloudflare app may be setting the [the Remix Config `server` field][remix-config-server] to generate a catch-all Cloudflare Function.
With Vite, this indirection is no longer necessary.
Instead, you can author a catch-all route directly for Cloudflare, just like how you would for Express or any other custom servers.

👉 **Create a catch-all route for Remix**

```ts filename=functions/[[page]].ts
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({
build,
getLoadContext: (context) => ({ env: context.env }),
});
```

While you'll mostly use Vite during development, you can also use Wrangler to preview and deploy your app.
To learn more, see [_Cloudflare > Vite & Wrangler_](#vite--wrangler).

👉 **Update your `package.json` scripts**

```json filename=package.json lines=[3-6]
{
"scripts": {
"dev": "remix vite:dev",
"build": "remix vite:build",
"preview": "wrangler pages dev ./build/client",
"deploy": "wrangler pages deploy ./build/client"
}
}
```

#### Migrate references to build output paths

When using the existing Remix compiler's default options, the server was compiled into `build` and the client was compiled into `public/build`. Due to differences with the way Vite typically works with its `public` directory compared to the existing Remix compiler, these output paths have changed.
Expand Down Expand Up @@ -916,6 +1031,23 @@ export default function BoundaryRoute() {

You would then nest all other routes within this, e.g. `app/routes/about.tsx` would become `app/routes/_boundary.about.tsx`, etc.

#### Wrangler errors in development

When using Cloudflare Pages, you may encounter the following error from `wrangler pages dev`:

```txt nonumber
ERROR: Your worker called response.clone(), but did not read the body of both clones.
This is wasteful, as it forces the system to buffer the entire response body
in memory, rather than streaming it through. This may cause your worker to be
unexpectedly terminated for going over the memory limit. If you only meant to
copy the response headers and metadata (e.g. in order to be able to modify
them), use `new Response(response.body, response)` instead.
```

This is a [known issue with Wrangler][cloudflare-request-clone-errors].

</docs-info>

## Acknowledgements

Vite is an amazing project, and we're grateful to the Vite team for their work.
Expand All @@ -938,8 +1070,7 @@ We're definitely late to the Vite party, but we're excited to be here now!

[vite]: https://vitejs.dev
[supported-with-some-deprecations]: #add-mdx-plugin
[template-vite]: https://github.com/remix-run/remix/tree/main/templates/unstable-vite
[template-vite-express]: https://github.com/remix-run/remix/tree/main/templates/unstable-vite-express
[template-vite-cloudflare]: https://github.com/remix-run/remix/tree/main/templates/unstable-vite-cloudflare
[remix-config]: ../file-conventions/remix-config
[app-directory]: ../file-conventions/remix-config#appdirectory
[assets-build-directory]: ../file-conventions/remix-config#assetsbuilddirectory
Expand Down Expand Up @@ -1007,3 +1138,13 @@ We're definitely late to the Vite party, but we're excited to be here now!
[hydrate-fallback]: ../route/hydrate-fallback
[react-canaries]: https://react.dev/blog/2023/05/03/react-canaries
[package-overrides]: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#overrides
[wrangler-toml-bindings]: https://developers.cloudflare.com/workers/wrangler/configuration/#bindings
[cloudflare-pages]: https://pages.cloudflare.com
[cloudflare-workers-sites]: https://developers.cloudflare.com/workers/configuration/sites
[cloudflare-pages-migration-guide]: https://developers.cloudflare.com/pages/migrations/migrating-from-workers
[cloudflare-request-clone-errors]: https://github.com/cloudflare/workers-sdk/issues/3259
[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://github.com/cloudflare/workers-sdk/pull/4523
[remix-config-server]: https://remix.run/docs/en/main/file-conventions/remix-config#server
3 changes: 2 additions & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"type-fest": "^4.0.0",
"typescript": "^5.1.0",
"vite-env-only": "^2.0.0",
"vite-tsconfig-paths": "^4.2.2"
"vite-tsconfig-paths": "^4.2.2",
"wrangler": "^3.24.0"
}
}
153 changes: 153 additions & 0 deletions integration/vite-cloudflare-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { test, expect } from "@playwright/test";
import getPort from "get-port";

import { VITE_CONFIG, createProject, using, viteDev } from "./helpers/vite.js";

test.describe("Vite / cloudflare", async () => {
let port: number;
let cwd: string;

test.beforeAll(async () => {
port = await getPort();
cwd = await createProject({
"package.json": JSON.stringify(
{
private: true,
sideEffects: false,
type: "module",
scripts: {
dev: "remix vite:dev",
build: "remix vite:build",
start: "wrangler pages dev ./build/client",
deploy: "wrangler pages deploy ./build/client",
typecheck: "tsc",
},
dependencies: {
"@remix-run/cloudflare": "*",
"@remix-run/cloudflare-pages": "*",
"@remix-run/react": "*",
isbot: "^4.1.0",
miniflare: "^3.20231030.4",
react: "^18.2.0",
"react-dom": "^18.2.0",
},
devDependencies: {
"@cloudflare/workers-types": "^4.20230518.0",
"@remix-run/dev": "*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"node-fetch": "^3.3.2",
typescript: "^5.1.6",
vite: "^5.0.0",
"vite-tsconfig-paths": "^4.2.1",
wrangler: "^3.24.0",
},
engines: {
node: ">=18.0.0",
},
},
null,
2
),
"vite.config.ts": await VITE_CONFIG({
port,
pluginOptions: `{ adapter: (await import("@remix-run/dev")).unstable_vitePluginAdapterCloudflare() }`,
}),
"functions/[[page]].ts": `
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// @ts-ignore - the server build file is generated by \`remix vite:build\`
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({
build,
getLoadContext: (context) => ({ env: context.env }),
});
`,
"wrangler.toml": `
kv_namespaces = [
{ id = "abc123", binding="MY_KV" }
]
`,
"app/routes/_index.tsx": `
import {
json,
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "@remix-run/cloudflare";
import { Form, useLoaderData } from "@remix-run/react";

const key = "__my-key__";

export async function loader({ context }: LoaderFunctionArgs) {
const { MY_KV } = context.env;
const value = await MY_KV.get(key);
return json({ value });
}

export async function action({ request, context }: ActionFunctionArgs) {
const { MY_KV: myKv } = context.env;

if (request.method === "POST") {
const formData = await request.formData();
const value = formData.get("value") as string;
await myKv.put(key, value);
return null;
}

if (request.method === "DELETE") {
await myKv.delete(key);
return null;
}

throw new Error(\`Method not supported: "\${request.method}"\`);
}

export default function Index() {
const { value } = useLoaderData<typeof loader>();
return (
<div>
<h1>Welcome to Remix</h1>
{value ? (
<>
<p data-text>Value: {value}</p>
<Form method="DELETE">
<button>Delete</button>
</Form>
</>
) : (
<>
<p data-text>No value</p>
<Form method="POST">
<label htmlFor="value">Set value:</label>
<input type="text" name="value" id="value" required />
<br />
<button>Save</button>
</Form>
</>
)}
</div>
);
}
`,
});
});

test("vite dev", async ({ page }) => {
await using(await viteDev({ cwd, port }), async () => {
let pageErrors: Error[] = [];
page.on("pageerror", (error) => pageErrors.push(error));

await page.goto(`http://localhost:${port}/`, {
waitUntil: "networkidle",
});
await expect(page.locator("[data-text]")).toHaveText("No value");

await page.getByLabel("Set value:").fill("my-value");
await page.getByRole("button").click();
await expect(page.locator("[data-text]")).toHaveText("Value: my-value");

expect(pageErrors).toEqual([]);
});
});
});
Loading
Loading