@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.
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/features/definitions/api.d.ts.map +1 -1
- package/dist/features/definitions/api.js +2 -1
- package/dist/features/definitions/api.js.map +1 -1
- package/dist/features/definitions/database.d.ts +3 -0
- package/dist/features/definitions/database.d.ts.map +1 -0
- package/dist/features/definitions/database.js +45 -0
- package/dist/features/definitions/database.js.map +1 -0
- package/dist/features/definitions/deployment.d.ts +3 -0
- package/dist/features/definitions/deployment.d.ts.map +1 -0
- package/dist/features/definitions/deployment.js +14 -0
- package/dist/features/definitions/deployment.js.map +1 -0
- package/dist/features/definitions/electron.d.ts +3 -0
- package/dist/features/definitions/electron.d.ts.map +1 -0
- package/dist/features/definitions/electron.js +23 -0
- package/dist/features/definitions/electron.js.map +1 -0
- package/dist/features/definitions/index.d.ts +3 -0
- package/dist/features/definitions/index.d.ts.map +1 -1
- package/dist/features/definitions/index.js +3 -0
- package/dist/features/definitions/index.js.map +1 -1
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +4 -1
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.d.ts +1 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/features/types.test.js +5 -2
- package/dist/features/types.test.js.map +1 -1
- package/dist/resources/docs.d.ts.map +1 -1
- package/dist/resources/docs.js +5 -0
- package/dist/resources/docs.js.map +1 -1
- package/dist/tools/add-features.js +1 -1
- package/dist/tools/add-features.js.map +1 -1
- package/dist/tools/get-features.test.js +7 -0
- package/dist/tools/get-features.test.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts +1 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +4 -1
- package/dist/tools/get-scaffold.js.map +1 -1
- package/dist/tools/get-scaffold.test.js +50 -0
- package/dist/tools/get-scaffold.test.js.map +1 -1
- package/dist/utils/docs.d.ts.map +1 -1
- package/dist/utils/docs.js +2 -0
- package/dist/utils/docs.js.map +1 -1
- package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/index.js +4 -1
- package/dist/utils/scaffold/claude-md/index.js.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.d.ts +3 -0
- package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.js +174 -2
- package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
- package/dist/utils/scaffold/compute.d.ts.map +1 -1
- package/dist/utils/scaffold/compute.js +4 -2
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts +7 -2
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +100 -22
- package/dist/utils/scaffold/generators.js.map +1 -1
- package/package.json +1 -1
- package/templates/.env.example +40 -12
- package/templates/.github/workflows/ci.yml +49 -2
- package/templates/.github/workflows/deploy.yml +46 -0
- package/templates/CLAUDE.md +180 -1
- package/templates/docs/AUTHENTICATION.md +325 -0
- package/templates/docs/DEPLOYMENT.md +296 -0
- package/templates/docs/E2E_TESTING.md +81 -4
- package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
- package/templates/docs/TESTING.md +195 -77
- package/templates/e2e/auth/auth.setup.ts +60 -0
- package/templates/e2e/fixtures/index.ts +11 -0
- package/templates/e2e/tests/profile.auth.spec.ts +103 -0
- package/templates/e2e/tests/profile.spec.ts +64 -0
- package/templates/e2e/tests/register-form.spec.ts +38 -0
- package/templates/forge.config.js +53 -0
- package/templates/gitignore +5 -0
- package/templates/package.json +13 -1
- package/templates/playwright.config.ts +33 -3
- package/templates/src/App.tsx +32 -19
- package/templates/src/components/layout/Header.test.tsx +17 -1
- package/templates/src/components/layout/Header.tsx +11 -0
- package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +3 -3
- package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
- package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
- package/templates/src/components/shared/ProfileSync/index.ts +1 -0
- package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +3 -3
- package/templates/src/components/shared/index.ts +1 -0
- package/templates/src/contexts/performanceContext.tsx +3 -3
- package/templates/src/contexts/queryContext.tsx +9 -8
- package/templates/src/contexts/supabaseContext.test.tsx +59 -0
- package/templates/src/contexts/supabaseContext.tsx +87 -0
- package/templates/src/hooks/index.ts +17 -0
- package/templates/src/hooks/supabase/index.ts +12 -0
- package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
- package/templates/src/hooks/supabase/useProfiles.ts +213 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
- package/templates/src/lib/api.test.ts +30 -38
- package/templates/src/lib/api.ts +1 -7
- package/templates/src/lib/config.ts +54 -4
- package/templates/src/lib/env.ts +36 -14
- package/templates/src/lib/index.ts +4 -2
- package/templates/src/lib/routes.ts +1 -0
- package/templates/src/lib/sentry.ts +13 -10
- package/templates/src/lib/supabase/client.ts +58 -0
- package/templates/src/lib/supabase/index.ts +5 -0
- package/templates/src/main.ts +227 -0
- package/templates/src/main.tsx +32 -42
- package/templates/src/mocks/constants.ts +31 -0
- package/templates/src/mocks/fixtures/index.ts +3 -1
- package/templates/src/mocks/fixtures/profiles.ts +55 -0
- package/templates/src/mocks/fixtures/users.ts +91 -0
- package/templates/src/mocks/handlers/index.ts +2 -1
- package/templates/src/mocks/handlers/supabase.ts +64 -0
- package/templates/src/mocks/handlers/todos.ts +1 -1
- package/templates/src/mocks/index.ts +6 -0
- package/templates/src/pages/Profile.test.tsx +263 -0
- package/templates/src/pages/Profile.tsx +171 -0
- package/templates/src/pages/index.ts +1 -0
- package/templates/src/preload.ts +26 -0
- package/templates/src/stores/preferencesStore.ts +2 -1
- package/templates/src/test/clerkMock.tsx +49 -9
- package/templates/src/test/fetchMock.ts +58 -0
- package/templates/src/test/index.ts +49 -3
- package/templates/src/test/mocks.ts +128 -1
- package/templates/src/test/providers.tsx +7 -4
- package/templates/src/test/supabaseMock.ts +112 -0
- package/templates/src/test-setup.ts +26 -0
- package/templates/src/types/database.ts +46 -0
- package/templates/src/types/global.d.ts +28 -0
- package/templates/src/types/index.ts +1 -0
- package/templates/src/types/supabase.ts +167 -0
- package/templates/src/vite-env.d.ts +6 -0
- package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
- package/templates/vite.main.config.mjs +20 -0
- package/templates/vite.preload.config.mjs +17 -0
- 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
|
|
11
|
-
vi.clearAllMocks();
|
|
14
|
+
vi.spyOn(global, 'fetch');
|
|
12
15
|
});
|
|
13
16
|
|
|
14
17
|
afterEach(() => {
|
|
15
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
110
|
+
expect(error.code).toBe('UNKNOWN');
|
|
119
111
|
});
|
|
120
112
|
|
|
121
113
|
it('returns undefined for 204 No Content', async () => {
|
|
122
|
-
|
|
114
|
+
mockFetchNoContent();
|
|
123
115
|
|
|
124
116
|
const result = await api.delete('/test/1');
|
|
125
117
|
|
package/templates/src/lib/api.ts
CHANGED
|
@@ -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:
|
|
8
|
-
url:
|
|
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:
|
|
13
|
-
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;
|
package/templates/src/lib/env.ts
CHANGED
|
@@ -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)
|
|
10
|
-
VITE_APP_URL: z.string().url()
|
|
11
|
-
VITE_API_URL: z.string().url()
|
|
12
|
-
VITE_SENTRY_DSN: z.string().url()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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.
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
throw new Error('Invalid environment configuration');
|
|
43
|
-
}
|
|
65
|
+
throw new Error(`Environment validation failed: ${allErrors}`);
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
return result.
|
|
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';
|
|
@@ -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<
|
|
10
|
-
// Skip in development or
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
21
|
+
dsn: SENTRY_CONFIG.dsn,
|
|
22
|
+
environment: SENTRY_CONFIG.environment,
|
|
21
23
|
sendDefaultPii: true,
|
|
22
24
|
integrations: [sentry.browserTracingIntegration()],
|
|
23
|
-
tracesSampleRate:
|
|
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,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;
|