// Types for configuration type EnvironmentName = 'development' | 'stage' | 'demo' | 'production' export type EnvironmentDetector = { name: EnvironmentName matcher: (hostname: string) => boolean } type EnvironmentConfig = { customDetectors?: EnvironmentDetector[] fallbackEnvironment?: EnvironmentName } type UrlMappingConfig = { customMappings?: Partial> customEnvironmentName?: EnvironmentName environmentConfig?: EnvironmentConfig } /** * Detects the current environment based on hostname patterns. * Uses an array-driven approach for both custom and default environment detection. * * @param config - Optional configuration for custom environment detection * @param config.customDetectors - Array of custom environment detectors that are checked before default logic. * Each detector has a `name` (environment to return) and a `matcher` function that receives the hostname. * Detectors are evaluated in order, and the first match wins. * @param config.fallbackEnvironment - Environment to return when no detectors match and default logic fails. * Defaults to 'production' if not specified. * * @returns The detected environment name * * @example * ```typescript * // Default usage - uses built-in hostname detection * const env = getEnvironmentName() * * // Custom detector for temporary environment * const env = getEnvironmentName({ * customDetectors: [ * { * name: 'accelerate', * matcher: (hostname) => hostname.includes('app-accelerate.pattern.com') * } * ] * }) * * // Override fallback for testing * const env = getEnvironmentName({ * fallbackEnvironment: 'development' * }) * ``` * */ export function getEnvironmentName( config?: EnvironmentConfig, ): EnvironmentName { const isClient = typeof window !== 'undefined' if (isClient) { const hostname = window.location.hostname // Check custom detectors first if provided if (config?.customDetectors) { for (const detector of config.customDetectors) { if (detector.matcher(hostname)) { return detector.name } } } // Default detection logic using array-driven approach // Benefits: easier to maintain, consistent with custom detectors, and easier to test const defaultDetectors: EnvironmentDetector[] = [ { name: 'development', matcher: (hostname) => hostname.includes('localhost') || hostname.includes('127.0.0.1') || hostname.includes('chromatic.com'), }, { name: 'demo', matcher: (hostname) => hostname.includes('demo.usepredict.com'), }, { name: 'stage', matcher: (hostname) => hostname.includes('staging') || hostname.includes('-stage.pattern.com') || hostname.includes('-stage.useshelf.com') || hostname.includes('-stage.patternasia.com.cn') || hostname.includes('-upgrade.pattern.com') || hostname.includes('amplifyapp.com'), }, ] // Check default detectors for (const detector of defaultDetectors) { if (detector.matcher(hostname)) { return detector.name } } } return config?.fallbackEnvironment || 'production' } /** * Generates API URL prefixes based on environment and backend service. * * @param backendToOverride - The backend service name to include in the URL (default: 'server') * @param config - Optional configuration for custom URL mapping * @param config.customMappings - Partial mapping of environments to URL patterns. * Use `{backend}` as a placeholder that will be replaced with the backendToOverride value. * Custom mappings take precedence over default logic for specified environments. * @param config.customEnvironmentName - Override the detected environment name. * Useful for forcing specific URL patterns regardless of actual environment. * @param config.environmentConfig - Configuration passed to getEnvironmentName() for custom detection. * Allows custom detectors and fallback environment to be used when determining the environment. * * @returns The API URL prefix string * * @example * ```typescript * // Default usage - uses detected environment * const url = getApiUrlPrefix('server') // '/server', '/staging-server', etc. * * // Custom mapping for specific environments * const url = getApiUrlPrefix('api', { * customMappings: { * 'accelerate': '/special-accelerate-{backend}', * 'development': '/local-{backend}' * } * }) * * // Force specific environment URL * const url = getApiUrlPrefix('service', { * customEnvironmentName: 'development' * }) * * // Use custom environment detection for URL mapping * const url = getApiUrlPrefix('api', { * environmentConfig: { * customDetectors: [ * { * name: 'accelerate', * matcher: (hostname) => hostname.includes('special-accelerate.com') * } * ] * }, * customMappings: { * 'accelerate': '/accelerate-{backend}' * } * }) * * // Combine all options * const url = getApiUrlPrefix('api', { * environmentConfig: { * fallbackEnvironment: 'development' * }, * customEnvironmentName: 'stage', // This takes precedence over environmentConfig * customMappings: { * 'stage': '/custom-staging-{backend}' * } * }) * ``` */ export function getApiUrlPrefix( backendToOverride = 'server', config?: UrlMappingConfig, ): string { const localOverride = typeof window !== 'undefined' && typeof localStorage !== 'undefined' && localStorage.getItem(`localBackendOverride:${backendToOverride}`) if (localOverride) return localOverride const environmentName = config?.customEnvironmentName || getEnvironmentName(config?.environmentConfig) // Check custom mappings first if provided if (config?.customMappings && config.customMappings[environmentName]) { const customMapping = config.customMappings[environmentName] return customMapping.replace('{backend}', backendToOverride) } // Default mapping logic switch (environmentName) { case 'development': case 'stage': return `/staging-${backendToOverride}` case 'demo': return `/demo-${backendToOverride}` default: return `/${backendToOverride}` } }