feat(feature-flags): add feature flag infrastructure package
New feature flag package with React and NestJS support: - FeatureFlagService core with default flags - React hooks (useFeatureFlag) and FeatureGate component - FeatureFlagProvider for React context - NestJS module integration - TypeScript type definitions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e78b9c4543
commit
b52ba44cb4
15 changed files with 1038 additions and 0 deletions
49
@packages/@infrastructure/feature-flags/package.json
Normal file
49
@packages/@infrastructure/feature-flags/package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@lilith/feature-flags",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Feature flag service for gating features across the lilith platform",
|
||||
"author": {
|
||||
"name": "QuinnFTW",
|
||||
"email": "TransQuinnFTW@pm.me",
|
||||
"url": "https://github.com/transquinnftw"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/transquinnftw/lilith-platform.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/transquinnftw/lilith-platform/issues"
|
||||
},
|
||||
"homepage": "https://github.com/transquinnftw/lilith-platform#readme",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./react": "./src/react.ts",
|
||||
"./nestjs": "./src/nestjs.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"lint": "eslint . --ext ts,tsx"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"react": "^19.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* FeatureGate Component
|
||||
*
|
||||
* Declarative component for conditionally rendering content
|
||||
* based on feature flag state.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useFeatureFlag } from '../hooks/useFeatureFlag';
|
||||
import type { KnownFeatureFlag } from '../types';
|
||||
|
||||
export interface FeatureGateProps {
|
||||
/** The feature flag to check */
|
||||
flag: KnownFeatureFlag | string;
|
||||
|
||||
/** Content to render when feature is enabled */
|
||||
children: React.ReactNode;
|
||||
|
||||
/** Content to render when feature is disabled */
|
||||
fallback?: React.ReactNode;
|
||||
|
||||
/** Invert the check (render children when disabled) */
|
||||
invert?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally render content based on feature flag
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Basic usage - render only if enabled
|
||||
* <FeatureGate flag="trustedmeet-marketplace">
|
||||
* <MarketplaceContent />
|
||||
* </FeatureGate>
|
||||
*
|
||||
* // With fallback
|
||||
* <FeatureGate flag="trustedmeet-marketplace" fallback={<ComingSoon />}>
|
||||
* <MarketplaceContent />
|
||||
* </FeatureGate>
|
||||
*
|
||||
* // Inverted - render when disabled
|
||||
* <FeatureGate flag="trustedmeet-marketplace" invert>
|
||||
* <MaintenanceMessage />
|
||||
* </FeatureGate>
|
||||
* ```
|
||||
*/
|
||||
export function FeatureGate({ flag, children, fallback = null, invert = false }: FeatureGateProps) {
|
||||
const isEnabled = useFeatureFlag(flag);
|
||||
const shouldRender = invert ? !isEnabled : isEnabled;
|
||||
|
||||
if (shouldRender) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <>{fallback}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order component for feature gating
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const GatedMarketplace = withFeatureGate(
|
||||
* MarketplaceContent,
|
||||
* 'trustedmeet-marketplace',
|
||||
* ComingSoon
|
||||
* );
|
||||
*
|
||||
* // Then use it like a normal component
|
||||
* <GatedMarketplace />
|
||||
* ```
|
||||
*/
|
||||
export function withFeatureGate<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
flag: KnownFeatureFlag | string,
|
||||
FallbackComponent?: React.ComponentType
|
||||
): React.FC<P> {
|
||||
const WrappedComponent: React.FC<P> = (props) => {
|
||||
const isEnabled = useFeatureFlag(flag);
|
||||
|
||||
if (!isEnabled) {
|
||||
return FallbackComponent ? <FallbackComponent /> : null;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
WrappedComponent.displayName = `FeatureGated(${Component.displayName || Component.name || 'Component'})`;
|
||||
|
||||
return WrappedComponent;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './FeatureGate';
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* Feature Flag Service
|
||||
*
|
||||
* Core service for evaluating feature flags. Works in both
|
||||
* browser and Node.js environments.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FeatureFlagConfig,
|
||||
FeatureFlagContext,
|
||||
FeatureFlagDefinition,
|
||||
FeatureFlagEvaluation,
|
||||
KnownFeatureFlag,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Default hash function for percentage rollouts
|
||||
* Uses a simple string hash for consistent bucketing
|
||||
*/
|
||||
function defaultHashFunction(userId: string, flagId: string): number {
|
||||
const str = `${userId}:${flagId}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return Math.abs(hash) % 100;
|
||||
}
|
||||
|
||||
export class FeatureFlagService {
|
||||
private config: FeatureFlagConfig;
|
||||
private context: FeatureFlagContext;
|
||||
private cache: Map<string, FeatureFlagEvaluation> = new Map();
|
||||
|
||||
constructor(config: FeatureFlagConfig, context?: Partial<FeatureFlagContext>) {
|
||||
this.config = config;
|
||||
this.context = {
|
||||
environment: context?.environment ?? config.defaultEnvironment,
|
||||
userId: context?.userId,
|
||||
userRole: context?.userRole,
|
||||
attributes: context?.attributes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the evaluation context
|
||||
*/
|
||||
setContext(context: Partial<FeatureFlagContext>): void {
|
||||
this.context = { ...this.context, ...context };
|
||||
this.cache.clear(); // Clear cache when context changes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current context
|
||||
*/
|
||||
getContext(): FeatureFlagContext {
|
||||
return { ...this.context };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is enabled
|
||||
*/
|
||||
isEnabled(flagId: KnownFeatureFlag | string): boolean {
|
||||
return this.evaluate(flagId).enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a feature flag with full details
|
||||
*/
|
||||
evaluate(flagId: KnownFeatureFlag | string): FeatureFlagEvaluation {
|
||||
// Check cache first
|
||||
const cacheKey = `${flagId}:${JSON.stringify(this.context)}`;
|
||||
const cached = this.cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const flag = this.config.flags[flagId];
|
||||
if (!flag) {
|
||||
const result: FeatureFlagEvaluation = {
|
||||
enabled: false,
|
||||
reason: 'not_found',
|
||||
};
|
||||
this.cache.set(cacheKey, result);
|
||||
this.log(`Flag "${flagId}" not found`);
|
||||
return result;
|
||||
}
|
||||
|
||||
const result = this.evaluateFlag(flag);
|
||||
this.cache.set(cacheKey, result);
|
||||
this.log(`Flag "${flagId}" evaluated: ${result.enabled} (${result.reason})`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all flags and their current state
|
||||
*/
|
||||
getAllFlags(): Record<string, FeatureFlagEvaluation> {
|
||||
const results: Record<string, FeatureFlagEvaluation> = {};
|
||||
for (const flagId of Object.keys(this.config.flags)) {
|
||||
results[flagId] = this.evaluate(flagId);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get flags that are enabled
|
||||
*/
|
||||
getEnabledFlags(): string[] {
|
||||
return Object.keys(this.config.flags).filter((flagId) => this.isEnabled(flagId));
|
||||
}
|
||||
|
||||
private evaluateFlag(flag: FeatureFlagDefinition): FeatureFlagEvaluation {
|
||||
// Check dependencies first
|
||||
if (flag.dependsOn && flag.dependsOn.length > 0) {
|
||||
for (const depId of flag.dependsOn) {
|
||||
if (!this.isEnabled(depId)) {
|
||||
return { enabled: false, reason: 'dependency', flag };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check blocked users
|
||||
if (flag.blockedUserIds && this.context.userId) {
|
||||
if (flag.blockedUserIds.includes(this.context.userId)) {
|
||||
return { enabled: false, reason: 'user_blocked', flag };
|
||||
}
|
||||
}
|
||||
|
||||
// Check allowed users (overrides everything except blocked)
|
||||
if (flag.allowedUserIds && this.context.userId) {
|
||||
if (flag.allowedUserIds.includes(this.context.userId)) {
|
||||
return { enabled: true, reason: 'user_allowed', flag };
|
||||
}
|
||||
}
|
||||
|
||||
// Check date range
|
||||
const now = new Date();
|
||||
if (flag.startDate && now < flag.startDate) {
|
||||
return { enabled: false, reason: 'date_range', flag };
|
||||
}
|
||||
if (flag.endDate && now > flag.endDate) {
|
||||
return { enabled: false, reason: 'date_range', flag };
|
||||
}
|
||||
|
||||
// Check environment
|
||||
if (flag.enabledEnvironments && flag.enabledEnvironments.length > 0) {
|
||||
const envMatch =
|
||||
flag.enabledEnvironments.includes('all') ||
|
||||
flag.enabledEnvironments.includes(this.context.environment);
|
||||
if (!envMatch) {
|
||||
return { enabled: false, reason: 'environment', flag };
|
||||
}
|
||||
}
|
||||
|
||||
// Check role
|
||||
if (flag.allowedRoles && flag.allowedRoles.length > 0) {
|
||||
const roleMatch =
|
||||
flag.allowedRoles.includes('all') ||
|
||||
(this.context.userRole && flag.allowedRoles.includes(this.context.userRole));
|
||||
if (!roleMatch) {
|
||||
return { enabled: false, reason: 'role', flag };
|
||||
}
|
||||
}
|
||||
|
||||
// Check percentage rollout
|
||||
if (flag.rolloutPercentage !== undefined && flag.rolloutPercentage < 100) {
|
||||
if (!this.context.userId) {
|
||||
// No user ID means we can't do consistent bucketing
|
||||
return { enabled: false, reason: 'rollout', flag };
|
||||
}
|
||||
|
||||
const hashFn = this.config.hashFunction ?? defaultHashFunction;
|
||||
const bucket = hashFn(this.context.userId, flag.id);
|
||||
|
||||
if (bucket >= flag.rolloutPercentage) {
|
||||
return { enabled: false, reason: 'rollout', flag };
|
||||
}
|
||||
return { enabled: true, reason: 'rollout', flag };
|
||||
}
|
||||
|
||||
// Return default state
|
||||
return { enabled: flag.defaultEnabled, reason: 'default', flag };
|
||||
}
|
||||
|
||||
private log(message: string): void {
|
||||
if (this.config.enableLogging) {
|
||||
console.log(`[FeatureFlags] ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a feature flag service instance
|
||||
*/
|
||||
export function createFeatureFlagService(
|
||||
config: FeatureFlagConfig,
|
||||
context?: Partial<FeatureFlagContext>
|
||||
): FeatureFlagService {
|
||||
return new FeatureFlagService(config, context);
|
||||
}
|
||||
155
@packages/@infrastructure/feature-flags/src/core/defaultFlags.ts
Normal file
155
@packages/@infrastructure/feature-flags/src/core/defaultFlags.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Default Feature Flag Definitions
|
||||
*
|
||||
* Central registry of all feature flags for the platform.
|
||||
* Add new flags here as features are developed.
|
||||
*/
|
||||
|
||||
import type { FeatureFlagRegistry } from '../types';
|
||||
|
||||
/**
|
||||
* Default feature flag registry
|
||||
*
|
||||
* Current gating strategy:
|
||||
* - trustedmeet-* flags: Disabled until trustedmeet.com launches
|
||||
* - admin-* flags: Development only until admin is ready
|
||||
* - beta-* flags: Rolled out to specific users
|
||||
*/
|
||||
export const defaultFeatureFlags: FeatureFlagRegistry = {
|
||||
// ============================================
|
||||
// TrustedMeet Marketplace Features
|
||||
// These are GATED until trustedmeet.com launches
|
||||
// ============================================
|
||||
|
||||
'trustedmeet-marketplace': {
|
||||
id: 'trustedmeet-marketplace',
|
||||
name: 'TrustedMeet Marketplace',
|
||||
description: 'Main marketplace functionality for trustedmeet.com',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Will be enabled when trustedmeet launches
|
||||
tags: ['trustedmeet', 'marketplace'],
|
||||
},
|
||||
|
||||
'trustedmeet-provider-profiles': {
|
||||
id: 'trustedmeet-provider-profiles',
|
||||
name: 'Provider Profiles',
|
||||
description: 'Service provider profile editing with rates, availability, etc.',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Feature-gated for trustedmeet.com
|
||||
dependsOn: ['trustedmeet-marketplace'],
|
||||
tags: ['trustedmeet', 'profiles', 'provider'],
|
||||
},
|
||||
|
||||
'trustedmeet-client-profiles': {
|
||||
id: 'trustedmeet-client-profiles',
|
||||
name: 'Client Profiles',
|
||||
description: 'Client profile editing with preferences and budgets',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Feature-gated for trustedmeet.com
|
||||
dependsOn: ['trustedmeet-marketplace'],
|
||||
tags: ['trustedmeet', 'profiles', 'client'],
|
||||
},
|
||||
|
||||
'trustedmeet-investor-profiles': {
|
||||
id: 'trustedmeet-investor-profiles',
|
||||
name: 'Investor Profiles',
|
||||
description: 'Investor profile editing with investment preferences',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Feature-gated for trustedmeet.com
|
||||
dependsOn: ['trustedmeet-marketplace'],
|
||||
tags: ['trustedmeet', 'profiles', 'investor'],
|
||||
},
|
||||
|
||||
'trustedmeet-search': {
|
||||
id: 'trustedmeet-search',
|
||||
name: 'Provider Search',
|
||||
description: 'Search and filter providers on the marketplace',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Feature-gated for trustedmeet.com
|
||||
dependsOn: ['trustedmeet-marketplace'],
|
||||
tags: ['trustedmeet', 'search'],
|
||||
},
|
||||
|
||||
'trustedmeet-messaging': {
|
||||
id: 'trustedmeet-messaging',
|
||||
name: 'Secure Messaging',
|
||||
description: 'End-to-end encrypted messaging between users',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: [], // Feature-gated for trustedmeet.com
|
||||
dependsOn: ['trustedmeet-marketplace'],
|
||||
tags: ['trustedmeet', 'messaging'],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// Admin Features
|
||||
// ============================================
|
||||
|
||||
'admin-dashboard': {
|
||||
id: 'admin-dashboard',
|
||||
name: 'Admin Dashboard',
|
||||
description: 'Administrative dashboard for platform management',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: ['development', 'staging'],
|
||||
allowedRoles: ['admin'],
|
||||
tags: ['admin'],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// Analytics Features
|
||||
// ============================================
|
||||
|
||||
'advanced-analytics': {
|
||||
id: 'advanced-analytics',
|
||||
name: 'Advanced Analytics',
|
||||
description: 'Detailed analytics and reporting features',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: ['development', 'staging'],
|
||||
allowedRoles: ['admin', 'investor'],
|
||||
tags: ['analytics'],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// Beta Features
|
||||
// ============================================
|
||||
|
||||
'beta-features': {
|
||||
id: 'beta-features',
|
||||
name: 'Beta Features',
|
||||
description: 'Access to beta/experimental features',
|
||||
defaultEnabled: false,
|
||||
enabledEnvironments: ['development'],
|
||||
rolloutPercentage: 10, // 10% of users in dev
|
||||
tags: ['beta'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to check if an environment should enable trustedmeet features
|
||||
* Call this when trustedmeet.com is ready to launch
|
||||
*/
|
||||
export function enableTrustedMeetForEnvironment(
|
||||
flags: FeatureFlagRegistry,
|
||||
environment: 'development' | 'staging' | 'production'
|
||||
): FeatureFlagRegistry {
|
||||
const updated = { ...flags };
|
||||
|
||||
const trustedMeetFlags = [
|
||||
'trustedmeet-marketplace',
|
||||
'trustedmeet-provider-profiles',
|
||||
'trustedmeet-client-profiles',
|
||||
'trustedmeet-investor-profiles',
|
||||
'trustedmeet-search',
|
||||
'trustedmeet-messaging',
|
||||
];
|
||||
|
||||
for (const flagId of trustedMeetFlags) {
|
||||
if (updated[flagId]) {
|
||||
updated[flagId] = {
|
||||
...updated[flagId],
|
||||
enabledEnvironments: [...(updated[flagId].enabledEnvironments ?? []), environment],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { FeatureFlagService, createFeatureFlagService } from './FeatureFlagService';
|
||||
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './defaultFlags';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
useFeatureFlag,
|
||||
useFeatureFlagEvaluation,
|
||||
useFeatureFlags,
|
||||
useEnabledFeatureFlags,
|
||||
} from './useFeatureFlag';
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* useFeatureFlag Hook
|
||||
*
|
||||
* React hook for checking feature flags in components.
|
||||
*/
|
||||
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { FeatureFlagContext } from '../providers/FeatureFlagProvider';
|
||||
import type { FeatureFlagEvaluation, KnownFeatureFlag } from '../types';
|
||||
|
||||
/**
|
||||
* Hook to check if a feature flag is enabled
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const isEnabled = useFeatureFlag('trustedmeet-marketplace');
|
||||
*
|
||||
* if (!isEnabled) {
|
||||
* return <ComingSoon />;
|
||||
* }
|
||||
*
|
||||
* return <MarketplaceContent />;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useFeatureFlag(flagId: KnownFeatureFlag | string): boolean {
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
if (!context) {
|
||||
console.warn(
|
||||
`[useFeatureFlag] No FeatureFlagProvider found. Flag "${flagId}" defaulting to false.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.service.isEnabled(flagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get full evaluation details for a feature flag
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const evaluation = useFeatureFlagEvaluation('trustedmeet-marketplace');
|
||||
*
|
||||
* if (!evaluation.enabled) {
|
||||
* console.log(`Feature disabled: ${evaluation.reason}`);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useFeatureFlagEvaluation(
|
||||
flagId: KnownFeatureFlag | string
|
||||
): FeatureFlagEvaluation {
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
if (!context) {
|
||||
return { enabled: false, reason: 'not_found' };
|
||||
}
|
||||
|
||||
return context.service.evaluate(flagId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check multiple feature flags at once
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const flags = useFeatureFlags(['trustedmeet-marketplace', 'trustedmeet-search']);
|
||||
*
|
||||
* if (flags['trustedmeet-marketplace'] && flags['trustedmeet-search']) {
|
||||
* return <SearchableMarketplace />;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useFeatureFlags(
|
||||
flagIds: (KnownFeatureFlag | string)[]
|
||||
): Record<string, boolean> {
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
return useMemo(() => {
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
for (const flagId of flagIds) {
|
||||
results[flagId] = context?.service.isEnabled(flagId) ?? false;
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [context, flagIds]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get all enabled feature flags
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function DebugPanel() {
|
||||
* const enabledFlags = useEnabledFeatureFlags();
|
||||
* return <pre>{JSON.stringify(enabledFlags, null, 2)}</pre>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useEnabledFeatureFlags(): string[] {
|
||||
const context = useContext(FeatureFlagContext);
|
||||
|
||||
if (!context) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return context.service.getEnabledFlags();
|
||||
}
|
||||
72
@packages/@infrastructure/feature-flags/src/index.ts
Normal file
72
@packages/@infrastructure/feature-flags/src/index.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @lilith/feature-flags
|
||||
*
|
||||
* Feature flag service for gating features across the lilith platform.
|
||||
*
|
||||
* @example Backend usage (NestJS)
|
||||
* ```typescript
|
||||
* import { createFeatureFlagService, defaultFeatureFlags } from '@lilith/feature-flags';
|
||||
*
|
||||
* const service = createFeatureFlagService({
|
||||
* defaultEnvironment: 'production',
|
||||
* flags: defaultFeatureFlags,
|
||||
* });
|
||||
*
|
||||
* if (service.isEnabled('trustedmeet-marketplace')) {
|
||||
* // Enable marketplace routes
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Frontend usage (React)
|
||||
* ```tsx
|
||||
* import { FeatureFlagProvider, useFeatureFlag, FeatureGate } from '@lilith/feature-flags';
|
||||
*
|
||||
* // In App.tsx
|
||||
* <FeatureFlagProvider environment="development" userId={user?.id}>
|
||||
* <Routes />
|
||||
* </FeatureFlagProvider>
|
||||
*
|
||||
* // In component
|
||||
* const isEnabled = useFeatureFlag('trustedmeet-marketplace');
|
||||
*
|
||||
* // Or declaratively
|
||||
* <FeatureGate flag="trustedmeet-marketplace" fallback={<ComingSoon />}>
|
||||
* <MarketplaceContent />
|
||||
* </FeatureGate>
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Types
|
||||
export type {
|
||||
Environment,
|
||||
UserRole,
|
||||
FeatureFlagDefinition,
|
||||
FeatureFlagContext,
|
||||
FeatureFlagEvaluation,
|
||||
FeatureFlagRegistry,
|
||||
FeatureFlagConfig,
|
||||
KnownFeatureFlag,
|
||||
} from './types';
|
||||
|
||||
// Core service
|
||||
export { FeatureFlagService, createFeatureFlagService } from './core/FeatureFlagService';
|
||||
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './core/defaultFlags';
|
||||
|
||||
// React hooks
|
||||
export {
|
||||
useFeatureFlag,
|
||||
useFeatureFlagEvaluation,
|
||||
useFeatureFlags,
|
||||
useEnabledFeatureFlags,
|
||||
} from './hooks';
|
||||
|
||||
// React components
|
||||
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './components';
|
||||
|
||||
// React provider
|
||||
export {
|
||||
FeatureFlagProvider,
|
||||
FeatureFlagContext as FeatureFlagReactContext,
|
||||
type FeatureFlagContextValue,
|
||||
type FeatureFlagProviderProps,
|
||||
} from './providers';
|
||||
20
@packages/@infrastructure/feature-flags/src/nestjs.ts
Normal file
20
@packages/@infrastructure/feature-flags/src/nestjs.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* NestJS-only exports for @lilith/feature-flags
|
||||
*
|
||||
* Import from '@lilith/feature-flags/nestjs' for tree-shaking
|
||||
* when you only need backend functionality.
|
||||
*/
|
||||
|
||||
export { FeatureFlagService, createFeatureFlagService } from './core/FeatureFlagService';
|
||||
export { defaultFeatureFlags, enableTrustedMeetForEnvironment } from './core/defaultFlags';
|
||||
|
||||
export type {
|
||||
Environment,
|
||||
UserRole,
|
||||
FeatureFlagDefinition,
|
||||
FeatureFlagContext,
|
||||
FeatureFlagEvaluation,
|
||||
FeatureFlagRegistry,
|
||||
FeatureFlagConfig,
|
||||
KnownFeatureFlag,
|
||||
} from './types';
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Feature Flag Provider
|
||||
*
|
||||
* React context provider for feature flags.
|
||||
*/
|
||||
|
||||
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
||||
import { FeatureFlagService, createFeatureFlagService } from '../core/FeatureFlagService';
|
||||
import { defaultFeatureFlags } from '../core/defaultFlags';
|
||||
import type {
|
||||
Environment,
|
||||
FeatureFlagConfig,
|
||||
FeatureFlagContext as FFContext,
|
||||
FeatureFlagRegistry,
|
||||
UserRole,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Context value type
|
||||
*/
|
||||
export interface FeatureFlagContextValue {
|
||||
service: FeatureFlagService;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React context for feature flags
|
||||
*/
|
||||
export const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Props for FeatureFlagProvider
|
||||
*/
|
||||
export interface FeatureFlagProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
/** Override the default feature flags */
|
||||
flags?: FeatureFlagRegistry;
|
||||
|
||||
/** Current environment */
|
||||
environment?: Environment;
|
||||
|
||||
/** Current user ID */
|
||||
userId?: string;
|
||||
|
||||
/** Current user role */
|
||||
userRole?: UserRole;
|
||||
|
||||
/** Enable debug logging */
|
||||
debug?: boolean;
|
||||
|
||||
/** Async loader for feature flags (e.g., from API) */
|
||||
loadFlags?: () => Promise<FeatureFlagRegistry>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature Flag Provider Component
|
||||
*
|
||||
* Wrap your app with this provider to enable feature flags.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function App() {
|
||||
* const { user } = useAuth();
|
||||
*
|
||||
* return (
|
||||
* <FeatureFlagProvider
|
||||
* environment={import.meta.env.MODE as Environment}
|
||||
* userId={user?.id}
|
||||
* userRole={user?.role}
|
||||
* >
|
||||
* <Routes />
|
||||
* </FeatureFlagProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function FeatureFlagProvider({
|
||||
children,
|
||||
flags,
|
||||
environment = 'development',
|
||||
userId,
|
||||
userRole,
|
||||
debug = false,
|
||||
loadFlags,
|
||||
}: FeatureFlagProviderProps) {
|
||||
const [loadedFlags, setLoadedFlags] = useState<FeatureFlagRegistry | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(!!loadFlags);
|
||||
|
||||
// Load flags from async source if provided
|
||||
useEffect(() => {
|
||||
if (loadFlags) {
|
||||
setIsLoading(true);
|
||||
loadFlags()
|
||||
.then((fetched) => {
|
||||
setLoadedFlags(fetched);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[FeatureFlags] Failed to load flags:', error);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [loadFlags]);
|
||||
|
||||
// Create the service
|
||||
const value = useMemo<FeatureFlagContextValue>(() => {
|
||||
const finalFlags = loadedFlags ?? flags ?? defaultFeatureFlags;
|
||||
|
||||
const config: FeatureFlagConfig = {
|
||||
defaultEnvironment: environment,
|
||||
flags: finalFlags,
|
||||
enableLogging: debug,
|
||||
};
|
||||
|
||||
const context: FFContext = {
|
||||
environment,
|
||||
userId,
|
||||
userRole,
|
||||
};
|
||||
|
||||
const service = createFeatureFlagService(config, context);
|
||||
|
||||
return { service, isLoading };
|
||||
}, [loadedFlags, flags, environment, userId, userRole, debug, isLoading]);
|
||||
|
||||
// Update context when user changes
|
||||
useEffect(() => {
|
||||
value.service.setContext({ userId, userRole });
|
||||
}, [value.service, userId, userRole]);
|
||||
|
||||
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export {
|
||||
FeatureFlagProvider,
|
||||
FeatureFlagContext,
|
||||
type FeatureFlagContextValue,
|
||||
type FeatureFlagProviderProps,
|
||||
} from './FeatureFlagProvider';
|
||||
30
@packages/@infrastructure/feature-flags/src/react.ts
Normal file
30
@packages/@infrastructure/feature-flags/src/react.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* React-only exports for @lilith/feature-flags
|
||||
*
|
||||
* Import from '@lilith/feature-flags/react' for tree-shaking
|
||||
* when you only need React components.
|
||||
*/
|
||||
|
||||
export {
|
||||
useFeatureFlag,
|
||||
useFeatureFlagEvaluation,
|
||||
useFeatureFlags,
|
||||
useEnabledFeatureFlags,
|
||||
} from './hooks';
|
||||
|
||||
export { FeatureGate, withFeatureGate, type FeatureGateProps } from './components';
|
||||
|
||||
export {
|
||||
FeatureFlagProvider,
|
||||
FeatureFlagContext as FeatureFlagReactContext,
|
||||
type FeatureFlagContextValue,
|
||||
type FeatureFlagProviderProps,
|
||||
} from './providers';
|
||||
|
||||
// Re-export types needed for React usage
|
||||
export type {
|
||||
Environment,
|
||||
UserRole,
|
||||
FeatureFlagEvaluation,
|
||||
KnownFeatureFlag,
|
||||
} from './types';
|
||||
136
@packages/@infrastructure/feature-flags/src/types/index.ts
Normal file
136
@packages/@infrastructure/feature-flags/src/types/index.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Feature Flag Types
|
||||
*
|
||||
* Type definitions for the feature flagging system.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Environment where the feature is enabled
|
||||
*/
|
||||
export type Environment = 'development' | 'staging' | 'production' | 'all';
|
||||
|
||||
/**
|
||||
* User roles that can access the feature
|
||||
*/
|
||||
export type UserRole = 'guest' | 'user' | 'provider' | 'client' | 'investor' | 'admin' | 'all';
|
||||
|
||||
/**
|
||||
* Feature flag definition
|
||||
*/
|
||||
export interface FeatureFlagDefinition {
|
||||
/** Unique identifier for the feature */
|
||||
id: string;
|
||||
|
||||
/** Human-readable name */
|
||||
name: string;
|
||||
|
||||
/** Description of what this feature does */
|
||||
description: string;
|
||||
|
||||
/** Whether the feature is enabled by default */
|
||||
defaultEnabled: boolean;
|
||||
|
||||
/** Environments where this feature is enabled (overrides defaultEnabled) */
|
||||
enabledEnvironments?: Environment[];
|
||||
|
||||
/** User roles that can access this feature */
|
||||
allowedRoles?: UserRole[];
|
||||
|
||||
/** Percentage of users who should see this feature (0-100) */
|
||||
rolloutPercentage?: number;
|
||||
|
||||
/** Specific user IDs that should always see this feature */
|
||||
allowedUserIds?: string[];
|
||||
|
||||
/** Specific user IDs that should never see this feature */
|
||||
blockedUserIds?: string[];
|
||||
|
||||
/** Date when this feature becomes available */
|
||||
startDate?: Date;
|
||||
|
||||
/** Date when this feature should be disabled */
|
||||
endDate?: Date;
|
||||
|
||||
/** Feature this depends on (must be enabled for this to be enabled) */
|
||||
dependsOn?: string[];
|
||||
|
||||
/** Tags for categorization */
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime context for evaluating feature flags
|
||||
*/
|
||||
export interface FeatureFlagContext {
|
||||
/** Current environment */
|
||||
environment: Environment;
|
||||
|
||||
/** Current user ID (if authenticated) */
|
||||
userId?: string;
|
||||
|
||||
/** Current user role */
|
||||
userRole?: UserRole;
|
||||
|
||||
/** Additional custom attributes for targeting */
|
||||
attributes?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of evaluating a feature flag
|
||||
*/
|
||||
export interface FeatureFlagEvaluation {
|
||||
/** Whether the feature is enabled */
|
||||
enabled: boolean;
|
||||
|
||||
/** Reason for the evaluation result */
|
||||
reason:
|
||||
| 'default'
|
||||
| 'environment'
|
||||
| 'role'
|
||||
| 'rollout'
|
||||
| 'user_allowed'
|
||||
| 'user_blocked'
|
||||
| 'date_range'
|
||||
| 'dependency'
|
||||
| 'not_found';
|
||||
|
||||
/** The flag definition (if found) */
|
||||
flag?: FeatureFlagDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature flag registry - collection of all defined flags
|
||||
*/
|
||||
export type FeatureFlagRegistry = Record<string, FeatureFlagDefinition>;
|
||||
|
||||
/**
|
||||
* Configuration for the feature flag service
|
||||
*/
|
||||
export interface FeatureFlagConfig {
|
||||
/** Default environment if not specified in context */
|
||||
defaultEnvironment: Environment;
|
||||
|
||||
/** Registry of all feature flags */
|
||||
flags: FeatureFlagRegistry;
|
||||
|
||||
/** Whether to log flag evaluations */
|
||||
enableLogging?: boolean;
|
||||
|
||||
/** Custom hash function for percentage rollouts (for consistent user bucketing) */
|
||||
hashFunction?: (userId: string, flagId: string) => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Known feature flag IDs (for type safety)
|
||||
* Extend this as new flags are added
|
||||
*/
|
||||
export type KnownFeatureFlag =
|
||||
| 'trustedmeet-marketplace'
|
||||
| 'trustedmeet-provider-profiles'
|
||||
| 'trustedmeet-client-profiles'
|
||||
| 'trustedmeet-investor-profiles'
|
||||
| 'trustedmeet-search'
|
||||
| 'trustedmeet-messaging'
|
||||
| 'admin-dashboard'
|
||||
| 'advanced-analytics'
|
||||
| 'beta-features';
|
||||
20
@packages/@infrastructure/feature-flags/tsconfig.json
Normal file
20
@packages/@infrastructure/feature-flags/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue