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/ARCHITECTURE.md +92 -1
- package/README.md +187 -5
- package/html.d.ts +120 -65
- package/package.json +1 -1
- package/src/html.js +872 -65
- package/src/render.js +205 -0
- package/src/runtime.js +8 -0
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
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
|
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
|
|
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) {
|