@cioky/vike-create 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +60 -0
  2. package/package.json +32 -0
  3. package/src/index.js +809 -0
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @cioky/vike-create
2
+
3
+ Scaffold a [Vike](https://vike.dev) + [Ripple TS](https://ripple-ts.com) project.
4
+
5
+ Part of the [vike-ripple monorepo](https://github.com/Opaius/vike-ripple).
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # With Tailwind CSS (default)
11
+ npx @cioky/vike-create my-app
12
+
13
+ # With Panda CSS
14
+ npx @cioky/vike-create my-app --style pandacss
15
+
16
+ # Without a CSS framework
17
+ npx @cioky/vike-create my-app --style none
18
+
19
+ # With Cloudflare Workers support
20
+ npx @cioky/vike-create my-app --style tailwind --cloudflare
21
+
22
+ # With Remult (SSE live query everywhere)
23
+ npx @cioky/vike-create my-app --style tailwind --remult
24
+
25
+ # With Remult + Cloudflare (DO-based realtime)
26
+ npx @cioky/vike-create my-app --style tailwind --remult --cloudflare
27
+ ```
28
+
29
+ ## Flags
30
+
31
+ | Flag | Description |
32
+ |------|-------------|
33
+ | `--style tailwind` | Tailwind CSS v4 (default) |
34
+ | `--style pandacss` | Panda CSS |
35
+ | `--style none` | No CSS framework |
36
+ | `--cloudflare` | Cloudflare Workers setup (wrangler, D1, DO config) |
37
+ | `--remult` | Remult ORM with realtime subscriptions |
38
+ | `--remult --cloudflare` | Remult with Durable Object-based realtime via `remult-partykit` |
39
+
40
+ ## What's included
41
+
42
+ The scaffold creates:
43
+
44
+ - Vike pages (`pages/index`, `pages/about`) with SSR
45
+ - Ripple TS `.tsrx` file support
46
+ - `@cioky/vike-core` config (Layout, Head, hooks)
47
+ - CSS framework config (tailwind.css or panda.config.ts)
48
+ - Cloudflare Workers config when `--cloudflare` is set
49
+ - Remult + realtime subscription setup when `--remult` is set
50
+
51
+ ## Generated Packages
52
+
53
+ | Package | Installed when |
54
+ |---------|---------------|
55
+ | `@cioky/vike-core` | Always |
56
+ | `@cioky/vike-tailwindcss` | `--style tailwind` |
57
+ | `@cioky/vike-pandacss` | `--style pandacss` |
58
+ | `remult-partykit` | `--remult --cloudflare` |
59
+
60
+ See the [main repo](https://github.com/Opaius/vike-ripple) for per-package documentation.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@cioky/vike-create",
3
+ "version": "0.5.2",
4
+ "description": "Scaffold a Vike + Ripple TS project",
5
+ "type": "module",
6
+ "bin": {
7
+ "@cioky/vike-create": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/Opaius/vike-ripple.git"
15
+ },
16
+ "homepage": "https://github.com/Opaius/vike-ripple",
17
+ "bugs": {
18
+ "url": "https://github.com/Opaius/vike-ripple/issues"
19
+ },
20
+ "keywords": [
21
+ "vike",
22
+ "ripple",
23
+ "ripplets",
24
+ "vite",
25
+ "scaffold",
26
+ "tailwindcss",
27
+ "pandacss",
28
+ "cloudflare",
29
+ "remult"
30
+ ],
31
+ "license": "MIT"
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,809 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { join, resolve } from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ // --- arg parse ---
7
+ const args = process.argv.slice(2);
8
+ if (args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ @cioky/vike-create — Scaffold a Vike + Ripple TS project
11
+
12
+ Usage:
13
+ @cioky/vike-create [name] [options]
14
+
15
+ Options:
16
+ --style <name> CSS framework: tailwind (default), pandacss, none
17
+ --cloudflare Add Cloudflare Workers configuration
18
+ --remult Add Remult ORM (DO-based realtime with CF, SSE without)
19
+ --betterauth Add Better Auth (requires --remult)
20
+ --help, -h Show this help message
21
+
22
+ Examples:
23
+ @cioky/vike-create my-app
24
+ @cioky/vike-create my-app --style pandacss
25
+ @cioky/vike-create my-app --cloudflare
26
+ @cioky/vike-create my-app --remult
27
+ @cioky/vike-create my-app --remult --cloudflare
28
+ `);
29
+ process.exit(0);
30
+ }
31
+
32
+ let name = null;
33
+ let style = 'tailwind';
34
+ let cloudflare = false;
35
+ let remult = false;
36
+ let betterauth = false;
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ if (args[i] === '--style' && args[i + 1]) { style = args[++i]; continue; }
40
+ if (args[i] === '--cloudflare') { cloudflare = true; continue; }
41
+ if (args[i] === '--remult') { remult = true; continue; }
42
+ if (args[i] === '--betterauth') { betterauth = true; continue; }
43
+ if (!args[i].startsWith('--') && !name) name = args[i];
44
+ }
45
+ if (!name && args.length && !args[0].startsWith('--')) name = args[0];
46
+ if (!name) name = 'my-vike-app';
47
+ if (!['tailwind', 'pandacss', 'none'].includes(style)) {
48
+ console.error(`Unknown style "${style}". Use tailwind, pandacss, or none.`);
49
+ process.exit(1);
50
+ }
51
+ if (betterauth && !remult) {
52
+ console.error('--betterauth requires --remult.');
53
+ process.exit(1);
54
+ }
55
+
56
+ const root = resolve(process.cwd(), name);
57
+ mkdirSync(join(root, 'renderer'), { recursive: true });
58
+ mkdirSync(join(root, 'src'), { recursive: true });
59
+ mkdirSync(join(root, 'pages', 'index'), { recursive: true });
60
+
61
+ // --- package.json ---
62
+ const deps = {
63
+ vike: '0.4.259',
64
+ '@cioky/vike-core': '0.5.6',
65
+ '@ripple-ts/vite-plugin': '0.3.85',
66
+ ripple: '0.3.85',
67
+ };
68
+ const devDeps = { vite: '8.1.0', typescript: '5.9.3', '@tsrx/typescript-plugin': '0.3.85' };
69
+ if (style === 'tailwind') {
70
+ deps['@cioky/vike-tailwindcss'] = 'latest';
71
+ deps['@tailwindcss/vite'] = 'latest';
72
+ }
73
+ if (style === 'pandacss') {
74
+ deps['@cioky/vike-pandacss'] = '0.1.0';
75
+ deps['@pandacss/dev'] = '1.11.3';
76
+ }
77
+ if (cloudflare) {
78
+ devDeps['@cloudflare/vite-plugin'] = '1.42.2';
79
+ devDeps['@cloudflare/workers-types'] = '4.20260624.1';
80
+ devDeps.wrangler = '4.104.0';
81
+ }
82
+ if (remult) {
83
+ deps.remult = '3.3.13';
84
+ if (cloudflare) {
85
+ deps['remult-partykit'] = '1.1.0';
86
+ deps.partyserver = '0.5.8';
87
+ deps.hono = '4.12.27';
88
+ deps['@vikejs/hono'] = '0.2.1';
89
+ }
90
+ }
91
+ if (betterauth) {
92
+ deps['better-auth'] = '1.6.20';
93
+ deps['@nerdfolio/remult-better-auth'] = '0.4.3';
94
+ }
95
+ const scripts = { dev: 'vite', build: 'vite build', preview: 'vite preview', check: 'tsrx-tsc --noEmit', postinstall: 'rm -f node_modules/~ && ln -sf .. node_modules/~' };
96
+ if (style === 'pandacss') {
97
+ scripts.codegen = 'panda codegen';
98
+ scripts.prepare = 'panda codegen';
99
+ }
100
+ if (cloudflare) {
101
+ scripts.types = 'wrangler types --env-interface Env worker-configuration.d.ts';
102
+ }
103
+ writeFileSync(join(root, 'package.json'), JSON.stringify({
104
+ name, private: true, type: 'module',
105
+ scripts, dependencies: deps, devDependencies: devDeps
106
+ }, null, 2) + '\n');
107
+
108
+ // --- vite.config.ts ---
109
+ const viteImports = [
110
+ `import { defineConfig } from 'vite'`,
111
+ `import { fileURLToPath } from 'node:url'`,
112
+ `import { dirname } from 'node:path'`,
113
+ `import vike from 'vike/plugin'`,
114
+ `import { ripple } from '@ripple-ts/vite-plugin'`,
115
+ `import vikeRipple from '@cioky/vike-core'`,
116
+ ];
117
+ const vitePlugins = [
118
+ ` vike(),`,
119
+ ` vikeRipple(),`,
120
+ ` ripple({ excludeRippleExternalModules: true }),`,
121
+ ];
122
+ if (cloudflare) {
123
+ viteImports.unshift(`import { cloudflare } from '@cloudflare/vite-plugin'`);
124
+ vitePlugins.unshift(` cloudflare({ viteEnvironment: { name: 'ssr' } }),`);
125
+ }
126
+ if (style === 'tailwind') {
127
+ viteImports.push(
128
+ `import vikeRippleTailwindcss from '@cioky/vike-tailwindcss'`,
129
+ `import tailwindcss from '@tailwindcss/vite'`
130
+ );
131
+ vitePlugins.push(` vikeRippleTailwindcss(),`, ` tailwindcss(),`);
132
+ }
133
+ if (style === 'pandacss') {
134
+ viteImports.push(`import vikeRipplePandacss from '@cioky/vike-pandacss'`);
135
+ vitePlugins.push(` vikeRipplePandacss(),`);
136
+ }
137
+ const vitExtras = [];
138
+ if (cloudflare) vitExtras.push(` environments: { ssr: { consumer: 'server' } },`);
139
+ if (style === 'pandacss') vitExtras.push(` css: { postcss: './postcss.config.js' },`);
140
+ writeFileSync(join(root, 'vite.config.ts'), [
141
+ ...viteImports, ``,
142
+ `const __dirname = dirname(fileURLToPath(import.meta.url))`,
143
+ `export default defineConfig({`,
144
+ ` resolve: { alias: { '~': __dirname } },`,
145
+ ...vitExtras,
146
+ ` optimizeDeps: { exclude: ['ripple'] },`,
147
+ ` plugins: [`, ...vitePlugins, ` ],`, `})`, ``
148
+ ].join('\n'));
149
+
150
+ // --- tsconfig.json ---
151
+ const tsPaths = { '~/*': ['./*'] };
152
+ if (style === 'pandacss') tsPaths['~styled-system/*'] = ['./styled-system/*'];
153
+ writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({
154
+ compilerOptions: {
155
+ strict: true, module: 'ESNext', moduleResolution: 'bundler',
156
+ target: 'ESNext', jsx: 'preserve', jsxImportSource: 'ripple',
157
+ esModuleInterop: true, isolatedModules: true,
158
+ experimentalDecorators: true,
159
+ verbatimModuleSyntax: true, skipLibCheck: true,
160
+ ...(cloudflare ? { types: ['@cloudflare/workers-types'] } : { types: ['vike/client'] }),
161
+ paths: tsPaths
162
+ },
163
+ include: ['**/*.ts', '**/*.tsx', '**/*.tsrx']
164
+ }, null, 2) + '\n');
165
+
166
+ // --- renderer/+config.ts ---
167
+ writeFileSync(join(root, 'renderer', '+config.ts'), [
168
+ `export default {`,
169
+ ` extends: ['import:@cioky/vike-core/config:default'],
170
+ ` server: true,`,
171
+ `}`, ``
172
+ ].join('\n'));
173
+
174
+ // --- pages/+Layout.tsrx ---
175
+ if (style === 'pandacss') {
176
+ writeFileSync(join(root, 'pages', '+Layout.tsrx'), [
177
+ `import { type TSRXElement } from 'ripple'`,
178
+ `import { css } from '~/styled-system/css'`,
179
+ `import '~/src/styles.css'`,
180
+ ``,
181
+ `export function Layout({ children }: { children: TSRXElement }) @{`,
182
+ ` <div class={css({ minH: 'screen', bg: 'white', color: 'gray.900' })}>`,
183
+ ` <nav class={css({ display: 'flex', gap: '4', borderBottom: '1px', px: '4', py: '3', fontSize: 'sm' })}>`,
184
+ ` <a href="/" data-vike-link class={css({ fontWeight: 600, color: 'gray.700', _hover: { color: 'black' } })}>Home</a>`,
185
+ ` <a href="/about" data-vike-link class={css({ color: 'gray.500', _hover: { color: 'black' } })}>About</a>`,
186
+ ` </nav>`,
187
+ ` {children}`,
188
+ ` </div>`,
189
+ `}`,
190
+ ``
191
+ ].join('\n'));
192
+ } else if (style === 'none') {
193
+ writeFileSync(join(root, 'pages', '+Layout.tsrx'), [
194
+ `import { type TSRXElement } from 'ripple'`, ``,
195
+ `export function Layout({ children }: { children: TSRXElement }) @{`,
196
+ ` <div>`, ` {children}`, ` </div>`,
197
+ `}`, ``
198
+ ].join('\n'));
199
+ } else {
200
+ writeFileSync(join(root, 'pages', '+Layout.tsrx'), [
201
+ `import { type TSRXElement } from 'ripple'`, ``,
202
+ `export function Layout({ children }: { children: TSRXElement }) @{`,
203
+ ` <div class="min-h-screen bg-white text-gray-900">`,
204
+ ` <nav class="flex gap-4 border-b px-4 py-3 text-sm">`,
205
+ ` <a href="/" data-vike-link class="font-semibold text-gray-700 hover:text-black">Home</a>`,
206
+ ` <a href="/about" data-vike-link class="text-gray-500 hover:text-black">About</a>`,
207
+ ` </nav>`, ` {children}`, ` </div>`,
208
+ `}`, ``
209
+ ].join('\n'));
210
+ }
211
+
212
+ // --- pages/index/+Page.tsrx ---
213
+ if (style === 'pandacss') {
214
+ writeFileSync(join(root, 'pages', 'index', '+Page.tsrx'), [
215
+ `import { css } from '~/styled-system/css'`, ``,
216
+ `export function Page() @{`,
217
+ ` <>`,
218
+ ` <head><title>Home</title></head>`,
219
+ ` <section class={css({ minH: 'screen', display: 'flex', flexDir: 'column', alignItems: 'center', justifyContent: 'center', gap: '4', p: '8' })}>`,
220
+ ` <h1 class={css({ fontSize: '4xl', fontWeight: 'bold' })}>Hello, Vike + Ripple!</h1>`,
221
+ ` <p class={css({ fontSize: 'lg', color: 'blue.600' })}>With Panda CSS</p>`,
222
+ ` </section>`,
223
+ ` </>`,
224
+ `}`, ``
225
+ ].join('\n'));
226
+ } else if (style === 'tailwind') {
227
+ writeFileSync(join(root, 'pages', 'index', '+Page.tsrx'), [
228
+ `import '../../tailwind.css'`, ``,
229
+ `export function Page() @{`,
230
+ ` <>`,
231
+ ` <head><title>Home</title></head>`,
232
+ ` <section class="min-h-screen flex flex-col items-center justify-center gap-4 p-8">`,
233
+ ` <h1 class="text-4xl font-bold">Hello, Vike + Ripple!</h1>`,
234
+ ` <p class="text-lg text-blue-600">With Tailwind CSS v4</p>`,
235
+ ` </section>`,
236
+ ` </>`,
237
+ `}`, ``
238
+ ].join('\n'));
239
+ } else {
240
+ writeFileSync(join(root, 'pages', 'index', '+Page.tsrx'), [
241
+ `export function Page() @{`,
242
+ ` <>`,
243
+ ` <head><title>Home</title></head>`,
244
+ ` <section>`, ` <h1>Hello, Vike + Ripple!</h1>`, ` </section>`,
245
+ ` </>`,
246
+ `}`, ``
247
+ ].join('\n'));
248
+ }
249
+
250
+ // --- pages/about/+Page.tsrx ---
251
+ mkdirSync(join(root, 'pages', 'about'), { recursive: true });
252
+ if (style === 'pandacss') {
253
+ writeFileSync(join(root, 'pages', 'about', '+Page.tsrx'), [
254
+ `import { css } from '~/styled-system/css'`, ``,
255
+ `export function Page() @{`,
256
+ ` <>`,
257
+ ` <head><title>About</title></head>`,
258
+ ` <section class={css({ mx: 'auto', maxW: '2xl', p: '8' })}>`,
259
+ ` <h1 class={css({ fontSize: '3xl', fontWeight: 'bold', mb: '4' })}>About</h1>`,
260
+ ` <p class={css({ color: 'gray.600' })}>This scaffold was created by @cioky/vike-create.</p>`,
261
+ ` <p class={css({ color: 'gray.600' })}>Scaffolded with Panda CSS + Ripple TS plugin.</p>`,
262
+ ` </section>`,
263
+ ` </>`,
264
+ `}`, ``
265
+ ].join('\n'));
266
+ } else if (style !== 'none') {
267
+ writeFileSync(join(root, 'pages', 'about', '+Page.tsrx'), [
268
+ `export function Page() @{`,
269
+ ` <>`,
270
+ ` <head><title>About</title></head>`,
271
+ ` <section class="mx-auto max-w-2xl p-8">`,
272
+ ` <h1 class="text-3xl font-bold mb-4">About</h1>`,
273
+ ` <p class="text-gray-600">This scaffold was created by @cioky/vike-create.</p>`,
274
+ ` </section>`,
275
+ ` </>`,
276
+ `}`, ``
277
+ ].join('\n'));
278
+ } else {
279
+ writeFileSync(join(root, 'pages', 'about', '+Page.tsrx'), [
280
+ `export function Page() @{`,
281
+ ` <>`,
282
+ ` <head><title>About</title></head>`,
283
+ ` <section>`, ` <h1>About</h1>`,
284
+ ` <p>This scaffold was created by @cioky/vike-create.</p>`,
285
+ ` </>`,
286
+ `}`, ``
287
+ ].join('\n'));
288
+ }
289
+
290
+ // --- Better Auth pages ---
291
+ if (betterauth) {
292
+ mkdirSync(join(root, 'pages', 'login'), { recursive: true });
293
+ mkdirSync(join(root, 'pages', 'register'), { recursive: true });
294
+ mkdirSync(join(root, 'pages', 'dashboard'), { recursive: true });
295
+ writeFileSync(join(root, 'pages', 'login', '+Page.tsrx'), [
296
+ `import { css } from '~/styled-system/css'`,
297
+ `import { track } from 'ripple'`,
298
+ `import { createAuthClient } from 'better-auth/client'`,
299
+ ``,
300
+ `const authClient = createAuthClient({ baseURL: typeof window !== 'undefined' ? window.location.origin : '' })`,
301
+ ``,
302
+ `export function Page() @{`,
303
+ ` let &[email] = track('')`,
304
+ ` let &[password] = track('')`,
305
+ ` let &[errorMsg] = track('')`,
306
+ ` let &[loading] = track(false)`,
307
+ ``,
308
+ ` function handleSubmit(e: Event) {`,
309
+ ` e.preventDefault()`,
310
+ ` loading = true`,
311
+ ` errorMsg = ''`,
312
+ ` authClient.signIn.email({ email, password }).then(function(res: unknown) {`,
313
+ ` const r = res as { error?: { message?: string; status?: string } }`,
314
+ ` if (r.error) {`,
315
+ ` errorMsg = r.error.message || r.error.status || 'Sign in failed'`,
316
+ ` } else {`,
317
+ ` window.location.href = '/dashboard'`,
318
+ ` }`,
319
+ ` loading = false`,
320
+ ` }).catch(function(err: Error) {`,
321
+ ` errorMsg = err.message`,
322
+ ` loading = false`,
323
+ ` })`,
324
+ ` }`,
325
+ ``,
326
+ ` <>`,
327
+ ` <head><title>Sign In</title></head>`,
328
+ ` <section class={css({ maxW: 'md', mx: 'auto', mt: '20', p: '8' })}>`,
329
+ ` <form onSubmit={handleSubmit} class={css({ display: 'flex', flexDir: 'column', gap: '4' })}>`,
330
+ ` <input type="email" placeholder="Email" value={email} onInput={(e) => { if (e.target) email = (e.target as HTMLInputElement).value }} required class={css({ p: '3', border: '1px', borderRadius: 'md', borderColor: 'gray.300', w: 'full' })} />`,
331
+ ` <input type="password" placeholder="Password" value={password} onInput={(e) => { if (e.target) password = (e.target as HTMLInputElement).value }} required class={css({ p: '3', border: '1px', borderRadius: 'md', borderColor: 'gray.300', w: 'full' })} />`,
332
+ ` @if (errorMsg) {`,
333
+ ` <p class={css({ color: 'red.500', fontSize: 'sm' })}>{errorMsg}</p>`,
334
+ ` }`,
335
+ ` <button type="submit" disabled={loading} class={css({ p: '3', bg: 'blue.600', color: 'white', borderRadius: 'md', fontWeight: 'bold', cursor: 'pointer', _hover: { bg: 'blue.700' }, _disabled: { opacity: 0.5 } })}>`,
336
+ ` {loading ? 'Signing in...' : 'Sign In'}`,
337
+ ` </button>`,
338
+ ` <p class={css({ textAlign: 'center', fontSize: 'sm', color: 'gray.600' })}>`,
339
+ ` Don't have an account? <a href="/register" class={css({ color: 'blue.600', textDecoration: 'underline' })}>Register</a>`,
340
+ ` </p>`,
341
+ ` </form>`,
342
+ ` </section>`,
343
+ ` </>`,
344
+ `}`,
345
+ ``
346
+ ].join('\n'));
347
+ writeFileSync(join(root, 'pages', 'register', '+Page.tsrx'), [
348
+ `import { css } from '~/styled-system/css'`,
349
+ `import { track } from 'ripple'`,
350
+ `import { createAuthClient } from 'better-auth/client'`,
351
+ ``,
352
+ `const authClient = createAuthClient({ baseURL: typeof window !== 'undefined' ? window.location.origin : '' })`,
353
+ ``,
354
+ `export function Page() @{`,
355
+ ` let &[name] = track('')`,
356
+ ` let &[email] = track('')`,
357
+ ` let &[password] = track('')`,
358
+ ` let &[errorMsg] = track('')`,
359
+ ` let &[loading] = track(false)`,
360
+ ``,
361
+ ` function handleSubmit(e: Event) {`,
362
+ ` e.preventDefault()`,
363
+ ` loading = true`,
364
+ ` errorMsg = ''`,
365
+ ` authClient.signUp.email({ name, email, password }).then(function(res: unknown) {`,
366
+ ` const r = res as { error?: { message?: string; status?: string } }`,
367
+ ` if (r.error) {`,
368
+ ` errorMsg = r.error.message || r.error.status || 'Registration failed'`,
369
+ ` } else {`,
370
+ ` window.location.href = '/dashboard'`,
371
+ ` }`,
372
+ ` loading = false`,
373
+ ` }).catch(function(err: Error) {`,
374
+ ` errorMsg = err.message`,
375
+ ` loading = false`,
376
+ ` })`,
377
+ ` }`,
378
+ ``,
379
+ ` <>`,
380
+ ` <head><title>Register</title></head>`,
381
+ ` <section class={css({ maxW: 'md', mx: 'auto', mt: '20', p: '8' })}>`,
382
+ ` <form onSubmit={handleSubmit} class={css({ display: 'flex', flexDir: 'column', gap: '4' })}>`,
383
+ ` <input type="text" placeholder="Name" value={name} onInput={(e) => { if (e.target) name = (e.target as HTMLInputElement).value }} required class={css({ p: '3', border: '1px', borderRadius: 'md', borderColor: 'gray.300', w: 'full' })} />`,
384
+ ` <input type="email" placeholder="Email" value={email} onInput={(e) => { if (e.target) email = (e.target as HTMLInputElement).value }} required class={css({ p: '3', border: '1px', borderRadius: 'md', borderColor: 'gray.300', w: 'full' })} />`,
385
+ ` <input type="password" placeholder="Password" value={password} onInput={(e) => { if (e.target) password = (e.target as HTMLInputElement).value }} required class={css({ p: '3', border: '1px', borderRadius: 'md', borderColor: 'gray.300', w: 'full' })} />`,
386
+ ` @if (errorMsg) {`,
387
+ ` <p class={css({ color: 'red.500', fontSize: 'sm' })}>{errorMsg}</p>`,
388
+ ` }`,
389
+ ` <button type="submit" disabled={loading} class={css({ p: '3', bg: 'blue.600', color: 'white', borderRadius: 'md', fontWeight: 'bold', cursor: 'pointer', _hover: { bg: 'blue.700' }, _disabled: { opacity: 0.5 } })}>`,
390
+ ` {loading ? 'Creating account...' : 'Register'}`,
391
+ ` </button>`,
392
+ ` <p class={css({ textAlign: 'center', fontSize: 'sm', color: 'gray.600' })}>`,
393
+ ` Already have an account? <a href="/login" class={css({ color: 'blue.600', textDecoration: 'underline' })}>Sign in</a>`,
394
+ ` </p>`,
395
+ ` </form>`,
396
+ ` </section>`,
397
+ ` </>`,
398
+ `}`,
399
+ ``
400
+ ].join('\n'));
401
+ writeFileSync(join(root, 'pages', 'dashboard', '+data.server.ts'), [
402
+ `import type { PageContextServer } from 'vike/types'`,
403
+ ``,
404
+ `export type Data = {`,
405
+ ` user: { name?: string; email?: string; id?: string } | null`,
406
+ `}`,
407
+ ``,
408
+ `export default async function data(pageContext: PageContextServer): Promise<Data> {`,
409
+ ` return { user: (pageContext as unknown as Record<string, unknown>).user as Data["user"] ?? null }`,
410
+ `}`,
411
+ ``
412
+ ].join('\n'));
413
+ writeFileSync(join(root, 'pages', 'dashboard', '+Page.tsrx'), [
414
+ `import { css } from '~/styled-system/css'`,
415
+ `import type { Data } from './+data.server'`,
416
+ ``,
417
+ `export function Page(data: Data) @{`,
418
+ ` const { user } = data`,
419
+ ``,
420
+ ` <>`,
421
+ ` <head><title>Dashboard</title></head>`,
422
+ ` <section class={css({ maxW: '2xl', mx: 'auto', mt: '20', p: '8' })}>`,
423
+ ` @if (!user) {`,
424
+ ` <div class={css({ textAlign: 'center', py: '10' })}>`,
425
+ ` <h1 class={css({ fontSize: '2xl', fontWeight: 'bold', mb: '4' })}>Not Authenticated</h1>`,
426
+ ` <p class={css({ color: 'gray.600', mb: '6' })}>Please sign in to view this page.</p>`,
427
+ ` <a href="/login" class={css({ display: 'inline-block', p: '3', bg: 'blue.600', color: 'white', borderRadius: 'md', textDecoration: 'none' })}>Sign In</a>`,
428
+ ` </div>`,
429
+ ` } @else {`,
430
+ ` <div>`,
431
+ ` <h1 class={css({ fontSize: '2xl', fontWeight: 'bold', mb: '6' })}>Dashboard</h1>`,
432
+ ` <div class={css({ bg: 'gray.50', p: '6', borderRadius: 'lg', border: '1px', borderColor: 'gray.200' })}>`,
433
+ ` <p class={css({ mb: '2' })}><strong>Welcome, {user.name || user.email}!</strong></p>`,
434
+ ` <p class={css({ color: 'gray.600', fontSize: 'sm' })}>Email: {user.email}</p>`,
435
+ ` <p class={css({ color: 'gray.600', fontSize: 'sm' })}>User ID: {user.id}</p>`,
436
+ ` </div>`,
437
+ ` <div class={css({ mt: '6', display: 'flex', gap: '4' })}>`,
438
+ ` <a href="/" class={css({ color: 'blue.600' })}>Home</a>`,
439
+ ` <a href="/about" class={css({ color: 'blue.600' })}>About</a>`,
440
+ ` </div>`,
441
+ ` </div>`,
442
+ ` }`,
443
+ ` </section>`,
444
+ ` </>`,
445
+ `}`,
446
+ ``
447
+ ].join('\n'));
448
+ }
449
+
450
+ // --- style-specific files ---
451
+ if (style === 'tailwind') {
452
+ writeFileSync(join(root, 'tailwind.css'), [`@import "tailwindcss";`, ``].join('\n'));
453
+ }
454
+ if (style === 'pandacss') {
455
+ writeFileSync(join(root, 'panda.config.ts'), [
456
+ `import { defineConfig } from '@pandacss/dev'`,
457
+ `import { pluginRipple } from '@cioky/vike-pandacss/panda-plugin'`,
458
+ `export default defineConfig({`,
459
+ ` preflight: true,`,
460
+ ` include: ['./pages/**/*.{tsrx,tsx}', './renderer/**/*.{ts,tsx}'],`,
461
+ ` exclude: [],`,
462
+ ` plugins: [pluginRipple()],`,
463
+ ` theme: { extend: {} },`,
464
+ ` outdir: 'styled-system',`,
465
+ `})`, ``
466
+ ].join('\n'));
467
+ writeFileSync(join(root, 'postcss.config.js'), [
468
+ `export default { plugins: { '@pandacss/dev/postcss': {} } }`, ``
469
+ ].join('\n'));
470
+ writeFileSync(join(root, 'src', 'styles.css'), [
471
+ `@layer reset, base, tokens, recipes, utilities;`, ``
472
+ ].join('\n'));
473
+ }
474
+
475
+ // --- CF basic ---
476
+ if (cloudflare && !(remult && cloudflare)) {
477
+ mkdirSync(join(root, '.wrangler'), { recursive: true });
478
+ writeFileSync(join(root, 'wrangler.jsonc'), JSON.stringify({
479
+ $schema: 'node_modules/wrangler/config-schema.json',
480
+ name, main: 'vike:server-entry',
481
+ compatibility_date: '2026-06-01',
482
+ compatibility_flags: ['nodejs_compat']
483
+ }, null, 2) + '\n');
484
+ writeFileSync(join(root, '.gitignore'), `node_modules/\ndist/\n.wrangler/\n*.log\n.env\n`);
485
+ }
486
+
487
+ // --- Remult + CF ---
488
+ if (remult && cloudflare) {
489
+ mkdirSync(join(root, 'server'), { recursive: true });
490
+ mkdirSync(join(root, 'lib'), { recursive: true });
491
+ mkdirSync(join(root, '.wrangler'), { recursive: true });
492
+ writeFileSync(join(root, 'wrangler.jsonc'), JSON.stringify({
493
+ $schema: 'node_modules/wrangler/config-schema.json', name, main: '+server.ts',
494
+ compatibility_date: '2026-06-01', compatibility_flags: ['nodejs_compat'],
495
+ d1_databases: [{ binding: 'DB', database_name: name, database_id: 'your-database-id-here' }],
496
+ durable_objects: {
497
+ bindings: [
498
+ { name: 'REMULT_ROOM', class_name: 'RemultPubSubRoom' },
499
+ { name: 'REMULT_LIVE_QUERY_STORAGE', class_name: 'RemultLiveQueryStorageRoom' }
500
+ ]
501
+ },
502
+ migrations: [{ tag: 'v1', new_sqlite_classes: ['RemultPubSubRoom', 'RemultLiveQueryStorageRoom'] }],
503
+ vars: {
504
+ BETTER_AUTH_URL: 'http://localhost:3000',
505
+ BETTER_AUTH_SECRET: 'dev-secret-change-in-production!!',
506
+ MAX_CONNECTIONS_PER_SHARD: '100',
507
+ REALTIME_LIVE_QUERY_ROOM_MODE: 'global'
508
+ }
509
+ }, null, 2) + '\n');
510
+ let honoBody = [
511
+ ``,
512
+ `const app = new Hono<{ Variables: { user: unknown; session: unknown } }>()`,
513
+ `let db: D1Database`,
514
+ ``,
515
+ `// Universal middleware: exposes auth instance + session to Vike pageContext`,
516
+ `const authMiddleware = async (`,
517
+ ` _request: Request,`,
518
+ ` context: Record<string, unknown>,`,
519
+ ` runtime: { hono?: { env: Cloudflare.Env } }`,
520
+ `) => {`,
521
+ ` const env = runtime.hono?.env`,
522
+ ` if (!env) return`,
523
+ ` const auth = await getAuth(env.DB, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL)`,
524
+ ` if (!auth) return`,
525
+ ` context.auth = auth`,
526
+ ` const session = await auth.api.getSession({ headers: _request.headers }).catch(() => null)`,
527
+ ` if (session) {`,
528
+ ` context.user = session.user`,
529
+ ` context.session = session.session`,
530
+ ` }`,
531
+ `}`,
532
+ ``,
533
+ `app.use('/api/*', async (c, next) => {`,
534
+ ` db = (c.env as Cloudflare.Env).DB`,
535
+ ` await next()`,
536
+ `})`,
537
+ ]
538
+ if (betterauth) {
539
+ honoBody.push(
540
+ `app.use('/api/auth/*', async (c) => {`,
541
+ ` const env = c.env as Cloudflare.Env`,
542
+ ` const auth = await getAuth(env.DB, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL)`,
543
+ ` if (!auth) return c.text('Auth not available', 500)`,
544
+ ` return auth.handler(c.req.raw)`,
545
+ `})`,
546
+ )
547
+ }
548
+ honoBody.push(
549
+ `app.route('/api', remultApi({`,
550
+ ` dataProvider: async () => new SqlDatabase(new D1DataProvider(new D1BindingClient(db))),`,
551
+ ` entities: [],`,
552
+ ` getUser: async () => undefined,`,
553
+ `}))`,
554
+ )
555
+ honoBody.push(
556
+ `app.use('/party/*', async (c) => {`,
557
+ ` const env = c.env as Cloudflare.Env`,
558
+ ` const ns = env.REMULT_ROOM`,
559
+ ` return ns.get(ns.idFromName('global')).fetch(c.req.raw)`,
560
+ `})`,
561
+ `vike(app, [authMiddleware])`,
562
+ `export { app }`, ``
563
+ )
564
+ writeFileSync(join(root, '+server.ts'), [
565
+ `import { RemultLiveQueryStorageRoom, RemultPartyRoom, resolveRoomIdFromChannel } from 'remult-partykit/durable-object'`,
566
+ `import { app } from './server/hono'`, ``,
567
+ `class PubSubRoom extends RemultPartyRoom<Cloudflare.Env> {`,
568
+ ` static options = { hibernate: false }`,
569
+ ` override options = { resolveRoomId: resolveRoomIdFromChannel };`,
570
+ ` override async onError(_connection: import('partyserver').Connection, error: unknown) {`,
571
+ ` console.error('PubSubRoom error:', error)`,
572
+ ` }`,
573
+ `}`, ``,
574
+ `export default { fetch: app.fetch }`,
575
+ `export { RemultLiveQueryStorageRoom, PubSubRoom as RemultPubSubRoom }`, ``
576
+ ].join('\n'));
577
+ const honoImports = [
578
+ `import { Hono } from 'hono'`,
579
+ `import { D1DataProvider, D1BindingClient } from 'remult/remult-d1'`,
580
+ `import { SqlDatabase } from 'remult'`,
581
+ `import { remultApi } from 'remult/remult-hono'`,
582
+ `import vike from '@vikejs/hono'`,
583
+ ]
584
+ if (betterauth) {
585
+ honoImports.push(
586
+ `import { getAuth } from './better-auth'`,
587
+ )
588
+ }
589
+ writeFileSync(join(root, 'server', 'hono.ts'), [...honoImports, ...honoBody].join('\n'))
590
+ writeFileSync(join(root, 'lib', 'remult-client.ts'), [
591
+ `import { RemultPartySubscriptionClient } from 'remult-partykit'`,
592
+ `import { remult } from 'remult'`,
593
+ `export function initRemultRealtime(host: string) {`,
594
+ ` const client = new RemultPartySubscriptionClient({`,
595
+ ` getSocketUrl: (roomName: string) => {`,
596
+ ` const wsHost = host.replace(/^http/, 'ws')`,
597
+ ` return \`\${wsHost}/party/remult?room=\${roomName}\``,
598
+ ` },`,
599
+ ` })`,
600
+ ` remult.apiClient.subscriptionClient = client`,
601
+ `}`, ``
602
+ ].join('\n'));
603
+ writeFileSync(join(root, '.gitignore'), `node_modules/\ndist/\n.wrangler/\n*.log\n.env\n`);
604
+ }
605
+
606
+ // --- Remult only ---
607
+ if (remult && !cloudflare) {
608
+ mkdirSync(join(root, 'server'), { recursive: true });
609
+ writeFileSync(join(root, 'server', 'remult.ts'), [
610
+ `import { remult } from 'remult'`,
611
+ `export const api = remult({ entities: [], getUser: async () => undefined })`, ``
612
+ ].join('\n'));
613
+ }
614
+ if (betterauth) {
615
+ mkdirSync(join(root, 'entities'), { recursive: true });
616
+ writeFileSync(join(root, 'entities', 'auth.ts'), [
617
+ `import { Allow, Entity, Fields, Relations, Validators } from 'remult'`,
618
+ ``,
619
+ `const Roles = { admin: 'admin' }`,
620
+ ``,
621
+ `@Entity('user', { allowApiCrud: Roles.admin, allowApiRead: Allow.authenticated })`,
622
+ `export class User {`,
623
+ ` @Fields.string({ required: true, minLength: 8, maxLength: 40, validate: Validators.unique(), allowApiUpdate: false })`,
624
+ ` id!: string`,
625
+ ` @Fields.string({ required: true })`,
626
+ ` name = ''`,
627
+ ` @Fields.string({})`,
628
+ ` email = ''`,
629
+ ` @Fields.boolean({})`,
630
+ ` emailVerified = false`,
631
+ ` @Fields.string({ required: false })`,
632
+ ` image = ''`,
633
+ ` @Fields.createdAt({})`,
634
+ ` createdAt!: Date`,
635
+ ` @Fields.updatedAt({})`,
636
+ ` updatedAt!: Date`,
637
+ `}`,
638
+ ``,
639
+ `@Entity('session', { allowApiCrud: Roles.admin })`,
640
+ `export class Session {`,
641
+ ` @Fields.string({ required: true, minLength: 8, maxLength: 40, validate: Validators.unique(), allowApiUpdate: false })`,
642
+ ` id!: string`,
643
+ ` @Fields.date({ required: true })`,
644
+ ` expiresAt = new Date()`,
645
+ ` @Fields.string({})`,
646
+ ` token = ''`,
647
+ ` @Fields.createdAt({})`,
648
+ ` createdAt!: Date`,
649
+ ` @Fields.updatedAt({ required: true, allowApiUpdate: false })`,
650
+ ` updatedAt!: Date`,
651
+ ` @Fields.string({ required: false })`,
652
+ ` ipAddress = ''`,
653
+ ` @Fields.string({ required: false })`,
654
+ ` userAgent = ''`,
655
+ ` @Fields.string({ required: true })`,
656
+ ` userId = ''`,
657
+ ` @Relations.toOne(() => User, 'id')`,
658
+ ` user!: User`,
659
+ `}`,
660
+ ``,
661
+ `@Entity('account', { allowApiCrud: Roles.admin })`,
662
+ `export class Account {`,
663
+ ` @Fields.string({ required: true, minLength: 8, maxLength: 40, validate: Validators.unique(), allowApiUpdate: false })`,
664
+ ` id!: string`,
665
+ ` @Fields.string({ required: true, allowApiUpdate: false })`,
666
+ ` accountId = ''`,
667
+ ` @Fields.string({ required: true, allowApiUpdate: false })`,
668
+ ` providerId = ''`,
669
+ ` @Fields.string({ required: true })`,
670
+ ` userId = ''`,
671
+ ` @Relations.toOne(() => User, 'id')`,
672
+ ` user!: User`,
673
+ ` @Fields.string({ required: false, allowApiUpdate: false })`,
674
+ ` accessToken = ''`,
675
+ ` @Fields.string({ required: false, allowApiUpdate: false })`,
676
+ ` refreshToken = ''`,
677
+ ` @Fields.string({ required: false })`,
678
+ ` idToken = ''`,
679
+ ` @Fields.date({ required: false })`,
680
+ ` accessTokenExpiresAt = new Date()`,
681
+ ` @Fields.date({ required: false })`,
682
+ ` refreshTokenExpiresAt = new Date()`,
683
+ ` @Fields.string({ required: false })`,
684
+ ` scope = ''`,
685
+ ` @Fields.string({ required: false, allowApiUpdate: false })`,
686
+ ` password = ''`,
687
+ ` @Fields.createdAt({ required: true, defaultValue: () => new Date(), allowApiUpdate: false })`,
688
+ ` createdAt!: Date`,
689
+ ` @Fields.updatedAt({ required: true, allowApiUpdate: false })`,
690
+ ` updatedAt!: Date`,
691
+ `}`,
692
+ ``,
693
+ `@Entity('verification', { allowApiCrud: Roles.admin })`,
694
+ `export class Verification {`,
695
+ ` @Fields.string({ required: true, minLength: 8, maxLength: 40, validate: Validators.unique(), allowApiUpdate: false })`,
696
+ ` id!: string`,
697
+ ` @Fields.string({ required: true })`,
698
+ ` identifier = ''`,
699
+ ` @Fields.string({ required: true })`,
700
+ ` value = ''`,
701
+ ` @Fields.date({ required: true })`,
702
+ ` expiresAt = new Date()`,
703
+ ` @Fields.createdAt({ required: true, defaultValue: () => new Date(), allowApiUpdate: false })`,
704
+ ` createdAt!: Date`,
705
+ ` @Fields.updatedAt({ required: true, defaultValue: () => new Date(), allowApiUpdate: false })`,
706
+ ` updatedAt!: Date`,
707
+ `}`,
708
+ ``
709
+ ].join('\n'));
710
+ mkdirSync(join(root, 'server'), { recursive: true });
711
+ writeFileSync(join(root, 'server', 'better-auth.ts'), [
712
+ `import { betterAuth } from 'better-auth'`,
713
+ `import { remultAdapter } from '@nerdfolio/remult-better-auth'`,
714
+ `import { User, Session, Account, Verification } from '../entities/auth'`,
715
+ `import type { BetterAuthOptions } from 'better-auth'`,
716
+ `import type { ClassType } from 'remult'`,
717
+ `import { SqlDatabase, withRemult } from 'remult'`,
718
+ `import { D1BindingClient, D1DataProvider } from 'remult/remult-d1'`,
719
+ ``,
720
+ `export function getAuthConfig(db: D1Database, secret: string, url?: string): BetterAuthOptions {`,
721
+ ` const dataProvider = new SqlDatabase(new D1DataProvider(new D1BindingClient(db)))`,
722
+ ``,
723
+ ` withRemult(`,
724
+ ` async (remult) => {`,
725
+ ` const entities = [User, Session, Account, Verification] as ClassType<unknown>[]`,
726
+ ` const metadata = entities.map((e) => remult.repo(e).metadata)`,
727
+ ` await dataProvider.ensureSchema(metadata)`,
728
+ ` },`,
729
+ ` { dataProvider }`,
730
+ ` ).catch((e: unknown) => console.error('Schema init failed:', e))`,
731
+ ``,
732
+ ` return {`,
733
+ ` secret,`,
734
+ ` baseURL: url,`,
735
+ ` database: remultAdapter({`,
736
+ ` authEntities: { User, Session, Account, Verification },`,
737
+ ` dataProvider`,
738
+ ` }),`,
739
+ ` emailAndPassword: { enabled: true }`,
740
+ ` }`,
741
+ `}`,
742
+ ``,
743
+ `let _auth: ReturnType<typeof betterAuth> | null = null`,
744
+ `let _schemaInit: Promise<void> | null = null`,
745
+ ``,
746
+ `async function ensureSchema(db: D1Database) {`,
747
+ ` if (!_schemaInit) {`,
748
+ ` _schemaInit = (async () => {`,
749
+ ` const dp = new SqlDatabase(new D1DataProvider(new D1BindingClient(db)))`,
750
+ ` await withRemult(`,
751
+ ` async (remult) => {`,
752
+ ` const entities = [User, Session, Account, Verification] as ClassType<unknown>[]`,
753
+ ` const metadata = entities.map((e) => remult.repo(e).metadata)`,
754
+ ` await dp.ensureSchema(metadata)`,
755
+ ` },`,
756
+ ` { dataProvider: dp }`,
757
+ ` )`,
758
+ ` })()`,
759
+ ` }`,
760
+ ` return _schemaInit`,
761
+ `}`,
762
+ ``,
763
+ `export async function getAuth(db: D1Database, secret: string, url?: string) {`,
764
+ ` if (!_auth) {`,
765
+ ` await ensureSchema(db)`,
766
+ ` const dp = new SqlDatabase(new D1DataProvider(new D1BindingClient(db)))`,
767
+ ` _auth = betterAuth<BetterAuthOptions>({`,
768
+ ` secret,`,
769
+ ` baseURL: url,`,
770
+ ` database: remultAdapter({`,
771
+ ` authEntities: { User, Session, Account, Verification },`,
772
+ ` dataProvider: dp`,
773
+ ` }),`,
774
+ ` emailAndPassword: { enabled: true }`,
775
+ ` })`,
776
+ ` }`,
777
+ ` return _auth`,
778
+ `}`,
779
+ ``,
780
+ ].join('\n'));
781
+ ``
782
+ }
783
+
784
+ // --- install ---
785
+ let label = `style: ${style}`;
786
+ if (cloudflare) label += ', CF Workers';
787
+ if (remult) label += ', Remult';
788
+ if (betterauth) label += ', Better Auth';
789
+ console.log(`\n \x1b[1mCreated ${name} (${label})\x1b[22m`);
790
+ console.log(`\n Installing dependencies...`);
791
+ execSync('npm install', { cwd: root, stdio: 'inherit' });
792
+ console.log(`\n Running @cioky/vike-core setup...`);
793
+ execSync('npx --yes @cioky/vike-core setup', { cwd: root, stdio: 'inherit' });
794
+ if (style === 'tailwind') {
795
+ console.log(`\n Running @cioky/vike-tailwindcss setup...`);
796
+ execSync('npx --yes @cioky/vike-tailwindcss setup', { cwd: root, stdio: 'inherit' });
797
+ }
798
+ if (style === 'pandacss') {
799
+ console.log(`\n Running @cioky/vike-pandacss setup...`);
800
+ execSync('npx --yes @cioky/vike-pandacss setup', { cwd: root, stdio: 'inherit' });
801
+ }
802
+
803
+
804
+ if (cloudflare) {
805
+ console.log(`\n Generating worker types...`);
806
+ execSync('npm run types', { cwd: root, stdio: 'inherit' });
807
+ }
808
+ console.log(`\n \x1b[1mDone!\x1b[22m`);
809
+ console.log(` cd ${name} && npm run dev`);