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 || '/'} +