solid-tag-runtime 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/html.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SolidTagRuntimeError } from "./errors.js";
1
+ import { ModuleResolutionError, SolidTagRuntimeError } from "./errors.js";
2
2
  import { createEventDispatcher } from "./events.js";
3
3
  import { HTMLRenderError, createDefaultHTMLRenderAdapter, resolveRenderComponent } from "./render.js";
4
4
 
@@ -83,6 +83,9 @@ export function createHTMLRuntime(runtime, options = {}) {
83
83
  queue: Promise.resolve(),
84
84
  lastError: undefined,
85
85
  connected: false,
86
+ registrationDepth: 0,
87
+ registrationPasses: 0,
88
+ solidRenderRetryScheduled: false,
86
89
  initial: emptyRegistrationResult(),
87
90
  };
88
91
 
@@ -127,10 +130,13 @@ export function createHTMLRuntime(runtime, options = {}) {
127
130
 
128
131
  controller.api = api;
129
132
  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();
133
+ // Do not define/upgrade <solid-render> here. Existing declarative script
134
+ // modules must be installed first; otherwise connectedCallback() can import
135
+ // a module before register()/observe() has defined the preceding <script>.
136
+ // register()/observe() register the element after their initial script batch.
137
+ controller.runtimeModuleDefinedUnsubscribe = runtime.subscribe?.("module-defined", event => {
138
+ scheduleSolidRenderRetryForModule(controller, event.id);
139
+ });
134
140
  controller.runtimeDisposeUnsubscribe = runtime.subscribe?.("runtime-disposed", () => {
135
141
  void disposeController(controller, { runtimeDisposed: true });
136
142
  });
@@ -147,21 +153,23 @@ export function createHTMLRuntime(runtime, options = {}) {
147
153
  const origin = callOptions.origin ?? "initial-scan";
148
154
  const discovered = Array.from(root.querySelectorAll(selector));
149
155
  const elements = [];
150
-
151
- for (const element of discovered) {
152
- const accepted = acceptsDiscoveredElement(controller, element, callOptions);
153
- emitHTML(controller, {
154
- type: "element-discovered",
155
- element,
156
- moduleId: nonEmpty(element?.getAttribute?.("module")),
157
- origin,
158
- accepted,
159
- });
160
- if (accepted) elements.push(element);
161
- else maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
162
- }
156
+ const pendingBeforeUpgrade = collectConnectedSolidRenderElements(controller, root);
157
+ controller.registrationDepth += 1;
163
158
 
164
159
  try {
160
+ for (const element of discovered) {
161
+ const accepted = acceptsDiscoveredElement(controller, element, callOptions);
162
+ emitHTML(controller, {
163
+ type: "element-discovered",
164
+ element,
165
+ moduleId: nonEmpty(element?.getAttribute?.("module")),
166
+ origin,
167
+ accepted,
168
+ });
169
+ if (accepted) elements.push(element);
170
+ else maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
171
+ }
172
+
165
173
  const result = await processElements(controller, elements, {
166
174
  ...callOptions,
167
175
  root,
@@ -169,11 +177,21 @@ export function createHTMLRuntime(runtime, options = {}) {
169
177
  executeEntries: callOptions.executeEntries ?? controller.executeEntries,
170
178
  executeRenders: callOptions.executeRenders ?? controller.executeRenders,
171
179
  });
180
+
181
+ // Existing upgraded instances may have connected before this controller's
182
+ // declaration batch was installed. Retry those after the batch completes.
172
183
  registerSolidRenderElement();
184
+ controller.registrationPasses += 1;
185
+ await retrySolidRenderElements(controller, pendingBeforeUpgrade, {
186
+ reason: "registration-complete",
187
+ forceMissingError: true,
188
+ });
173
189
  return result;
174
190
  } catch (error) {
175
191
  emitHTMLError(controller, "register", error, { origin });
176
192
  throw error;
193
+ } finally {
194
+ controller.registrationDepth = Math.max(0, controller.registrationDepth - 1);
177
195
  }
178
196
  }
179
197
 
@@ -186,6 +204,8 @@ export function createHTMLRuntime(runtime, options = {}) {
186
204
 
187
205
  const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime observe");
188
206
  controller.observerOptions = { ...callOptions };
207
+ const pendingBeforeUpgrade = collectConnectedSolidRenderElements(controller, root);
208
+ controller.registrationDepth += 1;
189
209
 
190
210
  try {
191
211
  controller.initial = await connectObserver(controller, root, callOptions, {
@@ -193,9 +213,16 @@ export function createHTMLRuntime(runtime, options = {}) {
193
213
  initial: true,
194
214
  });
195
215
  registerSolidRenderElement();
216
+ controller.registrationPasses += 1;
217
+ await retrySolidRenderElements(controller, pendingBeforeUpgrade, {
218
+ reason: "observation-ready",
219
+ forceMissingError: true,
220
+ });
196
221
  } catch (error) {
197
222
  disconnect();
198
223
  throw error;
224
+ } finally {
225
+ controller.registrationDepth = Math.max(0, controller.registrationDepth - 1);
199
226
  }
200
227
 
201
228
  return api;
@@ -645,6 +672,8 @@ export function registerSolidRenderElement(options = {}) {
645
672
  mount: undefined,
646
673
  controller: undefined,
647
674
  propObserver: undefined,
675
+ pendingModuleResolution: undefined,
676
+ pendingCheckGeneration: undefined,
648
677
  });
649
678
  }
650
679
 
@@ -700,6 +729,8 @@ function getSolidRenderState(element) {
700
729
  mount: undefined,
701
730
  controller: undefined,
702
731
  propObserver: undefined,
732
+ pendingModuleResolution: undefined,
733
+ pendingCheckGeneration: undefined,
703
734
  };
704
735
  solidRenderInternals.set(element, state);
705
736
  }
@@ -738,10 +769,12 @@ function startSolidRenderPropObserver(element, state) {
738
769
  state.propObserver = observer;
739
770
  }
740
771
 
741
- async function remountSolidRender(element, reason) {
772
+ async function remountSolidRender(element, reason, options = {}) {
742
773
  const state = getSolidRenderState(element);
743
774
  if (!state.connected) return;
744
775
  const generation = ++state.generation;
776
+ state.pendingModuleResolution = undefined;
777
+ state.pendingCheckGeneration = undefined;
745
778
  await disposeSolidRenderMount(element, state, reason === "connect" ? "reconnect" : reason);
746
779
  if (!state.connected || generation !== state.generation) return;
747
780
 
@@ -792,6 +825,8 @@ async function remountSolidRender(element, reason) {
792
825
 
793
826
  mount.mode = "container";
794
827
  state.mount = mount;
828
+ state.pendingModuleResolution = undefined;
829
+ state.pendingCheckGeneration = undefined;
795
830
  emitHTML(controller, {
796
831
  type: "render-mounted",
797
832
  element,
@@ -807,6 +842,26 @@ async function remountSolidRender(element, reason) {
807
842
  // failure belongs to the abandoned generation just like a stale success,
808
843
  // so it must not overwrite/report against the current element state.
809
844
  if (!state.connected || generation !== state.generation) return;
845
+ if (error instanceof ModuleResolutionError && controller) {
846
+ state.pendingModuleResolution = {
847
+ moduleId: nonEmpty(element.getAttribute?.("module")),
848
+ error,
849
+ };
850
+
851
+ // During startup or observer-driven DOM insertion a connected custom
852
+ // element can run before the matching declarative module has been
853
+ // installed. Treat that as a pending reference only until the relevant
854
+ // registration/observer queue has had a chance to settle.
855
+ if (!options.forceMissingError && (
856
+ controller.registrationDepth > 0
857
+ || controller.connected
858
+ || controller.registrationPasses === 0
859
+ )) {
860
+ schedulePendingSolidRenderResolutionCheck(element, state, controller, generation);
861
+ return;
862
+ }
863
+ }
864
+
810
865
  if (controller) {
811
866
  emitHTMLError(controller, "solid-render", error, {
812
867
  element,
@@ -985,6 +1040,116 @@ function mostSpecificController(controllers) {
985
1040
  return undefined;
986
1041
  }
987
1042
 
1043
+ function schedulePendingSolidRenderResolutionCheck(element, state, controller, generation) {
1044
+ if (state.pendingCheckGeneration === generation) return;
1045
+ state.pendingCheckGeneration = generation;
1046
+
1047
+ const schedule = globalThis.setTimeout ?? ((callback) => Promise.resolve().then(callback));
1048
+ schedule(async () => {
1049
+ if (!state.connected || generation !== state.generation) return;
1050
+ if (!state.pendingModuleResolution || state.mount) return;
1051
+
1052
+ // MutationObserver callbacks run before the next task. If this missing
1053
+ // module came from a script + solid-render insertion batch, controller.queue
1054
+ // now points at that registration work. Wait for it before deciding the
1055
+ // reference is genuinely unresolved.
1056
+ if (controller.connected) {
1057
+ try {
1058
+ await controller.queue;
1059
+ } catch {
1060
+ // Observer errors are reported through the normal controller path.
1061
+ }
1062
+ }
1063
+
1064
+ if (!state.connected || generation !== state.generation) return;
1065
+ if (!state.pendingModuleResolution || state.mount) return;
1066
+
1067
+ // A direct register() may still be loading external source. Do not report
1068
+ // a transient missing module while that pass is active.
1069
+ if (controller.registrationDepth > 0) {
1070
+ state.pendingCheckGeneration = undefined;
1071
+ schedulePendingSolidRenderResolutionCheck(element, state, controller, generation);
1072
+ return;
1073
+ }
1074
+
1075
+ state.pendingCheckGeneration = undefined;
1076
+ await remountSolidRender(element, "pending-module-settled", {
1077
+ forceMissingError: true,
1078
+ });
1079
+ }, 0);
1080
+ }
1081
+
1082
+ function collectConnectedSolidRenderElements(controller, root) {
1083
+ const elements = new Set();
1084
+
1085
+ for (const element of controller.renderElements ?? []) {
1086
+ const state = solidRenderInternals.get(element);
1087
+ if (state?.connected && rootContainsElement(root, element)) elements.add(element);
1088
+ }
1089
+
1090
+ if (root && typeof root.querySelectorAll === "function") {
1091
+ for (const element of Array.from(root.querySelectorAll("solid-render") ?? [])) {
1092
+ const state = solidRenderInternals.get(element);
1093
+ if (state?.connected) elements.add(element);
1094
+ }
1095
+ }
1096
+
1097
+ return [...elements];
1098
+ }
1099
+
1100
+ async function retrySolidRenderElements(controller, elements, options = {}) {
1101
+ for (const element of elements ?? []) {
1102
+ const state = solidRenderInternals.get(element);
1103
+ if (!state?.connected) continue;
1104
+ if (state.mount && !state.pendingModuleResolution) continue;
1105
+
1106
+ let selected;
1107
+ try {
1108
+ selected = resolveHTMLControllerForElement(element);
1109
+ } catch {
1110
+ continue;
1111
+ }
1112
+ if (selected !== controller) continue;
1113
+
1114
+ await remountSolidRender(element, options.reason ?? "retry", {
1115
+ forceMissingError: options.forceMissingError === true,
1116
+ });
1117
+ }
1118
+ }
1119
+
1120
+ function scheduleSolidRenderRetryForModule(controller, definedId) {
1121
+ if (controller.disposed || controller.solidRenderRetryScheduled) return;
1122
+ controller.solidRenderRetryScheduled = true;
1123
+
1124
+ const schedule = globalThis.queueMicrotask ?? (callback => Promise.resolve().then(callback));
1125
+ schedule(() => {
1126
+ controller.solidRenderRetryScheduled = false;
1127
+ if (controller.disposed) return;
1128
+
1129
+ const matches = [];
1130
+ for (const element of controller.renderElements) {
1131
+ const state = solidRenderInternals.get(element);
1132
+ if (!state?.connected || !state.pendingModuleResolution) continue;
1133
+ const requested = nonEmpty(element.getAttribute?.("module"));
1134
+ if (!requested) continue;
1135
+ let resolved;
1136
+ try {
1137
+ resolved = controller.runtime.resolve(requested);
1138
+ } catch {
1139
+ continue;
1140
+ }
1141
+ if (resolved === definedId) matches.push(element);
1142
+ }
1143
+
1144
+ if (matches.length > 0) {
1145
+ void retrySolidRenderElements(controller, matches, {
1146
+ reason: "module-defined",
1147
+ forceMissingError: false,
1148
+ });
1149
+ }
1150
+ });
1151
+ }
1152
+
988
1153
  function dispatchSolidRenderError(element, error) {
989
1154
  try {
990
1155
  const EventImpl = globalThis.CustomEvent;
@@ -1024,6 +1189,8 @@ async function disposeController(controller, options = {}) {
1024
1189
  }
1025
1190
 
1026
1191
  activeControllers.delete(controller);
1192
+ controller.runtimeModuleDefinedUnsubscribe?.();
1193
+ controller.runtimeModuleDefinedUnsubscribe = undefined;
1027
1194
  controller.runtimeDisposeUnsubscribe?.();
1028
1195
  controller.runtimeDisposeUnsubscribe = undefined;
1029
1196
  controller.events.clear();
@@ -1163,6 +1330,11 @@ async function enqueueObserverScan(controller, callOptions = {}) {
1163
1330
  }
1164
1331
 
1165
1332
  await cleanupDetachedRenderedDeclarations(controller, root);
1333
+ await retrySolidRenderElements(
1334
+ controller,
1335
+ collectConnectedSolidRenderElements(controller, root),
1336
+ { reason: "observer-batch-complete", forceMissingError: true },
1337
+ );
1166
1338
 
1167
1339
  emitHTML(controller, {
1168
1340
  type: "observer-batch",
package/src/index.js CHANGED
@@ -19,3 +19,9 @@ export {
19
19
  CircularDependencyError,
20
20
  ModuleCompileError,
21
21
  } from "./errors.js";
22
+
23
+ export {
24
+ createMemoryCompileCache,
25
+ createLocalStorageCompileCache,
26
+ createIndexedDBCompileCache,
27
+ } from "./compile-cache.js";