@react-spa-scaffold/mcp 2.2.0 → 2.4.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 (138) hide show
  1. package/dist/constants.d.ts +4 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +4 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/features/definitions/api.d.ts.map +1 -1
  6. package/dist/features/definitions/api.js +2 -1
  7. package/dist/features/definitions/api.js.map +1 -1
  8. package/dist/features/definitions/database.d.ts +3 -0
  9. package/dist/features/definitions/database.d.ts.map +1 -0
  10. package/dist/features/definitions/database.js +45 -0
  11. package/dist/features/definitions/database.js.map +1 -0
  12. package/dist/features/definitions/deployment.d.ts +3 -0
  13. package/dist/features/definitions/deployment.d.ts.map +1 -0
  14. package/dist/features/definitions/deployment.js +14 -0
  15. package/dist/features/definitions/deployment.js.map +1 -0
  16. package/dist/features/definitions/electron.d.ts +3 -0
  17. package/dist/features/definitions/electron.d.ts.map +1 -0
  18. package/dist/features/definitions/electron.js +23 -0
  19. package/dist/features/definitions/electron.js.map +1 -0
  20. package/dist/features/definitions/index.d.ts +3 -0
  21. package/dist/features/definitions/index.d.ts.map +1 -1
  22. package/dist/features/definitions/index.js +3 -0
  23. package/dist/features/definitions/index.js.map +1 -1
  24. package/dist/features/registry.d.ts.map +1 -1
  25. package/dist/features/registry.js +4 -1
  26. package/dist/features/registry.js.map +1 -1
  27. package/dist/features/types.d.ts +1 -0
  28. package/dist/features/types.d.ts.map +1 -1
  29. package/dist/features/types.test.js +5 -2
  30. package/dist/features/types.test.js.map +1 -1
  31. package/dist/resources/docs.d.ts.map +1 -1
  32. package/dist/resources/docs.js +5 -0
  33. package/dist/resources/docs.js.map +1 -1
  34. package/dist/tools/add-features.js +1 -1
  35. package/dist/tools/add-features.js.map +1 -1
  36. package/dist/tools/get-features.test.js +7 -0
  37. package/dist/tools/get-features.test.js.map +1 -1
  38. package/dist/tools/get-scaffold.d.ts +1 -0
  39. package/dist/tools/get-scaffold.d.ts.map +1 -1
  40. package/dist/tools/get-scaffold.js +4 -1
  41. package/dist/tools/get-scaffold.js.map +1 -1
  42. package/dist/tools/get-scaffold.test.js +50 -0
  43. package/dist/tools/get-scaffold.test.js.map +1 -1
  44. package/dist/utils/docs.d.ts.map +1 -1
  45. package/dist/utils/docs.js +2 -0
  46. package/dist/utils/docs.js.map +1 -1
  47. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  48. package/dist/utils/scaffold/claude-md/index.js +4 -1
  49. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  50. package/dist/utils/scaffold/claude-md/sections.d.ts +3 -0
  51. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  52. package/dist/utils/scaffold/claude-md/sections.js +174 -2
  53. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  54. package/dist/utils/scaffold/compute.d.ts.map +1 -1
  55. package/dist/utils/scaffold/compute.js +4 -2
  56. package/dist/utils/scaffold/compute.js.map +1 -1
  57. package/dist/utils/scaffold/generators.d.ts +7 -2
  58. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  59. package/dist/utils/scaffold/generators.js +100 -22
  60. package/dist/utils/scaffold/generators.js.map +1 -1
  61. package/package.json +1 -1
  62. package/templates/.env.example +40 -12
  63. package/templates/.github/workflows/ci.yml +49 -2
  64. package/templates/.github/workflows/deploy.yml +46 -0
  65. package/templates/CLAUDE.md +180 -1
  66. package/templates/docs/AUTHENTICATION.md +325 -0
  67. package/templates/docs/DEPLOYMENT.md +296 -0
  68. package/templates/docs/E2E_TESTING.md +81 -4
  69. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  70. package/templates/docs/TESTING.md +195 -77
  71. package/templates/e2e/auth/auth.setup.ts +60 -0
  72. package/templates/e2e/fixtures/index.ts +11 -0
  73. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  74. package/templates/e2e/tests/profile.spec.ts +64 -0
  75. package/templates/e2e/tests/register-form.spec.ts +38 -0
  76. package/templates/forge.config.js +53 -0
  77. package/templates/gitignore +5 -0
  78. package/templates/package.json +13 -1
  79. package/templates/playwright.config.ts +33 -3
  80. package/templates/src/App.tsx +32 -19
  81. package/templates/src/components/layout/Header.test.tsx +17 -1
  82. package/templates/src/components/layout/Header.tsx +11 -0
  83. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +3 -3
  84. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  85. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  86. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  87. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +3 -3
  88. package/templates/src/components/shared/index.ts +1 -0
  89. package/templates/src/contexts/performanceContext.tsx +3 -3
  90. package/templates/src/contexts/queryContext.tsx +9 -8
  91. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  92. package/templates/src/contexts/supabaseContext.tsx +87 -0
  93. package/templates/src/hooks/index.ts +17 -0
  94. package/templates/src/hooks/supabase/index.ts +12 -0
  95. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  96. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  97. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  98. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  99. package/templates/src/lib/api.test.ts +30 -38
  100. package/templates/src/lib/api.ts +1 -7
  101. package/templates/src/lib/config.ts +54 -4
  102. package/templates/src/lib/env.ts +36 -14
  103. package/templates/src/lib/index.ts +4 -2
  104. package/templates/src/lib/routes.ts +1 -0
  105. package/templates/src/lib/sentry.ts +13 -10
  106. package/templates/src/lib/supabase/client.ts +58 -0
  107. package/templates/src/lib/supabase/index.ts +5 -0
  108. package/templates/src/main.ts +227 -0
  109. package/templates/src/main.tsx +32 -42
  110. package/templates/src/mocks/constants.ts +31 -0
  111. package/templates/src/mocks/fixtures/index.ts +3 -1
  112. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  113. package/templates/src/mocks/fixtures/users.ts +91 -0
  114. package/templates/src/mocks/handlers/index.ts +2 -1
  115. package/templates/src/mocks/handlers/supabase.ts +64 -0
  116. package/templates/src/mocks/handlers/todos.ts +1 -1
  117. package/templates/src/mocks/index.ts +6 -0
  118. package/templates/src/pages/Profile.test.tsx +263 -0
  119. package/templates/src/pages/Profile.tsx +171 -0
  120. package/templates/src/pages/index.ts +1 -0
  121. package/templates/src/preload.ts +26 -0
  122. package/templates/src/stores/preferencesStore.ts +2 -1
  123. package/templates/src/test/clerkMock.tsx +49 -9
  124. package/templates/src/test/fetchMock.ts +58 -0
  125. package/templates/src/test/index.ts +49 -3
  126. package/templates/src/test/mocks.ts +128 -1
  127. package/templates/src/test/providers.tsx +7 -4
  128. package/templates/src/test/supabaseMock.ts +112 -0
  129. package/templates/src/test-setup.ts +26 -0
  130. package/templates/src/types/database.ts +46 -0
  131. package/templates/src/types/global.d.ts +28 -0
  132. package/templates/src/types/index.ts +1 -0
  133. package/templates/src/types/supabase.ts +167 -0
  134. package/templates/src/vite-env.d.ts +6 -0
  135. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
  136. package/templates/vite.main.config.mjs +20 -0
  137. package/templates/vite.preload.config.mjs +17 -0
  138. package/templates/vite.renderer.config.mjs +52 -0
@@ -1,18 +1,21 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { api, ApiClientError } from '@/lib/api';
4
+ import {
5
+ mockFetchError,
6
+ mockFetchNetworkError,
7
+ mockFetchNoContent,
8
+ mockFetchSuccess,
9
+ mockFetchUnknownError,
10
+ } from '@/test';
4
11
 
5
12
  describe('api client', () => {
6
- const mockFetch = vi.fn();
7
- const originalFetch = global.fetch;
8
-
9
13
  beforeEach(() => {
10
- global.fetch = mockFetch;
11
- vi.clearAllMocks();
14
+ vi.spyOn(global, 'fetch');
12
15
  });
13
16
 
14
17
  afterEach(() => {
15
- global.fetch = originalFetch;
18
+ vi.restoreAllMocks();
16
19
  });
17
20
 
18
21
  describe('HTTP methods', () => {
@@ -23,15 +26,11 @@ describe('api client', () => {
23
26
  { method: 'patch', httpMethod: 'PATCH' },
24
27
  { method: 'delete', httpMethod: 'DELETE' },
25
28
  ] as const)('$method makes $httpMethod request', async ({ method, httpMethod }) => {
26
- mockFetch.mockResolvedValueOnce({
27
- ok: true,
28
- status: 200,
29
- json: () => Promise.resolve({ id: 1 }),
30
- });
29
+ mockFetchSuccess({ id: 1 });
31
30
 
32
31
  await api[method]('/test');
33
32
 
34
- expect(mockFetch).toHaveBeenCalledWith(
33
+ expect(global.fetch).toHaveBeenCalledWith(
35
34
  expect.stringContaining('/test'),
36
35
  expect.objectContaining({ method: httpMethod }),
37
36
  );
@@ -39,30 +38,22 @@ describe('api client', () => {
39
38
 
40
39
  it('sends request body for POST/PUT/PATCH', async () => {
41
40
  const body = { name: 'Test' };
42
- mockFetch.mockResolvedValueOnce({
43
- ok: true,
44
- status: 200,
45
- json: () => Promise.resolve({ id: 1 }),
46
- });
41
+ mockFetchSuccess({ id: 1 });
47
42
 
48
43
  await api.post('/test', body);
49
44
 
50
- expect(mockFetch).toHaveBeenCalledWith(
45
+ expect(global.fetch).toHaveBeenCalledWith(
51
46
  expect.anything(),
52
47
  expect.objectContaining({ body: JSON.stringify(body) }),
53
48
  );
54
49
  });
55
50
 
56
51
  it('handles full URL without prepending base URL', async () => {
57
- mockFetch.mockResolvedValueOnce({
58
- ok: true,
59
- status: 200,
60
- json: () => Promise.resolve({}),
61
- });
52
+ mockFetchSuccess({});
62
53
 
63
54
  await api.get('https://external.api/data');
64
55
 
65
- expect(mockFetch).toHaveBeenCalledWith('https://external.api/data', expect.anything());
56
+ expect(global.fetch).toHaveBeenCalledWith('https://external.api/data', expect.anything());
66
57
  });
67
58
  });
68
59
 
@@ -71,12 +62,7 @@ describe('api client', () => {
71
62
  { status: 404, message: 'Resource not found', hasJson: true },
72
63
  { status: 500, message: 'Internal Server Error', hasJson: false },
73
64
  ])('handles $status error response', async ({ status, message, hasJson }) => {
74
- mockFetch.mockResolvedValueOnce({
75
- ok: false,
76
- status,
77
- statusText: message,
78
- json: hasJson ? () => Promise.resolve({ message }) : () => Promise.reject(new Error('No JSON')),
79
- });
65
+ mockFetchError(status, message, hasJson);
80
66
 
81
67
  const error = (await api.get('/error').catch((e) => e)) as ApiClientError;
82
68
 
@@ -87,7 +73,7 @@ describe('api client', () => {
87
73
 
88
74
  it('handles timeout with TIMEOUT code', async () => {
89
75
  vi.useFakeTimers();
90
- mockFetch.mockImplementationOnce(
76
+ vi.mocked(global.fetch).mockImplementationOnce(
91
77
  () =>
92
78
  new Promise((_, reject) => {
93
79
  const error = new Error('Aborted');
@@ -106,20 +92,26 @@ describe('api client', () => {
106
92
  vi.useRealTimers();
107
93
  });
108
94
 
109
- it.each([
110
- { rejection: new Error('Network failure'), code: 'NETWORK_ERROR' },
111
- { rejection: 'string error', code: 'UNKNOWN' },
112
- ])('handles $code errors', async ({ rejection, code }) => {
113
- mockFetch.mockRejectedValueOnce(rejection);
95
+ it('handles NETWORK_ERROR', async () => {
96
+ mockFetchNetworkError();
97
+
98
+ const error = (await api.get('/error').catch((e) => e)) as ApiClientError;
99
+
100
+ expect(error).toBeInstanceOf(ApiClientError);
101
+ expect(error.code).toBe('NETWORK_ERROR');
102
+ });
103
+
104
+ it('handles UNKNOWN errors', async () => {
105
+ mockFetchUnknownError('string error');
114
106
 
115
107
  const error = (await api.get('/error').catch((e) => e)) as ApiClientError;
116
108
 
117
109
  expect(error).toBeInstanceOf(ApiClientError);
118
- expect(error.code).toBe(code);
110
+ expect(error.code).toBe('UNKNOWN');
119
111
  });
120
112
 
121
113
  it('returns undefined for 204 No Content', async () => {
122
- mockFetch.mockResolvedValueOnce({ ok: true, status: 204 });
114
+ mockFetchNoContent();
123
115
 
124
116
  const result = await api.delete('/test/1');
125
117
 
@@ -5,13 +5,7 @@
5
5
 
6
6
  import type { ApiError } from '@/types/api';
7
7
 
8
- /**
9
- * API configuration.
10
- */
11
- export const API_CONFIG = {
12
- baseUrl: import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com',
13
- timeout: 30000,
14
- } as const;
8
+ import { API_CONFIG } from './config';
15
9
 
16
10
  /**
17
11
  * Custom API error class
@@ -1,15 +1,65 @@
1
1
  /**
2
2
  * Application configuration.
3
3
  * Centralized config for feature flags, etc.
4
+ *
5
+ * All environment variables flow through the validated `env` object from env.ts.
4
6
  */
5
7
 
8
+ import { env } from './env';
9
+
10
+ // =============================================================================
11
+ // App Configuration
12
+ // =============================================================================
13
+
6
14
  export const APP_CONFIG = {
7
- name: import.meta.env.VITE_APP_NAME || 'My App',
8
- url: import.meta.env.VITE_APP_URL || 'http://localhost:5173',
15
+ name: env.VITE_APP_NAME,
16
+ url: env.VITE_APP_URL,
17
+ } as const;
18
+
19
+ // =============================================================================
20
+ // API Configuration
21
+ // =============================================================================
22
+
23
+ export const API_CONFIG = {
24
+ baseUrl: env.VITE_API_URL,
25
+ timeout: 30000,
9
26
  } as const;
10
27
 
28
+ // =============================================================================
29
+ // Sentry Configuration
30
+ // =============================================================================
31
+
11
32
  export const SENTRY_CONFIG = {
12
- enabled: import.meta.env.VITE_SENTRY_ENABLED !== 'false',
13
- dsn: import.meta.env.VITE_SENTRY_DSN,
33
+ enabled: env.VITE_SENTRY_ENABLED,
34
+ dsn: env.VITE_SENTRY_DSN,
35
+ environment: env.MODE,
14
36
  tracesSampleRate: 0.1,
15
37
  } as const;
38
+
39
+ // =============================================================================
40
+ // Clerk Configuration
41
+ // =============================================================================
42
+
43
+ export const CLERK_CONFIG = {
44
+ publishableKey: env.VITE_CLERK_PUBLISHABLE_KEY,
45
+ } as const;
46
+
47
+ // =============================================================================
48
+ // Supabase Configuration
49
+ // =============================================================================
50
+
51
+ export const SUPABASE_CONFIG = {
52
+ url: env.VITE_SUPABASE_DATABASE_URL,
53
+ anonKey: env.VITE_SUPABASE_ANON_KEY,
54
+ /** Whether both URL and anon key are configured */
55
+ isConfigured: Boolean(env.VITE_SUPABASE_DATABASE_URL && env.VITE_SUPABASE_ANON_KEY),
56
+ } as const;
57
+
58
+ // =============================================================================
59
+ // Performance Configuration
60
+ // =============================================================================
61
+
62
+ export const PERFORMANCE_CONFIG = {
63
+ /** Enable performance tracking in dev or when VITE_PERF_TEST is set */
64
+ enabled: env.DEV || env.VITE_PERF_TEST,
65
+ } as const;
@@ -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,11 +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';
15
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
 
@@ -1,29 +1,32 @@
1
1
  import type * as SentryType from '@sentry/react';
2
2
 
3
+ import { SENTRY_CONFIG } from './config';
4
+ import { env } from './env';
5
+
3
6
  let sentryInstance: typeof SentryType | null = null;
4
7
 
5
8
  /**
6
9
  * Initialize Sentry error tracking.
7
- * Only runs in production when VITE_SENTRY_DSN is configured.
10
+ * Only runs in production when enabled and VITE_SENTRY_DSN is configured.
8
11
  */
9
- export async function initSentry(): Promise<void> {
10
- // Skip in development or if already initialized
11
- if (import.meta.env.DEV || sentryInstance) return;
12
-
13
- const dsn = import.meta.env.VITE_SENTRY_DSN;
14
- if (!dsn) return;
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
+ }
15
17
 
16
18
  const sentry = await import('@sentry/react');
17
19
 
18
20
  sentry.init({
19
- dsn,
20
- environment: import.meta.env.MODE,
21
+ dsn: SENTRY_CONFIG.dsn,
22
+ environment: SENTRY_CONFIG.environment,
21
23
  sendDefaultPii: true,
22
24
  integrations: [sentry.browserTracingIntegration()],
23
- tracesSampleRate: 0.1,
25
+ tracesSampleRate: SENTRY_CONFIG.tracesSampleRate,
24
26
  });
25
27
 
26
28
  sentryInstance = sentry;
29
+ return sentryInstance;
27
30
  }
28
31
 
29
32
  /**
@@ -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';
@@ -0,0 +1,227 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { app, BrowserWindow, ipcMain, Menu, type MenuItemConstructorOptions } from 'electron';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ // Track window state
9
+ interface WindowState {
10
+ alwaysOnTop: boolean;
11
+ contentProtection: boolean;
12
+ }
13
+
14
+ let mainWindow: BrowserWindow | null = null;
15
+ const windowState: WindowState = {
16
+ alwaysOnTop: false,
17
+ contentProtection: false,
18
+ };
19
+
20
+ function createMenu(): void {
21
+ const isMac = process.platform === 'darwin';
22
+
23
+ const template: MenuItemConstructorOptions[] = [
24
+ // App menu (macOS only)
25
+ ...(isMac
26
+ ? [
27
+ {
28
+ label: app.name,
29
+ submenu: [
30
+ { role: 'about' as const },
31
+ { type: 'separator' as const },
32
+ { role: 'services' as const },
33
+ { type: 'separator' as const },
34
+ { role: 'hide' as const },
35
+ { role: 'hideOthers' as const },
36
+ { role: 'unhide' as const },
37
+ { type: 'separator' as const },
38
+ { role: 'quit' as const },
39
+ ],
40
+ },
41
+ ]
42
+ : []),
43
+ // File menu
44
+ {
45
+ label: 'File',
46
+ submenu: [isMac ? { role: 'close' as const } : { role: 'quit' as const }],
47
+ },
48
+ // Edit menu
49
+ {
50
+ label: 'Edit',
51
+ submenu: [
52
+ { role: 'undo' as const },
53
+ { role: 'redo' as const },
54
+ { type: 'separator' as const },
55
+ { role: 'cut' as const },
56
+ { role: 'copy' as const },
57
+ { role: 'paste' as const },
58
+ ...(isMac
59
+ ? [{ role: 'pasteAndMatchStyle' as const }, { role: 'delete' as const }, { role: 'selectAll' as const }]
60
+ : [{ role: 'delete' as const }, { type: 'separator' as const }, { role: 'selectAll' as const }]),
61
+ ],
62
+ },
63
+ // View menu
64
+ {
65
+ label: 'View',
66
+ submenu: [
67
+ { role: 'reload' as const },
68
+ { role: 'forceReload' as const },
69
+ { role: 'toggleDevTools' as const },
70
+ { type: 'separator' as const },
71
+ { role: 'resetZoom' as const },
72
+ { role: 'zoomIn' as const },
73
+ { role: 'zoomOut' as const },
74
+ { type: 'separator' as const },
75
+ { role: 'togglefullscreen' as const },
76
+ { type: 'separator' as const },
77
+ {
78
+ label: 'Always on Top',
79
+ type: 'checkbox' as const,
80
+ checked: windowState.alwaysOnTop,
81
+ click: () => {
82
+ windowState.alwaysOnTop = !windowState.alwaysOnTop;
83
+ mainWindow?.setAlwaysOnTop(windowState.alwaysOnTop, 'floating');
84
+ createMenu(); // Refresh menu to update checkbox state
85
+ },
86
+ },
87
+ {
88
+ label: 'Hide from Screen Sharing',
89
+ type: 'checkbox' as const,
90
+ checked: windowState.contentProtection,
91
+ click: () => {
92
+ windowState.contentProtection = !windowState.contentProtection;
93
+ mainWindow?.setContentProtection(windowState.contentProtection);
94
+ createMenu(); // Refresh menu to update checkbox state
95
+ },
96
+ },
97
+ ],
98
+ },
99
+ // Window menu
100
+ {
101
+ label: 'Window',
102
+ submenu: [
103
+ { role: 'minimize' as const },
104
+ { role: 'zoom' as const },
105
+ ...(isMac
106
+ ? [
107
+ { type: 'separator' as const },
108
+ { role: 'front' as const },
109
+ { type: 'separator' as const },
110
+ { role: 'window' as const },
111
+ ]
112
+ : [{ role: 'close' as const }]),
113
+ ],
114
+ },
115
+ ];
116
+
117
+ const menu = Menu.buildFromTemplate(template);
118
+ Menu.setApplicationMenu(menu);
119
+ }
120
+
121
+ function createWindow(): void {
122
+ // Determine preload path based on environment
123
+ const preloadPath = path.join(__dirname, 'preload.js');
124
+
125
+ mainWindow = new BrowserWindow({
126
+ width: 1280,
127
+ height: 800,
128
+ minWidth: 800,
129
+ minHeight: 600,
130
+ webPreferences: {
131
+ preload: preloadPath,
132
+ contextIsolation: true,
133
+ nodeIntegration: false,
134
+ sandbox: true,
135
+ },
136
+ // macOS specific styling
137
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
138
+ trafficLightPosition: { x: 16, y: 16 },
139
+ });
140
+
141
+ // Load the app with retry logic for dev server
142
+ // Increased retries and delay to handle Vite restart during dep-scan
143
+ const loadApp = async (retries = 10, delay = 2000): Promise<void> => {
144
+ if (!mainWindow) return;
145
+
146
+ if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
147
+ try {
148
+ await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
149
+ } catch (error) {
150
+ if (retries > 0) {
151
+ console.log(`Failed to load dev server, retrying in ${delay}ms... (${retries} retries left)`);
152
+ await new Promise((resolve) => setTimeout(resolve, delay));
153
+ return loadApp(retries - 1, delay);
154
+ }
155
+ console.error('Failed to load dev server after all retries:', error);
156
+ }
157
+ } else {
158
+ mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
159
+ }
160
+ };
161
+
162
+ loadApp();
163
+
164
+ // Open DevTools in development
165
+ if (process.env.NODE_ENV === 'development' || MAIN_WINDOW_VITE_DEV_SERVER_URL) {
166
+ mainWindow.webContents.openDevTools();
167
+ }
168
+
169
+ mainWindow.on('closed', () => {
170
+ mainWindow = null;
171
+ });
172
+ }
173
+
174
+ // Setup IPC handlers
175
+ function setupIpcHandlers(): void {
176
+ // Toggle always on top
177
+ ipcMain.handle('window:setAlwaysOnTop', (_event, enable: boolean) => {
178
+ if (mainWindow) {
179
+ windowState.alwaysOnTop = enable;
180
+ mainWindow.setAlwaysOnTop(enable, 'floating');
181
+ createMenu(); // Update menu checkbox state
182
+ return true;
183
+ }
184
+ return false;
185
+ });
186
+
187
+ // Toggle content protection (hide from screen sharing)
188
+ ipcMain.handle('window:setContentProtection', (_event, enable: boolean) => {
189
+ if (mainWindow) {
190
+ windowState.contentProtection = enable;
191
+ mainWindow.setContentProtection(enable);
192
+ createMenu(); // Update menu checkbox state
193
+ return true;
194
+ }
195
+ return false;
196
+ });
197
+
198
+ // Get current window state
199
+ ipcMain.handle('window:getState', () => {
200
+ return { ...windowState };
201
+ });
202
+ }
203
+
204
+ // App lifecycle
205
+ app.whenReady().then(() => {
206
+ setupIpcHandlers();
207
+ createMenu();
208
+ createWindow();
209
+
210
+ app.on('activate', () => {
211
+ // On macOS, re-create window when dock icon is clicked
212
+ if (BrowserWindow.getAllWindows().length === 0) {
213
+ createWindow();
214
+ }
215
+ });
216
+ });
217
+
218
+ app.on('window-all-closed', () => {
219
+ // On macOS, keep app running until explicitly quit
220
+ if (process.platform !== 'darwin') {
221
+ app.quit();
222
+ }
223
+ });
224
+
225
+ // Declare globals injected by Electron Forge Vite plugin
226
+ declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
227
+ declare const MAIN_WINDOW_VITE_NAME: string;