create-velox-app 0.4.6 → 0.4.9

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 (47) hide show
  1. package/dist/index.js.map +1 -1
  2. package/dist/templates/compiler.d.ts.map +1 -1
  3. package/dist/templates/compiler.js.map +1 -1
  4. package/dist/templates/index.d.ts.map +1 -1
  5. package/dist/templates/index.js +9 -1
  6. package/dist/templates/index.js.map +1 -1
  7. package/dist/templates/placeholders.d.ts +9 -0
  8. package/dist/templates/placeholders.d.ts.map +1 -1
  9. package/dist/templates/placeholders.js +31 -5
  10. package/dist/templates/placeholders.js.map +1 -1
  11. package/dist/templates/shared/web-base.d.ts +4 -2
  12. package/dist/templates/shared/web-base.d.ts.map +1 -1
  13. package/dist/templates/shared/web-base.js +15 -6
  14. package/dist/templates/shared/web-base.js.map +1 -1
  15. package/dist/templates/trpc.d.ts +15 -0
  16. package/dist/templates/trpc.d.ts.map +1 -0
  17. package/dist/templates/trpc.js +89 -0
  18. package/dist/templates/trpc.js.map +1 -0
  19. package/dist/templates/types.d.ts +1 -1
  20. package/dist/templates/types.d.ts.map +1 -1
  21. package/dist/templates/types.js +6 -0
  22. package/dist/templates/types.js.map +1 -1
  23. package/package.json +2 -2
  24. package/src/templates/source/api/config/auth.ts +7 -0
  25. package/src/templates/source/api/index.auth.ts +13 -2
  26. package/src/templates/source/api/index.default.ts +6 -1
  27. package/src/templates/source/api/index.trpc.ts +64 -0
  28. package/src/templates/source/api/package.auth.json +1 -0
  29. package/src/templates/source/api/package.default.json +1 -0
  30. package/src/templates/source/api/prisma.config.ts +1 -0
  31. package/src/templates/source/api/procedures/auth.ts +14 -8
  32. package/src/templates/source/api/procedures/health.ts +1 -1
  33. package/src/templates/source/api/procedures/users.auth.ts +24 -68
  34. package/src/templates/source/api/procedures/users.default.ts +28 -58
  35. package/src/templates/source/api/schemas/user.ts +9 -4
  36. package/src/templates/source/api/tsconfig.json +3 -2
  37. package/src/templates/source/web/App.module.css +54 -2
  38. package/src/templates/source/web/api.ts +42 -0
  39. package/src/templates/source/web/main.tsx +63 -13
  40. package/src/templates/source/web/package.json +10 -8
  41. package/src/templates/source/web/routes/__root.tsx +42 -3
  42. package/src/templates/source/web/routes/about.tsx +8 -2
  43. package/src/templates/source/web/routes/index.auth.tsx +25 -71
  44. package/src/templates/source/web/routes/index.default.tsx +12 -32
  45. package/src/templates/source/web/routes/users.tsx +85 -0
  46. package/src/templates/source/web/tsconfig.json +2 -1
  47. package/src/templates/source/web/vite.config.ts +4 -3
@@ -1,19 +1,18 @@
1
+ import { createRouter, RouterProvider } from '@tanstack/react-router';
2
+ import { VeloxProvider } from '@veloxts/client/react';
1
3
  import { StrictMode } from 'react';
2
4
  import { createRoot } from 'react-dom/client';
3
- import { RouterProvider, createRouter } from '@tanstack/react-router';
4
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
+
5
6
  import { routeTree } from './routeTree.gen';
6
7
  import './styles/global.css';
7
8
 
8
- // Create query client for data fetching
9
- const queryClient = new QueryClient({
10
- defaultOptions: {
11
- queries: {
12
- staleTime: 1000 * 60, // 1 minute
13
- retry: 1,
14
- },
15
- },
16
- });
9
+ // Import router type from API for full type safety
10
+ import type { AppRouter } from '../../api/src/index.js';
11
+ /* @if auth */
12
+ // Import routes directly from backend - no manual duplication needed
13
+ import { routes } from '../../api/src/index.js';
14
+
15
+ /* @endif auth */
17
16
 
18
17
  // Create router with route tree
19
18
  const router = createRouter({ routeTree });
@@ -25,14 +24,65 @@ declare module '@tanstack/react-router' {
25
24
  }
26
25
  }
27
26
 
27
+ /* @if auth */
28
+ // Dynamic headers for auth - fetches token on each request
29
+ const getAuthHeaders = () => {
30
+ const token = localStorage.getItem('token');
31
+ return token ? { Authorization: `Bearer ${token}` } : {};
32
+ };
33
+
34
+ // Automatic token refresh on 401 responses
35
+ const handleUnauthorized = async (): Promise<boolean> => {
36
+ const refreshToken = localStorage.getItem('refreshToken');
37
+ if (!refreshToken) return false;
38
+
39
+ try {
40
+ const res = await fetch('/api/auth/refresh', {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ refreshToken }),
44
+ });
45
+
46
+ if (!res.ok) {
47
+ // Refresh failed - clear tokens
48
+ localStorage.removeItem('token');
49
+ localStorage.removeItem('refreshToken');
50
+ return false;
51
+ }
52
+
53
+ const data = (await res.json()) as { accessToken: string; refreshToken: string };
54
+ localStorage.setItem('token', data.accessToken);
55
+ localStorage.setItem('refreshToken', data.refreshToken);
56
+ return true; // Retry the original request
57
+ } catch {
58
+ // Network error during refresh
59
+ return false;
60
+ }
61
+ };
62
+ /* @endif auth */
63
+
28
64
  // Render application
29
65
  const rootElement = document.getElementById('root');
30
66
  if (!rootElement) throw new Error('Root element not found');
31
67
 
32
68
  createRoot(rootElement).render(
33
69
  <StrictMode>
34
- <QueryClientProvider client={queryClient}>
70
+ {/* @if default */}
71
+ <VeloxProvider<AppRouter> config={{ baseUrl: '/api' }}>
72
+ <RouterProvider router={router} />
73
+ </VeloxProvider>
74
+ {/* @endif default */}
75
+ {/* @if auth */}
76
+ <VeloxProvider<AppRouter>
77
+ config={{
78
+ baseUrl: '/api',
79
+ headers: getAuthHeaders,
80
+ routes,
81
+ onUnauthorized: handleUnauthorized,
82
+ }}
83
+ >
35
84
  <RouterProvider router={router} />
36
- </QueryClientProvider>
85
+ </VeloxProvider>
86
+ {/* @endif auth */}
37
87
  </StrictMode>
38
88
  );
@@ -10,17 +10,19 @@
10
10
  "type-check": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "react": "19.1.0",
14
- "react-dom": "19.1.0",
15
- "@tanstack/react-router": "1.140.0",
16
- "@tanstack/react-query": "5.90.12"
13
+ "@tanstack/react-query": "5.90.12",
14
+ "@tanstack/react-router": "1.140.5",
15
+ "@veloxts/client": "__VELOXTS_VERSION__",
16
+ "react": "19.2.1",
17
+ "react-dom": "19.2.1",
18
+ "react-error-boundary": "5.0.0"
17
19
  },
18
20
  "devDependencies": {
19
- "@types/react": "19.1.6",
20
- "@types/react-dom": "19.1.5",
21
+ "@types/react": "19.2.7",
22
+ "@types/react-dom": "19.2.3",
21
23
  "@vitejs/plugin-react": "5.1.2",
22
- "@tanstack/router-plugin": "1.140.0",
23
- "vite": "6.4.1",
24
+ "@tanstack/router-plugin": "1.140.5",
25
+ "vite": "7.2.7",
24
26
  "typescript": "5.9.3"
25
27
  }
26
28
  }
@@ -1,24 +1,63 @@
1
- import { createRootRoute, Outlet, Link } from '@tanstack/react-router';
1
+ import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
2
+ /* @if auth */
3
+ import { useQuery } from '@veloxts/client/react';
4
+
5
+ import type { AppRouter } from '../../../api/src/index.js';
2
6
  import styles from '@/App.module.css';
7
+ /* @endif auth */
3
8
 
4
9
  export const Route = createRootRoute({
5
10
  component: RootLayout,
6
11
  });
7
12
 
8
13
  function RootLayout() {
14
+ /* @if auth */
15
+ const { data: user } = useQuery<AppRouter, 'auth', 'getMe'>(
16
+ 'auth',
17
+ 'getMe',
18
+ {},
19
+ { retry: false }
20
+ );
21
+ const isAuthenticated = !!user;
22
+ /* @endif auth */
23
+
9
24
  return (
10
25
  <div className={styles.app}>
11
26
  <nav className={styles.nav}>
12
27
  <div className={styles.navBrand}>
13
28
  <Link to="/" className={styles.logo}>
14
- VeloxTS
29
+ Velox TS
15
30
  </Link>
16
31
  </div>
17
32
  <div className={styles.navLinks}>
18
33
  <Link to="/" className={styles.navLink} activeProps={{ className: styles.navLinkActive }}>
19
34
  Home
20
35
  </Link>
21
- <Link to="/about" className={styles.navLink} activeProps={{ className: styles.navLinkActive }}>
36
+ {/* @if default */}
37
+ <Link
38
+ to="/users"
39
+ className={styles.navLink}
40
+ activeProps={{ className: styles.navLinkActive }}
41
+ >
42
+ Users
43
+ </Link>
44
+ {/* @endif default */}
45
+ {/* @if auth */}
46
+ {isAuthenticated && (
47
+ <Link
48
+ to="/users"
49
+ className={styles.navLink}
50
+ activeProps={{ className: styles.navLinkActive }}
51
+ >
52
+ Users
53
+ </Link>
54
+ )}
55
+ {/* @endif auth */}
56
+ <Link
57
+ to="/about"
58
+ className={styles.navLink}
59
+ activeProps={{ className: styles.navLinkActive }}
60
+ >
22
61
  About
23
62
  </Link>
24
63
  </div>
@@ -1,4 +1,5 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
+
2
3
  import styles from '@/App.module.css';
3
4
 
4
5
  export const Route = createFileRoute('/about')({
@@ -18,12 +19,17 @@ function AboutPage() {
18
19
  <div className={styles.cards}>
19
20
  <div className={styles.card}>
20
21
  <h2>Type Safety</h2>
21
- <p>End-to-end type safety without code generation. Types flow from backend to frontend automatically.</p>
22
+ <p>
23
+ End-to-end type safety without code generation. Types flow from backend to frontend
24
+ automatically.
25
+ </p>
22
26
  </div>
23
27
 
24
28
  <div className={styles.card}>
25
29
  <h2>Developer Experience</h2>
26
- <p>Convention over configuration. Sensible defaults with escape hatches when you need them.</p>
30
+ <p>
31
+ Convention over configuration. Sensible defaults with escape hatches when you need them.
32
+ </p>
27
33
  </div>
28
34
 
29
35
  <div className={styles.card}>
@@ -1,48 +1,9 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { useQueryClient } from '@veloxts/client/react';
3
3
  import { useState } from 'react';
4
- import styles from '@/App.module.css';
5
-
6
- // API helpers
7
- const api = {
8
- get: async <T,>(path: string): Promise<T> => {
9
- const token = localStorage.getItem('accessToken');
10
- const res = await fetch(`/api${path}`, {
11
- headers: token ? { Authorization: `Bearer ${token}` } : {},
12
- });
13
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
14
- return res.json();
15
- },
16
- post: async <T,>(path: string, data: unknown): Promise<T> => {
17
- const token = localStorage.getItem('accessToken');
18
- const res = await fetch(`/api${path}`, {
19
- method: 'POST',
20
- headers: {
21
- 'Content-Type': 'application/json',
22
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
23
- },
24
- body: JSON.stringify(data),
25
- });
26
- if (!res.ok) {
27
- const error = await res.json().catch(() => ({}));
28
- throw new Error(error.message || `HTTP ${res.status}`);
29
- }
30
- return res.json();
31
- },
32
- };
33
-
34
- interface User {
35
- id: string;
36
- name: string;
37
- email: string;
38
- roles?: string[];
39
- }
40
4
 
41
- interface AuthResponse {
42
- user: User;
43
- accessToken: string;
44
- refreshToken: string;
45
- }
5
+ import styles from '@/App.module.css';
6
+ import { api } from '@/api';
46
7
 
47
8
  export const Route = createFileRoute('/')({
48
9
  component: HomePage,
@@ -57,19 +18,13 @@ function HomePage() {
57
18
  const [error, setError] = useState('');
58
19
 
59
20
  // Check if user is logged in
60
- const { data: user, isLoading } = useQuery({
61
- queryKey: ['me'],
62
- queryFn: () => api.get<User>('/auth/me'),
63
- retry: false,
64
- });
21
+ const { data: user, isLoading } = api.auth.getMe.useQuery({}, { retry: false });
65
22
 
66
- const login = useMutation({
67
- mutationFn: (data: { email: string; password: string }) =>
68
- api.post<AuthResponse>('/auth/login', data),
23
+ const login = api.auth.createSession.useMutation({
69
24
  onSuccess: (data) => {
70
- localStorage.setItem('accessToken', data.accessToken);
25
+ localStorage.setItem('token', data.accessToken);
71
26
  localStorage.setItem('refreshToken', data.refreshToken);
72
- queryClient.invalidateQueries({ queryKey: ['me'] });
27
+ api.auth.getMe.invalidate(undefined, queryClient);
73
28
  setError('');
74
29
  },
75
30
  onError: (err) => {
@@ -77,13 +32,11 @@ function HomePage() {
77
32
  },
78
33
  });
79
34
 
80
- const register = useMutation({
81
- mutationFn: (data: { name: string; email: string; password: string }) =>
82
- api.post<AuthResponse>('/auth/register', data),
35
+ const register = api.auth.createAccount.useMutation({
83
36
  onSuccess: (data) => {
84
- localStorage.setItem('accessToken', data.accessToken);
37
+ localStorage.setItem('token', data.accessToken);
85
38
  localStorage.setItem('refreshToken', data.refreshToken);
86
- queryClient.invalidateQueries({ queryKey: ['me'] });
39
+ api.auth.getMe.invalidate(undefined, queryClient);
87
40
  setError('');
88
41
  },
89
42
  onError: (err) => {
@@ -91,12 +44,11 @@ function HomePage() {
91
44
  },
92
45
  });
93
46
 
94
- const logout = useMutation({
95
- mutationFn: () => api.post('/auth/logout', {}),
47
+ const logout = api.auth.deleteSession.useMutation({
96
48
  onSuccess: () => {
97
- localStorage.removeItem('accessToken');
49
+ localStorage.removeItem('token');
98
50
  localStorage.removeItem('refreshToken');
99
- queryClient.setQueryData(['me'], null);
51
+ api.auth.getMe.setData({}, null, queryClient);
100
52
  },
101
53
  });
102
54
 
@@ -131,14 +83,18 @@ function HomePage() {
131
83
  <div className={styles.cards}>
132
84
  <div className={styles.card}>
133
85
  <h2>Your Profile</h2>
134
- <p><strong>ID:</strong> {user.id}</p>
135
- <p><strong>Roles:</strong> {user.roles?.join(', ') || 'user'}</p>
86
+ <p>
87
+ <strong>ID:</strong> {user.id}
88
+ </p>
89
+ <p>
90
+ <strong>Roles:</strong> {user.roles?.join(', ') || 'user'}
91
+ </p>
136
92
  </div>
137
93
 
138
94
  <div className={styles.card}>
139
95
  <h2>Actions</h2>
140
96
  <button
141
- onClick={() => logout.mutate()}
97
+ onClick={() => logout.mutate({})}
142
98
  className={styles.button}
143
99
  disabled={logout.isPending}
144
100
  >
@@ -155,9 +111,7 @@ function HomePage() {
155
111
  <div className={styles.container}>
156
112
  <div className={styles.hero}>
157
113
  <h1 className={styles.title}>Welcome to VeloxTS</h1>
158
- <p className={styles.subtitle}>
159
- Full-stack TypeScript with authentication.
160
- </p>
114
+ <p className={styles.subtitle}>Full-stack TypeScript with authentication.</p>
161
115
  </div>
162
116
 
163
117
  <div className={styles.authCard}>
@@ -215,14 +169,14 @@ function HomePage() {
215
169
  >
216
170
  {login.isPending || register.isPending
217
171
  ? 'Please wait...'
218
- : isLogin ? 'Sign In' : 'Create Account'}
172
+ : isLogin
173
+ ? 'Sign In'
174
+ : 'Create Account'}
219
175
  </button>
220
176
  </form>
221
177
 
222
178
  {!isLogin && (
223
- <p className={styles.formHint}>
224
- Password: 12+ chars, uppercase, lowercase, number
225
- </p>
179
+ <p className={styles.formHint}>Password: 12+ chars, uppercase, lowercase, number</p>
226
180
  )}
227
181
  </div>
228
182
  </div>
@@ -1,40 +1,20 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
- import { useQuery } from '@tanstack/react-query';
3
- import styles from '@/App.module.css';
4
2
 
5
- // API helper
6
- const api = {
7
- get: async <T,>(path: string): Promise<T> => {
8
- const res = await fetch(`/api${path}`);
9
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
10
- return res.json();
11
- },
12
- };
3
+ import styles from '@/App.module.css';
4
+ import { api } from '@/api';
13
5
 
14
6
  export const Route = createFileRoute('/')({
15
7
  component: HomePage,
16
8
  });
17
9
 
18
- interface HealthResponse {
19
- status: string;
20
- version: string;
21
- timestamp: string;
22
- uptime: number;
23
- }
24
-
25
10
  function HomePage() {
26
- const { data: health, isLoading, error } = useQuery({
27
- queryKey: ['health'],
28
- queryFn: () => api.get<HealthResponse>('/health'),
29
- });
11
+ const { data: health, isLoading, error } = api.health.check.useQuery({});
30
12
 
31
13
  return (
32
14
  <div className={styles.container}>
33
15
  <div className={styles.hero}>
34
16
  <h1 className={styles.title}>Welcome to VeloxTS</h1>
35
- <p className={styles.subtitle}>
36
- Full-stack TypeScript, beautifully simple.
37
- </p>
17
+ <p className={styles.subtitle}>Full-stack TypeScript, beautifully simple.</p>
38
18
  </div>
39
19
 
40
20
  <div className={styles.cards}>
@@ -45,19 +25,19 @@ function HomePage() {
45
25
  ) : error ? (
46
26
  <p className={styles.error}>Disconnected</p>
47
27
  ) : (
48
- <p className={styles.success}>
49
- {health?.status === 'ok' ? 'Connected' : 'Unknown'}
50
- </p>
51
- )}
52
- {health && (
53
- <p className={styles.meta}>v{health.version}</p>
28
+ <p className={styles.success}>{health?.status === 'ok' ? 'Connected' : 'Unknown'}</p>
54
29
  )}
30
+ {health && <p className={styles.meta}>v{health.version}</p>}
55
31
  </div>
56
32
 
57
33
  <div className={styles.card}>
58
34
  <h2>Get Started</h2>
59
- <p>Edit <code>apps/api/src/procedures</code> to add API endpoints.</p>
60
- <p>Edit <code>apps/web/src/routes</code> to add pages.</p>
35
+ <p>
36
+ Edit <code>apps/api/src/procedures</code> to add API endpoints.
37
+ </p>
38
+ <p>
39
+ Edit <code>apps/web/src/routes</code> to add pages.
40
+ </p>
61
41
  </div>
62
42
 
63
43
  <div className={styles.card}>
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Users Page - Demonstrates React 19 patterns with VeloxTS
3
+ *
4
+ * Uses useSuspenseQuery with Suspense boundaries for cleaner component code.
5
+ * Data is guaranteed to be available when the component renders.
6
+ */
7
+
8
+ import { createFileRoute } from '@tanstack/react-router';
9
+ import { Suspense } from 'react';
10
+ import { ErrorBoundary } from 'react-error-boundary';
11
+
12
+ import styles from '@/App.module.css';
13
+ import { api } from '@/api';
14
+
15
+ export const Route = createFileRoute('/users')({
16
+ component: UsersPage,
17
+ });
18
+
19
+ function UsersPage() {
20
+ return (
21
+ <div className={styles.container}>
22
+ <div className={styles.hero}>
23
+ <h1 className={styles.title}>Users</h1>
24
+ <p className={styles.subtitle}>Type-safe data fetching with React 19 Suspense</p>
25
+ </div>
26
+
27
+ <ErrorBoundary fallback={<UsersError />}>
28
+ <Suspense fallback={<UsersLoading />}>
29
+ <UsersTable />
30
+ </Suspense>
31
+ </ErrorBoundary>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Users table component - wrapped in Suspense
38
+ *
39
+ * With useSuspenseQuery, data is guaranteed to be available.
40
+ * No need for isLoading/error checks - handled by boundaries.
41
+ */
42
+ function UsersTable() {
43
+ const { data } = api.users.listUsers.useSuspenseQuery({});
44
+
45
+ return (
46
+ <div className={styles.tableContainer}>
47
+ <table className={styles.table}>
48
+ <thead>
49
+ <tr>
50
+ <th>Name</th>
51
+ <th>Email</th>
52
+ <th>Created</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ {data.data.map((user) => (
57
+ <tr key={user.id}>
58
+ <td>{user.name}</td>
59
+ <td>{user.email}</td>
60
+ <td>{new Date(user.createdAt).toLocaleDateString()}</td>
61
+ </tr>
62
+ ))}
63
+ {data.data.length === 0 && (
64
+ <tr>
65
+ <td colSpan={3} className={styles.emptyState}>
66
+ No users found. Create one via the API!
67
+ </td>
68
+ </tr>
69
+ )}
70
+ </tbody>
71
+ </table>
72
+ <p className={styles.meta}>
73
+ Page {data.meta.page} - {data.meta.total} total users
74
+ </p>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ function UsersLoading() {
80
+ return <p className={styles.loading}>Loading users...</p>;
81
+ }
82
+
83
+ function UsersError() {
84
+ return <p className={styles.error}>Failed to load users. Please try again.</p>;
85
+ }
@@ -20,5 +20,6 @@
20
20
  "@/*": ["./src/*"]
21
21
  }
22
22
  },
23
- "include": ["src"]
23
+ "include": ["src"],
24
+ "references": [{ "path": "../api" }]
24
25
  }
@@ -1,8 +1,9 @@
1
- import { defineConfig } from 'vite';
2
- import react from '@vitejs/plugin-react';
3
- import { tanstackRouter } from '@tanstack/router-plugin/vite';
4
1
  import path from 'node:path';
5
2
 
3
+ import { tanstackRouter } from '@tanstack/router-plugin/vite';
4
+ import react from '@vitejs/plugin-react';
5
+ import { defineConfig } from 'vite';
6
+
6
7
  export default defineConfig({
7
8
  plugins: [tanstackRouter(), react()],
8
9
  resolve: {