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

feat: add cloudflare worker template to cli #259

Merged
merged 2 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading