Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add baseUrl option #294

Merged
merged 10 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/violet-adults-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"frames.js": patch
---

feat: add baseURL option
24 changes: 18 additions & 6 deletions docs/pages/reference/core/createFrames.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +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 use URL of current request and override its path with `basePath` when generating target URLs.

### `baseUrl`

- Type: `string | URL`

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`

Expand All @@ -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<any, { foo?: string }> = async (
ctx,
next
) => {
return next({ foo: "bar" });
};
```
Expand Down Expand Up @@ -161,7 +167,7 @@ const handleRequest = frames(async (ctx) => {
aspectRatio: "1:1",
},
buttons: [<Button action="post">Refresh</Button>],
headers: {// [!code focus]
headers: { // [!code focus]
// Max cache age in seconds // [!code focus]
"Cache-Control": "max-age=0", // [!code focus]
}, // [!code focus]
Expand Down Expand Up @@ -208,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`

The resolved base URL for all relative URLs in the frame definition. All relative URLs are resolved relatively to this value.

### `initialState`

- Type: generic
Expand Down
90 changes: 90 additions & 0 deletions packages/frames.js/src/core/createFrames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
});

it("supports custom global middleware", async () => {
const customMiddleware: FramesMiddleware<any, { custom: string }> = async (

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
ctx,
next
) => {
Expand All @@ -65,7 +65,7 @@
});

it("supports per route middleware", async () => {
const customMiddleware: FramesMiddleware<any, { custom: string }> = async (

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
ctx,
next
) => {
Expand All @@ -90,13 +90,13 @@
});

it("works with parallel middleware", async () => {
const middleware0: FramesMiddleware<any, { test0: boolean }> = async (

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
context,
next
) => {
return next({ test0: true });
};
const middleware1: FramesMiddleware<any, { test1: boolean }> = async (

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

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
context,
next
) => {
Expand Down Expand Up @@ -137,4 +137,94 @@

expect(response).toBeInstanceOf(Response);
});

michalkvasnicak marked this conversation as resolved.
Show resolved Hide resolved
it("fails if invalid URL is set as baseUrl", () => {
expect(() => createFrames({ baseUrl: "invalid" })).toThrow(
"Invalid baseUrl: Invalid URL"
);
});

it("sets baseUrl on context if provided", async () => {
const handler = createFrames({ baseUrl: "http://override.com" });

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.toString()).toBe("http://override.com/");
return Response.json({ test: true });
});

await expect(
routeHandler(new Request("http://test.com"))
).resolves.toHaveProperty("status", 200);
});

it("resolves resolvedUrl against request URL and / if no basePath or baseUrl are provided", async () => {
const handler = createFrames();

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.toString()).toBe("http://test.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 request URL when only basePath is provided", async () => {
const handler = createFrames({ basePath: "/test" });

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.toString()).toBe("http://test.com/test");
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 / when only baseUrl is provided", async () => {
const handler = createFrames({ baseUrl: "http://override.com" });

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.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: "http://override.com",
basePath: "/test",
});

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.toString()).toBe("http://override.com/test");
return Response.json({ test: true });
});

await expect(
routeHandler(new Request("http://test.com/this-will-be-removed"))
).resolves.toHaveProperty("status", 200);
});

it("resolves basePath relatively to baseUrl", async () => {
const handler = createFrames({
baseUrl: "http://override.com/test",
basePath: "/test2",
});

const routeHandler = handler((ctx) => {
expect(ctx.baseUrl.toString()).toBe("http://override.com/test/test2");
return Response.json({ test: true });
});

await expect(
routeHandler(new Request("http://test.com/this-will-be-removed"))
).resolves.toHaveProperty("status", 200);
});
});
15 changes: 15 additions & 0 deletions packages/frames.js/src/core/createFrames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
FramesRequestHandlerFunction,
JsonValue,
} from "./types";
import { resolveBaseUrl } from "./utils";

export function createFrames<
TState extends JsonValue | undefined = JsonValue | undefined,
Expand All @@ -17,6 +18,7 @@ export function createFrames<
basePath = "/",
initialState,
middleware,
baseUrl,
}: FramesOptions<TState, TMiddlewares> = {}): FramesRequestHandlerFunction<
TState,
typeof coreMiddleware,
Expand All @@ -25,6 +27,18 @@ export function createFrames<
> {
const globalMiddleware: FramesMiddleware<TState, FramesContext<TState>>[] =
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
Expand Down Expand Up @@ -65,6 +79,7 @@ export function createFrames<
initialState: initialState as TState,
request,
url: new URL(request.url),
baseUrl: resolveBaseUrl(request, url, basePath),
};

const result = await composedMiddleware(context);
Expand Down
61 changes: 60 additions & 1 deletion packages/frames.js/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export type FramesContext<TState extends JsonValue | undefined = JsonValue> = {
* All frame relative targets will be resolved relative to this
*/
basePath: string;
/**
* URL resolved based on current request URL, baseUrl and basePath. This URL is used to generate target URLs.
*/
baseUrl: URL;
/**
* Values passed to createFrames()
*/
Expand Down Expand Up @@ -192,10 +196,65 @@ export type FramesOptions<
TFrameMiddleware extends FramesMiddleware<any, any>[] | 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 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.
*
* @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.
*
Expand Down
Loading
Loading