create-velox-app 0.6.31 → 0.6.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/GUIDE.md +230 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/index.js +14 -4
  5. package/dist/templates/auth.js +10 -0
  6. package/dist/templates/index.js +30 -1
  7. package/dist/templates/placeholders.js +0 -3
  8. package/dist/templates/rsc-auth.d.ts +12 -0
  9. package/dist/templates/rsc-auth.js +208 -0
  10. package/dist/templates/rsc.js +40 -1
  11. package/dist/templates/shared/css-generator.d.ts +26 -0
  12. package/dist/templates/shared/css-generator.js +553 -0
  13. package/dist/templates/shared/index.d.ts +3 -0
  14. package/dist/templates/shared/index.js +3 -0
  15. package/dist/templates/shared/rsc-styles.d.ts +54 -0
  16. package/dist/templates/shared/rsc-styles.js +68 -0
  17. package/dist/templates/shared/theme.d.ts +133 -0
  18. package/dist/templates/shared/theme.js +141 -0
  19. package/dist/templates/spa.js +10 -0
  20. package/dist/templates/trpc.js +10 -0
  21. package/dist/templates/types.d.ts +2 -1
  22. package/dist/templates/types.js +6 -0
  23. package/package.json +6 -3
  24. package/src/templates/source/api/config/database.ts +13 -32
  25. package/src/templates/source/api/docker-compose.yml +21 -0
  26. package/src/templates/source/root/CLAUDE.auth.md +6 -0
  27. package/src/templates/source/root/CLAUDE.default.md +6 -0
  28. package/src/templates/source/rsc/CLAUDE.md +56 -2
  29. package/src/templates/source/rsc/app/actions/posts.ts +1 -1
  30. package/src/templates/source/rsc/app/actions/users.ts +111 -20
  31. package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
  32. package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
  33. package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
  34. package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
  35. package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
  36. package/src/templates/source/rsc/docker-compose.yml +21 -0
  37. package/src/templates/source/rsc/package.json +3 -3
  38. package/src/templates/source/rsc/src/api/database.ts +13 -32
  39. package/src/templates/source/rsc/src/api/handler.ts +1 -1
  40. package/src/templates/source/rsc/src/entry.client.tsx +65 -18
  41. package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
  42. package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
  43. package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
  44. package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
  45. package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
  46. package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
  47. package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
  48. package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
  49. package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
  50. package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
  51. package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
  52. package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
  53. package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
  54. package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
  55. package/src/templates/source/rsc-auth/app.config.ts +12 -0
  56. package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
  57. package/src/templates/source/rsc-auth/env.example +11 -0
  58. package/src/templates/source/rsc-auth/gitignore +34 -0
  59. package/src/templates/source/rsc-auth/package.json +44 -0
  60. package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
  61. package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
  62. package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
  63. package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
  64. package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
  65. package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
  66. package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
  67. package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
  68. package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
  69. package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
  70. package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
  71. package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
  72. package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
  73. package/src/templates/source/rsc-auth/tsconfig.json +24 -0
  74. package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Client Entry Point
3
+ *
4
+ * Hydrates React client components on the client side.
5
+ * Only pages marked with 'use client' need hydration.
6
+ * Server-only components remain static HTML.
7
+ *
8
+ * IMPORTANT: The hydration tree must match exactly what the server rendered.
9
+ * Server renders: <div id="root"><MinimalContent><Page /></MinimalContent></div>
10
+ * Client hydrates: <MinimalContent><Page /></MinimalContent>
11
+ */
12
+
13
+ import { StrictMode } from 'react';
14
+ import { hydrateRoot } from 'react-dom/client';
15
+
16
+ // Import content layouts (shared between server and client)
17
+ import MinimalContent from '../app/layouts/minimal-content.tsx';
18
+ // Import client pages (only pages with 'use client' directive)
19
+ import LoginPage from '../app/pages/auth/login.tsx';
20
+ import RegisterPage from '../app/pages/auth/register.tsx';
21
+
22
+ // Route mapping for client pages with their layouts
23
+ // The layout must match what the server rendered inside <div id="root">
24
+ interface ClientRoute {
25
+ Page: React.ComponentType;
26
+ Layout: React.ComponentType<{ children: React.ReactNode }>;
27
+ }
28
+
29
+ const clientRoutes: Record<string, ClientRoute> = {
30
+ '/auth/login': { Page: LoginPage, Layout: MinimalContent },
31
+ '/auth/register': { Page: RegisterPage, Layout: MinimalContent },
32
+ };
33
+
34
+ const rootElement = document.getElementById('root');
35
+ const pathname = window.location.pathname;
36
+
37
+ // Only hydrate if this is a client page
38
+ const match = clientRoutes[pathname];
39
+
40
+ if (rootElement && match) {
41
+ const { Page, Layout } = match;
42
+
43
+ // Hydrate with the exact same tree structure as server rendered
44
+ hydrateRoot(
45
+ rootElement,
46
+ <StrictMode>
47
+ <Layout>
48
+ <Page />
49
+ </Layout>
50
+ </StrictMode>,
51
+ {
52
+ onRecoverableError: (error: unknown) => {
53
+ // Log hydration mismatches in development
54
+ if (process.env.NODE_ENV !== 'production') {
55
+ console.warn('[VeloxTS] Hydration warning:', error);
56
+ }
57
+ },
58
+ }
59
+ );
60
+ } else {
61
+ // Server-only page, no hydration needed
62
+ console.debug('[VeloxTS] Server-rendered page, no client hydration needed');
63
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Server Entry Point
3
+ *
4
+ * Handles server-side rendering of React Server Components.
5
+ * Uses h3 event handler for Vinxi compatibility.
6
+ */
7
+
8
+ import { PassThrough } from 'node:stream';
9
+
10
+ import type { ComponentType, ReactElement, ReactNode } from 'react';
11
+ import { renderToPipeableStream } from 'react-dom/server';
12
+
13
+ // Static imports for layout components
14
+ import DashboardLayout from '../app/layouts/dashboard.tsx';
15
+ import MarketingLayout from '../app/layouts/marketing.tsx';
16
+ import MinimalLayout from '../app/layouts/minimal.tsx';
17
+ import RootLayout from '../app/layouts/root.tsx';
18
+ // Static imports for page components
19
+ import NotFoundPage from '../app/pages/_not-found.tsx';
20
+ import LoginPage from '../app/pages/auth/login.tsx';
21
+ import RegisterPage from '../app/pages/auth/register.tsx';
22
+ import DashboardPage from '../app/pages/dashboard/index.tsx';
23
+ import HomePage from '../app/pages/index.tsx';
24
+ import UsersPage from '../app/pages/users.tsx';
25
+
26
+ // Page props type
27
+ interface PageProps {
28
+ params: Record<string, string>;
29
+ searchParams: Record<string, string | string[]>;
30
+ }
31
+
32
+ // Layout props type
33
+ interface LayoutProps {
34
+ children: ReactNode;
35
+ params: Record<string, string>;
36
+ }
37
+
38
+ type LayoutComponent = ComponentType<LayoutProps>;
39
+
40
+ // Route definition with pattern matching
41
+ interface RouteDefinition {
42
+ pattern: string;
43
+ component: ComponentType<PageProps>;
44
+ regex: RegExp;
45
+ paramNames: string[];
46
+ layouts: LayoutComponent[];
47
+ }
48
+
49
+ // Route match result
50
+ interface RouteMatch {
51
+ component: ComponentType<PageProps>;
52
+ params: Record<string, string>;
53
+ layouts: LayoutComponent[];
54
+ }
55
+
56
+ /**
57
+ * Compiles a file-based route pattern into a regex
58
+ */
59
+ function compileRoute(pattern: string): { regex: RegExp; paramNames: string[] } {
60
+ const paramNames: string[] = [];
61
+
62
+ const regexStr = pattern
63
+ .replace(/\[\.\.\.(\w+)\]/g, (_, name) => {
64
+ paramNames.push(name);
65
+ return '___CATCH_ALL___';
66
+ })
67
+ .replace(/\[(\w+)\]/g, (_, name) => {
68
+ paramNames.push(name);
69
+ return '___DYNAMIC___';
70
+ })
71
+ .replace(/[.+?^${}()|\\]/g, '\\$&')
72
+ .replace(/___CATCH_ALL___/g, '(.+)')
73
+ .replace(/___DYNAMIC___/g, '([^/]+)');
74
+
75
+ const regex = new RegExp(`^${regexStr}$`);
76
+ return { regex, paramNames };
77
+ }
78
+
79
+ /**
80
+ * Creates a route definition from pattern and component
81
+ */
82
+ function defineRoute(
83
+ pattern: string,
84
+ component: ComponentType<PageProps>,
85
+ layouts: LayoutComponent[] = [RootLayout]
86
+ ): RouteDefinition {
87
+ const { regex, paramNames } = compileRoute(pattern);
88
+ return { pattern, component, regex, paramNames, layouts };
89
+ }
90
+
91
+ /**
92
+ * Wraps a page element with its layout chain
93
+ */
94
+ function wrapWithLayouts(
95
+ pageElement: ReactElement,
96
+ layouts: LayoutComponent[],
97
+ params: Record<string, string>
98
+ ): ReactElement {
99
+ return layouts.reduceRight(
100
+ (children, Layout) => (
101
+ <Layout key={Layout.name || Layout.displayName || 'layout'} params={params}>
102
+ {children}
103
+ </Layout>
104
+ ),
105
+ pageElement
106
+ );
107
+ }
108
+
109
+ // Route registry (order matters - more specific first)
110
+ const routes: RouteDefinition[] = [
111
+ // Home
112
+ defineRoute('/', HomePage, [RootLayout, MarketingLayout]),
113
+ defineRoute('/index', HomePage, [RootLayout, MarketingLayout]),
114
+
115
+ // Auth pages (minimal layout - replaces root)
116
+ defineRoute('/auth/login', LoginPage, [MinimalLayout]),
117
+ defineRoute('/auth/register', RegisterPage, [MinimalLayout]),
118
+
119
+ // Protected pages (dashboard layout)
120
+ defineRoute('/dashboard', DashboardPage, [RootLayout, DashboardLayout]),
121
+
122
+ // Public pages
123
+ defineRoute('/users', UsersPage, [RootLayout, MarketingLayout]),
124
+ ];
125
+
126
+ console.log(
127
+ '[SSR] Available routes:',
128
+ routes.map((r) => r.pattern)
129
+ );
130
+
131
+ // H3 event type for Vinxi
132
+ interface H3Event {
133
+ node: {
134
+ req: { url?: string };
135
+ res: {
136
+ statusCode?: number;
137
+ setHeader: (name: string, value: string) => void;
138
+ end: (data?: unknown) => void;
139
+ write: (chunk: unknown) => boolean;
140
+ };
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Match a pathname against registered routes
146
+ */
147
+ function matchRoute(pathname: string): RouteMatch | null {
148
+ console.log('[SSR] Matching:', pathname);
149
+
150
+ for (const route of routes) {
151
+ const match = pathname.match(route.regex);
152
+ if (match) {
153
+ const params: Record<string, string> = {};
154
+ route.paramNames.forEach((name, index) => {
155
+ params[name] = match[index + 1];
156
+ });
157
+
158
+ console.log('[SSR] Matched:', route.pattern, 'params:', params);
159
+ return { component: route.component, params, layouts: route.layouts };
160
+ }
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Parse search params from URL
168
+ */
169
+ function parseSearchParams(url: URL): Record<string, string | string[]> {
170
+ const searchParams: Record<string, string | string[]> = {};
171
+ url.searchParams.forEach((value, key) => {
172
+ const existing = searchParams[key];
173
+ if (existing) {
174
+ searchParams[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
175
+ } else {
176
+ searchParams[key] = value;
177
+ }
178
+ });
179
+ return searchParams;
180
+ }
181
+
182
+ /**
183
+ * H3-compatible SSR handler for Vinxi
184
+ */
185
+ export default async function ssrHandler(event: H3Event): Promise<void> {
186
+ const res = event.node.res;
187
+ const url = new URL(event.node.req.url || '/', 'http://localhost');
188
+ const pathname = url.pathname;
189
+
190
+ console.log('[SSR] Handling:', pathname);
191
+
192
+ const match = matchRoute(pathname);
193
+
194
+ if (!match) {
195
+ const pageProps: PageProps = {
196
+ params: {},
197
+ searchParams: parseSearchParams(url),
198
+ };
199
+
200
+ const notFoundElement = <NotFoundPage {...pageProps} />;
201
+ const html = wrapWithLayouts(notFoundElement, [RootLayout], {});
202
+
203
+ const passThrough = new PassThrough();
204
+ const { pipe } = renderToPipeableStream(html, {
205
+ onShellReady() {
206
+ res.statusCode = 404;
207
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
208
+ passThrough.on('data', (chunk) => res.write(chunk));
209
+ passThrough.on('end', () => res.end());
210
+ pipe(passThrough);
211
+ },
212
+ onShellError(error) {
213
+ console.error('[SSR] 404 Shell error:', error);
214
+ res.statusCode = 404;
215
+ res.setHeader('Content-Type', 'text/html');
216
+ res.end(
217
+ `<!DOCTYPE html><html><body><h1>404 - Page Not Found</h1><p>Path: ${pathname}</p></body></html>`
218
+ );
219
+ },
220
+ });
221
+ return;
222
+ }
223
+
224
+ const { component: PageComponent, params, layouts } = match;
225
+
226
+ const pageProps: PageProps = {
227
+ params,
228
+ searchParams: parseSearchParams(url),
229
+ };
230
+
231
+ try {
232
+ const pageElement = <PageComponent {...pageProps} />;
233
+ const html = wrapWithLayouts(pageElement, layouts, params);
234
+
235
+ const passThrough = new PassThrough();
236
+ const { pipe } = renderToPipeableStream(html, {
237
+ onShellReady() {
238
+ res.statusCode = 200;
239
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
240
+ passThrough.on('data', (chunk) => res.write(chunk));
241
+ passThrough.on('end', () => res.end());
242
+ pipe(passThrough);
243
+ },
244
+ onShellError(error) {
245
+ console.error('[SSR] Shell error:', error);
246
+ res.statusCode = 500;
247
+ res.setHeader('Content-Type', 'text/html');
248
+ res.end(`<h1>Render Error</h1><pre>${error}</pre>`);
249
+ },
250
+ onError(error) {
251
+ console.error('[SSR] Render error:', error);
252
+ },
253
+ });
254
+ } catch (error) {
255
+ console.error('[SSR] Handler error:', error);
256
+ res.statusCode = 500;
257
+ res.setHeader('Content-Type', 'text/html');
258
+ res.end(
259
+ `<h1>Server Error</h1><pre>${error instanceof Error ? error.stack : String(error)}</pre>`
260
+ );
261
+ }
262
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "jsx": "react-jsx",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "paths": {
17
+ "@/*": ["./src/*"],
18
+ "@/app/*": ["./app/*"]
19
+ },
20
+ "types": ["node", "vite/client"]
21
+ },
22
+ "include": ["src/**/*", "src/**/*.d.ts", "app/**/*", "*.config.ts"],
23
+ "exclude": ["node_modules", "dist"]
24
+ }
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # check-client-imports.sh
3
+ # Checks for server-only imports in client bundle files
4
+ #
5
+ # Usage: ./scripts/check-client-imports.sh
6
+
7
+ set -e
8
+
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ NC='\033[0m' # No Color
12
+
13
+ # Patterns that should NOT appear in client files
14
+ FORBIDDEN_IMPORTS=(
15
+ "@veloxts/web/server"
16
+ "@veloxts/web/actions"
17
+ "@veloxts/core"
18
+ "@veloxts/orm"
19
+ "@veloxts/auth"
20
+ "@/api/database"
21
+ "@/api/procedures"
22
+ )
23
+
24
+ # Client file patterns to check
25
+ CLIENT_FILES=(
26
+ "src/entry.client.tsx"
27
+ "src/entry.client.ts"
28
+ )
29
+
30
+ # Find files with 'use client' directive
31
+ USE_CLIENT_FILES=$(grep -rl "^'use client'" app/ 2>/dev/null || true)
32
+
33
+ errors=0
34
+
35
+ echo "Checking for server imports in client files..."
36
+ echo ""
37
+
38
+ # Check explicit client entry files
39
+ for file in "${CLIENT_FILES[@]}"; do
40
+ if [ -f "$file" ]; then
41
+ for pattern in "${FORBIDDEN_IMPORTS[@]}"; do
42
+ if grep -q "from ['\"]${pattern}" "$file" 2>/dev/null; then
43
+ echo -e "${RED}ERROR:${NC} $file imports '$pattern'"
44
+ echo " Server-only imports cannot be used in client bundles."
45
+ ((errors++))
46
+ fi
47
+ done
48
+ fi
49
+ done
50
+
51
+ # Check files with 'use client' directive
52
+ for file in $USE_CLIENT_FILES; do
53
+ for pattern in "${FORBIDDEN_IMPORTS[@]}"; do
54
+ if grep -q "from ['\"]${pattern}" "$file" 2>/dev/null; then
55
+ echo -e "${RED}ERROR:${NC} $file imports '$pattern'"
56
+ echo " This file has 'use client' but imports server-only code."
57
+ ((errors++))
58
+ fi
59
+ done
60
+ done
61
+
62
+ echo ""
63
+
64
+ if [ $errors -gt 0 ]; then
65
+ echo -e "${RED}Found $errors server import(s) in client files.${NC}"
66
+ echo ""
67
+ echo "Solutions:"
68
+ echo "1. Use @veloxts/web/client for browser-safe exports"
69
+ echo "2. Move server logic to files with 'use server' directive"
70
+ echo "3. Import server actions (not server modules) in client components"
71
+ exit 1
72
+ else
73
+ echo -e "${GREEN}No forbidden imports found in client files.${NC}"
74
+ exit 0
75
+ fi