site-vigil-visitors 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -8
- package/bin/cli.mjs +274 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +18 -3
- package/dist/core.js.map +1 -1
- package/package.json +2 -2
- package/bin/init.mjs +0 -124
package/README.md
CHANGED
|
@@ -23,6 +23,47 @@ For React projects:
|
|
|
23
23
|
npm install react site-vigil-visitors
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
### Automated setup (React)
|
|
27
|
+
|
|
28
|
+
After installing, run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx site-vigil-visitors init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This prompts for your site ID, then adds the import and the `<Tracking />`
|
|
35
|
+
component to your app entry file (`src/App.tsx`, `src/App.jsx`, `src/main.tsx`,
|
|
36
|
+
or `src/main.jsx`). To skip the prompt:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx site-vigil-visitors init --site-id=my-site
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Running `init` again is safe — it does nothing if the snippet is already there.
|
|
43
|
+
|
|
44
|
+
> npm runs install scripts non-interactively, so the setup cannot prompt for a
|
|
45
|
+
> site ID during `npm install` itself. Always run `init` as a separate step.
|
|
46
|
+
|
|
47
|
+
## Uninstalling
|
|
48
|
+
|
|
49
|
+
Run the removal command **before** uninstalling the package (it needs the
|
|
50
|
+
package to still be installed):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx site-vigil-visitors remove
|
|
54
|
+
npm uninstall site-vigil-visitors
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`remove` scans every file under `src/` and strips all tracking code: import
|
|
58
|
+
statements referencing this package and any `<Tracking />` elements (single- or
|
|
59
|
+
multi-line, either quote style). If a file still references the package in a
|
|
60
|
+
way it cannot safely remove (for example custom `createTracker` usage), it
|
|
61
|
+
lists that file for manual review.
|
|
62
|
+
|
|
63
|
+
> npm 7+ no longer runs `preuninstall` scripts, so the package cannot clean up
|
|
64
|
+
> automatically when you run `npm uninstall` — the `remove` command is the
|
|
65
|
+
> supported way to do it.
|
|
66
|
+
|
|
26
67
|
## Quick start
|
|
27
68
|
|
|
28
69
|
### React
|
|
@@ -34,7 +75,7 @@ export function App() {
|
|
|
34
75
|
return (
|
|
35
76
|
<Tracking
|
|
36
77
|
siteId="my-site"
|
|
37
|
-
endpoint="https://
|
|
78
|
+
endpoint="https://t.site-vigil.com"
|
|
38
79
|
/>
|
|
39
80
|
)
|
|
40
81
|
}
|
|
@@ -47,7 +88,7 @@ import { createTracker } from 'site-vigil-visitors'
|
|
|
47
88
|
|
|
48
89
|
const tracker = createTracker({
|
|
49
90
|
siteId: 'my-site',
|
|
50
|
-
endpoint: 'https://
|
|
91
|
+
endpoint: 'https://t.site-vigil.com',
|
|
51
92
|
})
|
|
52
93
|
|
|
53
94
|
tracker.start()
|
|
@@ -60,7 +101,7 @@ import { createTracker } from 'site-vigil-visitors'
|
|
|
60
101
|
|
|
61
102
|
const tracker = createTracker({
|
|
62
103
|
siteId: 'my-site',
|
|
63
|
-
endpoint: 'https://
|
|
104
|
+
endpoint: 'https://t.site-vigil.com',
|
|
64
105
|
})
|
|
65
106
|
|
|
66
107
|
tracker.start()
|
|
@@ -77,7 +118,7 @@ export function App() {
|
|
|
77
118
|
<>
|
|
78
119
|
<Tracking
|
|
79
120
|
siteId="my-site"
|
|
80
|
-
endpoint="https://
|
|
121
|
+
endpoint="https://t.site-vigil.com"
|
|
81
122
|
/>
|
|
82
123
|
<main>Hello world</main>
|
|
83
124
|
</>
|
|
@@ -97,7 +138,7 @@ export default function App() {
|
|
|
97
138
|
<div>
|
|
98
139
|
<Tracking
|
|
99
140
|
siteId="demo-site"
|
|
100
|
-
endpoint="https://
|
|
141
|
+
endpoint="https://t.site-vigil.com"
|
|
101
142
|
/>
|
|
102
143
|
|
|
103
144
|
<h1>Visit Tracker Demo</h1>
|
|
@@ -120,7 +161,7 @@ import { createTracker } from 'site-vigil-visitors'
|
|
|
120
161
|
|
|
121
162
|
const tracker = createTracker({
|
|
122
163
|
siteId: 'demo-site',
|
|
123
|
-
endpoint: 'https://
|
|
164
|
+
endpoint: 'https://t.site-vigil.com',
|
|
124
165
|
})
|
|
125
166
|
|
|
126
167
|
tracker.trackEvent('button_click', { button: 'hero-cta' })
|
|
@@ -139,7 +180,7 @@ export default function TrackingClient() {
|
|
|
139
180
|
return (
|
|
140
181
|
<Tracking
|
|
141
182
|
siteId="next-app"
|
|
142
|
-
endpoint="https://
|
|
183
|
+
endpoint="https://t.site-vigil.com"
|
|
143
184
|
/>
|
|
144
185
|
)
|
|
145
186
|
}
|
|
@@ -155,7 +196,7 @@ Then include it in your app layout or page.
|
|
|
155
196
|
|
|
156
197
|
const tracker = createTracker({
|
|
157
198
|
siteId: 'marketing-site',
|
|
158
|
-
endpoint: 'https://
|
|
199
|
+
endpoint: 'https://t.site-vigil.com',
|
|
159
200
|
})
|
|
160
201
|
|
|
161
202
|
tracker.start()
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline'
|
|
3
|
+
import { readFile, writeFile, access, readdir } from 'node:fs/promises'
|
|
4
|
+
import { join, relative } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const ENDPOINT = 'https://t.site-vigil.com'
|
|
7
|
+
const PACKAGE = 'site-vigil-visitors'
|
|
8
|
+
|
|
9
|
+
const CANDIDATES = [
|
|
10
|
+
'src/App.tsx',
|
|
11
|
+
'src/App.jsx',
|
|
12
|
+
'src/main.tsx',
|
|
13
|
+
'src/main.jsx',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const SOURCE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// shared helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
async function findAppFile(cwd) {
|
|
23
|
+
for (const candidate of CANDIDATES) {
|
|
24
|
+
try {
|
|
25
|
+
await access(join(cwd, candidate))
|
|
26
|
+
return { path: join(cwd, candidate), rel: candidate }
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function collectSourceFiles(dir, results = []) {
|
|
33
|
+
let entries
|
|
34
|
+
try {
|
|
35
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
36
|
+
} catch {
|
|
37
|
+
return results
|
|
38
|
+
}
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
41
|
+
const full = join(dir, entry.name)
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
await collectSourceFiles(full, results)
|
|
44
|
+
} else if (SOURCE_EXTENSIONS.some(ext => entry.name.endsWith(ext))) {
|
|
45
|
+
results.push(full)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return results
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// init
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
async function promptSiteId() {
|
|
56
|
+
const arg = process.argv.find(a => a.startsWith('--site-id='))
|
|
57
|
+
if (arg) return arg.slice('--site-id='.length).trim()
|
|
58
|
+
|
|
59
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
60
|
+
return new Promise(resolve => {
|
|
61
|
+
rl.question('Enter your site ID (e.g. my-website): ', answer => {
|
|
62
|
+
rl.close()
|
|
63
|
+
resolve(answer.trim())
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detectQuote(content) {
|
|
69
|
+
const double = (content.match(/from "/g) || []).length
|
|
70
|
+
const single = (content.match(/from '/g) || []).length
|
|
71
|
+
return double > single ? '"' : "'"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function patchImports(content) {
|
|
75
|
+
if (new RegExp(`from ["']${PACKAGE}/react["']`).test(content)) return content
|
|
76
|
+
const matches = [...content.matchAll(/^import .+$/gm)]
|
|
77
|
+
if (!matches.length) return content
|
|
78
|
+
const q = detectQuote(content)
|
|
79
|
+
const last = matches.at(-1)
|
|
80
|
+
const pos = last.index + last[0].length
|
|
81
|
+
return (
|
|
82
|
+
content.slice(0, pos) +
|
|
83
|
+
`\nimport { Tracking } from ${q}${PACKAGE}/react${q}` +
|
|
84
|
+
content.slice(pos)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function patchJsx(content, siteId) {
|
|
89
|
+
if (content.includes('<Tracking ')) return content
|
|
90
|
+
|
|
91
|
+
// Insert <Tracking /> as the last child of the file's final JSX closing tag —
|
|
92
|
+
// in a typical app entry file that is the root element of the main return.
|
|
93
|
+
const closers = [...content.matchAll(/^([ \t]*)<\/(?:[A-Za-z][\w.]*)?>[ \t]*\r?$/gm)]
|
|
94
|
+
if (!closers.length) return null
|
|
95
|
+
|
|
96
|
+
const last = closers.at(-1)
|
|
97
|
+
const closeIndent = last[1]
|
|
98
|
+
|
|
99
|
+
// Indent like the preceding sibling line when it sits deeper than the
|
|
100
|
+
// closing tag, otherwise one level further in than the closing tag.
|
|
101
|
+
const prevLine = content.slice(0, last.index).split('\n').filter(l => l.trim()).at(-1) ?? ''
|
|
102
|
+
const prevIndent = prevLine.match(/^[ \t]*/)[0]
|
|
103
|
+
const indent = prevIndent.length > closeIndent.length ? prevIndent : closeIndent + ' '
|
|
104
|
+
|
|
105
|
+
const trackingLine = `${indent}<Tracking siteId="${siteId}" endpoint="${ENDPOINT}" />\n`
|
|
106
|
+
return content.slice(0, last.index) + trackingLine + content.slice(last.index)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printManual(siteId) {
|
|
110
|
+
console.log(`Add this import to your app entry file:
|
|
111
|
+
|
|
112
|
+
import { Tracking } from '${PACKAGE}/react'
|
|
113
|
+
|
|
114
|
+
Then add the component inside your JSX tree:
|
|
115
|
+
|
|
116
|
+
<Tracking
|
|
117
|
+
siteId="${siteId}"
|
|
118
|
+
endpoint="${ENDPOINT}"
|
|
119
|
+
/>
|
|
120
|
+
`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function init() {
|
|
124
|
+
console.log(`${PACKAGE} init\n`)
|
|
125
|
+
|
|
126
|
+
const siteId = await promptSiteId()
|
|
127
|
+
if (!siteId) {
|
|
128
|
+
console.error('Error: site ID is required.')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const found = await findAppFile(process.cwd())
|
|
133
|
+
if (!found) {
|
|
134
|
+
console.log('Could not find src/App.tsx or src/main.tsx. Add manually:\n')
|
|
135
|
+
printManual(siteId)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`Found: ${found.rel}`)
|
|
140
|
+
|
|
141
|
+
let content = await readFile(found.path, 'utf-8')
|
|
142
|
+
|
|
143
|
+
if (content.includes('<Tracking ')) {
|
|
144
|
+
console.log(`\n<Tracking /> is already present in ${found.rel}. Nothing to do.`)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
content = patchImports(content)
|
|
149
|
+
const patched = patchJsx(content, siteId)
|
|
150
|
+
|
|
151
|
+
if (!patched) {
|
|
152
|
+
console.log('Could not automatically patch JSX. Add manually:\n')
|
|
153
|
+
printManual(siteId)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await writeFile(found.path, patched, 'utf-8')
|
|
158
|
+
|
|
159
|
+
console.log(`
|
|
160
|
+
Done! Added <Tracking /> to ${found.rel}.
|
|
161
|
+
|
|
162
|
+
To verify:
|
|
163
|
+
1. Start your dev server
|
|
164
|
+
2. Open DevTools -> Network, filter for "t.site-vigil.com"
|
|
165
|
+
3. Reload - you should see a POST with status 200
|
|
166
|
+
|
|
167
|
+
Note: make sure "${siteId}" is registered in your Supabase websites table (tracking_id column).
|
|
168
|
+
`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// remove
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
function stripTracking(content) {
|
|
176
|
+
let result = content
|
|
177
|
+
|
|
178
|
+
// import lines referencing the package (any specifier, either quote style)
|
|
179
|
+
result = result.replace(
|
|
180
|
+
new RegExp(`^[ \\t]*import\\s[^\\n]*["']${PACKAGE}(?:/[^"']*)?["'][^\\n]*\\r?\\n?`, 'gm'),
|
|
181
|
+
''
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// <Tracking ... /> on its own line(s), self-closing (possibly multi-line)
|
|
185
|
+
result = result.replace(/^[ \t]*<Tracking\b[\s\S]*?\/>[ \t]*\r?\n?/gm, '')
|
|
186
|
+
|
|
187
|
+
// <Tracking>...</Tracking> with children, on its own line(s)
|
|
188
|
+
result = result.replace(/^[ \t]*<Tracking\b[\s\S]*?<\/Tracking>[ \t]*\r?\n?/gm, '')
|
|
189
|
+
|
|
190
|
+
// any inline occurrences that were not at the start of a line
|
|
191
|
+
result = result.replace(/<Tracking\b[\s\S]*?\/>/g, '')
|
|
192
|
+
result = result.replace(/<Tracking\b[\s\S]*?<\/Tracking>/g, '')
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function remove() {
|
|
198
|
+
console.log(`${PACKAGE} remove\n`)
|
|
199
|
+
|
|
200
|
+
const cwd = process.cwd()
|
|
201
|
+
const files = await collectSourceFiles(join(cwd, 'src'))
|
|
202
|
+
|
|
203
|
+
if (!files.length) {
|
|
204
|
+
console.log('No source files found under src/. Nothing to do.')
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const changed = []
|
|
209
|
+
const needsReview = []
|
|
210
|
+
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
const content = await readFile(file, 'utf-8')
|
|
213
|
+
if (!content.includes(PACKAGE) && !content.includes('<Tracking')) continue
|
|
214
|
+
|
|
215
|
+
const stripped = stripTracking(content)
|
|
216
|
+
if (stripped !== content) {
|
|
217
|
+
await writeFile(file, stripped, 'utf-8')
|
|
218
|
+
changed.push(relative(cwd, file))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (/site-vigil|createTracker|<Tracking\b/.test(stripped)) {
|
|
222
|
+
needsReview.push(relative(cwd, file))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (changed.length) {
|
|
227
|
+
console.log('Removed tracking code from:')
|
|
228
|
+
for (const rel of changed) console.log(` - ${rel}`)
|
|
229
|
+
} else {
|
|
230
|
+
console.log('No tracking code found in src/.')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (needsReview.length) {
|
|
234
|
+
console.log('\nWarning: these files still reference the package and need manual review:')
|
|
235
|
+
for (const rel of needsReview) console.log(` - ${rel}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`
|
|
239
|
+
Now remove the package itself:
|
|
240
|
+
|
|
241
|
+
npm uninstall ${PACKAGE}
|
|
242
|
+
`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// entry point
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function printUsage() {
|
|
250
|
+
console.log(`Usage: npx ${PACKAGE} <command>
|
|
251
|
+
|
|
252
|
+
Commands:
|
|
253
|
+
init Add the <Tracking /> snippet to your app (prompts for a site ID,
|
|
254
|
+
or pass --site-id=my-site)
|
|
255
|
+
remove Strip all tracking code from src/ (run BEFORE npm uninstall)
|
|
256
|
+
`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const command = process.argv[2] && !process.argv[2].startsWith('--')
|
|
260
|
+
? process.argv[2]
|
|
261
|
+
: 'init'
|
|
262
|
+
|
|
263
|
+
const run = { init, remove, uninstall: remove }[command]
|
|
264
|
+
|
|
265
|
+
if (!run) {
|
|
266
|
+
console.error(`Unknown command: ${command}\n`)
|
|
267
|
+
printUsage()
|
|
268
|
+
process.exit(1)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
run().catch(err => {
|
|
272
|
+
console.error(err.message)
|
|
273
|
+
process.exit(1)
|
|
274
|
+
})
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEnD,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;CAC/D;AAYD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,UAA6B,EAC7B,iBAAwB,GACzB,EAAE,aAAa,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEnD,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;CAC/D;AAYD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,UAA6B,EAC7B,iBAAwB,GACzB,EAAE,aAAa,GAAG,OAAO,CA6EzB"}
|
package/dist/core.js
CHANGED
|
@@ -32,24 +32,39 @@ export function createTracker({ siteId, endpoint, sessionKey = 'tithij_session',
|
|
|
32
32
|
...metadata,
|
|
33
33
|
};
|
|
34
34
|
const body = JSON.stringify(payload);
|
|
35
|
+
if (typeof navigator.sendBeacon === 'function') {
|
|
36
|
+
const blob = new Blob([body], { type: 'application/json' });
|
|
37
|
+
if (navigator.sendBeacon(endpoint, blob))
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
35
40
|
void fetch(endpoint, {
|
|
36
41
|
method: 'POST',
|
|
42
|
+
mode: 'cors',
|
|
43
|
+
credentials: 'omit',
|
|
37
44
|
headers: { 'Content-Type': 'application/json' },
|
|
38
45
|
body,
|
|
39
46
|
keepalive: true,
|
|
40
47
|
}).catch(() => { });
|
|
41
48
|
};
|
|
42
|
-
const
|
|
49
|
+
const handlePageLeave = () => send('leave');
|
|
50
|
+
const handleVisibilityChange = () => {
|
|
51
|
+
if (document.visibilityState === 'hidden')
|
|
52
|
+
handlePageLeave();
|
|
53
|
+
};
|
|
43
54
|
return {
|
|
44
55
|
start() {
|
|
45
56
|
send('visit');
|
|
46
57
|
if (includeLeaveEvent && isBrowser) {
|
|
47
|
-
window.addEventListener('beforeunload',
|
|
58
|
+
window.addEventListener('beforeunload', handlePageLeave);
|
|
59
|
+
window.addEventListener('pagehide', handlePageLeave);
|
|
60
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
48
61
|
}
|
|
49
62
|
},
|
|
50
63
|
destroy() {
|
|
51
64
|
if (includeLeaveEvent && isBrowser) {
|
|
52
|
-
window.removeEventListener('beforeunload',
|
|
65
|
+
window.removeEventListener('beforeunload', handlePageLeave);
|
|
66
|
+
window.removeEventListener('pagehide', handlePageLeave);
|
|
67
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
53
68
|
}
|
|
54
69
|
},
|
|
55
70
|
trackPageView() {
|
package/dist/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAgBA,SAAS,eAAe;IACtB,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IAEzE,IAAI,UAAU,EAAE,CAAC;QACf,OAAO,UAAU,EAAE,CAAA;IACrB,CAAC;IAED,OAAO,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,MAAM,EACN,QAAQ,EACR,UAAU,GAAG,gBAAgB,EAC7B,iBAAiB,GAAG,IAAI,GACV;IACd,MAAM,SAAS,GACb,OAAO,MAAM,KAAK,WAAW;QAC7B,OAAO,QAAQ,KAAK,WAAW;QAC/B,OAAO,SAAS,KAAK,WAAW,CAAA;IAElC,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,SAAS;YAAE,OAAO,QAAQ,CAAA;QAE/B,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAC1D,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAE7B,MAAM,EAAE,GAAG,eAAe,EAAE,CAAA;QAC5B,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;QAC7C,OAAO,EAAE,CAAA;IACX,CAAC,CAAA;IAED,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,WAA0B,EAAE,EAAE,EAAE;QAC5D,IAAI,CAAC,SAAS;YAAE,OAAM;QAEtB,MAAM,OAAO,GAAG;YACd,MAAM;YACN,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAC9B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;YAC3B,MAAM;YACN,SAAS,EAAE,YAAY,EAAE;YACzB,GAAG,QAAQ;SACZ,CAAA;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QAEpC,KAAK,KAAK,CAAC,QAAQ,EAAE;YACnB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI;YACJ,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACpB,CAAC,CAAA;IAED,MAAM,
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "site-vigil-visitors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight browser visit tracking with optional React integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
-
"site-vigil-visitors": "bin/
|
|
22
|
+
"site-vigil-visitors": "bin/cli.mjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
package/bin/init.mjs
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createInterface } from 'node:readline'
|
|
3
|
-
import { readFile, writeFile, access } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
|
|
6
|
-
const ENDPOINT = 'https://visit-tracker.tithij.workers.dev'
|
|
7
|
-
|
|
8
|
-
const CANDIDATES = [
|
|
9
|
-
'src/App.tsx',
|
|
10
|
-
'src/App.jsx',
|
|
11
|
-
'src/main.tsx',
|
|
12
|
-
'src/main.jsx',
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
async function findAppFile(cwd) {
|
|
16
|
-
for (const candidate of CANDIDATES) {
|
|
17
|
-
try {
|
|
18
|
-
await access(join(cwd, candidate))
|
|
19
|
-
return { path: join(cwd, candidate), rel: candidate }
|
|
20
|
-
} catch {}
|
|
21
|
-
}
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function promptSiteId() {
|
|
26
|
-
const arg = process.argv.find(a => a.startsWith('--site-id='))
|
|
27
|
-
if (arg) return arg.slice('--site-id='.length).trim()
|
|
28
|
-
|
|
29
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
30
|
-
return new Promise(resolve => {
|
|
31
|
-
rl.question('Enter your site ID (e.g. my-website): ', answer => {
|
|
32
|
-
rl.close()
|
|
33
|
-
resolve(answer.trim())
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function patchImports(content) {
|
|
39
|
-
if (content.includes("from 'site-vigil-visitors/react'")) return content
|
|
40
|
-
const matches = [...content.matchAll(/^import .+$/gm)]
|
|
41
|
-
if (!matches.length) return content
|
|
42
|
-
const last = matches.at(-1)
|
|
43
|
-
const pos = last.index + last[0].length
|
|
44
|
-
return (
|
|
45
|
-
content.slice(0, pos) +
|
|
46
|
-
"\nimport { Tracking } from 'site-vigil-visitors/react'" +
|
|
47
|
-
content.slice(pos)
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function patchJsx(content, siteId) {
|
|
52
|
-
if (content.includes('<Tracking ')) return content
|
|
53
|
-
|
|
54
|
-
// Match the first non-self-closing JSX opening tag (capital-letter component) and
|
|
55
|
-
// insert <Tracking /> as its first child, matching the sibling's indentation.
|
|
56
|
-
const firstParentTag = /^([ \t]+<[A-Z][^\n]*[^/]>)([ \t]*\n)([ \t]+<)/m
|
|
57
|
-
const m = content.match(firstParentTag)
|
|
58
|
-
if (!m) return null
|
|
59
|
-
|
|
60
|
-
const indent = m[3].match(/^([ \t]+)/)[1]
|
|
61
|
-
const trackingLine = `${indent}<Tracking siteId="${siteId}" endpoint="${ENDPOINT}" />\n`
|
|
62
|
-
return content.replace(firstParentTag, `$1$2${trackingLine}$3`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function printManual(siteId) {
|
|
66
|
-
console.log(`Add this import to your app entry file:
|
|
67
|
-
|
|
68
|
-
import { Tracking } from 'site-vigil-visitors/react'
|
|
69
|
-
|
|
70
|
-
Then add the component inside your JSX tree:
|
|
71
|
-
|
|
72
|
-
<Tracking
|
|
73
|
-
siteId="${siteId}"
|
|
74
|
-
endpoint="${ENDPOINT}"
|
|
75
|
-
/>
|
|
76
|
-
`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function main() {
|
|
80
|
-
console.log('site-vigil-visitors init\n')
|
|
81
|
-
|
|
82
|
-
const siteId = await promptSiteId()
|
|
83
|
-
if (!siteId) {
|
|
84
|
-
console.error('Error: site ID is required.')
|
|
85
|
-
process.exit(1)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const found = await findAppFile(process.cwd())
|
|
89
|
-
if (!found) {
|
|
90
|
-
console.log('Could not find src/App.tsx or src/main.tsx. Add manually:\n')
|
|
91
|
-
printManual(siteId)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log(`Found: ${found.rel}`)
|
|
96
|
-
|
|
97
|
-
let content = await readFile(found.path, 'utf-8')
|
|
98
|
-
content = patchImports(content)
|
|
99
|
-
const patched = patchJsx(content, siteId)
|
|
100
|
-
|
|
101
|
-
if (!patched) {
|
|
102
|
-
console.log('Could not automatically patch JSX. Add manually:\n')
|
|
103
|
-
printManual(siteId)
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await writeFile(found.path, patched, 'utf-8')
|
|
108
|
-
|
|
109
|
-
console.log(`
|
|
110
|
-
Done! Added <Tracking /> to ${found.rel}.
|
|
111
|
-
|
|
112
|
-
To verify:
|
|
113
|
-
1. Start your dev server
|
|
114
|
-
2. Open DevTools → Network, filter for "visit-tracker"
|
|
115
|
-
3. Reload — you should see a POST with status 200
|
|
116
|
-
|
|
117
|
-
Note: make sure "${siteId}" is registered in your Supabase websites table (tracking_id column).
|
|
118
|
-
`)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
main().catch(err => {
|
|
122
|
-
console.error(err.message)
|
|
123
|
-
process.exit(1)
|
|
124
|
-
})
|