create-blokd 0.1.0-beta.2
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.
- package/README.md +454 -0
- package/bin/create-blokd.js +243 -0
- package/package.json +28 -0
- package/templates/minimal/index.html +11 -0
- package/templates/minimal/package.json +21 -0
- package/templates/minimal/src/blokd-env.d.ts +4 -0
- package/templates/minimal/src/entry-client.ts +3 -0
- package/templates/minimal/src/resumables/demo.ts +4 -0
- package/templates/minimal/src/routes/_404.tsx +11 -0
- package/templates/minimal/src/routes/_error.tsx +17 -0
- package/templates/minimal/src/routes/_layout.tsx +34 -0
- package/templates/minimal/src/routes/about.tsx +17 -0
- package/templates/minimal/src/routes/index.tsx +34 -0
- package/templates/minimal/src/server.ts +19 -0
- package/templates/minimal/tsconfig.json +17 -0
- package/templates/minimal/vite.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# create-blokd
|
|
2
|
+
|
|
3
|
+
Create a new Blokd project from the command line.
|
|
4
|
+
|
|
5
|
+
Blokd is a tiny Hono-native meta-framework for HTML-first applications, with Solid-familiar signals, Web APIs, SSR, file-based routing, and resumable islands.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Create a new project with pnpm:
|
|
10
|
+
|
|
11
|
+
~~~sh
|
|
12
|
+
pnpm create blokd my-app
|
|
13
|
+
~~~
|
|
14
|
+
|
|
15
|
+
Or use the explicit beta tag:
|
|
16
|
+
|
|
17
|
+
~~~sh
|
|
18
|
+
pnpm create blokd@beta my-app
|
|
19
|
+
~~~
|
|
20
|
+
|
|
21
|
+
With npm:
|
|
22
|
+
|
|
23
|
+
~~~sh
|
|
24
|
+
npm create blokd@beta my-app
|
|
25
|
+
~~~
|
|
26
|
+
|
|
27
|
+
With yarn:
|
|
28
|
+
|
|
29
|
+
~~~sh
|
|
30
|
+
yarn create blokd my-app
|
|
31
|
+
~~~
|
|
32
|
+
|
|
33
|
+
With bun:
|
|
34
|
+
|
|
35
|
+
~~~sh
|
|
36
|
+
bun create blokd my-app
|
|
37
|
+
~~~
|
|
38
|
+
|
|
39
|
+
Then start the app:
|
|
40
|
+
|
|
41
|
+
~~~sh
|
|
42
|
+
cd my-app
|
|
43
|
+
pnpm install
|
|
44
|
+
pnpm dev
|
|
45
|
+
~~~
|
|
46
|
+
|
|
47
|
+
Open the local URL printed by Vite.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
~~~txt
|
|
52
|
+
Usage:
|
|
53
|
+
npm create blokd@beta my-app
|
|
54
|
+
pnpm create blokd my-app
|
|
55
|
+
yarn create blokd my-app
|
|
56
|
+
bun create blokd my-app
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
--template <name> Template to use. Default: minimal
|
|
60
|
+
--install Install dependencies after creating the project
|
|
61
|
+
--no-install Do not install dependencies
|
|
62
|
+
--pm <name> Package manager: pnpm, npm, yarn, bun
|
|
63
|
+
-h, --help Show help
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
Create a minimal project:
|
|
69
|
+
|
|
70
|
+
~~~sh
|
|
71
|
+
pnpm create blokd my-app
|
|
72
|
+
~~~
|
|
73
|
+
|
|
74
|
+
Create a minimal project and install dependencies:
|
|
75
|
+
|
|
76
|
+
~~~sh
|
|
77
|
+
pnpm create blokd my-app --install
|
|
78
|
+
~~~
|
|
79
|
+
|
|
80
|
+
Create a project with an explicit template:
|
|
81
|
+
|
|
82
|
+
~~~sh
|
|
83
|
+
pnpm create blokd my-app --template minimal
|
|
84
|
+
~~~
|
|
85
|
+
|
|
86
|
+
Create a project and force npm as the package manager:
|
|
87
|
+
|
|
88
|
+
~~~sh
|
|
89
|
+
pnpm create blokd my-app --pm npm
|
|
90
|
+
~~~
|
|
91
|
+
|
|
92
|
+
Create a project without installing dependencies:
|
|
93
|
+
|
|
94
|
+
~~~sh
|
|
95
|
+
pnpm create blokd my-app --no-install
|
|
96
|
+
~~~
|
|
97
|
+
|
|
98
|
+
## Templates
|
|
99
|
+
|
|
100
|
+
### minimal
|
|
101
|
+
|
|
102
|
+
The default template is `minimal`.
|
|
103
|
+
|
|
104
|
+
It includes:
|
|
105
|
+
|
|
106
|
+
- Hono server entry
|
|
107
|
+
- Blokd Vite plugin
|
|
108
|
+
- file-based routes
|
|
109
|
+
- root layout
|
|
110
|
+
- custom 404 page
|
|
111
|
+
- custom error page
|
|
112
|
+
- client entry for resumable islands
|
|
113
|
+
- one interactive signal example
|
|
114
|
+
- one resumable island example
|
|
115
|
+
- one static route
|
|
116
|
+
|
|
117
|
+
Project structure:
|
|
118
|
+
|
|
119
|
+
~~~txt
|
|
120
|
+
my-app/
|
|
121
|
+
package.json
|
|
122
|
+
tsconfig.json
|
|
123
|
+
vite.config.ts
|
|
124
|
+
index.html
|
|
125
|
+
src/
|
|
126
|
+
server.ts
|
|
127
|
+
entry-client.ts
|
|
128
|
+
resumables/
|
|
129
|
+
demo.ts
|
|
130
|
+
routes/
|
|
131
|
+
_layout.tsx
|
|
132
|
+
_404.tsx
|
|
133
|
+
_error.tsx
|
|
134
|
+
index.tsx
|
|
135
|
+
about.tsx
|
|
136
|
+
~~~
|
|
137
|
+
|
|
138
|
+
## Generated project commands
|
|
139
|
+
|
|
140
|
+
Inside the generated project:
|
|
141
|
+
|
|
142
|
+
~~~sh
|
|
143
|
+
pnpm dev
|
|
144
|
+
~~~
|
|
145
|
+
|
|
146
|
+
Starts the development server.
|
|
147
|
+
|
|
148
|
+
~~~sh
|
|
149
|
+
pnpm build
|
|
150
|
+
~~~
|
|
151
|
+
|
|
152
|
+
Builds the project.
|
|
153
|
+
|
|
154
|
+
~~~sh
|
|
155
|
+
pnpm typecheck
|
|
156
|
+
~~~
|
|
157
|
+
|
|
158
|
+
Runs TypeScript without emitting files.
|
|
159
|
+
|
|
160
|
+
## Generated project dependencies
|
|
161
|
+
|
|
162
|
+
The generated minimal starter includes:
|
|
163
|
+
|
|
164
|
+
~~~json
|
|
165
|
+
{
|
|
166
|
+
"dependencies": {
|
|
167
|
+
"blokd": "beta",
|
|
168
|
+
"hono": ">=4.5 <5"
|
|
169
|
+
},
|
|
170
|
+
"devDependencies": {
|
|
171
|
+
"@types/node": ">=20 <23",
|
|
172
|
+
"typescript": ">=5.5 <6",
|
|
173
|
+
"vite": ">=5 <9"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
~~~
|
|
177
|
+
|
|
178
|
+
`blokd` is installed from the beta dist-tag because Blokd is currently in public beta.
|
|
179
|
+
|
|
180
|
+
`hono` is included as an application dependency because Blokd apps use Hono directly for APIs, middleware, routing, cookies, headers, and deployment-specific server behavior.
|
|
181
|
+
|
|
182
|
+
`vite`, `typescript`, and `@types/node` are development dependencies because the starter uses Vite and TypeScript.
|
|
183
|
+
|
|
184
|
+
## Beta status
|
|
185
|
+
|
|
186
|
+
Blokd is currently in public beta.
|
|
187
|
+
|
|
188
|
+
The beta is intended for:
|
|
189
|
+
|
|
190
|
+
- experimentation
|
|
191
|
+
- early adopters
|
|
192
|
+
- small websites
|
|
193
|
+
- documentation sites
|
|
194
|
+
- local business websites
|
|
195
|
+
- simple dashboards
|
|
196
|
+
- Hono-backed SSR applications
|
|
197
|
+
- form-driven applications
|
|
198
|
+
- progressively enhanced web apps
|
|
199
|
+
|
|
200
|
+
The beta is not yet intended for:
|
|
201
|
+
|
|
202
|
+
- large production migrations
|
|
203
|
+
- highly regulated applications
|
|
204
|
+
- applications requiring long-term API stability
|
|
205
|
+
- complex full-app client routing
|
|
206
|
+
- deep ecosystem integrations
|
|
207
|
+
|
|
208
|
+
APIs may change before a stable `1.0.0` release.
|
|
209
|
+
|
|
210
|
+
## What Blokd is optimized for
|
|
211
|
+
|
|
212
|
+
Blokd is optimized for HTML-first applications.
|
|
213
|
+
|
|
214
|
+
That means:
|
|
215
|
+
|
|
216
|
+
- server-rendered pages by default
|
|
217
|
+
- Web APIs instead of framework-specific request abstractions
|
|
218
|
+
- Hono for server composition
|
|
219
|
+
- file-based routing for pages
|
|
220
|
+
- native forms for mutations
|
|
221
|
+
- small client JavaScript payloads
|
|
222
|
+
- resumable islands for focused interactivity
|
|
223
|
+
- no React dependency
|
|
224
|
+
- no virtual DOM dependency
|
|
225
|
+
|
|
226
|
+
## What Blokd is not
|
|
227
|
+
|
|
228
|
+
Blokd is not a full replacement for every app framework.
|
|
229
|
+
|
|
230
|
+
It is not:
|
|
231
|
+
|
|
232
|
+
- a React framework
|
|
233
|
+
- a full Qwik-compatible resumability system
|
|
234
|
+
- a full client-side app router
|
|
235
|
+
- a batteries-included CMS framework
|
|
236
|
+
- a mature ecosystem equivalent to Next.js, Astro, Remix, or SvelteKit
|
|
237
|
+
|
|
238
|
+
The goal is a small, explicit, Web Platform-oriented framework.
|
|
239
|
+
|
|
240
|
+
## Minimal starter walkthrough
|
|
241
|
+
|
|
242
|
+
The generated app has two public pages:
|
|
243
|
+
|
|
244
|
+
~~~txt
|
|
245
|
+
src/routes/index.tsx -> /
|
|
246
|
+
src/routes/about.tsx -> /about
|
|
247
|
+
~~~
|
|
248
|
+
|
|
249
|
+
It also has three special route files:
|
|
250
|
+
|
|
251
|
+
~~~txt
|
|
252
|
+
src/routes/_layout.tsx -> root document layout
|
|
253
|
+
src/routes/_404.tsx -> custom not found page
|
|
254
|
+
src/routes/_error.tsx -> custom error page
|
|
255
|
+
~~~
|
|
256
|
+
|
|
257
|
+
The root page demonstrates:
|
|
258
|
+
|
|
259
|
+
- `signal`
|
|
260
|
+
- an inline event handler
|
|
261
|
+
- `Island`
|
|
262
|
+
- `resumable`
|
|
263
|
+
|
|
264
|
+
The about page demonstrates a static server-rendered page with no client interactivity.
|
|
265
|
+
|
|
266
|
+
## Hono server entry
|
|
267
|
+
|
|
268
|
+
The generated app uses Hono as the server foundation:
|
|
269
|
+
|
|
270
|
+
~~~ts
|
|
271
|
+
import { Hono } from "hono";
|
|
272
|
+
import { createPages } from "blokd/hono";
|
|
273
|
+
import routes from "virtual:blokd/routes";
|
|
274
|
+
|
|
275
|
+
const app = new Hono();
|
|
276
|
+
|
|
277
|
+
app.get("/api/health", c => {
|
|
278
|
+
return c.json({ ok: true });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
app.route(
|
|
282
|
+
"/",
|
|
283
|
+
createPages({
|
|
284
|
+
routes,
|
|
285
|
+
entryClient: "/src/entry-client.ts"
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
export default app;
|
|
290
|
+
~~~
|
|
291
|
+
|
|
292
|
+
Use Hono directly for:
|
|
293
|
+
|
|
294
|
+
- API routes
|
|
295
|
+
- middleware
|
|
296
|
+
- authentication
|
|
297
|
+
- sessions
|
|
298
|
+
- cookies
|
|
299
|
+
- headers
|
|
300
|
+
- webhooks
|
|
301
|
+
- deployment adapters
|
|
302
|
+
|
|
303
|
+
## Vite config
|
|
304
|
+
|
|
305
|
+
The generated app uses the Blokd Vite plugin:
|
|
306
|
+
|
|
307
|
+
~~~ts
|
|
308
|
+
import { defineConfig } from "vite";
|
|
309
|
+
import { blokd } from "blokd/vite";
|
|
310
|
+
|
|
311
|
+
export default defineConfig({
|
|
312
|
+
plugins: [
|
|
313
|
+
blokd({
|
|
314
|
+
routesDir: "src/routes",
|
|
315
|
+
clientEntry: "/src/entry-client.ts"
|
|
316
|
+
})
|
|
317
|
+
]
|
|
318
|
+
});
|
|
319
|
+
~~~
|
|
320
|
+
|
|
321
|
+
The Vite plugin is responsible for:
|
|
322
|
+
|
|
323
|
+
- route manifest generation
|
|
324
|
+
- JSX transform
|
|
325
|
+
- static route analysis
|
|
326
|
+
- virtual route module support
|
|
327
|
+
|
|
328
|
+
## Resumable islands
|
|
329
|
+
|
|
330
|
+
The starter includes a minimal resumable island:
|
|
331
|
+
|
|
332
|
+
~~~tsx
|
|
333
|
+
<Island name="demo-island" state={{ message: "Hello from Blokd" }}>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
data-output
|
|
337
|
+
onClick={resumable("/src/resumables/demo.ts#sayHello")}
|
|
338
|
+
>
|
|
339
|
+
Run resumable handler
|
|
340
|
+
</button>
|
|
341
|
+
</Island>
|
|
342
|
+
~~~
|
|
343
|
+
|
|
344
|
+
The corresponding handler is:
|
|
345
|
+
|
|
346
|
+
~~~ts
|
|
347
|
+
export function sayHello(event: Event, ctx: any) {
|
|
348
|
+
const button = event.currentTarget as HTMLButtonElement;
|
|
349
|
+
button.textContent = ctx.state.message;
|
|
350
|
+
}
|
|
351
|
+
~~~
|
|
352
|
+
|
|
353
|
+
Blokd resumability is island-scoped. It is not full application graph serialization.
|
|
354
|
+
|
|
355
|
+
## Static route behavior
|
|
356
|
+
|
|
357
|
+
Blokd can analyze routes to determine whether they need the client entry.
|
|
358
|
+
|
|
359
|
+
A route with event handlers, signals, effects, `Island`, or `resumable` needs client behavior.
|
|
360
|
+
|
|
361
|
+
A route without client markers can be server-rendered without framework client JavaScript.
|
|
362
|
+
|
|
363
|
+
This keeps simple pages small by default.
|
|
364
|
+
|
|
365
|
+
## Package manager behavior
|
|
366
|
+
|
|
367
|
+
`create-blokd` detects the package manager from the current npm user agent when possible.
|
|
368
|
+
|
|
369
|
+
Fallback package manager:
|
|
370
|
+
|
|
371
|
+
~~~txt
|
|
372
|
+
pnpm
|
|
373
|
+
~~~
|
|
374
|
+
|
|
375
|
+
You can override this with:
|
|
376
|
+
|
|
377
|
+
~~~sh
|
|
378
|
+
pnpm create blokd my-app --pm npm
|
|
379
|
+
pnpm create blokd my-app --pm pnpm
|
|
380
|
+
pnpm create blokd my-app --pm yarn
|
|
381
|
+
pnpm create blokd my-app --pm bun
|
|
382
|
+
~~~
|
|
383
|
+
|
|
384
|
+
## Installing automatically
|
|
385
|
+
|
|
386
|
+
By default, the CLI creates the project and prints next steps.
|
|
387
|
+
|
|
388
|
+
To install dependencies immediately:
|
|
389
|
+
|
|
390
|
+
~~~sh
|
|
391
|
+
pnpm create blokd my-app --install
|
|
392
|
+
~~~
|
|
393
|
+
|
|
394
|
+
To explicitly skip installation:
|
|
395
|
+
|
|
396
|
+
~~~sh
|
|
397
|
+
pnpm create blokd my-app --no-install
|
|
398
|
+
~~~
|
|
399
|
+
|
|
400
|
+
## Directory safety
|
|
401
|
+
|
|
402
|
+
`create-blokd` refuses to write into a non-empty target directory.
|
|
403
|
+
|
|
404
|
+
This prevents accidentally overwriting an existing project.
|
|
405
|
+
|
|
406
|
+
Valid project names may contain:
|
|
407
|
+
|
|
408
|
+
- letters
|
|
409
|
+
- numbers
|
|
410
|
+
- dots
|
|
411
|
+
- dashes
|
|
412
|
+
- underscores
|
|
413
|
+
|
|
414
|
+
## Publishing
|
|
415
|
+
|
|
416
|
+
This package is published as:
|
|
417
|
+
|
|
418
|
+
~~~txt
|
|
419
|
+
create-blokd
|
|
420
|
+
~~~
|
|
421
|
+
|
|
422
|
+
The command:
|
|
423
|
+
|
|
424
|
+
~~~sh
|
|
425
|
+
npm create blokd
|
|
426
|
+
~~~
|
|
427
|
+
|
|
428
|
+
resolves to the `create-blokd` package.
|
|
429
|
+
|
|
430
|
+
For beta releases, publish with:
|
|
431
|
+
|
|
432
|
+
~~~sh
|
|
433
|
+
npm publish --tag beta
|
|
434
|
+
~~~
|
|
435
|
+
|
|
436
|
+
Do not publish beta releases without the beta dist-tag.
|
|
437
|
+
|
|
438
|
+
## Related packages
|
|
439
|
+
|
|
440
|
+
Main framework package:
|
|
441
|
+
|
|
442
|
+
~~~sh
|
|
443
|
+
pnpm add blokd@beta hono
|
|
444
|
+
~~~
|
|
445
|
+
|
|
446
|
+
Create package:
|
|
447
|
+
|
|
448
|
+
~~~sh
|
|
449
|
+
pnpm create blokd@beta my-app
|
|
450
|
+
~~~
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
MIT
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const packageRoot = resolve(__dirname, "..");
|
|
12
|
+
const templatesRoot = join(packageRoot, "templates");
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
|
|
16
|
+
const help = `
|
|
17
|
+
create-blokd
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
npm create blokd@beta my-app
|
|
21
|
+
pnpm create blokd my-app
|
|
22
|
+
yarn create blokd my-app
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--template <name> Template to use. Default: minimal
|
|
26
|
+
--install Install dependencies after creating the project
|
|
27
|
+
--no-install Do not install dependencies
|
|
28
|
+
--pm <name> Package manager: pnpm, npm, yarn, bun
|
|
29
|
+
-h, --help Show help
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
pnpm create blokd my-app
|
|
33
|
+
pnpm create blokd my-app --template minimal
|
|
34
|
+
pnpm create blokd my-app --install
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function main() {
|
|
38
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
39
|
+
console.log(help.trim());
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parsed = parseArgs(args);
|
|
44
|
+
|
|
45
|
+
if (!parsed.name) {
|
|
46
|
+
console.error("Missing project name.");
|
|
47
|
+
console.error("");
|
|
48
|
+
console.error("Usage:");
|
|
49
|
+
console.error(" pnpm create blokd my-app");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const templateName = parsed.template ?? "minimal";
|
|
54
|
+
const templateDir = join(templatesRoot, templateName);
|
|
55
|
+
|
|
56
|
+
if (!existsSync(templateDir)) {
|
|
57
|
+
console.error(`Unknown template: ${templateName}`);
|
|
58
|
+
console.error("");
|
|
59
|
+
console.error("Available templates:");
|
|
60
|
+
for (const name of listTemplates()) {
|
|
61
|
+
console.error(` - ${name}`);
|
|
62
|
+
}
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const targetDir = resolve(process.cwd(), parsed.name);
|
|
67
|
+
|
|
68
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
69
|
+
console.error(`Target directory is not empty: ${targetDir}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
mkdirSync(targetDir, { recursive: true });
|
|
74
|
+
copyDirectory(templateDir, targetDir);
|
|
75
|
+
|
|
76
|
+
updatePackageName(join(targetDir, "package.json"), parsed.name);
|
|
77
|
+
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log(`Created ${parsed.name} with the ${templateName} template.`);
|
|
80
|
+
console.log("");
|
|
81
|
+
|
|
82
|
+
const packageManager = parsed.pm || detectPackageManager();
|
|
83
|
+
|
|
84
|
+
if (parsed.install) {
|
|
85
|
+
console.log(`Installing dependencies with ${packageManager}...`);
|
|
86
|
+
const result = spawnSync(packageManager, ["install"], {
|
|
87
|
+
cwd: targetDir,
|
|
88
|
+
stdio: "inherit",
|
|
89
|
+
shell: process.platform === "win32"
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
console.error("");
|
|
94
|
+
console.error("Dependency installation failed.");
|
|
95
|
+
console.error(`Run manually: cd ${parsed.name} && ${packageManager} install`);
|
|
96
|
+
process.exit(result.status ?? 1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log("Done.");
|
|
101
|
+
console.log("");
|
|
102
|
+
console.log(`Next steps:`);
|
|
103
|
+
console.log(` cd ${parsed.name}`);
|
|
104
|
+
console.log(` ${packageManager} dev`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log("Next steps:");
|
|
109
|
+
console.log(` cd ${parsed.name}`);
|
|
110
|
+
console.log(` ${packageManager} install`);
|
|
111
|
+
console.log(` ${packageManager} dev`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseArgs(argv) {
|
|
115
|
+
const parsed = {
|
|
116
|
+
name: "",
|
|
117
|
+
template: "minimal",
|
|
118
|
+
install: false,
|
|
119
|
+
pm: undefined
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < argv.length; i++) {
|
|
123
|
+
const arg = argv[i];
|
|
124
|
+
|
|
125
|
+
if (arg === "--template") {
|
|
126
|
+
parsed.template = argv[++i];
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (arg.startsWith("--template=")) {
|
|
131
|
+
parsed.template = arg.slice("--template=".length);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (arg === "--install") {
|
|
136
|
+
parsed.install = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (arg === "--no-install") {
|
|
141
|
+
parsed.install = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (arg === "--pm") {
|
|
146
|
+
parsed.pm = argv[++i];
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (arg.startsWith("--pm=")) {
|
|
151
|
+
parsed.pm = arg.slice("--pm=".length);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!arg.startsWith("-") && !parsed.name) {
|
|
156
|
+
parsed.name = arg;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!parsed.name) parsed.name = "my-blokd-app";
|
|
162
|
+
|
|
163
|
+
validateProjectName(parsed.name);
|
|
164
|
+
|
|
165
|
+
if (parsed.pm && !["pnpm", "npm", "yarn", "bun"].includes(parsed.pm)) {
|
|
166
|
+
console.error(`Unsupported package manager: ${parsed.pm}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateProjectName(name) {
|
|
174
|
+
if (name === "." || name === "./") return;
|
|
175
|
+
|
|
176
|
+
const base = name.split(/[\\/]/).filter(Boolean).pop() ?? name;
|
|
177
|
+
|
|
178
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(base)) {
|
|
179
|
+
console.error(`Invalid project name: ${name}`);
|
|
180
|
+
console.error("Use letters, numbers, dots, dashes, or underscores.");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function detectPackageManager() {
|
|
186
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
187
|
+
|
|
188
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
189
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
190
|
+
if (ua.startsWith("bun")) return "bun";
|
|
191
|
+
if (ua.startsWith("npm")) return "npm";
|
|
192
|
+
|
|
193
|
+
return "pnpm";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function listTemplates() {
|
|
197
|
+
if (!existsSync(templatesRoot)) return [];
|
|
198
|
+
return readdirSync(templatesRoot).filter(name => {
|
|
199
|
+
const full = join(templatesRoot, name);
|
|
200
|
+
return statSync(full).isDirectory();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function copyDirectory(from, to) {
|
|
205
|
+
mkdirSync(to, { recursive: true });
|
|
206
|
+
|
|
207
|
+
for (const entry of readdirSync(from)) {
|
|
208
|
+
const source = join(from, entry);
|
|
209
|
+
const target = join(to, entry);
|
|
210
|
+
|
|
211
|
+
const stat = statSync(source);
|
|
212
|
+
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
copyDirectory(source, target);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const content = readFileSync(source);
|
|
219
|
+
writeFileSync(target, content);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updatePackageName(packageJsonPath, projectName) {
|
|
224
|
+
if (!existsSync(packageJsonPath)) return;
|
|
225
|
+
|
|
226
|
+
const json = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
227
|
+
const baseName = projectName.split(/[\\/]/).filter(Boolean).pop() ?? projectName;
|
|
228
|
+
|
|
229
|
+
json.name = sanitizePackageName(baseName);
|
|
230
|
+
|
|
231
|
+
writeFileSync(packageJsonPath, `${JSON.stringify(json, null, 2)}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sanitizePackageName(name) {
|
|
235
|
+
return name
|
|
236
|
+
.trim()
|
|
237
|
+
.toLowerCase()
|
|
238
|
+
.replace(/[^a-z0-9._-]/g, "-")
|
|
239
|
+
.replace(/^[._-]+/, "")
|
|
240
|
+
.replace(/[._-]+$/, "") || "my-blokd-app";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-blokd",
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"description": "Create a new Blokd project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-blokd": "./bin/create-blokd.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"blokd",
|
|
18
|
+
"create-blokd",
|
|
19
|
+
"starter",
|
|
20
|
+
"template",
|
|
21
|
+
"hono",
|
|
22
|
+
"vite",
|
|
23
|
+
"ssr"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.19"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -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-minimal-app",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@11.0.0",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite --host 0.0.0.0",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"blokd": "0.1.0-beta.2",
|
|
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,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,34 @@
|
|
|
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
|
+
</nav>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<main>{props.children}</main>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const meta = () => ({
|
|
2
|
+
title: "About | Blokd App",
|
|
3
|
+
description: "About this minimal Blokd application."
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export default function About() {
|
|
7
|
+
return (
|
|
8
|
+
<section>
|
|
9
|
+
<h1>About</h1>
|
|
10
|
+
|
|
11
|
+
<p>
|
|
12
|
+
This route has no event handlers or islands, so Blokd can treat it as a
|
|
13
|
+
static/server-rendered route.
|
|
14
|
+
</p>
|
|
15
|
+
</section>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { signal, Island, resumable } from "blokd";
|
|
2
|
+
|
|
3
|
+
export const meta = () => ({
|
|
4
|
+
title: "Blokd App",
|
|
5
|
+
description: "A minimal Blokd application."
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default function Home() {
|
|
9
|
+
const [count, setCount] = signal(0);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section>
|
|
13
|
+
<h1>Blokd App</h1>
|
|
14
|
+
|
|
15
|
+
<p>
|
|
16
|
+
This page demonstrates Solid-familiar signals and a resumable island.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<button onClick={() => setCount(c => c + 1)}>
|
|
20
|
+
Count: {count()}
|
|
21
|
+
</button>
|
|
22
|
+
|
|
23
|
+
<Island name="demo-island" state={{ message: "Hello from Blokd" }}>
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
data-output
|
|
27
|
+
onClick={resumable("/src/resumables/demo.ts#sayHello")}
|
|
28
|
+
>
|
|
29
|
+
Run resumable handler
|
|
30
|
+
</button>
|
|
31
|
+
</Island>
|
|
32
|
+
</section>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|