@react-spa-scaffold/mcp 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +2 -1
  2. package/dist/constants.d.ts +4 -0
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +4 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/features/definitions/auth.d.ts +3 -0
  7. package/dist/features/definitions/auth.d.ts.map +1 -0
  8. package/dist/features/definitions/auth.js +17 -0
  9. package/dist/features/definitions/auth.js.map +1 -0
  10. package/dist/features/definitions/core.d.ts.map +1 -1
  11. package/dist/features/definitions/core.js +16 -1
  12. package/dist/features/definitions/core.js.map +1 -1
  13. package/dist/features/definitions/database.d.ts +3 -0
  14. package/dist/features/definitions/database.d.ts.map +1 -0
  15. package/dist/features/definitions/database.js +45 -0
  16. package/dist/features/definitions/database.js.map +1 -0
  17. package/dist/features/definitions/deployment.d.ts +3 -0
  18. package/dist/features/definitions/deployment.d.ts.map +1 -0
  19. package/dist/features/definitions/deployment.js +14 -0
  20. package/dist/features/definitions/deployment.js.map +1 -0
  21. package/dist/features/definitions/forms.d.ts.map +1 -1
  22. package/dist/features/definitions/forms.js +4 -0
  23. package/dist/features/definitions/forms.js.map +1 -1
  24. package/dist/features/definitions/index.d.ts +3 -0
  25. package/dist/features/definitions/index.d.ts.map +1 -1
  26. package/dist/features/definitions/index.js +3 -0
  27. package/dist/features/definitions/index.js.map +1 -1
  28. package/dist/features/definitions/mobile.d.ts.map +1 -1
  29. package/dist/features/definitions/mobile.js +11 -2
  30. package/dist/features/definitions/mobile.js.map +1 -1
  31. package/dist/features/definitions/observability.js +1 -1
  32. package/dist/features/definitions/observability.js.map +1 -1
  33. package/dist/features/definitions/routing.d.ts.map +1 -1
  34. package/dist/features/definitions/routing.js +2 -1
  35. package/dist/features/definitions/routing.js.map +1 -1
  36. package/dist/features/definitions/state.d.ts.map +1 -1
  37. package/dist/features/definitions/state.js +9 -2
  38. package/dist/features/definitions/state.js.map +1 -1
  39. package/dist/features/definitions/testing.d.ts.map +1 -1
  40. package/dist/features/definitions/testing.js +4 -2
  41. package/dist/features/definitions/testing.js.map +1 -1
  42. package/dist/features/registry.d.ts.map +1 -1
  43. package/dist/features/registry.js +4 -1
  44. package/dist/features/registry.js.map +1 -1
  45. package/dist/features/types.test.js +6 -2
  46. package/dist/features/types.test.js.map +1 -1
  47. package/dist/resources/docs.d.ts.map +1 -1
  48. package/dist/resources/docs.js +5 -0
  49. package/dist/resources/docs.js.map +1 -1
  50. package/dist/tools/add-features.js +1 -1
  51. package/dist/tools/add-features.js.map +1 -1
  52. package/dist/utils/docs.d.ts.map +1 -1
  53. package/dist/utils/docs.js +2 -0
  54. package/dist/utils/docs.js.map +1 -1
  55. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  56. package/dist/utils/scaffold/claude-md/index.js +3 -1
  57. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  58. package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
  59. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  60. package/dist/utils/scaffold/claude-md/sections.js +132 -2
  61. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  62. package/dist/utils/scaffold/compute.js +1 -1
  63. package/dist/utils/scaffold/compute.js.map +1 -1
  64. package/dist/utils/scaffold/generators.d.ts +2 -2
  65. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  66. package/dist/utils/scaffold/generators.js +64 -22
  67. package/dist/utils/scaffold/generators.js.map +1 -1
  68. package/package.json +1 -1
  69. package/templates/.env.example +44 -10
  70. package/templates/.github/workflows/ci.yml +12 -4
  71. package/templates/.github/workflows/deploy.yml +59 -0
  72. package/templates/CLAUDE.md +251 -2
  73. package/templates/docs/ARCHITECTURE.md +13 -12
  74. package/templates/docs/AUTHENTICATION.md +325 -0
  75. package/templates/docs/CODING_STANDARDS.md +65 -0
  76. package/templates/docs/DEPLOYMENT.md +268 -0
  77. package/templates/docs/E2E_TESTING.md +133 -11
  78. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  79. package/templates/docs/TESTING.md +195 -77
  80. package/templates/e2e/auth/auth.setup.ts +60 -0
  81. package/templates/e2e/fixtures/index.ts +24 -2
  82. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  83. package/templates/e2e/tests/profile.spec.ts +64 -0
  84. package/templates/e2e/tests/register-form.spec.ts +38 -0
  85. package/templates/gitignore +5 -0
  86. package/templates/package.json +15 -3
  87. package/templates/playwright.config.ts +39 -4
  88. package/templates/src/App.tsx +32 -19
  89. package/templates/src/components/layout/Header.test.tsx +17 -1
  90. package/templates/src/components/layout/Header.tsx +13 -1
  91. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
  92. package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
  93. package/templates/src/components/shared/AccountButton/index.ts +1 -0
  94. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
  95. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
  96. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  97. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  98. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  99. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
  100. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
  101. package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
  102. package/templates/src/components/shared/index.ts +5 -2
  103. package/templates/src/contexts/clerkContext.tsx +45 -0
  104. package/templates/src/contexts/performanceContext.tsx +3 -3
  105. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  106. package/templates/src/contexts/supabaseContext.tsx +87 -0
  107. package/templates/src/hooks/index.ts +40 -2
  108. package/templates/src/hooks/supabase/index.ts +12 -0
  109. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  110. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  111. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  112. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  113. package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
  114. package/templates/src/hooks/useCopyFeedback.ts +41 -0
  115. package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
  116. package/templates/src/hooks/useDebouncedCallback.ts +47 -0
  117. package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
  118. package/templates/src/hooks/useDocumentTitle.ts +31 -0
  119. package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
  120. package/templates/src/hooks/useIOSViewportReset.ts +18 -0
  121. package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
  122. package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
  123. package/templates/src/hooks/useLocalStorage.test.ts +111 -0
  124. package/templates/src/hooks/useLocalStorage.ts +77 -0
  125. package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
  126. package/templates/src/hooks/useSyncedFormData.ts +21 -0
  127. package/templates/src/hooks/useSyncedState.test.ts +119 -0
  128. package/templates/src/hooks/useSyncedState.ts +30 -0
  129. package/templates/src/index.css +1 -0
  130. package/templates/src/lib/api.test.ts +30 -38
  131. package/templates/src/lib/api.ts +1 -7
  132. package/templates/src/lib/config.ts +54 -4
  133. package/templates/src/lib/constants.ts +10 -0
  134. package/templates/src/lib/createSelectors.test.ts +136 -0
  135. package/templates/src/lib/createSelectors.ts +31 -0
  136. package/templates/src/lib/env.ts +36 -14
  137. package/templates/src/lib/index.ts +5 -2
  138. package/templates/src/lib/routes.ts +1 -0
  139. package/templates/src/lib/sentry.ts +58 -0
  140. package/templates/src/lib/storage.ts +6 -2
  141. package/templates/src/lib/supabase/client.ts +58 -0
  142. package/templates/src/lib/supabase/index.ts +5 -0
  143. package/templates/src/main.tsx +19 -31
  144. package/templates/src/mocks/constants.ts +31 -0
  145. package/templates/src/mocks/fixtures/index.ts +3 -1
  146. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  147. package/templates/src/mocks/fixtures/users.ts +91 -0
  148. package/templates/src/mocks/handlers/index.ts +2 -1
  149. package/templates/src/mocks/handlers/supabase.ts +64 -0
  150. package/templates/src/mocks/handlers/todos.ts +1 -1
  151. package/templates/src/mocks/index.ts +6 -0
  152. package/templates/src/pages/Profile.test.tsx +263 -0
  153. package/templates/src/pages/Profile.tsx +171 -0
  154. package/templates/src/pages/index.ts +1 -0
  155. package/templates/src/stores/preferencesStore.ts +35 -9
  156. package/templates/src/test/clerkMock.tsx +137 -0
  157. package/templates/src/test/fetchMock.ts +58 -0
  158. package/templates/src/test/index.ts +51 -2
  159. package/templates/src/test/mocks.ts +128 -1
  160. package/templates/src/test/providers.tsx +10 -4
  161. package/templates/src/test/supabaseMock.ts +112 -0
  162. package/templates/src/test-setup.ts +42 -2
  163. package/templates/src/types/database.ts +46 -0
  164. package/templates/src/types/index.ts +1 -0
  165. package/templates/src/types/supabase.ts +167 -0
  166. package/templates/src/vite-env.d.ts +6 -0
  167. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
  168. package/templates/vitest.config.ts +9 -1
@@ -0,0 +1,136 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { create } from 'zustand';
4
+
5
+ import { createSelectors } from './createSelectors';
6
+
7
+ interface TestState {
8
+ count: number;
9
+ name: string;
10
+ increment: () => void;
11
+ setName: (name: string) => void;
12
+ }
13
+
14
+ describe('createSelectors', () => {
15
+ it('creates auto-generated selectors for all state properties', () => {
16
+ const useStoreBase = create<TestState>()((set) => ({
17
+ count: 0,
18
+ name: 'test',
19
+ increment: () => set((state) => ({ count: state.count + 1 })),
20
+ setName: (name) => set({ name }),
21
+ }));
22
+
23
+ const useStore = createSelectors(useStoreBase);
24
+
25
+ // Verify .use namespace exists
26
+ expect(useStore.use).toBeDefined();
27
+ expect(typeof useStore.use.count).toBe('function');
28
+ expect(typeof useStore.use.name).toBe('function');
29
+ expect(typeof useStore.use.increment).toBe('function');
30
+ expect(typeof useStore.use.setName).toBe('function');
31
+ });
32
+
33
+ it('selectors return correct state values', () => {
34
+ const useStoreBase = create<TestState>()((set) => ({
35
+ count: 42,
36
+ name: 'hello',
37
+ increment: () => set((state) => ({ count: state.count + 1 })),
38
+ setName: (name) => set({ name }),
39
+ }));
40
+
41
+ const useStore = createSelectors(useStoreBase);
42
+
43
+ const { result: countResult } = renderHook(() => useStore.use.count());
44
+ const { result: nameResult } = renderHook(() => useStore.use.name());
45
+
46
+ expect(countResult.current).toBe(42);
47
+ expect(nameResult.current).toBe('hello');
48
+ });
49
+
50
+ it('selectors return action functions', () => {
51
+ const useStoreBase = create<TestState>()((set) => ({
52
+ count: 0,
53
+ name: 'test',
54
+ increment: () => set((state) => ({ count: state.count + 1 })),
55
+ setName: (name) => set({ name }),
56
+ }));
57
+
58
+ const useStore = createSelectors(useStoreBase);
59
+
60
+ const { result: incrementResult } = renderHook(() => useStore.use.increment());
61
+ const { result: setNameResult } = renderHook(() => useStore.use.setName());
62
+
63
+ expect(typeof incrementResult.current).toBe('function');
64
+ expect(typeof setNameResult.current).toBe('function');
65
+ });
66
+
67
+ it('actions from selectors update state correctly', () => {
68
+ const useStoreBase = create<TestState>()((set) => ({
69
+ count: 0,
70
+ name: 'initial',
71
+ increment: () => set((state) => ({ count: state.count + 1 })),
72
+ setName: (name) => set({ name }),
73
+ }));
74
+
75
+ const useStore = createSelectors(useStoreBase);
76
+
77
+ // Get initial state
78
+ expect(useStore.getState().count).toBe(0);
79
+ expect(useStore.getState().name).toBe('initial');
80
+
81
+ // Use action from selector
82
+ const { result: incrementResult } = renderHook(() => useStore.use.increment());
83
+
84
+ act(() => {
85
+ incrementResult.current();
86
+ });
87
+
88
+ expect(useStore.getState().count).toBe(1);
89
+
90
+ // Use another action
91
+ const { result: setNameResult } = renderHook(() => useStore.use.setName());
92
+
93
+ act(() => {
94
+ setNameResult.current('updated');
95
+ });
96
+
97
+ expect(useStore.getState().name).toBe('updated');
98
+ });
99
+
100
+ it('preserves original store functionality', () => {
101
+ const useStoreBase = create<TestState>()((set) => ({
102
+ count: 0,
103
+ name: 'test',
104
+ increment: () => set((state) => ({ count: state.count + 1 })),
105
+ setName: (name) => set({ name }),
106
+ }));
107
+
108
+ const useStore = createSelectors(useStoreBase);
109
+
110
+ // Original hook still works
111
+ const { result } = renderHook(() => useStore((state) => state.count));
112
+ expect(result.current).toBe(0);
113
+
114
+ // getState still works
115
+ expect(useStore.getState().count).toBe(0);
116
+
117
+ // setState still works
118
+ act(() => {
119
+ useStore.setState({ count: 100 });
120
+ });
121
+ expect(useStore.getState().count).toBe(100);
122
+
123
+ // subscribe still works
124
+ let subscribedValue = 0;
125
+ const unsub = useStore.subscribe((state) => {
126
+ subscribedValue = state.count;
127
+ });
128
+
129
+ act(() => {
130
+ useStore.setState({ count: 200 });
131
+ });
132
+ expect(subscribedValue).toBe(200);
133
+
134
+ unsub();
135
+ });
136
+ });
@@ -0,0 +1,31 @@
1
+ import type { StoreApi, UseBoundStore } from 'zustand';
2
+
3
+ /**
4
+ * Type that extends a Zustand store with auto-generated selectors.
5
+ * Adds a `use` namespace with selector functions for each store property.
6
+ */
7
+ type WithSelectors<S> = S extends { getState: () => infer T } ? S & { use: { [K in keyof T]: () => T[K] } } : never;
8
+
9
+ /**
10
+ * Wraps a Zustand store and automatically generates selector hooks for all properties.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const useStoreBase = create<State>()((set) => ({ count: 0 }));
15
+ * export const useStore = createSelectors(useStoreBase);
16
+ *
17
+ * // Usage in components:
18
+ * const count = useStore.use.count();
19
+ * const increment = useStore.use.increment();
20
+ * ```
21
+ */
22
+ export function createSelectors<S extends UseBoundStore<StoreApi<object>>>(_store: S): WithSelectors<S> {
23
+ const store = _store as WithSelectors<typeof _store>;
24
+ store.use = {} as WithSelectors<S>['use'];
25
+
26
+ for (const key of Object.keys(store.getState())) {
27
+ (store.use as Record<string, () => unknown>)[key] = () => store((state) => state[key as keyof typeof state]);
28
+ }
29
+
30
+ return store;
31
+ }
@@ -1,25 +1,40 @@
1
1
  /**
2
2
  * Environment variable validation using Zod.
3
3
  * Validates at runtime to catch missing/invalid env vars early.
4
+ *
5
+ * All env vars are REQUIRED. The MCP scaffold tool strips unused vars
6
+ * when scaffolding builds without certain features.
4
7
  */
5
8
 
6
9
  import { z } from 'zod';
7
10
 
11
+ /**
12
+ * Transforms string env var to boolean.
13
+ * - 'true', '1' → true
14
+ * - 'false', '0' → false
15
+ */
16
+ const booleanEnv = z.enum(['true', 'false', '1', '0']).transform((val) => val === 'true' || val === '1');
17
+
8
18
  const envSchema = z.object({
9
- VITE_APP_NAME: z.string().min(1).optional(),
10
- VITE_APP_URL: z.string().url().optional(),
11
- VITE_API_URL: z.string().url().optional(),
12
- VITE_SENTRY_DSN: z.string().url().optional(),
13
- MODE: z.enum(['development', 'production', 'test']).default('development'),
14
- DEV: z.boolean().default(false),
15
- PROD: z.boolean().default(false),
19
+ VITE_APP_NAME: z.string().min(1),
20
+ VITE_APP_URL: z.string().url(),
21
+ VITE_API_URL: z.string().url(),
22
+ VITE_SENTRY_DSN: z.string().url(),
23
+ VITE_SENTRY_ENABLED: booleanEnv,
24
+ VITE_CLERK_PUBLISHABLE_KEY: z.string().min(1),
25
+ VITE_SUPABASE_DATABASE_URL: z.string().url(),
26
+ VITE_SUPABASE_ANON_KEY: z.string().min(1),
27
+ VITE_PERF_TEST: booleanEnv,
28
+ MODE: z.enum(['development', 'production', 'test']),
29
+ DEV: z.boolean(),
30
+ PROD: z.boolean(),
16
31
  });
17
32
 
18
33
  export type Env = z.infer<typeof envSchema>;
19
34
 
20
35
  /**
21
36
  * Validate environment variables and return typed env object.
22
- * Throws if validation fails in production.
37
+ * Throws if any required env var is missing or invalid.
23
38
  */
24
39
  export function validateEnv(): Env {
25
40
  const env = {
@@ -27,6 +42,11 @@ export function validateEnv(): Env {
27
42
  VITE_APP_URL: import.meta.env.VITE_APP_URL,
28
43
  VITE_API_URL: import.meta.env.VITE_API_URL,
29
44
  VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN,
45
+ VITE_SENTRY_ENABLED: import.meta.env.VITE_SENTRY_ENABLED,
46
+ VITE_CLERK_PUBLISHABLE_KEY: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY,
47
+ VITE_SUPABASE_DATABASE_URL: import.meta.env.VITE_SUPABASE_DATABASE_URL,
48
+ VITE_SUPABASE_ANON_KEY: import.meta.env.VITE_SUPABASE_ANON_KEY,
49
+ VITE_PERF_TEST: import.meta.env.VITE_PERF_TEST,
30
50
  MODE: import.meta.env.MODE,
31
51
  DEV: import.meta.env.DEV,
32
52
  PROD: import.meta.env.PROD,
@@ -35,15 +55,17 @@ export function validateEnv(): Env {
35
55
  const result = envSchema.safeParse(env);
36
56
 
37
57
  if (!result.success) {
38
- const errors = result.error.format();
39
- console.error('Environment validation failed:', errors);
58
+ const errors = result.error.flatten();
59
+ const fieldErrors = Object.entries(errors.fieldErrors)
60
+ .map(([key, msgs]) => `${key}: ${(msgs as string[]).join(', ')}`)
61
+ .join('; ');
62
+ const formErrors = errors.formErrors.join('; ');
63
+ const allErrors = [fieldErrors, formErrors].filter(Boolean).join('; ');
40
64
 
41
- if (import.meta.env.PROD) {
42
- throw new Error('Invalid environment configuration');
43
- }
65
+ throw new Error(`Environment validation failed: ${allErrors}`);
44
66
  }
45
67
 
46
- return result.success ? result.data : (env as Env);
68
+ return result.data;
47
69
  }
48
70
 
49
71
  /**
@@ -5,10 +5,13 @@
5
5
 
6
6
  export { cn } from './utils';
7
7
  export { STORAGE_KEYS, isAppKey } from './storageKeys';
8
- export { APP_CONFIG, SENTRY_CONFIG } from './config';
9
- export { API_CONFIG } from './api';
8
+ export { APP_CONFIG, API_CONFIG, SENTRY_CONFIG, CLERK_CONFIG, SUPABASE_CONFIG, PERFORMANCE_CONFIG } from './config';
10
9
  export { ROUTES, type AppRoute } from './routes';
11
10
  export { env, validateEnv, type Env } from './env';
12
11
  export { api, ApiClientError } from './api';
13
12
  export { getStorageItem, setStorageItem, removeStorageItem, clearAppStorage } from './storage';
14
13
  export { registerFormSchema, type RegisterFormData } from './validations';
14
+ export { createSelectors } from './createSelectors';
15
+
16
+ // Supabase
17
+ export { createSupabaseClient, type TypedSupabaseClient, type GetTokenFn } from './supabase';
@@ -5,6 +5,7 @@
5
5
 
6
6
  export const ROUTES = {
7
7
  HOME: '/',
8
+ PROFILE: '/profile',
8
9
  NOT_FOUND: '*',
9
10
  } as const;
10
11
 
@@ -0,0 +1,58 @@
1
+ import type * as SentryType from '@sentry/react';
2
+
3
+ import { SENTRY_CONFIG } from './config';
4
+ import { env } from './env';
5
+
6
+ let sentryInstance: typeof SentryType | null = null;
7
+
8
+ /**
9
+ * Initialize Sentry error tracking.
10
+ * Only runs in production when enabled and VITE_SENTRY_DSN is configured.
11
+ */
12
+ export async function initSentry(): Promise<typeof SentryType | null> {
13
+ // Skip if disabled, in development, already initialized, or no DSN
14
+ if (!SENTRY_CONFIG.enabled || env.DEV || sentryInstance || !SENTRY_CONFIG.dsn) {
15
+ return sentryInstance;
16
+ }
17
+
18
+ const sentry = await import('@sentry/react');
19
+
20
+ sentry.init({
21
+ dsn: SENTRY_CONFIG.dsn,
22
+ environment: SENTRY_CONFIG.environment,
23
+ sendDefaultPii: true,
24
+ integrations: [sentry.browserTracingIntegration()],
25
+ tracesSampleRate: SENTRY_CONFIG.tracesSampleRate,
26
+ });
27
+
28
+ sentryInstance = sentry;
29
+ return sentryInstance;
30
+ }
31
+
32
+ /**
33
+ * Lazily initialize Sentry after the app is interactive.
34
+ * Uses requestIdleCallback with fallback for older browsers.
35
+ */
36
+ export function lazySentryInit(): void {
37
+ const init = () => void initSentry();
38
+
39
+ if ('requestIdleCallback' in window) {
40
+ window.requestIdleCallback(init, { timeout: 2000 });
41
+ } else {
42
+ setTimeout(init, 1000);
43
+ }
44
+ }
45
+
46
+ interface CaptureContext {
47
+ componentStack?: string;
48
+ }
49
+
50
+ /**
51
+ * Capture an exception to Sentry.
52
+ * No-op if Sentry is not initialized.
53
+ */
54
+ export function captureException(error: unknown, context?: CaptureContext): void {
55
+ sentryInstance?.captureException(error, {
56
+ extra: context?.componentStack ? { componentStack: context.componentStack } : undefined,
57
+ });
58
+ }
@@ -4,7 +4,8 @@
4
4
 
5
5
  import { STORAGE_KEYS } from './storageKeys';
6
6
 
7
- type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
7
+ // Extract only string keys (exclude functions if any are added later)
8
+ type StorageKey = Extract<(typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS], string>;
8
9
 
9
10
  /**
10
11
  * Check if we're in a browser environment
@@ -81,7 +82,10 @@ export function clearAppStorage(): boolean {
81
82
 
82
83
  try {
83
84
  Object.values(STORAGE_KEYS).forEach((key) => {
84
- localStorage.removeItem(key);
85
+ // Skip function keys (like prompt()) if any exist
86
+ if (typeof key === 'string') {
87
+ localStorage.removeItem(key);
88
+ }
85
89
  });
86
90
  return true;
87
91
  } catch (error) {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Supabase client factory with Clerk authentication integration.
3
+ *
4
+ * Uses the modern `accessToken` pattern for third-party auth providers.
5
+ * The Clerk session token is automatically injected into Supabase requests.
6
+ *
7
+ * @see https://supabase.com/docs/guides/auth/third-party/clerk
8
+ */
9
+
10
+ import { createClient, type SupabaseClient } from '@supabase/supabase-js';
11
+
12
+ import type { Database } from '@/types/database';
13
+
14
+ import { SUPABASE_CONFIG } from '../config';
15
+
16
+ /**
17
+ * Typed Supabase client with database schema.
18
+ */
19
+ export type TypedSupabaseClient = SupabaseClient<Database>;
20
+
21
+ /**
22
+ * Token getter function type.
23
+ * Returns the Clerk session token or null if not authenticated.
24
+ */
25
+ export type GetTokenFn = () => Promise<string | null>;
26
+
27
+ /**
28
+ * Creates a Supabase client configured with Clerk authentication.
29
+ *
30
+ * Uses the `accessToken` configuration option which is the recommended
31
+ * approach for third-party auth providers like Clerk.
32
+ *
33
+ * @param getToken - Async function that returns the Clerk session token
34
+ * @returns Typed Supabase client
35
+ * @throws Error if Supabase environment variables are not configured
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const { session } = useSession();
40
+ * const supabase = createSupabaseClient(() => session?.getToken() ?? null);
41
+ *
42
+ * // Now use supabase with Clerk auth
43
+ * const { data } = await supabase.from('profiles').select();
44
+ * ```
45
+ */
46
+ export function createSupabaseClient(getToken: GetTokenFn): TypedSupabaseClient {
47
+ if (!SUPABASE_CONFIG.isConfigured) {
48
+ throw new Error(
49
+ 'Missing Supabase environment variables. ' +
50
+ 'Set VITE_SUPABASE_DATABASE_URL and VITE_SUPABASE_ANON_KEY in your .env file.',
51
+ );
52
+ }
53
+
54
+ // Type assertion safe here - isConfigured guarantees both are defined
55
+ return createClient<Database>(SUPABASE_CONFIG.url!, SUPABASE_CONFIG.anonKey!, {
56
+ accessToken: getToken,
57
+ });
58
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Supabase client exports.
3
+ */
4
+
5
+ export { createSupabaseClient, type TypedSupabaseClient, type GetTokenFn } from './client';
@@ -6,42 +6,26 @@ import { BrowserRouter } from 'react-router';
6
6
  import './index.css';
7
7
  import { ErrorBoundary } from '@/components/shared';
8
8
  import { Toaster } from '@/components/ui/sonner';
9
+ import { ClerkThemeProvider } from '@/contexts/clerkContext';
9
10
  import { MobileProvider } from '@/contexts/mobileContext';
10
11
  import { PerformanceProviderWrapper } from '@/contexts/performanceContext';
11
12
  import { QueryProvider } from '@/contexts/queryContext';
13
+ import { SupabaseProvider } from '@/contexts/supabaseContext';
12
14
  import { i18n, initI18n } from '@/i18n';
13
- import { SENTRY_CONFIG } from '@/lib/config';
15
+ import { CLERK_CONFIG } from '@/lib/config';
16
+ import { initSentry } from '@/lib/sentry';
14
17
  import { initPreferencesSync } from '@/stores/preferencesStore';
15
18
 
16
19
  import App from './App';
17
20
 
18
- /**
19
- * Lazy load Sentry after initial render to avoid blocking web vitals.
20
- * Returns the Sentry module for use in global error handlers.
21
- */
22
- async function initSentry() {
23
- if (import.meta.env.PROD && SENTRY_CONFIG.enabled && SENTRY_CONFIG.dsn) {
24
- try {
25
- const Sentry = await import('@sentry/react');
26
- Sentry.init({
27
- dsn: SENTRY_CONFIG.dsn,
28
- sendDefaultPii: true,
29
- integrations: [Sentry.browserTracingIntegration()],
30
- tracesSampleRate: SENTRY_CONFIG.tracesSampleRate,
31
- });
32
- return Sentry;
33
- } catch (error) {
34
- console.error('Failed to initialize Sentry:', error);
35
- }
36
- }
37
- return null;
38
- }
21
+ /** Sentry module type for error handlers */
22
+ type SentryModule = Awaited<ReturnType<typeof initSentry>>;
39
23
 
40
24
  /**
41
25
  * Setup global error handlers for uncaught errors and promise rejections.
42
26
  * @param Sentry - The Sentry module or null if not available
43
27
  */
44
- function setupGlobalErrorHandlers(Sentry: Awaited<ReturnType<typeof initSentry>>) {
28
+ function setupGlobalErrorHandlers(Sentry: SentryModule) {
45
29
  // Handle uncaught errors
46
30
  window.onerror = (message, source, lineno, colno, error) => {
47
31
  console.error('Uncaught error:', { message, source, lineno, colno, error });
@@ -74,14 +58,18 @@ initI18n().then(() => {
74
58
  <QueryProvider>
75
59
  <I18nProvider i18n={i18n}>
76
60
  <BrowserRouter>
77
- <MobileProvider>
78
- <ErrorBoundary>
79
- <PerformanceProviderWrapper>
80
- <App />
81
- <Toaster />
82
- </PerformanceProviderWrapper>
83
- </ErrorBoundary>
84
- </MobileProvider>
61
+ <ClerkThemeProvider publishableKey={CLERK_CONFIG.publishableKey!}>
62
+ <SupabaseProvider>
63
+ <MobileProvider>
64
+ <ErrorBoundary>
65
+ <PerformanceProviderWrapper>
66
+ <App />
67
+ <Toaster />
68
+ </PerformanceProviderWrapper>
69
+ </ErrorBoundary>
70
+ </MobileProvider>
71
+ </SupabaseProvider>
72
+ </ClerkThemeProvider>
85
73
  </BrowserRouter>
86
74
  </I18nProvider>
87
75
  </QueryProvider>
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared test constants for mocks and fixtures.
3
+ *
4
+ * Use these values across all mocks to ensure consistency
5
+ * between Clerk, Supabase, and other test utilities.
6
+ */
7
+
8
+ /** Mock user data - consistent across all auth/database mocks */
9
+ export const MOCK_USER = {
10
+ id: 'user_123',
11
+ email: 'test@example.com',
12
+ firstName: 'Test',
13
+ lastName: 'User',
14
+ fullName: 'Test User',
15
+ avatarUrl: 'https://example.com/avatar.jpg',
16
+ } as const;
17
+
18
+ /** Mock session ID for auth mocks */
19
+ export const MOCK_SESSION_ID = 'sess_123';
20
+
21
+ /** Mock auth token for API requests */
22
+ export const MOCK_AUTH_TOKEN = 'mock-auth-token';
23
+
24
+ /** Mock Supabase URL for MSW handlers */
25
+ export const MOCK_SUPABASE_URL = 'https://mock.supabase.co';
26
+
27
+ /** Default timestamps for fixtures */
28
+ export const MOCK_TIMESTAMPS = {
29
+ created: '2024-01-01T00:00:00.000Z',
30
+ updated: '2024-01-01T00:00:00.000Z',
31
+ } as const;
@@ -1 +1,3 @@
1
- export { mockTodos, createTodo, createTodos, type Todo } from './todos';
1
+ export { createTodo, createTodos, mockTodos, type Todo } from './todos';
2
+ export { createProfile, createProfiles, mockProfiles } from './profiles';
3
+ export { createUser, createUsers, defaultUser, mockUsers, type MockUser } from './users';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Mock profile fixtures for testing.
3
+ * Used by MSW handlers to simulate Supabase responses.
4
+ */
5
+
6
+ import type { Profile } from '@/types/database';
7
+
8
+ import { MOCK_TIMESTAMPS, MOCK_USER } from '../constants';
9
+
10
+ /** Counter for unique ID generation */
11
+ let idCounter = 0;
12
+
13
+ /**
14
+ * Sample profiles for MSW handlers.
15
+ */
16
+ export const mockProfiles: Profile[] = [
17
+ {
18
+ id: MOCK_USER.id,
19
+ email: MOCK_USER.email,
20
+ full_name: MOCK_USER.fullName,
21
+ avatar_url: null,
22
+ created_at: MOCK_TIMESTAMPS.created,
23
+ updated_at: MOCK_TIMESTAMPS.updated,
24
+ },
25
+ ];
26
+
27
+ /**
28
+ * Create a profile with optional overrides.
29
+ */
30
+ export function createProfile(overrides: Partial<Profile> = {}): Profile {
31
+ const now = new Date().toISOString();
32
+ return {
33
+ id: `user_${Date.now()}_${idCounter++}`,
34
+ email: MOCK_USER.email,
35
+ full_name: MOCK_USER.fullName,
36
+ avatar_url: null,
37
+ created_at: now,
38
+ updated_at: now,
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Create multiple profiles.
45
+ */
46
+ export function createProfiles(count: number, overrides: Partial<Profile> = {}): Profile[] {
47
+ return Array.from({ length: count }, (_, i) =>
48
+ createProfile({
49
+ id: `user_${i + 1}`,
50
+ email: `user${i + 1}@example.com`,
51
+ full_name: `User ${i + 1}`,
52
+ ...overrides,
53
+ }),
54
+ );
55
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Mock user fixtures for Clerk authentication testing.
3
+ * Used by clerkMock.tsx to simulate authenticated users.
4
+ */
5
+
6
+ import { MOCK_USER } from '../constants';
7
+
8
+ // =============================================================================
9
+ // Types
10
+ // =============================================================================
11
+
12
+ export interface MockUser {
13
+ id: string;
14
+ firstName: string;
15
+ lastName: string;
16
+ fullName?: string;
17
+ primaryEmailAddress?: { emailAddress: string };
18
+ imageUrl?: string;
19
+ }
20
+
21
+ // =============================================================================
22
+ // Default User
23
+ // =============================================================================
24
+
25
+ /** Default mock user based on MOCK_USER constants */
26
+ export const defaultUser: MockUser = {
27
+ id: MOCK_USER.id,
28
+ firstName: MOCK_USER.firstName,
29
+ lastName: MOCK_USER.lastName,
30
+ fullName: MOCK_USER.fullName,
31
+ primaryEmailAddress: { emailAddress: MOCK_USER.email },
32
+ imageUrl: MOCK_USER.avatarUrl,
33
+ };
34
+
35
+ // =============================================================================
36
+ // Static Fixtures
37
+ // =============================================================================
38
+
39
+ /** Sample users for MSW handlers */
40
+ export const mockUsers: MockUser[] = [{ ...defaultUser }];
41
+
42
+ // =============================================================================
43
+ // Factory Functions
44
+ // =============================================================================
45
+
46
+ /** Counter for unique ID generation */
47
+ let idCounter = 0;
48
+
49
+ /**
50
+ * Create a mock user with optional overrides.
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * const user = createUser({ firstName: 'Jane' });
55
+ * ```
56
+ */
57
+ export function createUser(overrides: Partial<MockUser> = {}): MockUser {
58
+ const id = overrides.id ?? `user_${Date.now()}_${idCounter++}`;
59
+ const firstName = overrides.firstName ?? MOCK_USER.firstName;
60
+ const lastName = overrides.lastName ?? MOCK_USER.lastName;
61
+
62
+ return {
63
+ id,
64
+ firstName,
65
+ lastName,
66
+ fullName: `${firstName} ${lastName}`,
67
+ primaryEmailAddress: { emailAddress: MOCK_USER.email },
68
+ imageUrl: MOCK_USER.avatarUrl,
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Create multiple mock users.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const users = createUsers(3);
79
+ * ```
80
+ */
81
+ export function createUsers(count: number, overrides: Partial<MockUser> = {}): MockUser[] {
82
+ return Array.from({ length: count }, (_, i) =>
83
+ createUser({
84
+ id: `user_${i + 1}`,
85
+ firstName: `User${i + 1}`,
86
+ lastName: 'Test',
87
+ primaryEmailAddress: { emailAddress: `user${i + 1}@example.com` },
88
+ ...overrides,
89
+ }),
90
+ );
91
+ }