solid-tag-runtime 0.0.8 → 0.0.11

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,5 +1,6 @@
1
1
  import { SolidTagRuntimeError } from "./errors.js";
2
2
  import { createEventDispatcher } from "./events.js";
3
+ import { HTMLRenderError, createDefaultHTMLRenderAdapter, resolveRenderComponent } from "./render.js";
3
4
 
4
5
  const DEFAULT_SELECTOR = [
5
6
  'script[type="solid-jsx"]',
@@ -13,6 +14,10 @@ const DEFAULT_SELECTOR = [
13
14
  const RUNTIME_SCOPE_ATTRIBUTE = "data-solid-runtime";
14
15
  const elementOwners = new WeakMap();
15
16
  const defaultControllers = new WeakMap();
17
+ const activeControllers = new Set();
18
+ const solidRenderClasses = new WeakMap();
19
+ const solidRenderInternals = new WeakMap();
20
+ const unresolvedRenderScopeWarnings = new WeakMap();
16
21
  let anonymousSequence = 0;
17
22
  let controllerSequence = 0;
18
23
 
@@ -35,6 +40,8 @@ export class HTMLModuleOwnershipError extends HTMLModuleError {
35
40
  }
36
41
  }
37
42
 
43
+ export { HTMLRenderError };
44
+
38
45
  /**
39
46
  * Create a persistent DOM/HTML adapter for one solid-tag-runtime instance.
40
47
  *
@@ -58,13 +65,17 @@ export function createHTMLRuntime(runtime, options = {}) {
58
65
  selector: options.selector ?? DEFAULT_SELECTOR,
59
66
  acceptUnscoped: options.acceptUnscoped !== false,
60
67
  executeEntries: options.executeEntries !== false,
68
+ executeRenders: options.executeRenders !== false,
61
69
  fetch: options.fetch,
62
70
  baseUrl: options.baseUrl,
63
71
  MutationObserver: options.MutationObserver,
64
72
  subtree: options.subtree !== false,
65
73
  createElement: options.createElement,
66
74
  appendTo: options.appendTo,
75
+ renderAdapter: options.renderAdapter ?? createDefaultHTMLRenderAdapter(runtime),
67
76
  bindings: new Map(),
77
+ renderElements: new Set(),
78
+ disposed: false,
68
79
  observer: undefined,
69
80
  observerRoot: undefined,
70
81
  observerOptions: undefined,
@@ -111,9 +122,18 @@ export function createHTMLRuntime(runtime, options = {}) {
111
122
  owns,
112
123
  getModuleId,
113
124
  getElement,
125
+ dispose,
114
126
  };
115
127
 
116
128
  controller.api = api;
129
+ activeControllers.add(controller);
130
+ // The HTML adapter owns the global custom-element registration. Registering
131
+ // here means a controller is already available before any existing
132
+ // <solid-render> elements are upgraded/connected. The helper is idempotent.
133
+ registerSolidRenderElement();
134
+ controller.runtimeDisposeUnsubscribe = runtime.subscribe?.("runtime-disposed", () => {
135
+ void disposeController(controller, { runtimeDisposed: true });
136
+ });
117
137
  return api;
118
138
 
119
139
  function subscribe(typesOrListener, maybeListener) {
@@ -138,15 +158,19 @@ export function createHTMLRuntime(runtime, options = {}) {
138
158
  accepted,
139
159
  });
140
160
  if (accepted) elements.push(element);
161
+ else maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
141
162
  }
142
163
 
143
164
  try {
144
- return await processElements(controller, elements, {
165
+ const result = await processElements(controller, elements, {
145
166
  ...callOptions,
146
167
  root,
147
168
  origin,
148
169
  executeEntries: callOptions.executeEntries ?? controller.executeEntries,
170
+ executeRenders: callOptions.executeRenders ?? controller.executeRenders,
149
171
  });
172
+ registerSolidRenderElement();
173
+ return result;
150
174
  } catch (error) {
151
175
  emitHTMLError(controller, "register", error, { origin });
152
176
  throw error;
@@ -168,6 +192,7 @@ export function createHTMLRuntime(runtime, options = {}) {
168
192
  registerExisting: callOptions.registerExisting !== false,
169
193
  initial: true,
170
194
  });
195
+ registerSolidRenderElement();
171
196
  } catch (error) {
172
197
  disconnect();
173
198
  throw error;
@@ -194,6 +219,10 @@ export function createHTMLRuntime(runtime, options = {}) {
194
219
  stopObserver(controller);
195
220
  }
196
221
 
222
+ async function dispose() {
223
+ await disposeController(controller, { runtimeDisposed: false });
224
+ }
225
+
197
226
  /**
198
227
  * Change the discovery/observation root. If observation is active, it is
199
228
  * rebound to the new root and existing matching scripts are registered by
@@ -297,6 +326,7 @@ export function createHTMLRuntime(runtime, options = {}) {
297
326
  origin: callOptions.origin ?? "define-element",
298
327
  explicit: true,
299
328
  executeEntries: false,
329
+ executeRenders: false,
300
330
  });
301
331
  return result.modules[0] ?? definitionFromOwnedElement(controller, element);
302
332
  }
@@ -308,6 +338,7 @@ export function createHTMLRuntime(runtime, options = {}) {
308
338
  origin: callOptions.origin ?? "register-element",
309
339
  explicit: true,
310
340
  executeEntries: callOptions.executeEntries ?? controller.executeEntries,
341
+ executeRenders: callOptions.executeRenders ?? controller.executeRenders,
311
342
  });
312
343
  return result.modules[0] ?? definitionFromOwnedElement(controller, element);
313
344
  }
@@ -342,15 +373,30 @@ export function createHTMLRuntime(runtime, options = {}) {
342
373
  ...record,
343
374
  format: metadata.format,
344
375
  entry: metadata.entry,
376
+ render: metadata.render,
377
+ component: metadata.component,
345
378
  sourceUrl: metadata.sourceUrl,
346
379
  inline: !metadata.sourceUrl,
347
380
  };
348
381
 
349
382
  const { source } = await prepareRecordSource(controller, candidate, callOptions);
383
+ const previousRender = record.render;
350
384
  runtime.update(record.moduleId, source, { format: candidate.format });
351
385
 
386
+ if (record.mount) {
387
+ const previousInPlace = previousRender === true;
388
+ const nextInPlace = candidate.render === true;
389
+ await disposeRecordMount(controller, record, {
390
+ reason: "element-update",
391
+ removeAnchors: previousInPlace && !nextInPlace,
392
+ restoreElement: previousInPlace && !nextInPlace,
393
+ });
394
+ }
395
+
352
396
  record.format = candidate.format;
353
397
  record.entry = candidate.entry;
398
+ record.render = candidate.render;
399
+ record.component = candidate.component;
354
400
  record.sourceUrl = candidate.sourceUrl;
355
401
  record.inline = candidate.inline;
356
402
  record.state = "defined";
@@ -358,33 +404,15 @@ export function createHTMLRuntime(runtime, options = {}) {
358
404
  record.definition = definitionFromRecord(record);
359
405
 
360
406
  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,
407
+ await executeEntryRecord(controller, record, {
408
+ ...callOptions,
409
+ origin: callOptions.origin ?? "update-element",
410
+ });
411
+ } else if (record.render !== undefined && (callOptions.executeRenders ?? controller.executeRenders) !== false) {
412
+ await renderDeclarationRecord(controller, record, {
413
+ ...callOptions,
366
414
  origin: callOptions.origin ?? "update-element",
367
415
  });
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
416
  }
389
417
 
390
418
  emitHTML(controller, {
@@ -417,6 +445,14 @@ export function createHTMLRuntime(runtime, options = {}) {
417
445
  const removeFromDOM = callOptions.removeFromDOM !== false;
418
446
  const invalidateDependents = callOptions.invalidateDependents !== false;
419
447
 
448
+ if (record.mount) {
449
+ await disposeRecordMount(controller, record, {
450
+ reason: "element-remove",
451
+ removeAnchors: record.render === true,
452
+ restoreElement: record.render === true && !removeFromDOM,
453
+ });
454
+ }
455
+
420
456
  let moduleRemoved = false;
421
457
  if (removeModule) {
422
458
  moduleRemoved = runtime.remove(record.moduleId, { invalidateDependents });
@@ -434,7 +470,7 @@ export function createHTMLRuntime(runtime, options = {}) {
434
470
 
435
471
  let elementRemoved = false;
436
472
  if (removeFromDOM) {
437
- elementRemoved = removeDOMElement(element);
473
+ elementRemoved = record.render === true ? true : removeDOMElement(element);
438
474
  } else {
439
475
  // Keep a released element in the observed tree from being immediately
440
476
  // rediscovered. Explicit registerElement() can still claim it again.
@@ -477,6 +513,7 @@ export function createHTMLRuntime(runtime, options = {}) {
477
513
  ...callOptions,
478
514
  origin: callOptions.origin ?? "append",
479
515
  executeEntries: callOptions.executeEntries ?? controller.executeEntries,
516
+ executeRenders: callOptions.executeRenders ?? controller.executeRenders,
480
517
  });
481
518
 
482
519
  if (!claim.fresh) await claim.record.promise;
@@ -515,6 +552,9 @@ export function createHTMLRuntime(runtime, options = {}) {
515
552
  if (id) element.setAttribute("module", id);
516
553
  if (moduleOptions.src) element.setAttribute("src", String(moduleOptions.src));
517
554
  if (moduleOptions.entry) element.setAttribute("entry", "");
555
+ if (moduleOptions.render === true) element.setAttribute("render", "");
556
+ else if (typeof moduleOptions.render === "string") element.setAttribute("render", moduleOptions.render);
557
+ if (moduleOptions.component) element.setAttribute("component", String(moduleOptions.component));
518
558
  if (moduleOptions.language) element.setAttribute("language", String(moduleOptions.language));
519
559
  if (moduleOptions.source != null) element.textContent = String(moduleOptions.source);
520
560
 
@@ -571,6 +611,424 @@ export async function defineScript(runtime, element, options = {}) {
571
611
  export const htmlModuleSelector = DEFAULT_SELECTOR;
572
612
  export const htmlRuntimeScopeAttribute = RUNTIME_SCOPE_ATTRIBUTE;
573
613
 
614
+
615
+ /**
616
+ * Register the global <solid-render> custom element once for the selected
617
+ * CustomElementRegistry. registerHTML()/observe() call this automatically in a
618
+ * browser; programmatic-only applications may call it explicitly.
619
+ */
620
+ export function registerSolidRenderElement(options = {}) {
621
+ const registry = options.customElements ?? globalThis.customElements;
622
+ const HTMLElementBase = options.HTMLElement ?? globalThis.HTMLElement;
623
+ if (!registry || typeof registry.define !== "function" || typeof registry.get !== "function" || typeof HTMLElementBase !== "function") {
624
+ return undefined;
625
+ }
626
+
627
+ const existing = registry.get("solid-render");
628
+ if (existing) return existing;
629
+ const cached = solidRenderClasses.get(registry);
630
+ if (cached) return cached;
631
+
632
+ class SolidRenderElement extends HTMLElementBase {
633
+ static get observedAttributes() {
634
+ return ["module", "component", RUNTIME_SCOPE_ATTRIBUTE];
635
+ }
636
+
637
+ constructor() {
638
+ super();
639
+ solidRenderInternals.set(this, {
640
+ connected: false,
641
+ generation: 0,
642
+ captured: false,
643
+ childrenTemplate: undefined,
644
+ programmaticProps: {},
645
+ mount: undefined,
646
+ controller: undefined,
647
+ propObserver: undefined,
648
+ });
649
+ }
650
+
651
+ connectedCallback() {
652
+ const state = getSolidRenderState(this);
653
+ state.connected = true;
654
+ captureSolidRenderChildren(this, state);
655
+ startSolidRenderPropObserver(this, state);
656
+ void remountSolidRender(this, "connect");
657
+ }
658
+
659
+ disconnectedCallback() {
660
+ const state = getSolidRenderState(this);
661
+ state.connected = false;
662
+ state.generation += 1;
663
+ state.propObserver?.disconnect?.();
664
+ state.propObserver = undefined;
665
+ void disposeSolidRenderMount(this, state, "disconnect");
666
+ }
667
+
668
+ attributeChangedCallback(name, oldValue, newValue) {
669
+ if (oldValue === newValue) return;
670
+ const state = getSolidRenderState(this);
671
+ if (!state.connected) return;
672
+ void remountSolidRender(this, `identity:${name}`);
673
+ }
674
+
675
+ get props() {
676
+ return getSolidRenderState(this).programmaticProps;
677
+ }
678
+
679
+ set props(value) {
680
+ const state = getSolidRenderState(this);
681
+ state.programmaticProps = value && typeof value === "object" ? value : {};
682
+ updateSolidRenderProps(this, state);
683
+ }
684
+ }
685
+
686
+ registry.define("solid-render", SolidRenderElement);
687
+ solidRenderClasses.set(registry, SolidRenderElement);
688
+ return SolidRenderElement;
689
+ }
690
+
691
+ function getSolidRenderState(element) {
692
+ let state = solidRenderInternals.get(element);
693
+ if (!state) {
694
+ state = {
695
+ connected: false,
696
+ generation: 0,
697
+ captured: false,
698
+ childrenTemplate: undefined,
699
+ programmaticProps: {},
700
+ mount: undefined,
701
+ controller: undefined,
702
+ propObserver: undefined,
703
+ };
704
+ solidRenderInternals.set(element, state);
705
+ }
706
+ return state;
707
+ }
708
+
709
+ function captureSolidRenderChildren(element, state) {
710
+ if (state.captured) return;
711
+ state.captured = true;
712
+ const document = element.ownerDocument ?? globalThis.document;
713
+ if (!document || typeof document.createDocumentFragment !== "function") return;
714
+ const template = document.createDocumentFragment();
715
+ for (const node of Array.from(element.childNodes ?? [])) {
716
+ template.appendChild?.(node.cloneNode ? node.cloneNode(true) : node);
717
+ }
718
+ state.childrenTemplate = template;
719
+ element.replaceChildren?.();
720
+ }
721
+
722
+ function startSolidRenderPropObserver(element, state) {
723
+ if (state.propObserver) return;
724
+ let controller;
725
+ try {
726
+ controller = resolveHTMLControllerForElement(element);
727
+ } catch {
728
+ controller = undefined;
729
+ }
730
+ const MutationObserverImpl = controller?.MutationObserver ?? globalThis.MutationObserver;
731
+ if (typeof MutationObserverImpl !== "function") return;
732
+
733
+ const observer = new MutationObserverImpl(records => {
734
+ if (!Array.from(records ?? []).some(record => String(record?.attributeName ?? "").startsWith("prop:"))) return;
735
+ updateSolidRenderProps(element, state);
736
+ });
737
+ observer.observe(element, { attributes: true });
738
+ state.propObserver = observer;
739
+ }
740
+
741
+ async function remountSolidRender(element, reason) {
742
+ const state = getSolidRenderState(element);
743
+ if (!state.connected) return;
744
+ const generation = ++state.generation;
745
+ await disposeSolidRenderMount(element, state, reason === "connect" ? "reconnect" : reason);
746
+ if (!state.connected || generation !== state.generation) return;
747
+
748
+ let controller;
749
+ try {
750
+ controller = resolveHTMLControllerForElement(element);
751
+ state.controller = controller;
752
+ controller.renderElements.add(element);
753
+
754
+ const moduleId = nonEmpty(element.getAttribute?.("module"));
755
+ if (!moduleId) {
756
+ throw new HTMLRenderError("<solid-render> requires a `module` attribute.", { element });
757
+ }
758
+ const componentName = nonEmpty(element.getAttribute?.("component"));
759
+ const exportName = componentName ?? "default";
760
+ const props = buildSolidRenderProps(element, state);
761
+
762
+ emitHTML(controller, {
763
+ type: "render-mounting",
764
+ element,
765
+ moduleId,
766
+ component: exportName,
767
+ mode: "container",
768
+ origin: "solid-render",
769
+ source: "solid-render",
770
+ });
771
+
772
+ const namespace = await controller.runtime.import(moduleId);
773
+ if (!state.connected || generation !== state.generation) return;
774
+ const { Component } = resolveRenderComponent(namespace, componentName, {
775
+ moduleId,
776
+ element,
777
+ });
778
+
779
+ element.replaceChildren?.();
780
+ const mount = await controller.renderAdapter.mountContainer({
781
+ runtime: controller.runtime,
782
+ target: element,
783
+ Component,
784
+ props,
785
+ element,
786
+ moduleId,
787
+ });
788
+ if (!state.connected || generation !== state.generation) {
789
+ await mount.dispose?.();
790
+ return;
791
+ }
792
+
793
+ mount.mode = "container";
794
+ state.mount = mount;
795
+ emitHTML(controller, {
796
+ type: "render-mounted",
797
+ element,
798
+ moduleId,
799
+ component: exportName,
800
+ mode: "container",
801
+ target: element,
802
+ origin: "solid-render",
803
+ source: "solid-render",
804
+ });
805
+ } catch (error) {
806
+ // Identity changes or disconnects invalidate in-flight work. A stale
807
+ // failure belongs to the abandoned generation just like a stale success,
808
+ // so it must not overwrite/report against the current element state.
809
+ if (!state.connected || generation !== state.generation) return;
810
+ if (controller) {
811
+ emitHTMLError(controller, "solid-render", error, {
812
+ element,
813
+ moduleId: nonEmpty(element.getAttribute?.("module")),
814
+ origin: "solid-render",
815
+ });
816
+ }
817
+ dispatchSolidRenderError(element, error);
818
+ }
819
+ }
820
+
821
+ async function disposeSolidRenderMount(element, state, reason = "dispose") {
822
+ const mount = state.mount;
823
+ const controller = state.controller;
824
+ if (!mount) {
825
+ controller?.renderElements?.delete(element);
826
+ state.controller = undefined;
827
+ return;
828
+ }
829
+
830
+ const moduleId = nonEmpty(element.getAttribute?.("module"));
831
+ const component = nonEmpty(element.getAttribute?.("component")) ?? "default";
832
+ if (controller) {
833
+ emitHTML(controller, {
834
+ type: "render-disposing",
835
+ element,
836
+ moduleId,
837
+ component,
838
+ mode: "container",
839
+ reason,
840
+ source: "solid-render",
841
+ origin: "solid-render",
842
+ });
843
+ }
844
+
845
+ try {
846
+ await mount.dispose?.();
847
+ } finally {
848
+ state.mount = undefined;
849
+ element.replaceChildren?.();
850
+ controller?.renderElements?.delete(element);
851
+ state.controller = undefined;
852
+ }
853
+
854
+ if (controller) {
855
+ emitHTML(controller, {
856
+ type: "render-disposed",
857
+ element,
858
+ moduleId,
859
+ component,
860
+ mode: "container",
861
+ reason,
862
+ source: "solid-render",
863
+ origin: "solid-render",
864
+ });
865
+ }
866
+ }
867
+
868
+ function updateSolidRenderProps(element, state) {
869
+ if (!state.mount?.updateProps) return;
870
+ state.mount.updateProps(buildSolidRenderProps(element, state));
871
+ }
872
+
873
+ function buildSolidRenderProps(element, state) {
874
+ const declarative = {};
875
+ for (const attribute of Array.from(element.attributes ?? [])) {
876
+ const name = String(attribute?.name ?? "").toLowerCase();
877
+ if (!name.startsWith("prop:")) continue;
878
+ const key = kebabToCamel(name.slice(5));
879
+ if (!key) continue;
880
+ declarative[key] = attribute.value === "" ? true : attribute.value;
881
+ }
882
+
883
+ if (state.childrenTemplate && !("children" in (state.programmaticProps ?? {}))) {
884
+ Object.defineProperty(declarative, "children", {
885
+ configurable: true,
886
+ enumerable: true,
887
+ get() {
888
+ return state.childrenTemplate.cloneNode?.(true) ?? state.childrenTemplate;
889
+ },
890
+ });
891
+ }
892
+
893
+ const merged = Object.create(null);
894
+ for (const key of Reflect.ownKeys(declarative)) {
895
+ const descriptor = Object.getOwnPropertyDescriptor(declarative, key);
896
+ if (descriptor) Object.defineProperty(merged, key, descriptor);
897
+ }
898
+ for (const key of Reflect.ownKeys(state.programmaticProps ?? {})) {
899
+ merged[key] = state.programmaticProps[key];
900
+ }
901
+ return merged;
902
+ }
903
+
904
+ function kebabToCamel(value) {
905
+ return String(value).replace(/-([a-z0-9])/g, (_match, character) => character.toUpperCase());
906
+ }
907
+
908
+ function resolveHTMLControllerForElement(element) {
909
+ const scope = nonEmpty(element.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
910
+ const live = [...activeControllers].filter(controller => !controller.disposed);
911
+
912
+ // <solid-render> must use the same routing semantics as discovered script
913
+ // declarations. In particular, an unscoped element may be handled by a
914
+ // scoped controller when that controller accepts unscoped declarations.
915
+ // Root containment is also authoritative: never fall back to a controller
916
+ // outside the element's DOM boundary merely because it is the only candidate.
917
+ const candidates = live.filter(controller => {
918
+ if (scope) return controller.scope === scope;
919
+ return controller.scope ? controller.acceptUnscoped !== false : true;
920
+ });
921
+ const contained = candidates.filter(controller =>
922
+ rootContainsElement(controller.root ?? globalThis.document, element)
923
+ );
924
+
925
+ if (contained.length === 1) return contained[0];
926
+ if (contained.length > 1) {
927
+ const specific = mostSpecificController(contained);
928
+ if (specific) return specific;
929
+ }
930
+
931
+ if (contained.length === 0) {
932
+ if (scope) {
933
+ if (candidates.length === 0) {
934
+ throw new HTMLRenderError(
935
+ `No HTML runtime is registered for data-solid-runtime=${JSON.stringify(scope)}.`,
936
+ { element },
937
+ );
938
+ }
939
+ throw new HTMLRenderError(
940
+ `HTML runtime scope ${JSON.stringify(scope)} is registered, but no matching controller root contains this <solid-render> element.`,
941
+ { element },
942
+ );
943
+ }
944
+
945
+ if (candidates.length === 0) {
946
+ throw new HTMLRenderError(
947
+ "No HTML runtime accepts this unscoped <solid-render>. Add data-solid-runtime or enable acceptUnscoped on the intended controller.",
948
+ { element },
949
+ );
950
+ }
951
+
952
+ throw new HTMLRenderError(
953
+ "No HTML runtime that accepts unscoped declarations has a root containing this <solid-render> element.",
954
+ { element },
955
+ );
956
+ }
957
+
958
+ throw new HTMLRenderError(
959
+ `Multiple HTML runtimes match <solid-render>${scope ? ` for scope ${JSON.stringify(scope)}` : ""}. Use data-solid-runtime, distinct scopes, or non-overlapping DOM roots.`,
960
+ { element },
961
+ );
962
+ }
963
+
964
+ function rootContainsElement(root, element) {
965
+ if (!root || !element) return false;
966
+ if (root === element) return true;
967
+ if (typeof root.contains === "function") return root.contains(element);
968
+ let node = element.parentNode;
969
+ while (node) {
970
+ if (node === root) return true;
971
+ node = node.parentNode;
972
+ }
973
+ return false;
974
+ }
975
+
976
+ function mostSpecificController(controllers) {
977
+ for (const candidate of controllers) {
978
+ const root = candidate.root ?? globalThis.document;
979
+ if (controllers.every(other => {
980
+ if (other === candidate) return true;
981
+ const otherRoot = other.root ?? globalThis.document;
982
+ return !rootContainsElement(root, otherRoot);
983
+ })) return candidate;
984
+ }
985
+ return undefined;
986
+ }
987
+
988
+ function dispatchSolidRenderError(element, error) {
989
+ try {
990
+ const EventImpl = globalThis.CustomEvent;
991
+ if (typeof element?.dispatchEvent === "function" && typeof EventImpl === "function") {
992
+ element.dispatchEvent(new EventImpl("solid-render-error", {
993
+ detail: { error },
994
+ bubbles: false,
995
+ composed: false,
996
+ }));
997
+ }
998
+ } catch {
999
+ // Error reporting must not create a second failure.
1000
+ }
1001
+ globalThis.console?.error?.("solid-tag-runtime <solid-render> error", error);
1002
+ }
1003
+
1004
+ async function disposeController(controller, options = {}) {
1005
+ if (controller.disposed) return;
1006
+ controller.disposed = true;
1007
+ stopObserver(controller, { reason: options.runtimeDisposed ? "runtime-disposed" : "controller-disposed" });
1008
+
1009
+ for (const record of controller.bindings.values()) {
1010
+ if (record.mount) {
1011
+ await disposeRecordMount(controller, record, {
1012
+ reason: options.runtimeDisposed ? "runtime-disposed" : "controller-disposed",
1013
+ removeAnchors: record.render === true,
1014
+ restoreElement: record.render === true,
1015
+ });
1016
+ }
1017
+ }
1018
+
1019
+ for (const element of [...controller.renderElements]) {
1020
+ const state = solidRenderInternals.get(element);
1021
+ if (!state || state.controller !== controller) continue;
1022
+ state.generation += 1;
1023
+ await disposeSolidRenderMount(element, state, options.runtimeDisposed ? "runtime-disposed" : "controller-disposed");
1024
+ }
1025
+
1026
+ activeControllers.delete(controller);
1027
+ controller.runtimeDisposeUnsubscribe?.();
1028
+ controller.runtimeDisposeUnsubscribe = undefined;
1029
+ controller.events.clear();
1030
+ }
1031
+
574
1032
  async function connectObserver(controller, root, callOptions = {}, mode = {}) {
575
1033
  const MutationObserverImpl = callOptions.MutationObserver ?? controller.MutationObserver ?? globalThis.MutationObserver;
576
1034
 
@@ -672,6 +1130,7 @@ async function enqueueObserverScan(controller, callOptions = {}) {
672
1130
  });
673
1131
  if (!accepted) {
674
1132
  ignored += 1;
1133
+ maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
675
1134
  continue;
676
1135
  }
677
1136
  matched += 1;
@@ -696,12 +1155,15 @@ async function enqueueObserverScan(controller, callOptions = {}) {
696
1155
  root,
697
1156
  origin,
698
1157
  executeEntries: callOptions.executeEntries ?? controller.executeEntries,
1158
+ executeRenders: callOptions.executeRenders ?? controller.executeRenders,
699
1159
  });
700
1160
  } catch (error) {
701
1161
  emitHTMLError(controller, "observer", error, { origin });
702
1162
  throw error;
703
1163
  }
704
1164
 
1165
+ await cleanupDetachedRenderedDeclarations(controller, root);
1166
+
705
1167
  emitHTML(controller, {
706
1168
  type: "observer-batch",
707
1169
  root,
@@ -804,52 +1266,324 @@ async function processClaims(controller, records, options = {}) {
804
1266
  }
805
1267
 
806
1268
  const entries = modules.filter(module => module.entry);
1269
+ const renders = modules.filter(module => module.render !== undefined);
807
1270
  const executed = [];
1271
+ const rendered = [];
808
1272
 
809
- if (options.executeEntries !== false) {
810
- for (const entry of entries) {
811
- const record = elementOwners.get(entry.element);
812
- if (record) record.state = "executing";
1273
+ // Every module in the batch is defined before any side-effect entry or render
1274
+ // action executes. This keeps document ordering independent from imports.
1275
+ for (const module of modules) {
1276
+ const record = elementOwners.get(module.element);
1277
+ if (!record) continue;
813
1278
 
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
- });
821
- const namespace = await controller.runtime.import(entry.id);
822
- if (record) record.state = "ready";
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
- });
831
- } catch (error) {
832
- if (record) {
833
- record.state = "error";
834
- record.error = error;
835
- }
836
- emitHTMLError(controller, "entry", error, {
837
- element: entry.element,
838
- moduleId: entry.id,
839
- origin: record?.origin ?? options.origin ?? "entry",
840
- });
841
- throw error;
842
- }
1279
+ if (record.entry && options.executeEntries !== false) {
1280
+ executed.push(await executeEntryRecord(controller, record, options));
1281
+ continue;
1282
+ }
1283
+
1284
+ if (record.render !== undefined && options.executeRenders !== false) {
1285
+ rendered.push(await renderDeclarationRecord(controller, record, options));
843
1286
  }
844
1287
  }
845
1288
 
1289
+ registerSolidRenderElement();
846
1290
  return {
847
1291
  modules,
848
1292
  entries: entries.map(entry => entry.id),
849
1293
  executed,
1294
+ renders: renders.map(render => render.id),
1295
+ rendered,
850
1296
  };
851
1297
  }
852
1298
 
1299
+
1300
+ async function executeEntryRecord(controller, record, options = {}) {
1301
+ record.state = "executing";
1302
+ const origin = record.origin ?? options.origin ?? "entry";
1303
+ try {
1304
+ emitHTML(controller, {
1305
+ type: "entry-executing",
1306
+ element: record.element,
1307
+ moduleId: record.moduleId,
1308
+ origin,
1309
+ });
1310
+ const namespace = await controller.runtime.import(record.moduleId);
1311
+ record.state = "ready";
1312
+ emitHTML(controller, {
1313
+ type: "entry-executed",
1314
+ element: record.element,
1315
+ moduleId: record.moduleId,
1316
+ namespace,
1317
+ origin,
1318
+ });
1319
+ return { id: record.moduleId, namespace };
1320
+ } catch (error) {
1321
+ record.state = "error";
1322
+ record.error = error;
1323
+ emitHTMLError(controller, "entry", error, {
1324
+ element: record.element,
1325
+ moduleId: record.moduleId,
1326
+ origin,
1327
+ });
1328
+ throw error;
1329
+ }
1330
+ }
1331
+
1332
+ async function renderDeclarationRecord(controller, record, options = {}) {
1333
+ const origin = record.origin ?? options.origin ?? "render";
1334
+ const mode = record.render === true ? "in-place" : "selector";
1335
+ const exportName = record.component ?? "default";
1336
+
1337
+ emitHTML(controller, {
1338
+ type: "render-mounting",
1339
+ element: record.element,
1340
+ moduleId: record.moduleId,
1341
+ component: exportName,
1342
+ mode,
1343
+ origin,
1344
+ source: "script-render",
1345
+ });
1346
+
1347
+ try {
1348
+ if (record.mount) {
1349
+ await disposeRecordMount(controller, record, {
1350
+ reason: "remount",
1351
+ removeAnchors: false,
1352
+ });
1353
+ }
1354
+
1355
+ const namespace = await controller.runtime.import(record.moduleId);
1356
+ const { Component } = resolveRenderComponent(namespace, record.component, {
1357
+ moduleId: record.moduleId,
1358
+ element: record.element,
1359
+ });
1360
+
1361
+ let mount;
1362
+ if (mode === "selector") {
1363
+ const target = resolveRenderTarget(controller, record.element, record.render, options);
1364
+ mount = await controller.renderAdapter.mountContainer({
1365
+ runtime: controller.runtime,
1366
+ target,
1367
+ Component,
1368
+ props: {},
1369
+ element: record.element,
1370
+ moduleId: record.moduleId,
1371
+ });
1372
+ mount.mode = "selector";
1373
+ mount.renderTarget = record.render;
1374
+ mount.target = target;
1375
+ } else {
1376
+ const range = ensureInPlaceRange(record);
1377
+ mount = await controller.renderAdapter.mountRange({
1378
+ runtime: controller.runtime,
1379
+ parent: range.parent,
1380
+ before: range.endMarker,
1381
+ startMarker: range.startMarker,
1382
+ endMarker: range.endMarker,
1383
+ Component,
1384
+ props: {},
1385
+ element: record.element,
1386
+ moduleId: record.moduleId,
1387
+ });
1388
+ mount.mode = "in-place";
1389
+ mount.range = range;
1390
+ }
1391
+
1392
+ record.mount = mount;
1393
+ record.state = "ready";
1394
+ const result = {
1395
+ id: record.moduleId,
1396
+ namespace,
1397
+ component: exportName,
1398
+ mode,
1399
+ target: mount.target,
1400
+ element: record.element,
1401
+ };
1402
+ emitHTML(controller, {
1403
+ type: "render-mounted",
1404
+ element: record.element,
1405
+ moduleId: record.moduleId,
1406
+ component: exportName,
1407
+ mode,
1408
+ target: mount.target,
1409
+ origin,
1410
+ source: "script-render",
1411
+ });
1412
+ return result;
1413
+ } catch (error) {
1414
+ record.state = "error";
1415
+ record.error = error;
1416
+ emitHTMLError(controller, "render", error, {
1417
+ element: record.element,
1418
+ moduleId: record.moduleId,
1419
+ origin,
1420
+ });
1421
+ throw error;
1422
+ }
1423
+ }
1424
+
1425
+ async function disposeRecordMount(controller, record, options = {}) {
1426
+ const mount = record?.mount;
1427
+ if (!mount) return false;
1428
+ const mode = mount.mode ?? (record.render === true ? "in-place" : "selector");
1429
+ emitHTML(controller, {
1430
+ type: "render-disposing",
1431
+ element: record.element,
1432
+ moduleId: record.moduleId,
1433
+ component: record.component ?? "default",
1434
+ mode,
1435
+ reason: options.reason ?? "dispose",
1436
+ source: "script-render",
1437
+ origin: record.origin ?? "render",
1438
+ });
1439
+
1440
+ try {
1441
+ await mount.dispose?.();
1442
+ } finally {
1443
+ if (mode === "in-place" && options.removeAnchors !== false) {
1444
+ removeRangeAnchors(record, { restoreElement: options.restoreElement === true });
1445
+ }
1446
+ record.mount = undefined;
1447
+ }
1448
+
1449
+ emitHTML(controller, {
1450
+ type: "render-disposed",
1451
+ element: record.element,
1452
+ moduleId: record.moduleId,
1453
+ component: record.component ?? "default",
1454
+ mode,
1455
+ reason: options.reason ?? "dispose",
1456
+ source: "script-render",
1457
+ origin: record.origin ?? "render",
1458
+ });
1459
+ return true;
1460
+ }
1461
+
1462
+ function resolveRenderTarget(controller, element, selector, options = {}) {
1463
+ if (typeof selector !== "string" || !selector) {
1464
+ throw new HTMLRenderError("Selector rendering requires a non-empty render selector.", {
1465
+ moduleId: elementOwners.get(element)?.moduleId,
1466
+ element,
1467
+ });
1468
+ }
1469
+
1470
+ const root = options.renderRoot
1471
+ ?? options.root
1472
+ ?? controller.root
1473
+ ?? element.ownerDocument
1474
+ ?? globalThis.document;
1475
+
1476
+ if (!root || typeof root.querySelectorAll !== "function") {
1477
+ throw new HTMLRenderError(`Cannot resolve render target ${JSON.stringify(selector)} without a queryable DOM root.`, {
1478
+ moduleId: elementOwners.get(element)?.moduleId,
1479
+ element,
1480
+ });
1481
+ }
1482
+
1483
+ let target;
1484
+ try {
1485
+ if (typeof root.querySelector === "function") target = root.querySelector(selector);
1486
+ else target = Array.from(root.querySelectorAll(selector))[0];
1487
+ } catch (cause) {
1488
+ throw new HTMLRenderError(`Invalid render selector ${JSON.stringify(selector)}.`, {
1489
+ cause,
1490
+ moduleId: elementOwners.get(element)?.moduleId,
1491
+ element,
1492
+ });
1493
+ }
1494
+
1495
+ if (!target) {
1496
+ throw new HTMLRenderError(`Render target ${JSON.stringify(selector)} was not found.`, {
1497
+ moduleId: elementOwners.get(element)?.moduleId,
1498
+ element,
1499
+ });
1500
+ }
1501
+ return target;
1502
+ }
1503
+
1504
+ function ensureInPlaceRange(record) {
1505
+ if (record.range?.startMarker?.parentNode && record.range?.endMarker?.parentNode) {
1506
+ return record.range;
1507
+ }
1508
+
1509
+ const element = record.element;
1510
+ const parent = element?.parentNode;
1511
+ if (!parent) {
1512
+ throw new HTMLRenderError("In-place rendering requires the declaration script to have a parent node.", {
1513
+ moduleId: record.moduleId,
1514
+ element,
1515
+ });
1516
+ }
1517
+
1518
+ const document = element.ownerDocument ?? globalThis.document;
1519
+ if (!document || typeof document.createComment !== "function") {
1520
+ throw new HTMLRenderError("In-place rendering requires document.createComment().", {
1521
+ moduleId: record.moduleId,
1522
+ element,
1523
+ });
1524
+ }
1525
+
1526
+ const startMarker = document.createComment("solid-runtime:start");
1527
+ const endMarker = document.createComment("solid-runtime:end");
1528
+
1529
+ if (typeof element.replaceWith === "function") {
1530
+ element.replaceWith(startMarker, endMarker);
1531
+ } else if (typeof parent.insertBefore === "function" && typeof parent.removeChild === "function") {
1532
+ parent.insertBefore(startMarker, element);
1533
+ parent.insertBefore(endMarker, element);
1534
+ parent.removeChild(element);
1535
+ } else {
1536
+ throw new HTMLRenderError("In-place rendering requires replaceWith() or insertBefore()/removeChild() support.", {
1537
+ moduleId: record.moduleId,
1538
+ element,
1539
+ });
1540
+ }
1541
+
1542
+ record.range = { parent, startMarker, endMarker };
1543
+ return record.range;
1544
+ }
1545
+
1546
+ function removeRangeAnchors(record, options = {}) {
1547
+ const range = record.range;
1548
+ if (!range) return;
1549
+ const { parent, startMarker, endMarker } = range;
1550
+
1551
+ if (options.restoreElement && parent && startMarker?.parentNode === parent) {
1552
+ if (typeof parent.insertBefore === "function") parent.insertBefore(record.element, startMarker);
1553
+ }
1554
+
1555
+ clearNodesBetween(startMarker, endMarker);
1556
+ startMarker?.parentNode?.removeChild?.(startMarker);
1557
+ endMarker?.parentNode?.removeChild?.(endMarker);
1558
+ record.range = undefined;
1559
+ }
1560
+
1561
+ function clearNodesBetween(start, end) {
1562
+ if (!start || !end) return;
1563
+ let node = start.nextSibling;
1564
+ while (node && node !== end) {
1565
+ const next = node.nextSibling;
1566
+ node.parentNode?.removeChild?.(node);
1567
+ node = next;
1568
+ }
1569
+ }
1570
+
1571
+ async function cleanupDetachedRenderedDeclarations(controller, root) {
1572
+ if (!root || typeof root.contains !== "function") return;
1573
+ for (const record of controller.bindings.values()) {
1574
+ if (!record.mount) continue;
1575
+ const anchor = record.mount.mode === "in-place"
1576
+ ? record.range?.startMarker
1577
+ : record.element;
1578
+ if (anchor && !root.contains(anchor)) {
1579
+ await disposeRecordMount(controller, record, {
1580
+ reason: "declaration-detached",
1581
+ removeAnchors: false,
1582
+ });
1583
+ }
1584
+ }
1585
+ }
1586
+
853
1587
  function claimElement(controller, element, options = {}) {
854
1588
  assertElement(element, options.origin ?? "HTML runtime");
855
1589
 
@@ -897,8 +1631,12 @@ function claimElement(controller, element, options = {}) {
897
1631
  moduleId,
898
1632
  format: metadata.format,
899
1633
  entry: metadata.entry,
1634
+ render: metadata.render,
1635
+ component: metadata.component,
900
1636
  sourceUrl: metadata.sourceUrl,
901
1637
  inline: !metadata.sourceUrl,
1638
+ mount: undefined,
1639
+ range: undefined,
902
1640
  state: "claimed",
903
1641
  origin: options.origin ?? "unknown",
904
1642
  promise,
@@ -916,6 +1654,8 @@ function claimElement(controller, element, options = {}) {
916
1654
  moduleId,
917
1655
  format: record.format,
918
1656
  entry: record.entry,
1657
+ render: record.render,
1658
+ component: record.component,
919
1659
  sourceUrl: record.sourceUrl,
920
1660
  origin: record.origin,
921
1661
  });
@@ -960,6 +1700,14 @@ async function prepareRecordSource(controller, record, options) {
960
1700
  function inspectElement(controller, element, options = {}) {
961
1701
  const type = normalizeType(element.getAttribute("type"));
962
1702
  const entry = hasAttribute(element, "entry");
1703
+ const render = inspectRenderAttribute(element);
1704
+ const component = nonEmpty(element.getAttribute("component"));
1705
+ if (entry && render !== undefined) {
1706
+ throw new HTMLModuleError(
1707
+ "Runtime script declarations cannot combine `entry` and `render`. Use separate declarations for side effects and UI mounting.",
1708
+ { element },
1709
+ );
1710
+ }
963
1711
  const srcAttribute = nonEmpty(element.getAttribute("src"));
964
1712
  const explicitId = nonEmpty(element.getAttribute("module"));
965
1713
  const baseUrl = options.baseUrl
@@ -977,15 +1725,16 @@ function inspectElement(controller, element, options = {}) {
977
1725
  sourceUrl,
978
1726
  });
979
1727
 
980
- const id = explicitId ?? sourceUrl ?? (entry ? options.existingId ?? createAnonymousId(format) : undefined);
1728
+ const action = entry ? "entry" : render !== undefined ? "render" : undefined;
1729
+ const id = explicitId ?? sourceUrl ?? (action ? options.existingId ?? createAnonymousId(format, action) : undefined);
981
1730
  if (!id) {
982
1731
  throw new HTMLModuleError(
983
- "Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
1732
+ "Inline solid runtime module scripts require a `module` attribute unless they are marked `entry` or `render`.",
984
1733
  { element },
985
1734
  );
986
1735
  }
987
1736
 
988
- return { id, format, entry, sourceUrl };
1737
+ return { id, format, entry, render, component, sourceUrl };
989
1738
  }
990
1739
 
991
1740
  function requireOwnedRecord(controller, element, caller) {
@@ -1022,6 +1771,8 @@ function definitionFromRecord(record) {
1022
1771
  id: record.moduleId,
1023
1772
  format: record.format,
1024
1773
  entry: record.entry,
1774
+ render: record.render,
1775
+ component: record.component,
1025
1776
  sourceUrl: record.sourceUrl,
1026
1777
  inline: record.inline,
1027
1778
  element: record.element,
@@ -1055,6 +1806,52 @@ function acceptsDiscoveredElement(controller, element, options = {}) {
1055
1806
  return !elementScope;
1056
1807
  }
1057
1808
 
1809
+ function maybeWarnUnresolvedRenderScope(controller, element, options = {}, origin = "discover") {
1810
+ if ((options.executeRenders ?? controller.executeRenders) === false) return false;
1811
+ if (!hasAttribute(element, "render")) return false;
1812
+
1813
+ const requestedScope = nonEmpty(element?.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
1814
+ if (!requestedScope) return false;
1815
+
1816
+ const live = [...activeControllers].filter(candidate => !candidate.disposed);
1817
+ const matchingScopeControllers = live.filter(candidate => candidate.scope === requestedScope);
1818
+ const handlingController = matchingScopeControllers.find(candidate =>
1819
+ rootContainsElement(candidate.root ?? globalThis.document, element)
1820
+ );
1821
+ if (handlingController) return false;
1822
+
1823
+ if (unresolvedRenderScopeWarnings.get(element) === requestedScope) return false;
1824
+ unresolvedRenderScopeWarnings.set(element, requestedScope);
1825
+
1826
+ const registeredScopes = [...new Set(live.map(candidate => candidate.scope).filter(Boolean))].sort();
1827
+ const moduleId = nonEmpty(element?.getAttribute?.("module"));
1828
+ const scopeLabel = JSON.stringify(requestedScope);
1829
+ const moduleLabel = moduleId ? ` module=${JSON.stringify(moduleId)}` : "";
1830
+ const message = matchingScopeControllers.length > 0
1831
+ ? `solid-tag-runtime/html: Unable to render <script${moduleLabel}> for data-solid-runtime=${scopeLabel} because matching HTML runtime controller(s) exist, but none has a root containing this declaration.`
1832
+ : `solid-tag-runtime/html: Unable to render <script${moduleLabel}> because data-solid-runtime=${scopeLabel} does not match any registered HTML runtime.`;
1833
+
1834
+ emitHTML(controller, {
1835
+ type: "html-warning",
1836
+ code: "unresolved-runtime-scope",
1837
+ operation: "render",
1838
+ message,
1839
+ element,
1840
+ moduleId,
1841
+ requestedScope,
1842
+ registeredScopes,
1843
+ matchingScopeRegistered: matchingScopeControllers.length > 0,
1844
+ origin,
1845
+ });
1846
+
1847
+ globalThis.console?.warn?.(message, {
1848
+ element,
1849
+ requestedScope,
1850
+ registeredScopes,
1851
+ });
1852
+ return true;
1853
+ }
1854
+
1058
1855
  function assertExplicitScopeCompatibility(controller, element, options = {}) {
1059
1856
  const requestedScope = normalizeScope(options.scope ?? controller.scope);
1060
1857
  const elementScope = nonEmpty(element.getAttribute(RUNTIME_SCOPE_ATTRIBUTE));
@@ -1146,6 +1943,7 @@ function getDefaultController(runtime, options = {}) {
1146
1943
  root: options.root,
1147
1944
  selector: options.selector,
1148
1945
  executeEntries: options.executeEntries,
1946
+ executeRenders: options.executeRenders,
1149
1947
  fetch: options.fetch,
1150
1948
  baseUrl: options.baseUrl,
1151
1949
  MutationObserver: options.MutationObserver,
@@ -1198,6 +1996,7 @@ function mergeOptions(controller, options) {
1198
1996
  root: options.root ?? controller.root,
1199
1997
  selector: options.selector ?? controller.selector,
1200
1998
  executeEntries: options.executeEntries ?? controller.executeEntries,
1999
+ executeRenders: options.executeRenders ?? controller.executeRenders,
1201
2000
  fetch: options.fetch ?? controller.fetch,
1202
2001
  baseUrl: options.baseUrl ?? controller.baseUrl,
1203
2002
  MutationObserver: options.MutationObserver ?? controller.MutationObserver,
@@ -1212,6 +2011,8 @@ function emptyRegistrationResult() {
1212
2011
  modules: [],
1213
2012
  entries: [],
1214
2013
  executed: [],
2014
+ renders: [],
2015
+ rendered: [],
1215
2016
  };
1216
2017
  }
1217
2018
 
@@ -1312,9 +2113,15 @@ function resolveSourceUrl(src, baseUrl) {
1312
2113
  }
1313
2114
  }
1314
2115
 
1315
- function createAnonymousId(format) {
2116
+ function createAnonymousId(format, kind = "entry") {
1316
2117
  const extension = format === "js" ? "js" : "jsx";
1317
- return `/__solid_tag_runtime__/html-entry-${++anonymousSequence}.${extension}`;
2118
+ return `/__solid_tag_runtime__/html-${kind}-${++anonymousSequence}.${extension}`;
2119
+ }
2120
+
2121
+ function inspectRenderAttribute(element) {
2122
+ if (!hasAttribute(element, "render")) return undefined;
2123
+ const value = element.getAttribute("render");
2124
+ return value == null || value === "" ? true : String(value);
1318
2125
  }
1319
2126
 
1320
2127
  function stripQueryAndHash(value) {