@erickxavier/no-js 1.5.2 → 1.7.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 +2 -2
- package/dist/cjs/no.js +6 -6
- package/dist/cjs/no.js.map +4 -4
- package/dist/esm/no.js +6 -6
- package/dist/esm/no.js.map +4 -4
- package/dist/iife/no.js +6 -6
- package/dist/iife/no.js.map +4 -4
- package/package.json +1 -1
- package/src/context.js +28 -2
- package/src/devtools.js +264 -0
- package/src/directives/http.js +3 -0
- package/src/directives/state.js +5 -0
- package/src/directives/validation.js +401 -64
- package/src/index.js +12 -13
- package/src/registry.js +18 -0
- package/src/router.js +8 -0
package/package.json
CHANGED
package/src/context.js
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
// REACTIVE CONTEXT
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _stores, _refs, _routerInstance, _currentEl } from "./globals.js";
|
|
5
|
+
import { _config, _stores, _refs, _routerInstance, _currentEl } from "./globals.js";
|
|
6
6
|
import { _i18n } from "./i18n.js";
|
|
7
|
+
import { _devtoolsEmit, _ctxRegistry } from "./devtools.js";
|
|
7
8
|
|
|
8
9
|
let _batchDepth = 0;
|
|
9
10
|
const _batchQueue = new Set();
|
|
11
|
+
let _ctxId = 0;
|
|
12
|
+
|
|
13
|
+
export function _resetCtxId() { _ctxId = 0; }
|
|
10
14
|
|
|
11
15
|
export function _startBatch() {
|
|
12
16
|
_batchDepth++;
|
|
17
|
+
_devtoolsEmit("batch:start", { depth: _batchDepth });
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export function _endBatch() {
|
|
16
21
|
_batchDepth--;
|
|
17
22
|
if (_batchDepth === 0 && _batchQueue.size > 0) {
|
|
23
|
+
_devtoolsEmit("batch:end", { depth: 0, queueSize: _batchQueue.size });
|
|
18
24
|
const fns = [..._batchQueue];
|
|
19
25
|
_batchQueue.clear();
|
|
20
26
|
fns.forEach((fn) => {
|
|
@@ -28,6 +34,7 @@ export function createContext(data = {}, parent = null) {
|
|
|
28
34
|
const listeners = new Set();
|
|
29
35
|
const raw = {};
|
|
30
36
|
Object.assign(raw, data);
|
|
37
|
+
if (_config.devtools) raw.__devtoolsId = ++_ctxId;
|
|
31
38
|
let notifying = false;
|
|
32
39
|
|
|
33
40
|
function notify() {
|
|
@@ -94,7 +101,15 @@ export function createContext(data = {}, parent = null) {
|
|
|
94
101
|
set(target, key, value) {
|
|
95
102
|
const old = target[key];
|
|
96
103
|
target[key] = value;
|
|
97
|
-
if (old !== value)
|
|
104
|
+
if (old !== value) {
|
|
105
|
+
notify();
|
|
106
|
+
_devtoolsEmit("ctx:updated", {
|
|
107
|
+
id: target.__devtoolsId,
|
|
108
|
+
key,
|
|
109
|
+
oldValue: old,
|
|
110
|
+
newValue: value,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
98
113
|
return true;
|
|
99
114
|
},
|
|
100
115
|
has(target, key) {
|
|
@@ -105,6 +120,17 @@ export function createContext(data = {}, parent = null) {
|
|
|
105
120
|
};
|
|
106
121
|
|
|
107
122
|
const proxy = new Proxy(raw, handler);
|
|
123
|
+
|
|
124
|
+
if (_config.devtools && raw.__devtoolsId) {
|
|
125
|
+
_ctxRegistry.set(raw.__devtoolsId, proxy);
|
|
126
|
+
_devtoolsEmit("ctx:created", {
|
|
127
|
+
id: raw.__devtoolsId,
|
|
128
|
+
parentId: parent?.__raw?.__devtoolsId ?? null,
|
|
129
|
+
keys: Object.keys(data),
|
|
130
|
+
elementTag: _currentEl?.tagName?.toLowerCase() ?? null,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
return proxy;
|
|
109
135
|
}
|
|
110
136
|
|
package/src/devtools.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// DEVTOOLS PROTOCOL
|
|
3
|
+
// Zero-overhead runtime inspection, mutation, and monitoring.
|
|
4
|
+
// All hooks are guarded by _config.devtools — no cost when disabled.
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { _config, _stores, _refs, _routerInstance } from "./globals.js";
|
|
8
|
+
import { _i18n } from "./i18n.js";
|
|
9
|
+
|
|
10
|
+
// ─── Context registry (populated by createContext when devtools enabled) ────
|
|
11
|
+
// Maps __devtoolsId → Proxy reference for inspect/mutate commands.
|
|
12
|
+
export const _ctxRegistry = new Map();
|
|
13
|
+
|
|
14
|
+
// ─── Emit a devtools event ──────────────────────────────────────────────────
|
|
15
|
+
export function _devtoolsEmit(type, data) {
|
|
16
|
+
if (!_config.devtools || typeof window === "undefined") return;
|
|
17
|
+
window.dispatchEvent(
|
|
18
|
+
new CustomEvent("nojs:devtools", {
|
|
19
|
+
detail: { type, data, timestamp: Date.now() },
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Serialization helpers ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function _safeSnapshot(proxy) {
|
|
27
|
+
if (!proxy || !proxy.__isProxy) return null;
|
|
28
|
+
const raw = proxy.__raw;
|
|
29
|
+
const snapshot = {};
|
|
30
|
+
for (const key of Object.keys(raw)) {
|
|
31
|
+
if (key.startsWith("__")) continue;
|
|
32
|
+
const val = raw[key];
|
|
33
|
+
if (val && typeof val === "object" && val.__isProxy) {
|
|
34
|
+
snapshot[key] = _safeSnapshot(val);
|
|
35
|
+
} else {
|
|
36
|
+
try {
|
|
37
|
+
// Verify serializable — drop functions and circular refs
|
|
38
|
+
JSON.stringify(val);
|
|
39
|
+
snapshot[key] = val;
|
|
40
|
+
} catch {
|
|
41
|
+
snapshot[key] = String(val);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return snapshot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _elementTag(el) {
|
|
49
|
+
if (!el || !el.tagName) return null;
|
|
50
|
+
const tag = el.tagName.toLowerCase();
|
|
51
|
+
const id = el.id ? `#${el.id}` : "";
|
|
52
|
+
const cls = el.className && typeof el.className === "string"
|
|
53
|
+
? "." + el.className.trim().split(/\s+/).join(".")
|
|
54
|
+
: "";
|
|
55
|
+
return tag + id + cls;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Inspect commands ───────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function _inspectElement(selector) {
|
|
61
|
+
const el = document.querySelector(selector);
|
|
62
|
+
if (!el) return { error: "Element not found", selector };
|
|
63
|
+
const ctx = el.__ctx;
|
|
64
|
+
return {
|
|
65
|
+
selector,
|
|
66
|
+
tag: _elementTag(el),
|
|
67
|
+
hasContext: !!ctx,
|
|
68
|
+
contextId: ctx?.__raw?.__devtoolsId ?? null,
|
|
69
|
+
data: ctx ? _safeSnapshot(ctx) : null,
|
|
70
|
+
directives: [...el.attributes]
|
|
71
|
+
.filter((a) => !["class", "id", "style"].includes(a.name))
|
|
72
|
+
.map((a) => ({ name: a.name, value: a.value })),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _inspectStore(name) {
|
|
77
|
+
const store = _stores[name];
|
|
78
|
+
if (!store) return { error: "Store not found", name };
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
contextId: store.__raw?.__devtoolsId ?? null,
|
|
82
|
+
data: _safeSnapshot(store),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _inspectTree(selector) {
|
|
87
|
+
const root = selector ? document.querySelector(selector) : document.body;
|
|
88
|
+
if (!root) return { error: "Root not found", selector };
|
|
89
|
+
|
|
90
|
+
function walk(el) {
|
|
91
|
+
const ctx = el.__ctx;
|
|
92
|
+
const node = {
|
|
93
|
+
tag: _elementTag(el),
|
|
94
|
+
contextId: ctx?.__raw?.__devtoolsId ?? null,
|
|
95
|
+
children: [],
|
|
96
|
+
};
|
|
97
|
+
for (const child of el.children) {
|
|
98
|
+
if (child.tagName === "TEMPLATE" || child.tagName === "SCRIPT") continue;
|
|
99
|
+
if (child.__ctx || child.__declared) {
|
|
100
|
+
node.children.push(walk(child));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return node;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return walk(root);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _mutateContext(id, key, value) {
|
|
110
|
+
const proxy = _ctxRegistry.get(id);
|
|
111
|
+
if (!proxy) return { error: "Context not found", id };
|
|
112
|
+
proxy[key] = value;
|
|
113
|
+
return { ok: true, id, key };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _mutateStore(name, key, value) {
|
|
117
|
+
const store = _stores[name];
|
|
118
|
+
if (!store) return { error: "Store not found", name };
|
|
119
|
+
store[key] = value;
|
|
120
|
+
return { ok: true, name, key };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _getStats() {
|
|
124
|
+
let listenerCount = 0;
|
|
125
|
+
for (const [, proxy] of _ctxRegistry) {
|
|
126
|
+
if (proxy.__listeners) listenerCount += proxy.__listeners.size;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
contexts: _ctxRegistry.size,
|
|
130
|
+
stores: Object.keys(_stores).length,
|
|
131
|
+
listeners: listenerCount,
|
|
132
|
+
refs: Object.keys(_refs).length,
|
|
133
|
+
hasRouter: !!_routerInstance,
|
|
134
|
+
locale: _i18n.locale,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Highlight overlay ──────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
let _highlightOverlay = null;
|
|
141
|
+
|
|
142
|
+
function _highlightElement(selector) {
|
|
143
|
+
_unhighlight();
|
|
144
|
+
const el = document.querySelector(selector);
|
|
145
|
+
if (!el) return;
|
|
146
|
+
const rect = el.getBoundingClientRect();
|
|
147
|
+
_highlightOverlay = document.createElement("div");
|
|
148
|
+
_highlightOverlay.id = "__nojs_devtools_highlight__";
|
|
149
|
+
Object.assign(_highlightOverlay.style, {
|
|
150
|
+
position: "fixed",
|
|
151
|
+
top: rect.top + "px",
|
|
152
|
+
left: rect.left + "px",
|
|
153
|
+
width: rect.width + "px",
|
|
154
|
+
height: rect.height + "px",
|
|
155
|
+
background: "rgba(66, 133, 244, 0.25)",
|
|
156
|
+
border: "2px solid rgba(66, 133, 244, 0.8)",
|
|
157
|
+
pointerEvents: "none",
|
|
158
|
+
zIndex: "2147483647",
|
|
159
|
+
borderRadius: "3px",
|
|
160
|
+
});
|
|
161
|
+
document.body.appendChild(_highlightOverlay);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _unhighlight() {
|
|
165
|
+
if (_highlightOverlay) {
|
|
166
|
+
_highlightOverlay.remove();
|
|
167
|
+
_highlightOverlay = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Command handler ────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function _handleDevtoolsCommand(event) {
|
|
174
|
+
const { command, args = {} } = event.detail || {};
|
|
175
|
+
let result;
|
|
176
|
+
|
|
177
|
+
switch (command) {
|
|
178
|
+
case "inspect:element":
|
|
179
|
+
result = _inspectElement(args.selector);
|
|
180
|
+
break;
|
|
181
|
+
case "inspect:store":
|
|
182
|
+
result = _inspectStore(args.name);
|
|
183
|
+
break;
|
|
184
|
+
case "inspect:tree":
|
|
185
|
+
result = _inspectTree(args.selector);
|
|
186
|
+
break;
|
|
187
|
+
case "mutate:context":
|
|
188
|
+
result = _mutateContext(args.id, args.key, args.value);
|
|
189
|
+
break;
|
|
190
|
+
case "mutate:store":
|
|
191
|
+
result = _mutateStore(args.name, args.key, args.value);
|
|
192
|
+
break;
|
|
193
|
+
case "get:config":
|
|
194
|
+
result = { ..._config };
|
|
195
|
+
break;
|
|
196
|
+
case "get:routes":
|
|
197
|
+
result = _routerInstance ? _routerInstance.routes || [] : [];
|
|
198
|
+
break;
|
|
199
|
+
case "get:stats":
|
|
200
|
+
result = _getStats();
|
|
201
|
+
break;
|
|
202
|
+
case "highlight:element":
|
|
203
|
+
_highlightElement(args.selector);
|
|
204
|
+
result = { ok: true };
|
|
205
|
+
break;
|
|
206
|
+
case "unhighlight":
|
|
207
|
+
_unhighlight();
|
|
208
|
+
result = { ok: true };
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
result = { error: "Unknown command", command };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Respond with result
|
|
215
|
+
window.dispatchEvent(
|
|
216
|
+
new CustomEvent("nojs:devtools:response", {
|
|
217
|
+
detail: { command, result, timestamp: Date.now() },
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Initialization ─────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export function initDevtools(nojs) {
|
|
225
|
+
if (!_config.devtools || typeof window === "undefined") return;
|
|
226
|
+
|
|
227
|
+
// Listen for commands
|
|
228
|
+
window.addEventListener("nojs:devtools:cmd", _handleDevtoolsCommand);
|
|
229
|
+
|
|
230
|
+
// Expose public API on window
|
|
231
|
+
window.__NOJS_DEVTOOLS__ = {
|
|
232
|
+
// Data access
|
|
233
|
+
stores: _stores,
|
|
234
|
+
config: _config,
|
|
235
|
+
refs: _refs,
|
|
236
|
+
router: _routerInstance,
|
|
237
|
+
version: nojs.version,
|
|
238
|
+
|
|
239
|
+
// Inspect API
|
|
240
|
+
inspect: (selector) => _inspectElement(selector),
|
|
241
|
+
inspectStore: (name) => _inspectStore(name),
|
|
242
|
+
inspectTree: (selector) => _inspectTree(selector),
|
|
243
|
+
stats: () => _getStats(),
|
|
244
|
+
|
|
245
|
+
// Mutation API
|
|
246
|
+
mutate: (id, key, value) => _mutateContext(id, key, value),
|
|
247
|
+
mutateStore: (name, key, value) => _mutateStore(name, key, value),
|
|
248
|
+
|
|
249
|
+
// Visual
|
|
250
|
+
highlight: (selector) => _highlightElement(selector),
|
|
251
|
+
unhighlight: () => _unhighlight(),
|
|
252
|
+
|
|
253
|
+
// Event subscription shorthand
|
|
254
|
+
on: (type, fn) => {
|
|
255
|
+
const handler = (e) => {
|
|
256
|
+
if (!type || e.detail.type === type) fn(e.detail);
|
|
257
|
+
};
|
|
258
|
+
window.addEventListener("nojs:devtools", handler);
|
|
259
|
+
return () => window.removeEventListener("nojs:devtools", handler);
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
console.log("[No.JS DevTools] enabled — access via window.__NOJS_DEVTOOLS__");
|
|
264
|
+
}
|
package/src/directives/http.js
CHANGED
|
@@ -16,6 +16,7 @@ import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
|
|
|
16
16
|
import { _doFetch, _cacheGet, _cacheSet } from "../fetch.js";
|
|
17
17
|
import { findContext, _clearDeclared, _cloneTemplate } from "../dom.js";
|
|
18
18
|
import { registerDirective, processTree } from "../registry.js";
|
|
19
|
+
import { _devtoolsEmit } from "../devtools.js";
|
|
19
20
|
|
|
20
21
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
21
22
|
|
|
@@ -199,6 +200,7 @@ for (const method of HTTP_METHODS) {
|
|
|
199
200
|
_routerInstance.push(redirectPath);
|
|
200
201
|
|
|
201
202
|
_emitEvent("fetch:success", { url: resolvedUrl, data });
|
|
203
|
+
_devtoolsEmit("fetch:success", { method, url: resolvedUrl });
|
|
202
204
|
} catch (err) {
|
|
203
205
|
// SwitchMap: silently ignore aborted requests
|
|
204
206
|
if (err.name === "AbortError") return;
|
|
@@ -209,6 +211,7 @@ for (const method of HTTP_METHODS) {
|
|
|
209
211
|
);
|
|
210
212
|
_emitEvent("fetch:error", { url: resolvedUrl, error: err });
|
|
211
213
|
_emitEvent("error", { url: resolvedUrl, error: err });
|
|
214
|
+
_devtoolsEmit("fetch:error", { method, url: resolvedUrl, error: err.message });
|
|
212
215
|
|
|
213
216
|
if (errorTpl) {
|
|
214
217
|
const clone = _cloneTemplate(errorTpl);
|
package/src/directives/state.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createContext } from "../context.js";
|
|
|
7
7
|
import { evaluate, _execStatement } from "../evaluate.js";
|
|
8
8
|
import { findContext } from "../dom.js";
|
|
9
9
|
import { registerDirective } from "../registry.js";
|
|
10
|
+
import { _devtoolsEmit } from "../devtools.js";
|
|
10
11
|
|
|
11
12
|
registerDirective("state", {
|
|
12
13
|
priority: 0,
|
|
@@ -63,6 +64,10 @@ registerDirective("store", {
|
|
|
63
64
|
? evaluate(valueAttr, createContext()) || {}
|
|
64
65
|
: {};
|
|
65
66
|
_stores[storeName] = createContext(data);
|
|
67
|
+
_devtoolsEmit("store:created", {
|
|
68
|
+
name: storeName,
|
|
69
|
+
keys: Object.keys(data),
|
|
70
|
+
});
|
|
66
71
|
}
|
|
67
72
|
_log("store", storeName);
|
|
68
73
|
},
|