@absolutejs/absolute 0.20.0-beta.66 → 0.20.0-beta.68

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.
@@ -0,0 +1,246 @@
1
+ import { installDeviceAdapter } from '@absolutejs/devices';
2
+ import {
3
+ createAbsoluteHttpTransport,
4
+ installAbsoluteHttpTransport
5
+ } from '@absolutejs/http';
6
+ import {
7
+ createTestDeviceAdapter,
8
+ type TestDeviceController
9
+ } from '@absolutejs/devices/testing';
10
+
11
+ type PreviewMessage =
12
+ | { type: 'absolute-preview:back' }
13
+ | { type: 'absolute-preview:deep-link'; url: string }
14
+ | { type: 'absolute-preview:keyboard'; heightPx: number; visible: boolean }
15
+ | {
16
+ type: 'absolute-preview:lifecycle';
17
+ state: 'active' | 'background' | 'inactive';
18
+ }
19
+ | {
20
+ type: 'absolute-preview:network';
21
+ connected: boolean;
22
+ connectionType: 'cellular' | 'none' | 'unknown' | 'wifi';
23
+ }
24
+ | {
25
+ type: 'absolute-preview:permission';
26
+ capability: 'camera' | 'location' | 'notifications';
27
+ state: 'blocked' | 'denied' | 'granted' | 'prompt';
28
+ };
29
+
30
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
31
+ typeof value === 'object' && value !== null;
32
+
33
+ const isPreviewMessage = (value: unknown): value is PreviewMessage => {
34
+ if (!isRecord(value) || typeof value.type !== 'string') return false;
35
+ if (value.type === 'absolute-preview:back') return true;
36
+ if (value.type === 'absolute-preview:deep-link')
37
+ return typeof value.url === 'string';
38
+ if (value.type === 'absolute-preview:keyboard')
39
+ return (
40
+ typeof value.heightPx === 'number' &&
41
+ Number.isFinite(value.heightPx) &&
42
+ typeof value.visible === 'boolean'
43
+ );
44
+ if (value.type === 'absolute-preview:lifecycle')
45
+ return ['active', 'background', 'inactive'].includes(
46
+ String(value.state)
47
+ );
48
+ if (value.type === 'absolute-preview:network')
49
+ return (
50
+ typeof value.connected === 'boolean' &&
51
+ ['cellular', 'none', 'unknown', 'wifi'].includes(
52
+ String(value.connectionType)
53
+ )
54
+ );
55
+ if (value.type === 'absolute-preview:permission')
56
+ return (
57
+ ['camera', 'location', 'notifications'].includes(
58
+ String(value.capability)
59
+ ) &&
60
+ ['blocked', 'denied', 'granted', 'prompt'].includes(
61
+ String(value.state)
62
+ )
63
+ );
64
+
65
+ return false;
66
+ };
67
+
68
+ const previewPlatform = () =>
69
+ new URLSearchParams(location.search).get('__absolute_preview_platform') ===
70
+ 'android'
71
+ ? 'android'
72
+ : 'ios';
73
+
74
+ const PREVIEW_STATE_KEY = Symbol.for('@absolutejs/mobile-preview/state');
75
+ type AbsoluteMobilePreviewState = { connected: boolean };
76
+ const isAbsoluteMobilePreviewState = (
77
+ value: unknown
78
+ ): value is AbsoluteMobilePreviewState =>
79
+ isRecord(value) && typeof value.connected === 'boolean';
80
+ const getPreviewState = () => {
81
+ const existing = Reflect.get(globalThis, PREVIEW_STATE_KEY);
82
+ if (isAbsoluteMobilePreviewState(existing)) return existing;
83
+ const created: AbsoluteMobilePreviewState = { connected: true };
84
+ Reflect.set(globalThis, PREVIEW_STATE_KEY, created);
85
+
86
+ return created;
87
+ };
88
+ const previewState = getPreviewState();
89
+ const browserFetch = globalThis.fetch;
90
+ const nativeFetch = browserFetch.bind(globalThis);
91
+ const isPreviewInfrastructureRequest = (url: URL) =>
92
+ url.origin === location.origin &&
93
+ (url.pathname.startsWith('/__absolute/') ||
94
+ url.pathname.startsWith('/@src/') ||
95
+ url.pathname === '/hmr-status');
96
+
97
+ const previewPreconnect = (
98
+ ...args: Parameters<typeof globalThis.fetch.preconnect>
99
+ ) => {
100
+ const preconnect = Reflect.get(browserFetch, 'preconnect');
101
+ if (typeof preconnect === 'function')
102
+ Reflect.apply(preconnect, browserFetch, args);
103
+ };
104
+
105
+ const previewFetch: typeof globalThis.fetch = Object.assign(
106
+ (
107
+ input: Parameters<typeof globalThis.fetch>[0],
108
+ init?: Parameters<typeof globalThis.fetch>[1]
109
+ ) => {
110
+ const requestUrl = new URL(
111
+ input instanceof Request ? input.url : String(input),
112
+ location.href
113
+ );
114
+ if (
115
+ !previewState.connected &&
116
+ !isPreviewInfrastructureRequest(requestUrl)
117
+ ) {
118
+ return Promise.reject(
119
+ new TypeError(
120
+ 'AbsoluteJS mobile preview is offline. Restore connectivity from the preview controls.'
121
+ )
122
+ );
123
+ }
124
+
125
+ return nativeFetch(input, init);
126
+ },
127
+ { preconnect: previewPreconnect }
128
+ );
129
+
130
+ const installPreviewFetch = () => {
131
+ globalThis.fetch = previewFetch;
132
+ };
133
+
134
+ let removePreviewHttpTransport: (() => void) | undefined;
135
+ const installPreviewHttpTransport = () => {
136
+ removePreviewHttpTransport?.();
137
+ removePreviewHttpTransport = installAbsoluteHttpTransport(
138
+ createAbsoluteHttpTransport({
139
+ fetch: previewFetch,
140
+ origin: location.origin,
141
+ runtime: 'web'
142
+ })
143
+ );
144
+ };
145
+
146
+ installPreviewFetch();
147
+
148
+ const postState = (
149
+ type: 'event' | 'ready',
150
+ detail: Record<string, unknown> = {}
151
+ ) => {
152
+ if (window.parent === window) return;
153
+ window.parent.postMessage(
154
+ {
155
+ ...detail,
156
+ platform: previewPlatform(),
157
+ type: `absolute-preview:${type}`
158
+ },
159
+ location.origin
160
+ );
161
+ };
162
+
163
+ const reportReady = () => {
164
+ const durationMs = Math.max(0, Math.round(performance.now()));
165
+ void nativeFetch('/__absolute/mobile-preview-telemetry', {
166
+ body: JSON.stringify({ durationMs, platform: previewPlatform() }),
167
+ headers: { 'content-type': 'application/json' },
168
+ keepalive: true,
169
+ method: 'POST'
170
+ }).catch(() => undefined);
171
+ };
172
+
173
+ const emitPermission = (
174
+ controller: TestDeviceController,
175
+ message: Extract<PreviewMessage, { type: 'absolute-preview:permission' }>
176
+ ) => {
177
+ const status = {
178
+ canRequest: message.state === 'prompt',
179
+ state: message.state
180
+ } as const;
181
+ if (message.capability === 'camera')
182
+ controller.cameraPermission.setStatus(status);
183
+ else if (message.capability === 'location')
184
+ controller.locationPermission.setStatus(status);
185
+ else controller.notificationPermission.setStatus(status);
186
+ };
187
+
188
+ const handlePreviewMessage = (
189
+ controller: TestDeviceController,
190
+ message: PreviewMessage
191
+ ) => {
192
+ if (message.type === 'absolute-preview:back') {
193
+ controller.emitBack({ canGoBack: history.length > 1 });
194
+ } else if (message.type === 'absolute-preview:deep-link') {
195
+ controller.emitLink(message.url);
196
+ } else if (message.type === 'absolute-preview:keyboard') {
197
+ controller.emitKeyboard({
198
+ heightPx: Math.max(0, Math.round(message.heightPx)),
199
+ visible: message.visible
200
+ });
201
+ } else if (message.type === 'absolute-preview:lifecycle') {
202
+ controller.emitLifecycle(message.state);
203
+ } else if (message.type === 'absolute-preview:network') {
204
+ previewState.connected = message.connected;
205
+ // Some framework runtimes replace window.fetch while bootstrapping.
206
+ // Reassert the raw-fetch simulation whenever connectivity changes;
207
+ // @absolutejs/http remains pinned to this provider independently.
208
+ installPreviewFetch();
209
+ installPreviewHttpTransport();
210
+ controller.emitNetwork({
211
+ connected: message.connected,
212
+ connectionType: message.connectionType
213
+ });
214
+ } else emitPermission(controller, message);
215
+ postState('event', { event: message.type });
216
+ };
217
+
218
+ export const installAbsoluteMobilePreview = () => {
219
+ if (Reflect.has(globalThis, '__ABS_MOBILE_PREVIEW__')) return;
220
+ const platform = previewPlatform();
221
+ const controller = createTestDeviceAdapter({
222
+ lifecycle: 'active',
223
+ network: { connected: true, connectionType: 'wifi' },
224
+ platform: {
225
+ formFactor: 'phone',
226
+ isNative: true,
227
+ os: platform,
228
+ runtime: 'capacitor',
229
+ safeAreaInsets:
230
+ platform === 'ios'
231
+ ? { bottom: 34, left: 0, right: 0, top: 59 }
232
+ : { bottom: 24, left: 0, right: 0, top: 24 }
233
+ }
234
+ });
235
+ installDeviceAdapter(controller.adapter);
236
+ installPreviewHttpTransport();
237
+ Reflect.set(globalThis, '__ABS_MOBILE_PREVIEW__', controller);
238
+ addEventListener('message', (event: MessageEvent<unknown>) => {
239
+ if (event.origin !== location.origin || event.source !== window.parent)
240
+ return;
241
+ if (!isPreviewMessage(event.data)) return;
242
+ handlePreviewMessage(controller, event.data);
243
+ });
244
+ postState('ready');
245
+ reportReady();
246
+ };