@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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +44 -0
- package/package.json +1 -1
- package/src/abideResolverPlugin.ts +3 -17
- package/src/appEntry.ts +18 -8
- package/src/controlServerWorker.ts +8 -1
- package/src/lib/cli/parseArgvForRpc.ts +8 -3
- package/src/lib/mcp/createMcpServer.ts +8 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +20 -2
- package/src/lib/mcp/toolResultFromResponse.ts +5 -0
- package/src/lib/server/rpc/parseArgs.ts +12 -1
- package/src/lib/server/runtime/acceptsGzip.ts +10 -1
- package/src/lib/server/runtime/buildInFlightSnapshot.ts +35 -0
- package/src/lib/server/runtime/buildInspectorSurface.ts +9 -1
- package/src/lib/server/runtime/createServer.ts +10 -1
- package/src/lib/server/runtime/gzipResponse.ts +2 -1
- package/src/lib/server/runtime/inFlightRequests.ts +13 -0
- package/src/lib/server/runtime/maybeMountInspector.ts +7 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +7 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -1
- package/src/lib/server/runtime/streamFromIterator.ts +8 -0
- package/src/lib/server/runtime/types/InspectorContext.ts +4 -0
- package/src/lib/server/runtime/types/InspectorInFlightRequest.ts +20 -0
- package/src/lib/server/runtime/types/InspectorInFlightSnapshot.ts +10 -0
- package/src/lib/server/runtime/types/InspectorPrompt.ts +15 -0
- package/src/lib/server/runtime/types/InspectorSurface.ts +6 -3
- package/src/lib/server/sockets/createSocketDispatcher.ts +7 -1
- package/src/lib/server/sockets/defineSocket.ts +6 -1
- package/src/lib/shared/cache.ts +6 -0
- package/src/lib/shared/createPushIterator.ts +7 -2
- package/src/lib/shared/emitLogRecord.ts +18 -5
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/asOutlet.ts +29 -0
- package/src/lib/ui/compile/componentWrapperTag.ts +12 -20
- package/src/lib/ui/compile/generateBuild.ts +45 -36
- package/src/lib/ui/compile/generateSSR.ts +31 -21
- package/src/lib/ui/compile/parseTemplate.ts +10 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -2
- package/src/lib/ui/compile/skeletonContext.ts +97 -34
- package/src/lib/ui/compile/types/SkeletonContext.ts +6 -0
- package/src/lib/ui/createScope.ts +11 -0
- package/src/lib/ui/dom/appendSnippet.ts +60 -20
- package/src/lib/ui/dom/fillBefore.ts +10 -0
- package/src/lib/ui/dom/hydrate.ts +2 -1
- package/src/lib/ui/dom/mount.ts +2 -1
- package/src/lib/ui/dom/mountSlot.ts +7 -2
- package/src/lib/ui/dom/scopeLabel.ts +19 -0
- package/src/lib/ui/dom/skeleton.ts +16 -1
- package/src/lib/ui/installInspectorBridge.ts +138 -0
- package/src/lib/ui/navigate.ts +11 -3
- package/src/lib/ui/persist.ts +4 -1
- package/src/lib/ui/router.ts +77 -9
- package/src/lib/ui/runtime/createDoc.ts +20 -7
- package/src/lib/ui/runtime/historyEntries.ts +113 -0
- package/src/lib/ui/runtime/liveScopes.ts +15 -0
- package/src/lib/ui/runtime/types/AbideHistoryState.ts +9 -0
- package/src/lib/ui/seedStreamedResolution.ts +10 -1
- package/src/lib/ui/startClient.ts +6 -0
- package/src/lib/ui/types/Scope.ts +3 -0
- package/src/lib/ui/compile/HTML_TAGS.ts +0 -132
package/src/lib/ui/router.ts
CHANGED
|
@@ -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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
])
|