@abide/abide 0.123.2 → 0.124.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/ABIDE.md +8 -5
- package/CHANGELOG.md +58 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +45 -12
- package/docs/RULEBOOK.md +24 -14
- package/docs/abide.md +999 -459
- package/package.json +1 -1
- package/src/compiler/ARRAY_MUTATORS.ts +20 -0
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/LoweredFile.ts +609 -0
- package/src/compiler/Lowering.ts +18 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowHost.ts +670 -0
- package/src/compiler/ShadowMap.ts +1 -1
- package/src/compiler/TypeSession.ts +76 -1743
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/appLints.ts +109 -0
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +2 -2
- package/src/compiler/classifyRead.ts +16 -1
- package/src/compiler/compileApp.ts +102 -71
- package/src/compiler/declaredExports.ts +46 -21
- package/src/compiler/diagnoseSources.ts +232 -0
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +7 -8
- package/src/compiler/handlersUnder.ts +32 -63
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/reactiveReads.ts +235 -0
- package/src/compiler/reactiveTypes.ts +13 -0
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +181 -82
- package/src/compiler/routesDeclaration.ts +46 -0
- package/src/compiler/scaffold.ts +7 -2
- package/src/compiler/serveLanguage.ts +46 -74
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/splitImports.ts +2 -2
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/DesktopSession.ts +213 -0
- package/src/server/Followers.ts +399 -0
- package/src/server/MODULE_QUERIES.ts +6 -2
- package/src/server/PageRender.ts +201 -0
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/abideModules.ts +7 -7
- package/src/server/answerFailure.ts +3 -2
- package/src/server/answerRequest.ts +16 -369
- package/src/server/answerRpc.ts +48 -302
- package/src/server/answerSocket.ts +41 -490
- package/src/server/appBuild.ts +377 -0
- package/src/server/appKit.ts +1 -1
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +38 -66
- package/src/server/csp.ts +1 -1
- package/src/server/deferredBuilds.ts +3 -16
- package/src/server/devWorker.ts +11 -15
- package/src/server/faceOf.ts +1 -1
- package/src/server/generatedSurfaces.ts +2 -1
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +3 -3
- package/src/server/installServer.ts +5 -9
- package/src/server/invokeHandler.ts +230 -0
- package/src/server/mountHandlers.ts +184 -0
- package/src/server/openDesktopWindow.ts +308 -0
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +21 -2
- package/src/server/principal.ts +5 -4
- package/src/server/remote.ts +2 -2
- package/src/server/render.ts +31 -59
- package/src/server/requestScope.ts +5 -36
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +20 -448
- package/src/server/seedBuffer.ts +12 -12
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/shell.ts +1 -1
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +3 -2
- package/src/server/versionOf.ts +7 -16
- package/src/shared/RPC_SERVER.ts +27 -31
- package/src/shared/Schema.ts +1 -2
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/index.ts +1 -1
- package/src/shared/{note.ts → issueReport.ts} +22 -0
- package/src/shared/loadCall.ts +66 -0
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +24 -16
- package/src/shared/reactive/Reactive.ts +37 -991
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/byOf.ts +2 -5
- package/src/shared/reactive/callLoad.ts +8 -14
- package/src/shared/reactive/cellShape.ts +734 -0
- package/src/shared/reactive/index.ts +12 -23
- package/src/shared/reactive/pairReactive.ts +5 -3
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/producer.ts +121 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/scoped.ts +3 -2
- package/src/shared/reactive/selections.ts +209 -0
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/reactive/watch.ts +3 -4
- package/src/shared/rpc.ts +20 -24
- package/src/shared/validateJson.ts +1 -2
- package/src/shared/writePath.ts +29 -0
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +106 -313
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +23 -11
- package/src/ui/outcomeOf.ts +120 -0
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +28 -13
- package/src/ui/wire.ts +9 -10
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -26
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/LOAD_OFFLINE.ts +0 -15
- package/src/shared/LOAD_SIGNAL.ts +0 -10
- package/src/shared/issuesOf.ts +0 -18
- package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- package/src/shared/reactive/canonicalKey.ts +0 -95
package/src/ui/Router.ts
CHANGED
|
@@ -1,41 +1,25 @@
|
|
|
1
1
|
import { CURRENT_ROUTER, type NavigateOptions } from '#shared/CURRENT_ROUTER.ts'
|
|
2
2
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
3
3
|
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
4
|
-
import { firstMatch,
|
|
4
|
+
import { firstMatch, partsOf } from '#shared/matchRoute.ts'
|
|
5
5
|
import { unmounted } from '#shared/mount.ts'
|
|
6
6
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
7
7
|
import { OFFLINE_SAVED } from '#shared/OFFLINE_CACHES.ts'
|
|
8
8
|
import { REACHED_MESSAGE } from '#shared/REACHED_MESSAGE.ts'
|
|
9
|
-
import type { ShareScope } from '#shared/reactive/share.ts'
|
|
10
9
|
import { arrive, route } from '#shared/route.ts'
|
|
11
10
|
import { rankRoutes, type Segment } from '#shared/routePattern.ts'
|
|
12
|
-
import { setUpLevels } from '#shared/setUpLevels.ts'
|
|
13
11
|
import { paintedCallsLanded } from '#shared/takeEarlyCall.ts'
|
|
14
12
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { LAYER_NAVIGATE } from './LAYER_NAMES.ts'
|
|
14
|
+
import { LevelChain, type PageModule } from './LevelChain.ts'
|
|
17
15
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* is a caller of a component, which is why nothing about the emitted module changes to be routable.
|
|
26
|
-
*/
|
|
27
|
-
export type PageMount = (
|
|
28
|
-
root: Region,
|
|
29
|
-
props?: Record<string, unknown>,
|
|
30
|
-
// 39.6 — A LEVEL'S HEAD IS ITS OWN MOUNT'S TO APPLY, from inside the instance its slots read
|
|
31
|
-
// (`emitClient`), so the router hands the module a region and says it is the router by handing
|
|
32
|
-
// it the level it stands at — a file invoked as a component too is mounted without it, and
|
|
33
|
-
// applies no head a row. 39.14 — the level is where the mount leaves its head for the router
|
|
34
|
-
// to read when it withdraws.
|
|
35
|
-
routed?: Level,
|
|
36
|
-
) => Scope
|
|
37
|
-
|
|
38
|
-
type PageModule = { default: PageMount }
|
|
16
|
+
import {
|
|
17
|
+
type Bearing,
|
|
18
|
+
type Found,
|
|
19
|
+
outcomeOf,
|
|
20
|
+
type Settled,
|
|
21
|
+
} from './outcomeOf.ts'
|
|
22
|
+
import { pageRegion, type Region } from './regions.ts'
|
|
39
23
|
|
|
40
24
|
export type Route = {
|
|
41
25
|
/** The route literal, in the bracket spelling the page's directory is already written in. */
|
|
@@ -52,8 +36,6 @@ export type Route = {
|
|
|
52
36
|
|
|
53
37
|
type Compiled = Route & { segments: Segment[] }
|
|
54
38
|
|
|
55
|
-
type Found = { route: Compiled; params: Params }
|
|
56
|
-
|
|
57
39
|
/** A window's scroll, `[scrollX, scrollY]`. */
|
|
58
40
|
type Scroll = readonly [number, number]
|
|
59
41
|
|
|
@@ -82,32 +64,25 @@ const SUPERSEDED = 'superseded'
|
|
|
82
64
|
*
|
|
83
65
|
* It holds what it put on the page, so a navigation can take it back off. A page mounted into the
|
|
84
66
|
* region owns a `Scope` (12.13) and the router is the only thing that can remove its nodes, so the
|
|
85
|
-
* router is where that scope is stopped.
|
|
67
|
+
* router — through its `LevelChain` — is where that scope is stopped. The router keeps the address,
|
|
68
|
+
* the history and the scroll.
|
|
86
69
|
*/
|
|
87
70
|
export class Router {
|
|
88
|
-
region: Region
|
|
89
|
-
routes: Compiled[]
|
|
71
|
+
readonly region: Region
|
|
72
|
+
private readonly routes: Compiled[]
|
|
90
73
|
/** The chain standing in the document, outermost layout first and the page last. */
|
|
91
|
-
levels:
|
|
92
|
-
/**
|
|
93
|
-
* The chain the address resolved to.
|
|
94
|
-
*
|
|
95
|
-
* A kept layout's `<slot/>` mounts from it whenever its region opens — read at that moment
|
|
96
|
-
* rather than captured when the layout mounted, or a slot reopened after a navigation put back
|
|
97
|
-
* the page the reader had left.
|
|
98
|
-
*/
|
|
99
|
-
chain: PageModule[]
|
|
74
|
+
readonly levels: LevelChain
|
|
100
75
|
/**
|
|
101
76
|
* The path the standing chain was mounted for: a step to it that moves only the query or the
|
|
102
77
|
* fragment keeps the page (30.23, 30.24).
|
|
103
78
|
*/
|
|
104
|
-
mounted: string | undefined
|
|
79
|
+
private mounted: string | undefined
|
|
105
80
|
/**
|
|
106
81
|
* The route the standing page was matched by.
|
|
107
82
|
*
|
|
108
83
|
* 30.23 — an address it matches again keeps it.
|
|
109
84
|
*/
|
|
110
|
-
standing: Compiled | undefined
|
|
85
|
+
private standing: Compiled | undefined
|
|
111
86
|
/**
|
|
112
87
|
* The ticket of the live navigation; a superseded one stops when it sees its own aborted.
|
|
113
88
|
*
|
|
@@ -124,40 +99,39 @@ export class Router {
|
|
|
124
99
|
* 30.31 — AN `AbortController`, so superseding a navigation also aborts the `middleware` request
|
|
125
100
|
* it is still waiting on, rather than leaving it to finish for nobody.
|
|
126
101
|
*/
|
|
127
|
-
navigation: AbortController
|
|
102
|
+
private navigation: AbortController
|
|
128
103
|
/**
|
|
129
104
|
* 35.18 — whether the document is an error page's. Its markup is no page this router has, so
|
|
130
105
|
* nothing hydrates over it, and leaving it is a document load.
|
|
131
106
|
*/
|
|
132
|
-
errorPage: boolean
|
|
107
|
+
private readonly errorPage: boolean
|
|
133
108
|
/** The place in this document's history of the entry the address bar stands at (`Entry`). */
|
|
134
|
-
index: number
|
|
109
|
+
private index: number
|
|
135
110
|
/**
|
|
136
111
|
* 30.29 — the scroll each entry was left at, by its `index`.
|
|
137
112
|
*
|
|
138
113
|
* HELD HERE AS WELL AS IN `history.state`, because a back or forward step has already moved the
|
|
139
114
|
* bar when `popstate` arrives, so the entry it left can no longer be written to.
|
|
140
115
|
*/
|
|
141
|
-
scrolls: (Scroll | undefined)[]
|
|
116
|
+
private readonly scrolls: (Scroll | undefined)[]
|
|
142
117
|
/**
|
|
143
118
|
* The routes whose chunks `prefetch` has already asked for.
|
|
144
119
|
*
|
|
145
120
|
* `pointerover` bubbles, so a pointer crossing a link's children asks again at every edge, and
|
|
146
121
|
* after the first time the answer is the module cache's anyway.
|
|
147
122
|
*/
|
|
148
|
-
prefetched: Set<Compiled>
|
|
123
|
+
private readonly prefetched: Set<Compiled>
|
|
149
124
|
/**
|
|
150
125
|
* Whether a navigation's write is a view transition, as the build read it (39.7, 39.8).
|
|
151
126
|
*
|
|
152
127
|
* Read off the compiled `<style>` blocks. Nothing here looks at the document's stylesheets.
|
|
153
128
|
*/
|
|
154
|
-
transitions: boolean
|
|
129
|
+
private readonly transitions: boolean
|
|
155
130
|
|
|
156
131
|
constructor(region: Region, routes: Compiled[], transitions: boolean) {
|
|
157
132
|
this.region = region
|
|
158
133
|
this.routes = routes
|
|
159
|
-
this.levels =
|
|
160
|
-
this.chain = []
|
|
134
|
+
this.levels = new LevelChain(region)
|
|
161
135
|
this.mounted = undefined
|
|
162
136
|
this.standing = undefined
|
|
163
137
|
this.navigation = new AbortController()
|
|
@@ -181,7 +155,7 @@ export class Router {
|
|
|
181
155
|
* as the address below the app's root. Every way in — a click, an intent, back and forward, a
|
|
182
156
|
* hydration — is matched here.
|
|
183
157
|
*/
|
|
184
|
-
match(pathname: string): Found | undefined {
|
|
158
|
+
match(pathname: string): Found<Compiled> | undefined {
|
|
185
159
|
const below = unmounted(pathname)
|
|
186
160
|
const parts = below === undefined ? undefined : partsOf(below)
|
|
187
161
|
return parts === undefined ? undefined : firstMatch(this.routes, parts)
|
|
@@ -214,33 +188,42 @@ export class Router {
|
|
|
214
188
|
* fills itself from the template it carries. One module, both ways in — the `fillEmpty`
|
|
215
189
|
* the emitter already writes is the whole of the difference.
|
|
216
190
|
*/
|
|
217
|
-
|
|
191
|
+
go(url: URL | string, options: NavigateOptions = {}): Promise<void> {
|
|
192
|
+
return this.travel(url, options, PUSHED)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Arrives at the entry a back or forward step has already moved the address bar to.
|
|
197
|
+
*
|
|
198
|
+
* The page is mounted where the bar already points and nothing is pushed for it. 30.20 — a step
|
|
199
|
+
* within the standing page, back over an in-page `#anchor` jump, is `follow`'s: nothing asked,
|
|
200
|
+
* nothing remounted. 30.29 — the scroll is put back once the page it belongs to is mounted.
|
|
201
|
+
*/
|
|
202
|
+
popped(): Promise<void> {
|
|
203
|
+
return this.travel(
|
|
204
|
+
location.href,
|
|
205
|
+
{ replace: true, keepScroll: true },
|
|
206
|
+
this.stepped(),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `go`, arriving as `step` says: a push, or a history step with the scroll it restores. */
|
|
211
|
+
private async travel(
|
|
218
212
|
url: URL | string,
|
|
219
|
-
options: NavigateOptions
|
|
220
|
-
step: Step
|
|
213
|
+
options: NavigateOptions,
|
|
214
|
+
step: Step,
|
|
221
215
|
): Promise<void> {
|
|
222
|
-
const
|
|
223
|
-
typeof url === 'string' ? new URL(url, location.href) : url
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
const found = this.match(target.pathname)
|
|
231
|
-
if (found === undefined) {
|
|
232
|
-
// NOT A ROUTE THIS BUILD KNOWS, so it is the server's to answer — which is a document
|
|
233
|
-
// load, and the one case where 30.7 does not apply because there is no route to run.
|
|
234
|
-
location.href = target.href
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
// 30.21, 30.23, 30.24 — THE ROUTE STANDING, AT ITS OWN PATH: another query, another fragment or
|
|
238
|
-
// the same address. The page reads its address through `route`, so it follows rather than
|
|
239
|
-
// being rebuilt, and there is no path for `middleware` to decide anything new about.
|
|
240
|
-
if (found.route === this.standing && this.mounted === target.pathname) {
|
|
241
|
-
this.follow(target, options, found, step)
|
|
242
|
-
return
|
|
216
|
+
const bearing: Bearing<Compiled> = {
|
|
217
|
+
target: typeof url === 'string' ? new URL(url, location.href) : url,
|
|
218
|
+
origin: location.origin,
|
|
219
|
+
errorPage: this.errorPage,
|
|
220
|
+
standing: this.standing,
|
|
221
|
+
mounted: this.mounted,
|
|
222
|
+
match: (pathname) => this.match(pathname),
|
|
243
223
|
}
|
|
224
|
+
const before = outcomeOf(bearing)
|
|
225
|
+
if (before.kind !== 'ask') return this.settle(before, options, step)
|
|
226
|
+
const found = before.found
|
|
244
227
|
const ticket = this.supersede()
|
|
245
228
|
route.navigating.set(true)
|
|
246
229
|
// EVERY WAY OUT CLEARS WHAT THE WAY IN SET: a load that fails, a setup that throws and a
|
|
@@ -249,70 +232,61 @@ export class Router {
|
|
|
249
232
|
try {
|
|
250
233
|
// 30.15 — THE SERVER IS ASKED ALONGSIDE THE LOAD, so its `middleware` decides the
|
|
251
234
|
// navigation for the price of the one round trip the chunks already cost.
|
|
235
|
+
// 30.27 — `loadOrMiss`, because a rejection here was dropped by the click's `void`,
|
|
236
|
+
// and the link did nothing.
|
|
252
237
|
const [answer, loaded] = await Promise.all([
|
|
253
|
-
ask(target, ticket.signal),
|
|
238
|
+
ask(bearing.target, ticket.signal),
|
|
254
239
|
loadOrMiss(found.route),
|
|
255
240
|
])
|
|
256
241
|
// SUPERSEDED, so this navigation is over. Nothing has been touched yet, which is the
|
|
257
242
|
// whole reason the ticket is checked here rather than after the mount — and
|
|
258
243
|
// `navigating` stays the newer navigation's to clear.
|
|
259
244
|
if (ticket.signal.aborted) return
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
) {
|
|
245
|
+
const after = outcomeOf(bearing, {
|
|
246
|
+
found,
|
|
247
|
+
answer,
|
|
248
|
+
loaded: loaded !== undefined,
|
|
249
|
+
online: navigator.onLine,
|
|
250
|
+
pushed: step.direction === 'push',
|
|
251
|
+
})
|
|
252
|
+
if (after.kind === 'offline') {
|
|
268
253
|
// 45.12 — UNLESS THE SERVICE WORKER SAVED THE PAGE, which the document load is
|
|
269
254
|
// then answered with.
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// document asks again and paints whatever the server says. A fetch that failed —
|
|
276
|
-
// offline, or a redirect to another origin that CORS will not let it follow — goes to
|
|
277
|
-
// the address it was sent to.
|
|
278
|
-
// 30.27 — AND CHUNKS THAT WOULD NOT LOAD, with the server down or a deploy that removed
|
|
279
|
-
// them: a rejection here was dropped by the click's `void`, and the link did nothing.
|
|
280
|
-
if (answer?.status !== 204 || loaded === undefined) {
|
|
281
|
-
location.href = answer?.url ?? target.href
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
// 30.17 — REDIRECTED, and the server already said yes to where it landed: the answer is
|
|
285
|
-
// the 204 at the end of the chain. That address is mounted in place of the one asked
|
|
286
|
-
// for, and a route the client has no row for is the document's (30.10).
|
|
287
|
-
const landed = answer.redirected ? new URL(answer.url) : target
|
|
288
|
-
const landing = answer.redirected
|
|
289
|
-
? this.match(landed.pathname)
|
|
290
|
-
: found
|
|
291
|
-
if (landing === undefined) {
|
|
292
|
-
location.href = landed.href
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
// 30.23 — THE ROUTE STANDING, AT ANOTHER PATH: `middleware` has said yes to the path, and
|
|
296
|
-
// the page it matched is the one already mounted, so it follows.
|
|
297
|
-
if (landing.route === this.standing) {
|
|
298
|
-
this.follow(landed, options, landing, step)
|
|
255
|
+
if (
|
|
256
|
+
(await savedOffline(bearing.target)) &&
|
|
257
|
+
!ticket.signal.aborted
|
|
258
|
+
)
|
|
259
|
+
location.href = after.href
|
|
299
260
|
return
|
|
300
261
|
}
|
|
262
|
+
if (after.kind !== 'mount') return this.settle(after, options, step)
|
|
301
263
|
const arriving =
|
|
302
|
-
|
|
264
|
+
after.found === found
|
|
265
|
+
? loaded
|
|
266
|
+
: await loadOrMiss(after.found.route)
|
|
303
267
|
if (ticket.signal.aborted) return
|
|
304
268
|
if (arriving === undefined) {
|
|
305
|
-
location.href =
|
|
269
|
+
location.href = after.at.href
|
|
306
270
|
return
|
|
307
271
|
}
|
|
308
272
|
await this.write(ticket, step.direction, () =>
|
|
309
|
-
this.commit(
|
|
273
|
+
this.commit(after.at, after.found, arriving, options, step),
|
|
310
274
|
)
|
|
311
275
|
} finally {
|
|
312
276
|
if (this.navigation === ticket) route.navigating.set(false)
|
|
313
277
|
}
|
|
314
278
|
}
|
|
315
279
|
|
|
280
|
+
/** Executes an outcome that loads nothing: a document load, or a step within the page. */
|
|
281
|
+
private settle(
|
|
282
|
+
outcome: Settled<Compiled>,
|
|
283
|
+
options: NavigateOptions,
|
|
284
|
+
step: Step,
|
|
285
|
+
): void {
|
|
286
|
+
if (outcome.kind === 'document') location.href = outcome.href
|
|
287
|
+
else this.follow(outcome.at, options, outcome.found, step)
|
|
288
|
+
}
|
|
289
|
+
|
|
316
290
|
/**
|
|
317
291
|
* Moves the address within the standing page, leaving the page mounted.
|
|
318
292
|
*
|
|
@@ -327,7 +301,7 @@ export class Router {
|
|
|
327
301
|
follow(
|
|
328
302
|
target: URL,
|
|
329
303
|
options: NavigateOptions,
|
|
330
|
-
found: Found
|
|
304
|
+
found: Found<Compiled>,
|
|
331
305
|
step: Step,
|
|
332
306
|
): void {
|
|
333
307
|
this.supersede()
|
|
@@ -354,7 +328,7 @@ export class Router {
|
|
|
354
328
|
* browser pushed itself — a link to a fragment — so it is stamped as the next entry, and
|
|
355
329
|
* arrived at as a push.
|
|
356
330
|
*/
|
|
357
|
-
stepped(): Step {
|
|
331
|
+
private stepped(): Step {
|
|
358
332
|
this.scrolls[this.index] = [scrollX, scrollY]
|
|
359
333
|
const state = history.state as Partial<Entry> | null
|
|
360
334
|
if (typeof state?.index !== 'number') {
|
|
@@ -457,7 +431,7 @@ export class Router {
|
|
|
457
431
|
*/
|
|
458
432
|
private commit(
|
|
459
433
|
target: URL,
|
|
460
|
-
found: Found
|
|
434
|
+
found: Found<Compiled>,
|
|
461
435
|
arriving: PageModule[],
|
|
462
436
|
options: NavigateOptions,
|
|
463
437
|
step: Step,
|
|
@@ -468,37 +442,15 @@ export class Router {
|
|
|
468
442
|
// honest is which fraction of click -> paint the router's WORK is.
|
|
469
443
|
beginLayer(LAYER_NAVIGATE)
|
|
470
444
|
try {
|
|
471
|
-
// 41.11 — WHAT THE TWO ADDRESSES SHARE IS NOT REBUILT.
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// exports is the same function either way.
|
|
476
|
-
const shared = this.sharedLevels(arriving)
|
|
477
|
-
// RECONCILED BEFORE THE STOP, while the leaving levels' heads read what they read on
|
|
478
|
-
// screen: what the server painted for them is adopted as theirs, so it leaves with them.
|
|
479
|
-
Level.reconcile(this.levels)
|
|
445
|
+
// 41.11 — WHAT THE TWO ADDRESSES SHARE IS NOT REBUILT.
|
|
446
|
+
const kept = this.levels.sharedWith(arriving)
|
|
447
|
+
// Read before anything is written: read after a region was emptied, it forces a layout
|
|
448
|
+
// and reads the offset that layout clamped.
|
|
480
449
|
const left: Scroll = [scrollX, scrollY]
|
|
481
|
-
this.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// NOWHERE, where that slot's region is closed: the arriving part is mounted by the slot
|
|
486
|
-
// when it opens, off `chain`, and not into a region no longer in the document.
|
|
487
|
-
const into =
|
|
488
|
-
shared === 0
|
|
489
|
-
? this.region
|
|
490
|
-
: (this.levels[shared - 1] as Level).slot
|
|
491
|
-
// EMPTIED THROUGH THE PRIMITIVE EVERY OTHER REGION IS. `fillRegion(region)` with no
|
|
492
|
-
// build is this removal, and it carries the 41.10 refusal for a region no longer in a
|
|
493
|
-
// document that a hand-written walk swallows.
|
|
494
|
-
if (into !== undefined) fillRegion(into)
|
|
495
|
-
this.writeAddress(target, options.replace === true, left)
|
|
496
|
-
// 39.6 — each arriving level applies its own head as it mounts into its emptied region,
|
|
497
|
-
// outermost first; a kept layout is not mounted again, so neither is its head.
|
|
498
|
-
this.mountChain(target, found, arriving, shared, into)
|
|
499
|
-
// 39.14 — what the leaving levels keyed and the arriving ones did not goes back to the
|
|
500
|
-
// nearest kept layout's, else leaves; their keyless nodes leave with them (39.16).
|
|
501
|
-
Level.reconcile(this.levels)
|
|
450
|
+
this.levels.replace(kept, arriving, () => {
|
|
451
|
+
this.writeAddress(target, options.replace === true, left)
|
|
452
|
+
this.arriveAt(target, found)
|
|
453
|
+
})
|
|
502
454
|
// 30.29 — where the entry was left, on a history step; else the top of the page, or —
|
|
503
455
|
// as the browser does on a load — the fragment's element.
|
|
504
456
|
if (step.scroll !== undefined)
|
|
@@ -509,134 +461,6 @@ export class Router {
|
|
|
509
461
|
}
|
|
510
462
|
}
|
|
511
463
|
|
|
512
|
-
/**
|
|
513
|
-
* How many levels of the arriving chain are already standing.
|
|
514
|
-
*
|
|
515
|
-
* Layouts only: a page reached here is another route's, the same route's being followed instead
|
|
516
|
-
* (30.23).
|
|
517
|
-
*
|
|
518
|
-
* EVERY STANDING LEVEL IS COMPARED, the page included: a page is never a layout, so it cannot
|
|
519
|
-
* match one, and a layout whose slot is closed stands with nothing below it.
|
|
520
|
-
*/
|
|
521
|
-
private sharedLevels(arriving: PageModule[]): number {
|
|
522
|
-
const layouts = arriving.length - 1
|
|
523
|
-
let shared = 0
|
|
524
|
-
while (
|
|
525
|
-
shared < layouts &&
|
|
526
|
-
shared < this.levels.length &&
|
|
527
|
-
(this.chain[shared] as PageModule).default ===
|
|
528
|
-
(arriving[shared] as PageModule).default
|
|
529
|
-
)
|
|
530
|
-
shared += 1
|
|
531
|
-
return shared
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Stops every level from `at` down, inside out, and drops them.
|
|
536
|
-
*
|
|
537
|
-
* Stopped from the INSIDE OUT, so a layout's disposers run after the page's — the order a
|
|
538
|
-
* scope's own teardown already has, and the one an author reading two `watch` cleanups in
|
|
539
|
-
* two files would expect.
|
|
540
|
-
*/
|
|
541
|
-
private stopFrom(at: number): void {
|
|
542
|
-
for (let index = this.levels.length - 1; index >= at; index -= 1) {
|
|
543
|
-
const level = this.levels[index] as Level
|
|
544
|
-
// OUT OF THE CHAIN BEFORE IT STOPS, so a slot stop it fires on the way finds it gone
|
|
545
|
-
// (`release`) and leaves the head to whoever stopped it.
|
|
546
|
-
this.levels.length = index
|
|
547
|
-
// The level's head leaves whole, so none of its components withdraws its own.
|
|
548
|
-
level.contributed.clear()
|
|
549
|
-
level.scope?.stop()
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Mounts the chain from level `at` into `region`, each level handed the next as `children`.
|
|
555
|
-
*
|
|
556
|
-
* 35.1 — each level is handed the one below it as `children` (32.18), so a layout puts the page
|
|
557
|
-
* in its `<slot/>` with the same mechanism a caller writing markup between two tags uses — and
|
|
558
|
-
* the router learns where that slot is on the way past, which is what lets it swap the page
|
|
559
|
-
* there later without the layout knowing.
|
|
560
|
-
*
|
|
561
|
-
* THE CHILD'S SCOPE IS THE ROUTER'S, not the layout's, so a kept layout cannot stop the page
|
|
562
|
-
* arriving under it. What `mount` hands the layout is a stop for WHATEVER STANDS IN THE SLOT
|
|
563
|
-
* when the slot's region leaves — a `<slot/>` inside a closed `{#if}` — rather than for the
|
|
564
|
-
* page that happened to be mounted first. And the slot mounts from `chain`, so a slot that
|
|
565
|
-
* opens again mounts the page the address resolves to now.
|
|
566
|
-
*/
|
|
567
|
-
private mountFrom(at: number, region: Region): void {
|
|
568
|
-
setUpLevels(
|
|
569
|
-
at,
|
|
570
|
-
this.chain.length,
|
|
571
|
-
this.levels[at - 1]?.share,
|
|
572
|
-
ROUTE_PROPS,
|
|
573
|
-
(below, props) => this.setUp(below, region, props),
|
|
574
|
-
)
|
|
575
|
-
this.markUp(at, region)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Runs level `at`'s setup over `props`, leaving its markup on the level for `markUp`, and
|
|
580
|
-
* answers the share scope the setup left.
|
|
581
|
-
*/
|
|
582
|
-
private setUp(
|
|
583
|
-
at: number,
|
|
584
|
-
region: Region,
|
|
585
|
-
props: Record<string, (() => unknown) | undefined>,
|
|
586
|
-
): ShareScope | null {
|
|
587
|
-
const module = this.chain[at] as PageModule
|
|
588
|
-
const level = new Level(this.levels, at)
|
|
589
|
-
this.levels[at] = level
|
|
590
|
-
if (at < this.chain.length - 1)
|
|
591
|
-
props.children = () => ({
|
|
592
|
-
// NO BUILD, because the level below carries its own (32.17): a module a route
|
|
593
|
-
// reaches fills the region it is mounted against when that region is empty, so
|
|
594
|
-
// the build and the mount are one call rather than two.
|
|
595
|
-
build: undefined,
|
|
596
|
-
mount: (slot: Region): Stoppable => {
|
|
597
|
-
// The level below as `mountFrom` set it up, the first time its slot opens; a
|
|
598
|
-
// slot opening again sets it up afresh.
|
|
599
|
-
const pending = this.levels[at + 1]?.markup !== undefined
|
|
600
|
-
if (!pending) this.release(level, at)
|
|
601
|
-
level.slot = slot
|
|
602
|
-
if (pending) this.markUp(at + 1, slot)
|
|
603
|
-
else this.mountFrom(at + 1, slot)
|
|
604
|
-
return { stop: () => this.release(level, at) }
|
|
605
|
-
},
|
|
606
|
-
})
|
|
607
|
-
// The region a level's markup lands in is its slot above, known only once that markup
|
|
608
|
-
// runs; a module mounted by the router leaves its markup rather than using this one.
|
|
609
|
-
// A deferred level answers the scope its setup made, so the watches that setup opened are
|
|
610
|
-
// stopped with the level even where its markup never runs — a slot that never opens.
|
|
611
|
-
level.scope = level.mount(() => module.default(region, props, level))
|
|
612
|
-
return level.share
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/** Runs the markup level `at`'s setup left, into `region`. */
|
|
616
|
-
private markUp(at: number, region: Region): void {
|
|
617
|
-
const level = this.levels[at] as Level
|
|
618
|
-
const markup = level.markup
|
|
619
|
-
if (markup === undefined) return
|
|
620
|
-
level.markup = undefined
|
|
621
|
-
level.scope = level.mount(() => markup(region))
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Stops what stands below `level` and forgets its slot.
|
|
626
|
-
*
|
|
627
|
-
* A no-op once the level itself has been replaced, which is how a stop handed to a layout that
|
|
628
|
-
* has since left does nothing.
|
|
629
|
-
*/
|
|
630
|
-
private release(level: Level, at: number): void {
|
|
631
|
-
if (this.levels[at] !== level) return
|
|
632
|
-
level.slot = undefined
|
|
633
|
-
// 39.14, 39.16 — THE LEVELS BELOW LEAVE WITH THE SLOT'S REGION, and their head with them:
|
|
634
|
-
// reconciled first, so what the server painted for them is adopted as theirs.
|
|
635
|
-
Level.reconcile(this.levels)
|
|
636
|
-
this.stopFrom(at + 1)
|
|
637
|
-
Level.reconcile(this.levels)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
464
|
/**
|
|
641
465
|
* Hydrates the page the server painted, wiring it where it stands.
|
|
642
466
|
*
|
|
@@ -663,38 +487,22 @@ export class Router {
|
|
|
663
487
|
if (ticket.signal.aborted) return
|
|
664
488
|
// THE HYDRATE SHARE. Same name as the navigation's mount because it is the same work on the
|
|
665
489
|
// same chain — what differs is the op it is a fraction of.
|
|
666
|
-
this.
|
|
490
|
+
this.arriveAt(new URL(location.href), found)
|
|
491
|
+
this.levels.hydrate(standing)
|
|
667
492
|
// 30.30 — A RELOAD lands on an entry that carries the scroll `remember` wrote on the way out.
|
|
668
493
|
const scroll = (history.state as Partial<Entry> | null)?.scroll
|
|
669
494
|
if (scroll != null) scrollTo(scroll[0], scroll[1])
|
|
670
495
|
}
|
|
671
496
|
|
|
672
497
|
/**
|
|
673
|
-
* Records the arriving
|
|
498
|
+
* Records the arriving route as standing at `target`.
|
|
674
499
|
*
|
|
675
|
-
*
|
|
676
|
-
* `shares()` SUMS every measure of a name inside the op — so a pair inside the recursion would
|
|
677
|
-
* count the innermost level once per level above it. 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S
|
|
678
|
-
* SETUP, which reads it.
|
|
500
|
+
* 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S SETUP, which reads it.
|
|
679
501
|
*/
|
|
680
|
-
private
|
|
681
|
-
target: URL,
|
|
682
|
-
found: Found,
|
|
683
|
-
modules: PageModule[],
|
|
684
|
-
from: number,
|
|
685
|
-
into: Region | undefined,
|
|
686
|
-
): void {
|
|
502
|
+
private arriveAt(target: URL, found: Found<Compiled>): void {
|
|
687
503
|
arrive(target.href, found.route.pattern, found.params)
|
|
688
|
-
this.chain = modules
|
|
689
504
|
this.standing = found.route
|
|
690
505
|
this.mounted = target.pathname
|
|
691
|
-
if (into === undefined) return
|
|
692
|
-
beginLayer(LAYER_MOUNT)
|
|
693
|
-
try {
|
|
694
|
-
this.mountFrom(from, into)
|
|
695
|
-
} finally {
|
|
696
|
-
endLayer(LAYER_MOUNT)
|
|
697
|
-
}
|
|
698
506
|
}
|
|
699
507
|
}
|
|
700
508
|
|
|
@@ -764,21 +572,6 @@ async function ask(
|
|
|
764
572
|
}
|
|
765
573
|
}
|
|
766
574
|
|
|
767
|
-
/**
|
|
768
|
-
* The props a routed page or layout is set up over, `children` aside: `params` as a live accessor.
|
|
769
|
-
*
|
|
770
|
-
* 33.4 — A PROP IS AN ACCESSOR. A page is mounted as any other component is, so its one prop is
|
|
771
|
-
* passed the way `renderComponent` passes a child's and the way `answerRequest` paints one: a
|
|
772
|
-
* function per key. Passed as the plain record it lowered to `params()` on a value that was not
|
|
773
|
-
* one, so every page with a `[param]` in its address hydrated dead — with its server markup
|
|
774
|
-
* already on screen, which is what kept it quiet.
|
|
775
|
-
*
|
|
776
|
-
* 33.5 — AND A LIVE ONE: `params` reads `route.params`, which every navigation writes. A layout
|
|
777
|
-
* kept across `/users/1` -> `/users/2` is not mounted again, so an accessor over the params it was
|
|
778
|
-
* mounted with went on answering user 1.
|
|
779
|
-
*/
|
|
780
|
-
const ROUTE_PROPS = { params: (): Params => route.params() }
|
|
781
|
-
|
|
782
575
|
function entryAt(index: number, scroll: Scroll | null): Entry {
|
|
783
576
|
return { index, scroll }
|
|
784
577
|
}
|
package/src/ui/Scope.ts
CHANGED
|
@@ -11,8 +11,22 @@ type Boundary = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** The boundary a scope built now falls under, or `undefined` outside every `{#try}` body. */
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
let ambientBoundary: Boundary | undefined
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs `run` with every scope it builds falling under `boundary` (32.16), restoring the one around it.
|
|
18
|
+
*
|
|
19
|
+
* The ambient is this module's so `Scope`'s field reads a local at construction; `TryRegion` is the
|
|
20
|
+
* one writer, and a page with no `{#try}` does not import it.
|
|
21
|
+
*/
|
|
22
|
+
export function underBoundary<T>(boundary: Boundary, run: () => T): T {
|
|
23
|
+
const previous = ambientBoundary
|
|
24
|
+
ambientBoundary = boundary
|
|
25
|
+
try {
|
|
26
|
+
return run()
|
|
27
|
+
} finally {
|
|
28
|
+
ambientBoundary = previous
|
|
29
|
+
}
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/**
|
|
@@ -54,7 +68,7 @@ export class Scope {
|
|
|
54
68
|
*
|
|
55
69
|
* 32.16 — read once at construction.
|
|
56
70
|
*/
|
|
57
|
-
boundary: Boundary | undefined =
|
|
71
|
+
boundary: Boundary | undefined = ambientBoundary
|
|
58
72
|
/**
|
|
59
73
|
* 32.67 — where in `stops` a row's item watches start (`itemWatch`), and how many there are:
|
|
60
74
|
* opened one after another by a row's mount, so two numbers find them and the row allocates
|