create-rudder-app 0.3.0 → 0.4.0

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 (243) hide show
  1. package/dist/index.js +100 -20
  2. package/dist/index.js.map +1 -1
  3. package/dist/templates/app/auth-controller.d.ts +2 -0
  4. package/dist/templates/app/auth-controller.d.ts.map +1 -0
  5. package/dist/templates/app/auth-controller.js +51 -0
  6. package/dist/templates/app/auth-controller.js.map +1 -0
  7. package/dist/templates/app/mcp-echo-server.d.ts +2 -0
  8. package/dist/templates/app/mcp-echo-server.d.ts.map +1 -0
  9. package/dist/templates/app/mcp-echo-server.js +13 -0
  10. package/dist/templates/app/mcp-echo-server.js.map +1 -0
  11. package/dist/templates/app/mcp-echo-tool.d.ts +2 -0
  12. package/dist/templates/app/mcp-echo-tool.d.ts.map +1 -0
  13. package/dist/templates/app/mcp-echo-tool.js +20 -0
  14. package/dist/templates/app/mcp-echo-tool.js.map +1 -0
  15. package/dist/templates/app/service-provider.d.ts +3 -0
  16. package/dist/templates/app/service-provider.d.ts.map +1 -0
  17. package/dist/templates/app/service-provider.js +27 -0
  18. package/dist/templates/app/service-provider.js.map +1 -0
  19. package/dist/templates/app/user-model.d.ts +2 -0
  20. package/dist/templates/app/user-model.d.ts.map +1 -0
  21. package/dist/templates/app/user-model.js +19 -0
  22. package/dist/templates/app/user-model.js.map +1 -0
  23. package/dist/templates/bootstrap/app.d.ts +3 -0
  24. package/dist/templates/bootstrap/app.d.ts.map +1 -0
  25. package/dist/templates/bootstrap/app.js +41 -0
  26. package/dist/templates/bootstrap/app.js.map +1 -0
  27. package/dist/templates/bootstrap/providers.d.ts +3 -0
  28. package/dist/templates/bootstrap/providers.d.ts.map +1 -0
  29. package/dist/templates/bootstrap/providers.js +27 -0
  30. package/dist/templates/bootstrap/providers.js.map +1 -0
  31. package/dist/templates/configs/ai.d.ts +2 -0
  32. package/dist/templates/configs/ai.d.ts.map +1 -0
  33. package/dist/templates/configs/ai.js +32 -0
  34. package/dist/templates/configs/ai.js.map +1 -0
  35. package/dist/templates/configs/app.d.ts +2 -0
  36. package/dist/templates/configs/app.d.ts.map +1 -0
  37. package/dist/templates/configs/app.js +12 -0
  38. package/dist/templates/configs/app.js.map +1 -0
  39. package/dist/templates/configs/auth.d.ts +3 -0
  40. package/dist/templates/configs/auth.d.ts.map +1 -0
  41. package/dist/templates/configs/auth.js +16 -0
  42. package/dist/templates/configs/auth.js.map +1 -0
  43. package/dist/templates/configs/cache.d.ts +2 -0
  44. package/dist/templates/configs/cache.d.ts.map +1 -0
  45. package/dist/templates/configs/cache.js +28 -0
  46. package/dist/templates/configs/cache.js.map +1 -0
  47. package/dist/templates/configs/cashier.d.ts +2 -0
  48. package/dist/templates/configs/cashier.d.ts.map +1 -0
  49. package/dist/templates/configs/cashier.js +22 -0
  50. package/dist/templates/configs/cashier.js.map +1 -0
  51. package/dist/templates/configs/crypt.d.ts +2 -0
  52. package/dist/templates/configs/crypt.d.ts.map +1 -0
  53. package/dist/templates/configs/crypt.js +16 -0
  54. package/dist/templates/configs/crypt.js.map +1 -0
  55. package/dist/templates/configs/database.d.ts +3 -0
  56. package/dist/templates/configs/database.d.ts.map +1 -0
  57. package/dist/templates/configs/database.js +28 -0
  58. package/dist/templates/configs/database.js.map +1 -0
  59. package/dist/templates/configs/hash.d.ts +2 -0
  60. package/dist/templates/configs/hash.d.ts.map +1 -0
  61. package/dist/templates/configs/hash.js +12 -0
  62. package/dist/templates/configs/hash.js.map +1 -0
  63. package/dist/templates/configs/horizon.d.ts +2 -0
  64. package/dist/templates/configs/horizon.d.ts.map +1 -0
  65. package/dist/templates/configs/horizon.js +30 -0
  66. package/dist/templates/configs/horizon.js.map +1 -0
  67. package/dist/templates/configs/index.d.ts +3 -0
  68. package/dist/templates/configs/index.d.ts.map +1 -0
  69. package/dist/templates/configs/index.js +96 -0
  70. package/dist/templates/configs/index.js.map +1 -0
  71. package/dist/templates/configs/localization.d.ts +2 -0
  72. package/dist/templates/configs/localization.d.ts.map +1 -0
  73. package/dist/templates/configs/localization.js +13 -0
  74. package/dist/templates/configs/localization.js.map +1 -0
  75. package/dist/templates/configs/log.d.ts +2 -0
  76. package/dist/templates/configs/log.d.ts.map +1 -0
  77. package/dist/templates/configs/log.js +40 -0
  78. package/dist/templates/configs/log.js.map +1 -0
  79. package/dist/templates/configs/mail.d.ts +2 -0
  80. package/dist/templates/configs/mail.d.ts.map +1 -0
  81. package/dist/templates/configs/mail.js +33 -0
  82. package/dist/templates/configs/mail.js.map +1 -0
  83. package/dist/templates/configs/passport.d.ts +2 -0
  84. package/dist/templates/configs/passport.d.ts.map +1 -0
  85. package/dist/templates/configs/passport.js +22 -0
  86. package/dist/templates/configs/passport.js.map +1 -0
  87. package/dist/templates/configs/pennant.d.ts +2 -0
  88. package/dist/templates/configs/pennant.d.ts.map +1 -0
  89. package/dist/templates/configs/pennant.js +16 -0
  90. package/dist/templates/configs/pennant.js.map +1 -0
  91. package/dist/templates/configs/pulse.d.ts +2 -0
  92. package/dist/templates/configs/pulse.d.ts.map +1 -0
  93. package/dist/templates/configs/pulse.js +21 -0
  94. package/dist/templates/configs/pulse.js.map +1 -0
  95. package/dist/templates/configs/queue.d.ts +2 -0
  96. package/dist/templates/configs/queue.d.ts.map +1 -0
  97. package/dist/templates/configs/queue.js +28 -0
  98. package/dist/templates/configs/queue.js.map +1 -0
  99. package/dist/templates/configs/sanctum.d.ts +2 -0
  100. package/dist/templates/configs/sanctum.d.ts.map +1 -0
  101. package/dist/templates/configs/sanctum.js +19 -0
  102. package/dist/templates/configs/sanctum.js.map +1 -0
  103. package/dist/templates/configs/server.d.ts +2 -0
  104. package/dist/templates/configs/server.d.ts.map +1 -0
  105. package/dist/templates/configs/server.js +15 -0
  106. package/dist/templates/configs/server.js.map +1 -0
  107. package/dist/templates/configs/session.d.ts +2 -0
  108. package/dist/templates/configs/session.d.ts.map +1 -0
  109. package/dist/templates/configs/session.js +26 -0
  110. package/dist/templates/configs/session.js.map +1 -0
  111. package/dist/templates/configs/socialite.d.ts +2 -0
  112. package/dist/templates/configs/socialite.d.ts.map +1 -0
  113. package/dist/templates/configs/socialite.js +27 -0
  114. package/dist/templates/configs/socialite.js.map +1 -0
  115. package/dist/templates/configs/storage.d.ts +2 -0
  116. package/dist/templates/configs/storage.d.ts.map +1 -0
  117. package/dist/templates/configs/storage.js +35 -0
  118. package/dist/templates/configs/storage.js.map +1 -0
  119. package/dist/templates/configs/sync.d.ts +3 -0
  120. package/dist/templates/configs/sync.d.ts.map +1 -0
  121. package/dist/templates/configs/sync.js +17 -0
  122. package/dist/templates/configs/sync.js.map +1 -0
  123. package/dist/templates/configs/telescope.d.ts +2 -0
  124. package/dist/templates/configs/telescope.d.ts.map +1 -0
  125. package/dist/templates/configs/telescope.js +25 -0
  126. package/dist/templates/configs/telescope.js.map +1 -0
  127. package/dist/templates/css/index.d.ts +3 -0
  128. package/dist/templates/css/index.d.ts.map +1 -0
  129. package/dist/templates/css/index.js +140 -0
  130. package/dist/templates/css/index.js.map +1 -0
  131. package/dist/templates/css/plain.d.ts +2 -0
  132. package/dist/templates/css/plain.d.ts.map +1 -0
  133. package/dist/templates/css/plain.js +373 -0
  134. package/dist/templates/css/plain.js.map +1 -0
  135. package/dist/templates/css/tailwind.d.ts +2 -0
  136. package/dist/templates/css/tailwind.d.ts.map +1 -0
  137. package/dist/templates/css/tailwind.js +176 -0
  138. package/dist/templates/css/tailwind.js.map +1 -0
  139. package/dist/templates/demos/bk-socket.d.ts +2 -0
  140. package/dist/templates/demos/bk-socket.d.ts.map +1 -0
  141. package/dist/templates/demos/bk-socket.js +95 -0
  142. package/dist/templates/demos/bk-socket.js.map +1 -0
  143. package/dist/templates/demos/contact.d.ts +3 -0
  144. package/dist/templates/demos/contact.d.ts.map +1 -0
  145. package/dist/templates/demos/contact.js +106 -0
  146. package/dist/templates/demos/contact.js.map +1 -0
  147. package/dist/templates/demos/index-view.d.ts +3 -0
  148. package/dist/templates/demos/index-view.d.ts.map +1 -0
  149. package/dist/templates/demos/index-view.js +67 -0
  150. package/dist/templates/demos/index-view.js.map +1 -0
  151. package/dist/templates/demos/live.d.ts +2 -0
  152. package/dist/templates/demos/live.d.ts.map +1 -0
  153. package/dist/templates/demos/live.js +97 -0
  154. package/dist/templates/demos/live.js.map +1 -0
  155. package/dist/templates/demos/registry.d.ts +13 -0
  156. package/dist/templates/demos/registry.d.ts.map +1 -0
  157. package/dist/templates/demos/registry.js +15 -0
  158. package/dist/templates/demos/registry.js.map +1 -0
  159. package/dist/templates/demos/ws.d.ts +2 -0
  160. package/dist/templates/demos/ws.d.ts.map +1 -0
  161. package/dist/templates/demos/ws.js +106 -0
  162. package/dist/templates/demos/ws.js.map +1 -0
  163. package/dist/templates/env.d.ts +7 -0
  164. package/dist/templates/env.d.ts.map +1 -0
  165. package/dist/templates/env.js +127 -0
  166. package/dist/templates/env.js.map +1 -0
  167. package/dist/templates/package-json.d.ts +3 -0
  168. package/dist/templates/package-json.d.ts.map +1 -0
  169. package/dist/templates/package-json.js +195 -0
  170. package/dist/templates/package-json.js.map +1 -0
  171. package/dist/templates/package-managers.d.ts +14 -0
  172. package/dist/templates/package-managers.d.ts.map +1 -0
  173. package/dist/templates/package-managers.js +49 -0
  174. package/dist/templates/package-managers.js.map +1 -0
  175. package/dist/templates/pages/ai-chat.d.ts +7 -0
  176. package/dist/templates/pages/ai-chat.d.ts.map +1 -0
  177. package/dist/templates/pages/ai-chat.js +285 -0
  178. package/dist/templates/pages/ai-chat.js.map +1 -0
  179. package/dist/templates/pages/demo.d.ts +4 -0
  180. package/dist/templates/pages/demo.d.ts.map +1 -0
  181. package/dist/templates/pages/demo.js +71 -0
  182. package/dist/templates/pages/demo.js.map +1 -0
  183. package/dist/templates/pages/error.d.ts +7 -0
  184. package/dist/templates/pages/error.d.ts.map +1 -0
  185. package/dist/templates/pages/error.js +148 -0
  186. package/dist/templates/pages/error.js.map +1 -0
  187. package/dist/templates/pages/index.d.ts +9 -0
  188. package/dist/templates/pages/index.d.ts.map +1 -0
  189. package/dist/templates/pages/index.js +311 -0
  190. package/dist/templates/pages/index.js.map +1 -0
  191. package/dist/templates/prisma/auth.d.ts +2 -0
  192. package/dist/templates/prisma/auth.d.ts.map +1 -0
  193. package/dist/templates/prisma/auth.js +22 -0
  194. package/dist/templates/prisma/auth.js.map +1 -0
  195. package/dist/templates/prisma/base.d.ts +3 -0
  196. package/dist/templates/prisma/base.d.ts.map +1 -0
  197. package/dist/templates/prisma/base.js +14 -0
  198. package/dist/templates/prisma/base.js.map +1 -0
  199. package/dist/templates/prisma/config.d.ts +3 -0
  200. package/dist/templates/prisma/config.d.ts.map +1 -0
  201. package/dist/templates/prisma/config.js +15 -0
  202. package/dist/templates/prisma/config.js.map +1 -0
  203. package/dist/templates/prisma/notification.d.ts +2 -0
  204. package/dist/templates/prisma/notification.d.ts.map +1 -0
  205. package/dist/templates/prisma/notification.js +16 -0
  206. package/dist/templates/prisma/notification.js.map +1 -0
  207. package/dist/templates/prisma/passport.d.ts +2 -0
  208. package/dist/templates/prisma/passport.d.ts.map +1 -0
  209. package/dist/templates/prisma/passport.js +69 -0
  210. package/dist/templates/prisma/passport.js.map +1 -0
  211. package/dist/templates/routes/api.d.ts +3 -0
  212. package/dist/templates/routes/api.d.ts.map +1 -0
  213. package/dist/templates/routes/api.js +118 -0
  214. package/dist/templates/routes/api.js.map +1 -0
  215. package/dist/templates/routes/console.d.ts +2 -0
  216. package/dist/templates/routes/console.d.ts.map +1 -0
  217. package/dist/templates/routes/console.js +22 -0
  218. package/dist/templates/routes/console.js.map +1 -0
  219. package/dist/templates/routes/web.d.ts +4 -0
  220. package/dist/templates/routes/web.d.ts.map +1 -0
  221. package/dist/templates/routes/web.js +108 -0
  222. package/dist/templates/routes/web.js.map +1 -0
  223. package/dist/templates/server.d.ts +2 -0
  224. package/dist/templates/server.d.ts.map +1 -0
  225. package/dist/templates/server.js +10 -0
  226. package/dist/templates/server.js.map +1 -0
  227. package/dist/templates/tsconfig.d.ts +3 -0
  228. package/dist/templates/tsconfig.d.ts.map +1 -0
  229. package/dist/templates/tsconfig.js +33 -0
  230. package/dist/templates/tsconfig.js.map +1 -0
  231. package/dist/templates/views/welcome.d.ts +6 -0
  232. package/dist/templates/views/welcome.d.ts.map +1 -0
  233. package/dist/templates/views/welcome.js +396 -0
  234. package/dist/templates/views/welcome.js.map +1 -0
  235. package/dist/templates/vite.d.ts +3 -0
  236. package/dist/templates/vite.d.ts.map +1 -0
  237. package/dist/templates/vite.js +61 -0
  238. package/dist/templates/vite.js.map +1 -0
  239. package/dist/templates.d.ts +28 -17
  240. package/dist/templates.d.ts.map +1 -1
  241. package/dist/templates.js +99 -3779
  242. package/dist/templates.js.map +1 -1
  243. package/package.json +3 -3
package/dist/templates.js CHANGED
@@ -1,51 +1,61 @@
1
- import { execSync } from 'node:child_process';
2
- /** Detect which package manager invoked the installer.
3
- * 1. Check npm_config_user_agent (set by pnpm/npm/yarn/bun create commands)
4
- * 2. Fall back to checking which binaries are available on PATH
5
- */
6
- export function detectPackageManager() {
7
- const ua = process.env['npm_config_user_agent'] ?? '';
8
- if (ua.startsWith('bun'))
9
- return 'bun';
10
- if (ua.startsWith('yarn'))
11
- return 'yarn';
12
- if (ua.startsWith('pnpm'))
13
- return 'pnpm';
14
- if (ua.startsWith('npm'))
15
- return 'npm';
16
- // Fallback: check which binaries exist on PATH (preference: pnpm > bun > yarn > npm)
17
- for (const pm of ['pnpm', 'bun', 'yarn']) {
18
- try {
19
- execSync(`${pm} --version`, { stdio: 'ignore' });
20
- return pm;
21
- }
22
- catch { /* not found */ }
23
- }
24
- return 'npm';
25
- }
26
- /** `<pm> exec <bin>` equivalent per package manager. */
27
- export function pmExec(pm, bin) {
28
- if (pm === 'bun')
29
- return `bunx ${bin}`;
30
- if (pm === 'yarn')
31
- return `yarn dlx ${bin}`;
32
- if (pm === 'npm')
33
- return `npx ${bin}`;
34
- return `pnpm exec ${bin}`;
35
- }
36
- /** `<pm> run <script>` equivalent (yarn/bun allow omitting "run"). */
37
- export function pmRun(pm, script) {
38
- if (pm === 'npm')
39
- return `npm run ${script}`;
40
- return `${pm} ${script}`;
41
- }
42
- /** `<pm> install` command. */
43
- export function pmInstall(pm) {
44
- return `${pm} install`;
45
- }
46
- function pageExt(fw) {
47
- return fw === 'vue' ? '.vue' : '.tsx';
48
- }
1
+ import { detectPackageManager, pmExec, pmInstall, pmRun, pageExt } from './templates/package-managers.js';
2
+ import { indexCss } from './templates/css/index.js';
3
+ import { prismaConfig } from './templates/prisma/config.js';
4
+ import { prismaBase } from './templates/prisma/base.js';
5
+ import { prismaAuth } from './templates/prisma/auth.js';
6
+ import { prismaNotification } from './templates/prisma/notification.js';
7
+ import { prismaPassport } from './templates/prisma/passport.js';
8
+ import { configApp } from './templates/configs/app.js';
9
+ import { configServer } from './templates/configs/server.js';
10
+ import { configLog } from './templates/configs/log.js';
11
+ import { configHash } from './templates/configs/hash.js';
12
+ import { configDatabase } from './templates/configs/database.js';
13
+ import { configQueue } from './templates/configs/queue.js';
14
+ import { configMail } from './templates/configs/mail.js';
15
+ import { configCache } from './templates/configs/cache.js';
16
+ import { configStorage } from './templates/configs/storage.js';
17
+ import { configAuth } from './templates/configs/auth.js';
18
+ import { configIndex } from './templates/configs/index.js';
19
+ import { configSession } from './templates/configs/session.js';
20
+ import { configAi } from './templates/configs/ai.js';
21
+ import { configSync } from './templates/configs/sync.js';
22
+ import { configPassport } from './templates/configs/passport.js';
23
+ import { configLocalization } from './templates/configs/localization.js';
24
+ import { configTelescope } from './templates/configs/telescope.js';
25
+ import { configSanctum } from './templates/configs/sanctum.js';
26
+ import { configSocialite } from './templates/configs/socialite.js';
27
+ import { configPulse } from './templates/configs/pulse.js';
28
+ import { configHorizon } from './templates/configs/horizon.js';
29
+ import { configCrypt } from './templates/configs/crypt.js';
30
+ import { configCashier } from './templates/configs/cashier.js';
31
+ import { configPennant } from './templates/configs/pennant.js';
32
+ import { dotenv, dotenvExample, envDts, gitignore, pnpmWorkspace } from './templates/env.js';
33
+ import { serverTs } from './templates/server.js';
34
+ import { bootstrapApp } from './templates/bootstrap/app.js';
35
+ import { bootstrapProviders } from './templates/bootstrap/providers.js';
36
+ import { userModel } from './templates/app/user-model.js';
37
+ import { appServiceProvider } from './templates/app/service-provider.js';
38
+ import { mcpEchoServer } from './templates/app/mcp-echo-server.js';
39
+ import { mcpEchoTool } from './templates/app/mcp-echo-tool.js';
40
+ import { authController } from './templates/app/auth-controller.js';
41
+ import { routesApi } from './templates/routes/api.js';
42
+ import { routesWeb, welcomeExt } from './templates/routes/web.js';
43
+ import { routesConsole } from './templates/routes/console.js';
44
+ import { pagesRootConfig, pagesIndexConfig, pagesIndexData, pagesIndexPage } from './templates/pages/index.js';
45
+ import { welcomeView } from './templates/views/welcome.js';
46
+ import { pagesErrorConfig, pagesErrorPage } from './templates/pages/error.js';
47
+ import { aiChatPageConfig, aiChatPage } from './templates/pages/ai-chat.js';
48
+ import { demoPageConfig, demoPage } from './templates/pages/demo.js';
49
+ import { demosIndexView } from './templates/demos/index-view.js';
50
+ import { demosContactView } from './templates/demos/contact.js';
51
+ import { demosWsView } from './templates/demos/ws.js';
52
+ import { demosLiveView } from './templates/demos/live.js';
53
+ import { bkSocketSource } from './templates/demos/bk-socket.js';
54
+ import { availableDemos } from './templates/demos/registry.js';
55
+ import { packageJson } from './templates/package-json.js';
56
+ import { tsconfigJson } from './templates/tsconfig.js';
57
+ import { viteConfig } from './templates/vite.js';
58
+ export { detectPackageManager, pmExec, pmInstall, pmRun };
49
59
  export function getTemplates(ctx) {
50
60
  const files = {};
51
61
  files['package.json'] = packageJson(ctx);
@@ -73,25 +83,22 @@ export function getTemplates(ctx) {
73
83
  files['src/index.css'] = indexCss(ctx);
74
84
  files['bootstrap/app.ts'] = bootstrapApp(ctx);
75
85
  files['bootstrap/providers.ts'] = bootstrapProviders(ctx);
76
- // Config files — always generated
86
+ // Config files — always generated (Tier A silent install + framework defaults)
77
87
  files['config/app.ts'] = configApp();
78
88
  files['config/server.ts'] = configServer();
79
89
  files['config/log.ts'] = configLog();
90
+ files['config/session.ts'] = configSession();
91
+ files['config/hash.ts'] = configHash();
92
+ files['config/cache.ts'] = configCache();
80
93
  // Config files — conditional on selected packages
81
94
  if (ctx.orm)
82
95
  files['config/database.ts'] = configDatabase(ctx);
83
96
  if (ctx.packages.auth)
84
97
  files['config/auth.ts'] = configAuth(ctx);
85
- if (ctx.packages.auth)
86
- files['config/session.ts'] = configSession();
87
- if (ctx.packages.auth)
88
- files['config/hash.ts'] = configHash();
89
98
  if (ctx.packages.queue)
90
99
  files['config/queue.ts'] = configQueue();
91
100
  if (ctx.packages.mail)
92
101
  files['config/mail.ts'] = configMail();
93
- if (ctx.packages.cache)
94
- files['config/cache.ts'] = configCache();
95
102
  if (ctx.packages.storage)
96
103
  files['config/storage.ts'] = configStorage();
97
104
  if (ctx.packages.ai)
@@ -100,10 +107,24 @@ export function getTemplates(ctx) {
100
107
  files['config/sync.ts'] = configSync(ctx);
101
108
  if (ctx.packages.passport)
102
109
  files['config/passport.ts'] = configPassport();
110
+ if (ctx.packages.sanctum)
111
+ files['config/sanctum.ts'] = configSanctum();
112
+ if (ctx.packages.socialite)
113
+ files['config/socialite.ts'] = configSocialite();
103
114
  if (ctx.packages.localization)
104
115
  files['config/localization.ts'] = configLocalization();
116
+ if (ctx.packages.cashierPaddle)
117
+ files['config/cashier.ts'] = configCashier();
118
+ if (ctx.packages.pennant)
119
+ files['config/pennant.ts'] = configPennant();
105
120
  if (ctx.packages.telescope)
106
121
  files['config/telescope.ts'] = configTelescope();
122
+ if (ctx.packages.pulse)
123
+ files['config/pulse.ts'] = configPulse();
124
+ if (ctx.packages.horizon)
125
+ files['config/horizon.ts'] = configHorizon();
126
+ if (ctx.packages.crypt)
127
+ files['config/crypt.ts'] = configCrypt();
107
128
  files['config/index.ts'] = configIndex(ctx);
108
129
  files['env.d.ts'] = envDts();
109
130
  if (ctx.packages.auth && ctx.orm)
@@ -146,3740 +167,39 @@ export function getTemplates(ctx) {
146
167
  files[`pages/${fw}-demo/+config.ts`] = demoPageConfig(fw);
147
168
  files[`pages/${fw}-demo/+Page${dext}`] = demoPage(fw, ctx);
148
169
  }
149
- // Demos — react primary only; ws/live require their respective packages.
150
- if (shouldScaffoldDemos(ctx)) {
170
+ // Demos — react primary only; per-demo opt-in via ctx.demos (see registry.ts).
171
+ if (shouldScaffoldAnyDemo(ctx)) {
151
172
  files['app/Views/Demos/Index.tsx'] = demosIndexView(ctx);
152
- files['app/Views/Demos/Contact.tsx'] = demosContactView(ctx);
153
- if (ctx.packages.broadcast) {
173
+ if (shouldScaffoldDemo(ctx, 'contact'))
174
+ files['app/Views/Demos/Contact.tsx'] = demosContactView(ctx);
175
+ if (shouldScaffoldDemo(ctx, 'ws')) {
154
176
  files['app/Views/Demos/Ws.tsx'] = demosWsView();
155
177
  files['src/BKSocket.ts'] = bkSocketSource();
156
178
  }
157
- if (ctx.packages.sync) {
179
+ if (shouldScaffoldDemo(ctx, 'live')) {
158
180
  files['app/Views/Demos/Live.tsx'] = demosLiveView();
159
181
  }
160
182
  }
161
183
  return files;
162
184
  }
163
- /** Demos are React-primary only for v1 — vue/solid variants aren't written yet. */
164
- function shouldScaffoldDemos(ctx) {
165
- return ctx.packages.demos && ctx.primary === 'react';
166
- }
167
- // ─── package.json ──────────────────────────────────────────
168
- function packageJson(ctx) {
169
- const { frameworks, tailwind, shadcn, db } = ctx;
170
- const hasReact = frameworks.includes('react');
171
- const hasVue = frameworks.includes('vue');
172
- const hasSolid = frameworks.includes('solid');
173
- const dbDeps = {
174
- sqlite: { 'better-sqlite3': '^12.0.0' },
175
- postgresql: {},
176
- mysql: {},
177
- };
178
- const dbDevDeps = {
179
- sqlite: { '@types/better-sqlite3': '^7.6.0' },
180
- postgresql: {},
181
- mysql: {},
182
- };
183
- const frameworkDeps = {};
184
- const frameworkDevDeps = {};
185
- if (hasReact) {
186
- frameworkDeps['react'] = '^19.0.0';
187
- frameworkDeps['react-dom'] = '^19.0.0';
188
- frameworkDeps['vike-react'] = '^0.6.20';
189
- frameworkDevDeps['@vitejs/plugin-react'] = '^4.3.4';
190
- frameworkDevDeps['@types/react'] = '^19.0.0';
191
- frameworkDevDeps['@types/react-dom'] = '^19.0.0';
192
- }
193
- if (hasVue) {
194
- frameworkDeps['vue'] = '^3.5.0';
195
- frameworkDeps['vike-vue'] = 'latest';
196
- frameworkDevDeps['@vitejs/plugin-vue'] = '^5.2.0';
197
- }
198
- if (hasSolid) {
199
- frameworkDeps['solid-js'] = '^1.9.0';
200
- frameworkDeps['vike-solid'] = 'latest';
201
- }
202
- const tailwindDeps = tailwind ? {
203
- 'tailwindcss': '^4.2.1',
204
- '@tailwindcss/vite': '^4.2.1',
205
- } : {};
206
- const tailwindDevDeps = tailwind ? {
207
- 'tw-animate-css': '^1.4.0',
208
- } : {};
209
- const shadcnDeps = shadcn ? {
210
- 'class-variance-authority': '^0.7.1',
211
- 'clsx': '^2.1.1',
212
- 'tailwind-merge': '^3.5.0',
213
- 'lucide-react': '^0.575.0',
214
- } : {};
215
- const shadcnDevDeps = shadcn ? {
216
- 'shadcn': 'latest',
217
- } : {};
218
- // Base framework deps (always included)
219
- const deps = {
220
- '@rudderjs/console': 'latest',
221
- '@rudderjs/vite': 'latest',
222
- '@rudderjs/contracts': 'latest',
223
- '@rudderjs/core': 'latest',
224
- '@rudderjs/log': 'latest',
225
- '@rudderjs/middleware': 'latest',
226
- '@rudderjs/router': 'latest',
227
- '@rudderjs/server-hono': 'latest',
228
- '@rudderjs/support': 'latest',
229
- '@rudderjs/view': 'latest',
230
- '@vikejs/hono': '^0.2.0',
231
- 'dotenv': '^16.4.0',
232
- 'reflect-metadata': '^0.2.2',
233
- 'vike': '^0.4.257',
234
- 'zod': '^4.0.0',
235
- ...frameworkDeps,
236
- ...tailwindDeps,
237
- ...shadcnDeps,
238
- ...dbDeps[db],
239
- };
240
- // ORM deps
241
- if (ctx.orm === 'prisma') {
242
- deps['@rudderjs/orm'] = 'latest';
243
- deps['@rudderjs/orm-prisma'] = 'latest';
244
- deps['@prisma/client'] = '^7.0.0';
245
- }
246
- else if (ctx.orm === 'drizzle') {
247
- deps['@rudderjs/orm'] = 'latest';
248
- deps['@rudderjs/orm-drizzle'] = 'latest';
249
- }
250
- // Optional package deps
251
- if (ctx.packages.auth) {
252
- deps['@rudderjs/auth'] = 'latest';
253
- deps['@rudderjs/session'] = 'latest';
254
- deps['@rudderjs/hash'] = 'latest';
255
- }
256
- if (ctx.packages.cache)
257
- deps['@rudderjs/cache'] = 'latest';
258
- if (ctx.packages.queue)
259
- deps['@rudderjs/queue'] = 'latest';
260
- if (ctx.packages.storage)
261
- deps['@rudderjs/storage'] = 'latest';
262
- if (ctx.packages.mail)
263
- deps['@rudderjs/mail'] = 'latest';
264
- if (ctx.packages.notifications)
265
- deps['@rudderjs/notification'] = 'latest';
266
- if (ctx.packages.scheduler)
267
- deps['@rudderjs/schedule'] = 'latest';
268
- if (ctx.packages.broadcast)
269
- deps['@rudderjs/broadcast'] = 'latest';
270
- if (ctx.packages.sync)
271
- deps['@rudderjs/sync'] = 'latest';
272
- if (shouldScaffoldDemos(ctx) && ctx.packages.sync)
273
- deps['y-websocket'] = '^2.0.0';
274
- if (ctx.packages.ai)
275
- deps['@rudderjs/ai'] = 'latest';
276
- if (ctx.packages.mcp)
277
- deps['@rudderjs/mcp'] = 'latest';
278
- if (ctx.packages.passport)
279
- deps['@rudderjs/passport'] = 'latest';
280
- if (ctx.packages.localization)
281
- deps['@rudderjs/localization'] = 'latest';
282
- if (ctx.packages.telescope)
283
- deps['@rudderjs/telescope'] = 'latest';
284
- const devDeps = {
285
- '@rudderjs/cli': 'latest',
286
- '@types/node': '^20.0.0',
287
- 'tsx': '^4.21.0',
288
- 'typescript': '^5.4.0',
289
- 'vite': '^7.1.0',
290
- ...frameworkDevDeps,
291
- ...tailwindDevDeps,
292
- ...shadcnDevDeps,
293
- ...dbDevDeps[db],
294
- };
295
- if (ctx.orm === 'prisma')
296
- devDeps['prisma'] = '^7.0.0';
297
- if (ctx.packages.boost)
298
- devDeps['@rudderjs/boost'] = 'latest';
299
- const builtDeps = ['esbuild'];
300
- if (ctx.orm === 'prisma') {
301
- builtDeps.push('@prisma/engines', 'prisma');
302
- }
303
- if (db === 'sqlite')
304
- builtDeps.unshift('better-sqlite3');
305
- const pmField = {};
306
- if (ctx.pm === 'pnpm') {
307
- pmField['pnpm'] = { onlyBuiltDependencies: builtDeps };
308
- }
309
- else if (ctx.pm === 'bun') {
310
- pmField['trustedDependencies'] = builtDeps;
311
- }
312
- // npm and yarn allow all lifecycle scripts by default — no extra field needed
313
- return JSON.stringify({
314
- name: ctx.name,
315
- version: '0.0.1',
316
- private: true,
317
- type: 'module',
318
- scripts: {
319
- dev: 'vike dev',
320
- 'dev:clean': 'pids=$(lsof -ti :24678 -ti :3000 2>/dev/null); if [ -n "$pids" ]; then kill -9 $pids; fi; vike dev',
321
- build: 'vike build',
322
- start: 'node ./dist/server/index.mjs',
323
- preview: 'node ./dist/server/index.mjs',
324
- typecheck: 'tsc --noEmit',
325
- rudder: 'tsx node_modules/@rudderjs/cli/dist/index.js',
326
- ...(ctx.orm ? {
327
- migrate: 'tsx node_modules/@rudderjs/cli/dist/index.js migrate',
328
- 'migrate:fresh': 'tsx node_modules/@rudderjs/cli/dist/index.js migrate:fresh',
329
- 'migrate:status': 'tsx node_modules/@rudderjs/cli/dist/index.js migrate:status',
330
- 'db:seed': 'tsx node_modules/@rudderjs/cli/dist/index.js db:seed',
331
- } : {}),
332
- },
333
- ...pmField,
334
- dependencies: deps,
335
- devDependencies: devDeps,
336
- }, null, 2) + '\n';
337
- }
338
- // ─── tsconfig.json ─────────────────────────────────────────
339
- function tsconfigJson(ctx) {
340
- const hasReact = ctx.frameworks.includes('react');
341
- const hasSolid = ctx.frameworks.includes('solid');
342
- const compilerOptions = {
343
- target: 'ES2022',
344
- module: 'ESNext',
345
- moduleResolution: 'bundler',
346
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
347
- strict: true,
348
- exactOptionalPropertyTypes: true,
349
- noUncheckedIndexedAccess: true,
350
- experimentalDecorators: true,
351
- emitDecoratorMetadata: true,
352
- skipLibCheck: true,
353
- noEmit: true,
354
- baseUrl: '.',
355
- paths: { '@/*': ['./src/*'], 'App/*': ['./app/*'] },
356
- allowImportingTsExtensions: true,
357
- };
358
- if (hasReact) {
359
- compilerOptions['jsx'] = 'react-jsx';
360
- }
361
- else if (hasSolid) {
362
- compilerOptions['jsx'] = 'preserve';
363
- compilerOptions['jsxImportSource'] = 'solid-js';
364
- }
365
- // Vue only — no jsx field needed
366
- return JSON.stringify({
367
- compilerOptions,
368
- include: ['src/**/*', 'pages/**/*', 'app/**/*', 'bootstrap/**/*', 'routes/**/*', 'config/**/*', '*.ts', '*.tsx'],
369
- }, null, 2) + '\n';
370
- }
371
- // ─── vite.config.ts ────────────────────────────────────────
372
- function viteConfig(ctx) {
373
- const { frameworks, primary, tailwind } = ctx;
374
- const hasReact = frameworks.includes('react');
375
- const hasVue = frameworks.includes('vue');
376
- const hasSolid = frameworks.includes('solid');
377
- const hasReactSolidConflict = hasReact && hasSolid;
378
- const imports = [
379
- `import { defineConfig } from 'vite'`,
380
- `import rudderjs from '@rudderjs/vite'`,
381
- ];
382
- if (tailwind)
383
- imports.push(`import tailwindcss from '@tailwindcss/vite'`);
384
- if (hasReact)
385
- imports.push(`import react from '@vitejs/plugin-react'`);
386
- if (hasVue)
387
- imports.push(`import vue from '@vitejs/plugin-vue'`);
388
- if (hasSolid)
389
- imports.push(`import solid from 'vike-solid/vite'`);
390
- const plugins = ['rudderjs()'];
391
- if (tailwind)
392
- plugins.push('tailwindcss()');
393
- if (hasReact) {
394
- if (hasReactSolidConflict) {
395
- if (primary === 'react') {
396
- plugins.push(`react({ exclude: ['**/pages/solid-demo/**'] })`);
397
- }
398
- else {
399
- plugins.push(`react({ include: ['**/pages/react-demo/**'] })`);
400
- }
401
- }
402
- else {
403
- plugins.push('react()');
404
- }
405
- }
406
- if (hasVue) {
407
- plugins.push('vue()');
408
- }
409
- if (hasSolid) {
410
- if (hasReactSolidConflict) {
411
- if (primary === 'solid') {
412
- plugins.push(`solid({ exclude: ['**/pages/react-demo/**'] })`);
413
- }
414
- else {
415
- plugins.push(`solid({ include: ['**/pages/solid-demo/**'] })`);
416
- }
417
- }
418
- else {
419
- plugins.push('solid()');
420
- }
421
- }
422
- const pluginsStr = plugins.map(p => ` ${p},`).join('\n');
423
- return `${imports.join('\n')}
424
-
425
- export default defineConfig({
426
- plugins: [
427
- ${pluginsStr}
428
- ],
429
- })
430
- `;
431
- }
432
- // ─── +server.ts ───────────────────────────────────────────
433
- function serverTs() {
434
- return `import type { Server } from 'vike/types'
435
- import app from './bootstrap/app.js'
436
-
437
- export default {
438
- fetch: app.fetch,
439
- } satisfies Server
440
- `;
441
- }
442
- // ─── prisma.config.ts ──────────────────────────────────────
443
- function prismaConfig(ctx) {
444
- const dbUrl = ctx.db === 'sqlite'
445
- ? "process.env['DATABASE_URL'] ?? 'file:./dev.db'"
446
- : "process.env['DATABASE_URL']!";
447
- return `import { defineConfig } from 'prisma/config'
448
-
449
- export default defineConfig({
450
- schema: 'prisma/schema',
451
- datasource: {
452
- url: ${dbUrl},
453
- },
454
- })
455
- `;
456
- }
457
- // ─── .env ──────────────────────────────────────────────────
458
- function dotenv(ctx) {
459
- const lines = [
460
- `APP_NAME=${ctx.name}`,
461
- 'APP_ENV=development',
462
- 'APP_DEBUG=true',
463
- 'APP_URL=http://localhost:3000',
464
- '',
465
- 'PORT=3000',
466
- ];
467
- if (ctx.orm) {
468
- lines.push('');
469
- if (ctx.db === 'sqlite')
470
- lines.push('DATABASE_URL="file:./dev.db"');
471
- else if (ctx.db === 'postgresql')
472
- lines.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"');
473
- else
474
- lines.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"');
475
- }
476
- if (ctx.packages.auth) {
477
- lines.push('');
478
- lines.push(`AUTH_SECRET=${ctx.authSecret}`);
479
- }
480
- if (ctx.packages.ai) {
481
- lines.push('');
482
- lines.push('AI_MODEL=anthropic/claude-sonnet-4-5');
483
- lines.push('ANTHROPIC_API_KEY=');
484
- lines.push('# OPENAI_API_KEY=');
485
- lines.push('# GOOGLE_AI_API_KEY=');
486
- lines.push('# OLLAMA_BASE_URL=http://localhost:11434');
487
- }
488
- return lines.join('\n') + '\n';
489
- }
490
- // ─── .env.example ──────────────────────────────────────────
491
- function dotenvExample(ctx) {
492
- const lines = [
493
- `APP_NAME=${ctx.name}`,
494
- 'APP_ENV=development',
495
- 'APP_DEBUG=false',
496
- 'APP_URL=http://localhost:3000',
497
- '',
498
- 'PORT=3000',
499
- ];
500
- if (ctx.orm) {
501
- lines.push('');
502
- if (ctx.db === 'sqlite')
503
- lines.push('DATABASE_URL="file:./dev.db"');
504
- else if (ctx.db === 'postgresql')
505
- lines.push('DATABASE_URL="postgresql://user:password@localhost:5432/mydb"');
506
- else
507
- lines.push('DATABASE_URL="mysql://user:password@localhost:3306/mydb"');
508
- }
509
- if (ctx.packages.auth) {
510
- lines.push('');
511
- lines.push('AUTH_SECRET=please-set-a-real-32-char-secret-here');
512
- }
513
- if (ctx.packages.ai) {
514
- lines.push('');
515
- lines.push('AI_MODEL=anthropic/claude-sonnet-4-5');
516
- lines.push('ANTHROPIC_API_KEY=');
517
- lines.push('# OPENAI_API_KEY=');
518
- lines.push('# GOOGLE_AI_API_KEY=');
519
- lines.push('# OLLAMA_BASE_URL=http://localhost:11434');
520
- }
521
- return lines.join('\n') + '\n';
522
- }
523
- // ─── .gitignore ────────────────────────────────────────────
524
- function gitignore() {
525
- return `node_modules/
526
- dist/
527
- .env
528
- *.db
529
- *.db-journal
530
- prisma/generated/
531
- bootstrap/cache/
532
- `;
533
- }
534
- function pnpmWorkspace() {
535
- return `# Standalone project — prevents pnpm from merging with a parent workspace\npackages: []\n`;
536
- }
537
- // ─── prisma/schema/*.prisma ─────────────────────────────────
538
- function prismaBase(ctx) {
539
- const provider = ctx.db === 'sqlite' ? 'sqlite'
540
- : ctx.db === 'postgresql' ? 'postgresql'
541
- : 'mysql';
542
- return `generator client {
543
- provider = "prisma-client-js"
544
- }
545
-
546
- datasource db {
547
- provider = "${provider}"
548
- }
549
- `;
550
- }
551
- function prismaAuth() {
552
- return `model User {
553
- id String @id @default(cuid())
554
- name String
555
- email String @unique
556
- password String?
557
- emailVerified Boolean @default(false)
558
- image String?
559
- role String @default("user")
560
- rememberToken String?
561
- createdAt DateTime @default(now())
562
- updatedAt DateTime @updatedAt
563
- }
564
-
565
- model PasswordResetToken {
566
- email String @id
567
- token String
568
- createdAt DateTime @default(now())
569
- }
570
- `;
571
- }
572
- function prismaNotification() {
573
- return `model Notification {
574
- id String @id @default(cuid())
575
- notifiable_id String
576
- notifiable_type String
577
- type String
578
- data String
579
- read_at String?
580
- created_at String
581
- updated_at String
582
-
583
- @@index([notifiable_type, notifiable_id])
584
- }
585
- `;
586
- }
587
- function prismaPassport() {
588
- return `model OAuthClient {
589
- id String @id @default(cuid())
590
- name String
591
- secret String?
592
- redirectUris String @default("[]")
593
- grantTypes String @default("[\\"authorization_code\\"]")
594
- scopes String @default("[]")
595
- confidential Boolean @default(true)
596
- revoked Boolean @default(false)
597
- createdAt DateTime @default(now())
598
- updatedAt DateTime @updatedAt
599
-
600
- @@map("oauth_clients")
601
- }
602
-
603
- model OAuthAccessToken {
604
- id String @id @default(cuid())
605
- userId String?
606
- clientId String
607
- name String?
608
- scopes String @default("[]")
609
- revoked Boolean @default(false)
610
- expiresAt DateTime
611
- createdAt DateTime @default(now())
612
-
613
- @@index([userId])
614
- @@map("oauth_access_tokens")
615
- }
616
-
617
- model OAuthRefreshToken {
618
- id String @id @default(cuid())
619
- accessTokenId String @unique
620
- revoked Boolean @default(false)
621
- expiresAt DateTime
622
-
623
- @@map("oauth_refresh_tokens")
624
- }
625
-
626
- model OAuthAuthCode {
627
- id String @id @default(cuid())
628
- userId String
629
- clientId String
630
- scopes String @default("[]")
631
- revoked Boolean @default(false)
632
- expiresAt DateTime
633
- codeChallenge String?
634
- codeChallengeMethod String?
635
-
636
- @@map("oauth_auth_codes")
637
- }
638
-
639
- model OAuthDeviceCode {
640
- id String @id @default(cuid())
641
- clientId String
642
- userCode String @unique
643
- deviceCode String @unique
644
- scopes String @default("[]")
645
- userId String?
646
- approved Boolean?
647
- expiresAt DateTime
648
- lastPolledAt DateTime?
649
- createdAt DateTime @default(now())
650
-
651
- @@map("oauth_device_codes")
652
- }
653
- `;
654
- }
655
- // ─── src/index.css ─────────────────────────────────────────
656
- function indexCss(ctx) {
657
- if (!ctx.tailwind) {
658
- return indexCssPlain();
659
- }
660
- if (!ctx.shadcn) {
661
- return `@import "tailwindcss";
662
- @import "tw-animate-css";
663
-
664
- ${semanticRulesApply()}`;
665
- }
666
- return `@import "tailwindcss";
667
- @import "tw-animate-css";
668
- @import "shadcn/tailwind.css";
669
-
670
- @custom-variant dark (&:is(.dark *));
671
-
672
- @theme inline {
673
- --radius-sm: calc(var(--radius) - 4px);
674
- --radius-md: calc(var(--radius) - 2px);
675
- --radius-lg: var(--radius);
676
- --radius-xl: calc(var(--radius) + 4px);
677
- --radius-2xl: calc(var(--radius) + 8px);
678
- --radius-3xl: calc(var(--radius) + 12px);
679
- --radius-4xl: calc(var(--radius) + 16px);
680
- --color-background: var(--background);
681
- --color-foreground: var(--foreground);
682
- --color-card: var(--card);
683
- --color-card-foreground: var(--card-foreground);
684
- --color-popover: var(--popover);
685
- --color-popover-foreground: var(--popover-foreground);
686
- --color-primary: var(--primary);
687
- --color-primary-foreground: var(--primary-foreground);
688
- --color-secondary: var(--secondary);
689
- --color-secondary-foreground: var(--secondary-foreground);
690
- --color-muted: var(--muted);
691
- --color-muted-foreground: var(--muted-foreground);
692
- --color-accent: var(--accent);
693
- --color-accent-foreground: var(--accent-foreground);
694
- --color-destructive: var(--destructive);
695
- --color-border: var(--border);
696
- --color-input: var(--input);
697
- --color-ring: var(--ring);
698
- --color-chart-1: var(--chart-1);
699
- --color-chart-2: var(--chart-2);
700
- --color-chart-3: var(--chart-3);
701
- --color-chart-4: var(--chart-4);
702
- --color-chart-5: var(--chart-5);
703
- --color-sidebar: var(--sidebar);
704
- --color-sidebar-foreground: var(--sidebar-foreground);
705
- --color-sidebar-primary: var(--sidebar-primary);
706
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
707
- --color-sidebar-accent: var(--sidebar-accent);
708
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
709
- --color-sidebar-border: var(--sidebar-border);
710
- --color-sidebar-ring: var(--sidebar-ring);
711
- }
712
-
713
- :root {
714
- --radius: 0.625rem;
715
- --background: oklch(1 0 0);
716
- --foreground: oklch(0.145 0 0);
717
- --card: oklch(1 0 0);
718
- --card-foreground: oklch(0.145 0 0);
719
- --popover: oklch(1 0 0);
720
- --popover-foreground: oklch(0.145 0 0);
721
- --primary: oklch(0.205 0 0);
722
- --primary-foreground: oklch(0.985 0 0);
723
- --secondary: oklch(0.97 0 0);
724
- --secondary-foreground: oklch(0.205 0 0);
725
- --muted: oklch(0.97 0 0);
726
- --muted-foreground: oklch(0.556 0 0);
727
- --accent: oklch(0.97 0 0);
728
- --accent-foreground: oklch(0.205 0 0);
729
- --destructive: oklch(0.577 0.245 27.325);
730
- --border: oklch(0.922 0 0);
731
- --input: oklch(0.922 0 0);
732
- --ring: oklch(0.708 0 0);
733
- --chart-1: oklch(0.646 0.222 41.116);
734
- --chart-2: oklch(0.6 0.118 184.704);
735
- --chart-3: oklch(0.398 0.07 227.392);
736
- --chart-4: oklch(0.828 0.189 84.429);
737
- --chart-5: oklch(0.769 0.188 70.08);
738
- --sidebar: oklch(0.985 0 0);
739
- --sidebar-foreground: oklch(0.145 0 0);
740
- --sidebar-primary: oklch(0.205 0 0);
741
- --sidebar-primary-foreground: oklch(0.985 0 0);
742
- --sidebar-accent: oklch(0.97 0 0);
743
- --sidebar-accent-foreground: oklch(0.205 0 0);
744
- --sidebar-border: oklch(0.922 0 0);
745
- --sidebar-ring: oklch(0.708 0 0);
746
- }
747
-
748
- .dark {
749
- --background: oklch(0.145 0 0);
750
- --foreground: oklch(0.985 0 0);
751
- --card: oklch(0.205 0 0);
752
- --card-foreground: oklch(0.985 0 0);
753
- --popover: oklch(0.205 0 0);
754
- --popover-foreground: oklch(0.985 0 0);
755
- --primary: oklch(0.922 0 0);
756
- --primary-foreground: oklch(0.205 0 0);
757
- --secondary: oklch(0.269 0 0);
758
- --secondary-foreground: oklch(0.985 0 0);
759
- --muted: oklch(0.269 0 0);
760
- --muted-foreground: oklch(0.708 0 0);
761
- --accent: oklch(0.269 0 0);
762
- --accent-foreground: oklch(0.985 0 0);
763
- --destructive: oklch(0.704 0.191 22.216);
764
- --border: oklch(1 0 0 / 10%);
765
- --input: oklch(1 0 0 / 15%);
766
- --ring: oklch(0.556 0 0);
767
- --chart-1: oklch(0.488 0.243 264.376);
768
- --chart-2: oklch(0.696 0.17 162.48);
769
- --chart-3: oklch(0.769 0.188 70.08);
770
- --chart-4: oklch(0.627 0.265 303.9);
771
- --chart-5: oklch(0.645 0.246 16.439);
772
- --sidebar: oklch(0.205 0 0);
773
- --sidebar-foreground: oklch(0.985 0 0);
774
- --sidebar-primary: oklch(0.488 0.243 264.376);
775
- --sidebar-primary-foreground: oklch(0.985 0 0);
776
- --sidebar-accent: oklch(0.269 0 0);
777
- --sidebar-accent-foreground: oklch(0.985 0 0);
778
- --sidebar-border: oklch(1 0 0 / 10%);
779
- --sidebar-ring: oklch(0.556 0 0);
780
- }
781
-
782
- @layer base {
783
- * {
784
- @apply border-border outline-ring/50;
785
- }
786
- body {
787
- @apply bg-background text-foreground;
788
- }
789
- }
790
-
791
- ${semanticRulesApply()}`;
792
- }
793
- function semanticRulesApply() {
794
- return `/* ─── Scaffolded view classes ────────────────────────────────
795
- Semantic classes shared by app/Views/Welcome and vendored
796
- @rudderjs/auth views. The --no-tailwind variant of this
797
- scaffolder emits equivalent hand-authored CSS under the
798
- same selectors. */
799
-
800
- .page {
801
- @apply min-h-svh bg-gradient-to-b from-white to-zinc-50 text-zinc-900 dark:from-zinc-950 dark:to-black dark:text-zinc-100;
802
- }
803
- .page-nav {
804
- @apply mx-auto flex max-w-6xl items-center justify-between px-6 py-5;
805
- }
806
- .page-footer {
807
- @apply border-t border-zinc-200 dark:border-zinc-900;
808
- }
809
- .footer-inner {
810
- @apply mx-auto flex max-w-6xl flex-col items-center gap-3 px-6 py-6 text-xs text-zinc-500 sm:flex-row sm:justify-between;
811
- }
812
- .footer-links {
813
- @apply flex gap-4;
814
- }
815
- .footer-link {
816
- @apply transition-colors hover:text-zinc-900 dark:hover:text-zinc-100;
817
- }
818
-
819
- .brand {
820
- @apply flex items-center gap-2 text-sm font-semibold tracking-tight;
821
- }
822
- .brand-dot {
823
- @apply inline-block h-2 w-2 rounded-full bg-emerald-500;
824
- }
825
- .nav-right {
826
- @apply flex items-center gap-4 text-sm;
827
- }
828
- .nav-badge {
829
- @apply text-zinc-500 dark:text-zinc-400;
830
- }
831
- .nav-badge strong {
832
- @apply font-medium text-zinc-900 dark:text-zinc-100;
833
- }
834
- .nav-button {
835
- @apply rounded-md border border-zinc-200 px-3 py-1.5 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-900;
836
- }
837
- .nav-link {
838
- @apply text-zinc-500 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100;
839
- }
840
-
841
- .hero {
842
- @apply mx-auto max-w-3xl px-6 pb-12 pt-20 text-center;
843
- }
844
- .hero-title {
845
- @apply text-5xl font-bold tracking-tight sm:text-6xl;
846
- }
847
- .hero-lead {
848
- @apply mt-6 text-lg text-zinc-600 dark:text-zinc-400;
849
- }
850
- .hero-meta {
851
- @apply mt-8 flex items-center justify-center gap-3 text-xs text-zinc-500;
852
- }
853
- .inline-code {
854
- @apply rounded bg-zinc-100 px-1.5 py-0.5 text-sm dark:bg-zinc-900;
855
- }
856
-
857
- .feature-section {
858
- @apply mx-auto max-w-6xl px-6 pb-20;
859
- }
860
- .feature-grid {
861
- @apply grid gap-4 md:grid-cols-2 lg:grid-cols-3;
862
- }
863
- .feature-card {
864
- @apply rounded-xl border border-zinc-200 bg-white p-6 transition-colors hover:border-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:border-zinc-100;
865
- }
866
- .feature-title {
867
- @apply font-semibold;
868
- }
869
- .feature-desc {
870
- @apply mt-2 text-sm text-zinc-600 dark:text-zinc-400;
871
- }
872
- .feature-card:hover .feature-desc {
873
- @apply text-zinc-900 dark:text-zinc-100;
874
- }
875
-
876
- /* Auth forms + error page (reused selectors) */
877
- .auth-wrap {
878
- @apply flex min-h-svh items-center justify-center p-4;
879
- }
880
- .auth-card {
881
- @apply w-full max-w-sm space-y-6;
882
- }
883
- .auth-head {
884
- @apply text-center;
885
- }
886
- .heading-lg {
887
- @apply text-2xl font-bold;
888
- }
889
- .muted {
890
- @apply text-sm text-zinc-500 dark:text-zinc-400;
891
- }
892
- .form-card {
893
- @apply space-y-4 rounded-lg border border-zinc-200 p-6 shadow-sm dark:border-zinc-800;
894
- }
895
- .form-error {
896
- @apply rounded-md bg-red-50 px-3 py-2 text-sm text-red-600 dark:bg-red-950 dark:text-red-400;
897
- }
898
- .form-success {
899
- @apply rounded-md bg-green-50 px-3 py-2 text-sm text-green-600 dark:bg-green-950 dark:text-green-400;
900
- }
901
- .form-label {
902
- @apply block text-sm font-medium mb-1;
903
- }
904
- .form-input {
905
- @apply w-full rounded-md border border-zinc-200 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:focus:ring-zinc-100;
906
- }
907
- .form-submit {
908
- @apply w-full rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900;
909
- }
910
- .form-link-row {
911
- @apply flex items-center justify-between text-sm text-zinc-500 dark:text-zinc-400;
912
- }
913
- .auth-link {
914
- @apply underline transition-colors hover:text-zinc-900 dark:hover:text-zinc-100;
915
- }
916
-
917
- .error-wrap {
918
- @apply flex min-h-svh flex-col items-center justify-center gap-4 p-4;
919
- }
920
- .error-link {
921
- @apply mt-4 text-sm underline transition-colors hover:text-zinc-900 dark:hover:text-zinc-100;
922
- }
923
- .empty-state {
924
- @apply py-8 text-center text-sm text-zinc-500 dark:text-zinc-400;
925
- }
926
-
927
- .form-inline {
928
- @apply flex w-full max-w-md gap-2;
929
- }
930
-
931
- /* AI chat */
932
- .chat-wrap {
933
- @apply flex min-h-svh flex-col items-center p-4;
934
- }
935
- .chat-column {
936
- @apply flex w-full max-w-2xl flex-1 flex-col;
937
- }
938
- .chat-header {
939
- @apply mb-4 flex items-center justify-between;
940
- }
941
- .chat-log {
942
- @apply flex-1 space-y-3 overflow-y-auto rounded-lg border border-zinc-200 p-4 dark:border-zinc-800;
943
- max-height: calc(100svh - 180px);
944
- }
945
- .chat-row {
946
- @apply flex;
947
- }
948
- .chat-row.is-user {
949
- @apply justify-end;
950
- }
951
- .chat-row.is-assistant {
952
- @apply justify-start;
953
- }
954
- .chat-bubble {
955
- @apply max-w-[80%] rounded-lg px-3 py-2 text-sm;
956
- }
957
- .chat-bubble.is-user {
958
- @apply bg-zinc-900 text-zinc-50 dark:bg-zinc-100 dark:text-zinc-900;
959
- }
960
- .chat-bubble.is-assistant {
961
- @apply bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
962
- }
963
- .chat-input {
964
- @apply mt-3;
965
- }
966
- `;
967
- }
968
- function indexCssPlain() {
969
- return `:root {
970
- --bg-start: #ffffff;
971
- --bg-end: #fafafa;
972
- --fg: #18181b;
973
- --fg-muted: #52525b;
974
- --fg-strong: #09090b;
975
- --border: #e4e4e7;
976
- --surface: #ffffff;
977
- --surface-muted: #f4f4f5;
978
- --accent: #10b981;
979
- --danger-bg: #fef2f2;
980
- --danger-fg: #dc2626;
981
- --success-bg: #f0fdf4;
982
- --success-fg: #16a34a;
983
- }
984
- @media (prefers-color-scheme: dark) {
985
- :root {
986
- --bg-start: #09090b;
987
- --bg-end: #000000;
988
- --fg: #fafafa;
989
- --fg-muted: #a1a1aa;
990
- --fg-strong: #ffffff;
991
- --border: #27272a;
992
- --surface: #09090b;
993
- --surface-muted: #18181b;
994
- --danger-bg: #450a0a;
995
- --danger-fg: #f87171;
996
- --success-bg: #052e16;
997
- --success-fg: #4ade80;
998
- }
999
- }
1000
-
1001
- *, *::before, *::after { box-sizing: border-box; }
1002
- html, body { margin: 0; padding: 0; }
1003
- body {
1004
- background: linear-gradient(to bottom, var(--bg-start), var(--bg-end));
1005
- color: var(--fg);
1006
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1007
- min-height: 100vh;
1008
- -webkit-font-smoothing: antialiased;
1009
- }
1010
- a { color: inherit; }
1011
-
1012
- /* Layout */
1013
- .page { min-height: 100svh; }
1014
- .page-nav {
1015
- max-width: 72rem;
1016
- margin: 0 auto;
1017
- padding: 1.25rem 1.5rem;
1018
- display: flex;
1019
- align-items: center;
1020
- justify-content: space-between;
1021
- }
1022
- .page-footer {
1023
- border-top: 1px solid var(--border);
1024
- }
1025
- .footer-inner {
1026
- max-width: 72rem;
1027
- margin: 0 auto;
1028
- padding: 1.5rem;
1029
- display: flex;
1030
- flex-direction: column;
1031
- align-items: center;
1032
- gap: 0.75rem;
1033
- font-size: 0.75rem;
1034
- color: var(--fg-muted);
1035
- }
1036
- @media (min-width: 640px) {
1037
- .footer-inner { flex-direction: row; justify-content: space-between; }
1038
- }
1039
- .footer-links { display: flex; gap: 1rem; }
1040
- .footer-link {
1041
- text-decoration: none;
1042
- transition: color 150ms;
1043
- }
1044
- .footer-link:hover { color: var(--fg-strong); }
1045
-
1046
- /* Welcome */
1047
- .brand {
1048
- display: flex;
1049
- align-items: center;
1050
- gap: 0.5rem;
1051
- font-size: 0.875rem;
1052
- font-weight: 600;
1053
- letter-spacing: -0.01em;
1054
- }
1055
- .brand-dot {
1056
- display: inline-block;
1057
- width: 0.5rem;
1058
- height: 0.5rem;
1059
- border-radius: 9999px;
1060
- background: var(--accent);
1061
- }
1062
- .nav-right {
1063
- display: flex;
1064
- align-items: center;
1065
- gap: 1rem;
1066
- font-size: 0.875rem;
1067
- }
1068
- .nav-badge { color: var(--fg-muted); }
1069
- .nav-badge strong { color: var(--fg-strong); font-weight: 500; }
1070
- .nav-button {
1071
- display: inline-block;
1072
- border: 1px solid var(--border);
1073
- border-radius: 0.375rem;
1074
- padding: 0.375rem 0.75rem;
1075
- font-size: 0.75rem;
1076
- font-weight: 500;
1077
- color: var(--fg);
1078
- background: transparent;
1079
- cursor: pointer;
1080
- text-decoration: none;
1081
- transition: background-color 150ms;
1082
- }
1083
- .nav-button:hover { background: var(--surface-muted); }
1084
- .nav-link {
1085
- color: var(--fg-muted);
1086
- text-decoration: none;
1087
- transition: color 150ms;
1088
- }
1089
- .nav-link:hover { color: var(--fg-strong); }
1090
-
1091
- .hero {
1092
- max-width: 48rem;
1093
- margin: 0 auto;
1094
- padding: 5rem 1.5rem 3rem;
1095
- text-align: center;
1096
- }
1097
- .hero-title {
1098
- font-size: 3rem;
1099
- font-weight: 700;
1100
- letter-spacing: -0.02em;
1101
- margin: 0;
1102
- }
1103
- @media (min-width: 640px) {
1104
- .hero-title { font-size: 3.75rem; }
1105
- }
1106
- .hero-lead {
1107
- margin: 1.5rem 0 0;
1108
- font-size: 1.125rem;
1109
- color: var(--fg-muted);
1110
- line-height: 1.6;
1111
- }
1112
- .hero-meta {
1113
- margin-top: 2rem;
1114
- display: flex;
1115
- align-items: center;
1116
- justify-content: center;
1117
- gap: 0.75rem;
1118
- font-size: 0.75rem;
1119
- color: var(--fg-muted);
1120
- }
1121
- .inline-code {
1122
- background: var(--surface-muted);
1123
- padding: 0.125rem 0.375rem;
1124
- border-radius: 0.25rem;
1125
- font-size: 0.875rem;
1126
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1127
- }
1128
-
1129
- .feature-section {
1130
- max-width: 72rem;
1131
- margin: 0 auto;
1132
- padding: 0 1.5rem 5rem;
1133
- }
1134
- .feature-grid {
1135
- display: grid;
1136
- gap: 1rem;
1137
- }
1138
- @media (min-width: 768px) { .feature-grid { grid-template-columns: repeat(2, 1fr); } }
1139
- @media (min-width: 1024px) { .feature-grid { grid-template-columns: repeat(3, 1fr); } }
1140
- .feature-card {
1141
- display: block;
1142
- border: 1px solid var(--border);
1143
- background: var(--surface);
1144
- border-radius: 0.75rem;
1145
- padding: 1.5rem;
1146
- text-decoration: none;
1147
- color: inherit;
1148
- transition: border-color 150ms, color 150ms;
1149
- }
1150
- .feature-card:hover { border-color: var(--fg-strong); }
1151
- .feature-title { font-weight: 600; margin: 0; }
1152
- .feature-desc {
1153
- margin: 0.5rem 0 0;
1154
- font-size: 0.875rem;
1155
- color: var(--fg-muted);
1156
- }
1157
- .feature-card:hover .feature-desc { color: var(--fg-strong); }
1158
-
1159
- /* Auth forms + error page */
1160
- .auth-wrap {
1161
- display: flex;
1162
- min-height: 100svh;
1163
- align-items: center;
1164
- justify-content: center;
1165
- padding: 1rem;
1166
- }
1167
- .auth-card {
1168
- width: 100%;
1169
- max-width: 24rem;
1170
- display: flex;
1171
- flex-direction: column;
1172
- gap: 1.5rem;
1173
- }
1174
- .auth-head { text-align: center; }
1175
- .heading-lg { font-size: 1.5rem; font-weight: 700; margin: 0; }
1176
- .muted { font-size: 0.875rem; color: var(--fg-muted); margin: 0; }
1177
- .auth-head .muted { margin-top: 0.25rem; }
1178
-
1179
- .form-card {
1180
- display: flex;
1181
- flex-direction: column;
1182
- gap: 1rem;
1183
- border: 1px solid var(--border);
1184
- border-radius: 0.5rem;
1185
- padding: 1.5rem;
1186
- background: var(--surface);
1187
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
1188
- }
1189
- .form-error {
1190
- background: var(--danger-bg);
1191
- color: var(--danger-fg);
1192
- font-size: 0.875rem;
1193
- padding: 0.5rem 0.75rem;
1194
- border-radius: 0.375rem;
1195
- margin: 0;
1196
- }
1197
- .form-success {
1198
- background: var(--success-bg);
1199
- color: var(--success-fg);
1200
- font-size: 0.875rem;
1201
- padding: 0.5rem 0.75rem;
1202
- border-radius: 0.375rem;
1203
- margin: 0;
1204
- }
1205
- .form-label {
1206
- display: block;
1207
- font-size: 0.875rem;
1208
- font-weight: 500;
1209
- margin-bottom: 0.25rem;
1210
- }
1211
- .form-input {
1212
- display: block;
1213
- width: 100%;
1214
- border: 1px solid var(--border);
1215
- border-radius: 0.375rem;
1216
- padding: 0.5rem 0.75rem;
1217
- font-size: 0.875rem;
1218
- background: var(--surface);
1219
- color: var(--fg);
1220
- outline: none;
1221
- transition: box-shadow 150ms, border-color 150ms;
1222
- }
1223
- .form-input:focus {
1224
- border-color: var(--fg-strong);
1225
- box-shadow: 0 0 0 2px var(--fg-strong);
1226
- }
1227
- .form-submit {
1228
- width: 100%;
1229
- background: var(--fg-strong);
1230
- color: var(--bg-start);
1231
- border: 0;
1232
- border-radius: 0.375rem;
1233
- padding: 0.625rem 1rem;
1234
- font-size: 0.875rem;
1235
- font-weight: 500;
1236
- cursor: pointer;
1237
- transition: opacity 150ms;
1238
- }
1239
- .form-submit:hover { opacity: 0.9; }
1240
- .form-submit:disabled { opacity: 0.5; cursor: not-allowed; }
1241
- .form-link-row {
1242
- display: flex;
1243
- align-items: center;
1244
- justify-content: space-between;
1245
- font-size: 0.875rem;
1246
- color: var(--fg-muted);
1247
- }
1248
- .auth-link {
1249
- text-decoration: underline;
1250
- transition: color 150ms;
1251
- }
1252
- .auth-link:hover { color: var(--fg-strong); }
1253
-
1254
- .error-wrap {
1255
- display: flex;
1256
- min-height: 100svh;
1257
- flex-direction: column;
1258
- align-items: center;
1259
- justify-content: center;
1260
- gap: 1rem;
1261
- padding: 1rem;
1262
- text-align: center;
1263
- }
1264
- .error-link {
1265
- margin-top: 1rem;
1266
- font-size: 0.875rem;
1267
- text-decoration: underline;
1268
- transition: color 150ms;
1269
- }
1270
- .error-link:hover { color: var(--fg-strong); }
1271
- .empty-state {
1272
- padding: 2rem 0;
1273
- text-align: center;
1274
- font-size: 0.875rem;
1275
- color: var(--fg-muted);
1276
- }
1277
-
1278
- .form-inline {
1279
- display: flex;
1280
- width: 100%;
1281
- max-width: 28rem;
1282
- gap: 0.5rem;
1283
- }
1284
-
1285
- /* AI chat */
1286
- .chat-wrap {
1287
- display: flex;
1288
- min-height: 100svh;
1289
- flex-direction: column;
1290
- align-items: center;
1291
- padding: 1rem;
1292
- }
1293
- .chat-column {
1294
- display: flex;
1295
- width: 100%;
1296
- max-width: 42rem;
1297
- flex: 1;
1298
- flex-direction: column;
1299
- }
1300
- .chat-header {
1301
- display: flex;
1302
- align-items: center;
1303
- justify-content: space-between;
1304
- margin-bottom: 1rem;
1305
- }
1306
- .chat-log {
1307
- flex: 1;
1308
- display: flex;
1309
- flex-direction: column;
1310
- gap: 0.75rem;
1311
- overflow-y: auto;
1312
- border: 1px solid var(--border);
1313
- border-radius: 0.5rem;
1314
- padding: 1rem;
1315
- background: var(--surface);
1316
- max-height: calc(100svh - 180px);
1317
- }
1318
- .chat-row { display: flex; }
1319
- .chat-row.is-user { justify-content: flex-end; }
1320
- .chat-row.is-assistant { justify-content: flex-start; }
1321
- .chat-bubble {
1322
- max-width: 80%;
1323
- border-radius: 0.5rem;
1324
- padding: 0.5rem 0.75rem;
1325
- font-size: 0.875rem;
1326
- }
1327
- .chat-bubble.is-user {
1328
- background: var(--fg-strong);
1329
- color: var(--bg-start);
1330
- }
1331
- .chat-bubble.is-assistant {
1332
- background: var(--surface-muted);
1333
- color: var(--fg);
1334
- }
1335
- .chat-input {
1336
- margin-top: 0.75rem;
1337
- }
1338
- `;
1339
- }
1340
- // ─── bootstrap/app.ts ──────────────────────────────────────
1341
- function bootstrapApp(ctx) {
1342
- const imports = [
1343
- "import 'reflect-metadata'",
1344
- "import 'dotenv/config'",
1345
- "import { Application } from '@rudderjs/core'",
1346
- "import { hono } from '@rudderjs/server-hono'",
1347
- "import { RateLimit } from '@rudderjs/middleware'",
1348
- "import configs from '../config/index.ts'",
1349
- "import providers from './providers.ts'",
1350
- ];
1351
- void ctx;
1352
- // Note (middleware groups):
1353
- // - m.use(...) runs on every request regardless of route group
1354
- // - m.web(...) / m.api(...) run only on routes loaded via withRouting({ web }) / { api }
1355
- // - sessionMiddleware + AuthMiddleware are auto-installed on the web group
1356
- // by @rudderjs/session and @rudderjs/auth — no manual wiring needed
1357
- return `${imports.join('\n')}
1358
-
1359
- export default Application.configure({
1360
- server: hono(configs.server),
1361
- config: configs,
1362
- providers,
1363
- })
1364
- .withRouting({
1365
- web: () => import('../routes/web.ts'),
1366
- api: () => import('../routes/api.ts'),
1367
- commands: () => import('../routes/console.ts'),
1368
- })
1369
- .withMiddleware((m) => {
1370
- // Per-group — separate rate-limit budgets for web pages and api calls
1371
- m.web(RateLimit.perMinute(120))
1372
- m.api(RateLimit.perMinute(60))
1373
-
1374
- // sessionMiddleware + AuthMiddleware are auto-installed on the web group
1375
- // by @rudderjs/session and @rudderjs/auth. Api routes are stateless by
1376
- // default — opt into bearer auth with RequireBearer() from @rudderjs/passport.
1377
- })
1378
- .create()
1379
- `;
1380
- }
1381
- // ─── bootstrap/providers.ts ────────────────────────────────
1382
- function bootstrapProviders(ctx) {
1383
- const imports = [
1384
- "import type { Application, ServiceProvider } from '@rudderjs/core'",
1385
- "import { defaultProviders, eventsProvider } from '@rudderjs/core'",
1386
- "import { AppServiceProvider } from '../app/Providers/AppServiceProvider.js'",
1387
- ];
1388
- const providers = [
1389
- '...(await defaultProviders()),',
1390
- 'eventsProvider({}),',
1391
- 'AppServiceProvider,',
1392
- ];
1393
- return `${imports.join('\n')}
1394
-
1395
- // All framework providers are auto-discovered from package.json metadata.
1396
- // Run \`pnpm rudder providers:discover\` after installing or removing packages.
1397
- //
1398
- // To skip a specific framework provider:
1399
- // ...(await defaultProviders({ skip: ['@rudderjs/horizon'] })),
1400
- //
1401
- // To turn off auto-discovery entirely, replace \`...(await defaultProviders())\`
1402
- // with explicit class imports — see the framework docs.
1403
- export default [
1404
- ${providers.join('\n ')}
1405
- ] satisfies (new (app: Application) => ServiceProvider)[]
1406
- `;
1407
- }
1408
- // ─── config files ──────────────────────────────────────────
1409
- function configApp() {
1410
- return `import { Env } from '@rudderjs/support'
1411
-
1412
- export default {
1413
- name: Env.get('APP_NAME', 'RudderJS'),
1414
- env: Env.get('APP_ENV', 'development'),
1415
- debug: Env.getBool('APP_DEBUG', false),
1416
- url: Env.get('APP_URL', 'http://localhost:3000'),
1417
- }
1418
- `;
1419
- }
1420
- function configServer() {
1421
- return `import { Env } from '@rudderjs/support'
1422
-
1423
- export default {
1424
- port: Env.getNumber('PORT', 3000),
1425
- trustProxy: Env.getBool('TRUST_PROXY', false),
1426
- cors: {
1427
- origin: Env.get('CORS_ORIGIN', '*'),
1428
- methods: Env.get('CORS_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'),
1429
- headers: Env.get('CORS_HEADERS', 'Content-Type,Authorization'),
1430
- },
1431
- }
1432
- `;
1433
- }
1434
- function configLog() {
1435
- return `import { Env } from '@rudderjs/support'
1436
- import type { LogConfig } from '@rudderjs/log'
1437
-
1438
- export default {
1439
- default: Env.get('LOG_CHANNEL', 'console'),
1440
-
1441
- channels: {
1442
- stack: {
1443
- driver: 'stack',
1444
- channels: ['console', 'daily'],
1445
- ignoreExceptions: false,
1446
- },
1447
-
1448
- console: {
1449
- driver: 'console',
1450
- level: Env.get('LOG_LEVEL', 'debug') as 'debug',
1451
- },
1452
-
1453
- single: {
1454
- driver: 'single',
1455
- path: 'storage/logs/rudderjs.log',
1456
- level: Env.get('LOG_LEVEL', 'debug') as 'debug',
1457
- },
1458
-
1459
- daily: {
1460
- driver: 'daily',
1461
- path: 'storage/logs/rudderjs.log',
1462
- days: 14,
1463
- level: Env.get('LOG_LEVEL', 'debug') as 'debug',
1464
- },
1465
-
1466
- null: {
1467
- driver: 'null',
1468
- },
1469
- },
1470
- } satisfies LogConfig
1471
- `;
1472
- }
1473
- function configHash() {
1474
- return `import { Env } from '@rudderjs/support'
1475
- import type { HashConfig } from '@rudderjs/hash'
1476
-
1477
- export default {
1478
- driver: Env.get('HASH_DRIVER', 'bcrypt') as 'bcrypt' | 'argon2',
1479
- bcrypt: { rounds: 12 },
1480
- argon2: { memory: 65536, time: 3, threads: 4 },
1481
- } satisfies HashConfig
1482
- `;
1483
- }
1484
- function configDatabase(ctx) {
1485
- const defaultConn = ctx.db;
1486
- const connections = {
1487
- sqlite: ` sqlite: {
1488
- driver: 'sqlite' as const,
1489
- url: Env.get('DATABASE_URL', 'file:./dev.db'),
1490
- },`,
1491
- postgresql: ` postgresql: {
1492
- driver: 'postgresql' as const,
1493
- url: Env.get('DATABASE_URL', ''),
1494
- },`,
1495
- mysql: ` mysql: {
1496
- driver: 'mysql' as const,
1497
- url: Env.get('DATABASE_URL', ''),
1498
- },`,
1499
- };
1500
- return `import { Env } from '@rudderjs/support'
1501
-
1502
- export default {
1503
- default: Env.get('DB_CONNECTION', '${defaultConn}'),
1504
-
1505
- connections: {
1506
- ${connections[ctx.db]}
1507
- },
1508
- }
1509
- `;
1510
- }
1511
- function configQueue() {
1512
- return `import { Env, isWebContainer } from '@rudderjs/support'
1513
- import type { QueueConfig } from '@rudderjs/queue'
1514
-
1515
- // In WebContainer, BullMQ (Redis over raw TCP) doesn't work — fall back to
1516
- // the in-process \`sync\` driver.
1517
- const defaultConnection = isWebContainer() ? 'sync' : Env.get('QUEUE_CONNECTION', 'sync')
1518
-
1519
- export default {
1520
- default: defaultConnection,
1521
-
1522
- connections: {
1523
- sync: {
1524
- driver: 'sync',
1525
- },
1526
-
1527
- inngest: {
1528
- driver: 'inngest',
1529
- appId: Env.get('INNGEST_APP_ID', 'my-app'),
1530
- eventKey: Env.get('INNGEST_EVENT_KEY', ''),
1531
- signingKey: Env.get('INNGEST_SIGNING_KEY', ''),
1532
- jobs: [],
1533
- },
1534
- },
1535
- } satisfies QueueConfig
1536
- `;
1537
- }
1538
- function configMail() {
1539
- return `import { Env, isWebContainer } from '@rudderjs/support'
1540
-
1541
- // In WebContainer, raw SMTP (TCP) doesn't work — fall back to the log driver
1542
- // (writes the rendered email to stdout instead of sending).
1543
- const defaultMailer = isWebContainer() ? 'log' : Env.get('MAIL_MAILER', 'log')
1544
-
1545
- export default {
1546
- default: defaultMailer,
1547
-
1548
- from: {
1549
- address: Env.get('MAIL_FROM_ADDRESS', 'hello@example.com'),
1550
- name: Env.get('MAIL_FROM_NAME', 'RudderJS'),
1551
- },
1552
-
1553
- mailers: {
1554
- log: {
1555
- driver: 'log',
1556
- },
1557
-
1558
- smtp: {
1559
- driver: 'smtp',
1560
- host: Env.get('MAIL_HOST', 'localhost'),
1561
- port: Env.getNumber('MAIL_PORT', 587),
1562
- username: Env.get('MAIL_USERNAME', ''),
1563
- password: Env.get('MAIL_PASSWORD', ''),
1564
- encryption: Env.get('MAIL_ENCRYPTION', 'tls'),
1565
- },
1566
- },
1567
- }
1568
- `;
1569
- }
1570
- function configCache() {
1571
- return `import { Env, isWebContainer } from '@rudderjs/support'
1572
- import type { CacheConfig } from '@rudderjs/cache'
1573
-
1574
- // In WebContainer, native Redis (raw TCP) doesn't work — fall back to memory.
1575
- const defaultStore = isWebContainer() ? 'memory' : Env.get('CACHE_STORE', 'memory')
1576
-
1577
- export default {
1578
- default: defaultStore,
1579
-
1580
- stores: {
1581
- memory: {
1582
- driver: 'memory',
1583
- },
1584
-
1585
- redis: {
1586
- driver: 'redis',
1587
- url: Env.get('REDIS_URL', ''),
1588
- host: Env.get('REDIS_HOST', '127.0.0.1'),
1589
- port: Env.getNumber('REDIS_PORT', 6379),
1590
- password: Env.get('REDIS_PASSWORD', ''),
1591
- prefix: Env.get('CACHE_PREFIX', 'rudderjs:'),
1592
- },
1593
- },
1594
- } satisfies CacheConfig
1595
- `;
1596
- }
1597
- function configStorage() {
1598
- return `import path from 'node:path'
1599
- import { Env } from '@rudderjs/support'
1600
- import type { StorageConfig } from '@rudderjs/storage'
1601
-
1602
- export default {
1603
- default: Env.get('FILESYSTEM_DISK', 'local'),
1604
-
1605
- disks: {
1606
- local: {
1607
- driver: 'local',
1608
- root: path.resolve(process.cwd(), 'storage/app'),
1609
- baseUrl: '/api/files',
1610
- },
1611
-
1612
- public: {
1613
- driver: 'local',
1614
- root: path.resolve(process.cwd(), 'storage/app/public'),
1615
- baseUrl: Env.get('APP_URL', 'http://localhost:3000') + '/storage',
1616
- },
1617
-
1618
- s3: {
1619
- driver: 's3',
1620
- bucket: Env.get('AWS_BUCKET', ''),
1621
- region: Env.get('AWS_DEFAULT_REGION', 'us-east-1'),
1622
- accessKeyId: Env.get('AWS_ACCESS_KEY_ID', ''),
1623
- secretAccessKey: Env.get('AWS_SECRET_ACCESS_KEY', ''),
1624
- endpoint: Env.get('AWS_ENDPOINT', ''),
1625
- baseUrl: Env.get('AWS_URL', ''),
1626
- },
1627
- },
1628
- } satisfies StorageConfig
1629
- `;
1630
- }
1631
- function configAuth(_ctx) {
1632
- return `import type { AuthConfig } from '@rudderjs/auth'
1633
- import { User } from '../app/Models/User.js'
1634
-
1635
- export default {
1636
- defaults: { guard: 'web' },
1637
- guards: {
1638
- web: { driver: 'session', provider: 'users' },
1639
- },
1640
- providers: {
1641
- users: { driver: 'eloquent', model: User },
1642
- },
1643
- } satisfies AuthConfig
1644
- `;
1645
- }
1646
- function configIndex(ctx) {
1647
- const imports = [
1648
- "import app from './app.js'",
1649
- "import server from './server.js'",
1650
- "import log from './log.js'",
1651
- ];
1652
- const keys = ['app', 'server', 'log'];
1653
- if (ctx.orm) {
1654
- imports.push("import database from './database.js'");
1655
- keys.push('database');
1656
- }
1657
- if (ctx.packages.auth) {
1658
- imports.push("import auth from './auth.js'");
1659
- imports.push("import session from './session.js'");
1660
- imports.push("import hash from './hash.js'");
1661
- keys.push('auth', 'session', 'hash');
1662
- }
1663
- if (ctx.packages.queue) {
1664
- imports.push("import queue from './queue.js'");
1665
- keys.push('queue');
1666
- }
1667
- if (ctx.packages.mail) {
1668
- imports.push("import mail from './mail.js'");
1669
- keys.push('mail');
1670
- }
1671
- if (ctx.packages.cache) {
1672
- imports.push("import cache from './cache.js'");
1673
- keys.push('cache');
1674
- }
1675
- if (ctx.packages.storage) {
1676
- imports.push("import storage from './storage.js'");
1677
- keys.push('storage');
1678
- }
1679
- if (ctx.packages.ai) {
1680
- imports.push("import ai from './ai.js'");
1681
- keys.push('ai');
1682
- }
1683
- if (ctx.packages.sync) {
1684
- imports.push("import sync from './sync.js'");
1685
- keys.push('sync');
1686
- }
1687
- if (ctx.packages.passport) {
1688
- imports.push("import passport from './passport.js'");
1689
- keys.push('passport');
1690
- }
1691
- if (ctx.packages.localization) {
1692
- imports.push("import localization from './localization.js'");
1693
- keys.push('localization');
1694
- }
1695
- if (ctx.packages.telescope) {
1696
- imports.push("import telescope from './telescope.js'");
1697
- keys.push('telescope');
1698
- }
1699
- return `${imports.join('\n')}
1700
-
1701
- const configs = { ${keys.join(', ')} }
1702
-
1703
- export type Configs = typeof configs
1704
-
1705
- export default configs
1706
- `;
1707
- }
1708
- function envDts() {
1709
- return `import type { Configs } from './config/index.js'
1710
-
1711
- declare module '@rudderjs/core' {
1712
- interface AppConfig extends Configs {}
1713
- }
1714
- `;
1715
- }
1716
- function configSession() {
1717
- return `import { Env, isWebContainer } from '@rudderjs/support'
1718
- import type { SessionConfig } from '@rudderjs/session'
1719
-
1720
- // In WebContainer, raw Redis (TCP) doesn't work — pin the session driver to
1721
- // \`cookie\` so sessions survive without a Redis backend.
1722
- const defaultDriver = isWebContainer()
1723
- ? 'cookie'
1724
- : (Env.get('SESSION_DRIVER', 'cookie') as 'cookie' | 'redis')
1725
-
1726
- export default {
1727
- driver: defaultDriver,
1728
- lifetime: 120,
1729
- secret: Env.get('SESSION_SECRET', 'change-me-in-production'),
1730
- cookie: {
1731
- name: 'rudderjs_session',
1732
- secure: Env.getBool('SESSION_SECURE', false),
1733
- httpOnly: true,
1734
- sameSite: 'lax' as const,
1735
- path: '/',
1736
- },
1737
- redis: { prefix: 'session:', url: Env.get('REDIS_URL', '') },
1738
- } satisfies SessionConfig
1739
- `;
1740
- }
1741
- function configAi() {
1742
- return `import { Env } from '@rudderjs/support'
1743
- import type { AiConfig } from '@rudderjs/ai'
1744
-
1745
- export default {
1746
- default: Env.get('AI_MODEL', 'anthropic/claude-sonnet-4-5'),
1747
-
1748
- providers: {
1749
- anthropic: {
1750
- driver: 'anthropic',
1751
- apiKey: Env.get('ANTHROPIC_API_KEY', ''),
1752
- },
1753
-
1754
- openai: {
1755
- driver: 'openai',
1756
- apiKey: Env.get('OPENAI_API_KEY', ''),
1757
- },
1758
-
1759
- google: {
1760
- driver: 'google',
1761
- apiKey: Env.get('GOOGLE_AI_API_KEY', ''),
1762
- },
1763
-
1764
- ollama: {
1765
- driver: 'ollama',
1766
- baseUrl: Env.get('OLLAMA_BASE_URL', 'http://localhost:11434'),
1767
- },
1768
- },
1769
- } satisfies AiConfig
1770
- `;
1771
- }
1772
- function configSync(ctx) {
1773
- const persistenceImport = ctx.orm === 'prisma' ? "\nimport { syncPrisma } from '@rudderjs/sync'" : '';
1774
- const persistenceLine = ctx.orm === 'prisma'
1775
- ? '\n // Server-side persistence — Y.Docs survive server restarts\n persistence: syncPrisma(),\n'
1776
- : '';
1777
- return `import { Env } from '@rudderjs/support'${persistenceImport}
1778
- import type { SyncConfig } from '@rudderjs/sync'
1779
-
1780
- export default {
1781
- path: Env.get('SYNC_PATH', '/ws-sync'),
1782
- ${persistenceLine}
1783
- // Client-side providers
1784
- providers: ['websocket', 'indexeddb'],
1785
- } satisfies SyncConfig
1786
- `;
1787
- }
1788
- function configPassport() {
1789
- return `import type { PassportConfig } from '@rudderjs/passport'
1790
-
1791
- export default {
1792
- // Keys loaded from filesystem — run \`pnpm rudder passport:keys\` to generate
1793
- keyPath: 'storage',
1794
-
1795
- // Token lifetimes
1796
- tokensExpireIn: 15 * 24 * 60 * 60 * 1000, // 15 days
1797
- refreshTokensExpireIn: 30 * 24 * 60 * 60 * 1000, // 30 days
1798
- personalAccessTokensExpireIn: 6 * 30 * 24 * 60 * 60 * 1000, // ~6 months
1799
-
1800
- // Available OAuth scopes
1801
- scopes: {
1802
- read: 'Read access to your data',
1803
- write: 'Modify your data',
1804
- admin: 'Full administrative access',
1805
- },
1806
- } satisfies PassportConfig
1807
- `;
1808
- }
1809
- function configLocalization() {
1810
- return `import { resolve } from 'node:path'
1811
- import { Env } from '@rudderjs/support'
1812
- import type { LocalizationConfig } from '@rudderjs/localization'
1813
-
1814
- export default {
1815
- locale: Env.get('APP_LOCALE', 'en'),
1816
- fallback: Env.get('APP_FALLBACK_LOCALE', 'en'),
1817
- path: resolve(process.cwd(), 'lang'),
1818
- } satisfies LocalizationConfig
1819
- `;
1820
- }
1821
- function configTelescope() {
1822
- return `import type { TelescopeConfig } from '@rudderjs/telescope'
1823
-
1824
- // Debug dashboard mounted at /telescope. 18 collectors record requests, queries,
1825
- // jobs, exceptions, logs, mail, events, cache, schedule, models, commands,
1826
- // broadcasts, live, HTTP client, gate checks, dumps, AI runs, and MCP calls.
1827
- //
1828
- // Storage defaults to in-memory (bounded, no extra deps). Switch to 'sqlite'
1829
- // for persistence across restarts — install better-sqlite3 first:
1830
- // pnpm add -D better-sqlite3
1831
- //
1832
- // In production, gate access by returning \`false\` from \`auth(req)\` or simply
1833
- // disable by setting \`enabled: false\` via an env var.
1834
- export default {
1835
- enabled: true,
1836
- path: 'telescope',
1837
- storage: 'memory',
1838
- maxEntries: 1000,
1839
- pruneAfterHours: 24,
1840
- slowQueryThreshold: 100,
1841
- ignoreRequests: ['/telescope*', '/health', '/@*'],
1842
- } satisfies TelescopeConfig
1843
- `;
1844
- }
1845
- // ─── app files ─────────────────────────────────────────────
1846
- function userModel() {
1847
- return `import { Model } from '@rudderjs/orm'
1848
-
1849
- export class User extends Model {
1850
- // Prisma accessor is the model name lowercased
1851
- static table = 'user'
1852
-
1853
- id!: string
1854
- name!: string
1855
- email!: string
1856
- password?: string | null
1857
- emailVerified!: boolean
1858
- role!: string
1859
- createdAt!: Date
1860
- updatedAt!: Date
1861
- }
1862
- `;
1863
- }
1864
- function appServiceProvider(ctx) {
1865
- const imports = ["import { ServiceProvider } from '@rudderjs/core'"];
1866
- const registerLines = [
1867
- '// Register your application-level services here:',
1868
- '// this.app.singleton(MyService, () => new MyService())',
1869
- ];
1870
- if (ctx.packages.mcp) {
1871
- imports.push("import { Mcp } from '@rudderjs/mcp'");
1872
- imports.push("import { EchoServer } from '../Mcp/EchoServer.js'");
1873
- registerLines.push('');
1874
- registerLines.push('// Expose the demo MCP server over HTTP at /mcp/echo');
1875
- registerLines.push("Mcp.web('/mcp/echo', EchoServer)");
1876
- }
1877
- return `${imports.join('\n')}
1878
-
1879
- export class AppServiceProvider extends ServiceProvider {
1880
- register(): void {
1881
- ${registerLines.join('\n ')}
1882
- }
1883
-
1884
- boot(): void {
1885
- console.log(\`[AppServiceProvider] booted — \${this.app.name}\`)
1886
- }
1887
- }
1888
- `;
1889
- }
1890
- function mcpEchoServer() {
1891
- return `import { McpServer, Name, Version, Instructions } from '@rudderjs/mcp'
1892
- import { EchoTool } from './EchoTool.js'
1893
-
1894
- @Name('echo-server')
1895
- @Version('1.0.0')
1896
- @Instructions('A demo MCP server that echoes messages back.')
1897
- export class EchoServer extends McpServer {
1898
- protected tools = [EchoTool]
1899
- }
1900
- `;
1901
- }
1902
- function mcpEchoTool() {
1903
- return `import { z } from 'zod'
1904
- import { McpTool, McpResponse, Description } from '@rudderjs/mcp'
1905
- import type { McpToolResult } from '@rudderjs/mcp'
1906
-
1907
- @Description('Echoes the given message back to the caller')
1908
- export class EchoTool extends McpTool {
1909
- schema() {
1910
- return z.object({
1911
- message: z.string().describe('The message to echo'),
1912
- })
1913
- }
1914
-
1915
- async handle(input: Record<string, unknown>): Promise<McpToolResult> {
1916
- return McpResponse.text(\`Echo: \${String(input['message'])}\`)
1917
- }
1918
- }
1919
- `;
1920
- }
1921
- function authController() {
1922
- return `import { Middleware } from '@rudderjs/router'
1923
- import { RateLimit } from '@rudderjs/middleware'
1924
- import {
1925
- BaseAuthController,
1926
- PasswordBroker,
1927
- MemoryTokenRepository,
1928
- EloquentUserProvider,
1929
- type AuthUserModelLike,
1930
- } from '@rudderjs/auth'
1931
- import { Hash } from '@rudderjs/hash'
1932
- import { User } from '../../Models/User.ts'
1933
-
1934
- // Per-IP + per-path rate limit — sign-in attempts don't exhaust the sign-up
1935
- // or password-reset budget for the same client.
1936
- const authLimit = RateLimit.perMinute(10)
1937
- .by(req => {
1938
- const ip = (req as unknown as Record<string, unknown>)['ip'] as string ?? '127.0.0.1'
1939
- return \`\${ip}:\${req.path}\`
1940
- })
1941
- .message('Too many auth attempts. Try again later.')
1942
-
1943
- // Swap MemoryTokenRepository for a persistent one (Prisma/Redis) in production.
1944
- const broker = new PasswordBroker(
1945
- new MemoryTokenRepository(),
1946
- new EloquentUserProvider(User as unknown as never, (plain, hashed) => Hash.check(plain, hashed)),
1947
- { expire: 60, throttle: 60 },
1948
- )
1949
-
1950
185
  /**
1951
- * Auth POST handlersLaravel Breeze-style.
1952
- *
1953
- * Extends \`BaseAuthController\` from @rudderjs/auth which provides the five
1954
- * standard endpoints (\`sign-in/email\`, \`sign-up/email\`, \`sign-out\`,
1955
- * \`request-password-reset\`, \`reset-password\`). Override any method to
1956
- * customize behavior — the base uses \`this.userModel\` / \`this.hash\` /
1957
- * \`this.passwordBroker\` for its defaults, so replacing those fields is
1958
- * usually all you need.
1959
- *
1960
- * Registered from \`routes/web.ts\` via \`Route.registerController(AuthController)\`
1961
- * so the handlers inherit SessionMiddleware + AuthMiddleware from the web group.
186
+ * Demos are React-primary only for v1 vue/solid variants aren't written yet.
187
+ * `name` is a demo ID from `templates/demos/registry.ts` (e.g. 'contact', 'ws', 'live').
188
+ * Returns true when the demo was selected AND its package gates are satisfied.
1962
189
  */
1963
- @Middleware([authLimit])
1964
- export class AuthController extends BaseAuthController {
1965
- protected userModel = User as unknown as AuthUserModelLike
1966
- protected hash = Hash
1967
- protected passwordBroker = broker
1968
- }
1969
- `;
1970
- }
1971
- // ─── routes ────────────────────────────────────────────────
1972
- function routesApi(ctx) {
1973
- const imports = [
1974
- "import { router } from '@rudderjs/router'",
1975
- ];
1976
- const lines = [];
1977
- if (ctx.packages.auth || ctx.packages.ai) {
1978
- imports.push("import { app } from '@rudderjs/core'");
1979
- }
1980
- if (ctx.packages.auth) {
1981
- imports.push("import { Auth, AuthManager, runWithAuth } from '@rudderjs/auth'");
1982
- imports.push("import { SessionMiddleware } from '@rudderjs/session'");
1983
- }
1984
- if (ctx.packages.ai) {
1985
- imports.push("import { AI } from '@rudderjs/ai'");
1986
- }
1987
- lines.push('');
1988
- lines.push("router.get('/api/health', (_req, res) => res.json({ status: 'ok' }))");
1989
- if (ctx.packages.auth) {
1990
- lines.push('');
1991
- lines.push(`// GET /api/me — returns current user or null (Laravel Sanctum SPA-style).
1992
- // Api routes are stateless by default, so session is opted in per-route.
1993
- // Session-mutating handlers (sign-in, sign-up, sign-out, password reset)
1994
- // live in routes/web.ts so they inherit SessionMiddleware from the web group.
1995
- router.get('/api/me', async (req, res) => {
1996
- const manager = app().make<AuthManager>('auth.manager')
1997
- let user: Record<string, unknown> | null = null
1998
- await runWithAuth(manager, async () => {
1999
- const authUser = await Auth.user()
2000
- if (authUser) {
2001
- user = { id: authUser.getAuthIdentifier() }
2002
- }
2003
- })
2004
- res.json({ user })
2005
- }, [SessionMiddleware()])`);
2006
- }
2007
- if (ctx.packages.ai) {
2008
- lines.push('');
2009
- lines.push(`// POST /api/ai/chat — simple AI chat endpoint
2010
- router.post('/api/ai/chat', async (req, res) => {
2011
- const { messages } = req.body as { messages: { role: string; content: string }[] }
2012
- const lastMessage = messages.at(-1)?.content ?? ''
2013
- const response = await AI.agent('You are a helpful assistant.').prompt(lastMessage)
2014
- res.json({ message: response.text })
2015
- })`);
2016
- }
2017
- if (ctx.packages.passport) {
2018
- imports.push("import { registerPassportRoutes, RequireBearer, scope } from '@rudderjs/passport'");
2019
- lines.push('');
2020
- lines.push(`// ── Passport OAuth 2 routes ──────────────────────────────
2021
- //
2022
- // Registers /oauth/authorize, /oauth/token, /oauth/tokens/:id,
2023
- // /oauth/scopes, /oauth/device/code, /oauth/device/approve.
2024
- //
2025
- // Requires: RSA keys via \`pnpm rudder passport:keys\` and an OAuth client
2026
- // via \`pnpm rudder passport:client <name>\`.
2027
- const passportRouter = {
2028
- get: (path: string, handler: any) => router.get(path, handler),
2029
- post: (path: string, handler: any) => router.post(path, handler),
2030
- delete: (path: string, handler: any) => router.delete(path, handler),
2031
- }
2032
- registerPassportRoutes(passportRouter as any)
2033
-
2034
- // Example: protected route requiring a Bearer token with 'read' scope
2035
- router.get('/api/passport/me', async (req, res) => {
2036
- res.json({
2037
- user: req.user ?? null,
2038
- scopes: (req.raw as any)?.__passport_scopes ?? [],
2039
- })
2040
- }, [RequireBearer(), scope('read')])`);
2041
- }
2042
- if (shouldScaffoldDemos(ctx)) {
2043
- imports.push(`import { z } from '@rudderjs/core'`);
2044
- if (ctx.packages.auth)
2045
- imports.push(`import { CsrfMiddleware } from '@rudderjs/middleware'`);
2046
- if (ctx.packages.broadcast)
2047
- imports.push(`import { broadcast, broadcastStats } from '@rudderjs/broadcast'`);
2048
- lines.push('');
2049
- lines.push(`// ── Demos ────────────────────────────────────────────────
2050
- // POST /api/contact — Zod validation${ctx.packages.auth ? ' + CSRF' : ''}.
2051
- const contactSchema = z.object({
2052
- name: z.string().min(2, 'Name must be at least 2 characters.'),
2053
- email: z.string().email('Please enter a valid email address.'),
2054
- message: z.string().min(10, 'Message must be at least 10 characters.'),
2055
- })
2056
-
2057
- router.post('/api/contact', async (req, res) => {
2058
- const result = contactSchema.safeParse(req.body)
2059
- if (!result.success) {
2060
- const errors = Object.fromEntries(result.error.issues.map(i => [i.path[0], i.message]))
2061
- return res.status(422).json({ errors })
2062
- }
2063
- return res.json({ ok: true, message: \`Thanks \${result.data.name}, your message has been received!\` })
2064
- }${ctx.packages.auth ? ', [CsrfMiddleware()]' : ''})`);
2065
- if (ctx.packages.broadcast) {
2066
- lines.push('');
2067
- lines.push(`// POST /api/ws/broadcast — push a chat message to subscribers of the 'chat' channel.
2068
- router.post('/api/ws/broadcast', async (req, res) => {
2069
- const { user, text } = req.body as { user: string; text: string }
2070
- broadcast('chat', 'message', { user, text, ts: Date.now() })
2071
- res.json({ ok: true })
2072
- })
2073
-
2074
- // GET /api/ws/ping — current connection / channel counts.
2075
- router.get('/api/ws/ping', (_req, res) => res.json(broadcastStats()))`);
2076
- }
2077
- }
2078
- lines.push('');
2079
- lines.push("// Catch-all: any unmatched /api/* route returns 404");
2080
- lines.push("router.all('/api/*', (_req, res) => res.status(404).json({ message: 'Route not found.' }))");
2081
- return imports.join('\n') + '\n' + lines.join('\n') + '\n';
2082
- }
2083
- function routesWeb(ctx) {
2084
- const hasAuth = ctx.packages.auth;
2085
- const hasWelcome = ctx.frameworks.length === 1;
2086
- // ── imports ─────────────────────────────────────────────
2087
- const imports = [`import { Route } from '@rudderjs/router'`];
2088
- if (hasWelcome) {
2089
- imports.push(`import { createRequire } from 'node:module'`);
2090
- imports.push(`import { view } from '@rudderjs/view'`);
2091
- imports.push(`import { config } from '@rudderjs/core'`);
2092
- }
2093
- if (hasAuth) {
2094
- imports.push(`import { CsrfMiddleware } from '@rudderjs/middleware'`);
2095
- imports.push(`import { registerAuthRoutes } from '@rudderjs/auth/routes'`);
2096
- imports.push(`import { auth } from '@rudderjs/auth'`);
2097
- imports.push(`import { AuthController } from '../app/Http/Controllers/AuthController.ts'`);
2098
- }
2099
- // ── middleware chain shared with auth routes + welcome ─
2100
- // SessionMiddleware + AuthMiddleware are auto-installed on the web group by
2101
- // their providers. Only CSRF stays per-route so specific endpoints (webhooks,
2102
- // server-to-server callbacks) can opt out.
2103
- const webMwBlock = hasAuth
2104
- ? `
2105
- // Per-route web middleware — session + auth are auto-applied on the web group.
2106
- const webMw = [CsrfMiddleware()]
2107
- `
2108
- : '';
2109
- // ── auth UI wiring ──────────────────────────────────────
2110
- // GET view pages come from `registerAuthRoutes`; the POST submit handlers
2111
- // come from `AuthController` (extends @rudderjs/auth's BaseAuthController).
2112
- // Both live in routes/web.ts so they inherit SessionMiddleware + AuthMiddleware
2113
- // from the web group. Customize the flow by editing app/Http/Controllers/AuthController.ts.
2114
- const authBlock = hasAuth
2115
- ? `
2116
- // GET pages — login/register/forgot-password/reset-password
2117
- // Views live in app/Views/Auth/ (vendored from @rudderjs/auth/views/${ctx.primary}/)
2118
- registerAuthRoutes(Route, { middleware: webMw })
2119
-
2120
- // POST handlers — sign-in/email, sign-up/email, sign-out, password reset.
2121
- // Edit app/Http/Controllers/AuthController.ts to customize.
2122
- Route.registerController(AuthController)
2123
- `
2124
- : '';
2125
- // ── welcome page wiring ─────────────────────────────────
2126
- const welcomeBlock = hasWelcome
2127
- ? `
2128
- // Read RudderJS version from @rudderjs/core's package.json at boot time.
2129
- const _require = createRequire(import.meta.url)
2130
- const rudderCorePkg = _require('@rudderjs/core/package.json') as { version: string }
2131
-
2132
- // Welcome page — delete this route and app/Views/Welcome.${welcomeExt(ctx.primary)} to replace it.
2133
- Route.get('/', async () => {${hasAuth ? `
2134
- // Resolve the current user (if signed in) — AuthMiddleware auto-installs on
2135
- // the web group, so auth() has a populated ALS context here.
2136
- const current = await auth().user() as Record<string, unknown> | null
2137
- const user = current
2138
- ? { name: String(current['name'] ?? ''), email: String(current['email'] ?? '') }
2139
- : null` : `
2140
- // Auth is not installed, so the welcome page never shows a signed-in user.
2141
- const user = null`}
2142
- return view('welcome', {
2143
- appName: config<string>('app.name', 'RudderJS'),
2144
- rudderVersion: rudderCorePkg.version,
2145
- nodeVersion: process.version.replace(/^v/, ''),
2146
- env: config<string>('app.env', 'development'),
2147
- user,
2148
- // Laravel's Route::has() — the welcome nav renders Log in / Register links
2149
- // only when the auth package registered these named routes. Install
2150
- // @rudderjs/auth + call registerAuthRoutes() and they appear automatically;
2151
- // uninstall and they vanish. No scaffold-time flag.
2152
- loginUrl: Route.getNamedRoute('login') ?? null,
2153
- registerUrl: Route.getNamedRoute('register') ?? null,
2154
- })
2155
- }${hasAuth ? ', webMw' : ''})
2156
- `
2157
- : '';
2158
- // ── demos wiring ────────────────────────────────────────
2159
- // Controllers for /demos and /demos/<name>. Views live under app/Views/Demos/.
2160
- let demosBlock = '';
2161
- if (shouldScaffoldDemos(ctx)) {
2162
- if (!hasWelcome) {
2163
- // Demo files exist but routesWeb already has `view` imports if hasWelcome.
2164
- // For multi-framework projects (no welcome) we still need the view import here.
2165
- imports.push(`import { view } from '@rudderjs/view'`);
2166
- }
2167
- const lines = [
2168
- `// Demos — see app/Views/Demos/`,
2169
- `Route.get('/demos', async () => view('demos.index'))`,
2170
- `Route.get('/demos/contact', async () => view('demos.contact'))`,
2171
- ];
2172
- if (ctx.packages.broadcast)
2173
- lines.push(`Route.get('/demos/ws', async () => view('demos.ws'))`);
2174
- if (ctx.packages.sync)
2175
- lines.push(`Route.get('/demos/live', async () => view('demos.live'))`);
2176
- demosBlock = '\n' + lines.join('\n') + '\n';
2177
- }
2178
- return `${imports.join('\n')}
2179
- ${webMwBlock}${authBlock}${welcomeBlock}${demosBlock}
2180
- // Web routes — HTML redirects, guards, and non-API server responses
2181
- // These run before Vike's file-based page routing
2182
- // Use this file for: redirects, server-side auth guards, download routes, sitemaps, etc.
2183
- `;
2184
- }
2185
- function welcomeExt(fw) {
2186
- return fw === 'vue' ? 'vue' : 'tsx';
2187
- }
2188
- function routesConsole() {
2189
- return `import { rudder } from '@rudderjs/console'
2190
-
2191
- rudder.command('inspire', () => {
2192
- const quotes = [
2193
- 'The best way to predict the future is to create it.',
2194
- 'Build something people want.',
2195
- 'Stay hungry, stay foolish.',
2196
- 'Code is poetry.',
2197
- 'Simplicity is the soul of efficiency.',
2198
- ]
2199
- const quote = quotes[Math.floor(Math.random() * quotes.length)]!
2200
- console.log(\`\\n "\${quote}"\\n\`)
2201
- }).description('Display an inspiring quote')
2202
-
2203
- rudder.command('db:seed', async () => {
2204
- // TODO: add your seed data here
2205
- console.log('No seed data configured. Edit routes/console.ts to add seed logic.')
2206
- }).description('Seed the database with sample data')
2207
- `;
2208
- }
2209
- // ─── pages ─────────────────────────────────────────────────
2210
- function pagesRootConfig(ctx) {
2211
- if (ctx.frameworks.length === 1) {
2212
- const rendererImport = ctx.primary === 'vue'
2213
- ? `import vikeVue from 'vike-vue/config'`
2214
- : ctx.primary === 'solid'
2215
- ? `import vikeSolid from 'vike-solid/config'`
2216
- : `import vikeReact from 'vike-react/config'`;
2217
- const rendererVar = ctx.primary === 'vue' ? 'vikeVue' : ctx.primary === 'solid' ? 'vikeSolid' : 'vikeReact';
2218
- return `import type { Config } from 'vike/types'
2219
- ${rendererImport}
2220
-
2221
- export default {
2222
- extends: [${rendererVar}],
2223
- } satisfies Config
2224
- `;
2225
- }
2226
- // Multi-framework: no renderer in root config — each page picks its own
2227
- return `import type { Config } from 'vike/types'
2228
-
2229
- export default {} satisfies Config
2230
- `;
2231
- }
2232
- function pagesIndexConfig(ctx) {
2233
- switch (ctx.primary) {
2234
- case 'vue':
2235
- return `import type { Config } from 'vike/types'
2236
- import vikeVue from 'vike-vue/config'
2237
-
2238
- export default {
2239
- extends: vikeVue,
2240
- } satisfies Config
2241
- `;
2242
- case 'solid':
2243
- return `import type { Config } from 'vike/types'
2244
- import vikeSolid from 'vike-solid/config'
2245
-
2246
- export default {
2247
- extends: vikeSolid,
2248
- } satisfies Config
2249
- `;
2250
- default: // react
2251
- return `import type { Config } from 'vike/types'
2252
- import vikeReact from 'vike-react/config'
2253
-
2254
- export default {
2255
- extends: vikeReact,
2256
- } satisfies Config
2257
- `;
2258
- }
2259
- }
2260
- function pagesIndexData(ctx) {
2261
- if (!ctx.packages.auth) {
2262
- return `export type Data = {
2263
- message: string
2264
- }
2265
-
2266
- export async function data(): Promise<Data> {
2267
- return { message: 'Welcome to RudderJS' }
2268
- }
2269
- `;
2270
- }
2271
- return `import { app } from '@rudderjs/core'
2272
- import { AuthManager, Auth, runWithAuth } from '@rudderjs/auth'
2273
-
2274
- export type Data = {
2275
- user: { id: string; name: string; email: string } | null
2276
- }
2277
-
2278
- export async function data(): Promise<Data> {
2279
- const manager = app().make<AuthManager>('auth.manager')
2280
- let user: Data['user'] = null
2281
- await runWithAuth(manager, async () => {
2282
- const authUser = await Auth.user()
2283
- if (authUser) {
2284
- const record = authUser as unknown as Record<string, unknown>
2285
- user = {
2286
- id: String(authUser.getAuthIdentifier()),
2287
- name: String(record['name'] ?? ''),
2288
- email: String(record['email'] ?? ''),
2289
- }
2290
- }
2291
- })
2292
- return { user }
2293
- }
2294
- `;
2295
- }
2296
- function pagesIndexPage(ctx) {
2297
- switch (ctx.primary) {
2298
- case 'vue': return pagesIndexPageVue(ctx);
2299
- case 'solid': return pagesIndexPageSolid(ctx);
2300
- default: return pagesIndexPageReact(ctx);
2301
- }
2302
- }
2303
- function pagesIndexPageReact(ctx) {
2304
- const cssImport = `import '@/index.css'\n`;
2305
- const extraLinks = [];
2306
- if (ctx.packages.ai)
2307
- extraLinks.push(' <a href="/ai-chat" className="auth-link">AI Chat</a>');
2308
- const extraLinksStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
2309
- if (!ctx.packages.auth) {
2310
- return `${cssImport}import { useData } from 'vike-react/useData'
2311
- import type { Data } from './+data.js'
2312
-
2313
- export default function Page() {
2314
- const data = useData<Data>()
2315
-
2316
- return (
2317
- <div className="error-wrap">
2318
- <h1 className="heading-lg">${ctx.name}</h1>
2319
- <p className="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2320
-
2321
- <div className="footer-links muted">
2322
- <a href="/api/health" className="auth-link">API Health</a>${extraLinksStr}
2323
- </div>
2324
- </div>
2325
- )
2326
- }
2327
- `;
2328
- }
2329
- return `${cssImport}import { useState } from 'react'
2330
- import { useData } from 'vike-react/useData'
2331
- import type { Data } from './+data.js'
2332
-
2333
- export default function Page() {
2334
- const data = useData<Data>()
2335
- const [user, setUser] = useState(data.user)
2336
-
2337
- async function signOut() {
2338
- await fetch('/api/auth/sign-out', {
2339
- method: 'POST',
2340
- headers: { 'Content-Type': 'application/json' },
2341
- body: '{}',
2342
- })
2343
- window.location.href = '/'
2344
- }
2345
-
2346
- return (
2347
- <div className="error-wrap">
2348
- <h1 className="heading-lg">${ctx.name}</h1>
2349
- <p className="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2350
-
2351
- {user ? (
2352
- <>
2353
- <p className="nav-badge">
2354
- Signed in as <strong>{user.name}</strong>
2355
- </p>
2356
- <div className="footer-links">
2357
- <button onClick={signOut} className="nav-button">Sign out</button>
2358
- </div>
2359
- </>
2360
- ) : (
2361
- <div className="footer-links">
2362
- <a href="/register" className="nav-button">Register</a>
2363
- <a href="/login" className="nav-button">Login</a>
2364
- </div>
2365
- )}
2366
-
2367
- <div className="footer-links muted">
2368
- <a href="/api/health" className="auth-link">API Health</a>
2369
- <a href="/api/me" className="auth-link">Session Info</a>${extraLinksStr}
2370
- </div>
2371
- </div>
2372
- )
2373
- }
2374
- `;
2375
- }
2376
- function pagesIndexPageVue(ctx) {
2377
- const cssImport = `import '@/index.css'\n`;
2378
- const extraLinks = [];
2379
- if (ctx.packages.ai)
2380
- extraLinks.push(' <a href="/ai-chat" class="auth-link">AI Chat</a>');
2381
- const extraStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
2382
- if (!ctx.packages.auth) {
2383
- return `<script setup lang="ts">
2384
- ${cssImport}import { useData } from 'vike-vue/useData'
2385
- import type { Data } from './+data.js'
2386
-
2387
- const data = useData<Data>()
2388
- </script>
2389
-
2390
- <template>
2391
- <div class="error-wrap">
2392
- <h1 class="heading-lg">${ctx.name}</h1>
2393
- <p class="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2394
-
2395
- <div class="footer-links muted">
2396
- <a href="/api/health" class="auth-link">API Health</a>${extraStr}
2397
- </div>
2398
- </div>
2399
- </template>
2400
- `;
2401
- }
2402
- return `<script setup lang="ts">
2403
- ${cssImport}import { ref } from 'vue'
2404
- import { useData } from 'vike-vue/useData'
2405
- import type { Data } from './+data.js'
2406
-
2407
- const data = useData<Data>()
2408
- const user = ref(data.user)
2409
-
2410
- async function signOut() {
2411
- await fetch('/api/auth/sign-out', {
2412
- method: 'POST',
2413
- headers: { 'Content-Type': 'application/json' },
2414
- body: '{}',
2415
- })
2416
- window.location.href = '/'
2417
- }
2418
- </script>
2419
-
2420
- <template>
2421
- <div class="error-wrap">
2422
- <h1 class="heading-lg">${ctx.name}</h1>
2423
- <p class="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2424
-
2425
- <template v-if="user">
2426
- <p class="nav-badge">
2427
- Signed in as <strong>{{ user.name }}</strong>
2428
- </p>
2429
- <div class="footer-links">
2430
- <button @click="signOut" class="nav-button">Sign out</button>
2431
- </div>
2432
- </template>
2433
- <div v-else class="footer-links">
2434
- <a href="/register" class="nav-button">Register</a>
2435
- <a href="/login" class="nav-button">Login</a>
2436
- </div>
2437
-
2438
- <div class="footer-links muted">
2439
- <a href="/api/health" class="auth-link">API Health</a>
2440
- <a href="/api/me" class="auth-link">Session Info</a>${extraStr}
2441
- </div>
2442
- </div>
2443
- </template>
2444
- `;
2445
- }
2446
- function pagesIndexPageSolid(ctx) {
2447
- const cssImport = `import '@/index.css'\n`;
2448
- const extraLinks = [];
2449
- if (ctx.packages.ai)
2450
- extraLinks.push(' <a href="/ai-chat" class="auth-link">AI Chat</a>');
2451
- const extraStr = extraLinks.length > 0 ? '\n' + extraLinks.join('\n') : '';
2452
- if (!ctx.packages.auth) {
2453
- return `${cssImport}import { useData } from 'vike-solid/useData'
2454
- import type { Data } from './+data.js'
2455
-
2456
- export default function Page() {
2457
- const data = useData<Data>()
2458
-
2459
- return (
2460
- <div class="error-wrap">
2461
- <h1 class="heading-lg">${ctx.name}</h1>
2462
- <p class="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2463
-
2464
- <div class="footer-links muted">
2465
- <a href="/api/health" class="auth-link">API Health</a>${extraStr}
2466
- </div>
2467
- </div>
2468
- )
2469
- }
2470
- `;
2471
- }
2472
- return `${cssImport}import { createSignal, Show } from 'solid-js'
2473
- import { useData } from 'vike-solid/useData'
2474
- import type { Data } from './+data.js'
2475
-
2476
- export default function Page() {
2477
- const data = useData<Data>()
2478
- const [user, setUser] = createSignal(data.user)
2479
-
2480
- async function signOut() {
2481
- await fetch('/api/auth/sign-out', {
2482
- method: 'POST',
2483
- headers: { 'Content-Type': 'application/json' },
2484
- body: '{}',
2485
- })
2486
- window.location.href = '/'
2487
- }
2488
-
2489
- return (
2490
- <div class="error-wrap">
2491
- <h1 class="heading-lg">${ctx.name}</h1>
2492
- <p class="muted">Built with RudderJS — Laravel-inspired Node.js framework.</p>
2493
-
2494
- <Show
2495
- when={user()}
2496
- fallback={
2497
- <div class="footer-links">
2498
- <a href="/register" class="nav-button">Register</a>
2499
- <a href="/login" class="nav-button">Login</a>
2500
- </div>
2501
- }
2502
- >
2503
- <p class="nav-badge">
2504
- Signed in as <strong>{user()!.name}</strong>
2505
- </p>
2506
- <div class="footer-links">
2507
- <button onClick={signOut} class="nav-button">Sign out</button>
2508
- </div>
2509
- </Show>
2510
-
2511
- <div class="footer-links muted">
2512
- <a href="/api/health" class="auth-link">API Health</a>
2513
- <a href="/api/me" class="auth-link">Session Info</a>${extraStr}
2514
- </div>
2515
- </div>
2516
- )
2517
- }
2518
- `;
2519
- }
2520
- // ─── welcome view (controller-returned) ─────────────────────
2521
- function welcomeView(ctx) {
2522
- switch (ctx.primary) {
2523
- case 'vue': return welcomeViewVue(ctx);
2524
- case 'solid': return welcomeViewSolid(ctx);
2525
- default: return welcomeViewReact(ctx);
2526
- }
2527
- }
2528
- const WELCOME_FEATURES = `const DEFAULT_DOCS = 'https://github.com/rudderjs/rudder'
2529
- const DEFAULT_GITHUB = 'https://github.com/rudderjs/rudder'
2530
-
2531
- const features: Feature[] = [
2532
- {
2533
- title: 'Controllers & Routing',
2534
- description: 'Explicit routes in routes/api.ts with middleware, params, named routes, and return types that just work.',
2535
- href: \`\${DEFAULT_DOCS}#routing\`,
2536
- },
2537
- {
2538
- title: 'Eloquent ORM',
2539
- description: 'Laravel-style models on Prisma or Drizzle. Query relationships, scopes, and eager loading without changing mental models.',
2540
- href: \`\${DEFAULT_DOCS}#orm\`,
2541
- },
2542
- {
2543
- title: 'Controller Views',
2544
- description: "The page you're looking at — return view() from a controller, rendered through Vike SSR. Zero adapter, full SPA nav.",
2545
- href: \`\${DEFAULT_DOCS}#views\`,
2546
- },
2547
- {
2548
- title: 'Rudder CLI',
2549
- description: 'Laravel-style make:* generators, schedule, db:seed, and custom commands. Run \\\`pnpm rudder\\\` for the full list.',
2550
- href: \`\${DEFAULT_DOCS}#cli\`,
2551
- },
2552
- {
2553
- title: 'Queues & Jobs',
2554
- description: 'Dispatch background jobs with sync, database, or Redis drivers. Monitor them with @rudderjs/horizon.',
2555
- href: \`\${DEFAULT_DOCS}#queue\`,
2556
- },
2557
- {
2558
- title: 'Auth, Guards, Policies',
2559
- description: 'Session-backed auth, password reset, gates, and RequireAuth / RequireGuest middleware — all through one provider.',
2560
- href: \`\${DEFAULT_DOCS}#auth\`,
2561
- },
2562
- ]`;
2563
- function welcomeViewReact(ctx) {
2564
- const cssImport = `import '@/index.css'\n\n`;
2565
- return `${cssImport}// URL this view is served at — MUST match the Route.get('/', ...) in routes/web.ts.
2566
- // The scanner reads this constant and writes it into the generated +route.ts,
2567
- // so Vike's client router can SPA-navigate here instead of doing full reloads.
2568
- export const route = '/'
2569
-
2570
- export interface WelcomeProps {
2571
- appName: string
2572
- rudderVersion: string
2573
- nodeVersion: string
2574
- env: string
2575
- user: { name: string; email: string } | null
2576
- // null when the auth package isn't installed (Laravel's Route::has() idiom).
2577
- loginUrl: string | null
2578
- registerUrl: string | null
2579
- signOutUrl?: string
2580
- docsUrl?: string
2581
- githubUrl?: string
2582
- }
2583
-
2584
- interface Feature {
2585
- title: string
2586
- description: string
2587
- href: string
2588
- }
2589
-
2590
- ${WELCOME_FEATURES}
2591
-
2592
- export default function Welcome(props: WelcomeProps) {
2593
- const signOutUrl = props.signOutUrl ?? '/api/auth/sign-out'
2594
- const docsUrl = props.docsUrl ?? DEFAULT_DOCS
2595
- const githubUrl = props.githubUrl ?? DEFAULT_GITHUB
2596
-
2597
- async function handleSignOut() {
2598
- await fetch(signOutUrl, {
2599
- method: 'POST',
2600
- headers: { 'Content-Type': 'application/json' },
2601
- body: '{}',
2602
- })
2603
- // Full reload so the server resolves a fresh pageContext (logged-out user).
2604
- window.location.href = '/'
2605
- }
2606
-
2607
- return (
2608
- <div className="page">
2609
- <nav className="page-nav">
2610
- <div className="brand">
2611
- <span className="brand-dot" />
2612
- RudderJS
2613
- </div>
2614
- <div className="nav-right">
2615
- {props.loginUrl && (props.user ? (
2616
- <>
2617
- <span className="nav-badge">
2618
- Signed in as <strong>{props.user.name}</strong>
2619
- </span>
2620
- <button type="button" onClick={handleSignOut} className="nav-button">
2621
- Sign out
2622
- </button>
2623
- </>
2624
- ) : (
2625
- <>
2626
- <a href={props.loginUrl} className="nav-link">Log in</a>
2627
- {props.registerUrl && (
2628
- <a href={props.registerUrl} className="nav-button">Register</a>
2629
- )}
2630
- </>
2631
- ))}
2632
- </div>
2633
- </nav>
2634
-
2635
- <section className="hero">
2636
- <h1 className="hero-title">{props.appName}</h1>
2637
- <p className="hero-lead">
2638
- Laravel&apos;s developer experience, Vike&apos;s performance, Node&apos;s ecosystem.
2639
- This page is served by a controller, rendered through{' '}
2640
- <code className="inline-code">view(&apos;welcome&apos;)</code>.
2641
- </p>
2642
- <div className="hero-meta">
2643
- <span>RudderJS v{props.rudderVersion}</span>
2644
- <span>•</span>
2645
- <span>Node {props.nodeVersion}</span>
2646
- <span>•</span>
2647
- <span>env={props.env}</span>
2648
- </div>
2649
- </section>
2650
-
2651
- <section className="feature-section">
2652
- <div className="feature-grid">
2653
- {features.map(f => (
2654
- <a key={f.title} href={f.href} className="feature-card">
2655
- <h3 className="feature-title">{f.title}</h3>
2656
- <p className="feature-desc">{f.description}</p>
2657
- </a>
2658
- ))}
2659
- </div>
2660
- </section>
2661
-
2662
- <footer className="page-footer">
2663
- <div className="footer-inner">
2664
- <div>Built with RudderJS. Edit <code>app/Views/Welcome.tsx</code> to customize this page.</div>
2665
- <div className="footer-links">
2666
- <a href={docsUrl} className="footer-link">Docs</a>
2667
- <a href={githubUrl} className="footer-link">GitHub</a>
2668
- </div>
2669
- </div>
2670
- </footer>
2671
- </div>
2672
- )
2673
- }
2674
- `;
2675
- }
2676
- function welcomeViewVue(ctx) {
2677
- const cssImport = `import '@/index.css'\n`;
2678
- // Vue SFC quirk: top-level `export` statements must live in a regular
2679
- // <script> block, NOT <script setup> (the compiler rejects exports there).
2680
- // The scanner reads both blocks as plain text, so the route override is
2681
- // still picked up. Keep this dual-script structure whenever a Vue view
2682
- // needs `export const route = '/...'`.
2683
- return `<script lang="ts">
2684
- // URL this view is served at — see the React variant for rationale.
2685
- export const route = '/'
2686
- </script>
2687
-
2688
- <script setup lang="ts">
2689
- ${cssImport}
2690
- export interface WelcomeProps {
2691
- appName: string
2692
- rudderVersion: string
2693
- nodeVersion: string
2694
- env: string
2695
- user: { name: string; email: string } | null
2696
- // null when the auth package isn't installed (Laravel's Route::has() idiom).
2697
- loginUrl: string | null
2698
- registerUrl: string | null
2699
- signOutUrl?: string
2700
- docsUrl?: string
2701
- githubUrl?: string
2702
- }
2703
-
2704
- const props = defineProps<WelcomeProps>()
2705
-
2706
- interface Feature {
2707
- title: string
2708
- description: string
2709
- href: string
2710
- }
2711
-
2712
- ${WELCOME_FEATURES}
2713
-
2714
- const signOutUrl = props.signOutUrl ?? '/api/auth/sign-out'
2715
- const docsUrl = props.docsUrl ?? DEFAULT_DOCS
2716
- const githubUrl = props.githubUrl ?? DEFAULT_GITHUB
2717
-
2718
- async function handleSignOut() {
2719
- await fetch(signOutUrl, {
2720
- method: 'POST',
2721
- headers: { 'Content-Type': 'application/json' },
2722
- body: '{}',
2723
- })
2724
- // Full reload so the server resolves a fresh pageContext (logged-out user).
2725
- window.location.href = '/'
2726
- }
2727
- </script>
2728
-
2729
- <template>
2730
- <div class="page">
2731
- <nav class="page-nav">
2732
- <div class="brand">
2733
- <span class="brand-dot"></span>
2734
- RudderJS
2735
- </div>
2736
- <div v-if="props.loginUrl" class="nav-right">
2737
- <template v-if="props.user">
2738
- <span class="nav-badge">
2739
- Signed in as <strong>{{ props.user.name }}</strong>
2740
- </span>
2741
- <button type="button" @click="handleSignOut" class="nav-button">
2742
- Sign out
2743
- </button>
2744
- </template>
2745
- <template v-else>
2746
- <a :href="props.loginUrl" class="nav-link">Log in</a>
2747
- <a v-if="props.registerUrl" :href="props.registerUrl" class="nav-button">Register</a>
2748
- </template>
2749
- </div>
2750
- </nav>
2751
-
2752
- <section class="hero">
2753
- <h1 class="hero-title">{{ props.appName }}</h1>
2754
- <p class="hero-lead">
2755
- Laravel's developer experience, Vike's performance, Node's ecosystem.
2756
- This page is served by a controller, rendered through
2757
- <code class="inline-code">view('welcome')</code>.
2758
- </p>
2759
- <div class="hero-meta">
2760
- <span>RudderJS v{{ props.rudderVersion }}</span>
2761
- <span>•</span>
2762
- <span>Node {{ props.nodeVersion }}</span>
2763
- <span>•</span>
2764
- <span>env={{ props.env }}</span>
2765
- </div>
2766
- </section>
2767
-
2768
- <section class="feature-section">
2769
- <div class="feature-grid">
2770
- <a v-for="f in features" :key="f.title" :href="f.href" class="feature-card">
2771
- <h3 class="feature-title">{{ f.title }}</h3>
2772
- <p class="feature-desc">{{ f.description }}</p>
2773
- </a>
2774
- </div>
2775
- </section>
2776
-
2777
- <footer class="page-footer">
2778
- <div class="footer-inner">
2779
- <div>Built with RudderJS. Edit <code>app/Views/Welcome.vue</code> to customize this page.</div>
2780
- <div class="footer-links">
2781
- <a :href="docsUrl" class="footer-link">Docs</a>
2782
- <a :href="githubUrl" class="footer-link">GitHub</a>
2783
- </div>
2784
- </div>
2785
- </footer>
2786
- </div>
2787
- </template>
2788
- `;
2789
- }
2790
- function welcomeViewSolid(ctx) {
2791
- const cssImport = `import '@/index.css'\n`;
2792
- return `${cssImport}import { For, Show } from 'solid-js'
2793
-
2794
- // URL this view is served at — see the React variant for rationale.
2795
- export const route = '/'
2796
-
2797
- export interface WelcomeProps {
2798
- appName: string
2799
- rudderVersion: string
2800
- nodeVersion: string
2801
- env: string
2802
- user: { name: string; email: string } | null
2803
- // null when the auth package isn't installed (Laravel's Route::has() idiom).
2804
- loginUrl: string | null
2805
- registerUrl: string | null
2806
- signOutUrl?: string
2807
- docsUrl?: string
2808
- githubUrl?: string
2809
- }
2810
-
2811
- interface Feature {
2812
- title: string
2813
- description: string
2814
- href: string
2815
- }
2816
-
2817
- ${WELCOME_FEATURES}
2818
-
2819
- export default function Welcome(props: WelcomeProps) {
2820
- const signOutUrl = () => props.signOutUrl ?? '/api/auth/sign-out'
2821
- const docsUrl = () => props.docsUrl ?? DEFAULT_DOCS
2822
- const githubUrl = () => props.githubUrl ?? DEFAULT_GITHUB
2823
-
2824
- async function handleSignOut() {
2825
- await fetch(signOutUrl(), {
2826
- method: 'POST',
2827
- headers: { 'Content-Type': 'application/json' },
2828
- body: '{}',
2829
- })
2830
- // Full reload so the server resolves a fresh pageContext (logged-out user).
2831
- window.location.href = '/'
2832
- }
2833
-
2834
- return (
2835
- <div class="page">
2836
- <nav class="page-nav">
2837
- <div class="brand">
2838
- <span class="brand-dot" />
2839
- RudderJS
2840
- </div>
2841
- <Show when={props.loginUrl}>
2842
- {(loginUrl) => (
2843
- <div class="nav-right">
2844
- <Show
2845
- when={props.user}
2846
- fallback={
2847
- <>
2848
- <a href={loginUrl()} class="nav-link">Log in</a>
2849
- <Show when={props.registerUrl}>
2850
- {(registerUrl) => (
2851
- <a href={registerUrl()} class="nav-button">Register</a>
2852
- )}
2853
- </Show>
2854
- </>
2855
- }
2856
- >
2857
- {(user) => (
2858
- <>
2859
- <span class="nav-badge">
2860
- Signed in as <strong>{user().name}</strong>
2861
- </span>
2862
- <button type="button" onClick={handleSignOut} class="nav-button">
2863
- Sign out
2864
- </button>
2865
- </>
2866
- )}
2867
- </Show>
2868
- </div>
2869
- )}
2870
- </Show>
2871
- </nav>
2872
-
2873
- <section class="hero">
2874
- <h1 class="hero-title">{props.appName}</h1>
2875
- <p class="hero-lead">
2876
- Laravel's developer experience, Vike's performance, Node's ecosystem.
2877
- This page is served by a controller, rendered through{' '}
2878
- <code class="inline-code">view('welcome')</code>.
2879
- </p>
2880
- <div class="hero-meta">
2881
- <span>RudderJS v{props.rudderVersion}</span>
2882
- <span>•</span>
2883
- <span>Node {props.nodeVersion}</span>
2884
- <span>•</span>
2885
- <span>env={props.env}</span>
2886
- </div>
2887
- </section>
2888
-
2889
- <section class="feature-section">
2890
- <div class="feature-grid">
2891
- <For each={features}>
2892
- {(f) => (
2893
- <a href={f.href} class="feature-card">
2894
- <h3 class="feature-title">{f.title}</h3>
2895
- <p class="feature-desc">{f.description}</p>
2896
- </a>
2897
- )}
2898
- </For>
2899
- </div>
2900
- </section>
2901
-
2902
- <footer class="page-footer">
2903
- <div class="footer-inner">
2904
- <div>Built with RudderJS. Edit <code>app/Views/Welcome.tsx</code> to customize this page.</div>
2905
- <div class="footer-links">
2906
- <a href={docsUrl()} class="footer-link">Docs</a>
2907
- <a href={githubUrl()} class="footer-link">GitHub</a>
2908
- </div>
2909
- </div>
2910
- </footer>
2911
- </div>
2912
- )
2913
- }
2914
- `;
2915
- }
2916
- function pagesErrorConfig(ctx) {
2917
- switch (ctx.primary) {
2918
- case 'vue':
2919
- return `import type { Config } from 'vike/types'
2920
- import vikeVue from 'vike-vue/config'
2921
-
2922
- export default {
2923
- extends: vikeVue,
2924
- } satisfies Config
2925
- `;
2926
- case 'solid':
2927
- return `import type { Config } from 'vike/types'
2928
- import vikeSolid from 'vike-solid/config'
2929
-
2930
- export default {
2931
- extends: vikeSolid,
2932
- } satisfies Config
2933
- `;
2934
- default:
2935
- return `import type { Config } from 'vike/types'
2936
- import vikeReact from 'vike-react/config'
2937
-
2938
- export default {
2939
- extends: vikeReact,
2940
- } satisfies Config
2941
- `;
2942
- }
2943
- }
2944
- function pagesErrorPage(ctx) {
2945
- switch (ctx.primary) {
2946
- case 'vue': return pagesErrorPageVue(ctx);
2947
- case 'solid': return pagesErrorPageSolid(ctx);
2948
- default: return pagesErrorPageReact(ctx);
2949
- }
2950
- }
2951
- function pagesErrorPageReact(ctx) {
2952
- const cssImport = `import '@/index.css'\n`;
2953
- return `${cssImport}import { usePageContext } from 'vike-react/usePageContext'
2954
-
2955
- export default function Page() {
2956
- const { is404, abortReason, abortStatusCode } = usePageContext() as {
2957
- is404: boolean
2958
- abortStatusCode?: number
2959
- abortReason?: string
2960
- }
2961
-
2962
- if (is404) {
2963
- return (
2964
- <div className="error-wrap">
2965
- <h1 className="heading-lg">404 — Page Not Found</h1>
2966
- <p className="muted">This page could not be found.</p>
2967
- <a href="/" className="error-link">Go home</a>
2968
- </div>
2969
- )
2970
- }
2971
-
2972
- if (abortStatusCode === 401) {
2973
- return (
2974
- <div className="error-wrap">
2975
- <h1 className="heading-lg">401 — Unauthorized</h1>
2976
- <p className="muted">{abortReason ?? 'You must be logged in to view this page.'}</p>
2977
- <a href="/" className="error-link">Go home</a>
2978
- </div>
2979
- )
2980
- }
2981
-
2982
- return (
2983
- <div className="error-wrap">
2984
- <h1 className="heading-lg">Something went wrong</h1>
2985
- <p className="muted">{abortReason ?? 'An unexpected error occurred.'}</p>
2986
- <a href="/" className="error-link">Go home</a>
2987
- </div>
2988
- )
2989
- }
2990
- `;
2991
- }
2992
- function pagesErrorPageVue(ctx) {
2993
- const cssImport = `import '@/index.css'\n`;
2994
- return `<script setup lang="ts">
2995
- ${cssImport}import { usePageContext } from 'vike-vue/usePageContext'
2996
-
2997
- const pageContext = usePageContext() as {
2998
- is404: boolean
2999
- abortStatusCode?: number
3000
- abortReason?: string
3001
- }
3002
- </script>
3003
-
3004
- <template>
3005
- <div v-if="pageContext.is404" class="error-wrap">
3006
- <h1 class="heading-lg">404 — Page Not Found</h1>
3007
- <p class="muted">This page could not be found.</p>
3008
- <a href="/" class="error-link">Go home</a>
3009
- </div>
3010
- <div v-else-if="pageContext.abortStatusCode === 401" class="error-wrap">
3011
- <h1 class="heading-lg">401 — Unauthorized</h1>
3012
- <p class="muted">{{ pageContext.abortReason ?? 'You must be logged in to view this page.' }}</p>
3013
- <a href="/" class="error-link">Go home</a>
3014
- </div>
3015
- <div v-else class="error-wrap">
3016
- <h1 class="heading-lg">Something went wrong</h1>
3017
- <p class="muted">{{ pageContext.abortReason ?? 'An unexpected error occurred.' }}</p>
3018
- <a href="/" class="error-link">Go home</a>
3019
- </div>
3020
- </template>
3021
- `;
3022
- }
3023
- function pagesErrorPageSolid(ctx) {
3024
- const cssImport = `import '@/index.css'\n`;
3025
- return `${cssImport}import { Switch, Match } from 'solid-js'
3026
- import { usePageContext } from 'vike-solid/usePageContext'
3027
-
3028
- export default function Page() {
3029
- const pageContext = usePageContext() as {
3030
- is404: boolean
3031
- abortStatusCode?: number
3032
- abortReason?: string
3033
- }
3034
-
3035
- return (
3036
- <Switch>
3037
- <Match when={pageContext.is404}>
3038
- <div class="error-wrap">
3039
- <h1 class="heading-lg">404 — Page Not Found</h1>
3040
- <p class="muted">This page could not be found.</p>
3041
- <a href="/" class="error-link">Go home</a>
3042
- </div>
3043
- </Match>
3044
- <Match when={pageContext.abortStatusCode === 401}>
3045
- <div class="error-wrap">
3046
- <h1 class="heading-lg">401 — Unauthorized</h1>
3047
- <p class="muted">{pageContext.abortReason ?? 'You must be logged in to view this page.'}</p>
3048
- <a href="/" class="error-link">Go home</a>
3049
- </div>
3050
- </Match>
3051
- <Match when={true}>
3052
- <div class="error-wrap">
3053
- <h1 class="heading-lg">Something went wrong</h1>
3054
- <p class="muted">{pageContext.abortReason ?? 'An unexpected error occurred.'}</p>
3055
- <a href="/" class="error-link">Go home</a>
3056
- </div>
3057
- </Match>
3058
- </Switch>
3059
- )
3060
- }
3061
- `;
3062
- }
3063
- // ─── AI chat page ─────────────────────────────────────────
3064
- function aiChatPageConfig(ctx) {
3065
- switch (ctx.primary) {
3066
- case 'vue':
3067
- return `import type { Config } from 'vike/types'
3068
- import vikeVue from 'vike-vue/config'
3069
-
3070
- export default {
3071
- extends: vikeVue,
3072
- } satisfies Config
3073
- `;
3074
- case 'solid':
3075
- return `import type { Config } from 'vike/types'
3076
- import vikeSolid from 'vike-solid/config'
3077
-
3078
- export default {
3079
- extends: vikeSolid,
3080
- } satisfies Config
3081
- `;
3082
- default:
3083
- return `import type { Config } from 'vike/types'
3084
- import vikeReact from 'vike-react/config'
3085
-
3086
- export default {
3087
- extends: vikeReact,
3088
- } satisfies Config
3089
- `;
3090
- }
3091
- }
3092
- function aiChatPage(ctx) {
3093
- switch (ctx.primary) {
3094
- case 'vue': return aiChatPageVue(ctx);
3095
- case 'solid': return aiChatPageSolid(ctx);
3096
- default: return aiChatPageReact(ctx);
3097
- }
3098
- }
3099
- function aiChatPageReact(ctx) {
3100
- const cssImport = `import '@/index.css'\n`;
3101
- return `${cssImport}import { useState, useRef, useEffect } from 'react'
3102
-
3103
- interface Message {
3104
- role: 'user' | 'assistant'
3105
- content: string
3106
- }
3107
-
3108
- export default function Page() {
3109
- const [messages, setMessages] = useState<Message[]>([])
3110
- const [input, setInput] = useState('')
3111
- const [loading, setLoading] = useState(false)
3112
- const scrollRef = useRef<HTMLDivElement>(null)
3113
-
3114
- useEffect(() => {
3115
- scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight)
3116
- }, [messages])
3117
-
3118
- async function send(e: React.FormEvent) {
3119
- e.preventDefault()
3120
- if (!input.trim() || loading) return
3121
-
3122
- const userMsg: Message = { role: 'user', content: input }
3123
- setMessages(prev => [...prev, userMsg])
3124
- setInput('')
3125
- setLoading(true)
3126
-
3127
- try {
3128
- const res = await fetch('/api/ai/chat', {
3129
- method: 'POST',
3130
- headers: { 'Content-Type': 'application/json' },
3131
- body: JSON.stringify({ messages: [...messages, userMsg] }),
3132
- })
3133
- const json = await res.json() as { message: string }
3134
- setMessages(prev => [...prev, { role: 'assistant', content: json.message }])
3135
- } catch {
3136
- setMessages(prev => [...prev, { role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' }])
3137
- } finally {
3138
- setLoading(false)
3139
- }
3140
- }
3141
-
3142
- return (
3143
- <div className="chat-wrap">
3144
- <div className="chat-column">
3145
- <div className="chat-header">
3146
- <h1 className="heading-lg">AI Chat</h1>
3147
- <a href="/" className="auth-link muted">← Home</a>
3148
- </div>
3149
-
3150
- <div ref={scrollRef} className="chat-log">
3151
- {messages.length === 0 && (
3152
- <p className="empty-state">Send a message to start chatting.</p>
3153
- )}
3154
- {messages.map((msg, i) => (
3155
- <div key={i} className={\`chat-row \${msg.role === 'user' ? 'is-user' : 'is-assistant'}\`}>
3156
- <div className={\`chat-bubble \${msg.role === 'user' ? 'is-user' : 'is-assistant'}\`}>
3157
- {msg.content}
3158
- </div>
3159
- </div>
3160
- ))}
3161
- {loading && (
3162
- <div className="chat-row is-assistant">
3163
- <div className="chat-bubble is-assistant muted">Thinking...</div>
3164
- </div>
3165
- )}
3166
- </div>
3167
-
3168
- <form onSubmit={send} className="form-inline chat-input">
3169
- <input
3170
- value={input}
3171
- onChange={e => setInput(e.target.value)}
3172
- placeholder="Type a message..."
3173
- disabled={loading}
3174
- className="form-input"
3175
- />
3176
- <button type="submit" disabled={loading} className="form-submit">
3177
- Send
3178
- </button>
3179
- </form>
3180
- </div>
3181
- </div>
3182
- )
3183
- }
3184
- `;
3185
- }
3186
- function aiChatPageVue(ctx) {
3187
- const cssImport = `import '@/index.css'\n`;
3188
- return `<script setup lang="ts">
3189
- ${cssImport}import { ref, nextTick } from 'vue'
3190
-
3191
- interface Message {
3192
- role: 'user' | 'assistant'
3193
- content: string
3194
- }
3195
-
3196
- const messages = ref<Message[]>([])
3197
- const input = ref('')
3198
- const loading = ref(false)
3199
- const scrollEl = ref<HTMLDivElement>()
3200
-
3201
- async function send(e: Event) {
3202
- e.preventDefault()
3203
- if (!input.value.trim() || loading.value) return
3204
-
3205
- const userMsg: Message = { role: 'user', content: input.value }
3206
- messages.value.push(userMsg)
3207
- input.value = ''
3208
- loading.value = true
3209
-
3210
- try {
3211
- const res = await fetch('/api/ai/chat', {
3212
- method: 'POST',
3213
- headers: { 'Content-Type': 'application/json' },
3214
- body: JSON.stringify({ messages: messages.value }),
3215
- })
3216
- const json = await res.json() as { message: string }
3217
- messages.value.push({ role: 'assistant', content: json.message })
3218
- } catch {
3219
- messages.value.push({ role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' })
3220
- } finally {
3221
- loading.value = false
3222
- await nextTick()
3223
- scrollEl.value?.scrollTo(0, scrollEl.value.scrollHeight)
3224
- }
3225
- }
3226
- </script>
3227
-
3228
- <template>
3229
- <div class="chat-wrap">
3230
- <div class="chat-column">
3231
- <div class="chat-header">
3232
- <h1 class="heading-lg">AI Chat</h1>
3233
- <a href="/" class="auth-link muted">← Home</a>
3234
- </div>
3235
-
3236
- <div ref="scrollEl" class="chat-log">
3237
- <p v-if="messages.length === 0" class="empty-state">Send a message to start chatting.</p>
3238
- <div v-for="(msg, i) in messages" :key="i" :class="['chat-row', msg.role === 'user' ? 'is-user' : 'is-assistant']">
3239
- <div :class="['chat-bubble', msg.role === 'user' ? 'is-user' : 'is-assistant']">
3240
- {{ msg.content }}
3241
- </div>
3242
- </div>
3243
- <div v-if="loading" class="chat-row is-assistant">
3244
- <div class="chat-bubble is-assistant muted">Thinking...</div>
3245
- </div>
3246
- </div>
3247
-
3248
- <form @submit="send" class="form-inline chat-input">
3249
- <input v-model="input" placeholder="Type a message..." :disabled="loading" class="form-input" />
3250
- <button type="submit" :disabled="loading" class="form-submit">Send</button>
3251
- </form>
3252
- </div>
3253
- </div>
3254
- </template>
3255
- `;
3256
- }
3257
- function aiChatPageSolid(ctx) {
3258
- const cssImport = `import '@/index.css'\n`;
3259
- return `${cssImport}import { createSignal, For, Show, onCleanup } from 'solid-js'
3260
-
3261
- interface Message {
3262
- role: 'user' | 'assistant'
3263
- content: string
3264
- }
3265
-
3266
- export default function Page() {
3267
- const [messages, setMessages] = createSignal<Message[]>([])
3268
- const [input, setInput] = createSignal('')
3269
- const [loading, setLoading] = createSignal(false)
3270
- let scrollEl: HTMLDivElement | undefined
3271
-
3272
- function scrollToBottom() {
3273
- setTimeout(() => scrollEl?.scrollTo(0, scrollEl.scrollHeight), 0)
3274
- }
3275
-
3276
- async function send(e: Event) {
3277
- e.preventDefault()
3278
- if (!input().trim() || loading()) return
3279
-
3280
- const userMsg: Message = { role: 'user', content: input() }
3281
- setMessages(prev => [...prev, userMsg])
3282
- setInput('')
3283
- setLoading(true)
3284
- scrollToBottom()
3285
-
3286
- try {
3287
- const res = await fetch('/api/ai/chat', {
3288
- method: 'POST',
3289
- headers: { 'Content-Type': 'application/json' },
3290
- body: JSON.stringify({ messages: [...messages()] }),
3291
- })
3292
- const json = await res.json() as { message: string }
3293
- setMessages(prev => [...prev, { role: 'assistant', content: json.message }])
3294
- } catch {
3295
- setMessages(prev => [...prev, { role: 'assistant', content: 'Something went wrong. Check your AI_PROVIDER and API key in .env.' }])
3296
- } finally {
3297
- setLoading(false)
3298
- scrollToBottom()
3299
- }
3300
- }
3301
-
3302
- return (
3303
- <div class="chat-wrap">
3304
- <div class="chat-column">
3305
- <div class="chat-header">
3306
- <h1 class="heading-lg">AI Chat</h1>
3307
- <a href="/" class="auth-link muted">← Home</a>
3308
- </div>
3309
-
3310
- <div ref={scrollEl} class="chat-log">
3311
- <Show when={messages().length === 0}>
3312
- <p class="empty-state">Send a message to start chatting.</p>
3313
- </Show>
3314
- <For each={messages()}>
3315
- {(msg) => (
3316
- <div class={\`chat-row \${msg.role === 'user' ? 'is-user' : 'is-assistant'}\`}>
3317
- <div class={\`chat-bubble \${msg.role === 'user' ? 'is-user' : 'is-assistant'}\`}>
3318
- {msg.content}
3319
- </div>
3320
- </div>
3321
- )}
3322
- </For>
3323
- <Show when={loading()}>
3324
- <div class="chat-row is-assistant">
3325
- <div class="chat-bubble is-assistant muted">Thinking...</div>
3326
- </div>
3327
- </Show>
3328
- </div>
3329
-
3330
- <form onSubmit={send} class="form-inline chat-input">
3331
- <input
3332
- value={input()}
3333
- onInput={e => setInput(e.currentTarget.value)}
3334
- placeholder="Type a message..."
3335
- disabled={loading()}
3336
- class="form-input"
3337
- />
3338
- <button type="submit" disabled={loading()} class="form-submit">
3339
- Send
3340
- </button>
3341
- </form>
3342
- </div>
3343
- </div>
3344
- )
3345
- }
3346
- `;
3347
- }
3348
- // ─── Demo pages (secondary frameworks) ─────────────────────
3349
- function demoPageConfig(fw) {
3350
- switch (fw) {
3351
- case 'vue':
3352
- return `import type { Config } from 'vike/types'
3353
- import vikeVue from 'vike-vue/config'
3354
-
3355
- export default {
3356
- extends: vikeVue,
3357
- } satisfies Config
3358
- `;
3359
- case 'solid':
3360
- return `import type { Config } from 'vike/types'
3361
- import vikeSolid from 'vike-solid/config'
3362
-
3363
- export default {
3364
- extends: vikeSolid,
3365
- } satisfies Config
3366
- `;
3367
- default: // react
3368
- return `import type { Config } from 'vike/types'
3369
- import vikeReact from 'vike-react/config'
3370
-
3371
- export default {
3372
- extends: vikeReact,
3373
- } satisfies Config
3374
- `;
3375
- }
3376
- }
3377
- function demoPage(fw, ctx) {
3378
- const { primary } = ctx;
3379
- switch (fw) {
3380
- case 'react':
3381
- return `export default function Page() {
3382
- return (
3383
- <div className="error-wrap">
3384
- <h1 className="heading-lg">Hello from React</h1>
3385
- <p className="muted">React demo page — running alongside ${primary}.</p>
3386
- <a href="/" className="auth-link muted">← Back to home</a>
3387
- </div>
3388
- )
3389
- }
3390
- `;
3391
- case 'vue':
3392
- return `<script setup lang="ts">
3393
- import '@/index.css'
3394
- </script>
3395
-
3396
- <template>
3397
- <div class="error-wrap">
3398
- <h1 class="heading-lg">Hello from Vue</h1>
3399
- <p class="muted">Vue demo page — running alongside ${primary}.</p>
3400
- <a href="/" class="auth-link muted">← Back to home</a>
3401
- </div>
3402
- </template>
3403
- `;
3404
- case 'solid':
3405
- return `import '@/index.css'
3406
-
3407
- export default function Page() {
3408
- return (
3409
- <div class="error-wrap">
3410
- <h1 class="heading-lg">Hello from Solid</h1>
3411
- <p class="muted">Solid demo page — running alongside ${primary}.</p>
3412
- <a href="/" class="auth-link muted">← Back to home</a>
3413
- </div>
3414
- )
3415
- }
3416
- `;
3417
- }
3418
- }
3419
- // ─── Demos ─────────────────────────────────────────────────
3420
- function demosIndexView(ctx) {
3421
- const cards = [
3422
- {
3423
- title: 'Contact form',
3424
- desc: 'CSRF-protected form with Zod validation. Demonstrates getCsrfToken() and FormRequest-style error handling.',
3425
- href: '/demos/contact',
3426
- pkgs: '@rudderjs/middleware · @rudderjs/core',
3427
- show: true,
3428
- },
3429
- {
3430
- title: 'WebSocket chat',
3431
- desc: 'Real-time chat + presence using @rudderjs/broadcast — multi-channel pub/sub over a single WebSocket connection.',
3432
- href: '/demos/ws',
3433
- pkgs: '@rudderjs/broadcast',
3434
- show: ctx.packages.broadcast,
3435
- },
3436
- {
3437
- title: 'Collaborative editor',
3438
- desc: 'Yjs CRDT live document with awareness cursors. Open in two tabs to see real-time sync over @rudderjs/sync.',
3439
- href: '/demos/live',
3440
- pkgs: '@rudderjs/sync',
3441
- show: ctx.packages.sync,
3442
- },
3443
- ].filter(c => c.show);
3444
- const cardsJsx = cards.map(c => ` <a key="${c.href}" href="${c.href}" className="feature-card">
3445
- <h3 className="feature-title">${c.title}</h3>
3446
- <p className="feature-desc">${c.desc}</p>
3447
- <p className="feature-desc" style={{ fontSize: '0.7rem', opacity: 0.7 }}>${c.pkgs}</p>
3448
- </a>`).join('\n');
3449
- return `import '@/index.css'
3450
-
3451
- // Override the id-derived URL ('/demos/index') so SPA nav matches the controller ('/demos').
3452
- export const route = '/demos'
3453
-
3454
- export default function DemosIndex() {
3455
- return (
3456
- <div className="page">
3457
- <nav className="page-nav">
3458
- <div className="brand">
3459
- <span className="brand-dot" />
3460
- RudderJS
3461
- </div>
3462
- <div className="nav-right">
3463
- <a href="/" className="nav-link">Home</a>
3464
- </div>
3465
- </nav>
3466
-
3467
- <section className="hero">
3468
- <h1 className="hero-title">Demos</h1>
3469
- <p className="hero-lead">
3470
- Small, focused examples of what the framework can do. Each one is a single
3471
- controller returning <code className="inline-code">view('demos.&lt;name&gt;')</code>.
3472
- </p>
3473
- </section>
3474
-
3475
- <section className="feature-section">
3476
- <div className="feature-grid">
3477
- ${cardsJsx}
3478
- </div>
3479
- </section>
3480
- </div>
3481
- )
3482
- }
3483
- `;
3484
- }
3485
- function demosContactView(ctx) {
3486
- // CSRF is only enforced when @rudderjs/auth is installed (CsrfMiddleware
3487
- // wraps the /api/contact route in routes/api.ts). Without auth, the
3488
- // unprotected form just succeeds.
3489
- const sendsCsrf = ctx.packages.auth ? 'true' : 'false';
3490
- return `import '@/index.css'
3491
- import { useState } from 'react'
3492
- import { getCsrfToken } from '@rudderjs/middleware'
3493
-
3494
- interface FormFields { name: string; email: string; message: string }
3495
- interface FormErrors { name?: string; email?: string; message?: string }
3496
-
3497
- export default function ContactDemo() {
3498
- const [fields, setFields] = useState<FormFields>({ name: '', email: '', message: '' })
3499
- const [errors, setErrors] = useState<FormErrors>({})
3500
- const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
3501
- const [message, setMessage] = useState('')
3502
-
3503
- function setField(key: keyof FormFields, value: string) {
3504
- setFields(f => ({ ...f, [key]: value }))
3505
- if (errors[key]) setErrors(e => ({ ...e, [key]: undefined }))
3506
- }
3507
-
3508
- async function submit(e: React.FormEvent) {
3509
- e.preventDefault()
3510
- setStatus('loading')
3511
- setErrors({})
3512
-
3513
- const headers: Record<string, string> = { 'Content-Type': 'application/json' }
3514
- if (${sendsCsrf}) headers['X-CSRF-Token'] = getCsrfToken()
3515
-
3516
- const res = await fetch('/api/contact', {
3517
- method: 'POST',
3518
- headers,
3519
- body: JSON.stringify(fields),
3520
- })
3521
- const data = await res.json() as { ok?: boolean; message?: string; errors?: FormErrors }
3522
-
3523
- if (res.ok) {
3524
- setStatus('success')
3525
- setMessage(data.message ?? 'Thanks!')
3526
- setFields({ name: '', email: '', message: '' })
3527
- } else if (res.status === 422) {
3528
- setStatus('error')
3529
- setErrors(data.errors ?? {})
3530
- } else {
3531
- setStatus('error')
3532
- setMessage(\`\${res.status} — \${data.message ?? 'Request failed.'}\`)
3533
- }
3534
- }
3535
-
3536
- return (
3537
- <div className="page">
3538
- <nav className="page-nav">
3539
- <div className="brand">
3540
- <span className="brand-dot" />
3541
- RudderJS
3542
- </div>
3543
- <div className="nav-right">
3544
- <a href="/demos" className="nav-link">← Demos</a>
3545
- </div>
3546
- </nav>
3547
-
3548
- <section className="hero">
3549
- <h1 className="hero-title">Contact</h1>
3550
- <p className="hero-lead">
3551
- POSTs to <code className="inline-code">/api/contact</code>${ctx.packages.auth
3552
- ? ' with an X-CSRF-Token header.'
3553
- : '. Add @rudderjs/auth to require CSRF.'}{' '}
3554
- Server-side validated with Zod.
3555
- </p>
3556
- </section>
3557
-
3558
- <section className="feature-section" style={{ maxWidth: '32rem', margin: '0 auto' }}>
3559
- <form onSubmit={submit} className="form-card">
3560
- <div>
3561
- <label className="form-label" htmlFor="name">Name</label>
3562
- <input id="name" className="form-input" value={fields.name}
3563
- onChange={e => setField('name', e.target.value)} />
3564
- {errors.name && <p className="form-error">{errors.name}</p>}
3565
- </div>
3566
- <div>
3567
- <label className="form-label" htmlFor="email">Email</label>
3568
- <input id="email" type="email" className="form-input" value={fields.email}
3569
- onChange={e => setField('email', e.target.value)} />
3570
- {errors.email && <p className="form-error">{errors.email}</p>}
3571
- </div>
3572
- <div>
3573
- <label className="form-label" htmlFor="message">Message</label>
3574
- <textarea id="message" rows={4} className="form-input" value={fields.message}
3575
- onChange={e => setField('message', e.target.value)} />
3576
- {errors.message && <p className="form-error">{errors.message}</p>}
3577
- </div>
3578
- <button type="submit" className="form-submit" disabled={status === 'loading'}>
3579
- {status === 'loading' ? 'Sending…' : 'Send message'}
3580
- </button>
3581
- {status === 'success' && <p className="form-success">{message}</p>}
3582
- {status === 'error' && message && <p className="form-error">{message}</p>}
3583
- </form>
3584
- </section>
3585
- </div>
3586
- )
3587
- }
3588
- `;
3589
- }
3590
- function demosWsView() {
3591
- return `import '@/index.css'
3592
- import { useEffect, useRef, useState } from 'react'
3593
- import { BKSocket } from '@/BKSocket'
3594
-
3595
- type Message = { user: string; text: string; ts: number }
3596
- type Member = { id: string; name: string }
3597
-
3598
- function getWsUrl() {
3599
- if (typeof window === 'undefined') return ''
3600
- return \`ws://\${window.location.host}/ws\`
3601
- }
3602
-
3603
- export default function WsDemo() {
3604
- const [me, setMe] = useState('')
3605
- const socketRef = useRef<BKSocket | null>(null)
3606
- const [connected, setConnected] = useState(false)
3607
- const [messages, setMessages] = useState<Message[]>([])
3608
- const [members, setMembers] = useState<Member[]>([])
3609
- const [input, setInput] = useState('')
3610
-
3611
- useEffect(() => { setMe(\`User-\${Math.floor(Math.random() * 1000)}\`) }, [])
3612
-
3613
- useEffect(() => {
3614
- if (!me) return
3615
- const socket = new BKSocket(getWsUrl())
3616
- socketRef.current = socket
3617
-
3618
- const chat = socket.channel('chat')
3619
- chat.on('message', d => setMessages(prev => [...prev, d as Message]))
3620
-
3621
- const room = socket.presence('lobby', 'demo-token')
3622
- room.on('presence.members', d => {
3623
- setMembers(d as Member[])
3624
- setConnected(true)
3625
- })
3626
- room.on('presence.joined', d => {
3627
- const u = d as Member
3628
- setMembers(prev => [...prev.filter(m => m.id !== u.id), u])
3629
- })
3630
- room.on('presence.left', d => {
3631
- const id = (d as { id: string }).id
3632
- setMembers(prev => prev.filter(m => m.id !== id))
3633
- })
3634
-
3635
- return () => { socket.close() }
3636
- }, [me])
3637
-
3638
- async function send() {
3639
- if (!input.trim()) return
3640
- await fetch('/api/ws/broadcast', {
3641
- method: 'POST',
3642
- headers: { 'Content-Type': 'application/json' },
3643
- body: JSON.stringify({ user: me, text: input.trim() }),
3644
- })
3645
- setInput('')
3646
- }
3647
-
3648
- return (
3649
- <div className="page">
3650
- <nav className="page-nav">
3651
- <div className="brand">
3652
- <span className="brand-dot" />
3653
- RudderJS
3654
- </div>
3655
- <div className="nav-right">
3656
- <a href="/demos" className="nav-link">← Demos</a>
3657
- </div>
3658
- </nav>
3659
-
3660
- <section className="hero">
3661
- <h1 className="hero-title">WebSocket chat</h1>
3662
- <p className="hero-lead">
3663
- Pub/sub + presence over a single WebSocket. Connected as <strong>{me}</strong>.{' '}
3664
- {connected ? '🟢 connected' : '⚪ connecting…'}
3665
- </p>
3666
- </section>
3667
-
3668
- <section className="feature-section" style={{ maxWidth: '40rem', margin: '0 auto' }}>
3669
- <p className="form-label">Members ({members.length})</p>
3670
- <ul style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
3671
- {members.map(m => (
3672
- <li key={m.id} className="inline-code">{m.name}</li>
3673
- ))}
3674
- </ul>
3675
-
3676
- <div style={{ minHeight: '12rem', marginBottom: '1rem' }}>
3677
- {messages.map((m, i) => (
3678
- <p key={i} style={{ margin: '0.25rem 0' }}>
3679
- <strong>{m.user}:</strong> {m.text}
3680
- </p>
3681
- ))}
3682
- </div>
3683
-
3684
- <form onSubmit={e => { e.preventDefault(); void send() }} style={{ display: 'flex', gap: '0.5rem' }}>
3685
- <input className="form-input" value={input}
3686
- onChange={e => setInput(e.target.value)} placeholder="Say something…" />
3687
- <button type="submit" className="form-submit" style={{ width: 'auto' }}>Send</button>
3688
- </form>
3689
- </section>
3690
- </div>
3691
- )
3692
- }
3693
- `;
3694
- }
3695
- function demosLiveView() {
3696
- return `import '@/index.css'
3697
- import { useEffect, useRef, useState } from 'react'
3698
- import * as Y from 'yjs'
3699
- import { WebsocketProvider } from 'y-websocket'
3700
-
3701
- function getWsUrl() {
3702
- if (typeof window === 'undefined') return ''
3703
- return \`ws://\${window.location.host}/ws-sync\`
3704
- }
3705
-
3706
- export default function LiveDemo() {
3707
- const [connected, setConnected] = useState(false)
3708
- const [text, setText] = useState('')
3709
- const [users, setUsers] = useState<{ name: string; color: string }[]>([])
3710
- const [myName] = useState(() => \`User-\${Math.floor(Math.random() * 1000)}\`)
3711
- const [myColor] = useState(() => \`hsl(\${Math.floor(Math.random() * 360)}, 70%, 50%)\`)
3712
-
3713
- const docRef = useRef<Y.Doc | null>(null)
3714
- const provRef = useRef<WebsocketProvider | null>(null)
3715
- const textareaRef = useRef<HTMLTextAreaElement>(null)
3716
-
3717
- useEffect(() => {
3718
- const doc = new Y.Doc()
3719
- const ytext = doc.getText('content')
3720
- const provider = new WebsocketProvider(getWsUrl(), 'live-demo', doc)
3721
-
3722
- docRef.current = doc
3723
- provRef.current = provider
3724
-
3725
- ytext.observe(() => setText(ytext.toString()))
3726
- provider.on('status', ({ status }: { status: string }) => setConnected(status === 'connected'))
3727
- provider.awareness.setLocalStateField('user', { name: myName, color: myColor })
3728
-
3729
- const syncUsers = () => {
3730
- const states = [...provider.awareness.getStates().values()] as { user?: { name: string; color: string } }[]
3731
- setUsers(states.map(s => s.user).filter((u): u is { name: string; color: string } => Boolean(u)))
3732
- }
3733
- provider.awareness.on('change', syncUsers)
3734
- syncUsers()
3735
-
3736
- return () => { provider.destroy(); doc.destroy() }
3737
- }, [myName, myColor])
3738
-
3739
- function onChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
3740
- const ytext = docRef.current?.getText('content')
3741
- if (!ytext) return
3742
- docRef.current?.transact(() => {
3743
- ytext.delete(0, ytext.length)
3744
- ytext.insert(0, e.target.value)
3745
- })
3746
- }
3747
-
3748
- return (
3749
- <div className="page">
3750
- <nav className="page-nav">
3751
- <div className="brand">
3752
- <span className="brand-dot" />
3753
- RudderJS
3754
- </div>
3755
- <div className="nav-right">
3756
- <a href="/demos" className="nav-link">← Demos</a>
3757
- </div>
3758
- </nav>
3759
-
3760
- <section className="hero">
3761
- <h1 className="hero-title">Collaborative editor</h1>
3762
- <p className="hero-lead">
3763
- Yjs CRDT over @rudderjs/sync. Open this page in two tabs to see real-time updates.{' '}
3764
- {connected ? '🟢 connected' : '⚪ connecting…'}
3765
- </p>
3766
- </section>
3767
-
3768
- <section className="feature-section" style={{ maxWidth: '40rem', margin: '0 auto' }}>
3769
- <p className="form-label">Active users:</p>
3770
- <ul style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
3771
- {users.map((u, i) => (
3772
- <li key={i} className="inline-code" style={{ borderLeft: \`3px solid \${u.color}\`, paddingLeft: '0.5rem' }}>
3773
- {u.name}
3774
- </li>
3775
- ))}
3776
- </ul>
3777
- <textarea
3778
- ref={textareaRef}
3779
- className="form-input"
3780
- rows={10}
3781
- value={text}
3782
- onChange={onChange}
3783
- placeholder="Start typing…"
3784
- />
3785
- </section>
3786
- </div>
3787
- )
3788
- }
3789
- `;
3790
- }
3791
- function bkSocketSource() {
3792
- return `// BKSocket — RudderJS WebSocket client
3793
- //
3794
- // Multiplexes channels and presence rooms over a single WebSocket connection.
3795
- // Mirrors the API expected by @rudderjs/broadcast on the server.
3796
-
3797
- type Listener = (data: unknown) => void
3798
-
3799
- class Channel {
3800
- private listeners = new Map<string, Set<Listener>>()
3801
-
3802
- constructor(
3803
- private readonly socket: BKSocket,
3804
- public readonly name: string,
3805
- ) {}
3806
-
3807
- on(event: string, fn: Listener): this {
3808
- if (!this.listeners.has(event)) this.listeners.set(event, new Set())
3809
- this.listeners.get(event)!.add(fn)
3810
- return this
3811
- }
3812
-
3813
- off(event: string, fn: Listener): this {
3814
- this.listeners.get(event)?.delete(fn)
3815
- return this
3816
- }
3817
-
3818
- /** @internal — invoked by BKSocket on incoming messages */
3819
- receive(event: string, data: unknown) {
3820
- this.listeners.get(event)?.forEach(fn => fn(data))
3821
- }
3822
-
3823
- /** @internal — invoked by BKSocket to (re)subscribe after connect */
3824
- subscribe() {
3825
- this.socket.send({ type: 'subscribe', channel: this.name })
3826
- }
3827
- }
3828
-
3829
- class Presence extends Channel {
3830
- constructor(socket: BKSocket, name: string, private readonly token: string) {
3831
- super(socket, name)
3832
- }
3833
-
3834
- override subscribe() {
3835
- this.socket.send({ type: 'presence.join', channel: this.name, token: this.token })
3836
- }
3837
- }
3838
-
3839
- export class BKSocket {
3840
- private ws?: WebSocket
3841
- private readonly channels = new Map<string, Channel>()
3842
- private reconnectTimer?: ReturnType<typeof setTimeout>
3843
-
3844
- constructor(private readonly url: string) {
3845
- this.connect()
3846
- }
3847
-
3848
- channel(name: string): Channel {
3849
- let ch = this.channels.get(name)
3850
- if (!ch) { ch = new Channel(this, name); this.channels.set(name, ch); ch.subscribe() }
3851
- return ch
3852
- }
3853
-
3854
- presence(name: string, token: string): Channel {
3855
- let ch = this.channels.get(name)
3856
- if (!ch) { ch = new Presence(this, name, token); this.channels.set(name, ch); ch.subscribe() }
3857
- return ch
3858
- }
3859
-
3860
- send(payload: Record<string, unknown>) {
3861
- if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(payload))
3862
- }
3863
-
3864
- close() {
3865
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
3866
- this.ws?.close()
3867
- }
3868
-
3869
- private connect() {
3870
- this.ws = new WebSocket(this.url)
3871
- this.ws.onopen = () => this.channels.forEach(ch => ch.subscribe())
3872
- this.ws.onmessage = (e) => {
3873
- try {
3874
- const msg = JSON.parse(e.data as string) as { channel?: string; event?: string; data?: unknown }
3875
- if (msg.channel && msg.event) this.channels.get(msg.channel)?.receive(msg.event, msg.data)
3876
- } catch { /* ignore non-JSON frames */ }
3877
- }
3878
- this.ws.onclose = () => {
3879
- this.reconnectTimer = setTimeout(() => this.connect(), 1500)
3880
- }
3881
- }
3882
- }
3883
- `;
190
+ export function shouldScaffoldDemo(ctx, name) {
191
+ if (ctx.primary !== 'react')
192
+ return false;
193
+ if (!ctx.demos.includes(name))
194
+ return false;
195
+ // Defense-in-depth: even if a stale demo id slipped through (e.g. user kept it
196
+ // checked while unticking the underlying package), filter by registry rules.
197
+ const allowed = availableDemos(ctx.orm, ctx.packages).map(d => d.value);
198
+ return allowed.includes(name);
199
+ }
200
+ export function shouldScaffoldAnyDemo(ctx) {
201
+ if (ctx.primary !== 'react')
202
+ return false;
203
+ return ctx.demos.some(name => shouldScaffoldDemo(ctx, name));
3884
204
  }
3885
205
  //# sourceMappingURL=templates.js.map