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