create-blokd 0.1.0-beta.2 → 0.1.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +34 -10
  2. package/bin/create-blokd.js +5 -2
  3. package/package.json +1 -1
  4. package/templates/dashboard/index.html +11 -0
  5. package/templates/dashboard/package.json +21 -0
  6. package/templates/dashboard/scripts/dev.mjs +105 -0
  7. package/templates/dashboard/src/blokd-env.d.ts +4 -0
  8. package/templates/dashboard/src/entry-client.ts +3 -0
  9. package/templates/dashboard/src/resumables/counter.ts +10 -0
  10. package/templates/dashboard/src/resumables/demo.ts +9 -0
  11. package/templates/dashboard/src/resumables/status.ts +10 -0
  12. package/templates/dashboard/src/routes/_404.tsx +11 -0
  13. package/templates/dashboard/src/routes/_error.tsx +17 -0
  14. package/templates/dashboard/src/routes/_layout.tsx +36 -0
  15. package/templates/dashboard/src/routes/about.tsx +23 -0
  16. package/templates/dashboard/src/routes/contact.tsx +58 -0
  17. package/templates/dashboard/src/routes/index.tsx +46 -0
  18. package/templates/dashboard/src/routes/reports.tsx +37 -0
  19. package/templates/dashboard/src/server.ts +19 -0
  20. package/templates/dashboard/tsconfig.json +17 -0
  21. package/templates/dashboard/vite.config.ts +11 -0
  22. package/templates/forms/index.html +11 -0
  23. package/templates/forms/package.json +21 -0
  24. package/templates/forms/scripts/dev.mjs +105 -0
  25. package/templates/forms/src/blokd-env.d.ts +4 -0
  26. package/templates/forms/src/entry-client.ts +3 -0
  27. package/templates/forms/src/resumables/counter.ts +10 -0
  28. package/templates/forms/src/resumables/demo.ts +9 -0
  29. package/templates/forms/src/routes/_404.tsx +11 -0
  30. package/templates/forms/src/routes/_error.tsx +17 -0
  31. package/templates/forms/src/routes/_layout.tsx +36 -0
  32. package/templates/forms/src/routes/about.tsx +23 -0
  33. package/templates/forms/src/routes/contact.tsx +58 -0
  34. package/templates/forms/src/routes/index.tsx +28 -0
  35. package/templates/forms/src/routes/newsletter.tsx +50 -0
  36. package/templates/forms/src/server.ts +19 -0
  37. package/templates/forms/tsconfig.json +17 -0
  38. package/templates/forms/vite.config.ts +11 -0
  39. package/templates/marketing/index.html +11 -0
  40. package/templates/marketing/package.json +21 -0
  41. package/templates/marketing/scripts/dev.mjs +105 -0
  42. package/templates/marketing/src/blokd-env.d.ts +4 -0
  43. package/templates/marketing/src/entry-client.ts +3 -0
  44. package/templates/marketing/src/resumables/counter.ts +10 -0
  45. package/templates/marketing/src/resumables/demo.ts +9 -0
  46. package/templates/marketing/src/routes/_404.tsx +11 -0
  47. package/templates/marketing/src/routes/_error.tsx +17 -0
  48. package/templates/marketing/src/routes/_layout.tsx +38 -0
  49. package/templates/marketing/src/routes/about.tsx +23 -0
  50. package/templates/marketing/src/routes/contact.tsx +19 -0
  51. package/templates/marketing/src/routes/index.tsx +24 -0
  52. package/templates/marketing/src/routes/pricing.tsx +24 -0
  53. package/templates/marketing/src/server.ts +19 -0
  54. package/templates/marketing/tsconfig.json +17 -0
  55. package/templates/marketing/vite.config.ts +11 -0
  56. package/templates/minimal/package.json +3 -3
  57. package/templates/minimal/scripts/dev.mjs +105 -0
  58. package/templates/minimal/src/resumables/counter.ts +10 -0
  59. package/templates/minimal/src/resumables/demo.ts +9 -4
  60. package/templates/minimal/src/routes/_layout.tsx +3 -1
  61. package/templates/minimal/src/routes/about.tsx +7 -1
  62. package/templates/minimal/src/routes/contact.tsx +58 -0
  63. package/templates/minimal/src/routes/index.tsx +14 -11
package/README.md CHANGED
@@ -110,7 +110,6 @@ It includes:
110
110
  - custom 404 page
111
111
  - custom error page
112
112
  - client entry for resumable islands
113
- - one interactive signal example
114
113
  - one resumable island example
115
114
  - one static route
116
115
 
@@ -239,11 +238,21 @@ The goal is a small, explicit, Web Platform-oriented framework.
239
238
 
240
239
  ## Minimal starter walkthrough
241
240
 
242
- The generated app has two public pages:
241
+ Available templates:
242
+
243
+ ~~~txt
244
+ minimal Balanced starter with islands, native forms, and static routes
245
+ forms Native form actions with no client runtime
246
+ dashboard Route-local islands plus static dashboard pages
247
+ marketing Static marketing pages with zero client budgets
248
+ ~~~
249
+
250
+ The generated app has three public pages:
243
251
 
244
252
  ~~~txt
245
253
  src/routes/index.tsx -> /
246
254
  src/routes/about.tsx -> /about
255
+ src/routes/contact.tsx -> /contact
247
256
  ~~~
248
257
 
249
258
  It also has three special route files:
@@ -257,11 +266,11 @@ src/routes/_error.tsx -> custom error page
257
266
  The root page demonstrates:
258
267
 
259
268
  - `signal`
260
- - an inline event handler
261
269
  - `Island`
262
270
  - `resumable`
263
271
 
264
272
  The about page demonstrates a static server-rendered page with no client interactivity.
273
+ The contact page demonstrates a native POST form action that re-renders success or validation UI without client JavaScript.
265
274
 
266
275
  ## Hono server entry
267
276
 
@@ -330,11 +339,11 @@ The Vite plugin is responsible for:
330
339
  The starter includes a minimal resumable island:
331
340
 
332
341
  ~~~tsx
333
- <Island name="demo-island" state={{ message: "Hello from Blokd" }}>
342
+ <Island name="demo-island" state={{ text: "Hello from Blokd" }}>
334
343
  <button
335
344
  type="button"
336
345
  data-output
337
- onClick={resumable("/src/resumables/demo.ts#sayHello")}
346
+ onClick={on("/src/resumables/demo.ts#show")}
338
347
  >
339
348
  Run resumable handler
340
349
  </button>
@@ -344,10 +353,15 @@ The starter includes a minimal resumable island:
344
353
  The corresponding handler is:
345
354
 
346
355
  ~~~ts
347
- export function sayHello(event: Event, ctx: any) {
348
- const button = event.currentTarget as HTMLButtonElement;
349
- button.textContent = ctx.state.message;
350
- }
356
+ import { defineAction } from "blokd/resume";
357
+
358
+ type MessageState = {
359
+ text: string;
360
+ };
361
+
362
+ export const show = defineAction<MessageState>(({ state, el }) => {
363
+ el.text(state.text);
364
+ });
351
365
  ~~~
352
366
 
353
367
  Blokd resumability is island-scoped. It is not full application graph serialization.
@@ -356,12 +370,22 @@ Blokd resumability is island-scoped. It is not full application graph serializat
356
370
 
357
371
  Blokd can analyze routes to determine whether they need the client entry.
358
372
 
359
- A route with event handlers, signals, effects, `Island`, or `resumable` needs client behavior.
373
+ A route with event handlers, signals, effects, `Island`, `on`, or `resumable` needs client behavior.
360
374
 
361
375
  A route without client markers can be server-rendered without framework client JavaScript.
362
376
 
363
377
  This keeps simple pages small by default.
364
378
 
379
+ Static starter routes also declare:
380
+
381
+ ~~~ts
382
+ export const budget = {
383
+ client: "0kb"
384
+ };
385
+ ~~~
386
+
387
+ This fails the build if client behavior is accidentally added to those routes.
388
+
365
389
  ## Package manager behavior
366
390
 
367
391
  `create-blokd` detects the package manager from the current npm user agent when possible.
@@ -22,7 +22,7 @@ Usage:
22
22
  yarn create blokd my-app
23
23
 
24
24
  Options:
25
- --template <name> Template to use. Default: minimal
25
+ --template <name> Template to use: minimal, forms, dashboard, marketing. Default: minimal
26
26
  --install Install dependencies after creating the project
27
27
  --no-install Do not install dependencies
28
28
  --pm <name> Package manager: pnpm, npm, yarn, bun
@@ -31,6 +31,9 @@ Options:
31
31
  Examples:
32
32
  pnpm create blokd my-app
33
33
  pnpm create blokd my-app --template minimal
34
+ pnpm create blokd my-app --template forms
35
+ pnpm create blokd my-app --template dashboard
36
+ pnpm create blokd my-app --template marketing
34
37
  pnpm create blokd my-app --install
35
38
  `;
36
39
 
@@ -240,4 +243,4 @@ function sanitizePackageName(name) {
240
243
  .replace(/[._-]+$/, "") || "my-blokd-app";
241
244
  }
242
245
 
243
- main();
246
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-blokd",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "Create a new Blokd project.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Blokd App</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ </head>
8
+ <body>
9
+ <p>This file is used by Vite during development.</p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "blokd-dashboard-app",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.0.0",
7
+ "scripts": {
8
+ "dev": "node scripts/dev.mjs",
9
+ "build": "vite build",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "blokd": "0.2.0-beta.0",
14
+ "hono": ">=4.5 <5"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": ">=20 <23",
18
+ "typescript": ">=5.5 <6",
19
+ "vite": ">=6 <9"
20
+ }
21
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { Readable } from "node:stream";
5
+ import { createServer as createViteServer } from "vite";
6
+
7
+ const host = process.env.HOST ?? "0.0.0.0";
8
+ const port = Number(process.env.PORT ?? 5173);
9
+
10
+ let handleRequest = (_req, res) => {
11
+ res.statusCode = 503;
12
+ res.end("Dev server is starting");
13
+ };
14
+
15
+ const httpServer = createServer((req, res) => {
16
+ handleRequest(req, res);
17
+ });
18
+
19
+ const vite = await createViteServer({
20
+ appType: "custom",
21
+ server: {
22
+ middlewareMode: true,
23
+ hmr: {
24
+ server: httpServer
25
+ }
26
+ }
27
+ });
28
+
29
+ handleRequest = async (req, res) => {
30
+ vite.middlewares(req, res, async () => {
31
+ try {
32
+ const mod = await vite.ssrLoadModule("/src/server.ts");
33
+ const app = mod.default;
34
+
35
+ if (!app || typeof app.fetch !== "function") {
36
+ throw new Error("src/server.ts must default-export a Hono app.");
37
+ }
38
+
39
+ const request = toWebRequest(req);
40
+ const response = await app.fetch(request);
41
+
42
+ await writeWebResponse(res, response);
43
+ } catch (error) {
44
+ vite.ssrFixStacktrace(error);
45
+
46
+ res.statusCode = 500;
47
+ res.setHeader("content-type", "text/plain; charset=utf-8");
48
+ res.end(error instanceof Error ? error.stack ?? error.message : String(error));
49
+ }
50
+ });
51
+ };
52
+
53
+ httpServer.listen(port, host, () => {
54
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
55
+
56
+ console.log("");
57
+ console.log(` Blokd dev server`);
58
+ console.log(` Local: http://${displayHost}:${port}/`);
59
+ console.log("");
60
+ });
61
+
62
+ function toWebRequest(req) {
63
+ const protocol = req.headers["x-forwarded-proto"] ?? "http";
64
+ const hostHeader = req.headers.host ?? `${host}:${port}`;
65
+ const url = `${protocol}://${hostHeader}${req.url ?? "/"}`;
66
+
67
+ const headers = new Headers();
68
+
69
+ for (const [key, value] of Object.entries(req.headers)) {
70
+ if (Array.isArray(value)) {
71
+ for (const item of value) headers.append(key, item);
72
+ } else if (value !== undefined) {
73
+ headers.set(key, value);
74
+ }
75
+ }
76
+
77
+ const init = {
78
+ method: req.method ?? "GET",
79
+ headers
80
+ };
81
+
82
+ if (init.method !== "GET" && init.method !== "HEAD") {
83
+ init.body = Readable.toWeb(req);
84
+ init.duplex = "half";
85
+ }
86
+
87
+ return new Request(url, init);
88
+ }
89
+
90
+ async function writeWebResponse(res, response) {
91
+ res.statusCode = response.status;
92
+ res.statusMessage = response.statusText;
93
+
94
+ response.headers.forEach((value, key) => {
95
+ res.setHeader(key, value);
96
+ });
97
+
98
+ if (!response.body) {
99
+ res.end();
100
+ return;
101
+ }
102
+
103
+ const stream = Readable.fromWeb(response.body);
104
+ stream.pipe(res);
105
+ }
@@ -0,0 +1,4 @@
1
+ declare module "virtual:blokd/routes" {
2
+ const routes: import("blokd/hono").RouteEntry[];
3
+ export default routes;
4
+ }
@@ -0,0 +1,3 @@
1
+ import { startResumability } from "blokd/client";
2
+
3
+ startResumability();
@@ -0,0 +1,10 @@
1
+ import { defineAction } from "blokd/resume";
2
+
3
+ type CounterState = {
4
+ count: number;
5
+ };
6
+
7
+ export const increment = defineAction<CounterState>(({ state, el }) => {
8
+ state.count += 1;
9
+ el.text(`Count: ${state.count}`);
10
+ });
@@ -0,0 +1,9 @@
1
+ import { defineAction } from "blokd/resume";
2
+
3
+ type MessageState = {
4
+ text: string;
5
+ };
6
+
7
+ export const show = defineAction<MessageState>(({ state, el }) => {
8
+ el.text(state.text);
9
+ });
@@ -0,0 +1,10 @@
1
+ import { defineAction } from "blokd/resume";
2
+
3
+ type StatusState = {
4
+ text: string;
5
+ };
6
+
7
+ export const refresh = defineAction<StatusState>(({ state, el }) => {
8
+ state.text = state.text === "Queue healthy" ? "Queue checked" : "Queue healthy";
9
+ el.text(state.text);
10
+ });
@@ -0,0 +1,11 @@
1
+ export default function NotFound() {
2
+ return (
3
+ <section>
4
+ <h1>Page not found</h1>
5
+ <p>The page you requested does not exist.</p>
6
+ <p>
7
+ <a href="/">Return home</a>
8
+ </p>
9
+ </section>
10
+ );
11
+ }
@@ -0,0 +1,17 @@
1
+ type ErrorPageProps = {
2
+ error?: {
3
+ message?: string;
4
+ };
5
+ };
6
+
7
+ export default function ErrorPage(props: ErrorPageProps) {
8
+ return (
9
+ <section>
10
+ <h1>Something went wrong</h1>
11
+ <p>
12
+ {props.error?.message ??
13
+ "The application encountered an unexpected error."}
14
+ </p>
15
+ </section>
16
+ );
17
+ }
@@ -0,0 +1,36 @@
1
+ type LayoutProps = {
2
+ children?: unknown;
3
+ meta?: {
4
+ title?: string;
5
+ description?: string;
6
+ };
7
+ };
8
+
9
+ export default function Layout(props: LayoutProps) {
10
+ return (
11
+ <html lang="en">
12
+ <head>
13
+ <title>{props.meta?.title ?? "Blokd App"}</title>
14
+ <meta
15
+ name="description"
16
+ content={props.meta?.description ?? "A minimal Blokd application."}
17
+ />
18
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
19
+ </head>
20
+
21
+ <body>
22
+ <header>
23
+ <nav>
24
+ <a href="/">Home</a>
25
+ {" · "}
26
+ <a href="/reports">Reports</a>
27
+ {" · "}
28
+ <a href="/contact">Contact</a>
29
+ </nav>
30
+ </header>
31
+
32
+ <main>{props.children}</main>
33
+ </body>
34
+ </html>
35
+ );
36
+ }
@@ -0,0 +1,23 @@
1
+ export const meta = () => ({
2
+ title: "About | Blokd App",
3
+ description: "About this minimal Blokd application."
4
+ });
5
+
6
+ export const runtime = "none";
7
+
8
+ export const budget = {
9
+ client: "0kb"
10
+ };
11
+
12
+ export default function About() {
13
+ return (
14
+ <section>
15
+ <h1>About</h1>
16
+
17
+ <p>
18
+ This route has no event handlers or islands, so Blokd can treat it as a
19
+ static/server-rendered route.
20
+ </p>
21
+ </section>
22
+ );
23
+ }
@@ -0,0 +1,58 @@
1
+ type ContactData = {
2
+ ok?: boolean;
3
+ email?: string;
4
+ error?: string;
5
+ };
6
+
7
+ type ContactProps = {
8
+ data?: ContactData;
9
+ };
10
+
11
+ export const meta = () => ({
12
+ title: "Contact | Blokd App",
13
+ description: "A native form route in a minimal Blokd application."
14
+ });
15
+
16
+ export const runtime = "none";
17
+
18
+ export const budget = {
19
+ client: "0kb"
20
+ };
21
+
22
+ export async function action({ request }: { request: Request }) {
23
+ const form = await request.formData();
24
+ const email = String(form.get("email") ?? "").trim();
25
+
26
+ if (!email.includes("@")) {
27
+ return {
28
+ ok: false,
29
+ error: "Enter a valid email."
30
+ };
31
+ }
32
+
33
+ return {
34
+ ok: true,
35
+ email
36
+ };
37
+ }
38
+
39
+ export default function Contact(props: ContactProps) {
40
+ return (
41
+ <section>
42
+ <h1>Contact</h1>
43
+
44
+ <p>This form submits with native browser POST and no client JavaScript.</p>
45
+
46
+ {props.data?.error ? <p role="alert">{props.data.error}</p> : null}
47
+ {props.data?.ok ? <p>{`Subscribed ${props.data.email}`}</p> : null}
48
+
49
+ <form method="post">
50
+ <label>
51
+ Email
52
+ <input name="email" type="email" required />
53
+ </label>
54
+ <button>Subscribe</button>
55
+ </form>
56
+ </section>
57
+ );
58
+ }
@@ -0,0 +1,46 @@
1
+ import { Island, on } from "blokd";
2
+
3
+ export const meta = () => ({
4
+ title: "Dashboard | Blokd App",
5
+ description: "A dashboard starter using route-local islands."
6
+ });
7
+
8
+ export default function Home() {
9
+ return (
10
+ <section>
11
+ <h1>Dashboard</h1>
12
+
13
+ <p>
14
+ Interactive controls are isolated to the dashboard route. Static routes
15
+ still ship no client runtime.
16
+ </p>
17
+
18
+ <Island name="counter" state={{ count: 0 }}>
19
+ <button
20
+ type="button"
21
+ onClick={on("/src/resumables/counter.ts#increment")}
22
+ >
23
+ Count: 0
24
+ </button>
25
+ </Island>
26
+
27
+ <Island name="demo-island" state={{ text: "Hello from Blokd" }}>
28
+ <button
29
+ type="button"
30
+ onClick={on("/src/resumables/demo.ts#show")}
31
+ >
32
+ Run resumable handler
33
+ </button>
34
+ </Island>
35
+
36
+ <Island name="status" state={{ text: "Queue healthy" }}>
37
+ <button
38
+ type="button"
39
+ onClick={on("/src/resumables/status.ts#refresh")}
40
+ >
41
+ Refresh status
42
+ </button>
43
+ </Island>
44
+ </section>
45
+ );
46
+ }
@@ -0,0 +1,37 @@
1
+ export const meta = () => ({
2
+ title: "Reports | Blokd App",
3
+ description: "A static dashboard report route."
4
+ });
5
+
6
+ export const runtime = "none";
7
+
8
+ export const budget = {
9
+ client: "0kb"
10
+ };
11
+
12
+ export default function Reports() {
13
+ return (
14
+ <section>
15
+ <h1>Reports</h1>
16
+ <p>This route is static and keeps the dashboard client runtime out.</p>
17
+ <table>
18
+ <thead>
19
+ <tr>
20
+ <th>Metric</th>
21
+ <th>Value</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <tr>
26
+ <td>Open tasks</td>
27
+ <td>12</td>
28
+ </tr>
29
+ <tr>
30
+ <td>Response time</td>
31
+ <td>42ms</td>
32
+ </tr>
33
+ </tbody>
34
+ </table>
35
+ </section>
36
+ );
37
+ }
@@ -0,0 +1,19 @@
1
+ import { Hono } from "hono";
2
+ import { createPages } from "blokd/hono";
3
+ import routes from "virtual:blokd/routes";
4
+
5
+ const app = new Hono();
6
+
7
+ app.get("/api/health", c => {
8
+ return c.json({ ok: true });
9
+ });
10
+
11
+ app.route(
12
+ "/",
13
+ createPages({
14
+ routes,
15
+ entryClient: "/src/entry-client.ts"
16
+ })
17
+ );
18
+
19
+ export default app;
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "blokd",
8
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
9
+ "types": ["node"],
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true
15
+ },
16
+ "include": ["src", "vite.config.ts"]
17
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vite";
2
+ import { blokd } from "blokd/vite";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ blokd({
7
+ routesDir: "src/routes",
8
+ clientEntry: "/src/entry-client.ts"
9
+ })
10
+ ]
11
+ });
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Blokd App</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ </head>
8
+ <body>
9
+ <p>This file is used by Vite during development.</p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "blokd-forms-app",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.0.0",
7
+ "scripts": {
8
+ "dev": "node scripts/dev.mjs",
9
+ "build": "vite build",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "blokd": "0.2.0-beta.0",
14
+ "hono": ">=4.5 <5"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": ">=20 <23",
18
+ "typescript": ">=5.5 <6",
19
+ "vite": ">=6 <9"
20
+ }
21
+ }