@abide/abide 0.36.0 → 0.38.0

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.
Files changed (61) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +44 -0
  3. package/package.json +1 -1
  4. package/src/abideResolverPlugin.ts +3 -17
  5. package/src/appEntry.ts +18 -8
  6. package/src/controlServerWorker.ts +8 -1
  7. package/src/lib/cli/parseArgvForRpc.ts +8 -3
  8. package/src/lib/mcp/createMcpServer.ts +8 -0
  9. package/src/lib/mcp/dispatchMcpRequest.ts +20 -2
  10. package/src/lib/mcp/toolResultFromResponse.ts +5 -0
  11. package/src/lib/server/rpc/parseArgs.ts +12 -1
  12. package/src/lib/server/runtime/acceptsGzip.ts +10 -1
  13. package/src/lib/server/runtime/buildInFlightSnapshot.ts +35 -0
  14. package/src/lib/server/runtime/buildInspectorSurface.ts +9 -1
  15. package/src/lib/server/runtime/createServer.ts +10 -1
  16. package/src/lib/server/runtime/gzipResponse.ts +2 -1
  17. package/src/lib/server/runtime/inFlightRequests.ts +13 -0
  18. package/src/lib/server/runtime/maybeMountInspector.ts +7 -0
  19. package/src/lib/server/runtime/runWithRequestScope.ts +7 -0
  20. package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -1
  21. package/src/lib/server/runtime/streamFromIterator.ts +8 -0
  22. package/src/lib/server/runtime/types/InspectorContext.ts +4 -0
  23. package/src/lib/server/runtime/types/InspectorInFlightRequest.ts +20 -0
  24. package/src/lib/server/runtime/types/InspectorInFlightSnapshot.ts +10 -0
  25. package/src/lib/server/runtime/types/InspectorPrompt.ts +15 -0
  26. package/src/lib/server/runtime/types/InspectorSurface.ts +6 -3
  27. package/src/lib/server/sockets/createSocketDispatcher.ts +7 -1
  28. package/src/lib/server/sockets/defineSocket.ts +6 -1
  29. package/src/lib/shared/cache.ts +6 -0
  30. package/src/lib/shared/createPushIterator.ts +7 -2
  31. package/src/lib/shared/emitLogRecord.ts +18 -5
  32. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -0
  33. package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
  34. package/src/lib/ui/compile/asOutlet.ts +29 -0
  35. package/src/lib/ui/compile/componentWrapperTag.ts +12 -20
  36. package/src/lib/ui/compile/generateBuild.ts +45 -36
  37. package/src/lib/ui/compile/generateSSR.ts +31 -21
  38. package/src/lib/ui/compile/parseTemplate.ts +10 -1
  39. package/src/lib/ui/compile/renameSignalRefs.ts +25 -2
  40. package/src/lib/ui/compile/skeletonContext.ts +97 -34
  41. package/src/lib/ui/compile/types/SkeletonContext.ts +6 -0
  42. package/src/lib/ui/createScope.ts +11 -0
  43. package/src/lib/ui/dom/appendSnippet.ts +60 -20
  44. package/src/lib/ui/dom/fillBefore.ts +10 -0
  45. package/src/lib/ui/dom/hydrate.ts +2 -1
  46. package/src/lib/ui/dom/mount.ts +2 -1
  47. package/src/lib/ui/dom/mountSlot.ts +7 -2
  48. package/src/lib/ui/dom/scopeLabel.ts +19 -0
  49. package/src/lib/ui/dom/skeleton.ts +16 -1
  50. package/src/lib/ui/installInspectorBridge.ts +138 -0
  51. package/src/lib/ui/navigate.ts +11 -3
  52. package/src/lib/ui/persist.ts +4 -1
  53. package/src/lib/ui/router.ts +77 -9
  54. package/src/lib/ui/runtime/createDoc.ts +20 -7
  55. package/src/lib/ui/runtime/historyEntries.ts +113 -0
  56. package/src/lib/ui/runtime/liveScopes.ts +15 -0
  57. package/src/lib/ui/runtime/types/AbideHistoryState.ts +9 -0
  58. package/src/lib/ui/seedStreamedResolution.ts +10 -1
  59. package/src/lib/ui/startClient.ts +6 -0
  60. package/src/lib/ui/types/Scope.ts +3 -0
  61. package/src/lib/ui/compile/HTML_TAGS.ts +0 -132
@@ -6,7 +6,9 @@ import { clientPage } from './runtime/clientPage.ts'
6
6
  import { enterRenderPass } from './runtime/enterRenderPass.ts'
7
7
  import { exitRenderPass } from './runtime/exitRenderPass.ts'
8
8
  import { firstOutlet } from './runtime/firstOutlet.ts'
9
+ import { historyEntries } from './runtime/historyEntries.ts'
9
10
  import { runtimePath } from './runtime/runtimePath.ts'
11
+ import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
10
12
  import type { NavVerdict } from './runtime/types/NavVerdict.ts'
11
13
  import type { Route } from './runtime/types/Route.ts'
12
14
  import type { RouteLoader } from './runtime/types/RouteLoader.ts'
@@ -20,6 +22,15 @@ type MountedLayout = { key: string; dispose: () => void; outlet: Element }
20
22
  /* A layout mount that returns no disposer still needs one for the chain teardown. */
21
23
  const noop = (): void => {}
22
24
 
25
+ /* The destination URL for a navigation `path`. On the server / headless there is no
26
+ `location`, so resolve against a localhost origin; in the browser, against the real
27
+ origin — `location` is already updated to `path` by the time a swap reads it, so this
28
+ matches `new URL(location.href)`. */
29
+ const resolveUrl = (path: string): URL =>
30
+ typeof location === 'undefined'
31
+ ? new URL(`http://localhost${path}`)
32
+ : new URL(path, location.origin)
33
+
23
34
  /*
24
35
  A minimal client router on the History API. `router` matches the current path
25
36
  against the route patterns (literal / `[name]` / `[...rest]`, via matchRoute),
@@ -34,6 +45,11 @@ is disposed and rebuilt, and the page (always the leaf) re-mounts every time. Th
34
45
  reactive `page` proxy means a persisted layout reading route/params updates in place
35
46
  without a remount. Each chunk loads only on first visit, cached after.
36
47
 
48
+ Scroll restoration is manual (`historyEntries`): because the page is rebuilt after the
49
+ browser would restore scroll, each history entry's offset is bucketed by an `abideEntry`
50
+ id and reapplied once the destination DOM exists — back/forward returns to its offset, a
51
+ fresh navigation scrolls to the top.
52
+
37
53
  `probe` (when given) runs each post-boot navigation's destination through the
38
54
  server's app.handle first, so auth/redirect gating applies to client navigation
39
55
  just as it does to a fresh load; its verdict either clears the mount, soft-redirects
@@ -134,9 +150,22 @@ export function router(
134
150
  run()
135
151
  }
136
152
 
153
+ const entryOf = (): number =>
154
+ (history.state as AbideHistoryState | null)?.abideEntry ?? historyEntries.current
137
155
  const onPopState = (): void => {
156
+ /* Bucket the leaving entry's scroll (current still its id) before adopting the
157
+ one back/forward landed on; `swap` restores the adopted entry's offset once
158
+ its DOM is rebuilt. */
159
+ historyEntries.save()
160
+ historyEntries.adopt(entryOf())
138
161
  runtimePath.value = location.pathname + location.search + location.hash
139
162
  }
163
+ const onPageHide = (): void => {
164
+ /* Mirror the live scroll into the active entry's state before it unloads, so a
165
+ reload can recover it — the in-memory bucket is gone and `manual` keeps the
166
+ browser from restoring. */
167
+ historyEntries.persist()
168
+ }
140
169
  const onClick = (event: MouseEvent): void => {
141
170
  /* Let the browser own anything that isn't a plain primary-button click:
142
171
  modified clicks (open in a new tab/window), middle/right buttons, and
@@ -175,7 +204,22 @@ export function router(
175
204
  navigate(destination.pathname + destination.search + destination.hash)
176
205
  }
177
206
  if (typeof window !== 'undefined') {
207
+ /* Own scroll restoration: the browser would restore against the pre-teardown
208
+ DOM. Adopt the initial entry's id (survives a reload) and stamp it onto the
209
+ landing entry — merging so any `scroll` a prior unload persisted into this
210
+ entry's state stays put for the first-paint `restore` to recover. */
211
+ if ('scrollRestoration' in history) {
212
+ history.scrollRestoration = 'manual'
213
+ }
214
+ historyEntries.adopt(entryOf())
215
+ const landingState = (history.state as AbideHistoryState | null) ?? {}
216
+ history.replaceState(
217
+ { ...landingState, abideEntry: historyEntries.current },
218
+ '',
219
+ location.href,
220
+ )
178
221
  window.addEventListener('popstate', onPopState)
222
+ window.addEventListener('pagehide', onPageHide)
179
223
  document.addEventListener('click', onClick as EventListener)
180
224
  }
181
225
 
@@ -200,6 +244,29 @@ export function router(
200
244
  /* The route matches on the pathname only; the query/hash ride along for
201
245
  the probe (so server gating sees them) and for clientPage.url. */
202
246
  const pathname = path.split(/[?#]/)[0] ?? path
247
+ /* A same-document navigation — only the `#hash` (and thus scroll) differs from
248
+ the mounted page — needs no teardown: the live page stays, page.url
249
+ republishes (so `page.url.hash` updates in place), and we restore the entry's
250
+ scroll bucket or scroll to the anchor. A differing pathname or query still
251
+ rebuilds (a query is page data). Skipped on first paint — nothing is mounted. */
252
+ const targetUrl = resolveUrl(path)
253
+ const mountedUrl = clientPage.value.url
254
+ if (
255
+ !first &&
256
+ mountedUrl.pathname === targetUrl.pathname &&
257
+ mountedUrl.search === targetUrl.search &&
258
+ mountedUrl.hash !== targetUrl.hash
259
+ ) {
260
+ /* Invalidate any in-flight full navigation so its late `.then` can't
261
+ rebuild over the page this hash hop keeps mounted (the token guard
262
+ only bails on a NEWER sequence). That bailed `.then` is also the only
263
+ writer of `navigating: false`, so clear the flag here — a hash hop is
264
+ synchronous and settles immediately. */
265
+ sequence += 1
266
+ clientPage.value = { ...clientPage.value, url: targetUrl, navigating: false }
267
+ historyEntries.restore(targetUrl.hash)
268
+ return
269
+ }
203
270
  const matched = matchRoute(patterns, pathname)
204
271
  const key = matched?.route ?? '*'
205
272
  const params = matched?.params ?? {}
@@ -275,19 +342,19 @@ export function router(
275
342
  while it's still mounted. Disposing first kills that scope; surviving
276
343
  prefix layouts then update in place on publish. */
277
344
  const base = disposeFrom(divergence)
278
- clientPage.value = {
279
- route: chainRoute,
280
- params,
281
- url:
282
- typeof location === 'undefined'
283
- ? new URL(`http://localhost${path}`)
284
- : new URL(location.href),
285
- navigating: false,
286
- }
345
+ const url = resolveUrl(path)
346
+ clientPage.value = { route: chainRoute, params, url, navigating: false }
287
347
  if (!hydrating) {
288
348
  base.textContent = ''
289
349
  }
290
350
  buildFrom(base, divergence, chainKeys, layoutViews, pageView, params, hydrating)
351
+ /* Reapply the destination entry's scroll once its DOM exists — a
352
+ back/forward restores its offset, a fresh nav scrolls to the `#hash`
353
+ anchor (now built) or the top. Runs on the initial paint too: with
354
+ `scrollRestoration='manual'` the browser does NOT restore a reload's
355
+ offset, so first paint recovers it from the persisted `history.state`
356
+ (a fresh load with no persisted offset falls through to hash/top). */
357
+ historyEntries.restore(url.hash)
291
358
  }
292
359
  /* Wrap the swap in a View Transition where the browser supports it, so
293
360
  the page change cross-fades (and shared `view-transition-name` elements
@@ -311,6 +378,7 @@ export function router(
311
378
  disposed = true
312
379
  if (typeof window !== 'undefined') {
313
380
  window.removeEventListener('popstate', onPopState)
381
+ window.removeEventListener('pagehide', onPageHide)
314
382
  document.removeEventListener('click', onClick as EventListener)
315
383
  }
316
384
  stop()
@@ -14,6 +14,13 @@ import { unescapeKey } from './unescapeKey.ts'
14
14
  import { walkPath } from './walkPath.ts'
15
15
  import { writeNode } from './writeNode.ts'
16
16
 
17
+ /* `path` minus its last segment — the parent container's path, '' at the root.
18
+ The same string `segments.slice(0, -1).join('/')` rebuilds, by one slice. */
19
+ function parentPathOf(path: string): string {
20
+ const lastSlash = path.lastIndexOf('/')
21
+ return lastSlash === -1 ? '' : path.slice(0, lastSlash)
22
+ }
23
+
17
24
  /*
18
25
  Builds a reactive document over `initial`. Each path read for the first time
19
26
  mints a signal node; the node is the notification token, the (mutable) tree is
@@ -132,10 +139,7 @@ export function createDoc(initial: unknown): Doc {
132
139
  remove (computed post-apply, below, to resolve an array append's index). */
133
140
  const before = PATCH_BUS.active ? walkPath(tree, patch.path) : undefined
134
141
  tree = applyPatchToTree(tree, patch, segments)
135
- /* parentPath is patch.path minus its last segment — the same string
136
- `segments.slice(0, -1).join('/')` rebuilds, taken by one slice instead. */
137
- const lastSlash = patch.path.lastIndexOf('/')
138
- const parentPath = lastSlash === -1 ? '' : patch.path.slice(0, lastSlash)
142
+ const parentPath = parentPathOf(patch.path)
139
143
  const parentValue = walkPath(tree, parentPath).value
140
144
  const leafKey = segments[segments.length - 1] as string | undefined
141
145
  /* A structural change (add/remove, or an array element replaced by index)
@@ -194,8 +198,7 @@ export function createDoc(initial: unknown): Doc {
194
198
  before: ReturnType<typeof walkPath> | undefined,
195
199
  ): Patch | undefined {
196
200
  if (patch.op === 'add') {
197
- const lastSlash = patch.path.lastIndexOf('/')
198
- const parentPath = lastSlash === -1 ? '' : patch.path.slice(0, lastSlash)
201
+ const parentPath = parentPathOf(patch.path)
199
202
  const parent = walkPath(tree, parentPath).value
200
203
  const resolved =
201
204
  Array.isArray(parent) && patch.path.endsWith('/-')
@@ -223,9 +226,19 @@ export function createDoc(initial: unknown): Doc {
223
226
  const node = nodeFor(path)
224
227
  const segments = path.split('/').map(unescapeKey)
225
228
  const leafKey = segments[segments.length - 1] as string
229
+ /* Auto-vivify missing ancestor objects so binding a nested path on a doc
230
+ booted shallow (e.g. `state({})`) doesn't crash, and a later `set` writes
231
+ into the LIVE tree (so snapshot/persist see it). Mirrors the container
232
+ assumption applyPatchToTree makes — except the patch path is authored, this
233
+ walk is compiler-emitted, so the intermediates may not exist yet. */
226
234
  let parent = tree as Record<string, unknown>
227
235
  for (const segment of segments.slice(0, -1)) {
228
- parent = parent[segment] as Record<string, unknown>
236
+ let next = parent[segment]
237
+ if (next === null || typeof next !== 'object') {
238
+ next = {}
239
+ parent[segment] = next
240
+ }
241
+ parent = next as Record<string, unknown>
229
242
  }
230
243
  return {
231
244
  get: () => readNode(node) as T,
@@ -0,0 +1,113 @@
1
+ import type { AbideHistoryState } from './types/AbideHistoryState.ts'
2
+
3
+ /*
4
+ Per-history-entry scroll buckets for manual scroll restoration. The browser's own
5
+ restoration is disabled (`history.scrollRestoration = 'manual'` at router boot)
6
+ because the router tears the page down and rebuilds it AFTER the browser would have
7
+ restored scroll — so the offset is lost against a node that no longer exists. Instead
8
+ each history entry carries a monotonic id (stamped into `history.state.abideEntry` by
9
+ `navigate`), and this module buckets that entry's scroll offset: `save()` records the
10
+ outgoing offset before history moves, `restore()` reapplies the destination entry's
11
+ offset after its DOM is rebuilt (or, for an entry seen for the first time — a fresh
12
+ navigation — honours a `#hash` anchor when one resolves, else scrolls to the top). A
13
+ back/forward `adopt`s the entry id read from history.state; a `replace` `discard`s the
14
+ current bucket (its content is superseded). The in-memory `offsets` Map covers the
15
+ same-session back/forward path; `persist()` also mirrors the live scroll into
16
+ `history.state` (on pagehide, when state still reflects the active entry) so a RELOAD
17
+ — where the Map is gone and `scrollRestoration='manual'` keeps the browser from
18
+ restoring — can still recover it via `restore()`'s `persistedOffset` fallback.
19
+
20
+ Scroll APIs are reached off `globalThis` (guarded) so the module is a no-op on the
21
+ server and in headless tests that never install a window. Shared mutable singleton on
22
+ one object, reached without a barrel — mirrors `reactiveAbortState`/`clientPage`.
23
+ */
24
+
25
+ /* The scroll surface: the real `Window` shape, made `Partial` so every member is
26
+ optional — the module degrades to a no-op on the server / in headless tests that
27
+ never install a window. */
28
+ const view = globalThis as unknown as Partial<Window>
29
+
30
+ /* The active entry's offset persisted in `history.state` — survives a reload (the
31
+ in-memory `offsets` Map does not). Honoured only when the stored id matches `current`,
32
+ so a foreign history entry's state never restores the wrong scroll. */
33
+ function persistedOffset(): [number, number] | undefined {
34
+ const state = view.history?.state as AbideHistoryState | null
35
+ if (state?.abideEntry === current && Array.isArray(state.scroll)) {
36
+ return state.scroll
37
+ }
38
+ return undefined
39
+ }
40
+
41
+ /* The element a `#hash` addresses, if the document holds one. */
42
+ function anchorFor(hash: string | undefined): HTMLElement | undefined {
43
+ if (hash === undefined || hash.length <= 1 || view.document === undefined) {
44
+ return undefined
45
+ }
46
+ return view.document.getElementById(hash.slice(1)) ?? undefined
47
+ }
48
+
49
+ /* The active entry's scroll offset is buckets.get(current); `seq` mints fresh ids. */
50
+ const offsets = new Map<number, [number, number]>()
51
+ let current = 0
52
+ let seq = 0
53
+
54
+ export const historyEntries = {
55
+ /* The active history entry's id — stamped into history.state by `navigate`. */
56
+ get current(): number {
57
+ return current
58
+ },
59
+ /* Mint the next entry id for a pushed history entry, making it active. */
60
+ next(): number {
61
+ current = seq += 1
62
+ return current
63
+ },
64
+ /* A back/forward landed on an existing entry — make it active so the following
65
+ save/restore target its bucket. Keeps `seq` ahead of any adopted id. */
66
+ adopt(entry: number): void {
67
+ current = entry
68
+ if (entry > seq) {
69
+ seq = entry
70
+ }
71
+ },
72
+ /* Bucket the active entry's current scroll offset, before history moves away. */
73
+ save(): void {
74
+ if (typeof view.scrollTo === 'function') {
75
+ offsets.set(current, [view.scrollX ?? 0, view.scrollY ?? 0])
76
+ }
77
+ },
78
+ /* Drop the active entry's bucket — a replace lands fresh content, so the saved
79
+ scroll no longer applies. (The replace's own `replaceState` overwrites the
80
+ persisted copy in `history.state`, so the durable fallback clears too.) */
81
+ discard(): void {
82
+ offsets.delete(current)
83
+ },
84
+ /* Mirror the live scroll into the active entry's `history.state` so it survives a
85
+ reload (the in-memory Map does not). Called on pagehide — when `history.state`
86
+ still reflects the active entry — merging to keep its `abideEntry` stamp. */
87
+ persist(): void {
88
+ if (typeof view.scrollTo !== 'function' || view.history === undefined) {
89
+ return
90
+ }
91
+ const state = (view.history.state as AbideHistoryState | null) ?? {}
92
+ view.history.replaceState({ ...state, scroll: [view.scrollX ?? 0, view.scrollY ?? 0] }, '')
93
+ },
94
+ /* Reapply the active entry's scroll once its DOM exists. A back/forward to a saved
95
+ entry returns to its offset (in-memory, else the reload-durable persisted copy);
96
+ a fresh entry honours a `#hash` anchor when one resolves, else scrolls to the top. */
97
+ restore(hash?: string): void {
98
+ if (typeof view.scrollTo !== 'function') {
99
+ return
100
+ }
101
+ const offset = offsets.get(current) ?? persistedOffset()
102
+ if (offset !== undefined) {
103
+ view.scrollTo(offset[0], offset[1])
104
+ return
105
+ }
106
+ const anchor = anchorFor(hash)
107
+ if (anchor !== undefined) {
108
+ anchor.scrollIntoView()
109
+ return
110
+ }
111
+ view.scrollTo(0, 0)
112
+ },
113
+ }
@@ -0,0 +1,15 @@
1
+ import type { Scope } from '../types/Scope.ts'
2
+
3
+ /*
4
+ Dev-only registry of every live scope, for the inspector's Reactive tab. `scopes`
5
+ stays empty and untouched unless installInspectorBridge flips `enabled` (gated by
6
+ the server-injected `__abideInspect`), so production allocates and tracks nothing.
7
+ createScope adds on construction and removes on dispose; the bridge reconstructs
8
+ the scope forest from each entry's `id` + `parent.id` (the Scope surface exposes
9
+ no children accessor, so the flat set + parent links is the traversal path). One
10
+ mutable singleton object — mirrors `reactiveAbortState`, reached without a barrel.
11
+ */
12
+ export const liveScopes: { enabled: boolean; scopes: Set<Scope> } = {
13
+ enabled: false,
14
+ scopes: new Set(),
15
+ }
@@ -0,0 +1,9 @@
1
+ /* The shape abide stamps into `History.state`: a monotonic `abideEntry` id the router
2
+ buckets scroll by, and the last `scroll` offset persisted for that entry (so a reload
3
+ can recover it — the in-memory bucket does not survive). Both optional: a foreign or
4
+ bare entry (one another script pushed, or the first landing before a stamp) carries
5
+ neither, which the readers treat as "no abide data". */
6
+ export type AbideHistoryState = {
7
+ abideEntry?: number
8
+ scroll?: [number, number]
9
+ }
@@ -18,5 +18,14 @@ export function seedStreamedResolution(resolution: StreamedResolution): void {
18
18
  if ('miss' in resolution) {
19
19
  return
20
20
  }
21
- activeCacheStore().entries.set(resolution.key, cacheEntryFromSnapshot(resolution))
21
+ /* Only seed when nothing live holds the key — or when the existing entry is itself
22
+ an unconsumed hydrated seed (`hydrated === true`, cleared by the first cache()
23
+ read). A live/settled non-hydrated entry is authoritative; clobbering it with a
24
+ stale snapshot would drop a fresher value (e.g. one a live fetch already wrote). */
25
+ const { entries } = activeCacheStore()
26
+ const existing = entries.get(resolution.key)
27
+ if (existing !== undefined && existing.hydrated !== true) {
28
+ return
29
+ }
30
+ entries.set(resolution.key, cacheEntryFromSnapshot(resolution))
22
31
  }
@@ -6,6 +6,7 @@ import { setPageResolver } from '../shared/setPageResolver.ts'
6
6
  import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
7
7
  import type { StreamedResolution } from '../shared/types/StreamedResolution.ts'
8
8
  import { installHotBridge } from './installHotBridge.ts'
9
+ import { installInspectorBridge } from './installInspectorBridge.ts'
9
10
  import { probeNavigation } from './probeNavigation.ts'
10
11
  import { router } from './router.ts'
11
12
  import { clientPage } from './runtime/clientPage.ts'
@@ -42,6 +43,11 @@ export function startClient(
42
43
  if ((globalThis as { __abideDev?: boolean }).__abideDev) {
43
44
  installHotBridge()
44
45
  }
46
+ /* Inspector only: the server injects `__abideInspect` when ABIDE_ENABLE_INSPECTOR
47
+ is on, so the scope/router bridge arms before the router builds any scope. */
48
+ if ((globalThis as { __abideInspect?: boolean }).__abideInspect) {
49
+ installInspectorBridge()
50
+ }
45
51
  const ssr = (globalThis as { __SSR__?: SsrPayload }).__SSR__ ?? {}
46
52
  setBaseResolver(() => ssr.base ?? '')
47
53
  /* The `page` proxy reads route/params/url off the router-updated snapshot. */
@@ -22,6 +22,9 @@ the ONLY public entry; everything else is a method reached through it (the
22
22
  */
23
23
  export type Scope = {
24
24
  readonly id: string
25
+ /* Dev-only display name (the host component/element it mounted into) for the
26
+ inspector's Reactive tab; undefined for SSR/detached/child scopes. */
27
+ readonly label?: string
25
28
  readonly parent: Scope | undefined
26
29
  /* data — mirrors Doc */
27
30
  read: <T>(path: string) => T
@@ -1,132 +0,0 @@
1
- /*
2
- Every standard HTML element name (lowercase), plus the two foreign-content roots
3
- `svg`/`math`. A component wrapper tag that collides with one of these is a real
4
- element with a content model and parser quirks — `<button>`/`<a>` reject interactive
5
- descendants, table/list/select families foster or auto-close non-conforming children,
6
- void elements self-close, and `<svg>`/`<math>` switch the parser into SVG/MathML
7
- namespace so the wrapper's HTML children break out (foster-parent) of it entirely —
8
- so the parser reparents the component's own markup out of the wrapper and hydration
9
- claims `null`. `componentWrapperTag` remaps any such name to a transparent custom
10
- element instead. Pure SVG/MathML descendant names (`circle`, `path`, `mrow`, …) are
11
- NOT here: in HTML context they are inert unknown elements that hold children fine, so
12
- a `<Circle>` component stays as-is like any non-element name. Superset of VOID_TAGS
13
- (which the parser/SSR still use for self-closing); this set is only the
14
- wrapper-safety check.
15
- */
16
- export const HTML_TAGS: ReadonlySet<string> = new Set([
17
- 'a',
18
- 'abbr',
19
- 'address',
20
- 'area',
21
- 'article',
22
- 'aside',
23
- 'audio',
24
- 'b',
25
- 'base',
26
- 'bdi',
27
- 'bdo',
28
- 'blockquote',
29
- 'body',
30
- 'br',
31
- 'button',
32
- 'canvas',
33
- 'caption',
34
- 'cite',
35
- 'code',
36
- 'col',
37
- 'colgroup',
38
- 'data',
39
- 'datalist',
40
- 'dd',
41
- 'del',
42
- 'details',
43
- 'dfn',
44
- 'dialog',
45
- 'div',
46
- 'dl',
47
- 'dt',
48
- 'em',
49
- 'embed',
50
- 'fieldset',
51
- 'figcaption',
52
- 'figure',
53
- 'footer',
54
- 'form',
55
- 'h1',
56
- 'h2',
57
- 'h3',
58
- 'h4',
59
- 'h5',
60
- 'h6',
61
- 'head',
62
- 'header',
63
- 'hgroup',
64
- 'hr',
65
- 'html',
66
- 'i',
67
- 'iframe',
68
- 'img',
69
- 'input',
70
- 'ins',
71
- 'kbd',
72
- 'label',
73
- 'legend',
74
- 'li',
75
- 'link',
76
- 'main',
77
- 'map',
78
- 'mark',
79
- 'math',
80
- 'menu',
81
- 'meta',
82
- 'meter',
83
- 'nav',
84
- 'noscript',
85
- 'object',
86
- 'ol',
87
- 'optgroup',
88
- 'option',
89
- 'output',
90
- 'p',
91
- 'param',
92
- 'picture',
93
- 'pre',
94
- 'progress',
95
- 'q',
96
- 'rp',
97
- 'rt',
98
- 'ruby',
99
- 's',
100
- 'samp',
101
- 'script',
102
- 'search',
103
- 'section',
104
- 'select',
105
- 'slot',
106
- 'small',
107
- 'source',
108
- 'span',
109
- 'strong',
110
- 'style',
111
- 'sub',
112
- 'summary',
113
- 'sup',
114
- 'svg',
115
- 'table',
116
- 'tbody',
117
- 'td',
118
- 'template',
119
- 'textarea',
120
- 'tfoot',
121
- 'th',
122
- 'thead',
123
- 'time',
124
- 'title',
125
- 'tr',
126
- 'track',
127
- 'u',
128
- 'ul',
129
- 'var',
130
- 'video',
131
- 'wbr',
132
- ])