diff --git a/.scripts/postbuild.ts b/.scripts/postbuild.ts
new file mode 100644
index 00000000..da185134
--- /dev/null
+++ b/.scripts/postbuild.ts
@@ -0,0 +1,16 @@
+import glob from 'fast-glob'
+
+await rewriteHonoJsx()
+
+async function rewriteHonoJsx() {
+ const files = await glob('./src/_lib/**/*.js')
+ for (const file of files) {
+ const content = await Bun.file(file).text()
+ await Bun.write(
+ file,
+ content
+ .replaceAll('hono/jsx/jsx-runtime', 'farc/jsx/jsx-runtime')
+ .replaceAll('hono/jsx/jsx-dev-runtime', 'farc/jsx/jsx-dev-runtime'),
+ )
+ }
+}
diff --git a/.scripts/preconstruct.ts b/.scripts/preconstruct.ts
index 956e4439..dff82dfe 100644
--- a/.scripts/preconstruct.ts
+++ b/.scripts/preconstruct.ts
@@ -85,7 +85,7 @@ for (const packagePath of packagePaths) {
await fs.mkdir(distDir, { recursive: true })
// Symlink src to dist file
- await fs.symlink(srcFilePath, distFilePath, 'file')
+ await fs.symlink(srcFilePath, distFilePath, 'file').catch(() => {})
}
}
}
diff --git a/bun.lockb b/bun.lockb
index 64e2c430..e8a72ccd 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/examples/_dev/package.json b/examples/_dev/package.json
index 3eef70eb..23dfd6f6 100644
--- a/examples/_dev/package.json
+++ b/examples/_dev/package.json
@@ -1,5 +1,5 @@
{
- "name": "example",
+ "name": "example-dev",
"private": true,
"scripts": {
"dev": "bun run --hot src/index.tsx"
diff --git a/examples/_dev/src/index.tsx b/examples/_dev/src/index.tsx
index 69dc5206..8f07882d 100644
--- a/examples/_dev/src/index.tsx
+++ b/examples/_dev/src/index.tsx
@@ -1,4 +1,5 @@
import { Button, Farc, TextInput } from 'farc'
+
import { app as todoApp } from './todos'
const app = new Farc({
diff --git a/examples/_dev/tsconfig.json b/examples/_dev/tsconfig.json
index c442b33f..a7c4db3d 100644
--- a/examples/_dev/tsconfig.json
+++ b/examples/_dev/tsconfig.json
@@ -1,7 +1,23 @@
{
"compilerOptions": {
- "strict": true,
+ "target": "ESNext",
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
"jsx": "react-jsx",
- "jsxImportSource": "hono/jsx"
- }
-}
\ No newline at end of file
+ "jsxImportSource": "hono/jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/examples/vercel/.gitignore b/examples/vercel/.gitignore
new file mode 100644
index 00000000..0192dfa7
--- /dev/null
+++ b/examples/vercel/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+
+.vercel
diff --git a/examples/vercel/README.md b/examples/vercel/README.md
new file mode 100644
index 00000000..6dd13e7c
--- /dev/null
+++ b/examples/vercel/README.md
@@ -0,0 +1,11 @@
+To install dependencies:
+```sh
+bun install
+```
+
+To run:
+```sh
+bun run dev
+```
+
+open http://localhost:3000
diff --git a/examples/vercel/api/index.tsx b/examples/vercel/api/index.tsx
new file mode 100644
index 00000000..08fedf02
--- /dev/null
+++ b/examples/vercel/api/index.tsx
@@ -0,0 +1,66 @@
+import { Button, Farc, TextInput } from 'farc'
+import { handle } from 'hono/vercel'
+
+export const config = {
+ runtime: 'edge',
+}
+
+const app = new Farc({ basePath: '/api' })
+
+app.frame('/', (context) => {
+ const { buttonValue, inputText, status } = context
+ const fruit = inputText || buttonValue
+ return {
+ image: (
+
+
+ {status === 'response'
+ ? `Nice choice.${fruit ? ` ${fruit.toUpperCase()}!!` : ''}`
+ : 'Welcome!'}
+
+
+ ),
+ intents: [
+ ,
+ ,
+ ,
+ ,
+ status === 'response' && Reset,
+ ],
+ }
+})
+
+export const GET = handle(app.hono)
+export const POST = handle(app.hono)
+
+if (process.env.NODE_ENV === 'development') {
+ const server = Bun.serve(app)
+ console.log(`𝑭𝒂𝒓𝒄 ▶︎ http://localhost:${server.port}/dev`)
+}
diff --git a/examples/vercel/package.json b/examples/vercel/package.json
new file mode 100644
index 00000000..53e7d968
--- /dev/null
+++ b/examples/vercel/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "example-vercel-edge",
+ "type": "module",
+ "scripts": {
+ "build": "farc vercel-build",
+ "start": "bun run --hot api/index.tsx",
+ "deploy": "vercel"
+ },
+ "dependencies": {
+ "farc": "workspace:*",
+ "hono": "^4.0.1"
+ },
+ "devDependencies": {
+ "tsx": "^4.7.1",
+ "typescript": "^5.3.3",
+ "vercel": "^32.4.1"
+ }
+}
diff --git a/examples/vercel/tsconfig.json b/examples/vercel/tsconfig.json
new file mode 100644
index 00000000..8f786b5f
--- /dev/null
+++ b/examples/vercel/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "NodeNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "NodeNext",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "farc/jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["**/*.ts", "**/*.tsx", "**/*.mtsx"]
+}
diff --git a/package.json b/package.json
index c1e9972f..51b69ef6 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"workspaces": ["create-farc", "examples/*", "src"],
"scripts": {
"dev": "bun run --hot ./examples/_dev/src/index.tsx",
- "build": "bun run clean && bun run build:farc && bun run build:create-farc",
+ "build": "bun run clean && bun run build:farc && bun run build:create-farc && bun .scripts/postbuild.ts",
"build:farc": "tsc --project ./tsconfig.build.json",
"build:create-farc": "rimraf create-farc/_lib && tsc -p create-farc/tsconfig.build.json",
"changeset": "changeset",
@@ -22,10 +22,11 @@
"@types/bun": "latest",
"@types/fs-extra": "^11.0.4",
"@vitest/coverage-v8": "^1.2.2",
+ "fast-glob": "^3.3.2",
"hono": "^4",
"rimraf": "^5.0.5",
"typed-htmx": "^0.2.1",
- "typescript": "^5.0.0",
+ "typescript": "^5.3.3",
"vitest": "^1.2.2"
}
}
diff --git a/src/cli/commands/vercel-build.ts b/src/cli/commands/vercel-build.ts
new file mode 100644
index 00000000..98a4f336
--- /dev/null
+++ b/src/cli/commands/vercel-build.ts
@@ -0,0 +1,52 @@
+import { extname, normalize, resolve } from 'node:path'
+import glob from 'fast-glob'
+import { ensureDirSync, writeJsonSync } from 'fs-extra/esm'
+
+export async function build() {
+ const files = await glob('./api/**/*.{js,jsx,ts,tsx}')
+ for (const file of files) {
+ const fileDir = normalize(file).replace(extname(file), '')
+ const dir = resolve(
+ process.cwd(),
+ `./.vercel/output/functions/${fileDir}.func`,
+ )
+ ensureDirSync(dir)
+ writeJsonSync(`${dir}/package.json`, { type: 'module' })
+ }
+
+ ensureDirSync('./.vercel/output')
+ ensureDirSync('./.vercel/output/static')
+ writeJsonSync('./.vercel/output/config.json', {
+ version: 3,
+ routes: [
+ {
+ handle: 'filesystem',
+ },
+ {
+ src: '^/api(?:/(.*))$',
+ dest: '/api',
+ check: true,
+ },
+ {
+ src: '^/api(/.*)?$',
+ status: 404,
+ },
+ {
+ handle: 'error',
+ },
+ {
+ status: 404,
+ src: '^(?!/api).*$',
+ dest: '/404.html',
+ },
+ {
+ handle: 'miss',
+ },
+ {
+ src: '^/api/(.+)(?:\\.(?:tsx))$',
+ dest: '/api/$1',
+ check: true,
+ },
+ ],
+ })
+}
diff --git a/src/cli/index.ts b/src/cli/index.ts
new file mode 100644
index 00000000..4dfb0b8a
--- /dev/null
+++ b/src/cli/index.ts
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+import { cac } from 'cac'
+
+import { build as build_vercel } from './commands/vercel-build.js'
+import { version } from './version.js'
+
+export const cli = cac('farc')
+
+cli.command('vercel-build').action(build_vercel)
+
+cli.help()
+cli.version(version)
+
+cli.parse()
diff --git a/src/cli/version.ts b/src/cli/version.ts
new file mode 100644
index 00000000..6af1fbc4
--- /dev/null
+++ b/src/cli/version.ts
@@ -0,0 +1 @@
+export const version = '0.0.1'
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index cbb8fd6b..a1ce46c3 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,4 +1,4 @@
-import { type HtmlEscapedString } from 'hono/utils/html'
+import type { HtmlEscapedString } from 'hono/utils/html'
export type ButtonProps = {
children: string
diff --git a/src/dev/routes.tsx b/src/dev/routes.tsx
new file mode 100644
index 00000000..7ad4acdb
--- /dev/null
+++ b/src/dev/routes.tsx
@@ -0,0 +1,187 @@
+import { Message } from '@farcaster/core'
+import { bytesToHex } from '@noble/curves/abstract/utils'
+import type { Env, Schema } from 'hono'
+import { inspectRoutes } from 'hono/dev'
+import { jsxRenderer } from 'hono/jsx-renderer'
+import { validator } from 'hono/validator'
+
+import type { FarcBase } from '../farc-base.js'
+import { parsePath } from '../utils/parsePath.js'
+import { Dev, Preview, Style } from './components.js'
+import {
+ fetchFrameMessage,
+ getData,
+ getRoutes,
+ htmlToFrame,
+ htmlToState,
+} from './utils.js'
+
+export function routes<
+ state,
+ env extends Env,
+ schema extends Schema,
+ basePath extends string,
+>(app: FarcBase, path: string) {
+ app.hono
+ .use(`${parsePath(path)}/dev`, (c, next) =>
+ jsxRenderer((props) => {
+ const { children } = props
+ const path = new URL(c.req.url).pathname.replace('/dev', '')
+ return (
+
+
+ 𝑭𝒂𝒓𝒄 {path || '/'}
+
+ {/* TODO: Vendor into project */}
+
+
+
+
+ {children}
+
+ )
+ })(c, next),
+ )
+ .get(async (c) => {
+ const baseUrl = c.req.url.replace('/dev', '')
+ const response = await fetch(baseUrl)
+ const text = await response.text()
+
+ const frame = htmlToFrame(text)
+ const state = htmlToState(text)
+ const routes = getRoutes(baseUrl, inspectRoutes(app.hono))
+
+ return c.render()
+ })
+ .post(
+ validator('form', (value, c) => {
+ try {
+ return getData(value)
+ } catch (e) {
+ return c.text('Invalid data', 400)
+ }
+ }),
+ async (c) => {
+ const baseUrl = c.req.url.replace('/dev', '')
+ const form = c.req.valid('form')
+ const { buttonIndex, castId, fid, inputText, postUrl } = form
+
+ const message = await fetchFrameMessage({
+ baseUrl,
+ buttonIndex,
+ castId,
+ fid,
+ inputText,
+ })
+
+ let response = await fetch(postUrl, {
+ method: 'POST',
+ body: JSON.stringify({
+ untrustedData: {
+ buttonIndex,
+ castId: {
+ fid: castId.fid,
+ hash: `0x${bytesToHex(castId.hash)}`,
+ },
+ fid,
+ inputText: inputText
+ ? Buffer.from(inputText).toString('utf-8')
+ : undefined,
+ messageHash: `0x${bytesToHex(message.hash)}`,
+ network: 1,
+ timestamp: message.data.timestamp,
+ url: baseUrl,
+ },
+ trustedData: {
+ messageBytes: Buffer.from(
+ Message.encode(message).finish(),
+ ).toString('hex'),
+ },
+ }),
+ })
+
+ // fetch initial state on error
+ const error = response.status !== 200 ? response.statusText : undefined
+ if (response.status !== 200) response = await fetch(baseUrl)
+
+ const text = await response.text()
+ // TODO: handle redirects
+ const frame = htmlToFrame(text)
+ const state = htmlToState(text)
+ const routes = getRoutes(baseUrl, inspectRoutes(app.hono))
+
+ return c.render(
+ ,
+ )
+ },
+ )
+
+ app.hono.post(
+ `${parsePath(path)}/dev/redirect`,
+ validator('json', (value, c) => {
+ try {
+ return getData(value)
+ } catch (e) {
+ return c.text('Invalid data', 400)
+ }
+ }),
+ async (c) => {
+ const baseUrl = c.req.url.replace('/dev', '')
+ const json = c.req.valid('json')
+ const { buttonIndex, castId, fid, inputText, postUrl } = json
+
+ const message = await fetchFrameMessage({
+ baseUrl,
+ buttonIndex,
+ castId,
+ fid,
+ inputText,
+ })
+
+ console.log(postUrl)
+ const response = await fetch(postUrl, {
+ method: 'POST',
+ body: JSON.stringify({
+ untrustedData: {
+ buttonIndex,
+ castId: {
+ fid: castId.fid,
+ hash: `0x${bytesToHex(castId.hash)}`,
+ },
+ fid,
+ inputText: inputText
+ ? Buffer.from(inputText).toString('utf-8')
+ : undefined,
+ messageHash: `0x${bytesToHex(message.hash)}`,
+ network: 1,
+ timestamp: message.data.timestamp,
+ url: baseUrl,
+ },
+ trustedData: {
+ messageBytes: Buffer.from(
+ Message.encode(message).finish(),
+ ).toString('hex'),
+ },
+ }),
+ })
+
+ // TODO: Get redirect url
+ console.log({ response })
+
+ return c.json({
+ success: true,
+ redirectUrl: '/',
+ })
+ },
+ )
+}
diff --git a/src/edge/index.ts b/src/edge/index.ts
new file mode 100644
index 00000000..2d2cd504
--- /dev/null
+++ b/src/edge/index.ts
@@ -0,0 +1,10 @@
+export {
+ Button,
+ type ButtonLinkProps,
+ type ButtonMintProps,
+ type ButtonProps,
+ type ButtonResetProps,
+} from '../components/Button.js'
+export { TextInput, type TextInputProps } from '../components/TextInput.js'
+
+export { FarcBase as Farc } from '../farc-base.js'
diff --git a/src/edge/package.json b/src/edge/package.json
new file mode 100644
index 00000000..b22de1f1
--- /dev/null
+++ b/src/edge/package.json
@@ -0,0 +1,5 @@
+{
+ "type": "module",
+ "types": "../_lib/edge/index.d.ts",
+ "module": "../_lib/edge/index.js"
+}
diff --git a/src/farc-base.tsx b/src/farc-base.tsx
new file mode 100644
index 00000000..2be36bc3
--- /dev/null
+++ b/src/farc-base.tsx
@@ -0,0 +1,195 @@
+import { Buffer } from 'node:buffer'
+import { Hono } from 'hono'
+import { ImageResponse } from 'hono-og'
+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'
+import { deserializeJson } from './utils/deserializeJson.js'
+import { getFrameContext } from './utils/getFrameContext.js'
+import { parseIntents } from './utils/parseIntents.js'
+import { parsePath } from './utils/parsePath.js'
+import { requestToContext } from './utils/requestToContext.js'
+import { serializeJson } from './utils/serializeJson.js'
+
+globalThis.Buffer = Buffer
+
+export type FarcConstructorParameters<
+ state = undefined,
+ env extends Env = Env,
+ basePath extends string = '/',
+> = {
+ basePath?: basePath | string | undefined
+ honoOptions?: HonoOptions | undefined
+ initialState?: state | undefined
+}
+
+export type FrameHandlerReturnType = {
+ action?: string | undefined
+ image: JSX.Element
+ imageAspectRatio?: FrameImageAspectRatio | undefined
+ intents?: FrameIntents | undefined
+}
+
+export class FarcBase<
+ state = undefined,
+ env extends Env = Env,
+ schema extends Schema = {},
+ basePath extends string = '/',
+> {
+ #initialState: state = undefined as state
+
+ hono: Hono
+ fetch: Hono['fetch']
+ get: Hono['get']
+ post: Hono['post']
+
+ constructor({
+ basePath,
+ honoOptions,
+ initialState,
+ }: FarcConstructorParameters = {}) {
+ this.hono = new Hono(honoOptions)
+ if (basePath) this.hono = this.hono.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)
+
+ if (initialState) this.#initialState = initialState
+ }
+
+ frame(
+ path: path,
+ handler: (
+ context: FrameContext,
+ previousContext: PreviousFrameContext | undefined,
+ ) => FrameHandlerReturnType | Promise,
+ ) {
+ // 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>(
+ query.previousContext,
+ )
+ : undefined
+ const context = await getFrameContext({
+ context: await requestToContext(c.req),
+ initialState: this.#initialState,
+ previousContext,
+ request: c.req,
+ })
+
+ if (context.url !== parsePath(c.req.url))
+ return c.redirect(
+ `${context.url}?previousContext=${query.previousContext}`,
+ )
+
+ const { action, imageAspectRatio, intents } = await handler(
+ context,
+ previousContext,
+ )
+ const parsedIntents = intents ? parseIntents(intents) : null
+
+ const serializedContext = serializeJson(context)
+ const serializedPreviousContext = serializeJson({
+ ...context,
+ intents: parsedIntents,
+ previousState: context.deriveState(),
+ })
+
+ const ogSearch = new URLSearchParams()
+ if (query.previousContext)
+ ogSearch.set('previousContext', query.previousContext)
+ if (serializedContext) ogSearch.set('context', serializedContext)
+
+ const postSearch = new URLSearchParams()
+ if (serializedPreviousContext)
+ postSearch.set('previousContext', serializedPreviousContext)
+
+ return c.render(
+
+
+
+
+
+
+
+ {parsedIntents}
+
+
+ {query.previousContext && (
+
+ )}
+
+
+
+ view 𝒇𝒓𝒂𝒎𝒆
+
+
+ ,
+ )
+ })
+
+ // OG Image Route
+ this.hono.get(`${parsePath(path)}/image`, async (c) => {
+ const query = c.req.query()
+ const previousContext = query.previousContext
+ ? deserializeJson>(
+ query.previousContext,
+ )
+ : undefined
+ const context = await getFrameContext({
+ context: deserializeJson>(query.context),
+ initialState: this.#initialState,
+ previousContext,
+ request: c.req,
+ })
+ const { image } = await handler(context, previousContext)
+ return new ImageResponse(image)
+ })
+ }
+
+ route<
+ subPath extends string,
+ subEnv extends Env,
+ subSchema extends Schema,
+ subBasePath extends string,
+ >(path: subPath, farc: FarcBase) {
+ return this.hono.route(path, farc.hono)
+ }
+}
diff --git a/src/farc.tsx b/src/farc.tsx
index 524e1984..61d7778c 100644
--- a/src/farc.tsx
+++ b/src/farc.tsx
@@ -1,370 +1,23 @@
-import { Message } from '@farcaster/core'
-import { bytesToHex } from '@noble/curves/abstract/utils'
-import { Hono } from 'hono'
-import { ImageResponse } from 'hono-og'
-import { inspectRoutes } from 'hono/dev'
-import { type HonoOptions } from 'hono/hono-base'
-import { jsxRenderer } from 'hono/jsx-renderer'
-import { type Env, type Schema } from 'hono/types'
-import { validator } from 'hono/validator'
-
-import { Dev, Preview, Style } from './dev/components.js'
-import {
- fetchFrameMessage,
- getData,
- getRoutes,
- htmlToFrame,
- htmlToState,
-} from './dev/utils.js'
-import {
- type FrameContext,
- type FrameImageAspectRatio,
- type FrameIntents,
- type PreviousFrameContext,
-} from './types.js'
-import { deserializeJson } from './utils/deserializeJson.js'
-import { getFrameContext } from './utils/getFrameContext.js'
-import { parseIntents } from './utils/parseIntents.js'
-import { parsePath } from './utils/parsePath.js'
-import { requestToContext } from './utils/requestToContext.js'
-import { serializeJson } from './utils/serializeJson.js'
-
-export type FarcConstructorParameters<
- state = undefined,
- env extends Env = Env,
- basePath extends string = '/',
-> = {
- basePath?: basePath | string | undefined
- honoOptions?: HonoOptions | undefined
- initialState?: state | undefined
-}
-
-export type FrameHandlerReturnType = {
- action?: string | undefined
- image: JSX.Element
- imageAspectRatio?: FrameImageAspectRatio | undefined
- intents?: FrameIntents | undefined
-}
+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'
export class Farc<
state = undefined,
env extends Env = Env,
schema extends Schema = {},
basePath extends string = '/',
-> {
- #initialState: state = undefined as state
-
- hono: Hono
- fetch: Hono['fetch']
- get: Hono['get']
- post: Hono['post']
-
- constructor({
- basePath,
- honoOptions,
- initialState,
- }: FarcConstructorParameters = {}) {
- this.hono = new Hono(honoOptions)
- if (basePath) this.hono = this.hono.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)
-
- if (initialState) this.#initialState = initialState
- }
-
- frame(
+> extends FarcBase {
+ override frame(
path: path,
handler: (
context: FrameContext,
previousContext: PreviousFrameContext | undefined,
) => FrameHandlerReturnType | Promise,
) {
- // 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>(
- query.previousContext,
- )
- : undefined
- const context = await getFrameContext({
- context: await requestToContext(c.req),
- initialState: this.#initialState,
- previousContext,
- request: c.req,
- })
-
- if (context.url !== parsePath(c.req.url))
- return c.redirect(
- `${context.url}?previousContext=${query.previousContext}`,
- )
-
- const { action, imageAspectRatio, intents } = await handler(
- context,
- previousContext,
- )
- const parsedIntents = intents ? parseIntents(intents) : null
-
- const serializedContext = serializeJson(context)
- const serializedPreviousContext = serializeJson({
- ...context,
- intents: parsedIntents,
- previousState: context.deriveState(),
- })
-
- const ogSearch = new URLSearchParams()
- if (query.previousContext)
- ogSearch.set('previousContext', query.previousContext)
- if (serializedContext) ogSearch.set('context', serializedContext)
-
- const postSearch = new URLSearchParams()
- if (serializedPreviousContext)
- postSearch.set('previousContext', serializedPreviousContext)
-
- return c.render(
-
-
-
-
-
-
-
- {parsedIntents}
-
-
- {query.previousContext && (
-
- )}
-
-
-
- view 𝒇𝒓𝒂𝒎𝒆
-
-
- ,
- )
- })
-
- // OG Image Route
- this.hono.get(`${parsePath(path)}/image`, async (c) => {
- const query = c.req.query()
- const previousContext = query.previousContext
- ? deserializeJson>(
- query.previousContext,
- )
- : undefined
- const context = await getFrameContext({
- context: deserializeJson>(query.context),
- initialState: this.#initialState,
- previousContext,
- request: c.req,
- })
- const { image } = await handler(context, previousContext)
- return new ImageResponse(image)
- })
-
- // Frame Dev Routes
- this.hono
- .use(`${parsePath(path)}/dev`, (c, next) =>
- jsxRenderer((props) => {
- const { children } = props
- const path = new URL(c.req.url).pathname.replace('/dev', '')
- return (
-
-
- 𝑭𝒂𝒓𝒄 {path || '/'}
-
- {/* TODO: Vendor into project */}
-
-
-
-
- {children}
-
- )
- })(c, next),
- )
- .get(async (c) => {
- const baseUrl = c.req.url.replace('/dev', '')
- const response = await fetch(baseUrl)
- const text = await response.text()
-
- const frame = htmlToFrame(text)
- const state = htmlToState(text)
- const routes = getRoutes(baseUrl, inspectRoutes(this.hono))
-
- return c.render()
- })
- .post(
- validator('form', (value, c) => {
- try {
- return getData(value)
- } catch (e) {
- return c.text('Invalid data', 400)
- }
- }),
- async (c) => {
- const baseUrl = c.req.url.replace('/dev', '')
- const form = c.req.valid('form')
- const { buttonIndex, castId, fid, inputText, postUrl } = form
-
- const message = await fetchFrameMessage({
- baseUrl,
- buttonIndex,
- castId,
- fid,
- inputText,
- })
-
- let response = await fetch(postUrl, {
- method: 'POST',
- body: JSON.stringify({
- untrustedData: {
- buttonIndex,
- castId: {
- fid: castId.fid,
- hash: `0x${bytesToHex(castId.hash)}`,
- },
- fid,
- inputText: inputText
- ? Buffer.from(inputText).toString('utf-8')
- : undefined,
- messageHash: `0x${bytesToHex(message.hash)}`,
- network: 1,
- timestamp: message.data.timestamp,
- url: baseUrl,
- },
- trustedData: {
- messageBytes: Buffer.from(
- Message.encode(message).finish(),
- ).toString('hex'),
- },
- }),
- })
-
- // fetch initial state on error
- const error =
- response.status !== 200 ? response.statusText : undefined
- if (response.status !== 200) response = await fetch(baseUrl)
-
- const text = await response.text()
- // TODO: handle redirects
- const frame = htmlToFrame(text)
- const state = htmlToState(text)
- const routes = getRoutes(baseUrl, inspectRoutes(this.hono))
-
- return c.render(
- ,
- )
- },
- )
-
- this.hono.post(
- `${parsePath(path)}/dev/redirect`,
- validator('json', (value, c) => {
- try {
- return getData(value)
- } catch (e) {
- return c.text('Invalid data', 400)
- }
- }),
- async (c) => {
- const baseUrl = c.req.url.replace('/dev', '')
- const json = c.req.valid('json')
- const { buttonIndex, castId, fid, inputText, postUrl } = json
-
- const message = await fetchFrameMessage({
- baseUrl,
- buttonIndex,
- castId,
- fid,
- inputText,
- })
-
- console.log(postUrl)
- const response = await fetch(postUrl, {
- method: 'POST',
- body: JSON.stringify({
- untrustedData: {
- buttonIndex,
- castId: {
- fid: castId.fid,
- hash: `0x${bytesToHex(castId.hash)}`,
- },
- fid,
- inputText: inputText
- ? Buffer.from(inputText).toString('utf-8')
- : undefined,
- messageHash: `0x${bytesToHex(message.hash)}`,
- network: 1,
- timestamp: message.data.timestamp,
- url: baseUrl,
- },
- trustedData: {
- messageBytes: Buffer.from(
- Message.encode(message).finish(),
- ).toString('hex'),
- },
- }),
- })
-
- // TODO: Get redirect url
- console.log({ response })
-
- return c.json({
- success: true,
- redirectUrl: '/',
- })
- },
- )
- }
+ super.frame(path, handler)
- route<
- subPath extends string,
- subEnv extends Env,
- subSchema extends Schema,
- subBasePath extends string,
- >(path: subPath, farc: Farc) {
- return this.hono.route(path, farc.hono)
+ devRoutes(this, path)
}
}
diff --git a/src/index.ts b/src/index.ts
index 6e387c65..653c6f07 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,4 +7,4 @@ export {
} from './components/Button.js'
export { TextInput, type TextInputProps } from './components/TextInput.js'
-export { Farc, type FrameHandlerReturnType } from './farc.js'
+export { Farc } from './farc.js'
diff --git a/src/jsx/index.ts b/src/jsx/index.ts
new file mode 100644
index 00000000..9acb8d96
--- /dev/null
+++ b/src/jsx/index.ts
@@ -0,0 +1 @@
+export * from 'hono/jsx'
diff --git a/src/jsx/jsx-dev-runtime/index.ts b/src/jsx/jsx-dev-runtime/index.ts
new file mode 100644
index 00000000..a6e123ac
--- /dev/null
+++ b/src/jsx/jsx-dev-runtime/index.ts
@@ -0,0 +1,24 @@
+import { type JSXNode, jsx } from 'hono/jsx'
+import type { HtmlEscapedString } from 'hono/utils/html'
+export { Fragment } from 'hono/jsx'
+
+export function jsxDEV(
+ tag: string | Function,
+ props: Record,
+ key?: string,
+): JSXNode {
+ let node: JSXNode
+ if (!props || !('children' in props)) {
+ node = jsx(tag, props, ...[])
+ } else {
+ const children = props.children as string | HtmlEscapedString
+ // biome-ignore lint/performance/noDelete:
+ // biome-ignore lint/complexity/useLiteralKeys:
+ delete props['children']
+ node = Array.isArray(children)
+ ? jsx(tag, props, ...children)
+ : jsx(tag, props, ...[children])
+ }
+ node.key = key
+ return node
+}
diff --git a/src/jsx/jsx-dev-runtime/package.json b/src/jsx/jsx-dev-runtime/package.json
new file mode 100644
index 00000000..128454d0
--- /dev/null
+++ b/src/jsx/jsx-dev-runtime/package.json
@@ -0,0 +1,5 @@
+{
+ "type": "module",
+ "types": "../../_lib/jsx/jsx-dev-runtime/index.d.ts",
+ "module": "../../_lib/jsx/jsx-dev-runtime/index.js"
+}
diff --git a/src/jsx/jsx-runtime/index.ts b/src/jsx/jsx-runtime/index.ts
new file mode 100644
index 00000000..b04b6d23
--- /dev/null
+++ b/src/jsx/jsx-runtime/index.ts
@@ -0,0 +1,8 @@
+export { jsxDEV as jsx, Fragment } from '../jsx-dev-runtime/index.js'
+export { jsxDEV as jsxs } from '../jsx-dev-runtime/index.js'
+
+import { html, raw } from 'hono/html'
+export { html as jsxTemplate }
+export const jsxAttr = (name: string, value: string) =>
+ raw(`${name}="${html`${value}`}"`)
+export const jsxEscape = (value: string) => value
diff --git a/src/jsx/jsx-runtime/package.json b/src/jsx/jsx-runtime/package.json
new file mode 100644
index 00000000..45a33266
--- /dev/null
+++ b/src/jsx/jsx-runtime/package.json
@@ -0,0 +1,5 @@
+{
+ "type": "module",
+ "types": "../../_lib/jsx/jsx-runtime/index.d.ts",
+ "module": "../../_lib/jsx/jsx-runtime/index.js"
+}
diff --git a/src/jsx/package.json b/src/jsx/package.json
new file mode 100644
index 00000000..e5918d24
--- /dev/null
+++ b/src/jsx/package.json
@@ -0,0 +1,5 @@
+{
+ "type": "module",
+ "types": "../_lib/jsx/index.d.ts",
+ "module": "../_lib/jsx/index.js"
+}
diff --git a/src/package.json b/src/package.json
index 054ed371..d86a213e 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,13 +1,37 @@
{
"name": "farc",
- "version": "0.0.1",
+ "version": "0.0.1-vercel.20",
"type": "module",
"module": "_lib/index.js",
"types": "_lib/index.d.ts",
"typings": "_lib/index.d.ts",
+ "bin": {
+ "farc": "./_lib/cli/index.js"
+ },
"sideEffects": false,
"exports": {
- ".": "./_lib/index.js"
+ ".": {
+ "types": "./_lib/index.d.ts",
+ "edge": "./_lib/edge/index.js",
+ "edge-light": "./_lib/edge/index.js",
+ "default": "./_lib/index.js"
+ },
+ "./edge": {
+ "types": "./_lib/edge/index.d.ts",
+ "default": "./_lib/edge/index.js"
+ },
+ "./jsx": {
+ "types": "./_lib/jsx/index.d.ts",
+ "default": "./_lib/jsx/index.js"
+ },
+ "./jsx/jsx-runtime": {
+ "types": "./_lib/jsx/jsx-runtime/index.d.ts",
+ "default": "./_lib/jsx/jsx-runtime/index.js"
+ },
+ "./jsx/jsx-dev-runtime": {
+ "types": "./_lib/jsx/jsx-dev-runtime/index.d.ts",
+ "default": "./_lib/jsx/jsx-dev-runtime/index.js"
+ }
},
"peerDependencies": {
"hono": "^4"
@@ -15,10 +39,12 @@
"dependencies": {
"@farcaster/core": "^0.13.7",
"@noble/curves": "^1.3.0",
+ "cac": "^6.7.14",
+ "fast-glob": "3.3.2",
+ "fs-extra": "^11.2.0",
"happy-dom": "^13.3.8",
- "hono-og": "~0.0.3",
+ "hono-og": "0.0.8",
"immer": "^10.0.3",
- "lz-string": "^1.5.0",
"shiki": "^1.0.0"
}
}
diff --git a/src/utils/deserializeJson.ts b/src/utils/deserializeJson.ts
index ca00eec5..2d5160c9 100644
--- a/src/utils/deserializeJson.ts
+++ b/src/utils/deserializeJson.ts
@@ -1,6 +1,4 @@
-import { default as lz } from 'lz-string'
-
-export function deserializeJson(data = ''): returnType {
+export function deserializeJson(data = '{}'): returnType {
if (data === 'undefined') return {} as returnType
- return JSON.parse(lz.decompressFromEncodedURIComponent(data))
+ return JSON.parse(decodeURIComponent(data))
}
diff --git a/src/utils/serializeJson.ts b/src/utils/serializeJson.ts
index dc7bcdce..f200bc97 100644
--- a/src/utils/serializeJson.ts
+++ b/src/utils/serializeJson.ts
@@ -1,5 +1,3 @@
-import { default as lz } from 'lz-string'
-
export function serializeJson(data: unknown = {}) {
- return lz.compressToEncodedURIComponent(JSON.stringify(data))
+ return encodeURIComponent(JSON.stringify(data))
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index addcfb96..2695887c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -31,8 +31,6 @@
"module": "NodeNext",
"target": "ES2021", // Setting this to `ES2021` enables native support for `Node v16+`: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping.
"lib": ["ES2022", "DOM"],
- "jsx": "react-jsx",
- "jsxImportSource": "hono/jsx",
"types": ["@types/bun", "typed-htmx"],
// Skip type checking for node modules
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 2bd43d01..73278580 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -9,6 +9,8 @@
"src/**/*.test-d.tsx"
],
"compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
diff --git a/tsconfig.json b/tsconfig.json
index 4d7693fb..efe68049 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,9 @@
{
// This configuration is used for local development and type checking.
"extends": "./tsconfig.base.json",
- "include": ["src"]
+ "include": ["src"],
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx"
+ }
}