From 9f25bf2ddf9d77dae36c3220dad4384d9075c473 Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Mon, 12 Feb 2024 14:41:24 -0500 Subject: [PATCH] feat(dev): basic error handling --- examples/_dev/src/todos.tsx | 22 ++++++++-- src/dev/components.tsx | 85 ++++++++++++++++++++++--------------- src/dev/utils.test.ts | 22 +++++----- src/dev/utils.ts | 38 +++++++++++++---- src/farc.tsx | 66 ++++++++++++++-------------- 5 files changed, 146 insertions(+), 87 deletions(-) diff --git a/examples/_dev/src/todos.tsx b/examples/_dev/src/todos.tsx index 403e454b..dd152355 100644 --- a/examples/_dev/src/todos.tsx +++ b/examples/_dev/src/todos.tsx @@ -18,10 +18,15 @@ export const app = new Farc({ app.frame('/', ({ buttonValue, deriveState, inputText }) => { const { index, todos } = deriveState((state) => { - if (inputText) state.todos.push({ completed: false, name: inputText }) - if (buttonValue === 'up') state.index = Math.max(0, state.index - 1) + if (inputText) { + state.todos.push({ completed: false, name: inputText }) + } + if (buttonValue === 'up') + state.index = + state.index - 1 < 0 ? state.todos.length - 1 : state.index - 1 if (buttonValue === 'down') - state.index = Math.min(state.todos.length - 1, state.index + 1) + state.index = + state.index + 1 > state.todos.length - 1 ? 0 : state.index + 1 if (buttonValue === 'completed') state.todos[state.index].completed = !state.todos[state.index].completed }) @@ -63,3 +68,14 @@ app.frame('/', ({ buttonValue, deriveState, inputText }) => { ], } }) + +app.frame('/foo', () => { + return { + image: ( +
+ hello world +
+ ), + intents: [, , ], + } +}) diff --git a/src/dev/components.tsx b/src/dev/components.tsx index 36bb93c6..fa35dd2d 100644 --- a/src/dev/components.tsx +++ b/src/dev/components.tsx @@ -8,19 +8,20 @@ import { } from './types.js' import { type State } from './utils.js' -export type AppProps = { +export type DevProps = { baseUrl: string + error?: string frame: FrameType routes: readonly string[] state: State } -export function App(props: AppProps) { - const { baseUrl, frame, routes, state } = props +export function Dev(props: DevProps) { + const { baseUrl, error, frame, routes, state } = props return (
- +
@@ -29,18 +30,19 @@ export function App(props: AppProps) { type PreviewProps = { baseUrl: string + error?: string | undefined frame: FrameType routes: readonly string[] state: State } export function Preview(props: PreviewProps) { - const { baseUrl, frame, routes, state } = props + const { baseUrl, error, frame, routes, state } = props const hxTarget = 'preview' return (
+ {error &&
{error}
} ) @@ -74,10 +77,10 @@ function Frame(props: FrameProps) {
- + {hasIntents && ( -
+
{input && } {buttons && ( @@ -121,6 +124,7 @@ function Img(props: ImgProps) { }border object-cover w-full`} style={{ aspectRatio: imageAspectRatio.replace(':', '/'), + minHeight: '269px', maxHeight: '526px', }} /> @@ -134,9 +138,11 @@ function Input(props: InputProps) { return ( ) } @@ -145,16 +151,22 @@ type ButtonProps = FrameButton & { name?: string | undefined } function Button(props: ButtonProps) { const { index, name = 'buttonIndex', title, type = 'post' } = props + return ( @@ -164,6 +176,7 @@ function Button(props: ButtonProps) { const linkIcon = ( ` const metaTags = htmlToMetaTags(html, selector) - const buttons = parseFrameButtons(metaTags) - const result = validateFrameButtons(buttons) + const buttons = parseButtons(metaTags) + const result = validateButtons(buttons) expect(result).toEqual({ buttonsAreOutOfOrder: true, invalidButtons: [], @@ -115,8 +115,8 @@ describe('validateFrameButtons', () => { ` const metaTags = htmlToMetaTags(html, selector) - const buttons = parseFrameButtons(metaTags) - const result = validateFrameButtons(buttons) + const buttons = parseButtons(metaTags) + const result = validateButtons(buttons) expect(result).toEqual({ buttonsAreOutOfOrder: true, invalidButtons: [], diff --git a/src/dev/utils.ts b/src/dev/utils.ts index f3a5aff2..65aa47d4 100644 --- a/src/dev/utils.ts +++ b/src/dev/utils.ts @@ -24,7 +24,7 @@ export function htmlToMetaTags(html: string, selector: string) { ) as unknown as readonly HTMLMetaElement[] } -export function parseFrameProperties(metaTags: readonly HTMLMetaElement[]) { +export function parseProperties(metaTags: readonly HTMLMetaElement[]) { const validPropertyNames = new Set([ 'fc:frame', 'fc:frame:image', @@ -70,7 +70,7 @@ export function parseFrameProperties(metaTags: readonly HTMLMetaElement[]) { } } -export function parseFrameButtons(metaTags: readonly HTMLMetaElement[]) { +export function parseButtons(metaTags: readonly HTMLMetaElement[]) { // https://regexr.com/7rlm0 const buttonRegex = /fc:frame:button:(1|2|3|4)(?::(action|target))?$/ @@ -128,7 +128,7 @@ export function parseFrameButtons(metaTags: readonly HTMLMetaElement[]) { return buttons.toSorted((a, b) => a.index - b.index) } -export function validateFrameButtons(buttons: readonly FrameButton[]) { +export function validateButtons(buttons: readonly FrameButton[]) { let buttonsAreOutOfOrder = false const invalidButtons: FrameButton['index'][] = [] for (let i = 0; i < buttons.length; i++) { @@ -178,8 +178,8 @@ export function htmlToFrame(html: string) { html, 'meta[property^="fc:"], meta[property^="og:"]', ) - const properties = parseFrameProperties(metaTags) - const buttons = parseFrameButtons(metaTags) + const properties = parseProperties(metaTags) + const buttons = parseButtons(metaTags) const fallbackImageToUrl = !properties.imageUrl const postUrlTooLong = properties.postUrl.length > 256 @@ -187,7 +187,7 @@ export function htmlToFrame(html: string) { ? properties.input.text.length > 32 : false - const { buttonsAreOutOfOrder, invalidButtons } = validateFrameButtons(buttons) + const { buttonsAreOutOfOrder, invalidButtons } = validateButtons(buttons) // TODO: Figure out how this is determined // https://warpcast.com/~/developers/frames @@ -222,12 +222,34 @@ export function htmlToFrame(html: string) { } satisfies Frame } -export function getFrameRoutes(routes: ReturnType) { +export function getRoutes( + baseUrl: string, + routes: ReturnType, +) { + // corrects route paths for `app.route(...)` routes + const pathname = new URL(baseUrl).pathname + let basePathname = '/' + for (const route of routes) { + if (route.path === '/') { + basePathname = pathname + } else { + const normalizedPathname = pathname.replace(route.path, '') + if (normalizedPathname === pathname) continue + basePathname = normalizedPathname + } + } + const frameRoutes = [] + if (basePathname !== '/') frameRoutes.push('/') + for (const route of routes) { if (route.isMiddleware) continue if (route.method !== 'ALL') continue - frameRoutes.push(route.path) + + let path: string + if (route.path === '/') path = basePathname + else path = (basePathname === '/' ? '' : basePathname) + route.path + frameRoutes.push(path) } return frameRoutes } diff --git a/src/farc.tsx b/src/farc.tsx index d077fa05..248245e1 100644 --- a/src/farc.tsx +++ b/src/farc.tsx @@ -13,8 +13,8 @@ import type { HonoOptions } from 'hono/hono-base' import { jsxRenderer } from 'hono/jsx-renderer' import { type Env, type Schema } from 'hono/types' -import { App, DevStyles, Preview } from './dev/components.js' -import { getFrameRoutes, htmlToFrame, htmlToState } from './dev/utils.js' +import { Dev, Style, Preview } from './dev/components.js' +import { getRoutes, htmlToFrame, htmlToState } from './dev/utils.js' import { type FrameContext, type FrameImageAspectRatio, @@ -178,29 +178,26 @@ export class Farc< }) // Frame Dev Routes - this.use( - `${parsePath(path)}/dev`, - jsxRenderer( - (props) => { - const { children } = props - return ( - - - 𝑭𝒂𝒓𝒄 {path} - - {/* TODO: Switch to bundling */} -