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
@@ -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,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="/contact">Contact</a>
27
+ {" · "}
28
+ <a href="/newsletter">Newsletter</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,28 @@
1
+ export const meta = () => ({
2
+ title: "Forms | Blokd App",
3
+ description: "Native form examples in Blokd."
4
+ });
5
+
6
+ export const runtime = "none";
7
+
8
+ export const budget = {
9
+ client: "0kb"
10
+ };
11
+
12
+ export default function Home() {
13
+ return (
14
+ <section>
15
+ <h1>Forms</h1>
16
+
17
+ <p>
18
+ This starter demonstrates native forms and route actions without client
19
+ JavaScript.
20
+ </p>
21
+
22
+ <ul>
23
+ <li><a href="/contact">Contact form</a></li>
24
+ <li><a href="/newsletter">Newsletter form</a></li>
25
+ </ul>
26
+ </section>
27
+ );
28
+ }
@@ -0,0 +1,50 @@
1
+ type NewsletterData = {
2
+ ok?: boolean;
3
+ email?: string;
4
+ error?: string;
5
+ };
6
+
7
+ type NewsletterProps = {
8
+ data?: NewsletterData;
9
+ };
10
+
11
+ export const meta = () => ({
12
+ title: "Newsletter | Blokd App",
13
+ description: "A native newsletter form."
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 { ok: false, error: "Enter a valid email." };
28
+ }
29
+
30
+ return { ok: true, email };
31
+ }
32
+
33
+ export default function Newsletter(props: NewsletterProps) {
34
+ return (
35
+ <section>
36
+ <h1>Newsletter</h1>
37
+
38
+ {props.data?.error ? <p role="alert">{props.data.error}</p> : null}
39
+ {props.data?.ok ? <p>{`Subscribed ${props.data.email}`}</p> : null}
40
+
41
+ <form method="post">
42
+ <label>
43
+ Email
44
+ <input name="email" type="email" required />
45
+ </label>
46
+ <button>Subscribe</button>
47
+ </form>
48
+ </section>
49
+ );
50
+ }
@@ -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-marketing-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,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,38 @@
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="/about">About</a>
27
+ {" · "}
28
+ <a href="/pricing">Pricing</a>
29
+ {" · "}
30
+ <a href="/contact">Contact</a>
31
+ </nav>
32
+ </header>
33
+
34
+ <main>{props.children}</main>
35
+ </body>
36
+ </html>
37
+ );
38
+ }
@@ -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
+ }