dalila 1.8.4 → 1.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/README.md +6 -0
- package/dist/core/dev.d.ts +11 -2
- package/dist/core/dev.js +26 -2
- package/dist/core/devtools.d.ts +65 -0
- package/dist/core/devtools.js +452 -0
- package/dist/core/scope.js +3 -0
- package/dist/core/signal.js +40 -3
- package/dist/runtime/bind.js +33 -11
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -94,6 +94,12 @@ bind(document.getElementById('app')!, ctx);
|
|
|
94
94
|
- [Scheduler](./docs/core/scheduler.md) — Batching and coordination
|
|
95
95
|
- [Keys](./docs/core/key.md) — Cache key encoding
|
|
96
96
|
- [Dev Mode](./docs/core/dev.md) — Warnings and helpers
|
|
97
|
+
- [Devtools Extension](./devtools-extension/README.md) — Browser panel for reactive graph and scopes
|
|
98
|
+
|
|
99
|
+
Firefox extension workflows:
|
|
100
|
+
|
|
101
|
+
- `npm run devtools:firefox:run` — launch Firefox with extension loaded for dev
|
|
102
|
+
- `npm run devtools:firefox:build` — package extension artifact for submission/signing
|
|
97
103
|
|
|
98
104
|
## Features
|
|
99
105
|
|
package/dist/core/dev.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { DevtoolsEvent, DevtoolsRuntimeOptions, DevtoolsSnapshot } from "./devtools.js";
|
|
1
2
|
export declare function setDevMode(enabled: boolean): void;
|
|
2
3
|
export declare function isInDevMode(): boolean;
|
|
4
|
+
export interface InitDevToolsOptions extends DevtoolsRuntimeOptions {
|
|
5
|
+
}
|
|
6
|
+
export declare function setDevtoolsEnabled(enabled: boolean, options?: DevtoolsRuntimeOptions): void;
|
|
7
|
+
export declare function isDevtoolsEnabled(): boolean;
|
|
8
|
+
export declare function configureDevtools(options: DevtoolsRuntimeOptions): void;
|
|
9
|
+
export declare function getDevtoolsSnapshot(): DevtoolsSnapshot;
|
|
10
|
+
export declare function onDevtoolsEvent(listener: (event: DevtoolsEvent) => void): () => void;
|
|
11
|
+
export declare function resetDevtools(): void;
|
|
3
12
|
/**
|
|
4
|
-
* Initialize dev tools
|
|
13
|
+
* Initialize dev tools runtime bridge for graph inspection.
|
|
5
14
|
* Returns a promise for future async initialization support.
|
|
6
15
|
*/
|
|
7
|
-
export declare function initDevTools(): Promise<void>;
|
|
16
|
+
export declare function initDevTools(options?: InitDevToolsOptions): Promise<void>;
|
package/dist/core/dev.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configure, getSnapshot, isEnabled, reset, setEnabled, subscribe, } from "./devtools.js";
|
|
1
2
|
let isDevMode = true;
|
|
2
3
|
export function setDevMode(enabled) {
|
|
3
4
|
isDevMode = enabled;
|
|
@@ -5,10 +6,33 @@ export function setDevMode(enabled) {
|
|
|
5
6
|
export function isInDevMode() {
|
|
6
7
|
return isDevMode;
|
|
7
8
|
}
|
|
9
|
+
export function setDevtoolsEnabled(enabled, options) {
|
|
10
|
+
setEnabled(enabled, options);
|
|
11
|
+
}
|
|
12
|
+
export function isDevtoolsEnabled() {
|
|
13
|
+
return isEnabled();
|
|
14
|
+
}
|
|
15
|
+
export function configureDevtools(options) {
|
|
16
|
+
configure(options);
|
|
17
|
+
}
|
|
18
|
+
export function getDevtoolsSnapshot() {
|
|
19
|
+
return getSnapshot();
|
|
20
|
+
}
|
|
21
|
+
export function onDevtoolsEvent(listener) {
|
|
22
|
+
return subscribe(listener);
|
|
23
|
+
}
|
|
24
|
+
export function resetDevtools() {
|
|
25
|
+
reset();
|
|
26
|
+
}
|
|
8
27
|
/**
|
|
9
|
-
* Initialize dev tools
|
|
28
|
+
* Initialize dev tools runtime bridge for graph inspection.
|
|
10
29
|
* Returns a promise for future async initialization support.
|
|
11
30
|
*/
|
|
12
|
-
export async function initDevTools() {
|
|
31
|
+
export async function initDevTools(options = {}) {
|
|
13
32
|
setDevMode(true);
|
|
33
|
+
setEnabled(true, {
|
|
34
|
+
exposeGlobalHook: options.exposeGlobalHook ?? true,
|
|
35
|
+
dispatchEvents: options.dispatchEvents ?? true,
|
|
36
|
+
maxEvents: options.maxEvents,
|
|
37
|
+
});
|
|
14
38
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type DevtoolsNodeType = "scope" | "signal" | "computed" | "effect" | "effectAsync";
|
|
2
|
+
export type DevtoolsEdgeKind = "dependency" | "ownership";
|
|
3
|
+
export interface DevtoolsNode {
|
|
4
|
+
id: number;
|
|
5
|
+
type: DevtoolsNodeType;
|
|
6
|
+
label: string;
|
|
7
|
+
disposed: boolean;
|
|
8
|
+
scopeId: number | null;
|
|
9
|
+
parentScopeId: number | null;
|
|
10
|
+
reads: number;
|
|
11
|
+
writes: number;
|
|
12
|
+
runs: number;
|
|
13
|
+
lastValue: string;
|
|
14
|
+
lastRunAt: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DevtoolsEdge {
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
kind: DevtoolsEdgeKind;
|
|
21
|
+
}
|
|
22
|
+
export interface DevtoolsEvent {
|
|
23
|
+
type: string;
|
|
24
|
+
at: number;
|
|
25
|
+
payload: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface DevtoolsSnapshot {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
nodes: DevtoolsNode[];
|
|
30
|
+
edges: DevtoolsEdge[];
|
|
31
|
+
events: DevtoolsEvent[];
|
|
32
|
+
}
|
|
33
|
+
export interface DevtoolsRuntimeOptions {
|
|
34
|
+
maxEvents?: number;
|
|
35
|
+
exposeGlobalHook?: boolean;
|
|
36
|
+
dispatchEvents?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface DevtoolsHighlightOptions {
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function configure(options?: DevtoolsRuntimeOptions): void;
|
|
42
|
+
export declare function setEnabled(next: boolean, options?: DevtoolsRuntimeOptions): void;
|
|
43
|
+
export declare function isEnabled(): boolean;
|
|
44
|
+
export declare function reset(): void;
|
|
45
|
+
export declare function subscribe(listener: (event: DevtoolsEvent) => void): () => void;
|
|
46
|
+
export declare function getSnapshot(): DevtoolsSnapshot;
|
|
47
|
+
export declare function registerScope(scopeRef: object, parentScopeRef: object | null): void;
|
|
48
|
+
export declare function withDevtoolsDomTarget<T>(element: Element | null, fn: () => T): T;
|
|
49
|
+
export declare function linkScopeToDom(scopeRef: object, element: Element, label?: string): void;
|
|
50
|
+
export declare function disposeScope(scopeRef: object): void;
|
|
51
|
+
export declare function registerSignal(signalRef: object, type: "signal" | "computed", options?: {
|
|
52
|
+
scopeRef?: object | null;
|
|
53
|
+
initialValue?: unknown;
|
|
54
|
+
}): void;
|
|
55
|
+
export declare function registerEffect(effectRef: object, type: "effect" | "effectAsync", scopeRef: object | null): void;
|
|
56
|
+
export declare function aliasEffectToNode(effectRef: object, targetRef: object): void;
|
|
57
|
+
export declare function linkSubscriberSetToSignal(subscriberSetRef: object, signalRef: object): void;
|
|
58
|
+
export declare function trackSignalRead(signalRef: object): void;
|
|
59
|
+
export declare function trackSignalWrite(signalRef: object, nextValue: unknown): void;
|
|
60
|
+
export declare function trackEffectRun(effectRef: object): void;
|
|
61
|
+
export declare function trackEffectDispose(effectRef: object): void;
|
|
62
|
+
export declare function trackDependency(signalRef: object, effectRef: object): void;
|
|
63
|
+
export declare function untrackDependencyBySet(subscriberSetRef: object, effectRef: object): void;
|
|
64
|
+
export declare function clearHighlights(): void;
|
|
65
|
+
export declare function highlightNode(nodeId: number, options?: DevtoolsHighlightOptions): boolean;
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const DEFAULT_MAX_EVENTS = 500;
|
|
2
|
+
const GLOBAL_HOOK_KEY = "__DALILA_DEVTOOLS__";
|
|
3
|
+
const GLOBAL_EVENT_NAME = "dalila:devtools:event";
|
|
4
|
+
let enabled = false;
|
|
5
|
+
let maxEvents = DEFAULT_MAX_EVENTS;
|
|
6
|
+
let exposeGlobalHook = false;
|
|
7
|
+
let dispatchEvents = false;
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
let refToId = new WeakMap();
|
|
10
|
+
const nodes = new Map();
|
|
11
|
+
const dependencyEdges = new Map();
|
|
12
|
+
const ownershipEdges = new Map();
|
|
13
|
+
let subscriberSetToSignalId = new WeakMap();
|
|
14
|
+
let effectAliasToNodeId = new WeakMap();
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let events = [];
|
|
17
|
+
const nodeDomTargets = new Map();
|
|
18
|
+
const highlightTimers = new WeakMap();
|
|
19
|
+
let currentDomTarget = null;
|
|
20
|
+
const HIGHLIGHT_ATTR = "data-dalila-devtools-highlight";
|
|
21
|
+
const HIGHLIGHT_LABEL_ATTR = "data-dalila-devtools-label";
|
|
22
|
+
const HIGHLIGHT_STYLE_ID = "dalila-devtools-highlight-style";
|
|
23
|
+
function canUseGlobalDispatch() {
|
|
24
|
+
return typeof globalThis.dispatchEvent === "function";
|
|
25
|
+
}
|
|
26
|
+
function canUseDOM() {
|
|
27
|
+
return typeof document !== "undefined" && typeof Element !== "undefined";
|
|
28
|
+
}
|
|
29
|
+
function clearHighlightOnElement(element) {
|
|
30
|
+
const timer = highlightTimers.get(element);
|
|
31
|
+
if (timer !== undefined && typeof globalThis.clearTimeout === "function") {
|
|
32
|
+
globalThis.clearTimeout(timer);
|
|
33
|
+
highlightTimers.delete(element);
|
|
34
|
+
}
|
|
35
|
+
element.removeAttribute(HIGHLIGHT_ATTR);
|
|
36
|
+
element.removeAttribute(HIGHLIGHT_LABEL_ATTR);
|
|
37
|
+
}
|
|
38
|
+
function ensureHighlightStyle() {
|
|
39
|
+
if (!canUseDOM())
|
|
40
|
+
return;
|
|
41
|
+
if (document.getElementById(HIGHLIGHT_STYLE_ID))
|
|
42
|
+
return;
|
|
43
|
+
const style = document.createElement("style");
|
|
44
|
+
style.id = HIGHLIGHT_STYLE_ID;
|
|
45
|
+
style.textContent = `
|
|
46
|
+
[${HIGHLIGHT_ATTR}="1"] {
|
|
47
|
+
outline: 2px solid #0f6d67 !important;
|
|
48
|
+
outline-offset: 2px !important;
|
|
49
|
+
box-shadow: 0 0 0 3px rgba(15, 109, 103, 0.22) !important;
|
|
50
|
+
transition: outline-color 120ms ease, box-shadow 120ms ease;
|
|
51
|
+
}`;
|
|
52
|
+
(document.head || document.documentElement).append(style);
|
|
53
|
+
}
|
|
54
|
+
function resolveNodeDomTargets(nodeId) {
|
|
55
|
+
const direct = nodeDomTargets.get(nodeId);
|
|
56
|
+
if (direct && direct.size > 0)
|
|
57
|
+
return Array.from(direct);
|
|
58
|
+
const node = nodes.get(nodeId);
|
|
59
|
+
if (!node)
|
|
60
|
+
return [];
|
|
61
|
+
if (node.type !== "scope" && node.scopeId !== null) {
|
|
62
|
+
const scopeTargets = nodeDomTargets.get(node.scopeId);
|
|
63
|
+
if (scopeTargets && scopeTargets.size > 0)
|
|
64
|
+
return Array.from(scopeTargets);
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
function addDomTarget(nodeId, element) {
|
|
69
|
+
let targets = nodeDomTargets.get(nodeId);
|
|
70
|
+
if (!targets) {
|
|
71
|
+
targets = new Set();
|
|
72
|
+
nodeDomTargets.set(nodeId, targets);
|
|
73
|
+
}
|
|
74
|
+
targets.add(element);
|
|
75
|
+
}
|
|
76
|
+
function previewValue(value) {
|
|
77
|
+
const type = typeof value;
|
|
78
|
+
if (type === "string") {
|
|
79
|
+
const quoted = JSON.stringify(value);
|
|
80
|
+
return quoted.length > 120 ? `${quoted.slice(0, 117)}...` : quoted;
|
|
81
|
+
}
|
|
82
|
+
if (value === null ||
|
|
83
|
+
type === "number" ||
|
|
84
|
+
type === "boolean" ||
|
|
85
|
+
type === "undefined" ||
|
|
86
|
+
type === "bigint" ||
|
|
87
|
+
type === "symbol") {
|
|
88
|
+
return String(value);
|
|
89
|
+
}
|
|
90
|
+
if (type === "function") {
|
|
91
|
+
const fn = value;
|
|
92
|
+
return `[Function ${fn.name || "anonymous"}]`;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const json = JSON.stringify(value);
|
|
96
|
+
if (json === undefined)
|
|
97
|
+
return Object.prototype.toString.call(value);
|
|
98
|
+
return json.length > 120 ? `${json.slice(0, 117)}...` : json;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return Object.prototype.toString.call(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getNodeId(ref) {
|
|
105
|
+
const alias = effectAliasToNodeId.get(ref);
|
|
106
|
+
if (alias)
|
|
107
|
+
return alias;
|
|
108
|
+
const existing = refToId.get(ref);
|
|
109
|
+
if (existing)
|
|
110
|
+
return existing;
|
|
111
|
+
const id = nextId++;
|
|
112
|
+
refToId.set(ref, id);
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
function edgeKey(from, to, kind) {
|
|
116
|
+
return `${kind}:${from}->${to}`;
|
|
117
|
+
}
|
|
118
|
+
function emit(type, payload) {
|
|
119
|
+
if (!enabled)
|
|
120
|
+
return;
|
|
121
|
+
const event = {
|
|
122
|
+
type,
|
|
123
|
+
at: Date.now(),
|
|
124
|
+
payload,
|
|
125
|
+
};
|
|
126
|
+
events.push(event);
|
|
127
|
+
if (events.length > maxEvents) {
|
|
128
|
+
events = events.slice(events.length - maxEvents);
|
|
129
|
+
}
|
|
130
|
+
for (const listener of listeners) {
|
|
131
|
+
try {
|
|
132
|
+
listener(event);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error("[Dalila] Devtools listener threw:", error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (dispatchEvents && canUseGlobalDispatch() && typeof CustomEvent !== "undefined") {
|
|
139
|
+
try {
|
|
140
|
+
globalThis.dispatchEvent(new CustomEvent(GLOBAL_EVENT_NAME, {
|
|
141
|
+
detail: event,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore environments without full event support.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function createNode(ref, type, label, options) {
|
|
150
|
+
const id = getNodeId(ref);
|
|
151
|
+
if (nodes.has(id))
|
|
152
|
+
return id;
|
|
153
|
+
const scopeId = options?.scopeRef ? getNodeId(options.scopeRef) : null;
|
|
154
|
+
const parentScopeId = options?.parentScopeRef ? getNodeId(options.parentScopeRef) : null;
|
|
155
|
+
const registeredScopeId = scopeId !== null && nodes.has(scopeId) ? scopeId : null;
|
|
156
|
+
const registeredParentScopeId = parentScopeId !== null && nodes.has(parentScopeId) ? parentScopeId : null;
|
|
157
|
+
nodes.set(id, {
|
|
158
|
+
id,
|
|
159
|
+
type,
|
|
160
|
+
label,
|
|
161
|
+
disposed: false,
|
|
162
|
+
scopeId: registeredScopeId,
|
|
163
|
+
parentScopeId: registeredParentScopeId,
|
|
164
|
+
reads: 0,
|
|
165
|
+
writes: 0,
|
|
166
|
+
runs: 0,
|
|
167
|
+
lastValue: options && "initialValue" in options ? previewValue(options.initialValue) : "",
|
|
168
|
+
lastRunAt: 0,
|
|
169
|
+
createdAt: Date.now(),
|
|
170
|
+
});
|
|
171
|
+
if (registeredScopeId !== null) {
|
|
172
|
+
addOwnershipEdge(registeredScopeId, id);
|
|
173
|
+
}
|
|
174
|
+
if (type === "scope" && registeredParentScopeId !== null) {
|
|
175
|
+
addOwnershipEdge(registeredParentScopeId, id);
|
|
176
|
+
}
|
|
177
|
+
emit("node.create", { id, type, label, scopeId: registeredScopeId, parentScopeId: registeredParentScopeId });
|
|
178
|
+
return id;
|
|
179
|
+
}
|
|
180
|
+
function addOwnershipEdge(from, to) {
|
|
181
|
+
if (!nodes.has(from) || !nodes.has(to))
|
|
182
|
+
return;
|
|
183
|
+
const key = edgeKey(from, to, "ownership");
|
|
184
|
+
if (ownershipEdges.has(key))
|
|
185
|
+
return;
|
|
186
|
+
const edge = { from, to, kind: "ownership" };
|
|
187
|
+
ownershipEdges.set(key, edge);
|
|
188
|
+
emit("edge.add", { from: edge.from, to: edge.to, kind: edge.kind });
|
|
189
|
+
}
|
|
190
|
+
function upsertDependencyEdge(from, to) {
|
|
191
|
+
const key = edgeKey(from, to, "dependency");
|
|
192
|
+
if (dependencyEdges.has(key))
|
|
193
|
+
return;
|
|
194
|
+
const edge = { from, to, kind: "dependency" };
|
|
195
|
+
dependencyEdges.set(key, edge);
|
|
196
|
+
emit("edge.add", { from: edge.from, to: edge.to, kind: edge.kind });
|
|
197
|
+
}
|
|
198
|
+
function removeDependencyEdge(from, to) {
|
|
199
|
+
const key = edgeKey(from, to, "dependency");
|
|
200
|
+
const existing = dependencyEdges.get(key);
|
|
201
|
+
if (!existing)
|
|
202
|
+
return;
|
|
203
|
+
dependencyEdges.delete(key);
|
|
204
|
+
emit("edge.remove", { from: existing.from, to: existing.to, kind: existing.kind });
|
|
205
|
+
}
|
|
206
|
+
function markDisposed(ref) {
|
|
207
|
+
if (!enabled)
|
|
208
|
+
return;
|
|
209
|
+
const node = nodes.get(getNodeId(ref));
|
|
210
|
+
if (!node || node.disposed)
|
|
211
|
+
return;
|
|
212
|
+
node.disposed = true;
|
|
213
|
+
emit("node.dispose", { id: node.id, type: node.type });
|
|
214
|
+
}
|
|
215
|
+
function markRead(ref) {
|
|
216
|
+
if (!enabled)
|
|
217
|
+
return;
|
|
218
|
+
const node = nodes.get(getNodeId(ref));
|
|
219
|
+
if (!node)
|
|
220
|
+
return;
|
|
221
|
+
node.reads += 1;
|
|
222
|
+
}
|
|
223
|
+
function markWrite(ref, nextValue) {
|
|
224
|
+
if (!enabled)
|
|
225
|
+
return;
|
|
226
|
+
const node = nodes.get(getNodeId(ref));
|
|
227
|
+
if (!node)
|
|
228
|
+
return;
|
|
229
|
+
node.writes += 1;
|
|
230
|
+
node.lastValue = previewValue(nextValue);
|
|
231
|
+
emit("signal.write", { id: node.id, type: node.type, nextValue: node.lastValue });
|
|
232
|
+
}
|
|
233
|
+
function markRun(ref) {
|
|
234
|
+
if (!enabled)
|
|
235
|
+
return;
|
|
236
|
+
const node = nodes.get(getNodeId(ref));
|
|
237
|
+
if (!node)
|
|
238
|
+
return;
|
|
239
|
+
node.runs += 1;
|
|
240
|
+
node.lastRunAt = Date.now();
|
|
241
|
+
}
|
|
242
|
+
function installGlobalHook() {
|
|
243
|
+
if (!exposeGlobalHook)
|
|
244
|
+
return;
|
|
245
|
+
const host = globalThis;
|
|
246
|
+
if (host[GLOBAL_HOOK_KEY])
|
|
247
|
+
return;
|
|
248
|
+
host[GLOBAL_HOOK_KEY] = {
|
|
249
|
+
version: 1,
|
|
250
|
+
getSnapshot,
|
|
251
|
+
subscribe,
|
|
252
|
+
reset,
|
|
253
|
+
setEnabled,
|
|
254
|
+
configure,
|
|
255
|
+
highlightNode,
|
|
256
|
+
clearHighlights,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function configure(options = {}) {
|
|
260
|
+
if (typeof options.maxEvents === "number" && Number.isFinite(options.maxEvents) && options.maxEvents > 0) {
|
|
261
|
+
maxEvents = Math.floor(options.maxEvents);
|
|
262
|
+
if (events.length > maxEvents) {
|
|
263
|
+
events = events.slice(events.length - maxEvents);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (typeof options.exposeGlobalHook === "boolean") {
|
|
267
|
+
exposeGlobalHook = options.exposeGlobalHook;
|
|
268
|
+
}
|
|
269
|
+
if (typeof options.dispatchEvents === "boolean") {
|
|
270
|
+
dispatchEvents = options.dispatchEvents;
|
|
271
|
+
}
|
|
272
|
+
if (enabled)
|
|
273
|
+
installGlobalHook();
|
|
274
|
+
}
|
|
275
|
+
export function setEnabled(next, options) {
|
|
276
|
+
if (options)
|
|
277
|
+
configure(options);
|
|
278
|
+
if (next) {
|
|
279
|
+
enabled = true;
|
|
280
|
+
installGlobalHook();
|
|
281
|
+
emit("devtools.enabled", { enabled: true });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (enabled) {
|
|
285
|
+
emit("devtools.enabled", { enabled: false });
|
|
286
|
+
}
|
|
287
|
+
enabled = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function isEnabled() {
|
|
291
|
+
return enabled;
|
|
292
|
+
}
|
|
293
|
+
export function reset() {
|
|
294
|
+
// Identity maps must be reset together with graph state. Otherwise existing
|
|
295
|
+
// refs keep stale ids and can generate edges pointing to missing nodes.
|
|
296
|
+
nextId = 1;
|
|
297
|
+
refToId = new WeakMap();
|
|
298
|
+
subscriberSetToSignalId = new WeakMap();
|
|
299
|
+
effectAliasToNodeId = new WeakMap();
|
|
300
|
+
dependencyEdges.clear();
|
|
301
|
+
ownershipEdges.clear();
|
|
302
|
+
nodes.clear();
|
|
303
|
+
nodeDomTargets.clear();
|
|
304
|
+
currentDomTarget = null;
|
|
305
|
+
events = [];
|
|
306
|
+
if (enabled) {
|
|
307
|
+
emit("devtools.reset", {});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function subscribe(listener) {
|
|
311
|
+
listeners.add(listener);
|
|
312
|
+
return () => listeners.delete(listener);
|
|
313
|
+
}
|
|
314
|
+
export function getSnapshot() {
|
|
315
|
+
return {
|
|
316
|
+
enabled,
|
|
317
|
+
nodes: Array.from(nodes.values()).map((node) => ({ ...node })),
|
|
318
|
+
edges: [...Array.from(ownershipEdges.values()), ...Array.from(dependencyEdges.values())],
|
|
319
|
+
events: events.map((event) => ({ ...event, payload: { ...event.payload } })),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export function registerScope(scopeRef, parentScopeRef) {
|
|
323
|
+
if (!enabled)
|
|
324
|
+
return;
|
|
325
|
+
createNode(scopeRef, "scope", "scope", {
|
|
326
|
+
parentScopeRef,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
export function withDevtoolsDomTarget(element, fn) {
|
|
330
|
+
if (!element || !canUseDOM())
|
|
331
|
+
return fn();
|
|
332
|
+
if (!(element instanceof Element))
|
|
333
|
+
return fn();
|
|
334
|
+
const prev = currentDomTarget;
|
|
335
|
+
currentDomTarget = element;
|
|
336
|
+
try {
|
|
337
|
+
return fn();
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
currentDomTarget = prev;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function linkScopeToDom(scopeRef, element, label) {
|
|
344
|
+
if (!enabled || !canUseDOM())
|
|
345
|
+
return;
|
|
346
|
+
if (!(element instanceof Element))
|
|
347
|
+
return;
|
|
348
|
+
const scopeId = getNodeId(scopeRef);
|
|
349
|
+
const node = nodes.get(scopeId);
|
|
350
|
+
if (!node)
|
|
351
|
+
return;
|
|
352
|
+
addDomTarget(scopeId, element);
|
|
353
|
+
if (label && node.label === "scope") {
|
|
354
|
+
node.label = label;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function disposeScope(scopeRef) {
|
|
358
|
+
markDisposed(scopeRef);
|
|
359
|
+
}
|
|
360
|
+
export function registerSignal(signalRef, type, options) {
|
|
361
|
+
if (!enabled)
|
|
362
|
+
return;
|
|
363
|
+
createNode(signalRef, type, type, {
|
|
364
|
+
scopeRef: options?.scopeRef ?? null,
|
|
365
|
+
initialValue: options?.initialValue,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
export function registerEffect(effectRef, type, scopeRef) {
|
|
369
|
+
if (!enabled)
|
|
370
|
+
return;
|
|
371
|
+
const id = createNode(effectRef, type, type, {
|
|
372
|
+
scopeRef,
|
|
373
|
+
});
|
|
374
|
+
if (currentDomTarget) {
|
|
375
|
+
addDomTarget(id, currentDomTarget);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
export function aliasEffectToNode(effectRef, targetRef) {
|
|
379
|
+
if (!enabled)
|
|
380
|
+
return;
|
|
381
|
+
const targetId = getNodeId(targetRef);
|
|
382
|
+
effectAliasToNodeId.set(effectRef, targetId);
|
|
383
|
+
}
|
|
384
|
+
export function linkSubscriberSetToSignal(subscriberSetRef, signalRef) {
|
|
385
|
+
if (!enabled)
|
|
386
|
+
return;
|
|
387
|
+
subscriberSetToSignalId.set(subscriberSetRef, getNodeId(signalRef));
|
|
388
|
+
}
|
|
389
|
+
export function trackSignalRead(signalRef) {
|
|
390
|
+
markRead(signalRef);
|
|
391
|
+
}
|
|
392
|
+
export function trackSignalWrite(signalRef, nextValue) {
|
|
393
|
+
markWrite(signalRef, nextValue);
|
|
394
|
+
}
|
|
395
|
+
export function trackEffectRun(effectRef) {
|
|
396
|
+
markRun(effectRef);
|
|
397
|
+
}
|
|
398
|
+
export function trackEffectDispose(effectRef) {
|
|
399
|
+
markDisposed(effectRef);
|
|
400
|
+
}
|
|
401
|
+
export function trackDependency(signalRef, effectRef) {
|
|
402
|
+
if (!enabled)
|
|
403
|
+
return;
|
|
404
|
+
const from = getNodeId(signalRef);
|
|
405
|
+
const to = getNodeId(effectRef);
|
|
406
|
+
if (!nodes.has(from) || !nodes.has(to))
|
|
407
|
+
return;
|
|
408
|
+
upsertDependencyEdge(from, to);
|
|
409
|
+
}
|
|
410
|
+
export function untrackDependencyBySet(subscriberSetRef, effectRef) {
|
|
411
|
+
if (!enabled)
|
|
412
|
+
return;
|
|
413
|
+
const from = subscriberSetToSignalId.get(subscriberSetRef);
|
|
414
|
+
if (!from)
|
|
415
|
+
return;
|
|
416
|
+
const to = getNodeId(effectRef);
|
|
417
|
+
removeDependencyEdge(from, to);
|
|
418
|
+
}
|
|
419
|
+
export function clearHighlights() {
|
|
420
|
+
if (!canUseDOM())
|
|
421
|
+
return;
|
|
422
|
+
for (const targets of nodeDomTargets.values()) {
|
|
423
|
+
for (const element of targets) {
|
|
424
|
+
clearHighlightOnElement(element);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export function highlightNode(nodeId, options = {}) {
|
|
429
|
+
if (!enabled || !canUseDOM())
|
|
430
|
+
return false;
|
|
431
|
+
const targets = resolveNodeDomTargets(nodeId);
|
|
432
|
+
if (targets.length === 0)
|
|
433
|
+
return false;
|
|
434
|
+
ensureHighlightStyle();
|
|
435
|
+
const node = nodes.get(nodeId);
|
|
436
|
+
const label = node ? `#${node.id} ${node.label || node.type}` : `#${nodeId}`;
|
|
437
|
+
const durationMs = typeof options.durationMs === "number" && Number.isFinite(options.durationMs) && options.durationMs > 0
|
|
438
|
+
? Math.floor(options.durationMs)
|
|
439
|
+
: 650;
|
|
440
|
+
for (const element of targets) {
|
|
441
|
+
clearHighlightOnElement(element);
|
|
442
|
+
element.setAttribute(HIGHLIGHT_ATTR, "1");
|
|
443
|
+
element.setAttribute(HIGHLIGHT_LABEL_ATTR, label);
|
|
444
|
+
if (typeof globalThis.setTimeout === "function") {
|
|
445
|
+
const timer = globalThis.setTimeout(() => {
|
|
446
|
+
clearHighlightOnElement(element);
|
|
447
|
+
}, durationMs);
|
|
448
|
+
highlightTimers.set(element, timer);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
package/dist/core/scope.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { disposeScope, registerScope } from "./devtools.js";
|
|
1
2
|
/** Tracks disposed scopes without mutating the public interface. */
|
|
2
3
|
const disposedScopes = new WeakSet();
|
|
3
4
|
const scopeCreateListeners = new Set();
|
|
@@ -73,6 +74,7 @@ export function createScope(parentOverride) {
|
|
|
73
74
|
if (errors.length > 0) {
|
|
74
75
|
console.error('[Dalila] scope.dispose() had cleanup errors:', errors);
|
|
75
76
|
}
|
|
77
|
+
disposeScope(scope);
|
|
76
78
|
for (const listener of scopeDisposeListeners) {
|
|
77
79
|
try {
|
|
78
80
|
listener(scope);
|
|
@@ -84,6 +86,7 @@ export function createScope(parentOverride) {
|
|
|
84
86
|
},
|
|
85
87
|
parent,
|
|
86
88
|
};
|
|
89
|
+
registerScope(scope, parent);
|
|
87
90
|
if (parent) {
|
|
88
91
|
parent.onCleanup(() => scope.dispose());
|
|
89
92
|
}
|
package/dist/core/signal.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCurrentScope, withScope } from './scope.js';
|
|
2
2
|
import { scheduleMicrotask, isBatching, queueInBatch } from './scheduler.js';
|
|
3
|
+
import { aliasEffectToNode, linkSubscriberSetToSignal, registerEffect, registerSignal, trackDependency, trackEffectDispose, trackEffectRun, trackSignalRead, trackSignalWrite, untrackDependencyBySet, } from './devtools.js';
|
|
3
4
|
/**
|
|
4
5
|
* Optional global error handler for reactive execution.
|
|
5
6
|
*
|
|
@@ -114,9 +115,12 @@ function scheduleEffect(eff) {
|
|
|
114
115
|
* - signals do not "own" subscriber lifetimes; they only maintain the set
|
|
115
116
|
*/
|
|
116
117
|
export function signal(initialValue) {
|
|
118
|
+
const owningScope = getCurrentScope();
|
|
117
119
|
let value = initialValue;
|
|
118
120
|
const subscribers = new Set();
|
|
121
|
+
let signalRef;
|
|
119
122
|
const read = () => {
|
|
123
|
+
trackSignalRead(signalRef);
|
|
120
124
|
if (activeEffect && !activeEffect.disposed) {
|
|
121
125
|
// Scope-aware subscription guard (best effort):
|
|
122
126
|
// - if the active effect is not scoped, allow subscription
|
|
@@ -125,6 +129,7 @@ export function signal(initialValue) {
|
|
|
125
129
|
if (!subscribers.has(activeEffect)) {
|
|
126
130
|
subscribers.add(activeEffect);
|
|
127
131
|
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
132
|
+
trackDependency(signalRef, activeEffect);
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
else {
|
|
@@ -133,6 +138,7 @@ export function signal(initialValue) {
|
|
|
133
138
|
if (!subscribers.has(activeEffect)) {
|
|
134
139
|
subscribers.add(activeEffect);
|
|
135
140
|
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
141
|
+
trackDependency(signalRef, activeEffect);
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
}
|
|
@@ -149,6 +155,7 @@ export function signal(initialValue) {
|
|
|
149
155
|
return;
|
|
150
156
|
// State updates are immediate even inside `batch()`.
|
|
151
157
|
value = nextValue;
|
|
158
|
+
trackSignalWrite(signalRef, nextValue);
|
|
152
159
|
// Notify now, or defer into the batch queue.
|
|
153
160
|
if (isBatching())
|
|
154
161
|
queueInBatch(notify);
|
|
@@ -178,6 +185,12 @@ export function signal(initialValue) {
|
|
|
178
185
|
subscriber.deps?.delete(subscribers);
|
|
179
186
|
};
|
|
180
187
|
};
|
|
188
|
+
signalRef = read;
|
|
189
|
+
registerSignal(signalRef, 'signal', {
|
|
190
|
+
scopeRef: owningScope,
|
|
191
|
+
initialValue,
|
|
192
|
+
});
|
|
193
|
+
linkSubscriberSetToSignal(subscribers, signalRef);
|
|
181
194
|
return read;
|
|
182
195
|
}
|
|
183
196
|
/**
|
|
@@ -199,8 +212,10 @@ export function effect(fn) {
|
|
|
199
212
|
const cleanupDeps = () => {
|
|
200
213
|
if (!run.deps)
|
|
201
214
|
return;
|
|
202
|
-
for (const depSet of run.deps)
|
|
215
|
+
for (const depSet of run.deps) {
|
|
203
216
|
depSet.delete(run);
|
|
217
|
+
untrackDependencyBySet(depSet, run);
|
|
218
|
+
}
|
|
204
219
|
run.deps.clear();
|
|
205
220
|
};
|
|
206
221
|
const dispose = () => {
|
|
@@ -209,10 +224,12 @@ export function effect(fn) {
|
|
|
209
224
|
run.disposed = true;
|
|
210
225
|
cleanupDeps();
|
|
211
226
|
pendingEffects.delete(run);
|
|
227
|
+
trackEffectDispose(run);
|
|
212
228
|
};
|
|
213
229
|
const run = (() => {
|
|
214
230
|
if (run.disposed)
|
|
215
231
|
return;
|
|
232
|
+
trackEffectRun(run);
|
|
216
233
|
// Dynamic deps: unsubscribe from previous reads.
|
|
217
234
|
cleanupDeps();
|
|
218
235
|
const prevEffect = activeEffect;
|
|
@@ -231,6 +248,7 @@ export function effect(fn) {
|
|
|
231
248
|
activeScope = prevScope;
|
|
232
249
|
}
|
|
233
250
|
});
|
|
251
|
+
registerEffect(run, 'effect', owningScope);
|
|
234
252
|
scheduleEffect(run);
|
|
235
253
|
if (owningScope)
|
|
236
254
|
owningScope.onCleanup(dispose);
|
|
@@ -252,9 +270,11 @@ export function effect(fn) {
|
|
|
252
270
|
* - other effects can subscribe to the computed like a normal signal
|
|
253
271
|
*/
|
|
254
272
|
export function computed(fn) {
|
|
273
|
+
const owningScope = getCurrentScope();
|
|
255
274
|
let value;
|
|
256
275
|
let dirty = true;
|
|
257
276
|
const subscribers = new Set();
|
|
277
|
+
let signalRef;
|
|
258
278
|
// Dep sets that `markDirty` is currently registered in (so we can unsubscribe on recompute).
|
|
259
279
|
let trackedDeps = new Set();
|
|
260
280
|
/**
|
|
@@ -271,19 +291,23 @@ export function computed(fn) {
|
|
|
271
291
|
markDirty.disposed = false;
|
|
272
292
|
markDirty.sync = true;
|
|
273
293
|
const cleanupDeps = () => {
|
|
274
|
-
for (const depSet of trackedDeps)
|
|
294
|
+
for (const depSet of trackedDeps) {
|
|
275
295
|
depSet.delete(markDirty);
|
|
296
|
+
untrackDependencyBySet(depSet, markDirty);
|
|
297
|
+
}
|
|
276
298
|
trackedDeps.clear();
|
|
277
299
|
if (markDirty.deps)
|
|
278
300
|
markDirty.deps.clear();
|
|
279
301
|
};
|
|
280
302
|
const read = () => {
|
|
303
|
+
trackSignalRead(signalRef);
|
|
281
304
|
// Allow effects to subscribe to this computed (same rules as signal()).
|
|
282
305
|
if (activeEffect && !activeEffect.disposed) {
|
|
283
306
|
if (!activeScope) {
|
|
284
307
|
if (!subscribers.has(activeEffect)) {
|
|
285
308
|
subscribers.add(activeEffect);
|
|
286
309
|
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
310
|
+
trackDependency(signalRef, activeEffect);
|
|
287
311
|
}
|
|
288
312
|
}
|
|
289
313
|
else {
|
|
@@ -292,6 +316,7 @@ export function computed(fn) {
|
|
|
292
316
|
if (!subscribers.has(activeEffect)) {
|
|
293
317
|
subscribers.add(activeEffect);
|
|
294
318
|
(activeEffect.deps ?? (activeEffect.deps = new Set())).add(subscribers);
|
|
319
|
+
trackDependency(signalRef, activeEffect);
|
|
295
320
|
}
|
|
296
321
|
}
|
|
297
322
|
}
|
|
@@ -365,6 +390,13 @@ export function computed(fn) {
|
|
|
365
390
|
subscriber.deps?.delete(subscribers);
|
|
366
391
|
};
|
|
367
392
|
};
|
|
393
|
+
signalRef = read;
|
|
394
|
+
registerSignal(signalRef, 'computed', {
|
|
395
|
+
scopeRef: owningScope,
|
|
396
|
+
});
|
|
397
|
+
linkSubscriberSetToSignal(subscribers, signalRef);
|
|
398
|
+
aliasEffectToNode(markDirty, signalRef);
|
|
399
|
+
registerEffect(markDirty, 'effect', owningScope);
|
|
368
400
|
return read;
|
|
369
401
|
}
|
|
370
402
|
/**
|
|
@@ -381,8 +413,10 @@ export function effectAsync(fn) {
|
|
|
381
413
|
const cleanupDeps = () => {
|
|
382
414
|
if (!run.deps)
|
|
383
415
|
return;
|
|
384
|
-
for (const depSet of run.deps)
|
|
416
|
+
for (const depSet of run.deps) {
|
|
385
417
|
depSet.delete(run);
|
|
418
|
+
untrackDependencyBySet(depSet, run);
|
|
419
|
+
}
|
|
386
420
|
run.deps.clear();
|
|
387
421
|
};
|
|
388
422
|
const dispose = () => {
|
|
@@ -393,10 +427,12 @@ export function effectAsync(fn) {
|
|
|
393
427
|
controller = null;
|
|
394
428
|
cleanupDeps();
|
|
395
429
|
pendingEffects.delete(run);
|
|
430
|
+
trackEffectDispose(run);
|
|
396
431
|
};
|
|
397
432
|
const run = (() => {
|
|
398
433
|
if (run.disposed)
|
|
399
434
|
return;
|
|
435
|
+
trackEffectRun(run);
|
|
400
436
|
// Abort previous run (if any), then create a new controller for this run.
|
|
401
437
|
controller?.abort();
|
|
402
438
|
controller = new AbortController();
|
|
@@ -417,6 +453,7 @@ export function effectAsync(fn) {
|
|
|
417
453
|
activeScope = prevScope;
|
|
418
454
|
}
|
|
419
455
|
});
|
|
456
|
+
registerEffect(run, 'effectAsync', owningScope);
|
|
420
457
|
scheduleEffect(run);
|
|
421
458
|
if (owningScope)
|
|
422
459
|
owningScope.onCleanup(dispose);
|
package/dist/runtime/bind.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { effect, createScope, withScope, isInDevMode, signal } from '../core/index.js';
|
|
10
10
|
import { WRAPPED_HANDLER } from '../form/form.js';
|
|
11
|
+
import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Utilities
|
|
13
14
|
// ============================================================================
|
|
@@ -81,6 +82,26 @@ function warn(message) {
|
|
|
81
82
|
console.warn(`[Dalila] ${message}`);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
function describeBindRoot(root) {
|
|
86
|
+
const explicit = root.getAttribute('data-component') ||
|
|
87
|
+
root.getAttribute('data-devtools-label') ||
|
|
88
|
+
root.getAttribute('aria-label') ||
|
|
89
|
+
root.getAttribute('id');
|
|
90
|
+
if (explicit)
|
|
91
|
+
return String(explicit);
|
|
92
|
+
const className = root.getAttribute('class');
|
|
93
|
+
if (className) {
|
|
94
|
+
const first = className.split(/\s+/).find(Boolean);
|
|
95
|
+
if (first)
|
|
96
|
+
return `${root.tagName.toLowerCase()}.${first}`;
|
|
97
|
+
}
|
|
98
|
+
return root.tagName.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
function bindEffect(target, fn) {
|
|
101
|
+
withDevtoolsDomTarget(target ?? null, () => {
|
|
102
|
+
effect(fn);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
84
105
|
const expressionCache = new Map();
|
|
85
106
|
const templateInterpolationPlanCache = new Map();
|
|
86
107
|
const TEMPLATE_PLAN_CACHE_MAX_ENTRIES = 250;
|
|
@@ -964,7 +985,7 @@ function bindTextNodeFromPlan(node, plan, ctx, benchSession) {
|
|
|
964
985
|
applyResult(evalExpressionAst(parsed.ast, ctx));
|
|
965
986
|
// Only schedule reactive updates when expression depends on reactive sources.
|
|
966
987
|
if (expressionDependsOnReactiveSource(parsed.ast, ctx)) {
|
|
967
|
-
|
|
988
|
+
bindEffect(node.parentElement, () => {
|
|
968
989
|
applyResult(evalExpressionAst(parsed.ast, ctx));
|
|
969
990
|
});
|
|
970
991
|
}
|
|
@@ -1055,7 +1076,7 @@ function bindWhen(root, ctx, cleanups) {
|
|
|
1055
1076
|
const initialValue = !!resolve(binding);
|
|
1056
1077
|
htmlEl.style.display = initialValue ? '' : 'none';
|
|
1057
1078
|
// Then create reactive effect to keep it updated
|
|
1058
|
-
|
|
1079
|
+
bindEffect(htmlEl, () => {
|
|
1059
1080
|
const value = !!resolve(binding);
|
|
1060
1081
|
htmlEl.style.display = value ? '' : 'none';
|
|
1061
1082
|
});
|
|
@@ -1107,7 +1128,7 @@ function bindMatch(root, ctx, cleanups) {
|
|
|
1107
1128
|
// Apply initial state
|
|
1108
1129
|
applyMatch();
|
|
1109
1130
|
// Then create reactive effect to keep it updated
|
|
1110
|
-
|
|
1131
|
+
bindEffect(el, () => {
|
|
1111
1132
|
applyMatch();
|
|
1112
1133
|
});
|
|
1113
1134
|
}
|
|
@@ -1321,7 +1342,7 @@ function bindEach(root, ctx, cleanups) {
|
|
|
1321
1342
|
}
|
|
1322
1343
|
if (isSignal(binding)) {
|
|
1323
1344
|
// Effect owned by templateScope — no manual stop needed
|
|
1324
|
-
|
|
1345
|
+
bindEffect(el, () => {
|
|
1325
1346
|
const value = binding();
|
|
1326
1347
|
renderList(Array.isArray(value) ? value : []);
|
|
1327
1348
|
});
|
|
@@ -1373,7 +1394,7 @@ function bindIf(root, ctx, cleanups) {
|
|
|
1373
1394
|
comment.parentNode?.insertBefore(htmlEl, comment);
|
|
1374
1395
|
}
|
|
1375
1396
|
// Then create reactive effect to keep it updated
|
|
1376
|
-
|
|
1397
|
+
bindEffect(htmlEl, () => {
|
|
1377
1398
|
const value = !!resolve(binding);
|
|
1378
1399
|
if (value) {
|
|
1379
1400
|
if (!htmlEl.parentNode) {
|
|
@@ -1409,7 +1430,7 @@ function bindHtml(root, ctx, cleanups) {
|
|
|
1409
1430
|
}
|
|
1410
1431
|
const htmlEl = el;
|
|
1411
1432
|
if (isSignal(binding)) {
|
|
1412
|
-
|
|
1433
|
+
bindEffect(htmlEl, () => {
|
|
1413
1434
|
const v = binding();
|
|
1414
1435
|
const html = v == null ? '' : String(v);
|
|
1415
1436
|
if (isInDevMode() && /<script[\s>]|javascript:|onerror\s*=/i.test(html)) {
|
|
@@ -1482,7 +1503,7 @@ function bindAttrs(root, ctx, cleanups) {
|
|
|
1482
1503
|
}
|
|
1483
1504
|
el.removeAttribute(attr.name);
|
|
1484
1505
|
if (isSignal(binding)) {
|
|
1485
|
-
|
|
1506
|
+
bindEffect(el, () => {
|
|
1486
1507
|
applyAttr(el, attrName, binding());
|
|
1487
1508
|
});
|
|
1488
1509
|
}
|
|
@@ -1589,7 +1610,7 @@ function bindField(root, ctx, cleanups) {
|
|
|
1589
1610
|
}
|
|
1590
1611
|
// Setup reactive aria-invalid based on error state
|
|
1591
1612
|
if ('error' in form && typeof form.error === 'function') {
|
|
1592
|
-
|
|
1613
|
+
bindEffect(htmlEl, () => {
|
|
1593
1614
|
// Read current path from DOM attribute inside effect
|
|
1594
1615
|
// This allows the effect to see updated paths after array reorder
|
|
1595
1616
|
const currentPath = htmlEl.getAttribute('data-field-path') || htmlEl.getAttribute('name') || fieldPath;
|
|
@@ -1644,7 +1665,7 @@ function bindError(root, ctx, cleanups) {
|
|
|
1644
1665
|
htmlEl.setAttribute('role', 'alert');
|
|
1645
1666
|
htmlEl.setAttribute('aria-live', 'polite');
|
|
1646
1667
|
// Reactive error display
|
|
1647
|
-
|
|
1668
|
+
bindEffect(htmlEl, () => {
|
|
1648
1669
|
// Read current path from DOM attribute inside effect
|
|
1649
1670
|
// This allows the effect to see updated paths after array reorder
|
|
1650
1671
|
const currentPath = htmlEl.getAttribute('data-error-path') || fieldPath;
|
|
@@ -1689,7 +1710,7 @@ function bindFormError(root, ctx, cleanups) {
|
|
|
1689
1710
|
htmlEl.setAttribute('role', 'alert');
|
|
1690
1711
|
htmlEl.setAttribute('aria-live', 'polite');
|
|
1691
1712
|
// Reactive form error display
|
|
1692
|
-
|
|
1713
|
+
bindEffect(htmlEl, () => {
|
|
1693
1714
|
const errorMsg = form.formError();
|
|
1694
1715
|
if (errorMsg) {
|
|
1695
1716
|
htmlEl.textContent = errorMsg;
|
|
@@ -1978,7 +1999,7 @@ function bindArray(root, ctx, cleanups) {
|
|
|
1978
1999
|
}
|
|
1979
2000
|
}
|
|
1980
2001
|
// Reactive rendering
|
|
1981
|
-
|
|
2002
|
+
bindEffect(el, () => {
|
|
1982
2003
|
renderList();
|
|
1983
2004
|
});
|
|
1984
2005
|
// Bind array operation buttons
|
|
@@ -2080,6 +2101,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
2080
2101
|
// Create a scope for this template binding
|
|
2081
2102
|
const templateScope = createScope();
|
|
2082
2103
|
const cleanups = [];
|
|
2104
|
+
linkScopeToDom(templateScope, root, describeBindRoot(root));
|
|
2083
2105
|
// Run all bindings within the template scope
|
|
2084
2106
|
withScope(templateScope, () => {
|
|
2085
2107
|
// 1. Form setup — must run very early to register form instances
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dalila",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "DOM-first reactive framework based on signals",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
"test": "npm run build && node --test",
|
|
104
104
|
"test:e2e": "npm run build && playwright test",
|
|
105
105
|
"test:watch": "jest --watch",
|
|
106
|
-
"clean": "rm -rf dist"
|
|
106
|
+
"clean": "rm -rf dist",
|
|
107
|
+
"devtools:firefox:run": "npx web-ext run --source-dir devtools-extension --devtools",
|
|
108
|
+
"devtools:firefox:build": "npx web-ext build --source-dir devtools-extension --artifacts-dir devtools-extension/dist --overwrite-dest"
|
|
107
109
|
},
|
|
108
110
|
"keywords": [
|
|
109
111
|
"framework",
|