Skip to content

Commit fe00123

Browse files
authored
[FSSDK-12092] add platform verification for imports (#1112)
This PR implements a comprehensive platform isolation system for a multi-platform JavaScript SDK that supports Browser, Node.js, and React Native. The system enforces that platform-specific code cannot be imported by incompatible platforms, preventing runtime errors when bundling for different targets. **Key changes:** - Adds validation scripts that check `__platforms` exports and import compatibility - Adds script to automatically add `__platforms` exports based on file name - Integrates ESLint rule to enforce platform declarations at development time - Adds extensive documentation and troubleshooting guides - Updates all source files to include `__platforms` exports
1 parent 860fca0 commit fe00123

File tree

140 files changed

+2753
-86
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+2753
-86
lines changed

.eslintrc.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ module.exports = {
88
'eslint:recommended',
99
'plugin:@typescript-eslint/recommended',
1010
],
11+
parser: '@typescript-eslint/parser',
12+
plugins: ['@typescript-eslint', 'local-rules'],
1113
globals: {
1214
Atomics: 'readonly',
1315
SharedArrayBuffer: 'readonly',
@@ -25,6 +27,23 @@ module.exports = {
2527
'rules': {
2628
'@typescript-eslint/explicit-module-boundary-types': ['error']
2729
}
30+
},
31+
{
32+
'files': ['lib/**/*.ts', 'src/**/*.ts'],
33+
'excludedFiles': [
34+
'**/platform_support.ts',
35+
'**/*.spec.ts',
36+
'**/*.test.ts',
37+
'**/*.tests.ts',
38+
'**/*.test-d.ts',
39+
'**/*.gen.ts',
40+
'**/*.d.ts',
41+
'**/__mocks__/**',
42+
'**/tests/**'
43+
],
44+
'rules': {
45+
'local-rules/require-platform-declaration': 'error',
46+
}
2847
}
2948
],
3049
rules: {

.platform-isolation.config.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Platform Isolation Configuration
3+
*
4+
* Configures which files should be validated by the platform isolation validator.
5+
*/
6+
7+
module.exports = {
8+
// Base directories to scan for source files
9+
include: [
10+
'lib/**/*.ts',
11+
'lib/**/*.js'
12+
],
13+
14+
// Files and patterns to exclude from validation
15+
exclude: [
16+
// Platform definition file (this file defines Platform type, doesn't need __platforms)
17+
'**/platform_support.ts',
18+
19+
// Test files
20+
'**/*.spec.ts',
21+
'**/*.test.ts',
22+
'**/*.tests.ts',
23+
'**/*.test.js',
24+
'**/*.spec.js',
25+
'**/*.tests.js',
26+
'**/*.umdtests.js',
27+
'**/*.test-d.ts',
28+
29+
// Generated files
30+
'**/*.gen.ts',
31+
32+
// Type declaration files
33+
'**/*.d.ts',
34+
35+
// Test directories and mocks
36+
'**/__mocks__/**',
37+
'**/tests/**'
38+
]
39+
};

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ If you're updating your SDK version, please check the appropriate migration guid
207207

208208
## SDK Development
209209

210+
### Platform Isolation
211+
212+
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:
213+
214+
- Every source file must declare which platforms it supports using `export const __platforms: Platform[] = [...]`
215+
- Files can only import from other files that support all their declared platforms
216+
- Universal files (`__platforms = ['__universal__']`) work everywhere but can only import from other universal files
217+
218+
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.
219+
220+
**For detailed documentation**, see [docs/PLATFORM_ISOLATION.md](docs/PLATFORM_ISOLATION.md).
221+
210222
### Unit Tests
211223

212224
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.

docs/PLATFORM_ISOLATION.md

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# Platform Isolation
2+
3+
## Overview
4+
5+
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.
6+
7+
## Platform Declaration
8+
9+
**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.
10+
11+
### Export Declaration (Required)
12+
13+
All files must include a `__platforms` export:
14+
15+
**For universal files (all platforms):**
16+
```typescript
17+
export const __platforms = ['__universal__'];
18+
```
19+
20+
**For platform-specific files:**
21+
```typescript
22+
export const __platforms = ['browser']; // Browser only
23+
export const __platforms = ['node']; // Node.js only
24+
export const __platforms = ['react_native']; // React Native only
25+
```
26+
27+
**For multi-platform files:**
28+
29+
```typescript
30+
// lib/utils/web-features.ts
31+
export const __platforms = ['browser', 'react_native'];
32+
33+
// Your code that works on both browser and react_native
34+
export function makeHttpRequest() {
35+
// Implementation that works on both platforms
36+
}
37+
```
38+
39+
Valid platform identifiers: `'browser'`, `'node'`, `'react_native'`, `'__universal__'`
40+
41+
**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.
42+
43+
### File Naming Convention (Optional)
44+
45+
While not enforced, you should use file name suffixes for clarity:
46+
- `.browser.ts` - Typically browser-specific
47+
- `.node.ts` - Typically Node.js-specific
48+
- `.react_native.ts` - Typically React Native-specific
49+
- `.ts` (no suffix) - Typically universal
50+
51+
**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.
52+
53+
## Import Rules
54+
55+
Each platform-specific file can **only** import from:
56+
57+
1. **Universal files** (no platform restrictions)
58+
2. **Compatible platform files** (files that support ALL the required platforms)
59+
3. **External packages** (node_modules)
60+
61+
A file is compatible if:
62+
- It's universal (no platform restrictions)
63+
- For single-platform files: The import supports at least that platform
64+
- For multi-platform files: The import supports ALL of those platforms
65+
66+
### Compatibility Examples
67+
68+
**Core Principle**: When file A imports file B, file B must support ALL platforms that file A runs on.
69+
70+
**Universal File (`__platforms = ['__universal__']`)**
71+
- ✅ Can import from: universal files (with `__universal__`)
72+
- ❌ Cannot import from: any platform-specific files, even `['browser', 'node', 'react_native']`
73+
- **Why**: Universal files run everywhere, so all imports must explicitly be universal
74+
- **Note**: Listing all platforms like `['browser', 'node', 'react_native']` is NOT considered universal
75+
76+
**Single Platform File (`__platforms = ['browser']`)**
77+
- ✅ Can import from: universal files, files with `['browser']`, multi-platform files that include browser like `['browser', 'react_native']`
78+
- ❌ Cannot import from: files without browser support like `['node']` or `['react_native']` only
79+
- **Why**: The import must support the browser platform
80+
81+
**Multi-Platform File (`__platforms = ['browser', 'react_native']`)**
82+
- ✅ Can import from: universal files, files with `['browser', 'react_native']`, supersets like `['browser', 'node', 'react_native']`
83+
- ❌ Cannot import from: files missing any platform like `['browser']` only or `['node']`
84+
- **Why**: The import must support BOTH browser AND react_native
85+
86+
**All-Platforms File (`__platforms = ['browser', 'node', 'react_native']`)**
87+
- ✅ Can import from: universal files, files with exactly `['browser', 'node', 'react_native']`
88+
- ❌ Cannot import from: any subset like `['browser']`, `['browser', 'react_native']`, etc.
89+
- **Why**: This is NOT considered universal - imports must support all three platforms
90+
- **Note**: If your code truly works everywhere, use `['__universal__']` instead
91+
92+
### Examples
93+
94+
**Valid Imports**
95+
96+
```typescript
97+
// In lib/index.browser.ts (Browser platform only)
98+
import { Config } from './shared_types'; // ✅ Universal file
99+
import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; // ✅ browser + react_native (supports browser)
100+
import { uuid } from 'uuid'; // ✅ External package
101+
```
102+
103+
```typescript
104+
// In lib/index.node.ts (Node platform only)
105+
import { Config } from './shared_types'; // ✅ Universal file
106+
import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; // ✅ Same platform
107+
```
108+
109+
```typescript
110+
// In lib/vuid/vuid_manager_factory.react_native.ts (React Native platform only)
111+
import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; // ✅ Compatible (react_native only)
112+
```
113+
114+
115+
116+
```typescript
117+
// In lib/event_processor/event_processor_factory.browser.ts (Browser platform only)
118+
import { Config } from '../shared_types'; // ✅ Universal file
119+
import defaultEventDispatcher from './event_dispatcher/default_dispatcher.browser'; // ✅ Compatible (browser + react_native, includes browser)
120+
```
121+
122+
```typescript
123+
// In lib/event_processor/event_dispatcher/default_dispatcher.browser.ts (Multi-platform: browser + react_native)
124+
import { Config } from '../../shared_types'; // ✅ Universal file
125+
import { BrowserRequestHandler } from '../../utils/http_request_handler/request_handler.browser'; // ✅ Compatible (also browser + react_native)
126+
```
127+
128+
**Invalid Imports**
129+
130+
```typescript
131+
// In lib/index.browser.ts (Browser platform only)
132+
import { NodeRequestHandler } from './utils/http_request_handler/request_handler.node'; // ❌ Node-only file
133+
```
134+
135+
```typescript
136+
// In lib/index.node.ts (Node platform only)
137+
import { BrowserRequestHandler } from './utils/http_request_handler/request_handler.browser'; // ❌ browser + react_native, doesn't support node
138+
```
139+
140+
```typescript
141+
// In lib/shared_types.ts (Universal file)
142+
import { AsyncStorageCache } from './utils/cache/async_storage_cache.react_native'; // ❌ React Native only, universal file needs imports that work everywhere
143+
```
144+
145+
```typescript
146+
// In lib/event_processor/event_dispatcher/default_dispatcher.browser.ts
147+
import { NodeRequestHandler } from '../../utils/http_request_handler/request_handler.node'; // ❌ Node-only, doesn't support browser or react_native
148+
// This file needs imports that work in BOTH browser AND react_native
149+
```
150+
151+
## Automatic Validation
152+
153+
Platform isolation is enforced automatically during the build process.
154+
155+
### Running Validation
156+
157+
```bash
158+
# Run validation manually
159+
npm run validate-platform-isolation
160+
161+
# Validation runs automatically before build
162+
npm run build
163+
```
164+
165+
### How It Works
166+
167+
The validation script (`scripts/validate-platform-isolation.js`):
168+
169+
1. Scans all TypeScript/JavaScript files configured in the in the `.platform-isolation.config.js` config file.
170+
2. **Verifies every file has a `__platforms` export** - fails immediately if any file is missing this
171+
3. **Validates all platform values** - ensures values in `__platforms` arrays are valid (read from Platform type)
172+
4. Parses import statements using TypeScript AST (ES6 imports, require, dynamic imports)
173+
5. **Checks import compatibility**: For each import, verifies that the imported file supports ALL platforms that the importing file runs on
174+
6. Fails the build if violations are found or if any file lacks `__platforms` export
175+
176+
**ESLint Integration:** The `require-platform-declaration` ESLint rule also enforces the `__platforms` export requirement during development.
177+
178+
### Build Integration
179+
180+
The validation is integrated into the build process:
181+
182+
```json
183+
{
184+
"scripts": {
185+
"build": "npm run validate-platform-isolation && tsc --noEmit && ..."
186+
}
187+
}
188+
```
189+
190+
If platform isolation is violated, the build will fail with a detailed error message showing:
191+
- Which files have violations
192+
- The line numbers of problematic imports
193+
- What platform the file belongs to
194+
- What platform it's incorrectly importing from
195+
196+
197+
198+
## Creating New Modules
199+
200+
### Universal Code
201+
202+
For code that works across all platforms, use `['__universal__']`:
203+
204+
**Example: Universal utility function**
205+
206+
```typescript
207+
// lib/utils/string-helpers.ts
208+
export const __platforms = ['__universal__'];
209+
210+
// Pure JavaScript that works everywhere
211+
export function capitalize(str: string): string {
212+
return str.charAt(0).toUpperCase() + str.slice(1);
213+
}
214+
215+
export function sanitizeKey(key: string): string {
216+
return key.replace(/[^a-zA-Z0-9_]/g, '_');
217+
}
218+
```
219+
220+
### Platform-Specific Code
221+
222+
**Single Platform**
223+
224+
1. **Add `__platforms` export** declaring the platform (e.g., `export const __platforms = ['browser'];`)
225+
2. Name the file with a platform suffix for clarity (e.g., `my-feature.browser.ts`)
226+
3. Only import from universal or compatible platform files
227+
228+
**Example:**
229+
230+
```typescript
231+
// lib/features/my-feature.ts (universal interface)
232+
export const __platforms = ['__universal__'];
233+
234+
export interface MyFeature {
235+
doSomething(): void;
236+
}
237+
238+
// lib/features/my-feature.browser.ts
239+
export const __platforms = ['browser'];
240+
241+
export class BrowserMyFeature implements MyFeature {
242+
doSomething(): void {
243+
// Browser-specific implementation
244+
}
245+
}
246+
247+
// lib/features/my-feature.node.ts
248+
export const __platforms = ['node'];
249+
250+
export class NodeMyFeature implements MyFeature {
251+
doSomething(): void {
252+
// Node.js-specific implementation
253+
}
254+
}
255+
```
256+
257+
**Multiple Platforms (But Not Universal)**
258+
259+
For code that works on multiple platforms but is not universal, use the `__platforms` export to declare the list of supported platforms:
260+
261+
**Example: Browser + React Native only**
262+
263+
```typescript
264+
// lib/utils/http-helpers.ts
265+
export const __platforms = ['browser', 'react_native'];
266+
267+
// This code works on both browser and react_native, but not node
268+
export function makeRequest(url: string): Promise<string> {
269+
// XMLHttpRequest is available in both browser and react_native
270+
return new Promise((resolve, reject) => {
271+
const xhr = new XMLHttpRequest();
272+
xhr.open('GET', url);
273+
xhr.onload = () => resolve(xhr.responseText);
274+
xhr.onerror = () => reject(new Error('Request failed'));
275+
xhr.send();
276+
});
277+
}
278+
```
279+
280+
## Benefits
281+
282+
- ✅ Prevents runtime errors from platform-incompatible code
283+
- ✅ Catches issues at build time, not in production
284+
- ✅ Makes platform boundaries explicit and maintainable
285+
- ✅ Ensures each bundle only includes relevant code

0 commit comments

Comments
 (0)