From 9ef6e806e5dd0f3e715e4d4abe8d80a242f45318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 2 Apr 2024 12:14:12 +0200 Subject: [PATCH 01/14] build: change eslint configuration --- .eslintrc.js | 4 ---- packages/eslint-config/library.js | 22 +++++++++++++--------- packages/frames.js/package.json | 2 ++ turbo.json | 3 ++- yarn.lock | 31 ++++++++++++++++++++++++++++--- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9e9676e90..fd0ba955a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,11 +2,7 @@ /** @type {import("eslint").Linter.Config} */ module.exports = { extends: ["@framesjs/eslint-config/library.js"], - parser: "@typescript-eslint/parser", parserOptions: { project: true, }, - env: { - jest: true, - }, }; diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index c667cd100..f5c2f74e1 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -4,15 +4,12 @@ const project = resolve(process.cwd(), "tsconfig.json"); /** @type {import("eslint").Linter.Config} */ module.exports = { - extends: ["eslint:recommended", "prettier", "eslint-config-turbo"], - plugins: ["only-warn"], - globals: { - React: true, - JSX: true, - }, - env: { - node: true, - }, + extends: [ + require.resolve("@vercel/style-guide/eslint/node"), + require.resolve("@vercel/style-guide/eslint/jest-react"), + require.resolve("@vercel/style-guide/eslint/react"), + require.resolve("@vercel/style-guide/eslint/typescript"), + ], settings: { "import/resolver": { typescript: { @@ -31,4 +28,11 @@ module.exports = { files: ["*.js?(x)", "*.ts?(x)"], }, ], + rules: { + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-unnecessary-condition": "warn", + "react/jsx-sort-props": "warn", + "unicorn/filename-case": "off", + }, }; diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index 50d7e30f7..0baef3560 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -250,6 +250,7 @@ "@types/express": "^4.17.21", "@xmtp/frames-client": "^0.4.3", "@xmtp/frames-validator": "^0.5.2", + "@xmtp/proto": "3.45.0", "@xmtp/xmtp-js": "^11.5.0", "express": "^4.19.1", "hono": "^4.1.3", @@ -262,6 +263,7 @@ "@cloudflare/workers-types": "^4.20240320.1", "@types/express": "^4.17.21", "@xmtp/frames-validator": "^0.5.2", + "@xmtp/proto": "3.45.0", "next": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/turbo.json b/turbo.json index 240e4b961..54a90ce2f 100644 --- a/turbo.json +++ b/turbo.json @@ -7,7 +7,8 @@ "outputs": [".next/**", "**/dist/**", "!.next/cache/**"] }, "lint": { - "dependsOn": ["^lint"] + "dependsOn": ["^lint"], + "cache": false }, "dev": { "cache": false, diff --git a/yarn.lock b/yarn.lock index c8b9c8e47..9bc1e1c61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13456,7 +13456,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13563,7 +13572,14 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -15018,7 +15034,7 @@ wrangler@3.39.0, wrangler@^3.39.0: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15036,6 +15052,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 790bbacde5d47cdfaa6c2aefaffa8cc28205af91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 2 Apr 2024 12:55:47 +0200 Subject: [PATCH 02/14] fix: some eslint stuff --- .../src/cloudflare-workers/index.test.tsx | 2 +- .../frames.js/src/cloudflare-workers/index.ts | 25 ++++++---- .../src/cloudflare-workers/middleware.ts | 4 +- .../src/cloudflare-workers/test.types.tsx | 9 ++-- packages/frames.js/src/core/components.ts | 2 +- .../frames.js/src/core/createFrames.test.ts | 4 +- packages/frames.js/src/core/test.types.tsx | 7 +-- packages/frames.js/src/core/types.ts | 14 +++--- packages/frames.js/src/core/utils.ts | 33 +++++++----- .../src/middleware/renderResponse.test.tsx | 2 +- .../src/middleware/stateMiddleware.ts | 11 ++-- packages/frames.js/src/next/fetchMetadata.ts | 19 ++++--- .../frames.js/src/next/getCurrentUrl.test.ts | 7 ++- packages/frames.js/src/next/getCurrentUrl.ts | 4 +- packages/frames.js/src/next/index.ts | 8 ++- packages/frames.js/src/next/pages-router.tsx | 18 ++++--- packages/frames.js/src/next/server.tsx | 50 ++++++++++--------- packages/frames.js/src/next/test.types.tsx | 9 ++-- .../frames.js/src/remix/fetchMetadata.test.ts | 9 ++-- packages/frames.js/src/remix/fetchMetadata.ts | 8 +-- packages/frames.js/src/remix/index.ts | 8 ++- packages/frames.js/src/remix/test.types.tsx | 8 +-- packages/frames.js/src/types.ts | 3 +- packages/frames.js/src/utils.ts | 33 ++++++------ .../frames.js/src/validateFrameMessage.ts | 41 ++++++++++++--- packages/frames.js/src/xmtp/index.ts | 10 ++-- 26 files changed, 202 insertions(+), 146 deletions(-) diff --git a/packages/frames.js/src/cloudflare-workers/index.test.tsx b/packages/frames.js/src/cloudflare-workers/index.test.tsx index 053a6fc4e..75c3746b1 100644 --- a/packages/frames.js/src/cloudflare-workers/index.test.tsx +++ b/packages/frames.js/src/cloudflare-workers/index.test.tsx @@ -12,7 +12,7 @@ describe("cloudflare workers adapter", () => { return { image: Test, - buttons: [Click me], + buttons: [Click me], }; }); diff --git a/packages/frames.js/src/cloudflare-workers/index.ts b/packages/frames.js/src/cloudflare-workers/index.ts index f2fc9b7b8..7fd0270a8 100644 --- a/packages/frames.js/src/cloudflare-workers/index.ts +++ b/packages/frames.js/src/cloudflare-workers/index.ts @@ -1,22 +1,23 @@ -export { Button, type types } from "../core"; -import { createFrames as coreCreateFrames, types } from "../core"; -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 { types } from "../core"; +import { createFrames as coreCreateFrames } from "../core"; import type { FramesMiddleware, FramesRequestHandlerFunction, JsonValue, } from "../core/types"; +import type { CoreMiddleware } from "../middleware"; +import { + type CloudflareWorkersMiddleware, + cloudflareWorkersMiddleware, +} from "./middleware"; + +export { Button, type types } from "../core"; 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 DefaultMiddleware = [ @@ -28,6 +29,7 @@ type DefaultMiddleware = [ * Creates Frames instance to use with you Hono server * * @example + * ```ts * import { createFrames, Button } from 'frames.js/cloudflare-workers'; * * const frames = createFrames(); @@ -41,8 +43,10 @@ type DefaultMiddleware = [ * ], * }; * }); + * ``` * * @example + * ```ts * // With custom type for Env and state * import { createFrames, Button, type types } from 'frames.js/cloudflare-workers'; * @@ -67,6 +71,7 @@ type DefaultMiddleware = [ * export default { * fetch, * } satisfies ExportedHandler; + * ``` */ export function createFrames< TState extends JsonValue | undefined = JsonValue | undefined, @@ -80,7 +85,7 @@ export function createFrames< TState, DefaultMiddleware, TFramesMiddleware, - ExportedHandlerFetchHandler + ExportedHandlerFetchHandler > { return function cloudflareWorkersFramesHandler< TPerRouteMiddleware extends @@ -107,7 +112,7 @@ export function createFrames< 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>; + ) as unknown as ReturnType; }; }; } diff --git a/packages/frames.js/src/cloudflare-workers/middleware.ts b/packages/frames.js/src/cloudflare-workers/middleware.ts index fd323f265..73905a181 100644 --- a/packages/frames.js/src/cloudflare-workers/middleware.ts +++ b/packages/frames.js/src/cloudflare-workers/middleware.ts @@ -3,7 +3,7 @@ import type { IncomingRequestCfProperties, Request, } from "@cloudflare/workers-types"; -import type { FramesMiddleware } from "../core/types"; +import type { FramesMiddleware, JsonValue } from "../core/types"; type CloudflareWorkersMiddlewareContext = { /** @@ -34,7 +34,7 @@ type CloudflareWorkersMiddlewareOptions = { }; export type CloudflareWorkersMiddleware = FramesMiddleware< - any, + JsonValue | undefined, CloudflareWorkersMiddlewareContext >; diff --git a/packages/frames.js/src/cloudflare-workers/test.types.tsx b/packages/frames.js/src/cloudflare-workers/test.types.tsx index 1da5822f3..b124b0da7 100644 --- a/packages/frames.js/src/cloudflare-workers/test.types.tsx +++ b/packages/frames.js/src/cloudflare-workers/test.types.tsx @@ -1,5 +1,6 @@ -import { ExecutionContext, Request as CfRequest, ExportedHandlerFetchHandler } from '@cloudflare/workers-types'; -import { createFrames, types } from '.'; +import type { ExecutionContext, Request as CfRequest, ExportedHandlerFetchHandler } from '@cloudflare/workers-types'; +import type { types } from '.'; +import { createFrames } from '.'; const framesWithoutState = createFrames(); framesWithoutState(async (ctx) => { @@ -26,7 +27,7 @@ framesWithInferredState(async (ctx) => { const framesWithExplicitState = createFrames<{ test: boolean }>({}); framesWithExplicitState(async (ctx) => { ctx.state satisfies { test: boolean }; - ctx satisfies { initialState?: {test: boolean}; message?: any, pressedButton?: any }; + ctx satisfies { initialState?: {test: boolean}; message?: unknown, pressedButton?: unknown }; ctx satisfies { cf: { env: unknown; ctx: ExecutionContext; req: CfRequest }} return { @@ -37,7 +38,7 @@ framesWithExplicitState(async (ctx) => { 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 { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; ctx satisfies { cf: { env: { secret: string }; ctx: ExecutionContext; req: CfRequest }} return { diff --git a/packages/frames.js/src/core/components.ts b/packages/frames.js/src/core/components.ts index 2b2e380e2..a21bd94d4 100644 --- a/packages/frames.js/src/core/components.ts +++ b/packages/frames.js/src/core/components.ts @@ -1,4 +1,4 @@ -import type { UrlObject } from "url"; +import type { UrlObject } from "node:url"; type PostButtonProps = { /** A 256-byte string which is label of the button */ diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index 90b7ae3f5..24ae00a60 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -1,7 +1,7 @@ -import { createFrames } from "./createFrames"; import { concurrentMiddleware } from "../middleware/concurrentMiddleware"; +import { createFrames } from "./createFrames"; import { redirect } from "./redirect"; -import { FramesMiddleware } from "./types"; +import type { FramesMiddleware } from "./types"; describe("createFrames", () => { it("returns a handler function", () => { diff --git a/packages/frames.js/src/core/test.types.tsx b/packages/frames.js/src/core/test.types.tsx index 52580336c..d734474d2 100644 --- a/packages/frames.js/src/core/test.types.tsx +++ b/packages/frames.js/src/core/test.types.tsx @@ -1,4 +1,5 @@ -import { createFrames, types } from '.'; +import type { types } from '.'; +import { createFrames } from '.'; type Handler = (req: Request) => Promise; @@ -27,7 +28,7 @@ framesWithInferredState(async (ctx) => { const framesWithExplicitState = createFrames<{ test: boolean }>({}); framesWithExplicitState(async (ctx) => { ctx.state satisfies { test: boolean }; - ctx satisfies { initialState?: {test: boolean}; message?: any, pressedButton?: any }; + ctx satisfies { initialState?: {test: boolean}; message?: unknown, pressedButton?: unknown }; return { image: 'http://test.png', @@ -37,7 +38,7 @@ framesWithExplicitState(async (ctx) => { const framesWithExplicitStateAndEnv = createFrames<{ test: boolean }>({}); framesWithExplicitStateAndEnv(async (ctx) => { ctx.state satisfies { test: boolean }; - ctx satisfies { initialState?: { test: boolean }; message?: any, pressedButton?: any; request: Request; }; + ctx satisfies { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; return { diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index a3ed3f1bd..05c935c9a 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -1,6 +1,6 @@ -import { ImageResponse } from "@vercel/og"; +import type { ImageResponse } from "@vercel/og"; import type { ClientProtocolId } from "../types"; -import { Button } from "./components"; +import type { Button } from "./components"; export type JsonObject = { [Key in string]: JsonValue } & { [Key in string]?: JsonValue | undefined; @@ -67,7 +67,7 @@ export type FrameDefinition = { image: React.ReactElement | string; imageOptions?: { /** - * @default '1.91:1' + * @defaultValue '1.91:1' */ aspectRatio?: "1.91:1" | "1:1"; } & ConstructorParameters[1]; @@ -139,7 +139,7 @@ export type FrameHandlerFunction< export type FramesContextFromMiddlewares< TMiddlewares extends | FramesMiddleware[] - | ReadonlyArray>, + | readonly FramesMiddleware[], > = UnionToIntersection< { [K in keyof TMiddlewares]: TMiddlewares[K] extends FramesMiddleware< @@ -160,7 +160,7 @@ export type FramesRequestHandlerFunctionOptions< export type FramesRequestHandlerFunction< TState extends JsonValue | undefined, TDefaultMiddleware extends - | ReadonlyArray> + | readonly FramesMiddleware[] | FramesMiddleware[] | undefined, TFrameMiddleware extends FramesMiddleware[] | undefined, @@ -191,7 +191,7 @@ export type FramesOptions< > = { /** * All frame relative targets will be resolved relative to this - * @default '/'' + * @defaultValue '/' */ basePath?: string; /** @@ -207,7 +207,7 @@ export type FramesOptions< export type CreateFramesFunctionDefinition< TDefaultMiddleware extends - | ReadonlyArray> + | readonly FramesMiddleware[] | FramesMiddleware[] | undefined, TRequestHandlerFunction extends Function, diff --git a/packages/frames.js/src/core/utils.ts b/packages/frames.js/src/core/utils.ts index c29adae7b..fc93e4402 100644 --- a/packages/frames.js/src/core/utils.ts +++ b/packages/frames.js/src/core/utils.ts @@ -1,6 +1,6 @@ +import type { UrlObject } from "node:url"; import { formatUrl } from "./formatUrl"; -import { FrameDefinition, FrameRedirect } from "./types"; -import type { UrlObject } from "url"; +import type { FrameDefinition, FrameRedirect, JsonValue } from "./types"; const buttonActionToCode = { post: "p", @@ -9,7 +9,7 @@ const buttonActionToCode = { const BUTTON_INFORMATION_SEARCH_PARAM_NAME = "__bi"; -function isValidButtonIndex(index: any): index is 1 | 2 | 3 | 4 { +function isValidButtonIndex(index: unknown): index is 1 | 2 | 3 | 4 { return ( typeof index === "number" && !Number.isNaN(index) && @@ -18,7 +18,9 @@ function isValidButtonIndex(index: any): index is 1 | 2 | 3 | 4 { ); } -function isValidButtonAction(action: any): action is "post" | "post_redirect" { +function isValidButtonAction( + action: unknown +): action is "post" | "post_redirect" { return ( typeof action === "string" && (action === "post" || action === "post_redirect") @@ -38,7 +40,7 @@ export function generateTargetURL({ if ( target && - typeof target == "string" && + typeof target === "string" && (target.startsWith("http://") || target.startsWith("https://")) ) { // handle absolute urls @@ -46,7 +48,7 @@ export function generateTargetURL({ } else if (target && typeof target === "string") { // resolve target relatively to basePath const baseUrl = new URL(basePath, currentURL); - const preformatted = baseUrl.pathname + "/" + target; + const preformatted = `${baseUrl.pathname}/${target}`; const parts = preformatted.split("/").filter(Boolean); const finalPathname = parts.join("/"); @@ -115,9 +117,7 @@ type ButtonInformation = { }; export function parseSearchParams(url: URL): { - searchParams: { - [k: string]: string; - }; + searchParams: Record; } { const searchParams = Object.fromEntries(url.searchParams); @@ -159,10 +159,17 @@ export function parseButtonInformationFromTargetURL( }; } -export function isFrameRedirect(value: any): value is FrameRedirect { - return value && typeof value === "object" && value.kind === "redirect"; +export function isFrameRedirect(value: unknown): value is FrameRedirect { + return ( + value !== null && + typeof value === "object" && + "kind" in value && + value.kind === "redirect" + ); } -export function isFrameDefinition(value: any): value is FrameDefinition { - return value && typeof value === "object" && "image" in value; +export function isFrameDefinition( + value: unknown +): value is FrameDefinition { + return value !== null && typeof value === "object" && "image" in value; } diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index 144f0ce4d..03207b949 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -1,9 +1,9 @@ +import * as vercelOg from "@vercel/og"; import { FRAMES_META_TAGS_HEADER } from "../core"; import { Button } from "../core/components"; import { redirect } from "../core/redirect"; import type { FramesContext } from "../core/types"; import { renderResponse } from "./renderResponse"; -import * as vercelOg from "@vercel/og"; jest.mock("@vercel/og", () => { const arrayBufferMock = jest.fn(() => new ArrayBuffer(10)); diff --git a/packages/frames.js/src/middleware/stateMiddleware.ts b/packages/frames.js/src/middleware/stateMiddleware.ts index 1da2dc5dd..52a182b29 100644 --- a/packages/frames.js/src/middleware/stateMiddleware.ts +++ b/packages/frames.js/src/middleware/stateMiddleware.ts @@ -23,18 +23,19 @@ export function stateMiddleware< ctx.message && "state" in ctx.message ) { - let state: TState = ctx.initialState as TState; + let state: TState = ctx.initialState; // since we are stringifyng state to JSON in renderResponse middleware, we need to parse decode JSON here // so it is easy to use in middleware chain and frames handler - if (ctx.message.state) { + if (typeof ctx.message.state === "string") { try { - state = JSON.parse(ctx.message.state as unknown as string); + state = JSON.parse(ctx.message.state) as TState; } catch (e) { + // eslint-disable-next-line no-console -- provide feedback console.warn( "Failed to parse state from frame message, are you sure that the state was constructed by frames.js?" ); - state = ctx.initialState as TState; + state = ctx.initialState; } } @@ -44,7 +45,7 @@ export function stateMiddleware< } return next({ - state: ctx.initialState as TState, + state: ctx.initialState, }); }; } diff --git a/packages/frames.js/src/next/fetchMetadata.ts b/packages/frames.js/src/next/fetchMetadata.ts index 8be82c3f8..e2a71ad7e 100644 --- a/packages/frames.js/src/next/fetchMetadata.ts +++ b/packages/frames.js/src/next/fetchMetadata.ts @@ -1,11 +1,12 @@ +import type { Metadata } from "next"; import type { FrameFlattened } from "../types"; import { FRAMES_META_TAGS_HEADER } from "../core"; -import type { Metadata } from "next"; /** * Fetches meta tags from your Frames app that can be used in Next.js generateMetadata() function. * * @example + * ```ts * import type { Metadata } from "next"; * import { fetchMetadata } from "frames.js/next"; * @@ -22,8 +23,9 @@ import type { Metadata } from "next"; * ), * }, * }; + * ``` * - * @param url Full URL of your Frames app + * @param url - Full URL of your Frames app */ export async function fetchMetadata( url: URL | string @@ -38,18 +40,23 @@ export async function fetchMetadata( cache: "no-cache", }); - if (response?.ok) { + if (response.ok) { // process the JSON value to nextjs compatible format const flattenedFrame: FrameFlattened = await response.json(); return flattenedFrame as NonNullable; - } else if (response?.status) { + } else if (response.status) { + // eslint-disable-next-line no-console -- provide feedback console.warn( - `Failed to fetch frame metadata from ${url}. Status code: ${response.status}` + `Failed to fetch frame metadata from ${url.toString()}. Status code: ${response.status}` ); } } catch (error) { - console.warn(`Failed to fetch frame metadata from ${url}.`, error); + // eslint-disable-next-line no-console -- provide feedback + console.warn( + `Failed to fetch frame metadata from ${url.toString()}.`, + error + ); } return {}; diff --git a/packages/frames.js/src/next/getCurrentUrl.test.ts b/packages/frames.js/src/next/getCurrentUrl.test.ts index dd1008692..49441a002 100644 --- a/packages/frames.js/src/next/getCurrentUrl.test.ts +++ b/packages/frames.js/src/next/getCurrentUrl.test.ts @@ -1,9 +1,8 @@ -/* eslint-disable turbo/no-undeclared-env-vars */ import { getCurrentUrl } from "./getCurrentUrl"; describe("getCurrentUrl", () => { beforeEach(() => { - // @ts-expect-error + // @ts-expect-error -- this works process.env.NODE_ENV = "test"; }); @@ -38,7 +37,7 @@ describe("getCurrentUrl", () => { it("takes value from process.env.VERCEL_URL and uses https if NODE_ENV=production if available", () => { process.env.VERCEL_URL = "test.com"; - // @ts-expect-error + // @ts-expect-error -- this works process.env.NODE_ENV = "production"; expect( @@ -66,7 +65,7 @@ describe("getCurrentUrl", () => { it("takes value from process.env.APP_URL and uses https if NODE_ENV=production if available", () => { process.env.APP_URL = "test.com"; - // @ts-expect-error + // @ts-expect-error -- this works process.env.NODE_ENV = "production"; expect( diff --git a/packages/frames.js/src/next/getCurrentUrl.ts b/packages/frames.js/src/next/getCurrentUrl.ts index fcf35c982..fc8c819b8 100644 --- a/packages/frames.js/src/next/getCurrentUrl.ts +++ b/packages/frames.js/src/next/getCurrentUrl.ts @@ -1,5 +1,5 @@ -import { IncomingMessage } from "http"; -import { NextApiRequest } from "next"; +import type { IncomingMessage } from "node:http"; +import type { NextApiRequest } from "next"; import type { NextRequest } from "next/server.js"; export function getCurrentUrl( diff --git a/packages/frames.js/src/next/index.ts b/packages/frames.js/src/next/index.ts index 8d79ebc79..06ddef69d 100644 --- a/packages/frames.js/src/next/index.ts +++ b/packages/frames.js/src/next/index.ts @@ -1,7 +1,9 @@ -import { createFrames as coreCreateFrames, types } from "../core"; import type { NextRequest, NextResponse } from "next/server"; +import type { types } from "../core"; +import { createFrames as coreCreateFrames } from "../core"; import type { CoreMiddleware } from "../middleware"; import { getCurrentUrl } from "./getCurrentUrl"; + export { Button, type types } from "../core"; export { fetchMetadata } from "./fetchMetadata"; @@ -15,6 +17,7 @@ type CreateFramesForNextJS = types.CreateFramesFunctionDefinition< * Creates Frames instance to use with you Next.js server * * @example + * ```tsx * import { createFrames, Button } from 'frames.js/next'; * import { NextApiRequest, NextApiResponse } from 'next'; * @@ -32,8 +35,9 @@ type CreateFramesForNextJS = types.CreateFramesFunctionDefinition< * * export const GET = nextHandler; * export const POST = nextHandler; + * ``` */ -// @ts-expect-error +// @ts-expect-error -- this is correct but the function does not satisfy the type export const createFrames: CreateFramesForNextJS = function createFramesForNextJS(options?: types.FramesOptions) { const frames = coreCreateFrames(options); diff --git a/packages/frames.js/src/next/pages-router.tsx b/packages/frames.js/src/next/pages-router.tsx index bf229f862..31d650ca5 100644 --- a/packages/frames.js/src/next/pages-router.tsx +++ b/packages/frames.js/src/next/pages-router.tsx @@ -1,11 +1,11 @@ import type { Metadata, NextApiRequest, NextApiResponse } from "next"; +import React from "react"; import { createFrames as coreCreateFrames } from "../core"; import { createReadableStreamFromReadable, writeReadableStreamToWritable, } from "../lib/stream-pump"; export { Button, type types } from "../core"; -import React from "react"; export { fetchMetadata } from "./fetchMetadata"; @@ -13,7 +13,7 @@ export const createFrames: typeof coreCreateFrames = function createFramesForNextJSPagesRouter(options: any) { const frames = coreCreateFrames(options); - // @ts-expect-error + // @ts-expect-error -- this is correct but the function does not satisfy the type return function createHandler(handler, handlerOptions) { const requestHandler = frames(handler, handlerOptions); @@ -31,6 +31,7 @@ export const createFrames: typeof coreCreateFrames = * Converts metadata returned from fetchMetadata() call to Next.js compatible components. * * @example + * ```tsx * import { fetchMetadata, metadataToMetaTags } from "frames.js/next/pages-router"; * * export const getServerSideProps = async function getServerSideProps() { @@ -54,8 +55,9 @@ export const createFrames: typeof coreCreateFrames = * * ); * } + * ``` */ -export function metadataToMetaTags(metadata: NonNullable) { +export function metadataToMetaTags(metadata: NonNullable): React.JSX.Element { return ( <> {Object.entries(metadata).map(([key, value]) => { @@ -107,12 +109,12 @@ function createRequest(req: NextApiRequest, res: NextApiResponse): Request { export function createRequestHeaders( requestHeaders: NextApiRequest["headers"] ): Headers { - let headers = new Headers(); + const headers = new Headers(); - for (let [key, values] of Object.entries(requestHeaders)) { + for (const [key, values] of Object.entries(requestHeaders)) { if (values) { if (Array.isArray(values)) { - for (let value of values) { + for (const value of values) { headers.append(key, value); } } else { @@ -124,11 +126,11 @@ export function createRequestHeaders( return headers; } -async function sendResponse(res: NextApiResponse, response: Response) { +async function sendResponse(res: NextApiResponse, response: Response): Promise { res.statusMessage = response.statusText; res.status(response.status); - for (let [key, value] of response.headers.entries()) { + for (const [key, value] of response.headers.entries()) { res.setHeader(key, value); } diff --git a/packages/frames.js/src/next/server.tsx b/packages/frames.js/src/next/server.tsx index 2b4f945ac..12c722129 100644 --- a/packages/frames.js/src/next/server.tsx +++ b/packages/frames.js/src/next/server.tsx @@ -1,10 +1,11 @@ -import { FrameActionMessage } from "../farcaster"; import { headers } from "next/headers"; +import { ImageResponse } from "@vercel/og"; import { type NextRequest, NextResponse as NextResponseBase, } from "next/server"; import React from "react"; +import type { FrameActionMessage } from "../farcaster"; import { type FrameMessageReturnType, type GetFrameMessageOptions, @@ -31,10 +32,9 @@ import type { RedirectMap, RedirectHandler, } from "./types"; + export * from "./types"; -import { ImageResponse } from "@vercel/og"; -import type { SatoriOptions } from "satori"; // this is ugly hack to go around the issue https://github.com/vercel/next.js/pull/61721 const NextResponse = ( @@ -60,7 +60,7 @@ export async function validateActionSignature( if (options?.hubHttpUrl) { if (!options.hubHttpUrl.startsWith("http")) { throw new Error( - `frames.js: Invalid Hub URL: ${options?.hubHttpUrl}, ensure you have included the protocol (e.g. https://)` + `frames.js: Invalid Hub URL: ${options.hubHttpUrl}, ensure you have included the protocol (e.g. https://)` ); } } @@ -101,6 +101,7 @@ export async function getFrameMessage( } if (!frameActionPayload) { + // eslint-disable-next-line no-console -- provide a feedback to user on server console.log( "info: no frameActionPayload, this is expected for the homeframe" ); @@ -199,9 +200,9 @@ export function parseFrameParams( /** * - * @param reducer a function taking a state and action and returning another action. This reducer is always called in the Frame to compute the state by calling it with the previous Frame + action - * @param initialState the initial state to use if there was no previous action - * @param initializerArg the previousFrame object to use to initialize the state + * @param reducer - a function taking a state and action and returning another action. This reducer is always called in the Frame to compute the state by calling it with the previous Frame + action + * @param initialState - the initial state to use if there was no previous action + * @param initializerArg - the previousFrame object to use to initialize the state * @returns An array of [State, Dispatch] where State is your reducer state, and dispatch is a function that doesn't do anything atm * * @deprecated please upgrade to new API, see https://framesjs.org/reference/core/next. @@ -249,6 +250,7 @@ function toUrl(req: NextRequest) { reqUrl.host = host || reqUrl.host; reqUrl.port = port; + // eslint-disable-next-line no-console -- provide feedback to user on server console.info( `frames.js: NEXT_PUBLIC_HOST not set, using x-forwarded-* headers for redirect (${reqUrl.toString()})` ); @@ -259,7 +261,7 @@ function toUrl(req: NextRequest) { /** * A function ready made for next.js in order to directly export it, which handles all incoming `POST` requests that apps will trigger when users press buttons in your Frame. * It handles all the redirecting for you, correctly, based on the props defined by the Frame that triggered the user action. - * @param req a `NextRequest` object from `next/server` (Next.js app router server components) + * @param req - a `NextRequest` object from `next/server` (Next.js app router server components) * @returns NextResponse * * @deprecated please upgrade to new API, see https://framesjs.org/reference/core/next. @@ -398,7 +400,7 @@ function isElementFrameInput( * * A React functional component that Wraps a Frame and processes it, validating certain properties of the Frames spec, as well as adding other props. It also generates the postUrl. * It throws an error if the Frame is invalid, which can be caught by using an error boundary. - * @param param0 + * * @returns React.JSXElement */ export function FrameContainer({ @@ -412,7 +414,7 @@ export function FrameContainer({ /** Either a relative e.g. "/frames" or an absolute path, e.g. "https://google.com/frames" */ postUrl: string; /** The elements to include in the Frame */ - children: Array | ChildType; + children: (ChildType | null)[] | ChildType; /** The current reducer state object, returned from useFramesReducer */ state: T; previousFrame: PreviousFrame; @@ -421,10 +423,12 @@ export function FrameContainer({ /** Client protocols to accept */ accepts?: ClientProtocolId[]; }) { - if (!pathname) + if (!pathname) { + // eslint-disable-next-line no-console -- provide feedback to user on server console.warn( "frames.js: warning: You have not specified a `pathname` prop on your . This is not recommended, as it will default to the root path and not work if your frame is being rendered at a different path. Please specify a `pathname` prop on your ." ); + } const nextIndexByComponentType: Record< "button" | "image" | "input", @@ -434,7 +438,7 @@ export function FrameContainer({ image: 1, input: 1, }; - let redirectMap: RedirectMap = {}; + const redirectMap: RedirectMap = {}; function createURLForPOSTHandler(target: string): URL { let url: URL; @@ -574,6 +578,7 @@ export function FrameContainer({ const postUrlFull = `${postUrlRoute}?${searchParams.toString()}`; if (getByteLength(postUrlFull) > 256) { + // eslint-disable-next-line no-console -- provide feedback to user on server console.error( `post_url is too long. ${postUrlFull.length} bytes, max is 256. The url is generated to include your state and the redirect urls in ({ {...accepts?.map(({ id, version }) => ( - + )) ?? []} {newTree} @@ -598,15 +603,14 @@ export function FrameContainer({ * * @deprecated please upgrade to new API, see https://framesjs.org/reference/core/next */ -export function FrameButton(props: FrameButtonProvidedProps) { - return null; -} +export const FrameButton: React.FunctionComponent = () => null; /** An internal component that handles FrameButtons */ function FFrameButtonShim({ actionIndex, target, action = "post", + // eslint-disable-next-line camelcase -- this is according to the spec post_url, children, }: FrameButtonProvidedProps & FrameButtonAutomatedProps) { @@ -635,11 +639,9 @@ function FFrameButtonShim({ * * @deprecated please upgrade to new API, see https://framesjs.org/reference/core/next */ -export function FrameInput({ text }: { text: string }) { +export function FrameInput({ text }:{ text: string }): React.JSX.Element { return ( - <> - - + ); } @@ -659,10 +661,10 @@ export async function FrameImage( | { /** Children to pass to satori to render to PNG. [Supports tailwind](https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images#tailwind-css-support) via the `tw=` prop instead of `className` */ children: React.ReactNode; - options?: SatoriOptions; + options?: ConstructorParameters[1]; } ) -) { +): Promise { let imgSrc: string; if ("children" in props) { @@ -720,11 +722,11 @@ export async function FrameImage( <> - {props.aspectRatio && ( + {Boolean(props.aspectRatio) && ( + /> )} ); diff --git a/packages/frames.js/src/next/test.types.tsx b/packages/frames.js/src/next/test.types.tsx index f808040f4..2c16ad0e8 100644 --- a/packages/frames.js/src/next/test.types.tsx +++ b/packages/frames.js/src/next/test.types.tsx @@ -1,5 +1,6 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { createFrames, types } from '.'; +import type { NextRequest, NextResponse } from 'next/server'; +import type { types } from '.'; +import { createFrames } from '.'; type Handler = (req: NextRequest) => Promise; @@ -40,8 +41,8 @@ framesWithExplicitState(async ctx => { test: boolean; }; ctx satisfies { - message?: any; - pressedButton?: any; + message?: unknown; + pressedButton?: unknown; request: Request; } diff --git a/packages/frames.js/src/remix/fetchMetadata.test.ts b/packages/frames.js/src/remix/fetchMetadata.test.ts index 3c909d013..6c8582580 100644 --- a/packages/frames.js/src/remix/fetchMetadata.test.ts +++ b/packages/frames.js/src/remix/fetchMetadata.test.ts @@ -1,14 +1,15 @@ -import nock from "nock"; -import { fetchMetadata } from "./fetchMetadata"; +// eslint-disable-next-line import/no-extraneous-dependencies -- this is dev dependency of root package +import nock, { disableNetConnect, enableNetConnect } from "nock"; import type { FrameFlattened } from ".."; +import { fetchMetadata } from "./fetchMetadata"; describe("fetchMetaData", () => { beforeAll(() => { - nock.disableNetConnect(); + disableNetConnect(); }); afterAll(() => { - nock.enableNetConnect(); + enableNetConnect(); }); it("returns correct metadata from flattened frame", async () => { diff --git a/packages/frames.js/src/remix/fetchMetadata.ts b/packages/frames.js/src/remix/fetchMetadata.ts index 08d5d1935..9d6c7760f 100644 --- a/packages/frames.js/src/remix/fetchMetadata.ts +++ b/packages/frames.js/src/remix/fetchMetadata.ts @@ -1,6 +1,6 @@ +import type { MetaFunction } from "@remix-run/node"; import type { FrameFlattened } from "../types"; import { FRAMES_META_TAGS_HEADER } from "../core"; -import type { MetaFunction } from "@remix-run/node"; type Metadata = ReturnType; @@ -8,6 +8,7 @@ type Metadata = ReturnType; * Fetches meta tags from your Frames app that can be used in Remix meta() function. * * @example + * ```ts * import { fetchMetadata } from "frames.js/remix"; * * export async function loader({ request }: LoaderFunctionArgs) { @@ -22,8 +23,9 @@ type Metadata = ReturnType; * ...data.metaTags, * ]; * }; + * ``` * - * @param url Full URL of your Frames app + * @param url - Full URL of your Frames app */ export async function fetchMetadata(url: URL | string): Promise { const response = await fetch(url, { @@ -48,6 +50,6 @@ export async function fetchMetadata(url: URL | string): Promise { } throw new Error( - `Failed to fetch frames metadata from ${url}. The server returned ${response.status} ${response.statusText} response.` + `Failed to fetch frames metadata from ${url.toString()}. The server returned ${response.status} ${response.statusText} response.` ); } diff --git a/packages/frames.js/src/remix/index.ts b/packages/frames.js/src/remix/index.ts index 7a17f14a3..35772c3b9 100644 --- a/packages/frames.js/src/remix/index.ts +++ b/packages/frames.js/src/remix/index.ts @@ -1,6 +1,8 @@ import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; -import { createFrames as coreCreateFrames, types } from "../core"; +import type { types } from "../core"; +import { createFrames as coreCreateFrames } from "../core"; import type { CoreMiddleware } from "../middleware"; + export { Button, type types } from "../core"; export { fetchMetadata } from "./fetchMetadata"; @@ -14,6 +16,7 @@ type CreateFramesForRemix = types.CreateFramesFunctionDefinition< * Creates Frames instance to use with you Remix server * * @example + * ```tsx * import { createFrames, Button } from 'frames.js/remix'; * import type { LoaderFunction, ActionFunction } from '@remix-run/node'; * @@ -31,8 +34,9 @@ type CreateFramesForRemix = types.CreateFramesFunctionDefinition< * * export const loader: LoaderFunction = remixHandler; * export const action: ActionFunction = remixHandler; + * ``` */ -// @ts-expect-error +// @ts-expect-error -- the function works fine but somehow it does not satisfy the expected type export const createFrames: CreateFramesForRemix = function createFramesForRemix( options?: types.FramesOptions ) { diff --git a/packages/frames.js/src/remix/test.types.tsx b/packages/frames.js/src/remix/test.types.tsx index e167c5d37..1e36e389b 100644 --- a/packages/frames.js/src/remix/test.types.tsx +++ b/packages/frames.js/src/remix/test.types.tsx @@ -1,5 +1,5 @@ -import { ActionFunction, LoaderFunction } from '@remix-run/node'; -import { createFrames, types } from '.'; +import type { ActionFunction, LoaderFunction } from '@remix-run/node'; +import { createFrames, type types } from '.'; type Handler = LoaderFunction | ActionFunction; @@ -40,8 +40,8 @@ framesWithExplicitState(async ctx => { test: boolean; }; ctx satisfies { - message?: any; - pressedButton?: any; + message?: unknown; + pressedButton?: unknown; request: Request; } diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index de4c10566..0dfe39139 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -133,7 +133,6 @@ export type FrameButtonPostRedirect = FrameButtonPost; export type FrameButton = | FrameButtonPost | FrameButtonLink - | FrameButtonPostRedirect | FrameButtonMint | FrameButtonTx; @@ -224,7 +223,7 @@ export type FrameActionPayload = { /** an optional transaction id property. For Ethereum, this must be the transaction hash. For other chains, this is not yet specified. */ transactionId?: string; }; - /** Open Frames spec: the identifier and version of the client protocol that sent the request e.g. farcaster@vNext */ + /** Open Frames spec: the identifier and version of the client protocol that sent the request e.g. farcaster\@vNext */ clientProtocol?: string; }; diff --git a/packages/frames.js/src/utils.ts b/packages/frames.js/src/utils.ts index 74664a627..58dc47f24 100644 --- a/packages/frames.js/src/utils.ts +++ b/packages/frames.js/src/utils.ts @@ -1,4 +1,5 @@ -import { CastId, Message, MessageType, Protocol } from "./farcaster"; +import type { CastId } from "./farcaster"; +import { Message, MessageType, Protocol } from "./farcaster"; import type { FrameActionPayload, FrameButton, @@ -26,7 +27,7 @@ export function isFrameButtonMint( } export function bytesToHexString(bytes: Uint8Array): `0x${string}` { - return ("0x" + Buffer.from(bytes).toString("hex")) as `0x${string}`; + return `0x${Buffer.from(bytes).toString("hex")}`; } export function getByteLength(str: string): number { @@ -34,9 +35,13 @@ export function getByteLength(str: string): number { } export function hexStringToUint8Array(hexstring: string): Uint8Array { - return new Uint8Array( - hexstring.match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16)) - ); + const matches = hexstring.match(/.{1,2}/g); + + if (!matches) { + throw new Error("Invalid hex string provided"); + } + + return new Uint8Array(matches.map((byte: string) => parseInt(byte, 16))); } export function normalizeCastId(castId: CastId): { @@ -55,14 +60,12 @@ export function normalizeCastId(castId: CastId): { export function getFrameMessageFromRequestBody( body: FrameActionPayload ): Message { - return Message.decode( - Buffer.from(body?.trustedData?.messageBytes ?? "", "hex") - ); + return Message.decode(Buffer.from(body.trustedData.messageBytes, "hex")); } /** * Validates whether the version param is valid - * @param version the version string to validate + * @param version - the version string to validate * @returns true if the provided version conforms to the Frames spec */ export function isValidVersion(version: string): boolean { @@ -104,7 +107,7 @@ export function getEnumKeyByEnumValue< } export function extractAddressFromJSONMessage( - message: any + message: unknown ): `0x${string}` | null { const { data } = Message.fromJSON(message); @@ -128,13 +131,5 @@ export function extractAddressFromJSONMessage( return null; } - /** - * This is ugly hack but we want to return the address as a string that is expected by the users ( essentially what they see in the response from the hub ). - * We could use Buffer.from(data.verificationAddAddressBody.address).toString('base64') here but that results in different base64. - * Therefore we return address from source message and not from decoded message. - * - * For example for value 0x8d25687829d6b85d9e0020b8c89e3ca24de20a89 from API we get 0x8d25687829d6b85d9e0020b8c89e3ca24de20a8w== from Buffer.from(...).toString('base64'). - * The values are the same if you compare them as Buffer.from(a).equals(Buffer.from(b)). - */ - return message.data.verificationAddAddressBody.address; + return bytesToHexString(data.verificationAddAddressBody.address); } diff --git a/packages/frames.js/src/validateFrameMessage.ts b/packages/frames.js/src/validateFrameMessage.ts index ec073595b..d10ea0262 100644 --- a/packages/frames.js/src/validateFrameMessage.ts +++ b/packages/frames.js/src/validateFrameMessage.ts @@ -1,7 +1,27 @@ import type { FrameActionPayload, HubHttpUrlOptions } from "./types"; import { hexStringToUint8Array } from "./utils"; import { DEFAULT_HUB_API_KEY, DEFAULT_HUB_API_URL } from "./default"; -import { type FrameActionMessage, Message } from "./farcaster"; +import type { FrameActionMessage } from "./farcaster"; +import { Message } from "./farcaster"; + +type ValidateMessageJson = { + valid: boolean; + message: Message; +}; + +function isValidateMessageJson(value: unknown): value is ValidateMessageJson { + if (typeof value !== "object" || !value) { + return false; + } + + const validateMessageJson = value as ValidateMessageJson; + + return ( + typeof validateMessageJson.valid === "boolean" && + validateMessageJson.valid && + typeof validateMessageJson.message === "string" + ); +} /** * @returns a Promise that resolves with whether the message signature is valid, by querying a Farcaster hub, as well as the message itself @@ -40,7 +60,14 @@ export async function validateFrameMessage( } ); - const validateMessageJson = await validateMessageResponse.json(); + const validateMessageJson: unknown = await validateMessageResponse.json(); + + if (!isValidateMessageJson(validateMessageJson)) { + return { + isValid: false, + message: undefined, + }; + } if (validateMessageJson.valid) { return { @@ -49,10 +76,10 @@ export async function validateFrameMessage( validateMessageJson.message ) as FrameActionMessage, }; - } else { - return { - isValid: false, - message: undefined, - }; } + + return { + isValid: false, + message: undefined, + }; } diff --git a/packages/frames.js/src/xmtp/index.ts b/packages/frames.js/src/xmtp/index.ts index c776bcf57..54358e365 100644 --- a/packages/frames.js/src/xmtp/index.ts +++ b/packages/frames.js/src/xmtp/index.ts @@ -1,8 +1,6 @@ -import { - validateFramesPost, - XmtpOpenFramesRequest, -} from "@xmtp/frames-validator"; -import { frames } from "@xmtp/proto"; +import type { XmtpOpenFramesRequest } from "@xmtp/frames-validator"; +import { validateFramesPost } from "@xmtp/frames-validator"; +import type { frames } from "@xmtp/proto"; export type XmtpFrameMessageReturnType = frames.FrameActionBody & { verifiedWalletAddress: string; @@ -25,7 +23,7 @@ export async function getXmtpFrameMessage( ): Promise { const { actionBody, verifiedWalletAddress } = await validateFramesPost({ ...frameActionPayload, - clientProtocol: frameActionPayload.clientProtocol as `xmtp@${string}`, + clientProtocol: frameActionPayload.clientProtocol, }); return { From 31364b38822dce47b40f46ff05b58ba96e20ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 2 Apr 2024 13:07:45 +0200 Subject: [PATCH 03/14] build: do not require strict check --- .eslintignore | 1 + packages/eslint-config/library.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..5e94b0c8c --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +packages/frames.js/src/farcaster/generated/** \ No newline at end of file diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index f5c2f74e1..8b66f3684 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -31,8 +31,11 @@ module.exports = { rules: { "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/require-await": "warn", + "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unnecessary-condition": "warn", - "react/jsx-sort-props": "warn", + "jest/expect-expect": "warn", + "react/jsx-sort-props": "off", "unicorn/filename-case": "off", + eqeqeq: "off", }, }; From 0bb5ee368a0b42356addbe729f51e3f6cc870300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 2 Apr 2024 16:40:40 +0200 Subject: [PATCH 04/14] fix: more errors fix: even more issues --- .eslintignore | 1 - .eslintrc.js | 1 + packages/eslint-config/library.js | 2 + .../src/cloudflare-workers/index.test.tsx | 4 +- .../src/cloudflare-workers/test.types.tsx | 12 ++ packages/frames.js/src/core/components.ts | 4 +- .../src/core/composeMiddleware.test.ts | 11 +- .../frames.js/src/core/composeMiddleware.ts | 4 +- .../frames.js/src/core/createFrames.test.ts | 30 +++-- packages/frames.js/src/core/createFrames.ts | 10 +- packages/frames.js/src/core/formatUrl.ts | 33 +++--- packages/frames.js/src/core/redirect.ts | 2 +- packages/frames.js/src/core/test.types.tsx | 14 ++- packages/frames.js/src/core/types.ts | 14 ++- packages/frames.js/src/core/utils.ts | 12 +- packages/frames.js/src/express/index.test.tsx | 26 ++-- packages/frames.js/src/express/index.ts | 49 +++++--- packages/frames.js/src/express/test.types.tsx | 31 ++++- packages/frames.js/src/farcaster/types.ts | 4 +- .../frames.js/src/getAddressForFid.test.ts | 7 +- packages/frames.js/src/getAddressForFid.ts | 4 + .../frames.js/src/getAddressesForFid.test.ts | 5 +- packages/frames.js/src/getAddressesForFid.ts | 4 +- packages/frames.js/src/getFrame.test.ts | 8 +- packages/frames.js/src/getFrame.ts | 73 +++++++----- .../frames.js/src/getFrameFlattened.test.ts | 6 +- packages/frames.js/src/getFrameFlattened.ts | 65 +++++----- packages/frames.js/src/getFrameHtml.ts | 20 ++-- packages/frames.js/src/getFrameMessage.ts | 28 ++--- .../frames.js/src/getTokenFromUrl.test.ts | 4 +- packages/frames.js/src/getTokenFromUrl.ts | 2 +- packages/frames.js/src/getTokenUrl.test.ts | 6 +- packages/frames.js/src/getTokenUrl.ts | 2 +- .../frames.js/src/getUserDataForFid.test.ts | 7 +- packages/frames.js/src/getUserDataForFid.ts | 47 ++++---- packages/frames.js/src/hono/index.test.tsx | 6 +- packages/frames.js/src/hono/index.ts | 24 ++-- packages/frames.js/src/hono/test.types.tsx | 31 ++++- packages/frames.js/src/lib/stream-pump.ts | 59 +++++---- .../middleware/concurrentMiddleware.test.ts | 23 ++-- .../src/middleware/concurrentMiddleware.ts | 9 +- .../src/middleware/farcaster.test.ts | 67 ++++++----- .../frames.js/src/middleware/farcaster.ts | 7 +- .../middleware/farcasterHubContext.test.ts | 57 +++++---- .../src/middleware/farcasterHubContext.ts | 7 +- .../src/middleware/framesjsMiddleware.test.ts | 29 ++--- .../src/middleware/framesjsMiddleware.ts | 6 +- .../src/middleware/openframes.test.ts | 112 +++++++++--------- .../frames.js/src/middleware/openframes.ts | 39 ++++-- .../src/middleware/renderResponse.test.tsx | 63 +++++----- .../src/middleware/renderResponse.ts | 15 ++- .../src/middleware/stateMiddleware.test.ts | 8 +- .../frames.js/src/next/fetchMetadata.test.ts | 9 +- packages/frames.js/src/next/fetchMetadata.ts | 2 +- packages/frames.js/src/next/getCurrentUrl.ts | 38 ++++-- packages/frames.js/src/next/index.test.ts | 4 +- packages/frames.js/src/next/pages-router.tsx | 28 +++-- packages/frames.js/src/next/server.tsx | 96 ++++++++------- packages/frames.js/src/next/test.types.tsx | 22 ++++ packages/frames.js/src/next/types.ts | 9 +- packages/frames.js/src/remix/fetchMetadata.ts | 3 +- packages/frames.js/src/remix/index.test.ts | 2 +- packages/frames.js/src/remix/test.types.tsx | 22 ++++ .../frames.js/src/validateFrameMessage.ts | 1 + 64 files changed, 791 insertions(+), 559 deletions(-) delete mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 5e94b0c8c..000000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -packages/frames.js/src/farcaster/generated/** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index fd0ba955a..22f3ef9bd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,4 +5,5 @@ module.exports = { parserOptions: { project: true, }, + ignorePatterns: ["**/farcaster/generated/*.ts"], }; diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index 8b66f3684..28223ab3f 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -33,9 +33,11 @@ module.exports = { "@typescript-eslint/require-await": "warn", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unnecessary-condition": "warn", + "@typescript-eslint/consistent-type-imports": "error", "jest/expect-expect": "warn", "react/jsx-sort-props": "off", "unicorn/filename-case": "off", eqeqeq: "off", + "no-await-in-loop": "off", }, }; diff --git a/packages/frames.js/src/cloudflare-workers/index.test.tsx b/packages/frames.js/src/cloudflare-workers/index.test.tsx index 75c3746b1..737c852b5 100644 --- a/packages/frames.js/src/cloudflare-workers/index.test.tsx +++ b/packages/frames.js/src/cloudflare-workers/index.test.tsx @@ -7,7 +7,7 @@ describe("cloudflare workers adapter", () => { it("correctly integrates with Cloudflare Workers", async () => { const frames = lib.createFrames(); - const handler = frames(async (ctx) => { + const handler = frames((ctx) => { expect(ctx.request.url).toBe("http://localhost:3000/"); return { @@ -35,7 +35,7 @@ describe("cloudflare workers adapter", () => { }, }); - const handler = frames(async (ctx) => { + const handler = frames((ctx) => { expect(ctx.state).toEqual({ test: false }); return { diff --git a/packages/frames.js/src/cloudflare-workers/test.types.tsx b/packages/frames.js/src/cloudflare-workers/test.types.tsx index b124b0da7..59f1107f8 100644 --- a/packages/frames.js/src/cloudflare-workers/test.types.tsx +++ b/packages/frames.js/src/cloudflare-workers/test.types.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/require-await -- we want to test that handler supports async*/ import type { ExecutionContext, Request as CfRequest, ExportedHandlerFetchHandler } from '@cloudflare/workers-types'; import type { types } from '.'; import { createFrames } from '.'; @@ -41,6 +42,17 @@ framesWithExplicitStateAndEnv(async (ctx) => { ctx satisfies { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; ctx satisfies { cf: { env: { secret: string }; ctx: ExecutionContext; req: CfRequest }} + return { + image: 'http://test.png', + }; +}) satisfies ExportedHandlerFetchHandler<{ secret: string }>; + +const framesWithExplicitStateAndEnvNoPromiseHandler = createFrames<{ test: boolean }, { secret: string }>({}); +framesWithExplicitStateAndEnvNoPromiseHandler((ctx) => { + ctx.state satisfies { test: boolean }; + ctx satisfies { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; + ctx satisfies { cf: { env: { secret: string }; ctx: ExecutionContext; req: CfRequest }} + return { image: 'http://test.png', }; diff --git a/packages/frames.js/src/core/components.ts b/packages/frames.js/src/core/components.ts index a21bd94d4..abdb940bf 100644 --- a/packages/frames.js/src/core/components.ts +++ b/packages/frames.js/src/core/components.ts @@ -63,6 +63,6 @@ export type ButtonProps = | LinkButtonProps | TxButtonProps; -export const Button: React.FunctionComponent = () => { +export function Button(_: ButtonProps): null { return null; -}; +} diff --git a/packages/frames.js/src/core/composeMiddleware.test.ts b/packages/frames.js/src/core/composeMiddleware.test.ts index e437b427b..825133f3c 100644 --- a/packages/frames.js/src/core/composeMiddleware.test.ts +++ b/packages/frames.js/src/core/composeMiddleware.test.ts @@ -10,8 +10,8 @@ describe("composeMiddleware", () => { it("properly works with just one middleware", async () => { const context = { a: 1 }; - const composedMiddleware = composeMiddleware([ - async (ctx, next) => { + const composedMiddleware = composeMiddleware([ + (ctx, next) => { ctx.a = 2; return next(); }, @@ -31,7 +31,10 @@ describe("composeMiddleware", () => { order: [], }; - const composedMiddleware = composeMiddleware>([ + const composedMiddleware = composeMiddleware< + typeof context, + Promise | string + >([ async (ctx: typeof context, next) => { ctx.order.push(1); const result = await next({ @@ -54,7 +57,7 @@ describe("composeMiddleware", () => { return result; }, - async (ctx: typeof context) => { + (ctx: typeof context) => { ctx.order.push(3); expect(ctx).toMatchObject({ 1: true, 2: "test" }); diff --git a/packages/frames.js/src/core/composeMiddleware.ts b/packages/frames.js/src/core/composeMiddleware.ts index 3e5ac2fea..665f78320 100644 --- a/packages/frames.js/src/core/composeMiddleware.ts +++ b/packages/frames.js/src/core/composeMiddleware.ts @@ -25,6 +25,8 @@ export function composeMiddleware( ); return (ctx) => { - return composedMiddleware(ctx, (() => {}) as any); + return composedMiddleware(ctx, (() => { + return undefined; + }) as () => TReturnType); }; } diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index 24ae00a60..c03ae3ae3 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -13,7 +13,7 @@ describe("createFrames", () => { it("provides default properties on context based on default middleware and internal logic", async () => { const handler = createFrames(); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { expect(ctx.url).toBeInstanceOf(URL); expect(ctx.url.href).toBe("http://test.com/"); @@ -32,7 +32,7 @@ describe("createFrames", () => { it("passes initialState to context", async () => { const handler = createFrames({ initialState: { test: true } }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { expect(ctx.initialState).toEqual({ test: true }); return redirect("http://test.com"); }); @@ -54,7 +54,7 @@ describe("createFrames", () => { middleware: [customMiddleware], }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { return redirect(ctx.custom); }); @@ -75,7 +75,7 @@ describe("createFrames", () => { const handler = createFrames(); const routeHandler = handler( - async (ctx) => { + (ctx) => { return redirect(ctx.custom); }, { @@ -90,16 +90,28 @@ describe("createFrames", () => { }); it("works with parallel middleware", async () => { - const middleware0 = async (context: any, next: any) => { + const middleware0: FramesMiddleware = async ( + context, + next + ) => { return next({ test0: true }); }; - const middleware1 = async (context: any, next: any) => { + const middleware1: FramesMiddleware = async ( + context, + next + ) => { return next({ test1: true }); }; - const middleware2 = async (context: any, next: any) => { + const middleware2: FramesMiddleware = async ( + context, + next + ) => { return next({ test2: true }); }; - const middleware3 = async (context: any, next: any) => { + const middleware3: FramesMiddleware = async ( + context, + next + ) => { return next({ test3: true }); }; @@ -111,7 +123,7 @@ describe("createFrames", () => { ], }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { expect(ctx).toMatchObject({ test0: true, test1: true, diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 4fd9a0524..8cf98fb6c 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -1,4 +1,4 @@ -import { coreMiddleware } from "../middleware"; +import { coreMiddleware } from "../middleware/default"; import { stateMiddleware } from "../middleware/stateMiddleware"; import { composeMiddleware } from "./composeMiddleware"; import type { @@ -21,7 +21,7 @@ export function createFrames< TState, typeof coreMiddleware, TMiddlewares, - (req: Request) => Promise + (req: Request) => Promise | Response > { const globalMiddleware: FramesMiddleware>[] = middleware || []; @@ -30,8 +30,10 @@ export function createFrames< * This function takes handler function that does the logic with the help of context and returns one of possible results */ return function createFramesRequestHandler(handler, options = {}) { - const perRouteMiddleware: FramesMiddleware>[] = - options && Array.isArray(options.middleware) ? options.middleware : []; + const perRouteMiddleware: FramesMiddleware< + JsonValue | undefined, + FramesContext + >[] = Array.isArray(options.middleware) ? options.middleware : []; const composedMiddleware = composeMiddleware< FramesContext, diff --git a/packages/frames.js/src/core/formatUrl.ts b/packages/frames.js/src/core/formatUrl.ts index a417eb9af..ed427a82d 100644 --- a/packages/frames.js/src/core/formatUrl.ts +++ b/packages/frames.js/src/core/formatUrl.ts @@ -20,8 +20,8 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -import type { UrlObject } from "url"; -import type { ParsedUrlQuery } from "querystring"; +import type { UrlObject } from "node:url"; +import type { ParsedUrlQuery } from "node:querystring"; // function searchParamsToUrlQuery(searchParams: URLSearchParams): ParsedUrlQuery { // const query: ParsedUrlQuery = {}; @@ -44,16 +44,18 @@ function stringifyUrlQueryParam(param: unknown): string { typeof param === "boolean" ) { return String(param); - } else { - return ""; } + + return ""; } function urlQueryToSearchParams(urlQuery: ParsedUrlQuery): URLSearchParams { const result = new URLSearchParams(); Object.entries(urlQuery).forEach(([key, value]) => { if (Array.isArray(value)) { - value.forEach((item) => result.append(key, stringifyUrlQueryParam(item))); + value.forEach((item) => { + result.append(key, stringifyUrlQueryParam(item)); + }); } else { result.set(key, stringifyUrlQueryParam(value)); } @@ -74,22 +76,23 @@ function urlQueryToSearchParams(urlQuery: ParsedUrlQuery): URLSearchParams { const slashedProtocols = /https?|ftp|gopher|file/; -export function formatUrl(urlObj: UrlObject) { - let { auth, hostname } = urlObj; +export function formatUrl(urlObj: UrlObject): string { + let { auth } = urlObj; + const { hostname } = urlObj; let protocol = urlObj.protocol || ""; let pathname = urlObj.pathname || ""; let hash = urlObj.hash || ""; let query = urlObj.query || ""; let host: string | false = false; - auth = auth ? encodeURIComponent(auth).replace(/%3A/i, ":") + "@" : ""; + auth = auth ? `${encodeURIComponent(auth).replace(/%3A/i, ":")}@` : ""; if (urlObj.host) { host = auth + urlObj.host; } else if (hostname) { - host = auth + (~hostname.indexOf(":") ? `[${hostname}]` : hostname); + host = auth + (hostname.includes(":") ? `[${hostname}]` : hostname); if (urlObj.port) { - host += ":" + urlObj.port; + host += `:${urlObj.port}`; } } @@ -105,14 +108,16 @@ export function formatUrl(urlObj: UrlObject) { urlObj.slashes || ((!protocol || slashedProtocols.test(protocol)) && host !== false) ) { - host = "//" + (host || ""); - if (pathname && pathname[0] !== "/") pathname = "/" + pathname; + host = `//${host || ""}`; + if (pathname && !pathname.startsWith("/")) { + pathname = `/${pathname}`; + } } else if (!host) { host = ""; } - if (hash && hash[0] !== "#") hash = "#" + hash; - if (search && search[0] !== "?") search = "?" + search; + if (hash && !hash.startsWith("#")) hash = `#${hash}`; + if (search && !search.startsWith("?")) search = `?${search}`; pathname = pathname.replace(/[?#]/g, encodeURIComponent); search = search.replace("#", "%23"); diff --git a/packages/frames.js/src/core/redirect.ts b/packages/frames.js/src/core/redirect.ts index 0eeb079ca..f3031220e 100644 --- a/packages/frames.js/src/core/redirect.ts +++ b/packages/frames.js/src/core/redirect.ts @@ -1,4 +1,4 @@ -import { FrameRedirect } from "./types"; +import type { FrameRedirect } from "./types"; /** * Creates a redirect with 302 as default status diff --git a/packages/frames.js/src/core/test.types.tsx b/packages/frames.js/src/core/test.types.tsx index d734474d2..28e333a49 100644 --- a/packages/frames.js/src/core/test.types.tsx +++ b/packages/frames.js/src/core/test.types.tsx @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await -- we are checking compatibility with promises */ import type { types } from '.'; import { createFrames } from '.'; -type Handler = (req: Request) => Promise; +type Handler = (req: Request) => Promise | Response; const framesWithoutState = createFrames(); framesWithoutState(async (ctx) => { @@ -41,6 +42,17 @@ framesWithExplicitStateAndEnv(async (ctx) => { ctx satisfies { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; + return { + image: 'http://test.png', + }; +}) satisfies Handler; + +const framesWithExplicitStateAndEnvNoPromise = createFrames<{ test: boolean }>({}); +framesWithExplicitStateAndEnvNoPromise((ctx) => { + ctx.state satisfies { test: boolean }; + ctx satisfies { initialState?: { test: boolean }; message?: unknown, pressedButton?: unknown; request: Request; }; + + return { image: 'http://test.png', }; diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index 05c935c9a..9d4b22e3f 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -134,7 +134,9 @@ export type FrameHandlerFunction< > = ( // we pass ctx.state here since it is made available internally by stateMiddleware but the inference would not work ctx: FramesContext & TFramesContext & { state: TState } -) => Promise>; +) => + | Promise> + | FramesHandlerFunctionReturnType; export type FramesContextFromMiddlewares< TMiddlewares extends @@ -164,7 +166,7 @@ export type FramesRequestHandlerFunction< | FramesMiddleware[] | undefined, TFrameMiddleware extends FramesMiddleware[] | undefined, - TRequestHandlerFunction extends Function, + TRequestHandlerFunction extends (...args: any[]) => any, > = < TPerRouteMiddleware extends | FramesMiddleware[] @@ -173,13 +175,13 @@ export type FramesRequestHandlerFunction< handler: FrameHandlerFunction< TState, (TDefaultMiddleware extends undefined - ? {} + ? Record : FramesContextFromMiddlewares>) & (TFrameMiddleware extends undefined - ? {} + ? Record : FramesContextFromMiddlewares>) & (TPerRouteMiddleware extends undefined - ? {} + ? Record : FramesContextFromMiddlewares>) >, options?: FramesRequestHandlerFunctionOptions @@ -210,7 +212,7 @@ export type CreateFramesFunctionDefinition< | readonly FramesMiddleware[] | FramesMiddleware[] | undefined, - TRequestHandlerFunction extends Function, + TRequestHandlerFunction extends (...args: any[]) => any, > = < TState extends JsonValue | undefined = JsonValue | undefined, TFrameMiddleware extends FramesMiddleware[] | undefined = undefined, diff --git a/packages/frames.js/src/core/utils.ts b/packages/frames.js/src/core/utils.ts index fc93e4402..176d7bbce 100644 --- a/packages/frames.js/src/core/utils.ts +++ b/packages/frames.js/src/core/utils.ts @@ -69,13 +69,11 @@ export function generateTargetURL({ port: url.port, // query: url.searchParams, ...target, - pathname: - "/" + - [basePath ?? "", "/", target.pathname ?? ""] - .join("") - .split("/") - .filter(Boolean) - .join("/"), + pathname: `/${[basePath, "/", target.pathname ?? ""] + .join("") + .split("/") + .filter(Boolean) + .join("/")}`, }) ); } diff --git a/packages/frames.js/src/express/index.test.tsx b/packages/frames.js/src/express/index.test.tsx index 0c2051125..680c4e0ee 100644 --- a/packages/frames.js/src/express/index.test.tsx +++ b/packages/frames.js/src/express/index.test.tsx @@ -1,7 +1,7 @@ import express from "express"; import request from "supertest"; -import * as lib from "."; import { FRAMES_META_TAGS_HEADER } from "../core"; +import * as lib from "."; describe("express adapter", () => { it.each(["createFrames", "Button"])("exports %s", (exportName) => { @@ -11,9 +11,9 @@ describe("express adapter", () => { it("properly correctly integrates with express.js app", async () => { const app = express(); const frames = lib.createFrames(); - const expressHandler = frames(async ({ request }) => { - expect(request).toBeInstanceOf(Request); - expect(request.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); + const expressHandler = frames(({ request: req }) => { + expect(req).toBeInstanceOf(Request); + expect(req.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); return { image: Nehehe, @@ -33,9 +33,9 @@ describe("express adapter", () => { it("properly correctly integrates with express.js app and returns JSON if asked to", async () => { const app = express(); const frames = lib.createFrames(); - const expressHandler = frames(async ({ request }) => { - expect(request).toBeInstanceOf(Request); - expect(request.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); + const expressHandler = frames(({ request: req }) => { + expect(req).toBeInstanceOf(Request); + expect(req.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); return { image: Nehehe, @@ -59,7 +59,7 @@ describe("express adapter", () => { it("properly handles error response", async () => { const app = express(); const frames = lib.createFrames(); - const expressHandler = frames(async () => { + const expressHandler = frames(() => { throw new Error("Something went wrong"); }); @@ -74,9 +74,9 @@ describe("express adapter", () => { it("resolves button targets correctly", async () => { const app = express(); const frames = lib.createFrames({ basePath: "/api" }); - const expressHandler = frames(async ({ request }) => { - expect(request).toBeInstanceOf(Request); - expect(request.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); + const expressHandler = frames(({ request: req }) => { + expect(req).toBeInstanceOf(Request); + expect(req.url).toMatch(/http:\/\/127\.0\.0\.1:\d+\//); return { image: Nehehe, @@ -96,7 +96,7 @@ describe("express adapter", () => { .expect(200) .expect("Content-type", "application/json") .expect((res) => { - expect(res.body["fc:frame:button:1:target"]).toMatch( + expect((res.body as Record)["fc:frame:button:1:target"]).toMatch( /http:\/\/127\.0\.0\.1:\d+\/api\/nested/ ); }); @@ -113,7 +113,7 @@ describe("express adapter", () => { }, }); - const expressHandler = frames(async (ctx) => { + const expressHandler = frames((ctx) => { expect(ctx.state).toEqual({ test: false }); return { diff --git a/packages/frames.js/src/express/index.ts b/packages/frames.js/src/express/index.ts index 7cfbebb96..7f9fbb976 100644 --- a/packages/frames.js/src/express/index.ts +++ b/packages/frames.js/src/express/index.ts @@ -1,15 +1,17 @@ +import type { IncomingHttpHeaders } from "node:http"; import type { Handler as ExpressHandler, Request as ExpressRequest, Response as ExpressResponse, } from "express"; -import type { IncomingHttpHeaders } from "node:http"; -import { createFrames as coreCreateFrames, types } from "../core"; +import type { types } from "../core"; +import { createFrames as coreCreateFrames } from "../core"; import { createReadableStreamFromReadable, writeReadableStreamToWritable, } from "../lib/stream-pump"; -import { CoreMiddleware } from "../middleware"; +import type { CoreMiddleware } from "../middleware"; + export { Button, type types } from "../core"; type CreateFramesForExpress = types.CreateFramesFunctionDefinition< @@ -21,6 +23,7 @@ type CreateFramesForExpress = types.CreateFramesFunctionDefinition< * Creates Frames instance to use with you Express.js server * * @example + * ```tsx * import { createFrames, Button } from 'frames.js/express'; * import express from 'express'; * @@ -37,8 +40,9 @@ type CreateFramesForExpress = types.CreateFramesFunctionDefinition< * }; * * app.use("/", expressHandler); + * ``` */ -// @ts-expect-error +// @ts-expect-error -- this code is correct just function doesn't satisfy the type export const createFrames: CreateFramesForExpress = function createFramesForExpress(options?: types.FramesOptions) { const frames = coreCreateFrames(options); @@ -51,15 +55,21 @@ export const createFrames: CreateFramesForExpress = ) { const framesHandler = frames(handler, handlerOptions); - return async function handleExpressRequest( + return function handleExpressRequest( req: ExpressRequest, res: ExpressResponse ) { // convert express.js req to Web API Request - - const response = await framesHandler(createRequest(req, res)); - - sendResponse(res, response); + const response = framesHandler(createRequest(req, res)); + + Promise.resolve(response) + .then((resolvedResponse) => sendResponse(res, resolvedResponse)) + .catch((error) => { + // eslint-disable-next-line no-console -- provide feedback + console.error(error); + res.writeHead(500, { "content-type": "text/plain" }); + res.end("Inernal server error"); + }); }; }; }; @@ -69,17 +79,19 @@ function createRequest(req: ExpressRequest, res: ExpressResponse): Request { // `X-Forwarded-Host` or `Host` const [, hostnamePort] = req.get("X-Forwarded-Host")?.split(":") ?? []; const [, hostPort] = req.get("Host")?.split(":") ?? []; - let port = hostnamePort || hostPort; + const port = hostnamePort || hostPort; // Use req.hostname here as it respects the "trust proxy" setting - let resolvedHost = `${req.hostname}${port ? `:${port}` : ""}`; + const resolvedHost = `${req.hostname}${port ? `:${port}` : ""}`; // Use `req.originalUrl` so Remix is aware of the full path - let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`); + const url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`); // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - res.on("close", () => controller.abort()); + const controller = new AbortController(); + res.on("close", () => { + controller.abort(); + }); - let init: RequestInit = { + const init: RequestInit = { method: req.method, headers: convertIncomingHTTPHeadersToHeaders(req.headers), signal: controller.signal, @@ -111,11 +123,14 @@ function convertIncomingHTTPHeadersToHeaders( return headers; } -async function sendResponse(res: ExpressResponse, response: Response) { +async function sendResponse( + res: ExpressResponse, + response: Response +): Promise { res.statusMessage = response.statusText; res.status(response.status); - for (let [key, value] of response.headers.entries()) { + for (const [key, value] of response.headers.entries()) { res.setHeader(key, value); } diff --git a/packages/frames.js/src/express/test.types.tsx b/packages/frames.js/src/express/test.types.tsx index 1ca690834..55ebd55be 100644 --- a/packages/frames.js/src/express/test.types.tsx +++ b/packages/frames.js/src/express/test.types.tsx @@ -1,5 +1,7 @@ -import { Handler } from 'express'; -import { createFrames, types } from '.'; +/* eslint-disable @typescript-eslint/require-await -- we want to test if the types are compatible */ +import type { Handler } from 'express'; +import type { types } from '.'; +import { createFrames } from '.'; const framesWithoutState = createFrames(); framesWithoutState(async ctx => { @@ -38,8 +40,29 @@ framesWithExplicitState(async ctx => { test: boolean; }; ctx satisfies { - message?: any; - pressedButton?: any; + message?: unknown; + pressedButton?: unknown; + request: Request; + } + + return { + image: 'http://test.png' + }; +}) satisfies Handler; + +const framesWithExplicitStateNoPromise = createFrames<{ + test: boolean; +}>({}); +framesWithExplicitStateNoPromise(ctx => { + ctx.state satisfies { + test: boolean; + }; + ctx.initialState satisfies { + test: boolean; + }; + ctx satisfies { + message?: unknown; + pressedButton?: unknown; request: Request; } diff --git a/packages/frames.js/src/farcaster/types.ts b/packages/frames.js/src/farcaster/types.ts index 234288c15..f2e5e26da 100644 --- a/packages/frames.js/src/farcaster/types.ts +++ b/packages/frames.js/src/farcaster/types.ts @@ -1,5 +1,5 @@ -import * as protobufs from "./generated/message"; -import { UserNameProof } from "./generated/username_proof"; +import type * as protobufs from "./generated/message"; +import type { UserNameProof } from "./generated/username_proof"; /** Message types */ diff --git a/packages/frames.js/src/getAddressForFid.test.ts b/packages/frames.js/src/getAddressForFid.test.ts index 981cd3756..1803114e9 100644 --- a/packages/frames.js/src/getAddressForFid.test.ts +++ b/packages/frames.js/src/getAddressForFid.test.ts @@ -1,10 +1,11 @@ -import nock from "nock"; -import { getAddressForFid } from "."; +// eslint-disable-next-line import/no-extraneous-dependencies -- dev dependency +import nock, { cleanAll } from "nock"; import { DEFAULT_HUB_API_URL } from "./default"; +import { getAddressForFid } from "."; describe("getAddressForFid", () => { beforeEach(() => { - nock.cleanAll(); + cleanAll(); }); it("returns address for fid with connected address", async () => { diff --git a/packages/frames.js/src/getAddressForFid.ts b/packages/frames.js/src/getAddressForFid.ts index ef0b2005d..c595d44d0 100644 --- a/packages/frames.js/src/getAddressForFid.ts +++ b/packages/frames.js/src/getAddressForFid.ts @@ -69,5 +69,9 @@ export async function getAddressForFid< }); } + if (!address) { + throw new Error(`No address found for fid ${fid}`); + } + return address as AddressReturnType; } diff --git a/packages/frames.js/src/getAddressesForFid.test.ts b/packages/frames.js/src/getAddressesForFid.test.ts index 31f71b641..0ffd3470b 100644 --- a/packages/frames.js/src/getAddressesForFid.test.ts +++ b/packages/frames.js/src/getAddressesForFid.test.ts @@ -1,10 +1,11 @@ -import nock from "nock"; +// eslint-disable-next-line import/no-extraneous-dependencies -- dev dependency +import nock, { cleanAll } from "nock"; import { getAddressesForFid } from "./getAddressesForFid"; import { DEFAULT_HUB_API_URL } from "./default"; describe("getAddressesForFid", () => { afterEach(() => { - nock.cleanAll(); + cleanAll(); }); it("returns address for fid with connected address", async () => { diff --git a/packages/frames.js/src/getAddressesForFid.ts b/packages/frames.js/src/getAddressesForFid.ts index a6f5369e1..fe46c3df3 100644 --- a/packages/frames.js/src/getAddressesForFid.ts +++ b/packages/frames.js/src/getAddressesForFid.ts @@ -70,7 +70,7 @@ export async function getAddressesForFid({ // filter out unsupported addresses .filter((val): val is AddressWithType => val !== null); return [...verifiedAddresses, custodyAddress]; - } else { - return [custodyAddress]; } + + return [custodyAddress]; } diff --git a/packages/frames.js/src/getFrame.test.ts b/packages/frames.js/src/getFrame.test.ts index 65e3bd02b..435667045 100644 --- a/packages/frames.js/src/getFrame.test.ts +++ b/packages/frames.js/src/getFrame.test.ts @@ -1,6 +1,6 @@ import { getFrame } from "./getFrame"; import { getFrameHtml } from "./getFrameHtml"; -import { Frame } from "./types"; +import type { Frame } from "./types"; describe("getFrame", () => { const sampleHtml = ` @@ -144,7 +144,7 @@ describe("getFrame", () => { url: "https://example.com", }); - expect(frame?.imageAspectRatio).toEqual("1:91"); + expect(frame.imageAspectRatio).toEqual("1:91"); const html2 = ` @@ -158,7 +158,7 @@ describe("getFrame", () => { url: "https://example.com", }); - expect(frame2?.imageAspectRatio).toEqual("1:1"); + expect(frame2.imageAspectRatio).toEqual("1:1"); }); it("should reject invalid aspect ratio", () => { @@ -244,7 +244,7 @@ describe("getFrame", () => { url: "https://example.com", }); - expect(frame?.accepts).toEqual([ + expect(frame.accepts).toEqual([ { id: "xmtp", version: "vNext" }, { id: "lens", version: "1.5" }, ]); diff --git a/packages/frames.js/src/getFrame.ts b/packages/frames.js/src/getFrame.ts index 6206e00aa..ffb08c11f 100644 --- a/packages/frames.js/src/getFrame.ts +++ b/packages/frames.js/src/getFrame.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console -- provide feedback */ import * as cheerio from "cheerio"; import { getByteLength, @@ -19,7 +20,7 @@ import type { */ /** - * @returns a { frame: Frame | null, errors: null | ErrorMessages } object, extracting the frame metadata from the given htmlString. + * @returns an object, extracting the frame metadata from the given htmlString. * If the Frame fails validation, the `errors` object will be non-null */ export function getFrame({ @@ -35,21 +36,22 @@ export function getFrame({ const $ = cheerio.load(htmlString); let errors: null | Record = null; - function addError({ key, message }: { key: string; message: string }) { - if (!errors) errors = {}; - if ( - errors.hasOwnProperty(key) && - errors[key] && - Array.isArray(errors[key]) - ) { + function addError({ key, message }: { key: string; message: string }): void { + if (!errors) { + errors = {}; + } + + const error = errors[key]; + + if (error && Array.isArray(error)) { console.error(`Error: ${key} ${message}`); - errors[key]!.push(message); + error.push(message); } else { errors[key] = [message]; } } - function getMetaContent(key: string) { + function getMetaContent(key: string): string | undefined { const selector = `meta[property='${key}'], meta[name='${key}']`; const content = $(selector).attr("content"); if (content) return content; @@ -57,6 +59,7 @@ export function getFrame({ } const pageTitle = $("title").text(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- just in case if (pageTitle === undefined) { // This should probably be a warning instead of an error. would help addError({ @@ -81,9 +84,12 @@ export function getFrame({ }) .map((i, el) => { const attribute = $(el).attr("name") || $(el).attr("property"); - const id = attribute?.substring("of:accepts:".length)!; - const version = $(el).attr("content")!; - return { id, version }; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is already checked in filter above + const id = attribute!.substring("of:accepts:".length); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is already checked in filter above + const detectedVersion = $(el).attr("content")!; + + return { id, version: detectedVersion }; }) .toArray(); @@ -98,7 +104,7 @@ export function getFrame({ `meta[property='fc:frame:button:${el}'], meta[name='fc:frame:button:${el}'], meta[property='of:button:${el}'], meta[name='of:button:${el}']` ) .map((i, elem) => parseButtonElement(elem)) - .filter((i, elem) => elem !== null) + .filter((i, elem) => Boolean(elem)) .toArray() // only take the first, to deduplicate of and fc:frame .slice(0, 1) @@ -108,7 +114,7 @@ export function getFrame({ `meta[property='fc:frame:button:${el}:action'], meta[name='fc:frame:button:${el}:action'], meta[property='of:button:${el}:action'], meta[name='of:button:${el}:action']` ) .map((i, elem) => parseButtonElement(elem)) - .filter((i, elem) => elem !== null) + .filter((i, elem) => Boolean(elem)) .toArray() // only take the first, to deduplicate of and fc:frame .slice(0, 1) @@ -119,7 +125,7 @@ export function getFrame({ `meta[property='fc:frame:button:${el}:target'], meta[name='fc:frame:button:${el}:target'], meta[property='of:button:${el}:target'], meta[name='of:button:${el}:target']` ) .map((i, elem) => parseButtonElement(elem)) - .filter((i, elem) => elem !== null) + .filter((i, elem) => Boolean(elem)) .toArray() // only take the first, to deduplicate of and fc:frame .slice(0, 1) @@ -130,23 +136,23 @@ export function getFrame({ `meta[property='fc:frame:button:${el}:post_url'], meta[name='fc:frame:button:${el}:post_url'], meta[property='of:button:${el}:post_url'], meta[name='of:button:${el}:post_url']` ) .map((i, elem) => parseButtonElement(elem)) - .filter((i, elem) => elem !== null) + .filter((i, elem) => Boolean(elem)) .toArray() // only take the first, to deduplicate of and fc:frame .slice(0, 1) ); - let buttonsValidation = [false, false, false, false]; + const buttonsValidation = [false, false, false, false]; const buttonsWithActions = buttonLabels .map((button): FrameButton & { buttonIndex: number } => { const buttonAction = buttonActions.find( - (action) => action?.buttonIndex === button?.buttonIndex + (action) => action.buttonIndex === button.buttonIndex ); const buttonTarget = buttonTargets.find( - (action) => action?.buttonIndex === button?.buttonIndex + (action) => action.buttonIndex === button.buttonIndex ); const buttonPostUrl = buttonPostUrls.find( - (action) => action?.buttonIndex === button?.buttonIndex + (action) => action.buttonIndex === button.buttonIndex ); if (buttonsValidation[button.buttonIndex - 1]) { addError({ @@ -164,7 +170,7 @@ export function getFrame({ } const action = - buttonAction?.content !== undefined ? buttonAction?.content : "post"; + buttonAction?.content !== undefined ? buttonAction.content : "post"; if (action === "link" || action === "tx") { if (!buttonTarget?.content) { addError({ @@ -279,14 +285,14 @@ export function getFrame({ }); if (!image) { addError({ message: "No image found in frame", key: "fc:frame:image" }); - } else if (!(image?.startsWith("http://") || image?.startsWith("https://"))) { + } else if (!(image.startsWith("http://") || image.startsWith("https://"))) { // validate image data url is not an svg if ( !( - image?.startsWith("data:image/png;base64,") || - image?.startsWith("data:image/jpg;base64,") || - image?.startsWith("data:image/jpeg;base64,") || - image?.startsWith("data:image/gif;base64,") + image.startsWith("data:image/png;base64,") || + image.startsWith("data:image/jpg;base64,") || + image.startsWith("data:image/jpeg;base64,") || + image.startsWith("data:image/gif;base64,") ) ) { if (image.startsWith("data:")) { @@ -356,7 +362,8 @@ export function getFrame({ return { frame: { version: version as "vNext" | `${number}-${number}-${number}`, - image: image!, + // @todo we should fix this + image: image ?? "", imageAspectRatio: imageAspectRatio as ImageAspectRatio, buttons: buttonsWithActions as FrameButtonsType, postUrl, @@ -368,17 +375,19 @@ export function getFrame({ }; } -export function parseButtonElement(elem: cheerio.Element) { - const nameAttr = elem.attribs["name"] || elem.attribs["property"]; +export function parseButtonElement( + elem: cheerio.Element +): { buttonIndex: number; content: string | undefined } | null { + const nameAttr = elem.attribs.name || elem.attribs.property; const buttonSegments = nameAttr?.split(":"); // Handles both cases of fc:frame:button:N and of:button:N const buttonIndex = - buttonSegments?.[0] === "fc" ? buttonSegments?.[3] : buttonSegments?.[2]; + buttonSegments?.[0] === "fc" ? buttonSegments[3] : buttonSegments?.[2]; try { return { buttonIndex: parseInt(buttonIndex || ""), - content: elem.attribs["content"], + content: elem.attribs.content, }; } catch (error) { return null; diff --git a/packages/frames.js/src/getFrameFlattened.test.ts b/packages/frames.js/src/getFrameFlattened.test.ts index 4486b3141..48e657a06 100644 --- a/packages/frames.js/src/getFrameFlattened.test.ts +++ b/packages/frames.js/src/getFrameFlattened.test.ts @@ -1,8 +1,8 @@ -import { Frame, getFrameFlattened, getTokenUrl } from "."; -import { zora } from "viem/chains"; +import type { Frame } from "."; +import { getFrameFlattened } from "."; describe("getFrameFlattened", () => { - it("should get flattened frame", async () => { + it("should get flattened frame", () => { const frame: Frame = { version: "vNext", image: "https://example.com/image.png", diff --git a/packages/frames.js/src/getFrameFlattened.ts b/packages/frames.js/src/getFrameFlattened.ts index 5ab321ee1..031fdeec2 100644 --- a/packages/frames.js/src/getFrameFlattened.ts +++ b/packages/frames.js/src/getFrameFlattened.ts @@ -2,41 +2,42 @@ import type { Frame, FrameFlattened } from "./types"; /** * Takes a `Frame` and formats it as an intermediate step before rendering as html - * @param frame The `Frame` to flatten + * @param frame - The `Frame` to flatten * @returns a plain object with frame metadata keys and values according to the frame spec, using their lengthened syntax, e.g. "fc:frame:image" */ export function getFrameFlattened(frame: Frame): FrameFlattened { - const openFrames = !!frame.accepts?.length - ? { - // custom of tags - [`of:version`]: frame.version, - ...frame.accepts?.reduce( - (acc: Record, { id, version }) => { - acc[`of:accepts:${id}`] = version; - return acc; - }, - {} - ), - // same as fc:frame tags - [`of:image`]: frame.image, - [`of:post_url`]: frame.postUrl, - [`of:input:text`]: frame.inputText, - ...(frame.state ? { [`of:state`]: frame.state } : {}), - ...(frame.imageAspectRatio - ? { [`of:image:aspect_ratio`]: frame.imageAspectRatio } - : {}), - ...frame.buttons?.reduce( - (acc, button, index) => ({ - ...acc, - [`of:button:${index + 1}`]: button.label, - [`of:button:${index + 1}:action`]: button.action, - [`of:button:${index + 1}:target`]: button.target, - [`of:button:${index + 1}:post_url`]: button.post_url, - }), - {} - ), - } - : {}; + const openFrames = + frame.accepts && Boolean(frame.accepts.length) + ? { + // custom of tags + [`of:version`]: frame.version, + ...frame.accepts.reduce( + (acc: Record, { id, version }) => { + acc[`of:accepts:${id}`] = version; + return acc; + }, + {} + ), + // same as fc:frame tags + [`of:image`]: frame.image, + [`of:post_url`]: frame.postUrl, + [`of:input:text`]: frame.inputText, + ...(frame.state ? { [`of:state`]: frame.state } : {}), + ...(frame.imageAspectRatio + ? { [`of:image:aspect_ratio`]: frame.imageAspectRatio } + : {}), + ...frame.buttons?.reduce( + (acc, button, index) => ({ + ...acc, + [`of:button:${index + 1}`]: button.label, + [`of:button:${index + 1}:action`]: button.action, + [`of:button:${index + 1}:target`]: button.target, + [`of:button:${index + 1}:post_url`]: button.post_url, + }), + {} + ), + } + : {}; const metadata: FrameFlattened = { [`fc:frame`]: frame.version, diff --git a/packages/frames.js/src/getFrameHtml.ts b/packages/frames.js/src/getFrameHtml.ts index f152a1aa4..639afb8df 100644 --- a/packages/frames.js/src/getFrameHtml.ts +++ b/packages/frames.js/src/getFrameHtml.ts @@ -1,4 +1,4 @@ -import { Frame } from "./types"; +import type { Frame } from "./types"; export interface GetFrameHtmlOptions { /** value for the OG "og:title" html tag*/ @@ -13,8 +13,8 @@ export interface GetFrameHtmlOptions { /** * Turns a `Frame` into html - * @param frame The Frame to turn into html - * @param options additional options passs into the html string + * @param frame - The Frame to turn into html + * @param options - additional options passs into the html string * @returns an html string */ export function getFrameHtml( @@ -36,7 +36,7 @@ export function getFrameHtml( /** * Formats a `Frame` ready to be included in a of an html string - * @param frame The `Frame` to get the contents for + * @param frame - The `Frame` to get the contents for * @returns an string with tags to be included in a */ export function getFrameHtmlHead(frame: Frame): string { @@ -54,9 +54,7 @@ export function getFrameHtmlHead(frame: Frame): string { : "", ...(frame.buttons?.flatMap((button, index) => [ ``, - button.action - ? `` - : "", + ``, button.target ? `` : "", @@ -81,9 +79,7 @@ export function getFrameHtmlHead(frame: Frame): string { : "", ...(frame.buttons?.flatMap((button, index) => [ ``, - button.action - ? `` - : "", + ``, button.target ? `` : "", @@ -92,10 +88,10 @@ export function getFrameHtmlHead(frame: Frame): string { : "", ]) ?? []), ], - ...(frame.accepts?.map( + ...frame.accepts.map( ({ id, version }) => `` - ) ?? []) + ) ); } diff --git a/packages/frames.js/src/getFrameMessage.ts b/packages/frames.js/src/getFrameMessage.ts index f0a4bf1d6..1b14da6bf 100644 --- a/packages/frames.js/src/getFrameMessage.ts +++ b/packages/frames.js/src/getFrameMessage.ts @@ -1,5 +1,5 @@ -import { type FrameActionMessage, Message } from "./farcaster"; import { bytesToHex } from "viem/utils"; +import { type FrameActionMessage, Message } from "./farcaster"; import { normalizeCastId } from "./utils"; import type { FrameActionDataParsed, @@ -46,11 +46,8 @@ export async function getFrameMessage( inputText: inputTextBytes, state: stateBytes, transactionId: transactionIdBytes, - } = (decodedMessage.data - .frameActionBody as typeof decodedMessage.data.frameActionBody) || {}; - const inputText = inputTextBytes - ? Buffer.from(inputTextBytes).toString("utf-8") - : undefined; + } = decodedMessage.data.frameActionBody; + const inputText = Buffer.from(inputTextBytes).toString("utf-8"); const transactionId = transactionIdBytes.length > 0 ? bytesToHex(transactionIdBytes) : undefined; const requesterFid = decodedMessage.data.fid; @@ -63,9 +60,7 @@ export async function getFrameMessage( ? bytesToHex(decodedMessage.data.frameActionBody.address) : undefined; - const state = stateBytes - ? Buffer.from(stateBytes).toString("utf-8") - : undefined; + const state = Buffer.from(stateBytes).toString("utf-8"); const parsedData: FrameActionDataParsed = { buttonIndex, @@ -125,7 +120,12 @@ export async function getFrameMessage( const requesterCustodyAddress = requesterEthAddresses.find( (item) => item.type === "custody" - )!.address; + )?.address; + + if (!requesterCustodyAddress) { + throw new Error("Custody address not found"); + } + const requesterVerifiedAddresses = requesterEthAddresses .filter((item) => item.type === "verified") .map((item) => item.address); @@ -133,8 +133,8 @@ export async function getFrameMessage( // Perform actions to fetch the HubFrameContext and then return the combined result const hubContext: FrameActionHubContext = { isValid: validationResult.isValid, - casterFollowsRequester: casterFollowsRequester, - requesterFollowsCaster: requesterFollowsCaster, + casterFollowsRequester, + requesterFollowsCaster, likedCast, recastedCast, requesterVerifiedAddresses, @@ -142,7 +142,7 @@ export async function getFrameMessage( requesterUserData, }; return { ...parsedData, ...hubContext } as FrameMessageReturnType; - } else { - return parsedData as FrameMessageReturnType; } + + return parsedData as FrameMessageReturnType; } diff --git a/packages/frames.js/src/getTokenFromUrl.test.ts b/packages/frames.js/src/getTokenFromUrl.test.ts index c06438561..d4fa36748 100644 --- a/packages/frames.js/src/getTokenFromUrl.test.ts +++ b/packages/frames.js/src/getTokenFromUrl.test.ts @@ -1,7 +1,7 @@ import { getTokenFromUrl } from "."; describe("getTokenFromUrl", () => { - it("should get the token from the token url", async () => { + it("should get the token from the token url", () => { expect( getTokenFromUrl( "eip155:7777777:0x060f3edd18c47f59bd23d063bbeb9aa4a8fec6df:123" @@ -14,7 +14,7 @@ describe("getTokenFromUrl", () => { }); }); - it("throws for invalid url", async () => { + it("throws for invalid url", () => { expect(() => getTokenFromUrl("a")).toThrow("Invalid token URL"); }); }); diff --git a/packages/frames.js/src/getTokenFromUrl.ts b/packages/frames.js/src/getTokenFromUrl.ts index cf3b12d5b..84c1aa936 100644 --- a/packages/frames.js/src/getTokenFromUrl.ts +++ b/packages/frames.js/src/getTokenFromUrl.ts @@ -15,7 +15,7 @@ export function getTokenFromUrl(url: string): ParsedToken { } return { - namespace: namespace, + namespace, chainId: parseInt(chainId), address, tokenId: tokenId || undefined, diff --git a/packages/frames.js/src/getTokenUrl.test.ts b/packages/frames.js/src/getTokenUrl.test.ts index c3e35e920..68a7aeafc 100644 --- a/packages/frames.js/src/getTokenUrl.test.ts +++ b/packages/frames.js/src/getTokenUrl.test.ts @@ -1,8 +1,8 @@ -import { getTokenUrl } from "."; import { zora } from "viem/chains"; +import { getTokenUrl } from "."; describe("getTokenUrl", () => { - it("should get token url", async () => { + it("should get token url", () => { expect( getTokenUrl({ address: "0x060f3edd18c47f59bd23d063bbeb9aa4a8fec6df", @@ -20,7 +20,7 @@ describe("getTokenUrl", () => { ).toBe(`eip155:1:0x060f3edd18c47f59bd23d063bbeb9aa4a8fec6df:123`); }); - it("should get token url with chain object", async () => { + it("should get token url with chain object", () => { expect( getTokenUrl({ address: "0x060f3edd18c47f59bd23d063bbeb9aa4a8fec6df", diff --git a/packages/frames.js/src/getTokenUrl.ts b/packages/frames.js/src/getTokenUrl.ts index 14e119a2b..3cb58a9fb 100644 --- a/packages/frames.js/src/getTokenUrl.ts +++ b/packages/frames.js/src/getTokenUrl.ts @@ -1,4 +1,4 @@ -import { Chain } from "viem/chains"; +import type { Chain } from "viem/chains"; type GetTokenUrlParameters = { /** Address of smart contract on the given chain */ diff --git a/packages/frames.js/src/getUserDataForFid.test.ts b/packages/frames.js/src/getUserDataForFid.test.ts index cd3d7239e..ed633968d 100644 --- a/packages/frames.js/src/getUserDataForFid.test.ts +++ b/packages/frames.js/src/getUserDataForFid.test.ts @@ -1,10 +1,11 @@ -import nock from "nock"; -import { getUserDataForFid } from "."; +// eslint-disable-next-line import/no-extraneous-dependencies -- this is dev dependency +import nock, { cleanAll } from "nock"; import { DEFAULT_HUB_API_URL } from "./default"; +import { getUserDataForFid } from "."; describe("getUserDataForFid", () => { beforeEach(() => { - nock.cleanAll(); + cleanAll(); }); it("returns latest user data for fid", async () => { diff --git a/packages/frames.js/src/getUserDataForFid.ts b/packages/frames.js/src/getUserDataForFid.ts index 62b38f25e..b3b1bd105 100644 --- a/packages/frames.js/src/getUserDataForFid.ts +++ b/packages/frames.js/src/getUserDataForFid.ts @@ -1,6 +1,6 @@ import { DEFAULT_HUB_API_KEY, DEFAULT_HUB_API_URL } from "./default"; import { MessageType, UserDataType, Message } from "./farcaster"; -import { HubHttpUrlOptions, UserDataReturnType } from "./types"; +import type { HubHttpUrlOptions, UserDataReturnType } from "./types"; /** * Returns the latest user data for a given Farcaster users Fid if available. @@ -39,29 +39,30 @@ export async function getUserDataForFid< })) as { messages?: Record[] }; if (messages && messages.length > 0) { - const valuesByType = messages.reduce( - (acc, messageJson) => { - const message = Message.fromJSON(messageJson); + const valuesByType = messages.reduce< + Partial> + >((acc, messageJson) => { + const message = Message.fromJSON(messageJson); - if (message.data?.type !== MessageType.USER_DATA_ADD) { - return acc; - } - - const timestamp = message.data.timestamp; - const { type, value } = message.data.userDataBody!; + if (message.data?.type !== MessageType.USER_DATA_ADD) { + return acc; + } - if (!acc[type]) { - acc[type] = { value, timestamp }; - } else if (message.data.timestamp > acc[type]!.timestamp) { - acc[type] = { value, timestamp }; - } + if (!message.data.userDataBody) { return acc; - }, - {} as Record< - UserDataType, - undefined | { value: string; timestamp: number } - > - ); + } + + const timestamp = message.data.timestamp; + const { type, value } = message.data.userDataBody; + + if (acc[type] && acc[type].timestamp < timestamp) { + acc[type] = { value, timestamp }; + } else { + acc[type] = { value, timestamp }; + } + + return acc; + }, {}); return { profileImage: valuesByType[UserDataType.PFP]?.value, @@ -69,7 +70,7 @@ export async function getUserDataForFid< username: valuesByType[UserDataType.USERNAME]?.value, bio: valuesByType[UserDataType.BIO]?.value, }; - } else { - return null; } + + return null; } diff --git a/packages/frames.js/src/hono/index.test.tsx b/packages/frames.js/src/hono/index.test.tsx index 205b61c25..de3949743 100644 --- a/packages/frames.js/src/hono/index.test.tsx +++ b/packages/frames.js/src/hono/index.test.tsx @@ -8,12 +8,12 @@ describe("hono adapter", () => { it("correctly integrates with Hono", async () => { const frames = lib.createFrames(); - const handler = frames(async (ctx) => { + const handler = frames((ctx) => { expect(ctx.request.url).toBe("http://localhost:3000/"); return { image: Test, - buttons: [Click me], + buttons: [Click me], }; }); @@ -39,7 +39,7 @@ describe("hono adapter", () => { }, }); - const handler = frames(async (ctx) => { + const handler = frames((ctx) => { expect(ctx.state).toEqual({ test: false }); return { diff --git a/packages/frames.js/src/hono/index.ts b/packages/frames.js/src/hono/index.ts index f580d6f4c..71a7f953b 100644 --- a/packages/frames.js/src/hono/index.ts +++ b/packages/frames.js/src/hono/index.ts @@ -1,7 +1,9 @@ -export { Button, type types } from "../core"; -import { createFrames as coreCreateFrames, types } from "../core"; import type { Handler } from "hono"; -import { CoreMiddleware } from "../middleware"; +import type { types } from "../core"; +import { createFrames as coreCreateFrames } from "../core"; +import type { CoreMiddleware } from "../middleware"; + +export { Button, type types } from "../core"; type CreateFramesForHono = types.CreateFramesFunctionDefinition< CoreMiddleware, @@ -12,6 +14,7 @@ type CreateFramesForHono = types.CreateFramesFunctionDefinition< * Creates Frames instance to use with you Hono server * * @example + * ```tsx * import { createFrames, Button } from 'frames.js/hono'; * import { Hono } from 'hono'; * @@ -30,17 +33,24 @@ type CreateFramesForHono = types.CreateFramesFunctionDefinition< * const app = new Hono(); * * app.on(['GET', 'POST'], '/', honoHandler); + * ``` */ -// @ts-expect-error +// @ts-expect-error -- this code is correct just function doesn't satisfy the type export const createFrames: CreateFramesForHono = function createFramesForHono( - options?: types.FramesOptions + options?: types.FramesOptions ) { const frames = coreCreateFrames(options); return function honoFramesHandler< - TPerRouteMiddleware extends types.FramesMiddleware[], + TPerRouteMiddleware extends types.FramesMiddleware< + types.JsonValue | undefined, + Record + >[], >( - handler: types.FrameHandlerFunction, + handler: types.FrameHandlerFunction< + types.JsonValue | undefined, + Record + >, handlerOptions?: types.FramesRequestHandlerFunctionOptions ) { const framesHandler = frames(handler, handlerOptions); diff --git a/packages/frames.js/src/hono/test.types.tsx b/packages/frames.js/src/hono/test.types.tsx index 29502138d..43368bf2a 100644 --- a/packages/frames.js/src/hono/test.types.tsx +++ b/packages/frames.js/src/hono/test.types.tsx @@ -1,5 +1,7 @@ -import { Handler } from 'hono'; -import { createFrames, types } from '.'; +/* eslint-disable @typescript-eslint/require-await -- we are testing for promise compatibility */ +import type { Handler } from 'hono'; +import type { types } from '.'; +import { createFrames } from '.'; const framesWithoutState = createFrames(); framesWithoutState(async ctx => { @@ -38,8 +40,29 @@ framesWithExplicitState(async ctx => { test: boolean; }; ctx satisfies { - message?: any; - pressedButton?: any; + message?: unknown; + pressedButton?: unknown; + request: Request; + } + + return { + image: 'http://test.png' + }; +}) satisfies Handler; + +const framesWithExplicitStateNoPromise = createFrames<{ + test: boolean; +}>({}); +framesWithExplicitStateNoPromise(ctx => { + ctx.state satisfies { + test: boolean; + }; + ctx.initialState satisfies { + test: boolean; + }; + ctx satisfies { + message?: unknown; + pressedButton?: unknown; request: Request; } diff --git a/packages/frames.js/src/lib/stream-pump.ts b/packages/frames.js/src/lib/stream-pump.ts index 0e643ba00..d89dd54b6 100644 --- a/packages/frames.js/src/lib/stream-pump.ts +++ b/packages/frames.js/src/lib/stream-pump.ts @@ -27,16 +27,13 @@ export class StreamPump { new Stream.Readable().readableHighWaterMark; this.accumalatedSize = 0; this.stream = stream; - this.enqueue = this.enqueue.bind(this); - this.error = this.error.bind(this); - this.close = this.close.bind(this); } - size(chunk: Uint8Array) { - return chunk?.byteLength || 0; + size(chunk: Uint8Array): number { + return chunk.byteLength || 0; } - start(controller: ReadableStreamController) { + start(controller: ReadableStreamController): void { this.controller = controller; this.stream.on("data", this.enqueue); this.stream.once("error", this.error); @@ -44,11 +41,11 @@ export class StreamPump { this.stream.once("close", this.close); } - pull() { + pull(): void { this.resume(); } - cancel(reason?: Error) { + cancel(reason?: Error): void { if (this.stream.destroy) { this.stream.destroy(reason); } @@ -59,17 +56,17 @@ export class StreamPump { this.stream.off("close", this.close); } - enqueue(chunk: Uint8Array | string) { + enqueue = (chunk: Uint8Array | string): void => { if (this.controller) { try { - let bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk); + const bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk); - let available = (this.controller.desiredSize || 0) - bytes.byteLength; + const available = (this.controller.desiredSize || 0) - bytes.byteLength; this.controller.enqueue(bytes); if (available <= 0) { this.pause(); } - } catch (error: any) { + } catch (error) { this.controller.error( new Error( "Could not create Buffer, chunk must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object" @@ -78,53 +75,54 @@ export class StreamPump { this.cancel(); } } - } + }; - pause() { + pause(): void { if (this.stream.pause) { this.stream.pause(); } } - resume() { + resume(): void { if (this.stream.readable && this.stream.resume) { this.stream.resume(); } } - close() { + close = (): void => { if (this.controller) { this.controller.close(); delete this.controller; } - } + }; - error(error: Error) { + error = (error: Error): void => { if (this.controller) { this.controller.error(error); delete this.controller; } - } + }; } -export const createReadableStreamFromReadable = ( +export function createReadableStreamFromReadable( source: Readable & { readableHighWaterMark?: number } -) => { - let pump = new StreamPump(source); - let stream = new ReadableStream(pump, pump); +): ReadableStream { + const pump = new StreamPump(source); + const stream = new ReadableStream(pump, pump); return stream; -}; +} -export async function writeReadableStreamToWritable( - stream: ReadableStream, +export async function writeReadableStreamToWritable( + stream: ReadableStream, writable: Writable -) { - let reader = stream.getReader(); - let flushable = writable as { flush?: Function }; +): Promise { + const reader = stream.getReader(); + const flushable = writable as { flush?: () => void }; try { + // eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition -- this is expected to be exhaustive while (true) { - let { done, value } = await reader.read(); + const { done, value } = await reader.read(); if (done) { writable.end(); @@ -132,6 +130,7 @@ export async function writeReadableStreamToWritable( } writable.write(value); + if (typeof flushable.flush === "function") { flushable.flush(); } diff --git a/packages/frames.js/src/middleware/concurrentMiddleware.test.ts b/packages/frames.js/src/middleware/concurrentMiddleware.test.ts index b99208c55..b78f0a31a 100644 --- a/packages/frames.js/src/middleware/concurrentMiddleware.test.ts +++ b/packages/frames.js/src/middleware/concurrentMiddleware.test.ts @@ -1,4 +1,6 @@ +/* eslint-disable @typescript-eslint/require-await -- middleware is always async */ import { redirect } from "../core/redirect"; +import type { FramesContext, FramesMiddleware } from "../core/types"; import { concurrentMiddleware } from "./concurrentMiddleware"; describe("concurrentMiddleware", () => { @@ -7,16 +9,18 @@ describe("concurrentMiddleware", () => { }); it("returns middleware as is if only one is provided", () => { - const middleware = async () => {}; + const middleware: FramesMiddleware = async () => { + return new Response(); + }; - expect(concurrentMiddleware(middleware as any)).toBe(middleware); + expect(concurrentMiddleware(middleware)).toBe(middleware); }); it("returns a middleware that wraps all provided middlewares", () => { - const middleware1 = async (context: any, next: any) => { + const middleware1: FramesMiddleware = async (context, next) => { return next(); }; - const middleware2 = async (context: any, next: any) => { + const middleware2: FramesMiddleware = async (context, next) => { return next(); }; @@ -26,18 +30,21 @@ describe("concurrentMiddleware", () => { }); it("calls all provided middlewares and properly mutates context and then calls next middleware", async () => { - const middleware1 = async (context: any, next: any) => { - await new Promise((resolve) => setTimeout(resolve, 5)); + const middleware1: FramesMiddleware = async (context, next) => { + await new Promise((resolve) => { + setTimeout(resolve, 5); + }); return next({ test1: true }); }; - const middleware2 = async (context: any, next: any) => { + const middleware2: FramesMiddleware = async (context, next) => { return next({ test2: true }); }; const parallelMiddleware = concurrentMiddleware(middleware1, middleware2); - let context: any = {}; + let context = {} as unknown as FramesContext; await parallelMiddleware(context, async (newCtx) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- we don't care really about the type of the value context = newCtx; return redirect("http://test.com"); }); diff --git a/packages/frames.js/src/middleware/concurrentMiddleware.ts b/packages/frames.js/src/middleware/concurrentMiddleware.ts index a3d834830..307b401af 100644 --- a/packages/frames.js/src/middleware/concurrentMiddleware.ts +++ b/packages/frames.js/src/middleware/concurrentMiddleware.ts @@ -17,12 +17,12 @@ export function concurrentMiddleware< throw new Error("No middlewares provided"); } - if (middlewares.length === 1) { - return middlewares[0]!; + if (middlewares.length === 1 && middlewares[0]) { + return middlewares[0]; } return async (context, next) => { - let ctx = context; + const ctx = context; const newContexts: any[] = []; await Promise.all( @@ -36,9 +36,10 @@ export function concurrentMiddleware< ) ); - let finalCtx = ctx; + let finalCtx: typeof ctx = ctx; for (const newCtx of newContexts) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- this is correct because we are merging objects finalCtx = { ...finalCtx, ...newCtx }; } diff --git a/packages/frames.js/src/middleware/farcaster.test.ts b/packages/frames.js/src/middleware/farcaster.test.ts index 512be0fb1..a9e0a9df5 100644 --- a/packages/frames.js/src/middleware/farcaster.test.ts +++ b/packages/frames.js/src/middleware/farcaster.test.ts @@ -1,10 +1,10 @@ +// eslint-disable-next-line import/no-extraneous-dependencies -- dev dependency +import { disableNetConnect, enableNetConnect } from "nock"; import { Message } from "../farcaster"; -import nock from "nock"; -import { FrameMessageReturnType } from "../getFrameMessage"; import { redirect } from "../core/redirect"; -import { FramesContext } from "../core/types"; -import { farcaster } from "./farcaster"; +import type { FramesContext } from "../core/types"; import { createFrames } from "../core"; +import { farcaster } from "./farcaster"; describe("farcaster middleware", () => { let sampleFrameActionRequest: Request; @@ -41,17 +41,17 @@ describe("farcaster middleware", () => { beforeEach(() => { // make sure we don't introduce unexpected network requests // TODO: Mock hub/onchain calls - nock.disableNetConnect(); + disableNetConnect(); }); afterEach(() => { - nock.enableNetConnect(); + enableNetConnect(); }); it("moves to next middleware without parsing if request is not POST request", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com"), - } as any; + } as unknown as FramesContext; const mw = farcaster(); const response = redirect("http://test.com"); @@ -62,12 +62,12 @@ describe("farcaster middleware", () => { }); it("moves to next middleware if request is POST but does not have a valid JSON body", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: "invalid json", }), - } as any; + } as unknown as FramesContext; const mw = farcaster(); const response = redirect("http://test.com"); @@ -78,12 +78,12 @@ describe("farcaster middleware", () => { }); it("moves to next middleware if request is POST with valid JSON but invalid body shape", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: JSON.stringify({}), }), - } as any; + } as unknown as FramesContext; const mw = farcaster(); const response = redirect("http://test.com"); @@ -94,33 +94,32 @@ describe("farcaster middleware", () => { }); it("parses frame message from request body and adds it to context", async () => { - const context: FramesContext = { + const context = { request: sampleFrameActionRequest.clone(), - } as any; + } as unknown as FramesContext; const mw = farcaster(); const next = jest.fn(() => Promise.resolve(new Response())); - nock.disableNetConnect(); + disableNetConnect(); await mw(context, next); - nock.enableNetConnect(); - - const { - message: calledArg, - }: { - message: FrameMessageReturnType<{ fetchHubContext: true }>; - } = (next.mock.calls[0]! as any)[0]; - - expect(calledArg.buttonIndex).toBe(1); - expect(calledArg.castId).toEqual({ - fid: 456, - hash: "0x", - }); - expect(calledArg.connectedAddress).toBe("0x89"); - expect(calledArg.inputText).toBe("hello"); - expect(calledArg.requesterFid).toBe(123); - expect(calledArg.state).toEqual(JSON.stringify({ test: true })); - expect(calledArg).not.toHaveProperty("requesterUserData"); + enableNetConnect(); + + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + message: { + buttonIndex: 1, + castId: { + fid: 456, + hash: "0x", + }, + connectedAddress: "0x789", + inputText: "hello", + requesterFid: 123, + state: JSON.stringify({ test: true }), + }, + }) + ); }); it("supports custom global typed context", async () => { @@ -130,7 +129,7 @@ describe("farcaster middleware", () => { middleware: [mw1], }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { return { image: `/?fid=${ctx.message?.requesterFid}`, }; diff --git a/packages/frames.js/src/middleware/farcaster.ts b/packages/frames.js/src/middleware/farcaster.ts index 37e6a8ec1..266f37c45 100644 --- a/packages/frames.js/src/middleware/farcaster.ts +++ b/packages/frames.js/src/middleware/farcaster.ts @@ -14,7 +14,7 @@ function isValidFrameActionPayload( ): value is FrameActionPayload { return ( typeof value === "object" && - !!value && + value !== null && "trustedData" in value && "untrustedData" in value ); @@ -25,12 +25,12 @@ async function decodeFrameActionPayloadFromRequest( ): Promise { try { // use clone just in case someone wants to read body somewhere along the way - const body = await request + const body = (await request .clone() .json() .catch(() => { throw new RequestBodyNotJSONError(); - }); + })) as JsonValue; if (!isValidFrameActionPayload(body)) { throw new InvalidFrameActionPayloadError(); @@ -45,6 +45,7 @@ async function decodeFrameActionPayloadFromRequest( return undefined; } + // eslint-disable-next-line no-console -- provide feedback to the developer console.error(e); return undefined; diff --git a/packages/frames.js/src/middleware/farcasterHubContext.test.ts b/packages/frames.js/src/middleware/farcasterHubContext.test.ts index d1ee23b9c..872f6a55d 100644 --- a/packages/frames.js/src/middleware/farcasterHubContext.test.ts +++ b/packages/frames.js/src/middleware/farcasterHubContext.test.ts @@ -1,10 +1,9 @@ import { Message } from "../farcaster"; -import nock from "nock"; -import { FrameMessageReturnType } from "../getFrameMessage"; import { redirect } from "../core/redirect"; -import { FramesContext } from "../core/types"; -import { farcasterHubContext } from "./farcasterHubContext"; +import type { FramesContext } from "../core/types"; import { createFrames } from "../core"; +import type { UserDataReturnType } from ".."; +import { farcasterHubContext } from "./farcasterHubContext"; describe("farcasterHubContext middleware", () => { let sampleFrameActionRequest: Request; @@ -49,9 +48,9 @@ describe("farcasterHubContext middleware", () => { }); it("moves to next middleware without parsing if request is not POST request", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com"), - } as any; + } as unknown as FramesContext; const mw = farcasterHubContext(); const response = redirect("http://test.com"); @@ -62,12 +61,12 @@ describe("farcasterHubContext middleware", () => { }); it("moves to next middleware if request is POST but does not have a valid JSON body", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: "invalid json", }), - } as any; + } as unknown as FramesContext; const mw = farcasterHubContext(); const response = redirect("http://test.com"); @@ -78,12 +77,12 @@ describe("farcasterHubContext middleware", () => { }); it("moves to next middleware if request is POST with valid JSON but invalid body shape", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: JSON.stringify({}), }), - } as any; + } as unknown as FramesContext; const mw = farcasterHubContext(); const response = redirect("http://test.com"); @@ -94,31 +93,31 @@ describe("farcasterHubContext middleware", () => { }); it("parses frame message from request body and fetches external hub context and adds it to context", async () => { - const context: FramesContext = { + const context = { request: sampleFrameActionRequest.clone(), - } as any; + } as unknown as FramesContext; const mw = farcasterHubContext(); const next = jest.fn(() => Promise.resolve(new Response())); await mw(context, next); - const { - message: calledArg, - }: { - message: FrameMessageReturnType<{ fetchHubContext: true }>; - } = (next.mock.calls[0]! as any)[0]; - - expect(calledArg.buttonIndex).toBe(1); - expect(calledArg.castId).toEqual({ - fid: 456, - hash: "0x", - }); - expect(calledArg.connectedAddress).toBe("0x89"); - expect(calledArg.inputText).toBe("hello"); - expect(calledArg.requesterFid).toBe(123); - expect(calledArg.state).toEqual(JSON.stringify({ test: true })); - expect(calledArg).toHaveProperty("requesterUserData"); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + message: { + buttonIndex: 1, + castId: { + fid: 456, + hash: "0x", + }, + connectedAddress: "0x89", + inputText: "hello", + requesterFid: 123, + state: JSON.stringify({ test: true }), + requestedUserData: expect.anything() as UserDataReturnType, + }, + }) + ); }); it("supports custom global typed context", async () => { @@ -128,7 +127,7 @@ describe("farcasterHubContext middleware", () => { middleware: [mw1], }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { return { image: `/?username=${ctx.message?.requesterUserData?.username}`, }; diff --git a/packages/frames.js/src/middleware/farcasterHubContext.ts b/packages/frames.js/src/middleware/farcasterHubContext.ts index fbbd7c9b3..e068b26bd 100644 --- a/packages/frames.js/src/middleware/farcasterHubContext.ts +++ b/packages/frames.js/src/middleware/farcasterHubContext.ts @@ -18,7 +18,7 @@ function isValidFrameActionPayload( ): value is FrameActionPayload { return ( typeof value === "object" && - !!value && + value !== null && "trustedData" in value && "untrustedData" in value ); @@ -29,12 +29,12 @@ async function decodeFrameActionPayloadFromRequest( ): Promise { try { // use clone just in case someone wants to read body somewhere along the way - const body = await request + const body = (await request .clone() .json() .catch(() => { throw new RequestBodyNotJSONError(); - }); + })) as JSON; if (!isValidFrameActionPayload(body)) { throw new InvalidFrameActionPayloadError(); @@ -49,6 +49,7 @@ async function decodeFrameActionPayloadFromRequest( return undefined; } + // eslint-disable-next-line no-console -- provide feedback to the developer console.error(e); return undefined; diff --git a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts index 6f37580e9..1943eb160 100644 --- a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts +++ b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console -- we are expecting console.log usage */ import { redirect } from "../core/redirect"; import type { FramesContext } from "../core/types"; import { generatePostButtonTargetURL } from "../core/utils"; @@ -5,10 +6,10 @@ import { framesjsMiddleware } from "./framesjsMiddleware"; describe("framesjsMiddleware middleware", () => { it("does not provide pressedButton to context if no supported button is detected", async () => { - const context: FramesContext = { + const context = { url: new URL("https://example.com"), request: new Request("https://example.com", { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(); const middleware = framesjsMiddleware(); @@ -31,10 +32,10 @@ describe("framesjsMiddleware middleware", () => { query: { test: true }, }, }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url, { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(); const middleware = framesjsMiddleware(); @@ -57,10 +58,10 @@ describe("framesjsMiddleware middleware", () => { currentURL: new URL("https://example.com"), target: "/test", }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url, { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(() => Promise.resolve(redirect("http://test.com"))); const middleware = framesjsMiddleware(); @@ -86,10 +87,10 @@ describe("framesjsMiddleware middleware", () => { currentURL: new URL("https://example.com"), target: "/test", }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url, { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(() => Promise.resolve(new Response(null, { status: 404 })) ); @@ -114,10 +115,10 @@ describe("framesjsMiddleware middleware", () => { query: { test: true }, }, }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url, { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(() => Promise.resolve(redirect("http://test.com"))); const middleware = framesjsMiddleware(); @@ -140,10 +141,10 @@ describe("framesjsMiddleware middleware", () => { query: { test: true }, }, }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url, { method: "POST" }), - } as any; + } as unknown as FramesContext; const next = jest.fn(() => Promise.resolve(new Response(null, { status: 200 })) ); @@ -167,11 +168,11 @@ describe("framesjsMiddleware middleware", () => { query: { test: true }, }, }); - const context: FramesContext = { + const context = { url: new URL(url), request: new Request(url), searchParams: {}, - } as any; + } as unknown as FramesContext; const next = jest.fn(); const middleware = framesjsMiddleware(); diff --git a/packages/frames.js/src/middleware/framesjsMiddleware.ts b/packages/frames.js/src/middleware/framesjsMiddleware.ts index d635a9bd6..dfd2a8d9a 100644 --- a/packages/frames.js/src/middleware/framesjsMiddleware.ts +++ b/packages/frames.js/src/middleware/framesjsMiddleware.ts @@ -7,9 +7,7 @@ import { type FramesjsMiddlewareContext = { /** the search params as an object. Empty will be an empty object */ - searchParams: { - [k: string]: string; - }; + searchParams: Record; /** * Button that was clicked on previous frame */ @@ -47,6 +45,7 @@ export function framesjsMiddleware(): FramesMiddleware< (result.status < 300 || result.status > 399)) || !isFrameRedirect(result) ) { + // eslint-disable-next-line no-console -- provide feedback to the developer console.warn( "The clicked button action was post_redirect, but the response was not a redirect" ); @@ -54,6 +53,7 @@ export function framesjsMiddleware(): FramesMiddleware< } else if (pressedButton?.action === "post") { // we support only frame definition as result for post button if (result instanceof Response || isFrameRedirect(result)) { + // eslint-disable-next-line no-console -- provide feedback to the developer console.warn( "The clicked button action was post, but the response was not a frame definition" ); diff --git a/packages/frames.js/src/middleware/openframes.test.ts b/packages/frames.js/src/middleware/openframes.test.ts index 64610b484..f9e437c83 100644 --- a/packages/frames.js/src/middleware/openframes.test.ts +++ b/packages/frames.js/src/middleware/openframes.test.ts @@ -1,35 +1,32 @@ import { FramesClient } from "@xmtp/frames-client"; -import { XmtpValidator } from "@xmtp/frames-validator"; -import { Client, Signer } from "@xmtp/xmtp-js"; -import { WalletClient, createWalletClient, http } from "viem"; +import type { Signer } from "@xmtp/xmtp-js"; +import { Client } from "@xmtp/xmtp-js"; +import type { WalletClient } from "viem"; +import { createWalletClient, http } from "viem"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { mainnet } from "viem/chains"; -import { - XmtpFrameMessageReturnType, - getXmtpFrameMessage, - isXmtpFrameActionPayload, -} from "../xmtp"; +import type { XmtpFrameMessageReturnType } from "../xmtp"; +import { getXmtpFrameMessage, isXmtpFrameActionPayload } from "../xmtp"; import { redirect } from "../core/redirect"; -import { +import type { FrameDefinition, FramesContext, FramesMiddleware, } from "../core/types"; import { isFrameDefinition } from "../core/utils"; -import { OpenFramesMessageContext, openframes } from "./openframes"; -import { renderResponse } from "./renderResponse"; import { createFrames } from "../core"; +import type { OpenFramesMessageContext } from "./openframes"; +import { openframes } from "./openframes"; +import { renderResponse } from "./renderResponse"; -export function convertWalletClientToSigner( - walletClient: WalletClient -): Signer { +function convertWalletClientToSigner(walletClient: WalletClient): Signer { const { account } = walletClient; - if (!account || !account.address) { + if (!account) { throw new Error("WalletClient is not configured"); } return { - getAddress: async () => account.address, + getAddress: () => Promise.resolve(account.address), signMessage: async (message: string | Uint8Array) => walletClient.signMessage({ message: typeof message === "string" ? message : { raw: message }, @@ -41,7 +38,6 @@ export function convertWalletClientToSigner( describe("openframes middleware", () => { let xmtpClient: Client; let framesClient: FramesClient; - let xmtpValidator: XmtpValidator; let xmtpMiddleware: FramesMiddleware< any, @@ -66,8 +62,6 @@ describe("openframes middleware", () => { xmtpClient = await Client.create(signer, { env: "dev" }); framesClient = new FramesClient(xmtpClient); - xmtpValidator = new XmtpValidator(); - xmtpMiddleware = openframes({ clientProtocol: { id: "xmtp", @@ -81,10 +75,12 @@ describe("openframes middleware", () => { } const result = await getXmtpFrameMessage(body); - let state: any; + let state: (typeof result)["state"] | undefined; + try { - state = JSON.parse(result.state); + state = JSON.parse(result.state) as typeof state; } catch (error) { + // eslint-disable-next-line no-console -- we are expecting invalid state console.error("openframes: Could not parse state"); } @@ -111,21 +107,21 @@ describe("openframes middleware", () => { state: JSON.stringify({ test: true }), }); - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: JSON.stringify(signedPayload), }), url: new URL("https://example.com").toString(), basePath: "/", - } as any; + } as unknown as FramesContext; const mw1 = openframes({ clientProtocol: "foo@vNext", handler: { isValidPayload: () => false, - getFrameMessage: async () => { - return {}; + getFrameMessage: () => { + return Promise.resolve({}); }, }, }); @@ -133,14 +129,13 @@ describe("openframes middleware", () => { clientProtocol: "bar@vNext", handler: { isValidPayload: () => false, - getFrameMessage: async () => { - return {}; + getFrameMessage: () => { + return Promise.resolve({}); }, }, }); - // eslint-disable-next-line no-unused-vars - const next1 = jest.fn((...args) => + const next1 = jest.fn(() => Promise.resolve({ image: "/test.png", } as FrameDefinition) @@ -154,8 +149,7 @@ describe("openframes middleware", () => { version: "vNext", }); - // eslint-disable-next-line no-unused-vars - const next2 = jest.fn((...args) => Promise.resolve(nextResult1)); + const next2 = jest.fn(() => Promise.resolve(nextResult1)); const nextResult2 = await mw2(context, next2); @@ -169,9 +163,9 @@ describe("openframes middleware", () => { version: "vNext", }); + // eslint-disable-next-line testing-library/render-result-naming-convention -- this is not a react renderer const responseMw = renderResponse(); - // eslint-disable-next-line no-unused-vars - const responseNext = jest.fn((...args) => Promise.resolve(nextResult2)); + const responseNext = jest.fn(() => Promise.resolve(nextResult2)); const response = (await responseMw(context, responseNext)) as Response; const responseText = await response.text(); expect(responseText).toContain( @@ -183,9 +177,9 @@ describe("openframes middleware", () => { }); it("moves to next middleware without parsing if request is not POST request", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com"), - } as any; + } as unknown as FramesContext; const mw = xmtpMiddleware; const response = redirect("http://test.com"); @@ -196,12 +190,12 @@ describe("openframes middleware", () => { }); it("moves to next middleware if request is POST but does not have a valid JSON body", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: "invalid json", }), - } as any; + } as unknown as FramesContext; const mw = xmtpMiddleware; const response = redirect("http://test.com"); @@ -216,8 +210,8 @@ describe("openframes middleware", () => { clientProtocol: "foo@vNext", handler: { isValidPayload: () => false, - getFrameMessage: async () => { - return { test1: true }; + getFrameMessage: () => { + return Promise.resolve({ test1: true }); }, }, }); @@ -225,8 +219,8 @@ describe("openframes middleware", () => { clientProtocol: "bar@vNext", handler: { isValidPayload: () => true, - getFrameMessage: async () => { - return { test2: true }; + getFrameMessage: () => { + return Promise.resolve({ test2: true }); }, }, }); @@ -235,7 +229,7 @@ describe("openframes middleware", () => { middleware: [mw1, mw2], }); - const routeHandler = handler(async (ctx) => { + const routeHandler = handler((ctx) => { return { image: `/?test1=${ctx.message?.test1}&test2=${ctx.message?.test2}`, }; @@ -254,12 +248,12 @@ describe("openframes middleware", () => { }); it("moves to next middleware if request is POST with valid JSON but invalid body shape", async () => { - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: JSON.stringify({}), }), - } as any; + } as unknown as FramesContext; const mw = xmtpMiddleware; const response = redirect("http://test.com"); @@ -282,28 +276,30 @@ describe("openframes middleware", () => { state: JSON.stringify({ test: true }), }); - const context: FramesContext = { + const context = { request: new Request("https://example.com", { method: "POST", body: JSON.stringify(signedPayload), }), - } as any; + } as unknown as FramesContext; const mw = xmtpMiddleware; - // eslint-disable-next-line no-unused-vars - const next = jest.fn((...args) => Promise.resolve(new Response())); + const next = jest.fn(() => Promise.resolve(new Response())); await mw(context, next); - const calledArg: { clientProtocol: string; message: any } = - next.mock.calls[0]![0]; - - expect(calledArg.message.buttonIndex).toBe(buttonIndex); - expect(calledArg.message.inputText).toBe("hello"); - expect(calledArg.message.state).toMatchObject({ test: true }); - expect(calledArg.clientProtocol).toEqual({ - id: "xmtp", - version: "2024-02-09", - }); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + message: { + buttonIndex: 1, + inputText: "hello", + state: { test: true }, + }, + clientProtocol: { + id: "xmtp", + version: "2024-02-09", + }, + }) + ); }); }); diff --git a/packages/frames.js/src/middleware/openframes.ts b/packages/frames.js/src/middleware/openframes.ts index 7805ae7ee..30e0345c1 100644 --- a/packages/frames.js/src/middleware/openframes.ts +++ b/packages/frames.js/src/middleware/openframes.ts @@ -1,6 +1,7 @@ import type { ClientProtocolId } from "../types"; import { RequestBodyNotJSONError } from "../core/errors"; import type { + FramesHandlerFunctionReturnType, FramesMiddleware, FramesMiddlewareReturnType, } from "../core/types"; @@ -14,21 +15,19 @@ export type OpenFramesMessageContext = { }; export type ClientProtocolHandler = { - // eslint-disable-next-line no-unused-vars getFrameMessage: (body: JSON) => Promise | undefined; - // eslint-disable-next-line no-unused-vars isValidPayload: (body: JSON) => boolean; }; async function cloneJsonBody(request: Request): Promise { try { // use clone just in case someone wants to read body somewhere along the way - const body = await request + const body = (await request .clone() .json() .catch(() => { throw new RequestBodyNotJSONError(); - }); + })) as JSON; return body; } catch (e) { @@ -36,6 +35,7 @@ async function cloneJsonBody(request: Request): Promise { return undefined; } + // eslint-disable-next-line no-console -- provide feedback to the developer console.error(e); return undefined; @@ -48,11 +48,13 @@ async function nextInjectAcceptedClient({ }: { nextResult: FramesMiddlewareReturnType; clientProtocol: ClientProtocolId; -}) { +}): Promise | Response> { const result = await nextResult; + if (isFrameDefinition(result)) { return { ...result, accepts: [...(result.accepts || []), clientProtocol] }; } + return result; } @@ -70,19 +72,30 @@ export function openframes({ handler: ClientProtocolHandler; }): FramesMiddleware> { return async (context, next) => { - const clientProtocol: ClientProtocolId = - typeof clientProtocolRaw === "string" - ? { - id: clientProtocolRaw.split("@")[0]!, - version: clientProtocolRaw.split("@")[1]!, - } - : clientProtocolRaw; + let clientProtocol: ClientProtocolId; + + if (typeof clientProtocolRaw === "string") { + const [id, version] = clientProtocolRaw.split("@"); + + if (!id || !version) { + throw new Error( + `Invalid client protocol string. Expected format is "id@version"` + ); + } + + clientProtocol = { + id, + version, + }; + } else { + clientProtocol = clientProtocolRaw; + } // frame message is available only if the request is a POST request if (context.request.method !== "POST") { return nextInjectAcceptedClient({ nextResult: next(), - clientProtocol: clientProtocol, + clientProtocol, }); } diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index 03207b949..b80551609 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -1,3 +1,6 @@ +/* eslint-disable no-console -- we are expecting console usage */ +/* eslint-disable @typescript-eslint/require-await -- middleware expectes async functions */ +/* eslint-disable testing-library/render-result-naming-convention -- we are not using react testing library here */ import * as vercelOg from "@vercel/og"; import { FRAMES_META_TAGS_HEADER } from "../core"; import { Button } from "../core/components"; @@ -18,9 +21,8 @@ jest.mock("@vercel/og", () => { arrayBufferMock, constructorMock, ImageResponse: class { - constructor(...args: any[]) { - // @ts-expect-error - return constructorMock(...args); + constructor() { + constructorMock(); } arrayBuffer = arrayBufferMock; }, @@ -28,8 +30,8 @@ jest.mock("@vercel/og", () => { }); describe("renderResponse middleware", () => { - let arrayBufferMock: jest.Mock = (vercelOg as any).arrayBufferMock; - let constructorMock: jest.Mock = (vercelOg as any).constructorMock; + const arrayBufferMock: jest.Mock = (vercelOg as unknown as { arrayBufferMock: jest.Mock }).arrayBufferMock; + const constructorMock: jest.Mock = (vercelOg as unknown as { constructorMock: jest.Mock }).constructorMock; const render = renderResponse(); const context: FramesContext = { basePath: "/", @@ -93,18 +95,21 @@ describe("renderResponse middleware", () => { buttons: [ , , , - , - , - , - , + , + , + , + , + , ], }; }); @@ -228,8 +235,8 @@ describe("renderResponse middleware", () => { return { image:
My image
, buttons: [ - // @ts-expect-error - , + // @ts-expect-error -- props are not matching the expected type + , ], }; }); @@ -242,7 +249,7 @@ describe("renderResponse middleware", () => { }); it("returns 500 if invalid button shape is provided", async () => { - // @ts-expect-error + // @ts-expect-error -- returns invalid object shape const result = await render(context, async () => { return { image:
My image
, @@ -283,9 +290,9 @@ describe("renderResponse middleware", () => { expect.any(Object), expect.any(Object) ); - expect(constructorMock.mock.calls[0][0]).toMatchSnapshot(); + expect((constructorMock.mock.calls[0] as unknown[])[0]).toMatchSnapshot(); - // @ts-expect-error + // @ts-expect-error -- we are modifying the context object newContext.something = "true"; result = await render(newContext, async () => { @@ -308,7 +315,7 @@ describe("renderResponse middleware", () => { expect.any(Object), expect.any(Object) ); - expect(constructorMock.mock.calls[1][0]).toMatchSnapshot(); + expect((constructorMock.mock.calls[1] as unknown[])[0]).toMatchSnapshot(); }); it("correctly renders tx button", async () => { @@ -321,14 +328,14 @@ describe("renderResponse middleware", () => { return { image:
My image
, buttons: [ - , ], }; }); - const json = await (result as Response).json(); + const json = await (result as Response).json() as Record; expect(json["fc:frame:button:1"]).toBe("Tx button"); expect(json["fc:frame:button:1:action"]).toBe("tx"); @@ -360,9 +367,9 @@ describe("renderResponse middleware", () => { expect(console.warn).toHaveBeenCalledTimes(1); - const json = await (result as Response).json(); + const json = await (result as Response).json() as Record; - expect(json["state"]).toBeUndefined(); + expect(json.state).toBeUndefined(); }); it("returns a state on POST requests (these are not initial since those are always reactions to clicks)", async () => { @@ -382,7 +389,7 @@ describe("renderResponse middleware", () => { expect(console.warn).not.toHaveBeenCalled(); - const json = await (result as Response).json(); + const json = await (result as Response).json() as Record; expect(console.warn).not.toHaveBeenCalled(); expect(json["fc:frame:state"]).toEqual(JSON.stringify({ test: true })); @@ -440,6 +447,7 @@ describe("renderResponse middleware", () => { buttons: [ , null, true, - , ], @@ -486,12 +495,12 @@ describe("renderResponse middleware", () => { expect(result).toBeInstanceOf(Response); expect((result as Response).status).toBe(200); - const json = await (result as Response).json(); + const json = await (result as Response).json() as Record; expect(json).toMatchObject({ "fc:frame:button:1": "Click me 1", - "fc:frame:button:1:target": expect.any(String), + "fc:frame:button:1:target": expect.any(String) as string, "fc:frame:button:2": "Click me 2", - "fc:frame:button:2:target": expect.any(String), + "fc:frame:button:2:target": expect.any(String) as string, }); }); }); diff --git a/packages/frames.js/src/middleware/renderResponse.ts b/packages/frames.js/src/middleware/renderResponse.ts index 6659f404b..b40ac9f1e 100644 --- a/packages/frames.js/src/middleware/renderResponse.ts +++ b/packages/frames.js/src/middleware/renderResponse.ts @@ -14,7 +14,7 @@ import { generateTargetURL, isFrameRedirect, } from "../core/utils"; -import { FRAMES_META_TAGS_HEADER } from "../core"; +import { FRAMES_META_TAGS_HEADER } from "../core/constants"; class InvalidButtonShapeError extends Error {} @@ -30,15 +30,16 @@ class ImageRenderError extends Error {} * If the accept header is set to application/json, it will return the metadata as JSON * so it is easy to parse it for metatags in existing applications. */ -export function renderResponse(): FramesMiddleware { +export function renderResponse(): FramesMiddleware> { return async (context, next) => { const wantsJSON = context.request.headers.get("accept") === FRAMES_META_TAGS_HEADER; - let result: FramesHandlerFunctionReturnType | Response; + let result: FramesHandlerFunctionReturnType | Response | undefined; try { result = await next(context); } catch (e) { + // eslint-disable-next-line no-console -- provide feedback to the user console.error(e); return new Response( @@ -55,6 +56,7 @@ export function renderResponse(): FramesMiddleware { ); } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can happen if the handler returns undefined if (!result) { return new Response( wantsJSON @@ -91,6 +93,7 @@ export function renderResponse(): FramesMiddleware { if (context.request.method === "POST") { state = JSON.stringify(result.state); } else { + // eslint-disable-next-line no-console -- provide feedback to the user console.warn( "State is not supported on initial request (the one initialized when Frames are rendered for first time, that uses GET request) and will be ignored" ); @@ -116,6 +119,7 @@ export function renderResponse(): FramesMiddleware { }) : await renderImage(result.image, result.imageOptions).catch( (e) => { + // eslint-disable-next-line no-console -- provide feedback to the user console.error(e); throw new ImageRenderError("Could not render image"); @@ -124,7 +128,7 @@ export function renderResponse(): FramesMiddleware { buttons: result.buttons ?.slice() .filter( - (v: any): v is FrameButtonElement => v && typeof v === "object" + (v): v is FrameButtonElement => typeof v === "object" && v !== null ) .map((button, i): NonNullable[number] => { if (!("type" in button && "props" in button)) { @@ -221,7 +225,7 @@ export function renderResponse(): FramesMiddleware { }, }); } catch (e) { - let message: string = "Internal Server Error"; + let message = "Internal Server Error"; // do not expose any unrecognized errors in response, use console.error instead if ( @@ -232,6 +236,7 @@ export function renderResponse(): FramesMiddleware { ) { message = e.message; } else { + // eslint-disable-next-line no-console -- provide feedback to the user console.error(e); } diff --git a/packages/frames.js/src/middleware/stateMiddleware.test.ts b/packages/frames.js/src/middleware/stateMiddleware.test.ts index 111c1d529..f530b74b2 100644 --- a/packages/frames.js/src/middleware/stateMiddleware.test.ts +++ b/packages/frames.js/src/middleware/stateMiddleware.test.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console -- we expect the usage of console.log */ +import type { FramesContext } from "../core/types"; import { stateMiddleware } from "./stateMiddleware"; describe("stateMiddleware", () => { @@ -10,7 +12,7 @@ describe("stateMiddleware", () => { const mw = stateMiddleware(); const next = jest.fn(); - await mw(ctx as any, next); + await mw(ctx as unknown as FramesContext, next); expect(next).toHaveBeenCalledWith({ state }); }); @@ -27,7 +29,7 @@ describe("stateMiddleware", () => { expect(console.warn).not.toHaveBeenCalled(); - await mw(ctx as any, next); + await mw(ctx as unknown as FramesContext, next); expect(console.warn).toHaveBeenCalled(); @@ -41,7 +43,7 @@ describe("stateMiddleware", () => { const mw = stateMiddleware(); const next = jest.fn(); - await mw(ctx as any, next); + await mw(ctx as unknown as FramesContext, next); expect(next).toHaveBeenCalledWith({ state: { initial: true } }); }); diff --git a/packages/frames.js/src/next/fetchMetadata.test.ts b/packages/frames.js/src/next/fetchMetadata.test.ts index 38dc57233..5b5063b36 100644 --- a/packages/frames.js/src/next/fetchMetadata.test.ts +++ b/packages/frames.js/src/next/fetchMetadata.test.ts @@ -1,14 +1,15 @@ -import nock from "nock"; +// eslint-disable-next-line import/no-extraneous-dependencies -- dev dependency +import nock, { enableNetConnect, disableNetConnect } from "nock"; +import type { FrameFlattened } from ".."; import { fetchMetadata } from "./fetchMetadata"; -import type { FrameFlattened } from "frames.js"; describe("fetchMetadata", () => { beforeAll(() => { - nock.disableNetConnect(); + disableNetConnect(); }); afterAll(() => { - nock.enableNetConnect(); + enableNetConnect(); }); it("returns metadata", async () => { diff --git a/packages/frames.js/src/next/fetchMetadata.ts b/packages/frames.js/src/next/fetchMetadata.ts index e2a71ad7e..06cb8b4ff 100644 --- a/packages/frames.js/src/next/fetchMetadata.ts +++ b/packages/frames.js/src/next/fetchMetadata.ts @@ -42,7 +42,7 @@ export async function fetchMetadata( if (response.ok) { // process the JSON value to nextjs compatible format - const flattenedFrame: FrameFlattened = await response.json(); + const flattenedFrame = (await response.json()) as FrameFlattened; return flattenedFrame as NonNullable; } else if (response.status) { diff --git a/packages/frames.js/src/next/getCurrentUrl.ts b/packages/frames.js/src/next/getCurrentUrl.ts index fc8c819b8..76b750ab3 100644 --- a/packages/frames.js/src/next/getCurrentUrl.ts +++ b/packages/frames.js/src/next/getCurrentUrl.ts @@ -7,20 +7,31 @@ export function getCurrentUrl( ): URL | undefined { const scheme = process.env.NODE_ENV === "production" ? "https://" : "http://"; const appUrl = process.env.APP_URL || process.env.VERCEL_URL; - const host: string | undefined = (req.headers as any)?.host; - - const pathname = req.url?.startsWith("/") - ? req.url - : req.url - ? new URL(req.url).pathname + new URL(req.url).search - : ""; + const host: string | undefined = ( + req.headers as unknown as Record | undefined + )?.host; + + let pathname: string; + + if (req.url?.startsWith("/")) { + pathname = req.url; + } else if (req.url) { + const url = new URL(req.url); + pathname = url.pathname + url.search; + } else { + pathname = ""; + } // Construct a valid URL from the Vercel URL environment variable if it exists - const parsedAppUrl = appUrl - ? appUrl.startsWith("http://") || appUrl.startsWith("https://") - ? new URL(pathname, appUrl) - : new URL(pathname, scheme + appUrl) - : undefined; + let parsedAppUrl: URL | undefined; + + if (appUrl) { + if (appUrl.startsWith("http://") || appUrl.startsWith("https://")) { + parsedAppUrl = new URL(pathname, appUrl); + } else { + parsedAppUrl = new URL(pathname, scheme + appUrl); + } + } // App URL if (parsedAppUrl) { @@ -40,8 +51,9 @@ export function getCurrentUrl( return undefined; } -function isValidUrl(url: string) { +function isValidUrl(url: string): boolean { try { + // eslint-disable-next-line no-new -- constructor will validate the URL new URL(url); return true; } catch (err) { diff --git a/packages/frames.js/src/next/index.test.ts b/packages/frames.js/src/next/index.test.ts index f13c781d2..a83524b60 100644 --- a/packages/frames.js/src/next/index.test.ts +++ b/packages/frames.js/src/next/index.test.ts @@ -1,5 +1,5 @@ -import * as lib from "."; import { NextRequest } from "next/server"; +import * as lib from "."; describe("next adapter", () => { it.each(["Button", "createFrames", "fetchMetadata"])( @@ -19,7 +19,7 @@ describe("next adapter", () => { }, }); - const handleRequest = frames(async (ctx) => { + const handleRequest = frames((ctx) => { expect(ctx.state).toEqual({ test: false }); return { diff --git a/packages/frames.js/src/next/pages-router.tsx b/packages/frames.js/src/next/pages-router.tsx index 31d650ca5..e2987a842 100644 --- a/packages/frames.js/src/next/pages-router.tsx +++ b/packages/frames.js/src/next/pages-router.tsx @@ -1,20 +1,26 @@ import type { Metadata, NextApiRequest, NextApiResponse } from "next"; import React from "react"; +import type { types } from "../core"; import { createFrames as coreCreateFrames } from "../core"; import { createReadableStreamFromReadable, writeReadableStreamToWritable, } from "../lib/stream-pump"; + export { Button, type types } from "../core"; export { fetchMetadata } from "./fetchMetadata"; export const createFrames: typeof coreCreateFrames = - function createFramesForNextJSPagesRouter(options: any) { + function createFramesForNextJSPagesRouter(options: types.FramesOptions) { const frames = coreCreateFrames(options); - // @ts-expect-error -- this is correct but the function does not satisfy the type - return function createHandler(handler, handlerOptions) { + return function createHandler< + TPerRouteMiddleware extends types.FramesMiddleware[], + >( + handler: types.FrameHandlerFunction, + handlerOptions?: types.FramesRequestHandlerFunctionOptions + ) { const requestHandler = frames(handler, handlerOptions); return async function handleNextJSApiRequest( @@ -78,21 +84,21 @@ function createRequest(req: NextApiRequest, res: NextApiResponse): Request { const normalizedXForwardedHost = Array.isArray(xForwardedHost) ? xForwardedHost[0] : xForwardedHost; - let [, hostnamePort] = normalizedXForwardedHost?.split(":") ?? []; - let [, hostPort] = req.headers["host"]?.split(":") ?? []; - let port = hostnamePort || hostPort; + const [, hostnamePort] = normalizedXForwardedHost?.split(":") ?? []; + const [, hostPort] = req.headers.host?.split(":") ?? []; + const port = hostnamePort || hostPort; // Use req.hostname here as it respects the "trust proxy" setting - let resolvedHost = `${req.headers["host"]}${!hostPort ? `:${port}` : ""}`; + const resolvedHost = `${req.headers.host}${!hostPort ? `:${port}` : ""}`; // Use `req.url` so NextJS is aware of the full path - let url = new URL( + const url = new URL( `${"encrypted" in req.socket && req.socket.encrypted ? "https" : "http"}://${resolvedHost}${req.url}` ); // Abort action/loaders once we can no longer write a response - let controller = new AbortController(); - res.on("close", () => controller.abort()); + const controller = new AbortController(); + res.on("close", () => { controller.abort(); }); - let init: RequestInit = { + const init: RequestInit = { method: req.method, headers: createRequestHeaders(req.headers), signal: controller.signal, diff --git a/packages/frames.js/src/next/server.tsx b/packages/frames.js/src/next/server.tsx index 12c722129..ca2fea25c 100644 --- a/packages/frames.js/src/next/server.tsx +++ b/packages/frames.js/src/next/server.tsx @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse as NextResponseBase, } from "next/server"; -import React from "react"; +import React, {Children, isValidElement} from "react"; import type { FrameActionMessage } from "../farcaster"; import { type FrameMessageReturnType, @@ -95,7 +95,7 @@ export async function getFrameMessage( if (options?.hubHttpUrl) { if (!options.hubHttpUrl.startsWith("http")) { throw new Error( - `frames.js: Invalid Hub URL: ${options?.hubHttpUrl}, ensure you have included the protocol (e.g. https://)` + `frames.js: Invalid Hub URL: ${options.hubHttpUrl}, ensure you have included the protocol (e.g. https://)` ); } } @@ -111,10 +111,6 @@ export async function getFrameMessage( const result = await _getFrameMessage(frameActionPayload, options); - if (!result) { - throw new Error("frames.js: something went wrong getting frame message"); - } - return result; } /** @@ -150,11 +146,11 @@ export function createPreviousFrame( PreviousFrame, "postBody" | "prevState" | "pathname" | "prevRedirects" >, - headers: HeadersList + headersList: HeadersList ): PreviousFrame { return { ...previousFrameFromParams, - headers: headers, + headers: headersList, }; } @@ -170,30 +166,30 @@ export function parseFrameParams( "postBody" | "prevState" | "pathname" | "prevRedirects" > { const frameActionReceived = - searchParams?.postBody && typeof searchParams?.postBody === "string" - ? (JSON.parse(searchParams?.postBody) as FrameActionPayload) + searchParams?.postBody && typeof searchParams.postBody === "string" + ? (JSON.parse(searchParams.postBody) as FrameActionPayload) : null; const framePrevState = - searchParams?.prevState && typeof searchParams?.prevState === "string" - ? (JSON.parse(searchParams?.prevState) as T) + searchParams?.prevState && typeof searchParams.prevState === "string" + ? (JSON.parse(searchParams.prevState) as T) : null; const framePrevRedirects = searchParams?.prevRedirects && - typeof searchParams?.prevRedirects === "string" - ? (JSON.parse(searchParams?.prevRedirects) as RedirectMap) + typeof searchParams.prevRedirects === "string" + ? (JSON.parse(searchParams.prevRedirects) as RedirectMap) : null; const pathname = - searchParams?.pathname && typeof searchParams?.pathname === "string" - ? searchParams?.pathname + searchParams?.pathname && typeof searchParams.pathname === "string" + ? searchParams.pathname : undefined; return { postBody: frameActionReceived, prevState: framePrevState, - pathname: pathname, + pathname, prevRedirects: framePrevRedirects, }; } @@ -220,12 +216,14 @@ export function useFramesReducer( } // doesn't do anything right now, but exists to make Button onClicks feel more natural and not magic. - function dispatch(actionIndex: ActionIndex) {} + function dispatch(_actionIndex: ActionIndex): void { + return undefined; + } return [frameReducerInit(initializerArg), dispatch]; } -function toUrl(req: NextRequest) { +function toUrl(req: NextRequest): URL { const reqUrl = new URL(req.url); if (process.env.NEXT_PUBLIC_HOST) { @@ -271,15 +269,20 @@ export async function POST( /** unused, but will most frequently be passed a res: NextResponse object. Should stay in here for easy consumption compatible with next.js */ res: typeof NextResponse, redirectHandler?: RedirectHandler -) { - const body = await req.json(); +): Promise { + const body = await req.json() as FrameActionPayload; const url = new URL(req.url); let newUrl = toUrl(req); - const isFullUrl = - url.searchParams.get("p")?.startsWith("http://") || - url.searchParams.get("p")?.startsWith("https://"); - if (isFullUrl) newUrl = new URL(url.searchParams.get("p")!); + const pathFromRequest = url.searchParams.get("p"); + + const isFullUrl = typeof pathFromRequest === 'string' && ( + pathFromRequest.startsWith("http://") || + pathFromRequest.startsWith("https://")); + + if (isFullUrl) { + newUrl = new URL(pathFromRequest); + } else newUrl.pathname = url.searchParams.get("p") || ""; // decompress from 256 bytes limitation of post_url @@ -295,27 +298,20 @@ export async function POST( Object.fromEntries(url.searchParams.entries()) ); + const clickedButtonIndex = prevFrame.postBody?.untrustedData.buttonIndex; + const clickedButtonHref = clickedButtonIndex ? prevFrame.prevRedirects?.[clickedButtonIndex] : null; + // Handle 'post_redirect' buttons with href values - if ( - prevFrame.postBody?.untrustedData.buttonIndex && - prevFrame.prevRedirects?.hasOwnProperty( - prevFrame.postBody?.untrustedData.buttonIndex - ) && - prevFrame.prevRedirects[prevFrame.postBody?.untrustedData.buttonIndex] - ) { + if (clickedButtonHref) { return NextResponse.redirect( - prevFrame.prevRedirects[ - `${prevFrame.postBody?.untrustedData.buttonIndex}` - ]!, + clickedButtonHref, { status: 302 } ); } // Handle 'post_redirect' buttons without defined href values if ( prevFrame.postBody?.untrustedData.buttonIndex && - prevFrame.prevRedirects?.hasOwnProperty( - `_${prevFrame.postBody?.untrustedData.buttonIndex}` - ) + prevFrame.prevRedirects && `_${prevFrame.postBody.untrustedData.buttonIndex}` in prevFrame.prevRedirects ) { if (!redirectHandler) { // Error! @@ -422,7 +418,7 @@ export function FrameContainer({ pathname?: string; /** Client protocols to accept */ accepts?: ClientProtocolId[]; -}) { +}): React.JSX.Element { if (!pathname) { // eslint-disable-next-line no-console -- provide feedback to user on server console.warn( @@ -464,10 +460,10 @@ export function FrameContainer({ const newTree = ( <> - {React.Children.map(children, (child) => { + {Children.map(children, (child) => { if (child === null) return; - if (!React.isValidElement(child)) { + if (!isValidElement(child)) { return child; } @@ -530,7 +526,7 @@ export function FrameContainer({ const rewrittenProps: React.ComponentProps = { - ...(child.props as React.ComponentProps), + ...child.props, ...(target ? { target: target.toString() } : {}), actionIndex: nextIndexByComponentType.button++ as ActionIndex, }; @@ -603,17 +599,18 @@ export function FrameContainer({ * * @deprecated please upgrade to new API, see https://framesjs.org/reference/core/next */ -export const FrameButton: React.FunctionComponent = () => null; +export function FrameButton(_props: FrameButtonProvidedProps): React.JSX.Element | null { + return null; +} /** An internal component that handles FrameButtons */ function FFrameButtonShim({ actionIndex, target, action = "post", - // eslint-disable-next-line camelcase -- this is according to the spec - post_url, + post_url: postUrl, children, -}: FrameButtonProvidedProps & FrameButtonAutomatedProps) { +}: FrameButtonProvidedProps & FrameButtonAutomatedProps): React.JSX.Element { return ( <> ) : null} - {post_url ? ( + { + postUrl ? ( ) : null} @@ -712,7 +710,7 @@ export async function FrameImage( ), imageOptions ); - const imgBuffer = await imageResponse?.arrayBuffer(); + const imgBuffer = await imageResponse.arrayBuffer(); imgSrc = `data:image/png;base64,${Buffer.from(imgBuffer).toString("base64")}`; } else { imgSrc = props.src; diff --git a/packages/frames.js/src/next/test.types.tsx b/packages/frames.js/src/next/test.types.tsx index 2c16ad0e8..4ac29a520 100644 --- a/packages/frames.js/src/next/test.types.tsx +++ b/packages/frames.js/src/next/test.types.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/require-await -- we want to test that types are compatible with promises */ import type { NextRequest, NextResponse } from 'next/server'; import type { types } from '.'; import { createFrames } from '.'; @@ -46,6 +47,27 @@ framesWithExplicitState(async ctx => { request: Request; } + return { + image: 'http://test.png' + }; +}) satisfies Handler; + +const framesWithExplicitStateNoPromise = createFrames<{ + test: boolean; +}>({}); +framesWithExplicitStateNoPromise(ctx => { + ctx.state satisfies { + test: boolean; + }; + ctx.initialState satisfies { + test: boolean; + }; + ctx satisfies { + message?: unknown; + pressedButton?: unknown; + request: Request; + } + return { image: 'http://test.png' }; diff --git a/packages/frames.js/src/next/types.ts b/packages/frames.js/src/next/types.ts index d397e5b91..1e87a55f8 100644 --- a/packages/frames.js/src/next/types.ts +++ b/packages/frames.js/src/next/types.ts @@ -19,10 +19,11 @@ export type HeadersList = { /** A subset of JS objects that are serializable */ type AnyJson = boolean | number | string | null | JsonArray | JsonMap; +// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style -- this is necessary interface JsonMap { [key: string]: AnyJson; } -interface JsonArray extends Array {} +type JsonArray = AnyJson[]; /** * FrameState constraints @@ -78,7 +79,7 @@ export type FrameButtonAutomatedProps = { /** * Does nothing at the moment, but may be used in the future. It also makes the syntax more logical, as it seems like `Button` `onClick` actually dispatches a state transition, although in reality it works differently. */ -export type Dispatch = (actionIndex: ActionIndex) => any; +export type Dispatch = (actionIndex: ActionIndex) => unknown; export type FrameButtonProvidedProps = | FrameButtonPostRedirectProvidedProps @@ -135,6 +136,6 @@ export type FrameButtonMintProvidedProps = { /** See https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional */ export type NextServerPageProps = { - params: {}; - searchParams?: { [key: string]: string | string[] | undefined }; + params: Record; + searchParams?: Record; }; diff --git a/packages/frames.js/src/remix/fetchMetadata.ts b/packages/frames.js/src/remix/fetchMetadata.ts index 9d6c7760f..751f88048 100644 --- a/packages/frames.js/src/remix/fetchMetadata.ts +++ b/packages/frames.js/src/remix/fetchMetadata.ts @@ -38,7 +38,8 @@ export async function fetchMetadata(url: URL | string): Promise { if (response.ok) { // process the JSON value to nextjs compatible format - const flattenedFrame: FrameFlattened = await response.json(); + // @todo we should validate the response shape + const flattenedFrame = (await response.json()) as FrameFlattened; // convert to remix compatible shape return Object.entries(flattenedFrame).map(([key, value]) => { diff --git a/packages/frames.js/src/remix/index.test.ts b/packages/frames.js/src/remix/index.test.ts index 582611120..6a997e7d8 100644 --- a/packages/frames.js/src/remix/index.test.ts +++ b/packages/frames.js/src/remix/index.test.ts @@ -18,7 +18,7 @@ describe("remix adapter", () => { }, }); - const handleRequest = frames(async (ctx) => { + const handleRequest = frames((ctx) => { expect(ctx.state).toEqual({ test: false }); return { diff --git a/packages/frames.js/src/remix/test.types.tsx b/packages/frames.js/src/remix/test.types.tsx index 1e36e389b..106b34725 100644 --- a/packages/frames.js/src/remix/test.types.tsx +++ b/packages/frames.js/src/remix/test.types.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/require-await -- we are checking compatibility with promises */ import type { ActionFunction, LoaderFunction } from '@remix-run/node'; import { createFrames, type types } from '.'; @@ -45,6 +46,27 @@ framesWithExplicitState(async ctx => { request: Request; } + return { + image: 'http://test.png' + }; +}) satisfies Handler; + +const framesWithExplicitStateNoPromises = createFrames<{ + test: boolean; +}>({}); +framesWithExplicitStateNoPromises(ctx => { + ctx.state satisfies { + test: boolean; + }; + ctx.initialState satisfies { + test: boolean; + }; + ctx satisfies { + message?: unknown; + pressedButton?: unknown; + request: Request; + } + return { image: 'http://test.png' }; diff --git a/packages/frames.js/src/validateFrameMessage.ts b/packages/frames.js/src/validateFrameMessage.ts index d10ea0262..6e4d9a035 100644 --- a/packages/frames.js/src/validateFrameMessage.ts +++ b/packages/frames.js/src/validateFrameMessage.ts @@ -40,6 +40,7 @@ export async function validateFrameMessage( isValid: boolean; message: FrameActionMessage | undefined; }> { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- just in case if (!body) { throw new Error( "Tried to call validateFrameMessage with no frame action payload. You may be calling it incorrectly on the homeframe" From 9b7ca5c927ae51356fb1f4bc63ce706c6c99d5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 09:50:00 +0200 Subject: [PATCH 05/14] build: disable implicing coercion warnings --- packages/eslint-config/library.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index 28223ab3f..273db5ba6 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -39,5 +39,6 @@ module.exports = { "unicorn/filename-case": "off", eqeqeq: "off", "no-await-in-loop": "off", + "no-implicit-coercion": "off", }, }; From 63b9a7170355754621c726c8d6a2c86fbd0d6894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 09:50:30 +0200 Subject: [PATCH 06/14] fix: correctly type Frame and FrameButton --- packages/frames.js/src/getFrame.ts | 2 -- packages/frames.js/src/getFrameFlattened.ts | 8 ++++++-- packages/frames.js/src/getFrameHtml.ts | 4 ++-- packages/frames.js/src/getUserDataForFid.ts | 3 ++- packages/frames.js/src/types.ts | 9 ++------- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/frames.js/src/getFrame.ts b/packages/frames.js/src/getFrame.ts index ffb08c11f..129ea6088 100644 --- a/packages/frames.js/src/getFrame.ts +++ b/packages/frames.js/src/getFrame.ts @@ -246,7 +246,6 @@ export function getFrame({ return { label: button.label, action: button.action, - post_url: button.post_url, target: button.target, }; @@ -261,7 +260,6 @@ export function getFrame({ return { label: button.label, action: button.action, - post_url: button.post_url, target: button.target, }; }); diff --git a/packages/frames.js/src/getFrameFlattened.ts b/packages/frames.js/src/getFrameFlattened.ts index 031fdeec2..db5f3f269 100644 --- a/packages/frames.js/src/getFrameFlattened.ts +++ b/packages/frames.js/src/getFrameFlattened.ts @@ -32,7 +32,9 @@ export function getFrameFlattened(frame: Frame): FrameFlattened { [`of:button:${index + 1}`]: button.label, [`of:button:${index + 1}:action`]: button.action, [`of:button:${index + 1}:target`]: button.target, - [`of:button:${index + 1}:post_url`]: button.post_url, + ...(button.action === "tx" + ? { [`of:button:${index + 1}:post_url`]: button.post_url } + : {}), }), {} ), @@ -54,7 +56,9 @@ export function getFrameFlattened(frame: Frame): FrameFlattened { [`fc:frame:button:${index + 1}`]: button.label, [`fc:frame:button:${index + 1}:action`]: button.action, [`fc:frame:button:${index + 1}:target`]: button.target, - [`fc:frame:button:${index + 1}:post_url`]: button.post_url, + ...(button.action === "tx" + ? { [`fc:frame:button:${index + 1}:post_url`]: button.post_url } + : {}), }), {} ), diff --git a/packages/frames.js/src/getFrameHtml.ts b/packages/frames.js/src/getFrameHtml.ts index 639afb8df..62800b521 100644 --- a/packages/frames.js/src/getFrameHtml.ts +++ b/packages/frames.js/src/getFrameHtml.ts @@ -58,7 +58,7 @@ export function getFrameHtmlHead(frame: Frame): string { button.target ? `` : "", - button.post_url + button.action === "tx" && button.post_url ? `` : "", ]) ?? []), @@ -83,7 +83,7 @@ export function getFrameHtmlHead(frame: Frame): string { button.target ? `` : "", - button.post_url + button.action === "tx" && button.post_url ? `` : "", ]) ?? []), diff --git a/packages/frames.js/src/getUserDataForFid.ts b/packages/frames.js/src/getUserDataForFid.ts index b3b1bd105..ba81b58f2 100644 --- a/packages/frames.js/src/getUserDataForFid.ts +++ b/packages/frames.js/src/getUserDataForFid.ts @@ -54,8 +54,9 @@ export async function getUserDataForFid< const timestamp = message.data.timestamp; const { type, value } = message.data.userDataBody; + const foundValue = acc[type]; - if (acc[type] && acc[type].timestamp < timestamp) { + if (foundValue && foundValue.timestamp < timestamp) { acc[type] = { value, timestamp }; } else { acc[type] = { value, timestamp }; diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index 0dfe39139..ca9c4da3d 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -11,7 +11,7 @@ export type Frame = { /** A valid frame version string. The string must be a release date (e.g. 2020-01-01 ) or vNext. Apps must ignore versions they do not understand. Currently, the only valid version is vNext. */ version: FrameVersion; /** A 256-byte string which contains a valid URL to send the Signature Packet to. If this prop is not present, apps must POST to the frame URL. */ - postUrl: string; + postUrl?: string; /** A page may contain 0 to 4 buttons. If more than 1 button is present, the idx values must be in sequence starting from 1 (e.g. 1, 2 3). If a broken sequence is present (e.g 1, 2, 4), apps must not render the frame and instead render an OG embed. */ buttons?: FrameButtonsType; /** An image which should have an aspect ratio of 1.91:1 or 1:1 */ @@ -34,6 +34,7 @@ type FrameOptionalStringKeys = | "fc:frame:image:aspect_ratio" | "fc:frame:input:text" | "fc:frame:state" + | "fc:frame:post_url" | keyof OpenFramesProperties; type FrameOptionalActionButtonTypeKeys = `fc:frame:button:${1 | 2 | 3 | 4}:action`; @@ -56,7 +57,6 @@ type MapFrameOptionalKeyToValueType = type FrameRequiredProperties = { "fc:frame": FrameVersion; "fc:frame:image": string; - "fc:frame:post_url": string; }; export type OpenFramesProperties = { @@ -90,8 +90,6 @@ export interface FrameButtonLink { action: "link"; /** required for action type 'link' */ target: string; - post_url?: undefined; - /** A 256-byte string which is label of the button */ label: string; } @@ -108,8 +106,6 @@ export interface FrameButtonMint { action: "mint"; /** The target property MUST be a valid [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md) address, plus an optional token_id . */ target: string; - post_url?: undefined; - /** A 256-byte string which is label of the button */ label: string; } @@ -124,7 +120,6 @@ export interface FrameButtonPost { * POST the packet to fc:frame:post_url if target was not present. */ target?: string; - post_url?: undefined; /** A 256-byte string which is label of the button */ label: string; } From 7d1cafe3961d0d15b95ac0aeff5de5deed469ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 10:20:29 +0200 Subject: [PATCH 07/14] fix: render package errors --- packages/render/package.json | 3 +- packages/render/src/farcaster/frames.tsx | 24 +- packages/render/src/farcaster/index.ts | 4 +- packages/render/src/farcaster/signers.tsx | 11 +- packages/render/src/frame-ui.tsx | 41 ++- packages/render/src/index.tsx | 4 +- packages/render/src/next/FrameImage.tsx | 5 +- packages/render/src/next/GET.tsx | 4 +- packages/render/src/next/POST.tsx | 16 +- packages/render/src/next/index.tsx | 6 +- packages/render/src/types.ts | 24 +- packages/render/src/use-frame.tsx | 321 +++++++++++++--------- 12 files changed, 272 insertions(+), 191 deletions(-) diff --git a/packages/render/package.json b/packages/render/package.json index 37ec95422..20fcad06a 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -79,7 +79,8 @@ "peerDependencies": { "next": "^14.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "viem": "^2.7.8" }, "dependencies": { "@farcaster/core": "^0.14.7", diff --git a/packages/render/src/farcaster/frames.tsx b/packages/render/src/farcaster/frames.tsx index 579fe03a3..63b3e64bd 100644 --- a/packages/render/src/farcaster/frames.tsx +++ b/packages/render/src/farcaster/frames.tsx @@ -1,19 +1,19 @@ -import { +import type { CastId, + FrameActionMessage +} from "@farcaster/core"; +import { NobleEd25519Signer, makeFrameAction, FarcasterNetwork, Message, - FrameActionBody, - FrameActionMessage, + FrameActionBody } from "@farcaster/core"; -import { FrameButton } from "frames.js"; -import { FrameActionBodyPayload, FrameContext } from "../types.js"; -import { FarcasterSignerState } from "./signers.js"; import { hexToBytes } from "viem"; +import type { FrameButton } from "frames.js"; +import type { FrameContext } from "../types"; +import type { FarcasterSignerState } from "./signers"; -export interface FarcasterFrameActionBodyPayload - extends FrameActionBodyPayload {} export type FarcasterFrameContext = { /** Connected address of user, only sent with transaction data request */ @@ -80,16 +80,16 @@ export const signFrameAction = async ({ } const searchParams = new URLSearchParams({ - postType: transactionId ? "post" : frameButton?.action || "post", + postType: transactionId ? "post" : frameButton.action, postUrl: target ?? "", }); return { - searchParams: searchParams, + searchParams, body: { untrustedData: { fid: signer.fid, - url: url, + url, messageHash: `0x${Buffer.from(message.hash).toString("hex")}`, timestamp: message.data.timestamp, network: 1, @@ -169,5 +169,5 @@ export async function createFrameActionMessageWithSignerKey( Message.encode(message._unsafeUnwrap()).finish() ).toString("hex"); - return { message: message.unwrapOr(null), trustedBytes: trustedBytes }; + return { message: message.unwrapOr(null), trustedBytes }; } diff --git a/packages/render/src/farcaster/index.ts b/packages/render/src/farcaster/index.ts index 69a6804dd..a20359e0f 100644 --- a/packages/render/src/farcaster/index.ts +++ b/packages/render/src/farcaster/index.ts @@ -1,2 +1,2 @@ -export * from "./frames.js"; -export * from "./signers.js"; +export * from "./frames"; +export * from "./signers"; diff --git a/packages/render/src/farcaster/signers.tsx b/packages/render/src/farcaster/signers.tsx index c00df6c14..8d76727c8 100644 --- a/packages/render/src/farcaster/signers.tsx +++ b/packages/render/src/farcaster/signers.tsx @@ -1,11 +1,8 @@ -import { SignerStateInstance } from "../types.js"; -import { FarcasterFrameActionBodyPayload } from "./frames.js"; +import type { SignerStateInstance } from ".."; -export interface FarcasterSignerState - extends SignerStateInstance< - FarcasterSigner | null, - FarcasterFrameActionBodyPayload - > {} +export type FarcasterSignerState = SignerStateInstance< + FarcasterSigner | null + > export interface FarcasterSigner { /* the Farcaster signer private key */ diff --git a/packages/render/src/frame-ui.tsx b/packages/render/src/frame-ui.tsx index 181270899..90c5b72eb 100644 --- a/packages/render/src/frame-ui.tsx +++ b/packages/render/src/frame-ui.tsx @@ -1,6 +1,7 @@ -import { FrameTheme, FrameState } from "./types.js"; -import React, { ImgHTMLAttributes, useEffect } from "react"; -import { FrameButton } from "frames.js"; +import type { ImgHTMLAttributes } from "react"; +import React, { useEffect, useState } from "react"; +import type { FrameButton } from "frames.js"; +import type { FrameTheme, FrameState } from "./types"; export const defaultTheme: Required = { buttonBg: "#fff", @@ -11,7 +12,7 @@ export const defaultTheme: Required = { bg: "#efefef", }; -const getThemeWithDefaults = (theme: FrameTheme) => { +const getThemeWithDefaults = (theme: FrameTheme): FrameTheme => { return { ...defaultTheme, ...theme, @@ -25,8 +26,12 @@ export type FrameUIProps = { }; /** A UI component only, that should be easy for any app to integrate */ -export function FrameUI({ frameState, theme, FrameImage }: FrameUIProps) { - const [isImageLoading, setIsImageLoading] = React.useState(true); +export function FrameUI({ + frameState, + theme, + FrameImage, +}: FrameUIProps): React.JSX.Element | null { + const [isImageLoading, setIsImageLoading] = useState(true); const isLoading = !!frameState.isLoading || isImageLoading; @@ -53,7 +58,7 @@ export function FrameUI({ frameState, theme, FrameImage }: FrameUIProps) { { setIsImageLoading(false); }} - onError={() => setIsImageLoading(false)} + onError={() => { + setIsImageLoading(false); + }} /> - {frameState.frame.inputText && ( + {frameState.frame.inputText ? ( frameState.setInputText(e.target.value)} + onChange={(e) => { + frameState.setInputText(e.target.value); + }} /> - )} + ) : null}
frameState.onButtonPress(frameButton, index)} + onClick={() => { + Promise.resolve(frameState.onButtonPress(frameButton, index)).catch((e: unknown) => { + // eslint-disable-next-line no-console -- provide feedback to the user + console.error(e); + }); + }} + // eslint-disable-next-line react/no-array-index-key -- this is fine key={index} > {frameButton.action === "mint" ? `⬗ ` : ""} @@ -124,7 +139,7 @@ export function FrameUI({ frameState, theme, FrameImage }: FrameUIProps) { height="12" fill="currentColor" > - + ) : ( "" diff --git a/packages/render/src/index.tsx b/packages/render/src/index.tsx index 336dd1abe..2b8da4fe0 100644 --- a/packages/render/src/index.tsx +++ b/packages/render/src/index.tsx @@ -1,4 +1,4 @@ -import { FrameContext } from "./types.js"; +import type { FrameContext } from "./types"; export const fallbackFrameContext: FrameContext = { castId: { @@ -10,6 +10,6 @@ export const fallbackFrameContext: FrameContext = { export * from "./frame-ui.js"; export * from "./types.js"; -export * from "./farcaster/index.js"; +export * from "./farcaster"; /** don't export use-frame from here, as it's a client component */ diff --git a/packages/render/src/next/FrameImage.tsx b/packages/render/src/next/FrameImage.tsx index c31c3a364..47d46a25a 100644 --- a/packages/render/src/next/FrameImage.tsx +++ b/packages/render/src/next/FrameImage.tsx @@ -1,9 +1,10 @@ import Image from "next/image"; -import React, { ImgHTMLAttributes } from "react"; +import type { ImgHTMLAttributes } from "react"; +import React from "react"; export function FrameImageNext( props: ImgHTMLAttributes & { src: string } -) { +): React.JSX.Element { return ( { return NextResponse.json({ frame, errors }); } catch (err) { + // eslint-disable-next-line no-console -- provide feedback to the developer console.error(err); return NextResponse.json({ message: err }, { status: 500 }); } diff --git a/packages/render/src/next/POST.tsx b/packages/render/src/next/POST.tsx index d9d3f034d..6f2f61fdb 100644 --- a/packages/render/src/next/POST.tsx +++ b/packages/render/src/next/POST.tsx @@ -1,14 +1,19 @@ +import type { FrameActionPayload} from "frames.js"; import { getFrame } from "frames.js"; -import { NextRequest } from "next/server"; +import type { NextRequest } from "next/server"; /** Proxies frame actions to avoid CORS issues and preserve user IP privacy */ -export async function POST(req: NextRequest) { - const body = await req.json(); +export async function POST(req: NextRequest): Promise { + const body = await req.json() as FrameActionPayload; const isPostRedirect = req.nextUrl.searchParams.get("postType") === "post_redirect"; const isTransactionRequest = req.nextUrl.searchParams.get("postType") === "tx"; - const postUrl = req.nextUrl.searchParams.get("postUrl")!; + const postUrl = req.nextUrl.searchParams.get("postUrl"); + + if (!postUrl) { + return Response.error(); + } try { const r = await fetch(postUrl, { @@ -31,7 +36,7 @@ export async function POST(req: NextRequest) { } if (isTransactionRequest) { - const transaction = await r.json(); + const transaction = await r.json() as JSON; return Response.json(transaction); } @@ -44,6 +49,7 @@ export async function POST(req: NextRequest) { return Response.json({ frame, errors }); } catch (err) { + // eslint-disable-next-line no-console -- provide feedback to the user console.error(err); return Response.error(); } diff --git a/packages/render/src/next/index.tsx b/packages/render/src/next/index.tsx index 6152bc031..373679f71 100644 --- a/packages/render/src/next/index.tsx +++ b/packages/render/src/next/index.tsx @@ -1,3 +1,3 @@ -export * from "./GET.js"; -export * from "./POST.js"; -export * from "./FrameImage.js"; +export * from "./GET"; +export * from "./POST"; +export * from "./FrameImage"; diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index ca68e060e..8992e6430 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -1,8 +1,8 @@ import type { Frame, FrameButton, TransactionTargetResponse } from "frames.js"; -import { FarcasterFrameContext } from "./farcaster/index.js"; +import type { FarcasterFrameContext } from "./farcaster/frames"; export type OnTransactionFunc = ( - t: onTransactionArgs + t: OnTransactionArgs ) => Promise<`0x${string}` | null>; export type UseFrameReturn< @@ -22,7 +22,7 @@ export type UseFrameReturn< /** the initial frame. if not specified will fetch it from the url prop */ frame?: Frame; /** a function to handle mint buttons */ - onMint?: (t: onMintArgs) => void; + onMint?: (t: OnMintArgs) => void; /** a function to handle transaction buttons, returns the transaction hash or null */ onTransaction?: OnTransactionFunc; /** the context of this frame, used for generating Frame Action payloads */ @@ -65,7 +65,6 @@ export interface SignerStateInstance< export type FrameRequest = | { method: "GET"; - request: {}; url: string; } | { @@ -97,10 +96,10 @@ export type FrameStackError = FrameStackBase & { requestError: unknown; }; -export type FramesStack = Array; +export type FramesStack = (FrameStackSuccess | FrameStackError)[]; export type FrameState = { - fetchFrame: (request: FrameRequest) => void; + fetchFrame: (request: FrameRequest) => void | Promise; clearFrameStack: () => void; /** The frame at the top of the stack (at index 0) */ frame: Frame | null; @@ -110,21 +109,24 @@ export type FrameState = { isLoading?: null | FrameStackPending; inputText: string; setInputText: (s: string) => void; - onButtonPress: (frameButton: FrameButton, index: number) => void; + onButtonPress: ( + frameButton: FrameButton, + index: number + ) => void | Promise; /** Whether the frame at the top of the stack has any frame validation errors. Undefined when the frame is not loaded or set */ isFrameValid: boolean | undefined; frameValidationErrors: Record | undefined | null; - error: null | unknown; + error: unknown; homeframeUrl: string | null; }; -export type onMintArgs = { +export type OnMintArgs = { target: string; frameButton: FrameButton; frame: Frame; }; -export type onTransactionArgs = { +export type OnTransactionArgs = { transactionData: TransactionTargetResponse; frameButton: FrameButton; frame: Frame; @@ -141,6 +143,6 @@ export const themeParams = [ export type FrameTheme = Partial>; -export interface FrameActionBodyPayload {} +export type FrameActionBodyPayload = Record; export type FrameContext = FarcasterFrameContext; diff --git a/packages/render/src/use-frame.tsx b/packages/render/src/use-frame.tsx index 1606e69e4..a5bfb0d1a 100644 --- a/packages/render/src/use-frame.tsx +++ b/packages/render/src/use-frame.tsx @@ -1,9 +1,19 @@ +/* eslint-disable @typescript-eslint/require-await -- we expect async functions */ +/* eslint-disable no-console -- provide feedback */ +/* eslint-disable no-alert -- provide feedback */ "use client"; -import { useEffect, useState } from "react"; -import { +import { useEffect, useMemo, useRef, useState } from "react"; +import type { + FrameActionPayload, + FrameButton, + TransactionTargetResponse, + getFrame, +} from "frames.js"; +import { getFarcasterTime } from "@farcaster/core"; +import type { FrameState, - onMintArgs, + OnMintArgs, FrameContext, SignerStateInstance, FrameActionBodyPayload, @@ -11,14 +21,11 @@ import { FrameStackPending, FrameRequest, UseFrameReturn, - onTransactionArgs, -} from "./types.js"; -import type { FrameButton, TransactionTargetResponse } from "frames.js"; -import { getFrame } from "frames.js"; -import { getFarcasterTime } from "@farcaster/core"; + OnTransactionArgs, +} from "./types"; -function onMintFallback({ target }: onMintArgs) { - window.alert("Mint requested: " + target!); +function onMintFallback({ target }: OnMintArgs): void { + window.alert(`Mint requested: ${target}`); } export const unsignedFrameAction: SignerStateInstance["signFrameAction"] = @@ -32,18 +39,18 @@ export const unsignedFrameAction: SignerStateInstance["signFrameAction"] = url, }) => { const searchParams = new URLSearchParams({ - postType: frameButton?.action || "post", + postType: frameButton.action, postUrl: target ?? "", }); return { - searchParams: searchParams, + searchParams, body: { untrustedData: { - url: url, + url, timestamp: getFarcasterTime()._unsafeUnwrap(), network: 1, - buttonIndex: buttonIndex, + buttonIndex, castId: { fid: frameContext.castId.fid, hash: frameContext.castId.hash, @@ -58,7 +65,10 @@ export const unsignedFrameAction: SignerStateInstance["signFrameAction"] = }, }; }; -async function onTransactionFallback({ transactionData }: onTransactionArgs) { + +async function onTransactionFallback({ + transactionData, +}: OnTransactionArgs): Promise { window.alert( `Requesting a transaction on chain with ID ${transactionData.chainId} with the following params: ${JSON.stringify(transactionData.params, null, 2)}` ); @@ -75,7 +85,7 @@ export const fallbackFrameContext: FrameContext = { export function useFrame< T = object, - B extends FrameActionBodyPayload = FrameActionBodyPayload, + B extends FrameActionBodyPayload = FrameActionPayload, >({ homeframeUrl, frameContext, @@ -91,14 +101,18 @@ export function useFrame< extraButtonRequestPayload, }: UseFrameReturn): FrameState { const [inputText, setInputText] = useState(""); - const initialFrame = frame ? { frame: frame, errors: null } : undefined; + const initialFrame = useMemo(() => { + if (frame) { + return { frame, errors: null }; + } + return undefined; + }, [frame]); const [framesStack, setFramesStack] = useState( initialFrame ? [ { method: "GET", - request: {}, responseStatus: 200, timestamp: new Date(), url: homeframeUrl ?? "", @@ -115,29 +129,28 @@ export function useFrame< // prevent flash of empty if will shortly set this in first rerender homeframeUrl && !initialFrame ? { - request: {}, method: "GET" as const, timestamp: new Date(), - url: homeframeUrl ?? "", + url: homeframeUrl, } : null ); - async function fetchFrame({ method, url, request }: FrameRequest) { - if (method === "GET") { + async function fetchFrame(frameRequest: FrameRequest): Promise { + if (frameRequest.method === "GET") { const tstart = new Date(); const frameStackBase = { request: {}, method: "GET" as const, timestamp: tstart, - url: url ?? "", + url: frameRequest.url, }; setIsLoading(frameStackBase); - let requestError: unknown | null = null; + let requestError: unknown = null; let newFrame: ReturnType | null = null; - const searchParams = new URLSearchParams({ url }); + const searchParams = new URLSearchParams({ url: frameRequest.url }); const proxiedUrl = `${frameGetProxy}?${searchParams.toString()}`; let stackItem: FramesStack[number]; @@ -151,7 +164,9 @@ export function useFrame< ...frameStackBase, frame: newFrame.frame, frameValidationErrors: newFrame.errors, - speed: +((tend.getTime() - tstart.getTime()) / 1000).toFixed(2), + speed: Number( + ((tend.getTime() - tstart.getTime()) / 1000).toFixed(2) + ), responseStatus: response.status, isValid: Object.keys(newFrame.errors ?? {}).length === 0, }; @@ -160,10 +175,12 @@ export function useFrame< stackItem = { ...frameStackBase, - url: url ?? "", + url: frameRequest.url, responseStatus: response?.status ?? 500, requestError, - speed: +((tend.getTime() - tstart.getTime()) / 1000).toFixed(2), + speed: Number( + ((tend.getTime() - tstart.getTime()) / 1000).toFixed(2) + ), }; requestError = err; @@ -174,15 +191,15 @@ export function useFrame< const frameStackBase = { method: "POST" as const, request: { - searchParams: request.searchParams, - body: request.body, + searchParams: frameRequest.request.searchParams, + body: frameRequest.request.body, }, timestamp: tstart, - url: url, + url: frameRequest.url, }; setIsLoading(frameStackBase); let stackItem: FramesStack[number] | undefined; - const proxiedUrl = `${frameActionProxy}?${request.searchParams.toString()}`; + const proxiedUrl = `${frameActionProxy}?${frameRequest.request.searchParams.toString()}`; let response; try { @@ -193,7 +210,7 @@ export function useFrame< }, body: JSON.stringify({ ...extraButtonRequestPayload, - ...request.body, + ...frameRequest.request.body, }), }); const dataRes = (await response.json()) as @@ -204,16 +221,16 @@ export function useFrame< if ("location" in dataRes) { const location = dataRes.location; - if ( - window.confirm("You are about to be redirected to " + location!) - ) { - window.open(location!, "_blank")?.focus(); + if (window.confirm(`You are about to be redirected to ${location}`)) { + window.open(location, "_blank")?.focus(); } } else { stackItem = { ...frameStackBase, responseStatus: response.status, - speed: +((tend.getTime() - tstart.getTime()) / 1000).toFixed(2), + speed: Number( + ((tend.getTime() - tstart.getTime()) / 1000).toFixed(2) + ), frame: dataRes.frame, frameValidationErrors: dataRes.errors, isValid: Object.keys(dataRes.errors ?? {}).length === 0, @@ -225,7 +242,9 @@ export function useFrame< ...frameStackBase, responseStatus: response?.status ?? 500, requestError: err, - speed: +((tend.getTime() - tstart.getTime()) / 1000).toFixed(2), + speed: Number( + ((tend.getTime() - tstart.getTime()) / 1000).toFixed(2) + ), }; console.error(err); @@ -236,46 +255,60 @@ export function useFrame< setIsLoading(null); } + const fetchFrameRef = useRef(fetchFrame); + fetchFrameRef.current = fetchFrame; + // Load initial frame if not defined useEffect(() => { if (!initialFrame && homeframeUrl) { - fetchFrame({ - url: homeframeUrl, - method: "GET", - request: {}, - }); + fetchFrameRef + .current({ + url: homeframeUrl, + method: "GET", + }) + .catch((e) => { + console.error(e); + }); } }, [initialFrame, homeframeUrl]); - function getCurrentFrame() { - const [frame] = framesStack; + function getCurrentFrame(): ReturnType["frame"] | null { + const [latestFrame] = framesStack; - return frame && "frame" in frame ? frame.frame : null; + return latestFrame && "frame" in latestFrame ? latestFrame.frame : null; } - function isCurrentFrameValid() { - const [frame] = framesStack; + function isCurrentFrameValid(): boolean | undefined { + const [latestFrame] = framesStack; - return frame && "frameValidationErrors" in frame - ? Object.keys(frame.frameValidationErrors ?? {}).length === 0 + return latestFrame && "frameValidationErrors" in latestFrame + ? Object.keys(latestFrame.frameValidationErrors ?? {}).length === 0 : undefined; } - function getCurrentFrameValidationErrors() { - const [frame] = framesStack; + function getCurrentFrameValidationErrors(): + | Record + | null + | undefined { + const [latestFrame] = framesStack; - return frame && "frameValidationErrors" in frame - ? frame.frameValidationErrors + return latestFrame && "frameValidationErrors" in latestFrame + ? latestFrame.frameValidationErrors : null; } - function getCurrentFrameRequestError() { - const [frame] = framesStack; + function getCurrentFrameRequestError(): unknown { + const [latestFrame] = framesStack; - return frame && "requestError" in frame ? frame.requestError : null; + return latestFrame && "requestError" in latestFrame + ? latestFrame.requestError + : null; } - const onButtonPress = async (frameButton: FrameButton, index: number) => { + async function onButtonPress( + frameButton: FrameButton, + index: number + ): Promise { const currentFrame = getCurrentFrame(); if (!currentFrame) { @@ -288,74 +321,101 @@ export function useFrame< // don't continue, let the app handle return; } - const target = frameButton.target ?? currentFrame.postUrl ?? homeframeUrl; - if (frameButton.action === "link") { - if (window.confirm("You are about to be redirected to " + target!)) { - parent.window.open(target!, "_blank"); + + switch (frameButton.action) { + case "link": { + if ( + window.confirm( + `You are about to be redirected to ${frameButton.target}` + ) + ) { + parent.window.open(frameButton.target, "_blank"); + } + break; } - } else if (frameButton.action === "mint") { - onMint({ frameButton, target, frame: currentFrame }); - } else if (frameButton.action === "tx") { - const transactionData = await onTransactionRequest({ - frameButton: frameButton, - target: target, - buttonIndex: index + 1, - postInputText: - currentFrame.inputText !== undefined ? inputText : undefined, - state: currentFrame.state, - }); - if (transactionData) { - const transactionId = await onTransaction({ + case "mint": { + onMint({ + frameButton, + target: frameButton.target, frame: currentFrame, - frameButton: frameButton, - transactionData, }); - if (transactionId) { + break; + } + case "tx": { + const transactionData = await onTransactionRequest({ + frameButton, + target: frameButton.target, + buttonIndex: index + 1, + postInputText: + currentFrame.inputText !== undefined ? inputText : undefined, + state: currentFrame.state, + }); + if (transactionData) { + const transactionId = await onTransaction({ + frame: currentFrame, + frameButton, + transactionData, + }); + + if (transactionId) { + await onPostButton({ + frameButton, + target: + frameButton.post_url || + currentFrame.postUrl || + frameButton.target, // transaction_ids must be posted to post_url or button post_url + buttonIndex: index + 1, + postInputText: + currentFrame.inputText !== undefined ? inputText : undefined, + state: currentFrame.state, + transactionId, + }); + } + } + break; + } + case "post": + case "post_redirect": { + try { + const target = + frameButton.target || currentFrame.postUrl || homeframeUrl; + + if (!target) { + throw new Error("missing target"); + } + await onPostButton({ - frameButton: frameButton, - target: frameButton.post_url || currentFrame.postUrl, // transaction_ids must be posted to post_url or button post_url + frameButton, + /** https://docs.farcaster.xyz/reference/frames/spec#handling-clicks + + POST the packet to fc:frame:button:$idx:action:target if present + POST the packet to fc:frame:post_url if target was not present. + POST the packet to or the frame's embed URL if neither target nor action were present. + */ + target, + dangerousSkipSigning, buttonIndex: index + 1, postInputText: currentFrame.inputText !== undefined ? inputText : undefined, state: currentFrame.state, - transactionId, }); + setInputText(""); + } catch (err) { + alert("error: check the console"); + console.error(err); } + break; } - } else if ( - frameButton.action === "post" || - frameButton.action === "post_redirect" - ) { - try { - await onPostButton({ - frameButton: frameButton, - target: target, - dangerousSkipSigning: dangerousSkipSigning, - - buttonIndex: index + 1, - /** https://docs.farcaster.xyz/reference/frames/spec#handling-clicks - - POST the packet to fc:frame:button:$idx:action:target if present - POST the packet to fc:frame:post_url if target was not present. - POST the packet to or the frame's embed URL if neither target nor action were present. - */ - postInputText: - currentFrame.inputText !== undefined ? inputText : undefined, - state: currentFrame.state, - }); - setInputText(""); - } catch (err) { - alert("error: check the console"); - console.error(err); - } + default: + throw new Error("Unrecognized frame button action"); } - }; + } - const onPostButton = async ({ + async function onPostButton({ buttonIndex, postInputText, frameButton, - dangerousSkipSigning, + dangerousSkipSigning: isDangerousSkipSigning, target, state, transactionId, @@ -368,14 +428,14 @@ export function useFrame< transactionId?: `0x${string}`; target: string; - }) => { + }): Promise { const currentFrame = getCurrentFrame(); - if (!dangerousSkipSigning && !signerState.hasSigner) { + if (!isDangerousSkipSigning && !signerState.hasSigner) { console.error("frames.js: missing required auth state"); return; } - if (!currentFrame || !currentFrame || !homeframeUrl || !frameButton) { + if (!currentFrame || !homeframeUrl) { console.error("frames.js: missing required value for post"); return; } @@ -393,7 +453,7 @@ export function useFrame< state, transactionId, }; - const { searchParams, body } = dangerousSkipSigning + const { searchParams, body } = isDangerousSkipSigning ? await unsignedFrameAction(frameSignatureContext) : await signerState.signFrameAction(frameSignatureContext); @@ -406,21 +466,17 @@ export function useFrame< body, }, }); - }; + } - const onTransactionRequest = async ({ - buttonIndex, - postInputText, - frameButton, - target, - state, + async function onTransactionRequest({ + buttonIndex, postInputText, frameButton, target, state, }: { frameButton: FrameButton; buttonIndex: number; postInputText: string | undefined; state?: string; target: string; - }) => { + }): Promise { // Send post request to get calldata const currentFrame = getCurrentFrame(); @@ -428,7 +484,7 @@ export function useFrame< console.error("frames.js: missing required auth state"); return; } - if (!currentFrame || !currentFrame || !homeframeUrl || !frameButton) { + if (!currentFrame || !homeframeUrl) { console.error("frames.js: missing required value for post"); return; } @@ -442,8 +498,8 @@ export function useFrame< }, url: homeframeUrl, target, - frameButton: frameButton, - buttonIndex: buttonIndex, + frameButton, + buttonIndex, state, }); searchParams.append("postType", "tx"); @@ -461,25 +517,26 @@ export function useFrame< ...body, }), }); - const transactionResponse = - (await response.json()) as TransactionTargetResponse; + const transactionResponse = (await response.json()) as TransactionTargetResponse; return transactionResponse; } catch { throw new Error( `frames.js: Could not fetch transaction data from "${searchParams.get("postUrl")}"` ); } - }; + } return { - isLoading: isLoading, + isLoading, inputText, setInputText, - clearFrameStack: () => setFramesStack([]), + clearFrameStack: () => { + setFramesStack([]); + }, onButtonPress, fetchFrame, homeframeUrl, - framesStack: framesStack, + framesStack, frame: getCurrentFrame() ?? null, isFrameValid: isCurrentFrameValid(), frameValidationErrors: getCurrentFrameValidationErrors(), From 5144c302f82ae30caee38f5465379c861da893fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 10:30:34 +0200 Subject: [PATCH 08/14] fix: debugger issues --- .../app/components/frame-debugger.tsx | 41 ++++++++++++------- packages/debugger/app/page.tsx | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index 0027899fd..2cfb9cb73 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -1,7 +1,7 @@ import { getFrameHtmlHead, getFrameFlattened } from "frames.js"; import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; import React from "react"; -import { FrameState, FrameRequest, FrameStackSuccess } from "frames.js/render"; +import { FrameState, FrameStackSuccess } from "@frames.js/render"; import { Table, TableBody, TableCell, TableRow } from "@/components/table"; import { AlertTriangle, @@ -10,7 +10,6 @@ import { HomeIcon, ListIcon, LoaderIcon, - MessageCircle, MessageCircleHeart, RefreshCwIcon, XCircle, @@ -190,20 +189,22 @@ export function FrameDebugger({ const [openAccordions, setOpenAccordions] = useState([]); + const [latestFrame] = frameState.framesStack; + useEffect(() => { if (!frameState.isLoading) { // make sure the first frame is open if ( !openAccordions.includes( - String(frameState.framesStack[0]?.timestamp.getTime()) + String(latestFrame?.timestamp.getTime()) ) ) setOpenAccordions((v) => [ ...v, - String(frameState.framesStack[0]?.timestamp.getTime()), + String(latestFrame?.timestamp.getTime()), ]); } - }, [frameState.isLoading]); + }, [frameState.isLoading, latestFrame?.timestamp, openAccordions]); return (
@@ -219,7 +220,6 @@ export function FrameDebugger({ frameState.fetchFrame({ url: frameState?.homeframeUrl, method: "GET", - request: {}, }); }} > @@ -235,7 +235,6 @@ export function FrameDebugger({ frameState.fetchFrame({ url: frameState?.homeframeUrl, method: "GET", - request: {}, }); } }} @@ -246,12 +245,21 @@ export function FrameDebugger({ className="flex flex-row gap-3 items-center shadow-sm border" variant={"outline"} onClick={() => { - if (frameState?.framesStack[0]?.request) { - frameState.fetchFrame({ - url: frameState?.framesStack[0].url, - method: frameState?.framesStack[0].method, - request: frameState.framesStack[0].request, - } as FrameRequest); + const [latestFrame] = frameState.framesStack; + + if (latestFrame) { + frameState.fetchFrame( + latestFrame.method === "GET" + ? { + method: "GET", + url: latestFrame.url, + } + : { + method: "POST", + request: latestFrame.request, + url: latestFrame.url, + } + ); } }} > @@ -269,11 +277,14 @@ export function FrameDebugger({ className={`px-4 py-3 flex flex-col gap-2 ${i !== 0 ? "border-t" : "bg-slate-50"} hover:bg-slate-50 w-full`} key={frameStackItem.timestamp.getTime()} onClick={() => { - frameState.fetchFrame({ + frameState.fetchFrame(frameStackItem.method === 'GET' ? { + method: 'GET', + url: frameStackItem.url, + } : { url: frameStackItem.url, method: frameStackItem.method, request: frameStackItem.request, - } as FrameRequest); + }); }} > diff --git a/packages/debugger/app/page.tsx b/packages/debugger/app/page.tsx index d172c9dde..4c2b8831a 100644 --- a/packages/debugger/app/page.tsx +++ b/packages/debugger/app/page.tsx @@ -60,7 +60,7 @@ export default function App({ }, []); useEffect(() => { - if (url !== urlInput && url) { + if (url) { setUrlInput(url); } }, [url]); From 1753737e7a1042a01c6958a607dade8ec4103753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 10:40:30 +0200 Subject: [PATCH 09/14] fix: type errors in test --- packages/frames.js/src/getFrame.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/frames.js/src/getFrame.test.ts b/packages/frames.js/src/getFrame.test.ts index 435667045..92f4a4f7b 100644 --- a/packages/frames.js/src/getFrame.test.ts +++ b/packages/frames.js/src/getFrame.test.ts @@ -21,25 +21,21 @@ describe("getFrame", () => { { label: "Green", action: "post", - post_url: undefined, target: undefined, }, { label: "Purple", action: "post", - post_url: undefined, target: undefined, }, { label: "Red", action: "post", - post_url: undefined, target: undefined, }, { label: "Blue", action: "post", - post_url: undefined, target: undefined, }, ], @@ -194,25 +190,21 @@ describe("getFrame", () => { { action: "post", label: "1", - post_url: undefined, target: undefined, }, { action: "post_redirect", label: "2", - post_url: undefined, target: undefined, }, { action: "link", label: "3", - post_url: undefined, target: "https://example.com", }, { action: "mint", label: "Mint", - post_url: undefined, target: "eip155:7777777:0x060f3edd18c47f59bd23d063bbeb9aa4a8fec6df", }, ], From d7a3edc1bdcf790ff9fbc4e2090ae1470db7758d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 10:42:54 +0200 Subject: [PATCH 10/14] build: build before linting --- .github/workflows/github-actions.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 0864acc1f..64e14b140 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -50,6 +50,9 @@ jobs: - name: Install dependencies run: yarn --frozen-lockfile + - name: Build + run: yarn build:ci + - name: Lint run: yarn lint From 7d22df398ff0d8b5ec8698e7fe496747c9f4a39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 10:43:54 +0200 Subject: [PATCH 11/14] chore: add changeset --- .changeset/chilly-parrots-sneeze.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/chilly-parrots-sneeze.md diff --git a/.changeset/chilly-parrots-sneeze.md b/.changeset/chilly-parrots-sneeze.md new file mode 100644 index 000000000..e4cb1c414 --- /dev/null +++ b/.changeset/chilly-parrots-sneeze.md @@ -0,0 +1,7 @@ +--- +"frames.js": patch +"@frames.js/debugger": patch +"@frames.js/render": patch +--- + +fix: minor bugs and code cleanup From 45204ac7b3c97aa7e3b0d3173ffe8b48b479fb6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 11:23:58 +0200 Subject: [PATCH 12/14] fix: issues in tests --- .../src/core/composeMiddleware.test.ts | 3 +- packages/frames.js/src/getAddressForFid.ts | 14 +++++----- packages/frames.js/src/getAddressesForFid.ts | 2 +- .../src/middleware/farcaster.test.ts | 28 +++++++++---------- .../middleware/farcasterHubContext.test.ts | 7 +++-- .../src/middleware/openframes.test.ts | 4 +-- .../src/middleware/renderResponse.test.tsx | 5 ++-- .../src/middleware/stateMiddleware.ts | 6 +++- packages/frames.js/src/utils.ts | 13 ++++++++- 9 files changed, 50 insertions(+), 32 deletions(-) diff --git a/packages/frames.js/src/core/composeMiddleware.test.ts b/packages/frames.js/src/core/composeMiddleware.test.ts index 825133f3c..9f4a8e169 100644 --- a/packages/frames.js/src/core/composeMiddleware.test.ts +++ b/packages/frames.js/src/core/composeMiddleware.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/require-await -- we expect promises */ import { composeMiddleware } from "./composeMiddleware"; describe("composeMiddleware", () => { @@ -11,7 +12,7 @@ describe("composeMiddleware", () => { const context = { a: 1 }; const composedMiddleware = composeMiddleware([ - (ctx, next) => { + async (ctx, next) => { ctx.a = 2; return next(); }, diff --git a/packages/frames.js/src/getAddressForFid.ts b/packages/frames.js/src/getAddressForFid.ts index c595d44d0..8360e89e7 100644 --- a/packages/frames.js/src/getAddressForFid.ts +++ b/packages/frames.js/src/getAddressForFid.ts @@ -40,7 +40,7 @@ export async function getAddressForFid< throw new Error( `Failed to parse response body as JSON because server hub returned response with status "${response.status}" and body "${await response.clone().text()}"` ); - })) as { messages?: Record[] }; + })) as { messages?: Record[] }; let address: AddressReturnType | null = null; @@ -55,7 +55,11 @@ export async function getAddressForFid< } } - if (!address && fallbackToCustodyAddress) { + if (address) { + return address; + } + + if (fallbackToCustodyAddress) { const publicClient = createPublicClient({ transport: http(), chain: optimism, @@ -69,9 +73,5 @@ export async function getAddressForFid< }); } - if (!address) { - throw new Error(`No address found for fid ${fid}`); - } - - return address as AddressReturnType; + return address as unknown as AddressReturnType; } diff --git a/packages/frames.js/src/getAddressesForFid.ts b/packages/frames.js/src/getAddressesForFid.ts index fe46c3df3..80918533d 100644 --- a/packages/frames.js/src/getAddressesForFid.ts +++ b/packages/frames.js/src/getAddressesForFid.ts @@ -51,7 +51,7 @@ export async function getAddressesForFid({ throw new Error( `Failed to parse response body as JSON because server hub returned response with status "${verificationsResponse.status}" and body "${await verificationsResponse.clone().text()}"` ); - })) as { messages?: Record[] }; + })) as { messages?: Record[] }; if (messages) { const verifiedAddresses = messages diff --git a/packages/frames.js/src/middleware/farcaster.test.ts b/packages/frames.js/src/middleware/farcaster.test.ts index a9e0a9df5..4bea6276a 100644 --- a/packages/frames.js/src/middleware/farcaster.test.ts +++ b/packages/frames.js/src/middleware/farcaster.test.ts @@ -105,21 +105,21 @@ describe("farcaster middleware", () => { await mw(context, next); enableNetConnect(); - expect(next).toHaveBeenCalledWith( - expect.objectContaining({ - message: { - buttonIndex: 1, - castId: { - fid: 456, - hash: "0x", - }, - connectedAddress: "0x789", - inputText: "hello", - requesterFid: 123, - state: JSON.stringify({ test: true }), + expect(next).toHaveBeenCalledWith({ + clientProtocol: { id: "farcaster", version: "vNext" }, + message: { + buttonIndex: 1, + castId: { + fid: 456, + hash: "0x", }, - }) - ); + connectedAddress: "0x89", + inputText: "hello", + requesterFid: 123, + state: JSON.stringify({ test: true }), + transactionId: undefined, + }, + }); }); it("supports custom global typed context", async () => { diff --git a/packages/frames.js/src/middleware/farcasterHubContext.test.ts b/packages/frames.js/src/middleware/farcasterHubContext.test.ts index 872f6a55d..c8ef00367 100644 --- a/packages/frames.js/src/middleware/farcasterHubContext.test.ts +++ b/packages/frames.js/src/middleware/farcasterHubContext.test.ts @@ -104,7 +104,8 @@ describe("farcasterHubContext middleware", () => { expect(next).toHaveBeenCalledWith( expect.objectContaining({ - message: { + clientProtocol: { id: "farcaster", version: "vNext" }, + message: expect.objectContaining({ buttonIndex: 1, castId: { fid: 456, @@ -114,8 +115,8 @@ describe("farcasterHubContext middleware", () => { inputText: "hello", requesterFid: 123, state: JSON.stringify({ test: true }), - requestedUserData: expect.anything() as UserDataReturnType, - }, + requesterUserData: expect.anything() as UserDataReturnType, + }) as unknown, }) ); }); diff --git a/packages/frames.js/src/middleware/openframes.test.ts b/packages/frames.js/src/middleware/openframes.test.ts index f9e437c83..1a2beeaa7 100644 --- a/packages/frames.js/src/middleware/openframes.test.ts +++ b/packages/frames.js/src/middleware/openframes.test.ts @@ -290,11 +290,11 @@ describe("openframes middleware", () => { expect(next).toHaveBeenCalledWith( expect.objectContaining({ - message: { + message: expect.objectContaining({ buttonIndex: 1, inputText: "hello", state: { test: true }, - }, + }) as unknown, clientProtocol: { id: "xmtp", version: "2024-02-09", diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index b80551609..0a1620772 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -21,8 +21,9 @@ jest.mock("@vercel/og", () => { arrayBufferMock, constructorMock, ImageResponse: class { - constructor() { - constructorMock(); + constructor(...args: unknown[]) { + // @ts-expect-error -- we are mocking the constructor + constructorMock(...args); } arrayBuffer = arrayBufferMock; }, diff --git a/packages/frames.js/src/middleware/stateMiddleware.ts b/packages/frames.js/src/middleware/stateMiddleware.ts index 52a182b29..c32d26304 100644 --- a/packages/frames.js/src/middleware/stateMiddleware.ts +++ b/packages/frames.js/src/middleware/stateMiddleware.ts @@ -27,8 +27,12 @@ export function stateMiddleware< // since we are stringifyng state to JSON in renderResponse middleware, we need to parse decode JSON here // so it is easy to use in middleware chain and frames handler - if (typeof ctx.message.state === "string") { + if (ctx.message.state) { try { + if (typeof ctx.message.state !== "string") { + throw new Error("State is not a string"); + } + state = JSON.parse(ctx.message.state) as TState; } catch (e) { // eslint-disable-next-line no-console -- provide feedback diff --git a/packages/frames.js/src/utils.ts b/packages/frames.js/src/utils.ts index 58dc47f24..7fcca5258 100644 --- a/packages/frames.js/src/utils.ts +++ b/packages/frames.js/src/utils.ts @@ -131,5 +131,16 @@ export function extractAddressFromJSONMessage( return null; } - return bytesToHexString(data.verificationAddAddressBody.address); + /** + * This is ugly hack but we want to return the address as a string that is expected by the users ( essentially what they see in the response from the hub ). + * We could use Buffer.from(data.verificationAddAddressBody.address).toString('base64') here but that results in different base64. + * Therefore we return address from source message and not from decoded message. + * + * For example for value 0x8d25687829d6b85d9e0020b8c89e3ca24de20a89 from API we get 0x8d25687829d6b85d9e0020b8c89e3ca24de20a8w== from Buffer.from(...).toString('base64'). + * The values are the same if you compare them as Buffer.from(a).equals(Buffer.from(b)). + */ + // @TODO type message properly or handle the return type properly + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- we know the data is there + return (message as Record).data.verificationAddAddressBody + .address as `0x${string}`; } From 9d4a8c54db06125bd14279b4268804fc094c5eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 11:41:16 +0200 Subject: [PATCH 13/14] fix: typo --- packages/frames.js/src/middleware/renderResponse.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index 0a1620772..dcca8df36 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-console -- we are expecting console usage */ -/* eslint-disable @typescript-eslint/require-await -- middleware expectes async functions */ +/* eslint-disable @typescript-eslint/require-await -- middleware expects async functions */ /* eslint-disable testing-library/render-result-naming-convention -- we are not using react testing library here */ import * as vercelOg from "@vercel/og"; import { FRAMES_META_TAGS_HEADER } from "../core"; From 84283a8b56f20de1d9c5d670479f5a090bfc0d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 3 Apr 2024 11:42:12 +0200 Subject: [PATCH 14/14] chore: update changeset --- .changeset/calm-carpets-divide.md | 5 +++++ .changeset/chilly-parrots-sneeze.md | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-carpets-divide.md diff --git a/.changeset/calm-carpets-divide.md b/.changeset/calm-carpets-divide.md new file mode 100644 index 000000000..c4d4dd979 --- /dev/null +++ b/.changeset/calm-carpets-divide.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": minor +--- + +feat: rename types diff --git a/.changeset/chilly-parrots-sneeze.md b/.changeset/chilly-parrots-sneeze.md index e4cb1c414..0db32b7d5 100644 --- a/.changeset/chilly-parrots-sneeze.md +++ b/.changeset/chilly-parrots-sneeze.md @@ -1,7 +1,6 @@ --- "frames.js": patch "@frames.js/debugger": patch -"@frames.js/render": patch --- fix: minor bugs and code cleanup