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.
- package/dist/index.js +100 -20
- package/dist/index.js.map +1 -1
- package/dist/templates/app/auth-controller.d.ts +2 -0
- package/dist/templates/app/auth-controller.d.ts.map +1 -0
- package/dist/templates/app/auth-controller.js +51 -0
- package/dist/templates/app/auth-controller.js.map +1 -0
- package/dist/templates/app/mcp-echo-server.d.ts +2 -0
- package/dist/templates/app/mcp-echo-server.d.ts.map +1 -0
- package/dist/templates/app/mcp-echo-server.js +13 -0
- package/dist/templates/app/mcp-echo-server.js.map +1 -0
- package/dist/templates/app/mcp-echo-tool.d.ts +2 -0
- package/dist/templates/app/mcp-echo-tool.d.ts.map +1 -0
- package/dist/templates/app/mcp-echo-tool.js +20 -0
- package/dist/templates/app/mcp-echo-tool.js.map +1 -0
- package/dist/templates/app/service-provider.d.ts +3 -0
- package/dist/templates/app/service-provider.d.ts.map +1 -0
- package/dist/templates/app/service-provider.js +27 -0
- package/dist/templates/app/service-provider.js.map +1 -0
- package/dist/templates/app/user-model.d.ts +2 -0
- package/dist/templates/app/user-model.d.ts.map +1 -0
- package/dist/templates/app/user-model.js +19 -0
- package/dist/templates/app/user-model.js.map +1 -0
- package/dist/templates/bootstrap/app.d.ts +3 -0
- package/dist/templates/bootstrap/app.d.ts.map +1 -0
- package/dist/templates/bootstrap/app.js +41 -0
- package/dist/templates/bootstrap/app.js.map +1 -0
- package/dist/templates/bootstrap/providers.d.ts +3 -0
- package/dist/templates/bootstrap/providers.d.ts.map +1 -0
- package/dist/templates/bootstrap/providers.js +27 -0
- package/dist/templates/bootstrap/providers.js.map +1 -0
- package/dist/templates/configs/ai.d.ts +2 -0
- package/dist/templates/configs/ai.d.ts.map +1 -0
- package/dist/templates/configs/ai.js +32 -0
- package/dist/templates/configs/ai.js.map +1 -0
- package/dist/templates/configs/app.d.ts +2 -0
- package/dist/templates/configs/app.d.ts.map +1 -0
- package/dist/templates/configs/app.js +12 -0
- package/dist/templates/configs/app.js.map +1 -0
- package/dist/templates/configs/auth.d.ts +3 -0
- package/dist/templates/configs/auth.d.ts.map +1 -0
- package/dist/templates/configs/auth.js +16 -0
- package/dist/templates/configs/auth.js.map +1 -0
- package/dist/templates/configs/cache.d.ts +2 -0
- package/dist/templates/configs/cache.d.ts.map +1 -0
- package/dist/templates/configs/cache.js +28 -0
- package/dist/templates/configs/cache.js.map +1 -0
- package/dist/templates/configs/cashier.d.ts +2 -0
- package/dist/templates/configs/cashier.d.ts.map +1 -0
- package/dist/templates/configs/cashier.js +22 -0
- package/dist/templates/configs/cashier.js.map +1 -0
- package/dist/templates/configs/crypt.d.ts +2 -0
- package/dist/templates/configs/crypt.d.ts.map +1 -0
- package/dist/templates/configs/crypt.js +16 -0
- package/dist/templates/configs/crypt.js.map +1 -0
- package/dist/templates/configs/database.d.ts +3 -0
- package/dist/templates/configs/database.d.ts.map +1 -0
- package/dist/templates/configs/database.js +28 -0
- package/dist/templates/configs/database.js.map +1 -0
- package/dist/templates/configs/hash.d.ts +2 -0
- package/dist/templates/configs/hash.d.ts.map +1 -0
- package/dist/templates/configs/hash.js +12 -0
- package/dist/templates/configs/hash.js.map +1 -0
- package/dist/templates/configs/horizon.d.ts +2 -0
- package/dist/templates/configs/horizon.d.ts.map +1 -0
- package/dist/templates/configs/horizon.js +30 -0
- package/dist/templates/configs/horizon.js.map +1 -0
- package/dist/templates/configs/index.d.ts +3 -0
- package/dist/templates/configs/index.d.ts.map +1 -0
- package/dist/templates/configs/index.js +96 -0
- package/dist/templates/configs/index.js.map +1 -0
- package/dist/templates/configs/localization.d.ts +2 -0
- package/dist/templates/configs/localization.d.ts.map +1 -0
- package/dist/templates/configs/localization.js +13 -0
- package/dist/templates/configs/localization.js.map +1 -0
- package/dist/templates/configs/log.d.ts +2 -0
- package/dist/templates/configs/log.d.ts.map +1 -0
- package/dist/templates/configs/log.js +40 -0
- package/dist/templates/configs/log.js.map +1 -0
- package/dist/templates/configs/mail.d.ts +2 -0
- package/dist/templates/configs/mail.d.ts.map +1 -0
- package/dist/templates/configs/mail.js +33 -0
- package/dist/templates/configs/mail.js.map +1 -0
- package/dist/templates/configs/passport.d.ts +2 -0
- package/dist/templates/configs/passport.d.ts.map +1 -0
- package/dist/templates/configs/passport.js +22 -0
- package/dist/templates/configs/passport.js.map +1 -0
- package/dist/templates/configs/pennant.d.ts +2 -0
- package/dist/templates/configs/pennant.d.ts.map +1 -0
- package/dist/templates/configs/pennant.js +16 -0
- package/dist/templates/configs/pennant.js.map +1 -0
- package/dist/templates/configs/pulse.d.ts +2 -0
- package/dist/templates/configs/pulse.d.ts.map +1 -0
- package/dist/templates/configs/pulse.js +21 -0
- package/dist/templates/configs/pulse.js.map +1 -0
- package/dist/templates/configs/queue.d.ts +2 -0
- package/dist/templates/configs/queue.d.ts.map +1 -0
- package/dist/templates/configs/queue.js +28 -0
- package/dist/templates/configs/queue.js.map +1 -0
- package/dist/templates/configs/sanctum.d.ts +2 -0
- package/dist/templates/configs/sanctum.d.ts.map +1 -0
- package/dist/templates/configs/sanctum.js +19 -0
- package/dist/templates/configs/sanctum.js.map +1 -0
- package/dist/templates/configs/server.d.ts +2 -0
- package/dist/templates/configs/server.d.ts.map +1 -0
- package/dist/templates/configs/server.js +15 -0
- package/dist/templates/configs/server.js.map +1 -0
- package/dist/templates/configs/session.d.ts +2 -0
- package/dist/templates/configs/session.d.ts.map +1 -0
- package/dist/templates/configs/session.js +26 -0
- package/dist/templates/configs/session.js.map +1 -0
- package/dist/templates/configs/socialite.d.ts +2 -0
- package/dist/templates/configs/socialite.d.ts.map +1 -0
- package/dist/templates/configs/socialite.js +27 -0
- package/dist/templates/configs/socialite.js.map +1 -0
- package/dist/templates/configs/storage.d.ts +2 -0
- package/dist/templates/configs/storage.d.ts.map +1 -0
- package/dist/templates/configs/storage.js +35 -0
- package/dist/templates/configs/storage.js.map +1 -0
- package/dist/templates/configs/sync.d.ts +3 -0
- package/dist/templates/configs/sync.d.ts.map +1 -0
- package/dist/templates/configs/sync.js +17 -0
- package/dist/templates/configs/sync.js.map +1 -0
- package/dist/templates/configs/telescope.d.ts +2 -0
- package/dist/templates/configs/telescope.d.ts.map +1 -0
- package/dist/templates/configs/telescope.js +25 -0
- package/dist/templates/configs/telescope.js.map +1 -0
- package/dist/templates/css/index.d.ts +3 -0
- package/dist/templates/css/index.d.ts.map +1 -0
- package/dist/templates/css/index.js +140 -0
- package/dist/templates/css/index.js.map +1 -0
- package/dist/templates/css/plain.d.ts +2 -0
- package/dist/templates/css/plain.d.ts.map +1 -0
- package/dist/templates/css/plain.js +373 -0
- package/dist/templates/css/plain.js.map +1 -0
- package/dist/templates/css/tailwind.d.ts +2 -0
- package/dist/templates/css/tailwind.d.ts.map +1 -0
- package/dist/templates/css/tailwind.js +176 -0
- package/dist/templates/css/tailwind.js.map +1 -0
- package/dist/templates/demos/bk-socket.d.ts +2 -0
- package/dist/templates/demos/bk-socket.d.ts.map +1 -0
- package/dist/templates/demos/bk-socket.js +95 -0
- package/dist/templates/demos/bk-socket.js.map +1 -0
- package/dist/templates/demos/contact.d.ts +3 -0
- package/dist/templates/demos/contact.d.ts.map +1 -0
- package/dist/templates/demos/contact.js +106 -0
- package/dist/templates/demos/contact.js.map +1 -0
- package/dist/templates/demos/index-view.d.ts +3 -0
- package/dist/templates/demos/index-view.d.ts.map +1 -0
- package/dist/templates/demos/index-view.js +67 -0
- package/dist/templates/demos/index-view.js.map +1 -0
- package/dist/templates/demos/live.d.ts +2 -0
- package/dist/templates/demos/live.d.ts.map +1 -0
- package/dist/templates/demos/live.js +97 -0
- package/dist/templates/demos/live.js.map +1 -0
- package/dist/templates/demos/registry.d.ts +13 -0
- package/dist/templates/demos/registry.d.ts.map +1 -0
- package/dist/templates/demos/registry.js +15 -0
- package/dist/templates/demos/registry.js.map +1 -0
- package/dist/templates/demos/ws.d.ts +2 -0
- package/dist/templates/demos/ws.d.ts.map +1 -0
- package/dist/templates/demos/ws.js +106 -0
- package/dist/templates/demos/ws.js.map +1 -0
- package/dist/templates/env.d.ts +7 -0
- package/dist/templates/env.d.ts.map +1 -0
- package/dist/templates/env.js +127 -0
- package/dist/templates/env.js.map +1 -0
- package/dist/templates/package-json.d.ts +3 -0
- package/dist/templates/package-json.d.ts.map +1 -0
- package/dist/templates/package-json.js +195 -0
- package/dist/templates/package-json.js.map +1 -0
- package/dist/templates/package-managers.d.ts +14 -0
- package/dist/templates/package-managers.d.ts.map +1 -0
- package/dist/templates/package-managers.js +49 -0
- package/dist/templates/package-managers.js.map +1 -0
- package/dist/templates/pages/ai-chat.d.ts +7 -0
- package/dist/templates/pages/ai-chat.d.ts.map +1 -0
- package/dist/templates/pages/ai-chat.js +285 -0
- package/dist/templates/pages/ai-chat.js.map +1 -0
- package/dist/templates/pages/demo.d.ts +4 -0
- package/dist/templates/pages/demo.d.ts.map +1 -0
- package/dist/templates/pages/demo.js +71 -0
- package/dist/templates/pages/demo.js.map +1 -0
- package/dist/templates/pages/error.d.ts +7 -0
- package/dist/templates/pages/error.d.ts.map +1 -0
- package/dist/templates/pages/error.js +148 -0
- package/dist/templates/pages/error.js.map +1 -0
- package/dist/templates/pages/index.d.ts +9 -0
- package/dist/templates/pages/index.d.ts.map +1 -0
- package/dist/templates/pages/index.js +311 -0
- package/dist/templates/pages/index.js.map +1 -0
- package/dist/templates/prisma/auth.d.ts +2 -0
- package/dist/templates/prisma/auth.d.ts.map +1 -0
- package/dist/templates/prisma/auth.js +22 -0
- package/dist/templates/prisma/auth.js.map +1 -0
- package/dist/templates/prisma/base.d.ts +3 -0
- package/dist/templates/prisma/base.d.ts.map +1 -0
- package/dist/templates/prisma/base.js +14 -0
- package/dist/templates/prisma/base.js.map +1 -0
- package/dist/templates/prisma/config.d.ts +3 -0
- package/dist/templates/prisma/config.d.ts.map +1 -0
- package/dist/templates/prisma/config.js +15 -0
- package/dist/templates/prisma/config.js.map +1 -0
- package/dist/templates/prisma/notification.d.ts +2 -0
- package/dist/templates/prisma/notification.d.ts.map +1 -0
- package/dist/templates/prisma/notification.js +16 -0
- package/dist/templates/prisma/notification.js.map +1 -0
- package/dist/templates/prisma/passport.d.ts +2 -0
- package/dist/templates/prisma/passport.d.ts.map +1 -0
- package/dist/templates/prisma/passport.js +69 -0
- package/dist/templates/prisma/passport.js.map +1 -0
- package/dist/templates/routes/api.d.ts +3 -0
- package/dist/templates/routes/api.d.ts.map +1 -0
- package/dist/templates/routes/api.js +118 -0
- package/dist/templates/routes/api.js.map +1 -0
- package/dist/templates/routes/console.d.ts +2 -0
- package/dist/templates/routes/console.d.ts.map +1 -0
- package/dist/templates/routes/console.js +22 -0
- package/dist/templates/routes/console.js.map +1 -0
- package/dist/templates/routes/web.d.ts +4 -0
- package/dist/templates/routes/web.d.ts.map +1 -0
- package/dist/templates/routes/web.js +108 -0
- package/dist/templates/routes/web.js.map +1 -0
- package/dist/templates/server.d.ts +2 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +10 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/tsconfig.d.ts +3 -0
- package/dist/templates/tsconfig.d.ts.map +1 -0
- package/dist/templates/tsconfig.js +33 -0
- package/dist/templates/tsconfig.js.map +1 -0
- package/dist/templates/views/welcome.d.ts +6 -0
- package/dist/templates/views/welcome.d.ts.map +1 -0
- package/dist/templates/views/welcome.js +396 -0
- package/dist/templates/views/welcome.js.map +1 -0
- package/dist/templates/vite.d.ts +3 -0
- package/dist/templates/vite.d.ts.map +1 -0
- package/dist/templates/vite.js +61 -0
- package/dist/templates/vite.js.map +1 -0
- package/dist/templates.d.ts +28 -17
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +99 -3779
- package/dist/templates.js.map +1 -1
- package/package.json +3 -3
package/dist/templates.js
CHANGED
|
@@ -1,51 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
|
150
|
-
if (
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
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
|
-
*
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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's developer experience, Vike's performance, Node's ecosystem.
|
|
2639
|
-
This page is served by a controller, rendered through{' '}
|
|
2640
|
-
<code className="inline-code">view('welcome')</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.<name>')</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
|