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/README.md b/README.md index 08ab9f5ad..3e22b28e3 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 and universal) 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..78e179da7 --- /dev/null +++ b/docs/PLATFORM_ISOLATION.md @@ -0,0 +1,285 @@ +# 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:** + +```typescript +// lib/utils/web-features.ts +export const __platforms = ['browser', 'react_native']; + +// Your code that works on both browser and react_native +export function makeHttpRequest() { + // 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 should 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/vuid/vuid_manager_factory.react_native.ts (React Native platform only) +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; // ✅ Compatible (react_native only) +``` + + + +```typescript +// In lib/event_processor/event_processor_factory.browser.ts (Browser platform only) +import { Config } from '../shared_types'; // ✅ Universal file +import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; // ✅ Compatible (browser + react_native, includes browser) +``` + +```typescript +// In lib/event_processor/event_dispatcher/default_dispatcher.browser.ts (Multi-platform: browser + react_native) +import { Config } from '../../shared_types'; // ✅ Universal file +import { BrowserRequestHandler } from '../../utils/http_request_handler/request_handler.browser'; // ✅ Compatible (also browser + 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) +import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; // ❌ browser + react_native, doesn't support node +``` + +```typescript +// In lib/shared_types.ts (Universal file) +import { AsyncStorageCache } from './utils/cache/async_storage_cache.react_native'; // ❌ React Native only, universal file needs imports that work everywhere +``` + +```typescript +// In lib/event_processor/event_dispatcher/default_dispatcher.browser.ts +import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; // ❌ Node-only, doesn't support browser or 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.js`): + +1. Scans all TypeScript/JavaScript files configured in the in the `.platform-isolation.config.js` config file. +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 Modules + +### Universal Code + +For code that works across all platforms, use `['__universal__']`: + +**Example: Universal utility function** + +```typescript +// lib/utils/string-helpers.ts +export const __platforms = ['__universal__']; + +// Pure JavaScript that works everywhere +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function sanitizeKey(key: string): string { + return key.replace(/[^a-zA-Z0-9_]/g, '_'); +} +``` + +### Platform-Specific Code + +**Single Platform** + +1. **Add `__platforms` export** declaring the platform (e.g., `export const __platforms = ['browser'];`) +2. Name the file with a platform suffix for clarity (e.g., `my-feature.browser.ts`) +3. Only import from universal or compatible platform files + +**Example:** + +```typescript +// lib/features/my-feature.ts (universal interface) +export const __platforms = ['__universal__']; + +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 + } +} +``` + +**Multiple Platforms (But Not Universal)** + +For code that works on multiple platforms but is not universal, use the `__platforms` export to declare the list of supported platforms: + +**Example: Browser + React Native only** + +```typescript +// lib/utils/http-helpers.ts +export const __platforms = ['browser', 'react_native']; + +// This code works on both browser and react_native, but not node +export function makeRequest(url: string): Promise { + // XMLHttpRequest is available in both browser and react_native + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = () => resolve(xhr.responseText); + xhr.onerror = () => reject(new Error('Request failed')); + xhr.send(); + }); +} +``` + +## 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 diff --git a/eslint-local-rules/README.md b/eslint-local-rules/README.md new file mode 100644 index 000000000..6a166c615 --- /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 configured 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. + +**Requirement:** Every configured source file MUST export `__platforms` array with valid platform values. + +**Valid Examples:** + +```typescript +// Universal file (all platforms) +export const __platforms: Platform[] = ['__universal__']; + +// Platform-specific file +export const __platforms: Platform[] = ['browser', 'node']; + +// Single platform +export const __platforms: Platform[] = ['react_native']; +``` + +**Invalid:** + +```typescript +// Missing __platforms export +// ESLint Error: File must export __platforms to declare which platforms it supports. Example: export const __platforms = ['__universal__']; + +// Not an array +export const __platforms: Platform[] = 'browser'; +// ESLint Error: __platforms must be an array literal. Example: export const __platforms = ['browser', 'node']; + +// Empty array +export const __platforms: Platform[] = []; +// ESLint Error: __platforms array cannot be empty. Specify at least one platform or use ['__universal__']. + +// Using variables or computed values +const myPlatform = 'browser'; +export const __platforms: Platform[] = [myPlatform]; +// ESLint Error: __platforms must only contain string literals. Do NOT use variables, computed values, or spread operators. + +// Invalid platform value +export const __platforms: Platform[] = ['desktop']; +// ESLint Error: Invalid platform value "desktop". Valid platforms are: 'browser', 'node', 'react_native', '__universal__' +``` + +## Configuration + +The rules are loaded via `eslint-plugin-local-rules` and configured in `.eslintrc.js`: + +## 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..04ae81c0c --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1,26 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 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..1e3f5f7fd --- /dev/null +++ b/eslint-local-rules/require-platform-declaration.js @@ -0,0 +1,137 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 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 + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ + +const { getValidPlatforms } = require('../scripts/platform-utils'); + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Require __platforms export with valid platform values in all source files', + category: 'Possible Problems', + recommended: true, + }, + messages: { + missingPlatformDeclaration: 'File must export __platforms to declare which platforms it supports. Example: export const __platforms = [\'__universal__\'];', + notArray: '__platforms must be an array literal. Example: export const __platforms = [\'browser\', \'node\'];', + emptyArray: '__platforms array cannot be empty. Specify at least one platform or use [\'__universal__\'].', + notLiterals: '__platforms must only contain string literals. Do NOT use variables, computed values, or spread operators.', + invalidValues: 'Invalid platform value "{{value}}". Valid platforms are: {{validPlatforms}}', + }, + schema: [], + }, + + create(context) { + const VALID_PLATFORMS = getValidPlatforms(); + let hasPlatformExport = false; + + return { + ExportNamedDeclaration(node) { + // Check for: export const __platforms = [...] + if (!node.declaration || node.declaration.type !== 'VariableDeclaration') return; + + for (const declarator of node.declaration.declarations) { + if (declarator.id.type !== 'Identifier' || declarator.id.name !== '__platforms') continue; + + hasPlatformExport = true; + + // 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: 'notArray', + }); + return; + } + + // Check if array is empty + if (init.elements.length === 0) { + context.report({ + node: init, + messageId: 'emptyArray', + }); + 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: 'invalidValues', + data: { + value: element.value, + validPlatforms: VALID_PLATFORMS.map(p => `'${p}'`).join(', ') + } + }); + } + } else { + // Not a string literal + context.report({ + node: element || init, + messageId: 'notLiterals', + }); + } + } + } + }, + + '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..6601a5739 100644 --- a/lib/client_factory.ts +++ b/lib/client_factory.ts @@ -28,6 +28,7 @@ import { CmabCacheValue, DefaultCmabService } from "./core/decision_service/cmab import { InMemoryLruCache } from "./utils/cache/in_memory_lru_cache"; import { transformCache, CacheWithRemove } from "./utils/cache/cache"; import { ConstantBackoff } from "./utils/repeater/repeater"; +import { Platform } from './platform_support'; export type OptimizelyFactoryConfig = Config & { requestHandler: RequestHandler; @@ -94,3 +95,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..a8950d0ff 100644 --- a/lib/common_exports.ts +++ b/lib/common_exports.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from './platform_support'; + 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..cc59b3f56 100644 --- a/lib/core/audience_evaluator/index.ts +++ b/lib/core/audience_evaluator/index.ts @@ -20,6 +20,7 @@ import { Audience, Condition, OptimizelyUserContext } from '../../shared_types'; import { CONDITION_EVALUATOR_ERROR, UNKNOWN_CONDITION_TYPE } from 'error_message'; import { AUDIENCE_EVALUATION_RESULT, EVALUATING_AUDIENCE} from 'log_message'; import { LoggerFacade } from '../../logging/logger'; +import { Platform } from '../../platform_support'; export class AudienceEvaluator { private logger?: LoggerFacade; @@ -119,3 +120,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..b090e9dce 100644 --- a/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts +++ b/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts @@ -16,6 +16,7 @@ import { UNKNOWN_MATCH_TYPE } from 'error_message'; import { LoggerFacade } from '../../../logging/logger'; import { Condition, OptimizelyUserContext } from '../../../shared_types'; +import { Platform } from '../../../platform_support'; const QUALIFIED_MATCH_TYPE = 'qualified'; @@ -66,3 +67,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..bec776994 100644 --- a/lib/core/bucketer/bucket_value_generator.ts +++ b/lib/core/bucketer/bucket_value_generator.ts @@ -16,6 +16,7 @@ import murmurhash from 'murmurhash'; import { INVALID_BUCKETING_ID } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; +import { Platform } from '../../platform_support'; const HASH_SEED = 1; const MAX_HASH_VALUE = Math.pow(2, 32); @@ -38,3 +39,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..479ad17c5 100644 --- a/lib/core/bucketer/index.ts +++ b/lib/core/bucketer/index.ts @@ -28,6 +28,7 @@ import { INVALID_GROUP_ID } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; import { generateBucketValue } from './bucket_value_generator'; import { DecisionReason } from '../decision_service'; +import { Platform } from '../../platform_support'; 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.'; @@ -208,3 +209,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..9efd4008a 100644 --- a/lib/core/condition_tree_evaluator/index.ts +++ b/lib/core/condition_tree_evaluator/index.ts @@ -14,6 +14,9 @@ * limitations under the License. * ***************************************************************************/ + +import { Platform } from '../../platform_support'; + 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..44ba02b2c 100644 --- a/lib/core/custom_attribute_condition_evaluator/index.ts +++ b/lib/core/custom_attribute_condition_evaluator/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * ***************************************************************************/ + import { Condition, OptimizelyUserContext } from '../../shared_types'; import fns from '../../utils/fns'; @@ -28,7 +29,7 @@ import { UNKNOWN_MATCH_TYPE } from 'error_message'; import { LoggerFacade } from '../../logging/logger'; - +import { Platform } from '../../platform_support'; const EXACT_MATCH_TYPE = 'exact'; const EXISTS_MATCH_TYPE = 'exists'; const GREATER_OR_EQUAL_THAN_MATCH_TYPE = 'ge'; @@ -478,3 +479,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..0be3e1d7a 100644 --- a/lib/core/decision/index.ts +++ b/lib/core/decision/index.ts @@ -15,12 +15,14 @@ */ import { DecisionObj } from '../decision_service'; - +import { Platform } from '../../platform_support'; /** * Get experiment key from the provided decision object * @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 +62,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..b5e90b343 100644 --- a/lib/core/decision_service/cmab/cmab_client.ts +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -23,6 +23,7 @@ import { RequestHandler } from "../../../utils/http_request_handler/http"; import { isSuccessStatusCode } from "../../../utils/http_request_handler/http_util"; import { BackoffController } from "../../../utils/repeater/repeater"; import { Producer } from "../../../utils/type"; +import { Platform } from '../../../platform_support'; export interface CmabClient { fetchDecision( @@ -119,3 +120,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..004a146c0 100644 --- a/lib/core/decision_service/cmab/cmab_service.ts +++ b/lib/core/decision_service/cmab/cmab_service.ts @@ -32,7 +32,7 @@ import { INVALIDATE_CMAB_CACHE, RESET_CMAB_CACHE, } from 'log_message'; - +import { Platform } from '../../../platform_support'; export type CmabDecision = { variationId: string, cmabUuid: string, @@ -196,3 +196,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..099156b39 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -75,6 +75,7 @@ import { OptimizelyError } from '../../error/optimizly_error'; 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 = @@ -1694,3 +1695,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..b04ff4a8c 100644 --- a/lib/error/error_handler.ts +++ b/lib/error/error_handler.ts @@ -17,6 +17,9 @@ * @export * @interface ErrorHandler */ + +import { Platform } from '../platform_support'; + 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..303431655 100644 --- a/lib/error/error_notifier.ts +++ b/lib/error/error_notifier.ts @@ -16,6 +16,7 @@ import { MessageResolver } from "../message/message_resolver"; import { ErrorHandler } from "./error_handler"; import { OptimizelyError } from "./optimizly_error"; +import { Platform } from '../platform_support'; export interface ErrorNotifier { notify(error: Error): void; @@ -44,3 +45,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..a7ee9fd39 100644 --- a/lib/error/error_notifier_factory.ts +++ b/lib/error/error_notifier_factory.ts @@ -17,6 +17,7 @@ import { errorResolver } from "../message/message_resolver"; import { Maybe } from "../utils/type"; import { ErrorHandler } from "./error_handler"; import { DefaultErrorNotifier } from "./error_notifier"; +import { Platform } from '../platform_support'; export const INVALID_ERROR_HANDLER = 'Invalid error handler'; @@ -46,3 +47,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..c465c0b9d 100644 --- a/lib/error/error_reporter.ts +++ b/lib/error/error_reporter.ts @@ -16,6 +16,7 @@ import { LoggerFacade } from "../logging/logger"; import { ErrorNotifier } from "./error_notifier"; import { OptimizelyError } from "./optimizly_error"; +import { Platform } from '../platform_support'; export class ErrorReporter { private logger?: LoggerFacade; @@ -53,3 +54,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..656aae456 100644 --- a/lib/error/optimizly_error.ts +++ b/lib/error/optimizly_error.ts @@ -15,6 +15,7 @@ */ import { MessageResolver } from "../message/message_resolver"; import { sprintf } from "../utils/fns"; +import { Platform } from '../platform_support'; export class OptimizelyError extends Error { baseMessage: string; @@ -38,3 +39,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..e4cb2476a 100644 --- a/lib/event_processor/batch_event_processor.react_native.ts +++ b/lib/event_processor/batch_event_processor.react_native.ts @@ -18,6 +18,7 @@ import { NetInfoState, addEventListener } from '@react-native-community/netinfo' import { BatchEventProcessor, BatchEventProcessorConfig } from './batch_event_processor'; import { Fn } from '../utils/type'; +import { Platform } from '../platform_support'; export class ReactNativeNetInfoEventProcessor extends BatchEventProcessor { private isInternetReachable = true; @@ -49,3 +50,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..81547352f 100644 --- a/lib/event_processor/batch_event_processor.ts +++ b/lib/event_processor/batch_event_processor.ts @@ -31,6 +31,7 @@ import { FAILED_TO_DISPATCH_EVENTS, SERVICE_NOT_RUNNING } from "error_message"; import { OptimizelyError } from "../error/optimizly_error"; import { sprintf } from "../utils/fns"; import { SERVICE_STOPPED_BEFORE_RUNNING } from "../service"; +import { Platform } from '../platform_support'; export const DEFAULT_MIN_BACKOFF = 1000; export const DEFAULT_MAX_BACKOFF = 32000; @@ -317,3 +318,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..1d13bb5fb 100644 --- a/lib/event_processor/event_builder/log_event.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -20,6 +20,7 @@ import { CONTROL_ATTRIBUTES } from '../../utils/enums'; import { LogEvent } from '../event_dispatcher/event_dispatcher'; import { EventTags } from '../../shared_types'; import { Region } from '../../project_config/project_config'; +import { Platform } from '../../platform_support'; const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' @@ -230,3 +231,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..76879db35 100644 --- a/lib/event_processor/event_builder/user_event.ts +++ b/lib/event_processor/event_builder/user_event.ts @@ -29,7 +29,7 @@ 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 key: string @@ -304,3 +304,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..9da3b132e 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts @@ -14,10 +14,15 @@ * limitations under the License. */ +// This implementation works in both browser and react_native environments + import { BrowserRequestHandler } from "../../utils/http_request_handler/request_handler.browser"; import { EventDispatcher } from './event_dispatcher'; import { DefaultEventDispatcher } from './default_dispatcher'; +import { Platform } from '../../platform_support'; 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..6d9e78dd7 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.node.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.node.ts @@ -16,7 +16,10 @@ import { EventDispatcher } from './event_dispatcher'; import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; import { DefaultEventDispatcher } from './default_dispatcher'; +import { Platform } from '../../platform_support'; 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..769efd554 100644 --- a/lib/event_processor/event_dispatcher/default_dispatcher.ts +++ b/lib/event_processor/event_dispatcher/default_dispatcher.ts @@ -17,6 +17,7 @@ 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'; +import { Platform } from '../../platform_support'; export class DefaultEventDispatcher implements EventDispatcher { private requestHandler: RequestHandler; @@ -43,3 +44,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..f9937c6a7 100644 --- a/lib/event_processor/event_dispatcher/event_dispatcher.ts +++ b/lib/event_processor/event_dispatcher/event_dispatcher.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { EventBatch } from "../event_builder/log_event"; +import { Platform } from '../../platform_support'; export type EventDispatcherResponse = { statusCode?: number @@ -28,3 +29,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..bf5258543 100644 --- a/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts +++ b/lib/event_processor/event_dispatcher/event_dispatcher_factory.ts @@ -19,8 +19,11 @@ import { DefaultEventDispatcher } from './default_dispatcher'; import { EventDispatcher } from './event_dispatcher'; import { validateRequestHandler } from '../../utils/http_request_handler/request_handler_validator'; +import { Platform } from '../../platform_support'; 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..a5fe6d496 100644 --- a/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts +++ b/lib/event_processor/event_dispatcher/send_beacon_dispatcher.browser.ts @@ -17,6 +17,7 @@ import { OptimizelyError } from '../../error/optimizly_error'; import { SEND_BEACON_FAILED } from 'error_message'; import { EventDispatcher, EventDispatcherResponse } from './event_dispatcher'; +import { Platform } from '../../platform_support'; export type Event = { url: string; @@ -51,3 +52,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..22718c841 100644 --- a/lib/event_processor/event_processor.ts +++ b/lib/event_processor/event_processor.ts @@ -18,6 +18,7 @@ import { LogEvent } from './event_dispatcher/event_dispatcher' import { Service } from '../service' import { Consumer, Fn } from '../utils/type'; import { LoggerFacade } from '../logging/logger'; +import { Platform } from '../platform_support'; export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s export const DEFAULT_BATCH_SIZE = 10 @@ -30,3 +31,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..b69219a7f 100644 --- a/lib/event_processor/event_processor_factory.browser.ts +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -29,7 +29,7 @@ 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; export const EVENT_MAX_RETRIES_BROWSER = 5; @@ -66,3 +66,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..15fe6f4e9 100644 --- a/lib/event_processor/event_processor_factory.node.ts +++ b/lib/event_processor/event_processor_factory.node.ts @@ -24,7 +24,7 @@ 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; export const EVENT_MAX_RETRIES_NODE = 5; @@ -55,3 +55,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..525dd91cf 100644 --- a/lib/event_processor/event_processor_factory.react_native.ts +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -28,7 +28,7 @@ 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; export const EVENT_MAX_RETRIES_REACT_NATIVE = 5; @@ -66,3 +66,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..7336dfa86 100644 --- a/lib/event_processor/event_processor_factory.ts +++ b/lib/event_processor/event_processor_factory.ts @@ -25,6 +25,7 @@ import { EventDispatcher } from "./event_dispatcher/event_dispatcher"; import { EventProcessor } from "./event_processor"; import { EVENT_STORE_PREFIX } from "./event_store"; import { ForwardingEventProcessor } from "./forwarding_event_processor"; +import { Platform } from '../platform_support'; export const INVALID_EVENT_DISPATCHER = 'Invalid event dispatcher'; @@ -175,3 +176,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..c2ec305aa 100644 --- a/lib/event_processor/event_processor_factory.universal.ts +++ b/lib/event_processor/event_processor_factory.universal.ts @@ -29,7 +29,7 @@ export const DEFAULT_EVENT_BATCH_SIZE = 10; export const DEFAULT_EVENT_FLUSH_INTERVAL = 1_000; import { FAILED_EVENT_RETRY_INTERVAL } from './event_processor_factory'; - +import { Platform } from '../platform_support'; export const createForwardingEventProcessor = ( eventDispatcher: EventDispatcher ): OpaqueEventProcessor => { @@ -59,3 +59,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..90ccf9ff6 100644 --- a/lib/event_processor/event_store.ts +++ b/lib/event_processor/event_store.ts @@ -12,7 +12,7 @@ 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?: { storedAt: number; @@ -151,3 +151,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..12decd5ab 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -25,6 +25,7 @@ import { EventEmitter } from '../utils/event_emitter/event_emitter'; import { Consumer, Fn } from '../utils/type'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; import { sprintf } from '../utils/fns'; +import { Platform } from '../platform_support'; export class ForwardingEventProcessor extends BaseService implements EventProcessor { private dispatcher: EventDispatcher; @@ -74,3 +75,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..51109d933 100644 --- a/lib/export_types.ts +++ b/lib/export_types.ts @@ -15,6 +15,9 @@ */ // config manager related types + +import { Platform } from './platform_support'; + 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..c567c46a3 100644 --- a/lib/feature_toggle.ts +++ b/lib/feature_toggle.ts @@ -34,4 +34,9 @@ // example feature flag definition // export const wipFeat = () => false as const; + +import { Platform } from './platform_support'; + 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..79e5968bf 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -19,13 +19,15 @@ 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 * @param {Config} config * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const client = getOptimizelyInstance({ ...config, @@ -62,3 +64,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.node.ts b/lib/index.node.ts index 02d162ed6..0f3c0adbf 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -18,13 +18,15 @@ 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 * @param {Config} config * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const nodeConfig: OptimizelyFactoryConfig = { ...config, @@ -52,3 +54,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..6914a5f97 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -21,13 +21,15 @@ 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 * @param {Config} config * @return {Client|null} the Optimizely client object * null on error */ + + export const createInstance = function(config: Config): Client { const rnConfig: OptimizelyFactoryConfig = { ...config, @@ -55,3 +57,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..3d731aa50 100644 --- a/lib/index.universal.ts +++ b/lib/index.universal.ts @@ -18,7 +18,7 @@ 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 +135,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..c11b1250d 100644 --- a/lib/logging/logger.ts +++ b/lib/logging/logger.ts @@ -16,6 +16,7 @@ import { OptimizelyError } from '../error/optimizly_error'; import { MessageResolver } from '../message/message_resolver'; import { sprintf } from '../utils/fns' +import { Platform } from '../platform_support'; export enum LogLevel { Debug, @@ -165,3 +166,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..57af2f50f 100644 --- a/lib/logging/logger_factory.ts +++ b/lib/logging/logger_factory.ts @@ -16,6 +16,7 @@ import { ConsoleLogHandler, LogHandler, LogLevel, OptimizelyLogger } from './logger'; import { errorResolver, infoResolver, MessageResolver } from '../message/message_resolver'; import { Maybe } from '../utils/type'; +import { Platform } from '../platform_support'; export const INVALID_LOG_HANDLER = 'Invalid log handler'; export const INVALID_LEVEL_PRESET = 'Invalid level preset'; @@ -127,3 +128,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..624e4085d 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Platform } from '../platform_support'; + 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..2e9180535 100644 --- a/lib/message/log_message.ts +++ b/lib/message/log_message.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../platform_support'; + 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..050207fc0 100644 --- a/lib/message/message_resolver.ts +++ b/lib/message/message_resolver.ts @@ -1,5 +1,6 @@ import { messages as infoMessages } from 'log_message'; import { messages as errorMessages } from 'error_message'; +import { Platform } from '../platform_support'; export interface MessageResolver { resolve(baseMessage: string): string; @@ -18,3 +19,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..ee792179b 100644 --- a/lib/notification_center/index.ts +++ b/lib/notification_center/index.ts @@ -23,6 +23,7 @@ import { EventEmitter } from '../utils/event_emitter/event_emitter'; import { NOTIFICATION_LISTENER_EXCEPTION } from 'error_message'; import { ErrorReporter } from '../error/error_reporter'; import { ErrorNotifier } from '../error/error_notifier'; +import { Platform } from '../platform_support'; interface NotificationCenterOptions { logger?: LoggerFacade; @@ -162,3 +163,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..1d423b13d 100644 --- a/lib/notification_center/type.ts +++ b/lib/notification_center/type.ts @@ -26,7 +26,7 @@ import { } from '../shared_types'; import { DecisionSource } from '../utils/enums'; import { Nullable } from '../utils/type'; - +import { Platform } from '../platform_support'; export type UserEventListenerPayload = { userId: string; attributes?: UserAttributes; @@ -150,3 +150,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..178e940ed 100644 --- a/lib/odp/constant.ts +++ b/lib/odp/constant.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../platform_support'; + 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..b237b8fc7 100644 --- a/lib/odp/event_manager/odp_event.ts +++ b/lib/odp/event_manager/odp_event.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../../platform_support'; + 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..c7b4165f5 100644 --- a/lib/odp/event_manager/odp_event_api_manager.ts +++ b/lib/odp/event_manager/odp_event_api_manager.ts @@ -18,6 +18,7 @@ import { LoggerFacade } from '../../logging/logger'; import { OdpEvent } from './odp_event'; import { HttpMethod, RequestHandler } from '../../utils/http_request_handler/http'; import { OdpConfig } from '../odp_config'; +import { Platform } from '../../platform_support'; export type EventDispatchResponse = { statusCode?: number; @@ -114,3 +115,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..67a22a9dc 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -37,7 +37,7 @@ 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; sendEvent(event: OdpEvent): void; @@ -246,3 +246,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..f858bbbd9 100644 --- a/lib/odp/odp_config.ts +++ b/lib/odp/odp_config.ts @@ -15,6 +15,7 @@ */ import { checkArrayEquality } from '../utils/fns'; +import { Platform } from '../platform_support'; export class OdpConfig { /** @@ -81,3 +82,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..7525d0efb 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -31,6 +31,7 @@ import { isVuid } from '../vuid/vuid'; import { Maybe } from '../utils/type'; import { sprintf } from '../utils/fns'; import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; +import { Platform } from '../platform_support'; export interface OdpManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; @@ -263,3 +264,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..30b7479e9 100644 --- a/lib/odp/odp_manager_factory.browser.ts +++ b/lib/odp/odp_manager_factory.browser.ts @@ -17,6 +17,7 @@ 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'; +import { Platform } from '../platform_support'; export const BROWSER_DEFAULT_API_TIMEOUT = 10_000; export const BROWSER_DEFAULT_BATCH_SIZE = 10; @@ -40,3 +41,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..d40757b88 100644 --- a/lib/odp/odp_manager_factory.node.ts +++ b/lib/odp/odp_manager_factory.node.ts @@ -17,6 +17,7 @@ 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'; +import { Platform } from '../platform_support'; export const NODE_DEFAULT_API_TIMEOUT = 10_000; export const NODE_DEFAULT_BATCH_SIZE = 10; @@ -40,3 +41,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..ca9a39c57 100644 --- a/lib/odp/odp_manager_factory.react_native.ts +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -18,6 +18,7 @@ import { BrowserRequestHandler } from '../utils/http_request_handler/request_han import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { OdpManager } from './odp_manager'; import { getOpaqueOdpManager, OdpManagerOptions, OpaqueOdpManager } from './odp_manager_factory'; +import { Platform } from '../platform_support'; export const RN_DEFAULT_API_TIMEOUT = 10_000; export const RN_DEFAULT_BATCH_SIZE = 10; @@ -41,3 +42,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..f61c25209 100644 --- a/lib/odp/odp_manager_factory.ts +++ b/lib/odp/odp_manager_factory.ts @@ -25,6 +25,7 @@ import { DefaultOdpManager, OdpManager } from "./odp_manager"; import { DefaultOdpSegmentApiManager } from "./segment_manager/odp_segment_api_manager"; import { DefaultOdpSegmentManager, OdpSegmentManager } from "./segment_manager/odp_segment_manager"; import { UserAgentParser } from "./ua_parser/user_agent_parser"; +import { Platform } from '../platform_support'; export const DEFAULT_CACHE_SIZE = 10_000; export const DEFAULT_CACHE_TIMEOUT = 600_000; @@ -136,3 +137,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..5b8ddec1e 100644 --- a/lib/odp/odp_manager_factory.universal.ts +++ b/lib/odp/odp_manager_factory.universal.ts @@ -18,6 +18,7 @@ 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'; +import { Platform } from '../platform_support'; export const DEFAULT_API_TIMEOUT = 10_000; export const DEFAULT_BATCH_SIZE = 1; @@ -38,3 +39,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..7ddf847a8 100644 --- a/lib/odp/odp_types.ts +++ b/lib/odp/odp_types.ts @@ -17,6 +17,9 @@ /** * Wrapper around valid data and error responses */ + +import { Platform } from '../platform_support'; + 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..cdf82493b 100644 --- a/lib/odp/segment_manager/odp_response_schema.ts +++ b/lib/odp/segment_manager/odp_response_schema.ts @@ -15,10 +15,12 @@ */ import { JSONSchema4 } from 'json-schema'; - +import { Platform } from '../../platform_support'; /** * 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 +186,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..59032013c 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -21,9 +21,12 @@ import { ODP_USER_KEY } from '../constant'; import { RequestHandler } from '../../utils/http_request_handler/http'; import { Response as GraphQLResponse } from '../odp_types'; import { log } from 'console'; +import { Platform } from '../../platform_support'; /** * 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..7a6fae790 100644 --- a/lib/odp/segment_manager/odp_segment_manager.ts +++ b/lib/odp/segment_manager/odp_segment_manager.ts @@ -21,6 +21,7 @@ import { OptimizelySegmentOption } from './optimizely_segment_option'; import { ODP_USER_KEY } from '../constant'; import { LoggerFacade } from '../../logging/logger'; import { ODP_CONFIG_NOT_AVAILABLE, ODP_NOT_INTEGRATED } from 'error_message'; +import { Platform } from '../../platform_support'; export interface OdpSegmentManager { fetchQualifiedSegments( @@ -128,3 +129,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..a28b82854 100644 --- a/lib/odp/segment_manager/optimizely_segment_option.ts +++ b/lib/odp/segment_manager/optimizely_segment_option.ts @@ -15,7 +15,12 @@ */ // Options for defining behavior of OdpSegmentManager's caching mechanism when calling fetchSegments() + +import { Platform } from '../../platform_support'; + 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..bf0bceae7 100644 --- a/lib/odp/ua_parser/ua_parser.ts +++ b/lib/odp/ua_parser/ua_parser.ts @@ -16,6 +16,7 @@ import { UAParser } from 'ua-parser-js'; import { UserAgentInfo } from './user_agent_info'; import { UserAgentParser } from './user_agent_parser'; +import { Platform } from '../../platform_support'; const userAgentParser: UserAgentParser = { parseUserAgentInfo(): UserAgentInfo { @@ -29,3 +30,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..813683db7 100644 --- a/lib/odp/ua_parser/user_agent_info.ts +++ b/lib/odp/ua_parser/user_agent_info.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../../platform_support'; + 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..0ff1c6eb2 100644 --- a/lib/odp/ua_parser/user_agent_parser.ts +++ b/lib/odp/ua_parser/user_agent_parser.ts @@ -15,7 +15,10 @@ */ import { UserAgentInfo } from "./user_agent_info"; +import { Platform } from '../../platform_support'; export interface UserAgentParser { parseUserAgentInfo(): UserAgentInfo, } + +export const __platforms: Platform[] = ['__universal__']; diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 2381e8a80..c9a82b551 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -103,7 +103,7 @@ import { ErrorReporter } from '../error/error_reporter'; import { OptimizelyError } from '../error/optimizly_error'; import { Value } from '../utils/promise/operation_value'; import { CmabService } from '../core/decision_service/cmab/cmab_service'; - +import { Platform } from '../platform_support'; const DEFAULT_ONREADY_TIMEOUT = 30000; // TODO: Make feature_key, user_id, variable_key, experiment_key, event_key camelCase @@ -1800,3 +1800,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..28d99b704 100644 --- a/lib/optimizely_decision/index.ts +++ b/lib/optimizely_decision/index.ts @@ -14,6 +14,7 @@ * limitations under the License. * ***************************************************************************/ import { OptimizelyUserContext, OptimizelyDecision } from '../shared_types'; +import { Platform } from '../platform_support'; export function newErrorDecision(key: string, user: OptimizelyUserContext, reasons: string[]): OptimizelyDecision { return { @@ -26,3 +27,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..0fbe2bec0 100644 --- a/lib/optimizely_user_context/index.ts +++ b/lib/optimizely_user_context/index.ts @@ -24,7 +24,7 @@ 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'; interface OptimizelyUserContextConfig { @@ -295,3 +295,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..8d23ffb82 100644 --- a/lib/project_config/config_manager_factory.browser.ts +++ b/lib/project_config/config_manager_factory.browser.ts @@ -16,6 +16,7 @@ import { BrowserRequestHandler } from '../utils/http_request_handler/request_handler.browser'; import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from './config_manager_factory'; +import { Platform } from '../platform_support'; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { @@ -24,3 +25,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..6ec76ccc3 100644 --- a/lib/project_config/config_manager_factory.node.ts +++ b/lib/project_config/config_manager_factory.node.ts @@ -16,6 +16,7 @@ import { NodeRequestHandler } from "../utils/http_request_handler/request_handler.node"; import { getOpaquePollingConfigManager, OpaqueConfigManager, PollingConfigManagerConfig } from "./config_manager_factory"; +import { Platform } from '../platform_support'; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { @@ -24,3 +25,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..747720be3 100644 --- a/lib/project_config/config_manager_factory.react_native.ts +++ b/lib/project_config/config_manager_factory.react_native.ts @@ -17,6 +17,7 @@ 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"; +import { Platform } from '../platform_support'; export const createPollingProjectConfigManager = (config: PollingConfigManagerConfig): OpaqueConfigManager => { const defaultConfig = { @@ -27,3 +28,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..ed27ee234 100644 --- a/lib/project_config/config_manager_factory.ts +++ b/lib/project_config/config_manager_factory.ts @@ -26,6 +26,7 @@ import { MIN_UPDATE_INTERVAL, UPDATE_INTERVAL_BELOW_MINIMUM_MESSAGE } from './co import { LogLevel } from '../logging/logger' import { Store } from "../utils/cache/store"; import { validateStore } from "../utils/cache/store_validator"; +import { Platform } from '../platform_support'; export const INVALID_CONFIG_MANAGER = "Invalid config manager"; @@ -129,3 +130,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..9f2b6e21b 100644 --- a/lib/project_config/config_manager_factory.universal.ts +++ b/lib/project_config/config_manager_factory.universal.ts @@ -17,6 +17,7 @@ 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"; +import { Platform } from '../platform_support'; export type UniversalPollingConfigManagerConfig = PollingConfigManagerConfig & { requestHandler: RequestHandler; @@ -29,3 +30,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..1fa922bcf 100644 --- a/lib/project_config/constant.ts +++ b/lib/project_config/constant.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../platform_support'; + 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..28df93375 100644 --- a/lib/project_config/datafile_manager.ts +++ b/lib/project_config/datafile_manager.ts @@ -19,6 +19,7 @@ import { RequestHandler } from '../utils/http_request_handler/http'; import { Fn, Consumer } from '../utils/type'; import { Repeater } from '../utils/repeater/repeater'; import { LoggerFacade } from '../logging/logger'; +import { Platform } from '../platform_support'; export interface DatafileManager extends Service { get(): string | undefined; @@ -39,3 +40,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..6965657fb 100644 --- a/lib/project_config/optimizely_config.ts +++ b/lib/project_config/optimizely_config.ts @@ -33,7 +33,7 @@ import { Variation, VariationVariable, } from '../shared_types'; - +import { Platform } from '../platform_support'; interface FeatureVariablesMap { [key: string]: FeatureVariable[]; } @@ -481,3 +481,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..d4f02e37e 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -39,7 +39,7 @@ import { LoggerFacade } from '../logging/logger'; 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 +266,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..2627abe09 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -52,7 +52,7 @@ import { } from 'error_message'; 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 +976,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..4a2191102 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -31,6 +31,7 @@ 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 +236,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..d940bbf2e 100644 --- a/lib/project_config/project_config_schema.ts +++ b/lib/project_config/project_config_schema.ts @@ -18,6 +18,7 @@ * Project Config JSON Schema file used to validate the project json datafile */ import { JSONSchema4 } from 'json-schema'; +import { Platform } from '../platform_support'; var schemaDefinition = { $schema: 'http://json-schema.org/draft-04/schema#', @@ -316,3 +317,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..cb2de2269 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -16,6 +16,7 @@ import { LoggerFacade, LogLevel, LogLevelToLower } from './logging/logger' import { resolvablePromise, ResolvablePromise } from "./utils/promise/resolvablePromise"; +import { Platform } from './platform_support'; export const SERVICE_FAILED_TO_START = '%s failed to start, reason: %s'; export const SERVICE_STOPPED_BEFORE_RUNNING = '%s stopped before running'; @@ -132,3 +133,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..00943811a 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -46,6 +46,7 @@ import { OpaqueEventProcessor } from './event_processor/event_processor_factory' import { OpaqueOdpManager } from './odp/odp_manager_factory'; import { OpaqueVuidManager } from './vuid/vuid_manager_factory'; import { CacheWithRemove } from './utils/cache/cache'; +import { Platform } from './platform_support'; export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; @@ -534,3 +535,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..f72e6347b 100644 --- a/lib/utils/attributes_validator/index.ts +++ b/lib/utils/attributes_validator/index.ts @@ -18,7 +18,7 @@ import { ObjectWithUnknownProperties } from '../../shared_types'; import fns from '../../utils/fns'; import { INVALID_ATTRIBUTES, UNDEFINED_ATTRIBUTE } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; - +import { Platform } from '../../platform_support'; /** * Validates user's provided attributes * @param {unknown} attributes @@ -26,6 +26,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 +54,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..049633fd6 100644 --- a/lib/utils/cache/async_storage_cache.react_native.ts +++ b/lib/utils/cache/async_storage_cache.react_native.ts @@ -17,6 +17,7 @@ import { Maybe } from "../type"; import { AsyncStore } from "./store"; import { getDefaultAsyncStorage } from "../import.react_native/@react-native-async-storage/async-storage"; +import { Platform } from '../../platform_support'; export class AsyncStorageCache implements AsyncStore { public readonly operation = 'async'; @@ -48,3 +49,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..dd05ab63c 100644 --- a/lib/utils/cache/cache.ts +++ b/lib/utils/cache/cache.ts @@ -15,6 +15,8 @@ */ 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 +70,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..36c4d4b56 100644 --- a/lib/utils/cache/in_memory_lru_cache.ts +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -16,6 +16,7 @@ import { Maybe } from "../type"; import { SyncCacheWithRemove } from "./cache"; +import { Platform } from '../../platform_support'; type CacheElement = { value: V; @@ -72,3 +73,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..3e5ede910 100644 --- a/lib/utils/cache/local_storage_cache.browser.ts +++ b/lib/utils/cache/local_storage_cache.browser.ts @@ -16,6 +16,7 @@ import { Maybe } from "../type"; import { SyncStore } from "./store"; +import { Platform } from '../../platform_support'; export class LocalStorageCache implements SyncStore { public readonly operation = 'sync'; @@ -52,3 +53,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..c3a3b1980 100644 --- a/lib/utils/cache/store.ts +++ b/lib/utils/cache/store.ts @@ -17,6 +17,7 @@ import { Transformer } from '../../utils/type'; import { Maybe } from '../../utils/type'; import { OpType, OpValue } from '../../utils/type'; +import { Platform } from '../../platform_support'; export interface OpStore { operation: OP; @@ -174,3 +175,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..1746d9c69 100644 --- a/lib/utils/cache/store_validator.ts +++ b/lib/utils/cache/store_validator.ts @@ -14,6 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Platform } from '../../platform_support'; + 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..4b59b066d 100644 --- a/lib/utils/config_validator/index.ts +++ b/lib/utils/config_validator/index.ts @@ -22,7 +22,7 @@ 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 +61,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..6657848b4 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -17,6 +17,9 @@ /** * Contains global enums used throughout the library */ + +import { Platform } from '../../platform_support'; + 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..a98f09572 100644 --- a/lib/utils/event_emitter/event_emitter.ts +++ b/lib/utils/event_emitter/event_emitter.ts @@ -15,6 +15,7 @@ */ import { Fn } from "../type"; +import { Platform } from '../../platform_support'; type Consumer = (arg: T) => void; @@ -55,3 +56,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..1254d5572 100644 --- a/lib/utils/event_tag_utils/index.ts +++ b/lib/utils/event_tag_utils/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { FAILED_TO_PARSE_REVENUE, FAILED_TO_PARSE_VALUE, @@ -23,7 +24,7 @@ import { LoggerFacade } from '../../logging/logger'; import { RESERVED_EVENT_KEYWORDS } from '../enums'; import { EventTags } from '../../shared_types'; - +import { Platform } from '../../platform_support'; /** * Provides utility method for parsing event tag values */ @@ -81,3 +82,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..eb69bb82d 100644 --- a/lib/utils/event_tags_validator/index.ts +++ b/lib/utils/event_tags_validator/index.ts @@ -19,13 +19,15 @@ */ import { OptimizelyError } from '../../error/optimizly_error'; import { INVALID_EVENT_TAGS } from 'error_message'; - +import { Platform } from '../../platform_support'; /** * Validates user's provided event tags * @param {unknown} eventTags * @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 +35,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..7de9376c7 100644 --- a/lib/utils/executor/backoff_retry_runner.ts +++ b/lib/utils/executor/backoff_retry_runner.ts @@ -3,6 +3,7 @@ import { RETRY_CANCELLED } from "error_message"; import { resolvablePromise, ResolvablePromise } from "../promise/resolvablePromise"; import { BackoffController } from "../repeater/repeater"; import { AsyncProducer, Fn } from "../type"; +import { Platform } from '../../platform_support'; export type RunResult = { result: Promise; @@ -52,3 +53,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..a4b9a5109 100644 --- a/lib/utils/executor/serial_runner.ts +++ b/lib/utils/executor/serial_runner.ts @@ -15,6 +15,7 @@ */ import { AsyncProducer } from "../type"; +import { Platform } from '../../platform_support'; class SerialRunner { private waitPromise: Promise = Promise.resolve(); @@ -34,3 +35,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..413d5f9d7 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { v4 } from 'uuid'; +import { Platform } from '../../platform_support'; const MAX_SAFE_INTEGER_LIMIT = Math.pow(2, 53); @@ -130,3 +131,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..4bc4154f3 100644 --- a/lib/utils/http_request_handler/http.ts +++ b/lib/utils/http_request_handler/http.ts @@ -17,6 +17,9 @@ /** * List of key-value pairs to be used in an HTTP requests */ + +import { Platform } from '../../platform_support'; + 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..bf0ef27d9 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..578002d17 100644 --- a/lib/utils/http_request_handler/request_handler.browser.ts +++ b/lib/utils/http_request_handler/request_handler.browser.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +// This implementation works in both browser and react_native environments + import { AbortableRequest, Headers, RequestHandler, Response } from './http'; import { LoggerFacade, LogLevel } from '../../logging/logger'; import { REQUEST_TIMEOUT_MS } from '../enums'; import { REQUEST_ERROR, REQUEST_TIMEOUT, UNABLE_TO_PARSE_AND_SKIPPED_HEADER } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; - +import { Platform } from '../../platform_support'; /** * Handles sending requests and receiving responses over HTTP via XMLHttpRequest */ @@ -130,3 +132,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..b972c0526 100644 --- a/lib/utils/http_request_handler/request_handler.node.ts +++ b/lib/utils/http_request_handler/request_handler.node.ts @@ -22,10 +22,12 @@ import { LoggerFacade } from '../../logging/logger'; import { REQUEST_TIMEOUT_MS } from '../enums'; import { NO_STATUS_CODE_IN_RESPONSE, REQUEST_ERROR, REQUEST_TIMEOUT, UNSUPPORTED_PROTOCOL } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; - +import { Platform } from '../../platform_support'; /** * 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 +186,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..a3373e561 100644 --- a/lib/utils/http_request_handler/request_handler_validator.ts +++ b/lib/utils/http_request_handler/request_handler_validator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { RequestHandler } from './http'; +import { Platform } from '../../platform_support'; export const INVALID_REQUEST_HANDLER = 'Invalid request handler'; @@ -26,3 +27,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..d73052026 100644 --- a/lib/utils/id_generator/index.ts +++ b/lib/utils/id_generator/index.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../../platform_support'; + 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..dcf394ae0 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 @@ -15,6 +15,7 @@ */ import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage' +import { Platform } from '../../../platform_support'; export const MODULE_NOT_FOUND_REACT_NATIVE_ASYNC_STORAGE = 'Module not found: @react-native-async-storage/async-storage'; @@ -26,3 +27,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..8c5760f59 100644 --- a/lib/utils/json_schema_validator/index.ts +++ b/lib/utils/json_schema_validator/index.ts @@ -18,7 +18,7 @@ import { JSONSchema4, validate as jsonSchemaValidator } from 'json-schema'; import schema from '../../project_config/project_config_schema'; import { INVALID_DATAFILE, INVALID_JSON, NO_JSON_PROVIDED } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; - +import { Platform } from '../../platform_support'; /** * Validate the given json object against the specified schema * @param {unknown} jsonObject The object to validate against the schema @@ -26,6 +26,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 +54,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..015b2f691 100644 --- a/lib/utils/microtask/index.ts +++ b/lib/utils/microtask/index.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +import { Platform } from '../../platform_support'; + 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..fd656e4e8 100644 --- a/lib/utils/promise/operation_value.ts +++ b/lib/utils/promise/operation_value.ts @@ -1,6 +1,7 @@ import { PROMISE_NOT_ALLOWED } from '../../message/error_message'; import { OptimizelyError } from '../../error/optimizly_error'; import { OpType, OpValue } from '../type'; +import { Platform } from '../../platform_support'; const isPromise = (val: any): boolean => { @@ -48,3 +49,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..87cff7224 100644 --- a/lib/utils/promise/resolvablePromise.ts +++ b/lib/utils/promise/resolvablePromise.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../../platform_support'; + 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..cdb9ec9f3 100644 --- a/lib/utils/repeater/repeater.ts +++ b/lib/utils/repeater/repeater.ts @@ -16,7 +16,7 @@ import { AsyncTransformer } from "../type"; import { scheduleMicrotask } from "../microtask"; - +import { Platform } from '../../platform_support'; // A repeater will invoke the task repeatedly. The time at which the task is invoked // is determined by the implementation. // The task is a function that takes a number as an argument and returns a promise. @@ -24,6 +24,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 +156,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..39a50df76 100644 --- a/lib/utils/semantic_version/index.ts +++ b/lib/utils/semantic_version/index.ts @@ -16,13 +16,15 @@ import { UNKNOWN_MATCH_TYPE } from 'error_message'; import { LoggerFacade } from '../../logging/logger'; import { VERSION_TYPE } from '../enums'; - +import { Platform } from '../../platform_support'; /** * Evaluate if provided string is number only * @param {unknown} content * @return {boolean} true if the string is number only * */ + + function isNumber(content: string): boolean { return /^\d+$/.test(content); } @@ -180,3 +182,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..b0fb85b03 100644 --- a/lib/utils/string_value_validator/index.ts +++ b/lib/utils/string_value_validator/index.ts @@ -19,6 +19,11 @@ * @param {unknown} input * @return {boolean} true for non-empty string, false otherwise */ + +import { Platform } from '../../platform_support'; + 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..ecc9916be 100644 --- a/lib/utils/type.ts +++ b/lib/utils/type.ts @@ -14,6 +14,9 @@ * limitations under the License. */ + +import { Platform } from '../platform_support'; + 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..3bb1b3e4e 100644 --- a/lib/utils/user_profile_service_validator/index.ts +++ b/lib/utils/user_profile_service_validator/index.ts @@ -22,7 +22,7 @@ import { ObjectWithUnknownProperties } from '../../shared_types'; import { INVALID_USER_PROFILE_SERVICE } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; - +import { Platform } from '../../platform_support'; /** * Validates user's provided user profile service instance * @param {unknown} userProfileServiceInstance @@ -30,6 +30,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 +42,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..19e8cbdca 100644 --- a/lib/vuid/vuid.ts +++ b/lib/vuid/vuid.ts @@ -15,6 +15,7 @@ */ import { v4 as uuidV4 } from 'uuid'; +import { Platform } from '../platform_support'; export const VUID_PREFIX = `vuid_`; export const VUID_MAX_LENGTH = 32; @@ -29,3 +30,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..1415b8ee4 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -17,6 +17,7 @@ import { LoggerFacade } from '../logging/logger'; import { Store } from '../utils/cache/store'; import { AsyncProducer, Maybe } from '../utils/type'; import { isVuid, makeVuid } from './vuid'; +import { Platform } from '../platform_support'; export interface VuidManager { getVuid(): Maybe; @@ -130,3 +131,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..eace086b3 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -16,6 +16,7 @@ import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; +import { Platform } from '../platform_support'; export const vuidCacheManager = new VuidCacheManager(); @@ -26,3 +27,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..9fbec12a4 100644 --- a/lib/vuid/vuid_manager_factory.node.ts +++ b/lib/vuid/vuid_manager_factory.node.ts @@ -14,7 +14,10 @@ * limitations under the License. */ import { OpaqueVuidManager, VuidManagerOptions, wrapVuidManager } from './vuid_manager_factory'; +import { Platform } from '../platform_support'; 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..57b2e9d3a 100644 --- a/lib/vuid/vuid_manager_factory.react_native.ts +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -16,6 +16,7 @@ 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'; +import { Platform } from '../platform_support'; export const vuidCacheManager = new VuidCacheManager(); @@ -26,3 +27,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..a4fa30100 100644 --- a/lib/vuid/vuid_manager_factory.ts +++ b/lib/vuid/vuid_manager_factory.ts @@ -17,6 +17,7 @@ import { Store } from '../utils/cache/store'; import { Maybe } from '../utils/type'; import { VuidManager } from './vuid_manager'; +import { Platform } from '../platform_support'; export type VuidManagerOptions = { vuidCache?: Store; @@ -42,3 +43,5 @@ export const wrapVuidManager = (vuidManager: Maybe): OpaqueVuidMana [vuidManagerSymbol]: vuidManager } }; + +export const __platforms: Platform[] = ['__universal__']; diff --git a/message_generator.ts b/message_generator.ts index fae725a1c..5571f84e7 100644 --- a/message_generator.ts +++ b/message_generator.ts @@ -18,7 +18,7 @@ const generate = async () => { let genOut = ''; Object.keys(exports).forEach((key, i) => { - if (key === 'messages') return; + if (key === 'messages' || key === '__platforms') return; genOut += `export const ${key} = '${i}';\n`; messages.push(exports[key]) }); diff --git a/package-lock.json b/package-lock.json index deb59a64d..ace39b5be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,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 +43,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", @@ -2949,6 +2951,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 +3005,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 +4885,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 +4969,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 +7311,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 +7419,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 +8182,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 +9317,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 +9328,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 +10427,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 +10795,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 +13480,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..148ec7e9f 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": "./scripts/platform-validator.js --validate", + "fix-platform-export": "./scripts/platform-validator.js --fix-export", + "test-isolation-rules": "./scripts/test-validator.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", @@ -114,6 +117,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 +128,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..43906185c --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,128 @@ +# Scripts + +This directory contains build and validation scripts for the JavaScript SDK. + +## platform-validator.js + +Main entry point for platform isolation validation and fixing. Provides a unified CLI interface. + +### Usage + +```bash +# Validate platform isolation (default) +npm run validate-platform-isolation +./scripts/platform-validator.js --validate +./scripts/platform-validator.js # --validate is default + +# Fix platform export issues +npm run fix-platform-export +./scripts/platform-validator.js --fix-export +``` + +**Note:** Cannot specify both `--validate` and `--fix-export` options at the same time. + +## validate-platform-isolation.js + +The 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.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 configured in the in the `.platform-isolation.config.js` config file. +2. **Requires every configured 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. +- Example: A browser file can import from universal files or any file that supports browser + +**Note:** The validator can be updated to 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 used for convenience. + + +## fix-platform-export.js + +Auto-fix script that adds or updates `__platforms` exports in files. This script helps maintain platform isolation by automatically fixing issues with platform export declarations. + +**Important:** This script only fixes `__platforms` export issues. It does not fix import compatibility issues - those must be resolved manually. + +### Usage + +```bash +# Run via npm script (recommended) +npm run fix-platform-export + +# Or via platform-validator +./scripts/platform-validator.js --fix-export + +# Or run directly +./scripts/fix-platform-export.js +``` + +### How It Works + +The script: +1. Scans all TypeScript/JavaScript files configured in `.platform-isolation.config.js` +2. **Ensures correct Platform import**: Normalizes all Platform imports to use the correct path and format +3. For each file, checks if it has a valid `__platforms` export +4. **Determines platform from filename**: Files with platform-specific naming (`.browser.ts`, `.node.ts`, `.react_native.ts`) get their specific platform(s) +5. **Defaults to universal**: Files without platform-specific naming get `['__universal__']` +6. **Moves export to end**: Places `__platforms` export at the end of the file for consistency +7. Preserves existing platform values for files that already have valid `__platforms` exports + +### Actions + +- **Added**: File was missing `__platforms` export - now added +- **Fixed**: File had invalid or incorrectly formatted `__platforms` export - now corrected +- **Moved**: File had valid `__platforms` export but not at the end - moved to end +- **Skipped**: File already has valid `__platforms` export at the end + +### Platform Detection + +- `file.browser.ts` → `['browser']` +- `file.node.ts` → `['node']` +- `file.react_native.ts` → `['react_native']` +- `file.browser.node.ts` → `['browser', 'node']` +- `file.ts` → `['__universal__']` + +## test-validator.js + +Comprehensive test suite for the platform isolation rules. Documents and validates all compatibility rules. + +### Usage + +```bash +# Run via npm script +npm run test-isolation-rules + +# 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/fix-platform-export.js b/scripts/fix-platform-export.js new file mode 100644 index 000000000..0a023fdab --- /dev/null +++ b/scripts/fix-platform-export.js @@ -0,0 +1,428 @@ +#!/usr/bin/env node + +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Fix platform export issues + * + * This script automatically fixes __platforms export issues in files: + * - Adds missing __platforms exports + * - Fixes invalid __platforms declarations + * - Moves __platforms to end of file + * - Ensures correct Platform import + * + * Note: This only fixes __platforms export issues. Import compatibility issues + * must be resolved manually. + * + * 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 import and type annotation + * 4. Inserts __platforms export at the end of the file + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { extractPlatformsFromFile, findSourceFiles } = require('./platform-utils'); + +const WORKSPACE_ROOT = path.join(__dirname, '..'); +const PLATFORMS = ['browser', 'node', 'react_native']; + +function getPlatformFromFilename(filename) { + const platforms = []; + const basename = path.basename(filename); + + // Extract all parts before the extension + // e.g., "file.browser.node.ts" -> ["file", "browser", "node"] + const parts = basename.split('.'); + + // Skip the last part (extension) + if (parts.length < 2) { + return null; + } + + // Check parts from right to left (excluding extension) for platform names + for (let i = parts.length - 2; i >= 0; i--) { + const part = parts[i]; + if (PLATFORMS.includes(part)) { + platforms.unshift(part); // Add to beginning to maintain order + } else { + // Stop when we encounter a non-platform part + break; + } + } + + return platforms.length > 0 ? platforms : null; +} + +/** + * 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)) return; + + lastImportEnd = node.end; + + const moduleSpecifier = node.moduleSpecifier; + if (!ts.isStringLiteral(moduleSpecifier)) return; + + // Check if this import is from platform_support + if (!moduleSpecifier.text.includes('platform_support')) return; + + // Check if it imports Platform type + if (!node.importClause?.namedBindings) return; + + const namedBindings = node.importClause.namedBindings; + if (!ts.isNamedImports(namedBindings)) return; + + for (const element of namedBindings.elements) { + if (element.name.text === 'Platform') { + hasPlatformImport = true; + break; + } + } + } + + 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 }; + } +} + +/** + * Check if __platforms export is at the end of the file + */ +function isPlatformExportAtEnd(content, filePath) { + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + let platformExportEnd = -1; + let lastStatementEnd = -1; + + function visit(node) { + // Track the last statement + if (ts.isStatement(node) && node.parent === sourceFile) { + lastStatementEnd = Math.max(lastStatementEnd, node.end); + } + + // Find __platforms export + if (!ts.isVariableStatement(node)) return; + + const hasExport = node.modifiers?.some( + mod => mod.kind === ts.SyntaxKind.ExportKeyword + ); + + if (!hasExport) return; + + for (const declaration of node.declarationList.declarations) { + if (ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.name.text === '__platforms') { + platformExportEnd = node.end; + } + } + } + + ts.forEachChild(sourceFile, visit); + + if (platformExportEnd === -1) { + return false; // No export found + } + + // Check if __platforms is the last statement (allowing for trailing whitespace/newlines) + return platformExportEnd === lastStatementEnd; +} + +/** + * Extract the existing __platforms export statement as-is + */ +function extractExistingPlatformExport(content, filePath) { + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true + ); + + const lines = content.split('\n'); + let exportStatement = null; + const linesToRemove = new Set(); + + function visit(node) { + if (!ts.isVariableStatement(node)) return; + + const hasExport = node.modifiers?.some( + mod => mod.kind === ts.SyntaxKind.ExportKeyword + ); + + if (!hasExport) return; + + for (const declaration of node.declarationList.declarations) { + if (!ts.isVariableDeclaration(declaration) || + !ts.isIdentifier(declaration.name) || + declaration.name.text !== '__platforms') { + continue; + } + + // Extract the full statement + const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + + const statementLines = []; + for (let i = startLine; i <= endLine; i++) { + linesToRemove.add(i); + statementLines.push(lines[i]); + } + + // If the next line is whitespace, extract it as well + if (endLine + 1 < lines.length && lines[endLine + 1].trim() === '') { + linesToRemove.add(endLine + 1); + } + + exportStatement = statementLines.join('\n'); + } + } + + ts.forEachChild(sourceFile, visit); + + if (!exportStatement) { + return { restContent: content, platformExportStatement: null }; + } + + const filteredLines = lines.filter((_, index) => !linesToRemove.has(index)); + return { restContent: filteredLines.join('\n'), platformExportStatement: exportStatement }; +} + +/** + * Add __platforms export at the end of the file + */ +function addPlatformExportStatement(content, statement) { + // Trim trailing whitespace and add the statement (with blank line before) + return content.trimEnd() + '\n\n' + statement + '\n'; +} + +/** + * Add __platforms export at the end of the file (when creating new) + */ +function addPlatformExport(content, platforms) { + const platformsStr = platforms.map(p => `'${p}'`).join(', '); + const exportStatement = `export const __platforms: Platform[] = [${platformsStr}];`; + return addPlatformExportStatement(content, exportStatement); +} + +/** + * Process a single file + */ +function processFile(filePath) { + let content = fs.readFileSync(filePath, 'utf-8'); + + // Use extractPlatformsFromFile which validates platform values + const result = extractPlatformsFromFile(filePath); + + // Extract platforms and error info from result + const existingPlatforms = result.success ? result.platforms : null; + const needsFixing = result.error && ['MISSING', 'NOT_ARRAY', 'EMPTY_ARRAY', 'NOT_LITERALS', 'INVALID_VALUES'].includes(result.error.type); + + // Determine platforms for this file + // If file already has valid platforms, use those (preserve existing values) + // Otherwise, determine from filename or default to universal + let platforms; + if (needsFixing) { + // No __platforms export or has errors, determine from filename + const platformsFromFilename = getPlatformFromFilename(filePath); + platforms = platformsFromFilename || ['__universal__']; + } else { + // Has valid __platforms, preserve the existing values + platforms = existingPlatforms; + } + + let modified = false; + let action = 'skipped'; + + if (needsFixing) { + // Has issues (MISSING, NOT_ARRAY, NOT_LITERALS, INVALID_VALUES, etc.), fix them + const extracted = extractExistingPlatformExport(content, filePath); + content = extracted.restContent; + + if (extracted.platformExportStatement) { + // There was an existing export statement, we removed it + // so we will be replacing it with a fixed one + action = 'fixed'; + } else { + action = 'added'; + } + modified = true; + + // Ensure Platform import exists + const importResult = ensurePlatformImport(content, filePath); + content = importResult.content; + + // Add __platforms export at the end + content = addPlatformExport(content, platforms); + } else if (existingPlatforms) { + // Has valid __platforms structure - check if it's already at the end + if (isPlatformExportAtEnd(content, filePath)) { + return { skipped: true, reason: 'already has export at end' }; + } + + // Extract it and move to end without modification + const extracted = extractExistingPlatformExport(content, filePath); + content = extracted.restContent; + + // Add the original statement at the end + content = addPlatformExportStatement(content, extracted.platformExportStatement); + action = 'moved'; + modified = true; + } + + if (modified) { + // Write back to file + fs.writeFileSync(filePath, content, 'utf-8'); + + return { skipped: false, action, platforms }; + } + + return { skipped: true, reason: 'no changes needed' }; +} + +function main() { + console.log('🔧 Processing __platforms exports...\n'); + + const files = findSourceFiles(); + 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(', ')}]`); + break; + case 'moved': + moved++; + console.log(`📍 ${relativePath} → moved to end [${result.platforms.join(', ')}]`); + break; + case 'fixed': + fixed++; + console.log(`🔧 ${relativePath} → fixed [${result.platforms.join(', ')}]`); + 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..278eaa153 --- /dev/null +++ b/scripts/platform-utils.js @@ -0,0 +1,354 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Platform Utilities + * + * Shared utilities for platform isolation validation + */ + + +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-inner-declarations */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { minimatch } = require('minimatch'); + +// Cache for valid platforms +let validPlatformsCache = null; + +// Cache for file platforms +const filePlatformCache = new Map(); + +// Cache for config +let configCache = null; + +/** + * Load platform isolation configuration + * + * @returns {Object} Configuration object with include/exclude patterns + */ +function loadConfig() { + if (configCache) { + return configCache; + } + + const workspaceRoot = path.join(__dirname, '..'); + const configPath = path.join(workspaceRoot, '.platform-isolation.config.js'); + + configCache = 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/**' + ] + }; + + return configCache; +} + +/** + * Extract valid platform values from Platform type definition in platform_support.ts + * Parses: type Platform = 'browser' | 'node' | 'react_native' | '__universal__'; + * + * @returns {string[]} Array of valid platform identifiers + */ +function getValidPlatforms() { + if (validPlatformsCache) { + return validPlatformsCache; + } + + const workspaceRoot = path.join(__dirname, '..'); + 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 only top-level statements since Platform type must be exported at top level + for (const node of sourceFile.statements) { + // 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); + } + } + } else if (ts.isLiteralTypeNode(node.type) && ts.isStringLiteral(node.type.literal)) { + // Handle single literal type: type Platform = 'browser'; + platforms.push(node.type.literal.text); + } + + break; // Found it, stop searching + } + } + + if (platforms.length === 0) { + throw new Error(`Could not extract Platform type from ${platformSupportPath}`); + } + + validPlatformsCache = platforms; + return validPlatformsCache; +} + +/** + * Extracts __platforms array from TypeScript AST with detailed error reporting + * + * Returns an object with: + * - success: boolean - whether extraction was successful + * - platforms: string[] - array of platform values (if successful) + * - error: object - detailed error information (if unsuccessful) + * - type: 'MISSING' | 'NOT_CONST' | 'NOT_ARRAY' | 'EMPTY_ARRAY' | 'NOT_LITERALS' | 'INVALID_VALUES' + * - message: string - human-readable error message + * - invalidValues: string[] - list of invalid platform values (for INVALID_VALUES type) + * + * @param {ts.SourceFile} sourceFile - TypeScript source file AST + * @param {string[]} validPlatforms - Array of valid platform values + * @returns {Object} + */ +function extractPlatformsFromAST(sourceFile, validPlatforms) { + let found = false; + let isArray = false; + let platforms = []; + let hasNonStringLiteral = false; + + // Visit only top-level children since __platforms must be exported at top level + for (const node of sourceFile.statements) { + // Look for: export const __platforms = [...] + if (!ts.isVariableStatement(node)) continue; + + // Check if it has export modifier + const hasExport = node.modifiers?.some( + mod => mod.kind === ts.SyntaxKind.ExportKeyword + ); + if (!hasExport) continue; + + for (const declaration of node.declarationList.declarations) { + if (!ts.isVariableDeclaration(declaration) || + !ts.isIdentifier(declaration.name) || + declaration.name.text !== '__platforms') { + continue; + } + + found = 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; + } + + // Check if it's an array + if (initializer && ts.isArrayLiteralExpression(initializer)) { + isArray = true; + + // Extract array elements + 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; + } + } + } + + break; // Found it, stop searching + } + + if (found) break; + } + + // Detailed error reporting + if (!found) { + return { + success: false, + error: { + type: 'MISSING', + message: `File does not export '__platforms' array` + } + }; + } + + if (!isArray) { + return { + success: false, + error: { + type: 'NOT_ARRAY', + message: `'__platforms' must be an array literal, found ${platforms.length === 0 ? 'non-array value' : 'other type'}` + } + }; + } + + if (hasNonStringLiteral) { + return { + success: false, + error: { + type: 'NOT_LITERALS', + message: `'__platforms' must only contain string literals, found non-literal values` + } + }; + } + + if (platforms.length === 0) { + return { + success: false, + error: { + type: 'EMPTY_ARRAY', + message: `'__platforms' array is empty, must contain at least one platform` + } + }; + } + + // Validate platform values if validPlatforms provided + if (validPlatforms) { + const invalidPlatforms = platforms.filter(p => !validPlatforms.includes(p)); + if (invalidPlatforms.length > 0) { + return { + success: false, + error: { + type: 'INVALID_VALUES', + message: `Invalid platform values found`, + invalidValues: invalidPlatforms + } + }; + } + } + + return { + success: true, + platforms: platforms + }; +} + +/** + * Extract platforms from a file path with detailed error reporting + * Uses caching to avoid re-parsing the same file multiple times. + * + * @param {string} absolutePath - Absolute path to the file + * @returns {Object} Result object with success, platforms, and error information + */ +function extractPlatformsFromFile(absolutePath) { + // Check cache first + if (filePlatformCache.has(absolutePath)) { + return filePlatformCache.get(absolutePath); + } + + let result; + try { + const validPlatforms = getValidPlatforms(); + const content = fs.readFileSync(absolutePath, 'utf-8'); + const sourceFile = ts.createSourceFile( + absolutePath, + content, + ts.ScriptTarget.Latest, + true + ); + result = extractPlatformsFromAST(sourceFile, validPlatforms); + } catch (error) { + result = { + success: false, + error: { + type: 'READ_ERROR', + message: `Failed to read or parse file: ${error.message}` + } + }; + } + + filePlatformCache.set(absolutePath, result); + return result; +} + +/** + * Find all source files matching include/exclude patterns from config + * + * @returns {string[]} Array of absolute file paths + */ +function findSourceFiles() { + const workspaceRoot = path.join(__dirname, '..'); + const config = loadConfig(); + + /** + * Check if file matches any pattern using minimatch + */ + function matchesPattern(filePath, patterns, options = {}) { + const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/'); + return patterns.some(pattern => minimatch(relativePath, pattern, options)); + } + + /** + * Recursively find all files matching include patterns and not matching exclude patterns + */ + function findFilesRecursive(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 + if (matchesPattern(fullPath, config.include, { partial: true })) { + findFilesRecursive(fullPath, files); + } + } else if (entry.isFile()) { + // Check if file matches include patterns and is NOT excluded + if (matchesPattern(fullPath, config.include) + && !matchesPattern(fullPath, config.exclude)) { + files.push(fullPath); + } + } + } + + return files; + } + + return findFilesRecursive(workspaceRoot); +} + +module.exports = { + getValidPlatforms, + extractPlatformsFromAST, + extractPlatformsFromFile, + findSourceFiles, + loadConfig, +}; diff --git a/scripts/platform-validator.js b/scripts/platform-validator.js new file mode 100755 index 000000000..b0077a837 --- /dev/null +++ b/scripts/platform-validator.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Platform validator CLI + * + * Provides a unified interface for validating and fixing platform isolation issues. + * + * Usage: + * node platform-validator.js --validate # Validate platform isolation (default) + * node platform-validator.js --fix-export # Fix platform export issues + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { execSync } = require('child_process'); + +function main() { + const args = process.argv.slice(2); + + const hasValidate = args.includes('--validate'); + const hasFixExport = args.includes('--fix-export'); + + // Check if both options are provided + if (hasValidate && hasFixExport) { + console.error('❌ Error: Cannot specify both --validate and --fix-export options'); + process.exit(1); + } + + // Determine which script to run (default to validate) + const shouldFix = hasFixExport; + + try { + if (shouldFix) { + console.log('🔧 Fixing platform export issues...\n'); + execSync('node scripts/fix-platform-export.js', { stdio: 'inherit' }); + } else { + console.log('🔍 Running platform isolation validation...\n'); + execSync('node scripts/validate-platform-isolation.js', { stdio: 'inherit' }); + } + } catch (error) { + process.exit(error.status || 1); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { main }; diff --git a/scripts/test-validator.js b/scripts/test-validator.js new file mode 100644 index 000000000..8c6d6dc03 --- /dev/null +++ b/scripts/test-validator.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node + +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Comprehensive test suite for platform isolation validator + * + * This test documents and validates all the compatibility rules + */ + + +/* eslint-disable @typescript-eslint/no-var-requires */ +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); +test('Universal file can import universal', + validator.isPlatformCompatible(['__universal__'], ['__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 can import from universal file', + validator.isPlatformCompatible(['browser'], ['__universal__']), 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('Node file can import from universal file', + validator.isPlatformCompatible(['node'], ['__universal__']), true); +test('React Native file can import from react_native file', + validator.isPlatformCompatible(['react_native'], ['react_native']), true); +test('React Native file can import from universal file', + validator.isPlatformCompatible(['react_native'], ['__universal__']), true); + + +console.log('\n3. UNIVERSAL IMPORTING FROM NON-UNIVERSAL'); +console.log('-'.repeat(70)); +test('Universal file CANNOT import from browser file', + validator.isPlatformCompatible(['__universal__'], ['browser']), false); +test('Universal file CANNOT import from node file', + validator.isPlatformCompatible(['__universal__'], ['node']), false); +test('Universal file CANNOT import from react_native file', + validator.isPlatformCompatible(['__universal__'], ['react_native']), false); +test('Universal file CANNOT import from [browser, react_native] file', + validator.isPlatformCompatible(['__universal__'], ['browser', 'react_native']), false); +test('Universal file CANNOT import from [browser, node] file', + validator.isPlatformCompatible(['__universal__'], ['browser', 'node']), false); + + +console.log('\n4. 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('\n5. 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 CAN import from universal file', + validator.isPlatformCompatible(['browser', 'react_native'], ['__universal__']), 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('\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.js b/scripts/validate-platform-isolation.js new file mode 100644 index 000000000..262c57d36 --- /dev/null +++ b/scripts/validate-platform-isolation.js @@ -0,0 +1,470 @@ +#!/usr/bin/env node + +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Platform Isolation Validator + * + * This script ensures that platform-specific modules only import + * from universal or compatible platform modules. + * + * Platform Detection: + * - ALL source files (except tests) MUST export __platforms array + * - Universal files use: export const __platforms = ['__universal__']; + * - Platform-specific files use platform names, e.g: 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 + */ + +/* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); +const { minimatch } = require('minimatch'); +const { getValidPlatforms, extractPlatformsFromFile, findSourceFiles, loadConfig } = require('./platform-utils'); + +const WORKSPACE_ROOT = path.join(__dirname, '..'); + +// Load tsconfig to get module resolution settings +const tsconfigPath = path.join(WORKSPACE_ROOT, 'tsconfig.json'); +const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf-8'); +const tsconfig = ts.parseConfigFileTextToJson(tsconfigPath, tsconfigContent).config; +const compilerOptions = ts.convertCompilerOptionsFromJson( + tsconfig.compilerOptions, + WORKSPACE_ROOT +).options; + +// Load configuration +const config = loadConfig(); +const configPath = path.join(WORKSPACE_ROOT, '.platform-isolation.config.js'); + +// Track files with errors in __platforms export +const fileErrors = new Map(); + +/** + * Gets the supported platforms for a file + * Returns: + * - string[] (platforms from __platforms) + * - null (file has errors) + * + * Note: ALL files must have __platforms export + */ +function getSupportedPlatforms(filePath) { + // Extract platforms from file with detailed error reporting (uses cache internally) + const result = extractPlatformsFromFile(filePath); + + if (result.success) { + return result.platforms; + } else { + // Store error for this file + fileErrors.set(filePath, result.error); + return null; + } +} + +/** + * 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 + * + * Note: Assumes platforms is a valid array (after first validation pass) + */ +function formatPlatforms(platforms) { + if (isUniversal(platforms)) return 'Universal (all 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: + * - 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 + * + * Note: This function assumes both parameters are valid platform arrays (non-null) + */ +function isPlatformCompatible(filePlatforms, importPlatforms) { + // 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 using TypeScript's module resolution + */ +function resolveImportPath(importPath, currentFilePath) { + // External imports (node_modules) - return as-is + if (!importPath.startsWith('.') && !importPath.startsWith('/')) { + return { isExternal: true, resolved: importPath }; + } + + // Use TypeScript's module resolution with settings from tsconfig + const result = ts.resolveModuleName( + importPath, + currentFilePath, + compilerOptions, + ts.sys + ); + + if (result.resolvedModule) { + return { isExternal: false, resolved: result.resolvedModule.resolvedFileName }; + } + + // If TypeScript can't resolve, throw an error + throw new Error(`Cannot resolve import "${importPath}" from ${path.relative(WORKSPACE_ROOT, currentFilePath)}`); +} + +/** + * Validate a single file + */ +function validateFile(filePath) { + const filePlatforms = getSupportedPlatforms(filePath); + 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; + } + + // Skip excluded files (e.g., platform_support.ts) + if (matchesPattern(resolved, config.exclude)) { + continue; + } + + const importPlatforms = getSupportedPlatforms(resolved); + + // Check compatibility + if (!isPlatformCompatible(filePlatforms, importPlatforms)) { + const message = `${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, options = {}) { + const relativePath = path.relative(WORKSPACE_ROOT, filePath).replace(/\\/g, '/'); + + return patterns.some(pattern => minimatch(relativePath, pattern, options)); +} + +/** + * Report platform export errors by type + */ +function reportPlatformErrors(errorsByType, validPlatforms) { + let hasErrors = false; + + const errorConfig = { + MISSING: { + message: (count) => `❌ Found ${count} file(s) missing __platforms export:\n` + }, + NOT_ARRAY: { + message: (count) => `❌ Found ${count} file(s) with __platforms not declared as an array:\n` + }, + EMPTY_ARRAY: { + message: (count) => `❌ Found ${count} file(s) with empty __platforms array:\n` + }, + NOT_LITERALS: { + message: (count) => `❌ Found ${count} file(s) with __platforms containing non-literal values:\n` + }, + INVALID_VALUES: { + message: (count) => `❌ Found ${count} file(s) with invalid platform values:\n`, + customHandler: (error) => { + const invalidValuesStr = error.invalidValues ? error.invalidValues.map(v => `${v}`).join(', ') : ''; + console.error(` Invalid values: ${invalidValuesStr}`); + console.error(` Valid platforms: ${validPlatforms.join(', ')}`); + } + }, + READ_ERROR: { + message: (count) => `❌ Found ${count} file(s) with read errors:\n`, + customHandler: (error) => { + console.error(` ${error.message}`); + } + } + }; + + for (const [errorType, config] of Object.entries(errorConfig)) { + const errors = errorsByType[errorType]; + if (errors.length === 0) continue; + + hasErrors = true; + console.error(config.message(errors.length)); + + for (const { filePath, error } of errors) { + console.error(` 📄 ${path.relative(WORKSPACE_ROOT, filePath)}`); + if (config.customHandler) { + config.customHandler(error); + } + } + console.error('\n'); + } + + return hasErrors; +} + + + +/** + * Main validation function + */ +function main() { + console.log('🔍 Validating platform isolation...\n'); + console.log(`📋 Configuration: ${path.relative(WORKSPACE_ROOT, configPath) || '.platform-isolation.config.js'}\n`); + + const files = findSourceFiles(); + + // Load valid platforms first + const validPlatforms = getValidPlatforms(); + 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 fileErrors + + // Group errors by type + const errorsByType = { + MISSING: [], + NOT_ARRAY: [], + EMPTY_ARRAY: [], + NOT_LITERALS: [], + INVALID_VALUES: [], + READ_ERROR: [] + }; + + for (const [filePath, error] of fileErrors) { + if (errorsByType[error.type]) { + errorsByType[error.type].push({ filePath, error }); + } + } + + // Report errors by type + const hasErrors = reportPlatformErrors(errorsByType, validPlatforms); + + if (hasErrors) { + process.exit(1); + } + + console.log('✅ All files have valid __platforms exports\n'); + + // Second pass: validate platform isolation + // At this point, all files are guaranteed to have valid __platforms exports + 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(WORKSPACE_ROOT, 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, + 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); + } +}