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 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://visit-tracker.tithij.workers.dev"
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://visit-tracker.tithij.workers.dev',
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://visit-tracker.tithij.workers.dev',
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://visit-tracker.tithij.workers.dev"
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://visit-tracker.tithij.workers.dev"
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://visit-tracker.tithij.workers.dev',
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://visit-tracker.tithij.workers.dev"
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://visit-tracker.tithij.workers.dev',
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
+ })
@@ -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,CA8DzB"}
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 handleBeforeUnload = () => send('leave');
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', handleBeforeUnload);
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', handleBeforeUnload);
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,kBAAkB,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAE9C,OAAO;QACL,KAAK;YACH,IAAI,CAAC,OAAO,CAAC,CAAA;YAEb,IAAI,iBAAiB,IAAI,SAAS,EAAE,CAAC;gBACnC,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;YAC7D,CAAC;QACH,CAAC;QACD,OAAO;YACL,IAAI,iBAAiB,IAAI,SAAS,EAAE,CAAC;gBACnC,MAAM,CAAC,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;YAChE,CAAC;QACH,CAAC;QACD,aAAa;YACX,IAAI,CAAC,OAAO,CAAC,CAAA;QACf,CAAC;QACD,UAAU,CAAC,MAAc,EAAE,WAA0B,EAAE;YACrD,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QACxB,CAAC;KACF,CAAA;AACH,CAAC"}
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.1.2",
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/init.mjs"
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
- })