diff --git a/.gitignore b/.gitignore index fc347eff..6f843372 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.map coverage -lib +_lib node_modules tsconfig.*.tsbuildinfo tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 223cd892..0e780996 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,5 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" - }, - "css.validate": false + } } diff --git a/biome.json b/biome.json index 64196dbd..917e99be 100644 --- a/biome.json +++ b/biome.json @@ -20,7 +20,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noUselessFragments": "off" + } } } } diff --git a/bun.lockb b/bun.lockb index aeb9bd05..73e1e616 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/example/package.json b/example/package.json index 3f7d0900..ccc53cab 100644 --- a/example/package.json +++ b/example/package.json @@ -2,9 +2,10 @@ "name": "example", "private": true, "scripts": { - "dev": "bun run --hot src/index.ts" + "dev": "bun run --hot src/index.tsx" }, "dependencies": { + "@wevm/framework": "workspace:*", "hono": "^3.12.8" }, "devDependencies": { diff --git a/example/src/index.ts b/example/src/index.ts deleted file mode 100644 index 9869eb80..00000000 --- a/example/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Hono } from "hono"; - -const app = new Hono(); - -app.get("/", (c) => { - return c.text("Hello Hono!"); -}); - -export default app; diff --git a/example/src/index.tsx b/example/src/index.tsx new file mode 100644 index 00000000..76b6b096 --- /dev/null +++ b/example/src/index.tsx @@ -0,0 +1,24 @@ +/** @jsx jsx */ +/** @jsxImportSource hono/jsx */ +/** @jsxFrag */ + +import { Button, Framework } from "@wevm/framework"; + +const app = new Framework(); + +app.frame("/", () => { + return { + image:
hello
, + intents: ( + <> + + + + ), + }; +}); + +export default { + port: 3001, + fetch: app.fetch, +}; diff --git a/package.json b/package.json index c331f167..dde3b3ce 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "clean": "rimraf src/index.js src/lib src/tsconfig.build.tsbuildinfo", "format": "biome format . --write", "lint": "biome check . --apply", + "preconstruct": "bun run scripts/preconstruct.ts", "test": "vitest", "typecheck": "tsc --noEmit" }, diff --git a/scripts/preconstruct.ts b/scripts/preconstruct.ts new file mode 100644 index 00000000..2ec38ebc --- /dev/null +++ b/scripts/preconstruct.ts @@ -0,0 +1,93 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { glob } from "glob"; + +// Symlinks package sources to dist for local development + +console.log("Setting up packages for development."); + +// Get all package.json files +const packagePaths = await glob("**/package.json", { + ignore: ["**/dist/**", "**/node_modules/**"], +}); + +let count = 0; +for (const packagePath of packagePaths) { + type Package = { + bin?: Record | undefined; + exports?: + | Record + | undefined; + name?: string | undefined; + private?: boolean | undefined; + }; + const file = Bun.file(packagePath); + const packageJson = (await file.json()) as Package; + + // Skip private packages + if (packageJson.private) continue; + if (!packageJson.exports) continue; + + count += 1; + console.log(`${packageJson.name} — ${path.dirname(packagePath)}`); + + const dir = path.resolve(path.dirname(packagePath)); + + // Empty dist directory + const distDirName = "_lib"; + const dist = path.resolve(dir, distDirName); + let files: string[] = []; + try { + files = await fs.readdir(dist); + } catch { + await fs.mkdir(dist); + } + + const promises: Promise[] = []; + for (const file of files) { + promises.push( + fs.rm(path.join(dist, file), { recursive: true, force: true }), + ); + } + await Promise.all(promises); + + // Link exports to dist locations + for (const [key, exports] of Object.entries(packageJson.exports)) { + // Skip `package.json` exports + if (/package\.json$/.test(key)) continue; + + let entries: string[][]; + if (typeof exports === "string") + entries = [ + ["default", exports], + ["types", exports.replace(".js", ".d.ts")], + ]; + else entries = Object.entries(exports); + + // Link exports to dist locations + for (const [, value] of entries as [ + type: "types" | "default", + value: string, + ][]) { + const srcDir = path.resolve( + dir, + path.dirname(value).replace(distDirName, ""), + ); + let srcFileName: string; + if (key === ".") srcFileName = "index.tsx"; + else srcFileName = path.basename(`${key}.tsx`); + const srcFilePath = path.resolve(srcDir, srcFileName); + + const distDir = path.resolve(dir, path.dirname(value)); + const distFileName = path.basename(value); + const distFilePath = path.resolve(distDir, distFileName); + + await fs.mkdir(distDir, { recursive: true }); + + // Symlink src to dist file + await fs.symlink(srcFilePath, distFilePath, "file"); + } + } +} + +console.log(`Done. Set up ${count} ${count === 1 ? "package" : "packages"}.`); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 1e29c457..00000000 --- a/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const hello = "world"; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 00000000..23fdb62f --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,79 @@ +import { type Context, Hono } from "hono"; +import { ImageResponse } from "hono-og"; +import { type JSXNode } from "hono/jsx"; + +type FrameReturnType = { + image: JSX.Element; + intents: JSX.Element; +}; + +export class Framework extends Hono { + frame( + path: string, + handler: (c: Context) => FrameReturnType | Promise, + ) { + this.get(path, async (c) => { + const { intents } = await handler(c); + return c.render( + + + + + + {parseIntents(intents)} + + , + ); + }); + + // TODO: don't slice + this.get(`${path.slice(1)}/_og`, async (c) => { + const { image } = await handler(c); + return new ImageResponse(image); + }); + } +} + +//////////////////////////////////////////////////////////////////////// +// Components + +export type ButtonProps = { + children: string; +}; + +export function Button({ children }: ButtonProps) { + return ; +} + +//////////////////////////////////////////////////////////////////////// +// Utilities + +type Counter = { button: number }; + +function parseIntents(intents_: JSX.Element) { + const intents = intents_ as unknown as JSXNode; + const counter: Counter = { + button: 0, + }; + + if (typeof intents.children[0] === "object") + return Object.assign(intents, { + children: intents.children.map((e) => parseIntent(e as JSXNode, counter)), + }); + return parseIntent(intents, counter); +} + +function parseIntent(node: JSXNode, counter: Counter) { + const intent = ( + 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; + } + + return Object.assign(intent, { props }); +} diff --git a/src/package.json b/src/package.json index 87f254c4..9df81242 100644 --- a/src/package.json +++ b/src/package.json @@ -2,11 +2,17 @@ "name": "@wevm/framework", "version": "0.0.0", "type": "module", - "module": "lib/index.js", - "types": "lib/index.d.ts", - "typings": "lib/index.d.ts", + "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" } } diff --git a/tsconfig.build.json b/tsconfig.build.json index d8c2f06e..2bd43d01 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,7 +12,7 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "./src/lib", + "outDir": "./src/_lib", "rootDir": "./src" } } \ No newline at end of file