create-velox-app 0.4.14 → 0.6.25

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 (113) hide show
  1. package/README.md +2 -43
  2. package/dist/cli.js +23 -13
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +26 -8
  5. package/dist/index.js.map +1 -1
  6. package/dist/templates/auth.d.ts.map +1 -1
  7. package/dist/templates/auth.js +24 -0
  8. package/dist/templates/auth.js.map +1 -1
  9. package/dist/templates/fullstack.d.ts +15 -0
  10. package/dist/templates/fullstack.d.ts.map +1 -0
  11. package/dist/templates/fullstack.js +110 -0
  12. package/dist/templates/fullstack.js.map +1 -0
  13. package/dist/templates/index.d.ts +1 -1
  14. package/dist/templates/index.d.ts.map +1 -1
  15. package/dist/templates/index.js +27 -5
  16. package/dist/templates/index.js.map +1 -1
  17. package/dist/templates/placeholders.d.ts +6 -1
  18. package/dist/templates/placeholders.d.ts.map +1 -1
  19. package/dist/templates/placeholders.js +15 -5
  20. package/dist/templates/placeholders.js.map +1 -1
  21. package/dist/templates/rsc.d.ts +15 -0
  22. package/dist/templates/rsc.d.ts.map +1 -0
  23. package/dist/templates/rsc.js +192 -0
  24. package/dist/templates/rsc.js.map +1 -0
  25. package/dist/templates/shared/root.d.ts +1 -0
  26. package/dist/templates/shared/root.d.ts.map +1 -1
  27. package/dist/templates/shared/root.js +4 -0
  28. package/dist/templates/shared/root.js.map +1 -1
  29. package/dist/templates/spa.d.ts +12 -0
  30. package/dist/templates/spa.d.ts.map +1 -0
  31. package/dist/templates/spa.js +101 -0
  32. package/dist/templates/spa.js.map +1 -0
  33. package/dist/templates/trpc.d.ts.map +1 -1
  34. package/dist/templates/trpc.js +16 -0
  35. package/dist/templates/trpc.js.map +1 -1
  36. package/dist/templates/types.d.ts +14 -1
  37. package/dist/templates/types.d.ts.map +1 -1
  38. package/dist/templates/types.js +35 -10
  39. package/dist/templates/types.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/templates/source/api/config/auth.ts +2 -2
  42. package/src/templates/source/api/config/database.ts +44 -10
  43. package/src/templates/source/api/index.auth.ts +10 -16
  44. package/src/templates/source/api/index.default.ts +10 -9
  45. package/src/templates/source/api/index.trpc.ts +9 -28
  46. package/src/templates/source/api/package.auth.json +7 -6
  47. package/src/templates/source/api/package.default.json +5 -4
  48. package/src/templates/source/api/prisma/schema.auth.prisma +3 -2
  49. package/src/templates/source/api/prisma/schema.default.prisma +3 -2
  50. package/src/templates/source/api/prisma.config.ts +7 -1
  51. package/src/templates/source/api/procedures/auth.ts +38 -66
  52. package/src/templates/source/api/procedures/health.ts +4 -9
  53. package/src/templates/source/api/procedures/users.auth.ts +7 -11
  54. package/src/templates/source/api/procedures/users.default.ts +7 -11
  55. package/src/templates/source/api/router.auth.ts +31 -0
  56. package/src/templates/source/api/router.default.ts +29 -0
  57. package/src/templates/source/api/router.trpc.ts +37 -0
  58. package/src/templates/source/api/router.types.auth.ts +88 -0
  59. package/src/templates/source/api/router.types.default.ts +73 -0
  60. package/src/templates/source/api/router.types.trpc.ts +73 -0
  61. package/src/templates/source/api/routes.auth.ts +66 -0
  62. package/src/templates/source/api/routes.default.ts +53 -0
  63. package/src/templates/source/api/schemas/auth.ts +79 -0
  64. package/src/templates/source/api/schemas/health.ts +21 -0
  65. package/src/templates/source/api/schemas/user.ts +62 -12
  66. package/src/templates/source/api/utils/auth.ts +157 -0
  67. package/src/templates/source/root/.cursorrules +187 -0
  68. package/src/templates/source/root/CLAUDE.auth.md +264 -0
  69. package/src/templates/source/root/CLAUDE.default.md +185 -0
  70. package/src/templates/source/root/package.json +7 -1
  71. package/src/templates/source/rsc/CLAUDE.md +104 -0
  72. package/src/templates/source/rsc/app/actions/posts.ts +93 -0
  73. package/src/templates/source/rsc/app/actions/users.ts +83 -0
  74. package/src/templates/source/rsc/app/layouts/dashboard.tsx +127 -0
  75. package/src/templates/source/rsc/app/layouts/marketing.tsx +25 -0
  76. package/src/templates/source/rsc/app/layouts/minimal.tsx +30 -0
  77. package/src/templates/source/rsc/app/layouts/root.tsx +241 -0
  78. package/src/templates/source/rsc/app/pages/(dashboard)/profile.tsx +71 -0
  79. package/src/templates/source/rsc/app/pages/(dashboard)/settings.tsx +104 -0
  80. package/src/templates/source/rsc/app/pages/(marketing)/about.tsx +52 -0
  81. package/src/templates/source/rsc/app/pages/_not-found.tsx +149 -0
  82. package/src/templates/source/rsc/app/pages/docs/[...slug].tsx +211 -0
  83. package/src/templates/source/rsc/app/pages/index.tsx +50 -0
  84. package/src/templates/source/rsc/app/pages/print.tsx +80 -0
  85. package/src/templates/source/rsc/app/pages/users/[id]/posts/[postId].tsx +89 -0
  86. package/src/templates/source/rsc/app/pages/users/[id]/posts/index.tsx +76 -0
  87. package/src/templates/source/rsc/app/pages/users/[id]/posts/new.tsx +79 -0
  88. package/src/templates/source/rsc/app/pages/users/[id].tsx +64 -0
  89. package/src/templates/source/rsc/app/pages/users/_layout.tsx +104 -0
  90. package/src/templates/source/rsc/app/pages/users.tsx +44 -0
  91. package/src/templates/source/rsc/app.config.ts +12 -0
  92. package/src/templates/source/rsc/env.example +6 -0
  93. package/src/templates/source/rsc/gitignore +34 -0
  94. package/src/templates/source/rsc/package.json +41 -0
  95. package/src/templates/source/rsc/prisma/schema.prisma +34 -0
  96. package/src/templates/source/rsc/prisma.config.ts +22 -0
  97. package/src/templates/source/rsc/public/favicon.svg +4 -0
  98. package/src/templates/source/rsc/src/api/database.ts +72 -0
  99. package/src/templates/source/rsc/src/api/handler.ts +53 -0
  100. package/src/templates/source/rsc/src/api/procedures/health.ts +48 -0
  101. package/src/templates/source/rsc/src/api/procedures/posts.ts +151 -0
  102. package/src/templates/source/rsc/src/api/procedures/users.ts +87 -0
  103. package/src/templates/source/rsc/src/api/schemas/post.ts +53 -0
  104. package/src/templates/source/rsc/src/api/schemas/user.ts +38 -0
  105. package/src/templates/source/rsc/src/entry.client.tsx +28 -0
  106. package/src/templates/source/rsc/src/entry.server.tsx +304 -0
  107. package/src/templates/source/rsc/tsconfig.json +24 -0
  108. package/src/templates/source/web/App.module.css +1 -1
  109. package/src/templates/source/web/api.ts +8 -1
  110. package/src/templates/source/web/main.tsx +4 -4
  111. package/src/templates/source/web/package.json +6 -6
  112. package/src/templates/source/web/routes/__root.tsx +2 -2
  113. package/src/templates/source/web/routes/index.auth.tsx +3 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * User Procedures
3
+ *
4
+ * CRUD operations for user management.
5
+ * Uses direct db import for proper PrismaClient typing.
6
+ */
7
+
8
+ import { defineProcedures, procedure } from '@veloxts/router';
9
+ import { z } from 'zod';
10
+
11
+ import { db } from '../database.js';
12
+ import { CreateUserSchema, UpdateUserSchema, UserSchema } from '../schemas/user.js';
13
+
14
+ export const userProcedures = defineProcedures('users', {
15
+ /**
16
+ * List all users
17
+ * GET /api/users
18
+ */
19
+ listUsers: procedure()
20
+ .output(z.array(UserSchema))
21
+ .query(async () => {
22
+ return db.user.findMany({
23
+ orderBy: { createdAt: 'desc' },
24
+ });
25
+ }),
26
+
27
+ /**
28
+ * Get a single user by ID
29
+ * GET /api/users/:id
30
+ */
31
+ getUser: procedure()
32
+ .input(z.object({ id: z.string().uuid() }))
33
+ .output(UserSchema)
34
+ .query(async ({ input }) => {
35
+ const user = await db.user.findUnique({
36
+ where: { id: input.id },
37
+ });
38
+
39
+ if (!user) {
40
+ throw Object.assign(new Error('User not found'), { statusCode: 404 });
41
+ }
42
+
43
+ return user;
44
+ }),
45
+
46
+ /**
47
+ * Create a new user
48
+ * POST /api/users
49
+ */
50
+ createUser: procedure()
51
+ .input(CreateUserSchema)
52
+ .output(UserSchema)
53
+ .mutation(async ({ input }) => {
54
+ return db.user.create({
55
+ data: input,
56
+ });
57
+ }),
58
+
59
+ /**
60
+ * Update an existing user
61
+ * PUT /api/users/:id
62
+ */
63
+ updateUser: procedure()
64
+ .input(UpdateUserSchema.extend({ id: z.string().uuid() }))
65
+ .output(UserSchema)
66
+ .mutation(async ({ input }) => {
67
+ const { id, ...data } = input;
68
+ return db.user.update({
69
+ where: { id },
70
+ data,
71
+ });
72
+ }),
73
+
74
+ /**
75
+ * Delete a user
76
+ * DELETE /api/users/:id
77
+ */
78
+ deleteUser: procedure()
79
+ .input(z.object({ id: z.string().uuid() }))
80
+ .output(z.object({ success: z.boolean() }))
81
+ .mutation(async ({ input }) => {
82
+ await db.user.delete({
83
+ where: { id: input.id },
84
+ });
85
+ return { success: true };
86
+ }),
87
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Post Schemas
3
+ *
4
+ * Zod schemas for post validation.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * Post entity schema
11
+ */
12
+ export const PostSchema = z.object({
13
+ id: z.string().uuid(),
14
+ title: z.string(),
15
+ content: z.string().nullable(),
16
+ published: z.boolean(),
17
+ createdAt: z.date(),
18
+ updatedAt: z.date(),
19
+ userId: z.string().uuid(),
20
+ });
21
+
22
+ /**
23
+ * Post with user relation
24
+ */
25
+ export const PostWithUserSchema = PostSchema.extend({
26
+ user: z.object({
27
+ id: z.string().uuid(),
28
+ name: z.string(),
29
+ }),
30
+ });
31
+
32
+ /**
33
+ * Create post input schema
34
+ */
35
+ export const CreatePostSchema = z.object({
36
+ title: z.string().min(1, 'Title is required'),
37
+ content: z.string().optional(),
38
+ published: z.boolean().optional().default(false),
39
+ });
40
+
41
+ /**
42
+ * Update post input schema (all fields optional)
43
+ */
44
+ export const UpdatePostSchema = z.object({
45
+ title: z.string().min(1).optional(),
46
+ content: z.string().optional(),
47
+ published: z.boolean().optional(),
48
+ });
49
+
50
+ export type Post = z.infer<typeof PostSchema>;
51
+ export type PostWithUser = z.infer<typeof PostWithUserSchema>;
52
+ export type CreatePost = z.infer<typeof CreatePostSchema>;
53
+ export type UpdatePost = z.infer<typeof UpdatePostSchema>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * User Schemas
3
+ *
4
+ * Zod schemas for user validation.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * User entity schema
11
+ */
12
+ export const UserSchema = z.object({
13
+ id: z.string().uuid(),
14
+ name: z.string(),
15
+ email: z.string().email(),
16
+ createdAt: z.date(),
17
+ updatedAt: z.date(),
18
+ });
19
+
20
+ /**
21
+ * Create user input schema
22
+ */
23
+ export const CreateUserSchema = z.object({
24
+ name: z.string().min(1, 'Name is required'),
25
+ email: z.string().email('Invalid email address'),
26
+ });
27
+
28
+ /**
29
+ * Update user input schema (all fields optional)
30
+ */
31
+ export const UpdateUserSchema = z.object({
32
+ name: z.string().min(1).optional(),
33
+ email: z.string().email().optional(),
34
+ });
35
+
36
+ export type User = z.infer<typeof UserSchema>;
37
+ export type CreateUser = z.infer<typeof CreateUserSchema>;
38
+ export type UpdateUser = z.infer<typeof UpdateUserSchema>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Client Entry Point
3
+ *
4
+ * Hydrates the React application on the client side.
5
+ * For SSR apps, the server streams the component tree as HTML.
6
+ * We hydrate the existing DOM to attach event handlers.
7
+ */
8
+
9
+ import { hydrateRoot } from '@veloxts/web';
10
+
11
+ const rootElement = document.getElementById('root');
12
+
13
+ if (rootElement?.firstElementChild) {
14
+ // Hydrate the server-rendered React tree
15
+ // This attaches event handlers and makes the app interactive
16
+ hydrateRoot(rootElement, rootElement.firstElementChild as unknown as React.ReactNode, {
17
+ onRecoverableError: (error: unknown) => {
18
+ // Log hydration mismatches in development
19
+ if (process.env.NODE_ENV !== 'production') {
20
+ console.warn('[VeloxTS] Hydration warning:', error);
21
+ }
22
+ },
23
+ });
24
+ } else {
25
+ console.error(
26
+ '[VeloxTS] Root element or content not found. Ensure #root exists with server-rendered content.'
27
+ );
28
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Server Entry Point
3
+ *
4
+ * Handles server-side rendering of React Server Components.
5
+ * Uses h3 event handler for Vinxi compatibility.
6
+ * Supports dynamic routes with [param] and [...catchAll] patterns.
7
+ */
8
+
9
+ import { PassThrough } from 'node:stream';
10
+
11
+ import type { ComponentType, ReactElement, ReactNode } from 'react';
12
+ import { renderToPipeableStream } from 'react-dom/server';
13
+
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
+ // Static imports for layout components
18
+ import RootLayout from '../app/layouts/root.tsx';
19
+ import NotFoundPage from '../app/pages/_not-found.tsx';
20
+ import ProfilePage from '../app/pages/(dashboard)/profile.tsx';
21
+ import SettingsPage from '../app/pages/(dashboard)/settings.tsx';
22
+ import AboutPage from '../app/pages/(marketing)/about.tsx';
23
+ import DocsPage from '../app/pages/docs/[...slug].tsx';
24
+ // Static imports for page components (using .tsx extension for Vite)
25
+ import HomePage from '../app/pages/index.tsx';
26
+ import PrintPage from '../app/pages/print.tsx';
27
+ import UsersLayout from '../app/pages/users/_layout.tsx';
28
+ import PostDetailPage from '../app/pages/users/[id]/posts/[postId].tsx';
29
+ import UserPostsPage from '../app/pages/users/[id]/posts/index.tsx';
30
+ import NewPostPage from '../app/pages/users/[id]/posts/new.tsx';
31
+ import UserDetailPage from '../app/pages/users/[id].tsx';
32
+ import UsersPage from '../app/pages/users.tsx';
33
+
34
+ // Page props type
35
+ interface PageProps {
36
+ params: Record<string, string>;
37
+ searchParams: Record<string, string | string[]>;
38
+ }
39
+
40
+ // Layout props type
41
+ interface LayoutProps {
42
+ children: ReactNode;
43
+ params: Record<string, string>;
44
+ }
45
+
46
+ type LayoutComponent = ComponentType<LayoutProps>;
47
+
48
+ // Route definition with pattern matching
49
+ interface RouteDefinition {
50
+ pattern: string;
51
+ component: ComponentType<PageProps>;
52
+ // Compiled regex and param names for matching
53
+ regex: RegExp;
54
+ paramNames: string[];
55
+ // Layout chain for this route (outermost first)
56
+ layouts: LayoutComponent[];
57
+ }
58
+
59
+ // Route match result
60
+ interface RouteMatch {
61
+ component: ComponentType<PageProps>;
62
+ params: Record<string, string>;
63
+ layouts: LayoutComponent[];
64
+ }
65
+
66
+ /**
67
+ * Compiles a file-based route pattern into a regex
68
+ * Supports:
69
+ * - Static segments: /users -> matches exactly /users
70
+ * - Dynamic segments: /users/[id] -> matches /users/123, extracts id=123
71
+ * - Catch-all: /posts/[...slug] -> matches /posts/a/b/c, extracts slug=a/b/c
72
+ */
73
+ function compileRoute(pattern: string): { regex: RegExp; paramNames: string[] } {
74
+ const paramNames: string[] = [];
75
+
76
+ // Process pattern replacements BEFORE escaping to preserve [...] and [] syntax
77
+ const regexStr = pattern
78
+ // Handle catch-all [...param] first (before escaping dots)
79
+ .replace(/\[\.\.\.(\w+)\]/g, (_, name) => {
80
+ paramNames.push(name);
81
+ return '___CATCH_ALL___';
82
+ })
83
+ // Handle dynamic [param]
84
+ .replace(/\[(\w+)\]/g, (_, name) => {
85
+ paramNames.push(name);
86
+ return '___DYNAMIC___';
87
+ })
88
+ // Now escape special regex chars
89
+ .replace(/[.+?^${}()|\\]/g, '\\$&')
90
+ // Restore placeholders with actual regex patterns
91
+ .replace(/___CATCH_ALL___/g, '(.+)')
92
+ .replace(/___DYNAMIC___/g, '([^/]+)');
93
+
94
+ // Exact match
95
+ const regex = new RegExp(`^${regexStr}$`);
96
+ return { regex, paramNames };
97
+ }
98
+
99
+ /**
100
+ * Creates a route definition from pattern and component
101
+ */
102
+ function defineRoute(
103
+ pattern: string,
104
+ component: ComponentType<PageProps>,
105
+ layouts: LayoutComponent[] = [RootLayout]
106
+ ): RouteDefinition {
107
+ const { regex, paramNames } = compileRoute(pattern);
108
+ return { pattern, component, regex, paramNames, layouts };
109
+ }
110
+
111
+ /**
112
+ * Wraps a page element with its layout chain.
113
+ * Layouts are applied from outermost (root) to innermost.
114
+ * Uses reduceRight to build the nested component tree.
115
+ */
116
+ function wrapWithLayouts(
117
+ pageElement: ReactElement,
118
+ layouts: LayoutComponent[],
119
+ params: Record<string, string>
120
+ ): ReactElement {
121
+ return layouts.reduceRight(
122
+ (children, Layout) => (
123
+ <Layout key={Layout.name || Layout.displayName || 'layout'} params={params}>
124
+ {children}
125
+ </Layout>
126
+ ),
127
+ pageElement
128
+ );
129
+ }
130
+
131
+ // Route registry with patterns (order matters - more specific first)
132
+ const routes: RouteDefinition[] = [
133
+ // Home
134
+ defineRoute('/', HomePage),
135
+ defineRoute('/index', HomePage),
136
+
137
+ // Users section (with segment layout)
138
+ defineRoute('/users', UsersPage, [RootLayout, UsersLayout]),
139
+ // Static 'new' before dynamic [postId] for proper precedence
140
+ defineRoute('/users/[id]/posts/new', NewPostPage, [RootLayout, UsersLayout]),
141
+ defineRoute('/users/[id]/posts/[postId]', PostDetailPage, [RootLayout, UsersLayout]),
142
+ defineRoute('/users/[id]/posts', UserPostsPage, [RootLayout, UsersLayout]),
143
+ defineRoute('/users/[id]', UserDetailPage, [RootLayout, UsersLayout]),
144
+
145
+ // Dashboard group pages (with dashboard layout)
146
+ defineRoute('/settings', SettingsPage, [RootLayout, DashboardLayout]),
147
+ defineRoute('/profile', ProfilePage, [RootLayout, DashboardLayout]),
148
+
149
+ // Marketing group pages
150
+ defineRoute('/about', AboutPage, [RootLayout, MarketingLayout]),
151
+
152
+ // Documentation catch-all
153
+ defineRoute('/docs/[...slug]', DocsPage),
154
+
155
+ // Per-route layout override - uses MinimalLayout instead of RootLayout
156
+ defineRoute('/print', PrintPage, [MinimalLayout]),
157
+ ];
158
+
159
+ console.log(
160
+ '[SSR] Available routes:',
161
+ routes.map((r) => r.pattern)
162
+ );
163
+
164
+ // H3 event type for Vinxi
165
+ interface H3Event {
166
+ node: {
167
+ req: { url?: string };
168
+ res: {
169
+ statusCode?: number;
170
+ setHeader: (name: string, value: string) => void;
171
+ end: (data?: unknown) => void;
172
+ write: (chunk: unknown) => boolean;
173
+ };
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Match a pathname against registered routes
179
+ * Returns the matched component and extracted params
180
+ */
181
+ function matchRoute(pathname: string): RouteMatch | null {
182
+ console.log('[SSR] Matching:', pathname);
183
+
184
+ for (const route of routes) {
185
+ const match = pathname.match(route.regex);
186
+ if (match) {
187
+ // Extract params from capture groups
188
+ const params: Record<string, string> = {};
189
+ route.paramNames.forEach((name, index) => {
190
+ params[name] = match[index + 1];
191
+ });
192
+
193
+ console.log('[SSR] Matched:', route.pattern, 'params:', params);
194
+ return { component: route.component, params, layouts: route.layouts };
195
+ }
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ /**
202
+ * Parse search params from URL
203
+ */
204
+ function parseSearchParams(url: URL): Record<string, string | string[]> {
205
+ const searchParams: Record<string, string | string[]> = {};
206
+ url.searchParams.forEach((value, key) => {
207
+ const existing = searchParams[key];
208
+ if (existing) {
209
+ searchParams[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
210
+ } else {
211
+ searchParams[key] = value;
212
+ }
213
+ });
214
+ return searchParams;
215
+ }
216
+
217
+ /**
218
+ * H3-compatible SSR handler for Vinxi
219
+ */
220
+ export default async function ssrHandler(event: H3Event): Promise<void> {
221
+ const res = event.node.res;
222
+ const url = new URL(event.node.req.url || '/', 'http://localhost');
223
+ const pathname = url.pathname;
224
+
225
+ console.log('[SSR] Handling:', pathname);
226
+
227
+ // Match route and extract params
228
+ const match = matchRoute(pathname);
229
+
230
+ if (!match) {
231
+ // Use the NotFoundPage component with RootLayout
232
+ const pageProps: PageProps = {
233
+ params: {},
234
+ searchParams: parseSearchParams(url),
235
+ };
236
+
237
+ const notFoundElement = <NotFoundPage {...pageProps} />;
238
+ const html = wrapWithLayouts(notFoundElement, [RootLayout], {});
239
+
240
+ const passThrough = new PassThrough();
241
+ const { pipe } = renderToPipeableStream(html, {
242
+ onShellReady() {
243
+ res.statusCode = 404;
244
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
245
+ passThrough.on('data', (chunk) => res.write(chunk));
246
+ passThrough.on('end', () => res.end());
247
+ pipe(passThrough);
248
+ },
249
+ onShellError(error) {
250
+ console.error('[SSR] 404 Shell error:', error);
251
+ res.statusCode = 404;
252
+ res.setHeader('Content-Type', 'text/html');
253
+ res.end(
254
+ `<!DOCTYPE html><html><body><h1>404 - Page Not Found</h1><p>Path: ${pathname}</p></body></html>`
255
+ );
256
+ },
257
+ });
258
+ return;
259
+ }
260
+
261
+ const { component: PageComponent, params, layouts } = match;
262
+
263
+ // Prepare page props with extracted params and search params
264
+ const pageProps: PageProps = {
265
+ params,
266
+ searchParams: parseSearchParams(url),
267
+ };
268
+
269
+ try {
270
+ // Create the page element
271
+ const pageElement = <PageComponent {...pageProps} />;
272
+
273
+ // Wrap page with layout chain (layouts provide the HTML shell)
274
+ const html = wrapWithLayouts(pageElement, layouts, params);
275
+
276
+ // Stream the response
277
+ const passThrough = new PassThrough();
278
+ const { pipe } = renderToPipeableStream(html, {
279
+ onShellReady() {
280
+ res.statusCode = 200;
281
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
282
+ passThrough.on('data', (chunk) => res.write(chunk));
283
+ passThrough.on('end', () => res.end());
284
+ pipe(passThrough);
285
+ },
286
+ onShellError(error) {
287
+ console.error('[SSR] Shell error:', error);
288
+ res.statusCode = 500;
289
+ res.setHeader('Content-Type', 'text/html');
290
+ res.end(`<h1>Render Error</h1><pre>${error}</pre>`);
291
+ },
292
+ onError(error) {
293
+ console.error('[SSR] Render error:', error);
294
+ },
295
+ });
296
+ } catch (error) {
297
+ console.error('[SSR] Handler error:', error);
298
+ res.statusCode = 500;
299
+ res.setHeader('Content-Type', 'text/html');
300
+ res.end(
301
+ `<h1>Server Error</h1><pre>${error instanceof Error ? error.stack : String(error)}</pre>`
302
+ );
303
+ }
304
+ }
@@ -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
+ }
@@ -296,7 +296,7 @@
296
296
  .emptyState {
297
297
  text-align: center;
298
298
  color: #666;
299
- padding: 2rem !important;
299
+ padding: 2rem;
300
300
  }
301
301
 
302
302
  /* Responsive */
@@ -22,7 +22,14 @@
22
22
 
23
23
  import { createVeloxHooks } from '@veloxts/client/react';
24
24
 
25
- import type { AppRouter } from '../../api/src/index.js';
25
+ /**
26
+ * AppRouter type imported from the type-only router file.
27
+ *
28
+ * The router.types.ts file contains ONLY type imports and declarations,
29
+ * which are completely erased at compile time. This means Vite never
30
+ * scans the @veloxts/* packages when building the frontend.
31
+ */
32
+ import type { AppRouter } from '../../api/src/router.types.js';
26
33
 
27
34
  /**
28
35
  * Type-safe API hooks with full autocomplete
@@ -6,11 +6,11 @@ import { createRoot } from 'react-dom/client';
6
6
  import { routeTree } from './routeTree.gen';
7
7
  import './styles/global.css';
8
8
 
9
- // Import router type from API for full type safety
10
- import type { AppRouter } from '../../api/src/index.js';
9
+ // Import router type from browser-safe types file
10
+ import type { AppRouter } from '../../api/src/router.types.js';
11
11
  /* @if auth */
12
- // Import routes directly from backend - no manual duplication needed
13
- import { routes } from '../../api/src/index.js';
12
+ // Import routes from browser-safe routes file
13
+ import { routes } from '../../api/src/routes.js';
14
14
 
15
15
  /* @endif auth */
16
16
 
@@ -5,25 +5,25 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
- "build": "tsc -b && vite build",
8
+ "build": "vite build",
9
9
  "preview": "vite preview",
10
10
  "type-check": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
13
  "@tanstack/react-query": "5.90.12",
14
- "@tanstack/react-router": "1.140.5",
14
+ "@tanstack/react-router": "1.141.8",
15
15
  "@veloxts/client": "__VELOXTS_VERSION__",
16
16
  "react": "19.2.3",
17
17
  "react-dom": "19.2.3",
18
- "react-error-boundary": "5.0.0"
18
+ "react-error-boundary": "6.0.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@tanstack/router-plugin": "1.140.5",
22
- "@types/node": "25.0.2",
21
+ "@tanstack/router-plugin": "1.142.0",
22
+ "@types/node": "25.0.3",
23
23
  "@types/react": "19.2.7",
24
24
  "@types/react-dom": "19.2.3",
25
25
  "@vitejs/plugin-react": "5.1.2",
26
26
  "typescript": "5.9.3",
27
- "vite": "7.2.7"
27
+ "vite": "7.3.0"
28
28
  }
29
29
  }
@@ -1,10 +1,10 @@
1
1
  import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
2
- import styles from '@/App.module.css';
3
2
  /* @if auth */
4
3
  import { useQuery } from '@veloxts/client/react';
5
4
 
6
- import type { AppRouter } from '../../../api/src/index.js';
5
+ import type { AppRouter } from '../../../api/src/router.types.js';
7
6
  /* @endif auth */
7
+ import styles from '@/App.module.css';
8
8
 
9
9
  export const Route = createRootRoute({
10
10
  component: RootLayout,
@@ -94,6 +94,7 @@ function HomePage() {
94
94
  <div className={styles.card}>
95
95
  <h2>Actions</h2>
96
96
  <button
97
+ type="button"
97
98
  onClick={() => logout.mutate({})}
98
99
  className={styles.button}
99
100
  disabled={logout.isPending}
@@ -117,12 +118,14 @@ function HomePage() {
117
118
  <div className={styles.authCard}>
118
119
  <div className={styles.authTabs}>
119
120
  <button
121
+ type="button"
120
122
  className={`${styles.authTab} ${isLogin ? styles.authTabActive : ''}`}
121
123
  onClick={() => setIsLogin(true)}
122
124
  >
123
125
  Login
124
126
  </button>
125
127
  <button
128
+ type="button"
126
129
  className={`${styles.authTab} ${!isLogin ? styles.authTabActive : ''}`}
127
130
  onClick={() => setIsLogin(false)}
128
131
  >