create-raiko 0.0.0-stage → 1.0.1

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/templates.mjs ADDED
@@ -0,0 +1,632 @@
1
+ // Generates every file of a new project from the installer answers.
2
+ import { V, LOCALES, isVite, run, exec, dbFiles, configFile, sharedFiles, serverPieces, authFiles, emailFiles, i18nFiles, adminFiles, deployFiles, envExample, gitignore } from './templates-common.mjs'
3
+
4
+ const json = (v) => JSON.stringify(v, null, 2) + '\n'
5
+ const MONSOON = 'https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css'
6
+ const COOKIEOWL = (o) => (o.cookies && o.cookies !== 'none' ? `<script src="https://cdn.jsdelivr.net/npm/cookieowl@0.2.0/dist/cookieowl.js" data-preset="${o.cookies}" data-accent="#ffd23f" data-policy-url="/privacy" defer></script>` : '')
7
+ const PWA = (o) => (o.pwa ? `<link rel="manifest" href="/manifest.webmanifest" /><script src="/installkite.js" data-sw="/sw.js" data-install="auto" defer></script>` : '')
8
+ const FAVICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0a0a0b"/><path d="M14 46 22 14l10 14 10-14 8 32-18-8z" fill="#ffd23f"/><path d="M36 22l-6 12h6l-4 12 10-16h-6l4-8z" fill="#0a0a0b"/></svg>`
9
+
10
+ export const FRONTENDS = [
11
+ { value: 'raiko', label: 'Raiko Pages + Voodoo.js', hint: 'server components, app router, zero JS by default' },
12
+ { value: 'react', label: 'React', hint: 'Vite' },
13
+ { value: 'vue', label: 'Vue', hint: 'Vite' },
14
+ { value: 'svelte', label: 'Svelte', hint: 'Vite' },
15
+ { value: 'solid', label: 'Solid', hint: 'Vite' },
16
+ { value: 'preact', label: 'Preact', hint: 'Vite' },
17
+ { value: 'angular', label: 'Angular', hint: 'Vite with Analog' },
18
+ ]
19
+ export const CSS = [
20
+ { value: 'monsoon', label: 'Monsoon', hint: 'no build step, theme from one color' },
21
+ { value: 'tailwind', label: 'Tailwind CSS', hint: 'v4' },
22
+ { value: 'stylex', label: 'StyleX', hint: 'by Meta, React only', only: ['react'] },
23
+ { value: 'unocss', label: 'UnoCSS' },
24
+ { value: 'none', label: 'Plain CSS' },
25
+ ]
26
+ export const ICONS = [
27
+ { value: 'glyphfox', label: 'glyphfox', hint: 'morphable icons, every framework' },
28
+ { value: 'lucide', label: 'Lucide' },
29
+ { value: 'heroicons', label: 'Heroicons', hint: 'React and Vue', only: ['react', 'vue'] },
30
+ { value: 'none', label: 'None' },
31
+ ]
32
+
33
+ const ext = { react: 'tsx', preact: 'tsx', solid: 'tsx', vue: 'vue', svelte: 'svelte', angular: 'ts' }
34
+
35
+ // ---------------------------------------------------------------- raiko pages
36
+
37
+ function raikoPages(o) {
38
+ const f = {}
39
+ const css = o.css === 'monsoon' ? `<link rel="stylesheet" href="${MONSOON}" />` : o.css === 'none' ? '<link rel="stylesheet" href="/styles.css" />' : `<link rel="stylesheet" href="/${o.css === 'unocss' ? 'uno' : 'styles'}.css" />`
40
+ const icon = o.icons === 'glyphfox' ? `import { toSvg } from 'glyphfox'\nimport { raw } from 'raiko.js'\n` : o.icons === 'lucide' ? `import { Zap } from 'lucide-static'\nimport { raw } from 'raiko.js'\n` : ''
41
+ const iconEl = o.icons === 'glyphfox' ? `{raw(toSvg('zap', { size: 28, color: '#ffd23f' }))}` : o.icons === 'lucide' ? `{raw(Zap)}` : ''
42
+ const t = (k, en) => (o.i18n ? `{t('${k}')}` : en)
43
+ f['app/layout.tsx'] = `import type { Metadata } from 'raiko.js/app'
44
+ import { SkipLink } from 'raiko.js/components'
45
+
46
+ export const metadata: Metadata = {
47
+ title: { default: ${JSON.stringify(o.name)}, template: '%s | ${o.name}' },
48
+ description: 'Built with Raiko.js',
49
+ icons: { icon: '/favicon.svg' },
50
+ }
51
+
52
+ export default function RootLayout({ children }: { children?: any }) {
53
+ return (
54
+ <html lang="en" data-theme="dark">
55
+ <head>
56
+ <meta charset="utf-8" />
57
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
58
+ ${css}${o.motion ? `\n <script type="module" src="/motion.js"></script>` : ''}${COOKIEOWL(o) ? `\n ${COOKIEOWL(o)}` : ''}${PWA(o) ? `\n ${PWA(o)}` : ''}
59
+ </head>
60
+ <body class="${o.css === 'monsoon' || o.css === 'tailwind' || o.css === 'unocss' ? 'min-h-screen bg-background text-foreground antialiased' : ''}">
61
+ <SkipLink />
62
+ <div id="main">{children}</div>${o.pwa ? `\n <button data-installkite-install hidden>Install the app</button>` : ''}
63
+ </body>
64
+ </html>
65
+ )
66
+ }
67
+ `
68
+ f['app/page.tsx'] = `${icon}${o.i18n ? "import { i18n } from '../locales'\nimport { headers } from 'raiko.js/server'\n" : ''}
69
+ /** A server component: it runs on the server, can be async and read the database. */
70
+ export default async function Home() {
71
+ ${o.i18n ? " const locale = i18n.detect(new Request('http://x', { headers: headers() }))\n const t = (k: any) => i18n.t(locale, k)\n" : ''} const started = new Date().toLocaleTimeString()
72
+ return (
73
+ <main class="mx-auto max-w-2xl p-8 space-y-6" style="font-family: ui-sans-serif, system-ui; padding: 48px">
74
+ <h1 class="text-4xl font-bold">${iconEl} ${t('title', `Welcome to ${o.name}`)}</h1>
75
+ <p class="text-muted-foreground">${t('subtitle', 'Edit app/page.tsx and save to reload.')}</p>
76
+ <p class="text-muted-foreground">Rendered on the server at {started}.</p>
77
+ {/* interactivity with Voodoo.js: no build, no hydration */}
78
+ <div v-data="{ count: 0 }">
79
+ <button class="btn btn-primary" v-on:click="count++">Clicked {'{ count }'} times</button>
80
+ </div>
81
+ <p><a class="link" href="https://kwy404.github.io/Raiko.js/docs/">${t('docs', 'Read the docs')}</a>${o.template === 'full' ? ` . <a class="link" href="/chat">Chat demo</a>` : ''}${o.admin ? ' . <a class="link" href="/admin">Admin</a>' : ''}</p>
82
+ </main>
83
+ )
84
+ }
85
+ `
86
+ f['app/not-found.tsx'] = `export default function NotFound() {
87
+ return (
88
+ <main style="display:grid;place-items:center;min-height:80vh;font-family:system-ui">
89
+ <div><h1 style="font-size:48px;margin:0">404</h1><p>This page could not be found. <a href="/">Go home</a></p></div>
90
+ </main>
91
+ )
92
+ }
93
+ `
94
+ f['app/error.tsx'] = `export default function ErrorPage({ error }: { error: Error }) {
95
+ return (
96
+ <main style="display:grid;place-items:center;min-height:80vh;font-family:system-ui">
97
+ <div><h1>Something went wrong</h1><p>We were told about it. Try again in a moment.</p></div>
98
+ </main>
99
+ )
100
+ }
101
+ `
102
+ f['app/api/hello/route.ts'] = `/** API routes live next to pages: GET /api/hello?name=Ana */
103
+ export const GET = ({ query }: { query: Record<string, string | undefined> }) => ({ hello: query.name ?? 'world', at: new Date().toISOString() })
104
+ `
105
+ if (o.template === 'full') {
106
+ f['app/loading.tsx'] = `export default function Loading() {\n return <p style="padding:48px">Loading...</p>\n}\n`
107
+ f['app/chat/socket.ts'] = `import { ChatMessage } from '../../shared/schemas'
108
+
109
+ /** WebSocket at /chat. Messages are validated, encrypted end to end, and broadcast to the room. */
110
+ export default {
111
+ body: ChatMessage,
112
+ encrypt: true,
113
+ open(ws: any) {
114
+ ws.subscribe('lobby')
115
+ },
116
+ message(ws: any, msg: { text: string }) {
117
+ const out = { id: ws.id.slice(-4), text: msg.text, at: Date.now() }
118
+ ws.send(out)
119
+ ws.publish('lobby', out)
120
+ },
121
+ }
122
+ `
123
+ f['app/chat/page.tsx'] = `export const metadata = { title: 'Chat' }
124
+
125
+ export default function Chat() {
126
+ return (
127
+ <main style="max-width:640px;margin:0 auto;padding:48px;font-family:system-ui">
128
+ <h1>Chat</h1>
129
+ <p class="text-muted-foreground">Open this page in two tabs. Messages are encrypted with AES-GCM.</p>
130
+ <ul id="log"></ul>
131
+ <form id="f"><input id="m" placeholder="Say something" autocomplete="off" /> <button class="btn btn-primary">Send</button></form>
132
+ <script type="module" src="/chat.js"></script>
133
+ </main>
134
+ )
135
+ }
136
+ `
137
+ f['public/chat.js'] = `// Browser client: reconnects, queues while offline, handles encryption.
138
+ import { RaikoClientSocket } from 'https://esm.sh/raiko.js@1/client'
139
+
140
+ const ws = new RaikoClientSocket(location.origin.replace(/^http/, 'ws') + '/chat')
141
+ const log = document.getElementById('log')
142
+ ws.on('message', (m) => {
143
+ const li = document.createElement('li')
144
+ li.textContent = m.id + ': ' + m.text
145
+ log.appendChild(li)
146
+ })
147
+ document.getElementById('f').addEventListener('submit', (e) => {
148
+ e.preventDefault()
149
+ const input = document.getElementById('m')
150
+ if (input.value) ws.send({ text: input.value })
151
+ input.value = ''
152
+ })
153
+ `
154
+ f['middleware.ts'] = `/** Runs before every page and API route. Return a Response to stop the request. */
155
+ export default function middleware(ctx: { path: string; set: { headers: Record<string, string> } }) {
156
+ ctx.set.headers['x-powered-by'] = 'Raiko.js'
157
+ }
158
+ export const config = { matcher: '^/(?!_raiko)' }
159
+ `
160
+ }
161
+ if (o.auth.length) {
162
+ f['app/dashboard/page.tsx'] = `import { currentUser } from 'raiko.js/server'
163
+
164
+ export const metadata = { title: 'Dashboard' }
165
+ /** Only signed in users. Use \`export const roles = ['admin']\` to require a role. */
166
+ export const auth = true
167
+
168
+ export default function Dashboard() {
169
+ const user = currentUser()
170
+ return (
171
+ <main style="max-width:720px;margin:0 auto;padding:48px;font-family:system-ui">
172
+ <h1>Hello, {String(user?.name ?? user?.email ?? 'there')}</h1>
173
+ <form method="post" action="/auth/signout"><button class="btn">Sign out</button></form>
174
+ </main>
175
+ )
176
+ }
177
+ `
178
+ f['app/forbidden.tsx'] = `export default function Forbidden() {
179
+ return (
180
+ <main style="display:grid;place-items:center;min-height:80vh;font-family:system-ui">
181
+ <div><h1>403</h1><p>You do not have access to this page. <a href="/">Go home</a></p></div>
182
+ </main>
183
+ )
184
+ }
185
+ `
186
+ f['app/login/page.tsx'] = `export const metadata = { title: 'Sign in' }
187
+
188
+ export default function Login({ searchParams }: { searchParams: Record<string, string> }) {
189
+ return (
190
+ <main style="max-width:380px;margin:10vh auto;font-family:system-ui">
191
+ <h1>Sign in</h1>
192
+ ${o.auth.includes('credentials') ? ` <form method="post" action="/auth/signin/credentials">
193
+ <input type="hidden" name="callbackUrl" value={searchParams.callbackUrl ?? '/'} />
194
+ <p><input name="email" type="email" placeholder="Email" required /></p>
195
+ <p><input name="password" type="password" placeholder="Password" required /></p>
196
+ <button class="btn btn-primary">Sign in</button>
197
+ </form>
198
+ ` : ''}${o.auth.filter((p) => p !== 'credentials').map((p) => ` <p><a class="btn" href="/auth/signin/${p}">Continue with ${p[0].toUpperCase() + p.slice(1)}</a></p>`).join('\n')}
199
+ </main>
200
+ )
201
+ }
202
+ `
203
+ if (o.twofa)
204
+ f['app/login/2fa/page.tsx'] = `export const metadata = { title: 'Two factor code' }
205
+
206
+ export default function TwoFactor() {
207
+ return (
208
+ <main style="max-width:380px;margin:10vh auto;font-family:system-ui">
209
+ <h1>Enter your code</h1>
210
+ <p>Open Google Authenticator (or Authy, 1Password) and type the 6 digit code.</p>
211
+ <form method="post" action="/auth/2fa">
212
+ <input name="code" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" required />
213
+ <button class="btn btn-primary">Verify</button>
214
+ </form>
215
+ </main>
216
+ )
217
+ }
218
+ `
219
+ }
220
+ if (o.cookies && o.cookies !== 'none')
221
+ f['app/privacy/page.tsx'] = `export const metadata = { title: '${o.cookies === 'lgpd' ? 'Privacidade' : 'Privacy'}' }
222
+
223
+ /** Write your real policy here. cookieowl links to this page from the banner. */
224
+ export default function Privacy() {
225
+ return (
226
+ <main style="max-width:720px;margin:0 auto;padding:48px;font-family:system-ui">
227
+ <h1>${o.cookies === 'lgpd' ? 'Politica de privacidade' : 'Privacy policy'}</h1>
228
+ <p>${o.cookies === 'lgpd' ? 'Explique quais dados voce coleta, por que, por quanto tempo e como a pessoa pede a exclusao (LGPD, art. 18).' : 'Explain what data you collect, why, for how long and how people ask for deletion (GDPR, art. 17).'}</p>
229
+ <p><a href="#" data-cookieowl-open>${o.cookies === 'lgpd' ? 'Preferencias de cookies' : 'Cookie settings'}</a></p>
230
+ </main>
231
+ )
232
+ }
233
+ `
234
+ if (o.css === 'none') f['public/styles.css'] = `:root{color-scheme:dark}body{margin:0;background:#0a0a0b;color:#ededed;font-family:ui-sans-serif,system-ui}a{color:#ffd23f}.btn{background:#18181b;color:#ededed;border:1px solid #27272a;border-radius:6px;padding:8px 14px;cursor:pointer}.btn-primary{background:#ffd23f;color:#0a0a0b;border:0}\n`
235
+ if (o.css === 'tailwind') f['app/globals.css'] = `@import "tailwindcss";\n@source "../app";\n\n@theme {\n --color-background: #0a0a0b;\n --color-foreground: #ededed;\n --color-muted-foreground: #a1a1aa;\n}\n\n.btn { @apply rounded-md px-4 py-2 font-semibold; }\n.btn-primary { @apply bg-yellow-300 text-black; }\n`
236
+ if (o.css === 'unocss') f['uno.config.ts'] = `import { defineConfig, presetUno } from 'unocss'\n\nexport default defineConfig({\n presets: [presetUno()],\n shortcuts: { btn: 'rounded-md px-4 py-2 font-semibold', 'btn-primary': 'bg-yellow-300 text-black', 'bg-background': 'bg-[#0a0a0b]', 'text-foreground': 'text-[#ededed]', 'text-muted-foreground': 'text-zinc-400' },\n})\n`
237
+ if (o.motion) f['public/motion.js'] = `// Motion (motion.dev): animate anything with data-animate
238
+ import { animate, inView } from 'https://cdn.jsdelivr.net/npm/motion@${V.motion}/+esm'
239
+
240
+ inView('[data-animate], h1', (el) => {
241
+ animate(el, { opacity: [0, 1], y: [12, 0] }, { duration: 0.5 })
242
+ })
243
+ `
244
+ const { imports, uses } = serverPieces({ ...o, serverDepth: './' })
245
+ f['server.ts'] = `${imports.join('\n')}
246
+ import { appRouter } from 'raiko.js/app'
247
+ import manifest from './.raiko/manifest'
248
+
249
+ /** The whole app: plugins first, then the app/ folder (pages, API routes, sockets). */
250
+ const app = new Raiko()
251
+ ${uses.join('\n')}
252
+ .use(await appRouter({
253
+ manifest,
254
+ locales: ${o.i18n ? 'config.i18n.locales' : 'undefined'},${o.auth.length ? `
255
+ // pages that export \`auth = true\` or \`roles = [...]\` need a signed in user
256
+ getUser: async (ctx) => (await auth.getSession(ctx.request))?.user ?? null,` : ''}
257
+ }))
258
+ .use(openapi({ title: config.name }))
259
+
260
+ export default app
261
+
262
+ if (isMain(import.meta.url)) {
263
+ const server = await app.listen(Number(process.env.PORT ?? 3000))
264
+ console.log(\` ready on \${server.url}\`)
265
+ }
266
+ `
267
+ return f
268
+ }
269
+
270
+ // ---------------------------------------------------------------- vite frameworks
271
+
272
+ function viteApp(o) {
273
+ const f = {}
274
+ const fw = o.frontend
275
+ const { imports, uses } = serverPieces({ ...o, serverDepth: '../' })
276
+ f['server/index.ts'] = `${imports.join('\n')}
277
+ import { t } from 'raiko.js'
278
+ import { staticFiles } from 'raiko.js/plugins'
279
+ import { ChatMessage, NewPost } from '../shared/schemas'
280
+
281
+ /** Backend: API, WebSockets, auth. In production it also serves the built frontend. */
282
+ const app = new Raiko()
283
+ ${uses.join('\n')}
284
+ .group('/api', (api) =>
285
+ api
286
+ .get('/hello', ({ query }) => ({ hello: query.name ?? 'world', at: new Date().toISOString() }), { query: t.Object({ name: t.Optional(t.String()) }) })
287
+ .post('/posts', ({ body, status }) => status(201, { id: Date.now(), ...body }), { body: NewPost }),
288
+ )
289
+ .ws('/ws/chat', {
290
+ body: ChatMessage,
291
+ encrypt: true,
292
+ open: (ws) => ws.subscribe('lobby'),
293
+ message(ws, msg) {
294
+ const out = { id: ws.id.slice(-4), text: msg.text, at: Date.now() }
295
+ ws.send(out)
296
+ ws.publish('lobby', out)
297
+ },
298
+ })
299
+ .use(openapi({ title: config.name }))
300
+ .use(process.env.NODE_ENV === 'production' ? staticFiles({ dir: 'dist', spa: true }) : new Raiko())
301
+
302
+ export default app
303
+ export type App = typeof app
304
+
305
+ if (isMain(import.meta.url)) {
306
+ const server = await app.listen(Number(process.env.PORT ?? 3000))
307
+ console.log(\` ready on \${server.url}\`)
308
+ }
309
+ `
310
+ f['web/src/api.ts'] = `/** Typed client: paths, params, bodies and responses come from server/index.ts. */
311
+ import { client } from 'raiko.js/client'
312
+ import type { App } from '../../server'
313
+
314
+ export const api = client<App>('')
315
+ `
316
+ const cssImport = o.css === 'tailwind' ? `import './styles.css'\n` : o.css === 'unocss' ? `import 'virtual:uno.css'\n` : o.css === 'none' ? `import './styles.css'\n` : ''
317
+ if (o.css === 'tailwind') f['web/src/styles.css'] = `@import "tailwindcss";\n`
318
+ if (o.css === 'none') f['web/src/styles.css'] = `:root{color-scheme:dark}body{margin:0;background:#0a0a0b;color:#ededed;font-family:ui-sans-serif,system-ui}a{color:#ffd23f}button{background:#ffd23f;color:#0a0a0b;border:0;border-radius:6px;padding:8px 14px;font-weight:600;cursor:pointer}\n`
319
+ if (o.css === 'unocss') f['uno.config.ts'] = `import { defineConfig, presetUno } from 'unocss'\n\nexport default defineConfig({ presets: [presetUno()] })\n`
320
+ f['web/index.html'] = `<!doctype html>
321
+ <html lang="en" data-theme="dark">
322
+ <head>
323
+ <meta charset="utf-8" />
324
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
325
+ <title>${o.name.replace(/[<>&"]/g, '')}</title>
326
+ <meta name="description" content="Built with Raiko.js" />
327
+ <link rel="icon" href="/favicon.svg" />${o.css === 'monsoon' ? `\n <link rel="stylesheet" href="${MONSOON}" />` : ''}${COOKIEOWL(o) ? `\n ${COOKIEOWL(o)}` : ''}${PWA(o) ? `\n ${PWA(o)}` : ''}
328
+ </head>
329
+ <body class="bg-background text-foreground">
330
+ ${fw === 'angular' ? '<app-root></app-root>' : '<div id="app"></div>'}
331
+ <script type="module" src="/src/main.ts${fw === 'react' || fw === 'preact' || fw === 'solid' ? 'x' : ''}"></script>
332
+ </body>
333
+ </html>
334
+ `
335
+ f['web/public/favicon.svg'] = FAVICON
336
+ const plugin = { react: ["import react from '@vitejs/plugin-react'", 'react()'], vue: ["import vue from '@vitejs/plugin-vue'", 'vue()'], svelte: ["import { svelte } from '@sveltejs/vite-plugin-svelte'", 'svelte()'], solid: ["import solid from 'vite-plugin-solid'", 'solid()'], preact: ["import preact from '@preact/preset-vite'", 'preact()'], angular: ["import angular from '@analogjs/vite-plugin-angular'", "angular({ tsconfig: 'tsconfig.app.json' })"] }[fw]
337
+ const extra = []
338
+ if (o.css === 'tailwind') extra.push(["import tailwind from '@tailwindcss/vite'", 'tailwind()'])
339
+ if (o.css === 'unocss') extra.push(["import uno from 'unocss/vite'", 'uno()'])
340
+ if (o.css === 'stylex') extra.push(["import stylex from 'vite-plugin-stylex'", 'stylex()'])
341
+ f['vite.config.ts'] = `import { defineConfig } from 'vite'
342
+ ${[plugin, ...extra].map((p) => p[0]).join('\n')}
343
+ import { raiko } from 'raiko.js/vite'
344
+
345
+ export default defineConfig({
346
+ root: 'web',
347
+ build: { outDir: '../dist', emptyOutDir: true },
348
+ // the backend runs inside the Vite dev server: one port, hot reload for both
349
+ plugins: [${[plugin, ...extra].map((p) => p[1]).join(', ')}, raiko({ entry: './server/index.ts', paths: ['/api', '/auth', '/admin', '/docs', '/_raiko', '/ws', '/health'] })],
350
+ })
351
+ `
352
+ const hello = `Welcome to ${o.name}`
353
+ if (fw === 'react' || fw === 'preact') {
354
+ const R = fw === 'react'
355
+ const iconImp = o.icons === 'glyphfox' ? `import { ZapIcon } from 'glyphfox/${fw}'\n` : o.icons === 'lucide' ? `import { Zap } from 'lucide-${fw}'\n` : o.icons === 'heroicons' ? `import { BoltIcon } from '@heroicons/react/24/solid'\n` : ''
356
+ const iconEl = o.icons === 'glyphfox' ? '<ZapIcon size={28} /> ' : o.icons === 'lucide' ? '<Zap size={28} /> ' : o.icons === 'heroicons' ? '<BoltIcon width={28} /> ' : ''
357
+ const sx = o.css === 'stylex'
358
+ f['web/src/main.tsx'] = R
359
+ ? `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\n${cssImport}import App from './App'\n\ncreateRoot(document.getElementById('app')!).render(<StrictMode><App /></StrictMode>)\n`
360
+ : `import { render } from 'preact'\n${cssImport}import App from './App'\n\nrender(<App />, document.getElementById('app')!)\n`
361
+ f['web/src/App.tsx'] = `${R ? "import { useState } from 'react'\nimport { useQuery, useSocket } from 'raiko.js/react'" : "import { useState } from 'preact/hooks'"}
362
+ ${o.motion && R ? "import { motion } from 'motion/react'\n" : ''}${sx ? "import * as stylex from '@stylexjs/stylex'\n" : ''}${iconImp}import { api } from './api'
363
+ ${sx ? `
364
+ const s = stylex.create({
365
+ main: { maxWidth: 640, margin: '0 auto', padding: 48, fontFamily: 'system-ui' },
366
+ button: { background: '#ffd23f', color: '#0a0a0b', border: 0, borderRadius: 6, padding: '8px 14px', fontWeight: 600 },
367
+ })
368
+ ` : ''}
369
+ export default function App() {
370
+ const [count, setCount] = useState(0)
371
+ ${R ? ` const hello = useQuery(() => api.get('/api/hello', { query: { name: 'Raiko' } }))
372
+ const [messages, setMessages] = useState<{ id: string; text: string }[]>([])
373
+ const chat = useSocket(() => api.ws<{ id: string; text: string }, { text: string }>('/ws/chat'), (m) => setMessages((all) => [...all, m]))` : ` const [hello, setHello] = useState<string>('...')
374
+ api.get('/api/hello').then((r) => setHello(r.data?.hello ?? 'error'))`}
375
+ const H = ${o.motion && R ? 'motion.h1' : "'h1'"}
376
+ return (
377
+ <main ${sx ? '{...stylex.props(s.main)}' : 'className="mx-auto max-w-2xl p-12 space-y-4"'}>
378
+ <H${o.motion && R ? ' initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}' : ''}>${iconEl}${hello}</H>
379
+ <p>API says: {${R ? "hello.loading ? '...' : hello.data?.hello" : 'hello'}}</p>
380
+ <button ${sx ? '{...stylex.props(s.button)}' : 'className="btn btn-primary"'} onClick={() => setCount(count + 1)}>Clicked {count} times</button>
381
+ ${R ? ` <p>Chat {chat.connected ? 'online (encrypted)' : 'connecting'}</p>
382
+ <button onClick={() => chat.send({ text: 'hi from React' })}>Send hi</button>
383
+ <ul>{messages.map((m, i) => <li key={i}>{m.id}: {m.text}</li>)}</ul>` : ''}
384
+ </main>
385
+ )
386
+ }
387
+ `
388
+ }
389
+ if (fw === 'vue') {
390
+ f['web/src/main.ts'] = `import { createApp } from 'vue'\n${cssImport}import App from './App.vue'\n\ncreateApp(App).mount('#app')\n`
391
+ f['web/src/App.vue'] = `<script setup lang="ts">
392
+ import { ref } from 'vue'
393
+ import { useQuery, useSocket } from 'raiko.js/vue'
394
+ ${o.icons === 'glyphfox' ? "import { ZapIcon } from 'glyphfox/vue'\n" : o.icons === 'lucide' ? "import { Zap } from 'lucide-vue-next'\n" : o.icons === 'heroicons' ? "import { BoltIcon } from '@heroicons/vue/24/solid'\n" : ''}import { api } from './api'
395
+
396
+ const count = ref(0)
397
+ const hello = useQuery(() => api.get('/api/hello', { query: { name: 'Raiko' } }))
398
+ const messages = ref<{ id: string; text: string }[]>([])
399
+ const chat = useSocket(() => api.ws<{ id: string; text: string }, { text: string }>('/ws/chat'), (m) => messages.value.push(m))
400
+ </script>
401
+
402
+ <template>
403
+ <main class="mx-auto max-w-2xl p-12 space-y-4">
404
+ <h1>${o.icons === 'glyphfox' ? '<ZapIcon :size="28" /> ' : o.icons === 'lucide' ? '<Zap :size="28" /> ' : o.icons === 'heroicons' ? '<BoltIcon style="width:28px" /> ' : ''}${hello}</h1>
405
+ <p>API says: {{ hello.loading.value ? '...' : hello.data.value?.hello }}</p>
406
+ <button class="btn btn-primary" @click="count++">Clicked {{ count }} times</button>
407
+ <p>Chat {{ chat.connected.value ? 'online (encrypted)' : 'connecting' }}</p>
408
+ <button @click="chat.send({ text: 'hi from Vue' })">Send hi</button>
409
+ <ul><li v-for="(m, i) in messages" :key="i">{{ m.id }}: {{ m.text }}</li></ul>
410
+ </main>
411
+ </template>
412
+ `
413
+ f['web/src/env.d.ts'] = `declare module '*.vue' {\n import type { DefineComponent } from 'vue'\n const c: DefineComponent\n export default c\n}\n`
414
+ }
415
+ if (fw === 'svelte') {
416
+ f['web/src/main.ts'] = `import { mount } from 'svelte'\n${cssImport}import App from './App.svelte'\n\nmount(App, { target: document.getElementById('app')! })\n`
417
+ f['web/src/App.svelte'] = `<script lang="ts">
418
+ import { api } from './api'
419
+ ${o.icons === 'glyphfox' ? " import { ZapIcon } from 'glyphfox/svelte'\n" : o.icons === 'lucide' ? " import { Zap } from 'lucide-svelte'\n" : ''} let count = $state(0)
420
+ let hello = $state('...')
421
+ let messages = $state<{ id: string; text: string }[]>([])
422
+ api.get('/api/hello', { query: { name: 'Raiko' } }).then((r) => (hello = r.data?.hello ?? 'error'))
423
+ const chat = api.ws<{ id: string; text: string }, { text: string }>('/ws/chat')
424
+ chat.on('message', (m) => (messages = [...messages, m]))
425
+ </script>
426
+
427
+ <main class="mx-auto max-w-2xl p-12 space-y-4">
428
+ <h1>${o.icons === 'glyphfox' ? '<ZapIcon size={28} /> ' : o.icons === 'lucide' ? '<Zap size={28} /> ' : ''}${hello}</h1>
429
+ <p>API says: {hello}</p>
430
+ <button class="btn btn-primary" onclick={() => count++}>Clicked {count} times</button>
431
+ <button onclick={() => chat.send({ text: 'hi from Svelte' })}>Send hi</button>
432
+ <ul>{#each messages as m}<li>{m.id}: {m.text}</li>{/each}</ul>
433
+ </main>
434
+ `
435
+ f['web/src/env.d.ts'] = `declare module '*.svelte' {\n const c: any\n export default c\n}\n`
436
+ }
437
+ if (fw === 'solid') {
438
+ f['web/src/main.tsx'] = `import { render } from 'solid-js/web'\n${cssImport}import App from './App'\n\nrender(() => <App />, document.getElementById('app')!)\n`
439
+ f['web/src/App.tsx'] = `import { createResource, createSignal, For } from 'solid-js'
440
+ ${o.icons === 'glyphfox' ? "import { ZapIcon } from 'glyphfox/solid'\n" : o.icons === 'lucide' ? "import { Zap } from 'lucide-solid'\n" : ''}import { api } from './api'
441
+
442
+ export default function App() {
443
+ const [count, setCount] = createSignal(0)
444
+ const [hello] = createResource(async () => (await api.get('/api/hello', { query: { name: 'Raiko' } })).data?.hello)
445
+ const [messages, setMessages] = createSignal<{ id: string; text: string }[]>([])
446
+ const chat = api.ws<{ id: string; text: string }, { text: string }>('/ws/chat')
447
+ chat.on('message', (m) => setMessages([...messages(), m]))
448
+ return (
449
+ <main class="mx-auto max-w-2xl p-12 space-y-4">
450
+ <h1>${o.icons === 'glyphfox' ? '<ZapIcon size={28} /> ' : o.icons === 'lucide' ? '<Zap size={28} /> ' : ''}${hello}</h1>
451
+ <p>API says: {hello() ?? '...'}</p>
452
+ <button class="btn btn-primary" onClick={() => setCount(count() + 1)}>Clicked {count()} times</button>
453
+ <button onClick={() => chat.send({ text: 'hi from Solid' })}>Send hi</button>
454
+ <ul><For each={messages()}>{(m) => <li>{m.id}: {m.text}</li>}</For></ul>
455
+ </main>
456
+ )
457
+ }
458
+ `
459
+ }
460
+ if (fw === 'angular') {
461
+ f['web/src/main.ts'] = `import '@angular/compiler'\nimport 'zone.js'\n${cssImport}import { bootstrapApplication } from '@angular/platform-browser'\nimport { AppComponent } from './app.component'\n\nbootstrapApplication(AppComponent).catch((e) => console.error(e))\n`
462
+ f['web/src/app.component.ts'] = `import { Component, signal } from '@angular/core'
463
+ import { api } from './api'
464
+
465
+ @Component({
466
+ selector: 'app-root',
467
+ standalone: true,
468
+ template: \`
469
+ <main class="mx-auto max-w-2xl p-12 space-y-4">
470
+ <h1>${hello}</h1>
471
+ <p>API says: {{ hello() }}</p>
472
+ <button class="btn btn-primary" (click)="count.set(count() + 1)">Clicked {{ count() }} times</button>
473
+ <button (click)="send()">Send hi</button>
474
+ <ul>@for (m of messages(); track $index) { <li>{{ m.id }}: {{ m.text }}</li> }</ul>
475
+ </main>
476
+ \`,
477
+ })
478
+ export class AppComponent {
479
+ count = signal(0)
480
+ hello = signal('...')
481
+ messages = signal<{ id: string; text: string }[]>([])
482
+ chat = api.ws<{ id: string; text: string }, { text: string }>('/ws/chat')
483
+ constructor() {
484
+ api.get('/api/hello', { query: { name: 'Raiko' } }).then((r) => this.hello.set(r.data?.hello ?? 'error'))
485
+ this.chat.on('message', (m) => this.messages.set([...this.messages(), m]))
486
+ }
487
+ send() {
488
+ this.chat.send({ text: 'hi from Angular' })
489
+ }
490
+ }
491
+ `
492
+ f['tsconfig.app.json'] = json({ extends: './tsconfig.json', compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false, jsx: undefined }, files: ['web/src/main.ts'], include: ['web/src/**/*.ts'] })
493
+ }
494
+ return f
495
+ }
496
+
497
+ // ---------------------------------------------------------------- package.json and readme
498
+
499
+ function packageJson(o) {
500
+ const scripts_extra = {}
501
+ const deps = { 'raiko.js': V.raiko }
502
+ // Angular's compiler only supports TypeScript below 5.6
503
+ const dev = { typescript: o.frontend === 'angular' ? '5.5.4' : V.typescript, '@types/node': V.nodeTypes }
504
+ if (o.pm === 'bun') dev['@types/bun'] = V.bunTypes
505
+ else Object.assign(dev, { tsx: V.tsx, esbuild: V.esbuild })
506
+ const fw = o.frontend
507
+ if (isVite(o)) Object.assign(dev, { vite: V.vite, terser: V.terser })
508
+ const F = {
509
+ react: [{ react: '18.3.1', 'react-dom': '18.3.1' }, { '@vitejs/plugin-react': '4.3.4', '@types/react': '18.3.12', '@types/react-dom': '18.3.1' }],
510
+ preact: [{ preact: '10.25.4' }, { '@preact/preset-vite': '2.9.3' }],
511
+ vue: [{ vue: '3.5.13' }, { '@vitejs/plugin-vue': '5.2.1' }],
512
+ svelte: [{ svelte: '5.16.0' }, { '@sveltejs/vite-plugin-svelte': '4.0.4' }],
513
+ solid: [{ 'solid-js': '1.9.3' }, { 'vite-plugin-solid': '2.11.0' }],
514
+ angular: [{ '@angular/core': '18.2.13', '@angular/common': '18.2.13', '@angular/compiler': '18.2.13', '@angular/platform-browser': '18.2.13', rxjs: '7.8.1', 'zone.js': '0.14.10', tslib: '2.8.1' }, { '@analogjs/vite-plugin-angular': '1.10.3', '@angular/compiler-cli': '18.2.13', '@angular-devkit/build-angular': '18.2.12' }],
515
+ }[fw]
516
+ if (F) Object.assign(deps, F[0]), Object.assign(dev, F[1])
517
+ if (o.css === 'tailwind') Object.assign(dev, isVite(o) ? { tailwindcss: '4.1.11', '@tailwindcss/vite': '4.1.11' } : { tailwindcss: '4.1.11', '@tailwindcss/cli': '4.1.11' })
518
+ if (o.css === 'unocss') Object.assign(dev, isVite(o) ? { unocss: '0.65.1' } : { unocss: '0.65.1', '@unocss/cli': '0.65.1' })
519
+ if (o.css === 'stylex') Object.assign(deps, { '@stylexjs/stylex': '0.9.3' }), Object.assign(dev, { 'vite-plugin-stylex': '0.13.0' })
520
+ if (o.css === 'monsoon' && isVite(o)) deps.monsooncss = '0.2.0'
521
+ if (o.icons === 'glyphfox') deps.glyphfox = V.glyphfox
522
+ if (o.icons === 'lucide') deps[{ raiko: 'lucide-static', react: 'lucide-react', preact: 'lucide-preact', vue: 'lucide-vue-next', svelte: 'lucide-svelte', solid: 'lucide-solid', angular: 'lucide-static' }[fw]] = V.lucide
523
+ if (o.icons === 'heroicons') deps[fw === 'vue' ? '@heroicons/vue' : '@heroicons/react'] = V.heroicons
524
+ if (o.motion && fw === 'react') deps.motion = V.motion
525
+ if (o.orm === 'kastor') deps['kastor-orm'] = V.kastor
526
+ if (o.orm === 'prisma') deps['@prisma/client'] = V.prisma, (dev.prisma = V.prisma)
527
+ if (o.orm === 'drizzle') deps['drizzle-orm'] = V.drizzle, (dev['drizzle-kit'] = V.drizzleKit)
528
+ if (o.orm !== 'none' && o.db === 'sqlite' && o.pm !== 'bun') deps['better-sqlite3'] = V.betterSqlite
529
+ if (o.orm === 'kastor' && o.db === 'postgres') deps.pg = V.pg
530
+ if (o.orm === 'drizzle' && o.db === 'postgres') deps.postgres = V.postgresJs
531
+ if (o.email === 'smtp') deps.nodemailer = V.nodemailer
532
+ if (o.pwa) Object.assign(dev, { installkite: '0.2.0' }), (scripts_extra.pwa = 'installkite build')
533
+ const entry = isVite(o) ? 'server/index.ts' : 'server.ts'
534
+ const cssDev = !isVite(o) && o.css === 'tailwind' ? 'tailwindcss -i app/globals.css -o public/styles.css' : !isVite(o) && o.css === 'unocss' ? 'unocss "app/**/*.tsx" -o public/uno.css' : null
535
+ const scripts = {
536
+ dev: cssDev ? `${cssDev} && raiko dev ${entry}` : `raiko dev${isVite(o) ? '' : ' ' + entry}`,
537
+ build: `${cssDev ? `${cssDev} --minify && ` : ''}raiko build ${entry}`,
538
+ start: 'raiko start',
539
+ typecheck: 'tsc --noEmit',
540
+ test: o.pm === 'bun' ? 'bun test' : 'node --test',
541
+ deploy: `raiko deploy ${o.deploy === 'node' ? 'docker' : o.deploy}`,
542
+ }
543
+ Object.assign(scripts, scripts_extra)
544
+ if (o.orm === 'kastor') scripts['db:migrate'] = 'kastor migrate dev'
545
+ if (o.orm === 'prisma') Object.assign(scripts, { 'db:migrate': 'prisma migrate dev', postinstall: 'prisma generate' })
546
+ if (o.orm === 'drizzle') scripts['db:migrate'] = 'drizzle-kit push'
547
+ return json({ name: o.name, version: '0.1.0', private: true, type: 'module', scripts, dependencies: deps, devDependencies: dev, raiko: { frontend: fw, css: o.css, orm: o.orm, deploy: o.deploy } })
548
+ }
549
+
550
+ function tsconfig(o) {
551
+ const jsx = { react: ['react-jsx', 'react'], preact: ['react-jsx', 'preact'], solid: ['preserve', 'solid-js'] }[o.frontend] ?? ['react-jsx', 'raiko.js']
552
+ return json({
553
+ compilerOptions: {
554
+ target: 'ES2022', module: 'ESNext', moduleResolution: 'Bundler', lib: ['ES2022', 'DOM', 'DOM.Iterable'], strict: true, noUncheckedIndexedAccess: true,
555
+ skipLibCheck: true, noEmit: true, allowImportingTsExtensions: false, resolveJsonModule: true, isolatedModules: true, verbatimModuleSyntax: false,
556
+ jsx: jsx[0], jsxImportSource: jsx[1], types: o.pm === 'bun' ? ['bun', 'node'] : ['node'],
557
+ },
558
+ include: ['**/*.ts', '**/*.tsx', ...(o.frontend === 'vue' ? ['**/*.vue'] : [])],
559
+ exclude: ['node_modules', 'dist', '.raiko/out'],
560
+ })
561
+ }
562
+
563
+ function readme(o) {
564
+ const roles = Object.entries(o.roles).map(([r, res]) => `| ${r} | ${Object.entries(res).map(([k, v]) => `${k}: ${v === '*' ? 'all' : v.join(', ') || 'none'}`).join('<br>')} |`).join('\n')
565
+ return `# ${o.name}
566
+
567
+ Made with [Raiko.js](https://kwy404.github.io/Raiko.js/).
568
+
569
+ \`\`\`bash
570
+ ${run(o, 'dev')} # http://localhost:${isVite(o) ? '5173' : '3000'}
571
+ ${run(o, 'build')} # production build
572
+ ${run(o, 'start')} # run the build
573
+ ${run(o, 'deploy')} # ${o.deploy}
574
+ \`\`\`
575
+
576
+ ## What is inside
577
+
578
+ | | |
579
+ | --- | --- |
580
+ | Frontend | ${FRONTENDS.find((x) => x.value === o.frontend).label} |
581
+ | CSS | ${CSS.find((x) => x.value === o.css).label} |
582
+ | Database | ${o.orm === 'none' ? 'none' : `${o.orm} on ${o.db}`} |
583
+ | Auth | ${o.auth.length ? o.auth.join(', ') + (o.twofa ? ' + TOTP 2FA' : '') : 'none'} |
584
+ | Email | ${o.email === 'none' ? 'console only' : o.email} (preview at /_raiko/mail) |
585
+ | i18n | ${o.i18n ? Object.keys(LOCALES).join(', ') : 'off'} |
586
+ | Admin | ${o.admin ? '/admin' : 'off'} |
587
+ | API docs | /docs (OpenAPI 3.1) |
588
+ | Deploy | ${o.deploy} |
589
+
590
+ Change any of it in \`raiko.config.ts\`.
591
+ ${o.admin ? `
592
+ ## Admin permissions
593
+
594
+ | Role | Allowed |
595
+ | --- | --- |
596
+ ${roles}
597
+ ` : ''}
598
+ ## Folders
599
+
600
+ \`\`\`
601
+ ${isVite(o) ? 'web/ frontend (' + o.frontend + ')\nserver/ backend: API, WebSockets, auth' : 'app/ pages, layouts, API routes and sockets\nserver.ts plugins and startup'}
602
+ shared/ schemas used by both sides
603
+ ${o.orm !== 'none' ? 'db/ database schema and client\n' : ''}${o.email !== 'none' || o.auth.length ? 'emails/ email templates in TSX\n' : ''}${o.i18n ? 'locales/ translations\n' : ''}\`\`\`
604
+ `
605
+ }
606
+
607
+ export function generate(input) {
608
+ // the name lands in code, TOML and HTML: keep only npm name characters, whatever the caller passed
609
+ const o = { ...input, name: String(input.name).toLowerCase().replace(/[^a-z0-9._-]/g, '-').replace(/^[._-]+/, '').slice(0, 214) || 'raiko-app' }
610
+ const files = {
611
+ 'package.json': packageJson(o),
612
+ 'tsconfig.json': tsconfig(o),
613
+ '.gitignore': gitignore(),
614
+ '.env.example': envExample(o),
615
+ 'README.md': readme(o),
616
+ 'raiko.config.ts': configFile(o),
617
+ ...sharedFiles(o),
618
+ ...(isVite(o) ? viteApp(o) : raikoPages(o)),
619
+ ...dbFiles(o),
620
+ ...authFiles(o),
621
+ ...emailFiles(o),
622
+ ...i18nFiles(o),
623
+ ...adminFiles(o),
624
+ ...deployFiles(o),
625
+ }
626
+ if (!isVite(o)) {
627
+ files['public/favicon.svg'] = FAVICON
628
+ files['.raiko/manifest.ts'] = '// replaced by `raiko dev` and `raiko build`\nexport default {}\n'
629
+ }
630
+ return files
631
+ }
632
+ export { run, exec }