Skip to content

Commit fab1f49

Browse files
committed
feat: client components
1 parent a6b270c commit fab1f49

File tree

13 files changed

+135
-22
lines changed

13 files changed

+135
-22
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ yarn-error.log*
3333
.DS_Store
3434
*.pem
3535

36-
data.db*
36+
data.db*
37+
static

example/0-todo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Hono } from "hono";
22
import { jsxRenderer } from "hono/jsx-renderer";
33
import { logger } from "hono/logger";
44
import { form, store } from "plainstack";
5-
import { bunSqlite, secret } from "plainstack/bun";
5+
import { secret, sqlite } from "plainstack/bun";
66
import { session } from "plainstack/session";
77

88
interface Items {
@@ -15,7 +15,7 @@ interface DB {
1515
items: Items;
1616
}
1717

18-
const { database, migrate } = bunSqlite<DB>();
18+
const { database, migrate } = sqlite<DB>();
1919

2020
await migrate(({ schema }) => {
2121
return schema

example/1-background-job.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,28 @@ import { Hono } from "hono";
22
import { jsxRenderer } from "hono/jsx-renderer";
33
import { logger } from "hono/logger";
44
import { JobStatus } from "plainjob";
5-
import { job, perform, work } from "plainstack";
6-
import { bunSqlite } from "plainstack/bun";
5+
import { job, perform, schedule, work } from "plainstack";
6+
import { sqlite } from "plainstack/bun";
77

8-
const { queue } = bunSqlite();
8+
const { queue } = sqlite();
99

1010
const randomJob = job<string>({
11-
name: "random",
11+
name: "fails-randomly",
1212
run: async ({ data }) => {
1313
if (Math.random() > 0.5) throw new Error("Random error");
1414
console.log("Processing job", data);
1515
},
1616
});
1717

18-
void work(queue, { job: randomJob }, {});
18+
const minuteSchedule = schedule({
19+
name: "every-minute",
20+
cron: "* * * * *",
21+
run: async () => {
22+
console.log("this runs every minute");
23+
},
24+
});
25+
26+
void work(queue, { randomJob }, { minuteSchedule });
1927

2028
const app = new Hono();
2129

example/2-client-component/app.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Hono } from "hono";
2+
import { serveStatic } from "hono/bun";
3+
import { build } from "plainstack/bun";
4+
import { render } from "plainstack/client";
5+
import { Counter } from "./counter";
6+
7+
const app = new Hono();
8+
9+
app.use("/static/*", serveStatic({ root: "./example/2-client-component" }));
10+
11+
await build({
12+
entrypoints: ["example/2-client-component/counter.tsx"],
13+
outdir: "example/2-client-component/static",
14+
});
15+
16+
app.get("/", async (c) => {
17+
return c.html(
18+
<html lang="en">
19+
<body>
20+
<div id="counter" />
21+
{render(Counter, { path: "/static" })}
22+
</body>
23+
</html>,
24+
);
25+
});
26+
27+
export default app;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useState } from "hono/jsx";
2+
import { mount } from "plainstack/client";
3+
4+
export function Counter() {
5+
const [count, setCount] = useState(0);
6+
return (
7+
<div>
8+
<p>Count: {count}</p>
9+
{/* biome-ignore lint/a11y/useButtonType: <explanation> */}
10+
<button onClick={() => setCount(count + 1)}>Increment</button>
11+
</div>
12+
);
13+
}
14+
15+
mount(Counter);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"exports": {
88
".": "./dist/plainstack.js",
99
"./bun": "./dist/bun.js",
10-
"./session": "./dist/middleware/session.js"
10+
"./session": "./dist/middleware/session.js",
11+
"./client": "./dist/client.js"
1112
},
1213
"types": "dist/plainstack.d.ts",
1314
"scripts": {

src/bun.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { Database } from "bun:sqlite";
22
import { randomBytes } from "node:crypto";
3+
import { readdir } from "node:fs/promises";
4+
import type { BuildConfig as BunBuildConfig } from "bun";
35
import { CamelCasePlugin, Kysely } from "kysely";
46
import { BunSqliteDialect } from "kysely-bun-sqlite";
57
import { bun } from "plainjob";
68
import { migrate as migrate_ } from "./database";
7-
import { test } from "./env";
9+
import { prod, test } from "./env";
810
import { queue } from "./job";
911

10-
export function bunSqlite<DB = unknown>() {
11-
const sqlite = new Database(test() ? ":memory:" : "data.db", {
12+
export function sqlite<DB = unknown>() {
13+
const sqlite_ = new Database(test() ? ":memory:" : "data.db", {
1214
strict: true,
1315
});
1416
const q = queue({
15-
connection: bun(sqlite),
17+
connection: bun(sqlite_),
1618
});
1719
const database = new Kysely<DB>({
1820
dialect: new BunSqliteDialect({
19-
database: sqlite,
21+
database: sqlite_,
2022
}),
2123
plugins: [new CamelCasePlugin()],
2224
});
2325
const migrate = migrate_(database);
24-
return { sqlite, database, migrate, queue: q };
26+
return { sqlite: sqlite_, database, migrate, queue: q };
2527
}
2628

2729
export async function secret(): Promise<string> {
@@ -36,3 +38,21 @@ export async function secret(): Promise<string> {
3638
}
3739
return newSecret;
3840
}
41+
42+
type BuildConfig = Omit<BunBuildConfig, "entrypoints"> & {
43+
entrypoints: string | string[];
44+
};
45+
46+
export async function build(options: BuildConfig) {
47+
const entrypointfiles =
48+
typeof options.entrypoints === "string"
49+
? await readdir(options.entrypoints)
50+
: options.entrypoints;
51+
return await Bun.build({
52+
outdir: "static",
53+
sourcemap: "linked",
54+
minify: false, // TODO
55+
...options,
56+
entrypoints: entrypointfiles,
57+
});
58+
}

src/client.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { kebabCase } from "change-case";
2+
import { type Child, render as renderHono } from "hono/jsx/dom";
3+
import type { JSX } from "hono/jsx/jsx-runtime";
4+
5+
export function render<T>(
6+
Component: (props: T) => Child,
7+
options: { path: string },
8+
props?: T,
9+
) {
10+
const name = kebabCase(Component.name);
11+
return (
12+
<>
13+
<div id={`${name}-data`} data-props={JSON.stringify(props)} />
14+
<script type="module" defer src={`${options.path}/${name}.js`} />
15+
</>
16+
);
17+
}
18+
19+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
20+
export function mount(Component: (props?: any) => JSX.Element) {
21+
if (typeof document !== "undefined") {
22+
const name = kebabCase(Component.name);
23+
const dataElement = document.getElementById(`${name}-data`);
24+
if (!dataElement) {
25+
throw new Error(
26+
`unable to mount client component ${name}, data element not found. make sure you have a <div id="${name}-data"></div> in your html`,
27+
);
28+
}
29+
const dataJson = dataElement.dataset.props;
30+
const data = JSON.parse(dataJson ?? "{}");
31+
console.info(`found data for client component ${name} in DOM`, data);
32+
const targetElement = document.getElementById(name);
33+
if (!targetElement) {
34+
throw new Error(
35+
`unable to render client component ${name}, target element not found. make sure you have a <div id="${name}"></div> in your html`,
36+
);
37+
}
38+
renderHono(<Component {...data} />, targetElement);
39+
}
40+
}

src/entity.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from "bun:test";
2-
import { bunSqlite } from "./bun";
2+
import { sqlite } from "./bun";
33
import { rollback, store } from "./entity";
44

55
type Database = {
@@ -20,7 +20,7 @@ type Database = {
2020
};
2121

2222
describe("entity crud operations", async () => {
23-
const { database, migrate } = bunSqlite<Database>();
23+
const { database, migrate } = sqlite<Database>();
2424

2525
await migrate(
2626
({ schema }) =>

src/job.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function isSchedule(schedule: unknown): schedule is Schedule {
4545
);
4646
}
4747

48-
export function defineSchedule(opts: {
48+
export function schedule(opts: {
4949
name: string;
5050
cron: string;
5151
run: () => Promise<void>;

0 commit comments

Comments
 (0)