Skip to content

Commit

Permalink
feat: add cloudflare worker template to cli (#259)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkvasnicak authored Mar 28, 2024
1 parent b691796 commit 3875369
Show file tree
Hide file tree
Showing 17 changed files with 1,038 additions and 194 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-islands-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-frames": minor
---

feat: add cloudflare worker template
6 changes: 6 additions & 0 deletions .changeset/tiny-papayas-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"frames.js": patch
"docs": patch
---

fix: add missing ctx, env, and req access to cloudflare workers handlers
168 changes: 10 additions & 158 deletions docs/pages/reference/core/cloudflare-workers/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,157 +2,33 @@

Frames.js can be easily deployed to [Cloudflare Workers](https://workers.cloudflare.com).

## Create a new project using `wrangler`

:::code-group

```bash [npm]
npm create cloudflare -- my-cf-frames --type hello-world --ts && cd ./my-cf-frames
```

```bash [yarn]
yarn create cloudflare my-cf-frames --type hello-world --ts && cd ./my-cf-frames
```

```bash [pnpm]
pnpm create cloudflare my-cf-frames --type hello-world --ts && cd ./my-cf-frames
```

:::

## Install `frames.js`

In order for `frames.js` to work properly in [Cloudflare Workers](https://workers.cloudflare.com) you must replace the `@vercel/og` dependency with `workers-og`. Follow following steps to override the dependency.

::::steps

### Override `@vercel/og` package with `workers-og`

Add following to your `package.json`.

:::code-group

```json [npm]
{
"overrides": {
"frames.js": {
"@vercel/og": "npm:workers-og@^0.0.23"
}
}
}
```

```json [yarn]
{
"resolutions": {
"@vercel/og": "npm:workers-og"
}
}
```

```json [pnpm]
{
"pnpm": {
"overrides": {
"@vercel/og": "npm:workers-og@^0.0.23"
}
}
}
```

:::

### Install the dependencies

After you have overridden the `@vercel/og` package with `workers-og`, you can install the dependencies.
## Create a new project from template

:::code-group

```bash [npm]
npm install frames.js react
npm create frames -- --name my-cf-frames --template cloudflare-worker && cd ./my-cf-frames
```

```bash [yarn]
yarn add frames.js react
yarn create frames --name my-cf-frames --template cloudflare-worker && cd ./my-cf-frames
```

```bash [pnpm]
pnpm add frames.js react
pnpm create frames --name my-cf-frames --template cloudflare-worker && cd ./my-cf-frames
```

:::
::::

## Write your Frames handler

::::steps

### Delete generated file

Delete the `src/index.ts` file that was generated by `wrangler`.

### Create a file with your Frames app handler

Create `src/index.tsx` file and paste the following code inside.

```tsx [src/index.tsx]
import { createFrames, Button } from "frames.js/cloudflare-workers";
## Edit the generated Frame handler

const frames = createFrames();

const fetch = frames(async ({ message, searchParams }) => {
const hasClicked = !!(message && searchParams.clicked);

return {
image: <span>{hasClicked ? `Clicked ✅` : `Clicked ❌`}</span>,a
buttons: !hasClicked
? [
<Button action="post" target={{ query: { clicked: true } }}>
Click Me
</Button>,
]
: [
<Button action="post" target="/">
Reset
</Button>,
],
};
});

export default {
fetch,
} satisfies ExportedHandler;
```

### Configure Typescript to use React jsx runtime

Open `tsconfig.json` and change the value of `compilerOptions.tsx` to `react-jsx`.

```json [tsconfig.json]
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
```

### Change the entrypoint for your Frames handler

Open `wrangler.toml` and change the value of `main` to `src/index.tsx`.

```toml [wrangler.toml]
main = "src/index.tsx"
```

::::
Open `src/index.tsx` and edit the handler to your needs.

## Develop and test locally

You can test your Cloudflare Worker locally using `wrangler dev` and our [debugger](/guides/debugger#local-debugger-cli). Follow these steps to start developing locally:

::::steps

#### Start the local server
Run following command to start the local server and debugger to test your Frames app locally.

:::code-group

Expand All @@ -170,35 +46,9 @@ pnpm dev

:::

#### Start the debugger

After you started the local server, you can start the debugger by running the following command where you replace `<local-url>` with a URL on which local server is running (see the output of above command, e.g. `http://localhost:8787`).

:::code-group

```bash [npm]
npx @frames.js/debugger --url <local-url>
```

```bash [yarn]
# yarn v1 doesn't have an alternative to npx, so you have to install the debugger globally (or use npx)
yarn global add @frames.js/debugger && frames --url <local-url>

# yarn v2
yarn dlx @frames.js/debugger --url <local-url>
```

```bash [pnpm]
pnpm dlx @frames.js/debugger --url <local-url>
```

:::

::::

## Deploy to Cloudflare Workers

When you tested your Frames app locally and you are ready to deploy it to Cloudflare Workers, run the following command.
When you are done with development and testing, run the following command to deploy your Frames app to Cloudflare Workers.

:::code-group

Expand All @@ -215,3 +65,5 @@ pnpm deploy
```

:::

::::
4 changes: 3 additions & 1 deletion packages/frames.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
"src"
],
"devDependencies": {
"@cloudflare/workers-types": "^4.20240320.1",
"@open-frames/types": "^0.0.6",
"@remix-run/node": "^2.8.1",
"@types/supertest": "^6.0.2",
Expand All @@ -255,6 +256,7 @@
},
"license": "MIT",
"peerDependencies": {
"@cloudflare/workers-types": "^4.20240320.1",
"@xmtp/frames-validator": "^0.5.2",
"next": "^14.1.0",
"react": "^18.2.0",
Expand All @@ -266,4 +268,4 @@
"protobufjs": "^7.2.6",
"viem": "^2.7.8"
}
}
}
95 changes: 75 additions & 20 deletions packages/frames.js/src/cloudflare-workers/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
export { Button, type types } from "../core";
import { createFrames as coreCreateFrames, types } from "../core";
import { CoreMiddleware } from "../middleware";
import type { CoreMiddleware } from "../middleware";
import { Buffer } from "node:buffer";
import {
type CloudflareWorkersMiddleware,
cloudflareWorkersMiddleware,
} from "./middleware";
import type { ExportedHandlerFetchHandler } from "@cloudflare/workers-types";
import type {
FramesMiddleware,
FramesRequestHandlerFunction,
JsonValue,
} from "../core/types";

export { cloudflareWorkersMiddleware } from "./middleware";

// make Buffer available on globalThis so it is compatible with cloudflare workers
// eslint-disable-next-line no-undef
globalThis.Buffer = Buffer;

type CreateFramesForCloudflareWorkers = types.CreateFramesFunctionDefinition<
CoreMiddleware,
(req: Request) => Promise<Response>
>;
type DefaultMiddleware<TEnv> = [
...CoreMiddleware,
CloudflareWorkersMiddleware<TEnv>,
];

/**
* Creates Frames instance to use with you Hono server
Expand All @@ -30,27 +42,70 @@ type CreateFramesForCloudflareWorkers = types.CreateFramesFunctionDefinition<
* };
* });
*
* @example
* // With custom type for Env
* import { createFrames, Button } from 'frames.js/cloudflare-workers';
*
* type Env = {
* secret: string;
* };
*
* const frames = createFrames<Env>();
* const fetch = frames(async (ctx) => {
* return {
* image: <span>{ctx.cf.env.secret}</span>,
* buttons: [
* <Button action="post">
* Click me
* </Button>,
* ],
* };
* });
*
* export default {
* fetch,
* } satisfies ExportedHandler;
*/
// @ts-expect-error
export const createFrames: CreateFramesForCloudflareWorkers =
function createFramesForCloudflareWorkers(
options?: types.FramesOptions<any, any>
export function createFrames<
TEnv = unknown,
TFramesMiddleware extends
| FramesMiddleware<any, any>[]
| undefined = undefined,
TState extends JsonValue = JsonValue,
>(
options?: types.FramesOptions<TState, TFramesMiddleware>
): FramesRequestHandlerFunction<
TState,
DefaultMiddleware<TEnv>,
TFramesMiddleware,
ExportedHandlerFetchHandler<TEnv, unknown>
> {
return function cloudflareWorkersFramesHandler<
TPerRouteMiddleware extends
| types.FramesMiddleware<any, any>[]
| undefined = undefined,
>(
handler: types.FrameHandlerFunction<any, any>,
handlerOptions?: types.FramesRequestHandlerFunctionOptions<TPerRouteMiddleware>
) {
const frames = coreCreateFrames(options);

return function cloudflareWorkersFramesHandler<
TPerRouteMiddleware extends types.FramesMiddleware<any, any>[],
>(
handler: types.FrameHandlerFunction<any, any>,
handlerOptions?: types.FramesRequestHandlerFunctionOptions<TPerRouteMiddleware>
) {
return function handleCloudflareWorkersRequest(req, env, ctx) {
const frames = coreCreateFrames({
...options,
middleware: [
cloudflareWorkersMiddleware<TEnv>({
ctx,
env,
req,
}),
...(options?.middleware || []),
],
});
const framesHandler = frames(handler, handlerOptions);

return async function handleCloudflareWorkersRequest(req) {
return framesHandler(req);
};
return framesHandler(
// @ts-expect-error - req is almost compatible, there are some differences in the types but it mostly fits all the needs
req
) as unknown as ReturnType<ExportedHandlerFetchHandler<unknown>>;
};
};
}
Loading

0 comments on commit 3875369

Please sign in to comment.