Comparing version 0.0.1-main.20240210T034054 to 0.0.1-main.20240211T223529
import { Hono } from 'hono'; | ||
import type { HonoOptions } from 'hono/hono-base'; | ||
import { type Env, type Schema } from 'hono/types'; | ||
import { type FrameContext, type FrameImageAspectRatio, type FrameIntents, type PreviousFrameContext } from './types.js'; | ||
export type FarcConstructorParameters<state = undefined, env extends Env = Env> = HonoOptions<env> & { | ||
initialState?: state | undefined; | ||
}; | ||
export type FrameHandlerReturnType = { | ||
@@ -10,5 +14,7 @@ action?: string | undefined; | ||
}; | ||
export declare class Farc<env extends Env = Env, schema extends Schema = {}, basePath extends string = '/'> extends Hono<env, schema, basePath> { | ||
frame<path extends string>(path: path, handler: (context: FrameContext<path>, previousContext?: PreviousFrameContext | undefined) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>): void; | ||
export declare class Farc<state = undefined, env extends Env = Env, schema extends Schema = {}, basePath extends string = '/'> extends Hono<env, schema, basePath> { | ||
#private; | ||
constructor({ initialState }?: FarcConstructorParameters<state, env>); | ||
frame<path extends string>(path: path, handler: (context: FrameContext<path, state>, previousContext: PreviousFrameContext<path, state> | undefined) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>): void; | ||
} | ||
//# sourceMappingURL=farc.d.ts.map |
@@ -0,1 +1,13 @@ | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var _Farc_initialState; | ||
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; | ||
@@ -17,5 +29,12 @@ import { FrameActionBody, Message, NobleEd25519Signer, makeFrameAction, } from '@farcaster/core'; | ||
import { parsePath } from './utils/parsePath.js'; | ||
import { requestToContext } from './utils/requestToContext.js'; | ||
import { serializeJson } from './utils/serializeJson.js'; | ||
import { toBaseUrl } from './utils/toBaseUrl.js'; | ||
export class Farc extends Hono { | ||
constructor({ initialState } = {}) { | ||
super(); | ||
_Farc_initialState.set(this, undefined); | ||
if (initialState) | ||
__classPrivateFieldSet(this, _Farc_initialState, initialState, "f"); | ||
} | ||
frame(path, handler) { | ||
@@ -28,3 +47,8 @@ // Frame Route (implements GET & POST). | ||
: undefined; | ||
const context = await getFrameContext(c, previousContext); | ||
const context = await getFrameContext({ | ||
context: await requestToContext(c.req), | ||
initialState: __classPrivateFieldGet(this, _Farc_initialState, "f"), | ||
previousContext, | ||
request: c.req, | ||
}); | ||
if (context.url !== parsePath(c.req.url)) | ||
@@ -38,2 +62,3 @@ return c.redirect(`${context.url}?previousContext=${query.previousContext}`); | ||
intents: parsedIntents, | ||
previousState: context.deriveState(), | ||
}); | ||
@@ -48,3 +73,5 @@ const ogSearch = new URLSearchParams(); | ||
postSearch.set('previousContext', serializedPreviousContext); | ||
return c.render(_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { property: "fc:frame", content: "vNext" }), _jsx("meta", { property: "fc:frame:image", content: `${parsePath(context.url)}/image?${ogSearch.toString()}` }), _jsx("meta", { property: "fc:frame:image:aspect_ratio", content: imageAspectRatio ?? '1.91:1' }), _jsx("meta", { property: "og:image", content: `${parsePath(context.url)}/image?${ogSearch.toString()}` }), _jsx("meta", { property: "fc:frame:post_url", content: `${action ? toBaseUrl(c.req.url) + (action || '') : context.url}?${postSearch}` }), parsedIntents, _jsx("meta", { property: "farc:context", content: serializedContext }), query.previousContext && (_jsx("meta", { property: "farc:prev_context", content: query.previousContext }))] }), _jsx("body", { style: { | ||
return c.render(_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { property: "fc:frame", content: "vNext" }), _jsx("meta", { property: "fc:frame:image", content: `${parsePath(context.url)}/image?${ogSearch.toString()}` }), _jsx("meta", { property: "fc:frame:image:aspect_ratio", content: imageAspectRatio ?? '1.91:1' }), _jsx("meta", { property: "og:image", content: `${parsePath(context.url)}/image?${ogSearch.toString()}` }), _jsx("meta", { property: "fc:frame:post_url", content: `${action | ||
? toBaseUrl(c.req.url) + parsePath(action || '') | ||
: context.url}?${postSearch}` }), parsedIntents, _jsx("meta", { property: "farc:context", content: serializedContext }), query.previousContext && (_jsx("meta", { property: "farc:prev_context", content: query.previousContext }))] }), _jsx("body", { style: { | ||
alignItems: 'center', | ||
@@ -60,7 +87,12 @@ display: 'flex', | ||
const query = c.req.query(); | ||
const parsedContext = deserializeJson(query.context); | ||
const parsedPreviousContext = query.previousContext | ||
const previousContext = query.previousContext | ||
? deserializeJson(query.previousContext) | ||
: undefined; | ||
const { image } = await handler({ ...parsedContext, request: c.req }, parsedPreviousContext); | ||
const context = await getFrameContext({ | ||
context: deserializeJson(query.context), | ||
initialState: __classPrivateFieldGet(this, _Farc_initialState, "f"), | ||
previousContext, | ||
request: c.req, | ||
}); | ||
const { image } = await handler(context, previousContext); | ||
return new ImageResponse(image); | ||
@@ -180,2 +212,3 @@ }); | ||
} | ||
_Farc_initialState = new WeakMap(); | ||
//# sourceMappingURL=farc.js.map |
import { type Context, type Env } from 'hono'; | ||
import { type JSXNode } from 'hono/jsx'; | ||
export type FrameContext<path extends string = string> = { | ||
export type FrameContext<path extends string = string, state = unknown> = { | ||
buttonIndex?: number | undefined; | ||
buttonValue?: string | undefined; | ||
deriveState: (fn?: (state: state) => void) => state; | ||
initialUrl: string; | ||
inputText?: string | undefined; | ||
previousState: state; | ||
request: Context<Env, path>['req']; | ||
@@ -19,3 +21,3 @@ /** | ||
}; | ||
export type PreviousFrameContext = FrameContext & { | ||
export type PreviousFrameContext<path extends string = string, state = unknown> = FrameContext<path, state> & { | ||
/** Intents from the previous frame. */ | ||
@@ -22,0 +24,0 @@ intents: readonly JSXNode[]; |
@@ -1,4 +0,11 @@ | ||
import { type Context } from 'hono'; | ||
import type { Context } from 'hono'; | ||
import { type FrameContext, type PreviousFrameContext } from '../types.js'; | ||
export declare function getFrameContext(ctx: Context, previousFrameContext: PreviousFrameContext | undefined): Promise<FrameContext>; | ||
type GetFrameContextParameters<state = unknown> = { | ||
context: Pick<FrameContext<string, state>, 'status' | 'trustedData' | 'untrustedData' | 'url'>; | ||
initialState?: state; | ||
previousContext: PreviousFrameContext<string, state> | undefined; | ||
request: Context['req']; | ||
}; | ||
export declare function getFrameContext<state>(options: GetFrameContextParameters<state>): Promise<FrameContext<string, state>>; | ||
export {}; | ||
//# sourceMappingURL=getFrameContext.d.ts.map |
@@ -1,25 +0,29 @@ | ||
import {} from 'hono'; | ||
import { produce } from 'immer'; | ||
import {} from '../types.js'; | ||
import { getIntentState } from './getIntentState.js'; | ||
import { parsePath } from './parsePath.js'; | ||
export async function getFrameContext(ctx, previousFrameContext) { | ||
const { req } = ctx; | ||
const { trustedData, untrustedData } = (await req.json().catch(() => { })) || {}; | ||
export async function getFrameContext(options) { | ||
const { context, previousContext, request } = options; | ||
const { trustedData, untrustedData } = context || {}; | ||
const { buttonIndex, buttonValue, inputText, reset } = getIntentState( | ||
// TODO: derive from untrusted data. | ||
untrustedData, previousFrameContext?.intents || []); | ||
untrustedData, previousContext?.intents || []); | ||
const status = (() => { | ||
if (reset) | ||
return 'initial'; | ||
if (req.method === 'POST') | ||
return 'response'; | ||
return 'initial'; | ||
return context.status || 'initial'; | ||
})(); | ||
// If there are no previous contexts, the initial URL is the current URL. | ||
const initialUrl = !previousFrameContext | ||
? parsePath(req.url) | ||
: previousFrameContext.initialUrl; | ||
const initialUrl = !previousContext | ||
? parsePath(context.url) | ||
: previousContext.initialUrl; | ||
// If the user has clicked a reset button, we want to set the URL back to the | ||
// initial URL. | ||
const url = reset ? initialUrl : parsePath(req.url); | ||
const url = reset ? initialUrl : parsePath(context.url); | ||
let previousState = previousContext?.previousState || options.initialState; | ||
function deriveState(derive) { | ||
if (status === 'response' && derive) | ||
previousState = produce(previousState, derive); | ||
return previousState; | ||
} | ||
return { | ||
@@ -30,3 +34,5 @@ buttonIndex, | ||
inputText, | ||
request: req, | ||
deriveState, | ||
previousState: previousState, | ||
request, | ||
status, | ||
@@ -33,0 +39,0 @@ trustedData, |
{ | ||
"name": "farc", | ||
"version": "0.0.1-main.20240210T034054", | ||
"version": "0.0.1-main.20240211T223529", | ||
"type": "module", | ||
@@ -20,2 +20,3 @@ "module": "_lib/index.js", | ||
"hono-og": "~0.0.2", | ||
"immer": "^10.0.3", | ||
"lz-string": "^1.5.0", | ||
@@ -22,0 +23,0 @@ "shiki": "^1.0.0" |
@@ -6,7 +6,9 @@ // TODO: TSDoc | ||
export type FrameContext<path extends string = string> = { | ||
export type FrameContext<path extends string = string, state = unknown> = { | ||
buttonIndex?: number | undefined | ||
buttonValue?: string | undefined | ||
deriveState: (fn?: (state: state) => void) => state | ||
initialUrl: string | ||
inputText?: string | undefined | ||
previousState: state | ||
request: Context<Env, path>['req'] | ||
@@ -24,3 +26,6 @@ /** | ||
export type PreviousFrameContext = FrameContext & { | ||
export type PreviousFrameContext< | ||
path extends string = string, | ||
state = unknown, | ||
> = FrameContext<path, state> & { | ||
/** Intents from the previous frame. */ | ||
@@ -27,0 +32,0 @@ intents: readonly JSXNode[] |
@@ -1,3 +0,3 @@ | ||
import { type Context } from 'hono' | ||
import type { Context } from 'hono' | ||
import { produce } from 'immer' | ||
import { type FrameContext, type PreviousFrameContext } from '../types.js' | ||
@@ -7,14 +7,22 @@ import { getIntentState } from './getIntentState.js' | ||
export async function getFrameContext( | ||
ctx: Context, | ||
previousFrameContext: PreviousFrameContext | undefined, | ||
): Promise<FrameContext> { | ||
const { req } = ctx | ||
const { trustedData, untrustedData } = | ||
(await req.json().catch(() => {})) || {} | ||
type GetFrameContextParameters<state = unknown> = { | ||
context: Pick< | ||
FrameContext<string, state>, | ||
'status' | 'trustedData' | 'untrustedData' | 'url' | ||
> | ||
initialState?: state | ||
previousContext: PreviousFrameContext<string, state> | undefined | ||
request: Context['req'] | ||
} | ||
export async function getFrameContext<state>( | ||
options: GetFrameContextParameters<state>, | ||
): Promise<FrameContext<string, state>> { | ||
const { context, previousContext, request } = options | ||
const { trustedData, untrustedData } = context || {} | ||
const { buttonIndex, buttonValue, inputText, reset } = getIntentState( | ||
// TODO: derive from untrusted data. | ||
untrustedData, | ||
previousFrameContext?.intents || [], | ||
previousContext?.intents || [], | ||
) | ||
@@ -24,15 +32,21 @@ | ||
if (reset) return 'initial' | ||
if (req.method === 'POST') return 'response' | ||
return 'initial' | ||
return context.status || 'initial' | ||
})() | ||
// If there are no previous contexts, the initial URL is the current URL. | ||
const initialUrl = !previousFrameContext | ||
? parsePath(req.url) | ||
: previousFrameContext.initialUrl | ||
const initialUrl = !previousContext | ||
? parsePath(context.url) | ||
: previousContext.initialUrl | ||
// If the user has clicked a reset button, we want to set the URL back to the | ||
// initial URL. | ||
const url = reset ? initialUrl : parsePath(req.url) | ||
const url = reset ? initialUrl : parsePath(context.url) | ||
let previousState = previousContext?.previousState || options.initialState | ||
function deriveState(derive?: (state: state) => void): state { | ||
if (status === 'response' && derive) | ||
previousState = produce(previousState, derive) | ||
return previousState as state | ||
} | ||
return { | ||
@@ -43,3 +57,5 @@ buttonIndex, | ||
inputText, | ||
request: req, | ||
deriveState, | ||
previousState: previousState as any, | ||
request, | ||
status, | ||
@@ -46,0 +62,0 @@ trustedData, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
305679
84
2636
8
+ Addedimmer@^10.0.3
+ Addedimmer@10.1.1(transitive)