spark-html-devtools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/package.json +36 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +133 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ⚡ spark-html-devtools
|
|
2
|
+
|
|
3
|
+
A tiny in-page inspector for [spark-html](https://github.com/wilkinnovo/spark)
|
|
4
|
+
apps. Drop it in during development and get a ⚡ panel (bottom-right) showing:
|
|
5
|
+
|
|
6
|
+
- **Stores** — every named store and its live state.
|
|
7
|
+
- **Components** — the component tree (`[name]` hosts) with each component's
|
|
8
|
+
reactive state.
|
|
9
|
+
- **Patches** — a counter, plus a brief amber outline on whichever component just
|
|
10
|
+
re-rendered, so Spark's surgical reactivity is *visible*.
|
|
11
|
+
|
|
12
|
+
It's read-only — it never mutates your app.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { devtools } from 'spark-html-devtools';
|
|
16
|
+
|
|
17
|
+
if (import.meta.env?.DEV) devtools(); // dev only
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -D spark-html-devtools
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const stop = devtools({ open: true }); // start expanded
|
|
30
|
+
// …
|
|
31
|
+
stop(); // remove the panel + restore hooks
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires `spark-html` ≥ 0.21.4 (uses its `inspectStores()` introspection).
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-html-devtools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tiny in-page devtools panel for spark-html — live store state, component tree, and patch activity.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"homepage": "https://wilkinnovo.github.io/spark",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node test/devtools.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/wilkinnovo/spark.git",
|
|
24
|
+
"directory": "packages/spark-html-devtools"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"spark-html": "^0.21.4"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"spark-html",
|
|
31
|
+
"devtools",
|
|
32
|
+
"debug",
|
|
33
|
+
"inspector"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-devtools — in-page inspector panel for spark-html apps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface DevtoolsOptions {
|
|
6
|
+
/** Start with the panel expanded (default false). */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mount the devtools panel (a ⚡ button bottom-right). Shows live store state,
|
|
12
|
+
* the component tree with each component's reactive state, a patch counter, and
|
|
13
|
+
* flashes whichever component just re-rendered. Read-only. Returns a teardown
|
|
14
|
+
* function. Intended for development — gate it behind `import.meta.env.DEV`.
|
|
15
|
+
*/
|
|
16
|
+
export function devtools(options?: DevtoolsOptions): () => void;
|
|
17
|
+
|
|
18
|
+
declare const _default: { devtools: typeof devtools };
|
|
19
|
+
export default _default;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-devtools — a tiny in-page panel for inspecting a Spark app.
|
|
3
|
+
*
|
|
4
|
+
* import { devtools } from 'spark-html-devtools';
|
|
5
|
+
* if (import.meta.env?.DEV) devtools(); // dev only
|
|
6
|
+
*
|
|
7
|
+
* Shows, live:
|
|
8
|
+
* • every named store + its state,
|
|
9
|
+
* • the component tree ([name] hosts) + each component's reactive state,
|
|
10
|
+
* • a patch counter, and a brief amber outline on whichever component just
|
|
11
|
+
* re-rendered — so "surgical reactivity" is visible.
|
|
12
|
+
*
|
|
13
|
+
* Reads from the DOM (component scopes) and `inspectStores()`; it never mutates
|
|
14
|
+
* the app. Toggle with the ⚡ button (bottom-right).
|
|
15
|
+
*/
|
|
16
|
+
import { inspectStores } from 'spark-html';
|
|
17
|
+
|
|
18
|
+
const AMBER = '#ffd24a';
|
|
19
|
+
let booted = false;
|
|
20
|
+
|
|
21
|
+
function safe(value) {
|
|
22
|
+
const seen = new WeakSet();
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value, (_k, v) => {
|
|
25
|
+
if (typeof v === 'function') return undefined;
|
|
26
|
+
if (v && typeof v === 'object') { if (seen.has(v)) return '[circular]'; seen.add(v); }
|
|
27
|
+
return v;
|
|
28
|
+
}, 1);
|
|
29
|
+
} catch { return String(value); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function esc(s) {
|
|
33
|
+
return String(s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readScope(scope) {
|
|
37
|
+
const o = {};
|
|
38
|
+
try {
|
|
39
|
+
for (const k of Object.keys(scope)) {
|
|
40
|
+
try { const v = scope[k]; if (typeof v !== 'function') o[k] = v; } catch { /* getter threw */ }
|
|
41
|
+
}
|
|
42
|
+
} catch { /* not enumerable */ }
|
|
43
|
+
return o;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function components() {
|
|
47
|
+
return [...document.querySelectorAll('[name]')]
|
|
48
|
+
.filter((el) => el.__sparkScope)
|
|
49
|
+
.map((el) => ({ name: el.getAttribute('name'), state: readScope(el.__sparkScope), el }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mount the devtools panel. Returns a teardown function.
|
|
54
|
+
* @param {object} [options]
|
|
55
|
+
* @param {boolean} [options.open=false] Start expanded.
|
|
56
|
+
*/
|
|
57
|
+
export function devtools(options = {}) {
|
|
58
|
+
if (booted || typeof document === 'undefined') return () => {};
|
|
59
|
+
booted = true;
|
|
60
|
+
|
|
61
|
+
let patches = 0;
|
|
62
|
+
let open = !!options.open;
|
|
63
|
+
|
|
64
|
+
const root = document.createElement('div');
|
|
65
|
+
root.setAttribute('data-spark-devtools', '');
|
|
66
|
+
root.innerHTML = `
|
|
67
|
+
<button class="sdt-toggle" title="Spark devtools">⚡</button>
|
|
68
|
+
<div class="sdt-panel">
|
|
69
|
+
<div class="sdt-head"><b>⚡ spark devtools</b><span class="sdt-meta"></span><button class="sdt-x">×</button></div>
|
|
70
|
+
<div class="sdt-body"></div>
|
|
71
|
+
</div>`;
|
|
72
|
+
|
|
73
|
+
const style = document.createElement('style');
|
|
74
|
+
style.textContent = `
|
|
75
|
+
[data-spark-devtools]{position:fixed;right:14px;bottom:14px;z-index:2147483647;font:12px/1.5 "JetBrains Mono",ui-monospace,monospace}
|
|
76
|
+
[data-spark-devtools] .sdt-toggle{width:40px;height:40px;border-radius:50%;border:1px solid #333;background:#000;color:${AMBER};font-size:20px;cursor:pointer}
|
|
77
|
+
[data-spark-devtools] .sdt-panel{display:none;position:absolute;right:0;bottom:48px;width:340px;max-height:70vh;overflow:auto;background:#0a0a0a;color:#e8e8e8;border:1px solid #333}
|
|
78
|
+
[data-spark-devtools][data-open] .sdt-panel{display:block}
|
|
79
|
+
[data-spark-devtools][data-open] .sdt-toggle{outline:2px solid ${AMBER}}
|
|
80
|
+
[data-spark-devtools] .sdt-head{display:flex;align-items:center;gap:8px;padding:9px 12px;border-bottom:1px solid #1a1a1a;position:sticky;top:0;background:#0a0a0a}
|
|
81
|
+
[data-spark-devtools] .sdt-head b{color:#fff}
|
|
82
|
+
[data-spark-devtools] .sdt-meta{margin-left:auto;color:#888}
|
|
83
|
+
[data-spark-devtools] .sdt-x{background:none;border:none;color:#888;font-size:16px;cursor:pointer}
|
|
84
|
+
[data-spark-devtools] .sdt-body{padding:10px 12px}
|
|
85
|
+
[data-spark-devtools] .sdt-sec{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#666;margin:12px 0 6px}
|
|
86
|
+
[data-spark-devtools] .sdt-item{border-left:2px solid #1a1a1a;padding:2px 0 2px 10px;margin:4px 0}
|
|
87
|
+
[data-spark-devtools] .sdt-name{color:${AMBER}}
|
|
88
|
+
[data-spark-devtools] pre{margin:2px 0 0;white-space:pre-wrap;color:#aaa;font-size:11px}
|
|
89
|
+
.sdt-flash{outline:1px solid ${AMBER} !important;outline-offset:-1px;transition:outline .15s}`;
|
|
90
|
+
document.head.appendChild(style);
|
|
91
|
+
document.body.appendChild(root);
|
|
92
|
+
|
|
93
|
+
const panelBody = root.querySelector('.sdt-body');
|
|
94
|
+
const meta = root.querySelector('.sdt-meta');
|
|
95
|
+
const setOpen = (v) => { open = v; if (v) root.setAttribute('data-open', ''); else root.removeAttribute('data-open'); if (v) render(); };
|
|
96
|
+
root.querySelector('.sdt-toggle').addEventListener('click', () => setOpen(!open));
|
|
97
|
+
root.querySelector('.sdt-x').addEventListener('click', () => setOpen(false));
|
|
98
|
+
|
|
99
|
+
function render() {
|
|
100
|
+
const stores = inspectStores();
|
|
101
|
+
const comps = components();
|
|
102
|
+
meta.textContent = `${comps.length} comp · ${patches} patches`;
|
|
103
|
+
const storeRows = Object.keys(stores).map((n) =>
|
|
104
|
+
`<div class="sdt-item"><span class="sdt-name">${esc(n)}</span><pre>${esc(safe(stores[n]))}</pre></div>`).join('') || '<div class="sdt-item">—</div>';
|
|
105
|
+
const compRows = comps.map((c) =>
|
|
106
|
+
`<div class="sdt-item"><span class="sdt-name">${esc(c.name)}</span><pre>${esc(safe(c.state))}</pre></div>`).join('') || '<div class="sdt-item">—</div>';
|
|
107
|
+
panelBody.innerHTML =
|
|
108
|
+
`<div class="sdt-sec">stores</div>${storeRows}<div class="sdt-sec">components</div>${compRows}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Patch hook (the runtime calls this per component patch). Chain any existing.
|
|
112
|
+
const prev = globalThis.__sparkTestOnPatch;
|
|
113
|
+
globalThis.__sparkTestOnPatch = (el) => {
|
|
114
|
+
patches++;
|
|
115
|
+
if (typeof prev === 'function') try { prev(el); } catch { /* ignore */ }
|
|
116
|
+
if (el && el.classList) {
|
|
117
|
+
el.classList.add('sdt-flash');
|
|
118
|
+
setTimeout(() => el.classList && el.classList.remove('sdt-flash'), 180);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const timer = setInterval(() => { if (open) render(); }, 400);
|
|
123
|
+
if (open) { root.setAttribute('data-open', ''); render(); }
|
|
124
|
+
|
|
125
|
+
return function teardown() {
|
|
126
|
+
clearInterval(timer);
|
|
127
|
+
globalThis.__sparkTestOnPatch = prev;
|
|
128
|
+
root.remove(); style.remove();
|
|
129
|
+
booted = false;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default { devtools };
|