diff --git a/bun.lockb b/bun.lockb
index 64e2c430..c785cb2b 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 41a81dae..2f5f6cf8 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..a2accb51 100644
--- a/examples/_dev/tsconfig.json
+++ b/examples/_dev/tsconfig.json
@@ -1,7 +1,27 @@
{
"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": "farc/jsx",
+ "paths": {
+ "farc/jsx/jsx-runtime": ["../../src/jsx/jsx-runtime/index.ts"],
+ "farc/jsx/jsx-dev-runtime": ["../../src/jsx/jsx-dev-runtime/index.ts"]
+ },
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/examples/vercel-edge/.gitignore b/examples/vercel-edge/.gitignore
new file mode 100644
index 00000000..0192dfa7
--- /dev/null
+++ b/examples/vercel-edge/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+
+.vercel
diff --git a/examples/vercel-edge/README.md b/examples/vercel-edge/README.md
new file mode 100644
index 00000000..6dd13e7c
--- /dev/null
+++ b/examples/vercel-edge/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-edge/api/index.tsx b/examples/vercel-edge/api/index.tsx
new file mode 100644
index 00000000..25a502a1
--- /dev/null
+++ b/examples/vercel-edge/api/index.tsx
@@ -0,0 +1,66 @@
+/** @jsx jsx */
+/** @jsxImportSource hono/jsx */
+
+import { Button, Farc, TextInput } from 'farc'
+// @ts-ignore
+import { jsx } from 'hono/jsx'
+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)
diff --git a/examples/vercel-edge/package.json b/examples/vercel-edge/package.json
new file mode 100644
index 00000000..187c0381
--- /dev/null
+++ b/examples/vercel-edge/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "example-vercel-edge",
+ "type": "module",
+ "scripts": {
+ "start": "vercel dev",
+ "deploy": "vercel"
+ },
+ "dependencies": {
+ "farc": "file:../../src",
+ "hono": "^4.0.1"
+ },
+ "devDependencies": {
+ "vercel": "^32.4.1"
+ }
+}
diff --git a/examples/vercel-edge/tsconfig.json b/examples/vercel-edge/tsconfig.json
new file mode 100644
index 00000000..9296f061
--- /dev/null
+++ b/examples/vercel-edge/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["**/*.ts", "**/*.tsx"]
+}
diff --git a/examples/vercel-edge/vercel.json b/examples/vercel-edge/vercel.json
new file mode 100644
index 00000000..6dfcb40a
--- /dev/null
+++ b/examples/vercel-edge/vercel.json
@@ -0,0 +1,8 @@
+{
+ "rewrites": [
+ {
+ "source": "/api/(.*)",
+ "destination": "/api"
+ }
+ ]
+}
diff --git a/src/dev/utils.test.ts b/src/dev/utils.test.ts
index 4bbbab19..4570c1eb 100644
--- a/src/dev/utils.test.ts
+++ b/src/dev/utils.test.ts
@@ -28,17 +28,18 @@ const html = `
`
-const selector = 'meta[property^="fc:"], meta[property^="og:"]'
-const metaTags = htmlToMetaTags(html, selector)
+const prefix = ['fc', 'og']
+const metaTags = htmlToMetaTags(html, prefix)
test('htmlToMetaTags', () => {
const html = `
foo
+
bar
`
- expect(htmlToMetaTags(html, selector).length).toEqual(2)
+ expect(htmlToMetaTags(html, prefix).length).toEqual(2)
})
test('parseFrameProperties', () => {
@@ -100,7 +101,7 @@ describe('validateFrameButtons', () => {
`
- const metaTags = htmlToMetaTags(html, selector)
+ const metaTags = htmlToMetaTags(html, prefix)
const buttons = parseButtons(metaTags)
const result = validateButtons(buttons)
expect(result).toEqual({
@@ -114,7 +115,7 @@ describe('validateFrameButtons', () => {
`
- const metaTags = htmlToMetaTags(html, selector)
+ const metaTags = htmlToMetaTags(html, prefix)
const buttons = parseButtons(metaTags)
const result = validateButtons(buttons)
expect(result).toEqual({
diff --git a/src/dev/utils.ts b/src/dev/utils.ts
index 65aa47d4..6fa8c8d7 100644
--- a/src/dev/utils.ts
+++ b/src/dev/utils.ts
@@ -1,4 +1,4 @@
-import { Window } from 'happy-dom'
+import { type Node, parseFromString } from 'dom-parser'
import { inspectRoutes } from 'hono/dev'
import {
@@ -15,16 +15,17 @@ import {
type FrameMetaTagPropertyName,
} from './types.js'
-export function htmlToMetaTags(html: string, selector: string) {
- const window = new Window()
- window.document.write(html)
- const document = window.document
- return document.querySelectorAll(
- selector,
- ) as unknown as readonly HTMLMetaElement[]
+export function htmlToMetaTags(html: string, prefix?: string[]) {
+ const dom = parseFromString(html)
+ const nodes = dom.getElementsByTagName('meta')
+ if (!prefix) return nodes
+ return nodes.filter((node) => {
+ const property = node.getAttribute('property')
+ return property && prefix.some((x) => property.startsWith(x))
+ })
}
-export function parseProperties(metaTags: readonly HTMLMetaElement[]) {
+export function parseProperties(metaTags: readonly Node[]) {
const validPropertyNames = new Set([
'fc:frame',
'fc:frame:image',
@@ -54,8 +55,11 @@ export function parseProperties(metaTags: readonly HTMLMetaElement[]) {
const imageAspectRatio =
(properties['fc:frame:image:aspect_ratio'] as FrameImageAspectRatio) ??
'1.91:1'
- const imageUrl = properties['fc:frame:image'] ?? ''
- const postUrl = properties['fc:frame:post_url'] ?? ''
+ const imageUrl = (properties['fc:frame:image'] ?? '').replaceAll('&', '&')
+ const postUrl = (properties['fc:frame:post_url'] ?? '').replaceAll(
+ '&',
+ '&',
+ )
const title = properties['og:title'] ?? ''
const version = (properties['fc:frame'] as FrameVersion) ?? 'vNext'
@@ -70,7 +74,7 @@ export function parseProperties(metaTags: readonly HTMLMetaElement[]) {
}
}
-export function parseButtons(metaTags: readonly HTMLMetaElement[]) {
+export function parseButtons(metaTags: readonly Node[]) {
// https://regexr.com/7rlm0
const buttonRegex = /fc:frame:button:(1|2|3|4)(?::(action|target))?$/
@@ -152,7 +156,7 @@ export type State = {
}
export function htmlToState(html: string) {
- const metaTags = htmlToMetaTags(html, 'meta[property^="farc:"]')
+ const metaTags = htmlToMetaTags(html, ['farc'])
const properties: Partial> = {}
for (const metaTag of metaTags) {
@@ -174,10 +178,7 @@ export function htmlToState(html: string) {
}
export function htmlToFrame(html: string) {
- const metaTags = htmlToMetaTags(
- html,
- 'meta[property^="fc:"], meta[property^="og:"]',
- )
+ const metaTags = htmlToMetaTags(html, ['fc', 'og'])
const properties = parseProperties(metaTags)
const buttons = parseButtons(metaTags)
diff --git a/src/farc.tsx b/src/farc.tsx
index 493cfdcc..f4c65650 100644
--- a/src/farc.tsx
+++ b/src/farc.tsx
@@ -1,3 +1,4 @@
+import { Buffer } from 'node:buffer'
import {
FrameActionBody,
Message,
@@ -28,6 +29,8 @@ 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,
diff --git a/src/jsx/jsx-dev-runtime/index.ts b/src/jsx/jsx-dev-runtime/index.ts
new file mode 100644
index 00000000..dd9b7aa7
--- /dev/null
+++ b/src/jsx/jsx-dev-runtime/index.ts
@@ -0,0 +1 @@
+export * from 'hono/jsx/jsx-dev-runtime'
diff --git a/src/jsx/jsx-dev-runtime/package.json b/src/jsx/jsx-dev-runtime/package.json
new file mode 100644
index 00000000..0c6d3b52
--- /dev/null
+++ b/src/jsx/jsx-dev-runtime/package.json
@@ -0,0 +1,6 @@
+{
+ "type": "module",
+ "types": "../../_lib/jsx/jsx-dev-runtime/index.d.ts",
+ "module": "../../_lib/jsx/jsx-dev-runtime/index.js",
+ "main": "../../_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..d50577b7
--- /dev/null
+++ b/src/jsx/jsx-runtime/index.ts
@@ -0,0 +1 @@
+export * from 'hono/jsx/jsx-runtime'
diff --git a/src/jsx/jsx-runtime/package.json b/src/jsx/jsx-runtime/package.json
new file mode 100644
index 00000000..ef21cfb8
--- /dev/null
+++ b/src/jsx/jsx-runtime/package.json
@@ -0,0 +1,6 @@
+{
+ "type": "module",
+ "types": "../../_lib/jsx/jsx-runtime/index.d.ts",
+ "module": "../../_lib/jsx/jsx-runtime/index.js",
+ "main": "../../_lib/jsx/jsx-runtime/index.js"
+}
diff --git a/src/package.json b/src/package.json
index 054ed371..97daeb22 100644
--- a/src/package.json
+++ b/src/package.json
@@ -7,7 +7,18 @@
"typings": "_lib/index.d.ts",
"sideEffects": false,
"exports": {
- ".": "./_lib/index.js"
+ ".": {
+ "types": "./_lib/index.d.ts",
+ "default": "./_lib/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"
@@ -16,7 +27,8 @@
"@farcaster/core": "^0.13.7",
"@noble/curves": "^1.3.0",
"happy-dom": "^13.3.8",
- "hono-og": "~0.0.3",
+ "dom-parser": "1.1.5",
+ "hono-og": "~0.0.5",
"immer": "^10.0.3",
"lz-string": "^1.5.0",
"shiki": "^1.0.0"
diff --git a/tsconfig.base.json b/tsconfig.base.json
index addcfb96..7e1a7dea 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -32,8 +32,12 @@
"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",
+ "jsxImportSource": "farc/jsx",
"types": ["@types/bun", "typed-htmx"],
+ "paths": {
+ "farc/jsx/jsx-runtime": ["./src/jsx/jsx-runtime/index.ts"],
+ "farc/jsx/jsx-dev-runtime": ["./src/jsx/jsx-dev-runtime/index.ts"]
+ },
// Skip type checking for node modules
"skipLibCheck": true