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