solid-tag-runtime 0.0.9 → 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 +60 -2
- package/README.md +47 -5
- package/html.d.ts +1 -0
- package/package.json +1 -1
- package/src/html.js +275 -29
- package/src/runtime.js +8 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -1122,6 +1122,7 @@ root-changed
|
|
|
1122
1122
|
append-target-changed
|
|
1123
1123
|
entry-executing
|
|
1124
1124
|
entry-executed
|
|
1125
|
+
html-warning
|
|
1125
1126
|
html-error
|
|
1126
1127
|
```
|
|
1127
1128
|
|
|
@@ -1180,7 +1181,7 @@ The shared render adapter lazily resolves the host application's own `solid-js`
|
|
|
1180
1181
|
|
|
1181
1182
|
1. It references an existing runtime module; it never defines source.
|
|
1182
1183
|
2. Missing `component` means `namespace.default`; `component="Name"` selects a named export.
|
|
1183
|
-
3. `data-solid-runtime` selects
|
|
1184
|
+
3. `data-solid-runtime` selects a matching HTML controller whose configured root contains the element. Without the attribute, controller selection follows the same rule as unscoped script discovery: a containing scoped controller may participate when `acceptUnscoped:true`, while an unscoped controller naturally accepts it. Equally specific matches are ambiguous and require explicit scope. A controller outside the element's root is never selected merely because it is the only candidate.
|
|
1184
1185
|
4. The element remains in the DOM and is the light-DOM mount container. Wrapperless rendering belongs to bare `<script render>`.
|
|
1185
1186
|
5. Renderer configuration (`module`, `component`, `data-solid-runtime`) is never forwarded as component props.
|
|
1186
1187
|
6. Declarative props use `prop:*`; kebab names normalize to camelCase. Empty/presence values are boolean `true`, other values remain strings.
|
|
@@ -1190,9 +1191,66 @@ The shared render adapter lazily resolves the host application's own `solid-js`
|
|
|
1190
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.
|
|
1191
1192
|
11. Multiple elements may share one evaluated module namespace while owning independent Solid component roots.
|
|
1192
1193
|
12. Disconnect disposes the mounted root; reconnect mounts a fresh component instance from the retained declaration inputs.
|
|
1193
|
-
13. Custom-element registration is global/idempotent per `CustomElementRegistry
|
|
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.
|
|
1194
1195
|
14. Runtime/controller disposal tears down both script-owned mounts and connected `solid-render` instances so no Solid owners are orphaned.
|
|
1195
1196
|
|
|
1196
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.
|
|
1197
1198
|
|
|
1198
1199
|
The first `solid-render` phase intentionally excludes named slots, Shadow DOM, automatic numeric/JSON coercion, arbitrary unprefixed prop forwarding, loading/fallback templates, and expression evaluation inside attributes.
|
|
1200
|
+
|
|
1201
|
+
### 0.0.10 — unresolved scoped render diagnostics
|
|
1202
|
+
|
|
1203
|
+
Decision: a declarative `<script render>` with an explicit `data-solid-runtime` scope must not disappear silently when no registered HTML controller can perform that immediate render action.
|
|
1204
|
+
|
|
1205
|
+
The diagnostic remains an HTML-adapter concern. Core runtime resolution and module semantics are unchanged.
|
|
1206
|
+
|
|
1207
|
+
Rules:
|
|
1208
|
+
|
|
1209
|
+
1. Plain scoped module declarations remain quiet when seen by a non-matching controller; scope filtering is still normal routing behavior.
|
|
1210
|
+
2. An active render declaration (`render` present and `executeRenders !== false`) is immediate work. If no live controller with the requested scope has a root containing that declaration, discovery emits one warning.
|
|
1211
|
+
3. The warning is deduplicated per element + requested scope across overlapping controller scans. Repeated `register()` / observer passes must not spam the console.
|
|
1212
|
+
4. If a matching scoped controller is already registered and can cover the element's root, other controllers silently ignore the declaration and no warning is emitted.
|
|
1213
|
+
5. `executeRenders:false` intentionally disables render execution and therefore suppresses the missing-scope warning.
|
|
1214
|
+
6. The diagnostic is observable as `html-warning` with `code: "unresolved-runtime-scope"`, and is also surfaced through `console.warn` for developers who have not installed lifecycle tooling.
|
|
1215
|
+
7. `<solid-render>` remains stricter: a connected custom element that cannot resolve its runtime reports a render error because it is itself the active renderer.
|
|
1216
|
+
|
|
1217
|
+
The warning belongs at the shared controller-registry/discovery boundary rather than being treated as an ownership or module-resolution error. This preserves the distinction between ordinary cross-scope filtering and an unfulfilled immediate render request.
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
### 0.0.11 — `solid-render` routing parity and virtual top-level import safety
|
|
1222
|
+
|
|
1223
|
+
Decision: `<solid-render>` controller selection must use the same scope/`acceptUnscoped`/root-boundary semantics as declarative script discovery.
|
|
1224
|
+
|
|
1225
|
+
The 0.0.10 custom-element resolver treated an unscoped `<solid-render>` differently from an unscoped `<script>` declaration: it preferred controllers whose own scope was empty and could fall back to a sole candidate even when that controller's root did not contain the element. In pages/playgrounds with multiple active controllers this could select the wrong runtime. A registered virtual module such as `/ui/Button.jsx` would then appear missing and `runtime.import()` could fall through to native ESM loading, producing misleading network URLs such as an esm.sh-relative path.
|
|
1226
|
+
|
|
1227
|
+
Rules now enforced:
|
|
1228
|
+
|
|
1229
|
+
1. Explicit `data-solid-runtime="name"` considers only live controllers with that scope and whose root contains the element.
|
|
1230
|
+
2. An unscoped `<solid-render>` considers every containing controller that would accept an unscoped declaration: unscoped controllers plus scoped controllers with `acceptUnscoped:true`.
|
|
1231
|
+
3. When multiple containing controllers match, the most-specific root wins when one root is strictly nested inside the others; otherwise the selection is ambiguous and reports a render error.
|
|
1232
|
+
4. A controller outside the element's DOM root is never used as a fallback.
|
|
1233
|
+
5. Top-level `runtime.import()` now mirrors static-link resolution for virtual paths: unresolved relative (`./x`) and absolute virtual (`/x`) specifiers throw `ModuleResolutionError` even when `allowNativeImports:true`.
|
|
1234
|
+
6. Native fallback remains available for unresolved bare specifiers when `allowNativeImports:true`, and real absolute URLs remain native-loadable.
|
|
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
|
+
|
|
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.
|
|
111
|
-
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.
|
|
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
|
-
|
|
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
|
|
|
@@ -287,6 +287,31 @@ registerSolidRenderElement();
|
|
|
287
287
|
|
|
288
288
|
Multiple `<solid-render>` elements may share one cached runtime module namespace while each mounted component owns independent Solid state. Async module changes are generation-guarded so a stale import can never replace a newer `module`/`component`/runtime selection.
|
|
289
289
|
|
|
290
|
+
`<solid-render>` uses the same runtime-routing rules as declarative scripts. An explicit `data-solid-runtime` selects a controller with that scope **whose root contains the element**. Without an explicit scope, any containing controller that accepts unscoped declarations (`acceptUnscoped: true`) may handle the element. If more than one equally specific controller can handle it, add `data-solid-runtime` to disambiguate. Controllers outside the element's DOM root are never selected as a fallback.
|
|
291
|
+
|
|
292
|
+
Both of these reference the same registered virtual module when used with the correct runtime:
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<solid-render module="/ui/Button.jsx"></solid-render>
|
|
296
|
+
<solid-render module="./ui/Button.jsx"></solid-render>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Top-level relative module references are normalized as virtual paths; they are not browser URL fetches.
|
|
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
|
+
|
|
290
315
|
A scope is written to owned script elements as:
|
|
291
316
|
|
|
292
317
|
```html
|
|
@@ -295,6 +320,18 @@ A scope is written to owned script elements as:
|
|
|
295
320
|
|
|
296
321
|
When multiple runtimes observe the same document, give each one a unique scope and normally set `acceptUnscoped: false`.
|
|
297
322
|
|
|
323
|
+
A scoped declaration that only defines a module may be ignored by non-matching controllers without noise. A scoped declaration with active `render` semantics is different: rendering requests immediate UI work. If no registered HTML runtime with that scope can handle the declaration's DOM root, the adapter emits one deduplicated console warning and an `html-warning` lifecycle event with `code: "unresolved-runtime-scope"`. `executeRenders: false` suppresses this diagnostic because rendering was explicitly disabled.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
html.subscribe("html-warning", event => {
|
|
327
|
+
if (event.code === "unresolved-runtime-scope") {
|
|
328
|
+
console.warn(event.requestedScope, event.moduleId);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`<solid-render>` keeps stronger semantics: when it actively connects and cannot resolve its selected runtime, that is a render error rather than only a warning.
|
|
334
|
+
|
|
298
335
|
### Declarative modules
|
|
299
336
|
|
|
300
337
|
```html
|
|
@@ -843,11 +880,14 @@ render-mounted
|
|
|
843
880
|
render-disposing
|
|
844
881
|
render-disposed
|
|
845
882
|
|
|
883
|
+
html-warning
|
|
846
884
|
html-error
|
|
847
885
|
```
|
|
848
886
|
|
|
849
887
|
Ownership-related events include an `origin` describing how the element entered the controller (`observer`, `append`, `register-element`, `add-module`, and related internal scan origins). This is useful for tracing observer/manual-registration races and proving that an explicit `append()` was not processed a second time by the observer.
|
|
850
888
|
|
|
889
|
+
`html-warning` currently reports non-fatal adapter diagnostics. `unresolved-runtime-scope` is emitted once per render declaration/scope when `data-solid-runtime` requests immediate rendering but no registered controller can handle that scoped declaration. The event includes `requestedScope`, `registeredScopes`, `moduleId` when available, and whether a matching scope exists outside the declaration's root.
|
|
890
|
+
|
|
851
891
|
`observer-batch` summarizes a DOM discovery pass instead of exposing noisy raw `MutationRecord` objects. `element-loading` / `element-loaded` are emitted for `src`-backed modules, and entry events distinguish evaluation caused by an HTML `entry` declaration from an ordinary `runtime.import()`. Render events cover both `<script render>` and `<solid-render>`; `source` identifies `script-render` versus `solid-render`, and `mode` identifies selector, in-place, or container mounting.
|
|
852
892
|
|
|
853
893
|
Like core runtime subscribers, HTML subscribers are observational and exception-isolated.
|
|
@@ -916,7 +956,7 @@ Default runtime resolution supports:
|
|
|
916
956
|
|
|
917
957
|
`allowNativeImports` defaults to `true`.
|
|
918
958
|
|
|
919
|
-
This means an unresolved bare import can be left for the browser's native module resolver/import map:
|
|
959
|
+
This means an unresolved **bare** import can be left for the browser's native module resolver/import map:
|
|
920
960
|
|
|
921
961
|
```ts
|
|
922
962
|
const runtime = createRuntime({
|
|
@@ -924,7 +964,9 @@ const runtime = createRuntime({
|
|
|
924
964
|
});
|
|
925
965
|
```
|
|
926
966
|
|
|
927
|
-
|
|
967
|
+
Relative and absolute virtual paths are different. If `/ui/Button.jsx` or `./ui/Button.jsx` does not resolve to a registered runtime module, `runtime.import()` throws `ModuleResolutionError`; it does not turn the path into a browser/native network import. Use `defineUrl()` or an actual absolute URL when native URL loading is intended.
|
|
968
|
+
|
|
969
|
+
Set `allowNativeImports` to `false` when even unresolved bare imports must be explicitly registered with the runtime.
|
|
928
970
|
|
|
929
971
|
## Runtime lifecycle
|
|
930
972
|
|
package/html.d.ts
CHANGED
|
@@ -216,6 +216,7 @@ export type HTMLRuntimeEvent =
|
|
|
216
216
|
| { htmlRuntimeId: string; scope?: string; timestamp: number; type: "render-mounted"; element: HTMLModuleScriptElement | SolidRenderElement; moduleId?: string; component: string; mode: HTMLRenderMode; target?: unknown; origin: HTMLRegistrationOrigin; source: HTMLRenderSource }
|
|
217
217
|
| { htmlRuntimeId: string; scope?: string; timestamp: number; type: "render-disposing"; element: HTMLModuleScriptElement | SolidRenderElement; moduleId?: string; component: string; mode: HTMLRenderMode; reason: string; origin: HTMLRegistrationOrigin; source: HTMLRenderSource }
|
|
218
218
|
| { htmlRuntimeId: string; scope?: string; timestamp: number; type: "render-disposed"; element: HTMLModuleScriptElement | SolidRenderElement; moduleId?: string; component: string; mode: HTMLRenderMode; reason: string; origin: HTMLRegistrationOrigin; source: HTMLRenderSource }
|
|
219
|
+
| { htmlRuntimeId: string; scope?: string; timestamp: number; type: "html-warning"; code: "unresolved-runtime-scope"; operation: "render"; message: string; element: HTMLModuleScriptElement; moduleId?: string; requestedScope: string; registeredScopes: string[]; matchingScopeRegistered: boolean; origin: HTMLRegistrationOrigin }
|
|
219
220
|
| { htmlRuntimeId: string; scope?: string; timestamp: number; type: "html-error"; operation: "discover" | "claim" | "fetch" | "register" | "update" | "remove" | "entry" | "render" | "solid-render" | "root-change" | "observer" | string; error: unknown; element?: HTMLModuleScriptElement | SolidRenderElement; moduleId?: string; origin?: HTMLRegistrationOrigin };
|
|
220
221
|
|
|
221
222
|
export type HTMLRuntimeEventType = HTMLRuntimeEvent["type"];
|
package/package.json
CHANGED
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
|
|
|
@@ -17,6 +17,7 @@ const defaultControllers = new WeakMap();
|
|
|
17
17
|
const activeControllers = new Set();
|
|
18
18
|
const solidRenderClasses = new WeakMap();
|
|
19
19
|
const solidRenderInternals = new WeakMap();
|
|
20
|
+
const unresolvedRenderScopeWarnings = new WeakMap();
|
|
20
21
|
let anonymousSequence = 0;
|
|
21
22
|
let controllerSequence = 0;
|
|
22
23
|
|
|
@@ -82,6 +83,9 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
82
83
|
queue: Promise.resolve(),
|
|
83
84
|
lastError: undefined,
|
|
84
85
|
connected: false,
|
|
86
|
+
registrationDepth: 0,
|
|
87
|
+
registrationPasses: 0,
|
|
88
|
+
solidRenderRetryScheduled: false,
|
|
85
89
|
initial: emptyRegistrationResult(),
|
|
86
90
|
};
|
|
87
91
|
|
|
@@ -126,10 +130,13 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
126
130
|
|
|
127
131
|
controller.api = api;
|
|
128
132
|
activeControllers.add(controller);
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
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
|
+
});
|
|
133
140
|
controller.runtimeDisposeUnsubscribe = runtime.subscribe?.("runtime-disposed", () => {
|
|
134
141
|
void disposeController(controller, { runtimeDisposed: true });
|
|
135
142
|
});
|
|
@@ -146,20 +153,23 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
146
153
|
const origin = callOptions.origin ?? "initial-scan";
|
|
147
154
|
const discovered = Array.from(root.querySelectorAll(selector));
|
|
148
155
|
const elements = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const accepted = acceptsDiscoveredElement(controller, element, callOptions);
|
|
152
|
-
emitHTML(controller, {
|
|
153
|
-
type: "element-discovered",
|
|
154
|
-
element,
|
|
155
|
-
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
156
|
-
origin,
|
|
157
|
-
accepted,
|
|
158
|
-
});
|
|
159
|
-
if (accepted) elements.push(element);
|
|
160
|
-
}
|
|
156
|
+
const pendingBeforeUpgrade = collectConnectedSolidRenderElements(controller, root);
|
|
157
|
+
controller.registrationDepth += 1;
|
|
161
158
|
|
|
162
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
|
+
|
|
163
173
|
const result = await processElements(controller, elements, {
|
|
164
174
|
...callOptions,
|
|
165
175
|
root,
|
|
@@ -167,11 +177,21 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
167
177
|
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
168
178
|
executeRenders: callOptions.executeRenders ?? controller.executeRenders,
|
|
169
179
|
});
|
|
180
|
+
|
|
181
|
+
// Existing upgraded instances may have connected before this controller's
|
|
182
|
+
// declaration batch was installed. Retry those after the batch completes.
|
|
170
183
|
registerSolidRenderElement();
|
|
184
|
+
controller.registrationPasses += 1;
|
|
185
|
+
await retrySolidRenderElements(controller, pendingBeforeUpgrade, {
|
|
186
|
+
reason: "registration-complete",
|
|
187
|
+
forceMissingError: true,
|
|
188
|
+
});
|
|
171
189
|
return result;
|
|
172
190
|
} catch (error) {
|
|
173
191
|
emitHTMLError(controller, "register", error, { origin });
|
|
174
192
|
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
controller.registrationDepth = Math.max(0, controller.registrationDepth - 1);
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
197
|
|
|
@@ -184,6 +204,8 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
184
204
|
|
|
185
205
|
const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime observe");
|
|
186
206
|
controller.observerOptions = { ...callOptions };
|
|
207
|
+
const pendingBeforeUpgrade = collectConnectedSolidRenderElements(controller, root);
|
|
208
|
+
controller.registrationDepth += 1;
|
|
187
209
|
|
|
188
210
|
try {
|
|
189
211
|
controller.initial = await connectObserver(controller, root, callOptions, {
|
|
@@ -191,9 +213,16 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
191
213
|
initial: true,
|
|
192
214
|
});
|
|
193
215
|
registerSolidRenderElement();
|
|
216
|
+
controller.registrationPasses += 1;
|
|
217
|
+
await retrySolidRenderElements(controller, pendingBeforeUpgrade, {
|
|
218
|
+
reason: "observation-ready",
|
|
219
|
+
forceMissingError: true,
|
|
220
|
+
});
|
|
194
221
|
} catch (error) {
|
|
195
222
|
disconnect();
|
|
196
223
|
throw error;
|
|
224
|
+
} finally {
|
|
225
|
+
controller.registrationDepth = Math.max(0, controller.registrationDepth - 1);
|
|
197
226
|
}
|
|
198
227
|
|
|
199
228
|
return api;
|
|
@@ -643,6 +672,8 @@ export function registerSolidRenderElement(options = {}) {
|
|
|
643
672
|
mount: undefined,
|
|
644
673
|
controller: undefined,
|
|
645
674
|
propObserver: undefined,
|
|
675
|
+
pendingModuleResolution: undefined,
|
|
676
|
+
pendingCheckGeneration: undefined,
|
|
646
677
|
});
|
|
647
678
|
}
|
|
648
679
|
|
|
@@ -698,6 +729,8 @@ function getSolidRenderState(element) {
|
|
|
698
729
|
mount: undefined,
|
|
699
730
|
controller: undefined,
|
|
700
731
|
propObserver: undefined,
|
|
732
|
+
pendingModuleResolution: undefined,
|
|
733
|
+
pendingCheckGeneration: undefined,
|
|
701
734
|
};
|
|
702
735
|
solidRenderInternals.set(element, state);
|
|
703
736
|
}
|
|
@@ -736,10 +769,12 @@ function startSolidRenderPropObserver(element, state) {
|
|
|
736
769
|
state.propObserver = observer;
|
|
737
770
|
}
|
|
738
771
|
|
|
739
|
-
async function remountSolidRender(element, reason) {
|
|
772
|
+
async function remountSolidRender(element, reason, options = {}) {
|
|
740
773
|
const state = getSolidRenderState(element);
|
|
741
774
|
if (!state.connected) return;
|
|
742
775
|
const generation = ++state.generation;
|
|
776
|
+
state.pendingModuleResolution = undefined;
|
|
777
|
+
state.pendingCheckGeneration = undefined;
|
|
743
778
|
await disposeSolidRenderMount(element, state, reason === "connect" ? "reconnect" : reason);
|
|
744
779
|
if (!state.connected || generation !== state.generation) return;
|
|
745
780
|
|
|
@@ -790,6 +825,8 @@ async function remountSolidRender(element, reason) {
|
|
|
790
825
|
|
|
791
826
|
mount.mode = "container";
|
|
792
827
|
state.mount = mount;
|
|
828
|
+
state.pendingModuleResolution = undefined;
|
|
829
|
+
state.pendingCheckGeneration = undefined;
|
|
793
830
|
emitHTML(controller, {
|
|
794
831
|
type: "render-mounted",
|
|
795
832
|
element,
|
|
@@ -805,6 +842,26 @@ async function remountSolidRender(element, reason) {
|
|
|
805
842
|
// failure belongs to the abandoned generation just like a stale success,
|
|
806
843
|
// so it must not overwrite/report against the current element state.
|
|
807
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
|
+
|
|
808
865
|
if (controller) {
|
|
809
866
|
emitHTMLError(controller, "solid-render", error, {
|
|
810
867
|
element,
|
|
@@ -906,30 +963,55 @@ function kebabToCamel(value) {
|
|
|
906
963
|
function resolveHTMLControllerForElement(element) {
|
|
907
964
|
const scope = nonEmpty(element.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
|
|
908
965
|
const live = [...activeControllers].filter(controller => !controller.disposed);
|
|
909
|
-
let candidates = scope
|
|
910
|
-
? live.filter(controller => controller.scope === scope)
|
|
911
|
-
: live.filter(controller => !controller.scope);
|
|
912
966
|
|
|
913
|
-
|
|
914
|
-
|
|
967
|
+
// <solid-render> must use the same routing semantics as discovered script
|
|
968
|
+
// declarations. In particular, an unscoped element may be handled by a
|
|
969
|
+
// scoped controller when that controller accepts unscoped declarations.
|
|
970
|
+
// Root containment is also authoritative: never fall back to a controller
|
|
971
|
+
// outside the element's DOM boundary merely because it is the only candidate.
|
|
972
|
+
const candidates = live.filter(controller => {
|
|
973
|
+
if (scope) return controller.scope === scope;
|
|
974
|
+
return controller.scope ? controller.acceptUnscoped !== false : true;
|
|
975
|
+
});
|
|
976
|
+
const contained = candidates.filter(controller =>
|
|
977
|
+
rootContainsElement(controller.root ?? globalThis.document, element)
|
|
978
|
+
);
|
|
979
|
+
|
|
915
980
|
if (contained.length === 1) return contained[0];
|
|
916
981
|
if (contained.length > 1) {
|
|
917
982
|
const specific = mostSpecificController(contained);
|
|
918
983
|
if (specific) return specific;
|
|
919
984
|
}
|
|
920
|
-
if (candidates.length === 1) return candidates[0];
|
|
921
985
|
|
|
922
|
-
if (
|
|
986
|
+
if (contained.length === 0) {
|
|
987
|
+
if (scope) {
|
|
988
|
+
if (candidates.length === 0) {
|
|
989
|
+
throw new HTMLRenderError(
|
|
990
|
+
`No HTML runtime is registered for data-solid-runtime=${JSON.stringify(scope)}.`,
|
|
991
|
+
{ element },
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
throw new HTMLRenderError(
|
|
995
|
+
`HTML runtime scope ${JSON.stringify(scope)} is registered, but no matching controller root contains this <solid-render> element.`,
|
|
996
|
+
{ element },
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (candidates.length === 0) {
|
|
1001
|
+
throw new HTMLRenderError(
|
|
1002
|
+
"No HTML runtime accepts this unscoped <solid-render>. Add data-solid-runtime or enable acceptUnscoped on the intended controller.",
|
|
1003
|
+
{ element },
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
923
1007
|
throw new HTMLRenderError(
|
|
924
|
-
|
|
925
|
-
? `No HTML runtime is registered for data-solid-runtime=${JSON.stringify(scope)}.`
|
|
926
|
-
: "No unambiguous HTML runtime is available for <solid-render>. Add data-solid-runtime when multiple runtimes exist.",
|
|
1008
|
+
"No HTML runtime that accepts unscoped declarations has a root containing this <solid-render> element.",
|
|
927
1009
|
{ element },
|
|
928
1010
|
);
|
|
929
1011
|
}
|
|
930
1012
|
|
|
931
1013
|
throw new HTMLRenderError(
|
|
932
|
-
`Multiple HTML runtimes match <solid-render>${scope ? ` for scope ${JSON.stringify(scope)}` : ""}. Use distinct scopes or DOM roots.`,
|
|
1014
|
+
`Multiple HTML runtimes match <solid-render>${scope ? ` for scope ${JSON.stringify(scope)}` : ""}. Use data-solid-runtime, distinct scopes, or non-overlapping DOM roots.`,
|
|
933
1015
|
{ element },
|
|
934
1016
|
);
|
|
935
1017
|
}
|
|
@@ -958,6 +1040,116 @@ function mostSpecificController(controllers) {
|
|
|
958
1040
|
return undefined;
|
|
959
1041
|
}
|
|
960
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
|
+
|
|
961
1153
|
function dispatchSolidRenderError(element, error) {
|
|
962
1154
|
try {
|
|
963
1155
|
const EventImpl = globalThis.CustomEvent;
|
|
@@ -997,6 +1189,8 @@ async function disposeController(controller, options = {}) {
|
|
|
997
1189
|
}
|
|
998
1190
|
|
|
999
1191
|
activeControllers.delete(controller);
|
|
1192
|
+
controller.runtimeModuleDefinedUnsubscribe?.();
|
|
1193
|
+
controller.runtimeModuleDefinedUnsubscribe = undefined;
|
|
1000
1194
|
controller.runtimeDisposeUnsubscribe?.();
|
|
1001
1195
|
controller.runtimeDisposeUnsubscribe = undefined;
|
|
1002
1196
|
controller.events.clear();
|
|
@@ -1103,6 +1297,7 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1103
1297
|
});
|
|
1104
1298
|
if (!accepted) {
|
|
1105
1299
|
ignored += 1;
|
|
1300
|
+
maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
|
|
1106
1301
|
continue;
|
|
1107
1302
|
}
|
|
1108
1303
|
matched += 1;
|
|
@@ -1135,6 +1330,11 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1135
1330
|
}
|
|
1136
1331
|
|
|
1137
1332
|
await cleanupDetachedRenderedDeclarations(controller, root);
|
|
1333
|
+
await retrySolidRenderElements(
|
|
1334
|
+
controller,
|
|
1335
|
+
collectConnectedSolidRenderElements(controller, root),
|
|
1336
|
+
{ reason: "observer-batch-complete", forceMissingError: true },
|
|
1337
|
+
);
|
|
1138
1338
|
|
|
1139
1339
|
emitHTML(controller, {
|
|
1140
1340
|
type: "observer-batch",
|
|
@@ -1778,6 +1978,52 @@ function acceptsDiscoveredElement(controller, element, options = {}) {
|
|
|
1778
1978
|
return !elementScope;
|
|
1779
1979
|
}
|
|
1780
1980
|
|
|
1981
|
+
function maybeWarnUnresolvedRenderScope(controller, element, options = {}, origin = "discover") {
|
|
1982
|
+
if ((options.executeRenders ?? controller.executeRenders) === false) return false;
|
|
1983
|
+
if (!hasAttribute(element, "render")) return false;
|
|
1984
|
+
|
|
1985
|
+
const requestedScope = nonEmpty(element?.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
|
|
1986
|
+
if (!requestedScope) return false;
|
|
1987
|
+
|
|
1988
|
+
const live = [...activeControllers].filter(candidate => !candidate.disposed);
|
|
1989
|
+
const matchingScopeControllers = live.filter(candidate => candidate.scope === requestedScope);
|
|
1990
|
+
const handlingController = matchingScopeControllers.find(candidate =>
|
|
1991
|
+
rootContainsElement(candidate.root ?? globalThis.document, element)
|
|
1992
|
+
);
|
|
1993
|
+
if (handlingController) return false;
|
|
1994
|
+
|
|
1995
|
+
if (unresolvedRenderScopeWarnings.get(element) === requestedScope) return false;
|
|
1996
|
+
unresolvedRenderScopeWarnings.set(element, requestedScope);
|
|
1997
|
+
|
|
1998
|
+
const registeredScopes = [...new Set(live.map(candidate => candidate.scope).filter(Boolean))].sort();
|
|
1999
|
+
const moduleId = nonEmpty(element?.getAttribute?.("module"));
|
|
2000
|
+
const scopeLabel = JSON.stringify(requestedScope);
|
|
2001
|
+
const moduleLabel = moduleId ? ` module=${JSON.stringify(moduleId)}` : "";
|
|
2002
|
+
const message = matchingScopeControllers.length > 0
|
|
2003
|
+
? `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.`
|
|
2004
|
+
: `solid-tag-runtime/html: Unable to render <script${moduleLabel}> because data-solid-runtime=${scopeLabel} does not match any registered HTML runtime.`;
|
|
2005
|
+
|
|
2006
|
+
emitHTML(controller, {
|
|
2007
|
+
type: "html-warning",
|
|
2008
|
+
code: "unresolved-runtime-scope",
|
|
2009
|
+
operation: "render",
|
|
2010
|
+
message,
|
|
2011
|
+
element,
|
|
2012
|
+
moduleId,
|
|
2013
|
+
requestedScope,
|
|
2014
|
+
registeredScopes,
|
|
2015
|
+
matchingScopeRegistered: matchingScopeControllers.length > 0,
|
|
2016
|
+
origin,
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
globalThis.console?.warn?.(message, {
|
|
2020
|
+
element,
|
|
2021
|
+
requestedScope,
|
|
2022
|
+
registeredScopes,
|
|
2023
|
+
});
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1781
2027
|
function assertExplicitScopeCompatibility(controller, element, options = {}) {
|
|
1782
2028
|
const requestedScope = normalizeScope(options.scope ?? controller.scope);
|
|
1783
2029
|
const elementScope = nonEmpty(element.getAttribute(RUNTIME_SCOPE_ATTRIBUTE));
|
package/src/runtime.js
CHANGED
|
@@ -245,6 +245,14 @@ export function createRuntime(options = {}) {
|
|
|
245
245
|
const record = records.get(resolved);
|
|
246
246
|
|
|
247
247
|
if (!record) {
|
|
248
|
+
// Top-level virtual path imports follow the same rule as static imports:
|
|
249
|
+
// unresolved relative/absolute virtual IDs are runtime resolution errors,
|
|
250
|
+
// not native browser fetches. Native fallback is reserved for absolute
|
|
251
|
+
// URLs and unresolved bare specifiers when allowNativeImports is enabled.
|
|
252
|
+
const raw = String(id);
|
|
253
|
+
if (isRelativeSpecifier(raw) || raw.startsWith("/")) {
|
|
254
|
+
throw new ModuleResolutionError(id, null);
|
|
255
|
+
}
|
|
248
256
|
if (!allowNativeImports && !isAbsoluteUrl(resolved)) {
|
|
249
257
|
throw new ModuleResolutionError(id, null);
|
|
250
258
|
}
|