From ef57739194deddfe64fe549664d943fb98ebcaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 12:48:04 +0200 Subject: [PATCH 01/10] feat: add baseURL option --- .changeset/violet-adults-compare.md | 5 ++ .../frames.js/src/core/createFrames.test.ts | 81 +++++++++++++++++++ packages/frames.js/src/core/createFrames.ts | 34 +++++++- packages/frames.js/src/core/types.ts | 24 ++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 .changeset/violet-adults-compare.md diff --git a/.changeset/violet-adults-compare.md b/.changeset/violet-adults-compare.md new file mode 100644 index 000000000..f4f42ef3b --- /dev/null +++ b/.changeset/violet-adults-compare.md @@ -0,0 +1,5 @@ +--- +"frames.js": minor +--- + +feat: add baseURL option diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index c03ae3ae3..baabbef46 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -137,4 +137,85 @@ describe("createFrames", () => { expect(response).toBeInstanceOf(Response); }); + + it("fails if invalid URL is set as baseURL", () => { + expect(() => createFrames({ baseURL: "invalid" })).toThrow( + "Invalid baseURL: Invalid URL" + ); + }); + + it("overrides context.url and request.url with provided baseURL (string)", async () => { + const handler = createFrames({ baseURL: "http://override.com" }); + + const routeHandler = handler((ctx) => { + expect(ctx.url.href).toBe("http://override.com/"); + expect(ctx.request.url).toBe("http://override.com/"); + return Response.json({ test: true }); + }); + + await expect( + routeHandler(new Request("http://test.com")) + ).resolves.toHaveProperty("status", 200); + }); + + it("overrides context.url and request.url with provided baseURL (URL)", async () => { + const handler = createFrames({ baseURL: new URL("http://override.com") }); + + const routeHandler = handler((ctx) => { + expect(ctx.url.href).toBe("http://override.com/"); + expect(ctx.request.url).toBe("http://override.com/"); + return Response.json({ test: true }); + }); + + await expect( + routeHandler(new Request("http://test.com")) + ).resolves.toHaveProperty("status", 200); + }); + + it("clones the request if the baseURL and request.url differ", async () => { + const handler = createFrames({ baseURL: new URL("http://override.com") }); + const request = new Request("http://test.com"); + const routeHandler = handler((ctx) => { + expect(ctx.url.href).toBe("http://override.com/"); + expect(ctx.request.url).toBe("http://override.com/"); + expect(ctx.request).not.toBe(request); + return Response.json({ test: true }); + }); + + await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + }); + + it("overrides the request.url completely with provided baseURL", async () => { + const handler = createFrames({ + baseURL: new URL("http://override.com/test.png"), + }); + const request = new Request( + "http://test.com/this-path-will-be-also-removed" + ); + const routeHandler = handler((ctx) => { + expect(ctx.url.href).toBe("http://override.com/test.png"); + expect(ctx.request.url).toBe("http://override.com/test.png"); + expect(ctx.request).not.toBe(request); + return Response.json({ test: true }); + }); + + await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + }); + + it("properly clones the request with body", async () => { + const handler = createFrames({ baseURL: new URL("http://override.com") }); + const request = new Request("http://test.com", { + method: "POST", + body: JSON.stringify({ test: true }), + }); + const routeHandler = handler(async (ctx) => { + expect(ctx.request).not.toBe(request); + expect(ctx.request.method).toBe("POST"); + await expect(ctx.request.json()).resolves.toEqual({ test: true }); + + return Response.json({ test: true }); + }); + + await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + }); }); diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 8cf98fb6c..83a93ae5a 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -10,6 +10,22 @@ import type { JsonValue, } from "./types"; +function inferURLFromRequestOrBaseURL(request: Request, baseURL?: URL): URL { + if (baseURL) { + return baseURL; + } + + return new URL(request.url); +} + +function cloneRequestWithInferedURL(request: Request, url: URL): Request { + if (request.url === url.toString()) { + return request; + } + + return new Request(url, request); +} + export function createFrames< TState extends JsonValue | undefined = JsonValue | undefined, TMiddlewares extends FramesMiddleware[] | undefined = undefined, @@ -17,6 +33,7 @@ export function createFrames< basePath = "/", initialState, middleware, + baseURL, }: FramesOptions = {}): FramesRequestHandlerFunction< TState, typeof coreMiddleware, @@ -25,6 +42,18 @@ export function createFrames< > { const globalMiddleware: FramesMiddleware>[] = middleware || []; + let url: URL | undefined; + + // validate baseURL + if (typeof baseURL === "string") { + try { + url = new URL(baseURL); + } catch (e) { + throw new Error(`Invalid baseURL: ${(e as Error).message}`); + } + } else { + url = baseURL; + } /** * This function takes handler function that does the logic with the help of context and returns one of possible results @@ -60,11 +89,12 @@ export function createFrames< * maps Response to frameworks response type. */ return async function handleFramesRequest(request: Request) { + const inferedURL = inferURLFromRequestOrBaseURL(request, url); const context: FramesContext = { basePath, initialState: initialState as TState, - request, - url: new URL(request.url), + request: cloneRequestWithInferedURL(request, inferedURL), + url: inferedURL, }; const result = await composedMiddleware(context); diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index b4c97a026..bcd2a3d41 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -196,6 +196,30 @@ export type FramesOptions< * @defaultValue '/' */ basePath?: string; + /** + * Overrides the detected URL of the request. URL is used in combination with `basePath` to generate target URLs for Buttons. + * Provided value must be full URL with protocol and domain. + * This is useful if the URL detection fails to recognize the correct URL or if you want to override it. + * + * This URL also overrides the request.url value with the provided value. + * + * @example + * ```ts + * // using string, the value of ctx.url and request.url will be set to this value + * { + * baseURL: 'https://example.com', + * } + * ``` + * + * @example + * ```ts + * // using URL, the value of ctx.url and request.url will be set to this value + * { + * baseURL: new URL('https://example.com'), + * } + * ``` + */ + baseURL?: string | URL; /** * Initial state, used if no state is provided in the message or you are on initial frame. * From 964b777d4b145437870986d1ad1cdad365266c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 12:58:20 +0200 Subject: [PATCH 02/10] chore: rename option to baseUrl --- packages/frames.js/src/core/createFrames.test.ts | 12 ++++++------ packages/frames.js/src/core/createFrames.ts | 8 ++++---- packages/frames.js/src/core/types.ts | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index baabbef46..4310380e2 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -139,13 +139,13 @@ describe("createFrames", () => { }); it("fails if invalid URL is set as baseURL", () => { - expect(() => createFrames({ baseURL: "invalid" })).toThrow( + expect(() => createFrames({ baseUrl: "invalid" })).toThrow( "Invalid baseURL: Invalid URL" ); }); it("overrides context.url and request.url with provided baseURL (string)", async () => { - const handler = createFrames({ baseURL: "http://override.com" }); + const handler = createFrames({ baseUrl: "http://override.com" }); const routeHandler = handler((ctx) => { expect(ctx.url.href).toBe("http://override.com/"); @@ -159,7 +159,7 @@ describe("createFrames", () => { }); it("overrides context.url and request.url with provided baseURL (URL)", async () => { - const handler = createFrames({ baseURL: new URL("http://override.com") }); + const handler = createFrames({ baseUrl: new URL("http://override.com") }); const routeHandler = handler((ctx) => { expect(ctx.url.href).toBe("http://override.com/"); @@ -173,7 +173,7 @@ describe("createFrames", () => { }); it("clones the request if the baseURL and request.url differ", async () => { - const handler = createFrames({ baseURL: new URL("http://override.com") }); + const handler = createFrames({ baseUrl: new URL("http://override.com") }); const request = new Request("http://test.com"); const routeHandler = handler((ctx) => { expect(ctx.url.href).toBe("http://override.com/"); @@ -187,7 +187,7 @@ describe("createFrames", () => { it("overrides the request.url completely with provided baseURL", async () => { const handler = createFrames({ - baseURL: new URL("http://override.com/test.png"), + baseUrl: new URL("http://override.com/test.png"), }); const request = new Request( "http://test.com/this-path-will-be-also-removed" @@ -203,7 +203,7 @@ describe("createFrames", () => { }); it("properly clones the request with body", async () => { - const handler = createFrames({ baseURL: new URL("http://override.com") }); + const handler = createFrames({ baseUrl: new URL("http://override.com") }); const request = new Request("http://test.com", { method: "POST", body: JSON.stringify({ test: true }), diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 83a93ae5a..189815f0e 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -33,7 +33,7 @@ export function createFrames< basePath = "/", initialState, middleware, - baseURL, + baseUrl, }: FramesOptions = {}): FramesRequestHandlerFunction< TState, typeof coreMiddleware, @@ -45,14 +45,14 @@ export function createFrames< let url: URL | undefined; // validate baseURL - if (typeof baseURL === "string") { + if (typeof baseUrl === "string") { try { - url = new URL(baseURL); + url = new URL(baseUrl); } catch (e) { throw new Error(`Invalid baseURL: ${(e as Error).message}`); } } else { - url = baseURL; + url = baseUrl; } /** diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index bcd2a3d41..9f8ad6964 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -207,7 +207,7 @@ export type FramesOptions< * ```ts * // using string, the value of ctx.url and request.url will be set to this value * { - * baseURL: 'https://example.com', + * baseUrl: 'https://example.com', * } * ``` * @@ -215,11 +215,11 @@ export type FramesOptions< * ```ts * // using URL, the value of ctx.url and request.url will be set to this value * { - * baseURL: new URL('https://example.com'), + * baseUrl: new URL('https://example.com'), * } * ``` */ - baseURL?: string | URL; + baseUrl?: string | URL; /** * Initial state, used if no state is provided in the message or you are on initial frame. * From 2ca4f38936d6ac5be933db4e5745b4142fe5dffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 13:01:35 +0200 Subject: [PATCH 03/10] chore: change version bump --- .changeset/violet-adults-compare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/violet-adults-compare.md b/.changeset/violet-adults-compare.md index f4f42ef3b..1f59d48d0 100644 --- a/.changeset/violet-adults-compare.md +++ b/.changeset/violet-adults-compare.md @@ -1,5 +1,5 @@ --- -"frames.js": minor +"frames.js": patch --- feat: add baseURL option From 9354bb0268c31d26b5c882b1e2204336041b0ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 13:13:38 +0200 Subject: [PATCH 04/10] chore: typo --- packages/frames.js/src/core/createFrames.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 189815f0e..1d8ecc0eb 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -89,12 +89,12 @@ export function createFrames< * maps Response to frameworks response type. */ return async function handleFramesRequest(request: Request) { - const inferedURL = inferURLFromRequestOrBaseURL(request, url); + const inferredURL = inferURLFromRequestOrBaseURL(request, url); const context: FramesContext = { basePath, initialState: initialState as TState, - request: cloneRequestWithInferedURL(request, inferedURL), - url: inferedURL, + request: cloneRequestWithInferedURL(request, inferredURL), + url: inferredURL, }; const result = await composedMiddleware(context); From acb520a1970f7b5f946da9f1801e192613a36658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 13:15:34 +0200 Subject: [PATCH 05/10] chore: update docs --- docs/pages/reference/core/createFrames.mdx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index be7865df2..642ea2f75 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -27,6 +27,12 @@ The function passed to `frames` will be called with the context of a frame actio A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. +### `baseUrl` + +- Type: `string | URL` + +A string or URL object that specifies the base URL for all relative URLs in the frame definition. It defaults to the current URL of the request. Can be used to override the URL detected from the request. + ### `initialState` - Type: generic @@ -50,10 +56,10 @@ For strong type support in the handler, the middleware should be typed as `Frame ```tsx import { createFrames, types } from "frames.js/next"; -const myMiddleware: types.FramesMiddleware< - any, - { foo?: string } -> = async (ctx, next) => { +const myMiddleware: types.FramesMiddleware = async ( + ctx, + next +) => { return next({ foo: "bar" }); }; ``` @@ -161,7 +167,8 @@ const handleRequest = frames(async (ctx) => { aspectRatio: "1:1", }, buttons: [], - headers: {// [!code focus] + headers: { + // [!code focus] // Max cache age in seconds // [!code focus] "Cache-Control": "max-age=0", // [!code focus] }, // [!code focus] From e8a4316413427d27f8db9b2f41f1d14414553885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 13:17:55 +0200 Subject: [PATCH 06/10] chore: rename tests --- packages/frames.js/src/core/createFrames.test.ts | 12 ++++++------ packages/frames.js/src/core/createFrames.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index 4310380e2..6578421ea 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -138,13 +138,13 @@ describe("createFrames", () => { expect(response).toBeInstanceOf(Response); }); - it("fails if invalid URL is set as baseURL", () => { + it("fails if invalid URL is set as baseUrl", () => { expect(() => createFrames({ baseUrl: "invalid" })).toThrow( - "Invalid baseURL: Invalid URL" + "Invalid baseUrl: Invalid URL" ); }); - it("overrides context.url and request.url with provided baseURL (string)", async () => { + it("overrides context.url and request.url with provided baseUrl (string)", async () => { const handler = createFrames({ baseUrl: "http://override.com" }); const routeHandler = handler((ctx) => { @@ -158,7 +158,7 @@ describe("createFrames", () => { ).resolves.toHaveProperty("status", 200); }); - it("overrides context.url and request.url with provided baseURL (URL)", async () => { + it("overrides context.url and request.url with provided baseUrl (URL)", async () => { const handler = createFrames({ baseUrl: new URL("http://override.com") }); const routeHandler = handler((ctx) => { @@ -172,7 +172,7 @@ describe("createFrames", () => { ).resolves.toHaveProperty("status", 200); }); - it("clones the request if the baseURL and request.url differ", async () => { + it("clones the request if the baseUrl and request.url differ", async () => { const handler = createFrames({ baseUrl: new URL("http://override.com") }); const request = new Request("http://test.com"); const routeHandler = handler((ctx) => { @@ -185,7 +185,7 @@ describe("createFrames", () => { await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); }); - it("overrides the request.url completely with provided baseURL", async () => { + it("overrides the request.url completely with provided baseUrl", async () => { const handler = createFrames({ baseUrl: new URL("http://override.com/test.png"), }); diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 1d8ecc0eb..329887361 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -49,7 +49,7 @@ export function createFrames< try { url = new URL(baseUrl); } catch (e) { - throw new Error(`Invalid baseURL: ${(e as Error).message}`); + throw new Error(`Invalid baseUrl: ${(e as Error).message}`); } } else { url = baseUrl; From 5ed02484a4c0b2a794ca5d5c7973c7a1bb4c7789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 13:26:11 +0200 Subject: [PATCH 07/10] chore: docs formatting --- docs/pages/reference/core/createFrames.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index 642ea2f75..8567ff2b9 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -167,8 +167,7 @@ const handleRequest = frames(async (ctx) => { aspectRatio: "1:1", }, buttons: [], - headers: { - // [!code focus] + headers: { // [!code focus] // Max cache age in seconds // [!code focus] "Cache-Control": "max-age=0", // [!code focus] }, // [!code focus] From 5e4b1bbb9950d3a3159f500a2e880fb764794778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 15:22:39 +0200 Subject: [PATCH 08/10] feat: use baseUrl only for url generation --- docs/pages/reference/core/createFrames.mdx | 16 +++- .../frames.js/src/core/createFrames.test.ts | 79 +++++++++------- packages/frames.js/src/core/createFrames.ts | 24 +---- packages/frames.js/src/core/types.ts | 44 ++++++++- packages/frames.js/src/core/utils.test.ts | 92 +++++++++++++------ packages/frames.js/src/core/utils.ts | 92 ++++++++++--------- .../renderResponse.test.tsx.snap | 2 +- .../src/middleware/framesjsMiddleware.test.ts | 24 ++--- .../src/middleware/openframes.test.ts | 1 + .../src/middleware/renderResponse.test.tsx | 15 ++- .../src/middleware/renderResponse.ts | 21 ++--- 11 files changed, 248 insertions(+), 162 deletions(-) diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index 8567ff2b9..bf22afe55 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -25,13 +25,13 @@ The function passed to `frames` will be called with the context of a frame actio - Type: `string` -A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. +A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. If `baseUrl` is provided, it will be resolved relatively to `baseUrl`. If the `baseUrl` option is not provided, it will overwrite the path of current request's URL. ### `baseUrl` - Type: `string | URL` -A string or URL object that specifies the base URL for all relative URLs in the frame definition. It defaults to the current URL of the request. Can be used to override the URL detected from the request. +A string or URL object that specifies the base URL for all relative URLs in the frame definition. Can be used to override the URL detected from the request. ### `initialState` @@ -214,6 +214,12 @@ Core middleware is included and executed by default and gives you access to the Specifies the base path for all relative URLs in the frame definition. +### `baseUrl` + +- Type: `URL | undefined` + +Specifies the base URL for all relative URLs in the frame definition. `baseUrl` is resolved relatively to this value. + ### `initialState` - Type: generic @@ -226,6 +232,12 @@ A JSON serializable value that is used if no state is provided in the message or The request object that was passed to the request handler. +### `resolvedBaseUrl` + +- Type: `URL` + +The resolved base URL for all relative URLs in the frame definition. All relative URLs are resolved relatively to this value. + ### `url` - Type: [Web API `URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index 6578421ea..45b07dced 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -144,12 +144,11 @@ describe("createFrames", () => { ); }); - it("overrides context.url and request.url with provided baseUrl (string)", async () => { + it("sets baseUrl on context if provided", async () => { const handler = createFrames({ baseUrl: "http://override.com" }); const routeHandler = handler((ctx) => { - expect(ctx.url.href).toBe("http://override.com/"); - expect(ctx.request.url).toBe("http://override.com/"); + expect(ctx.baseUrl?.toString()).toBe("http://override.com/"); return Response.json({ test: true }); }); @@ -158,64 +157,76 @@ describe("createFrames", () => { ).resolves.toHaveProperty("status", 200); }); - it("overrides context.url and request.url with provided baseUrl (URL)", async () => { - const handler = createFrames({ baseUrl: new URL("http://override.com") }); + it("resolves resolvedUrl against request URL and / if no basePath or baseUrl are provided", async () => { + const handler = createFrames(); const routeHandler = handler((ctx) => { - expect(ctx.url.href).toBe("http://override.com/"); - expect(ctx.request.url).toBe("http://override.com/"); + expect(ctx.resolvedBaseUrl.toString()).toBe("http://test.com/"); return Response.json({ test: true }); }); await expect( - routeHandler(new Request("http://test.com")) + routeHandler(new Request("http://test.com/this-will-be-removed")) ).resolves.toHaveProperty("status", 200); }); - it("clones the request if the baseUrl and request.url differ", async () => { - const handler = createFrames({ baseUrl: new URL("http://override.com") }); - const request = new Request("http://test.com"); + it("resolves resolvedUrl against request URL when only basePath is provided", async () => { + const handler = createFrames({ basePath: "/test" }); + const routeHandler = handler((ctx) => { - expect(ctx.url.href).toBe("http://override.com/"); - expect(ctx.request.url).toBe("http://override.com/"); - expect(ctx.request).not.toBe(request); + expect(ctx.resolvedBaseUrl.toString()).toBe("http://test.com/test"); return Response.json({ test: true }); }); - await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + await expect( + routeHandler(new Request("http://test.com/this-will-be-removed")) + ).resolves.toHaveProperty("status", 200); }); - it("overrides the request.url completely with provided baseUrl", async () => { + it("resolves resolvedUrl against baseUrl and / when only baseUrl is provided", async () => { + const handler = createFrames({ baseUrl: "http://override.com" }); + + const routeHandler = handler((ctx) => { + expect(ctx.resolvedBaseUrl.toString()).toBe("http://override.com/"); + return Response.json({ test: true }); + }); + + await expect( + routeHandler(new Request("http://test.com/this-will-be-removed")) + ).resolves.toHaveProperty("status", 200); + }); + + it("resolves resolvedUrl against baseUrl and basePath if both are provided", async () => { const handler = createFrames({ - baseUrl: new URL("http://override.com/test.png"), + baseUrl: "http://override.com", + basePath: "/test", }); - const request = new Request( - "http://test.com/this-path-will-be-also-removed" - ); + const routeHandler = handler((ctx) => { - expect(ctx.url.href).toBe("http://override.com/test.png"); - expect(ctx.request.url).toBe("http://override.com/test.png"); - expect(ctx.request).not.toBe(request); + expect(ctx.resolvedBaseUrl.toString()).toBe("http://override.com/test"); return Response.json({ test: true }); }); - await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + await expect( + routeHandler(new Request("http://test.com/this-will-be-removed")) + ).resolves.toHaveProperty("status", 200); }); - it("properly clones the request with body", async () => { - const handler = createFrames({ baseUrl: new URL("http://override.com") }); - const request = new Request("http://test.com", { - method: "POST", - body: JSON.stringify({ test: true }), + it("resolves basePath relatively to baseUrl", async () => { + const handler = createFrames({ + baseUrl: "http://override.com/test", + basePath: "/test2", }); - const routeHandler = handler(async (ctx) => { - expect(ctx.request).not.toBe(request); - expect(ctx.request.method).toBe("POST"); - await expect(ctx.request.json()).resolves.toEqual({ test: true }); + const routeHandler = handler((ctx) => { + expect(ctx.resolvedBaseUrl.toString()).toBe( + "http://override.com/test/test2" + ); return Response.json({ test: true }); }); - await expect(routeHandler(request)).resolves.toHaveProperty("status", 200); + await expect( + routeHandler(new Request("http://test.com/this-will-be-removed")) + ).resolves.toHaveProperty("status", 200); }); }); diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index 329887361..f78f5118c 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -9,22 +9,7 @@ import type { FramesRequestHandlerFunction, JsonValue, } from "./types"; - -function inferURLFromRequestOrBaseURL(request: Request, baseURL?: URL): URL { - if (baseURL) { - return baseURL; - } - - return new URL(request.url); -} - -function cloneRequestWithInferedURL(request: Request, url: URL): Request { - if (request.url === url.toString()) { - return request; - } - - return new Request(url, request); -} +import { resolveBaseUrl } from "./utils"; export function createFrames< TState extends JsonValue | undefined = JsonValue | undefined, @@ -89,12 +74,13 @@ export function createFrames< * maps Response to frameworks response type. */ return async function handleFramesRequest(request: Request) { - const inferredURL = inferURLFromRequestOrBaseURL(request, url); const context: FramesContext = { + baseUrl: url, basePath, initialState: initialState as TState, - request: cloneRequestWithInferedURL(request, inferredURL), - url: inferredURL, + request, + url: new URL(request.url), + resolvedBaseUrl: resolveBaseUrl(request, url, basePath), }; const result = await composedMiddleware(context); diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index 9f8ad6964..e3ae50f9c 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -41,6 +41,15 @@ export type FramesContext = { * All frame relative targets will be resolved relative to this */ basePath: string; + /** + * All frame relative targets will be resolved against this url if provided. + * basePath is always resolved relatively to this URL (if provided). + */ + baseUrl?: URL; + /** + * URL resolved based on current request URL, baseUrl and basePath. This URL is used to generate target URLs. + */ + resolvedBaseUrl: URL; /** * Values passed to createFrames() */ @@ -192,13 +201,44 @@ export type FramesOptions< TFrameMiddleware extends FramesMiddleware[] | undefined, > = { /** - * All frame relative targets will be resolved relative to this + * All frame relative targets will be resolved relative to this. `basePath` is always resolved relatively to baseUrl (if provided). If `baseUrl` is not provided then `basePath` overrides the path part of current request's URL. + * + * @example + * ```ts + * { + * basePath: '/foo' + * } + * + * // if the request URL is http://mydomain.dev/bar then context.url will be set to http://mydomain.dev/foo + * // if the request URL is http://mydomain.dev/ then context.url will be set to http://mydomain.dev/foo + * ``` + * + * @example + * ```ts + * { + * basePath: '/foo', + * baseUrl: 'http://mydomain.dev' + * } + * + * // context.url will be set to http://mydomain.dev/foo + * ``` + * + * @example + * ```ts + * { + * basePath: '/foo', + * baseUrl: 'http://localhost:3000/test' + * } + * + * // context.url will be set to http://localhost:3000/test/foo + * ``` + * * @defaultValue '/' */ basePath?: string; /** * Overrides the detected URL of the request. URL is used in combination with `basePath` to generate target URLs for Buttons. - * Provided value must be full URL with protocol and domain. + * Provided value must be full valid URL with protocol and domain. `basePath` if provided is resolved relatively to this URL. * This is useful if the URL detection fails to recognize the correct URL or if you want to override it. * * This URL also overrides the request.url value with the provided value. diff --git a/packages/frames.js/src/core/utils.test.ts b/packages/frames.js/src/core/utils.test.ts index 195f4db28..ee41cf154 100644 --- a/packages/frames.js/src/core/utils.test.ts +++ b/packages/frames.js/src/core/utils.test.ts @@ -1,20 +1,74 @@ import { generatePostButtonTargetURL, parseButtonInformationFromTargetURL, + resolveBaseUrl, } from "./utils"; +describe("resolveBaseUrl", () => { + it("uses URL from request if no baseUrl is provided", () => { + const request = new Request("http://test.com"); + const basePath = "/"; + + expect(resolveBaseUrl(request, undefined, basePath).toString()).toBe( + "http://test.com/" + ); + }); + + it('uses baseUrl if it is provided and basePath is "/"', () => { + const request = new Request("http://test.com"); + const basePath = "/"; + + expect( + resolveBaseUrl( + request, + new URL("http://override.com"), + basePath + ).toString() + ).toBe("http://override.com/"); + + expect( + resolveBaseUrl( + request, + new URL("http://override.com/test"), + basePath + ).toString() + ).toBe("http://override.com/test"); + }); + + it("resolves basePath relatively to baseUrl", () => { + const request = new Request("http://test.com"); + const basePath = "/test"; + + expect( + resolveBaseUrl( + request, + new URL("http://override.com"), + basePath + ).toString() + ).toBe("http://override.com/test"); + }); + + it("overrides path on request URL with basePath", () => { + const request = new Request("http://test.com/this-will-be-removed"); + const basePath = "/test"; + + expect(resolveBaseUrl(request, undefined, basePath).toString()).toBe( + "http://test.com/test" + ); + }); +}); + describe("generatePostButtonTargetURL", () => { it("generates an URL for post button without target and without state", () => { - const expected = new URL("/test", "http://test.com"); + const expected = new URL("/", "http://test.com"); expected.searchParams.set("__bi", "1-p"); expect( generatePostButtonTargetURL({ target: undefined, - basePath: "/", buttonAction: "post", buttonIndex: 1, - currentURL: new URL("http://test.com/test"), + resolvedBaseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); @@ -26,10 +80,9 @@ describe("generatePostButtonTargetURL", () => { expect( generatePostButtonTargetURL({ - basePath: "/", buttonAction: "post", buttonIndex: 1, - currentURL: new URL("http://test.com/test"), + resolvedBaseUrl: new URL("http://test.com"), target: { query: { test: "test" } }, }) ).toBe(expected.toString()); @@ -42,10 +95,9 @@ describe("generatePostButtonTargetURL", () => { expect( generatePostButtonTargetURL({ target: "/test", - basePath: "/", buttonAction: "post", buttonIndex: 1, - currentURL: new URL("http://test.com"), + resolvedBaseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); @@ -57,44 +109,24 @@ describe("generatePostButtonTargetURL", () => { expect( generatePostButtonTargetURL({ - basePath: "/", buttonAction: "post", buttonIndex: 1, - currentURL: new URL("http://test.com"), + resolvedBaseUrl: new URL("http://test.com"), target: { query: { test: "test" }, pathname: "/test" }, }) ).toBe(expected.toString()); }); - it.each(["/", "/test", "/test/test"])( - "resolves target relatively to basePath and current path %s", - (currentPath) => { - const expected = new URL("/prefixed/test/my-target", "http://test.com"); - expected.searchParams.set("__bi", "1-p"); - - expect( - generatePostButtonTargetURL({ - target: "/my-target", - basePath: "/prefixed/test", - buttonAction: "post", - buttonIndex: 1, - currentURL: new URL(currentPath, "http://test.com/"), - }) - ).toBe(expected.toString()); - } - ); - it("also supports post_redirect button", () => { const expected = new URL("/test", "http://test.com"); expected.searchParams.set("__bi", "1-pr"); expect( generatePostButtonTargetURL({ - target: undefined, - basePath: "/", + target: "/test", buttonAction: "post_redirect", buttonIndex: 1, - currentURL: new URL("http://test.com/test"), + resolvedBaseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); diff --git a/packages/frames.js/src/core/utils.ts b/packages/frames.js/src/core/utils.ts index 176d7bbce..fbf156d63 100644 --- a/packages/frames.js/src/core/utils.ts +++ b/packages/frames.js/src/core/utils.ts @@ -9,6 +9,28 @@ const buttonActionToCode = { const BUTTON_INFORMATION_SEARCH_PARAM_NAME = "__bi"; +export function joinPaths(pathA: string, pathB: string): string { + return pathB === "/" + ? pathA + : [pathA, pathB].join("/").replace(/\/{2,}/g, "/"); +} + +export function resolveBaseUrl( + request: Request, + baseUrl: URL | undefined, + basePath: string +): URL { + if (baseUrl) { + if (basePath === "/" || basePath === "") { + return baseUrl; + } + + return new URL(joinPaths(baseUrl.pathname, basePath), baseUrl); + } + + return new URL(basePath, request.url); +} + function isValidButtonIndex(index: unknown): index is 1 | 2 | 3 | 4 { return ( typeof index === "number" && @@ -28,57 +50,45 @@ function isValidButtonAction( } export function generateTargetURL({ - currentURL, + resolvedBaseUrl, target, - basePath, }: { - currentURL: URL; - basePath: string; + resolvedBaseUrl: URL; target: string | UrlObject | undefined; -}): string { - let url = new URL(currentURL); - - if ( - target && - typeof target === "string" && - (target.startsWith("http://") || target.startsWith("https://")) - ) { - // handle absolute urls - url = new URL(target); - } else if (target && typeof target === "string") { - // resolve target relatively to basePath - const baseUrl = new URL(basePath, currentURL); - const preformatted = `${baseUrl.pathname}/${target}`; - const parts = preformatted.split("/").filter(Boolean); - const finalPathname = parts.join("/"); - - url = new URL(`/${finalPathname}`, currentURL); - } else if (target && typeof target === "object") { - // resolve target relatively to basePath +}): URL { + if (!target) { + return new URL(resolvedBaseUrl); + } - url = new URL( + if (typeof target === "object") { + return new URL( formatUrl({ - host: url.host, - hash: url.hash, - hostname: url.hostname, - href: url.href, + host: resolvedBaseUrl.host, + hash: resolvedBaseUrl.hash, + hostname: resolvedBaseUrl.hostname, + href: resolvedBaseUrl.href, // pathname: url.pathname, - protocol: url.protocol, + protocol: resolvedBaseUrl.protocol, // we ignore existing search params and uses only new ones // search: url.search, - port: url.port, + port: resolvedBaseUrl.port, // query: url.searchParams, ...target, - pathname: `/${[basePath, "/", target.pathname ?? ""] - .join("") - .split("/") - .filter(Boolean) - .join("/")}`, + pathname: joinPaths(resolvedBaseUrl.pathname, target.pathname ?? ""), }) ); } - return url.toString(); + try { + // check if target is absolute url + return new URL(target); + } catch { + // resolve target relatively to basePath + const baseUrl = new URL(resolvedBaseUrl); + const finalPathname = joinPaths(baseUrl.pathname, target); + + return new URL(finalPathname, baseUrl); + } } /** @@ -88,17 +98,15 @@ export function generateTargetURL({ export function generatePostButtonTargetURL({ buttonIndex, buttonAction, - currentURL, target, - basePath, + resolvedBaseUrl, }: { buttonIndex: 1 | 2 | 3 | 4; buttonAction: "post" | "post_redirect"; - currentURL: URL; - basePath: string; target: string | UrlObject | undefined; + resolvedBaseUrl: URL; }): string { - const url = new URL(generateTargetURL({ currentURL, basePath, target })); + const url = new URL(generateTargetURL({ resolvedBaseUrl, target })); // Internal param, store what button has been clicked in the URL. url.searchParams.set( diff --git a/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap b/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap index 1338b4373..5f7e0fa29 100644 --- a/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap +++ b/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap @@ -82,6 +82,6 @@ exports[`renderResponse middleware correctly renders image wich conditional cont } `; -exports[`renderResponse middleware properly resolves against basePath 1`] = `""`; +exports[`renderResponse middleware properly resolves against resolvedBaseUrl 1`] = `""`; exports[`renderResponse middleware renders text input 1`] = `""`; diff --git a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts index 1943eb160..2499da670 100644 --- a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts +++ b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts @@ -1,14 +1,16 @@ /* 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"; +import { generatePostButtonTargetURL, resolveBaseUrl } from "../core/utils"; import { framesjsMiddleware } from "./framesjsMiddleware"; describe("framesjsMiddleware middleware", () => { it("does not provide pressedButton to context if no supported button is detected", async () => { + const request = new Request("https://example.com", { method: "POST" }); const context = { url: new URL("https://example.com"), - request: new Request("https://example.com", { method: "POST" }), + request, + resolvedBaseUrl: resolveBaseUrl(request, undefined, "/"), } as unknown as FramesContext; const next = jest.fn(); const middleware = framesjsMiddleware(); @@ -25,8 +27,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -54,8 +55,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post_redirect", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: "/test", }); const context = { @@ -83,8 +83,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post_redirect", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: "/test", }); const context = { @@ -108,8 +107,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -134,8 +132,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -161,8 +158,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - basePath: "/", - currentURL: new URL("https://example.com"), + resolvedBaseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, diff --git a/packages/frames.js/src/middleware/openframes.test.ts b/packages/frames.js/src/middleware/openframes.test.ts index 1a2beeaa7..316b97726 100644 --- a/packages/frames.js/src/middleware/openframes.test.ts +++ b/packages/frames.js/src/middleware/openframes.test.ts @@ -114,6 +114,7 @@ describe("openframes middleware", () => { }), url: new URL("https://example.com").toString(), basePath: "/", + resolvedBaseUrl: new URL("https://example.com"), } as unknown as FramesContext; const mw1 = openframes({ diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index 04f024c6c..ff8fa4fb4 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -7,6 +7,7 @@ import { Button } from "../core/components"; import { error } from "../core/error"; import { redirect } from "../core/redirect"; import type { FramesContext } from "../core/types"; +import { resolveBaseUrl } from "../core/utils"; import { renderResponse } from "./renderResponse"; jest.mock("@vercel/og", () => { @@ -39,18 +40,22 @@ describe("renderResponse middleware", () => { vercelOg as unknown as { constructorMock: jest.Mock } ).constructorMock; const render = renderResponse(); + const request =new Request("https://example.com"); const context: FramesContext = { basePath: "/", initialState: undefined, - request: new Request("https://example.com"), + request, url: new URL("https://example.com"), + resolvedBaseUrl: resolveBaseUrl(request, undefined, '/'), }; beforeEach(() => { arrayBufferMock.mockClear(); constructorMock.mockClear(); + context.basePath = "/"; context.request = new Request("https://example.com"); context.url = new URL("https://example.com"); + context.resolvedBaseUrl = resolveBaseUrl(context.request, undefined, "/"); }); it("returns redirect Response if redirect is returned from handler", async () => { @@ -158,8 +163,8 @@ describe("renderResponse middleware", () => { await expect((result as Response).text()).resolves.toMatchSnapshot(); }); - it("properly resolves against basePath", async () => { - const newContext = { ...context, basePath: "/prefixed" }; + it("properly resolves against resolvedBaseUrl", async () => { + const newContext = { ...context, resolveBaseUrl: new URL("https://example.com/prefixed") }; const result = await render(newContext, async () => { return { image:
My image
, @@ -501,7 +506,7 @@ describe("renderResponse middleware", () => { ); }); - it("properly renders button with targer object containing query", async () => { + it("properly renders button with target object containing query", async () => { const url = new URL("https://example.com"); url.searchParams.set("some_existing_param", "1"); @@ -512,7 +517,7 @@ describe("renderResponse middleware", () => { }, }); context.url = url; - context.basePath = "/test"; + context.resolvedBaseUrl = new URL("https://example.com/test"); const result = await render(context, async () => { return { image:
My image
, diff --git a/packages/frames.js/src/middleware/renderResponse.ts b/packages/frames.js/src/middleware/renderResponse.ts index 4af36fcc6..6e19ac4b1 100644 --- a/packages/frames.js/src/middleware/renderResponse.ts +++ b/packages/frames.js/src/middleware/renderResponse.ts @@ -126,9 +126,8 @@ export function renderResponse(): FramesMiddleware> { typeof result.image === "string" ? generateTargetURL({ target: result.image, - currentURL: context.url, - basePath: context.basePath, - }) + resolvedBaseUrl: context.resolvedBaseUrl, + }).toString() : await renderImage(result.image, result.imageOptions).catch( (e) => { // eslint-disable-next-line no-console -- provide feedback to the user @@ -160,9 +159,8 @@ export function renderResponse(): FramesMiddleware> { label: props.children, target: generateTargetURL({ target: props.target, - currentURL: context.url, - basePath: context.basePath, - }), + resolvedBaseUrl: context.resolvedBaseUrl, + }).toString(), }; case "mint": return { @@ -178,16 +176,14 @@ export function renderResponse(): FramesMiddleware> { buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: "post", target: props.target, - currentURL: context.url, - basePath: context.basePath, - }), + resolvedBaseUrl: context.resolvedBaseUrl, + }).toString(), post_url: props.post_url ? generatePostButtonTargetURL({ buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: "post", target: props.post_url, - currentURL: context.url, - basePath: context.basePath, + resolvedBaseUrl: context.resolvedBaseUrl, }) : undefined, }; @@ -200,8 +196,7 @@ export function renderResponse(): FramesMiddleware> { buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: props.action, target: props.target, - currentURL: context.url, - basePath: context.basePath, + resolvedBaseUrl: context.resolvedBaseUrl, }), }; default: From 5453668735d915ee4f80cebdc286888f2529d345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 15:49:51 +0200 Subject: [PATCH 09/10] chore: change description in docs --- docs/pages/reference/core/createFrames.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index bf22afe55..e46d62955 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -25,7 +25,7 @@ The function passed to `frames` will be called with the context of a frame actio - Type: `string` -A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. If `baseUrl` is provided, it will be resolved relatively to `baseUrl`. If the `baseUrl` option is not provided, it will overwrite the path of current request's URL. +A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. If `baseUrl` is provided, it will be resolved relatively to `baseUrl`. If the `baseUrl` option is not provided, it will use URL of current request and override its path with `basePath` when generating target URLs. ### `baseUrl` From cdc8a3337f51d57a1d3c8730df9971ab9f85e337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 5 Apr 2024 16:08:33 +0200 Subject: [PATCH 10/10] chore: rename resolvedBaseUrl to baseUrl --- docs/pages/reference/core/createFrames.mdx | 10 ++---- .../frames.js/src/core/createFrames.test.ts | 14 ++++---- packages/frames.js/src/core/createFrames.ts | 3 +- packages/frames.js/src/core/types.ts | 7 +--- packages/frames.js/src/core/utils.test.ts | 10 +++--- packages/frames.js/src/core/utils.ts | 32 +++++++++---------- .../renderResponse.test.tsx.snap | 2 +- .../src/middleware/framesjsMiddleware.test.ts | 14 ++++---- .../src/middleware/openframes.test.ts | 2 +- .../src/middleware/renderResponse.test.tsx | 10 +++--- .../src/middleware/renderResponse.ts | 10 +++--- 11 files changed, 50 insertions(+), 64 deletions(-) diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index e46d62955..e7e7e67ae 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -216,9 +216,9 @@ Specifies the base path for all relative URLs in the frame definition. ### `baseUrl` -- Type: `URL | undefined` +- Type: `URL` -Specifies the base URL for all relative URLs in the frame definition. `baseUrl` is resolved relatively to this value. +The resolved base URL for all relative URLs in the frame definition. All relative URLs are resolved relatively to this value. ### `initialState` @@ -232,12 +232,6 @@ A JSON serializable value that is used if no state is provided in the message or The request object that was passed to the request handler. -### `resolvedBaseUrl` - -- Type: `URL` - -The resolved base URL for all relative URLs in the frame definition. All relative URLs are resolved relatively to this value. - ### `url` - Type: [Web API `URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) diff --git a/packages/frames.js/src/core/createFrames.test.ts b/packages/frames.js/src/core/createFrames.test.ts index 45b07dced..c399b43fc 100644 --- a/packages/frames.js/src/core/createFrames.test.ts +++ b/packages/frames.js/src/core/createFrames.test.ts @@ -148,7 +148,7 @@ describe("createFrames", () => { const handler = createFrames({ baseUrl: "http://override.com" }); const routeHandler = handler((ctx) => { - expect(ctx.baseUrl?.toString()).toBe("http://override.com/"); + expect(ctx.baseUrl.toString()).toBe("http://override.com/"); return Response.json({ test: true }); }); @@ -161,7 +161,7 @@ describe("createFrames", () => { const handler = createFrames(); const routeHandler = handler((ctx) => { - expect(ctx.resolvedBaseUrl.toString()).toBe("http://test.com/"); + expect(ctx.baseUrl.toString()).toBe("http://test.com/"); return Response.json({ test: true }); }); @@ -174,7 +174,7 @@ describe("createFrames", () => { const handler = createFrames({ basePath: "/test" }); const routeHandler = handler((ctx) => { - expect(ctx.resolvedBaseUrl.toString()).toBe("http://test.com/test"); + expect(ctx.baseUrl.toString()).toBe("http://test.com/test"); return Response.json({ test: true }); }); @@ -187,7 +187,7 @@ describe("createFrames", () => { const handler = createFrames({ baseUrl: "http://override.com" }); const routeHandler = handler((ctx) => { - expect(ctx.resolvedBaseUrl.toString()).toBe("http://override.com/"); + expect(ctx.baseUrl.toString()).toBe("http://override.com/"); return Response.json({ test: true }); }); @@ -203,7 +203,7 @@ describe("createFrames", () => { }); const routeHandler = handler((ctx) => { - expect(ctx.resolvedBaseUrl.toString()).toBe("http://override.com/test"); + expect(ctx.baseUrl.toString()).toBe("http://override.com/test"); return Response.json({ test: true }); }); @@ -219,9 +219,7 @@ describe("createFrames", () => { }); const routeHandler = handler((ctx) => { - expect(ctx.resolvedBaseUrl.toString()).toBe( - "http://override.com/test/test2" - ); + expect(ctx.baseUrl.toString()).toBe("http://override.com/test/test2"); return Response.json({ test: true }); }); diff --git a/packages/frames.js/src/core/createFrames.ts b/packages/frames.js/src/core/createFrames.ts index f78f5118c..bd340f6ab 100644 --- a/packages/frames.js/src/core/createFrames.ts +++ b/packages/frames.js/src/core/createFrames.ts @@ -75,12 +75,11 @@ export function createFrames< */ return async function handleFramesRequest(request: Request) { const context: FramesContext = { - baseUrl: url, basePath, initialState: initialState as TState, request, url: new URL(request.url), - resolvedBaseUrl: resolveBaseUrl(request, url, basePath), + baseUrl: resolveBaseUrl(request, url, basePath), }; const result = await composedMiddleware(context); diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index e3ae50f9c..808346fda 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -41,15 +41,10 @@ export type FramesContext = { * All frame relative targets will be resolved relative to this */ basePath: string; - /** - * All frame relative targets will be resolved against this url if provided. - * basePath is always resolved relatively to this URL (if provided). - */ - baseUrl?: URL; /** * URL resolved based on current request URL, baseUrl and basePath. This URL is used to generate target URLs. */ - resolvedBaseUrl: URL; + baseUrl: URL; /** * Values passed to createFrames() */ diff --git a/packages/frames.js/src/core/utils.test.ts b/packages/frames.js/src/core/utils.test.ts index ee41cf154..f5442e12b 100644 --- a/packages/frames.js/src/core/utils.test.ts +++ b/packages/frames.js/src/core/utils.test.ts @@ -68,7 +68,7 @@ describe("generatePostButtonTargetURL", () => { target: undefined, buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("http://test.com"), + baseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); @@ -82,7 +82,7 @@ describe("generatePostButtonTargetURL", () => { generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("http://test.com"), + baseUrl: new URL("http://test.com"), target: { query: { test: "test" } }, }) ).toBe(expected.toString()); @@ -97,7 +97,7 @@ describe("generatePostButtonTargetURL", () => { target: "/test", buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("http://test.com"), + baseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); @@ -111,7 +111,7 @@ describe("generatePostButtonTargetURL", () => { generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("http://test.com"), + baseUrl: new URL("http://test.com"), target: { query: { test: "test" }, pathname: "/test" }, }) ).toBe(expected.toString()); @@ -126,7 +126,7 @@ describe("generatePostButtonTargetURL", () => { target: "/test", buttonAction: "post_redirect", buttonIndex: 1, - resolvedBaseUrl: new URL("http://test.com"), + baseUrl: new URL("http://test.com"), }) ).toBe(expected.toString()); }); diff --git a/packages/frames.js/src/core/utils.ts b/packages/frames.js/src/core/utils.ts index fbf156d63..70a460e52 100644 --- a/packages/frames.js/src/core/utils.ts +++ b/packages/frames.js/src/core/utils.ts @@ -50,31 +50,31 @@ function isValidButtonAction( } export function generateTargetURL({ - resolvedBaseUrl, + baseUrl, target, }: { - resolvedBaseUrl: URL; + baseUrl: URL; target: string | UrlObject | undefined; }): URL { if (!target) { - return new URL(resolvedBaseUrl); + return new URL(baseUrl); } if (typeof target === "object") { return new URL( formatUrl({ - host: resolvedBaseUrl.host, - hash: resolvedBaseUrl.hash, - hostname: resolvedBaseUrl.hostname, - href: resolvedBaseUrl.href, + host: baseUrl.host, + hash: baseUrl.hash, + hostname: baseUrl.hostname, + href: baseUrl.href, // pathname: url.pathname, - protocol: resolvedBaseUrl.protocol, + protocol: baseUrl.protocol, // we ignore existing search params and uses only new ones // search: url.search, - port: resolvedBaseUrl.port, + port: baseUrl.port, // query: url.searchParams, ...target, - pathname: joinPaths(resolvedBaseUrl.pathname, target.pathname ?? ""), + pathname: joinPaths(baseUrl.pathname, target.pathname ?? ""), }) ); } @@ -84,10 +84,10 @@ export function generateTargetURL({ return new URL(target); } catch { // resolve target relatively to basePath - const baseUrl = new URL(resolvedBaseUrl); - const finalPathname = joinPaths(baseUrl.pathname, target); + const url = new URL(baseUrl); + const finalPathname = joinPaths(url.pathname, target); - return new URL(finalPathname, baseUrl); + return new URL(finalPathname, url); } } @@ -99,14 +99,14 @@ export function generatePostButtonTargetURL({ buttonIndex, buttonAction, target, - resolvedBaseUrl, + baseUrl, }: { buttonIndex: 1 | 2 | 3 | 4; buttonAction: "post" | "post_redirect"; target: string | UrlObject | undefined; - resolvedBaseUrl: URL; + baseUrl: URL; }): string { - const url = new URL(generateTargetURL({ resolvedBaseUrl, target })); + const url = new URL(generateTargetURL({ baseUrl, target })); // Internal param, store what button has been clicked in the URL. url.searchParams.set( diff --git a/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap b/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap index 5f7e0fa29..06a96e583 100644 --- a/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap +++ b/packages/frames.js/src/middleware/__snapshots__/renderResponse.test.tsx.snap @@ -82,6 +82,6 @@ exports[`renderResponse middleware correctly renders image wich conditional cont } `; -exports[`renderResponse middleware properly resolves against resolvedBaseUrl 1`] = `""`; +exports[`renderResponse middleware properly resolves against baseUrl 1`] = `""`; exports[`renderResponse middleware renders text input 1`] = `""`; diff --git a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts index 2499da670..2f024f2a8 100644 --- a/packages/frames.js/src/middleware/framesjsMiddleware.test.ts +++ b/packages/frames.js/src/middleware/framesjsMiddleware.test.ts @@ -10,7 +10,7 @@ describe("framesjsMiddleware middleware", () => { const context = { url: new URL("https://example.com"), request, - resolvedBaseUrl: resolveBaseUrl(request, undefined, "/"), + baseUrl: resolveBaseUrl(request, undefined, "/"), } as unknown as FramesContext; const next = jest.fn(); const middleware = framesjsMiddleware(); @@ -27,7 +27,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -55,7 +55,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post_redirect", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: "/test", }); const context = { @@ -83,7 +83,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post_redirect", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: "/test", }); const context = { @@ -107,7 +107,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -132,7 +132,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, @@ -158,7 +158,7 @@ describe("framesjsMiddleware middleware", () => { const url = generatePostButtonTargetURL({ buttonAction: "post", buttonIndex: 1, - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), target: { pathname: "/test", query: { test: true }, diff --git a/packages/frames.js/src/middleware/openframes.test.ts b/packages/frames.js/src/middleware/openframes.test.ts index 316b97726..a3a98fa4d 100644 --- a/packages/frames.js/src/middleware/openframes.test.ts +++ b/packages/frames.js/src/middleware/openframes.test.ts @@ -114,7 +114,7 @@ describe("openframes middleware", () => { }), url: new URL("https://example.com").toString(), basePath: "/", - resolvedBaseUrl: new URL("https://example.com"), + baseUrl: new URL("https://example.com"), } as unknown as FramesContext; const mw1 = openframes({ diff --git a/packages/frames.js/src/middleware/renderResponse.test.tsx b/packages/frames.js/src/middleware/renderResponse.test.tsx index ff8fa4fb4..53ecb11a7 100644 --- a/packages/frames.js/src/middleware/renderResponse.test.tsx +++ b/packages/frames.js/src/middleware/renderResponse.test.tsx @@ -46,7 +46,7 @@ describe("renderResponse middleware", () => { initialState: undefined, request, url: new URL("https://example.com"), - resolvedBaseUrl: resolveBaseUrl(request, undefined, '/'), + baseUrl: resolveBaseUrl(request, undefined, '/'), }; beforeEach(() => { @@ -55,7 +55,7 @@ describe("renderResponse middleware", () => { context.basePath = "/"; context.request = new Request("https://example.com"); context.url = new URL("https://example.com"); - context.resolvedBaseUrl = resolveBaseUrl(context.request, undefined, "/"); + context.baseUrl = resolveBaseUrl(context.request, undefined, "/"); }); it("returns redirect Response if redirect is returned from handler", async () => { @@ -163,8 +163,8 @@ describe("renderResponse middleware", () => { await expect((result as Response).text()).resolves.toMatchSnapshot(); }); - it("properly resolves against resolvedBaseUrl", async () => { - const newContext = { ...context, resolveBaseUrl: new URL("https://example.com/prefixed") }; + it("properly resolves against baseUrl", async () => { + const newContext = { ...context, baseUrl: new URL("https://example.com/prefixed") }; const result = await render(newContext, async () => { return { image:
My image
, @@ -517,7 +517,7 @@ describe("renderResponse middleware", () => { }, }); context.url = url; - context.resolvedBaseUrl = new URL("https://example.com/test"); + context.baseUrl = new URL("https://example.com/test"); const result = await render(context, async () => { return { image:
My image
, diff --git a/packages/frames.js/src/middleware/renderResponse.ts b/packages/frames.js/src/middleware/renderResponse.ts index 6e19ac4b1..27e405c97 100644 --- a/packages/frames.js/src/middleware/renderResponse.ts +++ b/packages/frames.js/src/middleware/renderResponse.ts @@ -126,7 +126,7 @@ export function renderResponse(): FramesMiddleware> { typeof result.image === "string" ? generateTargetURL({ target: result.image, - resolvedBaseUrl: context.resolvedBaseUrl, + baseUrl: context.baseUrl, }).toString() : await renderImage(result.image, result.imageOptions).catch( (e) => { @@ -159,7 +159,7 @@ export function renderResponse(): FramesMiddleware> { label: props.children, target: generateTargetURL({ target: props.target, - resolvedBaseUrl: context.resolvedBaseUrl, + baseUrl: context.baseUrl, }).toString(), }; case "mint": @@ -176,14 +176,14 @@ export function renderResponse(): FramesMiddleware> { buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: "post", target: props.target, - resolvedBaseUrl: context.resolvedBaseUrl, + baseUrl: context.baseUrl, }).toString(), post_url: props.post_url ? generatePostButtonTargetURL({ buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: "post", target: props.post_url, - resolvedBaseUrl: context.resolvedBaseUrl, + baseUrl: context.baseUrl, }) : undefined, }; @@ -196,7 +196,7 @@ export function renderResponse(): FramesMiddleware> { buttonIndex: (i + 1) as 1 | 2 | 3 | 4, buttonAction: props.action, target: props.target, - resolvedBaseUrl: context.resolvedBaseUrl, + baseUrl: context.baseUrl, }), }; default: