solid-tag-runtime 0.0.9 → 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 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 the HTML runtime through the shared controller registry. With no attribute, a single unambiguous controller may be used.
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.
@@ -1196,3 +1197,41 @@ The shared render adapter lazily resolves the host application's own `solid-js`
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.
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.9",
111
- "solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.9/html"
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"
112
112
  }
113
113
  }
114
114
  ```
@@ -287,6 +287,17 @@ 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
+
290
301
  A scope is written to owned script elements as:
291
302
 
292
303
  ```html
@@ -295,6 +306,18 @@ A scope is written to owned script elements as:
295
306
 
296
307
  When multiple runtimes observe the same document, give each one a unique scope and normally set `acceptUnscoped: false`.
297
308
 
309
+ 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.
310
+
311
+ ```ts
312
+ html.subscribe("html-warning", event => {
313
+ if (event.code === "unresolved-runtime-scope") {
314
+ console.warn(event.requestedScope, event.moduleId);
315
+ }
316
+ });
317
+ ```
318
+
319
+ `<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.
320
+
298
321
  ### Declarative modules
299
322
 
300
323
  ```html
@@ -843,11 +866,14 @@ render-mounted
843
866
  render-disposing
844
867
  render-disposed
845
868
 
869
+ html-warning
846
870
  html-error
847
871
  ```
848
872
 
849
873
  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
874
 
875
+ `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.
876
+
851
877
  `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
878
 
853
879
  Like core runtime subscribers, HTML subscribers are observational and exception-isolated.
@@ -916,7 +942,7 @@ Default runtime resolution supports:
916
942
 
917
943
  `allowNativeImports` defaults to `true`.
918
944
 
919
- This means an unresolved bare import can be left for the browser's native module resolver/import map:
945
+ This means an unresolved **bare** import can be left for the browser's native module resolver/import map:
920
946
 
921
947
  ```ts
922
948
  const runtime = createRuntime({
@@ -924,7 +950,9 @@ const runtime = createRuntime({
924
950
  });
925
951
  ```
926
952
 
927
- Set it to `false` when all module dependencies must be explicitly registered with the runtime.
953
+ 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.
954
+
955
+ Set `allowNativeImports` to `false` when even unresolved bare imports must be explicitly registered with the runtime.
928
956
 
929
957
  ## Runtime lifecycle
930
958
 
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid-tag-runtime",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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
@@ -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
 
@@ -157,6 +158,7 @@ export function createHTMLRuntime(runtime, options = {}) {
157
158
  accepted,
158
159
  });
159
160
  if (accepted) elements.push(element);
161
+ else maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
160
162
  }
161
163
 
162
164
  try {
@@ -906,30 +908,55 @@ function kebabToCamel(value) {
906
908
  function resolveHTMLControllerForElement(element) {
907
909
  const scope = nonEmpty(element.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
908
910
  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
911
 
913
- if (!scope && candidates.length === 0 && live.length === 1) candidates = live;
914
- const contained = candidates.filter(controller => rootContainsElement(controller.root ?? globalThis.document, element));
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
+
915
925
  if (contained.length === 1) return contained[0];
916
926
  if (contained.length > 1) {
917
927
  const specific = mostSpecificController(contained);
918
928
  if (specific) return specific;
919
929
  }
920
- if (candidates.length === 1) return candidates[0];
921
930
 
922
- if (candidates.length === 0) {
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
+
923
952
  throw new HTMLRenderError(
924
- scope
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.",
953
+ "No HTML runtime that accepts unscoped declarations has a root containing this <solid-render> element.",
927
954
  { element },
928
955
  );
929
956
  }
930
957
 
931
958
  throw new HTMLRenderError(
932
- `Multiple HTML runtimes match <solid-render>${scope ? ` for scope ${JSON.stringify(scope)}` : ""}. Use distinct scopes or DOM roots.`,
959
+ `Multiple HTML runtimes match <solid-render>${scope ? ` for scope ${JSON.stringify(scope)}` : ""}. Use data-solid-runtime, distinct scopes, or non-overlapping DOM roots.`,
933
960
  { element },
934
961
  );
935
962
  }
@@ -1103,6 +1130,7 @@ async function enqueueObserverScan(controller, callOptions = {}) {
1103
1130
  });
1104
1131
  if (!accepted) {
1105
1132
  ignored += 1;
1133
+ maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
1106
1134
  continue;
1107
1135
  }
1108
1136
  matched += 1;
@@ -1778,6 +1806,52 @@ function acceptsDiscoveredElement(controller, element, options = {}) {
1778
1806
  return !elementScope;
1779
1807
  }
1780
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
+
1781
1855
  function assertExplicitScopeCompatibility(controller, element, options = {}) {
1782
1856
  const requestedScope = normalizeScope(options.scope ?? controller.scope);
1783
1857
  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
  }