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

fix: allow to explicitly set state using generics #262

Merged
merged 1 commit 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/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 @@
} from "./types";

export function createFrames<
TState extends JsonValue | undefined,
TState extends JsonValue | undefined = JsonValue | undefined,
TMiddlewares extends FramesMiddleware<any, any>[] | undefined = undefined,
>({
basePath = "/",
Expand All @@ -21,7 +21,7 @@
TState,
typeof coreMiddleware,
TMiddlewares,
(req: Request) => Promise<Response>

Check warning on line 24 in packages/frames.js/src/core/createFrames.ts

View workflow job for this annotation

GitHub Actions / lint

'req' is defined but never used
> {
const globalMiddleware: FramesMiddleware<TState, FramesContext<TState>>[] =
middleware || [];
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 @@ -2,8 +2,8 @@
import type { ClientProtocolId } from "../types";
import { Button } from "./components";

export type JsonObject = { [Key in string]: JsonValue } & {

Check warning on line 5 in packages/frames.js/src/core/types.ts

View workflow job for this annotation

GitHub Actions / lint

'Key' is defined but never used
[Key in string]?: JsonValue | undefined;

Check warning on line 6 in packages/frames.js/src/core/types.ts

View workflow job for this annotation

GitHub Actions / lint

'Key' is defined but never used
};

export type JsonArray = JsonValue[] | readonly JsonValue[];
Expand All @@ -20,12 +20,12 @@
Union extends unknown
? // The union type is used as the only argument to a function since the union
// of function arguments is an intersection.
(distributedUnion: Union) => void

Check warning on line 23 in packages/frames.js/src/core/types.ts

View workflow job for this annotation

GitHub Actions / lint

'distributedUnion' is defined but never used
: // This won't happen.
never
) extends // Infer the `Intersection` type since TypeScript represents the positional
// arguments of unions of functions as an intersection of the union.
(mergedIntersection: infer Intersection) => void

Check warning on line 28 in packages/frames.js/src/core/types.ts

View workflow job for this annotation

GitHub Actions / lint

'mergedIntersection' is defined but never used
? // The `& Union` is to allow indexing by the resulting type
Intersection & Union
: never;
Expand Down Expand Up @@ -212,8 +212,8 @@
| 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
Loading