Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Feb 14, 2024
1 parent fcaa0f6 commit 2b6d3c4
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 45 deletions.
2 changes: 1 addition & 1 deletion examples/_dev/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Button, Farc, TextInput } from 'farc'
import { app as todoApp } from './todos'

const app = new Farc({
// basePath: '/api'
// basePath: '/api',
})

app.frame('/', (context) => {
Expand Down
36 changes: 15 additions & 21 deletions src/farc-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from './types.js'
import { deserializeJson } from './utils/deserializeJson.js'
import { getFrameContext } from './utils/getFrameContext.js'
import { getIntentData } from './utils/getIntentData.js'
import { parseIntents } from './utils/parseIntents.js'
import { parsePath } from './utils/parsePath.js'
import { requestToContext } from './utils/requestToContext.js'
Expand Down Expand Up @@ -44,6 +45,7 @@ export class FarcBase<
> {
#initialState: state = undefined as state

basePath: string
hono: Hono<env, schema, basePath>
fetch: Hono<env, schema, basePath>['fetch']
get: Hono<env, schema, basePath>['get']
Expand All @@ -56,6 +58,7 @@ export class FarcBase<
}: FarcConstructorParameters<state, env, basePath> = {}) {
this.hono = new Hono<env, schema, basePath>(honoOptions)
if (basePath) this.hono = this.hono.basePath(basePath)
this.basePath = basePath ?? '/'
this.fetch = this.hono.fetch.bind(this.hono)
this.get = this.hono.get.bind(this.hono)
this.post = this.hono.post.bind(this.hono)
Expand All @@ -67,20 +70,15 @@ export class FarcBase<
path: path,
handler: (
context: FrameContext<path, state>,
previousContext: PreviousFrameContext<path, state> | undefined,
) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>,
) {
// Frame Route (implements GET & POST).
this.hono.use(parsePath(path), async (c) => {
const query = c.req.query()

const url = new URL(c.req.url)
const baseUrl = `${url.origin}${url.pathname}`

const previousContext = query.previousContext
? deserializeJson<PreviousFrameContext<path, state>>(
query.previousContext,
)
? deserializeJson<PreviousFrameContext<state>>(query.previousContext)
: undefined
const context = await getFrameContext({
context: await requestToContext(c.req),
Expand All @@ -89,21 +87,15 @@ export class FarcBase<
request: c.req,
})

if (context.url !== parsePath(c.req.url))
return c.redirect(
`${context.url}?previousContext=${query.previousContext}`,
)
if (context.url !== parsePath(c.req.url)) return c.redirect(context.url)

const { action, imageAspectRatio, intents } = await handler(
context,
previousContext,
)
const { action, imageAspectRatio, intents } = await handler(context)
const parsedIntents = intents ? parseIntents(intents) : null
const intentData = getIntentData(parsedIntents)

const serializedContext = serializeJson(context)
const serializedPreviousContext = serializeJson({
...context,
intents: parsedIntents,
intentData,
previousState: context.deriveState(),
})

Expand Down Expand Up @@ -135,7 +127,11 @@ export class FarcBase<
<meta
property="fc:frame:post_url"
content={`${
action ? baseUrl + parsePath(action || '') : context.url
action
? url.origin +
parsePath(this.basePath) +
parsePath(action || '')
: context.url
}?${postSearch}`}
/>
{parsedIntents}
Expand Down Expand Up @@ -169,17 +165,15 @@ export class FarcBase<
this.hono.get(`${parsePath(path)}/image`, async (c) => {
const query = c.req.query()
const previousContext = query.previousContext
? deserializeJson<PreviousFrameContext<path, state>>(
query.previousContext,
)
? deserializeJson<PreviousFrameContext<state>>(query.previousContext)
: undefined
const context = await getFrameContext({
context: deserializeJson<FrameContext<path, state>>(query.context),
initialState: this.#initialState,
previousContext,
request: c.req,
})
const { image } = await handler(context, previousContext)
const { image } = await handler(context)
return new ImageResponse(image)
})
}
Expand Down
3 changes: 1 addition & 2 deletions src/farc.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Env, Schema } from 'hono'
import { routes as devRoutes } from './dev/routes.js'
import { FarcBase, type FrameHandlerReturnType } from './farc-base.js'
import type { FrameContext, PreviousFrameContext } from './types.js'
import type { FrameContext } from './types.js'

export class Farc<
state = undefined,
Expand All @@ -13,7 +13,6 @@ export class Farc<
path: path,
handler: (
context: FrameContext<path, state>,
previousContext: PreviousFrameContext<path, state> | undefined,
) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>,
) {
super.frame(path, handler)
Expand Down
13 changes: 6 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// TODO: TSDoc

import { type Context, type Env } from 'hono'
import { type JSXNode } from 'hono/jsx'

export type FrameContext<path extends string = string, state = unknown> = {
buttonIndex?: number | undefined
Expand All @@ -22,12 +21,10 @@ export type FrameContext<path extends string = string, state = unknown> = {
url: Context['req']['url']
}

export type PreviousFrameContext<
path extends string = string,
state = unknown,
> = FrameContext<path, state> & {
/** Intents from the previous frame. */
intents: readonly JSXNode[]
export type PreviousFrameContext<state = unknown> = {
/** Intent data from the previous frame. */
intentData: readonly Record<string, string>[]
previousState: state
}

export type FrameData = {
Expand All @@ -48,6 +45,8 @@ export type FrameVersion = 'vNext'
export type FrameIntent = JSX.Element | false | null | undefined
export type FrameIntents = FrameIntent | FrameIntent[]

export type FrameIntentData = Record<string, string>

export type TrustedData = {
messageBytes: string
}
Expand Down
8 changes: 4 additions & 4 deletions src/utils/getFrameContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { parsePath } from './parsePath.js'
type GetFrameContextParameters<state = unknown> = {
context: Pick<
FrameContext<string, state>,
'status' | 'trustedData' | 'untrustedData' | 'url'
'initialUrl' | 'status' | 'trustedData' | 'untrustedData' | 'url'
>
initialState?: state
previousContext: PreviousFrameContext<string, state> | undefined
previousContext: PreviousFrameContext<state> | undefined
request: Context['req']
}

Expand All @@ -23,7 +23,7 @@ export async function getFrameContext<state>(
const { buttonIndex, buttonValue, inputText, reset } = getIntentState(
// TODO: derive from untrusted data.
untrustedData,
previousContext?.intents || [],
previousContext?.intentData || [],
)

const status = (() => {
Expand All @@ -34,7 +34,7 @@ export async function getFrameContext<state>(
// If there are no previous contexts, the initial URL is the current URL.
const initialUrl = !previousContext
? parsePath(context.url)
: previousContext.initialUrl
: context.initialUrl

// If the user has clicked a reset button, we want to set the URL back to the
// initial URL.
Expand Down
25 changes: 25 additions & 0 deletions src/utils/getIntentData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { JSXNode } from 'hono/jsx'
import type { FrameIntentData } from '../types.js'

export function getIntentData(
intents: readonly JSXNode[] | null,
): FrameIntentData[] {
if (!intents) return []

const intentData: FrameIntentData[] = []
for (const intent of intents) {
if (!intent) continue
const { property } = intent.props
const data: FrameIntentData = {}
for (const [key, value] of Object.entries(intent.props)) {
if (!key.startsWith('data-')) continue
data[key] = value
}
if (Object.keys(data).length === 0) continue
intentData.push({
property,
...data,
})
}
return intentData
}
16 changes: 7 additions & 9 deletions src/utils/getIntentState.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { type JSXNode } from 'hono/jsx'

import { type FrameData } from '../types.js'
import { type FrameData, type FrameIntentData } from '../types.js'

export function getIntentState(
frameData: FrameData | undefined,
intents: readonly JSXNode[] | null,
intentData: readonly FrameIntentData[] | null,
) {
const { buttonIndex, inputText } = frameData || {}
const state = { buttonIndex, buttonValue: undefined, inputText, reset: false }
if (!intents) return state
if (!intentData) return state
if (buttonIndex) {
const buttonIntents = intents.filter((intent) =>
intent?.props?.property?.match(/fc:frame:button:\d$/),
const buttonIntents = intentData.filter((intent) =>
intent?.property?.match(/fc:frame:button:\d$/),
)
const intent = buttonIntents[buttonIndex - 1]
state.buttonValue = intent.props['data-value']
if (intent.props['data-type'] === 'reset') state.reset = true
state.buttonValue = intent['data-value'] as any
if (intent['data-type'] === 'reset') state.reset = true
}
return state
}
3 changes: 2 additions & 1 deletion src/utils/requestToContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FrameContext } from '../types.js'

type RequestToContextReturnType = Pick<
FrameContext,
'status' | 'trustedData' | 'untrustedData' | 'url'
'initialUrl' | 'status' | 'trustedData' | 'untrustedData' | 'url'
>

export async function requestToContext(
Expand All @@ -13,6 +13,7 @@ export async function requestToContext(
(await request.json().catch(() => {})) || {}

return {
initialUrl: request.url,
status: request.method === 'POST' ? 'response' : 'initial',
trustedData,
untrustedData,
Expand Down

0 comments on commit 2b6d3c4

Please sign in to comment.