fimo-analytics 2.7.0-experimental.1
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 +242 -0
- package/components/Analytics.astro +33 -0
- package/components/Analytics.svelte +9 -0
- package/components/SvelteKit.svelte +16 -0
- package/components/svelte.d.ts +5 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +180 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.js +23 -0
- package/dist/next.d.ts +2 -0
- package/dist/next.js +38 -0
- package/dist/nuxt-plugin.mjs +23 -0
- package/dist/nuxt.d.ts +12 -0
- package/dist/nuxt.js +10 -0
- package/dist/react-router.d.ts +4 -0
- package/dist/react-router.js +13 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +10 -0
- package/dist/vue.d.ts +21 -0
- package/dist/vue.js +21 -0
- package/package.json +120 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# fimo-analytics
|
|
2
|
+
|
|
3
|
+
Fimo pageviews for existing websites. One framework-free tracker, with optional
|
|
4
|
+
framework adapters. No framework peer is needed for JavaScript or plain HTML.
|
|
5
|
+
|
|
6
|
+
This source introduces the package. Installation from npm requires a release
|
|
7
|
+
that contains it; source presence is not evidence of publication. The package
|
|
8
|
+
releases with the Fimo package group, but the standalone API has no dependency
|
|
9
|
+
on the Fimo CLI or a hosting adapter.
|
|
10
|
+
|
|
11
|
+
## Connection
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add fimo-analytics
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Configure the complete ingestion URL, including `/e/p`, for your project and
|
|
18
|
+
branch. Use the tenant API base URL supplied by your Fimo connection and append
|
|
19
|
+
`/e/p`; do not substitute your site's public URL or guess a tenant hostname.
|
|
20
|
+
An origin-relative endpoint such as `/analytics-proxy/e/p` requires a proxy you
|
|
21
|
+
already configured. There is no endpoint-discovery API in this package.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { init } from 'fimo-analytics';
|
|
25
|
+
|
|
26
|
+
const analytics = init({
|
|
27
|
+
endpoint: 'https://YOUR-TENANT-API-HOST/e/p',
|
|
28
|
+
enabled: true,
|
|
29
|
+
});
|
|
30
|
+
// When the application that owns the tracker is disposed:
|
|
31
|
+
analytics.destroy();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Standalone tracking is enabled by default. Pass `enabled: false` in development
|
|
35
|
+
and preview builds. Every send also checks for `bl_preview_token` in the current
|
|
36
|
+
URL. Disabled and server-side initialization is inert, with no storage or
|
|
37
|
+
network effects. Invalid active configuration throws before sending.
|
|
38
|
+
|
|
39
|
+
## Framework adapters
|
|
40
|
+
|
|
41
|
+
Install only the peers for your framework. Every component accepts `endpoint`
|
|
42
|
+
and `enabled`, renders no visible UI, and cleans up when removed. Put it in a
|
|
43
|
+
persistent application root. Generic React, Svelte and Vue observe browser
|
|
44
|
+
history. Framework router adapters emit after committed navigation.
|
|
45
|
+
|
|
46
|
+
### React and React Router
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Analytics } from 'fimo-analytics/react';
|
|
50
|
+
// React Router: import { Analytics } from 'fimo-analytics/react-router';
|
|
51
|
+
|
|
52
|
+
<Analytics endpoint={endpoint} enabled={isProduction} />;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The React Router component goes inside the router. It also accepts an optional
|
|
56
|
+
`pageName`. It reads the router location, including virtual HashRouter paths.
|
|
57
|
+
|
|
58
|
+
The upgraded `fimo-react-router` still exposes `Pageview({ routes })` and
|
|
59
|
+
`FimoProviders` still mounts it. They retain `VITE_API_URL`,
|
|
60
|
+
`VITE_ENABLE_ANALYTICS=true`, route-name matching and preview guards. No extra
|
|
61
|
+
component is needed when that integration already tracks your application.
|
|
62
|
+
|
|
63
|
+
### Next.js App and Pages routers
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { Analytics } from 'fimo-analytics/next';
|
|
67
|
+
|
|
68
|
+
// App Router: include this in app/layout.tsx, inside <body>.
|
|
69
|
+
// Pages Router: include it in pages/_app.tsx beside <Component {...pageProps} />.
|
|
70
|
+
<Analytics endpoint={endpoint} enabled={isProduction} />;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The adapter owns the client boundary and Suspense boundary for static App
|
|
74
|
+
Router pages. Pages Router uses successful route-completion events. Both track
|
|
75
|
+
query-only navigation; prefetching does not create pageviews.
|
|
76
|
+
|
|
77
|
+
For App Router, keep one installation in the root layout. It can commit an
|
|
78
|
+
intermediate URL before a server redirect reaches its destination. Both URLs can
|
|
79
|
+
produce pageviews. Destination-only counting is not guaranteed for these redirects.
|
|
80
|
+
Pages Router uses a different completion signal and does not share this limitation
|
|
81
|
+
in the tested redirect journey.
|
|
82
|
+
|
|
83
|
+
### Astro
|
|
84
|
+
|
|
85
|
+
```astro
|
|
86
|
+
---
|
|
87
|
+
import Analytics from 'fimo-analytics/astro';
|
|
88
|
+
const endpoint = 'https://YOUR-TENANT-API-HOST/e/p';
|
|
89
|
+
---
|
|
90
|
+
<html>
|
|
91
|
+
<body>
|
|
92
|
+
<slot />
|
|
93
|
+
<Analytics endpoint={endpoint} enabled={import.meta.env.PROD} />
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use your shared layout. No React island or `client:*` directive is needed.
|
|
99
|
+
Ordinary documents and Astro ClientRouter navigation are supported.
|
|
100
|
+
|
|
101
|
+
### Svelte and SvelteKit
|
|
102
|
+
|
|
103
|
+
```svelte
|
|
104
|
+
<script>
|
|
105
|
+
import Analytics from 'fimo-analytics/svelte';
|
|
106
|
+
// SvelteKit: import Analytics from 'fimo-analytics/sveltekit';
|
|
107
|
+
const endpoint = 'https://YOUR-TENANT-API-HOST/e/p';
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<Analytics {endpoint} enabled={import.meta.env.PROD} />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use the root component, or `src/routes/+layout.svelte` in SvelteKit.
|
|
114
|
+
The SvelteKit adapter uses `afterNavigate` and does not emit during SSR.
|
|
115
|
+
|
|
116
|
+
### Vue
|
|
117
|
+
|
|
118
|
+
```vue
|
|
119
|
+
<script setup>
|
|
120
|
+
import { Analytics } from 'fimo-analytics/vue';
|
|
121
|
+
const endpoint = 'https://YOUR-TENANT-API-HOST/e/p';
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template><Analytics :endpoint="endpoint" /></template>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For Vue Router, use the manual core in your root component instead of mounting
|
|
128
|
+
the generic history component. Wait for the initial router navigation before
|
|
129
|
+
mounting the application, as in `await router.isReady()`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { onMounted, onUnmounted } from 'vue';
|
|
133
|
+
import { useRouter } from 'vue-router';
|
|
134
|
+
import { init, type Analytics } from 'fimo-analytics';
|
|
135
|
+
|
|
136
|
+
const router = useRouter();
|
|
137
|
+
let analytics: Analytics | undefined;
|
|
138
|
+
let remove: (() => void) | undefined;
|
|
139
|
+
onMounted(() => {
|
|
140
|
+
analytics = init({ endpoint, routing: 'manual', enabled: isProduction });
|
|
141
|
+
analytics.pageview();
|
|
142
|
+
remove = router.afterEach((_to, _from, failure) => {
|
|
143
|
+
if (!failure) analytics?.pageview();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
onUnmounted(() => {
|
|
147
|
+
remove?.();
|
|
148
|
+
analytics?.destroy();
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Nuxt
|
|
153
|
+
|
|
154
|
+
The Nuxt module also needs the optional `@nuxt/kit` peer.
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
pnpm add -D @nuxt/kit
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// nuxt.config.ts
|
|
162
|
+
export default defineNuxtConfig({
|
|
163
|
+
modules: ['fimo-analytics/nuxt'],
|
|
164
|
+
analytics: {
|
|
165
|
+
endpoint: 'https://YOUR-TENANT-API-HOST/e/p',
|
|
166
|
+
enabled: process.env.NODE_ENV === 'production',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The module installs a client-only plugin, tracks successful router navigation
|
|
172
|
+
including reused pages and query changes, and cleans up during application
|
|
173
|
+
disposal. Its configuration is public browser data. Never put secrets in it.
|
|
174
|
+
|
|
175
|
+
### Plain HTML
|
|
176
|
+
|
|
177
|
+
Copy `node_modules/fimo-analytics/dist/browser.js` to your site's static assets,
|
|
178
|
+
then reference that file. Keep the copied asset pinned to the installed package.
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<script defer src="/fimo-analytics.js" data-endpoint="https://YOUR-TENANT-API-HOST/e/p" data-enabled="true"></script>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is a classic script, not a module script. It uses its own `data-endpoint`
|
|
185
|
+
and the same core. Use `data-enabled="false"` for disabled builds. No CDN or
|
|
186
|
+
server-response injection is configured automatically.
|
|
187
|
+
|
|
188
|
+
## Manual navigation and lifetime
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const analytics = init({ endpoint, routing: 'manual' });
|
|
192
|
+
// Call only after the application's router commits navigation.
|
|
193
|
+
analytics.pageview({ path: '/pricing?plan=team', pageName: 'Pricing' });
|
|
194
|
+
analytics.destroy();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
History mode is the default. It sends initially and observes `pushState`,
|
|
198
|
+
`replaceState` and `popstate`. Manual mode sends only when called. Default paths
|
|
199
|
+
include pathname and query; fragments are excluded. Default page names use the
|
|
200
|
+
pathname without a trailing slash. Explicit virtual paths support hash routers.
|
|
201
|
+
|
|
202
|
+
Consecutive same-path sends collapse across mounts and installed copies for the
|
|
203
|
+
same endpoint. `/a → /b → /a` counts three. Different active routing modes for
|
|
204
|
+
one endpoint throw. `destroy()` releases one handle, is safe to repeat, and
|
|
205
|
+
makes that handle inert. Last-handle disposal removes listeners and owned
|
|
206
|
+
history wrappers. Same-path remounts remain deduplicated for the document's
|
|
207
|
+
lifetime. Reloads and BFCache restores count as fresh visits.
|
|
208
|
+
|
|
209
|
+
## Compatibility and delivery
|
|
210
|
+
|
|
211
|
+
The package preserves `fimo.analytics.visitor-id` in localStorage and
|
|
212
|
+
`fimo.analytics.session-id` in sessionStorage, without an inactivity timeout.
|
|
213
|
+
Blocked storage falls back to stable IDs for this browser document, shared
|
|
214
|
+
across copies. A later full page load may receive new IDs when storage is blocked.
|
|
215
|
+
|
|
216
|
+
Requests use the existing URL-encoded `p,n,v,s,r,u` fields, document referrer,
|
|
217
|
+
user agent, keepalive, CORS, and omitted credentials. Query parameters remain
|
|
218
|
+
available for server-side campaign attribution. Project and branch are selected
|
|
219
|
+
by the configured tenant endpoint. This package adds no custom events or schema.
|
|
220
|
+
Delivery is best effort, with no retry after a network error, 429 or 202. A
|
|
221
|
+
successful HTTP response is not proof that the destination stored the event.
|
|
222
|
+
|
|
223
|
+
Earlier immutable applications keep their existing tracker and reporting.
|
|
224
|
+
Older Fimo releases do not gain these exports or instrument missing trackers
|
|
225
|
+
merely because this package is released. Never add a standalone tracker beside
|
|
226
|
+
an unmodified legacy sender; the legacy sender cannot join the shared registry.
|
|
227
|
+
Older template-based projects keep their copied sender until a supported
|
|
228
|
+
upgrade changes their dependencies. The package does not add or remove endpoint
|
|
229
|
+
aliases, alter historical rows, or change retention.
|
|
230
|
+
|
|
231
|
+
## Version qualification and validation
|
|
232
|
+
|
|
233
|
+
The fixture matrix pins React 19.2.4, React Router 7.18.2, Next.js 16.3.3,
|
|
234
|
+
Astro 7.2.2, Svelte 5.56.9, SvelteKit 2.70.3, Vue 3.5.42, Vue Router 5.3.1,
|
|
235
|
+
and Nuxt 4.5.2. Peer ranges express intended compatibility; they do not mean
|
|
236
|
+
that every old release in those ranges has been browser-tested. In particular,
|
|
237
|
+
React 18, React Router 8 and Next 15 require their own acceptance evidence before
|
|
238
|
+
claiming tested support. Install a release that actually contains these exports.
|
|
239
|
+
|
|
240
|
+
See [the fixture instructions](test/README.md) in the source checkout for exact
|
|
241
|
+
build, tarball smoke, bundle measurement and browser commands. Nothing here
|
|
242
|
+
asserts that the package has been npm-published or verified on a customer site.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AnalyticsProps } from '../dist/index.js';
|
|
3
|
+
interface Props extends AnalyticsProps {}
|
|
4
|
+
const { endpoint, enabled = true } = Astro.props;
|
|
5
|
+
---
|
|
6
|
+
<fimo-analytics data-endpoint={endpoint} data-enabled={String(enabled)} hidden></fimo-analytics>
|
|
7
|
+
<script>
|
|
8
|
+
import { type Analytics } from '../dist/index.js';
|
|
9
|
+
import { initOrWarn } from '../dist/internal.js';
|
|
10
|
+
if (!customElements.get('fimo-analytics')) {
|
|
11
|
+
customElements.define('fimo-analytics', class extends HTMLElement {
|
|
12
|
+
tracker?: Analytics;
|
|
13
|
+
commit = () => {
|
|
14
|
+
document.removeEventListener('astro:page-load', this.commit);
|
|
15
|
+
this.tracker?.pageview();
|
|
16
|
+
};
|
|
17
|
+
connectedCallback() {
|
|
18
|
+
this.tracker = initOrWarn({ endpoint: this.dataset.endpoint!, enabled: this.dataset.enabled !== 'false', routing: 'manual' });
|
|
19
|
+
// FIMO-2558: after-swap has the committed URL. Page-load can be delayed by
|
|
20
|
+
// scripts until another navigation commits, so use it only for initial mounting.
|
|
21
|
+
document.addEventListener('astro:after-swap', this.commit);
|
|
22
|
+
document.addEventListener('astro:page-load', this.commit, { once: true });
|
|
23
|
+
// Static pages have no router lifecycle.
|
|
24
|
+
if (!document.querySelector('meta[name="astro-view-transitions-enabled"]')) this.commit();
|
|
25
|
+
}
|
|
26
|
+
disconnectedCallback() {
|
|
27
|
+
document.removeEventListener('astro:after-swap', this.commit);
|
|
28
|
+
document.removeEventListener('astro:page-load', this.commit);
|
|
29
|
+
this.tracker?.destroy();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type AnalyticsProps } from '../dist/index.js';
|
|
3
|
+
import { initOrWarn } from '../dist/internal.js';
|
|
4
|
+
let { endpoint, enabled = true }: AnalyticsProps = $props();
|
|
5
|
+
$effect(() => {
|
|
6
|
+
const tracker = initOrWarn({ endpoint, enabled });
|
|
7
|
+
return () => tracker.destroy();
|
|
8
|
+
});
|
|
9
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
|
|
4
|
+
import { type AnalyticsProps } from '../dist/index.js';
|
|
5
|
+
import { initOrWarn } from '../dist/internal.js';
|
|
6
|
+
let { endpoint, enabled = true }: AnalyticsProps = $props();
|
|
7
|
+
$effect(() => {
|
|
8
|
+
const tracker = initOrWarn({ endpoint, enabled, routing: 'manual' });
|
|
9
|
+
$effect(() => {
|
|
10
|
+
// FIMO-2558: Shallow back/forward updates page.url without running afterNavigate.
|
|
11
|
+
const url = page.url;
|
|
12
|
+
tracker.pageview({ path: url.pathname + url.search });
|
|
13
|
+
});
|
|
14
|
+
return () => tracker.destroy();
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{var g=Symbol.for("fimo.analytics.registry.v1"),m=()=>({pageview(){},destroy(){}}),P=()=>window.location.pathname+window.location.search,S=()=>new URLSearchParams(window.location.search).has("bl_preview_token");function b(t){if(typeof t!="string"||!t||t.trim()!==t||/[\\\s]/.test(t)||!(t.startsWith("/")&&!t.startsWith("//")||/^https?:\/\//i.test(t)))throw new TypeError("fimo-analytics requires an HTTP(S) endpoint or an origin-relative URL");let n=new URL(t,window.location.origin);if(!/^https?:$/.test(n.protocol)||n.username||n.password||n.hash)throw new TypeError("fimo-analytics endpoint must not contain credentials or a fragment");return n.href}var p={p:2048,n:512,r:2048,u:1024},w=(t,n)=>{if(t.length<=n)return t;let s=t.slice(0,n),a=s.charCodeAt(n-1);return a>=55296&&a<=56319?s.slice(0,-1):s},A=t=>t.length>=8&&t.length<=128;function y(t,n){let s;try{s=window[t];let e=s.getItem(n);if(e&&A(e))return e}catch{}let a=window.crypto?.randomUUID?.()??`${Date.now().toString(36)}-${Math.random().toString(36).slice(2,10)}`;try{s?.setItem(n,a)}catch{}return a}function E(t){let n=!0,s=()=>{if(n)for(let i of t.destinations.values())i.routing==="history"&&i.handles.values().next().value?.pageview()},a=["pushState","replaceState"].map(i=>{let o=window.history[i],r=function(...c){let d=o.apply(this,c);return s(),d};return window.history[i]=r,()=>{window.history[i]===r&&(window.history[i]=o)}}),e=i=>{if(i.persisted)for(let o of t.destinations.values()){let r=o.handles.values().next().value;!r||!o.lastPage||(o.lastPath=void 0,r.pageview(o.routing==="manual"?o.lastPage:void 0))}};return window.addEventListener("popstate",s),window.addEventListener("pageshow",e),()=>{n=!1,a.forEach(i=>i()),window.removeEventListener("popstate",s),window.removeEventListener("pageshow",e)}}function v(t){if(typeof window>"u"||typeof document>"u"||t.enabled===!1)return m();let n=b(t.endpoint),s=t.routing??"history";if(s!=="history"&&s!=="manual")throw new TypeError("Invalid analytics routing mode");let a=window,e=a[g]??(a[g]={destinations:new Map}),i=e.destinations.get(n);if(i?.handles.size&&i.routing!==s)throw new TypeError("Conflicting active analytics routing modes for the same endpoint");i||(i={routing:s,handles:new Set},e.destinations.set(n,i)),i.routing=s;let o=i,r=!1,c={pageview(d={}){if(r||S())return;let l=w((d.path??P()).split("#")[0]||"/",p.p);if(o.lastPath===l)return;let h=d.pageName??(l.split("?")[0].replace(/\/+$/,"")||"/");o.lastPath=l,o.lastPage={path:l,pageName:h},e.visitor??(e.visitor=y("localStorage","fimo.analytics.visitor-id")),e.session??(e.session=y("sessionStorage","fimo.analytics.session-id"));let u=new URLSearchParams({p:l,n:w(h,p.n),v:e.visitor,s:e.session});document.referrer&&u.set("r",w(document.referrer,p.r)),window.navigator.userAgent&&u.set("u",w(window.navigator.userAgent,p.u));try{window.fetch(n,{method:"POST",body:u,keepalive:!0,mode:"cors",credentials:"omit"}).catch(()=>{})}catch{}},destroy(){r||(r=!0,o.handles.delete(c),[...e.destinations.values()].some(d=>d.handles.size)||(e.stop?.(),e.stop=void 0))}};return o.handles.add(c),e.stop??(e.stop=E(e)),s==="history"&&c.pageview(),c}var f=document.currentScript;if(f)try{v({endpoint:f.dataset.endpoint,enabled:f.dataset.enabled!=="false"})}catch(t){console.warn("[fimo-analytics]",t)}})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AnalyticsOptions = {
|
|
2
|
+
/** Complete tenant ingestion URL, including /e/p, or an explicit origin-relative proxy. */
|
|
3
|
+
endpoint: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
routing?: 'history' | 'manual';
|
|
6
|
+
};
|
|
7
|
+
export type AnalyticsProps = Pick<AnalyticsOptions, 'endpoint' | 'enabled'>;
|
|
8
|
+
export type Pageview = {
|
|
9
|
+
path?: string;
|
|
10
|
+
pageName?: string;
|
|
11
|
+
};
|
|
12
|
+
export type Analytics = {
|
|
13
|
+
pageview(page?: Pageview): void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
};
|
|
16
|
+
/** Initialize once at the application root; destroy the handle when its owner unmounts. */
|
|
17
|
+
export declare function init(options: AnalyticsOptions): Analytics;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// FIMO-2558: Share identity and consecutive-path dedupe across installed copies and HMR.
|
|
2
|
+
const registryKey = Symbol.for('fimo.analytics.registry.v1');
|
|
3
|
+
const inert = () => ({ pageview() { }, destroy() { } });
|
|
4
|
+
const browserPath = () => window.location.pathname + window.location.search;
|
|
5
|
+
const preview = () => new URLSearchParams(window.location.search).has('bl_preview_token');
|
|
6
|
+
function endpointUrl(endpoint) {
|
|
7
|
+
if (typeof endpoint !== 'string' ||
|
|
8
|
+
!endpoint ||
|
|
9
|
+
endpoint.trim() !== endpoint ||
|
|
10
|
+
/[\\\s]/.test(endpoint) ||
|
|
11
|
+
!((endpoint.startsWith('/') && !endpoint.startsWith('//')) || /^https?:\/\//i.test(endpoint))) {
|
|
12
|
+
throw new TypeError('fimo-analytics requires an HTTP(S) endpoint or an origin-relative URL');
|
|
13
|
+
}
|
|
14
|
+
const url = new URL(endpoint, window.location.origin);
|
|
15
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.hash) {
|
|
16
|
+
throw new TypeError('fimo-analytics endpoint must not contain credentials or a fragment');
|
|
17
|
+
}
|
|
18
|
+
return url.href;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ingestion validates the whole payload and rejects it outright, so a single
|
|
22
|
+
* over-long field drops the pageview rather than trimming it. A long referrer
|
|
23
|
+
* is the realistic case: some campaign and redirector URLs exceed 2048 bytes.
|
|
24
|
+
* Truncating records a slightly lossy visit instead of losing it entirely.
|
|
25
|
+
*/
|
|
26
|
+
const CAPS = { p: 2048, n: 512, r: 2048, u: 1024 };
|
|
27
|
+
const clamp = (value, max) => {
|
|
28
|
+
if (value.length <= max)
|
|
29
|
+
return value;
|
|
30
|
+
const cut = value.slice(0, max);
|
|
31
|
+
// Cutting between the halves of a surrogate pair leaves a lone surrogate,
|
|
32
|
+
// which the form encoder replaces with U+FFFD — a stored character the
|
|
33
|
+
// visitor never requested. Drop the orphan instead.
|
|
34
|
+
const last = cut.charCodeAt(max - 1);
|
|
35
|
+
return last >= 0xd800 && last <= 0xdbff ? cut.slice(0, -1) : cut;
|
|
36
|
+
};
|
|
37
|
+
/** Ingestion also bounds the identifiers; a stored value outside them would 400 every send. */
|
|
38
|
+
const usableId = (value) => value.length >= 8 && value.length <= 128;
|
|
39
|
+
function storedId(kind, key) {
|
|
40
|
+
let storage;
|
|
41
|
+
try {
|
|
42
|
+
storage = window[kind];
|
|
43
|
+
const existing = storage.getItem(key);
|
|
44
|
+
if (existing && usableId(existing))
|
|
45
|
+
return existing;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* Storage may be blocked, including the property getter. */
|
|
49
|
+
}
|
|
50
|
+
const id = window.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
51
|
+
try {
|
|
52
|
+
storage?.setItem(key, id);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* Keep the document's in-memory ID. */
|
|
56
|
+
}
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
function observe(registry) {
|
|
60
|
+
let active = true;
|
|
61
|
+
const navigate = () => {
|
|
62
|
+
if (!active)
|
|
63
|
+
return;
|
|
64
|
+
for (const destination of registry.destinations.values()) {
|
|
65
|
+
if (destination.routing === 'history')
|
|
66
|
+
destination.handles.values().next().value?.pageview();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const restores = ['pushState', 'replaceState'].map((method) => {
|
|
70
|
+
const original = window.history[method];
|
|
71
|
+
const wrapper = function (...args) {
|
|
72
|
+
const result = original.apply(this, args);
|
|
73
|
+
navigate();
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
window.history[method] = wrapper;
|
|
77
|
+
return () => {
|
|
78
|
+
// Do not overwrite a router or library that wrapped us after initialization.
|
|
79
|
+
if (window.history[method] === wrapper)
|
|
80
|
+
window.history[method] = original;
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const restorePage = (event) => {
|
|
84
|
+
if (!event.persisted)
|
|
85
|
+
return;
|
|
86
|
+
for (const destination of registry.destinations.values()) {
|
|
87
|
+
const handle = destination.handles.values().next().value;
|
|
88
|
+
if (!handle || !destination.lastPage)
|
|
89
|
+
continue;
|
|
90
|
+
destination.lastPath = undefined;
|
|
91
|
+
handle.pageview(destination.routing === 'manual' ? destination.lastPage : undefined);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
window.addEventListener('popstate', navigate);
|
|
95
|
+
window.addEventListener('pageshow', restorePage);
|
|
96
|
+
return () => {
|
|
97
|
+
active = false;
|
|
98
|
+
restores.forEach((restore) => restore());
|
|
99
|
+
window.removeEventListener('popstate', navigate);
|
|
100
|
+
window.removeEventListener('pageshow', restorePage);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Initialize once at the application root; destroy the handle when its owner unmounts. */
|
|
104
|
+
export function init(options) {
|
|
105
|
+
if (typeof window === 'undefined' || typeof document === 'undefined' || options.enabled === false)
|
|
106
|
+
return inert();
|
|
107
|
+
const endpoint = endpointUrl(options.endpoint);
|
|
108
|
+
const routing = options.routing ?? 'history';
|
|
109
|
+
if (routing !== 'history' && routing !== 'manual')
|
|
110
|
+
throw new TypeError('Invalid analytics routing mode');
|
|
111
|
+
const shared = window;
|
|
112
|
+
const registry = (shared[registryKey] ?? (shared[registryKey] = { destinations: new Map() }));
|
|
113
|
+
let destination = registry.destinations.get(endpoint);
|
|
114
|
+
if (destination?.handles.size && destination.routing !== routing) {
|
|
115
|
+
throw new TypeError('Conflicting active analytics routing modes for the same endpoint');
|
|
116
|
+
}
|
|
117
|
+
if (!destination) {
|
|
118
|
+
destination = { routing, handles: new Set() };
|
|
119
|
+
registry.destinations.set(endpoint, destination);
|
|
120
|
+
}
|
|
121
|
+
destination.routing = routing;
|
|
122
|
+
const state = destination;
|
|
123
|
+
let destroyed = false;
|
|
124
|
+
const handle = {
|
|
125
|
+
pageview(page = {}) {
|
|
126
|
+
if (destroyed || preview())
|
|
127
|
+
return;
|
|
128
|
+
// Clamp before the dedupe, not after: two navigations that differ only
|
|
129
|
+
// past the cap arrive as the same `p`, and comparing the raw values
|
|
130
|
+
// would send both and record them as duplicates of each other.
|
|
131
|
+
const path = clamp((page.path ?? browserPath()).split('#')[0] || '/', CAPS.p);
|
|
132
|
+
if (state.lastPath === path)
|
|
133
|
+
return;
|
|
134
|
+
const pageName = page.pageName ?? (path.split('?')[0].replace(/\/+$/, '') || '/');
|
|
135
|
+
state.lastPath = path;
|
|
136
|
+
state.lastPage = { path, pageName };
|
|
137
|
+
registry.visitor ?? (registry.visitor = storedId('localStorage', 'fimo.analytics.visitor-id'));
|
|
138
|
+
registry.session ?? (registry.session = storedId('sessionStorage', 'fimo.analytics.session-id'));
|
|
139
|
+
const body = new URLSearchParams({
|
|
140
|
+
p: path,
|
|
141
|
+
n: clamp(pageName, CAPS.n),
|
|
142
|
+
v: registry.visitor,
|
|
143
|
+
s: registry.session,
|
|
144
|
+
});
|
|
145
|
+
if (document.referrer)
|
|
146
|
+
body.set('r', clamp(document.referrer, CAPS.r));
|
|
147
|
+
if (window.navigator.userAgent)
|
|
148
|
+
body.set('u', clamp(window.navigator.userAgent, CAPS.u));
|
|
149
|
+
try {
|
|
150
|
+
void window
|
|
151
|
+
.fetch(endpoint, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body,
|
|
154
|
+
keepalive: true,
|
|
155
|
+
mode: 'cors',
|
|
156
|
+
credentials: 'omit',
|
|
157
|
+
})
|
|
158
|
+
.catch(() => { });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* Best effort; never retry an ambiguous delivery without server idempotency. */
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
destroy() {
|
|
165
|
+
if (destroyed)
|
|
166
|
+
return;
|
|
167
|
+
destroyed = true;
|
|
168
|
+
state.handles.delete(handle);
|
|
169
|
+
if (![...registry.destinations.values()].some((entry) => entry.handles.size)) {
|
|
170
|
+
registry.stop?.();
|
|
171
|
+
registry.stop = undefined;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
state.handles.add(handle);
|
|
176
|
+
registry.stop ?? (registry.stop = observe(registry));
|
|
177
|
+
if (routing === 'history')
|
|
178
|
+
handle.pageview();
|
|
179
|
+
return handle;
|
|
180
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Analytics, type AnalyticsOptions } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* An adapter must never take its host application down. `init` throws on a
|
|
4
|
+
* misconfigured endpoint — most often an unset environment variable
|
|
5
|
+
* interpolated into the URL, which arrives as the string `undefined/e/p` — and
|
|
6
|
+
* a throw inside a component's effect reaches the nearest error boundary, or
|
|
7
|
+
* blanks the root when there is none. Losing analytics is an acceptable
|
|
8
|
+
* outcome; losing the customer's page is not.
|
|
9
|
+
*
|
|
10
|
+
* `init` itself keeps throwing: it is the documented programmatic API, and a
|
|
11
|
+
* bad endpoint there is a programming error worth surfacing. The standalone
|
|
12
|
+
* browser script already degrades this way, and the framework adapters now
|
|
13
|
+
* match it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function initOrWarn(options: AnalyticsOptions): Analytics;
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { init } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* An adapter must never take its host application down. `init` throws on a
|
|
4
|
+
* misconfigured endpoint — most often an unset environment variable
|
|
5
|
+
* interpolated into the URL, which arrives as the string `undefined/e/p` — and
|
|
6
|
+
* a throw inside a component's effect reaches the nearest error boundary, or
|
|
7
|
+
* blanks the root when there is none. Losing analytics is an acceptable
|
|
8
|
+
* outcome; losing the customer's page is not.
|
|
9
|
+
*
|
|
10
|
+
* `init` itself keeps throwing: it is the documented programmatic API, and a
|
|
11
|
+
* bad endpoint there is a programming error worth surfacing. The standalone
|
|
12
|
+
* browser script already degrades this way, and the framework adapters now
|
|
13
|
+
* match it.
|
|
14
|
+
*/
|
|
15
|
+
export function initOrWarn(options) {
|
|
16
|
+
try {
|
|
17
|
+
return init(options);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.warn('[fimo-analytics]', error);
|
|
21
|
+
return { pageview() { }, destroy() { } };
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRouter } from 'next/compat/router.js';
|
|
4
|
+
import { usePathname, useSearchParams } from 'next/navigation.js';
|
|
5
|
+
import { Suspense, useEffect } from 'react';
|
|
6
|
+
import { initOrWarn } from './internal.js';
|
|
7
|
+
function AppAnalytics({ endpoint, enabled }) {
|
|
8
|
+
const pathname = usePathname();
|
|
9
|
+
const search = useSearchParams();
|
|
10
|
+
const query = search?.toString();
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (pathname === null)
|
|
13
|
+
return;
|
|
14
|
+
const tracker = initOrWarn({ endpoint, enabled, routing: 'manual' });
|
|
15
|
+
// Read the committed browser URL to retain its original query encoding and base path.
|
|
16
|
+
tracker.pageview();
|
|
17
|
+
return () => tracker.destroy();
|
|
18
|
+
}, [endpoint, enabled, pathname, query]);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function Analytics({ endpoint, enabled }) {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const ready = router?.isReady;
|
|
24
|
+
const events = router?.events;
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!ready || !events)
|
|
27
|
+
return;
|
|
28
|
+
const tracker = initOrWarn({ endpoint, enabled, routing: 'manual' });
|
|
29
|
+
const committed = () => tracker.pageview();
|
|
30
|
+
committed();
|
|
31
|
+
events.on('routeChangeComplete', committed);
|
|
32
|
+
return () => {
|
|
33
|
+
events.off('routeChangeComplete', committed);
|
|
34
|
+
tracker.destroy();
|
|
35
|
+
};
|
|
36
|
+
}, [endpoint, enabled, ready, events]);
|
|
37
|
+
return router ? null : (_jsx(Suspense, { fallback: null, children: _jsx(AppAnalytics, { endpoint: endpoint, enabled: enabled }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig, useRouter } from '#app';
|
|
2
|
+
|
|
3
|
+
import { initOrWarn } from './internal.js';
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin((app) => {
|
|
6
|
+
const options = useRuntimeConfig().public.fimoAnalytics;
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
let tracker;
|
|
9
|
+
const mounted = app.hook('app:mounted', () => {
|
|
10
|
+
tracker = initOrWarn({ ...options, routing: 'manual' });
|
|
11
|
+
tracker.pageview();
|
|
12
|
+
});
|
|
13
|
+
const remove = router.afterEach((_to, _from, failure) => {
|
|
14
|
+
if (!failure) tracker?.pageview();
|
|
15
|
+
});
|
|
16
|
+
const destroy = () => {
|
|
17
|
+
mounted();
|
|
18
|
+
remove();
|
|
19
|
+
tracker?.destroy();
|
|
20
|
+
};
|
|
21
|
+
app.vueApp.onUnmount(destroy);
|
|
22
|
+
if (import.meta.hot) import.meta.hot.dispose(destroy);
|
|
23
|
+
});
|
package/dist/nuxt.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
import type { AnalyticsProps } from './index.js';
|
|
3
|
+
declare const analytics: NuxtModule<AnalyticsProps>;
|
|
4
|
+
export default analytics;
|
|
5
|
+
declare module '@nuxt/schema' {
|
|
6
|
+
interface NuxtConfig {
|
|
7
|
+
analytics?: AnalyticsProps;
|
|
8
|
+
}
|
|
9
|
+
interface NuxtOptions {
|
|
10
|
+
analytics?: AnalyticsProps;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/nuxt.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
|
|
2
|
+
const analytics = defineNuxtModule({
|
|
3
|
+
meta: { name: 'fimo-analytics', configKey: 'analytics' },
|
|
4
|
+
defaults: { endpoint: '', enabled: true },
|
|
5
|
+
setup(options, nuxt) {
|
|
6
|
+
nuxt.options.runtimeConfig.public.fimoAnalytics = options;
|
|
7
|
+
addPlugin({ src: createResolver(import.meta.url).resolve('./nuxt-plugin.mjs'), mode: 'client' });
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
export default analytics;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { useLocation } from 'react-router';
|
|
4
|
+
import { initOrWarn } from './internal.js';
|
|
5
|
+
export function Analytics({ endpoint, enabled, pageName }) {
|
|
6
|
+
const { pathname, search } = useLocation();
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const tracker = initOrWarn({ endpoint, enabled, routing: 'manual' });
|
|
9
|
+
tracker.pageview({ path: pathname + search, pageName });
|
|
10
|
+
return () => tracker.destroy();
|
|
11
|
+
}, [endpoint, enabled, pathname, search, pageName]);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
package/dist/react.d.ts
ADDED
package/dist/react.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { initOrWarn } from './internal.js';
|
|
4
|
+
export function Analytics({ endpoint, enabled }) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const tracker = initOrWarn({ endpoint, enabled });
|
|
7
|
+
return () => tracker.destroy();
|
|
8
|
+
}, [endpoint, enabled]);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const Analytics: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
2
|
+
endpoint: {
|
|
3
|
+
type: StringConstructor;
|
|
4
|
+
required: true;
|
|
5
|
+
};
|
|
6
|
+
enabled: {
|
|
7
|
+
type: BooleanConstructor;
|
|
8
|
+
default: boolean;
|
|
9
|
+
};
|
|
10
|
+
}>, () => null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
11
|
+
endpoint: {
|
|
12
|
+
type: StringConstructor;
|
|
13
|
+
required: true;
|
|
14
|
+
};
|
|
15
|
+
enabled: {
|
|
16
|
+
type: BooleanConstructor;
|
|
17
|
+
default: boolean;
|
|
18
|
+
};
|
|
19
|
+
}>> & Readonly<{}>, {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineComponent, onMounted, onUnmounted, watch } from 'vue';
|
|
2
|
+
import { initOrWarn } from './internal.js';
|
|
3
|
+
export const Analytics = defineComponent({
|
|
4
|
+
name: 'FimoAnalytics',
|
|
5
|
+
props: { endpoint: { type: String, required: true }, enabled: { type: Boolean, default: true } },
|
|
6
|
+
setup(props) {
|
|
7
|
+
let tracker;
|
|
8
|
+
let stop;
|
|
9
|
+
onMounted(() => {
|
|
10
|
+
stop = watch(() => [props.endpoint, props.enabled], () => {
|
|
11
|
+
tracker?.destroy();
|
|
12
|
+
tracker = initOrWarn({ endpoint: props.endpoint, enabled: props.enabled });
|
|
13
|
+
}, { immediate: true });
|
|
14
|
+
});
|
|
15
|
+
onUnmounted(() => {
|
|
16
|
+
stop?.();
|
|
17
|
+
tracker?.destroy();
|
|
18
|
+
});
|
|
19
|
+
return () => null;
|
|
20
|
+
},
|
|
21
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fimo-analytics",
|
|
3
|
+
"version": "2.7.0-experimental.1",
|
|
4
|
+
"description": "Framework-independent Fimo pageview analytics with optional framework adapters.",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist/",
|
|
7
|
+
"components/",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"./dist/browser.js",
|
|
13
|
+
"./components/*.astro",
|
|
14
|
+
"./components/*.svelte"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./react": {
|
|
23
|
+
"types": "./dist/react.d.ts",
|
|
24
|
+
"import": "./dist/react.js"
|
|
25
|
+
},
|
|
26
|
+
"./react-router": {
|
|
27
|
+
"types": "./dist/react-router.d.ts",
|
|
28
|
+
"import": "./dist/react-router.js"
|
|
29
|
+
},
|
|
30
|
+
"./next": {
|
|
31
|
+
"types": "./dist/next.d.ts",
|
|
32
|
+
"import": "./dist/next.js"
|
|
33
|
+
},
|
|
34
|
+
"./vue": {
|
|
35
|
+
"types": "./dist/vue.d.ts",
|
|
36
|
+
"import": "./dist/vue.js"
|
|
37
|
+
},
|
|
38
|
+
"./nuxt": {
|
|
39
|
+
"types": "./dist/nuxt.d.ts",
|
|
40
|
+
"import": "./dist/nuxt.js"
|
|
41
|
+
},
|
|
42
|
+
"./svelte": {
|
|
43
|
+
"types": "./components/svelte.d.ts",
|
|
44
|
+
"svelte": "./components/Analytics.svelte",
|
|
45
|
+
"default": "./components/Analytics.svelte"
|
|
46
|
+
},
|
|
47
|
+
"./sveltekit": {
|
|
48
|
+
"types": "./components/svelte.d.ts",
|
|
49
|
+
"svelte": "./components/SvelteKit.svelte",
|
|
50
|
+
"default": "./components/SvelteKit.svelte"
|
|
51
|
+
},
|
|
52
|
+
"./astro": "./components/Analytics.astro",
|
|
53
|
+
"./browser": "./dist/browser.js"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json && node build.mjs",
|
|
60
|
+
"check:types": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"test": "pnpm build && node --test test/*.test.mjs",
|
|
62
|
+
"test:e2e": "node test/run-e2e.mjs",
|
|
63
|
+
"test:fixtures": "node test/fixtures.mjs",
|
|
64
|
+
"clean": "rm -rf dist"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@nuxt/kit": "4.5.2",
|
|
68
|
+
"@nuxt/schema": "4.5.2",
|
|
69
|
+
"@types/react": "19.2.14",
|
|
70
|
+
"esbuild": "^0.25.10",
|
|
71
|
+
"next": "16.3.3",
|
|
72
|
+
"react": "19.2.4",
|
|
73
|
+
"react-router": "7.18.2",
|
|
74
|
+
"typescript": "7.0.2",
|
|
75
|
+
"vue": "3.5.42"
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@nuxt/kit": "^4.5.2",
|
|
79
|
+
"@sveltejs/kit": "^2.12.0",
|
|
80
|
+
"astro": "^7.0.0",
|
|
81
|
+
"next": "^15.0.0 || ^16.0.0",
|
|
82
|
+
"nuxt": "^4.5.2",
|
|
83
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
84
|
+
"react-router": "^7.0.0 || ^8.0.0",
|
|
85
|
+
"svelte": "^5.0.0",
|
|
86
|
+
"vue": "^3.5.0"
|
|
87
|
+
},
|
|
88
|
+
"peerDependenciesMeta": {
|
|
89
|
+
"@nuxt/kit": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"nuxt": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"react": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"react-router": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"next": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"astro": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"svelte": {
|
|
108
|
+
"optional": true
|
|
109
|
+
},
|
|
110
|
+
"@sveltejs/kit": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
113
|
+
"vue": {
|
|
114
|
+
"optional": true
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"engines": {
|
|
118
|
+
"node": ">=20.12.0"
|
|
119
|
+
}
|
|
120
|
+
}
|