From 1ec43a9529a356464e3b77b5a975d7506c6cd541 Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Wed, 7 Feb 2024 13:57:50 -0500 Subject: [PATCH] feat: /preview --- biome.json | 7 ++ bun.lockb | Bin 184408 -> 185088 bytes package.json | 1 + src/index.tsx | 224 +++++++++++++++++++++++++++++++++++++---------- src/package.json | 29 +++--- src/types.ts | 43 +++++++++ 6 files changed, 244 insertions(+), 60 deletions(-) create mode 100644 src/types.ts diff --git a/biome.json b/biome.json index 917e99be..75f761c5 100644 --- a/biome.json +++ b/biome.json @@ -25,5 +25,12 @@ "noUselessFragments": "off" } } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingComma": "all", + "semicolons": "asNeeded" + } } } diff --git a/bun.lockb b/bun.lockb index 73e1e616eea905aef1e32a559952303c895a4916..d26d007e46a1248154fbf467885b45dab7bfb5fb 100755 GIT binary patch delta 34499 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 !J=MHuZb_`!3CROila_|9-$fv;`{Sd;CM2 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 FrameReturnType | Promise, ) { + this.get( + '/preview', + jsxRenderer( + ({ children }) => { + return ( + + {children} + + ) + }, + { docType: true }, + ), + ) + + this.get('/preview/*', async (c) => { + const baseUrl = c.req.url.replace('/preview', '') + const response = await fetch(baseUrl) + const html = await response.text() + const frame = htmlToFrame(html) + return c.render( +
+ Farcaster frame +
, + ) + }) + this.get(path, async (c) => { - const { intents } = await handler(c); + const { intents } = await handler(c) return c.render( @@ -38,18 +73,18 @@ export class Framework extends Hono { {parseIntents(intents)} , - ); - }); + ) + }) // TODO: don't slice this.get(`${path.slice(1)}_og`, async (c) => { - const { image } = await handler(c); - return new ImageResponse(image); - }); + const { image } = await handler(c) + return new ImageResponse(image) + }) this.post(path, async (c) => { - const context = await parsePostContext(c); - const { intents } = await handler(context); + const context = await parsePostContext(c) + const { intents } = await handler(context) return c.render( @@ -60,8 +95,8 @@ export class Framework extends Hono { {parseIntents(intents)} , - ); - }); + ) + }) } } @@ -69,48 +104,145 @@ export class Framework extends Hono { // Components export type ButtonProps = { - children: string; -}; + children: string +} export function Button({ children }: ButtonProps) { - return ; + return } //////////////////////////////////////////////////////////////////////// // Utilities -type Counter = { button: number }; +type Counter = { button: number } async function parsePostContext(ctx: Context): Promise { const { trustedData, untrustedData } = - (await ctx.req.json().catch(() => {})) || {}; - return Object.assign(ctx, { trustedData, untrustedData }); + (await ctx.req.json().catch(() => {})) || {} + return Object.assign(ctx, { trustedData, untrustedData }) } function parseIntents(intents_: JSX.Element) { - const intents = intents_ as unknown as JSXNode; + const intents = intents_ as unknown as JSXNode const counter: Counter = { - button: 0, - }; + button: 1, + } - if (typeof intents.children[0] === "object") + if (typeof intents.children[0] === 'object') return Object.assign(intents, { children: intents.children.map((e) => parseIntent(e as JSXNode, counter)), - }); - return parseIntent(intents, counter); + }) + return parseIntent(intents, counter) } function parseIntent(node: JSXNode, counter: Counter) { const intent = ( - typeof node.tag === "function" ? node.tag({}) : node - ) as JSXNode; + typeof node.tag === 'function' ? node.tag({}) : node + ) as JSXNode + + const props = intent.props || {} + + if (props.property === 'fc:frame:button') { + props.property = `fc:frame:button:${counter.button++}` + props.content = node.children + } - const props = intent.props || {}; + return Object.assign(intent, { props }) +} - if (props.property === "fc:frame:button") { - props.property = `fc:frame:button:${counter.button++}`; - props.content = node.children; +function htmlToFrame(html: string) { + const window = new Window() + window.document.write(html) + const document = window.document + const metaTags = document.querySelectorAll( + 'meta', + ) as unknown as readonly HTMLMetaElement[] + + const validPropertyNames = new Set([ + 'fc:frame', + 'fc:frame:image', + 'fc:frame:input:text', + 'fc:frame:post_url', + 'og:image', + 'og:title', + ]) + // https://regexr.com/7rlm0 + const buttonRegex = /fc:frame:button:(1|2|3|4)(?::(action|target))?$/ + + let currentButtonIndex = 0 + let buttonsAreMissing = false + let buttonsAreOutOfOrder = false + const buttonMap = new Map>() + const buttonActionMap = new Map() + const invalidButtons: FrameButton['index'][] = [] + + const properties: Partial> = {} + for (const metaTag of metaTags) { + const property = metaTag.getAttribute( + 'property', + ) as FrameMetaTagPropertyName | null + if (!property) continue + + const content = metaTag.getAttribute('content') ?? '' + if (validPropertyNames.has(property)) properties[property] = content + else if (buttonRegex.test(property)) { + const matchArray = property.match(buttonRegex) as [ + string, + string, + string | undefined, + ] + const index = parseInt(matchArray[1], 10) as FrameButton['index'] + const type = matchArray[2] as FrameButton['type'] | undefined + + if (type) buttonActionMap.set(index, content as FrameButton['type']) + else { + if (currentButtonIndex >= index) buttonsAreOutOfOrder = true + if (currentButtonIndex + 1 === index) currentButtonIndex = index + else buttonsAreMissing = true + + if (buttonsAreOutOfOrder || buttonsAreMissing) + invalidButtons.push(index) + + const title = content ?? index + buttonMap.set(index, { index, title }) + } + } } - return Object.assign(intent, { props }); + const image = properties['og:image'] ?? '' + const imageUrl = properties['fc:frame:image'] ?? '' + const postUrl = properties['fc:frame:post_url'] ?? '' + const title = properties['og:title'] ?? '' + const version = (properties['fc:frame'] as FrameVersion) ?? 'vNext' + + let buttons = [] as FrameButton[] + for (const [index, button] of buttonMap) { + buttons.push({ + ...button, + type: buttonActionMap.get(index) ?? 'post', + }) + } + buttons = buttons.toSorted((a, b) => a.index - b.index) + + const fallbackImageToUrl = !imageUrl + const postUrlTooLong = postUrl.length > 2_048 + // TODO: Figure out how this is determined + // https://warpcast.com/~/developers/frames + const valid = true + + const frame = { buttons, imageUrl, postUrl, version } + return { + ...frame, + debug: { + ...frame, + buttonsAreOutOfOrder: buttonsAreMissing || buttonsAreOutOfOrder, + fallbackImageToUrl, + htmlTags: metaTags.map((x) => x.outerHTML), + image, + invalidButtons, + postUrlTooLong, + valid, + }, + title, + } satisfies Frame } diff --git a/src/package.json b/src/package.json index 9df81242..c77d0c0e 100644 --- a/src/package.json +++ b/src/package.json @@ -1,18 +1,19 @@ { - "name": "@wevm/framework", - "version": "0.0.0", - "type": "module", - "module": "_lib/index.js", - "types": "_lib/index.d.ts", - "typings": "_lib/index.d.ts", - "sideEffects": false, - "exports": { + "name": "@wevm/framework", + "version": "0.0.0", + "type": "module", + "module": "_lib/index.js", + "types": "_lib/index.d.ts", + "typings": "_lib/index.d.ts", + "sideEffects": false, + "exports": { ".": "./_lib/index.js" }, - "peerDependencies": { - "hono": "^3" - }, - "dependencies": { - "hono-og": "~0.0.2" - } + "peerDependencies": { + "hono": "^3" + }, + "dependencies": { + "happy-dom": "^13.3.8", + "hono-og": "~0.0.2" + } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..9e0bb623 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,43 @@ +// TODO: TSDoc + +export type Frame = { + buttons?: readonly FrameButton[] | undefined + debug?: FrameDebug | undefined + imageUrl: string + postUrl: string + title: string + version: FrameVersion +} + +export type FrameDebug = { + buttons?: readonly FrameButton[] | undefined + buttonsAreOutOfOrder: boolean + fallbackImageToUrl: boolean + htmlTags: readonly string[] + image: string + imageUrl: string + invalidButtons: readonly FrameButton['index'][] + postUrl: string + postUrlTooLong: boolean + valid: boolean + version: FrameVersion +} + +export type FrameButton = { + index: 1 | 2 | 3 | 4 + title: string + type: 'post' | 'post_redirect' +} + +export type FrameVersion = 'vNext' + +export type FrameMetaTagPropertyName = + | 'fc:frame' + | 'fc:frame:image' + | 'fc:frame:input:text' + | 'fc:frame:post_url' + | 'og:image' + | 'og:title' + | `fc:frame:button:${FrameButton['index']}:action` + | `fc:frame:button:${FrameButton['index']}:target` + | `fc:frame:button:${FrameButton['index']}`