Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9750e1b
feat: implement feature flag infrastructure with Cloudflare Workers +…
taearls Nov 24, 2025
87dba3e
feat: implement feature flag infrastructure with Cloudflare Workers +…
taearls Nov 28, 2025
3e9e218
fix: address PR#74 critical issues and add comprehensive test coverage
taearls Nov 28, 2025
39a96f9
refactor: rename feature flag from contactForm to email-contact-form
taearls Nov 28, 2025
9c587b3
fix: add KV namespace binding to default Worker environment
taearls Nov 28, 2025
9b651c0
feat: configure separate KV namespaces for dev/staging/production env…
taearls Nov 28, 2025
c979d00
fix: correct syntax errors and update feature flag references
taearls Nov 28, 2025
0c1ff72
fix: configure separate test script for Worker tests
taearls Nov 28, 2025
2d5b7c8
refactor: remove skipped fetchFlags test placeholder
taearls Nov 28, 2025
17f1276
feat: configure feature flags to use remote KV storage in development
taearls Nov 28, 2025
4a413da
feat: configure custom domain for production Worker
taearls Nov 28, 2025
b2a8296
fix: correct custom domain configuration and deploy to production
taearls Nov 28, 2025
b10c31b
fix: prevent duplicate CORS headers from cached responses
taearls Nov 28, 2025
0069841
fix: resolve ESLint import extension errors for CI/CD
taearls Nov 28, 2025
e94bfc4
fix: add @cloudflare/vitest-pool-workers to ESLint ignore list
taearls Nov 28, 2025
f1a88b9
fix: add VITE_FEATURE_FLAGS_API_URL to integration test CI step
taearls Nov 28, 2025
c80a993
fix: disable sort-keys rule and add Worker test overrides
taearls Nov 28, 2025
01d6b55
fix: apply Prettier formatting to lint config and documentation
taearls Nov 28, 2025
06894ed
test: skip flaky CI test for feature flag loading
taearls Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Feature Flags Worker API URL
VITE_FEATURE_FLAGS_API_URL=http://localhost:8787/api/flags
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Feature Flags API Configuration (Development)
VITE_FEATURE_FLAGS_API_URL=http://localhost:8787/api/flags
2 changes: 2 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Feature Flags API Configuration (Production)
VITE_FEATURE_FLAGS_API_URL=https://portfolio-feature-flags.tyler-a-earls.workers.dev/api/flags
93 changes: 93 additions & 0 deletions .github/workflows/toggle-feature-flag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Toggle Feature Flag

on:
workflow_dispatch:
inputs:
feature:
description: "Feature flag to toggle"
required: true
type: choice
options:
- email-contact-form
enabled:
description: "Enable or disable the feature"
required: true
type: boolean
default: false
message:
description: "Optional message to display (for disabled features)"
required: false
type: string

jobs:
toggle-flag:
runs-on: ubuntu-latest
environment: production

steps:
- name: Validate inputs
run: |
echo "Feature: ${{ github.event.inputs.feature }}"
echo "Enabled: ${{ github.event.inputs.enabled }}"
echo "Message: ${{ github.event.inputs.message }}"

- name: Build JSON payload
id: build-json
run: |
# Build the feature flag JSON
if [ "${{ github.event.inputs.message }}" != "" ]; then
JSON=$(cat <<EOF
{
"${{ github.event.inputs.feature }}": {
"enabled": ${{ github.event.inputs.enabled }},
"message": "${{ github.event.inputs.message }}"
}
}
EOF
)
else
JSON=$(cat <<EOF
{
"${{ github.event.inputs.feature }}": {
"enabled": ${{ github.event.inputs.enabled }}
}
}
EOF
)
fi

# Remove newlines and extra spaces
JSON_COMPACT=$(echo "$JSON" | tr -d '\n' | tr -s ' ')

echo "json=$JSON_COMPACT" >> $GITHUB_OUTPUT
echo "Payload: $JSON_COMPACT"

- name: Update feature flag via API
run: |
RESPONSE=$(curl -X PUT "${{ secrets.FEATURE_FLAGS_API_URL }}" \
-H "X-API-Key: ${{ secrets.FEATURE_FLAGS_ADMIN_API_KEY }}" \
-H "Content-Type: application/json" \
-d '${{ steps.build-json.outputs.json }}' \
-w "\n%{http_code}" \
-s)

HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)

echo "HTTP Status: $HTTP_CODE"
echo "Response Body: $BODY"

if [ "$HTTP_CODE" != "200" ]; then
echo "::error::Failed to update feature flag. HTTP $HTTP_CODE"
echo "$BODY"
exit 1
fi

echo "::notice::Feature flag updated successfully!"
echo "$BODY"

- name: Create deployment notification
if: success()
run: |
STATUS="${{ github.event.inputs.enabled }}" == "true" && "enabled" || "disabled"
echo "::notice::Feature '${{ github.event.inputs.feature }}' has been $STATUS"
94 changes: 89 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ This is Tyler Earls' portfolio website built with React, Vite, TailwindCSS, and
### Development

- `npm run dev` - Start development server with Vite on port 3000 (auto-opens browser)
- `npm run dev:flags` - Start Cloudflare Worker for feature flags locally (port 8787)
- `npm run dev:all` - Run both React app and feature flags Worker concurrently
- `npm run build` - Build for production (runs TypeScript compiler first, then Vite)
- `npm run preview` - Preview production build locally

### Testing

Expand All @@ -30,6 +33,11 @@ This is Tyler Earls' portfolio website built with React, Vite, TailwindCSS, and
- `npm run oxlint:fix` - Auto-fix OxLint issues
- `npm run format:check` - Check Prettier formatting
- `npm run format:fix` - Auto-fix formatting with Prettier
- `npm run ci` - Run all quality checks (lint, format, test, build) - used in CI/CD

### Deployment

- `npm run deploy:flags` - Deploy feature flags Worker to Cloudflare

## Architecture

Expand All @@ -42,6 +50,14 @@ This is Tyler Earls' portfolio website built with React, Vite, TailwindCSS, and
- **TypeScript**: Strict configuration with separate configs for app/node/tests
- **Testing**: Vitest for unit tests, Cypress for integration tests

### Monorepo Structure

This project uses **npm workspaces** for managing multiple packages:

- **Root** - Main React application
- **`packages/shared-types/`** - Shared TypeScript types between React app and Worker
- **`packages/feature-flags/`** - Cloudflare Worker for feature flags (KV storage, CORS, caching)

### Project Structure

- `/src/components/` - React components organized by feature
Expand All @@ -51,23 +67,31 @@ This is Tyler Earls' portfolio website built with React, Vite, TailwindCSS, and
- CSS Modules use `.module.css` extension
- `/src/pages/` - Page components for routing
- `/src/state/` - State management with XState
- `/contexts/` - React contexts (ThemeContext uses XState actors)
- `/contexts/` - React contexts (ThemeContext, FeatureFlagContext use XState actors)
- `/machines/` - XState state machines (themeMachine, navigationMachine)
- `/src/hooks/` - Custom React hooks
- `/src/util/` - Utility functions and constants
- `/src/util/` - Utility functions and constants (includes `API_URIS`, `BASE_URLS`)
- `/src/types/` - TypeScript type definitions
- `/tests/` - Test files organized by type
- `/unit/` - Unit tests with Vitest
- `/integration/` - Cypress E2E tests
- `/component/` - Component-specific tests
- `/packages/` - Workspace packages
- `/feature-flags/` - Cloudflare Worker (wrangler.toml, KV namespaces)
- `/shared-types/` - Shared types for feature flags

### Key Patterns

- **Path Aliasing**: `@/` maps to `/src/` directory
- **CSS Modules**: Component styles use camelCase conversion
- **CSS Modules**: Component styles use camelCase conversion (e.g., `styles.myClass`)
- **State Machines**: XState for complex state logic (theme toggling, navigation)
- **Feature Flags**: Runtime configuration via Cloudflare Worker + KV
- Use `FeatureFlagWrapper` component for conditional rendering
- Feature flags fetched from Worker, cached in localStorage (60s TTL)
- Flags configured in Worker's KV namespace
- **Image Optimization**: Cloudinary integration for optimized image delivery
- **Performance Monitoring**: Why Did You Render development tool integrated
- **Performance Monitoring**: Why Did You Render development tool integrated (dev only)
- **React Compiler**: Babel plugin for automatic memoization (compilationMode: "infer")

### Testing Strategy

Expand All @@ -81,4 +105,64 @@ This is Tyler Earls' portfolio website built with React, Vite, TailwindCSS, and
- React Router configured for client-side only (SSR disabled)
- Vite dev server runs on port 3000
- CSS modules generate scoped class names (hash-based in production)
- The project includes performance profiling with why-did-you-render in development
- ESLint uses flat config format (eslint.config.mts)
- Two linters: ESLint (comprehensive) and OxLint (faster alternative)

## Feature Flags System

### Architecture

The feature flags system uses a **Cloudflare Worker + KV** architecture:

1. **Worker** (`packages/feature-flags/`) serves flags via `/api/flags` endpoint
2. **KV Storage** holds flag configuration as JSON
3. **React Context** (`FeatureFlagContext`) fetches and caches flags
4. **Shared Types** (`packages/shared-types/`) ensure type safety across stack

### Managing Feature Flags

**Update a flag value:**

```bash
npx wrangler kv key put --binding=FEATURE_FLAGS --preview false "flags" \
'{"contactForm":{"enabled":true}}' \
--config packages/feature-flags/wrangler.toml
```

**Deploy Worker changes:**

```bash
npm run deploy:flags
```

**Environment variables:**

- Development: `.env.development` → `VITE_FEATURE_FLAGS_API_URL=http://localhost:8787/api/flags`
- Production: `.env.production` → `VITE_FEATURE_FLAGS_API_URL=https://portfolio-feature-flags.tyler-a-earls.workers.dev/api/flags`

### Using Feature Flags

```tsx
import FeatureFlagWrapper from "@/components/FeatureFlagWrapper/FeatureFlagWrapper.tsx";

<FeatureFlagWrapper
flagKey="contactForm"
whenEnabled={<ContactEmailForm />}
whenDisabled={<ComingSoonMessage />}
whenLoading={<LoadingSpinner />}
/>;
```

**Available flags:**

- `contactForm` - Controls contact form visibility ({ enabled: boolean, message?: string })

## XState Integration

State machines live in `/src/state/machines/` and are consumed via React contexts in `/src/state/contexts/`. The pattern uses XState's actor model:

1. Define machine in `/machines/` (e.g., `themeMachine.ts`)
2. Create context provider wrapping `createActorContext`
3. Export hooks for accessing state and sending events

Example: `ThemeContext` wraps `themeMachine` and provides `useTheme()` hook.
Loading