create-rudder-app 0.0.14

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.
@@ -0,0 +1,2629 @@
1
+ import { execSync } from 'node:child_process';
2
+ /** Detect which package manager invoked the installer.
3
+ * 1. Check npm_config_user_agent (set by pnpm/npm/yarn/bun create commands)
4
+ * 2. Fall back to checking which binaries are available on PATH
5
+ */
6
+ export function detectPackageManager() {
7
+ const ua = process.env['npm_config_user_agent'] ?? '';
8
+ if (ua.startsWith('bun'))
9
+ return 'bun';
10
+ if (ua.startsWith('yarn'))
11
+ return 'yarn';
12
+ if (ua.startsWith('pnpm'))
13
+ return 'pnpm';
14
+ if (ua.startsWith('npm'))
15
+ return 'npm';
16
+ // Fallback: check which binaries exist on PATH (preference: pnpm > bun > yarn > npm)
17
+ for (const pm of ['pnpm', 'bun', 'yarn']) {
18
+ try {
19
+ execSync(`${pm} --version`, { stdio: 'ignore' });
20
+ return pm;
21
+ }
22
+ catch { /* not found */ }
23
+ }
24
+ return 'npm';
25
+ }
26
+ /** `<pm> exec <bin>` equivalent per package manager. */
27
+ export function pmExec(pm, bin) {
28
+ if (pm === 'bun')
29
+ return `bunx ${bin}`;
30
+ if (pm === 'yarn')
31
+ return `yarn dlx ${bin}`;
32
+ if (pm === 'npm')
33
+ return `npx ${bin}`;
34
+ return `pnpm exec ${bin}`;
35
+ }
36
+ /** `<pm> run <script>` equivalent (yarn/bun allow omitting "run"). */
37
+ export function pmRun(pm, script) {
38
+ if (pm === 'npm')
39
+ return `npm run ${script}`;
40
+ return `${pm} ${script}`;
41
+ }
42
+ /** `<pm> install` command. */
43
+ export function pmInstall(pm) {
44
+ return `${pm} install`;
45
+ }
46
+ function pageExt(fw) {
47
+ return fw === 'vue' ? '.vue' : '.tsx';
48
+ }
49
+ export function getTemplates(ctx) {
50
+ const files = {};
51
+ files['package.json'] = packageJson(ctx);
52
+ if (ctx.pm === 'pnpm') {
53
+ files['pnpm-workspace.yaml'] = pnpmWorkspace();
54
+ }
55
+ files['tsconfig.json'] = tsconfigJson(ctx);
56
+ files['vite.config.ts'] = viteConfig(ctx);
57
+ files['+server.ts'] = serverTs();
58
+ files['.env'] = dotenv(ctx);
59
+ files['.env.example'] = dotenvExample(ctx);
60
+ files['.gitignore'] = gitignore();
61
+ // Database schema
62
+ if (ctx.orm === 'prisma') {
63
+ files['prisma.config.ts'] = prismaConfig(ctx);
64
+ files['prisma/schema/base.prisma'] = prismaBase(ctx);
65
+ if (ctx.packages.auth)
66
+ files['prisma/schema/auth.prisma'] = prismaAuth();
67
+ if (ctx.packages.notifications)
68
+ files['prisma/schema/notification.prisma'] = prismaNotification();
69
+ if (ctx.withTodo)
70
+ files['prisma/schema/todo.prisma'] = prismaTodo();
71
+ files['prisma/schema/modules.prisma'] = '// <rudderjs:modules:start>\n// <rudderjs:modules:end>\n';
72
+ }
73
+ if (ctx.tailwind) {
74
+ files['src/index.css'] = indexCss(ctx);
75
+ }
76
+ files['bootstrap/app.ts'] = bootstrapApp();
77
+ files['bootstrap/providers.ts'] = bootstrapProviders(ctx);
78
+ // Config files — always generated
79
+ files['config/app.ts'] = configApp();
80
+ files['config/server.ts'] = configServer();
81
+ files['config/log.ts'] = configLog();
82
+ // Config files — conditional on selected packages
83
+ if (ctx.orm)
84
+ files['config/database.ts'] = configDatabase(ctx);
85
+ if (ctx.packages.auth)
86
+ files['config/auth.ts'] = configAuth(ctx);
87
+ if (ctx.packages.auth)
88
+ files['config/session.ts'] = configSession();
89
+ if (ctx.packages.auth)
90
+ files['config/hash.ts'] = configHash();
91
+ if (ctx.packages.queue)
92
+ files['config/queue.ts'] = configQueue();
93
+ if (ctx.packages.mail)
94
+ files['config/mail.ts'] = configMail();
95
+ if (ctx.packages.cache)
96
+ files['config/cache.ts'] = configCache();
97
+ if (ctx.packages.storage)
98
+ files['config/storage.ts'] = configStorage();
99
+ if (ctx.packages.ai)
100
+ files['config/ai.ts'] = configAi();
101
+ files['config/index.ts'] = configIndex(ctx);
102
+ files['env.d.ts'] = envDts();
103
+ if (ctx.packages.auth && ctx.orm)
104
+ files['app/Models/User.ts'] = userModel();
105
+ files['app/Providers/AppServiceProvider.ts'] = appServiceProvider();
106
+ files['app/Middleware/RequestIdMiddleware.ts'] = requestIdMiddleware();
107
+ files['routes/api.ts'] = routesApi(ctx);
108
+ files['routes/web.ts'] = routesWeb();
109
+ files['routes/console.ts'] = routesConsole();
110
+ const ext = pageExt(ctx.primary);
111
+ files['pages/+config.ts'] = pagesRootConfig(ctx);
112
+ if (ctx.frameworks.length > 1) {
113
+ files['pages/index/+config.ts'] = pagesIndexConfig(ctx);
114
+ }
115
+ files['pages/index/+data.ts'] = pagesIndexData(ctx);
116
+ files[`pages/index/+Page${ext}`] = pagesIndexPage(ctx);
117
+ files['pages/_error/+config.ts'] = pagesErrorConfig(ctx);
118
+ files[`pages/_error/+Page${ext}`] = pagesErrorPage(ctx);
119
+ if (ctx.withTodo) {
120
+ files['app/Modules/Todo/TodoSchema.ts'] = todoSchema();
121
+ files['app/Modules/Todo/TodoService.ts'] = todoService();
122
+ files['app/Modules/Todo/TodoServiceProvider.ts'] = todoServiceProvider();
123
+ files['pages/todos/+config.ts'] = todoPageConfig(ctx);
124
+ files['pages/todos/+data.ts'] = todoPageData();
125
+ files[`pages/todos/+Page${ext}`] = todoPage(ctx);
126
+ }
127
+ if (ctx.packages.ai) {
128
+ files['pages/ai-chat/+config.ts'] = aiChatPageConfig(ctx);
129
+ files[`pages/ai-chat/+Page${ext}`] = aiChatPage(ctx);
130
+ }
131
+ for (const fw of ctx.frameworks.filter(f => f !== ctx.primary)) {
132
+ const dext = pageExt(fw);
133
+ files[`pages/${fw}-demo/+config.ts`] = demoPageConfig(fw);
134
+ files[`pages/${fw}-demo/+Page${dext}`] = demoPage(fw, ctx);
135
+ }
136
+ return files;
137
+ }
138
+ // ─── package.json ──────────────────────────────────────────
139
+ function packageJson(ctx) {
140
+ const { frameworks, tailwind, shadcn, db } = ctx;
141
+ const hasReact = frameworks.includes('react');
142
+ const hasVue = frameworks.includes('vue');
143
+ const hasSolid = frameworks.includes('solid');
144
+ const dbDeps = {
145
+ sqlite: { 'better-sqlite3': '^12.0.0' },
146
+ postgresql: {},
147
+ mysql: {},
148
+ };
149
+ const dbDevDeps = {
150
+ sqlite: { '@types/better-sqlite3': '^7.6.0' },
151
+ postgresql: {},
152
+ mysql: {},
153
+ };
154
+ const frameworkDeps = {};
155
+ const frameworkDevDeps = {};
156
+ if (hasReact) {
157
+ frameworkDeps['react'] = '^19.0.0';
158
+ frameworkDeps['react-dom'] = '^19.0.0';
159
+ frameworkDeps['vike-react'] = '^0.6.20';
160
+ frameworkDevDeps['@vitejs/plugin-react'] = '^4.3.4';
161
+ frameworkDevDeps['@types/react'] = '^19.0.0';
162
+ frameworkDevDeps['@types/react-dom'] = '^19.0.0';
163
+ }
164
+ if (hasVue) {
165
+ frameworkDeps['vue'] = '^3.5.0';
166
+ frameworkDeps['vike-vue'] = 'latest';
167
+ frameworkDevDeps['@vitejs/plugin-vue'] = '^5.2.0';
168
+ }
169
+ if (hasSolid) {
170
+ frameworkDeps['solid-js'] = '^1.9.0';
171
+ frameworkDeps['vike-solid'] = 'latest';
172
+ }
173
+ const tailwindDeps = tailwind ? {
174
+ 'tailwindcss': '^4.2.1',
175
+ '@tailwindcss/vite': '^4.2.1',
176
+ } : {};
177
+ const tailwindDevDeps = tailwind ? {
178
+ 'tw-animate-css': '^1.4.0',
179
+ } : {};
180
+ const shadcnDeps = shadcn ? {
181
+ 'class-variance-authority': '^0.7.1',
182
+ 'clsx': '^2.1.1',
183
+ 'tailwind-merge': '^3.5.0',
184
+ 'lucide-react': '^0.575.0',
185
+ } : {};
186
+ const shadcnDevDeps = shadcn ? {
187
+ 'shadcn': 'latest',
188
+ } : {};
189
+ // Base framework deps (always included)
190
+ const deps = {
191
+ '@rudderjs/rudder': 'latest',
192
+ '@rudderjs/vite': 'latest',
193
+ '@rudderjs/contracts': 'latest',
194
+ '@rudderjs/core': 'latest',
195
+ '@rudderjs/log': 'latest',
196
+ '@rudderjs/middleware': 'latest',
197
+ '@rudderjs/router': 'latest',
198
+ '@rudderjs/server-hono': 'latest',
199
+ '@rudderjs/support': 'latest',
200
+ '@vikejs/hono': '^0.2.0',
201
+ 'dotenv': '^16.4.0',
202
+ 'reflect-metadata': '^0.2.2',
203
+ 'vike': '^0.4.257',
204
+ 'zod': '^4.0.0',
205
+ ...frameworkDeps,
206
+ ...tailwindDeps,
207
+ ...shadcnDeps,
208
+ ...dbDeps[db],
209
+ };
210
+ // ORM deps
211
+ if (ctx.orm === 'prisma') {
212
+ deps['@rudderjs/orm'] = 'latest';
213
+ deps['@rudderjs/orm-prisma'] = 'latest';
214
+ deps['@prisma/client'] = '^7.0.0';
215
+ }
216
+ else if (ctx.orm === 'drizzle') {
217
+ deps['@rudderjs/orm'] = 'latest';
218
+ deps['@rudderjs/orm-drizzle'] = 'latest';
219
+ }
220
+ // Optional package deps
221
+ if (ctx.packages.auth) {
222
+ deps['@rudderjs/auth'] = 'latest';
223
+ deps['@rudderjs/session'] = 'latest';
224
+ deps['@rudderjs/hash'] = 'latest';
225
+ }
226
+ if (ctx.packages.cache)
227
+ deps['@rudderjs/cache'] = 'latest';
228
+ if (ctx.packages.queue)
229
+ deps['@rudderjs/queue'] = 'latest';
230
+ if (ctx.packages.storage)
231
+ deps['@rudderjs/storage'] = 'latest';
232
+ if (ctx.packages.mail)
233
+ deps['@rudderjs/mail'] = 'latest';
234
+ if (ctx.packages.notifications)
235
+ deps['@rudderjs/notification'] = 'latest';
236
+ if (ctx.packages.scheduler)
237
+ deps['@rudderjs/schedule'] = 'latest';
238
+ if (ctx.packages.broadcast)
239
+ deps['@rudderjs/broadcast'] = 'latest';
240
+ if (ctx.packages.live)
241
+ deps['@rudderjs/live'] = 'latest';
242
+ if (ctx.packages.ai)
243
+ deps['@rudderjs/ai'] = 'latest';
244
+ if (ctx.packages.localization)
245
+ deps['@rudderjs/localization'] = 'latest';
246
+ const devDeps = {
247
+ '@rudderjs/cli': 'latest',
248
+ '@types/node': '^20.0.0',
249
+ 'tsx': '^4.21.0',
250
+ 'typescript': '^5.4.0',
251
+ 'vite': '^7.1.0',
252
+ ...frameworkDevDeps,
253
+ ...tailwindDevDeps,
254
+ ...shadcnDevDeps,
255
+ ...dbDevDeps[db],
256
+ };
257
+ if (ctx.orm === 'prisma')
258
+ devDeps['prisma'] = '^7.0.0';
259
+ const builtDeps = ['esbuild'];
260
+ if (ctx.orm === 'prisma') {
261
+ builtDeps.push('@prisma/engines', 'prisma');
262
+ }
263
+ if (db === 'sqlite')
264
+ builtDeps.unshift('better-sqlite3');
265
+ const pmField = {};
266
+ if (ctx.pm === 'pnpm') {
267
+ pmField['pnpm'] = { onlyBuiltDependencies: builtDeps };
268
+ }
269
+ else if (ctx.pm === 'bun') {
270
+ pmField['trustedDependencies'] = builtDeps;
271
+ }
272
+ // npm and yarn allow all lifecycle scripts by default — no extra field needed
273
+ return JSON.stringify({
274
+ name: ctx.name,
275
+ version: '0.0.1',
276
+ private: true,
277
+ type: 'module',
278
+ scripts: {
279
+ dev: 'vike dev',
280
+ 'dev:clean': 'pids=$(lsof -ti :24678 -ti :3000 2>/dev/null); if [ -n "$pids" ]; then kill -9 $pids; fi; vike dev',
281
+ build: 'vike build',
282
+ start: 'node ./dist/server/index.mjs',
283
+ preview: 'node ./dist/server/index.mjs',
284
+ typecheck: 'tsc --noEmit',
285
+ rudder: 'tsx node_modules/@rudderjs/cli/src/index.ts',
286
+ },
287
+ ...pmField,
288
+ dependencies: deps,
289
+ devDependencies: devDeps,
290
+ }, null, 2) + '\n';
291
+ }
292
+ // ─── tsconfig.json ─────────────────────────────────────────
293
+ function tsconfigJson(ctx) {
294
+ const hasReact = ctx.frameworks.includes('react');
295
+ const hasSolid = ctx.frameworks.includes('solid');
296
+ const compilerOptions = {
297
+ target: 'ES2022',
298
+ module: 'ESNext',
299
+ moduleResolution: 'bundler',
300
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
301
+ strict: true,
302
+ exactOptionalPropertyTypes: true,
303
+ noUncheckedIndexedAccess: true,
304
+ experimentalDecorators: true,
305
+ emitDecoratorMetadata: true,
306
+ skipLibCheck: true,
307
+ noEmit: true,
308
+ baseUrl: '.',
309
+ paths: { '@/*': ['./src/*'], 'App/*': ['./app/*'] },
310
+ allowImportingTsExtensions: true,
311
+ };
312
+ if (hasReact) {
313
+ compilerOptions['jsx'] = 'react-jsx';
314
+ }
315
+ else if (hasSolid) {
316
+ compilerOptions['jsx'] = 'preserve';
317
+ compilerOptions['jsxImportSource'] = 'solid-js';
318
+ }
319
+ // Vue only — no jsx field needed
320
+ return JSON.stringify({
321
+ compilerOptions,
322
+ include: ['src/**/*', 'pages/**/*', 'app/**/*', 'bootstrap/**/*', 'routes/**/*', 'config/**/*', '*.ts', '*.tsx'],
323
+ }, null, 2) + '\n';
324
+ }
325
+ // ─── vite.config.ts ────────────────────────────────────────
326
+ function viteConfig(ctx) {
327
+ const { frameworks, primary, tailwind } = ctx;
328
+ const hasReact = frameworks.includes('react');
329
+ const hasVue = frameworks.includes('vue');
330
+ const hasSolid = frameworks.includes('solid');
331
+ const hasReactSolidConflict = hasReact && hasSolid;
332
+ const imports = [
333
+ `import { defineConfig } from 'vite'`,
334
+ `import rudderjs from '@rudderjs/vite'`,
335
+ ];
336
+ if (tailwind)
337
+ imports.push(`import tailwindcss from '@tailwindcss/vite'`);
338
+ if (hasReact)
339
+ imports.push(`import react from '@vitejs/plugin-react'`);
340
+ if (hasVue)
341
+ imports.push(`import vue from '@vitejs/plugin-vue'`);
342
+ if (hasSolid)
343
+ imports.push(`import solid from 'vike-solid/vite'`);
344
+ const plugins = ['rudderjs()'];
345
+ if (tailwind)
346
+ plugins.push('tailwindcss()');
347
+ if (hasReact) {
348
+ if (hasReactSolidConflict) {
349
+ if (primary === 'react') {
350
+ plugins.push(`react({ exclude: ['**/pages/solid-demo/**'] })`);
351
+ }
352
+ else {
353
+ plugins.push(`react({ include: ['**/pages/react-demo/**'] })`);
354
+ }
355
+ }
356
+ else {
357
+ plugins.push('react()');
358
+ }
359
+ }
360
+ if (hasVue) {
361
+ plugins.push('vue()');
362
+ }
363
+ if (hasSolid) {
364
+ if (hasReactSolidConflict) {
365
+ if (primary === 'solid') {
366
+ plugins.push(`solid({ exclude: ['**/pages/react-demo/**'] })`);
367
+ }
368
+ else {
369
+ plugins.push(`solid({ include: ['**/pages/solid-demo/**'] })`);
370
+ }
371
+ }
372
+ else {
373
+ plugins.push('solid()');
374
+ }
375
+ }
376
+ const pluginsStr = plugins.map(p => ` ${p},`).join('\n');
377
+ return `${imports.join('\n')}
378
+
379
+ export default defineConfig({
380
+ plugins: [
381
+ ${pluginsStr}
382
+ ],
383
+ })
384
+ `;
385
+ }
386
+ // ─── +server.ts ───────────────────────────────────────────
387
+ function serverTs() {
388
+ return `import type { Server } from 'vike/types'
389
+ import app from './bootstrap/app.js'
390
+
391
+ export default {
392
+ fetch: app.fetch,
393
+ } satisfies Server
394
+ `;
395
+ }
396
+ // ─── prisma.config.ts ──────────────────────────────────────
397
+ function prismaConfig(ctx) {
398
+ const dbUrl = ctx.db === 'sqlite'
399
+ ? "process.env['DATABASE_URL'] ?? 'file:./dev.db'"
400
+ : "process.env['DATABASE_URL']!";
401
+ return `import { defineConfig } from 'prisma/config'
402
+
403
+ export default defineConfig({
404
+ schema: 'prisma/schema',
405
+ datasource: {
406
+ url: ${dbUrl},
407
+ },
408
+ })
409
+ `;
410
+ }
411
+ // ─── .env ──────────────────────────────────────────────────
412
+ function dotenv(ctx) {
413
+ const lines = [
414
+ `APP_NAME=${ctx.name}`,
415
+ 'APP_ENV=development',
416
+ 'APP_DEBUG=true',
417
+ 'APP_URL=http://localhost:3000',
418
+ '',
419
+ 'PORT=3000',
420
+ ];
421
+ if (ctx.orm) {
422
+ lines.push('');
423
+ if (ctx.db === 'sqlite')
424
+ lines.push('DATABASE_URL="file:./dev.db"');
425
+ else if (ctx.db === 'postgresql')
426
+ lines.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"');
427
+ else
428
+ lines.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"');
429
+ }
430
+ if (ctx.packages.auth) {
431
+ lines.push('');
432
+ lines.push(`AUTH_SECRET=${ctx.authSecret}`);
433
+ }
434
+ if (ctx.packages.ai) {
435
+ lines.push('');
436
+ lines.push('AI_MODEL=anthropic/claude-sonnet-4-5');
437
+ lines.push('ANTHROPIC_API_KEY=');
438
+ lines.push('# OPENAI_API_KEY=');
439
+ lines.push('# GOOGLE_AI_API_KEY=');
440
+ lines.push('# OLLAMA_BASE_URL=http://localhost:11434');
441
+ }
442
+ return lines.join('\n') + '\n';
443
+ }
444
+ // ─── .env.example ──────────────────────────────────────────
445
+ function dotenvExample(ctx) {
446
+ const lines = [
447
+ `APP_NAME=${ctx.name}`,
448
+ 'APP_ENV=development',
449
+ 'APP_DEBUG=false',
450
+ 'APP_URL=http://localhost:3000',
451
+ '',
452
+ 'PORT=3000',
453
+ ];
454
+ if (ctx.orm) {
455
+ lines.push('');
456
+ if (ctx.db === 'sqlite')
457
+ lines.push('DATABASE_URL="file:./dev.db"');
458
+ else if (ctx.db === 'postgresql')
459
+ lines.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"');
460
+ else
461
+ lines.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"');
462
+ }
463
+ if (ctx.packages.auth) {
464
+ lines.push('');
465
+ lines.push('AUTH_SECRET=please-set-a-real-32-char-secret-here');
466
+ }
467
+ if (ctx.packages.ai) {
468
+ lines.push('');
469
+ lines.push('AI_MODEL=anthropic/claude-sonnet-4-5');
470
+ lines.push('ANTHROPIC_API_KEY=');
471
+ lines.push('# OPENAI_API_KEY=');
472
+ lines.push('# GOOGLE_AI_API_KEY=');
473
+ lines.push('# OLLAMA_BASE_URL=http://localhost:11434');
474
+ }
475
+ return lines.join('\n') + '\n';
476
+ }
477
+ // ─── .gitignore ────────────────────────────────────────────
478
+ function gitignore() {
479
+ return `node_modules/
480
+ dist/
481
+ .env
482
+ *.db
483
+ *.db-journal
484
+ prisma/generated/
485
+ `;
486
+ }
487
+ function pnpmWorkspace() {
488
+ return `# Standalone project — prevents pnpm from merging with a parent workspace\npackages: []\n`;
489
+ }
490
+ // ─── prisma/schema/*.prisma ─────────────────────────────────
491
+ function prismaBase(ctx) {
492
+ const provider = ctx.db === 'sqlite' ? 'sqlite'
493
+ : ctx.db === 'postgresql' ? 'postgresql'
494
+ : 'mysql';
495
+ return `generator client {
496
+ provider = "prisma-client-js"
497
+ }
498
+
499
+ datasource db {
500
+ provider = "${provider}"
501
+ }
502
+ `;
503
+ }
504
+ function prismaAuth() {
505
+ return `model User {
506
+ id String @id @default(cuid())
507
+ name String
508
+ email String @unique
509
+ emailVerified Boolean @default(false)
510
+ image String?
511
+ role String @default("user")
512
+ createdAt DateTime @default(now())
513
+ updatedAt DateTime @updatedAt
514
+ sessions Session[]
515
+ accounts Account[]
516
+ }
517
+
518
+ model Session {
519
+ id String @id
520
+ expiresAt DateTime
521
+ token String @unique
522
+ createdAt DateTime
523
+ updatedAt DateTime
524
+ ipAddress String?
525
+ userAgent String?
526
+ userId String
527
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
528
+ }
529
+
530
+ model Account {
531
+ id String @id
532
+ accountId String
533
+ providerId String
534
+ userId String
535
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
536
+ accessToken String?
537
+ refreshToken String?
538
+ idToken String?
539
+ accessTokenExpiresAt DateTime?
540
+ refreshTokenExpiresAt DateTime?
541
+ scope String?
542
+ password String?
543
+ createdAt DateTime
544
+ updatedAt DateTime
545
+ }
546
+
547
+ model Verification {
548
+ id String @id
549
+ identifier String
550
+ value String
551
+ expiresAt DateTime
552
+ createdAt DateTime?
553
+ updatedAt DateTime?
554
+ }
555
+ `;
556
+ }
557
+ function prismaNotification() {
558
+ return `model Notification {
559
+ id String @id @default(cuid())
560
+ notifiable_id String
561
+ notifiable_type String
562
+ type String
563
+ data String
564
+ read_at String?
565
+ created_at String
566
+ updated_at String
567
+
568
+ @@index([notifiable_type, notifiable_id])
569
+ }
570
+ `;
571
+ }
572
+ function prismaTodo() {
573
+ return `// <rudderjs:modules:start>
574
+ // module: Todo (Todo.prisma)
575
+ model Todo {
576
+ id String @id @default(cuid())
577
+ title String
578
+ completed Boolean @default(false)
579
+ createdAt DateTime @default(now())
580
+ updatedAt DateTime @updatedAt
581
+ }
582
+ // <rudderjs:modules:end>
583
+ `;
584
+ }
585
+ // ─── src/index.css ─────────────────────────────────────────
586
+ function indexCss(ctx) {
587
+ if (!ctx.shadcn) {
588
+ return `@import "tailwindcss";
589
+ @import "tw-animate-css";
590
+ `;
591
+ }
592
+ return `@import "tailwindcss";
593
+ @import "tw-animate-css";
594
+ @import "shadcn/tailwind.css";
595
+
596
+ @custom-variant dark (&:is(.dark *));
597
+
598
+ @theme inline {
599
+ --radius-sm: calc(var(--radius) - 4px);
600
+ --radius-md: calc(var(--radius) - 2px);
601
+ --radius-lg: var(--radius);
602
+ --radius-xl: calc(var(--radius) + 4px);
603
+ --radius-2xl: calc(var(--radius) + 8px);
604
+ --radius-3xl: calc(var(--radius) + 12px);
605
+ --radius-4xl: calc(var(--radius) + 16px);
606
+ --color-background: var(--background);
607
+ --color-foreground: var(--foreground);
608
+ --color-card: var(--card);
609
+ --color-card-foreground: var(--card-foreground);
610
+ --color-popover: var(--popover);
611
+ --color-popover-foreground: var(--popover-foreground);
612
+ --color-primary: var(--primary);
613
+ --color-primary-foreground: var(--primary-foreground);
614
+ --color-secondary: var(--secondary);
615
+ --color-secondary-foreground: var(--secondary-foreground);
616
+ --color-muted: var(--muted);
617
+ --color-muted-foreground: var(--muted-foreground);
618
+ --color-accent: var(--accent);
619
+ --color-accent-foreground: var(--accent-foreground);
620
+ --color-destructive: var(--destructive);
621
+ --color-border: var(--border);
622
+ --color-input: var(--input);
623
+ --color-ring: var(--ring);
624
+ --color-chart-1: var(--chart-1);
625
+ --color-chart-2: var(--chart-2);
626
+ --color-chart-3: var(--chart-3);
627
+ --color-chart-4: var(--chart-4);
628
+ --color-chart-5: var(--chart-5);
629
+ --color-sidebar: var(--sidebar);
630
+ --color-sidebar-foreground: var(--sidebar-foreground);
631
+ --color-sidebar-primary: var(--sidebar-primary);
632
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
633
+ --color-sidebar-accent: var(--sidebar-accent);
634
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
635
+ --color-sidebar-border: var(--sidebar-border);
636
+ --color-sidebar-ring: var(--sidebar-ring);
637
+ }
638
+
639
+ :root {
640
+ --radius: 0.625rem;
641
+ --background: oklch(1 0 0);
642
+ --foreground: oklch(0.145 0 0);
643
+ --card: oklch(1 0 0);
644
+ --card-foreground: oklch(0.145 0 0);
645
+ --popover: oklch(1 0 0);
646
+ --popover-foreground: oklch(0.145 0 0);
647
+ --primary: oklch(0.205 0 0);
648
+ --primary-foreground: oklch(0.985 0 0);
649
+ --secondary: oklch(0.97 0 0);
650
+ --secondary-foreground: oklch(0.205 0 0);
651
+ --muted: oklch(0.97 0 0);
652
+ --muted-foreground: oklch(0.556 0 0);
653
+ --accent: oklch(0.97 0 0);
654
+ --accent-foreground: oklch(0.205 0 0);
655
+ --destructive: oklch(0.577 0.245 27.325);
656
+ --border: oklch(0.922 0 0);
657
+ --input: oklch(0.922 0 0);
658
+ --ring: oklch(0.708 0 0);
659
+ --chart-1: oklch(0.646 0.222 41.116);
660
+ --chart-2: oklch(0.6 0.118 184.704);
661
+ --chart-3: oklch(0.398 0.07 227.392);
662
+ --chart-4: oklch(0.828 0.189 84.429);
663
+ --chart-5: oklch(0.769 0.188 70.08);
664
+ --sidebar: oklch(0.985 0 0);
665
+ --sidebar-foreground: oklch(0.145 0 0);
666
+ --sidebar-primary: oklch(0.205 0 0);
667
+ --sidebar-primary-foreground: oklch(0.985 0 0);
668
+ --sidebar-accent: oklch(0.97 0 0);
669
+ --sidebar-accent-foreground: oklch(0.205 0 0);
670
+ --sidebar-border: oklch(0.922 0 0);
671
+ --sidebar-ring: oklch(0.708 0 0);
672
+ }
673
+
674
+ .dark {
675
+ --background: oklch(0.145 0 0);
676
+ --foreground: oklch(0.985 0 0);
677
+ --card: oklch(0.205 0 0);
678
+ --card-foreground: oklch(0.985 0 0);
679
+ --popover: oklch(0.205 0 0);
680
+ --popover-foreground: oklch(0.985 0 0);
681
+ --primary: oklch(0.922 0 0);
682
+ --primary-foreground: oklch(0.205 0 0);
683
+ --secondary: oklch(0.269 0 0);
684
+ --secondary-foreground: oklch(0.985 0 0);
685
+ --muted: oklch(0.269 0 0);
686
+ --muted-foreground: oklch(0.708 0 0);
687
+ --accent: oklch(0.269 0 0);
688
+ --accent-foreground: oklch(0.985 0 0);
689
+ --destructive: oklch(0.704 0.191 22.216);
690
+ --border: oklch(1 0 0 / 10%);
691
+ --input: oklch(1 0 0 / 15%);
692
+ --ring: oklch(0.556 0 0);
693
+ --chart-1: oklch(0.488 0.243 264.376);
694
+ --chart-2: oklch(0.696 0.17 162.48);
695
+ --chart-3: oklch(0.769 0.188 70.08);
696
+ --chart-4: oklch(0.627 0.265 303.9);
697
+ --chart-5: oklch(0.645 0.246 16.439);
698
+ --sidebar: oklch(0.205 0 0);
699
+ --sidebar-foreground: oklch(0.985 0 0);
700
+ --sidebar-primary: oklch(0.488 0.243 264.376);
701
+ --sidebar-primary-foreground: oklch(0.985 0 0);
702
+ --sidebar-accent: oklch(0.269 0 0);
703
+ --sidebar-accent-foreground: oklch(0.985 0 0);
704
+ --sidebar-border: oklch(1 0 0 / 10%);
705
+ --sidebar-ring: oklch(0.556 0 0);
706
+ }
707
+
708
+ @layer base {
709
+ * {
710
+ @apply border-border outline-ring/50;
711
+ }
712
+ body {
713
+ @apply bg-background text-foreground;
714
+ }
715
+ }
716
+ `;
717
+ }
718
+ // ─── bootstrap/app.ts ──────────────────────────────────────
719
+ function bootstrapApp() {
720
+ return `import 'reflect-metadata'
721
+ import 'dotenv/config'
722
+ import { Application } from '@rudderjs/core'
723
+ import { hono } from '@rudderjs/server-hono'
724
+ import { RateLimit, fromClass } from '@rudderjs/middleware'
725
+ import { RequestIdMiddleware } from '../app/Middleware/RequestIdMiddleware.ts'
726
+ import configs from '../config/index.ts'
727
+ import providers from './providers.ts'
728
+
729
+ export default Application.configure({
730
+ server: hono(configs.server),
731
+ config: configs,
732
+ providers,
733
+ })
734
+ .withRouting({
735
+ web: () => import('../routes/web.ts'),
736
+ api: () => import('../routes/api.ts'),
737
+ commands: () => import('../routes/console.ts'),
738
+ })
739
+ .withMiddleware((m) => {
740
+ m.use(RateLimit.perMinute(60))
741
+ m.use(fromClass(RequestIdMiddleware))
742
+ })
743
+ .create()
744
+ `;
745
+ }
746
+ // ─── bootstrap/providers.ts ────────────────────────────────
747
+ function bootstrapProviders(ctx) {
748
+ const imports = [
749
+ "import type { Application, ServiceProvider } from '@rudderjs/core'",
750
+ "import { events } from '@rudderjs/core'",
751
+ "import { log } from '@rudderjs/log'",
752
+ ];
753
+ const providers = [
754
+ '// ── Infrastructure (order matters) ──────────────────────',
755
+ 'log(configs.log),',
756
+ ];
757
+ if (ctx.orm === 'prisma') {
758
+ imports.push("import { database } from '@rudderjs/orm-prisma'");
759
+ providers.push('database(configs.database),');
760
+ }
761
+ else if (ctx.orm === 'drizzle') {
762
+ imports.push("import { database } from '@rudderjs/orm-drizzle'");
763
+ providers.push('database(configs.database),');
764
+ }
765
+ if (ctx.packages.auth) {
766
+ imports.push("import { session } from '@rudderjs/session'");
767
+ imports.push("import { hash } from '@rudderjs/hash'");
768
+ providers.push('session(configs.session),');
769
+ providers.push('hash(configs.hash),');
770
+ }
771
+ if (ctx.packages.cache) {
772
+ imports.push("import { cache } from '@rudderjs/cache'");
773
+ providers.push('cache(configs.cache),');
774
+ }
775
+ if (ctx.packages.auth) {
776
+ imports.push("import { auth } from '@rudderjs/auth'");
777
+ providers.push('auth(configs.auth),');
778
+ }
779
+ providers.push('');
780
+ providers.push('// ── Features ────────────────────────────────────────────');
781
+ providers.push('events({}),');
782
+ if (ctx.packages.queue) {
783
+ imports.push("import { queue } from '@rudderjs/queue'");
784
+ providers.push('queue(configs.queue),');
785
+ }
786
+ if (ctx.packages.mail) {
787
+ imports.push("import { mail } from '@rudderjs/mail'");
788
+ providers.push('mail(configs.mail),');
789
+ }
790
+ if (ctx.packages.storage) {
791
+ imports.push("import { storage } from '@rudderjs/storage'");
792
+ providers.push('storage(configs.storage),');
793
+ }
794
+ if (ctx.packages.localization) {
795
+ imports.push("import { resolve } from 'node:path'");
796
+ imports.push("import { localization } from '@rudderjs/localization'");
797
+ }
798
+ if (ctx.packages.scheduler) {
799
+ imports.push("import { scheduler } from '@rudderjs/schedule'");
800
+ providers.push('scheduler(),');
801
+ }
802
+ if (ctx.packages.notifications) {
803
+ imports.push("import { notifications } from '@rudderjs/notification'");
804
+ providers.push('notifications(),');
805
+ }
806
+ if (ctx.packages.broadcast) {
807
+ imports.push("import { broadcasting } from '@rudderjs/broadcast'");
808
+ providers.push('broadcasting(),');
809
+ }
810
+ if (ctx.packages.live) {
811
+ imports.push("import { live } from '@rudderjs/live'");
812
+ providers.push('live({}),');
813
+ }
814
+ if (ctx.packages.localization) {
815
+ providers.push(`localization({
816
+ locale: 'en',
817
+ fallback: 'en',
818
+ path: resolve(process.cwd(), 'lang'),
819
+ }),`);
820
+ }
821
+ if (ctx.packages.ai) {
822
+ imports.push("import { ai } from '@rudderjs/ai'");
823
+ providers.push('ai(configs.ai),');
824
+ }
825
+ providers.push('');
826
+ providers.push('// ── Application ─────────────────────────────────────────');
827
+ imports.push("import { AppServiceProvider } from '../app/Providers/AppServiceProvider.js'");
828
+ providers.push('AppServiceProvider,');
829
+ if (ctx.withTodo) {
830
+ imports.push("import { TodoServiceProvider } from '../app/Modules/Todo/TodoServiceProvider.js'");
831
+ providers.push('TodoServiceProvider,');
832
+ }
833
+ imports.push("import configs from '../config/index.js'");
834
+ return `${imports.join('\n')}
835
+
836
+ export default [
837
+ ${providers.join('\n ')}
838
+ ] satisfies (new (app: Application) => ServiceProvider)[]
839
+ `;
840
+ }
841
+ // ─── config files ──────────────────────────────────────────
842
+ function configApp() {
843
+ return `import { Env } from '@rudderjs/support'
844
+
845
+ export default {
846
+ name: Env.get('APP_NAME', 'RudderJS'),
847
+ env: Env.get('APP_ENV', 'development'),
848
+ debug: Env.getBool('APP_DEBUG', false),
849
+ url: Env.get('APP_URL', 'http://localhost:3000'),
850
+ }
851
+ `;
852
+ }
853
+ function configServer() {
854
+ return `import { Env } from '@rudderjs/support'
855
+
856
+ export default {
857
+ port: Env.getNumber('PORT', 3000),
858
+ trustProxy: Env.getBool('TRUST_PROXY', false),
859
+ cors: {
860
+ origin: Env.get('CORS_ORIGIN', '*'),
861
+ methods: Env.get('CORS_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'),
862
+ headers: Env.get('CORS_HEADERS', 'Content-Type,Authorization'),
863
+ },
864
+ }
865
+ `;
866
+ }
867
+ function configLog() {
868
+ return `import { Env } from '@rudderjs/support'
869
+ import type { LogConfig } from '@rudderjs/log'
870
+
871
+ export default {
872
+ default: Env.get('LOG_CHANNEL', 'console'),
873
+
874
+ channels: {
875
+ stack: {
876
+ driver: 'stack',
877
+ channels: ['console', 'daily'],
878
+ ignoreExceptions: false,
879
+ },
880
+
881
+ console: {
882
+ driver: 'console',
883
+ level: Env.get('LOG_LEVEL', 'debug') as 'debug',
884
+ },
885
+
886
+ single: {
887
+ driver: 'single',
888
+ path: 'storage/logs/rudderjs.log',
889
+ level: Env.get('LOG_LEVEL', 'debug') as 'debug',
890
+ },
891
+
892
+ daily: {
893
+ driver: 'daily',
894
+ path: 'storage/logs/rudderjs.log',
895
+ days: 14,
896
+ level: Env.get('LOG_LEVEL', 'debug') as 'debug',
897
+ },
898
+
899
+ null: {
900
+ driver: 'null',
901
+ },
902
+ },
903
+ } satisfies LogConfig
904
+ `;
905
+ }
906
+ function configHash() {
907
+ return `import { Env } from '@rudderjs/support'
908
+ import type { HashConfig } from '@rudderjs/hash'
909
+
910
+ export default {
911
+ driver: Env.get('HASH_DRIVER', 'bcrypt') as 'bcrypt' | 'argon2',
912
+ bcrypt: { rounds: 12 },
913
+ argon2: { memory: 65536, time: 3, threads: 4 },
914
+ } satisfies HashConfig
915
+ `;
916
+ }
917
+ function configDatabase(ctx) {
918
+ const defaultConn = ctx.db;
919
+ const connections = {
920
+ sqlite: ` sqlite: {
921
+ driver: 'sqlite' as const,
922
+ url: Env.get('DATABASE_URL', 'file:./dev.db'),
923
+ },`,
924
+ postgresql: ` postgresql: {
925
+ driver: 'postgresql' as const,
926
+ url: Env.get('DATABASE_URL', ''),
927
+ },`,
928
+ mysql: ` mysql: {
929
+ driver: 'mysql' as const,
930
+ url: Env.get('DATABASE_URL', ''),
931
+ },`,
932
+ };
933
+ return `import { Env } from '@rudderjs/support'
934
+
935
+ export default {
936
+ default: Env.get('DB_CONNECTION', '${defaultConn}'),
937
+
938
+ connections: {
939
+ ${connections[ctx.db]}
940
+ },
941
+ }
942
+ `;
943
+ }
944
+ function configQueue() {
945
+ return `import { Env } from '@rudderjs/support'
946
+ import type { QueueConfig } from '@rudderjs/queue'
947
+
948
+ export default {
949
+ default: Env.get('QUEUE_CONNECTION', 'sync'),
950
+
951
+ connections: {
952
+ sync: {
953
+ driver: 'sync',
954
+ },
955
+
956
+ inngest: {
957
+ driver: 'inngest',
958
+ appId: Env.get('INNGEST_APP_ID', 'my-app'),
959
+ eventKey: Env.get('INNGEST_EVENT_KEY', ''),
960
+ signingKey: Env.get('INNGEST_SIGNING_KEY', ''),
961
+ jobs: [],
962
+ },
963
+ },
964
+ } satisfies QueueConfig
965
+ `;
966
+ }
967
+ function configMail() {
968
+ return `import { Env } from '@rudderjs/support'
969
+
970
+ export default {
971
+ default: Env.get('MAIL_MAILER', 'log'),
972
+
973
+ from: {
974
+ address: Env.get('MAIL_FROM_ADDRESS', 'hello@example.com'),
975
+ name: Env.get('MAIL_FROM_NAME', 'RudderJS'),
976
+ },
977
+
978
+ mailers: {
979
+ log: {
980
+ driver: 'log',
981
+ },
982
+
983
+ smtp: {
984
+ driver: 'smtp',
985
+ host: Env.get('MAIL_HOST', 'localhost'),
986
+ port: Env.getNumber('MAIL_PORT', 587),
987
+ username: Env.get('MAIL_USERNAME', ''),
988
+ password: Env.get('MAIL_PASSWORD', ''),
989
+ encryption: Env.get('MAIL_ENCRYPTION', 'tls'),
990
+ },
991
+ },
992
+ }
993
+ `;
994
+ }
995
+ function configCache() {
996
+ return `import { Env } from '@rudderjs/support'
997
+ import type { CacheConfig } from '@rudderjs/cache'
998
+
999
+ export default {
1000
+ default: Env.get('CACHE_STORE', 'memory'),
1001
+
1002
+ stores: {
1003
+ memory: {
1004
+ driver: 'memory',
1005
+ },
1006
+
1007
+ redis: {
1008
+ driver: 'redis',
1009
+ url: Env.get('REDIS_URL', ''),
1010
+ host: Env.get('REDIS_HOST', '127.0.0.1'),
1011
+ port: Env.getNumber('REDIS_PORT', 6379),
1012
+ password: Env.get('REDIS_PASSWORD', ''),
1013
+ prefix: Env.get('CACHE_PREFIX', 'rudderjs:'),
1014
+ },
1015
+ },
1016
+ } satisfies CacheConfig
1017
+ `;
1018
+ }
1019
+ function configStorage() {
1020
+ return `import path from 'node:path'
1021
+ import { Env } from '@rudderjs/support'
1022
+ import type { StorageConfig } from '@rudderjs/storage'
1023
+
1024
+ export default {
1025
+ default: Env.get('FILESYSTEM_DISK', 'local'),
1026
+
1027
+ disks: {
1028
+ local: {
1029
+ driver: 'local',
1030
+ root: path.resolve(process.cwd(), 'storage/app'),
1031
+ baseUrl: '/api/files',
1032
+ },
1033
+
1034
+ public: {
1035
+ driver: 'local',
1036
+ root: path.resolve(process.cwd(), 'storage/app/public'),
1037
+ baseUrl: Env.get('APP_URL', 'http://localhost:3000') + '/storage',
1038
+ },
1039
+
1040
+ s3: {
1041
+ driver: 's3',
1042
+ bucket: Env.get('AWS_BUCKET', ''),
1043
+ region: Env.get('AWS_DEFAULT_REGION', 'us-east-1'),
1044
+ accessKeyId: Env.get('AWS_ACCESS_KEY_ID', ''),
1045
+ secretAccessKey: Env.get('AWS_SECRET_ACCESS_KEY', ''),
1046
+ endpoint: Env.get('AWS_ENDPOINT', ''),
1047
+ baseUrl: Env.get('AWS_URL', ''),
1048
+ },
1049
+ },
1050
+ } satisfies StorageConfig
1051
+ `;
1052
+ }
1053
+ function configAuth(_ctx) {
1054
+ return `import { Env } from '@rudderjs/support'
1055
+ import type { BetterAuthConfig } from '@rudderjs/auth'
1056
+
1057
+ export default {
1058
+ secret: Env.get('AUTH_SECRET', 'please-set-AUTH_SECRET-min-32-chars!!'),
1059
+ baseUrl: Env.get('APP_URL', 'http://localhost:3000'),
1060
+ emailAndPassword: { enabled: true },
1061
+ } satisfies BetterAuthConfig
1062
+ `;
1063
+ }
1064
+ function configIndex(ctx) {
1065
+ const imports = [
1066
+ "import app from './app.js'",
1067
+ "import server from './server.js'",
1068
+ "import log from './log.js'",
1069
+ ];
1070
+ const keys = ['app', 'server', 'log'];
1071
+ if (ctx.orm) {
1072
+ imports.push("import database from './database.js'");
1073
+ keys.push('database');
1074
+ }
1075
+ if (ctx.packages.auth) {
1076
+ imports.push("import auth from './auth.js'");
1077
+ imports.push("import session from './session.js'");
1078
+ imports.push("import hash from './hash.js'");
1079
+ keys.push('auth', 'session', 'hash');
1080
+ }
1081
+ if (ctx.packages.queue) {
1082
+ imports.push("import queue from './queue.js'");
1083
+ keys.push('queue');
1084
+ }
1085
+ if (ctx.packages.mail) {
1086
+ imports.push("import mail from './mail.js'");
1087
+ keys.push('mail');
1088
+ }
1089
+ if (ctx.packages.cache) {
1090
+ imports.push("import cache from './cache.js'");
1091
+ keys.push('cache');
1092
+ }
1093
+ if (ctx.packages.storage) {
1094
+ imports.push("import storage from './storage.js'");
1095
+ keys.push('storage');
1096
+ }
1097
+ if (ctx.packages.ai) {
1098
+ imports.push("import ai from './ai.js'");
1099
+ keys.push('ai');
1100
+ }
1101
+ return `${imports.join('\n')}
1102
+
1103
+ const configs = { ${keys.join(', ')} }
1104
+
1105
+ export type Configs = typeof configs
1106
+
1107
+ export default configs
1108
+ `;
1109
+ }
1110
+ function envDts() {
1111
+ return `import type { Configs } from './config/index.js'
1112
+
1113
+ declare module '@rudderjs/core' {
1114
+ interface AppConfig extends Configs {}
1115
+ }
1116
+ `;
1117
+ }
1118
+ function configSession() {
1119
+ return `import { Env } from '@rudderjs/support'
1120
+ import type { SessionConfig } from '@rudderjs/session'
1121
+
1122
+ export default {
1123
+ driver: Env.get('SESSION_DRIVER', 'cookie') as 'cookie' | 'redis',
1124
+ lifetime: 120,
1125
+ secret: Env.get('SESSION_SECRET', 'change-me-in-production'),
1126
+ cookie: {
1127
+ name: 'rudderjs_session',
1128
+ secure: Env.getBool('SESSION_SECURE', false),
1129
+ httpOnly: true,
1130
+ sameSite: 'lax' as const,
1131
+ path: '/',
1132
+ },
1133
+ redis: { prefix: 'session:', url: Env.get('REDIS_URL', '') },
1134
+ } satisfies SessionConfig
1135
+ `;
1136
+ }
1137
+ function configAi() {
1138
+ return `import { Env } from '@rudderjs/support'
1139
+ import type { AiConfig } from '@rudderjs/ai'
1140
+
1141
+ export default {
1142
+ default: Env.get('AI_MODEL', 'anthropic/claude-sonnet-4-5'),
1143
+
1144
+ providers: {
1145
+ anthropic: {
1146
+ driver: 'anthropic',
1147
+ apiKey: Env.get('ANTHROPIC_API_KEY', ''),
1148
+ },
1149
+
1150
+ openai: {
1151
+ driver: 'openai',
1152
+ apiKey: Env.get('OPENAI_API_KEY', ''),
1153
+ },
1154
+
1155
+ google: {
1156
+ driver: 'google',
1157
+ apiKey: Env.get('GOOGLE_AI_API_KEY', ''),
1158
+ },
1159
+
1160
+ ollama: {
1161
+ driver: 'ollama',
1162
+ baseUrl: Env.get('OLLAMA_BASE_URL', 'http://localhost:11434'),
1163
+ },
1164
+ },
1165
+ } satisfies AiConfig
1166
+ `;
1167
+ }
1168
+ // ─── app files ─────────────────────────────────────────────
1169
+ function userModel() {
1170
+ return `import { Model } from '@rudderjs/orm'
1171
+
1172
+ export class User extends Model {
1173
+ // Prisma accessor is the model name lowercased
1174
+ static table = 'user'
1175
+
1176
+ id!: string
1177
+ name!: string
1178
+ email!: string
1179
+ emailVerified!: boolean
1180
+ role!: string
1181
+ createdAt!: Date
1182
+ updatedAt!: Date
1183
+ }
1184
+ `;
1185
+ }
1186
+ function appServiceProvider() {
1187
+ return `import { ServiceProvider } from '@rudderjs/core'
1188
+
1189
+ export class AppServiceProvider extends ServiceProvider {
1190
+ register(): void {
1191
+ // Register your application-level services here:
1192
+ // this.app.singleton(MyService, () => new MyService())
1193
+ }
1194
+
1195
+ boot(): void {
1196
+ console.log(\`[AppServiceProvider] booted — \${this.app.name}\`)
1197
+ }
1198
+ }
1199
+ `;
1200
+ }
1201
+ function requestIdMiddleware() {
1202
+ return `import { Middleware } from '@rudderjs/middleware'
1203
+ import type { AppRequest, AppResponse } from '@rudderjs/contracts'
1204
+
1205
+ /**
1206
+ * Attaches a unique X-Request-Id header to every response.
1207
+ * Useful for distributed tracing and log correlation.
1208
+ *
1209
+ * Registered globally in bootstrap/app.ts via withMiddleware().
1210
+ */
1211
+ export class RequestIdMiddleware extends Middleware {
1212
+ async handle(req: AppRequest, res: AppResponse, next: () => Promise<void>): Promise<void> {
1213
+ const id = req.headers['x-request-id'] ?? crypto.randomUUID()
1214
+ ;(req as unknown as Record<string, unknown>)['requestId'] = id
1215
+ await next()
1216
+ res.header('X-Request-Id', id)
1217
+ }
1218
+ }
1219
+ `;
1220
+ }
1221
+ // ─── routes ────────────────────────────────────────────────
1222
+ function routesApi(ctx) {
1223
+ const imports = [
1224
+ "import { router } from '@rudderjs/router'",
1225
+ ];
1226
+ const lines = [];
1227
+ if (ctx.packages.auth || ctx.packages.ai) {
1228
+ imports.push("import { app } from '@rudderjs/core'");
1229
+ }
1230
+ if (ctx.packages.auth) {
1231
+ imports.push("import type { BetterAuthInstance } from '@rudderjs/auth'");
1232
+ imports.push("import { RateLimit } from '@rudderjs/middleware'");
1233
+ lines.push('');
1234
+ lines.push("const authLimit = RateLimit.perMinute(10).message('Too many auth attempts. Try again later.')");
1235
+ }
1236
+ if (ctx.packages.ai) {
1237
+ imports.push("import { AI } from '@rudderjs/ai'");
1238
+ }
1239
+ lines.push('');
1240
+ lines.push("router.get('/api/health', (_req, res) => res.json({ status: 'ok' }))");
1241
+ if (ctx.packages.auth) {
1242
+ lines.push('');
1243
+ lines.push(`// GET /api/me — returns current session or null
1244
+ router.get('/api/me', async (req) => {
1245
+ const auth = app().make<BetterAuthInstance>('auth')
1246
+ const session = await auth.api.getSession({
1247
+ headers: new Headers(req.headers as Record<string, string>),
1248
+ })
1249
+ return Response.json(session ?? { user: null, session: null })
1250
+ })`);
1251
+ }
1252
+ if (ctx.withTodo) {
1253
+ lines.push('');
1254
+ lines.push('// Todo routes are registered by TodoServiceProvider — see app/Modules/Todo/TodoServiceProvider.ts');
1255
+ }
1256
+ if (ctx.packages.auth) {
1257
+ lines.push('');
1258
+ lines.push(`// All /api/auth/* requests are handled by better-auth
1259
+ router.all('/api/auth/*', (req) => {
1260
+ const auth = app().make<BetterAuthInstance>('auth')
1261
+ const honoCtx = req.raw as { req: { raw: Request } }
1262
+ return auth.handler(honoCtx.req.raw)
1263
+ }, [authLimit])`);
1264
+ }
1265
+ if (ctx.packages.ai) {
1266
+ lines.push('');
1267
+ lines.push(`// POST /api/ai/chat — simple AI chat endpoint
1268
+ router.post('/api/ai/chat', async (req, res) => {
1269
+ const { messages } = req.body as { messages: { role: string; content: string }[] }
1270
+ const lastMessage = messages.at(-1)?.content ?? ''
1271
+ const response = await AI.agent('You are a helpful assistant.').prompt(lastMessage)
1272
+ res.json({ message: response.text })
1273
+ })`);
1274
+ }
1275
+ lines.push('');
1276
+ lines.push("// Catch-all: any unmatched /api/* route returns 404");
1277
+ lines.push("router.all('/api/*', (_req, res) => res.status(404).json({ message: 'Route not found.' }))");
1278
+ return imports.join('\n') + '\n' + lines.join('\n') + '\n';
1279
+ }
1280
+ function routesWeb() {
1281
+ return `import { router } from '@rudderjs/router'
1282
+
1283
+ // Web routes — HTML redirects, guards, and non-API server responses
1284
+ // These run before Vike's file-based page routing
1285
+ // Use this file for: redirects, server-side auth guards, download routes, sitemaps, etc.
1286
+
1287
+ // Example: redirect root to /todos
1288
+ // router.get('/', (_req, res) => res.redirect('/todos'))
1289
+ `;
1290
+ }
1291
+ function routesConsole() {
1292
+ return `import { rudder } from '@rudderjs/rudder'
1293
+
1294
+ rudder.command('inspire', () => {
1295
+ const quotes = [
1296
+ 'The best way to predict the future is to create it.',
1297
+ 'Build something people want.',
1298
+ 'Stay hungry, stay foolish.',
1299
+ 'Code is poetry.',
1300
+ 'Simplicity is the soul of efficiency.',
1301
+ ]
1302
+ const quote = quotes[Math.floor(Math.random() * quotes.length)]!
1303
+ console.log(\`\\n "\${quote}"\\n\`)
1304
+ }).description('Display an inspiring quote')
1305
+
1306
+ rudder.command('db:seed', async () => {
1307
+ // TODO: add your seed data here
1308
+ console.log('No seed data configured. Edit routes/console.ts to add seed logic.')
1309
+ }).description('Seed the database with sample data')
1310
+ `;
1311
+ }
1312
+ // ─── pages ─────────────────────────────────────────────────
1313
+ function pagesRootConfig(ctx) {
1314
+ if (ctx.frameworks.length === 1) {
1315
+ const rendererImport = ctx.primary === 'vue'
1316
+ ? `import vikeVue from 'vike-vue/config'`
1317
+ : ctx.primary === 'solid'
1318
+ ? `import vikeSolid from 'vike-solid/config'`
1319
+ : `import vikeReact from 'vike-react/config'`;
1320
+ const rendererVar = ctx.primary === 'vue' ? 'vikeVue' : ctx.primary === 'solid' ? 'vikeSolid' : 'vikeReact';
1321
+ return `import type { Config } from 'vike/types'
1322
+ ${rendererImport}
1323
+
1324
+ export default {
1325
+ extends: [${rendererVar}],
1326
+ } satisfies Config
1327
+ `;
1328
+ }
1329
+ // Multi-framework: no renderer in root config — each page picks its own
1330
+ return `import type { Config } from 'vike/types'
1331
+
1332
+ export default {} satisfies Config
1333
+ `;
1334
+ }
1335
+ function pagesIndexConfig(ctx) {
1336
+ switch (ctx.primary) {
1337
+ case 'vue':
1338
+ return `import type { Config } from 'vike/types'
1339
+ import vikeVue from 'vike-vue/config'
1340
+
1341
+ export default {
1342
+ extends: vikeVue,
1343
+ } satisfies Config
1344
+ `;
1345
+ case 'solid':
1346
+ return `import type { Config } from 'vike/types'
1347
+ import vikeSolid from 'vike-solid/config'
1348
+
1349
+ export default {
1350
+ extends: vikeSolid,
1351
+ } satisfies Config
1352
+ `;
1353
+ default: // react
1354
+ return `import type { Config } from 'vike/types'
1355
+ import vikeReact from 'vike-react/config'
1356
+
1357
+ export default {
1358
+ extends: vikeReact,
1359
+ } satisfies Config
1360
+ `;
1361
+ }
1362
+ }
1363
+ function pagesIndexData(ctx) {
1364
+ if (!ctx.packages.auth) {
1365
+ return `export type Data = {
1366
+ message: string
1367
+ }
1368
+
1369
+ export async function data(): Promise<Data> {
1370
+ return { message: 'Welcome to RudderJS' }
1371
+ }
1372
+ `;
1373
+ }
1374
+ return `import { app } from '@rudderjs/core'
1375
+ import type { BetterAuthInstance } from '@rudderjs/auth'
1376
+
1377
+ export type Data = {
1378
+ user: { id: string; name: string; email: string } | null
1379
+ }
1380
+
1381
+ export async function data(pageContext: unknown): Promise<Data> {
1382
+ const auth = app().make<BetterAuthInstance>('auth')
1383
+ const ctx = pageContext as { headers?: Record<string, string> }
1384
+ const session = await auth.api.getSession({
1385
+ headers: new Headers(ctx.headers ?? {}),
1386
+ })
1387
+ return { user: session?.user ?? null }
1388
+ }
1389
+ `;
1390
+ }
1391
+ function pagesIndexPage(ctx) {
1392
+ switch (ctx.primary) {
1393
+ case 'vue': return pagesIndexPageVue(ctx);
1394
+ case 'solid': return pagesIndexPageSolid(ctx);
1395
+ default: return pagesIndexPageReact(ctx);
1396
+ }
1397
+ }
1398
+ function pagesIndexPageReact(ctx) {
1399
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1400
+ const extraLinks = [];
1401
+ if (ctx.withTodo)
1402
+ extraLinks.push(' <a href="/todos" className="underline hover:text-foreground">Todos</a>');
1403
+ if (ctx.packages.ai)
1404
+ extraLinks.push(' <a href="/ai-chat" className="underline hover:text-foreground">AI Chat</a>');
1405
+ const extraLinksStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
1406
+ if (!ctx.packages.auth) {
1407
+ return `${cssImport}import { useData } from 'vike-react/useData'
1408
+ import type { Data } from './+data.js'
1409
+
1410
+ export default function Page() {
1411
+ const data = useData<Data>()
1412
+
1413
+ return (
1414
+ <div className="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1415
+ <h1 className="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1416
+ <p className="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1417
+
1418
+ <div className="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1419
+ <a href="/api/health" className="underline hover:text-foreground">API Health</a>\${extraLinksStr}
1420
+ </div>
1421
+ </div>
1422
+ )
1423
+ }
1424
+ `;
1425
+ }
1426
+ const todosLink = ctx.withTodo
1427
+ ? ` <a href="/todos" className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">View Todos</a>`
1428
+ : '';
1429
+ return `${cssImport}import { useState } from 'react'
1430
+ import { useData } from 'vike-react/useData'
1431
+ import type { Data } from './+data.js'
1432
+
1433
+ export default function Page() {
1434
+ const data = useData<Data>()
1435
+ const [user, setUser] = useState(data.user)
1436
+
1437
+ async function signOut() {
1438
+ await fetch('/api/auth/sign-out', {
1439
+ method: 'POST',
1440
+ headers: { 'Content-Type': 'application/json' },
1441
+ body: '{}',
1442
+ })
1443
+ window.location.href = '/'
1444
+ }
1445
+
1446
+ return (
1447
+ <div className="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1448
+ <h1 className="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1449
+ <p className="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1450
+
1451
+ {user ? (
1452
+ <div className="flex flex-col items-center gap-3">
1453
+ <p className="text-sm text-muted-foreground">
1454
+ Signed in as <span className="font-medium text-foreground">{user.name}</span>
1455
+ </p>
1456
+ <div className="flex gap-2">
1457
+ ${todosLink}
1458
+ <button
1459
+ onClick={signOut}
1460
+ className="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent"
1461
+ >
1462
+ Sign out
1463
+ </button>
1464
+ </div>
1465
+ </div>
1466
+ ) : (
1467
+ <div className="flex gap-2">
1468
+ ${todosLink}
1469
+ <a href="/register" className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">Register</a>
1470
+ <a href="/login" className="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent">Login</a>
1471
+ </div>
1472
+ )}
1473
+
1474
+ <div className="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1475
+ <a href="/api/health" className="underline hover:text-foreground">API Health</a>
1476
+ <a href="/api/me" className="underline hover:text-foreground">Session Info</a>\${extraLinksStr}
1477
+ </div>
1478
+ </div>
1479
+ )
1480
+ }
1481
+ `;
1482
+ }
1483
+ function pagesIndexPageVue(ctx) {
1484
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1485
+ const extraLinks = [];
1486
+ if (ctx.withTodo)
1487
+ extraLinks.push(' <a href="/todos" class="underline hover:text-foreground">Todos</a>');
1488
+ if (ctx.packages.ai)
1489
+ extraLinks.push(' <a href="/ai-chat" class="underline hover:text-foreground">AI Chat</a>');
1490
+ const extraStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
1491
+ if (!ctx.packages.auth) {
1492
+ return `<script setup lang="ts">
1493
+ ${cssImport}import { useData } from 'vike-vue/useData'
1494
+ import type { Data } from './+data.js'
1495
+
1496
+ const data = useData<Data>()
1497
+ </script>
1498
+
1499
+ <template>
1500
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1501
+ <h1 class="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1502
+ <p class="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1503
+
1504
+ <div class="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1505
+ <a href="/api/health" class="underline hover:text-foreground">API Health</a>${extraStr}
1506
+ </div>
1507
+ </div>
1508
+ </template>
1509
+ `;
1510
+ }
1511
+ const todosLink = ctx.withTodo
1512
+ ? `\n <a href="/todos" class="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">View Todos</a>`
1513
+ : '';
1514
+ return `<script setup lang="ts">
1515
+ ${cssImport}import { ref } from 'vue'
1516
+ import { useData } from 'vike-vue/useData'
1517
+ import type { Data } from './+data.js'
1518
+
1519
+ const data = useData<Data>()
1520
+ const user = ref(data.user)
1521
+
1522
+ async function signOut() {
1523
+ await fetch('/api/auth/sign-out', {
1524
+ method: 'POST',
1525
+ headers: { 'Content-Type': 'application/json' },
1526
+ body: '{}',
1527
+ })
1528
+ window.location.href = '/'
1529
+ }
1530
+ </script>
1531
+
1532
+ <template>
1533
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1534
+ <h1 class="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1535
+ <p class="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1536
+
1537
+ <div v-if="user" class="flex flex-col items-center gap-3">
1538
+ <p class="text-sm text-muted-foreground">
1539
+ Signed in as <span class="font-medium text-foreground">{{ user.name }}</span>
1540
+ </p>
1541
+ <div class="flex gap-2">${todosLink}
1542
+ <button @click="signOut" class="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent">
1543
+ Sign out
1544
+ </button>
1545
+ </div>
1546
+ </div>
1547
+ <div v-else class="flex gap-2">${todosLink}
1548
+ <a href="/register" class="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">Register</a>
1549
+ <a href="/login" class="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent">Login</a>
1550
+ </div>
1551
+
1552
+ <div class="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1553
+ <a href="/api/health" class="underline hover:text-foreground">API Health</a>
1554
+ <a href="/api/me" class="underline hover:text-foreground">Session Info</a>${extraStr}
1555
+ </div>
1556
+ </div>
1557
+ </template>
1558
+ `;
1559
+ }
1560
+ function pagesIndexPageSolid(ctx) {
1561
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1562
+ const extraLinks = [];
1563
+ if (ctx.withTodo)
1564
+ extraLinks.push(' <a href="/todos" class="underline hover:text-foreground">Todos</a>');
1565
+ if (ctx.packages.ai)
1566
+ extraLinks.push(' <a href="/ai-chat" class="underline hover:text-foreground">AI Chat</a>');
1567
+ const extraStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
1568
+ if (!ctx.packages.auth) {
1569
+ return `${cssImport}import { useData } from 'vike-solid/useData'
1570
+ import type { Data } from './+data.js'
1571
+
1572
+ export default function Page() {
1573
+ const data = useData<Data>()
1574
+
1575
+ return (
1576
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1577
+ <h1 class="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1578
+ <p class="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1579
+
1580
+ <div class="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1581
+ <a href="/api/health" class="underline hover:text-foreground">API Health</a>\${extraStr}
1582
+ </div>
1583
+ </div>
1584
+ )
1585
+ }
1586
+ `;
1587
+ }
1588
+ const todosLink = ctx.withTodo
1589
+ ? `\n <a href="/todos" class="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">View Todos</a>`
1590
+ : '';
1591
+ return `${cssImport}import { createSignal } from 'solid-js'
1592
+ import { useData } from 'vike-solid/useData'
1593
+ import type { Data } from './+data.js'
1594
+
1595
+ export default function Page() {
1596
+ const data = useData<Data>()
1597
+ const [user, setUser] = createSignal(data.user)
1598
+
1599
+ async function signOut() {
1600
+ await fetch('/api/auth/sign-out', {
1601
+ method: 'POST',
1602
+ headers: { 'Content-Type': 'application/json' },
1603
+ body: '{}',
1604
+ })
1605
+ window.location.href = '/'
1606
+ }
1607
+
1608
+ return (
1609
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
1610
+ <h1 class="text-4xl font-bold tracking-tight">${ctx.name}</h1>
1611
+ <p class="text-muted-foreground">Built with RudderJS — Laravel-inspired Node.js framework.</p>
1612
+
1613
+ {user() ? (
1614
+ <div class="flex flex-col items-center gap-3">
1615
+ <p class="text-sm text-muted-foreground">
1616
+ Signed in as <span class="font-medium text-foreground">{user()!.name}</span>
1617
+ </p>
1618
+ <div class="flex gap-2">${todosLink}
1619
+ <button
1620
+ onClick={signOut}
1621
+ class="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent"
1622
+ >
1623
+ Sign out
1624
+ </button>
1625
+ </div>
1626
+ </div>
1627
+ ) : (
1628
+ <div class="flex gap-2">${todosLink}
1629
+ <a href="/register" class="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">Register</a>
1630
+ <a href="/login" class="inline-flex h-9 items-center rounded-md border px-4 text-sm font-medium hover:bg-accent">Login</a>
1631
+ </div>
1632
+ )}
1633
+
1634
+ <div class="mt-4 flex flex-wrap gap-3 text-xs text-muted-foreground">
1635
+ <a href="/api/health" class="underline hover:text-foreground">API Health</a>
1636
+ <a href="/api/me" class="underline hover:text-foreground">Session Info</a>\${extraStr}
1637
+ </div>
1638
+ </div>
1639
+ )
1640
+ }
1641
+ `;
1642
+ }
1643
+ function pagesErrorConfig(ctx) {
1644
+ switch (ctx.primary) {
1645
+ case 'vue':
1646
+ return `import type { Config } from 'vike/types'
1647
+ import vikeVue from 'vike-vue/config'
1648
+
1649
+ export default {
1650
+ extends: vikeVue,
1651
+ } satisfies Config
1652
+ `;
1653
+ case 'solid':
1654
+ return `import type { Config } from 'vike/types'
1655
+ import vikeSolid from 'vike-solid/config'
1656
+
1657
+ export default {
1658
+ extends: vikeSolid,
1659
+ } satisfies Config
1660
+ `;
1661
+ default:
1662
+ return `import type { Config } from 'vike/types'
1663
+ import vikeReact from 'vike-react/config'
1664
+
1665
+ export default {
1666
+ extends: vikeReact,
1667
+ } satisfies Config
1668
+ `;
1669
+ }
1670
+ }
1671
+ function pagesErrorPage(ctx) {
1672
+ switch (ctx.primary) {
1673
+ case 'vue': return pagesErrorPageVue(ctx);
1674
+ case 'solid': return pagesErrorPageSolid(ctx);
1675
+ default: return pagesErrorPageReact(ctx);
1676
+ }
1677
+ }
1678
+ function pagesErrorPageReact(ctx) {
1679
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1680
+ return `${cssImport}import { usePageContext } from 'vike-react/usePageContext'
1681
+
1682
+ export default function Page() {
1683
+ const { is404, abortReason, abortStatusCode } = usePageContext() as {
1684
+ is404: boolean
1685
+ abortStatusCode?: number
1686
+ abortReason?: string
1687
+ }
1688
+
1689
+ if (is404) {
1690
+ return (
1691
+ <div className="flex min-h-svh flex-col items-center justify-center gap-2">
1692
+ <h1 className="text-2xl font-bold">404 — Page Not Found</h1>
1693
+ <p className="text-muted-foreground">This page could not be found.</p>
1694
+ <a href="/" className="mt-4 text-sm underline">Go home</a>
1695
+ </div>
1696
+ )
1697
+ }
1698
+
1699
+ if (abortStatusCode === 401) {
1700
+ return (
1701
+ <div className="flex min-h-svh flex-col items-center justify-center gap-2">
1702
+ <h1 className="text-2xl font-bold">401 — Unauthorized</h1>
1703
+ <p className="text-muted-foreground">{abortReason ?? 'You must be logged in to view this page.'}</p>
1704
+ <a href="/" className="mt-4 text-sm underline">Go home</a>
1705
+ </div>
1706
+ )
1707
+ }
1708
+
1709
+ return (
1710
+ <div className="flex min-h-svh flex-col items-center justify-center gap-2">
1711
+ <h1 className="text-2xl font-bold">Something went wrong</h1>
1712
+ <p className="text-muted-foreground">{abortReason ?? 'An unexpected error occurred.'}</p>
1713
+ <a href="/" className="mt-4 text-sm underline">Go home</a>
1714
+ </div>
1715
+ )
1716
+ }
1717
+ `;
1718
+ }
1719
+ function pagesErrorPageVue(ctx) {
1720
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1721
+ return `<script setup lang="ts">
1722
+ ${cssImport}import { usePageContext } from 'vike-vue/usePageContext'
1723
+
1724
+ const pageContext = usePageContext() as {
1725
+ is404: boolean
1726
+ abortStatusCode?: number
1727
+ abortReason?: string
1728
+ }
1729
+ </script>
1730
+
1731
+ <template>
1732
+ <div v-if="pageContext.is404" class="flex min-h-svh flex-col items-center justify-center gap-2">
1733
+ <h1 class="text-2xl font-bold">404 — Page Not Found</h1>
1734
+ <p class="text-muted-foreground">This page could not be found.</p>
1735
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1736
+ </div>
1737
+ <div v-else-if="pageContext.abortStatusCode === 401" class="flex min-h-svh flex-col items-center justify-center gap-2">
1738
+ <h1 class="text-2xl font-bold">401 — Unauthorized</h1>
1739
+ <p class="text-muted-foreground">{{ pageContext.abortReason ?? 'You must be logged in to view this page.' }}</p>
1740
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1741
+ </div>
1742
+ <div v-else class="flex min-h-svh flex-col items-center justify-center gap-2">
1743
+ <h1 class="text-2xl font-bold">Something went wrong</h1>
1744
+ <p class="text-muted-foreground">{{ pageContext.abortReason ?? 'An unexpected error occurred.' }}</p>
1745
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1746
+ </div>
1747
+ </template>
1748
+ `;
1749
+ }
1750
+ function pagesErrorPageSolid(ctx) {
1751
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1752
+ return `${cssImport}import { Switch, Match } from 'solid-js'
1753
+ import { usePageContext } from 'vike-solid/usePageContext'
1754
+
1755
+ export default function Page() {
1756
+ const pageContext = usePageContext() as {
1757
+ is404: boolean
1758
+ abortStatusCode?: number
1759
+ abortReason?: string
1760
+ }
1761
+
1762
+ return (
1763
+ <Switch>
1764
+ <Match when={pageContext.is404}>
1765
+ <div class="flex min-h-svh flex-col items-center justify-center gap-2">
1766
+ <h1 class="text-2xl font-bold">404 — Page Not Found</h1>
1767
+ <p class="text-muted-foreground">This page could not be found.</p>
1768
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1769
+ </div>
1770
+ </Match>
1771
+ <Match when={pageContext.abortStatusCode === 401}>
1772
+ <div class="flex min-h-svh flex-col items-center justify-center gap-2">
1773
+ <h1 class="text-2xl font-bold">401 — Unauthorized</h1>
1774
+ <p class="text-muted-foreground">{pageContext.abortReason ?? 'You must be logged in to view this page.'}</p>
1775
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1776
+ </div>
1777
+ </Match>
1778
+ <Match when={true}>
1779
+ <div class="flex min-h-svh flex-col items-center justify-center gap-2">
1780
+ <h1 class="text-2xl font-bold">Something went wrong</h1>
1781
+ <p class="text-muted-foreground">{pageContext.abortReason ?? 'An unexpected error occurred.'}</p>
1782
+ <a href="/" class="mt-4 text-sm underline">Go home</a>
1783
+ </div>
1784
+ </Match>
1785
+ </Switch>
1786
+ )
1787
+ }
1788
+ `;
1789
+ }
1790
+ // ─── Todo module ───────────────────────────────────────────
1791
+ function todoSchema() {
1792
+ return `import { z } from 'zod'
1793
+
1794
+ export const TodoInputSchema = z.object({
1795
+ title: z.string().min(1, 'Title is required'),
1796
+ completed: z.boolean().optional().default(false),
1797
+ })
1798
+
1799
+ export const TodoUpdateSchema = z.object({
1800
+ title: z.string().min(1).optional(),
1801
+ completed: z.boolean().optional(),
1802
+ })
1803
+
1804
+ export type TodoInput = z.infer<typeof TodoInputSchema>
1805
+ export type TodoUpdate = z.infer<typeof TodoUpdateSchema>
1806
+
1807
+ export interface Todo {
1808
+ id: string
1809
+ title: string
1810
+ completed: boolean
1811
+ createdAt: Date
1812
+ updatedAt: Date
1813
+ }
1814
+ `;
1815
+ }
1816
+ function todoService() {
1817
+ return `import { Injectable } from '@rudderjs/core'
1818
+ import { resolve } from '@rudderjs/core'
1819
+ import type { OrmAdapter } from '@rudderjs/orm'
1820
+ import type { Todo, TodoInput, TodoUpdate } from './TodoSchema.js'
1821
+
1822
+ @Injectable()
1823
+ export class TodoService {
1824
+ private get db(): OrmAdapter { return resolve<OrmAdapter>('db') }
1825
+
1826
+ findAll(): Promise<Todo[]> {
1827
+ return this.db.query<Todo>('todo').orderBy('createdAt', 'DESC').get()
1828
+ }
1829
+
1830
+ findById(id: string): Promise<Todo | null> {
1831
+ return this.db.query<Todo>('todo').find(id)
1832
+ }
1833
+
1834
+ create(input: TodoInput): Promise<Todo> {
1835
+ return this.db.query<Todo>('todo').create(input as Partial<Todo>)
1836
+ }
1837
+
1838
+ update(id: string, input: TodoUpdate): Promise<Todo> {
1839
+ return this.db.query<Todo>('todo').update(id, input as Partial<Todo>)
1840
+ }
1841
+
1842
+ delete(id: string): Promise<void> {
1843
+ return this.db.query<Todo>('todo').delete(id)
1844
+ }
1845
+ }
1846
+ `;
1847
+ }
1848
+ function todoServiceProvider() {
1849
+ return `import { ServiceProvider } from '@rudderjs/core'
1850
+ import { router } from '@rudderjs/router'
1851
+ import { TodoService } from './TodoService.js'
1852
+ import { TodoInputSchema, TodoUpdateSchema } from './TodoSchema.js'
1853
+
1854
+ export class TodoServiceProvider extends ServiceProvider {
1855
+ register(): void {
1856
+ this.app.singleton(TodoService, () => new TodoService())
1857
+ }
1858
+
1859
+ override async boot(): Promise<void> {
1860
+ const service = this.app.make<TodoService>(TodoService)
1861
+
1862
+ router.get('/api/todos', async (_req, res) => {
1863
+ const todos = await service.findAll()
1864
+ res.json({ data: todos })
1865
+ })
1866
+
1867
+ router.post('/api/todos', async (req, res) => {
1868
+ const parsed = TodoInputSchema.safeParse(req.body)
1869
+ if (!parsed.success) {
1870
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors })
1871
+ return
1872
+ }
1873
+ const todo = await service.create(parsed.data)
1874
+ res.status(201).json({ data: todo })
1875
+ })
1876
+
1877
+ router.patch('/api/todos/:id', async (req, res) => {
1878
+ const parsed = TodoUpdateSchema.safeParse(req.body)
1879
+ if (!parsed.success) {
1880
+ res.status(422).json({ errors: parsed.error.flatten().fieldErrors })
1881
+ return
1882
+ }
1883
+ const todo = await service.update(req.params['id']!, parsed.data)
1884
+ res.json({ data: todo })
1885
+ })
1886
+
1887
+ router.delete('/api/todos/:id', async (req, res) => {
1888
+ await service.delete(req.params['id']!)
1889
+ res.status(204).send('')
1890
+ })
1891
+ }
1892
+ }
1893
+ `;
1894
+ }
1895
+ function todoPageConfig(ctx) {
1896
+ switch (ctx.primary) {
1897
+ case 'vue':
1898
+ return `import type { Config } from 'vike/types'
1899
+ import vikeVue from 'vike-vue/config'
1900
+
1901
+ export default {
1902
+ extends: vikeVue,
1903
+ } satisfies Config
1904
+ `;
1905
+ case 'solid':
1906
+ return `import type { Config } from 'vike/types'
1907
+ import vikeSolid from 'vike-solid/config'
1908
+
1909
+ export default {
1910
+ extends: vikeSolid,
1911
+ } satisfies Config
1912
+ `;
1913
+ default:
1914
+ return `import type { Config } from 'vike/types'
1915
+ import vikeReact from 'vike-react/config'
1916
+
1917
+ export default {
1918
+ extends: vikeReact,
1919
+ } satisfies Config
1920
+ `;
1921
+ }
1922
+ }
1923
+ function todoPageData() {
1924
+ return `import { resolve } from '@rudderjs/core'
1925
+ import { TodoService } from '../../app/Modules/Todo/TodoService.js'
1926
+ import type { Todo } from '../../app/Modules/Todo/TodoSchema.js'
1927
+
1928
+ export type Data = { todos: Todo[] }
1929
+
1930
+ export async function data(): Promise<Data> {
1931
+ const service = resolve<TodoService>(TodoService)
1932
+ const todos = await service.findAll()
1933
+ return { todos }
1934
+ }
1935
+ `;
1936
+ }
1937
+ function todoPage(ctx) {
1938
+ switch (ctx.primary) {
1939
+ case 'vue': return todoPageVue(ctx);
1940
+ case 'solid': return todoPageSolid(ctx);
1941
+ default: return todoPageReact(ctx);
1942
+ }
1943
+ }
1944
+ function todoPageReact(ctx) {
1945
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
1946
+ return `${cssImport}import { useState } from 'react'
1947
+ import { useData } from 'vike-react/useData'
1948
+ import type { Data } from './+data.js'
1949
+ import type { Todo } from '../../app/Modules/Todo/TodoSchema.js'
1950
+
1951
+ export default function Page() {
1952
+ const data = useData<Data>()
1953
+ const [todos, setTodos] = useState<Todo[]>(data.todos)
1954
+ const [input, setInput] = useState('')
1955
+
1956
+ async function addTodo(e: React.FormEvent) {
1957
+ e.preventDefault()
1958
+ if (!input.trim()) return
1959
+ const res = await fetch('/api/todos', {
1960
+ method: 'POST',
1961
+ headers: { 'Content-Type': 'application/json' },
1962
+ body: JSON.stringify({ title: input }),
1963
+ })
1964
+ const json = await res.json() as { data: Todo }
1965
+ setTodos([json.data, ...todos])
1966
+ setInput('')
1967
+ }
1968
+
1969
+ async function toggleTodo(id: string, completed: boolean) {
1970
+ await fetch(\`/api/todos/\${id}\`, {
1971
+ method: 'PATCH',
1972
+ headers: { 'Content-Type': 'application/json' },
1973
+ body: JSON.stringify({ completed: !completed }),
1974
+ })
1975
+ setTodos(todos.map(t => t.id === id ? { ...t, completed: !completed } : t))
1976
+ }
1977
+
1978
+ async function deleteTodo(id: string) {
1979
+ await fetch(\`/api/todos/\${id}\`, { method: 'DELETE' })
1980
+ setTodos(todos.filter(t => t.id !== id))
1981
+ }
1982
+
1983
+ return (
1984
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 p-4">
1985
+ <h1 className="text-3xl font-bold">Todos</h1>
1986
+
1987
+ <form onSubmit={addTodo} className="flex w-full max-w-md gap-2">
1988
+ <input
1989
+ value={input}
1990
+ onChange={e => setInput(e.target.value)}
1991
+ placeholder="Add a new todo..."
1992
+ className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
1993
+ />
1994
+ <button
1995
+ type="submit"
1996
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
1997
+ >
1998
+ Add
1999
+ </button>
2000
+ </form>
2001
+
2002
+ <ul className="w-full max-w-md space-y-2">
2003
+ {todos.map(todo => (
2004
+ <li key={todo.id} className="flex items-center gap-3 rounded-lg border p-3">
2005
+ <input
2006
+ type="checkbox"
2007
+ checked={todo.completed}
2008
+ onChange={() => toggleTodo(todo.id, todo.completed)}
2009
+ className="h-4 w-4 cursor-pointer"
2010
+ />
2011
+ <span className={\`flex-1 text-sm \${todo.completed ? 'line-through text-muted-foreground' : ''}\`}>
2012
+ {todo.title}
2013
+ </span>
2014
+ <button
2015
+ onClick={() => deleteTodo(todo.id)}
2016
+ className="text-xs text-destructive hover:underline"
2017
+ >
2018
+ Delete
2019
+ </button>
2020
+ </li>
2021
+ ))}
2022
+ {todos.length === 0 && (
2023
+ <li className="py-8 text-center text-sm text-muted-foreground">
2024
+ No todos yet. Add one above!
2025
+ </li>
2026
+ )}
2027
+ </ul>
2028
+
2029
+ <a href="/" className="text-sm text-muted-foreground underline hover:text-foreground">
2030
+ ← Back to home
2031
+ </a>
2032
+ </div>
2033
+ )
2034
+ }
2035
+ `;
2036
+ }
2037
+ function todoPageVue(ctx) {
2038
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
2039
+ return `<script setup lang="ts">
2040
+ ${cssImport}import { ref } from 'vue'
2041
+ import { useData } from 'vike-vue/useData'
2042
+ import type { Data } from './+data.js'
2043
+ import type { Todo } from '../../app/Modules/Todo/TodoSchema.js'
2044
+
2045
+ const data = useData<Data>()
2046
+ const todos = ref<Todo[]>(data.todos)
2047
+ const input = ref('')
2048
+
2049
+ async function addTodo(e: Event) {
2050
+ e.preventDefault()
2051
+ if (!input.value.trim()) return
2052
+ const res = await fetch('/api/todos', {
2053
+ method: 'POST',
2054
+ headers: { 'Content-Type': 'application/json' },
2055
+ body: JSON.stringify({ title: input.value }),
2056
+ })
2057
+ const json = await res.json() as { data: Todo }
2058
+ todos.value = [json.data, ...todos.value]
2059
+ input.value = ''
2060
+ }
2061
+
2062
+ async function toggleTodo(id: string, completed: boolean) {
2063
+ await fetch(\`/api/todos/\${id}\`, {
2064
+ method: 'PATCH',
2065
+ headers: { 'Content-Type': 'application/json' },
2066
+ body: JSON.stringify({ completed: !completed }),
2067
+ })
2068
+ todos.value = todos.value.map(t => t.id === id ? { ...t, completed: !completed } : t)
2069
+ }
2070
+
2071
+ async function deleteTodo(id: string) {
2072
+ await fetch(\`/api/todos/\${id}\`, { method: 'DELETE' })
2073
+ todos.value = todos.value.filter(t => t.id !== id)
2074
+ }
2075
+ </script>
2076
+
2077
+ <template>
2078
+ <div class="flex min-h-svh flex-col items-center justify-center gap-6 p-4">
2079
+ <h1 class="text-3xl font-bold">Todos</h1>
2080
+
2081
+ <form @submit="addTodo" class="flex w-full max-w-md gap-2">
2082
+ <input
2083
+ v-model="input"
2084
+ placeholder="Add a new todo..."
2085
+ class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
2086
+ />
2087
+ <button
2088
+ type="submit"
2089
+ class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
2090
+ >
2091
+ Add
2092
+ </button>
2093
+ </form>
2094
+
2095
+ <ul class="w-full max-w-md space-y-2">
2096
+ <li v-for="todo in todos" :key="todo.id" class="flex items-center gap-3 rounded-lg border p-3">
2097
+ <input
2098
+ type="checkbox"
2099
+ :checked="todo.completed"
2100
+ @change="toggleTodo(todo.id, todo.completed)"
2101
+ class="h-4 w-4 cursor-pointer"
2102
+ />
2103
+ <span :class="['flex-1 text-sm', todo.completed ? 'line-through text-muted-foreground' : '']">
2104
+ {{ todo.title }}
2105
+ </span>
2106
+ <button @click="deleteTodo(todo.id)" class="text-xs text-destructive hover:underline">
2107
+ Delete
2108
+ </button>
2109
+ </li>
2110
+ <li v-if="todos.length === 0" class="py-8 text-center text-sm text-muted-foreground">
2111
+ No todos yet. Add one above!
2112
+ </li>
2113
+ </ul>
2114
+
2115
+ <a href="/" class="text-sm text-muted-foreground underline hover:text-foreground">
2116
+ ← Back to home
2117
+ </a>
2118
+ </div>
2119
+ </template>
2120
+ `;
2121
+ }
2122
+ function todoPageSolid(ctx) {
2123
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
2124
+ return `${cssImport}import { createSignal } from 'solid-js'
2125
+ import { For, Show } from 'solid-js'
2126
+ import { useData } from 'vike-solid/useData'
2127
+ import type { Data } from './+data.js'
2128
+ import type { Todo } from '../../app/Modules/Todo/TodoSchema.js'
2129
+
2130
+ export default function Page() {
2131
+ const data = useData<Data>()
2132
+ const [todos, setTodos] = createSignal<Todo[]>(data.todos)
2133
+ const [input, setInput] = createSignal('')
2134
+
2135
+ async function addTodo(e: Event) {
2136
+ e.preventDefault()
2137
+ if (!input().trim()) return
2138
+ const res = await fetch('/api/todos', {
2139
+ method: 'POST',
2140
+ headers: { 'Content-Type': 'application/json' },
2141
+ body: JSON.stringify({ title: input() }),
2142
+ })
2143
+ const json = await res.json() as { data: Todo }
2144
+ setTodos([json.data, ...todos()])
2145
+ setInput('')
2146
+ }
2147
+
2148
+ async function toggleTodo(id: string, completed: boolean) {
2149
+ await fetch(\`/api/todos/\${id}\`, {
2150
+ method: 'PATCH',
2151
+ headers: { 'Content-Type': 'application/json' },
2152
+ body: JSON.stringify({ completed: !completed }),
2153
+ })
2154
+ setTodos(todos().map(t => t.id === id ? { ...t, completed: !completed } : t))
2155
+ }
2156
+
2157
+ async function deleteTodo(id: string) {
2158
+ await fetch(\`/api/todos/\${id}\`, { method: 'DELETE' })
2159
+ setTodos(todos().filter(t => t.id !== id))
2160
+ }
2161
+
2162
+ return (
2163
+ <div class="flex min-h-svh flex-col items-center justify-center gap-6 p-4">
2164
+ <h1 class="text-3xl font-bold">Todos</h1>
2165
+
2166
+ <form onSubmit={addTodo} class="flex w-full max-w-md gap-2">
2167
+ <input
2168
+ value={input()}
2169
+ onInput={e => setInput(e.currentTarget.value)}
2170
+ placeholder="Add a new todo..."
2171
+ class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
2172
+ />
2173
+ <button
2174
+ type="submit"
2175
+ class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
2176
+ >
2177
+ Add
2178
+ </button>
2179
+ </form>
2180
+
2181
+ <ul class="w-full max-w-md space-y-2">
2182
+ <For each={todos()} fallback={
2183
+ <li class="py-8 text-center text-sm text-muted-foreground">No todos yet. Add one above!</li>
2184
+ }>
2185
+ {(todo) => (
2186
+ <li class="flex items-center gap-3 rounded-lg border p-3">
2187
+ <input
2188
+ type="checkbox"
2189
+ checked={todo.completed}
2190
+ onChange={() => toggleTodo(todo.id, todo.completed)}
2191
+ class="h-4 w-4 cursor-pointer"
2192
+ />
2193
+ <span class={\`flex-1 text-sm \${todo.completed ? 'line-through text-muted-foreground' : ''}\`}>
2194
+ {todo.title}
2195
+ </span>
2196
+ <button onClick={() => deleteTodo(todo.id)} class="text-xs text-destructive hover:underline">
2197
+ Delete
2198
+ </button>
2199
+ </li>
2200
+ )}
2201
+ </For>
2202
+ </ul>
2203
+
2204
+ <a href="/" class="text-sm text-muted-foreground underline hover:text-foreground">
2205
+ ← Back to home
2206
+ </a>
2207
+ </div>
2208
+ )
2209
+ }
2210
+ `;
2211
+ }
2212
+ // ─── AI chat page ─────────────────────────────────────────
2213
+ function aiChatPageConfig(ctx) {
2214
+ switch (ctx.primary) {
2215
+ case 'vue':
2216
+ return `import type { Config } from 'vike/types'
2217
+ import vikeVue from 'vike-vue/config'
2218
+
2219
+ export default {
2220
+ extends: vikeVue,
2221
+ } satisfies Config
2222
+ `;
2223
+ case 'solid':
2224
+ return `import type { Config } from 'vike/types'
2225
+ import vikeSolid from 'vike-solid/config'
2226
+
2227
+ export default {
2228
+ extends: vikeSolid,
2229
+ } satisfies Config
2230
+ `;
2231
+ default:
2232
+ return `import type { Config } from 'vike/types'
2233
+ import vikeReact from 'vike-react/config'
2234
+
2235
+ export default {
2236
+ extends: vikeReact,
2237
+ } satisfies Config
2238
+ `;
2239
+ }
2240
+ }
2241
+ function aiChatPage(ctx) {
2242
+ switch (ctx.primary) {
2243
+ case 'vue': return aiChatPageVue(ctx);
2244
+ case 'solid': return aiChatPageSolid(ctx);
2245
+ default: return aiChatPageReact(ctx);
2246
+ }
2247
+ }
2248
+ function aiChatPageReact(ctx) {
2249
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
2250
+ return `${cssImport}import { useState, useRef, useEffect } from 'react'
2251
+
2252
+ interface Message {
2253
+ role: 'user' | 'assistant'
2254
+ content: string
2255
+ }
2256
+
2257
+ export default function Page() {
2258
+ const [messages, setMessages] = useState<Message[]>([])
2259
+ const [input, setInput] = useState('')
2260
+ const [loading, setLoading] = useState(false)
2261
+ const scrollRef = useRef<HTMLDivElement>(null)
2262
+
2263
+ useEffect(() => {
2264
+ scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight)
2265
+ }, [messages])
2266
+
2267
+ async function send(e: React.FormEvent) {
2268
+ e.preventDefault()
2269
+ if (!input.trim() || loading) return
2270
+
2271
+ const userMsg: Message = { role: 'user', content: input }
2272
+ setMessages(prev => [...prev, userMsg])
2273
+ setInput('')
2274
+ setLoading(true)
2275
+
2276
+ try {
2277
+ const res = await fetch('/api/ai/chat', {
2278
+ method: 'POST',
2279
+ headers: { 'Content-Type': 'application/json' },
2280
+ body: JSON.stringify({ messages: [...messages, userMsg] }),
2281
+ })
2282
+ const json = await res.json() as { message: string }
2283
+ setMessages(prev => [...prev, { role: 'assistant', content: json.message }])
2284
+ } catch {
2285
+ setMessages(prev => [...prev, { role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' }])
2286
+ } finally {
2287
+ setLoading(false)
2288
+ }
2289
+ }
2290
+
2291
+ return (
2292
+ <div className="flex min-h-svh flex-col items-center p-4">
2293
+ <div className="flex w-full max-w-2xl flex-1 flex-col">
2294
+ <div className="mb-4 flex items-center justify-between">
2295
+ <h1 className="text-2xl font-bold">AI Chat</h1>
2296
+ <a href="/" className="text-sm text-muted-foreground underline hover:text-foreground">← Home</a>
2297
+ </div>
2298
+
2299
+ <div ref={scrollRef} className="flex-1 space-y-3 overflow-y-auto rounded-lg border p-4" style={{ maxHeight: 'calc(100svh - 180px)' }}>
2300
+ {messages.length === 0 && (
2301
+ <p className="py-12 text-center text-sm text-muted-foreground">Send a message to start chatting.</p>
2302
+ )}
2303
+ {messages.map((msg, i) => (
2304
+ <div key={i} className={\`flex \${msg.role === 'user' ? 'justify-end' : 'justify-start'}\`}>
2305
+ <div className={\`max-w-[80%] rounded-lg px-3 py-2 text-sm \${
2306
+ msg.role === 'user'
2307
+ ? 'bg-primary text-primary-foreground'
2308
+ : 'bg-muted text-foreground'
2309
+ }\`}>
2310
+ {msg.content}
2311
+ </div>
2312
+ </div>
2313
+ ))}
2314
+ {loading && (
2315
+ <div className="flex justify-start">
2316
+ <div className="rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">Thinking...</div>
2317
+ </div>
2318
+ )}
2319
+ </div>
2320
+
2321
+ <form onSubmit={send} className="mt-3 flex gap-2">
2322
+ <input
2323
+ value={input}
2324
+ onChange={e => setInput(e.target.value)}
2325
+ placeholder="Type a message..."
2326
+ disabled={loading}
2327
+ className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
2328
+ />
2329
+ <button
2330
+ type="submit"
2331
+ disabled={loading}
2332
+ className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
2333
+ >
2334
+ Send
2335
+ </button>
2336
+ </form>
2337
+ </div>
2338
+ </div>
2339
+ )
2340
+ }
2341
+ `;
2342
+ }
2343
+ function aiChatPageVue(ctx) {
2344
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
2345
+ return `<script setup lang="ts">
2346
+ ${cssImport}import { ref, nextTick } from 'vue'
2347
+
2348
+ interface Message {
2349
+ role: 'user' | 'assistant'
2350
+ content: string
2351
+ }
2352
+
2353
+ const messages = ref<Message[]>([])
2354
+ const input = ref('')
2355
+ const loading = ref(false)
2356
+ const scrollEl = ref<HTMLDivElement>()
2357
+
2358
+ async function send(e: Event) {
2359
+ e.preventDefault()
2360
+ if (!input.value.trim() || loading.value) return
2361
+
2362
+ const userMsg: Message = { role: 'user', content: input.value }
2363
+ messages.value.push(userMsg)
2364
+ input.value = ''
2365
+ loading.value = true
2366
+
2367
+ try {
2368
+ const res = await fetch('/api/ai/chat', {
2369
+ method: 'POST',
2370
+ headers: { 'Content-Type': 'application/json' },
2371
+ body: JSON.stringify({ messages: messages.value }),
2372
+ })
2373
+ const json = await res.json() as { message: string }
2374
+ messages.value.push({ role: 'assistant', content: json.message })
2375
+ } catch {
2376
+ messages.value.push({ role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' })
2377
+ } finally {
2378
+ loading.value = false
2379
+ await nextTick()
2380
+ scrollEl.value?.scrollTo(0, scrollEl.value.scrollHeight)
2381
+ }
2382
+ }
2383
+ </script>
2384
+
2385
+ <template>
2386
+ <div class="flex min-h-svh flex-col items-center p-4">
2387
+ <div class="flex w-full max-w-2xl flex-1 flex-col">
2388
+ <div class="mb-4 flex items-center justify-between">
2389
+ <h1 class="text-2xl font-bold">AI Chat</h1>
2390
+ <a href="/" class="text-sm text-muted-foreground underline hover:text-foreground">← Home</a>
2391
+ </div>
2392
+
2393
+ <div ref="scrollEl" class="flex-1 space-y-3 overflow-y-auto rounded-lg border p-4" :style="{ maxHeight: 'calc(100svh - 180px)' }">
2394
+ <p v-if="messages.length === 0" class="py-12 text-center text-sm text-muted-foreground">Send a message to start chatting.</p>
2395
+ <div v-for="(msg, i) in messages" :key="i" :class="['flex', msg.role === 'user' ? 'justify-end' : 'justify-start']">
2396
+ <div :class="['max-w-[80%] rounded-lg px-3 py-2 text-sm', msg.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground']">
2397
+ {{ msg.content }}
2398
+ </div>
2399
+ </div>
2400
+ <div v-if="loading" class="flex justify-start">
2401
+ <div class="rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">Thinking...</div>
2402
+ </div>
2403
+ </div>
2404
+
2405
+ <form @submit="send" class="mt-3 flex gap-2">
2406
+ <input
2407
+ v-model="input"
2408
+ placeholder="Type a message..."
2409
+ :disabled="loading"
2410
+ class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
2411
+ />
2412
+ <button
2413
+ type="submit"
2414
+ :disabled="loading"
2415
+ class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
2416
+ >
2417
+ Send
2418
+ </button>
2419
+ </form>
2420
+ </div>
2421
+ </div>
2422
+ </template>
2423
+ `;
2424
+ }
2425
+ function aiChatPageSolid(ctx) {
2426
+ const cssImport = ctx.tailwind ? `import '@/index.css'\n` : '';
2427
+ return `${cssImport}import { createSignal, For, Show, onCleanup } from 'solid-js'
2428
+
2429
+ interface Message {
2430
+ role: 'user' | 'assistant'
2431
+ content: string
2432
+ }
2433
+
2434
+ export default function Page() {
2435
+ const [messages, setMessages] = createSignal<Message[]>([])
2436
+ const [input, setInput] = createSignal('')
2437
+ const [loading, setLoading] = createSignal(false)
2438
+ let scrollEl: HTMLDivElement | undefined
2439
+
2440
+ function scrollToBottom() {
2441
+ setTimeout(() => scrollEl?.scrollTo(0, scrollEl.scrollHeight), 0)
2442
+ }
2443
+
2444
+ async function send(e: Event) {
2445
+ e.preventDefault()
2446
+ if (!input().trim() || loading()) return
2447
+
2448
+ const userMsg: Message = { role: 'user', content: input() }
2449
+ setMessages(prev => [...prev, userMsg])
2450
+ setInput('')
2451
+ setLoading(true)
2452
+ scrollToBottom()
2453
+
2454
+ try {
2455
+ const res = await fetch('/api/ai/chat', {
2456
+ method: 'POST',
2457
+ headers: { 'Content-Type': 'application/json' },
2458
+ body: JSON.stringify({ messages: [...messages()] }),
2459
+ })
2460
+ const json = await res.json() as { message: string }
2461
+ setMessages(prev => [...prev, { role: 'assistant', content: json.message }])
2462
+ } catch {
2463
+ setMessages(prev => [...prev, { role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' }])
2464
+ } finally {
2465
+ setLoading(false)
2466
+ scrollToBottom()
2467
+ }
2468
+ }
2469
+
2470
+ return (
2471
+ <div class="flex min-h-svh flex-col items-center p-4">
2472
+ <div class="flex w-full max-w-2xl flex-1 flex-col">
2473
+ <div class="mb-4 flex items-center justify-between">
2474
+ <h1 class="text-2xl font-bold">AI Chat</h1>
2475
+ <a href="/" class="text-sm text-muted-foreground underline hover:text-foreground">← Home</a>
2476
+ </div>
2477
+
2478
+ <div ref={scrollEl} class="flex-1 space-y-3 overflow-y-auto rounded-lg border p-4" style={{ "max-height": 'calc(100svh - 180px)' }}>
2479
+ <Show when={messages().length === 0}>
2480
+ <p class="py-12 text-center text-sm text-muted-foreground">Send a message to start chatting.</p>
2481
+ </Show>
2482
+ <For each={messages()}>
2483
+ {(msg) => (
2484
+ <div class={\`flex \${msg.role === 'user' ? 'justify-end' : 'justify-start'}\`}>
2485
+ <div class={\`max-w-[80%] rounded-lg px-3 py-2 text-sm \${
2486
+ msg.role === 'user'
2487
+ ? 'bg-primary text-primary-foreground'
2488
+ : 'bg-muted text-foreground'
2489
+ }\`}>
2490
+ {msg.content}
2491
+ </div>
2492
+ </div>
2493
+ )}
2494
+ </For>
2495
+ <Show when={loading()}>
2496
+ <div class="flex justify-start">
2497
+ <div class="rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">Thinking...</div>
2498
+ </div>
2499
+ </Show>
2500
+ </div>
2501
+
2502
+ <form onSubmit={send} class="mt-3 flex gap-2">
2503
+ <input
2504
+ value={input()}
2505
+ onInput={e => setInput(e.currentTarget.value)}
2506
+ placeholder="Type a message..."
2507
+ disabled={loading()}
2508
+ class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
2509
+ />
2510
+ <button
2511
+ type="submit"
2512
+ disabled={loading()}
2513
+ class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
2514
+ >
2515
+ Send
2516
+ </button>
2517
+ </form>
2518
+ </div>
2519
+ </div>
2520
+ )
2521
+ }
2522
+ `;
2523
+ }
2524
+ // ─── Demo pages (secondary frameworks) ─────────────────────
2525
+ function demoPageConfig(fw) {
2526
+ switch (fw) {
2527
+ case 'vue':
2528
+ return `import type { Config } from 'vike/types'
2529
+ import vikeVue from 'vike-vue/config'
2530
+
2531
+ export default {
2532
+ extends: vikeVue,
2533
+ } satisfies Config
2534
+ `;
2535
+ case 'solid':
2536
+ return `import type { Config } from 'vike/types'
2537
+ import vikeSolid from 'vike-solid/config'
2538
+
2539
+ export default {
2540
+ extends: vikeSolid,
2541
+ } satisfies Config
2542
+ `;
2543
+ default: // react
2544
+ return `import type { Config } from 'vike/types'
2545
+ import vikeReact from 'vike-react/config'
2546
+
2547
+ export default {
2548
+ extends: vikeReact,
2549
+ } satisfies Config
2550
+ `;
2551
+ }
2552
+ }
2553
+ function demoPage(fw, ctx) {
2554
+ const { primary, tailwind } = ctx;
2555
+ switch (fw) {
2556
+ case 'react':
2557
+ if (tailwind) {
2558
+ return `export default function Page() {
2559
+ return (
2560
+ <div className="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
2561
+ <h1 className="text-2xl font-bold">Hello from React</h1>
2562
+ <p className="text-muted-foreground">React demo page — running alongside ${primary}.</p>
2563
+ <a href="/" className="text-sm underline">← Back to home</a>
2564
+ </div>
2565
+ )
2566
+ }
2567
+ `;
2568
+ }
2569
+ return `export default function Page() {
2570
+ return (
2571
+ <div>
2572
+ <h1>Hello from React</h1>
2573
+ <p>React demo page — running alongside ${primary}.</p>
2574
+ <a href="/">← Back to home</a>
2575
+ </div>
2576
+ )
2577
+ }
2578
+ `;
2579
+ case 'vue':
2580
+ if (tailwind) {
2581
+ return `<script setup lang="ts">
2582
+ import '@/index.css'
2583
+ </script>
2584
+
2585
+ <template>
2586
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
2587
+ <h1 class="text-2xl font-bold">Hello from Vue</h1>
2588
+ <p class="text-muted-foreground">Vue demo page — running alongside ${primary}.</p>
2589
+ <a href="/" class="text-sm underline">← Back to home</a>
2590
+ </div>
2591
+ </template>
2592
+ `;
2593
+ }
2594
+ return `<template>
2595
+ <div>
2596
+ <h1>Hello from Vue</h1>
2597
+ <p>Vue demo page — running alongside ${primary}.</p>
2598
+ <a href="/">← Back to home</a>
2599
+ </div>
2600
+ </template>
2601
+ `;
2602
+ case 'solid':
2603
+ if (tailwind) {
2604
+ return `import '@/index.css'
2605
+
2606
+ export default function Page() {
2607
+ return (
2608
+ <div class="flex min-h-svh flex-col items-center justify-center gap-4 p-4">
2609
+ <h1 class="text-2xl font-bold">Hello from Solid</h1>
2610
+ <p class="text-muted-foreground">Solid demo page — running alongside ${primary}.</p>
2611
+ <a href="/" class="text-sm underline">← Back to home</a>
2612
+ </div>
2613
+ )
2614
+ }
2615
+ `;
2616
+ }
2617
+ return `export default function Page() {
2618
+ return (
2619
+ <div>
2620
+ <h1>Hello from Solid</h1>
2621
+ <p>Solid demo page — running alongside ${primary}.</p>
2622
+ <a href="/">← Back to home</a>
2623
+ </div>
2624
+ )
2625
+ }
2626
+ `;
2627
+ }
2628
+ }
2629
+ //# sourceMappingURL=templates.js.map