@react-spa-scaffold/mcp 2.2.0 → 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.
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -0
- package/dist/constants.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/index.d.ts +2 -0
- package/dist/features/definitions/index.d.ts.map +1 -1
- package/dist/features/definitions/index.js +2 -0
- package/dist/features/definitions/index.js.map +1 -1
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +3 -1
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.test.js +4 -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/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 +3 -1
- package/dist/utils/scaffold/claude-md/index.js.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
- package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.js +132 -2
- package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
- package/dist/utils/scaffold/compute.js +1 -1
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts +2 -2
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +57 -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 +4 -1
- package/templates/.github/workflows/deploy.yml +59 -0
- package/templates/CLAUDE.md +177 -1
- package/templates/docs/AUTHENTICATION.md +325 -0
- package/templates/docs/DEPLOYMENT.md +268 -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/gitignore +5 -0
- package/templates/package.json +8 -0
- 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/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.tsx +17 -39
- 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/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/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
|
@@ -30,6 +30,10 @@ export function generateViteEnvDts(featureIds) {
|
|
|
30
30
|
if (featureIds.includes(FEATURE.AUTH)) {
|
|
31
31
|
envVars.push(' readonly VITE_CLERK_PUBLISHABLE_KEY: string;');
|
|
32
32
|
}
|
|
33
|
+
if (featureIds.includes(FEATURE.DATABASE)) {
|
|
34
|
+
envVars.push(' readonly VITE_SUPABASE_DATABASE_URL: string;');
|
|
35
|
+
envVars.push(' readonly VITE_SUPABASE_ANON_KEY: string;');
|
|
36
|
+
}
|
|
33
37
|
// Vite built-in env vars (always required for TypeScript)
|
|
34
38
|
envVars.push(" readonly MODE: 'development' | 'production' | 'test';");
|
|
35
39
|
envVars.push(' readonly DEV: boolean;');
|
|
@@ -48,21 +52,20 @@ interface ImportMeta {
|
|
|
48
52
|
}
|
|
49
53
|
/** Generate env.ts content based on selected features. */
|
|
50
54
|
export function generateEnvTs(featureIds) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
];
|
|
55
|
+
// Check if we need the booleanEnv helper
|
|
56
|
+
const needsBooleanEnv = featureIds.includes(FEATURE.OBSERVABILITY) || featureIds.includes(FEATURE.PERFORMANCE);
|
|
57
|
+
const schemaFields = [' VITE_APP_NAME: z.string().min(1),', ' VITE_APP_URL: z.string().url(),'];
|
|
55
58
|
const envFields = [
|
|
56
59
|
' VITE_APP_NAME: import.meta.env.VITE_APP_NAME,',
|
|
57
60
|
' VITE_APP_URL: import.meta.env.VITE_APP_URL,',
|
|
58
61
|
];
|
|
59
62
|
if (featureIds.includes(FEATURE.API)) {
|
|
60
|
-
schemaFields.push(' VITE_API_URL: z.string().url()
|
|
63
|
+
schemaFields.push(' VITE_API_URL: z.string().url(),');
|
|
61
64
|
envFields.push(' VITE_API_URL: import.meta.env.VITE_API_URL,');
|
|
62
65
|
}
|
|
63
66
|
if (featureIds.includes(FEATURE.OBSERVABILITY)) {
|
|
64
|
-
schemaFields.push(' VITE_SENTRY_DSN: z.string().url()
|
|
65
|
-
schemaFields.push(' VITE_SENTRY_ENABLED:
|
|
67
|
+
schemaFields.push(' VITE_SENTRY_DSN: z.string().url(),');
|
|
68
|
+
schemaFields.push(' VITE_SENTRY_ENABLED: booleanEnv,');
|
|
66
69
|
envFields.push(' VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN,');
|
|
67
70
|
envFields.push(' VITE_SENTRY_ENABLED: import.meta.env.VITE_SENTRY_ENABLED,');
|
|
68
71
|
}
|
|
@@ -70,20 +73,45 @@ export function generateEnvTs(featureIds) {
|
|
|
70
73
|
schemaFields.push(' VITE_CLERK_PUBLISHABLE_KEY: z.string().min(1),');
|
|
71
74
|
envFields.push(' VITE_CLERK_PUBLISHABLE_KEY: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY,');
|
|
72
75
|
}
|
|
76
|
+
if (featureIds.includes(FEATURE.DATABASE)) {
|
|
77
|
+
schemaFields.push(' VITE_SUPABASE_DATABASE_URL: z.string().url(),');
|
|
78
|
+
schemaFields.push(' VITE_SUPABASE_ANON_KEY: z.string().min(1),');
|
|
79
|
+
envFields.push(' VITE_SUPABASE_DATABASE_URL: import.meta.env.VITE_SUPABASE_DATABASE_URL,');
|
|
80
|
+
envFields.push(' VITE_SUPABASE_ANON_KEY: import.meta.env.VITE_SUPABASE_ANON_KEY,');
|
|
81
|
+
}
|
|
82
|
+
if (featureIds.includes(FEATURE.PERFORMANCE)) {
|
|
83
|
+
schemaFields.push(' VITE_PERF_TEST: booleanEnv,');
|
|
84
|
+
envFields.push(' VITE_PERF_TEST: import.meta.env.VITE_PERF_TEST,');
|
|
85
|
+
}
|
|
73
86
|
// Vite built-in env vars (always included)
|
|
74
|
-
schemaFields.push(" MODE: z.enum(['development', 'production', 'test'])
|
|
75
|
-
schemaFields.push(' DEV: z.boolean()
|
|
76
|
-
schemaFields.push(' PROD: z.boolean()
|
|
87
|
+
schemaFields.push(" MODE: z.enum(['development', 'production', 'test']),");
|
|
88
|
+
schemaFields.push(' DEV: z.boolean(),');
|
|
89
|
+
schemaFields.push(' PROD: z.boolean(),');
|
|
77
90
|
envFields.push(' MODE: import.meta.env.MODE,');
|
|
78
91
|
envFields.push(' DEV: import.meta.env.DEV,');
|
|
79
92
|
envFields.push(' PROD: import.meta.env.PROD,');
|
|
93
|
+
// Build the booleanEnv helper if needed
|
|
94
|
+
const booleanEnvHelper = needsBooleanEnv
|
|
95
|
+
? `
|
|
96
|
+
/**
|
|
97
|
+
* Transforms string env var to boolean.
|
|
98
|
+
* - 'true', '1' → true
|
|
99
|
+
* - 'false', '0' → false
|
|
100
|
+
*/
|
|
101
|
+
const booleanEnv = z.enum(['true', 'false', '1', '0']).transform((val) => val === 'true' || val === '1');
|
|
102
|
+
|
|
103
|
+
`
|
|
104
|
+
: '';
|
|
80
105
|
return `/**
|
|
81
106
|
* Environment variable validation using Zod.
|
|
82
107
|
* Validates at runtime to catch missing/invalid env vars early.
|
|
108
|
+
*
|
|
109
|
+
* All env vars are REQUIRED. The MCP scaffold tool strips unused vars
|
|
110
|
+
* when scaffolding builds without certain features.
|
|
83
111
|
*/
|
|
84
112
|
|
|
85
113
|
import { z } from 'zod';
|
|
86
|
-
|
|
114
|
+
${booleanEnvHelper}
|
|
87
115
|
const envSchema = z.object({
|
|
88
116
|
${schemaFields.join('\n')}
|
|
89
117
|
});
|
|
@@ -92,7 +120,7 @@ export type Env = z.infer<typeof envSchema>;
|
|
|
92
120
|
|
|
93
121
|
/**
|
|
94
122
|
* Validate environment variables and return typed env object.
|
|
95
|
-
* Throws if
|
|
123
|
+
* Throws if any required env var is missing or invalid.
|
|
96
124
|
*/
|
|
97
125
|
export function validateEnv(): Env {
|
|
98
126
|
const env = {
|
|
@@ -102,15 +130,17 @@ ${envFields.join('\n')}
|
|
|
102
130
|
const result = envSchema.safeParse(env);
|
|
103
131
|
|
|
104
132
|
if (!result.success) {
|
|
105
|
-
const errors = result.error.
|
|
106
|
-
|
|
133
|
+
const errors = result.error.flatten();
|
|
134
|
+
const fieldErrors = Object.entries(errors.fieldErrors)
|
|
135
|
+
.map(([key, msgs]) => \`\${key}: \${(msgs as string[]).join(', ')}\`)
|
|
136
|
+
.join('; ');
|
|
137
|
+
const formErrors = errors.formErrors.join('; ');
|
|
138
|
+
const allErrors = [fieldErrors, formErrors].filter(Boolean).join('; ');
|
|
107
139
|
|
|
108
|
-
|
|
109
|
-
throw new Error('Invalid environment configuration');
|
|
110
|
-
}
|
|
140
|
+
throw new Error(\`Environment validation failed: \${allErrors}\`);
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
return result.data
|
|
143
|
+
return result.data;
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
/**
|
|
@@ -120,16 +150,21 @@ ${envFields.join('\n')}
|
|
|
120
150
|
export const env = validateEnv();
|
|
121
151
|
`;
|
|
122
152
|
}
|
|
123
|
-
/** Generates routes.ts content. */
|
|
124
|
-
export function generateRoutesTs() {
|
|
153
|
+
/** Generates routes.ts content based on selected features. */
|
|
154
|
+
export function generateRoutesTs(featureIds) {
|
|
155
|
+
const routes = [" HOME: '/',"];
|
|
156
|
+
// Add PROFILE route when database feature is selected
|
|
157
|
+
if (featureIds.includes(FEATURE.DATABASE)) {
|
|
158
|
+
routes.push(" PROFILE: '/profile',");
|
|
159
|
+
}
|
|
160
|
+
routes.push(" NOT_FOUND: '*',");
|
|
125
161
|
return `/**
|
|
126
162
|
* Typed route constants.
|
|
127
163
|
* Use these instead of hardcoded strings for type-safe navigation.
|
|
128
164
|
*/
|
|
129
165
|
|
|
130
166
|
export const ROUTES = {
|
|
131
|
-
|
|
132
|
-
NOT_FOUND: '*',
|
|
167
|
+
${routes.join('\n')}
|
|
133
168
|
} as const;
|
|
134
169
|
|
|
135
170
|
export type AppRoute = (typeof ROUTES)[keyof typeof ROUTES];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generators.js","sourceRoot":"","sources":["../../../src/utils/scaffold/generators.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,iEAAiE;AACjE,MAAM,UAAU,kBAAkB,CAAC,UAAuB;IACxD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,wCAAwC;IACxC,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,QAAQ,CAAC,IAAI,CAAC;;;EAGhB,CAAC,CAAC;IACF,CAAC;IAED,mCAAmC;IACnC,MAAM,OAAO,GAAa,CAAC,mCAAmC,EAAE,kCAAkC,CAAC,CAAC;IAEpG,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IACjE,CAAC;IAED,0DAA0D;IAC1D,OAAO,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;IACxE,OAAO,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAE1C,QAAQ,CAAC,IAAI,CAAC;;;EAGd,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;EAMlB,CAAC,CAAC;IAEF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;AACtC,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAAC,UAAuB;IACnD,MAAM,
|
|
1
|
+
{"version":3,"file":"generators.js","sourceRoot":"","sources":["../../../src/utils/scaffold/generators.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/templates/.env.example
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ─────────────────────────────────────────────────────────────
|
|
2
|
+
# Application (required)
|
|
3
|
+
# ─────────────────────────────────────────────────────────────
|
|
2
4
|
VITE_APP_NAME="My App"
|
|
3
5
|
VITE_APP_URL=http://localhost:5173
|
|
4
|
-
|
|
5
|
-
# Optional: Base URL for deployment subdirectory
|
|
6
|
-
# VITE_BASE_URL=/
|
|
6
|
+
VITE_API_URL=https://jsonplaceholder.typicode.com
|
|
7
7
|
|
|
8
8
|
# ─────────────────────────────────────────────────────────────
|
|
9
|
-
# Sentry Error Tracking (
|
|
9
|
+
# Sentry Error Tracking (required)
|
|
10
10
|
# ─────────────────────────────────────────────────────────────
|
|
11
|
-
# Set to 'false' to disable Sentry entirely (opt-out)
|
|
12
|
-
# Enabled by default when DSN is provided
|
|
13
|
-
# VITE_SENTRY_ENABLED=true
|
|
14
|
-
|
|
15
11
|
# Runtime DSN for error reporting (client-side, safe to expose)
|
|
16
|
-
#
|
|
12
|
+
# Get from: https://sentry.io/settings/projects/YOUR_PROJECT/keys/
|
|
13
|
+
VITE_SENTRY_DSN=https://xxxxx@o123456.ingest.sentry.io/789
|
|
14
|
+
VITE_SENTRY_ENABLED=true
|
|
17
15
|
|
|
18
16
|
# CI/CD secrets for source map upload (set in GitHub Secrets):
|
|
19
17
|
# - SENTRY_AUTH_TOKEN: API token for uploading source maps
|
|
@@ -21,7 +19,37 @@ VITE_APP_URL=http://localhost:5173
|
|
|
21
19
|
# - SENTRY_PROJECT: Sentry project slug
|
|
22
20
|
|
|
23
21
|
# ─────────────────────────────────────────────────────────────
|
|
24
|
-
# Clerk Authentication
|
|
22
|
+
# Clerk Authentication (required)
|
|
25
23
|
# ─────────────────────────────────────────────────────────────
|
|
26
24
|
# Get your Publishable Key from: https://dashboard.clerk.com/~/api-keys
|
|
27
|
-
VITE_CLERK_PUBLISHABLE_KEY=
|
|
25
|
+
VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────────────────────
|
|
28
|
+
# Supabase Database (required)
|
|
29
|
+
# ─────────────────────────────────────────────────────────────
|
|
30
|
+
# Get your Project URL and API Key from:
|
|
31
|
+
# https://supabase.com/dashboard/project/YOUR_PROJECT/settings/api
|
|
32
|
+
#
|
|
33
|
+
# For Netlify deployments, these can be auto-configured via the
|
|
34
|
+
# Netlify Supabase extension: Extensions > Supabase > Connect
|
|
35
|
+
VITE_SUPABASE_DATABASE_URL=https://your-project.supabase.co
|
|
36
|
+
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────
|
|
39
|
+
# Performance Testing (required)
|
|
40
|
+
# ─────────────────────────────────────────────────────────────
|
|
41
|
+
VITE_PERF_TEST=false
|
|
42
|
+
|
|
43
|
+
# Supabase CLI (required for npm run db:types)
|
|
44
|
+
# Project ID is the subdomain from your Supabase URL (e.g., abc123xyz from https://abc123xyz.supabase.co)
|
|
45
|
+
SUPABASE_PROJECT_ID=your-project-id
|
|
46
|
+
|
|
47
|
+
# ─────────────────────────────────────────────────────────────
|
|
48
|
+
# E2E Testing (optional - for authenticated Playwright tests)
|
|
49
|
+
# ─────────────────────────────────────────────────────────────
|
|
50
|
+
# Create a test user in Clerk and provide credentials here
|
|
51
|
+
# Required for running: npx playwright test --project=authenticated
|
|
52
|
+
# E2E_CLERK_USER_USERNAME=test@example.com
|
|
53
|
+
# E2E_CLERK_USER_PASSWORD=your-test-password
|
|
54
|
+
# CLERK_SECRET_KEY=sk_test_xxxxx
|
|
55
|
+
|
|
@@ -96,7 +96,7 @@ jobs:
|
|
|
96
96
|
upload-on: failure
|
|
97
97
|
- type: performance
|
|
98
98
|
project: performance
|
|
99
|
-
command: PERF_TEST=true npx playwright test --project=performance
|
|
99
|
+
command: PERF_TEST=true PERF_CI=true npx playwright test --project=performance
|
|
100
100
|
report-name: performance-report
|
|
101
101
|
upload-on: always
|
|
102
102
|
steps:
|
|
@@ -106,6 +106,9 @@ jobs:
|
|
|
106
106
|
with:
|
|
107
107
|
name: dist
|
|
108
108
|
path: dist/
|
|
109
|
+
- name: Rebuild with performance tracking
|
|
110
|
+
if: matrix.type == 'performance'
|
|
111
|
+
run: VITE_PERF_TEST=true npm run build
|
|
109
112
|
- name: Get Playwright version
|
|
110
113
|
id: playwright-version
|
|
111
114
|
run: echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pull-requests: write
|
|
13
|
+
deployments: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: deploy-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
deploy:
|
|
21
|
+
name: Deploy
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
timeout-minutes: 15
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
|
|
27
|
+
- uses: ./.github/actions/setup-node-deps
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build
|
|
31
|
+
|
|
32
|
+
- name: Deploy Preview
|
|
33
|
+
if: github.event_name == 'pull_request'
|
|
34
|
+
uses: nwtgck/actions-netlify@v3
|
|
35
|
+
with:
|
|
36
|
+
publish-dir: './dist'
|
|
37
|
+
production-deploy: false
|
|
38
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
39
|
+
deploy-message: 'Preview deploy from PR #${{ github.event.number }}'
|
|
40
|
+
enable-pull-request-comment: true
|
|
41
|
+
enable-commit-comment: false
|
|
42
|
+
overwrites-pull-request-comment: true
|
|
43
|
+
alias: pr-${{ github.event.number }}
|
|
44
|
+
env:
|
|
45
|
+
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
46
|
+
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
|
47
|
+
|
|
48
|
+
- name: Deploy Production
|
|
49
|
+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
50
|
+
uses: nwtgck/actions-netlify@v3
|
|
51
|
+
with:
|
|
52
|
+
publish-dir: './dist'
|
|
53
|
+
production-deploy: true
|
|
54
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
55
|
+
deploy-message: 'Production deploy from ${{ github.sha }}'
|
|
56
|
+
enable-commit-comment: true
|
|
57
|
+
env:
|
|
58
|
+
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
59
|
+
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -19,6 +19,9 @@ npm run e2e:mobile # Playwright E2E (mobile)
|
|
|
19
19
|
npm run e2e:all # Playwright E2E (all viewports)
|
|
20
20
|
npm run e2e:perf # Performance regression tests
|
|
21
21
|
npm run i18n:extract # Extract translations to .po
|
|
22
|
+
npm run db:types # Generate Supabase TypeScript types
|
|
23
|
+
npm run db:push # Push database migrations
|
|
24
|
+
npm run db:studio # Open Supabase Studio
|
|
22
25
|
```
|
|
23
26
|
|
|
24
27
|
## Project Structure
|
|
@@ -213,10 +216,183 @@ import { render, mockMatchMedia, server } from '@/test';
|
|
|
213
216
|
|
|
214
217
|
MSW handlers auto-reset after each test.
|
|
215
218
|
|
|
219
|
+
## Authentication (Clerk)
|
|
220
|
+
|
|
221
|
+
When the auth feature is enabled, Clerk authentication is required.
|
|
222
|
+
|
|
223
|
+
### Setup
|
|
224
|
+
|
|
225
|
+
1. Create an account at [clerk.com](https://clerk.com)
|
|
226
|
+
2. Get your Publishable Key from the dashboard
|
|
227
|
+
3. Copy `.env.example` to `.env` and set your key:
|
|
228
|
+
```
|
|
229
|
+
VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Usage
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
// Protect routes that require authentication
|
|
236
|
+
import { ProtectedRoute } from '@/components/shared';
|
|
237
|
+
|
|
238
|
+
<Route
|
|
239
|
+
path="/dashboard"
|
|
240
|
+
element={
|
|
241
|
+
<ProtectedRoute>
|
|
242
|
+
<DashboardPage />
|
|
243
|
+
</ProtectedRoute>
|
|
244
|
+
}
|
|
245
|
+
/>;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
// Conditional rendering based on auth state
|
|
250
|
+
import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/react-router';
|
|
251
|
+
|
|
252
|
+
<SignedIn>
|
|
253
|
+
<UserButton />
|
|
254
|
+
</SignedIn>
|
|
255
|
+
<SignedOut>
|
|
256
|
+
<SignInButton mode="modal">
|
|
257
|
+
<Button>Sign In</Button>
|
|
258
|
+
</SignInButton>
|
|
259
|
+
</SignedOut>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Testing
|
|
263
|
+
|
|
264
|
+
Clerk is automatically mocked in tests. Use test utilities to control auth state:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { setMockClerkSignedIn, resetClerkMocks } from '@/test';
|
|
268
|
+
|
|
269
|
+
beforeEach(() => resetClerkMocks());
|
|
270
|
+
|
|
271
|
+
it('shows sign-in when not authenticated', () => {
|
|
272
|
+
setMockClerkSignedIn(false);
|
|
273
|
+
// ...
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Database (Supabase)
|
|
278
|
+
|
|
279
|
+
Supabase provides PostgreSQL database with Row Level Security (RLS), integrated with Clerk authentication.
|
|
280
|
+
|
|
281
|
+
### Setup
|
|
282
|
+
|
|
283
|
+
1. Create a project at [supabase.com](https://supabase.com)
|
|
284
|
+
2. Configure Clerk as third-party auth provider:
|
|
285
|
+
- Supabase Dashboard → Authentication → Providers → Third-Party Auth → Add Clerk
|
|
286
|
+
3. Enable Supabase integration in Clerk:
|
|
287
|
+
- Clerk Dashboard → Integrations → Supabase → Activate
|
|
288
|
+
4. Set environment variables in `.env`:
|
|
289
|
+
```
|
|
290
|
+
VITE_SUPABASE_DATABASE_URL=https://your-project.supabase.co
|
|
291
|
+
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Database Commands
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npm run db:types # Generate TypeScript types from schema
|
|
298
|
+
npm run db:push # Push migrations to database
|
|
299
|
+
npm run db:reset # Reset database (WARNING: destructive)
|
|
300
|
+
npm run db:studio # Open Supabase Studio
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Usage
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { useSupabase, useSupabaseQuery, useProfile } from '@/hooks';
|
|
307
|
+
|
|
308
|
+
// Direct client access
|
|
309
|
+
const supabase = useSupabase();
|
|
310
|
+
const { data } = await supabase.from('profiles').select();
|
|
311
|
+
|
|
312
|
+
// TanStack Query wrapper for automatic caching
|
|
313
|
+
const { data, isLoading } = useSupabaseQuery({
|
|
314
|
+
table: 'profiles',
|
|
315
|
+
queryKey: ['current'],
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Convenience hook for current user's profile
|
|
319
|
+
const { profile, isLoading, exists } = useProfile();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Profile Mutations
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
import { useUpsertProfile, useUpdateProfile, useDeleteProfile } from '@/hooks';
|
|
326
|
+
|
|
327
|
+
// Create or update profile (upsert)
|
|
328
|
+
const upsertProfile = useUpsertProfile();
|
|
329
|
+
await upsertProfile.mutateAsync({ id: userId, email: 'user@example.com' });
|
|
330
|
+
|
|
331
|
+
// Update current user's profile
|
|
332
|
+
const updateProfile = useUpdateProfile();
|
|
333
|
+
await updateProfile.mutateAsync({ full_name: 'John' });
|
|
334
|
+
|
|
335
|
+
// Delete current user's profile
|
|
336
|
+
const deleteProfile = useDeleteProfile();
|
|
337
|
+
await deleteProfile.mutateAsync();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Auto-Sync with ProfileSync
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
import { ProfileSync } from '@/components/shared';
|
|
344
|
+
|
|
345
|
+
// Add to your app to auto-sync Clerk user data to Supabase
|
|
346
|
+
function App() {
|
|
347
|
+
return (
|
|
348
|
+
<>
|
|
349
|
+
<ProfileSync />
|
|
350
|
+
<Routes>...</Routes>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Row Level Security (RLS)
|
|
357
|
+
|
|
358
|
+
All tables should have RLS enabled. Policies use `auth.uid()` which equals the Clerk user_id:
|
|
359
|
+
|
|
360
|
+
```sql
|
|
361
|
+
-- Users can only access their own data
|
|
362
|
+
CREATE POLICY "Users can view own profile"
|
|
363
|
+
ON profiles FOR SELECT TO authenticated
|
|
364
|
+
USING (id = auth.uid());
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Testing
|
|
368
|
+
|
|
369
|
+
Supabase context is mocked in tests with state controls:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
import { render, setMockSupabaseData, setMockSupabaseError, createProfile, resetSupabaseMocks } from '@/test';
|
|
373
|
+
|
|
374
|
+
beforeEach(() => resetSupabaseMocks());
|
|
375
|
+
|
|
376
|
+
it('displays profile data', async () => {
|
|
377
|
+
setMockSupabaseData([createProfile({ full_name: 'Test User' })]);
|
|
378
|
+
render(<ProfileCard />);
|
|
379
|
+
// Assert profile is displayed
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('handles error', async () => {
|
|
383
|
+
setMockSupabaseError({ message: 'Failed', code: 'ERROR' });
|
|
384
|
+
render(<ProfileCard />);
|
|
385
|
+
// Assert error state
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
216
389
|
## Common Gotchas
|
|
217
390
|
|
|
218
391
|
1. **Node.js >= 22.0.0** required (check `.nvmrc`)
|
|
219
392
|
2. **Conventional commits** enforced by commitlint
|
|
220
|
-
3. **Context hooks throw** outside provider (e.g., `useMobileContext()`)
|
|
393
|
+
3. **Context hooks throw** outside provider (e.g., `useMobileContext()`, `useSupabase()`)
|
|
221
394
|
4. **Barrel exports** in each directory via `index.ts`
|
|
222
395
|
5. **UI components** import directly: `@/components/ui/button` (no barrel)
|
|
396
|
+
6. **Clerk auth required** when auth feature is enabled - set `VITE_CLERK_PUBLISHABLE_KEY` in `.env`
|
|
397
|
+
7. **Supabase requires Clerk** - SupabaseProvider must be inside ClerkProvider
|
|
398
|
+
8. **RLS policies required** - All Supabase tables should have Row Level Security enabled
|