@lark.js/sentry 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,770 @@
1
+ # @lark.js/sentry
2
+
3
+ `@lark.js/sentry` is a browser monitoring and analytics SDK for page views, declarative clicks, runtime errors, resource errors, HTTP requests, performance metrics, exposure tracking, white-screen detection, offline reporting, and screen record context.
4
+
5
+ The core entry is framework agnostic. React and Vue integrations are published as dedicated subpath exports so non-framework users do not load framework dependencies.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @lark.js/sentry
11
+ ```
12
+
13
+ React and Vue are optional peer dependencies. Install them only when the matching integration is used.
14
+
15
+ ```bash
16
+ npm install react
17
+ ```
18
+
19
+ ```bash
20
+ npm install vue
21
+ ```
22
+
23
+ ## Package Exports
24
+
25
+ ```ts
26
+ import { init, destroy, isInitialized, pluginEnable } from "@lark.js/sentry";
27
+ import { PerformancePlugin, ScreenRecordPlugin, ExposurePlugin } from "@lark.js/sentry/plugins";
28
+ import { ReactErrorBoundary } from "@lark.js/sentry/react";
29
+ import { vuePlugin } from "@lark.js/sentry/vue";
30
+ ```
31
+
32
+ Each public export provides ESM, CJS, and TypeScript declaration files.
33
+
34
+ ## Quick Start
35
+
36
+ ```ts
37
+ import { init, pluginEnable } from "@lark.js/sentry";
38
+ import { PerformancePlugin, ScreenRecordPlugin, ExposurePlugin } from "@lark.js/sentry/plugins";
39
+
40
+ init({
41
+ dsn: "/api/log",
42
+ projectId: "frontend-app",
43
+ userId: "anonymous",
44
+ });
45
+
46
+ pluginEnable(PerformancePlugin);
47
+ pluginEnable(ScreenRecordPlugin);
48
+ pluginEnable(ExposurePlugin);
49
+ ```
50
+
51
+ `dsn` must be a non-empty string. If `dsn` is empty, initialization is rejected.
52
+
53
+ ## Lifecycle APIs
54
+
55
+ ### init
56
+
57
+ `init(options)` validates the input with zod, merges it with default options, writes runtime configuration, installs capture listeners, starts page-view lifecycle tracking, and initializes visitor identity when enabled.
58
+
59
+ ```ts
60
+ import { init } from "@lark.js/sentry";
61
+
62
+ init({
63
+ dsn: "/api/log",
64
+ projectId: "checkout-web",
65
+ userId: "user-001",
66
+ enableFetch: true,
67
+ enableXhr: true,
68
+ enableClick: true,
69
+ });
70
+ ```
71
+
72
+ ### destroy
73
+
74
+ `destroy()` cleans plugin instances, event subscriptions, browser listeners, and decorated global methods.
75
+
76
+ ```ts
77
+ import { destroy } from "@lark.js/sentry";
78
+
79
+ destroy();
80
+ ```
81
+
82
+ Use it when resetting tests, unloading a micro-frontend, or dynamically disabling monitoring.
83
+
84
+ ### isInitialized
85
+
86
+ ```ts
87
+ import { init, isInitialized } from "@lark.js/sentry";
88
+
89
+ if (!isInitialized()) {
90
+ init({ dsn: "/api/log" });
91
+ }
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ `init` accepts partial options. Values not provided by the caller use SDK defaults.
97
+
98
+ | Option | Type | Default | Description |
99
+ | ---------------------------- | ---------------------- | --------------------------------------------------- | ------------------------------------------------------ |
100
+ | `dsn` | `string` | `""` | Report endpoint. Required for initialization. |
101
+ | `projectId` | `string` | `"unknown"` | Frontend project identifier. |
102
+ | `userId` | `string` | `"unknown"` | Current user identifier. |
103
+ | `disabled` | `boolean` | `false` | Disable the SDK. |
104
+ | `enableXhr` | `boolean` | `true` | Capture XMLHttpRequest requests. |
105
+ | `enableFetch` | `boolean` | `true` | Capture fetch requests. |
106
+ | `enableClick` | `boolean` | `true` | Capture declarative click events. |
107
+ | `enableError` | `boolean` | `true` | Capture runtime and resource errors. |
108
+ | `enableUnhandledRejection` | `boolean` | `true` | Capture unhandled promise rejections. |
109
+ | `enableHashChange` | `boolean` | `true` | Capture hash navigation. |
110
+ | `enableHistory` | `boolean` | `true` | Capture history navigation. |
111
+ | `enablePerformance` | `boolean` | `true` | Enable performance-related capture. |
112
+ | `enableScreenRecord` | `boolean` | `true` | Enable screen-record-related reporting. |
113
+ | `enableWhiteScreen` | `boolean` | `true` | Enable white-screen detection. |
114
+ | `enableFingerprint` | `boolean` | `false` | Enable FingerprintJS anonymous visitor identity. |
115
+ | `anonymousId` | `string` | `"unknown"` | SDK-generated anonymous visitor id. |
116
+ | `visitorId` | `string` | `"unknown"` | Backend-bound visitor id. |
117
+ | `useImageReport` | `boolean` | `false` | Allow image transport. |
118
+ | `screenRecordDurationMs` | `number` | `3000` | Rolling screen record window length. |
119
+ | `screenRecordEventTypes` | `EventType[]` | `[Error, Xhr, Fetch, Resource, UnhandledRejection]` | Event types that trigger screen record reporting. |
120
+ | `hasSkeleton` | `boolean` | `false` | Whether the page has a skeleton screen. |
121
+ | `rootCssSelectors` | `string[]` | `["html", "body", "#app", "#root"]` | Root selectors used by white-screen detection. |
122
+ | `clickThrottleDelay` | `number` | `0` | Click capture throttle delay in milliseconds. |
123
+ | `requestTimeoutMilliseconds` | `number` | `3000` | Request timeout in milliseconds. |
124
+ | `maxBreadcrumbs` | `number` | `30` | Breadcrumb capacity. |
125
+ | `repeatCodeError` | `boolean` | `false` | Report duplicate code errors. |
126
+ | `enableHttpPerformance` | `boolean` | `false` | Report successful HTTP requests as performance events. |
127
+ | `ignoreErrors` | `(string \| RegExp)[]` | `[]` | Runtime error ignore rules. |
128
+ | `excludeApis` | `(string \| RegExp)[]` | `[]` | HTTP request ignore rules. |
129
+ | `cacheMaxLength` | `number` | `10` | Maximum batch size. |
130
+ | `cacheWaitingTime` | `number` | `2000` | Batch wait time in milliseconds. |
131
+ | `maxQueueLength` | `number` | `200` | Maximum queued events while offline or retrying. |
132
+ | `retryIntervalMilliseconds` | `number` | `60000` | Server recovery probe interval. |
133
+ | `offlineCacheKey` | `string` | `"lark_sentry_offline_cache"` | localStorage key for offline cache. |
134
+ | `tracesSampleRate` | `number` | `1` | Sampling rate from 0 to 1. |
135
+ | `onBeforePushBreadcrumb` | `function` | `undefined` | Hook before storing a breadcrumb. |
136
+ | `onBeforeReportData` | `function` | `undefined` | Hook before one event enters Reporter queue. |
137
+ | `beforePushEventList` | `function` | `undefined` | Hook before a batch enters transport. |
138
+ | `afterSendData` | `function` | `undefined` | Hook after a batch enters transport successfully. |
139
+ | `handleHttpError` | `function` | `undefined` | Custom HTTP error callback. |
140
+
141
+ Example production configuration:
142
+
143
+ ```ts
144
+ import { init } from "@lark.js/sentry";
145
+
146
+ init({
147
+ dsn: "https://example.com/api/log",
148
+ projectId: "production-web",
149
+ userId: "unknown",
150
+ enableFingerprint: true,
151
+ enableHttpPerformance: true,
152
+ tracesSampleRate: 1,
153
+ excludeApis: ["https://example.com/api/log"],
154
+ ignoreErrors: [/ResizeObserver loop limit exceeded/],
155
+ });
156
+ ```
157
+
158
+ ## Report Data
159
+
160
+ Reporter sends `IReportData` objects to the configured `dsn`.
161
+
162
+ | Field | Description |
163
+ | ------------ | ------------------------------------------ |
164
+ | `id` | Reporter instance id. |
165
+ | `type` | Event type. |
166
+ | `name` | Event name. |
167
+ | `message` | Event message. |
168
+ | `status` | `OK` or `Error`. |
169
+ | `time` | Formatted time. |
170
+ | `timestamp` | Numeric timestamp. |
171
+ | `url` | Current page URL. |
172
+ | `userId` | User identifier. |
173
+ | `projectId` | Project identifier. |
174
+ | `sdkVersion` | SDK version. |
175
+ | `deviceInfo` | Device, browser, OS, and fingerprint data. |
176
+ | `payload` | Original event payload. |
177
+
178
+ ## Event Types
179
+
180
+ The SDK can report the following event categories:
181
+
182
+ | Type | Description |
183
+ | -------------------------- | ------------------------------- |
184
+ | `XMLHttpRequest` | XHR request. |
185
+ | `fetch` | fetch request. |
186
+ | `Click` | Declarative click. |
187
+ | `Event hashchange` | Hash navigation. |
188
+ | `History` | History navigation. |
189
+ | `Resource` | Static resource load failure. |
190
+ | `Event unhandledrejection` | Unhandled promise rejection. |
191
+ | `Error` | JavaScript runtime error. |
192
+ | `Vue` | Vue error. |
193
+ | `React` | React error. |
194
+ | `Performance` | Performance metric. |
195
+ | `ScreenRecord` | Screen record payload. |
196
+ | `Exposure` | Exposure duration event. |
197
+ | `WhiteScreen` | White-screen event. |
198
+ | `Custom` | Custom business event. |
199
+ | `PV` | Page view and dwell-time event. |
200
+
201
+ ## Error Capture
202
+
203
+ The SDK captures:
204
+
205
+ - `window` `error` events.
206
+ - Static resource load errors.
207
+ - `console.error` error objects or error text.
208
+ - Unhandled promise rejections.
209
+ - React ErrorBoundary errors.
210
+ - Vue `app.config.errorHandler` errors.
211
+
212
+ Duplicate code errors are deduplicated by default. Enable duplicate reporting with:
213
+
214
+ ```ts
215
+ init({
216
+ dsn: "/api/log",
217
+ repeatCodeError: true,
218
+ });
219
+ ```
220
+
221
+ Ignore known noise:
222
+
223
+ ```ts
224
+ init({
225
+ dsn: "/api/log",
226
+ ignoreErrors: ["Script error.", /ResizeObserver loop limit exceeded/],
227
+ });
228
+ ```
229
+
230
+ ## HTTP Capture
231
+
232
+ The SDK decorates `XMLHttpRequest.prototype.open`, `XMLHttpRequest.prototype.send`, and `globalThis.fetch`. It captures method, URL, status code, elapsed time, request data, response data, and `Server-Timing`.
233
+
234
+ Status classification:
235
+
236
+ | Status code | SDK status |
237
+ | ------------ | ---------- |
238
+ | 100 to 399 | `OK` |
239
+ | 400 to 599 | `Error` |
240
+ | Other values | `Error` |
241
+
242
+ Exclude report endpoints or health checks:
243
+
244
+ ```ts
245
+ init({
246
+ dsn: "/api/log",
247
+ excludeApis: ["/api/log", /\/health$/],
248
+ });
249
+ ```
250
+
251
+ Report successful HTTP requests as performance events:
252
+
253
+ ```ts
254
+ init({
255
+ dsn: "/api/log",
256
+ enableHttpPerformance: true,
257
+ });
258
+ ```
259
+
260
+ ## Page Views and Dwell Time
261
+
262
+ The SDK reports an initial `PageLoad` PV event during initialization.
263
+
264
+ On hash or history route changes, it:
265
+
266
+ - Deduplicates unchanged URLs.
267
+ - Reports `PageDwell` for the previous page.
268
+ - Reports a new PV for the next page.
269
+ - Flushes current dwell time on `beforeunload` when possible.
270
+
271
+ Dwell time less than or equal to 100 ms is ignored to reduce noise.
272
+
273
+ Manual page-view reporting:
274
+
275
+ ```ts
276
+ import { tracePageView } from "@lark.js/sentry";
277
+
278
+ tracePageView({
279
+ name: "ProductDetail",
280
+ message: location.href,
281
+ extra: {
282
+ productId: "sku-001",
283
+ },
284
+ });
285
+ ```
286
+
287
+ ## Declarative Clicks
288
+
289
+ Declarative click tracking uses `s-lark-*` attributes. Plain clicks are not reported unless the clicked element or one of its composed path ancestors has a tracking attribute.
290
+
291
+ ```html
292
+ <section s-lark-view="profile-card" s-lark-src="home">
293
+ <button s-lark-ev="save-profile" s-lark-msg="Save">Save</button>
294
+ </section>
295
+ ```
296
+
297
+ Reserved attributes:
298
+
299
+ | Attribute | Description |
300
+ | ------------- | ------------------------------ |
301
+ | `s-lark-ev` | Explicit event ID. |
302
+ | `s-lark-msg` | Human-readable message. |
303
+ | `s-lark-view` | View ID and event ID fallback. |
304
+
305
+ Custom `s-lark-*` attributes become `params`.
306
+
307
+ ```html
308
+ <a
309
+ s-lark-ev="open-banner"
310
+ s-lark-msg="Open campaign banner"
311
+ s-lark-campaign="spring"
312
+ s-lark-rank="1"
313
+ >
314
+ Campaign
315
+ </a>
316
+ ```
317
+
318
+ The reported click payload (`DeclarativeClickData`) includes:
319
+
320
+ | Field | Type | Description |
321
+ | ---------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
322
+ | `ev` | `string` | Event ID (from `s-lark-ev`, `title`, `s-lark-view`, or tag). |
323
+ | `msg` | `string` | Human-readable message (from `s-lark-msg`, text, `aria-label`, or tag). |
324
+ | `triggerPageUrl` | `string` | Current page URL (`location.href`). |
325
+ | `x` | `number` | Click X coordinate (element offset + scroll offset). |
326
+ | `y` | `number` | Click Y coordinate (element offset + scroll offset). |
327
+ | `params` | `Readonly<Record<string, string \| null>>` | Custom `s-lark-*` attributes (excluding reserved keys). |
328
+ | `elementPath` | `string` | XPath-like path from element to body (max 128 characters). |
329
+ | `triggerTime` | `number` | `Date.now()` at click time. |
330
+
331
+ ## White-Screen Detection
332
+
333
+ White-screen detection samples viewport points after the page is ready and checks whether those points still resolve to configured root elements.
334
+
335
+ Default root selectors:
336
+
337
+ ```ts
338
+ ["html", "body", "#app", "#root"];
339
+ ```
340
+
341
+ ```ts
342
+ init({
343
+ dsn: "/api/log",
344
+ enableWhiteScreen: true,
345
+ rootCssSelectors: ["html", "body", "#app"],
346
+ });
347
+ ```
348
+
349
+ Skeleton-screen pages can enable skeleton mode:
350
+
351
+ ```ts
352
+ init({
353
+ dsn: "/api/log",
354
+ enableWhiteScreen: true,
355
+ hasSkeleton: true,
356
+ });
357
+ ```
358
+
359
+ ## Visitor Identity
360
+
361
+ The SDK tracks three identity values:
362
+
363
+ | Field | Description |
364
+ | ------------- | ------------------------------------------------------------------- |
365
+ | `anonymousId` | Anonymous visitor id generated by FingerprintJS and stored locally. |
366
+ | `visitorId` | Backend-bound visitor id. |
367
+ | `userId` | Current logged-in user id. |
368
+
369
+ Enable FingerprintJS:
370
+
371
+ ```ts
372
+ import { getIdentity, init } from "@lark.js/sentry";
373
+
374
+ init({
375
+ dsn: "/api/log",
376
+ enableFingerprint: true,
377
+ });
378
+
379
+ console.log(getIdentity());
380
+ ```
381
+
382
+ Update user and visitor ids:
383
+
384
+ ```ts
385
+ import { setUserId, setVisitorId } from "@lark.js/sentry";
386
+
387
+ setUserId("user-001");
388
+ setVisitorId("visitor-001");
389
+ ```
390
+
391
+ Read identity:
392
+
393
+ ```ts
394
+ import { getIdentity } from "@lark.js/sentry";
395
+
396
+ const identity = getIdentity();
397
+ ```
398
+
399
+ `anonymousId` is stored in localStorage with the key `lark_sentry_anonymous_id`.
400
+
401
+ ## Reporter
402
+
403
+ Reporter is the unified data outlet. It transforms captured payloads into report data and sends batches to `dsn`.
404
+
405
+ Reporter behavior:
406
+
407
+ - Batches events.
408
+ - Applies sampling through `tracesSampleRate`.
409
+ - Persists offline events to localStorage.
410
+ - Flushes cached events after network recovery.
411
+ - Probes server recovery with HEAD requests after failed fetch reports.
412
+ - Avoids concurrent flush races with an `isFlushing` guard.
413
+ - Supports `sendBeacon`, image, and fetch transports.
414
+
415
+ Transport priority:
416
+
417
+ 1. Use `navigator.sendBeacon` for batches up to 60 KB.
418
+ 2. Use image transport when `useImageReport` is true and the batch is up to 2 KB.
419
+ 3. Use fetch POST as the fallback.
420
+
421
+ Flush offline cache manually:
422
+
423
+ ```ts
424
+ import { sendLocal } from "@lark.js/sentry";
425
+
426
+ await sendLocal();
427
+ ```
428
+
429
+ ## Reporter Hooks
430
+
431
+ Register hooks after initialization or provide equivalent hooks in `init` options.
432
+
433
+ ```ts
434
+ import { afterSendData, beforePushEventList, beforeSendData } from "@lark.js/sentry";
435
+
436
+ beforeSendData((data) => {
437
+ if (data.type === "Click") {
438
+ return false;
439
+ }
440
+ return data;
441
+ });
442
+
443
+ beforePushEventList((eventList) => {
444
+ return eventList.filter((item) => item.status !== "OK");
445
+ });
446
+
447
+ afterSendData((eventList) => {
448
+ console.log("reported", eventList.length);
449
+ });
450
+ ```
451
+
452
+ Equivalent initialization form:
453
+
454
+ ```ts
455
+ init({
456
+ dsn: "/api/log",
457
+ onBeforeReportData(data) {
458
+ return data;
459
+ },
460
+ beforePushEventList(eventList) {
461
+ return eventList;
462
+ },
463
+ afterSendData(eventList) {
464
+ console.log(eventList.length);
465
+ },
466
+ });
467
+ ```
468
+
469
+ ## Manual APIs
470
+
471
+ ### traceError
472
+
473
+ ```ts
474
+ import { traceError } from "@lark.js/sentry";
475
+
476
+ try {
477
+ throw new Error("Unexpected state");
478
+ } catch (error) {
479
+ traceError(error);
480
+ }
481
+ ```
482
+
483
+ ### tracePerformance
484
+
485
+ ```ts
486
+ import { tracePerformance } from "@lark.js/sentry";
487
+
488
+ tracePerformance({
489
+ name: "SearchLatency",
490
+ message: "/api/search",
491
+ value: 128,
492
+ });
493
+ ```
494
+
495
+ ### traceCustomEvent
496
+
497
+ ```ts
498
+ import { traceCustomEvent } from "@lark.js/sentry";
499
+
500
+ traceCustomEvent({
501
+ name: "CheckoutSuccess",
502
+ message: "Submit order",
503
+ extra: {
504
+ orderId: "order-001",
505
+ },
506
+ });
507
+ ```
508
+
509
+ ### tracePageView
510
+
511
+ ```ts
512
+ import { tracePageView } from "@lark.js/sentry";
513
+
514
+ tracePageView({
515
+ name: "ManualPageView",
516
+ message: location.href,
517
+ });
518
+ ```
519
+
520
+ ### getBaseInfo and getUserId
521
+
522
+ ```ts
523
+ import { getBaseInfo, getUserId } from "@lark.js/sentry";
524
+
525
+ const baseInfo = getBaseInfo();
526
+ const userId = getUserId();
527
+ ```
528
+
529
+ ### getIPs
530
+
531
+ `getIPs` attempts to collect WebRTC ICE candidate IP values in browsers that support the required APIs. It returns an empty array when unsupported.
532
+
533
+ ```ts
534
+ import { getIPs } from "@lark.js/sentry";
535
+
536
+ const ips = await getIPs();
537
+ ```
538
+
539
+ ## Plugin System
540
+
541
+ Plugins extend the SDK without coupling optional capabilities to the core entry. A plugin class extends `SentryPlugin`, implements `init`, and can implement `destroy` for cleanup.
542
+
543
+ ```ts
544
+ import { pluginEnable } from "@lark.js/sentry";
545
+ import { PerformancePlugin } from "@lark.js/sentry/plugins";
546
+
547
+ const plugin = pluginEnable(PerformancePlugin);
548
+ ```
549
+
550
+ Enabled plugins are stored in the plugin registry. `destroy()` calls each plugin's `destroy()` method when available.
551
+
552
+ ## PerformancePlugin
553
+
554
+ ```ts
555
+ import { pluginEnable } from "@lark.js/sentry";
556
+ import { PerformancePlugin } from "@lark.js/sentry/plugins";
557
+
558
+ pluginEnable(PerformancePlugin);
559
+ ```
560
+
561
+ The plugin collects:
562
+
563
+ - Web Vitals.
564
+ - Navigation Timing page-load metrics.
565
+ - Resource Timing metrics.
566
+ - Long Task entries.
567
+ - Fallback resource-element timing for dynamically inserted resources.
568
+ - `performance.measureUserAgentSpecificMemory` when supported.
569
+
570
+ Unsupported browser capabilities are skipped safely.
571
+
572
+ ## ScreenRecordPlugin
573
+
574
+ ```ts
575
+ import { pluginEnable } from "@lark.js/sentry";
576
+ import { ScreenRecordPlugin, unzipScreenRecord } from "@lark.js/sentry/plugins";
577
+
578
+ pluginEnable(ScreenRecordPlugin);
579
+
580
+ pluginEnable(ScreenRecordPlugin, {
581
+ durationMs: 5000,
582
+ });
583
+ ```
584
+
585
+ Screen recording is based on rrweb. The plugin keeps a rolling record window. When selected error or network events occur, the recent record window is reported as a `ScreenRecord` event.
586
+
587
+ Decode a record payload:
588
+
589
+ ```ts
590
+ const events = unzipScreenRecord(recordPayload);
591
+ ```
592
+
593
+ ## ExposurePlugin
594
+
595
+ ```ts
596
+ import { pluginEnable } from "@lark.js/sentry";
597
+ import { ExposurePlugin } from "@lark.js/sentry/plugins";
598
+
599
+ const exposure = pluginEnable(ExposurePlugin);
600
+ ```
601
+
602
+ Observe one element:
603
+
604
+ ```ts
605
+ const element = document.querySelector("#banner");
606
+
607
+ if (element) {
608
+ exposure.observe({
609
+ target: element,
610
+ threshold: 0.5,
611
+ params: {
612
+ bannerId: "spring-001",
613
+ },
614
+ });
615
+ }
616
+ ```
617
+
618
+ Observe multiple elements:
619
+
620
+ ```ts
621
+ const first = document.querySelector("#first");
622
+ const second = document.querySelector("#second");
623
+
624
+ if (first && second) {
625
+ exposure.observe([
626
+ {
627
+ target: first,
628
+ threshold: 0.5,
629
+ params: { position: "first" },
630
+ },
631
+ {
632
+ target: second,
633
+ threshold: 0.75,
634
+ params: { position: "second" },
635
+ },
636
+ ]);
637
+ }
638
+ ```
639
+
640
+ Cancel observation:
641
+
642
+ ```ts
643
+ exposure.unobserve(element);
644
+ exposure.unobserve([first, second]);
645
+ ```
646
+
647
+ Exposure events are reported when an observed element leaves the viewport after becoming visible. The payload contains threshold, observe time, show time, show end time, duration, and user params.
648
+
649
+ ## React Integration
650
+
651
+ ```tsx
652
+ import { init } from "@lark.js/sentry";
653
+ import { ReactErrorBoundary } from "@lark.js/sentry/react";
654
+
655
+ init({ dsn: "/api/log" });
656
+
657
+ export function App() {
658
+ return (
659
+ <ReactErrorBoundary fallback={<div>Something went wrong</div>}>
660
+ <Page />
661
+ </ReactErrorBoundary>
662
+ );
663
+ }
664
+ ```
665
+
666
+ `fallback` can also be a function:
667
+
668
+ ```tsx
669
+ <ReactErrorBoundary
670
+ fallback={(error, errorInfo) => (
671
+ <div>
672
+ {error.message}
673
+ {errorInfo.componentStack}
674
+ </div>
675
+ )}
676
+ >
677
+ <Page />
678
+ </ReactErrorBoundary>
679
+ ```
680
+
681
+ The boundary reports `React` events with the error, stack, and React `ErrorInfo`.
682
+
683
+ ## Vue 3 Integration
684
+
685
+ ```ts
686
+ import { createApp } from "vue";
687
+ import { vuePlugin } from "@lark.js/sentry/vue";
688
+ import App from "./app.vue";
689
+
690
+ const app = createApp(App);
691
+
692
+ app.use(vuePlugin, {
693
+ dsn: "/api/log",
694
+ projectId: "vue-app",
695
+ });
696
+
697
+ app.mount("#app");
698
+ ```
699
+
700
+ The plugin installs `app.config.errorHandler`, reports `Vue` events, and then calls the previous error handler if one existed.
701
+
702
+ ## Vite Dev-Server Plugin
703
+
704
+ The SDK provides a Vite plugin that creates a mock report endpoint during development, writing reported data to log files instead of sending it to a real server.
705
+
706
+ ```ts
707
+ // vite.config.ts
708
+ import { defineConfig } from "vite";
709
+ import { sentryPlugin } from "@lark.js/sentry/vite";
710
+
711
+ export default defineConfig({
712
+ // `url` should equals to @lark.js/sentry `init({ dsn: "/api/log" })` config `dsn`
713
+ plugins: [sentryPlugin({ url: "/api/log" })],
714
+ });
715
+ ```
716
+
717
+ ### Available Exports
718
+
719
+ | Export | Vite Version | Description |
720
+ | --------------- | ------------ | --------------------------------------- |
721
+ | `sentryPlugin` | Vite 6/8 | Default export. For current Vite. |
722
+ | `sentryPlugin7` | Vite 7 | For projects using Vite 7 specifically. |
723
+
724
+ ### Options
725
+
726
+ | Option | Type | Default | Description |
727
+ | ------ | -------- | ----------- | ---------------------------------------- |
728
+ | `url` | `string` | `"/sentry"` | The URL path to intercept POST requests. |
729
+
730
+ The plugin creates a `logs/` directory in `process.cwd()`, writes a timestamped log file (`sentry_YYYYMMDDHHMMSS.log`), parses each request body as JSON, and returns `{ code: 0, message: "success" }`.
731
+
732
+ ## Browser Compatibility
733
+
734
+ - `sendBeacon` is preferred for small batches.
735
+ - Image and fetch transports are used as fallbacks.
736
+ - `PerformanceObserver` powers Web Vitals, long task, and resource timing when available.
737
+ - `MutationObserver` is used as a fallback for dynamically inserted resources.
738
+ - `IntersectionObserver` is required by `ExposurePlugin`.
739
+ - `performance.measureUserAgentSpecificMemory` is optional.
740
+ - `@rrweb/record` is used only by the screen record plugin.
741
+
742
+ ## Quality Gates
743
+
744
+ ```bash
745
+ pnpm exec eslint ./sentry --quiet --ext .js,.jsx,.ts,.tsx
746
+ pnpm --filter @lark.js/sentry typecheck
747
+ pnpm --filter @lark.js/sentry test
748
+ pnpm test:coverage
749
+ pnpm build
750
+ ```
751
+
752
+ Coverage thresholds are 70 for lines, functions, branches, and statements.
753
+
754
+ ## Build and Publish
755
+
756
+ ```bash
757
+ pnpm build
758
+ pnpm build:tsup
759
+ python3 publish.py --dry-run
760
+ ```
761
+
762
+ Publish after npm login:
763
+
764
+ ```bash
765
+ python3 publish.py --publish --registry https://registry.npmjs.org/
766
+ ```
767
+
768
+ The publish script validates lint, typecheck, tests, coverage, build output, ESM imports, CJS requires, package exports, absence of sourcemaps, absence of `dist/node_modules`, and absence of package tarball residue.
769
+
770
+ Published npm files are limited to `dist` and package metadata. Source files, tests, sourcemaps, and temporary tarballs are not published.
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e="0.0.5",r={version:e};exports.default=r,exports.version=e;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e="0.0.7",r={version:e};exports.default=r,exports.version=e;
@@ -1 +1 @@
1
- var a="0.0.5",e={version:a};export{e as default,a as version};
1
+ var a="0.0.7",e={version:a};export{e as default,a as version};
package/dist/vite.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("node:buffer"),t=require("node:path"),n=require("node:fs");require("./constants/index.cjs"),require("./types/enums.cjs");var r=require("./utils/sentry.cjs"),s=require("./utils/logger.cjs"),i=require("zod");function o(t,n){return"string"==typeof n?t+n:e.Buffer.isBuffer(n)?t+n.toString("utf8"):n instanceof Uint8Array?t+e.Buffer.from(n).toString("utf8"):t}function c(e){return i.z.json().parse(e)}const u=(e,t)=>n=>{n.middlewares.use((n,r,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=o(e,t)}),n.on("end",()=>{if(e)try{const n=c(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}r.setHeader("Content-Type","application/json"),r.statusCode=200,r.end(JSON.stringify({code:0,message:"success"}))})}else s()})},a=(e,t)=>n=>{n.middlewares.use((n,r,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=o(e,t)}),n.on("end",()=>{if(e)try{const n=c(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}r.setHeader("Content-Type","application/json"),r.statusCode=200,r.end(JSON.stringify({code:0,message:"success"}))})}else s()})};function l({dsn:e}){const s=t.join(process.cwd(),"logs");n.existsSync(s)||n.mkdirSync(s,{recursive:!0});const i=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),o=t.join(s,`sentry_${i}.log`),c=n.createWriteStream(o,{flags:"a"});return{name:"vite-plugin-sentry",configureServer:a(e||r.default.options.dsn||"/sentry",c),closeBundle(){c&&c.close()}}}exports.default=l,exports.sentryPlugin=l,exports.sentryPlugin7=function({dsn:e}={}){const i=t.join(process.cwd(),"logs");n.existsSync(i)||n.mkdirSync(i,{recursive:!0});const o=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),c=t.join(i,`sentry_${o}.log`),a=n.createWriteStream(c,{flags:"a"});return s.sentryLogger.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${c}`),{name:"vite-plugin-sentry",configureServer:u(e||r.default.options.dsn||"/sentry",a),closeBundle(){a&&a.close()}}};
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("node:buffer"),t=require("node:path"),n=require("node:fs");require("./constants/index.cjs"),require("./types/enums.cjs");var r=require("./utils/sentry.cjs"),s=require("./utils/logger.cjs"),i=require("zod");function o(t,n){return"string"==typeof n?t+n:e.Buffer.isBuffer(n)?t+n.toString("utf8"):n instanceof Uint8Array?t+e.Buffer.from(n).toString("utf8"):t}function l(e){return i.z.json().parse(e)}const u=(e,t)=>n=>{n.middlewares.use((n,r,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=o(e,t)}),n.on("end",()=>{if(e)try{const n=l(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}r.setHeader("Content-Type","application/json"),r.statusCode=200,r.end(JSON.stringify({code:0,message:"success"}))})}else s()})},c=(e,t)=>n=>{n.middlewares.use((n,r,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=o(e,t)}),n.on("end",()=>{if(e)try{const n=l(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}r.setHeader("Content-Type","application/json"),r.statusCode=200,r.end(JSON.stringify({code:0,message:"success"}))})}else s()})};function a(){const e=t.join(process.cwd(),"logs");n.existsSync(e)||n.mkdirSync(e,{recursive:!0});const r=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),s=t.join(e,`sentry_${r}.log`);return{fileStream:n.createWriteStream(s,{flags:"a"}),logFile:s}}function f({dsn:e}){const{fileStream:t,logFile:n}=a();return s.sentryLogger.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${n}`),{name:"vite-plugin-sentry",configureServer:c(e||r.default.options.dsn||"/sentry",t),closeBundle(){t&&t.close()}}}exports.default=f,exports.sentryPlugin=f,exports.sentryPlugin7=function({dsn:e}={}){const{fileStream:t,logFile:n}=a();return s.sentryLogger.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${n}`),{name:"vite-plugin-sentry",configureServer:u(e||r.default.options.dsn||"/sentry",t),closeBundle(){t&&t.close()}}};
package/dist/vite.js CHANGED
@@ -1 +1 @@
1
- import{Buffer as e}from"node:buffer";import{join as t}from"node:path";import{existsSync as n,mkdirSync as s,createWriteStream as o}from"node:fs";import"./constants/index.js";import"./types/enums.js";import r from"./utils/sentry.js";import{sentryLogger as i}from"./utils/logger.js";import{z as c}from"zod";function l(t,n){return"string"==typeof n?t+n:e.isBuffer(n)?t+n.toString("utf8"):n instanceof Uint8Array?t+e.from(n).toString("utf8"):t}function a(e){return c.json().parse(e)}const f=(e,t)=>n=>{n.middlewares.use((n,s,o)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=l(e,t)}),n.on("end",()=>{if(e)try{const n=a(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}s.setHeader("Content-Type","application/json"),s.statusCode=200,s.end(JSON.stringify({code:0,message:"success"}))})}else o()})},d=(e,t)=>n=>{n.middlewares.use((n,s,o)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=l(e,t)}),n.on("end",()=>{if(e)try{const n=a(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}s.setHeader("Content-Type","application/json"),s.statusCode=200,s.end(JSON.stringify({code:0,message:"success"}))})}else o()})};function u({dsn:e}={}){const c=t(process.cwd(),"logs");n(c)||s(c,{recursive:!0});const l=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),a=t(c,`sentry_${l}.log`),d=o(a,{flags:"a"});return i.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${a}`),{name:"vite-plugin-sentry",configureServer:f(e||r.options.dsn||"/sentry",d),closeBundle(){d&&d.close()}}}function p({dsn:e}){const i=t(process.cwd(),"logs");n(i)||s(i,{recursive:!0});const c=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),l=t(i,`sentry_${c}.log`),a=o(l,{flags:"a"});return{name:"vite-plugin-sentry",configureServer:d(e||r.options.dsn||"/sentry",a),closeBundle(){a&&a.close()}}}export{p as default,p as sentryPlugin,u as sentryPlugin7};
1
+ import{Buffer as e}from"node:buffer";import{join as t}from"node:path";import{existsSync as n,mkdirSync as o,createWriteStream as s}from"node:fs";import"./constants/index.js";import"./types/enums.js";import r from"./utils/sentry.js";import{sentryLogger as i}from"./utils/logger.js";import{z as l}from"zod";function c(t,n){return"string"==typeof n?t+n:e.isBuffer(n)?t+n.toString("utf8"):n instanceof Uint8Array?t+e.from(n).toString("utf8"):t}function a(e){return l.json().parse(e)}const f=(e,t)=>n=>{n.middlewares.use((n,o,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=c(e,t)}),n.on("end",()=>{if(e)try{const n=a(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}o.setHeader("Content-Type","application/json"),o.statusCode=200,o.end(JSON.stringify({code:0,message:"success"}))})}else s()})},u=(e,t)=>n=>{n.middlewares.use((n,o,s)=>{if(n.url===e&&"POST"===n.method){let e="";n.on("data",t=>{e=c(e,t)}),n.on("end",()=>{if(e)try{const n=a(e);t.write(JSON.stringify(n)+"\n")}catch{t.write(e+"\n")}o.setHeader("Content-Type","application/json"),o.statusCode=200,o.end(JSON.stringify({code:0,message:"success"}))})}else s()})};function d(){const e=t(process.cwd(),"logs");n(e)||o(e,{recursive:!0});const r=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),i=t(e,`sentry_${r}.log`);return{fileStream:s(i,{flags:"a"}),logFile:i}}function m({dsn:e}={}){const{fileStream:t,logFile:n}=d();return i.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${n}`),{name:"vite-plugin-sentry",configureServer:f(e||r.options.dsn||"/sentry",t),closeBundle(){t&&t.close()}}}function g({dsn:e}){const{fileStream:t,logFile:n}=d();return i.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${n}`),{name:"vite-plugin-sentry",configureServer:u(e||r.options.dsn||"/sentry",t),closeBundle(){t&&t.close()}}}export{g as default,g as sentryPlugin,m as sentryPlugin7};
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("node:buffer"),t=require("node:path"),r=require("node:fs"),n=require("zod");require("./constants/index.cjs"),require("./types/enums.cjs");var s=require("./utils/sentry.cjs"),i=require("./utils/logger.cjs");function o(t,r){return(s,i,o)=>{if(s.url===t&&"POST"===s.method){let t="";s.on("data",r=>{t=function(t,r){return"string"==typeof r?t+r:e.Buffer.isBuffer(r)?t+r.toString("utf8"):r instanceof Uint8Array?t+e.Buffer.from(r).toString("utf8"):t}(t,r)}),s.on("end",()=>{if(t)try{const e=function(e){return n.z.json().parse(e)}(t);r.write(JSON.stringify(e)+"\n")}catch{r.write(t+"\n")}i.setHeader("Content-Type","application/json"),i.statusCode=200,i.end(JSON.stringify({code:0,message:"success"}))})}else o()}}function u(){const e=t.join(process.cwd(),"logs");r.existsSync(e)||r.mkdirSync(e,{recursive:!0});const n=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),s=t.join(e,`sentry_${n}.log`);return{fileStream:r.createWriteStream(s,{flags:"a"}),logFile:s}}class l{dsn;constructor(e={}){this.dsn=e.dsn}apply(e){const t=e.options.devServer,{fileStream:r,logFile:n}=u(),l=o(this.dsn||s.default.options.dsn||"/sentry",r);i.sentryLogger.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${n}`);const c=t.setupMiddlewares;t.setupMiddlewares=(e,t)=>{const r=c?c(e,t):e,n={name:"sentry-mock",middleware:l};return r.unshift(n),r},e.hooks.shutdown.tap("SentryWebpackPlugin",()=>{r&&!r.destroyed&&r.close()})}}function c(e={}){return new l(e)}exports.SentryWebpackPlugin=l,exports.default=c,exports.sentryMiddleware=function(e={}){const{fileStream:t,logFile:r}=u();return i.sentryLogger.info("@lark.js/sentry",`Sentry mock middleware initialized, logs will be written to ${r}`),o(e.dsn||s.default.options.dsn||"/sentry",t)},exports.sentryPlugin=c;
@@ -0,0 +1,55 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { WebpackPluginInstance, Compiler } from 'webpack';
3
+ import DevServer from 'webpack-dev-server';
4
+
5
+ type SentryDevMiddleware = (req: IncomingMessage, res: ServerResponse, next: DevServer.NextFunction) => void;
6
+ interface ISentryWebpackPluginOptions {
7
+ dsn?: string;
8
+ }
9
+ /**
10
+ * Connect/express-style middleware that mocks the sentry report endpoint
11
+ * during webpack-dev-server development. Mount it manually inside the
12
+ * `setupMiddlewares` option of webpack-dev-server.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { sentryMiddleware } from "@lark.js/sentry/webpack";
17
+ *
18
+ * export default {
19
+ * devServer: {
20
+ * setupMiddlewares(middlewares) {
21
+ * middlewares.unshift({
22
+ * name: "sentry-mock",
23
+ * middleware: sentryMiddleware({ dsn: "/api/log" }),
24
+ * });
25
+ * return middlewares;
26
+ * },
27
+ * },
28
+ * };
29
+ * ```
30
+ */
31
+ declare function sentryMiddleware(options?: ISentryWebpackPluginOptions): SentryDevMiddleware;
32
+ /**
33
+ * Webpack plugin that automatically wires the sentry log-collection middleware
34
+ * into webpack-dev-server. It only takes effect when
35
+ * `compiler.options.devServer` exists, so production builds remain untouched.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { sentryPlugin } from "@lark.js/sentry/webpack";
40
+ *
41
+ * export default {
42
+ * plugins: [sentryPlugin({ dsn: "/api/log" })],
43
+ * devServer: { ... },
44
+ * };
45
+ * ```
46
+ */
47
+ declare class SentryWebpackPlugin implements WebpackPluginInstance {
48
+ private readonly dsn;
49
+ constructor(options?: ISentryWebpackPluginOptions);
50
+ apply(compiler: Compiler): void;
51
+ }
52
+ declare function sentryPlugin(options?: ISentryWebpackPluginOptions): SentryWebpackPlugin;
53
+
54
+ export { SentryWebpackPlugin, sentryPlugin as default, sentryMiddleware, sentryPlugin };
55
+ export type { ISentryWebpackPluginOptions, SentryDevMiddleware };
@@ -0,0 +1 @@
1
+ import{Buffer as t}from"node:buffer";import{join as e}from"node:path";import{existsSync as n,mkdirSync as o,createWriteStream as r}from"node:fs";import{z as s}from"zod";import"./constants/index.js";import"./types/enums.js";import i from"./utils/sentry.js";import{sentryLogger as l}from"./utils/logger.js";function a(e,n){return(o,r,i)=>{if(o.url===e&&"POST"===o.method){let e="";o.on("data",n=>{e=function(e,n){return"string"==typeof n?e+n:t.isBuffer(n)?e+n.toString("utf8"):n instanceof Uint8Array?e+t.from(n).toString("utf8"):e}(e,n)}),o.on("end",()=>{if(e)try{const t=function(t){return s.json().parse(t)}(e);n.write(JSON.stringify(t)+"\n")}catch{n.write(e+"\n")}r.setHeader("Content-Type","application/json"),r.statusCode=200,r.end(JSON.stringify({code:0,message:"success"}))})}else i()}}function d(){const t=e(process.cwd(),"logs");n(t)||o(t,{recursive:!0});const s=(new Date).toISOString().replace(/[-:.]/g,"").slice(0,14),i=e(t,`sentry_${s}.log`);return{fileStream:r(i,{flags:"a"}),logFile:i}}function c(t={}){const{fileStream:e,logFile:n}=d();l.info("@lark.js/sentry",`Sentry mock middleware initialized, logs will be written to ${n}`);return a(t.dsn||i.options.dsn||"/sentry",e)}class f{dsn;constructor(t={}){this.dsn=t.dsn}apply(t){const e=t.options.devServer,{fileStream:n,logFile:o}=d(),r=a(this.dsn||i.options.dsn||"/sentry",n);l.info("@lark.js/sentry",`Sentry mock plugin initialized, logs will be written to ${o}`);const s=e.setupMiddlewares;e.setupMiddlewares=(t,e)=>{const n=s?s(t,e):t,o={name:"sentry-mock",middleware:r};return n.unshift(o),n},t.hooks.shutdown.tap("SentryWebpackPlugin",()=>{n&&!n.destroyed&&n.close()})}}function u(t={}){return new f(t)}export{f as SentryWebpackPlugin,u as default,c as sentryMiddleware,u as sentryPlugin};
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@lark.js/sentry",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "sentry sdk",
5
- "type": "module",
6
5
  "keywords": [
7
- "sentry",
8
- "sdk"
6
+ "sdk",
7
+ "sentry"
9
8
  ],
10
- "author": "github.com/hangtiancheng",
11
9
  "license": "MIT",
12
- "main": "./dist/index.cjs",
13
- "module": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
10
+ "author": "github.com/hangtiancheng",
15
11
  "files": [
16
12
  "dist",
17
- "../README.md"
13
+ "README.md"
18
14
  ],
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
19
  "exports": {
20
20
  ".": {
21
21
  "types": "./dist/index.d.ts",
@@ -41,6 +41,11 @@
41
41
  "types": "./dist/vite.d.ts",
42
42
  "import": "./dist/vite.js",
43
43
  "require": "./dist/vite.cjs"
44
+ },
45
+ "./webpack": {
46
+ "types": "./dist/webpack.d.ts",
47
+ "import": "./dist/webpack.js",
48
+ "require": "./dist/webpack.cjs"
44
49
  }
45
50
  },
46
51
  "scripts": {
@@ -48,7 +53,8 @@
48
53
  "test:watch": "vitest --config ./vitest.config.ts",
49
54
  "test:coverage": "vitest run --config ./vitest.config.ts --coverage",
50
55
  "typecheck": "tsc --noEmit",
51
- "vite7": "pnpm add -D vite7@npm:vite@7.3.3"
56
+ "vite7": "pnpm add -D vite7@npm:vite@7.3.3",
57
+ "format": "oxfmt --write ./"
52
58
  },
53
59
  "dependencies": {
54
60
  "@fingerprintjs/fingerprintjs": "^5.2.0",
@@ -63,24 +69,28 @@
63
69
  "@rollup/plugin-node-resolve": "^16.0.3",
64
70
  "@rollup/plugin-typescript": "^12.3.0",
65
71
  "@rrweb/record": "2.0.0-alpha.20",
66
- "@types/node": "^25.9.1",
72
+ "@types/node": "^25.9.2",
67
73
  "@types/pako": "^2.0.4",
68
- "@types/react": "^19.2.15",
74
+ "@types/react": "^19.2.17",
69
75
  "@types/react-dom": "^19.2.3",
70
76
  "@vitest/coverage-v8": "^1.6.1",
71
77
  "jsdom": "^24.1.3",
72
- "react": "^19.2.6",
73
- "rollup": "^4.61.0",
78
+ "oxfmt": "^0.54.0",
79
+ "react": "^19.2.7",
80
+ "rollup": "^4.61.1",
74
81
  "typescript": "^6.0.3",
75
82
  "vite": "^8.0.16",
76
83
  "vite7": "npm:vite@7.3.3",
77
84
  "vitest": "^1.6.1",
78
- "vue": "^3.5.35"
85
+ "vue": "^3.5.35",
86
+ "webpack": "^5.107.2",
87
+ "webpack-dev-server": "^5.2.4"
79
88
  },
80
89
  "peerDependencies": {
81
90
  "react": "^19.0.0",
82
91
  "vite": "^7.0.0 || ^8.0.0",
83
- "vue": "^3.0.0"
92
+ "vue": "^3.0.0",
93
+ "webpack": "^4.0.0 || ^5.0.0"
84
94
  },
85
95
  "peerDependenciesMeta": {
86
96
  "react": {
@@ -91,6 +101,12 @@
91
101
  },
92
102
  "vue": {
93
103
  "optional": true
104
+ },
105
+ "webpack": {
106
+ "optional": true
107
+ },
108
+ "webpack-dev-server": {
109
+ "optional": true
94
110
  }
95
111
  }
96
112
  }