solid-tag-runtime 0.0.11 → 0.0.12

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 CHANGED
@@ -1191,7 +1191,7 @@ The shared render adapter lazily resolves the host application's own `solid-js`
1191
1191
  10. Initial light-DOM child nodes are captured once and exposed as reusable `props.children`; named slots and dynamic child recapture are not part of this phase.
1192
1192
  11. Multiple elements may share one evaluated module namespace while owning independent Solid component roots.
1193
1193
  12. Disconnect disposes the mounted root; reconnect mounts a fresh component instance from the retained declaration inputs.
1194
- 13. Custom-element registration is global/idempotent per `CustomElementRegistry`. `createHTMLRuntime()` registers it after installing the controller in the shared registry; registration/observation paths remain idempotent. `registerSolidRenderElement()` remains available for explicit/custom-registry use.
1194
+ 13. Custom-element registration is global/idempotent per `CustomElementRegistry`, but controller creation does **not** eagerly upgrade existing `solid-render` elements. Initial `register()` / `observe()` first install the complete declarative script batch and then ensure the custom element is registered. `registerSolidRenderElement()` remains available for explicit/custom-registry use.
1195
1195
  14. Runtime/controller disposal tears down both script-owned mounts and connected `solid-render` instances so no Solid owners are orphaned.
1196
1196
 
1197
1197
  Rendering lifecycle joins the existing HTML event stream through `render-mounting`, `render-mounted`, `render-disposing`, and `render-disposed`; failures are also reported through `html-error` with `render`/`solid-render` operations. Lifecycle subscriptions remain observational and cannot alter rendering.
@@ -1235,3 +1235,22 @@ Rules now enforced:
1235
1235
  7. `/ui/Button.jsx` and `./ui/Button.jsx` both resolve to the same registered virtual ID `/ui/Button.jsx` when imported at top level from the correct runtime.
1236
1236
 
1237
1237
  This keeps DOM routing and module routing failures separate and produces actionable errors instead of accidental browser network requests.
1238
+
1239
+
1240
+ ### 0.0.12 — declarative registration barrier for `solid-render`
1241
+
1242
+ Decision: an existing `<solid-render>` must never fail simply because its referenced `<script module>` has not yet been installed by the HTML adapter's initial registration pass.
1243
+
1244
+ The 0.0.11 routing fix correctly selected the containing runtime, but `createHTMLRuntime()` still registered the global custom element immediately. Defining a custom element upgrades already-present `<solid-render>` nodes synchronously. Their `connectedCallback()` could therefore call `runtime.import("/ui/Button.jsx")` before `html.register()` had scanned and defined an earlier `<script module="/ui/Button.jsx">`, producing a legitimate but transient `ModuleResolutionError`.
1245
+
1246
+ Rules now enforced:
1247
+
1248
+ 1. `createHTMLRuntime()` installs the controller in the shared registry but does not eagerly define/upgrade `<solid-render>`.
1249
+ 2. Initial `register()` and initial `observe()` install their complete matching script batch first, then ensure the global custom element is registered. This preserves document-level declaration-before-instance semantics even though custom-element upgrade timing is synchronous.
1250
+ 3. If the `solid-render` class was already registered globally by another controller, a connected instance may still run before this controller's declaration pass. A missing virtual module during an active registration/observer pass is treated as a pending reference rather than a terminal render error.
1251
+ 4. After the registration/observer batch settles, pending instances inside that controller's root are retried. If the module is still missing after the batch, normal `solid-render` error semantics apply.
1252
+ 5. A pending instance is also retried when its selected runtime later emits `module-defined` for the resolved module ID. This supports dynamically arriving declarative/programmatic modules without remounting already-successful instances.
1253
+ 6. Retries remain generation-guarded, so stale async work cannot replace a newer module/component/runtime identity.
1254
+ 7. The existing graph invariant remains unchanged: all declarations in a discovered script batch are defined before entry/render execution or recovery mounts are allowed to succeed.
1255
+
1256
+ This separates three orderings that must all be correct: DOM parsing order, custom-element upgrade/connection timing, and runtime module-definition timing.
package/README.md CHANGED
@@ -107,8 +107,8 @@ Browser import maps must map the subpath explicitly as well as the package root:
107
107
  ```json
108
108
  {
109
109
  "imports": {
110
- "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.11",
111
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.11/html"
110
+ "solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.12",
111
+ "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.12/html"
112
112
  }
113
113
  }
114
114
  ```
@@ -275,7 +275,7 @@ Initial light-DOM children become `props.children`:
275
275
 
276
276
  The first implementation deliberately has no named-slot system and no Shadow DOM. Nested `<solid-render>` elements work through normal custom-element connection when captured children are instantiated.
277
277
 
278
- `createHTMLRuntime()` ensures the custom element is registered globally once after the controller has entered the shared runtime registry. `registerHTML()` / `observeHTML()` keep the same idempotent guarantee. Most applications therefore do not need to call a registration helper directly.
278
+ The HTML adapter registers the custom element automatically **after the initial declarative script batch is installed** by `register()` / `registerHTML()` or the initial `observe()` / `observeHTML()` pass. `createHTMLRuntime()` intentionally does not upgrade existing `<solid-render>` elements immediately, because doing so could make them import modules before preceding `<script module>` declarations have been registered. Most applications therefore do not need to call a registration helper directly.
279
279
 
280
280
  For custom registries, tests, or explicit registration, the helper remains available:
281
281
 
@@ -298,6 +298,20 @@ Both of these reference the same registered virtual module when used with the co
298
298
 
299
299
  Top-level relative module references are normalized as virtual paths; they are not browser URL fetches.
300
300
 
301
+ A document may safely declare a module and immediately reference it later in the same HTML before calling `register()`:
302
+
303
+ ```html
304
+ <script type="solid-jsx" module="/ui/Button.jsx">
305
+ export default function Button() {
306
+ return <button>Ready</button>;
307
+ }
308
+ </script>
309
+
310
+ <solid-render module="/ui/Button.jsx"></solid-render>
311
+ ```
312
+
313
+ The adapter installs the declaration batch before existing `solid-render` instances mount. If the custom element class was already registered by another runtime, unresolved instances are retried when the matching module becomes available. A transient startup ordering race therefore does not surface as a `ModuleResolutionError`. If the registration/observer queue settles and the referenced virtual module is still absent, the normal `solid-render` error is reported; the retry behavior does not hide genuine missing-module mistakes.
314
+
301
315
  A scope is written to owned script elements as:
302
316
 
303
317
  ```html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-tag-runtime",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Runtime module system for JSX modules compiled with solid-tag and executed through @solidjs/html",
5
5
  "type": "module",
6
6
  "exports": {
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",