Last active
April 18, 2024 17:55
-
-
Save dalechyn/b7caa24dc55e0b7ede945f32030ed192 to your computer and use it in GitHub Desktop.
frog-base.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Hono } from 'hono'; | |
import type { HonoOptions } from 'hono/hono-base'; | |
import type { Schema } from 'hono/types'; | |
import type { Env } from './types/env.js'; | |
import type { FrameImageAspectRatio, FrameResponse, ImageOptions } from './types/frame.js'; | |
import type { Hub } from './types/hub.js'; | |
import type { HandlerInterface, MiddlewareHandlerInterface } from './types/routes.js'; | |
import type { Vars } from './ui/vars.js'; | |
export type FrogConstructorParameters<env extends Env = Env, basePath extends string = '/', _state = env['State']> = Pick<FrameResponse, 'browserLocation'> & { | |
/** | |
* The base path for assets. | |
* | |
* @example '/' (commonly for Vercel Serverless Functions) | |
*/ | |
assetsPath?: basePath | string | undefined; | |
/** | |
* The base path for the server instance. | |
* | |
* @example '/api' (commonly for Vercel Serverless Functions) | |
*/ | |
basePath?: basePath | string | undefined; | |
/** | |
* @deprecated Use `devtools` from `'frog/dev'` instead. | |
* | |
* Options for built-in devtools. | |
*/ | |
dev?: { | |
/** @deprecated */ | |
enabled?: boolean | undefined; | |
/** @deprecated */ | |
appFid?: number | undefined; | |
/** @deprecated */ | |
appMnemonic?: string | undefined; | |
} | undefined; | |
/** | |
* HTTP response headers. | |
*/ | |
headers?: Record<string, string> | undefined; | |
/** | |
* Options to forward to the `Hono` instance. | |
*/ | |
honoOptions?: HonoOptions<env> | undefined; | |
/** | |
* @deprecated Use `hub` instead. | |
* | |
* Farcaster Hub API URL. | |
*/ | |
hubApiUrl?: string | undefined; | |
/** | |
* Farcaster Hub API configuration. | |
*/ | |
hub?: Hub | undefined; | |
/** | |
* Default image options. | |
* | |
* @see https://vercel.com/docs/functions/og-image-generation/og-image-api | |
* @see https://vercel.com/docs/functions/og-image-generation/og-image-examples#using-a-custom-font | |
* | |
* @example | |
* { width: 1200, height: 630 } | |
* | |
* @example | |
* async () => { | |
* const fontData = await fetch( | |
* new URL('./assets/inter.ttf', import.meta.url), | |
* ).then((res) => res.arrayBuffer()); | |
* | |
* return { fonts: [{ name: 'Inter', data: fontData, style: 'normal'}] } | |
* } | |
*/ | |
imageOptions?: ImageOptions | (() => Promise<ImageOptions>) | undefined; | |
/** | |
* Default image aspect ratio. | |
* | |
* @default '1.91:1' | |
*/ | |
imageAspectRatio?: FrameImageAspectRatio | undefined; | |
/** | |
* Wether to enable masking of intial frame image or not. | |
* If wrangler is used for deployments, this should be state to `false` as wrangler doesn't support fetching within a worker. | |
* @see https://community.cloudflare.com/t/get-error-code-1042-when-fetching-within-worker/288031/2 | |
* | |
* @default true | |
*/ | |
initialImageMasking?: boolean | undefined; | |
/** | |
* Initial state for the frames. | |
* | |
* @example | |
* ```ts | |
* initialState: { | |
* index: 0, | |
* todos: [], | |
* } | |
* ``` | |
*/ | |
initialState?: _state | undefined; | |
/** | |
* Origin URL of the server instance. | |
* | |
* @link https://developer.mozilla.org/en-US/docs/Web/API/URL/origin | |
*/ | |
origin?: string | undefined; | |
/** | |
* Key used to sign secret data. | |
* | |
* It is used for: | |
* - Signing frame state, and | |
* - Signing auth session cookies in the devtools. | |
* | |
* It's strongly recommended to add a strong secret key before deploying to production. | |
* | |
* @example | |
* '1zN3Uvl5QQd7OprLfp83IU96W6ip6KNPQ+l0MECPIZh8FBLYKQ+mPXE1CTxfwXGz' | |
*/ | |
secret?: string | undefined; | |
/** | |
* FrogUI configuration. | |
*/ | |
ui?: { | |
vars: Vars | undefined; | |
} | undefined; | |
/** | |
* Whether or not to verify frame data via the Farcaster Hub's `validateMessage` API. | |
* | |
* - When `true`, the frame will go through verification and throw an error if it fails. | |
* - When `"silent"`, the frame will go through verification, but not throw an error if it fails. | |
* Instead, the frame will receive `verified: false` in its context. | |
* - When `false`, the frame will not go through verification. | |
* | |
* @default true. | |
*/ | |
verify?: boolean | 'silent' | undefined; | |
}; | |
export type RouteOptions = Pick<FrogConstructorParameters, 'verify'> & { | |
fonts?: ImageOptions['fonts'] | (() => Promise<ImageOptions['fonts']>); | |
}; | |
/** | |
* A Frog instance. | |
* | |
* @param parameters - {@link FrogConstructorParameters} | |
* @returns instance. {@link Frog} | |
* | |
* @example | |
* ``` | |
* import { Frog } from 'frog' | |
* | |
* const app = new Frog() | |
* | |
* app.frame('/', (c) => { | |
* const { buttonValue, inputText, status } = c | |
* const fruit = inputText || buttonValue | |
* return c.res({ | |
* image: ( | |
* <div style={{ fontSize: 60 }}> | |
* {fruit ? `You selected: ${fruit}` : 'Welcome!'} | |
* </div> | |
* ), | |
* intents: [ | |
* <Button value="apples">Apples</Button>, | |
* <Button value="oranges">Oranges</Button>, | |
* <Button value="bananas">Bananas</Button>, | |
* ] | |
* }) | |
* }) | |
* ``` | |
*/ | |
export declare class FrogBase<env extends Env = Env, schema extends Schema = {}, basePath extends string = '/', _state = env['State']> { | |
_initialState: env['State']; | |
/** Path for assets. */ | |
assetsPath: string; | |
/** Base path of the server instance. */ | |
basePath: string; | |
/** URL to redirect to when the user is coming to the page via a browser. */ | |
browserLocation: string | undefined; | |
dev: FrogConstructorParameters['dev'] | undefined; | |
headers: FrogConstructorParameters['headers'] | undefined; | |
/** Hono instance. */ | |
hono: Hono<env, schema, basePath>; | |
/** Farcaster Hub API URL. */ | |
hubApiUrl: string | undefined; | |
/** Farcaster Hub API config. */ | |
hub: Hub | undefined; | |
/** Initial frame image masking. */ | |
initialImageMasking?: boolean | undefined; | |
/** Image aspect ratio. */ | |
imageAspectRatio: FrameImageAspectRatio; | |
/** Image options. */ | |
imageOptions: ImageOptions | (() => Promise<ImageOptions>) | undefined; | |
/** Origin URL of the server instance. */ | |
origin: string | undefined; | |
fetch: Hono<env, schema, basePath>['fetch']; | |
get: Hono<env, schema, basePath>['get']; | |
post: Hono<env, schema, basePath>['post']; | |
/** Key used to sign secret data. */ | |
secret: FrogConstructorParameters['secret'] | undefined; | |
/** FrogUI configuration. */ | |
ui: { | |
vars: Vars | undefined; | |
} | undefined; | |
/** Whether or not frames should be verified. */ | |
verify: FrogConstructorParameters['verify']; | |
_dev: string | undefined; | |
version: string; | |
constructor({ assetsPath, basePath, browserLocation, dev, headers, honoOptions, hubApiUrl, hub, initialImageMasking, imageAspectRatio, imageOptions, initialState, origin, secret, ui, verify, }?: FrogConstructorParameters<env, basePath, _state>); | |
castAction: HandlerInterface<env, 'cast-action', schema, basePath>; | |
frame: HandlerInterface<env, 'frame', schema, basePath>; | |
route<subPath extends string, subSchema extends Schema, subBasePath extends string>(path: subPath, frog: FrogBase<any, subSchema, subBasePath>): this; | |
transaction: HandlerInterface<env, 'transaction', schema, basePath>; | |
use: MiddlewareHandlerInterface<env, schema, basePath>; | |
} | |
//# sourceMappingURL=frog-base.d.ts.map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "frog/jsx/jsx-runtime"; | |
import { detect } from 'detect-browser'; | |
import { Hono } from 'hono'; | |
import { ImageResponse } from 'hono-og'; | |
import { html } from 'hono/html'; | |
import lz from 'lz-string'; | |
// TODO: maybe write our own "modern" universal path (or resolve) module. | |
// We are not using `node:path` to remain compatible with Edge runtimes. | |
import { default as p } from 'path-browserify'; | |
import { fromQuery } from './utils/fromQuery.js'; | |
import { getButtonValues } from './utils/getButtonValues.js'; | |
import { getCastActionContext } from './utils/getCastActionContext.js'; | |
import { getFrameContext } from './utils/getFrameContext.js'; | |
import { getFrameMetadata } from './utils/getFrameMetadata.js'; | |
import { getImagePaths } from './utils/getImagePaths.js'; | |
import { getRequestUrl } from './utils/getRequestUrl.js'; | |
import { getRouteParameters } from './utils/getRouteParameters.js'; | |
import { getTransactionContext } from './utils/getTransactionContext.js'; | |
import * as jws from './utils/jws.js'; | |
import { parseBrowserLocation } from './utils/parseBrowserLocation.js'; | |
import { parseFonts } from './utils/parseFonts.js'; | |
import { parseHonoPath } from './utils/parseHonoPath.js'; | |
import { parseImage } from './utils/parseImage.js'; | |
import { parseIntents } from './utils/parseIntents.js'; | |
import { parsePath } from './utils/parsePath.js'; | |
import { requestBodyToContext } from './utils/requestBodyToContext.js'; | |
import { serializeJson } from './utils/serializeJson.js'; | |
import { toSearchParams } from './utils/toSearchParams.js'; | |
import { version } from './version.js'; | |
/** | |
* A Frog instance. | |
* | |
* @param parameters - {@link FrogConstructorParameters} | |
* @returns instance. {@link Frog} | |
* | |
* @example | |
* ``` | |
* import { Frog } from 'frog' | |
* | |
* const app = new Frog() | |
* | |
* app.frame('/', (c) => { | |
* const { buttonValue, inputText, status } = c | |
* const fruit = inputText || buttonValue | |
* return c.res({ | |
* image: ( | |
* <div style={{ fontSize: 60 }}> | |
* {fruit ? `You selected: ${fruit}` : 'Welcome!'} | |
* </div> | |
* ), | |
* intents: [ | |
* <Button value="apples">Apples</Button>, | |
* <Button value="oranges">Oranges</Button>, | |
* <Button value="bananas">Bananas</Button>, | |
* ] | |
* }) | |
* }) | |
* ``` | |
*/ | |
export class FrogBase { | |
constructor({ assetsPath, basePath, browserLocation, dev, headers, honoOptions, hubApiUrl, hub, initialImageMasking, imageAspectRatio, imageOptions, initialState, origin, secret, ui, verify, } = {}) { | |
// Note: not using native `private` fields to avoid tslib being injected | |
// into bundled code. | |
Object.defineProperty(this, "_initialState", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: undefined | |
}); | |
/** Path for assets. */ | |
Object.defineProperty(this, "assetsPath", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Base path of the server instance. */ | |
Object.defineProperty(this, "basePath", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** URL to redirect to when the user is coming to the page via a browser. */ | |
Object.defineProperty(this, "browserLocation", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "dev", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "headers", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Hono instance. */ | |
Object.defineProperty(this, "hono", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Farcaster Hub API URL. */ | |
Object.defineProperty(this, "hubApiUrl", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Farcaster Hub API config. */ | |
Object.defineProperty(this, "hub", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Initial frame image masking. */ | |
Object.defineProperty(this, "initialImageMasking", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Image aspect ratio. */ | |
Object.defineProperty(this, "imageAspectRatio", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: '1.91:1' | |
}); | |
/** Image options. */ | |
Object.defineProperty(this, "imageOptions", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Origin URL of the server instance. */ | |
Object.defineProperty(this, "origin", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "fetch", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "get", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "post", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Key used to sign secret data. */ | |
Object.defineProperty(this, "secret", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** FrogUI configuration. */ | |
Object.defineProperty(this, "ui", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
/** Whether or not frames should be verified. */ | |
Object.defineProperty(this, "verify", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: true | |
}); | |
Object.defineProperty(this, "_dev", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: void 0 | |
}); | |
Object.defineProperty(this, "version", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: version | |
}); | |
Object.defineProperty(this, "castAction", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: (...parameters) => { | |
const [path, middlewares, handler, options = {}] = getRouteParameters(...parameters); | |
const { verify = this.verify } = options; | |
// Cast Action Route (implements POST). | |
this.hono.post(parseHonoPath(path), ...middlewares, async (c) => { | |
const { context } = getCastActionContext({ | |
context: await requestBodyToContext(c, { | |
hub: this.hub || | |
(this.hubApiUrl ? { apiUrl: this.hubApiUrl } : undefined), | |
secret: this.secret, | |
verify, | |
}), | |
}); | |
const response = await handler(context); | |
if (response instanceof Response) | |
return response; | |
if (response.status === 'error') { | |
c.status(response.error.statusCode ?? 400); | |
return c.json({ message: response.error.message }); | |
} | |
const { headers = this.headers, message } = response.data; | |
// Set response headers provided by consumer. | |
for (const [key, value] of Object.entries(headers ?? {})) | |
c.header(key, value); | |
return c.json({ message }); | |
}); | |
return this; | |
} | |
}); | |
Object.defineProperty(this, "frame", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: (...parameters) => { | |
const [path, middlewares, handler, options = {}] = getRouteParameters(...parameters); | |
const { verify = this.verify } = options; | |
// OG Image Route | |
const imagePaths = getImagePaths(parseHonoPath(path)); | |
for (const imagePath of imagePaths) { | |
this.hono.get(imagePath, async (c) => { | |
const url = getRequestUrl(c.req); | |
const query = c.req.query(); | |
if (!query.image && this.initialImageMasking) { | |
// If the query is doesn't have an image, it is an initial request to a frame. | |
// Therefore we need to get the link to fetch the original image and jump once again in this method to resolve the options, | |
// but now with query params set. | |
const metadata = await getFrameMetadata(url.href.slice(0, -6)); // Stripping `/image` (6 characters) from the end of the url. | |
const frogImage = metadata.find(({ property }) => property === 'frog:image'); | |
if (!frogImage) | |
throw new Error('Unexpected error: frog:image meta tag is not present in the frame.'); | |
// Redirect to this route but now with search params and return the response | |
return c.redirect(frogImage.content); | |
} | |
const defaultImageOptions = await (async () => { | |
if (typeof this.imageOptions === 'function') | |
return await this.imageOptions(); | |
return this.imageOptions; | |
})(); | |
const fonts = await (async () => { | |
if (this.ui?.vars?.fonts) | |
return Object.values(this.ui?.vars.fonts).flat(); | |
if (typeof options?.fonts === 'function') | |
return await options.fonts(); | |
if (options?.fonts) | |
return options.fonts; | |
return defaultImageOptions?.fonts; | |
})(); | |
const { headers = this.headers, image, imageOptions = defaultImageOptions, } = fromQuery(query); | |
const image_ = JSON.parse(lz.decompressFromEncodedURIComponent(image)); | |
return new ImageResponse(image_, { | |
width: 1200, | |
height: 630, | |
...imageOptions, | |
format: imageOptions?.format ?? 'png', | |
fonts: await parseFonts(fonts), | |
headers: imageOptions?.headers ?? headers, | |
}); | |
}); | |
} | |
// Frame Route (implements GET & POST). | |
this.hono.use(parseHonoPath(path), ...middlewares, async (c) => { | |
const url = getRequestUrl(c.req); | |
const origin = this.origin ?? url.origin; | |
const assetsUrl = origin + parsePath(this.assetsPath); | |
const baseUrl = origin + parsePath(this.basePath); | |
const { context, getState } = getFrameContext({ | |
context: await requestBodyToContext(c, { | |
hub: this.hub || | |
(this.hubApiUrl ? { apiUrl: this.hubApiUrl } : undefined), | |
secret: this.secret, | |
verify, | |
}), | |
initialState: this._initialState, | |
origin, | |
}); | |
if (context.url !== parsePath(url.href)) | |
return c.redirect(context.url); | |
const response = await handler(context); | |
if (response instanceof Response) | |
return response; | |
if (response.status === 'error') { | |
c.status(response.error.statusCode ?? 400); | |
return c.json({ message: response.error.message }); | |
} | |
const { action, browserLocation = this.browserLocation, headers = this.headers, imageAspectRatio = this.imageAspectRatio, image, imageOptions: imageOptions_ = this.imageOptions, intents, ogImage, title = 'Frog Frame', } = response.data; | |
const buttonValues = getButtonValues(parseIntents(intents)); | |
if (context.status === 'redirect' && context.buttonIndex) { | |
const buttonValue = buttonValues[context.buttonIndex - 1]; | |
const location = buttonValue?.replace(/^_r:/, ''); | |
if (!location) | |
throw new Error('location required to redirect'); | |
return c.redirect(location, 302); | |
} | |
const renderAsHTML = c.req.header('Accept') === 'text/html' || | |
c.req.query('accept') === 'text/html'; | |
// If the user is coming from a browser, and a `browserLocation` is set, | |
// then we will redirect the user to that location. | |
const browser = detect(c.req.header('user-agent')); | |
const browserLocation_ = parseBrowserLocation(c, browserLocation, { | |
basePath: this.basePath, | |
path, | |
}); | |
if (!renderAsHTML && browser?.name && browserLocation_) | |
return c.redirect(browserLocation_.startsWith('http') | |
? browserLocation_ | |
: `${origin + p.resolve(this.basePath, browserLocation_)}`, 302); | |
// Derive the previous state, and sign it if a secret is provided. | |
const previousState = await (async () => { | |
const state = await context.deriveState(); | |
if (!this.secret) | |
return state; | |
if (!state) | |
return state; | |
return jws.sign(JSON.stringify(state), this.secret); | |
})(); | |
// We need to pass some context to the next frame to derive button values, state, etc. | |
// Here, we are deriving two sets of "next frame state". | |
// 1. For the INITIAL FRAME, we need to pass through the state as a search parameter | |
// due to Farcaster's constraints with the `fc:frame:state` tag. It must be empty | |
// for the initial frame. | |
// 2. For SUBSEQUENT FRAMES, we can pass through the state as a serialized JSON object | |
// to the next frame via the `fc:frame:state` tag. | |
const nextFrameStateSearch = toSearchParams({ | |
initialPath: context.initialPath, | |
previousButtonValues: buttonValues, | |
}); | |
const nextFrameStateMeta = serializeJson({ | |
initialPath: context.initialPath, | |
previousButtonValues: buttonValues, | |
previousState, | |
}); | |
const imageOptions = await (async () => { | |
if (typeof imageOptions_ === 'function') | |
return await imageOptions_(); | |
return imageOptions_; | |
})(); | |
const imageUrl = await (async () => { | |
if (typeof image !== 'string') { | |
const compressedImage = lz.compressToEncodedURIComponent(JSON.stringify(await parseImage(_jsx("div", { style: { | |
display: 'flex', | |
flexDirection: 'column', | |
height: '100%', | |
width: '100%', | |
}, children: await image }), { | |
assetsUrl, | |
ui: { | |
...this.ui, | |
vars: { | |
...this.ui?.vars, | |
frame: { | |
height: imageOptions?.height, | |
width: imageOptions?.width, | |
}, | |
}, | |
}, | |
}))); | |
const imageParams = toSearchParams({ | |
image: compressedImage, | |
imageOptions: imageOptions | |
? { | |
...imageOptions, | |
// TODO: Remove once `fonts` is removed from `imageOptions`. | |
fonts: undefined, | |
} | |
: undefined, | |
headers, | |
}); | |
return `${parsePath(context.url)}/image?${imageParams}`; | |
} | |
if (image.startsWith('http') || image.startsWith('data')) | |
return image; | |
return `${assetsUrl + parsePath(image)}`; | |
})(); | |
const ogImageUrl = (() => { | |
if (!ogImage) | |
return undefined; | |
if (ogImage.startsWith('http')) | |
return ogImage; | |
return baseUrl + parsePath(ogImage); | |
})(); | |
const postUrl = (() => { | |
if (!action) | |
return context.url; | |
if (action.startsWith('http')) | |
return action; | |
return baseUrl + parsePath(action); | |
})(); | |
const parsedIntents = parseIntents(intents, { | |
baseUrl, | |
search: context.status === 'initial' | |
? nextFrameStateSearch.toString() | |
: undefined, | |
}); | |
// Set response headers provided by consumer. | |
for (const [key, value] of Object.entries(headers ?? {})) | |
c.header(key, value); | |
if (renderAsHTML) { | |
const height = imageOptions?.height ?? 630; | |
const width = imageOptions?.width ?? 1200; | |
// Convert `tw` to `class` | |
const __html = image.toString().replace(/tw=/g, 'class='); | |
const fonts = await (async () => { | |
if (this.ui?.vars?.fonts) | |
return Object.values(this.ui.vars.fonts).flat(); | |
if (typeof options?.fonts === 'function') | |
return await options.fonts(); | |
if (options?.fonts) | |
return options.fonts; | |
return imageOptions?.fonts; | |
})(); | |
const groupedFonts = new Map(); | |
if (fonts) | |
for (const font of fonts) { | |
const key = `${font.source ? `${font.source}:` : ''}${font.name}`; | |
if (groupedFonts.has(key)) | |
groupedFonts.get(key)?.push(font); | |
else | |
groupedFonts.set(key, [font]); | |
} | |
const googleFonts = []; | |
for (const item of groupedFonts) { | |
const [, fonts] = item; | |
const font = fonts[0]; | |
if (font?.source === 'google') { | |
const name = font.name.replace(' ', '+'); | |
const hasItalic = fonts.some((x) => x.style === 'italic'); | |
const attributeKeys = hasItalic ? 'ital,wght' : 'wght'; | |
const attributeValues = fonts | |
.map((x) => { | |
if (hasItalic) { | |
if (x.style === 'italic') | |
return `1,${x.weight}`; | |
return `0,${x.weight}`; | |
} | |
return x.weight; | |
}) | |
.join(';'); | |
const url = `https://fonts.googleapis.com/css2?family=${name}${attributeValues ? `:${attributeKeys}@${attributeValues}` : ''}&display=swap`; | |
googleFonts.push(url); | |
} | |
} | |
return c.html(_jsxs(_Fragment, { children: [_jsx("script", { src: "https://cdn.tailwindcss.com" }), _jsx("script", { children: html ` | |
tailwind.config = { | |
plugins: [{ | |
handler({ addBase }) { | |
addBase({ 'html': { 'line-height': 1.2 } }) | |
}, | |
}], | |
} | |
` }), _jsx("style", { | |
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> | |
dangerouslySetInnerHTML: { | |
__html: `@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Material+Icons');body{display:flex;height:100%;margin:0;tab-size:8;font-family:Inter,sans-serif;overflow:hidden}body>div,body>div *{box-sizing:border-box;display:flex}body{background:#1A1A19;}link,script,style{position: absolute;width: 1px;height: 1px;padding: 0;margin: -1px;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border-width: 0;}`, | |
} }), Boolean(googleFonts.length) && (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: true }), googleFonts.map((url) => (_jsx("link", { href: url, rel: "stylesheet" })))] })), _jsx("div", { className: "bg-black", | |
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> | |
dangerouslySetInnerHTML: { __html }, style: { height, width } })] })); | |
} | |
return c.render(_jsxs(_Fragment, { children: [html `<!DOCTYPE html>`, _jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { property: "fc:frame", content: "vNext" }), _jsx("meta", { property: "fc:frame:image:aspect_ratio", content: imageAspectRatio }), _jsx("meta", { property: "fc:frame:image", content: context.status === 'initial' && this.initialImageMasking | |
? `${context.url}/image` | |
: imageUrl }), _jsx("meta", { property: "og:image", content: ogImageUrl ?? | |
(context.status === 'initial' && this.initialImageMasking) | |
? `${context.url}/image` | |
: imageUrl }), _jsx("meta", { property: "og:title", content: title }), _jsx("meta", { property: "fc:frame:post_url", content: context.status === 'initial' | |
? `${postUrl}?${nextFrameStateSearch.toString()}` | |
: postUrl }), context.status !== 'initial' && (_jsx("meta", { property: "fc:frame:state", content: nextFrameStateMeta })), parsedIntents, _jsx("meta", { property: "frog:version", content: version }), c.req.header('x-frog-dev') !== undefined && (_jsx("meta", { property: "frog:context", content: serializeJson({ | |
...context, | |
// note: unserializable entities are undefined. | |
env: context.env | |
? Object.assign(context.env, { | |
incoming: undefined, | |
outgoing: undefined, | |
}) | |
: undefined, | |
req: undefined, | |
state: getState(), | |
}) })), this.initialImageMasking && (_jsx("meta", { property: "frog:image", content: imageUrl }))] }), _jsx("body", {})] })] })); | |
}); | |
return this; | |
} | |
}); | |
Object.defineProperty(this, "transaction", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: (...parameters) => { | |
const [path, middlewares, handler, options = {}] = getRouteParameters(...parameters); | |
const { verify = this.verify } = options; | |
this.hono.post(parseHonoPath(path), ...middlewares, async (c) => { | |
const { context } = getTransactionContext({ | |
context: await requestBodyToContext(c, { | |
hub: this.hub || | |
(this.hubApiUrl ? { apiUrl: this.hubApiUrl } : undefined), | |
secret: this.secret, | |
verify, | |
}), | |
req: c.req, | |
}); | |
const response = await handler(context); | |
if (response instanceof Response) | |
return response; | |
if (response.status === 'error') { | |
c.status(response.error.statusCode ?? 400); | |
return c.json({ message: response.error.message }); | |
} | |
return c.json(response.data); | |
}); | |
return this; | |
} | |
}); | |
Object.defineProperty(this, "use", { | |
enumerable: true, | |
configurable: true, | |
writable: true, | |
value: (...args) => { | |
this.hono.use(...args); | |
return this; | |
} | |
}); | |
this.hono = new Hono(honoOptions); | |
if (basePath) | |
this.hono = this.hono.basePath(basePath); | |
if (browserLocation) | |
this.browserLocation = browserLocation; | |
if (headers) | |
this.headers = headers; | |
if (hubApiUrl) | |
this.hubApiUrl = hubApiUrl; | |
if (hub) | |
this.hub = hub; | |
if (imageAspectRatio) | |
this.imageAspectRatio = imageAspectRatio; | |
if (imageOptions) | |
this.imageOptions = imageOptions; | |
if (origin) | |
this.origin = origin; | |
if (secret) | |
this.secret = secret; | |
if (ui) | |
this.ui = ui; | |
if (typeof verify !== 'undefined') | |
this.verify = verify; | |
this.basePath = basePath ?? '/'; | |
this.assetsPath = assetsPath ?? this.basePath; | |
this.fetch = this.hono.fetch.bind(this.hono); | |
this.get = this.hono.get.bind(this.hono); | |
this.initialImageMasking = initialImageMasking ?? true; | |
this.post = this.hono.post.bind(this.hono); | |
if (initialState) | |
this._initialState = initialState; | |
if (dev) | |
this.dev = { enabled: true, ...(dev ?? {}) }; | |
this._dev = undefined; // this is set `true` by `devtools` helper | |
// allow devtools to work with dynamic params off base path | |
this.hono.all('*', async (c, next) => { | |
if (this._dev) | |
for (const { handler, path } of c.req.matchedRoutes) | |
if (path === this._dev) | |
return handler(c, next); | |
await next(); | |
}); | |
} | |
route(path, frog) { | |
if (frog.assetsPath === '/') | |
frog.assetsPath = this.assetsPath; | |
if (frog.basePath === '/') | |
frog.basePath = parsePath(this.basePath) + parsePath(path); | |
if (!frog.browserLocation) | |
frog.browserLocation = this.browserLocation; | |
if (!frog.dev) | |
frog.dev = this.dev; | |
if (!frog.headers) | |
frog.headers = this.headers; | |
if (!frog.hubApiUrl) | |
frog.hubApiUrl = this.hubApiUrl; | |
if (!frog.hub) | |
frog.hub = this.hub; | |
if (!frog.imageOptions) | |
frog.imageOptions = this.imageOptions; | |
if (!frog.origin) | |
frog.origin = this.origin; | |
if (!frog.secret) | |
frog.secret = this.secret; | |
if (!frog.ui) | |
frog.ui = this.ui; | |
if (!frog.verify) | |
frog.verify = this.verify; | |
this.hono.route(path, frog.hono); | |
return this; | |
} | |
} | |
//# sourceMappingURL=frog-base.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment