@kfiross44/valtio-inspector 0.9.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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/README.md +99 -0
- package/client/app.js +329 -0
- package/client/index.html +203 -0
- package/dist/attachInspector.d.ts +19 -0
- package/dist/attachInspector.d.ts.map +1 -0
- package/dist/attachInspector.js +80 -0
- package/dist/attachInspector.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +107 -0
- package/dist/server.js.map +1 -0
- package/example.ts +53 -0
- package/kfiross44-valtio-inspector-1.0.0.tgz +0 -0
- package/package.json +32 -0
- package/script/build.ts +14 -0
- package/script/changelog.sh +13 -0
- package/src/attachInspector.ts +106 -0
- package/src/cli.ts +5 -0
- package/src/index.ts +1 -0
- package/src/server.ts +129 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
|
|
6
|
+
|
|
7
|
+
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "restricted",
|
|
8
|
+
"baseBranch": "main",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Valtio Inspector
|
|
2
|
+
|
|
3
|
+
A dev-only state inspector for [Valtio](https://github.com/pmndrs/valtio) with live WebSocket updates, multiple store support, history, and time travel.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔴 **Live WebSocket updates** — no polling
|
|
8
|
+
- 🗂 **Multiple stores** — sidebar navigation
|
|
9
|
+
- 🌳 **Collapsible JSON tree** — expand/collapse nodes
|
|
10
|
+
- 📜 **History log** — every change tracked with timestamps
|
|
11
|
+
- ⏪ **Time travel** — click any history entry to inspect that snapshot
|
|
12
|
+
- 🔍 **Diff view** — see exactly what changed between snapshots
|
|
13
|
+
- ⚡ **Auto-reconnect** — if the server restarts
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
### 1. Install & run the inspector server
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd valtio-inspector
|
|
23
|
+
npm install
|
|
24
|
+
npm run dev
|
|
25
|
+
# → http://localhost:7777
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Instrument your stores
|
|
29
|
+
|
|
30
|
+
Copy `instrument/attachInspector.ts` into your app (requires `valtio` installed).
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { proxy } from 'valtio'
|
|
34
|
+
import { attachInspector } from './attachInspector'
|
|
35
|
+
|
|
36
|
+
export const authStore = proxy({
|
|
37
|
+
user: { id: 1, name: 'kfir' },
|
|
38
|
+
isLoggedIn: false
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const uiStore = proxy({
|
|
42
|
+
darkMode: true,
|
|
43
|
+
sidebarOpen: false
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Only in dev!
|
|
47
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
48
|
+
attachInspector(authStore, { name: 'auth' })
|
|
49
|
+
attachInspector(uiStore, { name: 'ui' })
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`attachInspector` returns a cleanup function (for React `useEffect`, etc.):
|
|
54
|
+
```ts
|
|
55
|
+
const cleanup = attachInspector(store, { name: 'myStore' })
|
|
56
|
+
// later:
|
|
57
|
+
cleanup()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
attachInspector(store, {
|
|
64
|
+
name: 'myStore', // required — display name
|
|
65
|
+
debounce: 100, // optional — ms debounce (default: 100)
|
|
66
|
+
inspectorUrl: 'http://localhost:7777' // optional — if running elsewhere
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
The server exposes these endpoints (used internally by the UI):
|
|
75
|
+
|
|
76
|
+
| Method | Path | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| `POST` | `/state` | Push a store update |
|
|
79
|
+
| `GET` | `/state` | Get the current full state tree |
|
|
80
|
+
| `DELETE` | `/state` | Clear all state + history |
|
|
81
|
+
| `GET` | `/history` | Get full history log |
|
|
82
|
+
| `GET` | `/history/:id` | Get a specific history snapshot |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Building for production (server)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run build
|
|
90
|
+
npm start
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## ⚠️ Security
|
|
96
|
+
|
|
97
|
+
- **Never expose port 7777 publicly.** This is a dev-only tool.
|
|
98
|
+
- Always gate `attachInspector` behind `process.env.NODE_ENV !== 'production'`.
|
|
99
|
+
- Do not run the inspector server in production builds.
|
package/client/app.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/* ── State ── */
|
|
2
|
+
let stateTree = {}
|
|
3
|
+
let historyLog = []
|
|
4
|
+
let selectedStore = null
|
|
5
|
+
let selectedHistoryId = null
|
|
6
|
+
let activeTab = 'tree'
|
|
7
|
+
let collapsedPaths = new Set()
|
|
8
|
+
let lastDiff = {}
|
|
9
|
+
|
|
10
|
+
/* ── WebSocket ── */
|
|
11
|
+
const wsUrl = `ws://${location.host}`
|
|
12
|
+
let ws
|
|
13
|
+
|
|
14
|
+
function connect() {
|
|
15
|
+
ws = new WebSocket(wsUrl)
|
|
16
|
+
|
|
17
|
+
ws.onopen = () => {
|
|
18
|
+
setStatus(true)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ws.onclose = () => {
|
|
22
|
+
setStatus(false)
|
|
23
|
+
setTimeout(connect, 2000)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ws.onerror = () => {
|
|
27
|
+
setStatus(false)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ws.onmessage = (event) => {
|
|
31
|
+
const msg = JSON.parse(event.data)
|
|
32
|
+
|
|
33
|
+
if (msg.type === 'INIT') {
|
|
34
|
+
stateTree = msg.state || {}
|
|
35
|
+
historyLog = msg.history || []
|
|
36
|
+
renderAll()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (msg.type === 'STATE_UPDATE') {
|
|
40
|
+
stateTree = msg.state
|
|
41
|
+
if (msg.entry) {
|
|
42
|
+
historyLog.push(msg.entry)
|
|
43
|
+
if (historyLog.length > 100) historyLog.shift()
|
|
44
|
+
if (msg.entry.store === selectedStore && msg.entry.diff) {
|
|
45
|
+
lastDiff[selectedStore] = msg.entry.diff
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
flashStore(msg.entry?.store)
|
|
49
|
+
renderAll()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (msg.type === 'CLEAR') {
|
|
53
|
+
stateTree = {}
|
|
54
|
+
historyLog = []
|
|
55
|
+
selectedStore = null
|
|
56
|
+
selectedHistoryId = null
|
|
57
|
+
lastDiff = {}
|
|
58
|
+
renderAll()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setStatus(connected) {
|
|
64
|
+
const pill = document.getElementById('status-pill')
|
|
65
|
+
const text = document.getElementById('status-text')
|
|
66
|
+
pill.className = 'status-pill' + (connected ? ' connected' : '')
|
|
67
|
+
text.textContent = connected ? 'connected' : 'disconnected'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── Render all ── */
|
|
71
|
+
function renderAll() {
|
|
72
|
+
renderSidebar()
|
|
73
|
+
renderMain()
|
|
74
|
+
renderHistory()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── Sidebar ── */
|
|
78
|
+
const storeColors = ['#7c6af7', '#56e0a0', '#f7a26a', '#f06c8a', '#7ec8e3', '#c8e37e']
|
|
79
|
+
function storeColor(name) {
|
|
80
|
+
let hash = 0
|
|
81
|
+
for (let c of name) hash = (hash * 31 + c.charCodeAt(0)) & 0xffffffff
|
|
82
|
+
return storeColors[Math.abs(hash) % storeColors.length]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderSidebar() {
|
|
86
|
+
const list = document.getElementById('store-list')
|
|
87
|
+
const stores = Object.keys(stateTree)
|
|
88
|
+
|
|
89
|
+
if (stores.length === 0) {
|
|
90
|
+
list.innerHTML = ''
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
list.innerHTML = stores.map(name => {
|
|
95
|
+
const color = storeColor(name)
|
|
96
|
+
const initial = name[0].toUpperCase()
|
|
97
|
+
const active = name === selectedStore ? ' active' : ''
|
|
98
|
+
return `
|
|
99
|
+
<div class="store-item${active}" onclick="selectStore('${name}')">
|
|
100
|
+
<div class="store-icon" style="color:${color};border:1px solid ${color}33">${initial}</div>
|
|
101
|
+
<span class="store-name">${name}</span>
|
|
102
|
+
<div class="store-flash" id="flash-${name}"></div>
|
|
103
|
+
</div>`
|
|
104
|
+
}).join('')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function flashStore(name) {
|
|
108
|
+
if (!name) return
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
const el = document.getElementById(`flash-${name}`)
|
|
111
|
+
if (el) {
|
|
112
|
+
el.parentElement.classList.remove('flash')
|
|
113
|
+
void el.parentElement.offsetWidth
|
|
114
|
+
el.parentElement.classList.add('flash')
|
|
115
|
+
}
|
|
116
|
+
}, 0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectStore(name) {
|
|
120
|
+
selectedStore = name
|
|
121
|
+
selectedHistoryId = null
|
|
122
|
+
renderAll()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Main panel ── */
|
|
126
|
+
function renderMain() {
|
|
127
|
+
const title = document.getElementById('main-title')
|
|
128
|
+
const tag = document.getElementById('main-tag')
|
|
129
|
+
const emptyState = document.getElementById('empty-state')
|
|
130
|
+
const treeView = document.getElementById('tree-view')
|
|
131
|
+
const rawView = document.getElementById('raw-view')
|
|
132
|
+
const diffView = document.getElementById('diff-view')
|
|
133
|
+
|
|
134
|
+
const hasStores = Object.keys(stateTree).length > 0
|
|
135
|
+
|
|
136
|
+
if (!selectedStore || !stateTree[selectedStore]) {
|
|
137
|
+
emptyState.style.display = hasStores ? '' : ''
|
|
138
|
+
treeView.style.display = 'none'
|
|
139
|
+
rawView.style.display = 'none'
|
|
140
|
+
diffView.style.display = 'none'
|
|
141
|
+
emptyState.style.display = ''
|
|
142
|
+
title.textContent = '—'
|
|
143
|
+
tag.textContent = 'no store selected'
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const data = selectedHistoryId != null
|
|
148
|
+
? (historyLog.find(h => h.id === selectedHistoryId)?.state?.[selectedStore] ?? stateTree[selectedStore])
|
|
149
|
+
: stateTree[selectedStore]
|
|
150
|
+
|
|
151
|
+
title.textContent = selectedStore
|
|
152
|
+
tag.textContent = selectedHistoryId != null ? `time travel #${selectedHistoryId}` : 'live'
|
|
153
|
+
tag.style.color = selectedHistoryId != null ? 'var(--accent3)' : 'var(--text-dim)'
|
|
154
|
+
|
|
155
|
+
emptyState.style.display = 'none'
|
|
156
|
+
treeView.style.display = activeTab === 'tree' ? '' : 'none'
|
|
157
|
+
rawView.style.display = activeTab === 'raw' ? '' : 'none'
|
|
158
|
+
diffView.style.display = activeTab === 'diff' ? '' : 'none'
|
|
159
|
+
|
|
160
|
+
if (activeTab === 'tree') {
|
|
161
|
+
treeView.innerHTML = ''
|
|
162
|
+
treeView.appendChild(renderJsonTree(data, selectedStore))
|
|
163
|
+
} else if (activeTab === 'raw') {
|
|
164
|
+
document.getElementById('raw-content').textContent = JSON.stringify(data, null, 2)
|
|
165
|
+
} else if (activeTab === 'diff') {
|
|
166
|
+
renderDiffView(diffView, lastDiff[selectedStore] || {})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ── JSON Tree ── */
|
|
171
|
+
function renderJsonTree(value, path) {
|
|
172
|
+
const wrap = document.createElement('div')
|
|
173
|
+
wrap.className = 'json-tree'
|
|
174
|
+
buildNode(wrap, value, null, path, 0)
|
|
175
|
+
return wrap
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildNode(parent, value, key, path, depth) {
|
|
179
|
+
const type = typeof value
|
|
180
|
+
const row = document.createElement('div')
|
|
181
|
+
row.className = 'json-row'
|
|
182
|
+
|
|
183
|
+
const isObj = value !== null && type === 'object'
|
|
184
|
+
const isArr = Array.isArray(value)
|
|
185
|
+
const childCount = isObj ? Object.keys(value).length : 0
|
|
186
|
+
|
|
187
|
+
if (isObj && childCount > 0) {
|
|
188
|
+
const collapsed = collapsedPaths.has(path)
|
|
189
|
+
const toggle = document.createElement('span')
|
|
190
|
+
toggle.className = 'json-collapse'
|
|
191
|
+
toggle.textContent = collapsed ? '▶' : '▼'
|
|
192
|
+
toggle.onclick = () => {
|
|
193
|
+
if (collapsedPaths.has(path)) collapsedPaths.delete(path)
|
|
194
|
+
else collapsedPaths.add(path)
|
|
195
|
+
renderMain()
|
|
196
|
+
}
|
|
197
|
+
row.appendChild(toggle)
|
|
198
|
+
if (key !== null) {
|
|
199
|
+
const k = document.createElement('span')
|
|
200
|
+
k.className = 'json-key'
|
|
201
|
+
k.textContent = JSON.stringify(key)
|
|
202
|
+
row.appendChild(k)
|
|
203
|
+
row.appendChild(makeSpan('json-colon', ': '))
|
|
204
|
+
}
|
|
205
|
+
row.appendChild(makeSpan('', isArr ? `[${childCount}]` : `{${childCount}}`))
|
|
206
|
+
row.style.color = 'var(--text-dim)'
|
|
207
|
+
parent.appendChild(row)
|
|
208
|
+
|
|
209
|
+
if (!collapsed) {
|
|
210
|
+
const kids = document.createElement('div')
|
|
211
|
+
kids.className = 'json-children'
|
|
212
|
+
for (const [k2, v2] of Object.entries(value)) {
|
|
213
|
+
buildNode(kids, v2, k2, `${path}.${k2}`, depth + 1)
|
|
214
|
+
}
|
|
215
|
+
parent.appendChild(kids)
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
const spacer = document.createElement('span')
|
|
219
|
+
spacer.style.display = 'inline-block'
|
|
220
|
+
spacer.style.width = '14px'
|
|
221
|
+
row.appendChild(spacer)
|
|
222
|
+
|
|
223
|
+
if (key !== null) {
|
|
224
|
+
row.appendChild(makeSpan('json-key', JSON.stringify(key)))
|
|
225
|
+
row.appendChild(makeSpan('json-colon', ': '))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (value === null) {
|
|
229
|
+
row.appendChild(makeSpan('json-null', 'null'))
|
|
230
|
+
} else if (type === 'string') {
|
|
231
|
+
row.appendChild(makeSpan('json-string', JSON.stringify(value)))
|
|
232
|
+
} else if (type === 'number') {
|
|
233
|
+
row.appendChild(makeSpan('json-number', String(value)))
|
|
234
|
+
} else if (type === 'boolean') {
|
|
235
|
+
row.appendChild(makeSpan('json-bool', String(value)))
|
|
236
|
+
} else {
|
|
237
|
+
row.appendChild(makeSpan('', '{}'))
|
|
238
|
+
}
|
|
239
|
+
parent.appendChild(row)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function makeSpan(cls, text) {
|
|
244
|
+
const s = document.createElement('span')
|
|
245
|
+
if (cls) s.className = cls
|
|
246
|
+
s.textContent = text
|
|
247
|
+
return s
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ── Diff view ── */
|
|
251
|
+
function renderDiffView(container, diff) {
|
|
252
|
+
container.innerHTML = ''
|
|
253
|
+
if (!diff || Object.keys(diff).length === 0) {
|
|
254
|
+
const empty = document.createElement('div')
|
|
255
|
+
empty.className = 'empty-state'
|
|
256
|
+
empty.innerHTML = '<div class="big">≈</div><div class="hint">No diff recorded yet.<br>Change a store value to see what changed.</div>'
|
|
257
|
+
container.appendChild(empty)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const [key, change] of Object.entries(diff)) {
|
|
262
|
+
const line = document.createElement('div')
|
|
263
|
+
line.className = 'diff-line changed'
|
|
264
|
+
line.innerHTML = `
|
|
265
|
+
<span class="diff-key">${key}</span>
|
|
266
|
+
<span class="diff-arrow">:</span>
|
|
267
|
+
<span class="diff-old">${JSON.stringify(change.from)}</span>
|
|
268
|
+
<span class="diff-arrow">→</span>
|
|
269
|
+
<span class="diff-new">${JSON.stringify(change.to)}</span>
|
|
270
|
+
`
|
|
271
|
+
container.appendChild(line)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ── History panel ── */
|
|
276
|
+
function renderHistory() {
|
|
277
|
+
const list = document.getElementById('history-list')
|
|
278
|
+
const count = document.getElementById('history-count')
|
|
279
|
+
count.textContent = historyLog.length
|
|
280
|
+
|
|
281
|
+
if (historyLog.length === 0) {
|
|
282
|
+
list.innerHTML = '<div style="padding:12px 14px;color:var(--text-dim);font-size:11px">No history yet</div>'
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
list.innerHTML = [...historyLog].reverse().map(entry => {
|
|
287
|
+
const active = entry.id === selectedHistoryId ? ' active' : ''
|
|
288
|
+
const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 2 })
|
|
289
|
+
const pills = entry.diff ? Object.entries(entry.diff).slice(0, 4).map(([k]) =>
|
|
290
|
+
`<span class="hist-pill change">${k}</span>`
|
|
291
|
+
).join('') : ''
|
|
292
|
+
return `
|
|
293
|
+
<div class="hist-item${active}" onclick="selectHistory(${entry.id})">
|
|
294
|
+
<span class="hist-store">${entry.store}</span>
|
|
295
|
+
<span class="hist-time">${time}</span>
|
|
296
|
+
${pills ? `<div class="hist-diff-pills">${pills}</div>` : ''}
|
|
297
|
+
</div>`
|
|
298
|
+
}).join('')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function selectHistory(id) {
|
|
302
|
+
if (selectedHistoryId === id) {
|
|
303
|
+
selectedHistoryId = null
|
|
304
|
+
} else {
|
|
305
|
+
selectedHistoryId = id
|
|
306
|
+
const entry = historyLog.find(h => h.id === id)
|
|
307
|
+
if (entry) selectedStore = entry.store
|
|
308
|
+
}
|
|
309
|
+
renderAll()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ── Tabs ── */
|
|
313
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
314
|
+
tab.addEventListener('click', () => {
|
|
315
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'))
|
|
316
|
+
tab.classList.add('active')
|
|
317
|
+
activeTab = tab.dataset.tab
|
|
318
|
+
renderMain()
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
/* ── Clear ── */
|
|
323
|
+
document.getElementById('btn-clear').addEventListener('click', () => {
|
|
324
|
+
fetch('/state', { method: 'DELETE' }).catch(() => {})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
/* ── Boot ── */
|
|
328
|
+
connect()
|
|
329
|
+
renderAll()
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Valtio Inspector</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0d0f14;
|
|
14
|
+
--bg2: #12151c;
|
|
15
|
+
--bg3: #191d27;
|
|
16
|
+
--bg4: #1f2432;
|
|
17
|
+
--border: #262c3d;
|
|
18
|
+
--border2: #2e3549;
|
|
19
|
+
--text: #c8d0e8;
|
|
20
|
+
--text-dim: #5a6480;
|
|
21
|
+
--text-mid: #8892b0;
|
|
22
|
+
--accent: #7c6af7;
|
|
23
|
+
--accent2: #56e0a0;
|
|
24
|
+
--accent3: #f7a26a;
|
|
25
|
+
--accent4: #f06c8a;
|
|
26
|
+
--add: #56e0a0;
|
|
27
|
+
--remove: #f06c8a;
|
|
28
|
+
--change: #f7a26a;
|
|
29
|
+
--dot-size: 6px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 13px; overflow: hidden; }
|
|
33
|
+
|
|
34
|
+
/* ── Layout ── */
|
|
35
|
+
#app { display: grid; grid-template-columns: 220px 1fr 260px; grid-template-rows: 48px 1fr; height: 100vh; }
|
|
36
|
+
|
|
37
|
+
/* ── Topbar ── */
|
|
38
|
+
#topbar {
|
|
39
|
+
grid-column: 1 / -1;
|
|
40
|
+
display: flex; align-items: center; gap: 16px;
|
|
41
|
+
padding: 0 20px;
|
|
42
|
+
background: var(--bg2);
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
position: relative;
|
|
45
|
+
z-index: 10;
|
|
46
|
+
}
|
|
47
|
+
#topbar .logo { display: flex; align-items: center; gap: 8px; font-family: 'Space Mono', monospace; font-size: 14px; font-weight: 700; letter-spacing: -0.02em; }
|
|
48
|
+
#topbar .logo .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent); animation: pulse 2s ease-in-out infinite; }
|
|
49
|
+
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.6;transform:scale(.85)} }
|
|
50
|
+
#topbar .spacer { flex: 1; }
|
|
51
|
+
.status-pill { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-dim); padding: 4px 10px; border-radius: 20px; border: 1px solid var(--border); }
|
|
52
|
+
.status-pill .led { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); transition: background .3s, box-shadow .3s; }
|
|
53
|
+
.status-pill.connected .led { background: var(--accent2); box-shadow: 0 0 8px var(--accent2); }
|
|
54
|
+
.status-pill.connected { border-color: #2a4040; color: var(--accent2); }
|
|
55
|
+
#btn-clear { background: none; border: 1px solid var(--border); border-radius: 6px; color: var(--text-dim); cursor: pointer; padding: 4px 12px; font-family: inherit; font-size: 11px; transition: all .2s; }
|
|
56
|
+
#btn-clear:hover { border-color: var(--accent4); color: var(--accent4); }
|
|
57
|
+
|
|
58
|
+
/* ── Sidebar ── */
|
|
59
|
+
#sidebar {
|
|
60
|
+
background: var(--bg2);
|
|
61
|
+
border-right: 1px solid var(--border);
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
display: flex; flex-direction: column;
|
|
64
|
+
}
|
|
65
|
+
#sidebar-head { padding: 12px 14px 8px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .12em; color: var(--text-dim); }
|
|
66
|
+
.store-item {
|
|
67
|
+
display: flex; align-items: center; gap: 10px;
|
|
68
|
+
padding: 8px 14px; cursor: pointer;
|
|
69
|
+
border-left: 2px solid transparent;
|
|
70
|
+
transition: background .15s, border-color .15s;
|
|
71
|
+
position: relative;
|
|
72
|
+
}
|
|
73
|
+
.store-item:hover { background: var(--bg3); }
|
|
74
|
+
.store-item.active { background: var(--bg3); border-left-color: var(--accent); }
|
|
75
|
+
.store-item .store-icon { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 11px; background: var(--bg4); color: var(--accent); flex-shrink: 0; }
|
|
76
|
+
.store-item .store-name { flex: 1; color: var(--text); font-size: 12px; }
|
|
77
|
+
.store-item .store-flash { position: absolute; inset: 0; background: var(--accent); opacity: 0; pointer-events: none; border-radius: 0; }
|
|
78
|
+
.store-item.flash .store-flash { animation: flash-anim .4s ease-out; }
|
|
79
|
+
@keyframes flash-anim { 0%{opacity:.12} 100%{opacity:0} }
|
|
80
|
+
|
|
81
|
+
/* ── Main ── */
|
|
82
|
+
#main { display: flex; flex-direction: column; overflow: hidden; background: var(--bg); }
|
|
83
|
+
#main-head { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--border); background: var(--bg2); flex-shrink: 0; }
|
|
84
|
+
#main-head .store-label { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
85
|
+
#main-head .store-tag { font-size: 10px; color: var(--text-dim); background: var(--bg4); border: 1px solid var(--border); padding: 2px 8px; border-radius: 4px; }
|
|
86
|
+
#tabs { display: flex; gap: 2px; padding: 8px 16px 0; background: var(--bg2); border-bottom: 1px solid var(--border); }
|
|
87
|
+
.tab { padding: 6px 14px 7px; cursor: pointer; font-size: 11px; color: var(--text-dim); border-bottom: 2px solid transparent; transition: all .2s; border-radius: 4px 4px 0 0; }
|
|
88
|
+
.tab:hover { color: var(--text); background: var(--bg3); }
|
|
89
|
+
.tab.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--bg3); }
|
|
90
|
+
#view { flex: 1; overflow-y: auto; padding: 16px; }
|
|
91
|
+
|
|
92
|
+
/* ── JSON tree ── */
|
|
93
|
+
.json-tree { line-height: 1.7; }
|
|
94
|
+
.json-node { display: flex; flex-direction: column; }
|
|
95
|
+
.json-row { display: flex; align-items: baseline; gap: 6px; cursor: default; padding: 1px 4px; border-radius: 3px; }
|
|
96
|
+
.json-row:hover { background: var(--bg3); }
|
|
97
|
+
.json-key { color: #7ec8e3; }
|
|
98
|
+
.json-colon { color: var(--text-dim); }
|
|
99
|
+
.json-string { color: var(--accent2); }
|
|
100
|
+
.json-number { color: #f9c46b; }
|
|
101
|
+
.json-bool { color: var(--accent3); }
|
|
102
|
+
.json-null { color: var(--text-dim); }
|
|
103
|
+
.json-collapse { color: var(--text-dim); font-size: 10px; cursor: pointer; user-select: none; padding: 0 3px; }
|
|
104
|
+
.json-collapse:hover { color: var(--text); }
|
|
105
|
+
.json-children { padding-left: 18px; border-left: 1px solid var(--border); margin-left: 8px; }
|
|
106
|
+
|
|
107
|
+
/* ── History panel ── */
|
|
108
|
+
#history-panel { background: var(--bg2); border-left: 1px solid var(--border); overflow-y: auto; display: flex; flex-direction: column; }
|
|
109
|
+
#history-head { padding: 12px 14px 8px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .12em; color: var(--text-dim); border-bottom: 1px solid var(--border); flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; }
|
|
110
|
+
#history-count { font-size: 10px; color: var(--accent); background: #1e1a3a; border: 1px solid #2d2660; padding: 1px 7px; border-radius: 10px; }
|
|
111
|
+
#history-list { flex: 1; overflow-y: auto; }
|
|
112
|
+
.hist-item {
|
|
113
|
+
padding: 8px 14px; cursor: pointer;
|
|
114
|
+
border-bottom: 1px solid var(--border);
|
|
115
|
+
transition: background .15s;
|
|
116
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
117
|
+
}
|
|
118
|
+
.hist-item:hover { background: var(--bg3); }
|
|
119
|
+
.hist-item.active { background: #191630; border-left: 2px solid var(--accent); }
|
|
120
|
+
.hist-item .hist-store { font-size: 11px; color: var(--accent); font-weight: 500; }
|
|
121
|
+
.hist-item .hist-time { font-size: 10px; color: var(--text-dim); }
|
|
122
|
+
.hist-item .hist-diff-pills { display: flex; flex-wrap: wrap; gap: 3px; }
|
|
123
|
+
.hist-pill { font-size: 9px; padding: 1px 6px; border-radius: 10px; font-weight: 500; }
|
|
124
|
+
.hist-pill.change { background: #2c200d; color: var(--change); border: 1px solid #3d2c12; }
|
|
125
|
+
.hist-pill.add { background: #0d2316; color: var(--add); border: 1px solid #123020; }
|
|
126
|
+
.hist-pill.remove { background: #2c0d14; color: var(--remove); border: 1px solid #3d1220; }
|
|
127
|
+
|
|
128
|
+
/* ── Empty / no store states ── */
|
|
129
|
+
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 10px; color: var(--text-dim); text-align: center; }
|
|
130
|
+
.empty-state .big { font-size: 32px; opacity: .3; }
|
|
131
|
+
.empty-state .hint { font-size: 11px; line-height: 1.8; max-width: 280px; }
|
|
132
|
+
.empty-state code { background: var(--bg3); padding: 2px 6px; border-radius: 4px; font-size: 11px; color: var(--accent); }
|
|
133
|
+
|
|
134
|
+
/* ── Diff view ── */
|
|
135
|
+
.diff-line { display: flex; gap: 8px; padding: 3px 8px; border-radius: 3px; font-size: 12px; align-items: baseline; }
|
|
136
|
+
.diff-line.changed { background: #1e1505; border-left: 2px solid var(--change); }
|
|
137
|
+
.diff-line.added { background: #061510; border-left: 2px solid var(--add); }
|
|
138
|
+
.diff-line.removed { background: #150509; border-left: 2px solid var(--remove); }
|
|
139
|
+
.diff-label { font-size: 10px; min-width: 50px; opacity: .7; }
|
|
140
|
+
.diff-key { color: #7ec8e3; }
|
|
141
|
+
.diff-arrow { color: var(--text-dim); }
|
|
142
|
+
.diff-val { color: var(--text); }
|
|
143
|
+
.diff-old { color: var(--remove); text-decoration: line-through; opacity: .7; }
|
|
144
|
+
.diff-new { color: var(--add); }
|
|
145
|
+
|
|
146
|
+
/* scrollbar */
|
|
147
|
+
::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
148
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
149
|
+
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
150
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
|
|
151
|
+
</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
<div id="app">
|
|
155
|
+
<!-- Topbar -->
|
|
156
|
+
<div id="topbar">
|
|
157
|
+
<div class="logo"><span class="dot"></span> valtio inspector</div>
|
|
158
|
+
<div class="spacer"></div>
|
|
159
|
+
<div class="status-pill" id="status-pill"><span class="led"></span><span id="status-text">disconnected</span></div>
|
|
160
|
+
<button id="btn-clear">⌫ clear</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Sidebar: stores -->
|
|
164
|
+
<div id="sidebar">
|
|
165
|
+
<div id="sidebar-head">Stores</div>
|
|
166
|
+
<div id="store-list"></div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- Main panel -->
|
|
170
|
+
<div id="main">
|
|
171
|
+
<div id="main-head">
|
|
172
|
+
<span class="store-label" id="main-title">—</span>
|
|
173
|
+
<span class="store-tag" id="main-tag">no store selected</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div id="tabs">
|
|
176
|
+
<div class="tab active" data-tab="tree">Tree</div>
|
|
177
|
+
<div class="tab" data-tab="raw">Raw JSON</div>
|
|
178
|
+
<div class="tab" data-tab="diff">Last Diff</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div id="view">
|
|
181
|
+
<div class="empty-state" id="empty-state">
|
|
182
|
+
<div class="big">◎</div>
|
|
183
|
+
<div class="hint">No stores connected yet.<br>In your app, call <code>attachInspector(store, { name: 'myStore' })</code> and make sure the inspector server is running.</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div id="tree-view" style="display:none"></div>
|
|
186
|
+
<div id="raw-view" style="display:none"><pre id="raw-content" style="line-height:1.7;color:var(--text-mid)"></pre></div>
|
|
187
|
+
<div id="diff-view" style="display:none"></div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- History panel -->
|
|
192
|
+
<div id="history-panel">
|
|
193
|
+
<div id="history-head">
|
|
194
|
+
<span>History</span>
|
|
195
|
+
<span id="history-count">0</span>
|
|
196
|
+
</div>
|
|
197
|
+
<div id="history-list"></div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<script src="app.js"></script>
|
|
202
|
+
</body>
|
|
203
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type AttachOptions = {
|
|
2
|
+
/** Display name for this store in the inspector */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Debounce delay in ms (default: 100) */
|
|
5
|
+
debounce?: number;
|
|
6
|
+
/** Custom inspector URL (default: http://localhost:7777) */
|
|
7
|
+
inspectorUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Attach a Valtio proxy store to the inspector.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* attachInspector(myStore, { name: 'auth' })
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ Wrap in process.env.NODE_ENV !== 'production' to exclude from builds.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attachInspector(state: object, options: AttachOptions): () => void;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=attachInspector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachInspector.d.ts","sourceRoot":"","sources":["../src/attachInspector.ts"],"names":[],"mappings":"AAEA,KAAK,aAAa,GAAG;IACnB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAA;IACZ,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,GACrB,MAAM,IAAI,CAmFZ"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachInspector = attachInspector;
|
|
4
|
+
const valtio_1 = require("valtio");
|
|
5
|
+
/**
|
|
6
|
+
* Attach a Valtio proxy store to the inspector.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* attachInspector(myStore, { name: 'auth' })
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ Wrap in process.env.NODE_ENV !== 'production' to exclude from builds.
|
|
12
|
+
*/
|
|
13
|
+
function attachInspector(state, options) {
|
|
14
|
+
if (typeof window === 'undefined' && typeof process !== 'undefined') {
|
|
15
|
+
// Node.js / SSR — skip silently
|
|
16
|
+
return () => { };
|
|
17
|
+
}
|
|
18
|
+
const { name, debounce = 100, inspectorUrl = 'http://localhost:7777' } = options;
|
|
19
|
+
const wsUrl = inspectorUrl.replace('http', 'ws');
|
|
20
|
+
let timeout = null;
|
|
21
|
+
let ws = null;
|
|
22
|
+
let reconnectAttempts = 0;
|
|
23
|
+
const queue = [];
|
|
24
|
+
function connect() {
|
|
25
|
+
ws = new WebSocket(wsUrl);
|
|
26
|
+
ws.onopen = () => {
|
|
27
|
+
reconnectAttempts = 0;
|
|
28
|
+
// flush queue
|
|
29
|
+
while (queue.length) {
|
|
30
|
+
ws.send(JSON.stringify(queue.shift()));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
ws.onclose = () => {
|
|
34
|
+
const delay = Math.min(1000 * 2 ** reconnectAttempts, 10000);
|
|
35
|
+
reconnectAttempts++;
|
|
36
|
+
setTimeout(connect, delay);
|
|
37
|
+
};
|
|
38
|
+
ws.onerror = () => {
|
|
39
|
+
ws?.close();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function send() {
|
|
43
|
+
const payload = {
|
|
44
|
+
store: name,
|
|
45
|
+
data: (0, valtio_1.snapshot)(state)
|
|
46
|
+
};
|
|
47
|
+
// try WS first
|
|
48
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
49
|
+
ws.send(JSON.stringify(payload));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// queue for later
|
|
53
|
+
queue.push(payload);
|
|
54
|
+
// fallback to HTTP (optional)
|
|
55
|
+
fetch(`${inspectorUrl}/state`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(payload)
|
|
59
|
+
}).catch(() => {
|
|
60
|
+
// Inspector not running — fail silently
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// init connection
|
|
65
|
+
connect();
|
|
66
|
+
// Send initial snapshot immediately
|
|
67
|
+
send();
|
|
68
|
+
const unsubscribe = (0, valtio_1.subscribe)(state, () => {
|
|
69
|
+
if (timeout)
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
timeout = setTimeout(send, debounce);
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
if (timeout)
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
unsubscribe();
|
|
77
|
+
ws?.close();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=attachInspector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachInspector.js","sourceRoot":"","sources":["../src/attachInspector.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;AAEA,qCAAuC;AAEvC,IAAA,oBAAW,GAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachInspector = void 0;
|
|
4
|
+
var attachInspector_1 = require("./attachInspector");
|
|
5
|
+
Object.defineProperty(exports, "attachInspector", { enumerable: true, get: function () { return attachInspector_1.attachInspector; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAmD;AAA1C,kHAAA,eAAe,OAAA"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAAa,QAwH9C"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startServer = startServer;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const ws_1 = require("ws");
|
|
9
|
+
const http_1 = __importDefault(require("http"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const cors_1 = __importDefault(require("cors"));
|
|
12
|
+
function startServer(port = 7777) {
|
|
13
|
+
const app = (0, express_1.default)();
|
|
14
|
+
app.use((0, cors_1.default)({
|
|
15
|
+
origin: 'http://localhost:5173'
|
|
16
|
+
}));
|
|
17
|
+
app.use(express_1.default.json());
|
|
18
|
+
const server = http_1.default.createServer(app);
|
|
19
|
+
const wss = new ws_1.WebSocketServer({ server });
|
|
20
|
+
let stateTree = {};
|
|
21
|
+
let history = [];
|
|
22
|
+
let historyIdCounter = 0;
|
|
23
|
+
const MAX_HISTORY = 100;
|
|
24
|
+
function computeDiff(prev, next) {
|
|
25
|
+
if (!prev)
|
|
26
|
+
return null;
|
|
27
|
+
const diff = {};
|
|
28
|
+
const allKeys = new Set([...Object.keys(prev || {}), ...Object.keys(next || {})]);
|
|
29
|
+
for (const key of allKeys) {
|
|
30
|
+
if (JSON.stringify(prev[key]) !== JSON.stringify(next[key])) {
|
|
31
|
+
diff[key] = { from: prev[key], to: next[key] };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(diff).length > 0 ? diff : null;
|
|
35
|
+
}
|
|
36
|
+
function broadcast(payload) {
|
|
37
|
+
const msg = JSON.stringify(payload);
|
|
38
|
+
wss.clients.forEach(client => {
|
|
39
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
40
|
+
client.send(msg);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
app.post('/state', (req, res) => {
|
|
45
|
+
const { store, data } = req.body;
|
|
46
|
+
if (!store || data === undefined) {
|
|
47
|
+
res.sendStatus(400);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const prev = stateTree[store];
|
|
51
|
+
stateTree[store] = data;
|
|
52
|
+
const diff = computeDiff(prev, data);
|
|
53
|
+
const entry = {
|
|
54
|
+
id: ++historyIdCounter,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
store,
|
|
57
|
+
state: JSON.parse(JSON.stringify(stateTree)),
|
|
58
|
+
diff: diff ?? undefined
|
|
59
|
+
};
|
|
60
|
+
history.push(entry);
|
|
61
|
+
if (history.length > MAX_HISTORY) {
|
|
62
|
+
history.shift();
|
|
63
|
+
}
|
|
64
|
+
broadcast({
|
|
65
|
+
type: 'STATE_UPDATE',
|
|
66
|
+
state: stateTree,
|
|
67
|
+
entry: { id: entry.id, timestamp: entry.timestamp, store, diff }
|
|
68
|
+
});
|
|
69
|
+
res.sendStatus(200);
|
|
70
|
+
});
|
|
71
|
+
app.get('/history', (_req, res) => res.json(history));
|
|
72
|
+
app.get('/history/:id', (req, res) => {
|
|
73
|
+
const id = parseInt(req.params.id);
|
|
74
|
+
const entry = history.find(h => h.id === id);
|
|
75
|
+
if (!entry) {
|
|
76
|
+
res.sendStatus(404);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.json(entry);
|
|
80
|
+
});
|
|
81
|
+
app.get('/state', (_req, res) => res.json(stateTree));
|
|
82
|
+
app.delete('/state', (_req, res) => {
|
|
83
|
+
stateTree = {};
|
|
84
|
+
history = [];
|
|
85
|
+
broadcast({ type: 'CLEAR' });
|
|
86
|
+
res.sendStatus(200);
|
|
87
|
+
});
|
|
88
|
+
app.use(express_1.default.static(path_1.default.join(__dirname, '../client')));
|
|
89
|
+
wss.on('connection', (ws) => {
|
|
90
|
+
ws.send(JSON.stringify({
|
|
91
|
+
type: 'INIT',
|
|
92
|
+
state: stateTree,
|
|
93
|
+
history: history.map(h => ({
|
|
94
|
+
id: h.id,
|
|
95
|
+
timestamp: h.timestamp,
|
|
96
|
+
store: h.store,
|
|
97
|
+
diff: h.diff
|
|
98
|
+
}))
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
server.listen(port, () => {
|
|
102
|
+
console.log('\x1b[36m%s\x1b[0m', ` ▶ Valtio Inspector → http://localhost:${port}`);
|
|
103
|
+
console.log('\x1b[90m%s\x1b[0m', ` WebSocket ready on ws://localhost:${port}`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// startServer()
|
|
107
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/example.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: how to wire up valtio stores with the inspector
|
|
3
|
+
* Drop this into your app as a reference.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { proxy } from 'valtio'
|
|
7
|
+
import { attachInspector } from './instrument/attachInspector'
|
|
8
|
+
|
|
9
|
+
// ── Define your stores ──────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export const authStore = proxy({
|
|
12
|
+
user: { id: 1, name: 'kfir', email: 'kfir@example.com' },
|
|
13
|
+
isLoggedIn: false,
|
|
14
|
+
token: null as string | null
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const uiStore = proxy({
|
|
18
|
+
darkMode: true,
|
|
19
|
+
sidebarOpen: false,
|
|
20
|
+
activeModal: null as string | null,
|
|
21
|
+
notifications: [] as string[]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const cartStore = proxy({
|
|
25
|
+
items: [] as Array<{ id: string; name: string; qty: number; price: number }>,
|
|
26
|
+
coupon: null as string | null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ── Attach inspector (dev only) ─────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
32
|
+
attachInspector(authStore, { name: 'auth' })
|
|
33
|
+
attachInspector(uiStore, { name: 'ui' })
|
|
34
|
+
attachInspector(cartStore, { name: 'cart' })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Test: mutate stores after 1 second ─────────────────────────────────────
|
|
38
|
+
// (remove in real app)
|
|
39
|
+
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
authStore.isLoggedIn = true
|
|
42
|
+
authStore.token = 'jwt_abc123'
|
|
43
|
+
}, 1000)
|
|
44
|
+
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
uiStore.sidebarOpen = true
|
|
47
|
+
uiStore.activeModal = 'checkout'
|
|
48
|
+
}, 2000)
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
cartStore.items.push({ id: 'p1', name: 'Mechanical Keyboard', qty: 1, price: 149.99 })
|
|
52
|
+
cartStore.items.push({ id: 'p2', name: 'USB-C Hub', qty: 2, price: 34.99 })
|
|
53
|
+
}, 3000)
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kfiross44/valtio-inspector",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A dev-only Valtio state inspector with WebSocket live updates",
|
|
5
|
+
"main": "dist/server/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"valtio-inspector": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "ts-node server/index.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"release": "bun run build && changeset publish",
|
|
13
|
+
"start": "node dist/server/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cors": "^2.8.6",
|
|
17
|
+
"express": "^4.18.2",
|
|
18
|
+
"ws": "^8.16.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"valtio": "^2.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@changesets/cli": "^2.30.0",
|
|
25
|
+
"@types/cors": "^2.8.19",
|
|
26
|
+
"@types/express": "^4.17.21",
|
|
27
|
+
"@types/node": "^20.19.39",
|
|
28
|
+
"@types/ws": "^8.5.10",
|
|
29
|
+
"ts-node": "^10.9.2",
|
|
30
|
+
"typescript": "^5.3.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/script/build.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Glob, $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
await $`rm -rf dist`
|
|
4
|
+
const files = new Glob('./src/**/*.{ts,tsx}').scan()
|
|
5
|
+
for await (const file of files) {
|
|
6
|
+
await Bun.build({
|
|
7
|
+
format: 'esm',
|
|
8
|
+
outdir: 'dist/esm',
|
|
9
|
+
external: ['*'],
|
|
10
|
+
root: 'src',
|
|
11
|
+
entrypoints: [file],
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
await $`tsc --outDir dist/types --declaration --emitDeclarationOnly --declarationMap`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Get the package version from package.json
|
|
4
|
+
PACKAGE_VERSION=$(node -p -e "require('./package.json').version || ''")
|
|
5
|
+
|
|
6
|
+
# Check if PACKAGE_VERSION is empty
|
|
7
|
+
if [ -z "$PACKAGE_VERSION" ]; then
|
|
8
|
+
echo "Error: Failed to retrieve version from package.json"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Run git-cliff with the correct tag
|
|
13
|
+
bun x git-cliff -o './CHANGELOG.md' --tag "$PACKAGE_VERSION"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { subscribe, snapshot } from 'valtio'
|
|
2
|
+
|
|
3
|
+
type AttachOptions = {
|
|
4
|
+
/** Display name for this store in the inspector */
|
|
5
|
+
name: string
|
|
6
|
+
/** Debounce delay in ms (default: 100) */
|
|
7
|
+
debounce?: number
|
|
8
|
+
/** Custom inspector URL (default: http://localhost:7777) */
|
|
9
|
+
inspectorUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Attach a Valtio proxy store to the inspector.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* attachInspector(myStore, { name: 'auth' })
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ Wrap in process.env.NODE_ENV !== 'production' to exclude from builds.
|
|
19
|
+
*/
|
|
20
|
+
export function attachInspector(
|
|
21
|
+
state: object,
|
|
22
|
+
options: AttachOptions
|
|
23
|
+
): () => void {
|
|
24
|
+
if (typeof window === 'undefined' && typeof process !== 'undefined') {
|
|
25
|
+
// Node.js / SSR — skip silently
|
|
26
|
+
return () => {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
name,
|
|
31
|
+
debounce = 100,
|
|
32
|
+
inspectorUrl = 'http://localhost:7777'
|
|
33
|
+
} = options
|
|
34
|
+
|
|
35
|
+
const wsUrl = inspectorUrl.replace('http', 'ws')
|
|
36
|
+
|
|
37
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
38
|
+
let ws: WebSocket | null = null
|
|
39
|
+
let reconnectAttempts = 0
|
|
40
|
+
const queue: any[] = []
|
|
41
|
+
|
|
42
|
+
function connect() {
|
|
43
|
+
ws = new WebSocket(wsUrl)
|
|
44
|
+
|
|
45
|
+
ws.onopen = () => {
|
|
46
|
+
reconnectAttempts = 0
|
|
47
|
+
|
|
48
|
+
// flush queue
|
|
49
|
+
while (queue.length) {
|
|
50
|
+
ws!.send(JSON.stringify(queue.shift()))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ws.onclose = () => {
|
|
55
|
+
const delay = Math.min(1000 * 2 ** reconnectAttempts, 10000)
|
|
56
|
+
reconnectAttempts++
|
|
57
|
+
|
|
58
|
+
setTimeout(connect, delay)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ws.onerror = () => {
|
|
62
|
+
ws?.close()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function send() {
|
|
67
|
+
const payload = {
|
|
68
|
+
store: name,
|
|
69
|
+
data: snapshot(state)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// try WS first
|
|
73
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
74
|
+
ws.send(JSON.stringify(payload))
|
|
75
|
+
} else {
|
|
76
|
+
// queue for later
|
|
77
|
+
queue.push(payload)
|
|
78
|
+
|
|
79
|
+
// fallback to HTTP (optional)
|
|
80
|
+
fetch(`${inspectorUrl}/state`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify(payload)
|
|
84
|
+
}).catch(() => {
|
|
85
|
+
// Inspector not running — fail silently
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// init connection
|
|
91
|
+
connect()
|
|
92
|
+
|
|
93
|
+
// Send initial snapshot immediately
|
|
94
|
+
send()
|
|
95
|
+
|
|
96
|
+
const unsubscribe = subscribe(state, () => {
|
|
97
|
+
if (timeout) clearTimeout(timeout)
|
|
98
|
+
timeout = setTimeout(send, debounce)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
if (timeout) clearTimeout(timeout)
|
|
103
|
+
unsubscribe()
|
|
104
|
+
ws?.close()
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/cli.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { attachInspector } from './attachInspector'
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
3
|
+
import http from 'http'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import cors from 'cors'
|
|
6
|
+
|
|
7
|
+
export function startServer(port: number = 7777) {
|
|
8
|
+
const app = express()
|
|
9
|
+
|
|
10
|
+
app.use(cors({
|
|
11
|
+
origin: 'http://localhost:5173'
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
app.use(express.json())
|
|
15
|
+
|
|
16
|
+
const server = http.createServer(app)
|
|
17
|
+
const wss = new WebSocketServer({ server })
|
|
18
|
+
|
|
19
|
+
interface HistoryEntry {
|
|
20
|
+
id: number
|
|
21
|
+
timestamp: number
|
|
22
|
+
store: string
|
|
23
|
+
state: Record<string, any>
|
|
24
|
+
diff?: Record<string, any>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let stateTree: Record<string, any> = {}
|
|
28
|
+
let history: HistoryEntry[] = []
|
|
29
|
+
let historyIdCounter = 0
|
|
30
|
+
const MAX_HISTORY = 100
|
|
31
|
+
|
|
32
|
+
function computeDiff(prev: any, next: any): Record<string, any> | null {
|
|
33
|
+
if (!prev) return null
|
|
34
|
+
const diff: Record<string, any> = {}
|
|
35
|
+
const allKeys = new Set([...Object.keys(prev || {}), ...Object.keys(next || {})])
|
|
36
|
+
for (const key of allKeys) {
|
|
37
|
+
if (JSON.stringify(prev[key]) !== JSON.stringify(next[key])) {
|
|
38
|
+
diff[key] = { from: prev[key], to: next[key] }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(diff).length > 0 ? diff : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function broadcast(payload: object) {
|
|
45
|
+
const msg = JSON.stringify(payload)
|
|
46
|
+
wss.clients.forEach(client => {
|
|
47
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
48
|
+
client.send(msg)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
app.post('/state', (req, res) => {
|
|
54
|
+
const { store, data } = req.body
|
|
55
|
+
if (!store || data === undefined) {
|
|
56
|
+
res.sendStatus(400)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const prev = stateTree[store]
|
|
61
|
+
stateTree[store] = data
|
|
62
|
+
|
|
63
|
+
const diff = computeDiff(prev, data)
|
|
64
|
+
|
|
65
|
+
const entry: HistoryEntry = {
|
|
66
|
+
id: ++historyIdCounter,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
store,
|
|
69
|
+
state: JSON.parse(JSON.stringify(stateTree)),
|
|
70
|
+
diff: diff ?? undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
history.push(entry)
|
|
74
|
+
if (history.length > MAX_HISTORY) {
|
|
75
|
+
history.shift()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
broadcast({
|
|
79
|
+
type: 'STATE_UPDATE',
|
|
80
|
+
state: stateTree,
|
|
81
|
+
entry: { id: entry.id, timestamp: entry.timestamp, store, diff }
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
res.sendStatus(200)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
app.get('/history', (_req, res) => res.json(history))
|
|
88
|
+
|
|
89
|
+
app.get('/history/:id', (req, res) => {
|
|
90
|
+
const id = parseInt(req.params.id)
|
|
91
|
+
const entry = history.find(h => h.id === id)
|
|
92
|
+
if (!entry) {
|
|
93
|
+
res.sendStatus(404)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
res.json(entry)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
app.get('/state', (_req, res) => res.json(stateTree))
|
|
100
|
+
|
|
101
|
+
app.delete('/state', (_req, res) => {
|
|
102
|
+
stateTree = {}
|
|
103
|
+
history = []
|
|
104
|
+
broadcast({ type: 'CLEAR' })
|
|
105
|
+
res.sendStatus(200)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
app.use(express.static(path.join(__dirname, '../client')))
|
|
109
|
+
|
|
110
|
+
wss.on('connection', (ws) => {
|
|
111
|
+
ws.send(JSON.stringify({
|
|
112
|
+
type: 'INIT',
|
|
113
|
+
state: stateTree,
|
|
114
|
+
history: history.map(h => ({
|
|
115
|
+
id: h.id,
|
|
116
|
+
timestamp: h.timestamp,
|
|
117
|
+
store: h.store,
|
|
118
|
+
diff: h.diff
|
|
119
|
+
}))
|
|
120
|
+
}))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
server.listen(port, () => {
|
|
124
|
+
console.log('\x1b[36m%s\x1b[0m', ` ▶ Valtio Inspector → http://localhost:${port}`)
|
|
125
|
+
console.log('\x1b[90m%s\x1b[0m', ` WebSocket ready on ws://localhost:${port}`)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// startServer()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "client"]
|
|
18
|
+
}
|