Skip to content

Commit

Permalink
fix: allow to explicitly set state using generics
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkvasnicak committed Mar 28, 2024
1 parent 92987b2 commit dbabaa9
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-hounds-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"frames.js": patch
---

fix: allow to explicitly set state using generics
5 changes: 4 additions & 1 deletion .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ jobs:
- name: Install dependencies
run: yarn --frozen-lockfile

- name: Typecheck
- name: Build
run: yarn build:ci

- name: Typecheck
run: yarn typecheck

test:
needs: [lint, typecheck]
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"lint": "turbo lint --filter=!template-*",
"test:ci": "jest --ci",
"test": "cd ./packages/frames.js && npm run test:watch",
"typecheck": "turbo typecheck",
"publish-packages": "yarn build lint && changeset version && changeset publish && git push --follow-tags origin main",
"publish-canary": "turbo run build lint && cd ./packages/frames.js && yarn publish --tag canary && git push --follow-tags origin main",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
Expand Down Expand Up @@ -41,4 +42,4 @@
"templates/*"
],
"version": "0.3.0-canary.0"
}
}
4 changes: 3 additions & 1 deletion packages/frames.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"build": "NODE_OPTIONS='--max-old-space-size=16384' tsup",
"dev": "npm run build -- --watch",
"test:watch": "jest --watch",
"update:proto": "curl https://raw.githubusercontent.com/farcasterxyz/hub-monorepo/main/packages/core/src/protobufs/generated/message.ts -o src/farcaster/generated/message.ts"
"update:proto": "curl https://raw.githubusercontent.com/farcasterxyz/hub-monorepo/main/packages/core/src/protobufs/generated/message.ts -o src/farcaster/generated/message.ts",
"typecheck": "tsc --noEmit"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -257,6 +258,7 @@
"license": "MIT",
"peerDependencies": {
"@cloudflare/workers-types": "^4.20240320.1",
"@types/express": "^4.17.21",
"@xmtp/frames-validator": "^0.5.2",
"next": "^14.1.0",
"react": "^18.2.0",
Expand Down
28 changes: 28 additions & 0 deletions packages/frames.js/src/cloudflare-workers/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,32 @@ describe("cloudflare workers adapter", () => {
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
});

it('works properly with state', async () => {
type State = {
test: boolean;
};
const frames = lib.createFrames<State>({
initialState: {
test: false,
},
});

const handler = frames(async (ctx) => {
expect(ctx.state).toEqual({ test: false });

return {
image: 'http://test.png',
state: ctx.state satisfies State,
};
});

const request = new Request("http://localhost:3000");

// @ts-expect-error - expects fetcher property on request but it is not used by our lib
const response = await handler(request, {}, {});

expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
});
});
12 changes: 7 additions & 5 deletions packages/frames.js/src/cloudflare-workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ type DefaultMiddleware<TEnv> = [
* });
*
* @example
* // With custom type for Env
* import { createFrames, Button } from 'frames.js/cloudflare-workers';
* // With custom type for Env and state
* import { createFrames, Button, type types } from 'frames.js/cloudflare-workers';
*
* type Env = {
* secret: string;
* secret: string;
* };
*
* const frames = createFrames<Env>();
* type State = { test: boolean };
*
* const frames = createFrames<State, Env>();
* const fetch = frames(async (ctx) => {
* return {
* image: <span>{ctx.cf.env.secret}</span>,
Expand All @@ -67,11 +69,11 @@ type DefaultMiddleware<TEnv> = [
* } satisfies ExportedHandler;
*/
export function createFrames<
TState extends JsonValue | undefined = JsonValue | undefined,
TEnv = unknown,
TFramesMiddleware extends
| FramesMiddleware<any, any>[]
| undefined = undefined,
TState extends JsonValue = JsonValue,
>(
options?: types.FramesOptions<TState, TFramesMiddleware>
): FramesRequestHandlerFunction<
Expand Down
46 changes: 46 additions & 0 deletions packages/frames.js/src/cloudflare-workers/test.types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ExecutionContext, Request as CfRequest, ExportedHandlerFetchHandler } from '@cloudflare/workers-types';
import { createFrames, types } from '.';

const framesWithoutState = createFrames();
framesWithoutState(async (ctx) => {
ctx.initialState satisfies types.JsonValue | undefined;
ctx.state satisfies types.JsonValue | undefined;

return {
image: 'http://test.png',
};
}) satisfies ExportedHandlerFetchHandler;

const framesWithInferredState = createFrames({
initialState: { test: true },
});

framesWithInferredState(async (ctx) => {
ctx.state satisfies { test: boolean; };

return {
image: 'http://test.png',
};
}) satisfies ExportedHandlerFetchHandler;

const framesWithExplicitState = createFrames<{ test: boolean }>({});
framesWithExplicitState(async (ctx) => {
ctx.state satisfies { test: boolean };
ctx satisfies { initialState?: {test: boolean}; message?: any, pressedButton?: any };
ctx satisfies { cf: { env: unknown; ctx: ExecutionContext; req: CfRequest }}

return {
image: 'http://test.png',
};
}) satisfies ExportedHandlerFetchHandler;

const framesWithExplicitStateAndEnv = createFrames<{ test: boolean }, { secret: string }>({});
framesWithExplicitStateAndEnv(async (ctx) => {
ctx.state satisfies { test: boolean };
ctx satisfies { initialState?: { test: boolean }; message?: any, pressedButton?: any; request: Request; };
ctx satisfies { cf: { env: { secret: string }; ctx: ExecutionContext; req: CfRequest }}

return {
image: 'http://test.png',
};
}) satisfies ExportedHandlerFetchHandler<{ secret: string }>;
2 changes: 1 addition & 1 deletion packages/frames.js/src/core/createFrames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from "./types";

export function createFrames<
TState extends JsonValue | undefined,
TState extends JsonValue | undefined = JsonValue | undefined,
TMiddlewares extends FramesMiddleware<any, any>[] | undefined = undefined,
>({
basePath = "/",
Expand Down
46 changes: 46 additions & 0 deletions packages/frames.js/src/core/test.types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createFrames, types } from '.';

type Handler = (req: Request) => Promise<Response>;

Check warning on line 3 in packages/frames.js/src/core/test.types.tsx

View workflow job for this annotation

GitHub Actions / lint

'req' is defined but never used

const framesWithoutState = createFrames();
framesWithoutState(async (ctx) => {
ctx.initialState satisfies types.JsonValue | undefined;
ctx.state satisfies types.JsonValue | undefined;

return {
image: 'http://test.png',
};
}) satisfies Handler;

const framesWithInferredState = createFrames({
initialState: { test: true },
});

framesWithInferredState(async (ctx) => {
ctx.state satisfies { test: boolean };

return {
image: 'http://test.png',
};
}) satisfies Handler;

const framesWithExplicitState = createFrames<{ test: boolean }>({});
framesWithExplicitState(async (ctx) => {
ctx.state satisfies { test: boolean };
ctx satisfies { initialState?: {test: boolean}; message?: any, pressedButton?: any };

return {
image: 'http://test.png',
};
}) satisfies Handler;

const framesWithExplicitStateAndEnv = createFrames<{ test: boolean }>({});
framesWithExplicitStateAndEnv(async (ctx) => {
ctx.state satisfies { test: boolean };
ctx satisfies { initialState?: { test: boolean }; message?: any, pressedButton?: any; request: Request; };


return {
image: 'http://test.png',
};
}) satisfies Handler;
2 changes: 1 addition & 1 deletion packages/frames.js/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ export type CreateFramesFunctionDefinition<
| undefined,
TRequestHandlerFunction extends Function,
> = <
TState extends JsonValue | undefined = JsonValue | undefined,
TFrameMiddleware extends FramesMiddleware<any, any>[] | undefined = undefined,
TState extends JsonValue = JsonValue,
>(
options?: FramesOptions<TState, TFrameMiddleware>
) => FramesRequestHandlerFunction<
Expand Down
28 changes: 28 additions & 0 deletions packages/frames.js/src/express/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,32 @@ describe("express adapter", () => {
);
});
});

it('works properly with state', async () => {
type State = {
test: boolean;
};
const app = express();
const frames = lib.createFrames<State>({
initialState: {
test: false,
},
});

const expressHandler = frames(async (ctx) => {
expect(ctx.state).toEqual({ test: false });

return {
image: 'http://test.png',
state: ctx.state satisfies State,
};
});

app.use("/", expressHandler);

await request(app)
.get("/")
.expect("Content-Type", "text/html")
.expect(200);
});
});
49 changes: 49 additions & 0 deletions packages/frames.js/src/express/test.types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Handler } from 'express';
import { createFrames, types } from '.';

const framesWithoutState = createFrames();
framesWithoutState(async ctx => {
ctx.initialState satisfies types.JsonValue | undefined;
ctx.state satisfies types.JsonValue | undefined;

return {
image: 'http://test.png'
};
}) satisfies Handler;

const framesWithInferredState = createFrames({
initialState: {
test: true
}
});
framesWithInferredState(async ctx => {
ctx.initialState satisfies { test: boolean; };
ctx.state satisfies {
test: boolean;
};

return {
image: 'http://test.png'
};
}) satisfies Handler;

const framesWithExplicitState = createFrames<{
test: boolean;
}>({});
framesWithExplicitState(async ctx => {
ctx.state satisfies {
test: boolean;
};
ctx.initialState satisfies {
test: boolean;
};
ctx satisfies {
message?: any;
pressedButton?: any;
request: Request;
}

return {
image: 'http://test.png'
};
}) satisfies Handler;
31 changes: 31 additions & 0 deletions packages/frames.js/src/hono/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,35 @@ describe("hono adapter", () => {
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
});

it('works properly with state', async () => {
type State = {
test: boolean;
};
const frames = lib.createFrames<State>({
initialState: {
test: false,
},
});

const handler = frames(async (ctx) => {
expect(ctx.state).toEqual({ test: false });

return {
image: 'http://test.png',
state: ctx.state satisfies State,
};
});

const app = new Hono();

app.on(["GET", "POST"], "/", handler);

const request = new Request("http://localhost:3000");

const response = await app.request(request);

expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
});
});
Loading

0 comments on commit dbabaa9

Please sign in to comment.