@rozenite/network-activity-plugin 1.0.0-alpha.8 → 1.0.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.
Files changed (160) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-R2ZMH9wJ.css → App-BrSkOkws.css} +269 -2
  4. package/dist/assets/{App-lNMijPJ4.js → App-C6wCDVkW.js} +17485 -10814
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +4 -1
  8. package/dist/react-native.js +4 -1
  9. package/dist/rozenite.json +1 -1
  10. package/dist/src/react-native/config.d.ts +20 -0
  11. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  12. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  13. package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +7 -1
  14. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  15. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  16. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  17. package/dist/src/react-native/sse/types.d.ts +6 -0
  18. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  19. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  20. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  21. package/dist/src/react-native/utils.d.ts +6 -0
  22. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  23. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  24. package/dist/src/shared/client.d.ts +53 -6
  25. package/dist/src/shared/sse-events.d.ts +38 -0
  26. package/dist/src/shared/websocket-events.d.ts +60 -0
  27. package/dist/src/ui/components/Badge.d.ts +1 -1
  28. package/dist/src/ui/components/Button.d.ts +2 -2
  29. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  30. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  31. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  32. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  33. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  34. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  35. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  36. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  37. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  38. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  39. package/dist/src/ui/components/RequestList.d.ts +13 -28
  40. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  41. package/dist/src/ui/components/Section.d.ts +8 -0
  42. package/dist/src/ui/components/Separator.d.ts +2 -1
  43. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  44. package/dist/src/ui/components/Tabs.d.ts +7 -0
  45. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  46. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  47. package/dist/src/ui/state/derived.d.ts +5 -0
  48. package/dist/src/ui/state/hooks.d.ts +21 -0
  49. package/dist/src/ui/state/model.d.ts +103 -0
  50. package/dist/src/ui/state/store.d.ts +48 -0
  51. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  52. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  53. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  54. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  55. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  56. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  57. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  58. package/dist/src/ui/types.d.ts +4 -1
  59. package/dist/src/ui/utils/assert.d.ts +1 -0
  60. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  61. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  62. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  63. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  64. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  65. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  66. package/dist/src/ui/utils/getId.d.ts +1 -0
  67. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  68. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  69. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  70. package/dist/src/utils/cookieParser.d.ts +6 -0
  71. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  72. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  73. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  74. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  75. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  76. package/dist/src/utils/safeStringify.d.ts +1 -0
  77. package/dist/src/utils/typeChecks.d.ts +9 -0
  78. package/dist/useNetworkActivityDevTools.cjs +724 -40
  79. package/dist/useNetworkActivityDevTools.js +723 -41
  80. package/package.json +22 -8
  81. package/react-native.ts +6 -1
  82. package/src/react-native/config.ts +43 -0
  83. package/src/react-native/http/network-inspector.ts +388 -0
  84. package/src/react-native/http/overrides-registry.ts +32 -0
  85. package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +19 -2
  86. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  87. package/src/react-native/sse/event-source.ts +25 -0
  88. package/src/react-native/sse/sse-inspector.ts +139 -0
  89. package/src/react-native/sse/sse-interceptor.ts +180 -0
  90. package/src/react-native/sse/types.ts +9 -0
  91. package/src/react-native/useNetworkActivityDevTools.ts +156 -4
  92. package/src/react-native/utils/getBlobName.ts +45 -0
  93. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  94. package/src/react-native/utils.ts +43 -0
  95. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  96. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  97. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  98. package/src/shared/client.ts +79 -6
  99. package/src/shared/sse-events.ts +47 -0
  100. package/src/shared/websocket-events.ts +79 -0
  101. package/src/ui/components/Button.tsx +1 -0
  102. package/src/ui/components/CodeBlock.tsx +19 -0
  103. package/src/ui/components/CodeEditor.tsx +26 -0
  104. package/src/ui/components/CookieCard.tsx +64 -0
  105. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  106. package/src/ui/components/DropdownMenu.tsx +206 -0
  107. package/src/ui/components/FilterBar.tsx +117 -0
  108. package/src/ui/components/Input.tsx +1 -1
  109. package/src/ui/components/JsonTree.tsx +20 -0
  110. package/src/ui/components/JsonTreeCopyableItem.tsx +37 -0
  111. package/src/ui/components/KeyValueGrid.tsx +51 -0
  112. package/src/ui/components/OverrideResponse.tsx +132 -0
  113. package/src/ui/components/RequestBody.tsx +86 -0
  114. package/src/ui/components/RequestList.tsx +101 -131
  115. package/src/ui/components/ScrollArea.tsx +1 -0
  116. package/src/ui/components/Section.tsx +46 -0
  117. package/src/ui/components/SidePanel.tsx +333 -0
  118. package/src/ui/components/Tabs.tsx +1 -1
  119. package/src/ui/components/Toolbar.tsx +45 -0
  120. package/src/ui/globals.css +4 -0
  121. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  122. package/src/ui/state/derived.ts +112 -0
  123. package/src/ui/state/hooks.ts +52 -0
  124. package/src/ui/state/model.ts +140 -0
  125. package/src/ui/state/store.ts +669 -0
  126. package/src/ui/tabs/CookiesTab.tsx +61 -278
  127. package/src/ui/tabs/HeadersTab.tsx +85 -103
  128. package/src/ui/tabs/MessagesTab.tsx +276 -0
  129. package/src/ui/tabs/RequestTab.tsx +58 -51
  130. package/src/ui/tabs/ResponseTab.tsx +101 -74
  131. package/src/ui/tabs/SSEMessagesTab.tsx +224 -0
  132. package/src/ui/tabs/TimingTab.tsx +30 -43
  133. package/src/ui/types.ts +4 -1
  134. package/src/ui/utils/assert.ts +5 -0
  135. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  136. package/src/ui/utils/copyToClipboard.ts +3 -0
  137. package/src/ui/utils/escapeShellArg.ts +12 -0
  138. package/src/ui/utils/generateCurlCommand.ts +83 -0
  139. package/src/ui/utils/generateFetchCall.ts +64 -0
  140. package/src/ui/utils/generateMultipartBody.ts +19 -0
  141. package/src/ui/utils/getId.ts +10 -0
  142. package/src/ui/utils/getStatusColor.ts +15 -0
  143. package/src/ui/views/InspectorView.tsx +35 -319
  144. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  145. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  146. package/src/utils/cookieParser.ts +126 -0
  147. package/src/utils/getContentTypeMimeType.ts +17 -0
  148. package/src/utils/getHttpHeader.ts +17 -0
  149. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  150. package/src/utils/getStringSizeInBytes.ts +3 -0
  151. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  152. package/src/utils/safeStringify.ts +7 -0
  153. package/src/utils/typeChecks.ts +27 -0
  154. package/tailwind.config.ts +3 -0
  155. package/vite.config.ts +12 -0
  156. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  157. package/src/react-native/network-inspector.ts +0 -247
  158. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
  159. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  160. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
@@ -1,348 +1,64 @@
1
- import { useState, useMemo, useEffect } from 'react';
2
- import { Badge } from '../components/Badge';
3
- import { Button } from '../components/Button';
4
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/Tabs';
5
- import { HeadersTab } from '../tabs/HeadersTab';
6
- import { RequestTab } from '../tabs/RequestTab';
7
- import { ResponseTab } from '../tabs/ResponseTab';
8
- import { CookiesTab } from '../tabs/CookiesTab';
9
- import { TimingTab } from '../tabs/TimingTab';
1
+ import { useEffect, useState } from 'react';
2
+ import { Toolbar } from '../components/Toolbar';
3
+ import { RequestList } from '../components/RequestList';
4
+ import { SidePanel } from '../components/SidePanel';
5
+ import { FilterBar, FilterState } from '../components/FilterBar';
6
+ import { NetworkActivityDevToolsClient } from '../../shared/client';
10
7
  import {
11
- RequestList,
12
- processNetworkEntries,
13
- getTypeColor,
14
- getStatusColor,
15
- } from '../components/RequestList';
16
- import { Circle, Square, Trash2, X } from 'lucide-react';
17
- import {
18
- NetworkActivityDevToolsClient,
19
- NetworkActivityEventMap,
20
- RequestId,
21
- } from '../../shared/client';
22
- import { NetworkEntry } from '../types';
8
+ useNetworkActivityClientManagement,
9
+ useHasSelectedRequest,
10
+ useNetworkActivityActions,
11
+ useOverrides,
12
+ } from '../state/hooks';
23
13
 
24
14
  export type InspectorViewProps = {
25
15
  client: NetworkActivityDevToolsClient;
26
16
  };
27
17
 
28
18
  export const InspectorView = ({ client }: InspectorViewProps) => {
29
- const [isRecording, setIsRecording] = useState(true);
30
- const [selectedRequestId, setSelectedRequestId] = useState<RequestId | null>(
31
- null
32
- );
33
- const [networkEntries, setNetworkEntries] = useState<
34
- Map<RequestId, NetworkEntry>
35
- >(new Map());
36
-
37
- const selectedRequest = useMemo(() => {
38
- if (!selectedRequestId) return null;
39
- const processedRequests = processNetworkEntries(networkEntries);
40
- return (
41
- processedRequests.find((request) => request.id === selectedRequestId) ||
42
- null
43
- );
44
- }, [selectedRequestId, networkEntries]);
19
+ const actions = useNetworkActivityActions();
20
+ const clientManagement = useNetworkActivityClientManagement();
21
+ const hasSelectedRequest = useHasSelectedRequest();
22
+ const overrides = useOverrides();
23
+ const [filter, setFilter] = useState<FilterState>({
24
+ text: '',
25
+ types: new Set(['http', 'websocket', 'sse']),
26
+ });
45
27
 
46
28
  useEffect(() => {
47
- const handleRequestSent = (
48
- data: NetworkActivityEventMap['request-sent']
49
- ) => {
50
- const entry: NetworkEntry = {
51
- requestId: data.requestId,
52
- url: data.request.url,
53
- method: data.request.method,
54
- headers: data.request.headers,
55
- postData: data.request.postData,
56
- status: 'pending',
57
- startTime: data.timestamp,
58
- type: data.type,
59
- initiator: data.initiator,
60
- request: data.request,
61
- };
62
-
63
- setNetworkEntries((prev) => new Map(prev).set(data.requestId, entry));
64
- };
65
-
66
- const handleResponseReceived = (
67
- data: NetworkActivityEventMap['response-received']
68
- ) => {
69
- setNetworkEntries((prev) => {
70
- const entry = prev.get(data.requestId);
71
- if (!entry) return prev;
72
-
73
- const updatedEntry: NetworkEntry = {
74
- ...entry,
75
- status: 'loading',
76
- response: data.response,
77
- };
78
-
79
- return new Map(prev).set(data.requestId, updatedEntry);
80
- });
81
- };
82
-
83
- const handleRequestCompleted = (
84
- data: NetworkActivityEventMap['request-completed']
85
- ) => {
86
- setNetworkEntries((prev) => {
87
- const entry = prev.get(data.requestId);
88
- if (!entry) return prev;
89
-
90
- const updatedEntry: NetworkEntry = {
91
- ...entry,
92
- status: 'finished',
93
- endTime: data.timestamp,
94
- duration: data.duration,
95
- ttfb: data.ttfb,
96
- size: data.size,
97
- };
98
-
99
- return new Map(prev).set(data.requestId, updatedEntry);
100
- });
101
- };
102
-
103
- const handleRequestFailed = (
104
- data: NetworkActivityEventMap['request-failed']
105
- ) => {
106
- setNetworkEntries((prev) => {
107
- const entry = prev.get(data.requestId);
108
- if (!entry) return prev;
109
-
110
- const updatedEntry: NetworkEntry = {
111
- ...entry,
112
- status: 'failed',
113
- error: data.error,
114
- canceled: data.canceled,
115
- };
116
-
117
- return new Map(prev).set(data.requestId, updatedEntry);
118
- });
119
- };
120
-
121
- // Subscribe to network events
122
- const unsubscribeRequestSent = client.onMessage(
123
- 'request-sent',
124
- handleRequestSent
125
- );
126
- const unsubscribeResponseReceived = client.onMessage(
127
- 'response-received',
128
- handleResponseReceived
129
- );
130
- const unsubscribeRequestCompleted = client.onMessage(
131
- 'request-completed',
132
- handleRequestCompleted
133
- );
134
- const unsubscribeRequestFailed = client.onMessage(
135
- 'request-failed',
136
- handleRequestFailed
137
- );
138
-
139
- const handleResponseBody = (
140
- data: NetworkActivityEventMap['response-body']
141
- ) => {
142
- setNetworkEntries((prev) => {
143
- const entry = prev.get(data.requestId);
144
- if (!entry) return prev;
145
-
146
- const updatedEntry: NetworkEntry = {
147
- ...entry,
148
- responseBody: {
149
- ...entry.responseBody,
150
- body: data.body,
151
- },
152
- };
29
+ if (!client) {
30
+ return;
31
+ }
153
32
 
154
- console.log(updatedEntry);
155
- return new Map(prev).set(data.requestId, updatedEntry);
156
- });
157
- };
33
+ clientManagement.setupClient(client);
34
+ actions.setRecording(true);
158
35
 
159
- const unsubscribeResponseBody = client.onMessage(
160
- 'response-body',
161
- handleResponseBody
162
- );
36
+ client.send('set-overrides', {
37
+ overrides: Array.from(overrides.entries()),
38
+ });
163
39
 
164
40
  return () => {
165
- unsubscribeRequestSent.remove();
166
- unsubscribeResponseReceived.remove();
167
- unsubscribeRequestCompleted.remove();
168
- unsubscribeRequestFailed.remove();
169
- unsubscribeResponseBody.remove();
41
+ actions.setRecording(false);
42
+ clientManagement.cleanupClient();
170
43
  };
171
- }, [client]);
172
-
173
- useEffect(() => {
174
- if (isRecording) {
175
- client.send('network-enable', {});
176
-
177
- return () => {
178
- client.send('network-disable', {});
179
- };
180
- }
181
- }, [isRecording, client]);
182
-
183
- const clearRequests = () => {
184
- setNetworkEntries(new Map());
185
- setSelectedRequestId(null);
186
- };
187
-
188
- const closeSidePanel = () => {
189
- setSelectedRequestId(null);
190
- };
44
+ }, [client, clientManagement, actions, overrides]);
191
45
 
192
46
  return (
193
47
  <div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
194
- {/* Toolbar */}
195
- <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
196
- <Button
197
- variant="ghost"
198
- size="sm"
199
- onClick={() => setIsRecording(!isRecording)}
200
- className={`h-8 w-8 p-0 ${
201
- isRecording
202
- ? 'text-red-400 hover:text-red-300'
203
- : 'text-gray-400 hover:text-blue-400'
204
- }`}
205
- >
206
- {isRecording ? (
207
- <Circle className="h-4 w-4 fill-current" />
208
- ) : (
209
- <Square className="h-4 w-4" />
210
- )}
211
- </Button>
212
- <Button
213
- variant="ghost"
214
- size="sm"
215
- onClick={clearRequests}
216
- className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
217
- >
218
- <Trash2 className="h-4 w-4" />
219
- </Button>
220
- </div>
48
+ <Toolbar />
49
+ <FilterBar filter={filter} onFilterChange={setFilter} />
221
50
 
222
51
  <div className="flex flex-1 overflow-hidden">
223
52
  {/* Request List */}
224
53
  <div
225
54
  className={`flex flex-col ${
226
- selectedRequest ? 'w-1/2' : 'w-full'
55
+ hasSelectedRequest ? 'w-1/2' : 'w-full'
227
56
  } border-r border-gray-700 overflow-hidden`}
228
57
  >
229
- <RequestList
230
- networkEntries={networkEntries}
231
- selectedRequestId={selectedRequestId}
232
- onRequestSelect={setSelectedRequestId}
233
- />
58
+ <RequestList filter={filter} />
234
59
  </div>
235
60
 
236
- {/* Side Panel */}
237
- {selectedRequest && (
238
- <div className="w-1/2 flex flex-col bg-gray-900">
239
- {/* Side Panel Header */}
240
- <div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800">
241
- <div className="flex items-center gap-2">
242
- <div
243
- className={`w-3 h-3 rounded-full ${getTypeColor(
244
- selectedRequest.type
245
- )}`}
246
- ></div>
247
- <span className="font-medium">{selectedRequest.name}</span>
248
- <Badge
249
- variant="outline"
250
- className={`${getStatusColor(
251
- selectedRequest.status
252
- )} border-current`}
253
- >
254
- {selectedRequest.status}
255
- </Badge>
256
- </div>
257
- <Button
258
- variant="ghost"
259
- size="sm"
260
- onClick={closeSidePanel}
261
- className="h-6 w-6 p-0 text-gray-400 hover:text-blue-400"
262
- >
263
- <X className="h-4 w-4" />
264
- </Button>
265
- </div>
266
-
267
- {/* Side Panel Content */}
268
- <div className="flex-1 overflow-hidden">
269
- <Tabs defaultValue="headers" className="h-full flex flex-col">
270
- <TabsList className="grid w-full grid-cols-5 bg-gray-800 rounded-none border-b border-gray-700">
271
- <TabsTrigger
272
- value="headers"
273
- className="data-[state=active]:bg-gray-700"
274
- >
275
- Headers
276
- </TabsTrigger>
277
- <TabsTrigger
278
- value="request"
279
- className="data-[state=active]:bg-gray-700"
280
- >
281
- Request
282
- </TabsTrigger>
283
- <TabsTrigger
284
- value="response"
285
- className="data-[state=active]:bg-gray-700"
286
- >
287
- Response
288
- </TabsTrigger>
289
- <TabsTrigger
290
- value="cookies"
291
- className="data-[state=active]:bg-gray-700"
292
- >
293
- Cookies
294
- </TabsTrigger>
295
- <TabsTrigger
296
- value="timing"
297
- className="data-[state=active]:bg-gray-700"
298
- >
299
- Timing
300
- </TabsTrigger>
301
- </TabsList>
302
-
303
- <TabsContent
304
- value="headers"
305
- className="flex-1 m-0 overflow-hidden"
306
- >
307
- <HeadersTab
308
- selectedRequest={selectedRequest}
309
- networkEntries={networkEntries}
310
- getStatusColor={getStatusColor}
311
- />
312
- </TabsContent>
313
-
314
- <TabsContent value="request" className="flex-1 m-0 overflow-hidden">
315
- <RequestTab selectedRequest={selectedRequest} />
316
- </TabsContent>
317
-
318
- <TabsContent value="response" className="flex-1 m-0 overflow-hidden">
319
- <ResponseTab
320
- selectedRequest={selectedRequest}
321
- onRequestResponseBody={(requestId) => {
322
- client.send('get-response-body', {
323
- requestId,
324
- });
325
- }}
326
- />
327
- </TabsContent>
328
-
329
- <TabsContent value="cookies" className="flex-1 m-0 overflow-hidden">
330
- <CookiesTab
331
- selectedRequest={selectedRequest}
332
- networkEntries={networkEntries}
333
- />
334
- </TabsContent>
335
-
336
- <TabsContent value="timing" className="flex-1 m-0 overflow-hidden">
337
- <TimingTab
338
- selectedRequest={selectedRequest}
339
- networkEntries={networkEntries}
340
- />
341
- </TabsContent>
342
- </Tabs>
343
- </div>
344
- </div>
345
- )}
61
+ {hasSelectedRequest && <SidePanel />}
346
62
  </div>
347
63
  </div>
348
64
  );
@@ -0,0 +1,30 @@
1
+ import { HttpHeaders, RequestPostData } from '../shared/client';
2
+ import { getHttpHeader } from './getHttpHeader';
3
+ import { inferContentTypeFromPostData } from './inferContentTypeFromPostData';
4
+
5
+ /**
6
+ * Partially emulates React Native's behavior for setting HTTP headers.
7
+ *
8
+ * @see https://github.com/facebook/react-native/blob/de5093c88771977b58f7bec3f3ffa64a9595334e/packages/react-native/Libraries/Network/RCTNetworking.mm#L345-L349
9
+ */
10
+ export function applyReactNativeRequestHeadersLogic(
11
+ headers: HttpHeaders,
12
+ postData?: RequestPostData
13
+ ): HttpHeaders {
14
+ const existingContentType = getHttpHeader(headers, 'content-type');
15
+
16
+ if (existingContentType) {
17
+ return headers;
18
+ }
19
+
20
+ const inferredContentType = inferContentTypeFromPostData(postData);
21
+
22
+ if (!inferredContentType) {
23
+ return headers;
24
+ }
25
+
26
+ return {
27
+ ...headers,
28
+ 'Content-Type': inferredContentType,
29
+ };
30
+ }
@@ -0,0 +1,28 @@
1
+ import { HttpHeaders, XHRHeaders } from '../shared/client';
2
+ import { splitSetCookieHeaderByComma } from './cookieParser';
3
+ import { getHttpHeader } from './getHttpHeader';
4
+
5
+ /**
6
+ * Applies React Native specific logic to response headers.
7
+ * React Native concatenates multiple header values into single strings,
8
+ * this function parses them back into arrays where appropriate.
9
+ *
10
+ * @see https://github.com/facebook/react-native/blob/588f0c5ce6c283f116228456da2170d2adc3cbf4/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java#L637
11
+ */
12
+ export const applyReactNativeResponseHeadersLogic = (
13
+ headers: XHRHeaders
14
+ ): HttpHeaders => {
15
+ const parsedHeaders: HttpHeaders = { ...headers };
16
+
17
+ const setCookieHeader = getHttpHeader(headers, 'set-cookie');
18
+
19
+ if (setCookieHeader) {
20
+ const { value, originalKey } = setCookieHeader;
21
+
22
+ const cookies = splitSetCookieHeaderByComma(value);
23
+
24
+ parsedHeaders[originalKey] = cookies.length > 0 ? cookies : value;
25
+ }
26
+
27
+ return parsedHeaders;
28
+ };
@@ -0,0 +1,126 @@
1
+ import { Cookie, HttpHeaders } from '../shared/client';
2
+ import { getHttpHeader } from './getHttpHeader';
3
+
4
+ export const parseSetCookieHeader = (setCookieStr: string): Cookie => {
5
+ const parts = setCookieStr.split(';').map((p) => p.trim());
6
+ const [nameValue, ...attributes] = parts;
7
+ const [name, ...valueParts] = nameValue.split('=');
8
+
9
+ const value = valueParts.join('=');
10
+
11
+ const cookieObj: Cookie = {
12
+ name: name?.trim() || '',
13
+ value: value?.trim() || '',
14
+ };
15
+
16
+ attributes.forEach((attr) => {
17
+ const [attrName, ...attrValueParts] = attr.split('=');
18
+ const lowerAttrName = attrName.trim().toLowerCase();
19
+ const attrValue = attrValueParts.join('=');
20
+
21
+ switch (lowerAttrName) {
22
+ case 'domain':
23
+ cookieObj.domain = attrValue;
24
+
25
+ break;
26
+
27
+ case 'path':
28
+ cookieObj.path = attrValue;
29
+
30
+ break;
31
+
32
+ case 'expires':
33
+ cookieObj.expires = attrValue;
34
+
35
+ break;
36
+
37
+ case 'max-age':
38
+ cookieObj.maxAge = attrValue;
39
+
40
+ break;
41
+
42
+ case 'secure':
43
+ cookieObj.secure = true;
44
+
45
+ break;
46
+
47
+ case 'httponly':
48
+ cookieObj.httpOnly = true;
49
+
50
+ break;
51
+
52
+ case 'samesite':
53
+ cookieObj.sameSite = attrValue;
54
+
55
+ break;
56
+ }
57
+ });
58
+
59
+ return cookieObj;
60
+ };
61
+
62
+ export const splitSetCookieHeaderByComma = (header: string): string[] => {
63
+ const regex = /(?:^|,\s)([^=;,]+=[^;]+(?:;[^,]*)*)/g;
64
+ const matches: string[] = [];
65
+
66
+ let match;
67
+
68
+ while ((match = regex.exec(header)) !== null) {
69
+ matches.push(match[1].trim());
70
+ }
71
+
72
+ return matches;
73
+ };
74
+
75
+ export const parseCookieHeader = (cookieString: string): Cookie[] => {
76
+ if (!cookieString) return [];
77
+
78
+ return cookieString
79
+ .split(';')
80
+ .map((cookieStr) => {
81
+ const [name, ...valueParts] = cookieStr.trim().split('=');
82
+ const value = valueParts.join('=');
83
+
84
+ return {
85
+ name: name?.trim() || '',
86
+ value: value?.trim() || '',
87
+ };
88
+ })
89
+ .filter((cookieObj) => cookieObj.name);
90
+ };
91
+
92
+ export const parseRequestCookiesFromHeaders = (
93
+ headers: HttpHeaders
94
+ ): Cookie[] => {
95
+ const cookieHeader = getHttpHeader(headers, 'cookie');
96
+
97
+ if (!cookieHeader) {
98
+ return [];
99
+ }
100
+
101
+ const { value } = cookieHeader;
102
+
103
+ if (Array.isArray(value)) {
104
+ return value.flatMap(parseCookieHeader);
105
+ }
106
+
107
+ return parseCookieHeader(value);
108
+ };
109
+
110
+ export const parseResponseCookiesFromHeaders = (
111
+ headers: HttpHeaders
112
+ ): Cookie[] => {
113
+ const setCookieHeader = getHttpHeader(headers, 'set-cookie');
114
+
115
+ if (!setCookieHeader) {
116
+ return [];
117
+ }
118
+
119
+ const { value } = setCookieHeader;
120
+
121
+ if (Array.isArray(value)) {
122
+ return value.flatMap(parseSetCookieHeader);
123
+ }
124
+
125
+ return [parseSetCookieHeader(value)];
126
+ };
@@ -0,0 +1,17 @@
1
+ import { HttpHeaders } from '../shared/client';
2
+ import { getHttpHeader } from './getHttpHeader';
3
+
4
+ export function getContentTypeMime(headers: HttpHeaders) {
5
+ const contentType = getHttpHeader(headers, 'content-type');
6
+
7
+ if (!contentType) {
8
+ return undefined;
9
+ }
10
+
11
+ const { value } = contentType;
12
+
13
+ // Content-Type can't be an array, but if it does we simply get the first element.
14
+ const actualValue = Array.isArray(value) ? value[0] : value;
15
+
16
+ return actualValue.split(';')[0].trim();
17
+ }
@@ -0,0 +1,17 @@
1
+ import { HttpHeaders, XHRHeaders } from '../shared/client';
2
+
3
+ // Utility to get header value and actual key case-insensitively
4
+ export function getHttpHeader<T extends HttpHeaders | XHRHeaders>(
5
+ headers: T,
6
+ name: string
7
+ ) {
8
+ const lowerName = name.toLowerCase();
9
+
10
+ for (const key in headers) {
11
+ if (key.toLowerCase() === lowerName) {
12
+ return { value: headers[key], originalKey: key };
13
+ }
14
+ }
15
+
16
+ return undefined;
17
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Combines multiple HTTP header values according to RFC 7230 Section 3.2.2
3
+ *
4
+ * Per RFC 7230 Section 3.2.2: "A recipient MAY combine multiple header fields
5
+ * with the same field name into one 'field-name: field-value' pair, without
6
+ * changing the semantics of the message, by appending each subsequent field
7
+ * value to the combined field value in order, separated by a comma."
8
+ *
9
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.2
10
+ */
11
+ export function getHttpHeaderValueAsString(value: string | string[]): string {
12
+ return Array.isArray(value) ? value.join(', ') : value;
13
+ }
@@ -0,0 +1,3 @@
1
+ export const getStringSizeInBytes = (value: string) => {
2
+ return new TextEncoder().encode(value).length;
3
+ };
@@ -0,0 +1,9 @@
1
+ import { RequestPostData } from '../shared/client';
2
+
3
+ export function inferContentTypeFromPostData(postData: RequestPostData) {
4
+ if (postData?.type === 'form-data') {
5
+ return 'multipart/form-data';
6
+ }
7
+
8
+ return undefined;
9
+ }
@@ -0,0 +1,7 @@
1
+ export function safeStringify(data: unknown): string {
2
+ try {
3
+ return typeof data === 'string' ? data : JSON.stringify(data);
4
+ } catch {
5
+ return String(data);
6
+ }
7
+ }
@@ -0,0 +1,27 @@
1
+ export const isBlob = (value: unknown): value is Blob => value instanceof Blob;
2
+
3
+ export const isArrayBuffer = (
4
+ value: unknown
5
+ ): value is ArrayBuffer | ArrayBufferView =>
6
+ value instanceof ArrayBuffer || ArrayBuffer.isView(value);
7
+
8
+ export const isFormData = (value: unknown): value is FormData =>
9
+ value instanceof FormData;
10
+
11
+ export const isNullOrUndefined = (value: unknown): value is null | undefined =>
12
+ value === null || value === undefined;
13
+
14
+ export const isString = (value: unknown): value is string =>
15
+ typeof value === 'string';
16
+
17
+ export const isNumber = (value: unknown): value is number =>
18
+ typeof value === 'number' && !isNaN(value);
19
+
20
+ export const isBoolean = (value: unknown): value is boolean =>
21
+ typeof value === 'boolean';
22
+
23
+ export const isObject = (value: unknown): value is object =>
24
+ typeof value === 'object' && value !== null;
25
+
26
+ export const isArray = (value: unknown): value is unknown[] =>
27
+ Array.isArray(value);
@@ -7,6 +7,9 @@ const config: Config = {
7
7
  content: ['./src/ui/**/*.{js,ts,jsx,tsx,mdx}'],
8
8
  theme: {
9
9
  extend: {
10
+ translate: {
11
+ '0.75': '0.1875rem',
12
+ },
10
13
  colors: {
11
14
  background: 'hsl(var(--background))',
12
15
  foreground: 'hsl(var(--foreground))',