@flighthq/platform 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @flighthq/platform
2
+
3
+ Environment identification — OS name, family, architecture, runtime shell, and engine — over a swappable web/native backend.
4
+
5
+ `platform` is the **root identification seam** of the desktop/mobile integration suite. Every public function is a free function that delegates to the active `PlatformBackend`. A web/DOM backend is lazily available, so every function works on the web; a native host (Electron, Tauri, Capacitor, a C/C++ shell) replaces it via `setPlatformBackend`. Snapshot reads fill an `out` value and return it. Unknown or unavailable fields resolve to sentinels (`''` / `-1` / `'unknown'` / `false`), never throwing. Import is side-effect-free: nothing registers until a host calls `setPlatformBackend`.
6
+
7
+ This package answers _what host am I running on_. Live, event-bearing, or richer concerns live in their own cells (see the delegation table) — `PlatformInfo` is a static identity snapshot.
8
+
9
+ ## Functions
10
+
11
+ | Function | Purpose |
12
+ | --- | --- |
13
+ | `comparePlatformVersions(a, b)` | Compare two dotted version strings numerically, segment by segment. Returns `-1` / `0` / `1`; `''` sorts lowest. |
14
+ | `createPlatformInfo()` | Allocate a zeroed `PlatformInfo` (strings `''`, `kind`/`name`/`runtime`/`engine` `'unknown'`, `endianness` `'unknown'`, `pointerWidth` `-1`, `isTouch` `false`) to pass as `out`. |
15
+ | `createWebPlatformBackend()` | Build the default web backend. |
16
+ | `getPlatformBackend()` | Return the active backend, lazily creating the web default. There is always a backend. |
17
+ | `getPlatformEngine()` | Return the browser engine family. Convenience over `getPlatformInfo`. |
18
+ | `getPlatformInfo(out)` | Fill `out` with the running platform's identity snapshot and return it. |
19
+ | `getPlatformKind()` | Return the platform family (`desktop` / `mobile` / `web` / `unknown`). Convenience over `getPlatformInfo`. |
20
+ | `getPlatformName()` | Return the OS/runtime name. Convenience over `getPlatformInfo`. |
21
+ | `getPlatformRuntime()` | Return the host shell / runtime environment. Convenience over `getPlatformInfo`. |
22
+ | `isPlatformDesktop()` | `true` on a desktop host (Electron/Tauri/native window shell). |
23
+ | `isPlatformMobile()` | `true` on a mobile host (iOS/Android via Capacitor or a native shell). |
24
+ | `isPlatformNative()` | `true` when running inside a host shell, not a plain browser page. |
25
+ | `isPlatformTouch()` | `true` on a touch-primary device, independent of desktop/mobile classification. |
26
+ | `isPlatformVersionAtLeast(minimum)` | `true` when the live OS version is at or above `minimum`. `false` when the version is `''`. |
27
+ | `isPlatformWeb()` | `true` when running as a plain web page with no native host registered. |
28
+ | `setPlatformBackend(backend)` | Install a native host backend. Pass `null` to fall back to the web default. |
29
+
30
+ ## `PlatformInfo` fields
31
+
32
+ `PlatformInfo` carries 14 fields. Every field is filled on the web from the user-agent string, `navigator`, and runtime probes; native hosts fill the same shape from OS APIs. Fields a web page cannot honestly report resolve to a sentinel rather than guessing.
33
+
34
+ | Field | Type | Value space | Sentinel | Web source | Native source |
35
+ | --- | --- | --- | --- | --- | --- |
36
+ | `name` | `PlatformName` | `'web'` `'windows'` `'macos'` `'linux'` `'ios'` `'android'` `'unknown'` | `'unknown'` | parsed from UA string | OS name |
37
+ | `kind` | `PlatformKind` | `'desktop'` `'mobile'` `'web'` `'unknown'` | `'unknown'` | derived from `name` | OS family |
38
+ | `version` | `string` | dotted OS version, e.g. `'10.15.7'` | `''` | parsed from UA string (best-effort) | OS version API |
39
+ | `arch` | `string` | `'x64'` `'arm64'` `'x86'` `'arm'` `'wasm'`, … | `''` | parsed from UA string | CPU arch |
40
+ | `locale` | `string` | BCP-47 tag, e.g. `'en-US'` | `''` | `navigator.language` | OS locale |
41
+ | `isTouch` | `boolean` | touch-primary input | `false` | `navigator.maxTouchPoints > 0` | input enumeration |
42
+ | `runtime` | `PlatformRuntime` | `'web'` `'electron'` `'tauri'` `'capacitor'` `'native'` `'unknown'` | `'unknown'` | probed from `window` globals | host shell identity |
43
+ | `engine` | `PlatformEngine` | `'gecko'` `'blink'` `'webkit'` `'unknown'` | `'unknown'` | parsed from UA string | `'unknown'` (no browser engine) |
44
+ | `engineVersion` | `string` | dotted engine version | `''` | parsed from UA string | `''` (no browser engine) |
45
+ | `endianness` | `PlatformEndianness` | `'big'` `'little'` `'unknown'` | `'unknown'` | `ArrayBuffer` byte-order probe | CPU byte order |
46
+ | `pointerWidth` | `32 \| 64 \| -1` | word width in bits | `-1` | inferred from `arch` | inferred from `arch` |
47
+ | `osBuild` | `string` | OS build identifier | `''` | **native-reserved** — always `''` | OS build string |
48
+ | `distro` | `string` | Linux distribution id, e.g. `'ubuntu'` | `''` | **native-reserved** — always `''` | `os-release` id |
49
+ | `distroVersion` | `string` | Linux distribution version | `''` | **native-reserved** — always `''` | `os-release` version |
50
+
51
+ `osBuild`, `distro`, and `distroVersion` exist in the shape so a native backend can fill them without a breaking type change. The web backend cannot honestly report them, so it returns `''`; they stay empty until a native host provides them. Treat `''` here as **native-reserved**, not an error.
52
+
53
+ ## Cross-package delegation
54
+
55
+ `platform` reports only host **identity**. Richer or live concerns belong to their own cells; reach for these rather than expecting them on `PlatformInfo`:
56
+
57
+ | Concern | Owner | Why not here |
58
+ | --- | --- | --- |
59
+ | Device model, manufacturer, memory, safe-area insets | `@flighthq/device` | Hardware/device identity is a distinct, larger snapshot. |
60
+ | Battery, charging, low-power, keep-awake | `@flighthq/power` | Live, event-bearing state — not a static snapshot. |
61
+ | Display enumeration, work area, scale factor | `@flighthq/screen` | Multi-display geometry is its own live surface. |
62
+ | App name/version, quit/relaunch, single-instance lock | `@flighthq/app` | Application/process identity, not host identity. |
63
+
64
+ The UA-string parsing primitives (`parseUserAgent*`, `probeEndianness`) live in `@flighthq/useragent`, the shared value-leaf both `platform` and `device` build their web backends on.
65
+
66
+ ## Usage
67
+
68
+ ```ts
69
+ import { createPlatformInfo, getPlatformInfo, isPlatformVersionAtLeast } from '@flighthq/platform';
70
+
71
+ const info = getPlatformInfo(createPlatformInfo());
72
+ console.log(info.name, info.kind, info.arch, info.runtime);
73
+
74
+ if (isPlatformVersionAtLeast('10.15')) {
75
+ // gate a feature on a minimum OS version
76
+ }
77
+ ```
@@ -0,0 +1,2 @@
1
+ export * from './platform';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './platform';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { PlatformBackend, PlatformEngine, PlatformInfo, PlatformKind, PlatformName, PlatformRuntime } from '@flighthq/types';
2
+ export declare function comparePlatformVersions(a: string, b: string): -1 | 0 | 1;
3
+ export declare function createPlatformInfo(): PlatformInfo;
4
+ export declare function createWebPlatformBackend(): PlatformBackend;
5
+ export declare function getPlatformBackend(): PlatformBackend;
6
+ export declare function getPlatformEngine(): PlatformEngine;
7
+ export declare function getPlatformInfo(out: PlatformInfo): PlatformInfo;
8
+ export declare function getPlatformKind(): PlatformKind;
9
+ export declare function getPlatformName(): PlatformName;
10
+ export declare function getPlatformRuntime(): PlatformRuntime;
11
+ export declare function isPlatformDesktop(): boolean;
12
+ export declare function isPlatformMobile(): boolean;
13
+ export declare function isPlatformNative(): boolean;
14
+ export declare function isPlatformTouch(): boolean;
15
+ export declare function isPlatformVersionAtLeast(minimum: string): boolean;
16
+ export declare function isPlatformWeb(): boolean;
17
+ export declare function setPlatformBackend(backend: PlatformBackend | null): void;
18
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,eAAe,EAChB,MAAM,iBAAiB,CAAC;AAgBzB,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAcxE;AAGD,wBAAgB,kBAAkB,IAAI,YAAY,CAiBjD;AAID,wBAAgB,wBAAwB,IAAI,eAAe,CAE1D;AAID,wBAAgB,kBAAkB,IAAI,eAAe,CAGpD;AAID,wBAAgB,iBAAiB,IAAI,cAAc,CAElD;AAGD,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,YAAY,CAE/D;AAGD,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAGD,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAID,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAGD,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAGD,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAID,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAGD,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAKD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAIjE;AAGD,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAExE"}
@@ -0,0 +1,137 @@
1
+ import { detectEndianness, parseUserAgentArch, parseUserAgentEngine, parseUserAgentEngineVersion, parseUserAgentKind, parseUserAgentName, parseUserAgentPointerWidth, parseUserAgentRuntime, parseUserAgentVersion, } from '@flighthq/useragent';
2
+ // Compares two dotted version strings numerically, segment by segment. Returns -1, 0, or 1.
3
+ // Non-numeric trailing segments are ignored; '' sorts lowest. '' compared with '' returns 0.
4
+ // Example: comparePlatformVersions('10.15.7', '10.15.6') === 1.
5
+ export function comparePlatformVersions(a, b) {
6
+ if (a === b)
7
+ return 0;
8
+ const aParts = a === '' ? [] : a.split('.');
9
+ const bParts = b === '' ? [] : b.split('.');
10
+ const len = Math.max(aParts.length, bParts.length);
11
+ for (let i = 0; i < len; i++) {
12
+ const aNum = i < aParts.length ? parseInt(aParts[i], 10) : 0;
13
+ const bNum = i < bParts.length ? parseInt(bParts[i], 10) : 0;
14
+ const aN = isNaN(aNum) ? 0 : aNum;
15
+ const bN = isNaN(bNum) ? 0 : bNum;
16
+ if (aN < bN)
17
+ return -1;
18
+ if (aN > bN)
19
+ return 1;
20
+ }
21
+ return 0;
22
+ }
23
+ // Allocates a zeroed PlatformInfo; use as the `out` for getPlatformInfo or when building a backend.
24
+ export function createPlatformInfo() {
25
+ return {
26
+ arch: '',
27
+ distro: '',
28
+ distroVersion: '',
29
+ endianness: 'unknown',
30
+ engine: 'unknown',
31
+ engineVersion: '',
32
+ isTouch: false,
33
+ kind: 'unknown',
34
+ locale: '',
35
+ name: 'unknown',
36
+ osBuild: '',
37
+ pointerWidth: -1,
38
+ runtime: 'unknown',
39
+ version: '',
40
+ };
41
+ }
42
+ // The fixed point of the suite: a host backend that identifies the running platform. Resolves to the
43
+ // registered native backend, or a lazily-created web backend so there is always an answer.
44
+ export function createWebPlatformBackend() {
45
+ return { getInfo: getWebPlatformInfo };
46
+ }
47
+ // The active platform backend, or the web default when no native host has registered one. Capability
48
+ // packages call their own get*Backend; this is the root one for environment identification.
49
+ export function getPlatformBackend() {
50
+ if (_backend === null)
51
+ _backend = createWebPlatformBackend();
52
+ return _backend;
53
+ }
54
+ // The browser rendering engine — 'blink' | 'gecko' | 'webkit' | 'unknown'. Convenience over
55
+ // getPlatformInfo. 'unknown' on native hosts where no browser engine is present.
56
+ export function getPlatformEngine() {
57
+ return getPlatformInfo(_scratch).engine;
58
+ }
59
+ // Fills `out` with the running platform's identity and returns it. Cheap; reads the active backend.
60
+ export function getPlatformInfo(out) {
61
+ return getPlatformBackend().getInfo(out);
62
+ }
63
+ // The platform family — 'desktop' | 'mobile' | 'web' | 'unknown'. Convenience over getPlatformInfo.
64
+ export function getPlatformKind() {
65
+ return getPlatformInfo(_scratch).kind;
66
+ }
67
+ // The specific OS/runtime name — 'windows' | 'macos' | 'ios' | 'android' | 'linux' | 'web' | 'unknown'.
68
+ export function getPlatformName() {
69
+ return getPlatformInfo(_scratch).name;
70
+ }
71
+ // The host shell / runtime environment — 'web' | 'electron' | 'tauri' | 'capacitor' | 'native' |
72
+ // 'unknown'. Convenience over getPlatformInfo. Distinguishes plain web from a host shell.
73
+ export function getPlatformRuntime() {
74
+ return getPlatformInfo(_scratch).runtime;
75
+ }
76
+ // True on a desktop host (Electron/Tauri/native window shell). False on mobile and plain web.
77
+ export function isPlatformDesktop() {
78
+ return getPlatformKind() === 'desktop';
79
+ }
80
+ // True on a mobile host (iOS/Android via Capacitor or a native shell).
81
+ export function isPlatformMobile() {
82
+ return getPlatformKind() === 'mobile';
83
+ }
84
+ // True when the app is running inside a host shell (Electron/Tauri/Capacitor/native), not a plain
85
+ // browser page. Convenience over `getPlatformRuntime() !== 'web' && !== 'unknown'`.
86
+ export function isPlatformNative() {
87
+ const runtime = getPlatformRuntime();
88
+ return runtime !== 'web' && runtime !== 'unknown';
89
+ }
90
+ // True on a touch-primary device, independent of desktop/mobile classification.
91
+ export function isPlatformTouch() {
92
+ return getPlatformInfo(_scratch).isTouch;
93
+ }
94
+ // True when the running platform's OS version is at or above `minimum`. Reads the live version via
95
+ // getPlatformInfo. Returns false when the version is '' (unknown). The comparison is numeric and
96
+ // segment-wise (see comparePlatformVersions).
97
+ export function isPlatformVersionAtLeast(minimum) {
98
+ const version = getPlatformInfo(_scratch).version;
99
+ if (version === '')
100
+ return false;
101
+ return comparePlatformVersions(version, minimum) >= 0;
102
+ }
103
+ // True when running as a plain web page with no native host registered.
104
+ export function isPlatformWeb() {
105
+ return getPlatformKind() === 'web';
106
+ }
107
+ // Installs a native host backend (Electron/Tauri/Capacitor/native). Pass null to fall back to web.
108
+ // Opt-in and side-effect-free at import: nothing registers until a host calls this.
109
+ export function setPlatformBackend(backend) {
110
+ _backend = backend;
111
+ }
112
+ let _backend = null;
113
+ // Single-threaded JS no-alloc scratch for scalar convenience reads (getPlatformKind, etc.).
114
+ // Rust/native mirror uses a per-call local or thread-local instead.
115
+ const _scratch = createPlatformInfo();
116
+ function getWebPlatformInfo(out) {
117
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
118
+ const ua = nav?.userAgent ?? '';
119
+ out.name = parseUserAgentName(ua);
120
+ out.kind = parseUserAgentKind(out.name);
121
+ out.version = parseUserAgentVersion(ua, out.name);
122
+ out.arch = parseUserAgentArch(ua);
123
+ out.locale = nav?.language ?? '';
124
+ out.isTouch =
125
+ typeof navigator !== 'undefined' && 'maxTouchPoints' in navigator ? navigator.maxTouchPoints > 0 : false;
126
+ out.runtime = parseUserAgentRuntime(typeof window !== 'undefined' ? window : null);
127
+ out.engine = parseUserAgentEngine(ua);
128
+ out.engineVersion = parseUserAgentEngineVersion(ua, out.engine);
129
+ out.endianness = detectEndianness();
130
+ out.pointerWidth = parseUserAgentPointerWidth(out.arch);
131
+ // osBuild, distro, distroVersion are native-only; web always returns ''.
132
+ out.osBuild = '';
133
+ out.distro = '';
134
+ out.distroVersion = '';
135
+ return out;
136
+ }
137
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/platform",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0",
31
+ "@flighthq/useragent": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Platform identification seam for the desktop/mobile integration suite: OS/kind/arch detection with a swappable host backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,581 @@
1
+ import type { PlatformBackend, PlatformInfo } from '@flighthq/types';
2
+
3
+ import {
4
+ comparePlatformVersions,
5
+ createPlatformInfo,
6
+ createWebPlatformBackend,
7
+ getPlatformBackend,
8
+ getPlatformEngine,
9
+ getPlatformInfo,
10
+ getPlatformKind,
11
+ getPlatformName,
12
+ getPlatformRuntime,
13
+ isPlatformDesktop,
14
+ isPlatformMobile,
15
+ isPlatformNative,
16
+ isPlatformTouch,
17
+ isPlatformVersionAtLeast,
18
+ isPlatformWeb,
19
+ setPlatformBackend,
20
+ } from './platform';
21
+
22
+ function fakeBackend(info: Partial<PlatformInfo>): PlatformBackend {
23
+ return {
24
+ getInfo(out) {
25
+ Object.assign(out, createPlatformInfo(), info);
26
+ return out;
27
+ },
28
+ };
29
+ }
30
+
31
+ afterEach(() => setPlatformBackend(null));
32
+
33
+ describe('comparePlatformVersions', () => {
34
+ it('returns 0 for identical strings', () => {
35
+ expect(comparePlatformVersions('10.15.7', '10.15.7')).toBe(0);
36
+ });
37
+
38
+ it('returns 0 for two empty strings', () => {
39
+ expect(comparePlatformVersions('', '')).toBe(0);
40
+ });
41
+
42
+ it('empty string sorts lower than any version', () => {
43
+ expect(comparePlatformVersions('', '1.0')).toBe(-1);
44
+ expect(comparePlatformVersions('1.0', '')).toBe(1);
45
+ });
46
+
47
+ it('returns -1 when a is lower', () => {
48
+ expect(comparePlatformVersions('10.15.6', '10.15.7')).toBe(-1);
49
+ expect(comparePlatformVersions('9', '10')).toBe(-1);
50
+ expect(comparePlatformVersions('10.0', '10.0.1')).toBe(-1);
51
+ });
52
+
53
+ it('returns 1 when a is higher', () => {
54
+ expect(comparePlatformVersions('10.15.7', '10.15.6')).toBe(1);
55
+ expect(comparePlatformVersions('11', '10')).toBe(1);
56
+ expect(comparePlatformVersions('14', '13.0.1')).toBe(1);
57
+ });
58
+
59
+ it('compares numeric segments, not lexicographic', () => {
60
+ expect(comparePlatformVersions('10', '9')).toBe(1);
61
+ expect(comparePlatformVersions('2.10', '2.9')).toBe(1);
62
+ });
63
+
64
+ it('treats missing trailing segments as 0', () => {
65
+ expect(comparePlatformVersions('10.0', '10.0.0')).toBe(0);
66
+ expect(comparePlatformVersions('10', '10.0.0')).toBe(0);
67
+ });
68
+ });
69
+
70
+ describe('createPlatformInfo', () => {
71
+ it('allocates a zeroed PlatformInfo with all new fields', () => {
72
+ expect(createPlatformInfo()).toEqual({
73
+ arch: '',
74
+ distro: '',
75
+ distroVersion: '',
76
+ endianness: 'unknown',
77
+ engine: 'unknown',
78
+ engineVersion: '',
79
+ isTouch: false,
80
+ kind: 'unknown',
81
+ locale: '',
82
+ name: 'unknown',
83
+ osBuild: '',
84
+ pointerWidth: -1,
85
+ runtime: 'unknown',
86
+ version: '',
87
+ });
88
+ });
89
+ });
90
+
91
+ describe('createWebPlatformBackend', () => {
92
+ it('produces a backend that fills the out info', () => {
93
+ const out = createPlatformInfo();
94
+ const result = createWebPlatformBackend().getInfo(out);
95
+ expect(result).toBe(out);
96
+ expect(typeof result.name).toBe('string');
97
+ });
98
+
99
+ it('sets engine to a known canonical value', () => {
100
+ const out = createPlatformInfo();
101
+ createWebPlatformBackend().getInfo(out);
102
+ expect(['blink', 'gecko', 'webkit', 'unknown']).toContain(out.engine);
103
+ });
104
+
105
+ it('sets runtime to a known canonical value', () => {
106
+ const out = createPlatformInfo();
107
+ createWebPlatformBackend().getInfo(out);
108
+ expect(['web', 'electron', 'tauri', 'capacitor', 'native', 'unknown']).toContain(out.runtime);
109
+ });
110
+
111
+ it('sets kind to a known canonical value', () => {
112
+ const out = createPlatformInfo();
113
+ createWebPlatformBackend().getInfo(out);
114
+ expect(['desktop', 'mobile', 'web', 'unknown']).toContain(out.kind);
115
+ });
116
+
117
+ it('sets name to a known canonical value', () => {
118
+ const out = createPlatformInfo();
119
+ createWebPlatformBackend().getInfo(out);
120
+ expect(['web', 'windows', 'macos', 'linux', 'ios', 'android', 'unknown']).toContain(out.name);
121
+ });
122
+
123
+ it('sets endianness to a known canonical value', () => {
124
+ const out = createPlatformInfo();
125
+ createWebPlatformBackend().getInfo(out);
126
+ expect(['little', 'big', 'unknown']).toContain(out.endianness);
127
+ });
128
+
129
+ it('sets pointerWidth to a known canonical value', () => {
130
+ const out = createPlatformInfo();
131
+ createWebPlatformBackend().getInfo(out);
132
+ expect([-1, 32, 64]).toContain(out.pointerWidth);
133
+ });
134
+
135
+ it('sets osBuild, distro, distroVersion to empty string on web', () => {
136
+ const out = createPlatformInfo();
137
+ createWebPlatformBackend().getInfo(out);
138
+ expect(out.osBuild).toBe('');
139
+ expect(out.distro).toBe('');
140
+ expect(out.distroVersion).toBe('');
141
+ });
142
+ });
143
+
144
+ describe('getPlatformBackend', () => {
145
+ it('falls back to a web backend when none is registered', () => {
146
+ expect(getPlatformBackend()).not.toBeNull();
147
+ });
148
+
149
+ it('returns the registered backend', () => {
150
+ const backend = fakeBackend({ kind: 'desktop', name: 'windows' });
151
+ setPlatformBackend(backend);
152
+ expect(getPlatformBackend()).toBe(backend);
153
+ });
154
+ });
155
+
156
+ describe('getPlatformEngine', () => {
157
+ it('returns the engine from the active backend', () => {
158
+ setPlatformBackend(fakeBackend({ engine: 'blink' }));
159
+ expect(getPlatformEngine()).toBe('blink');
160
+ });
161
+
162
+ it('returns gecko when set', () => {
163
+ setPlatformBackend(fakeBackend({ engine: 'gecko' }));
164
+ expect(getPlatformEngine()).toBe('gecko');
165
+ });
166
+
167
+ it('returns webkit when set', () => {
168
+ setPlatformBackend(fakeBackend({ engine: 'webkit' }));
169
+ expect(getPlatformEngine()).toBe('webkit');
170
+ });
171
+
172
+ it('returns unknown for native backends', () => {
173
+ setPlatformBackend(fakeBackend({ engine: 'unknown' }));
174
+ expect(getPlatformEngine()).toBe('unknown');
175
+ });
176
+ });
177
+
178
+ describe('getPlatformInfo', () => {
179
+ it('fills and returns the out parameter', () => {
180
+ setPlatformBackend(fakeBackend({ arch: 'arm64', kind: 'mobile', name: 'ios' }));
181
+ const out = createPlatformInfo();
182
+ expect(getPlatformInfo(out)).toBe(out);
183
+ expect(out.name).toBe('ios');
184
+ expect(out.arch).toBe('arm64');
185
+ });
186
+ });
187
+
188
+ describe('getPlatformKind', () => {
189
+ it('returns the active backend kind', () => {
190
+ setPlatformBackend(fakeBackend({ kind: 'desktop' }));
191
+ expect(getPlatformKind()).toBe('desktop');
192
+ });
193
+ });
194
+
195
+ describe('getPlatformName', () => {
196
+ it('returns the active backend name', () => {
197
+ setPlatformBackend(fakeBackend({ name: 'macos' }));
198
+ expect(getPlatformName()).toBe('macos');
199
+ });
200
+ });
201
+
202
+ describe('getPlatformRuntime', () => {
203
+ it('returns web when no host shell is detected', () => {
204
+ setPlatformBackend(fakeBackend({ runtime: 'web' }));
205
+ expect(getPlatformRuntime()).toBe('web');
206
+ });
207
+
208
+ it('returns electron when set', () => {
209
+ setPlatformBackend(fakeBackend({ runtime: 'electron' }));
210
+ expect(getPlatformRuntime()).toBe('electron');
211
+ });
212
+
213
+ it('returns tauri when set', () => {
214
+ setPlatformBackend(fakeBackend({ runtime: 'tauri' }));
215
+ expect(getPlatformRuntime()).toBe('tauri');
216
+ });
217
+
218
+ it('returns capacitor when set', () => {
219
+ setPlatformBackend(fakeBackend({ runtime: 'capacitor' }));
220
+ expect(getPlatformRuntime()).toBe('capacitor');
221
+ });
222
+
223
+ it('returns native when set by a native backend', () => {
224
+ setPlatformBackend(fakeBackend({ runtime: 'native' }));
225
+ expect(getPlatformRuntime()).toBe('native');
226
+ });
227
+ });
228
+
229
+ describe('isPlatformDesktop', () => {
230
+ it('is true only for desktop kind', () => {
231
+ setPlatformBackend(fakeBackend({ kind: 'desktop' }));
232
+ expect(isPlatformDesktop()).toBe(true);
233
+ setPlatformBackend(fakeBackend({ kind: 'web' }));
234
+ expect(isPlatformDesktop()).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('isPlatformMobile', () => {
239
+ it('is true only for mobile kind', () => {
240
+ setPlatformBackend(fakeBackend({ kind: 'mobile' }));
241
+ expect(isPlatformMobile()).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('isPlatformNative', () => {
246
+ it('is true for electron runtime', () => {
247
+ setPlatformBackend(fakeBackend({ runtime: 'electron' }));
248
+ expect(isPlatformNative()).toBe(true);
249
+ });
250
+
251
+ it('is true for tauri runtime', () => {
252
+ setPlatformBackend(fakeBackend({ runtime: 'tauri' }));
253
+ expect(isPlatformNative()).toBe(true);
254
+ });
255
+
256
+ it('is true for capacitor runtime', () => {
257
+ setPlatformBackend(fakeBackend({ runtime: 'capacitor' }));
258
+ expect(isPlatformNative()).toBe(true);
259
+ });
260
+
261
+ it('is true for native runtime', () => {
262
+ setPlatformBackend(fakeBackend({ runtime: 'native' }));
263
+ expect(isPlatformNative()).toBe(true);
264
+ });
265
+
266
+ it('is false for web runtime', () => {
267
+ setPlatformBackend(fakeBackend({ runtime: 'web' }));
268
+ expect(isPlatformNative()).toBe(false);
269
+ });
270
+
271
+ it('is false for unknown runtime', () => {
272
+ setPlatformBackend(fakeBackend({ runtime: 'unknown' }));
273
+ expect(isPlatformNative()).toBe(false);
274
+ });
275
+ });
276
+
277
+ describe('isPlatformTouch', () => {
278
+ it('reflects the backend isTouch flag', () => {
279
+ setPlatformBackend(fakeBackend({ isTouch: true }));
280
+ expect(isPlatformTouch()).toBe(true);
281
+ });
282
+ });
283
+
284
+ describe('isPlatformVersionAtLeast', () => {
285
+ it('is true when version equals minimum', () => {
286
+ setPlatformBackend(fakeBackend({ version: '14.0' }));
287
+ expect(isPlatformVersionAtLeast('14.0')).toBe(true);
288
+ });
289
+
290
+ it('is true when version exceeds minimum', () => {
291
+ setPlatformBackend(fakeBackend({ version: '15.0' }));
292
+ expect(isPlatformVersionAtLeast('14.0')).toBe(true);
293
+ });
294
+
295
+ it('is false when version is below minimum', () => {
296
+ setPlatformBackend(fakeBackend({ version: '13.0' }));
297
+ expect(isPlatformVersionAtLeast('14.0')).toBe(false);
298
+ });
299
+
300
+ it('is false when version is empty (unknown)', () => {
301
+ setPlatformBackend(fakeBackend({ version: '' }));
302
+ expect(isPlatformVersionAtLeast('1.0')).toBe(false);
303
+ });
304
+
305
+ it('handles patch-level comparison', () => {
306
+ setPlatformBackend(fakeBackend({ version: '10.15.7' }));
307
+ expect(isPlatformVersionAtLeast('10.15.6')).toBe(true);
308
+ expect(isPlatformVersionAtLeast('10.15.7')).toBe(true);
309
+ expect(isPlatformVersionAtLeast('10.15.8')).toBe(false);
310
+ });
311
+ });
312
+
313
+ describe('isPlatformWeb', () => {
314
+ it('is true only for web kind', () => {
315
+ setPlatformBackend(fakeBackend({ kind: 'web' }));
316
+ expect(isPlatformWeb()).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe('setPlatformBackend', () => {
321
+ it('clears back to the web fallback when passed null', () => {
322
+ setPlatformBackend(fakeBackend({ name: 'linux' }));
323
+ setPlatformBackend(null);
324
+ expect(getPlatformBackend()).not.toBeNull();
325
+ });
326
+ });
327
+
328
+ // Web backend UA-detection tests — use the internal createWebPlatformBackend to exercise detection
329
+ // heuristics under controlled UA strings. These run in jsdom which may have its own navigator.
330
+ describe('web backend UA detection', () => {
331
+ // Helper: temporarily replace navigator.userAgent for a block (jsdom only).
332
+ function withUserAgent(ua: string, fn: () => void): void {
333
+ const original = navigator.userAgent;
334
+ Object.defineProperty(navigator, 'userAgent', { configurable: true, value: ua });
335
+ try {
336
+ fn();
337
+ } finally {
338
+ Object.defineProperty(navigator, 'userAgent', { configurable: true, value: original });
339
+ }
340
+ }
341
+
342
+ describe('arch detection', () => {
343
+ it('detects arm64 from aarch64', () => {
344
+ withUserAgent('Mozilla/5.0 (Linux; aarch64) AppleWebKit/537.36', () => {
345
+ const out = createPlatformInfo();
346
+ createWebPlatformBackend().getInfo(out);
347
+ expect(out.arch).toBe('arm64');
348
+ });
349
+ });
350
+
351
+ it('detects arm64 from arm64 token', () => {
352
+ withUserAgent('Mozilla/5.0 (iPhone; CPU arm64)', () => {
353
+ const out = createPlatformInfo();
354
+ createWebPlatformBackend().getInfo(out);
355
+ expect(out.arch).toBe('arm64');
356
+ });
357
+ });
358
+
359
+ it('detects x64 from Win64', () => {
360
+ withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', () => {
361
+ const out = createPlatformInfo();
362
+ createWebPlatformBackend().getInfo(out);
363
+ expect(out.arch).toBe('x64');
364
+ });
365
+ });
366
+
367
+ it('detects x64 from WOW64', () => {
368
+ withUserAgent('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36', () => {
369
+ const out = createPlatformInfo();
370
+ createWebPlatformBackend().getInfo(out);
371
+ expect(out.arch).toBe('x64');
372
+ });
373
+ });
374
+
375
+ it('returns empty string when arch is undetectable', () => {
376
+ withUserAgent('Mozilla/5.0 (X11; Linux) AppleWebKit/537.36', () => {
377
+ const out = createPlatformInfo();
378
+ createWebPlatformBackend().getInfo(out);
379
+ expect(out.arch).toBe('');
380
+ });
381
+ });
382
+ });
383
+
384
+ describe('canonical token normalization', () => {
385
+ const KNOWN_NAMES = ['web', 'windows', 'macos', 'linux', 'ios', 'android', 'unknown'];
386
+ const KNOWN_KINDS = ['desktop', 'mobile', 'web', 'unknown'];
387
+ const KNOWN_RUNTIMES = ['web', 'electron', 'tauri', 'capacitor', 'native', 'unknown'];
388
+ const KNOWN_ENGINES = ['blink', 'gecko', 'webkit', 'unknown'];
389
+ const KNOWN_ENDIANNESSES = ['little', 'big', 'unknown'];
390
+ const KNOWN_POINTER_WIDTHS = [-1, 32, 64];
391
+
392
+ const TEST_UAS = [
393
+ // Windows Chrome
394
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
395
+ // macOS Safari
396
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
397
+ // Firefox
398
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
399
+ // iOS Safari
400
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15',
401
+ // Android Chrome
402
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
403
+ ];
404
+
405
+ for (const ua of TEST_UAS) {
406
+ it(`only emits known canonical tokens for UA: ${ua.slice(0, 60)}…`, () => {
407
+ withUserAgent(ua, () => {
408
+ const out = createPlatformInfo();
409
+ createWebPlatformBackend().getInfo(out);
410
+ expect(KNOWN_NAMES).toContain(out.name);
411
+ expect(KNOWN_KINDS).toContain(out.kind);
412
+ expect(KNOWN_RUNTIMES).toContain(out.runtime);
413
+ expect(KNOWN_ENGINES).toContain(out.engine);
414
+ expect(KNOWN_ENDIANNESSES).toContain(out.endianness);
415
+ expect(KNOWN_POINTER_WIDTHS).toContain(out.pointerWidth);
416
+ expect(typeof out.version).toBe('string');
417
+ expect(typeof out.engineVersion).toBe('string');
418
+ expect(typeof out.arch).toBe('string');
419
+ });
420
+ });
421
+ }
422
+ });
423
+
424
+ describe('endianness detection', () => {
425
+ it('returns a known canonical value', () => {
426
+ const out = createPlatformInfo();
427
+ createWebPlatformBackend().getInfo(out);
428
+ expect(['little', 'big', 'unknown']).toContain(out.endianness);
429
+ });
430
+
431
+ it('returns little on jsdom (x64 host)', () => {
432
+ // jsdom runs on Node.js on x64 hardware — always little-endian.
433
+ const out = createPlatformInfo();
434
+ createWebPlatformBackend().getInfo(out);
435
+ expect(out.endianness).toBe('little');
436
+ });
437
+ });
438
+
439
+ describe('engine detection', () => {
440
+ it('detects blink from Chrome UA', () => {
441
+ withUserAgent(
442
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
443
+ () => {
444
+ const out = createPlatformInfo();
445
+ createWebPlatformBackend().getInfo(out);
446
+ expect(out.engine).toBe('blink');
447
+ },
448
+ );
449
+ });
450
+
451
+ it('detects gecko from Firefox UA', () => {
452
+ withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0', () => {
453
+ const out = createPlatformInfo();
454
+ createWebPlatformBackend().getInfo(out);
455
+ expect(out.engine).toBe('gecko');
456
+ });
457
+ });
458
+
459
+ it('detects webkit from Safari UA', () => {
460
+ withUserAgent(
461
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
462
+ () => {
463
+ const out = createPlatformInfo();
464
+ createWebPlatformBackend().getInfo(out);
465
+ expect(out.engine).toBe('webkit');
466
+ },
467
+ );
468
+ });
469
+ });
470
+
471
+ describe('engineVersion detection', () => {
472
+ it('extracts Firefox version', () => {
473
+ withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0', () => {
474
+ const out = createPlatformInfo();
475
+ createWebPlatformBackend().getInfo(out);
476
+ expect(out.engineVersion).toBe('120.0');
477
+ });
478
+ });
479
+
480
+ it('extracts Chrome version', () => {
481
+ withUserAgent(
482
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36',
483
+ () => {
484
+ const out = createPlatformInfo();
485
+ createWebPlatformBackend().getInfo(out);
486
+ expect(out.engineVersion).toBe('120.0.6099.109');
487
+ },
488
+ );
489
+ });
490
+
491
+ it('extracts Edge version (Edg/ token)', () => {
492
+ withUserAgent(
493
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133',
494
+ () => {
495
+ const out = createPlatformInfo();
496
+ createWebPlatformBackend().getInfo(out);
497
+ expect(out.engineVersion).toBe('120.0.2210.133');
498
+ },
499
+ );
500
+ });
501
+
502
+ it('extracts Safari version from Version/ token', () => {
503
+ withUserAgent(
504
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
505
+ () => {
506
+ const out = createPlatformInfo();
507
+ createWebPlatformBackend().getInfo(out);
508
+ expect(out.engineVersion).toBe('16.0');
509
+ },
510
+ );
511
+ });
512
+ });
513
+
514
+ describe('pointerWidth detection', () => {
515
+ it('returns 64 for x64 arch', () => {
516
+ withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', () => {
517
+ const out = createPlatformInfo();
518
+ createWebPlatformBackend().getInfo(out);
519
+ expect(out.pointerWidth).toBe(64);
520
+ });
521
+ });
522
+
523
+ it('returns 64 for arm64 arch', () => {
524
+ withUserAgent('Mozilla/5.0 (Linux; aarch64) AppleWebKit/537.36', () => {
525
+ const out = createPlatformInfo();
526
+ createWebPlatformBackend().getInfo(out);
527
+ expect(out.pointerWidth).toBe(64);
528
+ });
529
+ });
530
+
531
+ it('returns -1 when arch is undetectable', () => {
532
+ withUserAgent('Mozilla/5.0 (X11; Linux) AppleWebKit/537.36', () => {
533
+ const out = createPlatformInfo();
534
+ createWebPlatformBackend().getInfo(out);
535
+ expect(out.pointerWidth).toBe(-1);
536
+ });
537
+ });
538
+ });
539
+
540
+ describe('version detection', () => {
541
+ it('parses Windows version from NT string', () => {
542
+ withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)', () => {
543
+ const out = createPlatformInfo();
544
+ createWebPlatformBackend().getInfo(out);
545
+ expect(out.version).toBe('10.0');
546
+ });
547
+ });
548
+
549
+ it('parses macOS version with underscore separators', () => {
550
+ withUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', () => {
551
+ const out = createPlatformInfo();
552
+ createWebPlatformBackend().getInfo(out);
553
+ expect(out.version).toBe('10.15.7');
554
+ });
555
+ });
556
+
557
+ it('parses iOS version', () => {
558
+ withUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X)', () => {
559
+ const out = createPlatformInfo();
560
+ createWebPlatformBackend().getInfo(out);
561
+ expect(out.version).toBe('17.4.1');
562
+ });
563
+ });
564
+
565
+ it('parses Android version', () => {
566
+ withUserAgent('Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36', () => {
567
+ const out = createPlatformInfo();
568
+ createWebPlatformBackend().getInfo(out);
569
+ expect(out.version).toBe('14');
570
+ });
571
+ });
572
+
573
+ it('returns empty string when version is undetectable', () => {
574
+ withUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36', () => {
575
+ const out = createPlatformInfo();
576
+ createWebPlatformBackend().getInfo(out);
577
+ expect(out.version).toBe('');
578
+ });
579
+ });
580
+ });
581
+ });