create-velox-app 0.4.13 → 0.6.23
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/README.md +2 -43
- package/dist/cli.js +23 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/templates/auth.d.ts.map +1 -1
- package/dist/templates/auth.js +24 -0
- package/dist/templates/auth.js.map +1 -1
- package/dist/templates/fullstack.d.ts +15 -0
- package/dist/templates/fullstack.d.ts.map +1 -0
- package/dist/templates/fullstack.js +110 -0
- package/dist/templates/fullstack.js.map +1 -0
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +27 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/placeholders.d.ts +6 -1
- package/dist/templates/placeholders.d.ts.map +1 -1
- package/dist/templates/placeholders.js +15 -5
- package/dist/templates/placeholders.js.map +1 -1
- package/dist/templates/rsc.d.ts +15 -0
- package/dist/templates/rsc.d.ts.map +1 -0
- package/dist/templates/rsc.js +192 -0
- package/dist/templates/rsc.js.map +1 -0
- package/dist/templates/shared/root.d.ts +1 -0
- package/dist/templates/shared/root.d.ts.map +1 -1
- package/dist/templates/shared/root.js +4 -0
- package/dist/templates/shared/root.js.map +1 -1
- package/dist/templates/spa.d.ts +12 -0
- package/dist/templates/spa.d.ts.map +1 -0
- package/dist/templates/spa.js +101 -0
- package/dist/templates/spa.js.map +1 -0
- package/dist/templates/trpc.d.ts.map +1 -1
- package/dist/templates/trpc.js +16 -0
- package/dist/templates/trpc.js.map +1 -1
- package/dist/templates/types.d.ts +14 -1
- package/dist/templates/types.d.ts.map +1 -1
- package/dist/templates/types.js +35 -10
- package/dist/templates/types.js.map +1 -1
- package/package.json +3 -3
- package/src/templates/source/api/config/auth.ts +2 -2
- package/src/templates/source/api/config/database.ts +44 -10
- package/src/templates/source/api/index.auth.ts +10 -16
- package/src/templates/source/api/index.default.ts +10 -9
- package/src/templates/source/api/index.trpc.ts +9 -28
- package/src/templates/source/api/package.auth.json +7 -6
- package/src/templates/source/api/package.default.json +5 -4
- package/src/templates/source/api/prisma/schema.auth.prisma +3 -2
- package/src/templates/source/api/prisma/schema.default.prisma +3 -2
- package/src/templates/source/api/prisma.config.ts +7 -1
- package/src/templates/source/api/procedures/auth.ts +38 -66
- package/src/templates/source/api/procedures/health.ts +4 -9
- package/src/templates/source/api/procedures/users.auth.ts +7 -11
- package/src/templates/source/api/procedures/users.default.ts +7 -11
- package/src/templates/source/api/router.auth.ts +31 -0
- package/src/templates/source/api/router.default.ts +29 -0
- package/src/templates/source/api/router.trpc.ts +37 -0
- package/src/templates/source/api/router.types.auth.ts +88 -0
- package/src/templates/source/api/router.types.default.ts +73 -0
- package/src/templates/source/api/router.types.trpc.ts +73 -0
- package/src/templates/source/api/routes.auth.ts +66 -0
- package/src/templates/source/api/routes.default.ts +53 -0
- package/src/templates/source/api/schemas/auth.ts +79 -0
- package/src/templates/source/api/schemas/health.ts +21 -0
- package/src/templates/source/api/schemas/user.ts +62 -12
- package/src/templates/source/api/utils/auth.ts +157 -0
- package/src/templates/source/root/.cursorrules +187 -0
- package/src/templates/source/root/CLAUDE.auth.md +264 -0
- package/src/templates/source/root/CLAUDE.default.md +185 -0
- package/src/templates/source/root/package.json +7 -1
- package/src/templates/source/rsc/CLAUDE.md +104 -0
- package/src/templates/source/rsc/app/actions/posts.ts +93 -0
- package/src/templates/source/rsc/app/actions/users.ts +83 -0
- package/src/templates/source/rsc/app/layouts/dashboard.tsx +127 -0
- package/src/templates/source/rsc/app/layouts/marketing.tsx +25 -0
- package/src/templates/source/rsc/app/layouts/minimal.tsx +30 -0
- package/src/templates/source/rsc/app/layouts/root.tsx +241 -0
- package/src/templates/source/rsc/app/pages/(dashboard)/profile.tsx +71 -0
- package/src/templates/source/rsc/app/pages/(dashboard)/settings.tsx +104 -0
- package/src/templates/source/rsc/app/pages/(marketing)/about.tsx +52 -0
- package/src/templates/source/rsc/app/pages/_not-found.tsx +149 -0
- package/src/templates/source/rsc/app/pages/docs/[...slug].tsx +211 -0
- package/src/templates/source/rsc/app/pages/index.tsx +50 -0
- package/src/templates/source/rsc/app/pages/print.tsx +80 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/[postId].tsx +89 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/index.tsx +76 -0
- package/src/templates/source/rsc/app/pages/users/[id]/posts/new.tsx +79 -0
- package/src/templates/source/rsc/app/pages/users/[id].tsx +64 -0
- package/src/templates/source/rsc/app/pages/users/_layout.tsx +104 -0
- package/src/templates/source/rsc/app/pages/users.tsx +44 -0
- package/src/templates/source/rsc/app.config.ts +12 -0
- package/src/templates/source/rsc/env.example +6 -0
- package/src/templates/source/rsc/gitignore +34 -0
- package/src/templates/source/rsc/package.json +41 -0
- package/src/templates/source/rsc/prisma/schema.prisma +34 -0
- package/src/templates/source/rsc/prisma.config.ts +22 -0
- package/src/templates/source/rsc/public/favicon.svg +4 -0
- package/src/templates/source/rsc/src/api/database.ts +72 -0
- package/src/templates/source/rsc/src/api/handler.ts +53 -0
- package/src/templates/source/rsc/src/api/procedures/health.ts +48 -0
- package/src/templates/source/rsc/src/api/procedures/posts.ts +151 -0
- package/src/templates/source/rsc/src/api/procedures/users.ts +87 -0
- package/src/templates/source/rsc/src/api/schemas/post.ts +53 -0
- package/src/templates/source/rsc/src/api/schemas/user.ts +38 -0
- package/src/templates/source/rsc/src/entry.client.tsx +28 -0
- package/src/templates/source/rsc/src/entry.server.tsx +304 -0
- package/src/templates/source/rsc/tsconfig.json +24 -0
- package/src/templates/source/web/App.module.css +1 -1
- package/src/templates/source/web/api.ts +8 -1
- package/src/templates/source/web/main.tsx +4 -4
- package/src/templates/source/web/package.json +6 -6
- package/src/templates/source/web/routes/__root.tsx +2 -2
- package/src/templates/source/web/routes/index.auth.tsx +3 -0
- package/src/templates/source/web/routes/users.tsx +1 -1
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
import { createVeloxHooks } from '@veloxts/client/react';
|
|
24
24
|
|
|
25
|
-
|
|
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
|
|
10
|
-
import type { AppRouter } from '../../api/src/
|
|
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
|
|
13
|
-
import { routes } from '../../api/src/
|
|
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": "
|
|
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.
|
|
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": "
|
|
18
|
+
"react-error-boundary": "6.0.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@tanstack/router-plugin": "1.
|
|
22
|
-
"@types/node": "25.0.
|
|
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.
|
|
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/
|
|
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
|
>
|
|
@@ -40,7 +40,7 @@ function UsersPage() {
|
|
|
40
40
|
* No need for isLoading/error checks - handled by boundaries.
|
|
41
41
|
*/
|
|
42
42
|
function UsersTable() {
|
|
43
|
-
const { data } = api.users.listUsers.useSuspenseQuery(
|
|
43
|
+
const { data } = api.users.listUsers.useSuspenseQuery();
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
46
|
<div className={styles.tableContainer}>
|