@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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Clerk authentication integration with shadcn theming and Supabase token injection.
|
|
4
|
+
For quick-start usage, see [CLAUDE.md](../CLAUDE.md#authentication-clerk).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Why Clerk
|
|
9
|
+
|
|
10
|
+
This project uses Clerk instead of Supabase Auth for:
|
|
11
|
+
|
|
12
|
+
- **Production-ready UI** - Pre-built `SignInButton`, `UserButton` components with modal flows
|
|
13
|
+
- **Superior OAuth** - 20+ providers vs ~10, dashboard configuration vs code
|
|
14
|
+
- **Automatic session management** - Cross-tab sync, token refresh handled transparently
|
|
15
|
+
- **Separation of concerns** - Clerk = authentication (who), Supabase = authorization + data (what)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ClerkProvider (ClerkThemeProvider wrapper)
|
|
23
|
+
│
|
|
24
|
+
├── useAuth() → { isLoaded, isSignedIn, userId, getToken }
|
|
25
|
+
├── useUser() → { user: { id, email, fullName, imageUrl } }
|
|
26
|
+
└── useSession() → { session.getToken() } → JWT Token
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
SupabaseProvider injects token
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
Supabase validates JWT
|
|
33
|
+
auth.uid() = Clerk user_id
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Authentication Flow:**
|
|
37
|
+
|
|
38
|
+
1. User clicks Sign In → Clerk modal opens
|
|
39
|
+
2. User authenticates (email, OAuth, etc.)
|
|
40
|
+
3. Clerk creates session, `useAuth()` returns `isSignedIn: true`
|
|
41
|
+
4. `useSession().getToken()` provides JWT for Supabase
|
|
42
|
+
5. RLS policies grant access via `auth.uid()`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Setup
|
|
47
|
+
|
|
48
|
+
### 1. Environment Variable
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Get from [Clerk Dashboard](https://dashboard.clerk.com) → API Keys.
|
|
55
|
+
|
|
56
|
+
### 2. Dashboard Setup
|
|
57
|
+
|
|
58
|
+
1. Create application at [clerk.com](https://clerk.com)
|
|
59
|
+
2. Configure sign-in methods (Email, Google, GitHub, etc.)
|
|
60
|
+
3. **For Supabase**: Integrations → Supabase → Activate (adds `role: authenticated` claim)
|
|
61
|
+
4. Copy Clerk domain → Supabase Dashboard → Authentication → Add Clerk provider
|
|
62
|
+
|
|
63
|
+
### 3. Provider Hierarchy
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// main.tsx - SupabaseProvider MUST be inside ClerkProvider
|
|
67
|
+
<ClerkThemeProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
|
|
68
|
+
<SupabaseProvider>
|
|
69
|
+
<App />
|
|
70
|
+
</SupabaseProvider>
|
|
71
|
+
</ClerkThemeProvider>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## File Structure
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
src/
|
|
80
|
+
├── contexts/
|
|
81
|
+
│ └── clerkContext.tsx # ClerkThemeProvider with shadcn theme
|
|
82
|
+
├── components/shared/
|
|
83
|
+
│ ├── AccountButton/ # Sign in / User button
|
|
84
|
+
│ ├── ProtectedRoute/ # Auth guard wrapper
|
|
85
|
+
│ └── ProfileSync/ # Auto-sync Clerk → Supabase
|
|
86
|
+
├── test/
|
|
87
|
+
│ └── clerkMock.tsx # Comprehensive Clerk mocks
|
|
88
|
+
├── mocks/
|
|
89
|
+
│ ├── constants.ts # Mock user/session constants
|
|
90
|
+
│ └── fixtures/users.ts # Mock user factories
|
|
91
|
+
└── index.css # Includes @clerk/themes/shadcn.css
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Theme Configuration
|
|
97
|
+
|
|
98
|
+
### ClerkThemeProvider
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// src/contexts/clerkContext.tsx
|
|
102
|
+
import { ClerkProvider } from '@clerk/react-router';
|
|
103
|
+
import { shadcn } from '@clerk/themes';
|
|
104
|
+
|
|
105
|
+
const appearance: Appearance = {
|
|
106
|
+
baseTheme: shadcn,
|
|
107
|
+
variables: {
|
|
108
|
+
fontFamily: '"Inter Variable", sans-serif',
|
|
109
|
+
borderRadius: '0.45rem',
|
|
110
|
+
},
|
|
111
|
+
elements: {
|
|
112
|
+
modalBackdrop: 'backdrop-blur-sm',
|
|
113
|
+
modalContent: 'sm:max-w-md max-sm:min-h-svh max-sm:min-w-full max-sm:rounded-none',
|
|
114
|
+
card: 'max-sm:rounded-none max-sm:shadow-none',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function ClerkThemeProvider({ children, publishableKey }: Props) {
|
|
119
|
+
return (
|
|
120
|
+
<ClerkProvider publishableKey={publishableKey} afterSignOutUrl="/" appearance={appearance}>
|
|
121
|
+
{children}
|
|
122
|
+
</ClerkProvider>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### CSS Import
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
/* src/index.css */
|
|
131
|
+
@import '@clerk/themes/shadcn.css';
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Enables automatic light/dark mode adaptation with shadcn CSS variables.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Components
|
|
139
|
+
|
|
140
|
+
| Component | Location | Purpose |
|
|
141
|
+
| -------------------- | ----------------------------------- | ---------------------------------------------------- |
|
|
142
|
+
| `ClerkThemeProvider` | `contexts/clerkContext.tsx` | Clerk wrapper with shadcn theme |
|
|
143
|
+
| `AccountButton` | `components/shared/AccountButton/` | Sign-in button (logged out) / UserButton (logged in) |
|
|
144
|
+
| `ProtectedRoute` | `components/shared/ProtectedRoute/` | Auth guard, redirects to sign-in |
|
|
145
|
+
| `ProfileSync` | `components/shared/ProfileSync/` | Auto-syncs Clerk user → Supabase profiles |
|
|
146
|
+
|
|
147
|
+
### AccountButton
|
|
148
|
+
|
|
149
|
+
Shows `SignInButton` when logged out, `UserButton` when logged in. Displays skeleton while loading.
|
|
150
|
+
|
|
151
|
+
### ProtectedRoute
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<Route
|
|
155
|
+
path="/dashboard"
|
|
156
|
+
element={
|
|
157
|
+
<ProtectedRoute>
|
|
158
|
+
<DashboardPage />
|
|
159
|
+
</ProtectedRoute>
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Returns `<PageLoading />` while checking auth, `<RedirectToSignIn />` if not authenticated.
|
|
165
|
+
|
|
166
|
+
### ProfileSync
|
|
167
|
+
|
|
168
|
+
Invisible component that syncs Clerk user data to Supabase on sign-in:
|
|
169
|
+
|
|
170
|
+
- `id` → Clerk user ID
|
|
171
|
+
- `email` → Primary email
|
|
172
|
+
- `full_name` → Full name
|
|
173
|
+
- `avatar_url` → Profile image
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Hooks Reference
|
|
178
|
+
|
|
179
|
+
All hooks imported from `@clerk/react-router`:
|
|
180
|
+
|
|
181
|
+
| Hook | Key Returns | Use Case |
|
|
182
|
+
| -------------- | ------------------------------------------------------------- | -------------------------------------------- |
|
|
183
|
+
| `useAuth()` | `isLoaded`, `isSignedIn`, `userId`, `sessionId`, `getToken()` | Auth state without user details |
|
|
184
|
+
| `useUser()` | `isLoaded`, `user` | User profile (id, email, fullName, imageUrl) |
|
|
185
|
+
| `useSession()` | `isLoaded`, `session.getToken()` | JWT token for API calls |
|
|
186
|
+
| `useClerk()` | `signOut({ redirectUrl })` | Programmatic sign-out |
|
|
187
|
+
|
|
188
|
+
### User Object Properties
|
|
189
|
+
|
|
190
|
+
| Property | Type | Description |
|
|
191
|
+
| ---------------------------------------- | ---------------- | ------------- |
|
|
192
|
+
| `user.id` | `string` | Clerk user ID |
|
|
193
|
+
| `user.primaryEmailAddress?.emailAddress` | `string` | Email |
|
|
194
|
+
| `user.fullName` | `string \| null` | Full name |
|
|
195
|
+
| `user.imageUrl` | `string` | Avatar URL |
|
|
196
|
+
|
|
197
|
+
For usage examples, see [CLAUDE.md](../CLAUDE.md#authentication-clerk).
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Supabase Integration
|
|
202
|
+
|
|
203
|
+
Clerk tokens are injected into Supabase for authenticated database access:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// src/contexts/supabaseContext.tsx
|
|
207
|
+
const { session } = useSession();
|
|
208
|
+
|
|
209
|
+
const supabase = useMemo(
|
|
210
|
+
() =>
|
|
211
|
+
createSupabaseClient(async () => {
|
|
212
|
+
if (!session) return null;
|
|
213
|
+
return session.getToken(); // Clerk JWT injected
|
|
214
|
+
}),
|
|
215
|
+
[session?.id], // Only recreate on sign in/out, not every render
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Key points:**
|
|
220
|
+
|
|
221
|
+
- JWT includes `sub` (user ID) and `role: authenticated` claims
|
|
222
|
+
- Supabase `auth.uid()` equals Clerk user ID
|
|
223
|
+
- RLS policies enforce user-scoped access
|
|
224
|
+
|
|
225
|
+
See [SUPABASE_INTEGRATION.md](./SUPABASE_INTEGRATION.md) for full details.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Testing
|
|
230
|
+
|
|
231
|
+
### Mock Utilities
|
|
232
|
+
|
|
233
|
+
Import from `@/test`:
|
|
234
|
+
|
|
235
|
+
| Utility | Purpose |
|
|
236
|
+
| ---------------------------- | ---------------------------------------- |
|
|
237
|
+
| `setMockClerkSignedIn(bool)` | Set sign-in status |
|
|
238
|
+
| `setMockClerkLoaded(bool)` | Set loading state |
|
|
239
|
+
| `setMockClerkState({ ... })` | Set multiple values |
|
|
240
|
+
| `setMockClerkUser({ ... })` | Customize mock user |
|
|
241
|
+
| `resetClerkMocks()` | Reset to defaults (call in `beforeEach`) |
|
|
242
|
+
|
|
243
|
+
### Mock Components
|
|
244
|
+
|
|
245
|
+
| Component | Test ID |
|
|
246
|
+
| ------------------ | --------------------- |
|
|
247
|
+
| `SignInButton` | `sign-in-button` |
|
|
248
|
+
| `SignUpButton` | `sign-up-button` |
|
|
249
|
+
| `UserButton` | `user-button` |
|
|
250
|
+
| `RedirectToSignIn` | `redirect-to-sign-in` |
|
|
251
|
+
|
|
252
|
+
### Mock Constants & Fixtures
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// src/mocks/constants.ts
|
|
256
|
+
export const MOCK_USER = {
|
|
257
|
+
id: 'user_123',
|
|
258
|
+
email: 'test@example.com',
|
|
259
|
+
fullName: 'Test User',
|
|
260
|
+
avatarUrl: 'https://example.com/avatar.jpg',
|
|
261
|
+
};
|
|
262
|
+
export const MOCK_SESSION_ID = 'sess_123';
|
|
263
|
+
export const MOCK_AUTH_TOKEN = 'mock-auth-token';
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// src/mocks/fixtures/users.ts
|
|
268
|
+
import { createUser, createUsers } from '@/test';
|
|
269
|
+
|
|
270
|
+
const user = createUser({ fullName: 'Jane Doe' }); // Single user with overrides
|
|
271
|
+
const users = createUsers(3); // Array of mock users
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### E2E Testing
|
|
275
|
+
|
|
276
|
+
For Playwright tests requiring authentication, use `@clerk/testing`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
CLERK_SECRET_KEY=sk_test_xxxxx
|
|
280
|
+
E2E_CLERK_USER_USERNAME=test@example.com
|
|
281
|
+
E2E_CLERK_USER_PASSWORD=your-password
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
See [E2E_TESTING.md](./E2E_TESTING.md#authenticated-testing) for full details.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Troubleshooting
|
|
289
|
+
|
|
290
|
+
| Issue | Cause | Fix |
|
|
291
|
+
| ------------------------------------------- | -------------------------- | ----------------------------------------------------- |
|
|
292
|
+
| "Missing VITE_CLERK_PUBLISHABLE_KEY" | Env var not set | Add to `.env`, restart dev server |
|
|
293
|
+
| UI not matching theme | Missing CSS import | Add `@import '@clerk/themes/shadcn.css'` to index.css |
|
|
294
|
+
| "useAuth must be used within ClerkProvider" | Component outside provider | Check `main.tsx` provider order |
|
|
295
|
+
| Modal not opening | Missing `mode="modal"` | Use `<SignInButton mode="modal">` |
|
|
296
|
+
| Supabase not getting tokens | Wrong provider order | SupabaseProvider must be inside ClerkProvider |
|
|
297
|
+
| User data undefined | Checking before loaded | Wait for `isLoaded === true` before accessing user |
|
|
298
|
+
|
|
299
|
+
### Debug Auth State
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const { isLoaded, isSignedIn, userId } = useAuth();
|
|
303
|
+
console.log({ isLoaded, isSignedIn, userId });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Debug Token Claims
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const { session } = useSession();
|
|
310
|
+
const token = await session?.getToken();
|
|
311
|
+
if (token) {
|
|
312
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
313
|
+
console.log(payload); // { sub: "user_xxx", role: "authenticated", ... }
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Resources
|
|
320
|
+
|
|
321
|
+
- [Clerk Documentation](https://clerk.com/docs)
|
|
322
|
+
- [Clerk React Router Integration](https://clerk.com/docs/references/react-router/overview)
|
|
323
|
+
- [Clerk Supabase Integration](https://clerk.com/docs/integrations/databases/supabase)
|
|
324
|
+
- [Clerk Appearance Customization](https://clerk.com/docs/customization/overview)
|
|
325
|
+
- [SUPABASE_INTEGRATION.md](./SUPABASE_INTEGRATION.md) - Database integration with Clerk tokens
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Deployment Guide
|
|
2
|
+
|
|
3
|
+
Automated deployment to Netlify with GitHub Actions for preview and production environments.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
PR Created/Updated → GitHub Actions → Build → Netlify Preview
|
|
11
|
+
↓
|
|
12
|
+
Comment with preview URL on PR
|
|
13
|
+
|
|
14
|
+
Push to main → GitHub Actions → Build → Netlify Production
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Features:**
|
|
18
|
+
|
|
19
|
+
- Automatic preview deploys for pull requests
|
|
20
|
+
- Production deploys on push to main/master
|
|
21
|
+
- PR comments with preview URLs
|
|
22
|
+
- Manual deploy via workflow_dispatch
|
|
23
|
+
- Security headers pre-configured
|
|
24
|
+
|
|
25
|
+
**Note:** Enable branch protection rules to require CI to pass before merging to main.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Netlify Setup
|
|
30
|
+
|
|
31
|
+
### 1. Create Netlify Site
|
|
32
|
+
|
|
33
|
+
1. Go to [app.netlify.com](https://app.netlify.com) and sign in
|
|
34
|
+
2. Click "Add new site" → "Import an existing project"
|
|
35
|
+
3. Connect your GitHub repository
|
|
36
|
+
4. Configure build settings (auto-detected from `netlify.toml`):
|
|
37
|
+
- Build command: `npm run build`
|
|
38
|
+
- Publish directory: `dist`
|
|
39
|
+
5. Click "Deploy site"
|
|
40
|
+
|
|
41
|
+
### 2. Get API Credentials
|
|
42
|
+
|
|
43
|
+
1. **Personal Access Token** (for `NETLIFY_AUTH_TOKEN`):
|
|
44
|
+
- User Settings → Applications → Personal access tokens
|
|
45
|
+
- Click "New access token", name it, and copy the token
|
|
46
|
+
|
|
47
|
+
2. **Site ID** (for `NETLIFY_SITE_ID`):
|
|
48
|
+
- Site Settings → General → Site details → Site ID
|
|
49
|
+
|
|
50
|
+
### 3. Add GitHub Secrets
|
|
51
|
+
|
|
52
|
+
Go to your repository → Settings → Secrets and variables → Actions → New repository secret:
|
|
53
|
+
|
|
54
|
+
| Secret Name | Description |
|
|
55
|
+
| -------------------- | -------------------------------- |
|
|
56
|
+
| `NETLIFY_AUTH_TOKEN` | Personal access token from above |
|
|
57
|
+
| `NETLIFY_SITE_ID` | Site ID from above |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Environment Variables
|
|
62
|
+
|
|
63
|
+
### Build-Time Variables
|
|
64
|
+
|
|
65
|
+
Set in Netlify Dashboard → Site Settings → Environment variables:
|
|
66
|
+
|
|
67
|
+
| Variable | Required | Description |
|
|
68
|
+
| ---------------------------- | ------------- | --------------------- |
|
|
69
|
+
| `VITE_CLERK_PUBLISHABLE_KEY` | If using auth | Clerk publishable key |
|
|
70
|
+
| `VITE_SUPABASE_DATABASE_URL` | If using db | Supabase project URL |
|
|
71
|
+
| `VITE_SUPABASE_ANON_KEY` | If using db | Supabase anon key |
|
|
72
|
+
|
|
73
|
+
### Context-Specific Variables
|
|
74
|
+
|
|
75
|
+
Use Netlify CLI to set variables for specific contexts:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Set for all contexts
|
|
79
|
+
netlify env:set VAR_NAME value
|
|
80
|
+
|
|
81
|
+
# Set for production only
|
|
82
|
+
netlify env:set VAR_NAME value --context production
|
|
83
|
+
|
|
84
|
+
# Set for deploy previews only
|
|
85
|
+
netlify env:set VAR_NAME value --context deploy-preview
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Supabase Integration
|
|
91
|
+
|
|
92
|
+
If using the database feature, connect Supabase to Netlify for automatic environment variable sync.
|
|
93
|
+
|
|
94
|
+
### Extension Setup
|
|
95
|
+
|
|
96
|
+
1. **Netlify Dashboard** → Extensions → Search "Supabase" → Install
|
|
97
|
+
2. **Project Settings** → General → Supabase → Connect
|
|
98
|
+
3. Authorize with Supabase and select your project
|
|
99
|
+
4. For Vite projects:
|
|
100
|
+
- Framework: Select "Other"
|
|
101
|
+
- Environment variable prefix: Enter `VITE_`
|
|
102
|
+
|
|
103
|
+
### Auto-Configured Variables
|
|
104
|
+
|
|
105
|
+
After connecting, these are automatically injected:
|
|
106
|
+
|
|
107
|
+
| Variable | Description |
|
|
108
|
+
| ---------------------------- | ---------------------------------------- |
|
|
109
|
+
| `VITE_SUPABASE_DATABASE_URL` | Project URL |
|
|
110
|
+
| `VITE_SUPABASE_ANON_KEY` | Client API key |
|
|
111
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | Server-side only (not exposed to client) |
|
|
112
|
+
|
|
113
|
+
### Local Development
|
|
114
|
+
|
|
115
|
+
Run `netlify dev` to inject Supabase variables locally:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install -g netlify-cli
|
|
119
|
+
netlify login
|
|
120
|
+
netlify link # Link to your Netlify site
|
|
121
|
+
netlify dev # Starts dev server with injected env vars
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Preview Deploys
|
|
127
|
+
|
|
128
|
+
Every pull request automatically gets a preview deployment:
|
|
129
|
+
|
|
130
|
+
1. Open a PR against main/master
|
|
131
|
+
2. GitHub Actions builds and deploys to Netlify
|
|
132
|
+
3. Bot comments on PR with preview URL
|
|
133
|
+
4. Preview updates on each push to the PR
|
|
134
|
+
5. Preview is deleted when PR is closed
|
|
135
|
+
|
|
136
|
+
### Preview URL Pattern
|
|
137
|
+
|
|
138
|
+
- PR previews: `https://pr-{number}--{site-name}.netlify.app`
|
|
139
|
+
- Branch deploys: `https://{branch}--{site-name}.netlify.app`
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Production Deploys
|
|
144
|
+
|
|
145
|
+
Pushing to main/master triggers production deployment:
|
|
146
|
+
|
|
147
|
+
1. Deploy workflow builds the app
|
|
148
|
+
2. Deploys to production URL: `https://your-site.netlify.app`
|
|
149
|
+
|
|
150
|
+
**Important:** Enable branch protection rules on main/master to require CI to pass before merging. This ensures production only gets code that passed all checks.
|
|
151
|
+
|
|
152
|
+
### Manual Deploys
|
|
153
|
+
|
|
154
|
+
Use workflow_dispatch for manual production deploys:
|
|
155
|
+
|
|
156
|
+
1. Go to Actions → Deploy → Run workflow
|
|
157
|
+
2. Select branch
|
|
158
|
+
3. Click "Run workflow"
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Configuration
|
|
163
|
+
|
|
164
|
+
### netlify.toml
|
|
165
|
+
|
|
166
|
+
The `netlify.toml` file in your project root configures:
|
|
167
|
+
|
|
168
|
+
- **Build settings**: Command and publish directory
|
|
169
|
+
- **Redirects**: SPA fallback to index.html
|
|
170
|
+
- **Headers**: Security headers and caching rules
|
|
171
|
+
- **Context overrides**: Environment-specific settings
|
|
172
|
+
|
|
173
|
+
### Customizing Headers
|
|
174
|
+
|
|
175
|
+
Add custom headers in `netlify.toml`:
|
|
176
|
+
|
|
177
|
+
```toml
|
|
178
|
+
[[headers]]
|
|
179
|
+
for = "/api/*"
|
|
180
|
+
[headers.values]
|
|
181
|
+
Access-Control-Allow-Origin = "https://example.com"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Redirect Rules
|
|
185
|
+
|
|
186
|
+
Add redirects before the SPA fallback:
|
|
187
|
+
|
|
188
|
+
```toml
|
|
189
|
+
[[redirects]]
|
|
190
|
+
from = "/old-path"
|
|
191
|
+
to = "/new-path"
|
|
192
|
+
status = 301
|
|
193
|
+
|
|
194
|
+
# SPA fallback (keep last)
|
|
195
|
+
[[redirects]]
|
|
196
|
+
from = "/*"
|
|
197
|
+
to = "/index.html"
|
|
198
|
+
status = 200
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## CLI Commands
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Install Netlify CLI
|
|
207
|
+
npm install -g netlify-cli
|
|
208
|
+
|
|
209
|
+
# Login and link
|
|
210
|
+
netlify login
|
|
211
|
+
netlify link
|
|
212
|
+
|
|
213
|
+
# Local development with Netlify env vars
|
|
214
|
+
netlify dev
|
|
215
|
+
|
|
216
|
+
# Manual deploys
|
|
217
|
+
npm run deploy:preview # Deploy preview build
|
|
218
|
+
npm run deploy:prod # Deploy to production
|
|
219
|
+
|
|
220
|
+
# Environment variables
|
|
221
|
+
netlify env:list # List all variables
|
|
222
|
+
netlify env:set KEY value # Set variable
|
|
223
|
+
netlify env:get KEY # Get variable value
|
|
224
|
+
netlify env:unset KEY # Remove variable
|
|
225
|
+
|
|
226
|
+
# Build locally
|
|
227
|
+
netlify build # Test production build
|
|
228
|
+
netlify build --context deploy-preview # Test preview build
|
|
229
|
+
|
|
230
|
+
# Check status
|
|
231
|
+
netlify status
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Troubleshooting
|
|
237
|
+
|
|
238
|
+
| Issue | Cause | Solution |
|
|
239
|
+
| ---------------------------------- | ---------------------------- | --------------------------------------------- |
|
|
240
|
+
| Deploy fails with "Site not found" | Missing `NETLIFY_SITE_ID` | Add secret to GitHub repository |
|
|
241
|
+
| Deploy fails with "Unauthorized" | Invalid `NETLIFY_AUTH_TOKEN` | Regenerate token in Netlify |
|
|
242
|
+
| Preview not commenting on PR | Missing permissions | Check workflow has `pull-requests: write` |
|
|
243
|
+
| Env vars undefined in build | Not set in Netlify | Add to Netlify Dashboard or use `netlify env` |
|
|
244
|
+
| 404 on page refresh | SPA fallback not working | Check `netlify.toml` has `/* -> /index.html` |
|
|
245
|
+
| Production deploy not triggered | CI workflow failed | Check CI workflow status first |
|
|
246
|
+
|
|
247
|
+
### Debug Build Locally
|
|
248
|
+
|
|
249
|
+
Test production build locally:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npm run build
|
|
253
|
+
npx serve dist
|
|
254
|
+
|
|
255
|
+
# Or with Netlify CLI:
|
|
256
|
+
netlify build
|
|
257
|
+
netlify deploy --dir=dist
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Resources
|
|
263
|
+
|
|
264
|
+
- [Netlify Documentation](https://docs.netlify.com/)
|
|
265
|
+
- [Netlify CLI Reference](https://cli.netlify.com/)
|
|
266
|
+
- [File-based Configuration](https://docs.netlify.com/configure-builds/file-based-configuration/)
|
|
267
|
+
- [GitHub Actions for Netlify](https://github.com/nwtgck/actions-netlify)
|
|
268
|
+
- [Netlify Supabase Extension](https://www.netlify.com/integrations/supabase/)
|
|
@@ -10,16 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
e2e/
|
|
13
|
+
├── auth/
|
|
14
|
+
│ └── auth.setup.ts # Clerk authentication setup
|
|
13
15
|
├── fixtures/
|
|
14
16
|
│ └── index.ts # setupPage, setupCleanPage, test, expect
|
|
15
17
|
├── tests/ # Functional E2E tests
|
|
16
18
|
│ ├── home.spec.ts # Page structure, accessibility
|
|
17
19
|
│ ├── theme.spec.ts # Theme toggle, persistence
|
|
18
20
|
│ ├── language.spec.ts # Language switcher
|
|
19
|
-
│
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
│ ├── navigation.spec.ts # Routing, 404
|
|
22
|
+
│ ├── profile.spec.ts # Unauthenticated profile tests
|
|
23
|
+
│ └── profile.auth.spec.ts # Authenticated profile tests
|
|
24
|
+
├── performance/ # Performance regression tests
|
|
25
|
+
│ ├── setup.ts # Performance test fixture
|
|
26
|
+
│ └── home.spec.ts # Home page performance tests
|
|
27
|
+
└── .clerk/ # Auth state storage (gitignored)
|
|
28
|
+
└── user.json # Saved auth state for tests
|
|
23
29
|
```
|
|
24
30
|
|
|
25
31
|
## Imports
|
|
@@ -111,8 +117,79 @@ npm run e2e:all # Run both desktop and mobile
|
|
|
111
117
|
npm run e2e:ui # Interactive UI mode
|
|
112
118
|
npm run e2e:perf # Run performance tests
|
|
113
119
|
npm run e2e:perf:ui # Performance tests with interactive UI
|
|
120
|
+
|
|
121
|
+
# Authenticated tests (requires credentials)
|
|
122
|
+
npx playwright test --project=authenticated
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Authenticated Testing
|
|
126
|
+
|
|
127
|
+
Tests requiring authentication use `@clerk/testing` with Playwright. These tests run with a real authenticated user session.
|
|
128
|
+
|
|
129
|
+
### Setup
|
|
130
|
+
|
|
131
|
+
1. Install the testing package (already included):
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm install -D @clerk/testing
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
2. Set environment variables in `.env`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
CLERK_SECRET_KEY=sk_test_xxxxx
|
|
141
|
+
E2E_CLERK_USER_USERNAME=test@example.com
|
|
142
|
+
E2E_CLERK_USER_PASSWORD=your-test-password
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
3. Create a test user in your Clerk dashboard with the above credentials.
|
|
146
|
+
|
|
147
|
+
### File Naming Convention
|
|
148
|
+
|
|
149
|
+
- `*.spec.ts` - Regular tests (run in `desktop`/`mobile` projects)
|
|
150
|
+
- `*.auth.spec.ts` - Authenticated tests (run in `authenticated` project only)
|
|
151
|
+
|
|
152
|
+
### Writing Authenticated Tests
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// e2e/tests/my-feature.auth.spec.ts
|
|
156
|
+
import { expect, test } from '@playwright/test';
|
|
157
|
+
import { existsSync } from 'fs';
|
|
158
|
+
import { dirname, join } from 'path';
|
|
159
|
+
import { fileURLToPath } from 'url';
|
|
160
|
+
|
|
161
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
162
|
+
const authFile = join(__dirname, '../.clerk/user.json');
|
|
163
|
+
const hasAuthState = existsSync(authFile);
|
|
164
|
+
|
|
165
|
+
test.describe('My Authenticated Feature', () => {
|
|
166
|
+
// Skip if auth state doesn't exist
|
|
167
|
+
test.skip(!hasAuthState, 'Authentication required');
|
|
168
|
+
|
|
169
|
+
test.beforeEach(async ({ page }) => {
|
|
170
|
+
// User is already authenticated via storageState
|
|
171
|
+
await page.goto('/protected-page');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('can access protected content', async ({ page }) => {
|
|
175
|
+
await expect(page.getByText('Protected Content')).toBeVisible();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
114
178
|
```
|
|
115
179
|
|
|
180
|
+
### How It Works
|
|
181
|
+
|
|
182
|
+
1. **Setup project** runs `auth.setup.ts` which:
|
|
183
|
+
- Calls `clerkSetup()` to get a testing token
|
|
184
|
+
- Signs in with test credentials
|
|
185
|
+
- Saves auth state to `e2e/.clerk/user.json`
|
|
186
|
+
|
|
187
|
+
2. **Authenticated project** uses the saved state:
|
|
188
|
+
- Loads `storageState` from `user.json`
|
|
189
|
+
- Tests run with pre-authenticated session
|
|
190
|
+
|
|
191
|
+
3. **Tests skip gracefully** when credentials aren't configured
|
|
192
|
+
|
|
116
193
|
## Mobile Testing
|
|
117
194
|
|
|
118
195
|
Tests run on both desktop (Chrome) and mobile (Pixel 5) viewports. Use the `isMobile` fixture for device-specific behavior.
|