solid-tag-runtime 0.0.6 → 0.0.8
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/ARCHITECTURE.md +167 -3
- package/README.md +233 -3
- package/html.d.ts +79 -1
- package/index.d.ts +71 -0
- package/package.json +1 -1
- package/src/events.js +63 -0
- package/src/html.js +420 -23
- package/src/runtime.js +392 -78
package/src/events.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function createEventDispatcher(options = {}) {
|
|
2
|
+
const subscriptions = new Set();
|
|
3
|
+
const onListenerError = options.onListenerError;
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
subscribe,
|
|
7
|
+
emit,
|
|
8
|
+
clear,
|
|
9
|
+
get size() {
|
|
10
|
+
return subscriptions.size;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function subscribe(typesOrListener, maybeListener) {
|
|
15
|
+
let types;
|
|
16
|
+
let listener;
|
|
17
|
+
|
|
18
|
+
if (typeof typesOrListener === "function") {
|
|
19
|
+
listener = typesOrListener;
|
|
20
|
+
types = undefined;
|
|
21
|
+
} else {
|
|
22
|
+
listener = maybeListener;
|
|
23
|
+
if (typeof typesOrListener === "string") {
|
|
24
|
+
types = new Set([typesOrListener]);
|
|
25
|
+
} else if (Array.isArray(typesOrListener)) {
|
|
26
|
+
if (typesOrListener.length === 0 || typesOrListener.some(type => typeof type !== "string" || !type)) {
|
|
27
|
+
throw new TypeError("subscribe(types, listener) expects a non-empty event type or event type array.");
|
|
28
|
+
}
|
|
29
|
+
types = new Set(typesOrListener);
|
|
30
|
+
} else {
|
|
31
|
+
throw new TypeError("subscribe() expects a listener, event type + listener, or event type array + listener.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof listener !== "function") {
|
|
36
|
+
throw new TypeError("subscribe() expects a listener function.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const subscription = { types, listener };
|
|
40
|
+
subscriptions.add(subscription);
|
|
41
|
+
|
|
42
|
+
return () => subscriptions.delete(subscription);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function emit(event) {
|
|
46
|
+
for (const subscription of [...subscriptions]) {
|
|
47
|
+
if (subscription.types && !subscription.types.has(event.type)) continue;
|
|
48
|
+
try {
|
|
49
|
+
subscription.listener(event);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
try {
|
|
52
|
+
onListenerError?.(error, event);
|
|
53
|
+
} catch {
|
|
54
|
+
// Listener error reporting must never break the emitter.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clear() {
|
|
61
|
+
subscriptions.clear();
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/html.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SolidTagRuntimeError } from "./errors.js";
|
|
2
|
+
import { createEventDispatcher } from "./events.js";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_SELECTOR = [
|
|
4
5
|
'script[type="solid-jsx"]',
|
|
@@ -47,6 +48,11 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
47
48
|
const controller = {
|
|
48
49
|
id: `solid-tag-html-${++controllerSequence}`,
|
|
49
50
|
runtime,
|
|
51
|
+
events: createEventDispatcher({
|
|
52
|
+
onListenerError(error) {
|
|
53
|
+
globalThis.console?.error?.("solid-tag-runtime/html lifecycle subscriber error", error);
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
50
56
|
scope: normalizeScope(options.scope),
|
|
51
57
|
root: options.root,
|
|
52
58
|
selector: options.selector ?? DEFAULT_SELECTOR,
|
|
@@ -88,6 +94,7 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
88
94
|
get appendTarget() {
|
|
89
95
|
return getAppendTarget(controller);
|
|
90
96
|
},
|
|
97
|
+
subscribe,
|
|
91
98
|
register,
|
|
92
99
|
observe,
|
|
93
100
|
flush,
|
|
@@ -97,6 +104,8 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
97
104
|
setAppendTarget,
|
|
98
105
|
defineElement,
|
|
99
106
|
registerElement,
|
|
107
|
+
updateElement,
|
|
108
|
+
removeElement,
|
|
100
109
|
append,
|
|
101
110
|
addModule,
|
|
102
111
|
owns,
|
|
@@ -107,20 +116,41 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
107
116
|
controller.api = api;
|
|
108
117
|
return api;
|
|
109
118
|
|
|
119
|
+
function subscribe(typesOrListener, maybeListener) {
|
|
120
|
+
return controller.events.subscribe(typesOrListener, maybeListener);
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
/** One-shot scan of matching scripts. */
|
|
111
124
|
async function register(callOptions = {}) {
|
|
112
125
|
const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime register");
|
|
113
126
|
const selector = callOptions.selector ?? controller.selector;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
const origin = callOptions.origin ?? "initial-scan";
|
|
128
|
+
const discovered = Array.from(root.querySelectorAll(selector));
|
|
129
|
+
const elements = [];
|
|
130
|
+
|
|
131
|
+
for (const element of discovered) {
|
|
132
|
+
const accepted = acceptsDiscoveredElement(controller, element, callOptions);
|
|
133
|
+
emitHTML(controller, {
|
|
134
|
+
type: "element-discovered",
|
|
135
|
+
element,
|
|
136
|
+
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
137
|
+
origin,
|
|
138
|
+
accepted,
|
|
139
|
+
});
|
|
140
|
+
if (accepted) elements.push(element);
|
|
141
|
+
}
|
|
117
142
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
try {
|
|
144
|
+
return await processElements(controller, elements, {
|
|
145
|
+
...callOptions,
|
|
146
|
+
root,
|
|
147
|
+
origin,
|
|
148
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
emitHTMLError(controller, "register", error, { origin });
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
124
154
|
}
|
|
125
155
|
|
|
126
156
|
/**
|
|
@@ -172,6 +202,15 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
172
202
|
*/
|
|
173
203
|
async function setRoot(root, callOptions = {}) {
|
|
174
204
|
const nextRoot = getRoot({ root }, "HTML runtime setRoot");
|
|
205
|
+
const previousRoot = controller.root ?? globalThis.document;
|
|
206
|
+
const previousAppendTarget = getAppendTarget(controller);
|
|
207
|
+
emitHTML(controller, {
|
|
208
|
+
type: "root-changing",
|
|
209
|
+
previousRoot,
|
|
210
|
+
root: nextRoot,
|
|
211
|
+
connected: controller.connected,
|
|
212
|
+
registerExisting: callOptions.registerExisting ?? controller.connected,
|
|
213
|
+
});
|
|
175
214
|
|
|
176
215
|
// Finish work scheduled against the old root before rebinding observer state.
|
|
177
216
|
await controller.queue;
|
|
@@ -184,7 +223,7 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
184
223
|
const wasConnected = controller.connected;
|
|
185
224
|
const previousObserverOptions = controller.observerOptions ?? {};
|
|
186
225
|
|
|
187
|
-
if (wasConnected) stopObserver(controller, { preserveOptions: true });
|
|
226
|
+
if (wasConnected) stopObserver(controller, { preserveOptions: true, reason: "root-change" });
|
|
188
227
|
|
|
189
228
|
controller.root = nextRoot;
|
|
190
229
|
if (Object.prototype.hasOwnProperty.call(callOptions, "appendTo")) {
|
|
@@ -202,10 +241,13 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
202
241
|
};
|
|
203
242
|
controller.observerOptions = { ...observeOptions };
|
|
204
243
|
try {
|
|
205
|
-
|
|
244
|
+
const result = await connectObserver(controller, nextRoot, observeOptions, {
|
|
206
245
|
registerExisting,
|
|
207
246
|
initial: false,
|
|
247
|
+
reason: "root-change",
|
|
208
248
|
});
|
|
249
|
+
emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
|
|
250
|
+
return result;
|
|
209
251
|
} catch (error) {
|
|
210
252
|
stopObserver(controller, { preserveOptions: true });
|
|
211
253
|
throw error;
|
|
@@ -213,9 +255,12 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
if (registerExisting) {
|
|
216
|
-
|
|
258
|
+
const result = await register({ ...callOptions, root: nextRoot, origin: callOptions.origin ?? "root-change-scan" });
|
|
259
|
+
emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
|
|
260
|
+
return result;
|
|
217
261
|
}
|
|
218
262
|
|
|
263
|
+
emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
|
|
219
264
|
return emptyRegistrationResult();
|
|
220
265
|
}
|
|
221
266
|
|
|
@@ -233,7 +278,15 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
233
278
|
if (!target || (typeof target.append !== "function" && typeof target.appendChild !== "function")) {
|
|
234
279
|
throw new TypeError("setAppendTarget() requires a target with append() or appendChild().");
|
|
235
280
|
}
|
|
281
|
+
const previousTarget = getAppendTarget(controller);
|
|
236
282
|
controller.appendTo = target;
|
|
283
|
+
if (previousTarget !== target) {
|
|
284
|
+
emitHTML(controller, {
|
|
285
|
+
type: "append-target-changed",
|
|
286
|
+
previousTarget,
|
|
287
|
+
target,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
237
290
|
return api;
|
|
238
291
|
}
|
|
239
292
|
|
|
@@ -259,6 +312,152 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
259
312
|
return result.modules[0] ?? definitionFromOwnedElement(controller, element);
|
|
260
313
|
}
|
|
261
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Re-read an owned script element and replace its runtime module source. The
|
|
317
|
+
* logical module id is stable; changing `module`/src-derived identity requires
|
|
318
|
+
* removeElement() + registerElement() instead.
|
|
319
|
+
*/
|
|
320
|
+
async function updateElement(element, callOptions = {}) {
|
|
321
|
+
const record = requireOwnedRecord(controller, element, "updateElement");
|
|
322
|
+
emitHTML(controller, {
|
|
323
|
+
type: "element-updating",
|
|
324
|
+
element,
|
|
325
|
+
moduleId: record.moduleId,
|
|
326
|
+
origin: callOptions.origin ?? "update-element",
|
|
327
|
+
});
|
|
328
|
+
const metadata = inspectElement(controller, element, {
|
|
329
|
+
...callOptions,
|
|
330
|
+
existingId: record.moduleId,
|
|
331
|
+
});
|
|
332
|
+
const nextModuleId = normalizeModuleId(runtime, metadata.id);
|
|
333
|
+
|
|
334
|
+
if (nextModuleId !== record.moduleId) {
|
|
335
|
+
throw new HTMLModuleError(
|
|
336
|
+
`updateElement() cannot change module identity from ${JSON.stringify(record.moduleId)} to ${JSON.stringify(nextModuleId)}. Remove and register the element again instead.`,
|
|
337
|
+
{ element, moduleId: record.moduleId, sourceUrl: metadata.sourceUrl },
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const candidate = {
|
|
342
|
+
...record,
|
|
343
|
+
format: metadata.format,
|
|
344
|
+
entry: metadata.entry,
|
|
345
|
+
sourceUrl: metadata.sourceUrl,
|
|
346
|
+
inline: !metadata.sourceUrl,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const { source } = await prepareRecordSource(controller, candidate, callOptions);
|
|
350
|
+
runtime.update(record.moduleId, source, { format: candidate.format });
|
|
351
|
+
|
|
352
|
+
record.format = candidate.format;
|
|
353
|
+
record.entry = candidate.entry;
|
|
354
|
+
record.sourceUrl = candidate.sourceUrl;
|
|
355
|
+
record.inline = candidate.inline;
|
|
356
|
+
record.state = "defined";
|
|
357
|
+
record.error = undefined;
|
|
358
|
+
record.definition = definitionFromRecord(record);
|
|
359
|
+
|
|
360
|
+
if (record.entry && (callOptions.executeEntries ?? controller.executeEntries) !== false) {
|
|
361
|
+
record.state = "executing";
|
|
362
|
+
emitHTML(controller, {
|
|
363
|
+
type: "entry-executing",
|
|
364
|
+
element,
|
|
365
|
+
moduleId: record.moduleId,
|
|
366
|
+
origin: callOptions.origin ?? "update-element",
|
|
367
|
+
});
|
|
368
|
+
try {
|
|
369
|
+
const namespace = await runtime.import(record.moduleId);
|
|
370
|
+
record.state = "ready";
|
|
371
|
+
emitHTML(controller, {
|
|
372
|
+
type: "entry-executed",
|
|
373
|
+
element,
|
|
374
|
+
moduleId: record.moduleId,
|
|
375
|
+
namespace,
|
|
376
|
+
origin: callOptions.origin ?? "update-element",
|
|
377
|
+
});
|
|
378
|
+
} catch (error) {
|
|
379
|
+
record.state = "error";
|
|
380
|
+
record.error = error;
|
|
381
|
+
emitHTMLError(controller, "entry", error, {
|
|
382
|
+
element,
|
|
383
|
+
moduleId: record.moduleId,
|
|
384
|
+
origin: callOptions.origin ?? "update-element",
|
|
385
|
+
});
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
emitHTML(controller, {
|
|
391
|
+
type: "element-updated",
|
|
392
|
+
element,
|
|
393
|
+
moduleId: record.moduleId,
|
|
394
|
+
definition: record.definition,
|
|
395
|
+
origin: callOptions.origin ?? "update-element",
|
|
396
|
+
});
|
|
397
|
+
return record.definition;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Release an element from this HTML controller. By default both the runtime
|
|
402
|
+
* module and DOM element are removed. DOM removal and module removal can be
|
|
403
|
+
* controlled independently.
|
|
404
|
+
*/
|
|
405
|
+
async function removeElement(element, callOptions = {}) {
|
|
406
|
+
const record = requireOwnedRecord(controller, element, "removeElement");
|
|
407
|
+
const origin = callOptions.origin ?? "remove-element";
|
|
408
|
+
emitHTML(controller, {
|
|
409
|
+
type: "element-removing",
|
|
410
|
+
element,
|
|
411
|
+
moduleId: record.moduleId,
|
|
412
|
+
origin,
|
|
413
|
+
removeModule: callOptions.removeModule !== false,
|
|
414
|
+
removeFromDOM: callOptions.removeFromDOM !== false,
|
|
415
|
+
});
|
|
416
|
+
const removeModule = callOptions.removeModule !== false;
|
|
417
|
+
const removeFromDOM = callOptions.removeFromDOM !== false;
|
|
418
|
+
const invalidateDependents = callOptions.invalidateDependents !== false;
|
|
419
|
+
|
|
420
|
+
let moduleRemoved = false;
|
|
421
|
+
if (removeModule) {
|
|
422
|
+
moduleRemoved = runtime.remove(record.moduleId, { invalidateDependents });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
controller.bindings.delete(record.moduleId);
|
|
426
|
+
elementOwners.delete(element);
|
|
427
|
+
record.state = "removed";
|
|
428
|
+
emitHTML(controller, {
|
|
429
|
+
type: "element-released",
|
|
430
|
+
element,
|
|
431
|
+
moduleId: record.moduleId,
|
|
432
|
+
origin,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
let elementRemoved = false;
|
|
436
|
+
if (removeFromDOM) {
|
|
437
|
+
elementRemoved = removeDOMElement(element);
|
|
438
|
+
} else {
|
|
439
|
+
// Keep a released element in the observed tree from being immediately
|
|
440
|
+
// rediscovered. Explicit registerElement() can still claim it again.
|
|
441
|
+
controller.ignoredExisting.add(element);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = {
|
|
445
|
+
id: record.moduleId,
|
|
446
|
+
element,
|
|
447
|
+
moduleRemoved,
|
|
448
|
+
elementRemoved,
|
|
449
|
+
};
|
|
450
|
+
emitHTML(controller, {
|
|
451
|
+
type: "element-removed",
|
|
452
|
+
element,
|
|
453
|
+
moduleId: record.moduleId,
|
|
454
|
+
origin,
|
|
455
|
+
moduleRemoved,
|
|
456
|
+
elementRemoved,
|
|
457
|
+
});
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
262
461
|
/**
|
|
263
462
|
* Claim a user-created script before DOM insertion, append it to this
|
|
264
463
|
* controller's target, then register it directly. The observer will see the
|
|
@@ -404,6 +603,13 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
404
603
|
childList: true,
|
|
405
604
|
subtree: callOptions.subtree ?? controller.subtree,
|
|
406
605
|
});
|
|
606
|
+
emitHTML(controller, {
|
|
607
|
+
type: "observer-connected",
|
|
608
|
+
root,
|
|
609
|
+
subtree: callOptions.subtree ?? controller.subtree,
|
|
610
|
+
registerExisting,
|
|
611
|
+
reason: mode.reason ?? (mode.initial ? "observe" : "connect"),
|
|
612
|
+
});
|
|
407
613
|
|
|
408
614
|
const result = registerExisting
|
|
409
615
|
? await enqueueObserverScan(controller, {
|
|
@@ -418,11 +624,17 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
418
624
|
|
|
419
625
|
function stopObserver(controller, options = {}) {
|
|
420
626
|
if (!controller.connected && !controller.observer) return;
|
|
627
|
+
const root = controller.observerRoot;
|
|
421
628
|
controller.connected = false;
|
|
422
629
|
controller.observer?.disconnect();
|
|
423
630
|
controller.observer = undefined;
|
|
424
631
|
controller.observerRoot = undefined;
|
|
425
632
|
if (!options.preserveOptions) controller.observerOptions = undefined;
|
|
633
|
+
emitHTML(controller, {
|
|
634
|
+
type: "observer-disconnected",
|
|
635
|
+
root,
|
|
636
|
+
reason: options.reason ?? "disconnect",
|
|
637
|
+
});
|
|
426
638
|
}
|
|
427
639
|
|
|
428
640
|
async function enqueueObserverScan(controller, callOptions = {}) {
|
|
@@ -431,23 +643,76 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
431
643
|
|
|
432
644
|
const root = controller.observerRoot ?? getRoot(mergeOptions(controller, callOptions), "HTML runtime observer");
|
|
433
645
|
const selector = callOptions.selector ?? controller.selector;
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
646
|
+
const origin = callOptions.origin ?? "observer";
|
|
647
|
+
const discovered = Array.from(root.querySelectorAll(selector));
|
|
648
|
+
const elements = [];
|
|
649
|
+
let matched = 0;
|
|
650
|
+
let ignored = 0;
|
|
651
|
+
|
|
652
|
+
for (const element of discovered) {
|
|
653
|
+
if (controller.ignoredExisting.has(element)) {
|
|
654
|
+
ignored += 1;
|
|
655
|
+
emitHTML(controller, {
|
|
656
|
+
type: "element-discovered",
|
|
657
|
+
element,
|
|
658
|
+
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
659
|
+
origin,
|
|
660
|
+
accepted: false,
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const accepted = acceptsDiscoveredElement(controller, element, callOptions);
|
|
666
|
+
emitHTML(controller, {
|
|
667
|
+
type: "element-discovered",
|
|
668
|
+
element,
|
|
669
|
+
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
670
|
+
origin,
|
|
671
|
+
accepted,
|
|
672
|
+
});
|
|
673
|
+
if (!accepted) {
|
|
674
|
+
ignored += 1;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
matched += 1;
|
|
437
678
|
|
|
438
679
|
const ownership = elementOwners.get(element);
|
|
439
|
-
if (!ownership)
|
|
440
|
-
|
|
680
|
+
if (!ownership) {
|
|
681
|
+
elements.push(element);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (ownership.owner === controller) {
|
|
685
|
+
ignored += 1;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
441
688
|
|
|
442
689
|
throw ownershipError(controller, element, ownership);
|
|
443
|
-
}
|
|
690
|
+
}
|
|
444
691
|
|
|
445
|
-
|
|
446
|
-
|
|
692
|
+
let result;
|
|
693
|
+
try {
|
|
694
|
+
result = await processElements(controller, elements, {
|
|
695
|
+
...callOptions,
|
|
696
|
+
root,
|
|
697
|
+
origin,
|
|
698
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
699
|
+
});
|
|
700
|
+
} catch (error) {
|
|
701
|
+
emitHTMLError(controller, "observer", error, { origin });
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
emitHTML(controller, {
|
|
706
|
+
type: "observer-batch",
|
|
447
707
|
root,
|
|
448
|
-
origin
|
|
449
|
-
|
|
708
|
+
origin,
|
|
709
|
+
discovered: discovered.length,
|
|
710
|
+
matched,
|
|
711
|
+
claimed: result.modules.length,
|
|
712
|
+
ignored,
|
|
713
|
+
modules: result.modules.map(module => module.id),
|
|
450
714
|
});
|
|
715
|
+
return result;
|
|
451
716
|
});
|
|
452
717
|
|
|
453
718
|
controller.queue = task.then(
|
|
@@ -494,6 +759,12 @@ async function processClaims(controller, records, options = {}) {
|
|
|
494
759
|
try {
|
|
495
760
|
prepared = await Promise.all(records.map(record => prepareRecordSource(controller, record, options)));
|
|
496
761
|
} catch (error) {
|
|
762
|
+
const failed = records.find(record => record.state === "loading") ?? records[0];
|
|
763
|
+
emitHTMLError(controller, failed?.sourceUrl ? "fetch" : "register", error, {
|
|
764
|
+
element: failed?.element,
|
|
765
|
+
moduleId: failed?.moduleId,
|
|
766
|
+
origin: failed?.origin ?? options.origin,
|
|
767
|
+
});
|
|
497
768
|
for (const record of records) rejectRecord(record, error);
|
|
498
769
|
throw error;
|
|
499
770
|
}
|
|
@@ -511,8 +782,21 @@ async function processClaims(controller, records, options = {}) {
|
|
|
511
782
|
record.definition = definitionFromRecord(record);
|
|
512
783
|
record.resolve(record.definition);
|
|
513
784
|
modules.push(record.definition);
|
|
785
|
+
emitHTML(controller, {
|
|
786
|
+
type: "element-registered",
|
|
787
|
+
element: record.element,
|
|
788
|
+
moduleId: record.moduleId,
|
|
789
|
+
definition: record.definition,
|
|
790
|
+
origin: record.origin,
|
|
791
|
+
});
|
|
514
792
|
}
|
|
515
793
|
} catch (error) {
|
|
794
|
+
const failed = records.find(record => record.state === "claimed" || record.state === "loading") ?? records[0];
|
|
795
|
+
emitHTMLError(controller, "register", error, {
|
|
796
|
+
element: failed?.element,
|
|
797
|
+
moduleId: failed?.moduleId,
|
|
798
|
+
origin: failed?.origin ?? options.origin,
|
|
799
|
+
});
|
|
516
800
|
for (const record of records) {
|
|
517
801
|
if (record.state === "claimed" || record.state === "loading") rejectRecord(record, error);
|
|
518
802
|
}
|
|
@@ -528,14 +812,32 @@ async function processClaims(controller, records, options = {}) {
|
|
|
528
812
|
if (record) record.state = "executing";
|
|
529
813
|
|
|
530
814
|
try {
|
|
815
|
+
emitHTML(controller, {
|
|
816
|
+
type: "entry-executing",
|
|
817
|
+
element: entry.element,
|
|
818
|
+
moduleId: entry.id,
|
|
819
|
+
origin: record?.origin ?? options.origin ?? "entry",
|
|
820
|
+
});
|
|
531
821
|
const namespace = await controller.runtime.import(entry.id);
|
|
532
822
|
if (record) record.state = "ready";
|
|
533
823
|
executed.push({ id: entry.id, namespace });
|
|
824
|
+
emitHTML(controller, {
|
|
825
|
+
type: "entry-executed",
|
|
826
|
+
element: entry.element,
|
|
827
|
+
moduleId: entry.id,
|
|
828
|
+
namespace,
|
|
829
|
+
origin: record?.origin ?? options.origin ?? "entry",
|
|
830
|
+
});
|
|
534
831
|
} catch (error) {
|
|
535
832
|
if (record) {
|
|
536
833
|
record.state = "error";
|
|
537
834
|
record.error = error;
|
|
538
835
|
}
|
|
836
|
+
emitHTMLError(controller, "entry", error, {
|
|
837
|
+
element: entry.element,
|
|
838
|
+
moduleId: entry.id,
|
|
839
|
+
origin: record?.origin ?? options.origin ?? "entry",
|
|
840
|
+
});
|
|
539
841
|
throw error;
|
|
540
842
|
}
|
|
541
843
|
}
|
|
@@ -608,6 +910,15 @@ function claimElement(controller, element, options = {}) {
|
|
|
608
910
|
|
|
609
911
|
elementOwners.set(element, record);
|
|
610
912
|
controller.bindings.set(moduleId, record);
|
|
913
|
+
emitHTML(controller, {
|
|
914
|
+
type: "element-claimed",
|
|
915
|
+
element,
|
|
916
|
+
moduleId,
|
|
917
|
+
format: record.format,
|
|
918
|
+
entry: record.entry,
|
|
919
|
+
sourceUrl: record.sourceUrl,
|
|
920
|
+
origin: record.origin,
|
|
921
|
+
});
|
|
611
922
|
return { fresh: true, record };
|
|
612
923
|
}
|
|
613
924
|
|
|
@@ -616,12 +927,29 @@ async function prepareRecordSource(controller, record, options) {
|
|
|
616
927
|
|
|
617
928
|
let source;
|
|
618
929
|
if (record.sourceUrl) {
|
|
930
|
+
const startedAt = performanceNow();
|
|
931
|
+
emitHTML(controller, {
|
|
932
|
+
type: "element-loading",
|
|
933
|
+
element: record.element,
|
|
934
|
+
moduleId: record.moduleId,
|
|
935
|
+
sourceUrl: record.sourceUrl,
|
|
936
|
+
origin: record.origin,
|
|
937
|
+
});
|
|
619
938
|
source = await fetchSource(
|
|
620
939
|
record.sourceUrl,
|
|
621
940
|
options.fetch ?? controller.fetch ?? globalThis.fetch,
|
|
622
941
|
record.element,
|
|
623
942
|
record.moduleId,
|
|
624
943
|
);
|
|
944
|
+
emitHTML(controller, {
|
|
945
|
+
type: "element-loaded",
|
|
946
|
+
element: record.element,
|
|
947
|
+
moduleId: record.moduleId,
|
|
948
|
+
sourceUrl: record.sourceUrl,
|
|
949
|
+
origin: record.origin,
|
|
950
|
+
bytes: String(source).length,
|
|
951
|
+
durationMs: performanceNow() - startedAt,
|
|
952
|
+
});
|
|
625
953
|
} else {
|
|
626
954
|
source = record.element.textContent ?? "";
|
|
627
955
|
}
|
|
@@ -649,7 +977,7 @@ function inspectElement(controller, element, options = {}) {
|
|
|
649
977
|
sourceUrl,
|
|
650
978
|
});
|
|
651
979
|
|
|
652
|
-
const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
|
|
980
|
+
const id = explicitId ?? sourceUrl ?? (entry ? options.existingId ?? createAnonymousId(format) : undefined);
|
|
653
981
|
if (!id) {
|
|
654
982
|
throw new HTMLModuleError(
|
|
655
983
|
"Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
|
|
@@ -660,6 +988,35 @@ function inspectElement(controller, element, options = {}) {
|
|
|
660
988
|
return { id, format, entry, sourceUrl };
|
|
661
989
|
}
|
|
662
990
|
|
|
991
|
+
function requireOwnedRecord(controller, element, caller) {
|
|
992
|
+
assertElement(element, caller);
|
|
993
|
+
const record = elementOwners.get(element);
|
|
994
|
+
if (!record) {
|
|
995
|
+
throw new HTMLModuleOwnershipError(
|
|
996
|
+
`${caller}() requires an element already owned by this HTML runtime.`,
|
|
997
|
+
{ element, requestedScope: controller.scope },
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (record.owner !== controller) throw ownershipError(controller, element, record);
|
|
1001
|
+
return record;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function removeDOMElement(element) {
|
|
1005
|
+
if (typeof element?.remove === "function") {
|
|
1006
|
+
const wasConnected = Boolean(element.parentNode);
|
|
1007
|
+
element.remove();
|
|
1008
|
+
return wasConnected || !element.parentNode;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const parent = element?.parentNode;
|
|
1012
|
+
if (parent && typeof parent.removeChild === "function") {
|
|
1013
|
+
parent.removeChild(element);
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
663
1020
|
function definitionFromRecord(record) {
|
|
664
1021
|
return {
|
|
665
1022
|
id: record.moduleId,
|
|
@@ -741,6 +1098,46 @@ function ownershipError(controller, element, ownership) {
|
|
|
741
1098
|
);
|
|
742
1099
|
}
|
|
743
1100
|
|
|
1101
|
+
function emitHTML(controller, event) {
|
|
1102
|
+
controller.events.emit({
|
|
1103
|
+
htmlRuntimeId: controller.id,
|
|
1104
|
+
scope: controller.scope,
|
|
1105
|
+
timestamp: Date.now(),
|
|
1106
|
+
...event,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function emitHTMLError(controller, operation, error, context = {}) {
|
|
1111
|
+
emitHTML(controller, {
|
|
1112
|
+
type: "html-error",
|
|
1113
|
+
operation,
|
|
1114
|
+
error,
|
|
1115
|
+
...context,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function emitRootChanged(controller, previousRoot, root, registerExisting, previousAppendTarget) {
|
|
1120
|
+
emitHTML(controller, {
|
|
1121
|
+
type: "root-changed",
|
|
1122
|
+
previousRoot,
|
|
1123
|
+
root,
|
|
1124
|
+
registerExisting,
|
|
1125
|
+
connected: controller.connected,
|
|
1126
|
+
});
|
|
1127
|
+
const nextAppendTarget = getAppendTarget(controller);
|
|
1128
|
+
if (previousAppendTarget !== nextAppendTarget) {
|
|
1129
|
+
emitHTML(controller, {
|
|
1130
|
+
type: "append-target-changed",
|
|
1131
|
+
previousTarget: previousAppendTarget,
|
|
1132
|
+
target: nextAppendTarget,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function performanceNow() {
|
|
1138
|
+
return globalThis.performance?.now?.() ?? Date.now();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
744
1141
|
function getDefaultController(runtime, options = {}) {
|
|
745
1142
|
assertRuntime(runtime);
|
|
746
1143
|
let controller = defaultControllers.get(runtime);
|