@pageviews/tracker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,514 @@
1
+ 'use strict';
2
+
3
+ function sendEvent(endpoint, payload, callback, siteId) {
4
+ if (typeof fetch === 'undefined')
5
+ return;
6
+ const headers = { 'Content-Type': 'text/plain' };
7
+ if (siteId)
8
+ headers['X-Site-ID'] = siteId;
9
+ fetch(endpoint, {
10
+ method: 'POST',
11
+ headers,
12
+ keepalive: true,
13
+ body: JSON.stringify(payload),
14
+ })
15
+ .then((response) => {
16
+ if (callback)
17
+ callback({ status: response.status });
18
+ })
19
+ .catch((error) => {
20
+ if (callback)
21
+ callback({ error });
22
+ });
23
+ }
24
+ function sendBeaconEvent(endpoint, payload, siteId) {
25
+ const body = JSON.stringify(payload);
26
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
27
+ // sendBeacon cannot set custom headers; Go service falls back to domain resolution
28
+ const blob = new Blob([body], { type: 'text/plain' });
29
+ const queued = navigator.sendBeacon(endpoint, blob);
30
+ if (queued)
31
+ return;
32
+ }
33
+ // Fallback to fetch with keepalive (can set X-Site-ID)
34
+ sendEvent(endpoint, payload, undefined, siteId);
35
+ }
36
+
37
+ // src/core/utils.ts
38
+ function getScreenSize() {
39
+ return `${window.innerWidth}x${window.innerHeight}`;
40
+ }
41
+ function isLocalhost(hostname) {
42
+ return (hostname === 'localhost' ||
43
+ /^127(\.[0-9]+){0,2}\.[0-9]+$/.test(hostname) ||
44
+ hostname === '[::1]');
45
+ }
46
+ function isIgnored() {
47
+ try {
48
+ return window.localStorage.getItem('pageviews_ignore') === 'true';
49
+ }
50
+ catch (_a) {
51
+ return false;
52
+ }
53
+ }
54
+ function isBot() {
55
+ const w = window;
56
+ const nav = navigator;
57
+ return !!(w._phantom ||
58
+ w.__nightmare ||
59
+ nav.webdriver ||
60
+ w.Cypress ||
61
+ w.__puppeteer_evaluation_script__ ||
62
+ w.__playwright);
63
+ }
64
+
65
+ function createTracker(config) {
66
+ const ignored = isLocalhost(location.hostname) || isIgnored() || isBot();
67
+ function send(payload, callback) {
68
+ if (ignored) {
69
+ callback === null || callback === void 0 ? void 0 : callback();
70
+ return;
71
+ }
72
+ sendEvent(config.endpoint, payload, callback, config.siteId);
73
+ }
74
+ function trackPageview(url) {
75
+ const payload = {
76
+ n: 'pageview',
77
+ d: config.site,
78
+ u: url || location.href,
79
+ r: document.referrer || null,
80
+ s: getScreenSize(),
81
+ };
82
+ if (config.hashRouting)
83
+ payload.h = true;
84
+ send(payload);
85
+ }
86
+ function track(eventName, options) {
87
+ const payload = {
88
+ n: eventName,
89
+ d: config.site,
90
+ u: (options === null || options === void 0 ? void 0 : options.url) || location.href,
91
+ r: document.referrer || null,
92
+ s: getScreenSize(),
93
+ };
94
+ if (options === null || options === void 0 ? void 0 : options.props) {
95
+ // Coerce all prop values to strings — the ingest service stores Map(String,String)
96
+ const coerced = {};
97
+ for (const [k, v] of Object.entries(options.props)) {
98
+ coerced[k] = String(v);
99
+ }
100
+ payload.p = coerced;
101
+ }
102
+ if (options === null || options === void 0 ? void 0 : options.revenue) {
103
+ const { amount, currency } = options.revenue;
104
+ if (amount > 0 && amount <= 1000000000 && currency) {
105
+ payload.rv = { a: amount, c: currency };
106
+ }
107
+ }
108
+ if (config.hashRouting)
109
+ payload.h = true;
110
+ send(payload, options === null || options === void 0 ? void 0 : options.callback);
111
+ }
112
+ function identify(data) {
113
+ const payload = {
114
+ n: 'identify',
115
+ d: config.site,
116
+ u: location.href,
117
+ r: null,
118
+ s: getScreenSize(),
119
+ uid: data.userId,
120
+ };
121
+ send(payload);
122
+ }
123
+ return { trackPageview, track, identify };
124
+ }
125
+
126
+ let cleanup = null;
127
+ function startNavTracking(onNavigate, hashRouting) {
128
+ let lastPathname = location.pathname;
129
+ const handleNav = () => {
130
+ if (!hashRouting && location.pathname === lastPathname)
131
+ return;
132
+ lastPathname = location.pathname;
133
+ onNavigate();
134
+ };
135
+ // Initial pageview
136
+ onNavigate();
137
+ if (hashRouting) {
138
+ window.addEventListener('hashchange', handleNav);
139
+ cleanup = () => window.removeEventListener('hashchange', handleNav);
140
+ }
141
+ else {
142
+ const originalPushState = history.pushState;
143
+ history.pushState = function (...args) {
144
+ originalPushState.apply(this, args);
145
+ handleNav();
146
+ };
147
+ const onPopState = () => {
148
+ lastPathname = location.pathname;
149
+ onNavigate();
150
+ };
151
+ window.addEventListener('popstate', onPopState);
152
+ cleanup = () => {
153
+ history.pushState = originalPushState;
154
+ window.removeEventListener('popstate', onPopState);
155
+ };
156
+ }
157
+ // Handle bfcache (back-forward cache)
158
+ const onPageShow = (e) => {
159
+ if (e.persisted)
160
+ onNavigate();
161
+ };
162
+ window.addEventListener('pageshow', onPageShow);
163
+ const prevCleanup = cleanup;
164
+ cleanup = () => {
165
+ prevCleanup === null || prevCleanup === void 0 ? void 0 : prevCleanup();
166
+ window.removeEventListener('pageshow', onPageShow);
167
+ };
168
+ }
169
+ function stopNavTracking() {
170
+ cleanup === null || cleanup === void 0 ? void 0 : cleanup();
171
+ cleanup = null;
172
+ }
173
+
174
+ let startTime = 0;
175
+ let accumulatedTime = 0;
176
+ let isActive = false;
177
+ let maxScrollDepth = 0;
178
+ let scrollTicking = false;
179
+ let cachedPageHeight = 0;
180
+ function computePageHeight() {
181
+ const body = document.body || {};
182
+ const html = document.documentElement || {};
183
+ return Math.max(body.scrollHeight || 0, body.offsetHeight || 0, html.scrollHeight || 0, html.offsetHeight || 0);
184
+ }
185
+ function getCurrentScrollDepth() {
186
+ if (cachedPageHeight === 0)
187
+ return 0;
188
+ const viewportHeight = window.innerHeight || 0;
189
+ if (cachedPageHeight <= viewportHeight)
190
+ return 100;
191
+ const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
192
+ return Math.round(((scrollTop + viewportHeight) / cachedPageHeight) * 100);
193
+ }
194
+ function onResize() {
195
+ cachedPageHeight = computePageHeight();
196
+ }
197
+ function onVisibilityChange() {
198
+ if (document.visibilityState === 'hidden') {
199
+ if (isActive) {
200
+ accumulatedTime += Date.now() - startTime;
201
+ isActive = false;
202
+ }
203
+ }
204
+ else if (document.visibilityState === 'visible') {
205
+ startTime = Date.now();
206
+ isActive = true;
207
+ }
208
+ }
209
+ function onScroll() {
210
+ if (scrollTicking)
211
+ return;
212
+ scrollTicking = true;
213
+ requestAnimationFrame(() => {
214
+ const depth = getCurrentScrollDepth();
215
+ if (depth > maxScrollDepth) {
216
+ maxScrollDepth = depth;
217
+ }
218
+ scrollTicking = false;
219
+ });
220
+ }
221
+ function stopEngagement() {
222
+ document.removeEventListener('visibilitychange', onVisibilityChange);
223
+ document.removeEventListener('scroll', onScroll);
224
+ window.removeEventListener('resize', onResize);
225
+ }
226
+ function startEngagement() {
227
+ stopEngagement();
228
+ startTime = Date.now();
229
+ accumulatedTime = 0;
230
+ maxScrollDepth = 0;
231
+ isActive = true;
232
+ cachedPageHeight = computePageHeight();
233
+ document.addEventListener('visibilitychange', onVisibilityChange);
234
+ document.addEventListener('scroll', onScroll, { passive: true });
235
+ window.addEventListener('resize', onResize, { passive: true });
236
+ }
237
+ function resetEngagement() {
238
+ startTime = Date.now();
239
+ accumulatedTime = 0;
240
+ maxScrollDepth = 0;
241
+ isActive = true;
242
+ }
243
+ function getEngagementData() {
244
+ const currentActive = isActive ? Date.now() - startTime : 0;
245
+ return {
246
+ activeTime: accumulatedTime + currentActive,
247
+ maxScrollDepth,
248
+ };
249
+ }
250
+
251
+ // src/core/events.ts
252
+ const DEFAULT_FILE_TYPES = [
253
+ 'pdf', 'xlsx', 'docx', 'txt', 'rtf', 'csv', 'exe', 'key', 'pps', 'ppt',
254
+ 'pptx', '7z', 'pkg', 'rar', 'gz', 'zip', 'avi', 'mov', 'mp4', 'mpeg',
255
+ 'wmv', 'midi', 'mp3', 'wav', 'wma', 'dmg',
256
+ ];
257
+ let clickHandler = null;
258
+ let submitHandler = null;
259
+ function findLink(target) {
260
+ let el = target;
261
+ while (el && el.tagName !== 'A') {
262
+ el = el.parentElement;
263
+ }
264
+ return el;
265
+ }
266
+ function isOutbound(link) {
267
+ return link.host !== '' && link.host !== location.host;
268
+ }
269
+ function isFileDownload(href) {
270
+ var _a;
271
+ const ext = (_a = href.split('?')[0].split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
272
+ return ext ? DEFAULT_FILE_TYPES.includes(ext) : false;
273
+ }
274
+ function startEventTracking(trackFn, config) {
275
+ stopEventTracking();
276
+ clickHandler = (e) => {
277
+ const link = findLink(e.target);
278
+ if (!link || !link.href)
279
+ return;
280
+ const href = link.href;
281
+ const cleanUrl = href.split('?')[0];
282
+ if (config.fileDownloads && isFileDownload(href)) {
283
+ trackFn('File Download', { props: { url: cleanUrl } });
284
+ return;
285
+ }
286
+ if (config.outboundLinks && isOutbound(link)) {
287
+ trackFn('Outbound Link: Click', { props: { url: href } });
288
+ }
289
+ };
290
+ document.addEventListener('click', clickHandler);
291
+ if (config.formSubmissions) {
292
+ submitHandler = (e) => {
293
+ const form = e.target;
294
+ if (form.hasAttribute('novalidate') || form.checkValidity()) {
295
+ trackFn('Form Submission');
296
+ }
297
+ };
298
+ document.addEventListener('submit', submitHandler, true);
299
+ }
300
+ }
301
+ function stopEventTracking() {
302
+ if (clickHandler) {
303
+ document.removeEventListener('click', clickHandler);
304
+ clickHandler = null;
305
+ }
306
+ if (submitHandler) {
307
+ document.removeEventListener('submit', submitHandler, true);
308
+ submitHandler = null;
309
+ }
310
+ }
311
+
312
+ let scrollHandler = null;
313
+ function startScrollGoals(trackFn, targets, currentPath) {
314
+ const matchingTargets = targets.filter((t) => t.path === currentPath);
315
+ if (matchingTargets.length === 0)
316
+ return;
317
+ const fired = new Set();
318
+ let ticking = false;
319
+ scrollHandler = () => {
320
+ if (ticking)
321
+ return;
322
+ ticking = true;
323
+ requestAnimationFrame(() => {
324
+ var _a;
325
+ const pageHeight = Math.max(document.documentElement.scrollHeight || 0, ((_a = document.body) === null || _a === void 0 ? void 0 : _a.scrollHeight) || 0);
326
+ const viewportHeight = window.innerHeight || 0;
327
+ const scrollTop = window.scrollY || 0;
328
+ const scrollPercent = pageHeight <= viewportHeight
329
+ ? 100
330
+ : Math.round(((scrollTop + viewportHeight) / pageHeight) * 100);
331
+ for (const target of matchingTargets) {
332
+ if (!fired.has(target.goalName) && scrollPercent >= target.threshold) {
333
+ fired.add(target.goalName);
334
+ trackFn(target.goalName);
335
+ }
336
+ }
337
+ ticking = false;
338
+ });
339
+ };
340
+ document.addEventListener('scroll', scrollHandler, { passive: true });
341
+ }
342
+ function stopScrollGoals() {
343
+ if (scrollHandler) {
344
+ document.removeEventListener('scroll', scrollHandler);
345
+ scrollHandler = null;
346
+ }
347
+ }
348
+
349
+ /** Minimum active time (ms) before an engagement event is worth sending */
350
+ const MIN_ENGAGEMENT_MS = 1000;
351
+ function setupTracking(config, tracker) {
352
+ stopEngagement();
353
+ startEngagement();
354
+ let isFirstPageview = true;
355
+ let currentUrl = location.href;
356
+ startNavTracking(() => {
357
+ // Send engagement for previous page (skip on first pageview)
358
+ if (!isFirstPageview) {
359
+ const engagement = getEngagementData();
360
+ if (engagement.activeTime >= MIN_ENGAGEMENT_MS) {
361
+ sendEvent(config.endpoint, {
362
+ n: 'engagement',
363
+ d: config.site,
364
+ u: currentUrl,
365
+ r: null,
366
+ s: getScreenSize(),
367
+ sd: engagement.maxScrollDepth,
368
+ e: engagement.activeTime,
369
+ }, undefined, config.siteId);
370
+ }
371
+ resetEngagement();
372
+ }
373
+ isFirstPageview = false;
374
+ currentUrl = location.href;
375
+ tracker.trackPageview();
376
+ stopScrollGoals();
377
+ if (config.scrollTargets.length > 0) {
378
+ startScrollGoals((goalName) => tracker.track(goalName), config.scrollTargets, location.pathname);
379
+ }
380
+ }, config.hashRouting);
381
+ startEventTracking((name, options) => tracker.track(name, options), {
382
+ outboundLinks: config.outboundLinks,
383
+ fileDownloads: config.fileDownloads,
384
+ formSubmissions: config.formSubmissions,
385
+ });
386
+ // Send engagement on page unload
387
+ const onUnload = () => {
388
+ if (document.visibilityState === 'hidden') {
389
+ const engagement = getEngagementData();
390
+ if (engagement.activeTime >= MIN_ENGAGEMENT_MS) {
391
+ sendBeaconEvent(config.endpoint, {
392
+ n: 'engagement',
393
+ d: config.site,
394
+ u: currentUrl,
395
+ r: null,
396
+ s: getScreenSize(),
397
+ sd: engagement.maxScrollDepth,
398
+ e: engagement.activeTime,
399
+ }, config.siteId);
400
+ }
401
+ }
402
+ };
403
+ document.addEventListener('visibilitychange', onUnload);
404
+ // Return cleanup function
405
+ return () => {
406
+ stopNavTracking();
407
+ stopEngagement();
408
+ stopEventTracking();
409
+ stopScrollGoals();
410
+ document.removeEventListener('visibilitychange', onUnload);
411
+ };
412
+ }
413
+ function initTracking(config) {
414
+ const tracker = createTracker(config);
415
+ let cleanup = null;
416
+ if (document.visibilityState === 'prerender') {
417
+ const onVisible = () => {
418
+ if (document.visibilityState === 'visible') {
419
+ document.removeEventListener('visibilitychange', onVisible);
420
+ cleanup = setupTracking(config, tracker);
421
+ }
422
+ };
423
+ document.addEventListener('visibilitychange', onVisible);
424
+ return {
425
+ tracker,
426
+ destroy: () => {
427
+ document.removeEventListener('visibilitychange', onVisible);
428
+ cleanup === null || cleanup === void 0 ? void 0 : cleanup();
429
+ },
430
+ };
431
+ }
432
+ cleanup = setupTracking(config, tracker);
433
+ return {
434
+ tracker,
435
+ destroy: () => { cleanup === null || cleanup === void 0 ? void 0 : cleanup(); },
436
+ };
437
+ }
438
+
439
+ const CACHE_KEY = 'pageviews_config';
440
+ const CACHE_TTL = 1000 * 60 * 60; // 1 hour
441
+ let instance = null;
442
+ let initialized = false;
443
+ function getCachedConfig(siteId) {
444
+ try {
445
+ const raw = localStorage.getItem(`${CACHE_KEY}_${siteId}`);
446
+ if (!raw)
447
+ return null;
448
+ const { config, ts } = JSON.parse(raw);
449
+ if (Date.now() - ts > CACHE_TTL)
450
+ return null;
451
+ return config;
452
+ }
453
+ catch (_a) {
454
+ return null;
455
+ }
456
+ }
457
+ function setCachedConfig(siteId, config) {
458
+ try {
459
+ localStorage.setItem(`${CACHE_KEY}_${siteId}`, JSON.stringify({ config, ts: Date.now() }));
460
+ }
461
+ catch (_a) {
462
+ // localStorage unavailable
463
+ }
464
+ }
465
+ async function fetchConfig(endpoint, siteId) {
466
+ const res = await fetch(`${endpoint}/api/config/${siteId}`);
467
+ if (!res.ok)
468
+ throw new Error(`Config fetch failed: HTTP ${res.status}`);
469
+ return res.json();
470
+ }
471
+ function startTracking(config) {
472
+ instance = initTracking(config);
473
+ }
474
+ async function init(options) {
475
+ if (initialized)
476
+ throw new Error('pageviews.init() can only be called once');
477
+ initialized = true;
478
+ const ingestEndpoint = (options.endpoint || 'https://data.pageviews.ai') + '/api/event';
479
+ const cfgEndpoint = options.configEndpoint || 'https://app.pageviews.ai';
480
+ const cached = getCachedConfig(options.siteId);
481
+ if (cached) {
482
+ startTracking(Object.assign(Object.assign({}, cached), { siteId: options.siteId, endpoint: ingestEndpoint }));
483
+ // Refresh cache in background
484
+ fetchConfig(cfgEndpoint, options.siteId).then((fresh) => {
485
+ setCachedConfig(options.siteId, fresh);
486
+ }).catch(() => { });
487
+ return;
488
+ }
489
+ const configData = await fetchConfig(cfgEndpoint, options.siteId);
490
+ setCachedConfig(options.siteId, configData);
491
+ startTracking(Object.assign(Object.assign({}, configData), { siteId: options.siteId, endpoint: ingestEndpoint }));
492
+ }
493
+ function track(eventName, options) {
494
+ if (!instance)
495
+ throw new Error('pageviews.track() requires init() to be called first');
496
+ instance.tracker.track(eventName, options);
497
+ }
498
+ function identify(data) {
499
+ if (!instance)
500
+ throw new Error('pageviews.identify() requires init() to be called first');
501
+ instance.tracker.identify(data);
502
+ }
503
+ function destroy() {
504
+ if (!instance)
505
+ return;
506
+ instance.destroy();
507
+ instance = null;
508
+ initialized = false;
509
+ }
510
+
511
+ exports.destroy = destroy;
512
+ exports.identify = identify;
513
+ exports.init = init;
514
+ exports.track = track;
@@ -0,0 +1,46 @@
1
+ interface SiteConfig {
2
+ site: string;
3
+ siteId: string;
4
+ endpoint: string;
5
+ hashRouting: boolean;
6
+ outboundLinks: boolean;
7
+ fileDownloads: boolean;
8
+ formSubmissions: boolean;
9
+ scrollTargets: ScrollTarget[];
10
+ }
11
+ interface ScrollTarget {
12
+ goalName: string;
13
+ path: string;
14
+ threshold: number;
15
+ }
16
+ interface TrackOptions {
17
+ props?: Record<string, string | number | boolean>;
18
+ revenue?: {
19
+ amount: number;
20
+ currency: string;
21
+ };
22
+ url?: string;
23
+ callback?: (result?: {
24
+ status: number;
25
+ } | {
26
+ error: unknown;
27
+ }) => void;
28
+ }
29
+ interface IdentifyData {
30
+ userId: string;
31
+ }
32
+
33
+ interface InitOptions {
34
+ siteId: string;
35
+ /** Ingest service base URL for sending events. Default: https://data.pageviews.ai */
36
+ endpoint?: string;
37
+ /** Laravel app base URL for fetching site config. Default: https://app.pageviews.ai */
38
+ configEndpoint?: string;
39
+ }
40
+ declare function init(options: InitOptions): Promise<void>;
41
+ declare function track(eventName: string, options?: TrackOptions): void;
42
+ declare function identify(data: IdentifyData): void;
43
+ declare function destroy(): void;
44
+
45
+ export { destroy, identify, init, track };
46
+ export type { IdentifyData, SiteConfig, TrackOptions };
@@ -0,0 +1,509 @@
1
+ function sendEvent(endpoint, payload, callback, siteId) {
2
+ if (typeof fetch === 'undefined')
3
+ return;
4
+ const headers = { 'Content-Type': 'text/plain' };
5
+ if (siteId)
6
+ headers['X-Site-ID'] = siteId;
7
+ fetch(endpoint, {
8
+ method: 'POST',
9
+ headers,
10
+ keepalive: true,
11
+ body: JSON.stringify(payload),
12
+ })
13
+ .then((response) => {
14
+ if (callback)
15
+ callback({ status: response.status });
16
+ })
17
+ .catch((error) => {
18
+ if (callback)
19
+ callback({ error });
20
+ });
21
+ }
22
+ function sendBeaconEvent(endpoint, payload, siteId) {
23
+ const body = JSON.stringify(payload);
24
+ if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
25
+ // sendBeacon cannot set custom headers; Go service falls back to domain resolution
26
+ const blob = new Blob([body], { type: 'text/plain' });
27
+ const queued = navigator.sendBeacon(endpoint, blob);
28
+ if (queued)
29
+ return;
30
+ }
31
+ // Fallback to fetch with keepalive (can set X-Site-ID)
32
+ sendEvent(endpoint, payload, undefined, siteId);
33
+ }
34
+
35
+ // src/core/utils.ts
36
+ function getScreenSize() {
37
+ return `${window.innerWidth}x${window.innerHeight}`;
38
+ }
39
+ function isLocalhost(hostname) {
40
+ return (hostname === 'localhost' ||
41
+ /^127(\.[0-9]+){0,2}\.[0-9]+$/.test(hostname) ||
42
+ hostname === '[::1]');
43
+ }
44
+ function isIgnored() {
45
+ try {
46
+ return window.localStorage.getItem('pageviews_ignore') === 'true';
47
+ }
48
+ catch (_a) {
49
+ return false;
50
+ }
51
+ }
52
+ function isBot() {
53
+ const w = window;
54
+ const nav = navigator;
55
+ return !!(w._phantom ||
56
+ w.__nightmare ||
57
+ nav.webdriver ||
58
+ w.Cypress ||
59
+ w.__puppeteer_evaluation_script__ ||
60
+ w.__playwright);
61
+ }
62
+
63
+ function createTracker(config) {
64
+ const ignored = isLocalhost(location.hostname) || isIgnored() || isBot();
65
+ function send(payload, callback) {
66
+ if (ignored) {
67
+ callback === null || callback === void 0 ? void 0 : callback();
68
+ return;
69
+ }
70
+ sendEvent(config.endpoint, payload, callback, config.siteId);
71
+ }
72
+ function trackPageview(url) {
73
+ const payload = {
74
+ n: 'pageview',
75
+ d: config.site,
76
+ u: url || location.href,
77
+ r: document.referrer || null,
78
+ s: getScreenSize(),
79
+ };
80
+ if (config.hashRouting)
81
+ payload.h = true;
82
+ send(payload);
83
+ }
84
+ function track(eventName, options) {
85
+ const payload = {
86
+ n: eventName,
87
+ d: config.site,
88
+ u: (options === null || options === void 0 ? void 0 : options.url) || location.href,
89
+ r: document.referrer || null,
90
+ s: getScreenSize(),
91
+ };
92
+ if (options === null || options === void 0 ? void 0 : options.props) {
93
+ // Coerce all prop values to strings — the ingest service stores Map(String,String)
94
+ const coerced = {};
95
+ for (const [k, v] of Object.entries(options.props)) {
96
+ coerced[k] = String(v);
97
+ }
98
+ payload.p = coerced;
99
+ }
100
+ if (options === null || options === void 0 ? void 0 : options.revenue) {
101
+ const { amount, currency } = options.revenue;
102
+ if (amount > 0 && amount <= 1000000000 && currency) {
103
+ payload.rv = { a: amount, c: currency };
104
+ }
105
+ }
106
+ if (config.hashRouting)
107
+ payload.h = true;
108
+ send(payload, options === null || options === void 0 ? void 0 : options.callback);
109
+ }
110
+ function identify(data) {
111
+ const payload = {
112
+ n: 'identify',
113
+ d: config.site,
114
+ u: location.href,
115
+ r: null,
116
+ s: getScreenSize(),
117
+ uid: data.userId,
118
+ };
119
+ send(payload);
120
+ }
121
+ return { trackPageview, track, identify };
122
+ }
123
+
124
+ let cleanup = null;
125
+ function startNavTracking(onNavigate, hashRouting) {
126
+ let lastPathname = location.pathname;
127
+ const handleNav = () => {
128
+ if (!hashRouting && location.pathname === lastPathname)
129
+ return;
130
+ lastPathname = location.pathname;
131
+ onNavigate();
132
+ };
133
+ // Initial pageview
134
+ onNavigate();
135
+ if (hashRouting) {
136
+ window.addEventListener('hashchange', handleNav);
137
+ cleanup = () => window.removeEventListener('hashchange', handleNav);
138
+ }
139
+ else {
140
+ const originalPushState = history.pushState;
141
+ history.pushState = function (...args) {
142
+ originalPushState.apply(this, args);
143
+ handleNav();
144
+ };
145
+ const onPopState = () => {
146
+ lastPathname = location.pathname;
147
+ onNavigate();
148
+ };
149
+ window.addEventListener('popstate', onPopState);
150
+ cleanup = () => {
151
+ history.pushState = originalPushState;
152
+ window.removeEventListener('popstate', onPopState);
153
+ };
154
+ }
155
+ // Handle bfcache (back-forward cache)
156
+ const onPageShow = (e) => {
157
+ if (e.persisted)
158
+ onNavigate();
159
+ };
160
+ window.addEventListener('pageshow', onPageShow);
161
+ const prevCleanup = cleanup;
162
+ cleanup = () => {
163
+ prevCleanup === null || prevCleanup === void 0 ? void 0 : prevCleanup();
164
+ window.removeEventListener('pageshow', onPageShow);
165
+ };
166
+ }
167
+ function stopNavTracking() {
168
+ cleanup === null || cleanup === void 0 ? void 0 : cleanup();
169
+ cleanup = null;
170
+ }
171
+
172
+ let startTime = 0;
173
+ let accumulatedTime = 0;
174
+ let isActive = false;
175
+ let maxScrollDepth = 0;
176
+ let scrollTicking = false;
177
+ let cachedPageHeight = 0;
178
+ function computePageHeight() {
179
+ const body = document.body || {};
180
+ const html = document.documentElement || {};
181
+ return Math.max(body.scrollHeight || 0, body.offsetHeight || 0, html.scrollHeight || 0, html.offsetHeight || 0);
182
+ }
183
+ function getCurrentScrollDepth() {
184
+ if (cachedPageHeight === 0)
185
+ return 0;
186
+ const viewportHeight = window.innerHeight || 0;
187
+ if (cachedPageHeight <= viewportHeight)
188
+ return 100;
189
+ const scrollTop = window.scrollY || document.documentElement.scrollTop || 0;
190
+ return Math.round(((scrollTop + viewportHeight) / cachedPageHeight) * 100);
191
+ }
192
+ function onResize() {
193
+ cachedPageHeight = computePageHeight();
194
+ }
195
+ function onVisibilityChange() {
196
+ if (document.visibilityState === 'hidden') {
197
+ if (isActive) {
198
+ accumulatedTime += Date.now() - startTime;
199
+ isActive = false;
200
+ }
201
+ }
202
+ else if (document.visibilityState === 'visible') {
203
+ startTime = Date.now();
204
+ isActive = true;
205
+ }
206
+ }
207
+ function onScroll() {
208
+ if (scrollTicking)
209
+ return;
210
+ scrollTicking = true;
211
+ requestAnimationFrame(() => {
212
+ const depth = getCurrentScrollDepth();
213
+ if (depth > maxScrollDepth) {
214
+ maxScrollDepth = depth;
215
+ }
216
+ scrollTicking = false;
217
+ });
218
+ }
219
+ function stopEngagement() {
220
+ document.removeEventListener('visibilitychange', onVisibilityChange);
221
+ document.removeEventListener('scroll', onScroll);
222
+ window.removeEventListener('resize', onResize);
223
+ }
224
+ function startEngagement() {
225
+ stopEngagement();
226
+ startTime = Date.now();
227
+ accumulatedTime = 0;
228
+ maxScrollDepth = 0;
229
+ isActive = true;
230
+ cachedPageHeight = computePageHeight();
231
+ document.addEventListener('visibilitychange', onVisibilityChange);
232
+ document.addEventListener('scroll', onScroll, { passive: true });
233
+ window.addEventListener('resize', onResize, { passive: true });
234
+ }
235
+ function resetEngagement() {
236
+ startTime = Date.now();
237
+ accumulatedTime = 0;
238
+ maxScrollDepth = 0;
239
+ isActive = true;
240
+ }
241
+ function getEngagementData() {
242
+ const currentActive = isActive ? Date.now() - startTime : 0;
243
+ return {
244
+ activeTime: accumulatedTime + currentActive,
245
+ maxScrollDepth,
246
+ };
247
+ }
248
+
249
+ // src/core/events.ts
250
+ const DEFAULT_FILE_TYPES = [
251
+ 'pdf', 'xlsx', 'docx', 'txt', 'rtf', 'csv', 'exe', 'key', 'pps', 'ppt',
252
+ 'pptx', '7z', 'pkg', 'rar', 'gz', 'zip', 'avi', 'mov', 'mp4', 'mpeg',
253
+ 'wmv', 'midi', 'mp3', 'wav', 'wma', 'dmg',
254
+ ];
255
+ let clickHandler = null;
256
+ let submitHandler = null;
257
+ function findLink(target) {
258
+ let el = target;
259
+ while (el && el.tagName !== 'A') {
260
+ el = el.parentElement;
261
+ }
262
+ return el;
263
+ }
264
+ function isOutbound(link) {
265
+ return link.host !== '' && link.host !== location.host;
266
+ }
267
+ function isFileDownload(href) {
268
+ var _a;
269
+ const ext = (_a = href.split('?')[0].split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
270
+ return ext ? DEFAULT_FILE_TYPES.includes(ext) : false;
271
+ }
272
+ function startEventTracking(trackFn, config) {
273
+ stopEventTracking();
274
+ clickHandler = (e) => {
275
+ const link = findLink(e.target);
276
+ if (!link || !link.href)
277
+ return;
278
+ const href = link.href;
279
+ const cleanUrl = href.split('?')[0];
280
+ if (config.fileDownloads && isFileDownload(href)) {
281
+ trackFn('File Download', { props: { url: cleanUrl } });
282
+ return;
283
+ }
284
+ if (config.outboundLinks && isOutbound(link)) {
285
+ trackFn('Outbound Link: Click', { props: { url: href } });
286
+ }
287
+ };
288
+ document.addEventListener('click', clickHandler);
289
+ if (config.formSubmissions) {
290
+ submitHandler = (e) => {
291
+ const form = e.target;
292
+ if (form.hasAttribute('novalidate') || form.checkValidity()) {
293
+ trackFn('Form Submission');
294
+ }
295
+ };
296
+ document.addEventListener('submit', submitHandler, true);
297
+ }
298
+ }
299
+ function stopEventTracking() {
300
+ if (clickHandler) {
301
+ document.removeEventListener('click', clickHandler);
302
+ clickHandler = null;
303
+ }
304
+ if (submitHandler) {
305
+ document.removeEventListener('submit', submitHandler, true);
306
+ submitHandler = null;
307
+ }
308
+ }
309
+
310
+ let scrollHandler = null;
311
+ function startScrollGoals(trackFn, targets, currentPath) {
312
+ const matchingTargets = targets.filter((t) => t.path === currentPath);
313
+ if (matchingTargets.length === 0)
314
+ return;
315
+ const fired = new Set();
316
+ let ticking = false;
317
+ scrollHandler = () => {
318
+ if (ticking)
319
+ return;
320
+ ticking = true;
321
+ requestAnimationFrame(() => {
322
+ var _a;
323
+ const pageHeight = Math.max(document.documentElement.scrollHeight || 0, ((_a = document.body) === null || _a === void 0 ? void 0 : _a.scrollHeight) || 0);
324
+ const viewportHeight = window.innerHeight || 0;
325
+ const scrollTop = window.scrollY || 0;
326
+ const scrollPercent = pageHeight <= viewportHeight
327
+ ? 100
328
+ : Math.round(((scrollTop + viewportHeight) / pageHeight) * 100);
329
+ for (const target of matchingTargets) {
330
+ if (!fired.has(target.goalName) && scrollPercent >= target.threshold) {
331
+ fired.add(target.goalName);
332
+ trackFn(target.goalName);
333
+ }
334
+ }
335
+ ticking = false;
336
+ });
337
+ };
338
+ document.addEventListener('scroll', scrollHandler, { passive: true });
339
+ }
340
+ function stopScrollGoals() {
341
+ if (scrollHandler) {
342
+ document.removeEventListener('scroll', scrollHandler);
343
+ scrollHandler = null;
344
+ }
345
+ }
346
+
347
+ /** Minimum active time (ms) before an engagement event is worth sending */
348
+ const MIN_ENGAGEMENT_MS = 1000;
349
+ function setupTracking(config, tracker) {
350
+ stopEngagement();
351
+ startEngagement();
352
+ let isFirstPageview = true;
353
+ let currentUrl = location.href;
354
+ startNavTracking(() => {
355
+ // Send engagement for previous page (skip on first pageview)
356
+ if (!isFirstPageview) {
357
+ const engagement = getEngagementData();
358
+ if (engagement.activeTime >= MIN_ENGAGEMENT_MS) {
359
+ sendEvent(config.endpoint, {
360
+ n: 'engagement',
361
+ d: config.site,
362
+ u: currentUrl,
363
+ r: null,
364
+ s: getScreenSize(),
365
+ sd: engagement.maxScrollDepth,
366
+ e: engagement.activeTime,
367
+ }, undefined, config.siteId);
368
+ }
369
+ resetEngagement();
370
+ }
371
+ isFirstPageview = false;
372
+ currentUrl = location.href;
373
+ tracker.trackPageview();
374
+ stopScrollGoals();
375
+ if (config.scrollTargets.length > 0) {
376
+ startScrollGoals((goalName) => tracker.track(goalName), config.scrollTargets, location.pathname);
377
+ }
378
+ }, config.hashRouting);
379
+ startEventTracking((name, options) => tracker.track(name, options), {
380
+ outboundLinks: config.outboundLinks,
381
+ fileDownloads: config.fileDownloads,
382
+ formSubmissions: config.formSubmissions,
383
+ });
384
+ // Send engagement on page unload
385
+ const onUnload = () => {
386
+ if (document.visibilityState === 'hidden') {
387
+ const engagement = getEngagementData();
388
+ if (engagement.activeTime >= MIN_ENGAGEMENT_MS) {
389
+ sendBeaconEvent(config.endpoint, {
390
+ n: 'engagement',
391
+ d: config.site,
392
+ u: currentUrl,
393
+ r: null,
394
+ s: getScreenSize(),
395
+ sd: engagement.maxScrollDepth,
396
+ e: engagement.activeTime,
397
+ }, config.siteId);
398
+ }
399
+ }
400
+ };
401
+ document.addEventListener('visibilitychange', onUnload);
402
+ // Return cleanup function
403
+ return () => {
404
+ stopNavTracking();
405
+ stopEngagement();
406
+ stopEventTracking();
407
+ stopScrollGoals();
408
+ document.removeEventListener('visibilitychange', onUnload);
409
+ };
410
+ }
411
+ function initTracking(config) {
412
+ const tracker = createTracker(config);
413
+ let cleanup = null;
414
+ if (document.visibilityState === 'prerender') {
415
+ const onVisible = () => {
416
+ if (document.visibilityState === 'visible') {
417
+ document.removeEventListener('visibilitychange', onVisible);
418
+ cleanup = setupTracking(config, tracker);
419
+ }
420
+ };
421
+ document.addEventListener('visibilitychange', onVisible);
422
+ return {
423
+ tracker,
424
+ destroy: () => {
425
+ document.removeEventListener('visibilitychange', onVisible);
426
+ cleanup === null || cleanup === void 0 ? void 0 : cleanup();
427
+ },
428
+ };
429
+ }
430
+ cleanup = setupTracking(config, tracker);
431
+ return {
432
+ tracker,
433
+ destroy: () => { cleanup === null || cleanup === void 0 ? void 0 : cleanup(); },
434
+ };
435
+ }
436
+
437
+ const CACHE_KEY = 'pageviews_config';
438
+ const CACHE_TTL = 1000 * 60 * 60; // 1 hour
439
+ let instance = null;
440
+ let initialized = false;
441
+ function getCachedConfig(siteId) {
442
+ try {
443
+ const raw = localStorage.getItem(`${CACHE_KEY}_${siteId}`);
444
+ if (!raw)
445
+ return null;
446
+ const { config, ts } = JSON.parse(raw);
447
+ if (Date.now() - ts > CACHE_TTL)
448
+ return null;
449
+ return config;
450
+ }
451
+ catch (_a) {
452
+ return null;
453
+ }
454
+ }
455
+ function setCachedConfig(siteId, config) {
456
+ try {
457
+ localStorage.setItem(`${CACHE_KEY}_${siteId}`, JSON.stringify({ config, ts: Date.now() }));
458
+ }
459
+ catch (_a) {
460
+ // localStorage unavailable
461
+ }
462
+ }
463
+ async function fetchConfig(endpoint, siteId) {
464
+ const res = await fetch(`${endpoint}/api/config/${siteId}`);
465
+ if (!res.ok)
466
+ throw new Error(`Config fetch failed: HTTP ${res.status}`);
467
+ return res.json();
468
+ }
469
+ function startTracking(config) {
470
+ instance = initTracking(config);
471
+ }
472
+ async function init(options) {
473
+ if (initialized)
474
+ throw new Error('pageviews.init() can only be called once');
475
+ initialized = true;
476
+ const ingestEndpoint = (options.endpoint || 'https://data.pageviews.ai') + '/api/event';
477
+ const cfgEndpoint = options.configEndpoint || 'https://app.pageviews.ai';
478
+ const cached = getCachedConfig(options.siteId);
479
+ if (cached) {
480
+ startTracking(Object.assign(Object.assign({}, cached), { siteId: options.siteId, endpoint: ingestEndpoint }));
481
+ // Refresh cache in background
482
+ fetchConfig(cfgEndpoint, options.siteId).then((fresh) => {
483
+ setCachedConfig(options.siteId, fresh);
484
+ }).catch(() => { });
485
+ return;
486
+ }
487
+ const configData = await fetchConfig(cfgEndpoint, options.siteId);
488
+ setCachedConfig(options.siteId, configData);
489
+ startTracking(Object.assign(Object.assign({}, configData), { siteId: options.siteId, endpoint: ingestEndpoint }));
490
+ }
491
+ function track(eventName, options) {
492
+ if (!instance)
493
+ throw new Error('pageviews.track() requires init() to be called first');
494
+ instance.tracker.track(eventName, options);
495
+ }
496
+ function identify(data) {
497
+ if (!instance)
498
+ throw new Error('pageviews.identify() requires init() to be called first');
499
+ instance.tracker.identify(data);
500
+ }
501
+ function destroy() {
502
+ if (!instance)
503
+ return;
504
+ instance.destroy();
505
+ instance = null;
506
+ initialized = false;
507
+ }
508
+
509
+ export { destroy, identify, init, track };
@@ -0,0 +1 @@
1
+ !function(){"use strict";function t(t,e,n,i){if("undefined"==typeof fetch)return;const o={"Content-Type":"text/plain"};i&&(o["X-Site-ID"]=i),fetch(t,{method:"POST",headers:o,keepalive:!0,body:JSON.stringify(e)}).then(t=>{n&&n({status:t.status})}).catch(t=>{n&&n({error:t})})}function e(){return`${window.innerWidth}x${window.innerHeight}`}let n=null,i=0,o=0,r=!1,s=0,c=!1,a=0;function l(){const t=document.body||{},e=document.documentElement||{};return Math.max(t.scrollHeight||0,t.offsetHeight||0,e.scrollHeight||0,e.offsetHeight||0)}function u(){a=l()}function d(){"hidden"===document.visibilityState?r&&(o+=Date.now()-i,r=!1):"visible"===document.visibilityState&&(i=Date.now(),r=!0)}function v(){c||(c=!0,requestAnimationFrame(()=>{const t=function(){if(0===a)return 0;const t=window.innerHeight||0;if(a<=t)return 100;const e=window.scrollY||document.documentElement.scrollTop||0;return Math.round((e+t)/a*100)}();t>s&&(s=t),c=!1}))}function m(){document.removeEventListener("visibilitychange",d),document.removeEventListener("scroll",v),window.removeEventListener("resize",u)}function f(){const t=r?Date.now()-i:0;return{activeTime:o+t,maxScrollDepth:s}}const h=["pdf","xlsx","docx","txt","rtf","csv","exe","key","pps","ppt","pptx","7z","pkg","rar","gz","zip","avi","mov","mp4","mpeg","wmv","midi","mp3","wav","wma","dmg"];let p=null,g=null;function w(){p&&(document.removeEventListener("click",p),p=null),g&&(document.removeEventListener("submit",g,!0),g=null)}let y=null;function b(){y&&(document.removeEventListener("scroll",y),y=null)}function E(c,E){m(),m(),i=Date.now(),o=0,s=0,r=!0,a=l(),document.addEventListener("visibilitychange",d),document.addEventListener("scroll",v,{passive:!0}),window.addEventListener("resize",u,{passive:!0});let L=!0,S=location.href;!function(t,e){let i=location.pathname;const o=()=>{(e||location.pathname!==i)&&(i=location.pathname,t())};if(t(),e)window.addEventListener("hashchange",o),n=()=>window.removeEventListener("hashchange",o);else{const e=history.pushState;history.pushState=function(...t){e.apply(this,t),o()};const r=()=>{i=location.pathname,t()};window.addEventListener("popstate",r),n=()=>{history.pushState=e,window.removeEventListener("popstate",r)}}const r=e=>{e.persisted&&t()};window.addEventListener("pageshow",r);const s=n;n=()=>{null==s||s(),window.removeEventListener("pageshow",r)}}(()=>{if(!L){const n=f();n.activeTime>=1e3&&t(c.endpoint,{n:"engagement",d:c.site,u:S,r:null,s:e(),sd:n.maxScrollDepth,e:n.activeTime},void 0,c.siteId),i=Date.now(),o=0,s=0,r=!0}L=!1,S=location.href,E.trackPageview(),b(),c.scrollTargets.length>0&&function(t,e,n){const i=e.filter(t=>t.path===n);if(0===i.length)return;const o=new Set;let r=!1;y=()=>{r||(r=!0,requestAnimationFrame(()=>{var e;const n=Math.max(document.documentElement.scrollHeight||0,(null===(e=document.body)||void 0===e?void 0:e.scrollHeight)||0),s=window.innerHeight||0,c=window.scrollY||0,a=n<=s?100:Math.round((c+s)/n*100);for(const e of i)!o.has(e.goalName)&&a>=e.threshold&&(o.add(e.goalName),t(e.goalName));r=!1}))},document.addEventListener("scroll",y,{passive:!0})}(t=>E.track(t),c.scrollTargets,location.pathname)},c.hashRouting),function(t,e){w(),p=n=>{const i=function(t){let e=t;for(;e&&"A"!==e.tagName;)e=e.parentElement;return e}(n.target);if(!i||!i.href)return;const o=i.href,r=o.split("?")[0];e.fileDownloads&&function(t){var e;const n=null===(e=t.split("?")[0].split(".").pop())||void 0===e?void 0:e.toLowerCase();return!!n&&h.includes(n)}(o)?t("File Download",{props:{url:r}}):e.outboundLinks&&function(t){return""!==t.host&&t.host!==location.host}(i)&&t("Outbound Link: Click",{props:{url:o}})},document.addEventListener("click",p),e.formSubmissions&&(g=e=>{const n=e.target;(n.hasAttribute("novalidate")||n.checkValidity())&&t("Form Submission")},document.addEventListener("submit",g,!0))}((t,e)=>E.track(t,e),{outboundLinks:c.outboundLinks,fileDownloads:c.fileDownloads,formSubmissions:c.formSubmissions});const k=()=>{if("hidden"===document.visibilityState){const n=f();n.activeTime>=1e3&&function(e,n,i){const o=JSON.stringify(n);if("undefined"!=typeof navigator&&navigator.sendBeacon){const t=new Blob([o],{type:"text/plain"});if(navigator.sendBeacon(e,t))return}t(e,n,void 0,i)}(c.endpoint,{n:"engagement",d:c.site,u:S,r:null,s:e(),sd:n.maxScrollDepth,e:n.activeTime},c.siteId)}};return document.addEventListener("visibilitychange",k),()=>{null==n||n(),n=null,m(),w(),b(),document.removeEventListener("visibilitychange",k)}}!function(){var n;const i=document.currentScript;if(!i)return;const o="undefined"!=typeof __PAGEVIEWS_CONFIG__?__PAGEVIEWS_CONFIG__:null;if(!o)return;const r=i.getAttribute("data-api")||void 0,s=function(t,e){return e||"https://data.pageviews.ai/api/event"}(i.src,r),c=Object.assign(Object.assign({},o),{endpoint:s}),{tracker:a}=function(n){const i=function(n){const i="localhost"===(o=location.hostname)||/^127(\.[0-9]+){0,2}\.[0-9]+$/.test(o)||"[::1]"===o||function(){try{return"true"===window.localStorage.getItem("pageviews_ignore")}catch(t){return!1}}()||function(){const t=window,e=navigator;return!!(t._phantom||t.__nightmare||e.webdriver||t.Cypress||t.__puppeteer_evaluation_script__||t.__playwright)}();var o;function r(e,o){i?null==o||o():t(n.endpoint,e,o,n.siteId)}return{trackPageview:function(t){const i={n:"pageview",d:n.site,u:t||location.href,r:document.referrer||null,s:e()};n.hashRouting&&(i.h=!0),r(i)},track:function(t,i){const o={n:t,d:n.site,u:(null==i?void 0:i.url)||location.href,r:document.referrer||null,s:e()};if(null==i?void 0:i.props){const t={};for(const[e,n]of Object.entries(i.props))t[e]=String(n);o.p=t}if(null==i?void 0:i.revenue){const{amount:t,currency:e}=i.revenue;t>0&&t<=1e9&&e&&(o.rv={a:t,c:e})}n.hashRouting&&(o.h=!0),r(o,null==i?void 0:i.callback)},identify:function(t){r({n:"identify",d:n.site,u:location.href,r:null,s:e(),uid:t.userId})}}}(n);let o=null;if("prerender"===document.visibilityState){const t=()=>{"visible"===document.visibilityState&&(document.removeEventListener("visibilitychange",t),o=E(n,i))};return document.addEventListener("visibilitychange",t),{tracker:i,destroy:()=>{document.removeEventListener("visibilitychange",t),null==o||o()}}}return o=E(n,i),{tracker:i,destroy:()=>{null==o||o()}}}(c),l=i.getAttribute("data-user-id");l&&a.identify({userId:l});const u=(t,...e)=>{"track"===t?a.track(e[0],e[1]):"identify"===t&&a.identify(e[0])},d=null===(n=window.pageviews)||void 0===n?void 0:n.q;Array.isArray(d)&&d.forEach(t=>u(t[0],...t.slice(1))),window.pageviews=u}()}();
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@pageviews/tracker",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight, cookie-free analytics tracker for PageViews.ai",
5
+ "author": "Pageviews <hello@pageviews.ai>",
6
+ "homepage": "https://pageviews.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/pageviews-ai/tracker"
10
+ },
11
+ "keywords": [
12
+ "analytics",
13
+ "pageviews",
14
+ "tracking",
15
+ "cookie-free",
16
+ "privacy",
17
+ "gdpr"
18
+ ],
19
+ "main": "dist/module/index.cjs",
20
+ "module": "dist/module/index.mjs",
21
+ "types": "dist/module/index.d.ts",
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/module/index.d.ts",
26
+ "import": "./dist/module/index.mjs",
27
+ "require": "./dist/module/index.cjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "rollup -c",
35
+ "prepublishOnly": "npm run test && npm run typecheck && npm run build",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "license": "MIT",
41
+ "devDependencies": {
42
+ "@rollup/plugin-terser": "^1.0.0",
43
+ "@rollup/plugin-typescript": "^12.3.0",
44
+ "@vitest/coverage-v8": "^4.0.18",
45
+ "jsdom": "^28.1.0",
46
+ "rollup": "^4.59.0",
47
+ "rollup-plugin-dts": "^6.3.0",
48
+ "tslib": "^2.8.1",
49
+ "typescript": "^5.9.3",
50
+ "vitest": "^4.0.18"
51
+ }
52
+ }