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.
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +250 -0
- package/dist/index.js.map +1 -0
- package/dist/templates.d.ts +39 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +2629 -0
- package/dist/templates.js.map +1 -0
- package/package.json +40 -0
|
@@ -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
|