solid-tag-runtime 0.0.7 → 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/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,
@@ -109,20 +116,41 @@ export function createHTMLRuntime(runtime, options = {}) {
109
116
  controller.api = api;
110
117
  return api;
111
118
 
119
+ function subscribe(typesOrListener, maybeListener) {
120
+ return controller.events.subscribe(typesOrListener, maybeListener);
121
+ }
122
+
112
123
  /** One-shot scan of matching scripts. */
113
124
  async function register(callOptions = {}) {
114
125
  const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime register");
115
126
  const selector = callOptions.selector ?? controller.selector;
116
- const elements = Array.from(root.querySelectorAll(selector)).filter(element =>
117
- acceptsDiscoveredElement(controller, element, callOptions),
118
- );
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
+ }
119
142
 
120
- return processElements(controller, elements, {
121
- ...callOptions,
122
- root,
123
- origin: callOptions.origin ?? "initial-scan",
124
- executeEntries: callOptions.executeEntries ?? controller.executeEntries,
125
- });
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
+ }
126
154
  }
127
155
 
128
156
  /**
@@ -174,6 +202,15 @@ export function createHTMLRuntime(runtime, options = {}) {
174
202
  */
175
203
  async function setRoot(root, callOptions = {}) {
176
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
+ });
177
214
 
178
215
  // Finish work scheduled against the old root before rebinding observer state.
179
216
  await controller.queue;
@@ -186,7 +223,7 @@ export function createHTMLRuntime(runtime, options = {}) {
186
223
  const wasConnected = controller.connected;
187
224
  const previousObserverOptions = controller.observerOptions ?? {};
188
225
 
189
- if (wasConnected) stopObserver(controller, { preserveOptions: true });
226
+ if (wasConnected) stopObserver(controller, { preserveOptions: true, reason: "root-change" });
190
227
 
191
228
  controller.root = nextRoot;
192
229
  if (Object.prototype.hasOwnProperty.call(callOptions, "appendTo")) {
@@ -204,10 +241,13 @@ export function createHTMLRuntime(runtime, options = {}) {
204
241
  };
205
242
  controller.observerOptions = { ...observeOptions };
206
243
  try {
207
- return await connectObserver(controller, nextRoot, observeOptions, {
244
+ const result = await connectObserver(controller, nextRoot, observeOptions, {
208
245
  registerExisting,
209
246
  initial: false,
247
+ reason: "root-change",
210
248
  });
249
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
250
+ return result;
211
251
  } catch (error) {
212
252
  stopObserver(controller, { preserveOptions: true });
213
253
  throw error;
@@ -215,9 +255,12 @@ export function createHTMLRuntime(runtime, options = {}) {
215
255
  }
216
256
 
217
257
  if (registerExisting) {
218
- return register({ ...callOptions, root: nextRoot });
258
+ const result = await register({ ...callOptions, root: nextRoot, origin: callOptions.origin ?? "root-change-scan" });
259
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
260
+ return result;
219
261
  }
220
262
 
263
+ emitRootChanged(controller, previousRoot, nextRoot, registerExisting, previousAppendTarget);
221
264
  return emptyRegistrationResult();
222
265
  }
223
266
 
@@ -235,7 +278,15 @@ export function createHTMLRuntime(runtime, options = {}) {
235
278
  if (!target || (typeof target.append !== "function" && typeof target.appendChild !== "function")) {
236
279
  throw new TypeError("setAppendTarget() requires a target with append() or appendChild().");
237
280
  }
281
+ const previousTarget = getAppendTarget(controller);
238
282
  controller.appendTo = target;
283
+ if (previousTarget !== target) {
284
+ emitHTML(controller, {
285
+ type: "append-target-changed",
286
+ previousTarget,
287
+ target,
288
+ });
289
+ }
239
290
  return api;
240
291
  }
241
292
 
@@ -268,6 +319,12 @@ export function createHTMLRuntime(runtime, options = {}) {
268
319
  */
269
320
  async function updateElement(element, callOptions = {}) {
270
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
+ });
271
328
  const metadata = inspectElement(controller, element, {
272
329
  ...callOptions,
273
330
  existingId: record.moduleId,
@@ -302,16 +359,41 @@ export function createHTMLRuntime(runtime, options = {}) {
302
359
 
303
360
  if (record.entry && (callOptions.executeEntries ?? controller.executeEntries) !== false) {
304
361
  record.state = "executing";
362
+ emitHTML(controller, {
363
+ type: "entry-executing",
364
+ element,
365
+ moduleId: record.moduleId,
366
+ origin: callOptions.origin ?? "update-element",
367
+ });
305
368
  try {
306
- await runtime.import(record.moduleId);
369
+ const namespace = await runtime.import(record.moduleId);
307
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
+ });
308
378
  } catch (error) {
309
379
  record.state = "error";
310
380
  record.error = error;
381
+ emitHTMLError(controller, "entry", error, {
382
+ element,
383
+ moduleId: record.moduleId,
384
+ origin: callOptions.origin ?? "update-element",
385
+ });
311
386
  throw error;
312
387
  }
313
388
  }
314
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
+ });
315
397
  return record.definition;
316
398
  }
317
399
 
@@ -322,6 +404,15 @@ export function createHTMLRuntime(runtime, options = {}) {
322
404
  */
323
405
  async function removeElement(element, callOptions = {}) {
324
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
+ });
325
416
  const removeModule = callOptions.removeModule !== false;
326
417
  const removeFromDOM = callOptions.removeFromDOM !== false;
327
418
  const invalidateDependents = callOptions.invalidateDependents !== false;
@@ -334,6 +425,12 @@ export function createHTMLRuntime(runtime, options = {}) {
334
425
  controller.bindings.delete(record.moduleId);
335
426
  elementOwners.delete(element);
336
427
  record.state = "removed";
428
+ emitHTML(controller, {
429
+ type: "element-released",
430
+ element,
431
+ moduleId: record.moduleId,
432
+ origin,
433
+ });
337
434
 
338
435
  let elementRemoved = false;
339
436
  if (removeFromDOM) {
@@ -344,12 +441,21 @@ export function createHTMLRuntime(runtime, options = {}) {
344
441
  controller.ignoredExisting.add(element);
345
442
  }
346
443
 
347
- return {
444
+ const result = {
348
445
  id: record.moduleId,
349
446
  element,
350
447
  moduleRemoved,
351
448
  elementRemoved,
352
449
  };
450
+ emitHTML(controller, {
451
+ type: "element-removed",
452
+ element,
453
+ moduleId: record.moduleId,
454
+ origin,
455
+ moduleRemoved,
456
+ elementRemoved,
457
+ });
458
+ return result;
353
459
  }
354
460
 
355
461
  /**
@@ -497,6 +603,13 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
497
603
  childList: true,
498
604
  subtree: callOptions.subtree ?? controller.subtree,
499
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
+ });
500
613
 
501
614
  const result = registerExisting
502
615
  ? await enqueueObserverScan(controller, {
@@ -511,11 +624,17 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
511
624
 
512
625
  function stopObserver(controller, options = {}) {
513
626
  if (!controller.connected && !controller.observer) return;
627
+ const root = controller.observerRoot;
514
628
  controller.connected = false;
515
629
  controller.observer?.disconnect();
516
630
  controller.observer = undefined;
517
631
  controller.observerRoot = undefined;
518
632
  if (!options.preserveOptions) controller.observerOptions = undefined;
633
+ emitHTML(controller, {
634
+ type: "observer-disconnected",
635
+ root,
636
+ reason: options.reason ?? "disconnect",
637
+ });
519
638
  }
520
639
 
521
640
  async function enqueueObserverScan(controller, callOptions = {}) {
@@ -524,23 +643,76 @@ async function enqueueObserverScan(controller, callOptions = {}) {
524
643
 
525
644
  const root = controller.observerRoot ?? getRoot(mergeOptions(controller, callOptions), "HTML runtime observer");
526
645
  const selector = callOptions.selector ?? controller.selector;
527
- const elements = Array.from(root.querySelectorAll(selector)).filter(element => {
528
- if (controller.ignoredExisting.has(element)) return false;
529
- if (!acceptsDiscoveredElement(controller, element, callOptions)) return false;
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;
530
678
 
531
679
  const ownership = elementOwners.get(element);
532
- if (!ownership) return true;
533
- if (ownership.owner === controller) return false;
680
+ if (!ownership) {
681
+ elements.push(element);
682
+ continue;
683
+ }
684
+ if (ownership.owner === controller) {
685
+ ignored += 1;
686
+ continue;
687
+ }
534
688
 
535
689
  throw ownershipError(controller, element, ownership);
536
- });
690
+ }
537
691
 
538
- return processElements(controller, elements, {
539
- ...callOptions,
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",
540
707
  root,
541
- origin: callOptions.origin ?? "observer",
542
- executeEntries: callOptions.executeEntries ?? controller.executeEntries,
708
+ origin,
709
+ discovered: discovered.length,
710
+ matched,
711
+ claimed: result.modules.length,
712
+ ignored,
713
+ modules: result.modules.map(module => module.id),
543
714
  });
715
+ return result;
544
716
  });
545
717
 
546
718
  controller.queue = task.then(
@@ -587,6 +759,12 @@ async function processClaims(controller, records, options = {}) {
587
759
  try {
588
760
  prepared = await Promise.all(records.map(record => prepareRecordSource(controller, record, options)));
589
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
+ });
590
768
  for (const record of records) rejectRecord(record, error);
591
769
  throw error;
592
770
  }
@@ -604,8 +782,21 @@ async function processClaims(controller, records, options = {}) {
604
782
  record.definition = definitionFromRecord(record);
605
783
  record.resolve(record.definition);
606
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
+ });
607
792
  }
608
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
+ });
609
800
  for (const record of records) {
610
801
  if (record.state === "claimed" || record.state === "loading") rejectRecord(record, error);
611
802
  }
@@ -621,14 +812,32 @@ async function processClaims(controller, records, options = {}) {
621
812
  if (record) record.state = "executing";
622
813
 
623
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
+ });
624
821
  const namespace = await controller.runtime.import(entry.id);
625
822
  if (record) record.state = "ready";
626
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
+ });
627
831
  } catch (error) {
628
832
  if (record) {
629
833
  record.state = "error";
630
834
  record.error = error;
631
835
  }
836
+ emitHTMLError(controller, "entry", error, {
837
+ element: entry.element,
838
+ moduleId: entry.id,
839
+ origin: record?.origin ?? options.origin ?? "entry",
840
+ });
632
841
  throw error;
633
842
  }
634
843
  }
@@ -701,6 +910,15 @@ function claimElement(controller, element, options = {}) {
701
910
 
702
911
  elementOwners.set(element, record);
703
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
+ });
704
922
  return { fresh: true, record };
705
923
  }
706
924
 
@@ -709,12 +927,29 @@ async function prepareRecordSource(controller, record, options) {
709
927
 
710
928
  let source;
711
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
+ });
712
938
  source = await fetchSource(
713
939
  record.sourceUrl,
714
940
  options.fetch ?? controller.fetch ?? globalThis.fetch,
715
941
  record.element,
716
942
  record.moduleId,
717
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
+ });
718
953
  } else {
719
954
  source = record.element.textContent ?? "";
720
955
  }
@@ -863,6 +1098,46 @@ function ownershipError(controller, element, ownership) {
863
1098
  );
864
1099
  }
865
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
+
866
1141
  function getDefaultController(runtime, options = {}) {
867
1142
  assertRuntime(runtime);
868
1143
  let controller = defaultControllers.get(runtime);