diff --git a/.eslintrc.js b/.eslintrc.js index 20feb2cb4..4114a7859 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,8 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'local-rules'], globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly', @@ -25,6 +27,23 @@ module.exports = { 'rules': { '@typescript-eslint/explicit-module-boundary-types': ['error'] } + }, + { + 'files': ['lib/**/*.ts', 'src/**/*.ts'], + 'excludedFiles': [ + '**/platform_support.ts', + '**/*.spec.ts', + '**/*.test.ts', + '**/*.tests.ts', + '**/*.test-d.ts', + '**/*.gen.ts', + '**/*.d.ts', + '**/__mocks__/**', + '**/tests/**' + ], + 'rules': { + 'local-rules/require-platform-declaration': 'error', + } } ], rules: { diff --git a/.platform-isolation.config.js b/.platform-isolation.config.js new file mode 100644 index 000000000..e7a1de0a4 --- /dev/null +++ b/.platform-isolation.config.js @@ -0,0 +1,39 @@ +/** + * Platform Isolation Configuration + * + * Configures which files should be validated by the platform isolation validator. + */ + +module.exports = { + // Base directories to scan for source files + include: [ + 'lib/**/*.ts', + 'lib/**/*.js' + ], + + // Files and patterns to exclude from validation + exclude: [ + // Platform definition file (this file defines Platform type, doesn't need __platforms) + '**/platform_support.ts', + + // Test files + '**/*.spec.ts', + '**/*.test.ts', + '**/*.tests.ts', + '**/*.test.js', + '**/*.spec.js', + '**/*.tests.js', + '**/*.umdtests.js', + '**/*.test-d.ts', + + // Generated files + '**/*.gen.ts', + + // Type declaration files + '**/*.d.ts', + + // Test directories and mocks + '**/__mocks__/**', + '**/tests/**' + ] +}; diff --git a/ESLINT_TROUBLESHOOTING.md b/ESLINT_TROUBLESHOOTING.md new file mode 100644 index 000000000..1731c5e71 --- /dev/null +++ b/ESLINT_TROUBLESHOOTING.md @@ -0,0 +1,84 @@ +# ESLint Rule Troubleshooting + +## The Rule is Working! + +The `require-platform-declaration` rule **is** working correctly from the command line: + +```bash +$ npx eslint lib/core/custom_attribute_condition_evaluator/index.ts + +lib/core/custom_attribute_condition_evaluator/index.ts + 16:1 error File must export __platforms to declare which platforms + it supports. Example: export const __platforms = ['__universal__'] as const; +``` + +## VSCode Not Showing Errors? + +If VSCode isn't showing the ESLint errors, try these steps: + +### 1. Restart ESLint Server +- Open Command Palette: `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +- Type: `ESLint: Restart ESLint Server` +- Press Enter + +### 2. Check ESLint Extension is Installed +- Open Extensions panel: `Cmd+Shift+X` (Mac) or `Ctrl+Shift+X` (Windows/Linux) +- Search for "ESLint" by Microsoft +- Make sure it's installed and enabled + +### 3. Check ESLint Output +- Open Output panel: `Cmd+Shift+U` (Mac) or `Ctrl+Shift+U` (Windows/Linux) +- Select "ESLint" from the dropdown +- Look for any error messages + +### 4. Reload VSCode Window +- Open Command Palette: `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +- Type: `Developer: Reload Window` +- Press Enter + +### 5. Check File is Being Linted +The rule only applies to: +- ✅ Files in `lib/` or `src/` directory +- ✅ TypeScript files (`.ts`) +- ❌ Test files (`.spec.ts`, `.test.ts`, etc.) +- ❌ Declaration files (`.d.ts`) + +### 6. Verify ESLint Configuration +Check that `.eslintrc.js` has the parser set: +```javascript +parser: '@typescript-eslint/parser', +``` + +And that the rule is in the overrides: +```javascript +overrides: [{ + files: ['*.ts', '!*.spec.ts', '!*.test.ts', '!*.tests.ts', '!*.test-d.ts'], + rules: { + 'local-rules/require-platform-declaration': 'error', + } +}] +``` + +## Manual Verification + +You can always verify the rule works by running: + +```bash +# Check a specific file +npx eslint lib/service.ts + +# Check all lib files (shows only errors) +npx eslint lib/**/*.ts --quiet +``` + +## Adding __platforms + +To fix the error, add this export to your file (after imports): + +```typescript +// Universal file (all platforms) +export const __platforms = ['__universal__'] as const; + +// OR platform-specific file +export const __platforms = ['browser', 'node'] as const; +``` diff --git a/README.md b/README.md index 08ab9f5ad..ebed7c3b8 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,18 @@ If you're updating your SDK version, please check the appropriate migration guid ## SDK Development +### Platform Isolation + +The SDK supports multiple JavaScript platforms (Browser, Node.js, React Native) with a unified codebase. To prevent runtime errors from platform-specific code being bundled incorrectly, we enforce **platform isolation** constraints: + +- Every source file must declare which platforms it supports using `export const __platforms: Platform[] = [...]` +- Files can only import from other files that support all their declared platforms +- Universal files (`__platforms = ['__universal__']`) work everywhere but can only import from other universal files + +This system is enforced at build time through ESLint rules and validation scripts, ensuring platform-specific code (like browser DOM APIs or Node.js `fs` module) never leaks into incompatible builds. + +**For detailed documentation**, see [docs/PLATFORM_ISOLATION.md](docs/PLATFORM_ISOLATION.md). + ### Unit Tests There is a mix of testing paradigms used within the JavaScript SDK which include Mocha, Chai, Karma, and Vitest, indicated by their respective `*.tests.js` and `*.spec.ts` filenames. diff --git a/docs/PLATFORM_ISOLATION.md b/docs/PLATFORM_ISOLATION.md new file mode 100644 index 000000000..a3b718d55 --- /dev/null +++ b/docs/PLATFORM_ISOLATION.md @@ -0,0 +1,298 @@ +# Platform Isolation + +## Overview + +This project supports multiple runtime platforms (Browser, Node.js, React Native, and Universal), with separate entry points for each. To ensure the build artifacts work correctly, platform-specific code must not be mixed. + +## Platform Declaration + +**Every non-test source file MUST export a `__platforms` array** to declare which platforms it supports. This is enforced by ESLint and validated at build time. + +### Export Declaration (Required) + +All files must include a `__platforms` export: + +**For universal files (all platforms):** +```typescript +export const __platforms = ['__universal__']; +``` + +**For platform-specific files:** +```typescript +export const __platforms = ['browser']; // Browser only +export const __platforms = ['node']; // Node.js only +export const __platforms = ['react_native']; // React Native only +``` + +**For multi-platform files (but not all):** + +```typescript +// lib/utils/web-features.ts +export const __platforms = ['browser', 'react_native']; + +// Your code that works on both browser and react_native +export function getWindowSize() { + // Implementation that works on both platforms +} +``` + +Valid platform identifiers: `'browser'`, `'node'`, `'react_native'`, `'__universal__'` + +**Important**: Only files that explicitly include `'__universal__'` in their `__platforms` array are considered universal. Files that list all concrete platforms (e.g., `['browser', 'node', 'react_native']`) are treated as multi-platform files, NOT universal files. They must still ensure imports support all their declared platforms. + +### File Naming Convention (Optional) + +While not enforced, you may optionally use file name suffixes for clarity: +- `.browser.ts` - Typically browser-specific +- `.node.ts` - Typically Node.js-specific +- `.react_native.ts` - Typically React Native-specific +- `.ts` (no suffix) - Typically universal + +**Note:** The validator currently enforces only the `__platforms` export declaration. File naming is informational and not validated. The `__platforms` export is the source of truth. + +## Import Rules + +Each platform-specific file can **only** import from: + +1. **Universal files** (no platform restrictions) +2. **Compatible platform files** (files that support ALL the required platforms) +3. **External packages** (node_modules) + +A file is compatible if: +- It's universal (no platform restrictions) +- For single-platform files: The import supports at least that platform +- For multi-platform files: The import supports ALL of those platforms + +### Compatibility Examples + +**Core Principle**: When file A imports file B, file B must support ALL platforms that file A runs on. + +**Universal File (`__platforms = ['__universal__']`)** +- ✅ Can import from: universal files (with `__universal__`) +- ❌ Cannot import from: any platform-specific files, even `['browser', 'node', 'react_native']` +- **Why**: Universal files run everywhere, so all imports must explicitly be universal +- **Note**: Listing all platforms like `['browser', 'node', 'react_native']` is NOT considered universal + +**Single Platform File (`__platforms = ['browser']`)** +- ✅ Can import from: universal files, files with `['browser']`, multi-platform files that include browser like `['browser', 'react_native']` +- ❌ Cannot import from: files without browser support like `['node']` or `['react_native']` only +- **Why**: The import must support the browser platform + +**Multi-Platform File (`__platforms = ['browser', 'react_native']`)** +- ✅ Can import from: universal files, files with `['browser', 'react_native']`, supersets like `['browser', 'node', 'react_native']` +- ❌ Cannot import from: files missing any platform like `['browser']` only or `['node']` +- **Why**: The import must support BOTH browser AND react_native + +**All-Platforms File (`__platforms = ['browser', 'node', 'react_native']`)** +- ✅ Can import from: universal files, files with exactly `['browser', 'node', 'react_native']` +- ❌ Cannot import from: any subset like `['browser']`, `['browser', 'react_native']`, etc. +- **Why**: This is NOT considered universal - imports must support all three platforms +- **Note**: If your code truly works everywhere, use `['__universal__']` instead + +### Examples + +✅ **Valid Imports** + +```typescript +// In lib/index.browser.ts (Browser platform only) +import { Config } from './shared_types'; // ✅ Universal file +import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; // ✅ browser + react_native (supports browser) +import { uuid } from 'uuid'; // ✅ External package +``` + +```typescript +// In lib/index.node.ts (Node platform only) +import { Config } from './shared_types'; // ✅ Universal file +import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; // ✅ Same platform +``` + +```typescript +// In lib/index.react_native.ts (React Native platform only) +import { Config } from './shared_types'; // ✅ Universal file + +// If web-features.ts has: __platforms = ['browser', 'react_native'] +import { getWindowSize } from './utils/web-features'; // ✅ Compatible (supports react_native) +``` + +```typescript +// In lib/utils/web-api.ts +// export const __platforms = ['browser', 'react_native']; + +import { Config } from './shared_types'; // ✅ Universal file + +// If dom-helpers.ts has: __platforms = ['browser', 'react_native'] +import { helpers } from './dom-helpers'; // ✅ Compatible (supports BOTH browser and react_native) +``` + +❌ **Invalid Imports** + +```typescript +// In lib/index.browser.ts (Browser platform only) +import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; // ❌ Node-only file +``` + +```typescript +// In lib/index.node.ts (Node platform only) +// If web-features.ts has: __platforms = ['browser', 'react_native'] +import { getWindowSize } from './utils/web-features'; // ❌ Not compatible with Node +``` + +```typescript +// In lib/shared_types.ts (Universal file) +// export const __platforms = ['__universal__']; + +import { helper } from './helper.browser'; // ❌ Browser-only, universal file needs imports that work everywhere +``` + +```typescript +// In lib/utils/web-api.ts +// export const __platforms = ['browser', 'react_native']; + +// If helper.browser.ts is browser-only +import { helper } from './helper.browser'; // ❌ Browser-only, doesn't support react_native + +// This file needs imports that work in BOTH browser AND react_native +``` + +## Automatic Validation + +Platform isolation is enforced automatically during the build process. + +### Running Validation + +```bash +# Run validation manually +npm run validate-platform-isolation + +# Validation runs automatically before build +npm run build +``` + +### How It Works + +The validation script (`scripts/validate-platform-isolation-ts.js`): + +1. Scans all source files in the `lib/` directory (excluding tests) +2. **Verifies every file has a `__platforms` export** - fails immediately if any file is missing this +3. **Validates all platform values** - ensures values in `__platforms` arrays are valid (read from Platform type) +4. Parses import statements using TypeScript AST (ES6 imports, require, dynamic imports) +5. **Checks import compatibility**: For each import, verifies that the imported file supports ALL platforms that the importing file runs on +6. Fails the build if violations are found or if any file lacks `__platforms` export + +**ESLint Integration:** The `require-platform-declaration` ESLint rule also enforces the `__platforms` export requirement during development. + +### Build Integration + +The validation is integrated into the build process: + +```json +{ + "scripts": { + "build": "npm run validate-platform-isolation && tsc --noEmit && ..." + } +} +``` + +If platform isolation is violated, the build will fail with a detailed error message showing: +- Which files have violations +- The line numbers of problematic imports +- What platform the file belongs to +- What platform it's incorrectly importing from + +## Creating New Platform-Specific Code + +When creating new platform-specific implementations: + +### Single Platform + +1. **Add `__platforms` export** declaring the platform (e.g., `export const __platforms = ['browser'];`) +2. Optionally name the file with a platform suffix for clarity (e.g., `my-feature.browser.ts`) +3. Only import from universal or compatible platform files +4. Create a universal factory or interface if multiple platforms need different implementations + +**Example:** + +```typescript +// lib/features/my-feature.ts (universal interface) +export interface MyFeature { + doSomething(): void; +} + +// lib/features/my-feature.browser.ts +export const __platforms = ['browser']; + +export class BrowserMyFeature implements MyFeature { + doSomething(): void { + // Browser-specific implementation + } +} + +// lib/features/my-feature.node.ts +export const __platforms = ['node']; + +export class NodeMyFeature implements MyFeature { + doSomething(): void { + // Node.js-specific implementation + } +} + +// lib/features/factory.browser.ts +import { BrowserMyFeature } from './my-feature.browser'; +export const createMyFeature = () => new BrowserMyFeature(); + +// lib/features/factory.node.ts +import { NodeMyFeature } from './my-feature.node'; +export const createMyFeature = () => new NodeMyFeature(); +``` + +### Multiple Platforms (But Not All) + +For code that works on multiple platforms but not all, use the `__platforms` export: + +**Example: Browser + React Native only** + +```typescript +// lib/utils/dom-helpers.ts +export const __platforms = ['browser', 'react_native']; + +// This code works on both browser and react_native, but not node +export function getElementById(id: string): Element | null { + if (typeof document !== 'undefined') { + return document.getElementById(id); + } + // React Native polyfill or alternative + return null; +} +``` + +**Example: Node + React Native only** + +```typescript +// lib/utils/native-crypto.ts +export const __platforms = ['node', 'react_native']; + +import crypto from 'crypto'; // Available in both Node and React Native + +export function generateHash(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex'); +} +``` + +## Troubleshooting + +If you encounter a platform isolation error: + +1. **Check the error message** - It will tell you which file and line has the violation +2. **Identify the issue** - Look at the import statement on that line +3. **Fix the import**: + - If the code should be universal, remove the platform suffix from the imported file + - If the code must be platform-specific, create separate implementations for each platform + - Use factory patterns to abstract platform-specific instantiation + +## Benefits + +- ✅ Prevents runtime errors from platform-incompatible code +- ✅ Catches issues at build time, not in production +- ✅ Makes platform boundaries explicit and maintainable +- ✅ Ensures each bundle only includes relevant code +- ✅ Works independently of linting tools (ESLint, Biome, etc.) diff --git a/eslint-local-rules/README.md b/eslint-local-rules/README.md new file mode 100644 index 000000000..7df08767e --- /dev/null +++ b/eslint-local-rules/README.md @@ -0,0 +1,79 @@ +# Local ESLint Rules + +This directory contains custom ESLint rules specific to this project. + +## Rules + +### `require-platform-declaration` + +**Purpose:** **Enforces that every non-test source file exports `__platforms`** to declare which platforms it supports. + +**Why:** This is a mandatory requirement for platform isolation. The rule catches missing declarations at lint time, before build or runtime. + +**Requirement:** Every `.ts`/`.js` file in `lib/` (except tests) MUST export `__platforms` array with valid platform values. + +**Enforcement:** +- ✅ Enabled for all `.ts` files in `lib/` directory +- ❌ Disabled for test files (`.spec.ts`, `.test.ts`, etc.) +- ❌ Disabled for `__mocks__` and `tests` directories + +**Valid Examples:** + +```typescript +// Universal file (all platforms) +export const __platforms = ['__universal__'] as const; + +// Platform-specific file +export const __platforms = ['browser', 'node'] as const; + +// With type annotation +export const __platforms: Platform[] = ['react_native'] as const; +``` + +**Invalid:** + +```typescript +// Missing __platforms export +// ESLint Error: File must export __platforms to declare which platforms it supports +``` + +## Configuration + +The rules are loaded via `eslint-plugin-local-rules` and configured in `.eslintrc.js`: + +```javascript +{ + plugins: ['local-rules'], + overrides: [{ + files: ['*.ts', '!*.spec.ts', '!*.test.ts'], + rules: { + 'local-rules/require-platform-declaration': 'error' + } + }] +} +``` + +## Adding New Rules + +1. Create a new rule file in this directory (e.g., `my-rule.js`) +2. Export the rule following ESLint's rule format +3. Add it to `index.js`: + ```javascript + module.exports = { + 'require-platform-declaration': require('./require-platform-declaration'), + 'my-rule': require('./my-rule'), // Add here + }; + ``` +4. Enable it in `.eslintrc.js` + +## Testing Rules + +Run ESLint on specific files to test: + +```bash +# Test on a specific file +npx eslint lib/service.ts + +# Test on all lib files +npx eslint lib/**/*.ts --quiet +``` diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js new file mode 100644 index 000000000..587ccd041 --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1,10 @@ +/** + * Local ESLint Rules + * + * Custom ESLint rules for the project. + * Loaded by eslint-plugin-local-rules. + */ + +module.exports = { + 'require-platform-declaration': require('./require-platform-declaration'), +}; diff --git a/eslint-local-rules/require-platform-declaration.js b/eslint-local-rules/require-platform-declaration.js new file mode 100644 index 000000000..549822239 --- /dev/null +++ b/eslint-local-rules/require-platform-declaration.js @@ -0,0 +1,148 @@ +/** + * ESLint Rule: require-platform-declaration + * + * Ensures that all source files export __platforms with valid platform values. + * + * File exclusions (test files, generated files, etc.) should be configured + * in .eslintrc.js using the 'excludedFiles' option. + * + * Valid: + * export const __platforms = ['browser']; + * export const __platforms = ['__universal__']; + * export const __platforms = ['browser', 'node']; + * + * Invalid: + * // Missing __platforms export + * // Invalid platform values (must match Platform type definition in platform_support.ts) + * // Not exported as const array + */ + +const path = require('path'); +const { getValidPlatforms } = require('../scripts/platform-utils'); + +// Cache for valid platforms per workspace +const validPlatformsCache = new Map(); + +function getValidPlatformsForContext(context) { + const filename = context.getFilename(); + const workspaceRoot = filename.split('/lib/')[0]; + + if (validPlatformsCache.has(workspaceRoot)) { + return validPlatformsCache.get(workspaceRoot); + } + + const platforms = getValidPlatforms(workspaceRoot); + validPlatformsCache.set(workspaceRoot, platforms); + return platforms; +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Require __platforms export with valid platform values in all source files', + category: 'Best Practices', + recommended: true, + }, + messages: { + missingPlatformDeclaration: 'File must export __platforms to declare which platforms it supports. Example: export const __platforms = [\'__universal__\'];', + invalidPlatformDeclaration: '__platforms must be exported as a const array. Example: export const __platforms = [\'browser\', \'node\'];', + invalidPlatformValue: '__platforms contains invalid platform value "{{value}}". Valid platforms are: {{validPlatforms}}', + emptyPlatformArray: '__platforms array cannot be empty. Specify at least one platform or use [\'__universal__\']', + }, + schema: [], + }, + + create(context) { + const VALID_PLATFORMS = getValidPlatformsForContext(context); + let hasPlatformExport = false; + + return { + ExportNamedDeclaration(node) { + // Check for: export const __platforms = [...] + if (node.declaration && + node.declaration.type === 'VariableDeclaration') { + + for (const declarator of node.declaration.declarations) { + if (declarator.id.type === 'Identifier' && + declarator.id.name === '__platforms') { + + hasPlatformExport = true; + + // Validate it's a const + if (node.declaration.kind !== 'const') { + context.report({ + node: declarator, + messageId: 'invalidPlatformDeclaration', + }); + return; + } + + // Validate it's an array expression + let init = declarator.init; + + // Handle TSAsExpression: [...] as const + if (init && init.type === 'TSAsExpression') { + init = init.expression; + } + + // Handle TSTypeAssertion: [...] + if (init && init.type === 'TSTypeAssertion') { + init = init.expression; + } + + if (!init || init.type !== 'ArrayExpression') { + context.report({ + node: declarator, + messageId: 'invalidPlatformDeclaration', + }); + return; + } + + // Check if array is empty + if (init.elements.length === 0) { + context.report({ + node: init, + messageId: 'emptyPlatformArray', + }); + return; + } + + // Validate each array element is a valid platform string + for (const element of init.elements) { + if (element && element.type === 'Literal' && typeof element.value === 'string') { + if (!VALID_PLATFORMS.includes(element.value)) { + context.report({ + node: element, + messageId: 'invalidPlatformValue', + data: { + value: element.value, + validPlatforms: VALID_PLATFORMS.map(p => `'${p}'`).join(', ') + } + }); + } + } else { + // Not a string literal + context.report({ + node: element || init, + messageId: 'invalidPlatformDeclaration', + }); + } + } + } + } + } + }, + + 'Program:exit'(node) { + // At the end of the file, check if __platforms was exported + if (!hasPlatformExport) { + context.report({ + node, + messageId: 'missingPlatformDeclaration', + }); + } + }, + }; + }, +}; diff --git a/lib/client_factory.ts b/lib/client_factory.ts index 3be99b554..5f4bac98a 100644 --- a/lib/client_factory.ts +++ b/lib/client_factory.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './platform_support'; import { Config } from "./shared_types"; import { extractLogger } from "./logging/logger_factory"; import { extractErrorNotifier } from "./error/error_notifier_factory"; @@ -29,6 +30,7 @@ import { InMemoryLruCache } from "./utils/cache/in_memory_lru_cache"; import { transformCache, CacheWithRemove } from "./utils/cache/cache"; import { ConstantBackoff } from "./utils/repeater/repeater"; + export type OptimizelyFactoryConfig = Config & { requestHandler: RequestHandler; } @@ -94,3 +96,5 @@ export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Optimize return new Optimizely(optimizelyOptions); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/common_exports.ts b/lib/common_exports.ts index 801fb7728..b3cbd940c 100644 --- a/lib/common_exports.ts +++ b/lib/common_exports.ts @@ -1,3 +1,5 @@ +import { Platform } from './platform_support'; + /** * Copyright 2023-2025 Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export { createStaticProjectConfigManager } from './project_config/config_manager_factory'; export { LogLevel } from './logging/logger'; @@ -35,3 +38,5 @@ export { export { NOTIFICATION_TYPES, DECISION_NOTIFICATION_TYPES } from './notification_center/type'; export { OptimizelyDecideOption } from './shared_types'; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/audience_evaluator/index.ts b/lib/core/audience_evaluator/index.ts index e2b3bce0a..335e61fd0 100644 --- a/lib/core/audience_evaluator/index.ts +++ b/lib/core/audience_evaluator/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import * as conditionTreeEvaluator from '../condition_tree_evaluator'; import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; import * as odpSegmentsConditionEvaluator from './odp_segment_condition_evaluator'; @@ -21,6 +22,7 @@ import { CONDITION_EVALUATOR_ERROR, UNKNOWN_CONDITION_TYPE } from 'error_message import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE} from 'log_message'; import { LoggerFacade } from '../../logging/logger'; + export class AudienceEvaluator { private logger?: LoggerFacade; @@ -119,3 +121,5 @@ export default AudienceEvaluator; export const createAudienceEvaluator = function(UNSTABLE_conditionEvaluators: unknown, logger?: LoggerFacade): AudienceEvaluator { return new AudienceEvaluator(UNSTABLE_conditionEvaluators, logger); }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts index 7380c9269..ab9a8d004 100644 --- a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ +import { Platform } from './../../../platform_support'; import { UNKNOWN_MATCH_TYPE } from 'error_message'; import { LoggerFacade } from '../../../logging/logger'; import { Condition, OptimizelyUserContext } from '../../../shared_types'; + const QUALIFIED_MATCH_TYPE = 'qualified'; const MATCH_TYPES = [ @@ -66,3 +68,5 @@ function evaluate(condition: Condition, user: OptimizelyUserContext, logger?: Lo function qualifiedEvaluator(condition: Condition, user: OptimizelyUserContext): boolean { return user.isQualifiedFor(condition.value as string); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/bucketer/bucket_value_generator.ts b/lib/core/bucketer/bucket_value_generator.ts index c5f85303b..17a08c14d 100644 --- a/lib/core/bucketer/bucket_value_generator.ts +++ b/lib/core/bucketer/bucket_value_generator.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import murmurhash from 'murmurhash'; import { INVALID_BUCKETING_ID } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; + const HASH_SEED = 1; const MAX_HASH_VALUE = Math.pow(2, 32); const MAX_TRAFFIC_VALUE = 10000; @@ -38,3 +40,5 @@ export const generateBucketValue = function(bucketingKey: string): number { throw new OptimizelyError(INVALID_BUCKETING_ID, bucketingKey, ex.message); } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/bucketer/index.ts b/lib/core/bucketer/index.ts index e31c8df4b..58d10ee92 100644 --- a/lib/core/bucketer/index.ts +++ b/lib/core/bucketer/index.ts @@ -17,6 +17,7 @@ /** * Bucketer API for determining the variation id from the specified parameters */ +import { Platform } from './../../platform_support'; import { LoggerFacade } from '../../logging/logger'; import { DecisionResponse, @@ -29,6 +30,7 @@ import { OptimizelyError } from '../../error/optimizly_error'; import { generateBucketValue } from './bucket_value_generator'; import { DecisionReason } from '../decision_service'; + export const USER_NOT_IN_ANY_EXPERIMENT = 'User %s is not in any experiment of group %s.'; export const USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP = 'User %s is not in experiment %s of group %s.'; export const USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP = 'User %s is in experiment %s of group %s.'; @@ -208,3 +210,5 @@ export default { bucket: bucket, bucketUserIntoExperiment: bucketUserIntoExperiment, }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/condition_tree_evaluator/index.ts b/lib/core/condition_tree_evaluator/index.ts index 7b0c8df9d..434fd5d0a 100644 --- a/lib/core/condition_tree_evaluator/index.ts +++ b/lib/core/condition_tree_evaluator/index.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /**************************************************************************** * Copyright 2018, 2021, Optimizely, Inc. and contributors * * * @@ -14,6 +16,7 @@ * limitations under the License. * ***************************************************************************/ + const AND_CONDITION = 'and'; const OR_CONDITION = 'or'; const NOT_CONDITION = 'not'; @@ -129,3 +132,5 @@ function orEvaluator(conditions: ConditionTree, leafEvaluator: LeafE } return null; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/custom_attribute_condition_evaluator/index.ts b/lib/core/custom_attribute_condition_evaluator/index.ts index 797a7d4e0..936350159 100644 --- a/lib/core/custom_attribute_condition_evaluator/index.ts +++ b/lib/core/custom_attribute_condition_evaluator/index.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ + +import { Platform } from './../../platform_support'; import { Condition, OptimizelyUserContext } from '../../shared_types'; import fns from '../../utils/fns'; @@ -478,3 +480,5 @@ function semverLessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUs } return result <= 0; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/decision/index.ts b/lib/core/decision/index.ts index 27fd1c734..9e2b67eda 100644 --- a/lib/core/decision/index.ts +++ b/lib/core/decision/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { DecisionObj } from '../decision_service'; /** @@ -21,6 +22,8 @@ import { DecisionObj } from '../decision_service'; * @param {DecisionObj} decisionObj Object representing decision * @returns {string} Experiment key or empty string if experiment is null */ + + export function getExperimentKey(decisionObj: DecisionObj): string { return decisionObj.experiment?.key ?? ''; } @@ -60,3 +63,5 @@ export function getExperimentId(decisionObj: DecisionObj): string | null { export function getVariationId(decisionObj: DecisionObj): string | null { return decisionObj.variation?.id ?? null; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/decision_service/cmab/cmab_client.ts b/lib/core/decision_service/cmab/cmab_client.ts index a6925713a..7528936e1 100644 --- a/lib/core/decision_service/cmab/cmab_client.ts +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../../../platform_support'; import { OptimizelyError } from "../../../error/optimizly_error"; import { CMAB_FETCH_FAILED, INVALID_CMAB_FETCH_RESPONSE } from "../../../message/error_message"; import { UserAttributes } from "../../../shared_types"; @@ -24,6 +25,7 @@ import { isSuccessStatusCode } from "../../../utils/http_request_handler/http_ut import { BackoffController } from "../../../utils/repeater/repeater"; import { Producer } from "../../../utils/type"; + export interface CmabClient { fetchDecision( ruleId: string, @@ -119,3 +121,5 @@ export class DefaultCmabClient implements CmabClient { return body.predictions && body.predictions.length > 0 && body.predictions[0].variation_id; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/decision_service/cmab/cmab_service.ts b/lib/core/decision_service/cmab/cmab_service.ts index 1963df613..6e739e79b 100644 --- a/lib/core/decision_service/cmab/cmab_service.ts +++ b/lib/core/decision_service/cmab/cmab_service.ts @@ -32,6 +32,8 @@ import { INVALIDATE_CMAB_CACHE, RESET_CMAB_CACHE, } from 'log_message'; +import { Platform } from '../../../platform_support'; + export type CmabDecision = { variationId: string, @@ -196,3 +198,5 @@ export class DefaultCmabService implements CmabService { return `${len}-${userId}-${ruleId}`; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 33fd85eb1..1041de7c4 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -76,6 +76,9 @@ import { CmabService } from './cmab/cmab_service'; import { Maybe, OpType, OpValue } from '../../utils/type'; import { Value } from '../../utils/promise/operation_value'; +import { Platform } from '../../platform_support'; + + export const EXPERIMENT_NOT_RUNNING = 'Experiment %s is not running.'; export const RETURNING_STORED_VARIATION = 'Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.'; @@ -1694,3 +1697,5 @@ export class DecisionService { export function createDecisionService(options: DecisionServiceOptions): DecisionService { return new DecisionService(options); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/entrypoint.test-d.ts b/lib/entrypoint.test-d.ts index 366889ea8..09be638c0 100644 --- a/lib/entrypoint.test-d.ts +++ b/lib/entrypoint.test-d.ts @@ -56,8 +56,12 @@ import { LogLevel } from './logging/logger'; import { OptimizelyDecideOption } from './shared_types'; import { Maybe } from './utils/type'; +import { Platform } from './platform_support'; export type Entrypoint = { + // platform declaration + __platforms: Platform[]; + // client factory createInstance: (config: Config) => Client; diff --git a/lib/entrypoint.universal.test-d.ts b/lib/entrypoint.universal.test-d.ts index 184583a35..c399fc169 100644 --- a/lib/entrypoint.universal.test-d.ts +++ b/lib/entrypoint.universal.test-d.ts @@ -50,10 +50,14 @@ import { LogLevel } from './logging/logger'; import { OptimizelyDecideOption } from './shared_types'; import { UniversalConfig } from './index.universal'; import { OpaqueOdpManager } from './odp/odp_manager_factory'; +import { Platform } from './platform_support'; import { UniversalOdpManagerOptions } from './odp/odp_manager_factory.universal'; export type UniversalEntrypoint = { + // platform declaration + __platforms: Platform[]; + // client factory createInstance: (config: UniversalConfig) => Client; diff --git a/lib/error/error_handler.ts b/lib/error/error_handler.ts index 4a772c71c..4bad9ecfa 100644 --- a/lib/error/error_handler.ts +++ b/lib/error/error_handler.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2019, 2025, Optimizely * @@ -17,6 +19,7 @@ * @export * @interface ErrorHandler */ + export interface ErrorHandler { /** * @param {Error} exception @@ -24,3 +27,5 @@ export interface ErrorHandler { */ handleError(exception: Error): void } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/error/error_notifier.ts b/lib/error/error_notifier.ts index 174c163e2..807052591 100644 --- a/lib/error/error_notifier.ts +++ b/lib/error/error_notifier.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { MessageResolver } from "../message/message_resolver"; import { ErrorHandler } from "./error_handler"; import { OptimizelyError } from "./optimizly_error"; + export interface ErrorNotifier { notify(error: Error): void; child(name: string): ErrorNotifier; @@ -44,3 +46,5 @@ export class DefaultErrorNotifier implements ErrorNotifier { return new DefaultErrorNotifier(this.errorHandler, this.messageResolver, name); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/error/error_notifier_factory.ts b/lib/error/error_notifier_factory.ts index 994564f1a..6122ea36e 100644 --- a/lib/error/error_notifier_factory.ts +++ b/lib/error/error_notifier_factory.ts @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { errorResolver } from "../message/message_resolver"; import { Maybe } from "../utils/type"; import { ErrorHandler } from "./error_handler"; import { DefaultErrorNotifier } from "./error_notifier"; + export const INVALID_ERROR_HANDLER = 'Invalid error handler'; const errorNotifierSymbol = Symbol(); @@ -46,3 +48,5 @@ export const extractErrorNotifier = (errorNotifier: Maybe): return errorNotifier[errorNotifierSymbol] as Maybe; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/error/error_reporter.ts b/lib/error/error_reporter.ts index 130527928..b1d909a0a 100644 --- a/lib/error/error_reporter.ts +++ b/lib/error/error_reporter.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { LoggerFacade } from "../logging/logger"; import { ErrorNotifier } from "./error_notifier"; import { OptimizelyError } from "./optimizly_error"; + export class ErrorReporter { private logger?: LoggerFacade; private errorNotifier?: ErrorNotifier; @@ -53,3 +55,5 @@ export class ErrorReporter { this.errorNotifier = errorNotifier; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/error/optimizly_error.ts b/lib/error/optimizly_error.ts index 76a07511a..7081daa01 100644 --- a/lib/error/optimizly_error.ts +++ b/lib/error/optimizly_error.ts @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { MessageResolver } from "../message/message_resolver"; import { sprintf } from "../utils/fns"; + export class OptimizelyError extends Error { baseMessage: string; params: any[]; @@ -38,3 +40,5 @@ export class OptimizelyError extends Error { } } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/batch_event_processor.react_native.ts b/lib/event_processor/batch_event_processor.react_native.ts index 28741380a..f1a770f78 100644 --- a/lib/event_processor/batch_event_processor.react_native.ts +++ b/lib/event_processor/batch_event_processor.react_native.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { NetInfoState, addEventListener } from '@react-native-community/netinfo'; import { BatchEventProcessor, BatchEventProcessorConfig } from './batch_event_processor'; import { Fn } from '../utils/type'; + export class ReactNativeNetInfoEventProcessor extends BatchEventProcessor { private isInternetReachable = true; private unsubscribeNetInfo?: Fn; @@ -49,3 +51,5 @@ export class ReactNativeNetInfoEventProcessor extends BatchEventProcessor { super.stop(); } } + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts index ba9931f06..22f56e99c 100644 --- a/lib/event_processor/batch_event_processor.ts +++ b/lib/event_processor/batch_event_processor.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { EventProcessor, ProcessableEvent } from "./event_processor"; import { getBatchedAsync, getBatchedSync, Store } from "../utils/cache/store"; import { EventDispatcher, EventDispatcherResponse, LogEvent } from "./event_dispatcher/event_dispatcher"; @@ -32,6 +33,7 @@ import { OptimizelyError } from "../error/optimizly_error"; import { sprintf } from "../utils/fns"; import { SERVICE_STOPPED_BEFORE_RUNNING } from "../service"; + export const DEFAULT_MIN_BACKOFF = 1000; export const DEFAULT_MAX_BACKOFF = 32000; export const MAX_EVENTS_IN_STORE = 500; @@ -317,3 +319,5 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { }); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts index 4d4048950..ef137fc1f 100644 --- a/lib/event_processor/event_builder/log_event.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { ConversionEvent, ImpressionEvent, UserEvent } from './user_event'; import { CONTROL_ATTRIBUTES } from '../../utils/enums'; @@ -21,6 +22,7 @@ import { LogEvent } from '../event_dispatcher/event_dispatcher'; import { EventTags } from '../../shared_types'; import { Region } from '../../project_config/project_config'; + const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' @@ -230,3 +232,5 @@ export function buildLogEvent(events: UserEvent[]): LogEvent { params: makeEventBatch(events), } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_builder/user_event.ts b/lib/event_processor/event_builder/user_event.ts index ae33d65da..04d0e256f 100644 --- a/lib/event_processor/event_builder/user_event.ts +++ b/lib/event_processor/event_builder/user_event.ts @@ -29,6 +29,8 @@ import { import { EventTags, UserAttributes } from '../../shared_types'; import { LoggerFacade } from '../../logging/logger'; import { DECISION_SOURCES } from '../../common_exports'; +import { Platform } from '../../platform_support'; + export type VisitorAttribute = { entityId: string @@ -304,3 +306,5 @@ const buildVisitorAttributes = ( return builtAttributes; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts b/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts index d38d266aa..3b4200ce1 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +// This implementation works in both browser and react_native environments + +import { Platform } from './../../platform_support'; import { BrowserRequestHandler } from "../../utils/http_request_handler/request_handler.browser"; import { EventDispatcher } from './event_dispatcher'; import { DefaultEventDispatcher } from './default_dispatcher'; @@ -21,3 +24,5 @@ import { DefaultEventDispatcher } from './default_dispatcher'; const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new BrowserRequestHandler()); export default eventDispatcher; + +export const __platforms: Platform[] = ['browser', 'react_native']; diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.node.ts b/lib/event_processor/event_dispatcher/default_dispatcher.node.ts index 65dc115af..a7193dc96 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.node.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.node.ts @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { EventDispatcher } from './event_dispatcher'; import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; import { DefaultEventDispatcher } from './default_dispatcher'; + const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new NodeRequestHandler()); export default eventDispatcher; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/event_processor/event_dispatcher/default_dispatcher.ts b/lib/event_processor/event_dispatcher/default_dispatcher.ts index b786ffda2..d82659078 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.ts @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { OptimizelyError } from '../../error/optimizly_error'; import { ONLY_POST_REQUESTS_ARE_SUPPORTED } from 'error_message'; import { RequestHandler } from '../../utils/http_request_handler/http'; import { EventDispatcher, EventDispatcherResponse, LogEvent } from './event_dispatcher'; + export class DefaultEventDispatcher implements EventDispatcher { private requestHandler: RequestHandler; @@ -43,3 +45,5 @@ export class DefaultEventDispatcher implements EventDispatcher { return abortableRequest.responsePromise; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_dispatcher/event_dispatcher.ts b/lib/event_processor/event_dispatcher/event_dispatcher.ts index 4dfda8f30..d8a1e3973 100644 --- a/lib/event_processor/event_dispatcher/event_dispatcher.ts +++ b/lib/event_processor/event_dispatcher/event_dispatcher.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { EventBatch } from "../event_builder/log_event"; + export type EventDispatcherResponse = { statusCode?: number } @@ -28,3 +30,5 @@ export interface LogEvent { httpVerb: 'POST' | 'PUT' | 'GET' | 'PATCH' params: EventBatch, } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts index 383ad8380..08a1e32a0 100644 --- a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts +++ b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts @@ -14,13 +14,17 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { RequestHandler } from '../../utils/http_request_handler/http'; import { DefaultEventDispatcher } from './default_dispatcher'; import { EventDispatcher } from './event_dispatcher'; import { validateRequestHandler } from '../../utils/http_request_handler/request_handler_validator'; + export const createEventDispatcher = (requestHander: RequestHandler): EventDispatcher => { validateRequestHandler(requestHander); return new DefaultEventDispatcher(requestHander); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts index 006adedd6..e5b66f704 100644 --- a/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts +++ b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { OptimizelyError } from '../../error/optimizly_error'; import { SEND_BEACON_FAILED } from 'error_message'; import { EventDispatcher, EventDispatcherResponse } from './event_dispatcher'; + export type Event = { url: string; httpVerb: 'POST'; @@ -51,3 +53,5 @@ const eventDispatcher : EventDispatcher = { } export default eventDispatcher; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/event_processor/event_processor.ts b/lib/event_processor/event_processor.ts index 585c71f68..153accdf4 100644 --- a/lib/event_processor/event_processor.ts +++ b/lib/event_processor/event_processor.ts @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { ConversionEvent, ImpressionEvent } from './event_builder/user_event' import { LogEvent } from './event_dispatcher/event_dispatcher' import { Service } from '../service' import { Consumer, Fn } from '../utils/type'; import { LoggerFacade } from '../logging/logger'; + export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s export const DEFAULT_BATCH_SIZE = 10 @@ -30,3 +32,5 @@ export interface EventProcessor extends Service { setLogger(logger: LoggerFacade): void; flushImmediately(): Promise; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_processor_factory.browser.ts b/lib/event_processor/event_processor_factory.browser.ts index 1e8b251ef..22db5492f 100644 --- a/lib/event_processor/event_processor_factory.browser.ts +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -29,6 +29,8 @@ import sendBeaconEventDispatcher from './event_dispatcher/send_beacon_dispatcher import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; import { DEFAULT_MAX_EVENTS_IN_STORE, EventStore } from './event_store'; +import { Platform } from '../platform_support'; + export const DEFAULT_EVENT_BATCH_SIZE = 10; export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; @@ -66,3 +68,5 @@ export const createBatchEventProcessor = ( storeTtl: options.storeTtl, }); }; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/event_processor/event_processor_factory.node.ts b/lib/event_processor/event_processor_factory.node.ts index cfa10feae..57f4c18f7 100644 --- a/lib/event_processor/event_processor_factory.node.ts +++ b/lib/event_processor/event_processor_factory.node.ts @@ -24,6 +24,8 @@ import { wrapEventProcessor, getForwardingEventProcessor, } from './event_processor_factory'; +import { Platform } from '../platform_support'; + export const DEFAULT_EVENT_BATCH_SIZE = 10; export const DEFAULT_EVENT_FLUSH_INTERVAL = 30_000; @@ -55,3 +57,5 @@ export const createBatchEventProcessor = ( eventStore, }); }; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/event_processor/event_processor_factory.react_native.ts b/lib/event_processor/event_processor_factory.react_native.ts index 0d2f00971..018160457 100644 --- a/lib/event_processor/event_processor_factory.react_native.ts +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -28,6 +28,8 @@ import { EventWithId } from './batch_event_processor'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { ReactNativeNetInfoEventProcessor } from './batch_event_processor.react_native'; import { DEFAULT_MAX_EVENTS_IN_STORE, EventStore } from './event_store'; +import { Platform } from '../platform_support'; + export const DEFAULT_EVENT_BATCH_SIZE = 10; export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; @@ -66,3 +68,5 @@ export const createBatchEventProcessor = ( ReactNativeNetInfoEventProcessor ); }; + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/event_processor/event_processor_factory.ts b/lib/event_processor/event_processor_factory.ts index 7c7fda93d..139dd965b 100644 --- a/lib/event_processor/event_processor_factory.ts +++ b/lib/event_processor/event_processor_factory.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { LogLevel } from "../logging/logger"; import { StartupLog } from "../service"; import { AsyncPrefixStore, Store, SyncPrefixStore } from "../utils/cache/store"; @@ -26,6 +27,7 @@ import { EventProcessor } from "./event_processor"; import { EVENT_STORE_PREFIX } from "./event_store"; import { ForwardingEventProcessor } from "./forwarding_event_processor"; + export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; export const FAILED_EVENT_RETRY_INTERVAL = 20 * 1000; @@ -175,3 +177,5 @@ export function getForwardingEventProcessor(dispatcher: EventDispatcher): EventP validateEventDispatcher(dispatcher); return new ForwardingEventProcessor(dispatcher); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_processor_factory.universal.ts b/lib/event_processor/event_processor_factory.universal.ts index 0a3b2ec56..8a522f5f8 100644 --- a/lib/event_processor/event_processor_factory.universal.ts +++ b/lib/event_processor/event_processor_factory.universal.ts @@ -24,6 +24,8 @@ import { wrapEventProcessor, getPrefixEventStore, } from './event_processor_factory'; +import { Platform } from '../platform_support'; + export const DEFAULT_EVENT_BATCH_SIZE = 10; export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; @@ -59,3 +61,5 @@ export const createBatchEventProcessor = ( eventStore: eventStore, }); }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/event_store.ts b/lib/event_processor/event_store.ts index b55520a19..d4b051a28 100644 --- a/lib/event_processor/event_store.ts +++ b/lib/event_processor/event_store.ts @@ -12,6 +12,8 @@ import { import { SerialRunner } from "../utils/executor/serial_runner"; import { Maybe } from "../utils/type"; import { EventWithId } from "./batch_event_processor"; +import { Platform } from '../platform_support'; + export type StoredEvent = EventWithId & { _time?: { @@ -151,3 +153,5 @@ export class EventStore extends AsyncStoreWithBatchedGet implements return values.map((value, index) => this.processStoredEvent(keys[index], value)); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts index f578992c7..73532da30 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -15,6 +15,7 @@ */ +import { Platform } from './../platform_support'; import { LogEvent } from './event_dispatcher/event_dispatcher'; import { EventProcessor, ProcessableEvent } from './event_processor'; @@ -26,6 +27,7 @@ import { Consumer, Fn } from '../utils/type'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; import { sprintf } from '../utils/fns'; + export class ForwardingEventProcessor extends BaseService implements EventProcessor { private dispatcher: EventDispatcher; private eventEmitter: EventEmitter<{ dispatch: LogEvent }>; @@ -74,3 +76,5 @@ export class ForwardingEventProcessor extends BaseService implements EventProces return Promise.resolve(); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/export_types.ts b/lib/export_types.ts index b620fbb8e..86aba4aca 100644 --- a/lib/export_types.ts +++ b/lib/export_types.ts @@ -1,3 +1,5 @@ +import { Platform } from './platform_support'; + /** * Copyright 2022-2024, Optimizely * @@ -15,6 +17,7 @@ */ // config manager related types + export type { StaticConfigManagerConfig, PollingConfigManagerConfig, @@ -103,3 +106,5 @@ export type { NotificationCenter, OptimizelySegmentOption, } from './shared_types'; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/feature_toggle.ts b/lib/feature_toggle.ts index 67ccb3d83..0e2a6fad9 100644 --- a/lib/feature_toggle.ts +++ b/lib/feature_toggle.ts @@ -1,3 +1,5 @@ +import { Platform } from './platform_support'; + /** * Copyright 2025, Optimizely * @@ -34,4 +36,7 @@ // example feature flag definition // export const wipFeat = () => false as const; + export type IfActive boolean, Y, N = unknown> = ReturnType extends true ? Y : N; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 0f644a844..b7498f007 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -19,6 +19,7 @@ import { getOptimizelyInstance } from './client_factory'; import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; import { JAVASCRIPT_CLIENT_ENGINE } from './utils/enums'; import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; +import { Platform } from './platform_support'; /** * Creates an instance of the Optimizely class @@ -26,6 +27,8 @@ import { BrowserRequestHandler } from './utils/http_request_handler/request_hand * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const client = getOptimizelyInstance({ ...config, @@ -62,3 +65,5 @@ export * from './common_exports'; export * from './export_types'; export const clientEngine: string = JAVASCRIPT_CLIENT_ENGINE; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/index.browser.umdtests.js b/lib/index.browser.umdtests.js index a13f5046b..a9cbdeed2 100644 --- a/lib/index.browser.umdtests.js +++ b/lib/index.browser.umdtests.js @@ -25,6 +25,8 @@ import packageJSON from '../package.json'; import eventDispatcher from './plugins/event_dispatcher/index.browser'; import { INVALID_CONFIG_OR_SOMETHING } from './exception_messages'; +export const __platforms = ['browser'] as const; + describe('javascript-sdk', function() { describe('APIs', function() { describe('createInstance', function() { diff --git a/lib/index.node.ts b/lib/index.node.ts index 02d162ed6..48b99c159 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -18,6 +18,7 @@ import { Client, Config } from './shared_types'; import { getOptimizelyInstance, OptimizelyFactoryConfig } from './client_factory'; import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; +import { Platform } from './platform_support'; /** * Creates an instance of the Optimizely class @@ -25,6 +26,8 @@ import { NodeRequestHandler } from './utils/http_request_handler/request_handler * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const nodeConfig: OptimizelyFactoryConfig = { ...config, @@ -52,3 +55,5 @@ export * from './common_exports'; export * from './export_types'; export const clientEngine: string = NODE_CLIENT_ENGINE; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index c393261b7..ee3afdd8a 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -21,6 +21,7 @@ import { getOptimizelyInstance, OptimizelyFactoryConfig } from './client_factory import { REACT_NATIVE_JS_CLIENT_ENGINE } from './utils/enums'; import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; +import { Platform } from './platform_support'; /** * Creates an instance of the Optimizely class @@ -28,6 +29,8 @@ import { BrowserRequestHandler } from './utils/http_request_handler/request_hand * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const rnConfig: OptimizelyFactoryConfig = { ...config, @@ -55,3 +58,5 @@ export * from './common_exports'; export * from './export_types'; export const clientEngine: string = REACT_NATIVE_JS_CLIENT_ENGINE; + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/index.universal.ts b/lib/index.universal.ts index 11c39c1d1..b3f5f90de 100644 --- a/lib/index.universal.ts +++ b/lib/index.universal.ts @@ -18,6 +18,8 @@ import { getOptimizelyInstance } from './client_factory'; import { JAVASCRIPT_CLIENT_ENGINE } from './utils/enums'; import { RequestHandler } from './utils/http_request_handler/http'; +import { Platform } from './platform_support'; + export type UniversalConfig = Config & { requestHandler: RequestHandler; @@ -135,3 +137,5 @@ export type { NotificationCenter, OptimizelySegmentOption, } from './shared_types'; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/logging/logger.ts b/lib/logging/logger.ts index 8414d544a..b959d6d16 100644 --- a/lib/logging/logger.ts +++ b/lib/logging/logger.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { OptimizelyError } from '../error/optimizly_error'; import { MessageResolver } from '../message/message_resolver'; import { sprintf } from '../utils/fns' + export enum LogLevel { Debug, Info, @@ -165,3 +167,5 @@ export class OptimizelyLogger implements LoggerFacade { this.handleLog(level, resolvedMessage, args); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/logging/logger_factory.ts b/lib/logging/logger_factory.ts index 2aee1b535..807802d55 100644 --- a/lib/logging/logger_factory.ts +++ b/lib/logging/logger_factory.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { ConsoleLogHandler, LogHandler, LogLevel, OptimizelyLogger } from './logger'; import { errorResolver, infoResolver, MessageResolver } from '../message/message_resolver'; import { Maybe } from '../utils/type'; + export const INVALID_LOG_HANDLER = 'Invalid log handler'; export const INVALID_LEVEL_PRESET = 'Invalid level preset'; @@ -127,3 +129,5 @@ export const extractLogger = (logger: Maybe): Maybe; }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts index 61f876f4a..6a1e02c3c 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2024-2025, Optimizely * @@ -13,6 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export const NOTIFICATION_LISTENER_EXCEPTION = 'Notification listener for (%s) threw exception: %s'; export const CONDITION_EVALUATOR_ERROR = 'Error evaluating audience condition of type %s: %s'; export const EXPERIMENT_KEY_NOT_IN_DATAFILE = 'Experiment key %s is not in datafile.'; @@ -99,3 +102,5 @@ export const SERVICE_NOT_RUNNING = "%s not running"; export const EVENT_STORE_FULL = 'Event store is full. Not saving event with id %d.'; export const messages: string[] = []; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/message/log_message.ts b/lib/message/log_message.ts index b4757e2d3..1f4066c74 100644 --- a/lib/message/log_message.ts +++ b/lib/message/log_message.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2024, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export const FEATURE_ENABLED_FOR_USER = 'Feature %s is enabled for user %s.'; export const FEATURE_NOT_ENABLED_FOR_USER = 'Feature %s is not enabled for user %s.'; export const FAILED_TO_PARSE_VALUE = 'Failed to parse event value "%s" from event tags.'; @@ -68,3 +71,5 @@ export const CMAB_CACHE_ATTRIBUTES_MISMATCH = 'CMAB cache attributes mismatch fo export const CMAB_CACHE_MISS = 'Cache miss for user %s and rule %s.'; export const messages: string[] = []; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/message/message_resolver.ts b/lib/message/message_resolver.ts index 07a0cefdf..cc04d0aa4 100644 --- a/lib/message/message_resolver.ts +++ b/lib/message/message_resolver.ts @@ -1,6 +1,8 @@ +import { Platform } from './../platform_support'; import { messages as infoMessages } from 'log_message'; import { messages as errorMessages } from 'error_message'; + export interface MessageResolver { resolve(baseMessage: string): string; } @@ -18,3 +20,5 @@ export const errorResolver: MessageResolver = { return errorMessages[messageNum] || baseMessage; } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/notification_center/index.ts b/lib/notification_center/index.ts index 7b17ba658..089cfcddb 100644 --- a/lib/notification_center/index.ts +++ b/lib/notification_center/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { LoggerFacade } from '../logging/logger'; import { objectValues } from '../utils/fns'; @@ -24,6 +25,7 @@ import { NOTIFICATION_LISTENER_EXCEPTION } from 'error_message'; import { ErrorReporter } from '../error/error_reporter'; import { ErrorNotifier } from '../error/error_notifier'; + interface NotificationCenterOptions { logger?: LoggerFacade; errorNotifier?: ErrorNotifier; @@ -162,3 +164,5 @@ export class DefaultNotificationCenter implements NotificationCenter, Notificati export function createNotificationCenter(options: NotificationCenterOptions): DefaultNotificationCenter { return new DefaultNotificationCenter(options); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/notification_center/type.ts b/lib/notification_center/type.ts index 28b3dfeb0..81b0d9839 100644 --- a/lib/notification_center/type.ts +++ b/lib/notification_center/type.ts @@ -26,6 +26,8 @@ import { } from '../shared_types'; import { DecisionSource } from '../utils/enums'; import { Nullable } from '../utils/type'; +import { Platform } from '../platform_support'; + export type UserEventListenerPayload = { userId: string; @@ -150,3 +152,5 @@ export const NOTIFICATION_TYPES: NotificationTypeValues = { OPTIMIZELY_CONFIG_UPDATE: 'OPTIMIZELY_CONFIG_UPDATE', TRACK: 'TRACK', }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/constant.ts b/lib/odp/constant.ts index c33f3f0c9..2f60082f3 100644 --- a/lib/odp/constant.ts +++ b/lib/odp/constant.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2024, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export enum ODP_USER_KEY { VUID = 'vuid', FS_USER_ID = 'fs_user_id', @@ -26,3 +29,5 @@ export enum ODP_EVENT_ACTION { } export const ODP_DEFAULT_EVENT_TYPE = 'fullstack'; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/event_manager/odp_event.ts b/lib/odp/event_manager/odp_event.ts index 062798d1b..aed60ccb5 100644 --- a/lib/odp/event_manager/odp_event.ts +++ b/lib/odp/event_manager/odp_event.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2022-2024, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export class OdpEvent { /** * Type of event (typically "fullstack") @@ -49,3 +52,5 @@ export class OdpEvent { this.data = data ?? new Map(); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/event_manager/odp_event_api_manager.ts b/lib/odp/event_manager/odp_event_api_manager.ts index 79154b06e..2ef3e1af9 100644 --- a/lib/odp/event_manager/odp_event_api_manager.ts +++ b/lib/odp/event_manager/odp_event_api_manager.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { LoggerFacade } from '../../logging/logger'; import { OdpEvent } from './odp_event'; import { HttpMethod, RequestHandler } from '../../utils/http_request_handler/http'; import { OdpConfig } from '../odp_config'; + export type EventDispatchResponse = { statusCode?: number; }; @@ -114,3 +116,5 @@ export const eventApiRequestGenerator: EventRequestGenerator = (odpConfig: OdpCo }), }; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index d1a30d3ff..f9f77a6dc 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -37,6 +37,8 @@ import { OptimizelyError } from '../../error/optimizly_error'; import { LoggerFacade } from '../../logging/logger'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../../service'; import { sprintf } from '../../utils/fns'; +import { Platform } from '../../platform_support'; + export interface OdpEventManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void; @@ -246,3 +248,5 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/odp_config.ts b/lib/odp/odp_config.ts index 5003e1238..e8982d74d 100644 --- a/lib/odp/odp_config.ts +++ b/lib/odp/odp_config.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { checkArrayEquality } from '../utils/fns'; + export class OdpConfig { /** * Host of ODP audience segments API. @@ -81,3 +83,5 @@ export const odpIntegrationsAreEqual = (config1: OdpIntegrationConfig, config2: } export type OdpIntegrationConfig = OdpNotIntegratedConfig | OdpIntegratedConfig; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index feaca24b9..5fe26a5fe 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { v4 as uuidV4} from 'uuid'; import { LoggerFacade } from '../logging/logger'; @@ -32,6 +33,7 @@ import { Maybe } from '../utils/type'; import { sprintf } from '../utils/fns'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + export interface OdpManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; fetchQualifiedSegments(userId: string, options?: Array): Promise; @@ -263,3 +265,5 @@ export class DefaultOdpManager extends BaseService implements OdpManager { }); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/odp_manager_factory.browser.ts b/lib/odp/odp_manager_factory.browser.ts index e5d97d8e1..dc8f2eabc 100644 --- a/lib/odp/odp_manager_factory.browser.ts +++ b/lib/odp/odp_manager_factory.browser.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + export const BROWSER_DEFAULT_API_TIMEOUT = 10_000; export const BROWSER_DEFAULT_BATCH_SIZE = 10; export const BROWSER_DEFAULT_FLUSH_INTERVAL = 1000; @@ -40,3 +42,5 @@ export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpMana eventRequestGenerator: eventApiRequestGenerator, }); }; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/odp/odp_manager_factory.node.ts b/lib/odp/odp_manager_factory.node.ts index 7b8f737a7..5b2ef97cf 100644 --- a/lib/odp/odp_manager_factory.node.ts +++ b/lib/odp/odp_manager_factory.node.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { NodeRequestHandler } from '../utils/http_request_handler/request_handler.node'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + export const NODE_DEFAULT_API_TIMEOUT = 10_000; export const NODE_DEFAULT_BATCH_SIZE = 10; export const NODE_DEFAULT_FLUSH_INTERVAL = 1000; @@ -40,3 +42,5 @@ export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpMana eventRequestGenerator: eventApiRequestGenerator, }); }; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/odp/odp_manager_factory.react_native.ts b/lib/odp/odp_manager_factory.react_native.ts index c76312d6d..167cc54f4 100644 --- a/lib/odp/odp_manager_factory.react_native.ts +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { OdpManager } from './odp_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + export const RN_DEFAULT_API_TIMEOUT = 10_000; export const RN_DEFAULT_BATCH_SIZE = 10; export const RN_DEFAULT_FLUSH_INTERVAL = 1000; @@ -41,3 +43,5 @@ export const createOdpManager = (options: OdpManagerOptions = {}): OpaqueOdpMana eventRequestGenerator: eventApiRequestGenerator, }); }; + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts index 45c79e591..9b6fc271f 100644 --- a/lib/odp/odp_manager_factory.ts +++ b/lib/odp/odp_manager_factory.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { RequestHandler } from "../shared_types"; import { Cache } from "../utils/cache/cache"; import { InMemoryLruCache } from "../utils/cache/in_memory_lru_cache"; @@ -26,6 +27,7 @@ import { DefaultOdpSegmentApiManager } from "./segment_manager/odp_segment_api_m import { DefaultOdpSegmentManager, OdpSegmentManager } from "./segment_manager/odp_segment_manager"; import { UserAgentParser } from "./ua_parser/user_agent_parser"; + export const DEFAULT_CACHE_SIZE = 10_000; export const DEFAULT_CACHE_TIMEOUT = 600_000; @@ -136,3 +138,5 @@ export const extractOdpManager = (manager: Maybe): Maybe; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/odp_manager_factory.universal.ts b/lib/odp/odp_manager_factory.universal.ts index 6bf509611..9fea52040 100644 --- a/lib/odp/odp_manager_factory.universal.ts +++ b/lib/odp/odp_manager_factory.universal.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { RequestHandler } from '../utils/http_request_handler/http'; import { validateRequestHandler } from '../utils/http_request_handler/request_handler_validator'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; + export const DEFAULT_API_TIMEOUT = 10_000; export const DEFAULT_BATCH_SIZE = 1; export const DEFAULT_FLUSH_INTERVAL = 1000; @@ -38,3 +40,5 @@ export const createOdpManager = (options: UniversalOdpManagerOptions): OpaqueOdp eventRequestGenerator: eventApiRequestGenerator, }); }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/odp_types.ts b/lib/odp/odp_types.ts index abe47b245..34807a80b 100644 --- a/lib/odp/odp_types.ts +++ b/lib/odp/odp_types.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2022-2024, Optimizely * @@ -17,6 +19,7 @@ /** * Wrapper around valid data and error responses */ + export interface Response { data: Data; errors: Error[]; @@ -83,3 +86,5 @@ export interface Node { name: string; state: string; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/segment_manager/odp_response_schema.ts b/lib/odp/segment_manager/odp_response_schema.ts index 4221178af..5041d0d93 100644 --- a/lib/odp/segment_manager/odp_response_schema.ts +++ b/lib/odp/segment_manager/odp_response_schema.ts @@ -14,11 +14,14 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { JSONSchema4 } from 'json-schema'; /** * JSON Schema used to validate the ODP GraphQL response */ + + export const OdpResponseSchema = { $schema: 'https://json-schema.org/draft/2019-09/schema', $id: 'https://example.com/example.json', @@ -184,3 +187,5 @@ export const OdpResponseSchema = { }, examples: [], } as JSONSchema4; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts index 92eeaa02e..386d44884 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { LoggerFacade } from '../../logging/logger'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from './odp_response_schema'; @@ -24,6 +25,8 @@ import { log } from 'console'; /** * Expected value for a qualified/valid segment */ + + const QUALIFIED = 'qualified'; /** * Return value when no valid segments found @@ -196,3 +199,5 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { return EMPTY_JSON_RESPONSE; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/segment_manager/odp_segment_manager.ts b/lib/odp/segment_manager/odp_segment_manager.ts index 4ff125672..af2602fb9 100644 --- a/lib/odp/segment_manager/odp_segment_manager.ts +++ b/lib/odp/segment_manager/odp_segment_manager.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Cache } from '../../utils/cache/cache'; import { OdpSegmentApiManager } from './odp_segment_api_manager'; import { OdpIntegrationConfig } from '../odp_config'; @@ -22,6 +23,7 @@ import { ODP_USER_KEY } from '../constant'; import { LoggerFacade } from '../../logging/logger'; import { ODP_CONFIG_NOT_AVAILABLE, ODP_NOT_INTEGRATED } from 'error_message'; + export interface OdpSegmentManager { fetchQualifiedSegments( userKey: ODP_USER_KEY, @@ -128,3 +130,5 @@ export class DefaultOdpSegmentManager implements OdpSegmentManager { this.segmentsCache.reset(); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/segment_manager/optimizely_segment_option.ts b/lib/odp/segment_manager/optimizely_segment_option.ts index cf7c801ef..7dda97e70 100644 --- a/lib/odp/segment_manager/optimizely_segment_option.ts +++ b/lib/odp/segment_manager/optimizely_segment_option.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2022, 2024, Optimizely * @@ -15,7 +17,10 @@ */ // Options for defining behavior of OdpSegmentManager's caching mechanism when calling fetchSegments() + export enum OptimizelySegmentOption { IGNORE_CACHE = 'IGNORE_CACHE', RESET_CACHE = 'RESET_CACHE', } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/ua_parser/ua_parser.ts b/lib/odp/ua_parser/ua_parser.ts index 8622b0ade..61d44f34f 100644 --- a/lib/odp/ua_parser/ua_parser.ts +++ b/lib/odp/ua_parser/ua_parser.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { UAParser } from 'ua-parser-js'; import { UserAgentInfo } from './user_agent_info'; import { UserAgentParser } from './user_agent_parser'; + const userAgentParser: UserAgentParser = { parseUserAgentInfo(): UserAgentInfo { const parser = new UAParser(); @@ -29,3 +31,5 @@ const userAgentParser: UserAgentParser = { export function getUserAgentParser(): UserAgentParser { return userAgentParser; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/ua_parser/user_agent_info.ts b/lib/odp/ua_parser/user_agent_info.ts index e83b3b032..929c2d468 100644 --- a/lib/odp/ua_parser/user_agent_info.ts +++ b/lib/odp/ua_parser/user_agent_info.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2023, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export type UserAgentInfo = { os: { name?: string, @@ -24,3 +27,5 @@ export type UserAgentInfo = { model?: string, } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/odp/ua_parser/user_agent_parser.ts b/lib/odp/ua_parser/user_agent_parser.ts index 9ca30c141..793367cc0 100644 --- a/lib/odp/ua_parser/user_agent_parser.ts +++ b/lib/odp/ua_parser/user_agent_parser.ts @@ -14,8 +14,12 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { UserAgentInfo } from "./user_agent_info"; + export interface UserAgentParser { parseUserAgentInfo(): UserAgentInfo, } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 2381e8a80..bb579d0c0 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -46,6 +46,8 @@ import { createDecisionService, DecisionService, DecisionObj } from '../core/dec import { buildLogEvent } from '../event_processor/event_builder/log_event'; import { buildImpressionEvent, buildConversionEvent } from '../event_processor/event_builder/user_event'; import { isSafeInteger } from '../utils/fns'; +import { Platform } from '../platform_support'; + import { validate } from '../utils/attributes_validator'; import * as eventTagsValidator from '../utils/event_tags_validator'; import * as projectConfig from '../project_config/project_config'; @@ -1800,3 +1802,5 @@ export default class Optimizely extends BaseService implements Client { return this.vuidManager.getVuid(); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/optimizely_decision/index.ts b/lib/optimizely_decision/index.ts index b4adaed14..73d221a97 100644 --- a/lib/optimizely_decision/index.ts +++ b/lib/optimizely_decision/index.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ +import { Platform } from './../platform_support'; import { OptimizelyUserContext, OptimizelyDecision } from '../shared_types'; + export function newErrorDecision(key: string, user: OptimizelyUserContext, reasons: string[]): OptimizelyDecision { return { variationKey: null, @@ -26,3 +28,5 @@ export function newErrorDecision(key: string, user: OptimizelyUserContext, reaso reasons: reasons, }; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/optimizely_user_context/index.ts b/lib/optimizely_user_context/index.ts index 7b2af6488..3b7856a08 100644 --- a/lib/optimizely_user_context/index.ts +++ b/lib/optimizely_user_context/index.ts @@ -24,6 +24,8 @@ import { UserAttributes, } from '../shared_types'; import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; +import { Platform } from '../platform_support'; + export const FORCED_DECISION_NULL_RULE_KEY = '$opt_null_rule_key'; @@ -295,3 +297,5 @@ export default class OptimizelyUserContext implements IOptimizelyUserContext { return this._qualifiedSegments.indexOf(segment) > -1; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/platform_support.ts b/lib/platform_support.ts new file mode 100644 index 000000000..e8e5834a4 --- /dev/null +++ b/lib/platform_support.ts @@ -0,0 +1,17 @@ + +/** + * ⚠️ WARNING: DO NOT MOVE, DELETE, OR RENAME THIS FILE + * + * This file is used by the build system and validation scripts: + * - scripts/validate-platform-isolation-ts.js + * - scripts/platform-utils.js + * - eslint-local-rules/require-platform-declaration.js + * + * These tools parse this file at build time to extract the Platform type definition. + * Moving or renaming this file will break the build. + */ + +/** + * Valid platform identifiers + */ +export type Platform = 'browser' | 'node' | 'react_native' | '__universal__'; diff --git a/lib/project_config/config_manager_factory.browser.ts b/lib/project_config/config_manager_factory.browser.ts index 17741acb2..6b1e9a541 100644 --- a/lib/project_config/config_manager_factory.browser.ts +++ b/lib/project_config/config_manager_factory.browser.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; + export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { autoUpdate: false, @@ -24,3 +26,5 @@ export const createPollingProjectConfigManager = (config: PollingConfigManagerCo }; return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); }; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/project_config/config_manager_factory.node.ts b/lib/project_config/config_manager_factory.node.ts index 8e063e347..953d1cea1 100644 --- a/lib/project_config/config_manager_factory.node.ts +++ b/lib/project_config/config_manager_factory.node.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { NodeRequestHandler } from "../utils/http_request_handler/request_handler.node"; import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; + export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { autoUpdate: true, @@ -24,3 +26,5 @@ export const createPollingProjectConfigManager = (config: PollingConfigManagerCo }; return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); }; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/project_config/config_manager_factory.react_native.ts b/lib/project_config/config_manager_factory.react_native.ts index e30a565ca..9c1ddf023 100644 --- a/lib/project_config/config_manager_factory.react_native.ts +++ b/lib/project_config/config_manager_factory.react_native.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { AsyncStorageCache } from "../utils/cache/async_storage_cache.react_native"; import { BrowserRequestHandler } from "../utils/http_request_handler/request_handler.browser"; import { getOpaquePollingConfigManager, PollingConfigManagerConfig, OpaqueConfigManager } from "./config_manager_factory"; + export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { autoUpdate: true, @@ -27,3 +29,5 @@ export const createPollingProjectConfigManager = (config: PollingConfigManagerCo return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); }; + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/project_config/config_manager_factory.ts b/lib/project_config/config_manager_factory.ts index 3224d4f91..48dbd50dd 100644 --- a/lib/project_config/config_manager_factory.ts +++ b/lib/project_config/config_manager_factory.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { RequestHandler } from "../utils/http_request_handler/http"; import { Maybe, Transformer } from "../utils/type"; import { DatafileManagerConfig } from "./datafile_manager"; @@ -27,6 +28,7 @@ import { LogLevel } from '../logging/logger' import { Store } from "../utils/cache/store"; import { validateStore } from "../utils/cache/store_validator"; + export const INVALID_CONFIG_MANAGER = "Invalid config manager"; const configManagerSymbol: unique symbol = Symbol(); @@ -129,3 +131,5 @@ export const extractConfigManager = (opaqueConfigManager: OpaqueConfigManager): return opaqueConfigManager[configManagerSymbol] as ProjectConfigManager; }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/config_manager_factory.universal.ts b/lib/project_config/config_manager_factory.universal.ts index bcc664082..3e662f4c0 100644 --- a/lib/project_config/config_manager_factory.universal.ts +++ b/lib/project_config/config_manager_factory.universal.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; import { RequestHandler } from "../utils/http_request_handler/http"; import { validateRequestHandler } from "../utils/http_request_handler/request_handler_validator"; + export type UniversalPollingConfigManagerConfig = PollingConfigManagerConfig & { requestHandler: RequestHandler; } @@ -29,3 +31,5 @@ export const createPollingProjectConfigManager = (config: UniversalPollingConfig }; return getOpaquePollingConfigManager({ ...defaultConfig, ...config }); }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/constant.ts b/lib/project_config/constant.ts index 55e69a33e..632a55598 100644 --- a/lib/project_config/constant.ts +++ b/lib/project_config/constant.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2022-2023, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + const DEFAULT_UPDATE_INTERVAL_MINUTES = 5; /** Standard interval (5 minutes in milliseconds) for polling datafile updates.; */ export const DEFAULT_UPDATE_INTERVAL = DEFAULT_UPDATE_INTERVAL_MINUTES * 60 * 1000; @@ -31,3 +34,5 @@ export const DEFAULT_AUTHENTICATED_URL_TEMPLATE = `https://config.optimizely.com export const BACKOFF_BASE_WAIT_SECONDS_BY_ERROR_COUNT = [0, 8, 16, 32, 64, 128, 256, 512]; export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/datafile_manager.ts b/lib/project_config/datafile_manager.ts index b7c724113..8447e1829 100644 --- a/lib/project_config/datafile_manager.ts +++ b/lib/project_config/datafile_manager.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { Service, StartupLog } from '../service'; import { Store } from '../utils/cache/store'; import { RequestHandler } from '../utils/http_request_handler/http'; @@ -20,6 +21,7 @@ import { Fn, Consumer } from '../utils/type'; import { Repeater } from '../utils/repeater/repeater'; import { LoggerFacade } from '../logging/logger'; + export interface DatafileManager extends Service { get(): string | undefined; onUpdate(listener: Consumer): Fn; @@ -39,3 +41,5 @@ export type DatafileManagerConfig = { logger?: LoggerFacade; startupLogs?: StartupLog[]; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/optimizely_config.ts b/lib/project_config/optimizely_config.ts index b01255c43..36a369ea9 100644 --- a/lib/project_config/optimizely_config.ts +++ b/lib/project_config/optimizely_config.ts @@ -34,6 +34,10 @@ import { VariationVariable, } from '../shared_types'; + +import { DATAFILE_VERSIONS } from '../utils/enums'; +import { Platform } from '../platform_support'; + interface FeatureVariablesMap { [key: string]: FeatureVariable[]; } @@ -481,3 +485,5 @@ export class OptimizelyConfig { export function createOptimizelyConfig(configObj: ProjectConfig, datafile: string, logger?: LoggerFacade): OptimizelyConfig { return new OptimizelyConfig(configObj, datafile, logger); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index 7e928b8f8..f4e939e60 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -40,6 +40,8 @@ export const LOGGER_NAME = 'PollingDatafileManager'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; +import { Platform } from '../platform_support'; + export const FAILED_TO_FETCH_DATAFILE = 'Failed to fetch datafile'; export class PollingDatafileManager extends BaseService implements DatafileManager { @@ -266,3 +268,5 @@ export class PollingDatafileManager extends BaseService implements DatafileManag } } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index 1194b15cb..34c9802b3 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -53,6 +53,8 @@ import { import { SKIPPING_JSON_VALIDATION, VALID_DATAFILE } from 'log_message'; import { OptimizelyError } from '../error/optimizly_error'; +import { Platform } from '../platform_support'; + interface TryCreatingProjectConfigConfig { // TODO[OASIS-6649]: Don't use object type // eslint-disable-next-line @typescript-eslint/ban-types @@ -976,3 +978,5 @@ export default { tryCreatingProjectConfig, getTrafficAllocation, }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index 8d7002c03..f15c0065f 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -31,6 +31,8 @@ export const NO_SDKKEY_OR_DATAFILE = 'sdkKey or datafile must be provided'; export const GOT_INVALID_DATAFILE = 'got invalid datafile'; import { sprintf } from '../utils/fns'; +import { Platform } from '../platform_support'; + interface ProjectConfigManagerConfig { datafile?: string | Record; jsonSchemaValidator?: Transformer, @@ -235,3 +237,5 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf }); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/project_config/project_config_schema.ts b/lib/project_config/project_config_schema.ts index f842179dc..947f86305 100644 --- a/lib/project_config/project_config_schema.ts +++ b/lib/project_config/project_config_schema.ts @@ -17,8 +17,10 @@ /** * Project Config JSON Schema file used to validate the project json datafile */ +import { Platform } from './../platform_support'; import { JSONSchema4 } from 'json-schema'; + var schemaDefinition = { $schema: 'http://json-schema.org/draft-04/schema#', title: 'Project Config JSON Schema', @@ -316,3 +318,5 @@ var schemaDefinition = { const schema = schemaDefinition as JSONSchema4 export default schema + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/service.ts b/lib/service.ts index 3022aa806..a9f83f60c 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Platform } from './platform_support'; import { LoggerFacade, LogLevel, LogLevelToLower } from './logging/logger' import { resolvablePromise, ResolvablePromise } from "./utils/promise/resolvablePromise"; + export const SERVICE_FAILED_TO_START = '%s failed to start, reason: %s'; export const SERVICE_STOPPED_BEFORE_RUNNING = '%s stopped before running'; @@ -132,3 +134,5 @@ export abstract class BaseService implements Service { abstract stop(): void; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 1b450b1dd..f250404f4 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -20,6 +20,7 @@ */ // import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from './modules/logging'; +import { Platform } from './platform_support'; import { LoggerFacade, LogLevel } from './logging/logger'; import { ErrorHandler } from './error/error_handler'; @@ -47,6 +48,7 @@ import { OpaqueOdpManager } from './odp/odp_manager_factory'; import { OpaqueVuidManager } from './vuid/vuid_manager_factory'; import { CacheWithRemove } from './utils/cache/cache'; + export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; export { NotificationCenter } from './notification_center'; @@ -534,3 +536,5 @@ export { OdpEventManager, OdpManager, }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/attributes_validator/index.ts b/lib/utils/attributes_validator/index.ts index 08b50eb43..1a7950f8f 100644 --- a/lib/utils/attributes_validator/index.ts +++ b/lib/utils/attributes_validator/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { ObjectWithUnknownProperties } from '../../shared_types'; import fns from '../../utils/fns'; @@ -26,6 +27,7 @@ import { OptimizelyError } from '../../error/optimizly_error'; * @throws If the attributes are not valid */ + export function validate(attributes: unknown): boolean { if (typeof attributes === 'object' && !Array.isArray(attributes) && attributes !== null) { Object.keys(attributes).forEach(function(key) { @@ -53,3 +55,5 @@ export function isAttributeValid(attributeKey: unknown, attributeValue: unknown) (fns.isNumber(attributeValue) && fns.isSafeInteger(attributeValue))) ); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/cache/async_storage_cache.react_native.ts b/lib/utils/cache/async_storage_cache.react_native.ts index e5e76024e..544437364 100644 --- a/lib/utils/cache/async_storage_cache.react_native.ts +++ b/lib/utils/cache/async_storage_cache.react_native.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Maybe } from "../type"; import { AsyncStore } from "./store"; import { getDefaultAsyncStorage } from "../import.react_native/@react-native-async-storage/async-storage"; + export class AsyncStorageCache implements AsyncStore { public readonly operation = 'async'; private asyncStorage = getDefaultAsyncStorage(); @@ -48,3 +50,5 @@ export class AsyncStorageCache implements AsyncStore { return items.map(([key, value]) => value ? JSON.parse(value) : undefined); } } + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/utils/cache/cache.ts b/lib/utils/cache/cache.ts index 685b43a7b..c291e3d22 100644 --- a/lib/utils/cache/cache.ts +++ b/lib/utils/cache/cache.ts @@ -15,6 +15,10 @@ */ import { OpType, OpValue } from '../../utils/type'; import { Transformer } from '../../utils/type'; +import { Platform } from '../../platform_support'; + + + export interface OpCache { operation: OP; save(key: string, value: V): OpValue; @@ -68,3 +72,5 @@ export const transformCache = ( return transformedCache as CacheWithRemove; }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/cache/in_memory_lru_cache.ts b/lib/utils/cache/in_memory_lru_cache.ts index 6ed92d1fd..a61867c43 100644 --- a/lib/utils/cache/in_memory_lru_cache.ts +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Maybe } from "../type"; import { SyncCacheWithRemove } from "./cache"; + type CacheElement = { value: V; expiresAt?: number; @@ -72,3 +74,5 @@ export class InMemoryLruCache implements SyncCacheWithRemove { return Array.from(this.data.keys()); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/cache/local_storage_cache.browser.ts b/lib/utils/cache/local_storage_cache.browser.ts index b16d77571..32df3fea9 100644 --- a/lib/utils/cache/local_storage_cache.browser.ts +++ b/lib/utils/cache/local_storage_cache.browser.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Maybe } from "../type"; import { SyncStore } from "./store"; + export class LocalStorageCache implements SyncStore { public readonly operation = 'sync'; @@ -52,3 +54,5 @@ export class LocalStorageCache implements SyncStore { return keys.map((k) => this.get(k)); } } + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/utils/cache/store.ts b/lib/utils/cache/store.ts index c2df7bb66..9302c671d 100644 --- a/lib/utils/cache/store.ts +++ b/lib/utils/cache/store.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Transformer } from '../../utils/type'; import { Maybe } from '../../utils/type'; import { OpType, OpValue } from '../../utils/type'; + export interface OpStore { operation: OP; set(key: string, value: V): OpValue; @@ -174,3 +176,5 @@ export class AsyncPrefixStore implements AsyncStore { return values.map((value) => value ? this.transformGet(value) : undefined); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/cache/store_validator.ts b/lib/utils/cache/store_validator.ts index 949bb25c3..fa15c50ff 100644 --- a/lib/utils/cache/store_validator.ts +++ b/lib/utils/cache/store_validator.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2025, Optimizely @@ -14,6 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export const INVALID_STORE = 'Invalid store'; export const INVALID_STORE_METHOD = 'Invalid store method %s'; @@ -34,3 +37,5 @@ export const validateStore = (store: any): void => { throw new Error(errors.join(', ')); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/config_validator/index.ts b/lib/utils/config_validator/index.ts index 49c927f49..b5114034d 100644 --- a/lib/utils/config_validator/index.ts +++ b/lib/utils/config_validator/index.ts @@ -22,6 +22,8 @@ import { NO_DATAFILE_SPECIFIED, } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; +import { Platform } from '../../platform_support'; + const SUPPORTED_VERSIONS = [DATAFILE_VERSIONS.V2, DATAFILE_VERSIONS.V3, DATAFILE_VERSIONS.V4]; @@ -61,3 +63,5 @@ export const validateDatafile = function(datafile: unknown): any { export default { validateDatafile: validateDatafile, } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index 0364a34b1..d49aa2d34 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2016-2025, Optimizely * @@ -17,6 +19,7 @@ /** * Contains global enums used throughout the library */ + export const LOG_LEVEL = { NOTSET: 0, DEBUG: 1, @@ -106,3 +109,5 @@ export const DEFAULT_CMAB_CACHE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes export const DEFAULT_CMAB_CACHE_SIZE = 10_000; export const DEFAULT_CMAB_RETRIES = 1; export const DEFAULT_CMAB_BACKOFF_MS = 100; // 100 milliseconds + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/event_emitter/event_emitter.ts b/lib/utils/event_emitter/event_emitter.ts index 6bfa57f8d..a03caba60 100644 --- a/lib/utils/event_emitter/event_emitter.ts +++ b/lib/utils/event_emitter/event_emitter.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { Fn } from "../type"; + type Consumer = (arg: T) => void; type Listeners = { @@ -55,3 +57,5 @@ export class EventEmitter { this.listeners = {}; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/event_tag_utils/index.ts b/lib/utils/event_tag_utils/index.ts index d50292a39..e56a02f91 100644 --- a/lib/utils/event_tag_utils/index.ts +++ b/lib/utils/event_tag_utils/index.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Platform } from './../../platform_support'; import { FAILED_TO_PARSE_REVENUE, FAILED_TO_PARSE_VALUE, @@ -81,3 +83,5 @@ export function getEventValue(eventTags: EventTags, logger?: LoggerFacade): numb return null; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/event_tags_validator/index.ts b/lib/utils/event_tags_validator/index.ts index 421321f69..0f280d9fd 100644 --- a/lib/utils/event_tags_validator/index.ts +++ b/lib/utils/event_tags_validator/index.ts @@ -17,6 +17,7 @@ /** * Provides utility method for validating that event tags user has provided are valid */ +import { Platform } from './../../platform_support'; import { OptimizelyError } from '../../error/optimizly_error'; import { INVALID_EVENT_TAGS } from 'error_message'; @@ -26,6 +27,8 @@ import { INVALID_EVENT_TAGS } from 'error_message'; * @return {boolean} true if event tags are valid * @throws If event tags are not valid */ + + export function validate(eventTags: unknown): boolean { if (typeof eventTags === 'object' && !Array.isArray(eventTags) && eventTags !== null) { return true; @@ -33,3 +36,5 @@ export function validate(eventTags: unknown): boolean { throw new OptimizelyError(INVALID_EVENT_TAGS); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/executor/backoff_retry_runner.ts b/lib/utils/executor/backoff_retry_runner.ts index f0b185a99..033aa42d5 100644 --- a/lib/utils/executor/backoff_retry_runner.ts +++ b/lib/utils/executor/backoff_retry_runner.ts @@ -1,9 +1,11 @@ +import { Platform } from './../../platform_support'; import { OptimizelyError } from "../../error/optimizly_error"; import { RETRY_CANCELLED } from "error_message"; import { resolvablePromise, ResolvablePromise } from "../promise/resolvablePromise"; import { BackoffController } from "../repeater/repeater"; import { AsyncProducer, Fn } from "../type"; + export type RunResult = { result: Promise; cancelRetry: Fn; @@ -52,3 +54,5 @@ export const runWithRetry = ( runTask(task, returnPromise, cancelSignal, backoff, maxRetries); return { cancelRetry, result: returnPromise.promise }; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/executor/serial_runner.ts b/lib/utils/executor/serial_runner.ts index 243cae0b1..f2bf5b776 100644 --- a/lib/utils/executor/serial_runner.ts +++ b/lib/utils/executor/serial_runner.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { AsyncProducer } from "../type"; + class SerialRunner { private waitPromise: Promise = Promise.resolve(); @@ -34,3 +36,5 @@ class SerialRunner { } export { SerialRunner }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts index 5b07b3aad..da60ffaba 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { v4 } from 'uuid'; + const MAX_SAFE_INTEGER_LIMIT = Math.pow(2, 53); export function currentTimestamp(): number { @@ -130,3 +132,5 @@ export default { find, sprintf, }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/http_request_handler/http.ts b/lib/utils/http_request_handler/http.ts index ca7e63ae3..4c973b402 100644 --- a/lib/utils/http_request_handler/http.ts +++ b/lib/utils/http_request_handler/http.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2019-2020, 2022, 2024 Optimizely * @@ -17,6 +19,7 @@ /** * List of key-value pairs to be used in an HTTP requests */ + export interface Headers { [header: string]: string | undefined; } @@ -46,3 +49,5 @@ export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' export interface RequestHandler { makeRequest(requestUrl: string, headers: Headers, method: HttpMethod, data?: string): AbortableRequest; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/http_request_handler/http_util.ts b/lib/utils/http_request_handler/http_util.ts index c38217a40..e499bf092 100644 --- a/lib/utils/http_request_handler/http_util.ts +++ b/lib/utils/http_request_handler/http_util.ts @@ -1,4 +1,9 @@ +import { Platform } from './../../platform_support'; + + export const isSuccessStatusCode = (statusCode: number): boolean => { return statusCode >= 200 && statusCode < 400; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/http_request_handler/request_handler.browser.ts b/lib/utils/http_request_handler/request_handler.browser.ts index 340dcca33..4cff94858 100644 --- a/lib/utils/http_request_handler/request_handler.browser.ts +++ b/lib/utils/http_request_handler/request_handler.browser.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +// This implementation works in both browser and react_native environments + +import { Platform } from './../../platform_support'; import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { LoggerFacade, LogLevel } from '../../logging/logger'; import { REQUEST_TIMEOUT_MS } from '../enums'; @@ -130,3 +133,5 @@ export class BrowserRequestHandler implements RequestHandler { return headers; } } + +export const __platforms: Platform[] = ['browser', 'react_native']; diff --git a/lib/utils/http_request_handler/request_handler.node.ts b/lib/utils/http_request_handler/request_handler.node.ts index 520a8f3ed..21327548a 100644 --- a/lib/utils/http_request_handler/request_handler.node.ts +++ b/lib/utils/http_request_handler/request_handler.node.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import http from 'http'; import https from 'https'; import url from 'url'; @@ -26,6 +27,8 @@ import { OptimizelyError } from '../../error/optimizly_error'; /** * Handles sending requests and receiving responses over HTTP via NodeJS http module */ + + export class NodeRequestHandler implements RequestHandler { private readonly logger?: LoggerFacade; private readonly timeout: number; @@ -184,3 +187,5 @@ export class NodeRequestHandler implements RequestHandler { return { abort, responsePromise }; } } + +export const __platforms: Platform[] = ['node']; diff --git a/lib/utils/http_request_handler/request_handler_validator.ts b/lib/utils/http_request_handler/request_handler_validator.ts index a9df4cc7c..8e82f6e30 100644 --- a/lib/utils/http_request_handler/request_handler_validator.ts +++ b/lib/utils/http_request_handler/request_handler_validator.ts @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { RequestHandler } from './http'; + export const INVALID_REQUEST_HANDLER = 'Invalid request handler'; export const validateRequestHandler = (requestHandler: RequestHandler): void => { @@ -26,3 +28,5 @@ export const validateRequestHandler = (requestHandler: RequestHandler): void => throw new Error(INVALID_REQUEST_HANDLER); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/id_generator/index.ts b/lib/utils/id_generator/index.ts index 5f3c72387..508ee5af0 100644 --- a/lib/utils/id_generator/index.ts +++ b/lib/utils/id_generator/index.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2022-2024, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + const idSuffixBase = 10_000; export class IdGenerator { @@ -29,3 +32,5 @@ export class IdGenerator { return `${timestamp}${idSuffix}`; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts b/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts index 4a2fb77ed..399133028 100644 --- a/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts +++ b/lib/utils/import.react_native/@react-native-async-storage/async-storage.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { Platform } from './../../../platform_support'; import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage' + export const MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE = 'Module not found: @react-native-async-storage/async-storage'; export const getDefaultAsyncStorage = (): AsyncStorageStatic => { @@ -26,3 +28,5 @@ export const getDefaultAsyncStorage = (): AsyncStorageStatic => { throw new Error(MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE); } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/json_schema_validator/index.ts b/lib/utils/json_schema_validator/index.ts index 42fe19f11..243eb9bad 100644 --- a/lib/utils/json_schema_validator/index.ts +++ b/lib/utils/json_schema_validator/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { JSONSchema4, validate as jsonSchemaValidator } from 'json-schema'; import schema from '../../project_config/project_config_schema'; @@ -26,6 +27,8 @@ import { OptimizelyError } from '../../error/optimizly_error'; * @param {boolean} shouldThrowOnError Should validation throw if invalid JSON object * @return {boolean} true if the given object is valid; throws or false if invalid */ + + export function validate( jsonObject: unknown, validationSchema: JSONSchema4 = schema, @@ -52,3 +55,5 @@ export function validate( throw new OptimizelyError(INVALID_JSON); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/microtask/index.ts b/lib/utils/microtask/index.ts index 02e2c474e..57ae25999 100644 --- a/lib/utils/microtask/index.ts +++ b/lib/utils/microtask/index.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2024, Optimizely * @@ -16,6 +18,7 @@ type Callback = () => void; + export const scheduleMicrotask = (callback: Callback): void => { if (typeof queueMicrotask === 'function') { queueMicrotask(callback); @@ -23,3 +26,5 @@ export const scheduleMicrotask = (callback: Callback): void => { Promise.resolve().then(callback); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/promise/operation_value.ts b/lib/utils/promise/operation_value.ts index 7f7aa3779..0912a29dd 100644 --- a/lib/utils/promise/operation_value.ts +++ b/lib/utils/promise/operation_value.ts @@ -1,8 +1,10 @@ +import { Platform } from './../../platform_support'; import { PROMISE_NOT_ALLOWED } from '../../message/error_message'; import { OptimizelyError } from '../../error/optimizly_error'; import { OpType, OpValue } from '../type'; + const isPromise = (val: any): boolean => { return val && typeof val.then === 'function'; } @@ -48,3 +50,5 @@ export class Value { return new Value(op, Promise.resolve(val) as OpValue); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/promise/resolvablePromise.ts b/lib/utils/promise/resolvablePromise.ts index 354df2b7d..e998d1762 100644 --- a/lib/utils/promise/resolvablePromise.ts +++ b/lib/utils/promise/resolvablePromise.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2024, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + const noop = () => {}; export type ResolvablePromise = { @@ -32,3 +35,5 @@ export function resolvablePromise(): ResolvablePromise { }); return { promise, resolve, reject, then: promise.then.bind(promise) }; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/repeater/repeater.ts b/lib/utils/repeater/repeater.ts index 829702729..716194781 100644 --- a/lib/utils/repeater/repeater.ts +++ b/lib/utils/repeater/repeater.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Platform } from './../../platform_support'; import { AsyncTransformer } from "../type"; import { scheduleMicrotask } from "../microtask"; @@ -24,6 +25,8 @@ import { scheduleMicrotask } from "../microtask"; // If the retuned promise resolves, the repeater will assume the task succeeded, // and will reset the failure count. If the promise is rejected, the repeater will // assume the task failed and will increase the current consecutive failure count. + + export interface Repeater { // If immediateExecution is true, the first exection of // the task will be immediate but asynchronous. @@ -154,3 +157,5 @@ export class IntervalRepeater implements Repeater { this.task = task; } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/semantic_version/index.ts b/lib/utils/semantic_version/index.ts index 56fad06a5..dec9fc68a 100644 --- a/lib/utils/semantic_version/index.ts +++ b/lib/utils/semantic_version/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../../platform_support'; import { UNKNOWN_MATCH_TYPE } from 'error_message'; import { LoggerFacade } from '../../logging/logger'; import { VERSION_TYPE } from '../enums'; @@ -23,6 +24,8 @@ import { VERSION_TYPE } from '../enums'; * @return {boolean} true if the string is number only * */ + + function isNumber(content: string): boolean { return /^\d+$/.test(content); } @@ -180,3 +183,5 @@ export function compareVersion(conditionsVersion: string, userProvidedVersion: s return 0; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/string_value_validator/index.ts b/lib/utils/string_value_validator/index.ts index fd0ceb5f0..9d4940e20 100644 --- a/lib/utils/string_value_validator/index.ts +++ b/lib/utils/string_value_validator/index.ts @@ -1,3 +1,5 @@ +import { Platform } from './../../platform_support'; + /** * Copyright 2018, 2020, Optimizely * @@ -19,6 +21,9 @@ * @param {unknown} input * @return {boolean} true for non-empty string, false otherwise */ + export function validate(input: unknown): boolean { return typeof input === 'string' && input !== ''; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/type.ts b/lib/utils/type.ts index c60f85d60..7d572d1b4 100644 --- a/lib/utils/type.ts +++ b/lib/utils/type.ts @@ -1,3 +1,5 @@ +import { Platform } from './../platform_support'; + /** * Copyright 2024-2025, Optimizely * @@ -14,6 +16,7 @@ * limitations under the License. */ + export type Fn = () => unknown; export type AsyncFn = () => Promise; export type AsyncTransformer = (arg: A) => Promise; @@ -37,3 +40,5 @@ export type OrNull = T | null; export type Nullable = { [P in keyof T]: P extends K ? OrNull : T[P]; } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/utils/user_profile_service_validator/index.ts b/lib/utils/user_profile_service_validator/index.ts index 95e8cf61a..a1faf649c 100644 --- a/lib/utils/user_profile_service_validator/index.ts +++ b/lib/utils/user_profile_service_validator/index.ts @@ -18,6 +18,7 @@ * Provides utility method for validating that the given user profile service implementation is valid. */ +import { Platform } from './../../platform_support'; import { ObjectWithUnknownProperties } from '../../shared_types'; import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; @@ -30,6 +31,7 @@ import { OptimizelyError } from '../../error/optimizly_error'; * @throws If the instance is not valid */ + export function validate(userProfileServiceInstance: unknown): boolean { if (typeof userProfileServiceInstance === 'object' && userProfileServiceInstance !== null) { if (typeof (userProfileServiceInstance as ObjectWithUnknownProperties)['lookup'] !== 'function') { @@ -41,3 +43,5 @@ export function validate(userProfileServiceInstance: unknown): boolean { } throw new OptimizelyError(INVALID_USER_PROFILE_SERVICE, 'Not an object'); } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/vuid/vuid.ts b/lib/vuid/vuid.ts index d335c329d..f8974c88a 100644 --- a/lib/vuid/vuid.ts +++ b/lib/vuid/vuid.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { v4 as uuidV4 } from 'uuid'; + export const VUID_PREFIX = `vuid_`; export const VUID_MAX_LENGTH = 32; @@ -29,3 +31,5 @@ export const makeVuid = (): string => { return vuidFull.length <= VUID_MAX_LENGTH ? vuidFull : vuidFull.substring(0, VUID_MAX_LENGTH); }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts index dd0c0322a..396d123f6 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { LoggerFacade } from '../logging/logger'; import { Store } from '../utils/cache/store'; import { AsyncProducer, Maybe } from '../utils/type'; import { isVuid, makeVuid } from './vuid'; + export interface VuidManager { getVuid(): Maybe; isVuidEnabled(): boolean; @@ -130,3 +132,5 @@ export class DefaultVuidManager implements VuidManager { this.vuid = await this.vuidCacheManager.load(); } } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts index 0691fd5e7..dfb449ea4 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { @@ -26,3 +28,5 @@ export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidM enableVuid: options.enableVuid })); }; + +export const __platforms: Platform[] = ['browser']; diff --git a/lib/vuid/vuid_manager_factory.node.ts b/lib/vuid/vuid_manager_factory.node.ts index 439e70ec1..c43ebf8f5 100644 --- a/lib/vuid/vuid_manager_factory.node.ts +++ b/lib/vuid/vuid_manager_factory.node.ts @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { return wrapVuidManager(undefined); }; + +export const __platforms: Platform[] = ['node']; diff --git a/lib/vuid/vuid_manager_factory.react_native.ts b/lib/vuid/vuid_manager_factory.react_native.ts index 0aeb1c537..231b106fe 100644 --- a/lib/vuid/vuid_manager_factory.react_native.ts +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Platform } from './../platform_support'; import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; + export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidManager => { @@ -26,3 +28,5 @@ export const createVuidManager = (options: VuidManagerOptions = {}): OpaqueVuidM enableVuid: options.enableVuid })); }; + +export const __platforms: Platform[] = ['react_native']; diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts index f7f1b760f..a3263bb78 100644 --- a/lib/vuid/vuid_manager_factory.ts +++ b/lib/vuid/vuid_manager_factory.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { Platform } from './../platform_support'; import { Store } from '../utils/cache/store'; import { Maybe } from '../utils/type'; import { VuidManager } from './vuid_manager'; + export type VuidManagerOptions = { vuidCache?: Store; enableVuid?: boolean; @@ -42,3 +44,5 @@ export const wrapVuidManager = (vuidManager: Maybe): OpaqueVuidMana [vuidManagerSymbol]: vuidManager } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/package-lock.json b/package-lock.json index deb59a64d..82f14cc6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "uuid": "^10.0.0" }, "devDependencies": { + "@biomejs/biome": "^2.3.7", "@react-native-async-storage/async-storage": "^2", "@react-native-community/netinfo": "^11.3.2", "@rollup/plugin-commonjs": "^11.0.2", @@ -32,6 +33,7 @@ "coveralls-next": "^4.2.0", "eslint": "^8.21.0", "eslint-config-prettier": "^6.10.0", + "eslint-plugin-local-rules": "^3.0.2", "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^16.6.0", "jiti": "^2.4.1", @@ -42,6 +44,7 @@ "karma-mocha": "^2.0.1", "karma-webpack": "^5.0.1", "lodash": "^4.17.11", + "minimatch": "^9.0.5", "mocha": "^10.2.0", "mocha-lcov-reporter": "^1.3.0", "nise": "^1.4.10", @@ -2484,6 +2487,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.7.tgz", + "integrity": "sha512-CTbAS/jNAiUc6rcq94BrTB8z83O9+BsgWj2sBCQg9rD6Wkh2gjfR87usjx0Ncx0zGXP1NKgT7JNglay5Zfs9jw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.7", + "@biomejs/cli-darwin-x64": "2.3.7", + "@biomejs/cli-linux-arm64": "2.3.7", + "@biomejs/cli-linux-arm64-musl": "2.3.7", + "@biomejs/cli-linux-x64": "2.3.7", + "@biomejs/cli-linux-x64-musl": "2.3.7", + "@biomejs/cli-win32-arm64": "2.3.7", + "@biomejs/cli-win32-x64": "2.3.7" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.7.tgz", + "integrity": "sha512-LirkamEwzIUULhXcf2D5b+NatXKeqhOwilM+5eRkbrnr6daKz9rsBL0kNZ16Hcy4b8RFq22SG4tcLwM+yx/wFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.7.tgz", + "integrity": "sha512-Q4TO633kvrMQkKIV7wmf8HXwF0dhdTD9S458LGE24TYgBjSRbuhvio4D5eOQzirEYg6eqxfs53ga/rbdd8nBKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.7.tgz", + "integrity": "sha512-inHOTdlstUBzgjDcx0ge71U4SVTbwAljmkfi3MC5WzsYCRhancqfeL+sa4Ke6v2ND53WIwCFD5hGsYExoI3EZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.7.tgz", + "integrity": "sha512-/afy8lto4CB8scWfMdt+NoCZtatBUF62Tk3ilWH2w8ENd5spLhM77zKlFZEvsKJv9AFNHknMl03zO67CiklL2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.7.tgz", + "integrity": "sha512-fJMc3ZEuo/NaMYo5rvoWjdSS5/uVSW+HPRQujucpZqm2ZCq71b8MKJ9U4th9yrv2L5+5NjPF0nqqILCl8HY/fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.7.tgz", + "integrity": "sha512-CQUtgH1tIN6e5wiYSJqzSwJumHYolNtaj1dwZGCnZXm2PZU1jOJof9TsyiP3bXNDb+VOR7oo7ZvY01If0W3iFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.7.tgz", + "integrity": "sha512-aJAE8eCNyRpcfx2JJAtsPtISnELJ0H4xVVSwnxm13bzI8RwbXMyVtxy2r5DV1xT3WiSP+7LxORcApWw0LM8HiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.7.tgz", + "integrity": "sha512-pulzUshqv9Ed//MiE8MOUeeEkbkSHVDVY5Cz5wVAnH1DUqliCQG3j6s1POaITTFqFfo7AVIx2sWdKpx/GS+Nqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2949,6 +3115,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.49.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", @@ -2990,6 +3169,19 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -4857,15 +5049,6 @@ "vitest": "2.1.9" } }, - "node_modules/@vitest/coverage-istanbul/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@vitest/coverage-istanbul/node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -4950,21 +5133,6 @@ "node": ">=10" } }, - "node_modules/@vitest/coverage-istanbul/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@vitest/coverage-istanbul/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7307,6 +7475,13 @@ "eslint": ">=3.14.1" } }, + "node_modules/eslint-plugin-local-rules": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-local-rules/-/eslint-plugin-local-rules-3.0.2.tgz", + "integrity": "sha512-IWME7GIYHXogTkFsToLdBCQVJ0U4kbSuVyDT+nKoR4UgtnVrrVeNWuAZkdEu1nxkvi9nsPccGehEEF6dgA28IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-prettier": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", @@ -7408,6 +7583,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -8158,6 +8346,19 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "13.22.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", @@ -9280,30 +9481,6 @@ "webpack": "^5.0.0" } }, - "node_modules/karma-webpack/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/karma-webpack/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -9315,6 +9492,19 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/karma/node_modules/ua-parser-js": { "version": "0.7.38", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", @@ -10401,15 +10591,29 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/minimist": { @@ -10755,6 +10959,20 @@ "node": ">= 0.10.5" } }, + "node_modules/node-dir/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -13426,6 +13644,19 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-encoding": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", diff --git a/package.json b/package.json index cc543dd1b..1014756ae 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,9 @@ "clean": "rm -rf dist", "clean:win": "(if exist dist rd /s/q dist)", "lint": "tsc --noEmit && eslint 'lib/**/*.js' 'lib/**/*.ts'", + "validate-platform-isolation": "node scripts/validate-platform-isolation-ts.js", + "test-platform-isolation": "node scripts/test-validator.js", + "add-platform-exports": "node scripts/add-platform-exports.js", "test-vitest": "vitest run", "test-mocha": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register -r tsconfig-paths/register -r lib/tests/exit_on_unhandled_rejection.js 'lib/**/*.tests.ts' 'lib/**/*.tests.js'", "test": "npm run test-mocha && npm run test-vitest", @@ -66,7 +69,7 @@ "test-umdbrowser": "npm run build-browser-umd && karma start karma.umd.conf.js --single-run", "test-karma-local": "karma start karma.local_chrome.bs.conf.js && npm run build-browser-umd && karma start karma.local_chrome.umd.conf.js", "prebuild": "npm run clean", - "build": "tsc --noEmit && npm run genmsg && rollup -c && cp dist/index.browser.d.ts dist/index.d.ts", + "build": "npm run validate-platform-isolation && tsc --noEmit && npm run genmsg && rollup -c && cp dist/index.browser.d.ts dist/index.d.ts", "build:win": "tsc --noEmit && npm run genmsg && rollup -c && type nul > dist/optimizely.lite.es.d.ts && type nul > dist/optimizely.lite.es.min.d.ts && type nul > dist/optimizely.lite.min.d.ts", "build-browser-umd": "rollup -c --config-umd", "coveralls": "nyc --reporter=lcov npm test", @@ -97,6 +100,7 @@ "uuid": "^10.0.0" }, "devDependencies": { + "@biomejs/biome": "^2.3.7", "@react-native-async-storage/async-storage": "^2", "@react-native-community/netinfo": "^11.3.2", "@rollup/plugin-commonjs": "^11.0.2", @@ -114,6 +118,7 @@ "coveralls-next": "^4.2.0", "eslint": "^8.21.0", "eslint-config-prettier": "^6.10.0", + "eslint-plugin-local-rules": "^3.0.2", "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^16.6.0", "jiti": "^2.4.1", @@ -124,6 +129,7 @@ "karma-mocha": "^2.0.1", "karma-webpack": "^5.0.1", "lodash": "^4.17.11", + "minimatch": "^9.0.5", "mocha": "^10.2.0", "mocha-lcov-reporter": "^1.3.0", "nise": "^1.4.10", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..cd2758e71 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,68 @@ +# Scripts + +This directory contains build and validation scripts for the JavaScript SDK. + +## validate-platform-isolation-ts.js + +The main platform isolation validator that ensures platform-specific code is properly isolated to prevent runtime errors when building for different platforms (Browser, Node.js, React Native). + +**Configuration:** File patterns to include/exclude are configured in `.platform-isolation.config.js` at the workspace root. + +### Usage + +```bash +# Run manually +node scripts/validate-platform-isolation-ts.js + +# Run via npm script +npm run validate-platform-isolation + +# Runs automatically during build +npm run build +``` + +### How It Works + +The script: +1. Scans all TypeScript/JavaScript files in the `lib/` directory +2. **Requires every non-test file to export `__platforms` array** declaring supported platforms +3. **Validates platform values** by reading the Platform type definition from `platform_support.ts` +4. Parses import statements (ES6 imports, require(), dynamic imports) using TypeScript AST +5. **Validates import compatibility**: For each import, ensures the imported file supports ALL platforms that the importing file runs on +6. Fails with exit code 1 if any violations are found, if `__platforms` export is missing, or if invalid platform values are used + +**Import Rule**: When file A imports file B, file B must support ALL platforms that file A runs on. +- Example: A universal file can only import from universal files (or files supporting all platforms) +- Example: A browser file can import from universal files or any file that supports browser + +**Note:** The validator can theoretically support file naming conventions (`.browser.ts`, etc.) in addition to `__platforms` exports, but currently enforces only the `__platforms` export. File naming is not validated and is considered redundant. + +### Exit Codes + +- `0`: All platform-specific files are properly isolated +- `1`: Violations found or script error + +## test-validator.js + +Comprehensive test suite for the platform isolation validator. Documents and validates all compatibility rules. + +### Usage + +```bash +# Run via npm script +npm run test-platform-isolation + +# Or run directly +node scripts/test-validator.js +``` + +Tests cover: +- Universal imports (always compatible) +- Single platform file imports +- Single platform importing from multi-platform files +- Multi-platform file imports (strictest rules) +- `__platforms` extraction + +--- + +See [../docs/PLATFORM_ISOLATION.md](../docs/PLATFORM_ISOLATION.md) for detailed documentation on platform isolation rules. diff --git a/scripts/add-platform-exports.js b/scripts/add-platform-exports.js new file mode 100644 index 000000000..347e7c262 --- /dev/null +++ b/scripts/add-platform-exports.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node + +/** + * Auto-add __platforms to files + * + * This script automatically adds __platforms export to files that don't have it. + * Uses TypeScript parser to analyze files and add proper type annotations. + * + * Strategy: + * 1. Files with platform-specific naming (.browser.ts, .node.ts, .react_native.ts) get their specific platform(s) + * 2. All other files are assumed to be universal and get ['__universal__'] + * 3. Adds Platform type import and type annotation + * 4. Inserts __platforms export at the end of the file + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { minimatch } = require('minimatch'); +const { extractPlatformsFromAST } = require('./platform-utils'); + +const WORKSPACE_ROOT = path.join(__dirname, '..'); +const PLATFORMS = ['browser', 'node', 'react_native']; + +// Load configuration +const configPath = path.join(WORKSPACE_ROOT, '.platform-isolation.config.js'); +const config = fs.existsSync(configPath) + ? require(configPath) + : { + include: ['lib/**/*.ts', 'lib/**/*.js'], + exclude: [ + '**/*.spec.ts', '**/*.test.ts', '**/*.tests.ts', + '**/*.test.js', '**/*.spec.js', '**/*.tests.js', + '**/*.umdtests.js', '**/*.test-d.ts', '**/*.gen.ts', + '**/*.d.ts', '**/__mocks__/**', '**/tests/**' + ] + }; + +function getPlatformFromFilename(filename) { + const platforms = []; + for (const platform of PLATFORMS) { + if (filename.includes(`.${platform}.`)) { + platforms.push(platform); + } + } + return platforms.length > 0 ? platforms : null; +} + +/** + * Check if file matches any pattern using minimatch + */ +function matchesPattern(filePath, patterns) { + const relativePath = path.relative(WORKSPACE_ROOT, filePath).replace(/\\/g, '/'); + + return patterns.some(pattern => minimatch(relativePath, pattern)); +} + +/** + * Calculate relative import path for Platform type + */ +function getRelativeImportPath(filePath) { + const platformSupportPath = path.join(WORKSPACE_ROOT, 'lib', 'platform_support.ts'); + const fileDir = path.dirname(filePath); + let relativePath = path.relative(fileDir, platformSupportPath); + + // Normalize to forward slashes and remove .ts extension + relativePath = relativePath.replace(/\\/g, '/').replace(/\.ts$/, ''); + + // Ensure it starts with ./ + if (!relativePath.startsWith('.')) { + relativePath = './' + relativePath; + } + + return relativePath; +} + +/** + * Find or add Platform import in the file + * Returns the updated content and whether import was added + */ +function ensurePlatformImport(content, filePath) { + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + // Check if Platform import already exists + let hasPlatformImport = false; + let lastImportEnd = 0; + + function visit(node) { + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + // Check if this import is from platform_support + if (moduleSpecifier.text.includes('platform_support')) { + // Check if it imports Platform type + if (node.importClause && node.importClause.namedBindings) { + const namedBindings = node.importClause.namedBindings; + if (ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + if (element.name.text === 'Platform') { + hasPlatformImport = true; + break; + } + } + } + } + } + } + lastImportEnd = node.end; + } + } + + ts.forEachChild(sourceFile, visit); + + if (hasPlatformImport) { + return { content, added: false }; + } + + // Add Platform import + const importPath = getRelativeImportPath(filePath); + const importStatement = `import type { Platform } from '${importPath}';\n`; + + if (lastImportEnd > 0) { + // Add after last import + const lines = content.split('\n'); + let insertLine = 0; + let currentPos = 0; + + for (let i = 0; i < lines.length; i++) { + currentPos += lines[i].length + 1; // +1 for newline + if (currentPos >= lastImportEnd) { + insertLine = i + 1; + break; + } + } + + lines.splice(insertLine, 0, importStatement.trim()); + return { content: lines.join('\n'), added: true }; + } else { + // Add at the beginning (after shebang/comments if any) + const lines = content.split('\n'); + let insertLine = 0; + + // Skip shebang and leading comments + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (trimmed.startsWith('#!') || trimmed.startsWith('//') || + trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed === '') { + insertLine = i + 1; + } else { + break; + } + } + + lines.splice(insertLine, 0, importStatement.trim(), ''); + return { content: lines.join('\n'), added: true }; + } +} + +/** + * Remove existing __platforms export from the content + */ +function removeExistingPlatformExport(content, filePath) { + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + const lines = content.split('\n'); + const linesToRemove = new Set(); + + function visit(node) { + if (ts.isVariableStatement(node)) { + const hasExport = node.modifiers?.some( + mod => mod.kind === ts.SyntaxKind.ExportKeyword + ); + + if (hasExport) { + for (const declaration of node.declarationList.declarations) { + if (ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.name.text === '__platforms') { + // Mark this line for removal + const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + + for (let i = startLine; i <= endLine; i++) { + linesToRemove.add(i); + } + } + } + } + } + } + + ts.forEachChild(sourceFile, visit); + + if (linesToRemove.size === 0) { + return { content, removed: false }; + } + + const filteredLines = lines.filter((_, index) => !linesToRemove.has(index)); + return { content: filteredLines.join('\n'), removed: true }; +} + +/** + * Add __platforms export at the end of the file + */ +function addPlatformExport(content, platforms) { + const platformsStr = platforms.map(p => `'${p}'`).join(', '); + const exportStatement = `\n\nexport const __platforms: Platform[] = [${platformsStr}];\n`; + + // Trim trailing whitespace and ensure we end with the export (with blank line before) + return content.trimEnd() + exportStatement; +} + +/** + * Process a single file + */ +function processFile(filePath) { + let content = fs.readFileSync(filePath, 'utf-8'); + + // Use TypeScript parser to check for existing __platforms + const existingPlatforms = extractPlatformsFromAST( + ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true) + ); + + // Determine platforms for this file + // If file already has platforms, use those (preserve existing values) + // Otherwise, determine from filename or default to universal + let platforms; + if (existingPlatforms === null) { + // No __platforms export, determine from filename + const platformsFromFilename = getPlatformFromFilename(filePath); + platforms = platformsFromFilename || ['__universal__']; + } else if (Array.isArray(existingPlatforms)) { + // Has valid __platforms, preserve the existing values + platforms = existingPlatforms; + } else { + // Has issues (NOT_CONST, NOT_LITERALS), determine from filename + const platformsFromFilename = getPlatformFromFilename(filePath); + platforms = platformsFromFilename || ['__universal__']; + } + + let modified = false; + let action = 'skipped'; + + if (existingPlatforms === null) { + // No __platforms export, add it + action = 'added'; + modified = true; + } else if (Array.isArray(existingPlatforms)) { + // Has __platforms but might need to be moved or updated + // Remove existing and re-add at the end + const removed = removeExistingPlatformExport(content, filePath); + if (removed.removed) { + content = removed.content; + action = 'moved'; + modified = true; + } else { + return { skipped: true, reason: 'already has export at end' }; + } + } else { + // Has issues (NOT_CONST, NOT_LITERALS), fix them + const removed = removeExistingPlatformExport(content, filePath); + content = removed.content; + action = 'fixed'; + modified = true; + } + + if (modified) { + // Ensure Platform import exists + const importResult = ensurePlatformImport(content, filePath); + content = importResult.content; + + // Add __platforms export at the end + content = addPlatformExport(content, platforms); + + // Write back to file + fs.writeFileSync(filePath, content, 'utf-8'); + + return { skipped: false, action, platforms, addedImport: importResult.added }; + } + + return { skipped: true, reason: 'no changes needed' }; +} + +/** + * Recursively find all files matching include patterns and not matching exclude patterns + */ +function findSourceFiles(dir, files = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Check if this directory path could potentially contain files matching include patterns + // Use minimatch with partial mode to test if pattern could match files under this directory + const relativePath = path.relative(WORKSPACE_ROOT, fullPath).replace(/\\/g, '/'); + const couldMatch = config.include.some(pattern => { + return minimatch(relativePath, pattern, { partial: true }); + }); + + if (couldMatch) { + findSourceFiles(fullPath, files); + } + } else if (entry.isFile()) { + // Check if file matches include patterns + if (matchesPattern(fullPath, config.include)) { + // Check if file is NOT excluded + if (!matchesPattern(fullPath, config.exclude)) { + files.push(fullPath); + } + } + } + } + + return files; +} + +function main() { + console.log('🔧 Processing __platforms exports...\n'); + console.log(`📋 Configuration: ${path.relative(WORKSPACE_ROOT, configPath) || '.platform-isolation.config.js'}\n`); + + const files = findSourceFiles(WORKSPACE_ROOT); + let added = 0; + let moved = 0; + let fixed = 0; + let skipped = 0; + + for (const file of files) { + const result = processFile(file); + const relativePath = path.relative(process.cwd(), file); + + if (result.skipped) { + skipped++; + } else { + switch (result.action) { + case 'added': + added++; + console.log(`➕ ${relativePath} → [${result.platforms.join(', ')}]${result.addedImport ? ' (added import)' : ''}`); + break; + case 'moved': + moved++; + console.log(`📍 ${relativePath} → moved to end [${result.platforms.join(', ')}]${result.addedImport ? ' (added import)' : ''}`); + break; + case 'fixed': + fixed++; + console.log(`🔧 ${relativePath} → fixed [${result.platforms.join(', ')}]${result.addedImport ? ' (added import)' : ''}`); + break; + } + } + } + + console.log(`\n📊 Summary:`); + console.log(` Added: ${added} files`); + console.log(` Moved to end: ${moved} files`); + console.log(` Fixed: ${fixed} files`); + console.log(` Skipped: ${skipped} files`); + console.log(` Total: ${files.length} files\n`); + + console.log('✅ Done! Run npm run validate-platform-isolation to verify.\n'); +} + +if (require.main === module) { + main(); +} + +module.exports = { processFile }; diff --git a/scripts/platform-utils.js b/scripts/platform-utils.js new file mode 100644 index 000000000..7ce90b39a --- /dev/null +++ b/scripts/platform-utils.js @@ -0,0 +1,189 @@ +/** + * Platform Utilities + * + * Shared utilities for platform isolation validation used by both + * the validation script and ESLint rule. + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +// Cache for valid platforms +let validPlatformsCache = null; + +/** + * Extract valid platform values from Platform type definition in platform_support.ts + * Parses: type Platform = 'browser' | 'node' | 'react_native' | '__universal__'; + * + * @param {string} workspaceRoot - The root directory of the workspace + * @returns {string[]} Array of valid platform identifiers + */ +function getValidPlatforms(workspaceRoot) { + if (validPlatformsCache) { + return validPlatformsCache; + } + + try { + const platformSupportPath = path.join(workspaceRoot, 'lib', 'platform_support.ts'); + + if (!fs.existsSync(platformSupportPath)) { + throw new Error(`platform_support.ts not found at ${platformSupportPath}`); + } + + const content = fs.readFileSync(platformSupportPath, 'utf8'); + const sourceFile = ts.createSourceFile( + platformSupportPath, + content, + ts.ScriptTarget.Latest, + true + ); + + const platforms = []; + + // Visit all nodes in the AST + function visit(node) { + // Look for: export type Platform = 'browser' | 'node' | ... + if (ts.isTypeAliasDeclaration(node) && + node.name.text === 'Platform' && + node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + + // Parse the union type + if (ts.isUnionTypeNode(node.type)) { + for (const type of node.type.types) { + if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) { + platforms.push(type.literal.text); + } + } + } + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + + if (platforms.length > 0) { + validPlatformsCache = platforms; + return validPlatformsCache; + } + } catch (error) { + console.warn('Could not parse platform_support.ts, using fallback values:', error.message); + } + + // Fallback to default platforms + validPlatformsCache = ['browser', 'node', 'react_native', '__universal__']; + return validPlatformsCache; +} + +/** + * Extracts __platforms array from TypeScript AST + * + * Returns: + * - string[] if valid platforms array found + * - 'NOT_CONST' if __platforms is not declared as const + * - 'NOT_LITERALS' if array contains non-literal values + * - null if __platforms export not found + * + * @param {ts.SourceFile} sourceFile - TypeScript source file AST + * @returns {string[] | 'NOT_CONST' | 'NOT_LITERALS' | null} + */ +function extractPlatformsFromAST(sourceFile) { + let platforms = null; + let hasNonStringLiteral = false; + let isNotConst = false; + + function visit(node) { + // Look for: export const __platforms = [...] + if (ts.isVariableStatement(node)) { + // Check if it has export modifier + const hasExport = node.modifiers?.some( + mod => mod.kind === ts.SyntaxKind.ExportKeyword + ); + + if (hasExport) { + // Check if declaration is const + const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0; + + for (const declaration of node.declarationList.declarations) { + if (ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.name.text === '__platforms') { + + if (!isConst) { + isNotConst = true; + } + + let initializer = declaration.initializer; + + // Handle "as const" assertion: [...] as const + if (initializer && ts.isAsExpression(initializer)) { + initializer = initializer.expression; + } + + // Handle type assertion: [...] + if (initializer && ts.isTypeAssertionExpression(initializer)) { + initializer = initializer.expression; + } + + // Extract array elements + if (initializer && ts.isArrayLiteralExpression(initializer)) { + platforms = []; + for (const element of initializer.elements) { + if (ts.isStringLiteral(element)) { + platforms.push(element.text); + } else { + // Non-string literal found (variable, computed value, etc.) + hasNonStringLiteral = true; + } + } + return; // Found it, stop visiting + } + } + } + } + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + + if (platforms !== null) { + if (isNotConst) { + return 'NOT_CONST'; + } + if (hasNonStringLiteral) { + return 'NOT_LITERALS'; + } + } + + return platforms; +} + +/** + * Extract platforms from a file path + * + * @param {string} filePath - Absolute path to the file + * @returns {string[] | 'NOT_CONST' | 'NOT_LITERALS' | null} + */ +function extractPlatformsFromFile(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + return extractPlatformsFromAST(sourceFile); + } catch (error) { + return null; + } +} + +module.exports = { + getValidPlatforms, + extractPlatformsFromAST, + extractPlatformsFromFile, +}; diff --git a/scripts/test-validator.js b/scripts/test-validator.js new file mode 100644 index 000000000..e0e0ccfe1 --- /dev/null +++ b/scripts/test-validator.js @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +/** + * Comprehensive test suite for platform isolation validator + * + * This test documents and validates all the compatibility rules + */ + +const assert = require('assert'); +const validator = require('./validate-platform-isolation.js'); + +let passed = 0; +let failed = 0; + +function test(description, actual, expected) { + try { + assert.strictEqual(actual, expected); + console.log(`✅ ${description}`); + passed++; + } catch (e) { + console.log(`❌ ${description}`); + console.log(` Expected: ${expected}, Got: ${actual}`); + failed++; + } +} + +console.log('Platform Isolation Validator - Comprehensive Test Suite\n'); +console.log('=' .repeat(70)); + +console.log('\n1. UNIVERSAL IMPORTS (always compatible)'); +console.log('-'.repeat(70)); +test('Browser file can import universal', + validator.isPlatformCompatible(['browser'], ['__universal__']), true); +test('Node file can import universal', + validator.isPlatformCompatible(['node'], ['__universal__']), true); +test('Multi-platform file can import universal', + validator.isPlatformCompatible(['browser', 'react_native'], ['__universal__']), true); + +console.log('\n2. SINGLE PLATFORM FILES'); +console.log('-'.repeat(70)); +test('Browser file can import from browser file', + validator.isPlatformCompatible(['browser'], ['browser']), true); +test('Browser file CANNOT import from node file', + validator.isPlatformCompatible(['browser'], ['node']), false); +test('Node file can import from node file', + validator.isPlatformCompatible(['node'], ['node']), true); +test('React Native file can import from react_native file', + validator.isPlatformCompatible(['react_native'], ['react_native']), true); + +console.log('\n3. SINGLE PLATFORM IMPORTING FROM MULTI-PLATFORM'); +console.log('-'.repeat(70)); +test('Browser file CAN import from [browser, react_native] file', + validator.isPlatformCompatible(['browser'], ['browser', 'react_native']), true); +test('React Native file CAN import from [browser, react_native] file', + validator.isPlatformCompatible(['react_native'], ['browser', 'react_native']), true); +test('Node file CANNOT import from [browser, react_native] file', + validator.isPlatformCompatible(['node'], ['browser', 'react_native']), false); + +console.log('\n4. MULTI-PLATFORM FILES (strictest rules)'); +console.log('-'.repeat(70)); +test('[browser, react_native] file CAN import from [browser, react_native] file', + validator.isPlatformCompatible(['browser', 'react_native'], ['browser', 'react_native']), true); +test('[browser, react_native] file CANNOT import from browser-only file', + validator.isPlatformCompatible(['browser', 'react_native'], 'browser'), false); +test('[browser, react_native] file CANNOT import from react_native-only file', + validator.isPlatformCompatible(['browser', 'react_native'], 'react_native'), false); +test('[browser, react_native] file CANNOT import from node file', + validator.isPlatformCompatible(['browser', 'react_native'], 'node'), false); + +console.log('\n5. SUPPORTED PLATFORMS EXTRACTION'); +console.log('-'.repeat(70)); +const testExport1 = `export const __platforms = ['browser', 'react_native'];`; +const platforms1 = validator.extractSupportedPlatforms(testExport1); +test('Extract __platforms array', + JSON.stringify(platforms1), JSON.stringify(['browser', 'react_native'])); + +const testExport2 = `export const __platforms: string[] = ["browser", "node"];`; +const platforms2 = validator.extractSupportedPlatforms(testExport2); +test('Extract __platforms with type annotation', + JSON.stringify(platforms2), JSON.stringify(['browser', 'node'])); + +const testExport3 = `export const __platforms = ['__universal__'];`; +const platforms3 = validator.extractSupportedPlatforms(testExport3); +test('Extract __universal__ marker', + JSON.stringify(platforms3), JSON.stringify(['__universal__'])); + +console.log('\n' + '='.repeat(70)); +console.log(`\nResults: ${passed} passed, ${failed} failed`); + +if (failed > 0) { + process.exit(1); +} + +console.log('\n✅ All tests passed!'); diff --git a/scripts/validate-platform-isolation-ts.js b/scripts/validate-platform-isolation-ts.js new file mode 100644 index 000000000..9019c5f76 --- /dev/null +++ b/scripts/validate-platform-isolation-ts.js @@ -0,0 +1,651 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/no-var-requires */ + +/** + * Platform Isolation Validator + * + * This script ensures that platform-specific entry points only import + * from universal or compatible platform files. + * + * Platform Detection: + * - ALL source files (except tests) MUST export __platforms array + * - Universal files use: export const __platforms = ['__universal__']; + * - Platform-specific files use: export const __platforms = ['browser', 'node']; + * - Valid platform values are dynamically read from Platform type in platform_support.ts + * + * Rules: + * - Platform-specific files can only import from: + * - Universal files (containing '__universal__' or all concrete platform values) + * - Files supporting the same platforms + * - External packages (node_modules) + * + * Usage: node scripts/validate-platform-isolation-ts.js + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { minimatch } = require('minimatch'); +const { getValidPlatforms, extractPlatformsFromAST } = require('./platform-utils'); + +const WORKSPACE_ROOT = path.join(__dirname, '..'); + +// Load configuration +const configPath = path.join(WORKSPACE_ROOT, '.platform-isolation.config.js'); +const config = fs.existsSync(configPath) + ? require(configPath) + : { + include: ['lib/**/*.ts', 'lib/**/*.js'], + exclude: [ + '**/*.spec.ts', '**/*.test.ts', '**/*.tests.ts', + '**/*.test.js', '**/*.spec.js', '**/*.tests.js', + '**/*.umdtests.js', '**/*.test-d.ts', '**/*.gen.ts', + '**/*.d.ts', '**/__mocks__/**', '**/tests/**' + ] + }; + +// Cache for __platforms exports +const platformCache = new Map(); + +// Valid platforms (loaded dynamically) +let VALID_PLATFORMS = null; +let ALL_CONCRETE_PLATFORMS = null; + +/** + * Get valid platforms from source + */ +function getValidPlatformsFromSource() { + if (VALID_PLATFORMS !== null) { + return VALID_PLATFORMS; + } + + VALID_PLATFORMS = getValidPlatforms(WORKSPACE_ROOT); + ALL_CONCRETE_PLATFORMS = VALID_PLATFORMS.filter(p => p !== '__universal__'); + return VALID_PLATFORMS; +} + +/** + * Gets a human-readable platform name + */ +function getPlatformFromFilename(filename) { + const validPlatforms = getValidPlatformsFromSource(); + const concretePlatforms = validPlatforms.filter(p => p !== '__universal__'); + + for (const platform of concretePlatforms) { + if (filename.includes(`.${platform}.`)) { + return platform; + } + } + return null; +} + +// Track files missing __platforms export +const filesWithoutExport = []; + +// Track files with invalid platform values +const filesWithInvalidPlatforms = []; + +// Track files with non-const declaration +const filesWithNonConst = []; + +// Track files with non-literal values +const filesWithNonLiterals = []; + +/** + * Validates that platform values are valid according to Platform type + */ +function validatePlatformValues(platforms, filePath) { + if (!platforms || platforms.length === 0) { + return { valid: false, invalidValues: [] }; + } + + const validPlatforms = getValidPlatformsFromSource(); + const invalidValues = []; + + for (const platform of platforms) { + if (!validPlatforms.includes(platform)) { + invalidValues.push(platform); + } + } + + if (invalidValues.length > 0) { + filesWithInvalidPlatforms.push({ + filePath, + platforms, + invalidValues + }); + return { valid: false, invalidValues }; + } + + return { valid: true, invalidValues: [] }; +} + +/** + * Gets the supported platforms for a file (with caching) + * Returns: + * - string[] (platforms from __platforms) + * - 'MISSING' (file is missing __platforms export) + * + * Note: ALL files must have __platforms export + */ +function getSupportedPlatforms(filePath) { + // Check cache first + if (platformCache.has(filePath)) { + return platformCache.get(filePath); + } + + let result; + + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Parse with TypeScript + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + // Extract platforms from AST + const supportedPlatforms = extractPlatformsFromAST(sourceFile); + + if (supportedPlatforms === 'NOT_CONST') { + filesWithNonConst.push(filePath); + result = 'NOT_CONST'; + platformCache.set(filePath, result); + return result; + } + + if (supportedPlatforms === 'NOT_LITERALS') { + filesWithNonLiterals.push(filePath); + result = 'NOT_LITERALS'; + platformCache.set(filePath, result); + return result; + } + + if (supportedPlatforms && supportedPlatforms.length > 0) { + // Validate platform values + const validation = validatePlatformValues(supportedPlatforms, filePath); + if (!validation.valid) { + // Still cache it but it will be reported as error + result = supportedPlatforms; + platformCache.set(filePath, result); + return result; + } + + result = supportedPlatforms; + platformCache.set(filePath, result); + return result; + } + + // File exists but missing __platforms export + result = 'MISSING'; + platformCache.set(filePath, result); + filesWithoutExport.push(filePath); + return result; + + } catch (error) { + // If file doesn't exist or can't be read, return MISSING + result = 'MISSING'; + platformCache.set(filePath, result); + return result; + } +} + +/** + * Gets a human-readable platform name + */ +function getPlatformName(platform) { + const names = { + 'browser': 'Browser', + 'node': 'Node.js', + 'react_native': 'React Native' + }; + return names[platform] || platform; +} + +/** + * Formats platform info for display + */ +function formatPlatforms(platforms) { + if (platforms === 'MISSING') return 'MISSING __platforms'; + if (!platforms || platforms.length === 0) return 'Unknown'; + if (Array.isArray(platforms) && platforms.length === 1 && platforms[0] === '__universal__') return 'Universal (all platforms)'; + if (typeof platforms === 'string') return getPlatformName(platforms); + return platforms.map(p => getPlatformName(p)).join(' + '); +} + +/** + * Checks if platforms represent universal (all platforms) + * + * A file is universal if and only if: + * 1. It contains '__universal__' in its platforms array + * + * Note: If array contains '__universal__' plus other values (e.g., ['__universal__', 'browser']), + * it's still considered universal because __universal__ makes it available everywhere. + * + * Files that list all concrete platforms (e.g., ['browser', 'node', 'react_native']) are NOT + * considered universal - they must explicitly declare '__universal__' to be universal. + */ +function isUniversal(platforms) { + if (!Array.isArray(platforms) || platforms.length === 0) { + return false; + } + + // ONLY if it explicitly declares __universal__, it's universal + return platforms.includes('__universal__'); +} + +/** + * Checks if a platform is compatible with target platforms + * + * Rules: + * - If either file is MISSING __platforms, not compatible + * - Import must support ALL platforms that the importing file runs on + * - Universal imports can be used by any file (they support all platforms) + * - Platform-specific files can only import from universal or files supporting all their platforms + */ +function isPlatformCompatible(filePlatforms, importPlatforms) { + // If either has any error state, not compatible + if (filePlatforms === 'MISSING' || importPlatforms === 'MISSING' || + filePlatforms === 'NOT_CONST' || importPlatforms === 'NOT_CONST' || + filePlatforms === 'NOT_LITERALS' || importPlatforms === 'NOT_LITERALS') { + return false; + } + + // If import is universal, always compatible (universal supports all platforms) + if (isUniversal(importPlatforms)) { + return true; + } + + // If file is universal but import is not, NOT compatible + // (universal file runs everywhere, so imports must also run everywhere) + if (isUniversal(filePlatforms)) { + return false; + } + + // Otherwise, import must support ALL platforms that the file runs on + // For each platform the file runs on, check if the import also supports it + for (const platform of filePlatforms) { + if (!importPlatforms.includes(platform)) { + return false; + } + } + + return true; +} + +/** + * Extract imports using TypeScript AST + */ +function extractImports(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + const imports = []; + + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + function visit(node) { + // Import declarations: import ... from '...' + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + imports.push({ + type: 'import', + path: moduleSpecifier.text, + line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1 + }); + } + } + + // Export declarations: export ... from '...' + if (ts.isExportDeclaration(node) && node.moduleSpecifier) { + if (ts.isStringLiteral(node.moduleSpecifier)) { + imports.push({ + type: 'export', + path: node.moduleSpecifier.text, + line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1 + }); + } + } + + // Call expressions: require('...') or import('...') + if (ts.isCallExpression(node)) { + const expression = node.expression; + + // require('...') + if (ts.isIdentifier(expression) && expression.text === 'require') { + const arg = node.arguments[0]; + if (arg && ts.isStringLiteral(arg)) { + imports.push({ + type: 'require', + path: arg.text, + line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1 + }); + } + } + + // import('...') + if (expression.kind === ts.SyntaxKind.ImportKeyword) { + const arg = node.arguments[0]; + if (arg && ts.isStringLiteral(arg)) { + imports.push({ + type: 'dynamic-import', + path: arg.text, + line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1 + }); + } + } + } + + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return imports; +} + +/** + * Resolve import path relative to current file + */ +function resolveImportPath(importPath, currentFilePath) { + // External imports (node_modules) - return as-is + if (!importPath.startsWith('.') && !importPath.startsWith('/')) { + return { isExternal: true, resolved: importPath }; + } + + const currentDir = path.dirname(currentFilePath); + let resolved = path.resolve(currentDir, importPath); + + // Check if it's a directory - if so, look for index file + if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { + const extensions = ['.ts', '.js', '.tsx', '.jsx']; + for (const ext of extensions) { + const indexFile = path.join(resolved, `index${ext}`); + if (fs.existsSync(indexFile)) { + return { isExternal: false, resolved: indexFile }; + } + } + // Directory exists but no index file found + return { isExternal: false, resolved }; + } + + // Check if file exists as-is (with extension already) + if (fs.existsSync(resolved)) { + return { isExternal: false, resolved }; + } + + // Try different extensions + const extensions = ['.ts', '.js', '.tsx', '.jsx']; + for (const ext of extensions) { + const withExt = resolved + ext; + if (fs.existsSync(withExt)) { + return { isExternal: false, resolved: withExt }; + } + } + + // Try index files (for cases where the directory doesn't exist yet) + for (const ext of extensions) { + const indexFile = path.join(resolved, `index${ext}`); + if (fs.existsSync(indexFile)) { + return { isExternal: false, resolved: indexFile }; + } + } + + // Return the resolved path even if it doesn't exist + // (getSupportedPlatforms will handle it) + return { isExternal: false, resolved }; +} + +/** + * Validate a single file + */ +function validateFile(filePath) { + const filePlatforms = getSupportedPlatforms(filePath); + + // If file is missing __platforms, that's a validation error handled separately + if (filePlatforms === 'MISSING') { + return { valid: true, errors: [] }; // Reported separately + } + + const imports = extractImports(filePath); + const errors = []; + + for (const importInfo of imports) { + const { isExternal, resolved } = resolveImportPath(importInfo.path, filePath); + + // External imports are always allowed + if (isExternal) { + continue; + } + + const importPlatforms = getSupportedPlatforms(resolved); + + // Check compatibility + if (!isPlatformCompatible(filePlatforms, importPlatforms)) { + const message = importPlatforms === 'MISSING' + ? `Import is missing __platforms export: "${importInfo.path}"` + : `${formatPlatforms(filePlatforms)} file cannot import from ${formatPlatforms(importPlatforms)}-only file: "${importInfo.path}"`; + + errors.push({ + line: importInfo.line, + importPath: importInfo.path, + filePlatforms, + importPlatforms, + message + }); + } + } + + return { valid: errors.length === 0, errors }; +} + +/** + * Check if file matches any pattern using minimatch + */ +function matchesPattern(filePath, patterns) { + const relativePath = path.relative(WORKSPACE_ROOT, filePath).replace(/\\/g, '/'); + + return patterns.some(pattern => minimatch(relativePath, pattern)); +} + +/** + * Recursively find all files matching include patterns and not matching exclude patterns + */ +function findSourceFiles(dir, files = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + // Check if this directory path could potentially contain files matching include patterns + // Use minimatch with partial mode to test if pattern could match files under this directory + const relativePath = path.relative(WORKSPACE_ROOT, fullPath).replace(/\\/g, '/'); + const couldMatch = config.include.some(pattern => { + return minimatch(relativePath, pattern, { partial: true }); + }); + + if (couldMatch) { + findSourceFiles(fullPath, files); + } + } else if (entry.isFile()) { + // Check if file matches include patterns + if (matchesPattern(fullPath, config.include)) { + // Check if file is NOT excluded + if (!matchesPattern(fullPath, config.exclude)) { + files.push(fullPath); + } + } + } + } + + return files; +} + +/** + * Main validation function + */ +function main() { + console.log('🔍 Validating platform isolation (using TypeScript parser)...\n'); + console.log(`📋 Configuration: ${path.relative(WORKSPACE_ROOT, configPath) || '.platform-isolation.config.js'}\n`); + + const files = findSourceFiles(WORKSPACE_ROOT); + + // Load valid platforms first + const validPlatforms = getValidPlatformsFromSource(); + console.log(`Valid platforms: ${validPlatforms.join(', ')}\n`); + + // First pass: check for __platforms export + console.log(`Found ${files.length} source files\n`); + console.log('Checking for __platforms exports...\n'); + + files.forEach(f => getSupportedPlatforms(f)); // Populate cache and filesWithoutExport + + // Report files missing __platforms + if (filesWithoutExport.length > 0) { + console.error(`❌ Found ${filesWithoutExport.length} file(s) missing __platforms export:\n`); + + for (const file of filesWithoutExport) { + const relativePath = path.relative(process.cwd(), file); + console.error(` 📄 ${relativePath}`); + } + + console.error('\n'); + console.error('REQUIRED: Every source file must export __platforms array'); + console.error(''); + console.error('Examples:'); + console.error(' // Platform-specific file'); + console.error(' export const __platforms = [\'browser\', \'react_native\'];'); + console.error(''); + console.error(' // Universal file (all platforms)'); + console.error(' export const __platforms = [\'__universal__\'];'); + console.error(''); + console.error('See lib/platform_support.ts for type definitions.\n'); + + process.exit(1); + } + + console.log('✅ All files have __platforms export\n'); + + // Report files with non-const declaration + if (filesWithNonConst.length > 0) { + console.error(`❌ Found ${filesWithNonConst.length} file(s) with __platforms not declared as const:\n`); + + for (const file of filesWithNonConst) { + const relativePath = path.relative(process.cwd(), file); + console.error(` 📄 ${relativePath}`); + } + + console.error('\n'); + console.error('REQUIRED: __platforms must be declared as const'); + console.error('Use: export const __platforms: Platform[] = [...]\n'); + + process.exit(1); + } + + // Report files with non-literal values + if (filesWithNonLiterals.length > 0) { + console.error(`❌ Found ${filesWithNonLiterals.length} file(s) with __platforms containing non-literal values:\n`); + + for (const file of filesWithNonLiterals) { + const relativePath = path.relative(process.cwd(), file); + console.error(` 📄 ${relativePath}`); + } + + console.error('\n'); + console.error('REQUIRED: __platforms array must contain only string literals'); + console.error('Use: export const __platforms: Platform[] = [\'browser\', \'node\']'); + console.error('Do NOT use variables, computed values, or spread operators\n'); + + process.exit(1); + } + + console.log('✅ All __platforms declarations are const with string literals\n'); + + // Report files with invalid platform values + if (filesWithInvalidPlatforms.length > 0) { + console.error(`❌ Found ${filesWithInvalidPlatforms.length} file(s) with invalid platform values:\n`); + + for (const { filePath, platforms, invalidValues } of filesWithInvalidPlatforms) { + const relativePath = path.relative(process.cwd(), filePath); + console.error(` 📄 ${relativePath}`); + console.error(` Declared: [${platforms.map(p => `'${p}'`).join(', ')}]`); + console.error(` Invalid values: [${invalidValues.map(p => `'${p}'`).join(', ')}]`); + } + + console.error('\n'); + console.error(`Valid platform values: ${validPlatforms.map(p => `'${p}'`).join(', ')}`); + console.error('See lib/platform_support.ts for Platform type definition.\n'); + + process.exit(1); + } + + console.log('✅ All __platforms arrays have valid values\n'); + + // Second pass: validate platform isolation + console.log('Validating platform compatibility...\n'); + + let totalErrors = 0; + const filesWithErrors = []; + + for (const file of files) { + const result = validateFile(file); + + if (!result.valid) { + totalErrors += result.errors.length; + filesWithErrors.push({ file, errors: result.errors }); + } + } + + if (totalErrors === 0) { + console.log('✅ All files are properly isolated!\n'); + process.exit(0); + } else { + console.error(`❌ Found ${totalErrors} platform isolation violation(s) in ${filesWithErrors.length} file(s):\n`); + + for (const { file, errors } of filesWithErrors) { + const relativePath = path.relative(process.cwd(), file); + const filePlatforms = getSupportedPlatforms(file); + console.error(`\n📄 ${relativePath} [${formatPlatforms(filePlatforms)}]`); + + for (const error of errors) { + console.error(` Line ${error.line}: ${error.message}`); + } + } + + console.error('\n'); + console.error('Platform isolation rules:'); + console.error(' - Files can only import from files supporting ALL their platforms'); + console.error(' - Universal files ([\'__universal__\']) can be imported by any file'); + console.error(' - All files must have __platforms export\n'); + + process.exit(1); + } +} + +// Export functions for testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + isPlatformCompatible, + extractPlatformsFromAST, + getSupportedPlatforms, + extractImports, + }; +} + +// Run the validator +if (require.main === module) { + try { + main(); + } catch (error) { + console.error('❌ Validation failed with error:', error.message); + console.error(error.stack); + process.exit(1); + } +} diff --git a/vitest.config.mts b/vitest.config.mts index 1bce36eb0..cc25cd3c2 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -31,5 +31,15 @@ export default defineConfig({ enabled: true, tsconfig: 'tsconfig.spec.json', }, + coverage: { + provider: 'istanbul', + include: ['lib/**/*.ts'], + exclude: [ + '**/tests/**', + '**/*.spec.ts', + '**/*.gen.ts', + '**/*d.ts', + ], + }, }, });