@flareapp/react-native 2.6.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 +60 -0
- package/dist/index.cjs +484 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.mts +85 -0
- package/dist/index.mjs +444 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @flareapp/react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK for [Flare](https://flareapp.io) error tracking by Spatie.
|
|
4
|
+
|
|
5
|
+
Pure JavaScript — works on both Expo (managed) and bare React Native with no
|
|
6
|
+
required native module.
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
React Native **0.79+** (Expo SDK 53+). The package relies on Metro's package
|
|
11
|
+
exports support, which is on by default from RN 0.79. It ships a Metro-targeted
|
|
12
|
+
CJS build via the `"react-native"` export condition, and the `FlareErrorBoundary`
|
|
13
|
+
imports `@flareapp/react/inject` (an export-map subpath) — both need package
|
|
14
|
+
exports enabled.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @flareapp/react-native @flareapp/react
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { flare, FlareErrorBoundary } from '@flareapp/react-native';
|
|
26
|
+
|
|
27
|
+
flare.light('your-project-key');
|
|
28
|
+
flare.setUser({ id: 1, email: 'user@example.com' });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Wrap your app in the boundary to capture React render errors:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<FlareErrorBoundary>
|
|
35
|
+
<App />
|
|
36
|
+
</FlareErrorBoundary>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What it captures
|
|
40
|
+
|
|
41
|
+
- Uncaught JS errors (via `ErrorUtils`)
|
|
42
|
+
- React render errors (via `FlareErrorBoundary`)
|
|
43
|
+
- Unhandled promise rejections (best-effort; uses the active JS engine's hook —
|
|
44
|
+
Hermes or JSC)
|
|
45
|
+
|
|
46
|
+
Device/app context is collected from React Native core, enriched with
|
|
47
|
+
`expo-device` / `expo-application` when present. Note: on iOS, Expo's `app.id`
|
|
48
|
+
(Android package name) is not available as a sync constant, so that attribute is
|
|
49
|
+
Android-only.
|
|
50
|
+
|
|
51
|
+
Delivery is best-effort. Reports are sent with `fetch`, which React Native does
|
|
52
|
+
not back with `keepalive`, so a report fired during a fatal JS crash (or while
|
|
53
|
+
the app is being backgrounded/suspended) may not finish sending. This applies to
|
|
54
|
+
JS-fatal errors too, not only native crashes.
|
|
55
|
+
|
|
56
|
+
## Not yet included
|
|
57
|
+
|
|
58
|
+
- Native crash capture (requires a native module)
|
|
59
|
+
- Metro sourcemap upload (planned as a separate package)
|
|
60
|
+
- Automatic breadcrumbs (use `flare.glow(...)` manually for now)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _flareapp_core = require("@flareapp/core");
|
|
3
|
+
let react_native = require("react-native");
|
|
4
|
+
let _flareapp_react_inject = require("@flareapp/react/inject");
|
|
5
|
+
let react = require("react");
|
|
6
|
+
|
|
7
|
+
//#region src/context/expo.ts
|
|
8
|
+
/**
|
|
9
|
+
* Lazy, synchronous Expo load. The `require(...)` calls are DIRECT string
|
|
10
|
+
* literals on purpose: Metro statically collects only literal `require('pkg')`
|
|
11
|
+
* calls and treats those inside a try/catch as OPTIONAL dependencies
|
|
12
|
+
* (`allowOptionalDependencies` is on by default for the React Native CLI and
|
|
13
|
+
* Expo), so a missing package degrades to a caught runtime throw instead of a
|
|
14
|
+
* build error. Aliasing `require` to a local (`const req = require; req('pkg')`)
|
|
15
|
+
* would defeat that static collection — Metro never adds the module to this
|
|
16
|
+
* file's dependency map, so the require would fail to resolve even when the
|
|
17
|
+
* package IS installed. So do NOT reintroduce an alias here.
|
|
18
|
+
*
|
|
19
|
+
* The `typeof require` guard keeps non-Metro/ESM environments (e.g. some test
|
|
20
|
+
* runners, where `require` is undefined) safe; under Metro `require` always
|
|
21
|
+
* exists in the `react-native`/CJS build this package ships.
|
|
22
|
+
*/
|
|
23
|
+
function loadExpoModules() {
|
|
24
|
+
const mods = {};
|
|
25
|
+
if (typeof require === "undefined") return mods;
|
|
26
|
+
try {
|
|
27
|
+
mods.device = require("expo-device");
|
|
28
|
+
} catch {}
|
|
29
|
+
try {
|
|
30
|
+
mods.application = require("expo-application");
|
|
31
|
+
} catch {}
|
|
32
|
+
return mods;
|
|
33
|
+
}
|
|
34
|
+
const DEVICE_TYPE_LABELS = {
|
|
35
|
+
1: "phone",
|
|
36
|
+
2: "tablet",
|
|
37
|
+
3: "desktop",
|
|
38
|
+
4: "tv"
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Project the synchronous Expo constants into report attributes. Only the
|
|
42
|
+
* fields that are present (non-null, non-undefined) are emitted. Async Expo
|
|
43
|
+
* getters are intentionally not used (the context collector is synchronous).
|
|
44
|
+
*/
|
|
45
|
+
function projectExpoContext(expo) {
|
|
46
|
+
const attrs = {};
|
|
47
|
+
const device = expo.device;
|
|
48
|
+
if (device) {
|
|
49
|
+
if (device.modelName != null) attrs["device.model.name"] = device.modelName;
|
|
50
|
+
if (device.osName != null) attrs["os.name"] = device.osName;
|
|
51
|
+
if (device.osVersion != null) attrs["os.version"] = device.osVersion;
|
|
52
|
+
if (device.deviceType != null) {
|
|
53
|
+
const label = DEVICE_TYPE_LABELS[device.deviceType];
|
|
54
|
+
if (label) attrs["device.type"] = label;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const application = expo.application;
|
|
58
|
+
if (application) {
|
|
59
|
+
if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
|
|
60
|
+
if (application.applicationId != null) attrs["app.id"] = application.applicationId;
|
|
61
|
+
}
|
|
62
|
+
return attrs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/context/collectReactNative.ts
|
|
67
|
+
/**
|
|
68
|
+
* Build the React Native `ContextCollector` that core's `Flare` calls on every
|
|
69
|
+
* report. Synchronous, matching `ContextCollector = (config) => Attributes`.
|
|
70
|
+
*
|
|
71
|
+
* Sources, layered:
|
|
72
|
+
* 1. RN core (`Platform`, `Dimensions`) — read on every call.
|
|
73
|
+
* 2. Expo constants — resolved once (injected for tests, else `loadExpoModules()`)
|
|
74
|
+
* and projected; absent on bare RN.
|
|
75
|
+
*
|
|
76
|
+
* The authenticated user is NOT projected here: `Flare.setUser` (inherited from
|
|
77
|
+
* core) writes the `user.*` identity keys straight to the active scope, the same
|
|
78
|
+
* model node and electron use.
|
|
79
|
+
*
|
|
80
|
+
* `Platform.Version` is a string on iOS but a number on Android, so it is
|
|
81
|
+
* stringified for the `os.version` attribute. `Platform.OS` maps to `os.name`
|
|
82
|
+
* (NOT `os.type`, which conventionally means the kernel family); when Expo is
|
|
83
|
+
* present its `osName`/`osVersion` overwrite these coarser values.
|
|
84
|
+
*/
|
|
85
|
+
function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
86
|
+
const expoAttrs = projectExpoContext(expo);
|
|
87
|
+
return (_config) => {
|
|
88
|
+
const screen = react_native.Dimensions.get("window");
|
|
89
|
+
const attrs = {
|
|
90
|
+
"os.name": react_native.Platform.OS,
|
|
91
|
+
"os.version": String(react_native.Platform.Version),
|
|
92
|
+
"device.screen.width": screen.width,
|
|
93
|
+
"device.screen.height": screen.height,
|
|
94
|
+
"device.screen.scale": screen.scale,
|
|
95
|
+
...expoAttrs
|
|
96
|
+
};
|
|
97
|
+
if (attrs["device.model.name"] == null) {
|
|
98
|
+
const model = nativeModelName();
|
|
99
|
+
if (model) attrs["device.model.name"] = model;
|
|
100
|
+
}
|
|
101
|
+
const device = buildDeviceContext(attrs);
|
|
102
|
+
if (Object.keys(device).length > 0) attrs["context.device"] = device;
|
|
103
|
+
return attrs;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Native device model from `Platform.constants`. Android exposes
|
|
108
|
+
* `Model`/`Manufacturer`/`Brand`; iOS core does not surface a device model (it
|
|
109
|
+
* needs `expo-device` or a native module), so iOS returns undefined. Prefixes the
|
|
110
|
+
* model with its maker when available (e.g. `Google Pixel 7`).
|
|
111
|
+
*/
|
|
112
|
+
function nativeModelName() {
|
|
113
|
+
if (react_native.Platform.OS !== "android") return void 0;
|
|
114
|
+
const constants = react_native.Platform.constants;
|
|
115
|
+
if (!constants?.Model) return void 0;
|
|
116
|
+
const maker = constants.Manufacturer ?? constants.Brand;
|
|
117
|
+
return maker ? `${maker} ${constants.Model}` : constants.Model;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build a human-readable `context.device` group from the collected semantic
|
|
121
|
+
* attributes. Only present fields are included, so the bare app (no Expo)
|
|
122
|
+
* naturally omits `model` / `appVersion` / `appId`.
|
|
123
|
+
*/
|
|
124
|
+
function buildDeviceContext(attrs) {
|
|
125
|
+
const device = {};
|
|
126
|
+
if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
|
|
127
|
+
const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
|
|
128
|
+
if (os) device.OS = os;
|
|
129
|
+
const width = attrs["device.screen.width"];
|
|
130
|
+
const height = attrs["device.screen.height"];
|
|
131
|
+
const scale = attrs["device.screen.scale"];
|
|
132
|
+
if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
|
|
133
|
+
if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
|
|
134
|
+
if (attrs["app.id"] != null) device.appId = attrs["app.id"];
|
|
135
|
+
return device;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/handlers/appStateFlush.ts
|
|
140
|
+
/**
|
|
141
|
+
* Flush the log buffer when the app moves to the background. iOS fires
|
|
142
|
+
* `inactive` on every transient interruption (app-switcher peek, Control Center,
|
|
143
|
+
* incoming call), so we gate on `background` ONLY to avoid flooding the network
|
|
144
|
+
* with redundant flushes. This mirrors the browser scheduler gating on `hidden`
|
|
145
|
+
* (not every blur).
|
|
146
|
+
*
|
|
147
|
+
* Delivery is best-effort: see `ReactNativeFlushScheduler`.
|
|
148
|
+
*
|
|
149
|
+
* Returns an uninstaller that removes the listener via the subscription handle
|
|
150
|
+
* (modern RN API — do NOT use the removed `AppState.removeEventListener`).
|
|
151
|
+
*/
|
|
152
|
+
function installAppStateFlush(getFlush) {
|
|
153
|
+
const subscription = react_native.AppState.addEventListener("change", (state) => {
|
|
154
|
+
if (state === "background") getFlush()?.();
|
|
155
|
+
});
|
|
156
|
+
return () => subscription.remove();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/devMode.ts
|
|
161
|
+
/** True only in a React Native dev bundle. Safe (false) everywhere else. */
|
|
162
|
+
function inDevMode() {
|
|
163
|
+
return typeof __DEV__ !== "undefined" && __DEV__ === true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/handlers/globalErrorHandler.ts
|
|
168
|
+
function getErrorUtils() {
|
|
169
|
+
return globalThis.ErrorUtils;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Wrap RN's `ErrorUtils` global handler. The wrapper reports the error and then
|
|
173
|
+
* delegates to the previously-registered handler, so React Native's own behavior
|
|
174
|
+
* (red box in dev, process crash in prod) is preserved — we observe, we do not
|
|
175
|
+
* swallow. No-op when `ErrorUtils` is unavailable.
|
|
176
|
+
*
|
|
177
|
+
* Fatal delivery (`onFatal`). On a FATAL error in a PRODUCTION bundle the
|
|
178
|
+
* previous handler is what tears the app down, and our report is an async
|
|
179
|
+
* `fetch` the OS would kill the instant the app dies — so a bare report almost
|
|
180
|
+
* never sends. When `onFatal` is supplied we DEFER the previous handler until
|
|
181
|
+
* `onFatal()` settles; it drains the transport via core's `flush(timeoutMs)`,
|
|
182
|
+
* buying the report time to send before the crash. RN does not crash on its own
|
|
183
|
+
* (the default handler triggers it), so deferring the delegate genuinely delays
|
|
184
|
+
* the crash. This mirrors Sentry's React Native SDK. It is skipped in `__DEV__`
|
|
185
|
+
* (don't fight the red box / debugger) and guarded by a re-entrancy latch, so a
|
|
186
|
+
* second fatal arriving mid-flush delegates immediately rather than racing two
|
|
187
|
+
* shutdowns.
|
|
188
|
+
*
|
|
189
|
+
* Returns an uninstaller that restores the previous handler.
|
|
190
|
+
*/
|
|
191
|
+
function installGlobalErrorHandler(report, onFatal) {
|
|
192
|
+
const errorUtils = getErrorUtils();
|
|
193
|
+
if (!errorUtils) return () => {};
|
|
194
|
+
const previous = errorUtils.getGlobalHandler();
|
|
195
|
+
let handlingFatal = false;
|
|
196
|
+
const handler = (error, isFatal) => {
|
|
197
|
+
try {
|
|
198
|
+
report((0, _flareapp_core.convertToError)(error), Boolean(isFatal));
|
|
199
|
+
} catch {}
|
|
200
|
+
if (isFatal && onFatal && !inDevMode() && !handlingFatal) {
|
|
201
|
+
handlingFatal = true;
|
|
202
|
+
onFatal().catch(() => {}).then(() => {
|
|
203
|
+
try {
|
|
204
|
+
previous?.(error, isFatal);
|
|
205
|
+
} finally {
|
|
206
|
+
handlingFatal = false;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
previous?.(error, isFatal);
|
|
212
|
+
};
|
|
213
|
+
errorUtils.setGlobalHandler(handler);
|
|
214
|
+
return () => {
|
|
215
|
+
errorUtils.setGlobalHandler(previous ?? (() => {}));
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/handlers/rejectionTracking.ts
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the promise-rejection enabler for the ACTIVE JS engine.
|
|
223
|
+
*
|
|
224
|
+
* - Hermes (RN's default engine since 0.70) tracks rejections on its native
|
|
225
|
+
* Promise via `global.HermesInternal.enablePromiseRejectionTracker`. The
|
|
226
|
+
* `promise` npm polyfill is NOT the runtime Promise on Hermes, so the
|
|
227
|
+
* polyfill's `rejection-tracking.enable()` would hook unused objects and
|
|
228
|
+
* silently never fire — we must use the Hermes hook here.
|
|
229
|
+
* - JSC / non-Hermes: RN polyfills `global.Promise` with the `promise` package,
|
|
230
|
+
* so `promise/setimmediate/rejection-tracking.enable()` is the real hook.
|
|
231
|
+
*
|
|
232
|
+
* Returns null when neither is reachable. Exported (with injectable deps) for
|
|
233
|
+
* direct unit testing of the ordering; not re-exported from the package entry.
|
|
234
|
+
*/
|
|
235
|
+
function resolveRejectionEnabler(deps = {}) {
|
|
236
|
+
const hermes = deps.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
|
|
237
|
+
if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
|
|
238
|
+
const req = deps.requirePolyfill !== void 0 ? deps.requirePolyfill : typeof require !== "undefined" ? require : null;
|
|
239
|
+
if (req) try {
|
|
240
|
+
const tracker = req("promise/setimmediate/rejection-tracking");
|
|
241
|
+
if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
|
|
242
|
+
} catch {}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Best-effort capture of unhandled promise rejections, engine-aware. RN routes
|
|
247
|
+
* these through its engine's tracker (NOT `window.onunhandledrejection`):
|
|
248
|
+
* `HermesInternal.enablePromiseRejectionTracker` on Hermes, the `promise`
|
|
249
|
+
* polyfill on JSC. Reasons are routed exactly like the browser
|
|
250
|
+
* `unhandledrejection` handler (core's `routeRejection`), so Error reasons keep
|
|
251
|
+
* their stack via `reportSilently`.
|
|
252
|
+
*
|
|
253
|
+
* Engine-dependent, NOT version-dependent: if no engine hook is reachable this
|
|
254
|
+
* is a no-op (uncaught throws via ErrorUtils still work) and emits a dev-only
|
|
255
|
+
* debug line; it must never crash. The `enable(...)` invocation is wrapped so a
|
|
256
|
+
* throwing engine hook degrades to no-op instead of propagating.
|
|
257
|
+
*
|
|
258
|
+
* Chaining caveat: enabling REPLACES the engine's current callbacks (RN
|
|
259
|
+
* registers its own dev warning) and neither engine exposes a getter for the
|
|
260
|
+
* previous ones, so we cannot truly chain RN's default. To avoid swallowing that
|
|
261
|
+
* developer signal, `onUnhandled` re-emits a `console.warn` in dev (`__DEV__`).
|
|
262
|
+
*
|
|
263
|
+
* Returns an uninstaller that re-enables with no-op callbacks (no clean disable
|
|
264
|
+
* exists on either engine).
|
|
265
|
+
*/
|
|
266
|
+
function installRejectionTracking(reporter, deps = {}) {
|
|
267
|
+
const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
|
|
268
|
+
if (!enable) {
|
|
269
|
+
if (inDevMode()) console.debug("[flare] No promise-rejection hook for this JS engine; rejections not captured.");
|
|
270
|
+
return () => {};
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
enable({
|
|
274
|
+
allRejections: true,
|
|
275
|
+
onUnhandled: (_id, error) => {
|
|
276
|
+
if (inDevMode()) console.warn("[flare] Unhandled promise rejection:", error);
|
|
277
|
+
(0, _flareapp_core.routeRejection)(reporter, error);
|
|
278
|
+
},
|
|
279
|
+
onHandled: () => {}
|
|
280
|
+
});
|
|
281
|
+
} catch {
|
|
282
|
+
if (inDevMode()) console.debug("[flare] Promise-rejection hook threw on enable; rejections not captured.");
|
|
283
|
+
return () => {};
|
|
284
|
+
}
|
|
285
|
+
return () => {
|
|
286
|
+
try {
|
|
287
|
+
enable({
|
|
288
|
+
allRejections: true,
|
|
289
|
+
onUnhandled: () => {},
|
|
290
|
+
onHandled: () => {}
|
|
291
|
+
});
|
|
292
|
+
} catch {}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/ReactNativeFlushScheduler.ts
|
|
298
|
+
/**
|
|
299
|
+
* Passive flush scheduler for React Native. Core's `Logger` calls `register`
|
|
300
|
+
* once during construction; this stores the flush callback and exposes a plain,
|
|
301
|
+
* argument-less caller via `getFlush()`. The actual trigger (AppState ->
|
|
302
|
+
* background) is wired separately in `Flare.install()` so it stays symmetric
|
|
303
|
+
* with handler teardown.
|
|
304
|
+
*
|
|
305
|
+
* Deliberately calls `flush()` WITHOUT `{ keepalive: true }`: RN's fetch (over
|
|
306
|
+
* XMLHttpRequest) does not reliably honor keepalive, so a backgrounding flush is
|
|
307
|
+
* best-effort and may be dropped if the OS suspends the app mid-request.
|
|
308
|
+
*/
|
|
309
|
+
var ReactNativeFlushScheduler = class {
|
|
310
|
+
flushFn = null;
|
|
311
|
+
register(flush) {
|
|
312
|
+
this.flushFn = flush;
|
|
313
|
+
}
|
|
314
|
+
getFlush() {
|
|
315
|
+
const flush = this.flushFn;
|
|
316
|
+
if (!flush) return void 0;
|
|
317
|
+
return () => {
|
|
318
|
+
flush();
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/Flare.ts
|
|
325
|
+
const RN_SDK_NAME = "@flareapp/react-native";
|
|
326
|
+
const RN_SDK_VERSION = "2.6.0";
|
|
327
|
+
const RN_FRAMEWORK_NAME = "React Native";
|
|
328
|
+
const FATAL_FLUSH_TIMEOUT_MS = 2e3;
|
|
329
|
+
/**
|
|
330
|
+
* React Native `Flare` singleton (exposed as `flare` from the package root).
|
|
331
|
+
*
|
|
332
|
+
* Subclasses core's `Flare`, injecting the RN seams:
|
|
333
|
+
* - core `Api` (fetch is native in RN),
|
|
334
|
+
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
335
|
+
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
336
|
+
* follow-up),
|
|
337
|
+
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
338
|
+
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
339
|
+
*
|
|
340
|
+
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
341
|
+
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
342
|
+
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
343
|
+
*/
|
|
344
|
+
var ReactNativeFlare = class extends _flareapp_core.Flare {
|
|
345
|
+
scheduler;
|
|
346
|
+
rejectionDeps;
|
|
347
|
+
installed = false;
|
|
348
|
+
uninstallers = [];
|
|
349
|
+
/**
|
|
350
|
+
* @param rejectionDeps test seam for the rejection hook. Defaults to `{}`
|
|
351
|
+
* (resolve the active engine's tracker — Hermes or JSC). Tests pass
|
|
352
|
+
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
353
|
+
* tracker as a leaking side effect.
|
|
354
|
+
*/
|
|
355
|
+
constructor(rejectionDeps = {}) {
|
|
356
|
+
const scheduler = new ReactNativeFlushScheduler();
|
|
357
|
+
const collector = makeReactNativeContextCollector();
|
|
358
|
+
super(new _flareapp_core.Api(), collector, new _flareapp_core.NullFileReader(), new _flareapp_core.GlobalScopeProvider(), scheduler);
|
|
359
|
+
this.scheduler = scheduler;
|
|
360
|
+
this.rejectionDeps = rejectionDeps;
|
|
361
|
+
this.setSdkInfo({
|
|
362
|
+
name: RN_SDK_NAME,
|
|
363
|
+
version: RN_SDK_VERSION
|
|
364
|
+
});
|
|
365
|
+
this.setFramework({ name: RN_FRAMEWORK_NAME });
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Force the framework identity to "React Native". The wrapped
|
|
369
|
+
* `@flareapp/react` boundary tags every flare it injects as `React` (via
|
|
370
|
+
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
371
|
+
* name here while preserving whatever version the caller supplied (the React
|
|
372
|
+
* renderer version when the boundary tags it).
|
|
373
|
+
*/
|
|
374
|
+
setFramework(framework) {
|
|
375
|
+
return super.setFramework({
|
|
376
|
+
...framework,
|
|
377
|
+
name: RN_FRAMEWORK_NAME
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Set the API key (and optional debug flag), then install the global
|
|
382
|
+
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
383
|
+
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
384
|
+
* idempotent).
|
|
385
|
+
*/
|
|
386
|
+
light(key, debug) {
|
|
387
|
+
super.light(key, debug);
|
|
388
|
+
this.install();
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
393
|
+
* and clear the install guard so a later `light()` re-installs. For tests
|
|
394
|
+
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
395
|
+
*/
|
|
396
|
+
removeHandlers() {
|
|
397
|
+
for (const uninstall of this.uninstallers) try {
|
|
398
|
+
uninstall();
|
|
399
|
+
} catch {}
|
|
400
|
+
this.uninstallers = [];
|
|
401
|
+
this.installed = false;
|
|
402
|
+
}
|
|
403
|
+
install() {
|
|
404
|
+
if (this.installed) return;
|
|
405
|
+
this.installed = true;
|
|
406
|
+
this.uninstallers.push(installGlobalErrorHandler((error, isFatal) => {
|
|
407
|
+
this.reportSilently(error, { "error.fatal": isFatal });
|
|
408
|
+
}, () => this.flush(FATAL_FLUSH_TIMEOUT_MS)), installRejectionTracking({
|
|
409
|
+
reportSilently: (error) => this.reportSilently(error),
|
|
410
|
+
reportUnhandledRejection: (message) => this.reportUnhandledRejection(message)
|
|
411
|
+
}, this.rejectionDeps), installAppStateFlush(() => this.scheduler.getFlush()));
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/singleton.ts
|
|
417
|
+
const flare = new ReactNativeFlare();
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/FlareErrorBoundary.ts
|
|
421
|
+
/**
|
|
422
|
+
* React Native error boundary. A thin wrapper over `@flareapp/react`'s
|
|
423
|
+
* `/inject` boundary that injects the RN `flare` singleton.
|
|
424
|
+
*
|
|
425
|
+
* `flare` is applied AFTER `{...props}` so a consumer cannot override the
|
|
426
|
+
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
427
|
+
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
428
|
+
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
429
|
+
* the boundary only calls a core-level method (`reportSilently`), which
|
|
430
|
+
* `ReactNativeFlare` inherits.
|
|
431
|
+
*/
|
|
432
|
+
function FlareErrorBoundary(props) {
|
|
433
|
+
return (0, react.createElement)(_flareapp_react_inject.FlareErrorBoundary, {
|
|
434
|
+
...props,
|
|
435
|
+
flare
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
Object.defineProperty(exports, 'DEFAULT_URL_DENYLIST', {
|
|
441
|
+
enumerable: true,
|
|
442
|
+
get: function () {
|
|
443
|
+
return _flareapp_core.DEFAULT_URL_DENYLIST;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
Object.defineProperty(exports, 'Flare', {
|
|
447
|
+
enumerable: true,
|
|
448
|
+
get: function () {
|
|
449
|
+
return _flareapp_core.Flare;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
exports.FlareErrorBoundary = FlareErrorBoundary;
|
|
453
|
+
Object.defineProperty(exports, 'GlobalScopeProvider', {
|
|
454
|
+
enumerable: true,
|
|
455
|
+
get: function () {
|
|
456
|
+
return _flareapp_core.GlobalScopeProvider;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
Object.defineProperty(exports, 'NullFileReader', {
|
|
460
|
+
enumerable: true,
|
|
461
|
+
get: function () {
|
|
462
|
+
return _flareapp_core.NullFileReader;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
exports.ReactNativeFlare = ReactNativeFlare;
|
|
466
|
+
Object.defineProperty(exports, 'convertToError', {
|
|
467
|
+
enumerable: true,
|
|
468
|
+
get: function () {
|
|
469
|
+
return _flareapp_core.convertToError;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
exports.flare = flare;
|
|
473
|
+
Object.defineProperty(exports, 'redactUrlQuery', {
|
|
474
|
+
enumerable: true,
|
|
475
|
+
get: function () {
|
|
476
|
+
return _flareapp_core.redactUrlQuery;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
Object.defineProperty(exports, 'resolveDenylist', {
|
|
480
|
+
enumerable: true,
|
|
481
|
+
get: function () {
|
|
482
|
+
return _flareapp_core.resolveDenylist;
|
|
483
|
+
}
|
|
484
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AttributeValue, Attributes, Config, ContextCollector, DEFAULT_URL_DENYLIST, FileReader, Flare, Flare as Flare$1, FlushFn, FlushScheduler, Framework, Framework as Framework$1, GlobalScopeProvider, Glow, MessageLevel, NullFileReader, Report, ScopeProvider, SdkInfo, StackFrame, User, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
|
|
2
|
+
import { FlareErrorBoundaryProps } from "@flareapp/react/inject";
|
|
3
|
+
import { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/handlers/rejectionTracking.d.ts
|
|
6
|
+
type EnableOptions = {
|
|
7
|
+
allRejections?: boolean;
|
|
8
|
+
onUnhandled?: (id: number, error: unknown) => void;
|
|
9
|
+
onHandled?: (id: number) => void;
|
|
10
|
+
};
|
|
11
|
+
type RejectionEnabler = (options: EnableOptions) => void;
|
|
12
|
+
type RejectionDeps = {
|
|
13
|
+
enable?: RejectionEnabler | null;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/Flare.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* React Native `Flare` singleton (exposed as `flare` from the package root).
|
|
19
|
+
*
|
|
20
|
+
* Subclasses core's `Flare`, injecting the RN seams:
|
|
21
|
+
* - core `Api` (fetch is native in RN),
|
|
22
|
+
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
23
|
+
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
24
|
+
* follow-up),
|
|
25
|
+
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
26
|
+
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
27
|
+
*
|
|
28
|
+
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
29
|
+
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
30
|
+
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
31
|
+
*/
|
|
32
|
+
declare class ReactNativeFlare extends Flare$1 {
|
|
33
|
+
private readonly scheduler;
|
|
34
|
+
private readonly rejectionDeps;
|
|
35
|
+
private installed;
|
|
36
|
+
private uninstallers;
|
|
37
|
+
/**
|
|
38
|
+
* @param rejectionDeps test seam for the rejection hook. Defaults to `{}`
|
|
39
|
+
* (resolve the active engine's tracker — Hermes or JSC). Tests pass
|
|
40
|
+
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
41
|
+
* tracker as a leaking side effect.
|
|
42
|
+
*/
|
|
43
|
+
constructor(rejectionDeps?: RejectionDeps);
|
|
44
|
+
/**
|
|
45
|
+
* Force the framework identity to "React Native". The wrapped
|
|
46
|
+
* `@flareapp/react` boundary tags every flare it injects as `React` (via
|
|
47
|
+
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
48
|
+
* name here while preserving whatever version the caller supplied (the React
|
|
49
|
+
* renderer version when the boundary tags it).
|
|
50
|
+
*/
|
|
51
|
+
setFramework(framework: Framework$1): this;
|
|
52
|
+
/**
|
|
53
|
+
* Set the API key (and optional debug flag), then install the global
|
|
54
|
+
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
55
|
+
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
56
|
+
* idempotent).
|
|
57
|
+
*/
|
|
58
|
+
light(key?: string, debug?: boolean): this;
|
|
59
|
+
/**
|
|
60
|
+
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
61
|
+
* and clear the install guard so a later `light()` re-installs. For tests
|
|
62
|
+
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
63
|
+
*/
|
|
64
|
+
removeHandlers(): void;
|
|
65
|
+
private install;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/singleton.d.ts
|
|
69
|
+
declare const flare: ReactNativeFlare;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/FlareErrorBoundary.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* React Native error boundary. A thin wrapper over `@flareapp/react`'s
|
|
74
|
+
* `/inject` boundary that injects the RN `flare` singleton.
|
|
75
|
+
*
|
|
76
|
+
* `flare` is applied AFTER `{...props}` so a consumer cannot override the
|
|
77
|
+
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
78
|
+
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
79
|
+
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
80
|
+
* the boundary only calls a core-level method (`reportSilently`), which
|
|
81
|
+
* `ReactNativeFlare` inherits.
|
|
82
|
+
*/
|
|
83
|
+
declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { type AttributeValue, type Attributes, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type FileReader, Flare, FlareErrorBoundary, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type MessageLevel, NullFileReader, ReactNativeFlare, type Report, type ScopeProvider, type SdkInfo, type StackFrame, type User, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AttributeValue, Attributes, Config, ContextCollector, DEFAULT_URL_DENYLIST, FileReader, Flare, Flare as Flare$1, FlushFn, FlushScheduler, Framework, Framework as Framework$1, GlobalScopeProvider, Glow, MessageLevel, NullFileReader, Report, ScopeProvider, SdkInfo, StackFrame, User, convertToError, redactUrlQuery, resolveDenylist } from "@flareapp/core";
|
|
2
|
+
import { FlareErrorBoundaryProps } from "@flareapp/react/inject";
|
|
3
|
+
import { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/handlers/rejectionTracking.d.ts
|
|
6
|
+
type EnableOptions = {
|
|
7
|
+
allRejections?: boolean;
|
|
8
|
+
onUnhandled?: (id: number, error: unknown) => void;
|
|
9
|
+
onHandled?: (id: number) => void;
|
|
10
|
+
};
|
|
11
|
+
type RejectionEnabler = (options: EnableOptions) => void;
|
|
12
|
+
type RejectionDeps = {
|
|
13
|
+
enable?: RejectionEnabler | null;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/Flare.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* React Native `Flare` singleton (exposed as `flare` from the package root).
|
|
19
|
+
*
|
|
20
|
+
* Subclasses core's `Flare`, injecting the RN seams:
|
|
21
|
+
* - core `Api` (fetch is native in RN),
|
|
22
|
+
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
23
|
+
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
24
|
+
* follow-up),
|
|
25
|
+
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
26
|
+
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
27
|
+
*
|
|
28
|
+
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
29
|
+
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
30
|
+
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
31
|
+
*/
|
|
32
|
+
declare class ReactNativeFlare extends Flare$1 {
|
|
33
|
+
private readonly scheduler;
|
|
34
|
+
private readonly rejectionDeps;
|
|
35
|
+
private installed;
|
|
36
|
+
private uninstallers;
|
|
37
|
+
/**
|
|
38
|
+
* @param rejectionDeps test seam for the rejection hook. Defaults to `{}`
|
|
39
|
+
* (resolve the active engine's tracker — Hermes or JSC). Tests pass
|
|
40
|
+
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
41
|
+
* tracker as a leaking side effect.
|
|
42
|
+
*/
|
|
43
|
+
constructor(rejectionDeps?: RejectionDeps);
|
|
44
|
+
/**
|
|
45
|
+
* Force the framework identity to "React Native". The wrapped
|
|
46
|
+
* `@flareapp/react` boundary tags every flare it injects as `React` (via
|
|
47
|
+
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
48
|
+
* name here while preserving whatever version the caller supplied (the React
|
|
49
|
+
* renderer version when the boundary tags it).
|
|
50
|
+
*/
|
|
51
|
+
setFramework(framework: Framework$1): this;
|
|
52
|
+
/**
|
|
53
|
+
* Set the API key (and optional debug flag), then install the global
|
|
54
|
+
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
55
|
+
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
56
|
+
* idempotent).
|
|
57
|
+
*/
|
|
58
|
+
light(key?: string, debug?: boolean): this;
|
|
59
|
+
/**
|
|
60
|
+
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
61
|
+
* and clear the install guard so a later `light()` re-installs. For tests
|
|
62
|
+
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
63
|
+
*/
|
|
64
|
+
removeHandlers(): void;
|
|
65
|
+
private install;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/singleton.d.ts
|
|
69
|
+
declare const flare: ReactNativeFlare;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/FlareErrorBoundary.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* React Native error boundary. A thin wrapper over `@flareapp/react`'s
|
|
74
|
+
* `/inject` boundary that injects the RN `flare` singleton.
|
|
75
|
+
*
|
|
76
|
+
* `flare` is applied AFTER `{...props}` so a consumer cannot override the
|
|
77
|
+
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
78
|
+
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
79
|
+
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
80
|
+
* the boundary only calls a core-level method (`reportSilently`), which
|
|
81
|
+
* `ReactNativeFlare` inherits.
|
|
82
|
+
*/
|
|
83
|
+
declare function FlareErrorBoundary(props: Omit<FlareErrorBoundaryProps, 'flare'>): ReactElement;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { type AttributeValue, type Attributes, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type FileReader, Flare, FlareErrorBoundary, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type MessageLevel, NullFileReader, ReactNativeFlare, type Report, type ScopeProvider, type SdkInfo, type StackFrame, type User, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Api, DEFAULT_URL_DENYLIST, Flare, Flare as Flare$1, GlobalScopeProvider, GlobalScopeProvider as GlobalScopeProvider$1, NullFileReader, NullFileReader as NullFileReader$1, convertToError, convertToError as convertToError$1, redactUrlQuery, resolveDenylist, routeRejection } from "@flareapp/core";
|
|
3
|
+
import { AppState, Dimensions, Platform } from "react-native";
|
|
4
|
+
import { FlareErrorBoundary as FlareErrorBoundary$1 } from "@flareapp/react/inject";
|
|
5
|
+
import { createElement } from "react";
|
|
6
|
+
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/context/expo.ts
|
|
12
|
+
/**
|
|
13
|
+
* Lazy, synchronous Expo load. The `require(...)` calls are DIRECT string
|
|
14
|
+
* literals on purpose: Metro statically collects only literal `require('pkg')`
|
|
15
|
+
* calls and treats those inside a try/catch as OPTIONAL dependencies
|
|
16
|
+
* (`allowOptionalDependencies` is on by default for the React Native CLI and
|
|
17
|
+
* Expo), so a missing package degrades to a caught runtime throw instead of a
|
|
18
|
+
* build error. Aliasing `require` to a local (`const req = require; req('pkg')`)
|
|
19
|
+
* would defeat that static collection — Metro never adds the module to this
|
|
20
|
+
* file's dependency map, so the require would fail to resolve even when the
|
|
21
|
+
* package IS installed. So do NOT reintroduce an alias here.
|
|
22
|
+
*
|
|
23
|
+
* The `typeof require` guard keeps non-Metro/ESM environments (e.g. some test
|
|
24
|
+
* runners, where `require` is undefined) safe; under Metro `require` always
|
|
25
|
+
* exists in the `react-native`/CJS build this package ships.
|
|
26
|
+
*/
|
|
27
|
+
function loadExpoModules() {
|
|
28
|
+
const mods = {};
|
|
29
|
+
if (typeof __require === "undefined") return mods;
|
|
30
|
+
try {
|
|
31
|
+
mods.device = __require("expo-device");
|
|
32
|
+
} catch {}
|
|
33
|
+
try {
|
|
34
|
+
mods.application = __require("expo-application");
|
|
35
|
+
} catch {}
|
|
36
|
+
return mods;
|
|
37
|
+
}
|
|
38
|
+
const DEVICE_TYPE_LABELS = {
|
|
39
|
+
1: "phone",
|
|
40
|
+
2: "tablet",
|
|
41
|
+
3: "desktop",
|
|
42
|
+
4: "tv"
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Project the synchronous Expo constants into report attributes. Only the
|
|
46
|
+
* fields that are present (non-null, non-undefined) are emitted. Async Expo
|
|
47
|
+
* getters are intentionally not used (the context collector is synchronous).
|
|
48
|
+
*/
|
|
49
|
+
function projectExpoContext(expo) {
|
|
50
|
+
const attrs = {};
|
|
51
|
+
const device = expo.device;
|
|
52
|
+
if (device) {
|
|
53
|
+
if (device.modelName != null) attrs["device.model.name"] = device.modelName;
|
|
54
|
+
if (device.osName != null) attrs["os.name"] = device.osName;
|
|
55
|
+
if (device.osVersion != null) attrs["os.version"] = device.osVersion;
|
|
56
|
+
if (device.deviceType != null) {
|
|
57
|
+
const label = DEVICE_TYPE_LABELS[device.deviceType];
|
|
58
|
+
if (label) attrs["device.type"] = label;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const application = expo.application;
|
|
62
|
+
if (application) {
|
|
63
|
+
if (application.nativeApplicationVersion != null) attrs["app.version"] = application.nativeApplicationVersion;
|
|
64
|
+
if (application.applicationId != null) attrs["app.id"] = application.applicationId;
|
|
65
|
+
}
|
|
66
|
+
return attrs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/context/collectReactNative.ts
|
|
71
|
+
/**
|
|
72
|
+
* Build the React Native `ContextCollector` that core's `Flare` calls on every
|
|
73
|
+
* report. Synchronous, matching `ContextCollector = (config) => Attributes`.
|
|
74
|
+
*
|
|
75
|
+
* Sources, layered:
|
|
76
|
+
* 1. RN core (`Platform`, `Dimensions`) — read on every call.
|
|
77
|
+
* 2. Expo constants — resolved once (injected for tests, else `loadExpoModules()`)
|
|
78
|
+
* and projected; absent on bare RN.
|
|
79
|
+
*
|
|
80
|
+
* The authenticated user is NOT projected here: `Flare.setUser` (inherited from
|
|
81
|
+
* core) writes the `user.*` identity keys straight to the active scope, the same
|
|
82
|
+
* model node and electron use.
|
|
83
|
+
*
|
|
84
|
+
* `Platform.Version` is a string on iOS but a number on Android, so it is
|
|
85
|
+
* stringified for the `os.version` attribute. `Platform.OS` maps to `os.name`
|
|
86
|
+
* (NOT `os.type`, which conventionally means the kernel family); when Expo is
|
|
87
|
+
* present its `osName`/`osVersion` overwrite these coarser values.
|
|
88
|
+
*/
|
|
89
|
+
function makeReactNativeContextCollector(expo = loadExpoModules()) {
|
|
90
|
+
const expoAttrs = projectExpoContext(expo);
|
|
91
|
+
return (_config) => {
|
|
92
|
+
const screen = Dimensions.get("window");
|
|
93
|
+
const attrs = {
|
|
94
|
+
"os.name": Platform.OS,
|
|
95
|
+
"os.version": String(Platform.Version),
|
|
96
|
+
"device.screen.width": screen.width,
|
|
97
|
+
"device.screen.height": screen.height,
|
|
98
|
+
"device.screen.scale": screen.scale,
|
|
99
|
+
...expoAttrs
|
|
100
|
+
};
|
|
101
|
+
if (attrs["device.model.name"] == null) {
|
|
102
|
+
const model = nativeModelName();
|
|
103
|
+
if (model) attrs["device.model.name"] = model;
|
|
104
|
+
}
|
|
105
|
+
const device = buildDeviceContext(attrs);
|
|
106
|
+
if (Object.keys(device).length > 0) attrs["context.device"] = device;
|
|
107
|
+
return attrs;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Native device model from `Platform.constants`. Android exposes
|
|
112
|
+
* `Model`/`Manufacturer`/`Brand`; iOS core does not surface a device model (it
|
|
113
|
+
* needs `expo-device` or a native module), so iOS returns undefined. Prefixes the
|
|
114
|
+
* model with its maker when available (e.g. `Google Pixel 7`).
|
|
115
|
+
*/
|
|
116
|
+
function nativeModelName() {
|
|
117
|
+
if (Platform.OS !== "android") return void 0;
|
|
118
|
+
const constants = Platform.constants;
|
|
119
|
+
if (!constants?.Model) return void 0;
|
|
120
|
+
const maker = constants.Manufacturer ?? constants.Brand;
|
|
121
|
+
return maker ? `${maker} ${constants.Model}` : constants.Model;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a human-readable `context.device` group from the collected semantic
|
|
125
|
+
* attributes. Only present fields are included, so the bare app (no Expo)
|
|
126
|
+
* naturally omits `model` / `appVersion` / `appId`.
|
|
127
|
+
*/
|
|
128
|
+
function buildDeviceContext(attrs) {
|
|
129
|
+
const device = {};
|
|
130
|
+
if (attrs["device.model.name"] != null) device.model = attrs["device.model.name"];
|
|
131
|
+
const os = [attrs["os.name"], attrs["os.version"]].filter((v) => v != null).join(" ");
|
|
132
|
+
if (os) device.OS = os;
|
|
133
|
+
const width = attrs["device.screen.width"];
|
|
134
|
+
const height = attrs["device.screen.height"];
|
|
135
|
+
const scale = attrs["device.screen.scale"];
|
|
136
|
+
if (width != null && height != null) device.screen = scale != null ? `${width} × ${height} @ ${scale}x` : `${width} × ${height}`;
|
|
137
|
+
if (attrs["app.version"] != null) device.appVersion = attrs["app.version"];
|
|
138
|
+
if (attrs["app.id"] != null) device.appId = attrs["app.id"];
|
|
139
|
+
return device;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/handlers/appStateFlush.ts
|
|
144
|
+
/**
|
|
145
|
+
* Flush the log buffer when the app moves to the background. iOS fires
|
|
146
|
+
* `inactive` on every transient interruption (app-switcher peek, Control Center,
|
|
147
|
+
* incoming call), so we gate on `background` ONLY to avoid flooding the network
|
|
148
|
+
* with redundant flushes. This mirrors the browser scheduler gating on `hidden`
|
|
149
|
+
* (not every blur).
|
|
150
|
+
*
|
|
151
|
+
* Delivery is best-effort: see `ReactNativeFlushScheduler`.
|
|
152
|
+
*
|
|
153
|
+
* Returns an uninstaller that removes the listener via the subscription handle
|
|
154
|
+
* (modern RN API — do NOT use the removed `AppState.removeEventListener`).
|
|
155
|
+
*/
|
|
156
|
+
function installAppStateFlush(getFlush) {
|
|
157
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
158
|
+
if (state === "background") getFlush()?.();
|
|
159
|
+
});
|
|
160
|
+
return () => subscription.remove();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/devMode.ts
|
|
165
|
+
/** True only in a React Native dev bundle. Safe (false) everywhere else. */
|
|
166
|
+
function inDevMode() {
|
|
167
|
+
return typeof __DEV__ !== "undefined" && __DEV__ === true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/handlers/globalErrorHandler.ts
|
|
172
|
+
function getErrorUtils() {
|
|
173
|
+
return globalThis.ErrorUtils;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Wrap RN's `ErrorUtils` global handler. The wrapper reports the error and then
|
|
177
|
+
* delegates to the previously-registered handler, so React Native's own behavior
|
|
178
|
+
* (red box in dev, process crash in prod) is preserved — we observe, we do not
|
|
179
|
+
* swallow. No-op when `ErrorUtils` is unavailable.
|
|
180
|
+
*
|
|
181
|
+
* Fatal delivery (`onFatal`). On a FATAL error in a PRODUCTION bundle the
|
|
182
|
+
* previous handler is what tears the app down, and our report is an async
|
|
183
|
+
* `fetch` the OS would kill the instant the app dies — so a bare report almost
|
|
184
|
+
* never sends. When `onFatal` is supplied we DEFER the previous handler until
|
|
185
|
+
* `onFatal()` settles; it drains the transport via core's `flush(timeoutMs)`,
|
|
186
|
+
* buying the report time to send before the crash. RN does not crash on its own
|
|
187
|
+
* (the default handler triggers it), so deferring the delegate genuinely delays
|
|
188
|
+
* the crash. This mirrors Sentry's React Native SDK. It is skipped in `__DEV__`
|
|
189
|
+
* (don't fight the red box / debugger) and guarded by a re-entrancy latch, so a
|
|
190
|
+
* second fatal arriving mid-flush delegates immediately rather than racing two
|
|
191
|
+
* shutdowns.
|
|
192
|
+
*
|
|
193
|
+
* Returns an uninstaller that restores the previous handler.
|
|
194
|
+
*/
|
|
195
|
+
function installGlobalErrorHandler(report, onFatal) {
|
|
196
|
+
const errorUtils = getErrorUtils();
|
|
197
|
+
if (!errorUtils) return () => {};
|
|
198
|
+
const previous = errorUtils.getGlobalHandler();
|
|
199
|
+
let handlingFatal = false;
|
|
200
|
+
const handler = (error, isFatal) => {
|
|
201
|
+
try {
|
|
202
|
+
report(convertToError$1(error), Boolean(isFatal));
|
|
203
|
+
} catch {}
|
|
204
|
+
if (isFatal && onFatal && !inDevMode() && !handlingFatal) {
|
|
205
|
+
handlingFatal = true;
|
|
206
|
+
onFatal().catch(() => {}).then(() => {
|
|
207
|
+
try {
|
|
208
|
+
previous?.(error, isFatal);
|
|
209
|
+
} finally {
|
|
210
|
+
handlingFatal = false;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
previous?.(error, isFatal);
|
|
216
|
+
};
|
|
217
|
+
errorUtils.setGlobalHandler(handler);
|
|
218
|
+
return () => {
|
|
219
|
+
errorUtils.setGlobalHandler(previous ?? (() => {}));
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/handlers/rejectionTracking.ts
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the promise-rejection enabler for the ACTIVE JS engine.
|
|
227
|
+
*
|
|
228
|
+
* - Hermes (RN's default engine since 0.70) tracks rejections on its native
|
|
229
|
+
* Promise via `global.HermesInternal.enablePromiseRejectionTracker`. The
|
|
230
|
+
* `promise` npm polyfill is NOT the runtime Promise on Hermes, so the
|
|
231
|
+
* polyfill's `rejection-tracking.enable()` would hook unused objects and
|
|
232
|
+
* silently never fire — we must use the Hermes hook here.
|
|
233
|
+
* - JSC / non-Hermes: RN polyfills `global.Promise` with the `promise` package,
|
|
234
|
+
* so `promise/setimmediate/rejection-tracking.enable()` is the real hook.
|
|
235
|
+
*
|
|
236
|
+
* Returns null when neither is reachable. Exported (with injectable deps) for
|
|
237
|
+
* direct unit testing of the ordering; not re-exported from the package entry.
|
|
238
|
+
*/
|
|
239
|
+
function resolveRejectionEnabler(deps = {}) {
|
|
240
|
+
const hermes = deps.hermes !== void 0 ? deps.hermes : globalThis.HermesInternal;
|
|
241
|
+
if (hermes && typeof hermes.enablePromiseRejectionTracker === "function") return (options) => hermes.enablePromiseRejectionTracker(options);
|
|
242
|
+
const req = deps.requirePolyfill !== void 0 ? deps.requirePolyfill : typeof __require !== "undefined" ? __require : null;
|
|
243
|
+
if (req) try {
|
|
244
|
+
const tracker = req("promise/setimmediate/rejection-tracking");
|
|
245
|
+
if (tracker && typeof tracker.enable === "function") return (options) => tracker.enable(options);
|
|
246
|
+
} catch {}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Best-effort capture of unhandled promise rejections, engine-aware. RN routes
|
|
251
|
+
* these through its engine's tracker (NOT `window.onunhandledrejection`):
|
|
252
|
+
* `HermesInternal.enablePromiseRejectionTracker` on Hermes, the `promise`
|
|
253
|
+
* polyfill on JSC. Reasons are routed exactly like the browser
|
|
254
|
+
* `unhandledrejection` handler (core's `routeRejection`), so Error reasons keep
|
|
255
|
+
* their stack via `reportSilently`.
|
|
256
|
+
*
|
|
257
|
+
* Engine-dependent, NOT version-dependent: if no engine hook is reachable this
|
|
258
|
+
* is a no-op (uncaught throws via ErrorUtils still work) and emits a dev-only
|
|
259
|
+
* debug line; it must never crash. The `enable(...)` invocation is wrapped so a
|
|
260
|
+
* throwing engine hook degrades to no-op instead of propagating.
|
|
261
|
+
*
|
|
262
|
+
* Chaining caveat: enabling REPLACES the engine's current callbacks (RN
|
|
263
|
+
* registers its own dev warning) and neither engine exposes a getter for the
|
|
264
|
+
* previous ones, so we cannot truly chain RN's default. To avoid swallowing that
|
|
265
|
+
* developer signal, `onUnhandled` re-emits a `console.warn` in dev (`__DEV__`).
|
|
266
|
+
*
|
|
267
|
+
* Returns an uninstaller that re-enables with no-op callbacks (no clean disable
|
|
268
|
+
* exists on either engine).
|
|
269
|
+
*/
|
|
270
|
+
function installRejectionTracking(reporter, deps = {}) {
|
|
271
|
+
const enable = deps.enable !== void 0 ? deps.enable : resolveRejectionEnabler();
|
|
272
|
+
if (!enable) {
|
|
273
|
+
if (inDevMode()) console.debug("[flare] No promise-rejection hook for this JS engine; rejections not captured.");
|
|
274
|
+
return () => {};
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
enable({
|
|
278
|
+
allRejections: true,
|
|
279
|
+
onUnhandled: (_id, error) => {
|
|
280
|
+
if (inDevMode()) console.warn("[flare] Unhandled promise rejection:", error);
|
|
281
|
+
routeRejection(reporter, error);
|
|
282
|
+
},
|
|
283
|
+
onHandled: () => {}
|
|
284
|
+
});
|
|
285
|
+
} catch {
|
|
286
|
+
if (inDevMode()) console.debug("[flare] Promise-rejection hook threw on enable; rejections not captured.");
|
|
287
|
+
return () => {};
|
|
288
|
+
}
|
|
289
|
+
return () => {
|
|
290
|
+
try {
|
|
291
|
+
enable({
|
|
292
|
+
allRejections: true,
|
|
293
|
+
onUnhandled: () => {},
|
|
294
|
+
onHandled: () => {}
|
|
295
|
+
});
|
|
296
|
+
} catch {}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/ReactNativeFlushScheduler.ts
|
|
302
|
+
/**
|
|
303
|
+
* Passive flush scheduler for React Native. Core's `Logger` calls `register`
|
|
304
|
+
* once during construction; this stores the flush callback and exposes a plain,
|
|
305
|
+
* argument-less caller via `getFlush()`. The actual trigger (AppState ->
|
|
306
|
+
* background) is wired separately in `Flare.install()` so it stays symmetric
|
|
307
|
+
* with handler teardown.
|
|
308
|
+
*
|
|
309
|
+
* Deliberately calls `flush()` WITHOUT `{ keepalive: true }`: RN's fetch (over
|
|
310
|
+
* XMLHttpRequest) does not reliably honor keepalive, so a backgrounding flush is
|
|
311
|
+
* best-effort and may be dropped if the OS suspends the app mid-request.
|
|
312
|
+
*/
|
|
313
|
+
var ReactNativeFlushScheduler = class {
|
|
314
|
+
flushFn = null;
|
|
315
|
+
register(flush) {
|
|
316
|
+
this.flushFn = flush;
|
|
317
|
+
}
|
|
318
|
+
getFlush() {
|
|
319
|
+
const flush = this.flushFn;
|
|
320
|
+
if (!flush) return void 0;
|
|
321
|
+
return () => {
|
|
322
|
+
flush();
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/Flare.ts
|
|
329
|
+
const RN_SDK_NAME = "@flareapp/react-native";
|
|
330
|
+
const RN_SDK_VERSION = "2.6.0";
|
|
331
|
+
const RN_FRAMEWORK_NAME = "React Native";
|
|
332
|
+
const FATAL_FLUSH_TIMEOUT_MS = 2e3;
|
|
333
|
+
/**
|
|
334
|
+
* React Native `Flare` singleton (exposed as `flare` from the package root).
|
|
335
|
+
*
|
|
336
|
+
* Subclasses core's `Flare`, injecting the RN seams:
|
|
337
|
+
* - core `Api` (fetch is native in RN),
|
|
338
|
+
* - `makeReactNativeContextCollector(() => this.user)` for device/app/user attrs,
|
|
339
|
+
* - core `NullFileReader` (no runtime source snippets; sourcemaps are a Metro
|
|
340
|
+
* follow-up),
|
|
341
|
+
* - core `GlobalScopeProvider` (RN is a single app scope),
|
|
342
|
+
* - `ReactNativeFlushScheduler` (flush on background, best-effort).
|
|
343
|
+
*
|
|
344
|
+
* Adds RN-only surface: `removeHandlers` and an idempotent handler install folded
|
|
345
|
+
* into `light()`. `setUser` is inherited from core, which writes the backend-read
|
|
346
|
+
* `user.*` identity keys to the active scope (RN uses the single global scope).
|
|
347
|
+
*/
|
|
348
|
+
var ReactNativeFlare = class extends Flare$1 {
|
|
349
|
+
scheduler;
|
|
350
|
+
rejectionDeps;
|
|
351
|
+
installed = false;
|
|
352
|
+
uninstallers = [];
|
|
353
|
+
/**
|
|
354
|
+
* @param rejectionDeps test seam for the rejection hook. Defaults to `{}`
|
|
355
|
+
* (resolve the active engine's tracker — Hermes or JSC). Tests pass
|
|
356
|
+
* `{ enable: null }` so `light()` does NOT enable a global rejection
|
|
357
|
+
* tracker as a leaking side effect.
|
|
358
|
+
*/
|
|
359
|
+
constructor(rejectionDeps = {}) {
|
|
360
|
+
const scheduler = new ReactNativeFlushScheduler();
|
|
361
|
+
const collector = makeReactNativeContextCollector();
|
|
362
|
+
super(new Api(), collector, new NullFileReader$1(), new GlobalScopeProvider$1(), scheduler);
|
|
363
|
+
this.scheduler = scheduler;
|
|
364
|
+
this.rejectionDeps = rejectionDeps;
|
|
365
|
+
this.setSdkInfo({
|
|
366
|
+
name: RN_SDK_NAME,
|
|
367
|
+
version: RN_SDK_VERSION
|
|
368
|
+
});
|
|
369
|
+
this.setFramework({ name: RN_FRAMEWORK_NAME });
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Force the framework identity to "React Native". The wrapped
|
|
373
|
+
* `@flareapp/react` boundary tags every flare it injects as `React` (via
|
|
374
|
+
* `tagReactFramework`), which is wrong on the RN singleton — so coerce the
|
|
375
|
+
* name here while preserving whatever version the caller supplied (the React
|
|
376
|
+
* renderer version when the boundary tags it).
|
|
377
|
+
*/
|
|
378
|
+
setFramework(framework) {
|
|
379
|
+
return super.setFramework({
|
|
380
|
+
...framework,
|
|
381
|
+
name: RN_FRAMEWORK_NAME
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Set the API key (and optional debug flag), then install the global
|
|
386
|
+
* handlers. The install is idempotent — calling `light()` twice does NOT
|
|
387
|
+
* double-wrap `ErrorUtils` (which, unlike node's reconcile, is not naturally
|
|
388
|
+
* idempotent).
|
|
389
|
+
*/
|
|
390
|
+
light(key, debug) {
|
|
391
|
+
super.light(key, debug);
|
|
392
|
+
this.install();
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Detach the global error handler, rejection tracker, and AppState listener,
|
|
397
|
+
* and clear the install guard so a later `light()` re-installs. For tests
|
|
398
|
+
* and manual teardown (mirrors node's `removeProcessListeners`).
|
|
399
|
+
*/
|
|
400
|
+
removeHandlers() {
|
|
401
|
+
for (const uninstall of this.uninstallers) try {
|
|
402
|
+
uninstall();
|
|
403
|
+
} catch {}
|
|
404
|
+
this.uninstallers = [];
|
|
405
|
+
this.installed = false;
|
|
406
|
+
}
|
|
407
|
+
install() {
|
|
408
|
+
if (this.installed) return;
|
|
409
|
+
this.installed = true;
|
|
410
|
+
this.uninstallers.push(installGlobalErrorHandler((error, isFatal) => {
|
|
411
|
+
this.reportSilently(error, { "error.fatal": isFatal });
|
|
412
|
+
}, () => this.flush(FATAL_FLUSH_TIMEOUT_MS)), installRejectionTracking({
|
|
413
|
+
reportSilently: (error) => this.reportSilently(error),
|
|
414
|
+
reportUnhandledRejection: (message) => this.reportUnhandledRejection(message)
|
|
415
|
+
}, this.rejectionDeps), installAppStateFlush(() => this.scheduler.getFlush()));
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/singleton.ts
|
|
421
|
+
const flare = new ReactNativeFlare();
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/FlareErrorBoundary.ts
|
|
425
|
+
/**
|
|
426
|
+
* React Native error boundary. A thin wrapper over `@flareapp/react`'s
|
|
427
|
+
* `/inject` boundary that injects the RN `flare` singleton.
|
|
428
|
+
*
|
|
429
|
+
* `flare` is applied AFTER `{...props}` so a consumer cannot override the
|
|
430
|
+
* singleton. The `as unknown as Flare` cast is required: the boundary prop is
|
|
431
|
+
* typed against `@flareapp/js/browser`'s `Flare` (a superset of
|
|
432
|
+
* `ReactNativeFlare`), so structural assignment does not hold. Safe at runtime —
|
|
433
|
+
* the boundary only calls a core-level method (`reportSilently`), which
|
|
434
|
+
* `ReactNativeFlare` inherits.
|
|
435
|
+
*/
|
|
436
|
+
function FlareErrorBoundary(props) {
|
|
437
|
+
return createElement(FlareErrorBoundary$1, {
|
|
438
|
+
...props,
|
|
439
|
+
flare
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
//#endregion
|
|
444
|
+
export { DEFAULT_URL_DENYLIST, Flare, FlareErrorBoundary, GlobalScopeProvider, NullFileReader, ReactNativeFlare, convertToError, flare, redactUrlQuery, resolveDenylist };
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flareapp/react-native",
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "React Native SDK for flareapp.io",
|
|
5
|
+
"homepage": "https://flareapp.io",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/spatie/flare-client-js/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/spatie/flare-client-js.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Spatie",
|
|
16
|
+
"email": "info@spatie.be"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.mjs",
|
|
23
|
+
"types": "./dist/index.d.cts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"react-native": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
},
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"prepublishOnly": "npm run build",
|
|
43
|
+
"build": "tsdown src/index.ts --format cjs,esm --dts --env.FLARE_JS_CLIENT_VERSION=$(node -p \"require('./package.json').version\") --clean",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typescript": "tsc --noEmit",
|
|
46
|
+
"release": "release-it"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@flareapp/core": "2.6.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@flareapp/react": "^2.5.0",
|
|
53
|
+
"react": "^18.0.0||^19.0.0",
|
|
54
|
+
"react-native": ">=0.79.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@flareapp/js": "file:../js",
|
|
58
|
+
"@flareapp/react": "file:../react",
|
|
59
|
+
"@types/react": "^19.0.0",
|
|
60
|
+
"react": "^19.0.0",
|
|
61
|
+
"react-native": "^0.79.0",
|
|
62
|
+
"tsdown": "^0.20.3",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vitest": "^4.0.18"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
}
|
|
69
|
+
}
|