@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.
- package/README.md +60 -0
- package/package.json +32 -0
- 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`);
|