create-vesk 0.1.0

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 (3) hide show
  1. package/README.md +16 -0
  2. package/package.json +27 -0
  3. package/src/index.js +579 -0
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # create-vesk
2
+
3
+ Scaffold a new Vesk project with zero configuration.
4
+
5
+ ## Usage
6
+
7
+ ```sh
8
+ npx create-vesk my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ ## License
15
+
16
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "create-vesk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Create a new Vesk project with zero configuration",
6
+ "bin": {
7
+ "create-vesk": "./src/index.js"
8
+ },
9
+ "dependencies": {},
10
+ "engines": {
11
+ "node": ">=20.0.0"
12
+ },
13
+ "license": "MIT",
14
+ "author": "emeraldlinks",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/emeraldlinks/veskTs.git"
18
+ },
19
+ "homepage": "https://github.com/emeraldlinks/veskTs#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/emeraldlinks/veskTs/issues"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "README.md"
26
+ ]
27
+ }
package/src/index.js ADDED
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync } from 'fs'
3
+ import { join, basename, resolve } from 'path'
4
+ import { argv, cwd, exit } from 'process'
5
+
6
+ const args = argv.slice(2)
7
+ const projectName = args[0]
8
+
9
+ if (!projectName || projectName.startsWith('-')) {
10
+ console.error('Usage: npx create-vesk@latest <project-name>')
11
+ exit(1)
12
+ }
13
+
14
+ const targetDir = resolve(cwd(), projectName)
15
+
16
+ if (existsSync(targetDir)) {
17
+ console.error(`Error: directory "${projectName}" already exists`)
18
+ exit(1)
19
+ }
20
+
21
+ const pkgName = basename(projectName)
22
+ const appDirPath = join(targetDir, 'app')
23
+ const srcDir = join(targetDir, 'src')
24
+
25
+ const dirs = [
26
+ appDirPath,
27
+ join(appDirPath, 'about'),
28
+ join(appDirPath, 'blog'),
29
+ join(appDirPath, 'blog', '[slug]'),
30
+ join(appDirPath, 'posts'),
31
+ join(appDirPath, 'statements'),
32
+ join(appDirPath, 'api', 'posts'),
33
+ join(appDirPath, 'api', 'hello'),
34
+ join(appDirPath, 'api', 'echo', '[msg]'),
35
+ srcDir,
36
+ join(targetDir, 'public'),
37
+ ]
38
+ for (const d of dirs) mkdirSync(d, { recursive: true })
39
+
40
+ // ── package.json ──
41
+ // haul is the default engine: `haul dev` / `haul build` / `haul start` run the
42
+ // native binary (embedded compiler sidecar, no VESK_SIDECAR needed). The
43
+ // `vesk` package provides both the `haul` dispatcher and the JS `vesk` CLI.
44
+ writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
45
+ name: pkgName,
46
+ private: true,
47
+ type: 'module',
48
+ scripts: {
49
+ dev: 'haul dev',
50
+ build: 'haul build',
51
+ start: 'haul start',
52
+ typecheck: 'tsc --noEmit',
53
+ 'dev:vesk': 'vesk dev',
54
+ 'build:vesk': 'vesk build',
55
+ 'start:vesk': 'vesk start',
56
+ },
57
+ dependencies: {
58
+ '@vesk/compiler': '^0.1.0',
59
+ '@vesk/runtime': '^0.1.0',
60
+ '@vesk/vesk-cli': '^0.1.0',
61
+ '@vesk/adapter': '^0.1.0',
62
+ '@vesk/plugin-tailwind': '^0.1.0',
63
+ },
64
+ optionalDependencies: {
65
+ '@vesk/haul-darwin-arm64': '^0.1.0',
66
+ '@vesk/haul-darwin-x64': '^0.1.0',
67
+ '@vesk/haul-linux-arm64': '^0.1.0',
68
+ '@vesk/haul-linux-x64': '^0.1.0',
69
+ '@vesk/haul-win32-x64': '^0.1.0',
70
+ },
71
+ devDependencies: {
72
+ tailwindcss: '^4.0.0',
73
+ typescript: '^5.8.0',
74
+ },
75
+ }, null, 2) + '\n')
76
+
77
+ // ── vesk.config.ts ──
78
+ writeFileSync(join(targetDir, 'vesk.config.ts'), [
79
+ `import { defineConfig, preset } from '@vesk/compiler'`,
80
+ `import tailwindcss from '@vesk/plugin-tailwind'`,
81
+ ``,
82
+ `export default defineConfig({`,
83
+ `\tappDir: './app',`,
84
+ `\toutDir: './dist',`,
85
+ `\tpublicDir: './public',`,
86
+ `\t// security: 'strict', // preset string ("strict"|"minimal"|"off")`,
87
+ `\t// security: preset('production'), // environment preset`,
88
+ `\tsecurity: preset('production', { // preset + overrides`,
89
+ `\t\ttrustProxy: true, // set to true if behind nginx/Cloudflare`,
90
+ `\t\t// rateLimit: { windowMs: 60000, max: 100 },`,
91
+ `\t\t// cors: { origin: ['https://app.example.com'] },`,
92
+ `\t}),`,
93
+ `\tplugins: [`,
94
+ `\t\ttailwindcss({ entry: 'src/global.css', appDir: 'app' }),`,
95
+ `\t],`,
96
+ `\tssg: {},`,
97
+ `});`,
98
+ '',
99
+ ].join('\n'))
100
+
101
+ // ── tsconfig.json ──
102
+ writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
103
+ compilerOptions: {
104
+ target: 'ES2022',
105
+ module: 'ESNext',
106
+ moduleResolution: 'bundler',
107
+ allowJs: true,
108
+ checkJs: true,
109
+ noEmit: true,
110
+ strict: true,
111
+ esModuleInterop: true,
112
+ skipLibCheck: true,
113
+ forceConsistentCasingInFileNames: true,
114
+ resolveJsonModule: true,
115
+ jsx: 'preserve',
116
+ jsxImportSource: '@vesk/compiler',
117
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
118
+ baseUrl: '.',
119
+ paths: {
120
+ '@/*': ['./src/*'],
121
+ '@app/*': ['./app/*'],
122
+ },
123
+ },
124
+ include: ['**/*.vsk', '**/*.js', '**/*.ts'],
125
+ exclude: ['node_modules', 'dist'],
126
+ }, null, 2) + '\n')
127
+
128
+ // ── src/global.css ──
129
+ writeFileSync(join(srcDir, 'global.css'), [
130
+ `@import 'tailwindcss';`,
131
+ ``,
132
+ `@layer base {`,
133
+ `\thtml { scroll-behavior: smooth; }`,
134
+ `}`,
135
+ '',
136
+ ].join('\n'))
137
+
138
+ // ── app/layout.vsk ──
139
+ writeFileSync(join(appDirPath, 'layout.vsk'), [
140
+ `import { NavLink } from '@vesk/runtime';`,
141
+ ``,
142
+ `component Layout(props) {`,
143
+ `\t<nav class="flex gap-6 px-8 py-4 border-b border-gray-200 bg-white">`,
144
+ `\t\t<NavLink href="/" class="text-gray-500 hover:text-black font-medium no-underline">Home</NavLink>`,
145
+ `\t\t<NavLink href="/about" class="text-gray-500 hover:text-black font-medium no-underline">About</NavLink>`,
146
+ `\t\t<NavLink href="/blog" class="text-gray-500 hover:text-black font-medium no-underline">Blog</NavLink>`,
147
+ `\t\t<NavLink href="/posts" class="text-gray-500 hover:text-black font-medium no-underline">Posts</NavLink>`,
148
+ `\t\t<NavLink href="/statements" class="text-gray-500 hover:text-black font-medium no-underline">Statements</NavLink>`,
149
+ `\t</nav>`,
150
+ `\t<main class="max-w-3xl mx-auto my-8 px-4">{props.children}</main>`,
151
+ `\t<footer class="text-center py-8 text-gray-400 text-sm">`,
152
+ `\t\t<p>Powered by Vesk</p>`,
153
+ `\t</footer>`,
154
+ `}`,
155
+ '',
156
+ ].join('\n'))
157
+
158
+ // ── app/page.vsk ──
159
+ writeFileSync(join(appDirPath, 'page.vsk'), [
160
+ `import { track } from '@vesk/runtime'`,
161
+ ``,
162
+ `component Home {`,
163
+ `\t<Head>`,
164
+ `\t\t<title>${pkgName}</title>`,
165
+ `\t</Head>`,
166
+ `\tconst &[count] = track(0)`,
167
+ ``,
168
+ `\t<h1 class="text-4xl font-bold mb-2">Welcome to Vesk</h1>`,
169
+ `\t<p class="text-gray-500 mb-4">`,
170
+ `\t\tA compiler-first reactive UI framework for the post-VDOM web.`,
171
+ `\t</p>`,
172
+ `\t<p class="text-2xl font-semibold">count: {count}</p>`,
173
+ `\t<button onClick={() => count++} class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg mr-2">+</button>`,
174
+ `\t<button onClick={() => count--} class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">-</button>`,
175
+ `\t<div class="bg-white rounded-xl p-6 mt-8 shadow-sm border border-gray-100">`,
176
+ `\t\t<h2 class="text-xl font-semibold mb-2">Getting Started</h2>`,
177
+ `\t\t<p>Edit <code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono">app/page.vsk</code> to change this page.</p>`,
178
+ `\t</div>`,
179
+ `}`,
180
+ '',
181
+ ].join('\n'))
182
+
183
+ // ── app/about/page.vsk ──
184
+ writeFileSync(join(appDirPath, 'about', 'page.vsk'), [
185
+ `component About {`,
186
+ `\t<h1 class="text-3xl font-bold mb-4">About Vesk</h1>`,
187
+ `\t<p class="text-gray-600 mb-3">`,
188
+ `\t\tVesk is a compiler-first reactive UI framework. It compiles .vsk components`,
189
+ `\t\tto standard ESM with SSR, hydration, and fine-grained reactivity.`,
190
+ `\t</p>`,
191
+ `\t<p class="text-gray-600 mb-3">`,
192
+ `\t\tKey features include zero-JS pages, islands architecture, AOT event delegation,`,
193
+ `\t\tand streaming SSR.`,
194
+ `\t</p>`,
195
+ `}`,
196
+ '',
197
+ ].join('\n'))
198
+
199
+ // ── app/blog/page.vsk ──
200
+ writeFileSync(join(appDirPath, 'blog', 'page.vsk'), [
201
+ `import { Link } from '@vesk/runtime';`,
202
+ ``,
203
+ `component Blog {`,
204
+ `\t<h1 class="text-3xl font-bold mb-4">Blog</h1>`,
205
+ `\t<div class="bg-white rounded-lg p-5 mb-4 shadow-sm border border-gray-100">`,
206
+ `\t\t<h2 class="text-lg font-semibold mb-1">`,
207
+ `\t\t\t<Link href="/blog/hello-world" class="text-gray-900 no-underline hover:text-blue-600">Hello World</Link>`,
208
+ `\t\t</h2>`,
209
+ `\t\t<p class="text-gray-400 text-sm">First post powered by Vesk</p>`,
210
+ `\t</div>`,
211
+ `\t<div class="bg-white rounded-lg p-5 mb-4 shadow-sm border border-gray-100">`,
212
+ `\t\t<h2 class="text-lg font-semibold mb-1">`,
213
+ `\t\t\t<Link href="/blog/ssr-in-vesk" class="text-gray-900 no-underline hover:text-blue-600">SSR in Vesk</Link>`,
214
+ `\t\t</h2>`,
215
+ `\t\t<p class="text-gray-400 text-sm">How server-side rendering works</p>`,
216
+ `\t</div>`,
217
+ `}`,
218
+ '',
219
+ ].join('\n'))
220
+
221
+ // ── app/blog/[slug]/page.vsk ──
222
+ writeFileSync(join(appDirPath, 'blog', '[slug]', 'page.vsk'), [
223
+ `import { Link } from '@vesk/runtime';`,
224
+ ``,
225
+ `component BlogPost(props: { params: { slug: string } }) {`,
226
+ `\t<Link href="/blog" class="inline-block mb-6 text-blue-600 no-underline hover:underline">`,
227
+ `\t\t← Back to blog`,
228
+ `\t</Link>`,
229
+ `\t<h1 class="text-3xl font-bold mb-2">Post: {props.params.slug}</h1>`,
230
+ `\t<div class="text-gray-600 leading-relaxed">`,
231
+ `\t\t<p>This is a dynamic blog post rendered at <code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono">/{props.params.slug}</code>.</p>`,
232
+ `\t</div>`,
233
+ `}`,
234
+ '',
235
+ ].join('\n'))
236
+
237
+ // ── app/posts/page.vsk ──
238
+ writeFileSync(join(appDirPath, 'posts', 'page.vsk'), [
239
+ `import { track } from '@vesk/runtime'`,
240
+ ``,
241
+ `component PostCard(props) {`,
242
+ `\t<article class="bg-white rounded-lg p-6 mb-4 shadow-sm border border-gray-100">`,
243
+ `\t\t<div class="flex items-center justify-between mb-2">`,
244
+ `\t\t\t<h2 class="text-xl font-semibold">{props.post.title}</h2>`,
245
+ `\t\t\t<span class="text-gray-400 text-sm">{props.post.date}</span>`,
246
+ `\t\t</div>`,
247
+ `\t\t<p class="text-gray-500 mb-3">{props.post.excerpt}</p>`,
248
+ `\t\t<div class="flex gap-2 mb-3">`,
249
+ `\t\t\tfor (const tag in props.post.tags) {`,
250
+ `\t\t\t\t<span class="bg-blue-50 text-blue-600 text-xs px-2 py-1 rounded-full">{tag}</span>`,
251
+ `\t\t\t}`,
252
+ `\t\t</div>`,
253
+ `\t\t<p class="text-gray-400 text-sm">By {props.post.author}</p>`,
254
+ `\t</article>`,
255
+ `}`,
256
+ ``,
257
+ `export default component Posts {`,
258
+ `\t<Head>`,
259
+ `\t\t<title>Posts — useFetch demo</title>`,
260
+ `\t</Head>`,
261
+ `\tlet &[posts] = track<{ id: number; title: string; slug: string; excerpt: string; author: string; tags: string[]; date: string }[]>([])`,
262
+ `\tconst postsResource = useFetch('/api/posts', {`,
263
+ `\t\tkey: 'posts',`,
264
+ `\t\tinto: posts,`,
265
+ `\t\tstaleTime: 30000,`,
266
+ `\t\tkeepPreviousData: true,`,
267
+ `\t\tretry: 2,`,
268
+ `\t\tretryDelay: 400,`,
269
+ `\t\ttimeout: 8000,`,
270
+ `\t})`,
271
+ `\t<div class="flex items-center justify-between mb-6">`,
272
+ `\t\t<div>`,
273
+ `\t\t\t<h1 class="text-3xl font-bold mb-1">Posts</h1>`,
274
+ `\t\t\t<p class="text-gray-500">`,
275
+ `\t\t\t\tFetched with useFetch — deduped, cached with staleTime, retried with backoff, timed out,`,
276
+ `\t\t\t\tand written into a tracked cell.`,
277
+ `\t\t\t</p>`,
278
+ `\t\t</div>`,
279
+ `\t\t<div class="flex items-center gap-3">`,
280
+ `\t\t\t<span class="text-sm text-gray-400">{postsResource.loading ? (posts.length > 0 ? 'Refreshing…' : 'Loading…') : 'Fresh'}</span>`,
281
+ `\t\t\t<button onClick={() => postsResource.refresh()} class="bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 rounded-lg">Refresh</button>`,
282
+ `\t\t</div>`,
283
+ `\t</div>`,
284
+ `\tif (postsResource.error) {`,
285
+ `\t\t<div class="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 mb-4">`,
286
+ `\t\t\t<p class="mb-2">Failed to load posts: {postsResource.error.message}</p>`,
287
+ `\t\t\t<button onClick={() => postsResource.refresh()} class="bg-red-600 hover:bg-red-700 text-white text-sm px-4 py-2 rounded-lg">Retry</button>`,
288
+ `\t\t</div>`,
289
+ `\t}`,
290
+ `\tfor (const post of posts) {`,
291
+ `\t\t<PostCard post={post} />`,
292
+ `\t}`,
293
+ `}`,
294
+ '',
295
+ ].join('\n'))
296
+
297
+ // ── app/statements/page.vsk ──
298
+ writeFileSync(join(appDirPath, 'statements', 'page.vsk'), [
299
+ `component Statements {`,
300
+ `\t<Head>`,
301
+ `\t\t<title>Statements — every JS construct</title>`,
302
+ `\t</Head>`,
303
+ `\tconst items = ['alpha', 'beta', 'gamma']`,
304
+ `\tconst obj = { name: 'Vesk', year: 2026, tags: ['fast', 'reactive'] }`,
305
+ `\tconst score = 7`,
306
+ `\tlet n = 0`,
307
+ ``,
308
+ `\t<h1 class="text-3xl font-bold mb-4">JS Statement Demo</h1>`,
309
+ ``,
310
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">if / else</h2>`,
311
+ `\tif (score > 5) {`,
312
+ `\t\t<p class="text-green-600">Score {score} is above the threshold</p>`,
313
+ `\t} else {`,
314
+ `\t\t<p class="text-red-600">Score {score} is low</p>`,
315
+ `\t}`,
316
+ ``,
317
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">ternary</h2>`,
318
+ `\t<p>{score % 2 === 0 ? 'even' : 'odd'}</p>`,
319
+ ``,
320
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">switch</h2>`,
321
+ `\tswitch (score) {`,
322
+ `\t\tcase 1:`,
323
+ `\t\t\t<p>One</p>`,
324
+ `\t\t\tbreak`,
325
+ `\t\tcase 7:`,
326
+ `\t\t\t<p>Seven</p>`,
327
+ `\t\t\tbreak`,
328
+ `\t\tdefault:`,
329
+ `\t\t\t<p>Something else</p>`,
330
+ `\t}`,
331
+ ``,
332
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">for loop</h2>`,
333
+ `\tfor (let i = 0; i < 3; i++) {`,
334
+ `\t\t<span class="mr-2">i={i}</span>`,
335
+ `\t}`,
336
+ ``,
337
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">for-of (array values)</h2>`,
338
+ `\tfor (const item of items) {`,
339
+ `\t\t<span class="mr-2">{item}</span>`,
340
+ `\t}`,
341
+ ``,
342
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">for-in (object keys)</h2>`,
343
+ `\tfor (const key in obj) {`,
344
+ `\t\t<span class="mr-2">{key}:{obj[key]}</span>`,
345
+ `\t}`,
346
+ ``,
347
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">while</h2>`,
348
+ `\twhile (n < 3) {`,
349
+ `\t\t<span class="mr-2">{n}</span>`,
350
+ `\t\tn = n + 1`,
351
+ `\t}`,
352
+ ``,
353
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">try / catch / throw</h2>`,
354
+ `\ttry {`,
355
+ `\t\tthrow new Error('Boom!')`,
356
+ `\t} catch(e) {`,
357
+ `\t\t<p class="text-red-600">Caught: {e.message}</p>`,
358
+ `\t}`,
359
+ ``,
360
+ `\t<h2 class="text-xl font-semibold mt-6 mb-2">runtime statements</h2>`,
361
+ `\tconst total = items.length * 2`,
362
+ `\t<p>items.length * 2 = {total}</p>`,
363
+ `}`,
364
+ '',
365
+ ].join('\n'))
366
+
367
+ // ── app/middleware.ts ──
368
+ writeFileSync(join(appDirPath, 'middleware.ts'), [
369
+ `// Vesk Middleware — onion model (ctx, next)`,
370
+ `// ctx = { request, params, url, locals, cookies, set, get }`,
371
+ `// ctx.set('user', val) → ctx.locals.user`,
372
+ `// ctx.user → ctx.locals.user`,
373
+ `// next() — passes to next middleware or page render`,
374
+ `// next('/rewrite') — rewrites URL in place`,
375
+ `// Short-circuit: return Response without calling next()`,
376
+ ``,
377
+ `export async function middleware(ctx, next) {`,
378
+ `\tctx.set('startTime', Date.now());`,
379
+ `\treturn next();`,
380
+ `}`,
381
+ '',
382
+ ].join('\n'))
383
+
384
+ // ── app/not-found.vsk ──
385
+ writeFileSync(join(appDirPath, 'not-found.vsk'), [
386
+ `import { Link } from '@vesk/runtime';`,
387
+ ``,
388
+ `component NotFound404(props) {`,
389
+ `\t<main class="max-w-3xl mx-auto my-16 px-4 text-center">`,
390
+ `\t\t<h1 class="text-6xl font-bold text-gray-200 mb-4">404</h1>`,
391
+ `\t\t<h2 class="text-2xl font-semibold mb-2">Page Not Found</h2>`,
392
+ `\t\t<p class="text-gray-500 mb-8">Sorry, we couldn't find <code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono">{props.url}</code></p>`,
393
+ `\t\t<Link href="/" class="text-blue-600 no-underline hover:underline font-medium">← Go home</Link>`,
394
+ `\t</main>`,
395
+ `}`,
396
+ '',
397
+ ].join('\n'))
398
+
399
+ // ── app/error.vsk ──
400
+ writeFileSync(join(appDirPath, 'error.vsk'), [
401
+ `component ErrorPage(props) {`,
402
+ `\t<div class="min-h-screen flex items-center justify-center bg-gray-50">`,
403
+ `\t\t<div class="max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-sm border border-gray-200">`,
404
+ `\t\t\t<h1 class="text-4xl font-bold text-red-600 mb-4">Error {props.statusCode}</h1>`,
405
+ `\t\t\t<p class="text-lg text-gray-700 mb-6">{props.error}</p>`,
406
+ `\t\t\t<pre class="bg-gray-100 p-4 rounded-lg text-sm font-mono overflow-x-auto max-h-64 overflow-y-auto">{props.stack}</pre>`,
407
+ `\t\t\t<p class="mt-6 text-gray-500 text-sm">{props.url}</p>`,
408
+ `\t\t</div>`,
409
+ `\t</div>`,
410
+ `}`,
411
+ '',
412
+ ].join('\n'))
413
+
414
+ // ── app/api/posts/route.ts ──
415
+ writeFileSync(join(appDirPath, 'api', 'posts', 'route.ts'), [
416
+ `import { VeskRequest, VeskResponse } from '@vesk/runtime/server';`,
417
+ ``,
418
+ `export interface Post {`,
419
+ `\tid: number;`,
420
+ `\ttitle: string;`,
421
+ `\tslug: string;`,
422
+ `\texcerpt: string;`,
423
+ `\tbody: string;`,
424
+ `\tauthor: string;`,
425
+ `\ttags: string[];`,
426
+ `\tdate: string;`,
427
+ `}`,
428
+ ``,
429
+ `const posts: Post[] = [`,
430
+ `\t{`,
431
+ `\t\tid: 1,`,
432
+ `\t\ttitle: 'Hello Vesk',`,
433
+ `\t\tslug: 'hello-vesk',`,
434
+ `\t\texcerpt: 'First post powered by Vesk — a compiler-first reactive UI framework for the post-VDOM web.',`,
435
+ `\t\tbody: 'Vesk compiles your components to targeted, minimal JavaScript with a ripple-reactive runtime. No virtual DOM, no diffing — just direct DOM updates where things change.',`,
436
+ `\t\tauthor: 'Vesk Team',`,
437
+ `\t\ttags: ['intro', 'compiler'],`,
438
+ `\t\tdate: '2026-07-01',`,
439
+ `\t},`,
440
+ `\t{`,
441
+ `\t\tid: 2,`,
442
+ `\t\ttitle: 'SSR in Vesk',`,
443
+ `\t\tslug: 'ssr-in-vesk',`,
444
+ `\t\texcerpt: 'How server-side rendering works, including awaiting in-flight fetches before writing the body.',`,
445
+ `\t\tbody: 'Server components render to HTML while useFetch promises are in flight. The renderer awaits them, re-renders with data, and serializes the results so the client hydrates without re-fetching.',`,
446
+ `\t\tauthor: 'Vesk Team',`,
447
+ `\t\ttags: ['ssr', 'fetch'],`,
448
+ `\t\tdate: '2026-07-08',`,
449
+ `\t},`,
450
+ `\t{`,
451
+ `\t\tid: 3,`,
452
+ `\t\ttitle: 'Reactivity without a VDOM',`,
453
+ `\t\tslug: 'no-vdom',`,
454
+ `\t\texcerpt: 'Ripple tracked cells and fine-grained effects mean only the exact nodes that changed are updated.',`,
455
+ `\t\tbody: 'Tracked cells, derived values, and scoped effects let Vesk update exactly the DOM that depends on a change — no tree diffing, no reconciliation pass. Mutate a cell and the precise text nodes, attributes, or lists re-render.',`,
456
+ `\t\tauthor: 'Vesk Team',`,
457
+ `\t\ttags: ['reactivity', 'performance'],`,
458
+ `\t\tdate: '2026-07-22',`,
459
+ `\t},`,
460
+ `];`,
461
+ ``,
462
+ `export async function GET(req: VeskRequest) {`,
463
+ `\tconst limit = Math.min(Number(req.query.limit) || posts.length, posts.length);`,
464
+ `\tconst list = posts.slice(0, limit).map(({ body: _body, ...rest }) => rest);`,
465
+ `\treturn VeskResponse.json(list);`,
466
+ `}`,
467
+ '',
468
+ ].join('\n'))
469
+
470
+ // ── app/api/hello/route.ts ──
471
+ writeFileSync(join(appDirPath, 'api', 'hello', 'route.ts'), [
472
+ `import { VeskRequest, VeskResponse } from '@vesk/runtime/server';`,
473
+ ``,
474
+ `export async function GET(req: VeskRequest) {`,
475
+ `\treturn VeskResponse.json({ message: 'Hello from Vesk!' })`,
476
+ `\t\t.setCookie('session', 'abc123', { httpOnly: true, secure: true, path: '/', maxAge: 3600 });`,
477
+ `}`,
478
+ ``,
479
+ `export async function POST(req: VeskRequest) {`,
480
+ `\tconst body = await req.json();`,
481
+ `\treturn VeskResponse.json({ received: body, ok: true }, { status: 201 });`,
482
+ `}`,
483
+ '',
484
+ ].join('\n'))
485
+
486
+ // ── app/api/echo/[msg]/route.ts ──
487
+ writeFileSync(join(appDirPath, 'api', 'echo', '[msg]', 'route.ts'), [
488
+ `// Dynamic API route — /api/echo/hello → params.msg === "hello"`,
489
+ ``,
490
+ `export async function GET(request: Request, { params }: { params: Promise<Record<string, string>> }) {`,
491
+ `\tconst { msg } = await params;`,
492
+ `\treturn Response.json({ message: msg || '(empty)', method: 'GET' });`,
493
+ `}`,
494
+ '',
495
+ ].join('\n'))
496
+
497
+ // ── public/favicon.svg ──
498
+ writeFileSync(join(targetDir, 'public', 'favicon.svg'), [
499
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#2563eb"/><text x="16" y="22" text-anchor="middle" fill="white" font-size="18" font-family="system-ui" font-weight="bold">V</text></svg>`,
500
+ '',
501
+ ].join('\n'))
502
+
503
+ // ── .env.example ──
504
+ writeFileSync(join(targetDir, '.env.example'), [
505
+ `# Vesk environment variables (copy to .env.local for local overrides)`,
506
+ `# These are loaded automatically in dev and build commands.`,
507
+ ``,
508
+ `# Example:`,
509
+ `# DATABASE_URL=postgres://user:pass@localhost:5432/db`,
510
+ `# STRIPE_SECRET=sk_test_...`,
511
+ `# PUBLIC_API_URL=https://api.example.com`,
512
+ '',
513
+ ].join('\n'))
514
+
515
+ // ── .gitignore ──
516
+ writeFileSync(join(targetDir, '.gitignore'), [
517
+ `node_modules/`,
518
+ `dist/`,
519
+ `.vesk/`,
520
+ `.vsk-cache/`,
521
+ `*.log`,
522
+ `.DS_Store`,
523
+ `.env`,
524
+ `.env.local`,
525
+ `.env.*.local`,
526
+ '',
527
+ ].join('\n'))
528
+
529
+ // ── README.md ──
530
+ writeFileSync(join(targetDir, 'README.md'), [
531
+ `# ${pkgName}`,
532
+ '',
533
+ `Created with [create-vesk](https://www.npmjs.com/package/create-vesk) — a new [Vesk](https://vesk.dev) project.`,
534
+ '',
535
+ `## Getting started`,
536
+ '',
537
+ '```bash',
538
+ 'npm install',
539
+ 'npm run dev',
540
+ '```',
541
+ '',
542
+ '## Scripts',
543
+ '',
544
+ `- \`npm run dev\` — dev server with HMR at http://localhost:3000 (native \`haul\` engine)`,
545
+ `- \`npm run build\` — production build (SSG + SSR) into \`.vesk/\``,
546
+ `- \`npm run start\` — run the production server`,
547
+ `- \`npm run typecheck\` — typecheck \`app/\` and \`src/\``,
548
+ `- \`npm run dev:vesk\` / \`build:vesk\` / \`start:vesk\` — same commands via the JS \`vesk\` CLI instead of \`haul\``,
549
+ '',
550
+ '## Project structure',
551
+ '',
552
+ '```',
553
+ 'app/',
554
+ ' layout.vsk # root layout (nav + {props.children})',
555
+ ' page.vsk # / — tracked counter',
556
+ ' about/page.vsk # /about',
557
+ ' blog/page.vsk # /blog',
558
+ ' blog/[slug]/page.vsk # /blog/:slug (dynamic)',
559
+ ' posts/page.vsk # /posts — useFetch + tracked cell',
560
+ ' statements/page.vsk # /statements — every JS construct',
561
+ ' not-found.vsk # custom 404',
562
+ ' error.vsk # custom error page',
563
+ ' middleware.ts # request middleware (onion model)',
564
+ ' api/posts/route.ts # /api/posts',
565
+ ' api/hello/route.ts # /api/hello',
566
+ 'src/global.css # tailwind entry',
567
+ 'public/ # static assets',
568
+ 'vesk.config.ts # framework config',
569
+ '```',
570
+ '',
571
+ ].join('\n'))
572
+
573
+ console.log('')
574
+ console.log(` ${pkgName} created successfully!`)
575
+ console.log('')
576
+ console.log(` cd ${projectName}`)
577
+ console.log(' npm install')
578
+ console.log(' npm run dev')
579
+ console.log('')