@abide/abide 0.101.0 → 0.102.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/CHANGELOG.md +16 -0
- package/bin/abide.ts +223 -164
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +5 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -59
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +102 -30
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +114 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +73 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the
|
|
2
|
-
// page is filed
|
|
1
|
+
// A ROUTE LITERAL, PARSED. One grammar for two things that would otherwise drift: the file a
|
|
2
|
+
// page is filed as (`invoices/[id]/page.abide`) and the literal an author writes
|
|
3
3
|
// (`'/invoices/[id]'`). The bracket spelling is the file system's, so the address is read off the
|
|
4
4
|
// tree rather than declared beside it and the two cannot disagree.
|
|
5
5
|
//
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// required takes a value, optional may be left out and omits its segment with it, and rest takes
|
|
8
8
|
// an array as well as a scalar.
|
|
9
9
|
//
|
|
10
|
-
// A LEAF:
|
|
11
|
-
// a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
10
|
+
// A LEAF: nothing imported but another leaf, because the compiler, the client router and a server
|
|
11
|
+
// router all read it and a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
12
|
+
|
|
13
|
+
import { PUBLIC_DIRECTORY, SERVER_DIRECTORY } from './RESERVED_DIRECTORIES.ts'
|
|
12
14
|
|
|
13
15
|
type SegmentKind = 'static' | 'required' | 'optional' | 'rest'
|
|
14
16
|
|
|
@@ -40,26 +42,23 @@ export function parseRoutePattern(pattern: string): Segment[] {
|
|
|
40
42
|
return out
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const ROUTES = 'ui/pages/'
|
|
44
|
-
|
|
45
45
|
/** Whether a source is a page, a layout, an error page or a component. */
|
|
46
46
|
export type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Whether the `.abide` at `path` is a page, a layout, an error page or a component.
|
|
50
50
|
*
|
|
51
|
-
* AN ADDRESS RATHER THAN A NAME.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* AN ADDRESS RATHER THAN A NAME. The file name says which of the three it is, and only a file
|
|
52
|
+
* whose place is an `address` (`placeOf`) is one: a `page.abide` under a `server` directory is a
|
|
53
|
+
* component.
|
|
54
|
+
* One test for every reader: the build's routes, the checker's `Routes` and the `route.params` a
|
|
55
|
+
* page is typed by.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* to the app or the project, or absolute.
|
|
57
|
+
* `path` is RELATIVE TO THE APP'S ROOT: its directories are its route, so an absolute path would
|
|
58
|
+
* read the machine's directories as the app's.
|
|
60
59
|
*/
|
|
61
60
|
export function fileKind(path: string): FileKind {
|
|
62
|
-
if (
|
|
61
|
+
if (placeOf(path) !== 'address') return 'component'
|
|
63
62
|
// The WHOLE basename: `homepage.abide` ends with `page.abide` and is a component.
|
|
64
63
|
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
65
64
|
if (name === 'page.abide') return 'page'
|
|
@@ -67,30 +66,50 @@ export function fileKind(path: string): FileKind {
|
|
|
67
66
|
return name === 'error.abide' ? 'error' : 'component'
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
/** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
|
|
70
|
+
export type Place = 'address' | 'server' | 'public' | 'dependency' | 'skipped'
|
|
71
|
+
|
|
70
72
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `page.abide` is an address rather than a name (CLAUDE.md), so the directories above it ARE the
|
|
74
|
-
* route and the file itself contributes nothing.
|
|
73
|
+
* Where the file at `path`, relative to the app's root, sits — THE ONE ANSWER every walk, fence and
|
|
74
|
+
* emit reads, so no two of them disagree about a path.
|
|
75
75
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* - `dependency` — 34.28: a package's, outside the root or under `node_modules`, whose directories
|
|
77
|
+
* are its package's layout and none of the app's;
|
|
78
|
+
* - `skipped` — under a dot directory, which no walk of the app enters;
|
|
79
|
+
* - `server` or `public` — 23.15: under that reserved directory, the nearest to the root deciding
|
|
80
|
+
* (23.16 refuses one inside the other);
|
|
81
|
+
* - `address` — 23.14: everywhere else, its directories the segments of its route.
|
|
79
82
|
*/
|
|
80
|
-
export function
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
for (
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
export function placeOf(path: string): Place {
|
|
84
|
+
if (path.startsWith('../') || path.startsWith('/')) return 'dependency'
|
|
85
|
+
const parts = path.split('/')
|
|
86
|
+
// The last part is the file itself, which is never a directory.
|
|
87
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
88
|
+
const part = parts[index] as string
|
|
89
|
+
if (part === 'node_modules') return 'dependency'
|
|
90
|
+
if (part.startsWith('.')) return 'skipped'
|
|
91
|
+
if (part === SERVER_DIRECTORY) return 'server'
|
|
92
|
+
if (part === PUBLIC_DIRECTORY) return 'public'
|
|
93
|
+
}
|
|
94
|
+
return 'address'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether a walk of the app passes over the entry `name`: a dot file or directory, or `node_modules`. */
|
|
98
|
+
export function skipped(name: string): boolean {
|
|
99
|
+
return name.startsWith('.') || name === 'node_modules'
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The route a file answers at — its DIRECTORY's below the root, whatever its name.
|
|
104
|
+
*
|
|
105
|
+
* 23.14 — `media/[id]/page.abide` is `/media/[id]`, and so is every other file beside it, which is
|
|
106
|
+
* what lets a directory's handlers sit in as many files as an author likes (16.2). `path` is
|
|
107
|
+
* relative to the app's root, as `fileKind`'s is.
|
|
108
|
+
*/
|
|
109
|
+
export function routeOf(path: string): string {
|
|
110
|
+
const parts = path.split('/')
|
|
111
|
+
parts.pop()
|
|
112
|
+
return `/${parts.filter((part) => part !== '').join('/')}`
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
/**
|
|
@@ -107,27 +126,44 @@ function routesAt(path: string): number {
|
|
|
107
126
|
* themselves, and `/x` ranked BELOW `/[[a]]/[[b]]` for the path `/x`: a static segment beaten by
|
|
108
127
|
* two optionals, which is 30.9 exactly inverted. Both were live on the scalar this replaced.
|
|
109
128
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
129
|
+
* WHERE ONE PATTERN ENDS, ITS END RANKS AS A SEGMENT: below a static or a required one and above an
|
|
130
|
+
* optional or a rest. `/a` admits one address and `/a/[[b]]` admits two, so the shorter is the
|
|
131
|
+
* narrower claim; but `/[[lang]]` and `/[[lang]]/about` both match `/about`, the longer by a static
|
|
132
|
+
* segment where the shorter spends `about` on an optional, so there the longer is.
|
|
114
133
|
*/
|
|
115
134
|
export function compareRoutes(a: Segment[], b: Segment[]): number {
|
|
116
|
-
const
|
|
117
|
-
for (let at = 0; at <
|
|
118
|
-
const kind =
|
|
119
|
-
specificity(b[at] as Segment) - specificity(a[at] as Segment)
|
|
135
|
+
const longest = a.length > b.length ? a.length : b.length
|
|
136
|
+
for (let at = 0; at < longest; at += 1) {
|
|
137
|
+
const kind = specificity(b[at]) - specificity(a[at])
|
|
120
138
|
if (kind !== 0) return kind
|
|
121
139
|
}
|
|
122
|
-
return
|
|
140
|
+
return 0
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
function specificity(segment: Segment): number {
|
|
126
|
-
if (segment
|
|
127
|
-
if (segment.kind === '
|
|
143
|
+
function specificity(segment: Segment | undefined): number {
|
|
144
|
+
if (segment === undefined) return 2
|
|
145
|
+
if (segment.kind === 'static') return 4
|
|
146
|
+
if (segment.kind === 'required') return 3
|
|
128
147
|
return segment.kind === 'optional' ? 1 : 0
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Whether `outer` is the front of `route`: a static segment by name, a parameter by kind.
|
|
152
|
+
*
|
|
153
|
+
* 35.1, 37.2 — what a layout wraps and what a route's hooks run over: the routes at or below its
|
|
154
|
+
* own, a parameter's name aside, so `/media/[id]` covers `/media/[slug]/edit`.
|
|
155
|
+
*/
|
|
156
|
+
export function coversRoute(outer: Segment[], route: Segment[]): boolean {
|
|
157
|
+
if (outer.length > route.length) return false
|
|
158
|
+
for (let at = 0; at < outer.length; at += 1) {
|
|
159
|
+
const one = outer[at] as Segment
|
|
160
|
+
const two = route[at] as Segment
|
|
161
|
+
if (one.kind !== two.kind) return false
|
|
162
|
+
if (one.kind === 'static' && one.name !== two.name) return false
|
|
163
|
+
}
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
/**
|
|
132
168
|
* A route table compiled for matching: each row with its pattern parsed, most specific first.
|
|
133
169
|
*
|
package/src/shared/rpc.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// A HANDLER IS A `reactive` OR BECOMES ONE (16.1), so an `Rpc` is not a second kind of value: the
|
|
7
7
|
// call hands back the handler's own `Reactive` (16.3), and every probe, trigger and failure on it is
|
|
8
8
|
// the reactive core's — the `Rpc`'s own `busy`, `ready`, `refresh` and `invalidate` are the
|
|
9
|
-
// handler's, passed through. What this file adds is the
|
|
9
|
+
// handler's, passed through. What this file adds is the normalizing — a plain function or a value becomes the
|
|
10
10
|
// `reactive` it would have been (16.6) — and the method.
|
|
11
11
|
//
|
|
12
12
|
// A HANDLER DECLARING `middleware` IS REACHED THROUGH ITS RUNGS (16.11), in process as over the wire:
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// handler's own.
|
|
15
15
|
|
|
16
16
|
import { type Failed, isFailedNamed } from './Failed.ts'
|
|
17
|
+
import { filledSegments } from './filledPath.ts'
|
|
17
18
|
import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
|
|
18
19
|
import { compose, type Middleware } from './Middleware.ts'
|
|
19
20
|
import { principalRotated } from './principalRotated.ts'
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
type Gated,
|
|
24
25
|
gateOf,
|
|
25
26
|
keyOf,
|
|
26
|
-
|
|
27
|
+
normalized,
|
|
27
28
|
} from './reactive/argsContract.ts'
|
|
28
29
|
import { callLoad } from './reactive/callLoad.ts'
|
|
29
30
|
import {
|
|
@@ -48,7 +49,8 @@ import { MUTATION } from './reactive/MUTATION.ts'
|
|
|
48
49
|
import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
|
|
49
50
|
import { isForwarder } from './reactive/scoped.ts'
|
|
50
51
|
import { checkArgs } from './refuse.ts'
|
|
51
|
-
import {
|
|
52
|
+
import { parseRoutePattern, type Segment } from './routePattern.ts'
|
|
53
|
+
import { pathRefusal, rpcRequest } from './rpcRequest.ts'
|
|
52
54
|
import type { Schema } from './Schema.ts'
|
|
53
55
|
|
|
54
56
|
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
@@ -131,7 +133,7 @@ export type RpcContext<Args = unknown> = { request: Request; args: () => Args }
|
|
|
131
133
|
* The context key holding the args as sent.
|
|
132
134
|
*
|
|
133
135
|
* 11.26 — THE ARGS AS SENT, carried on the context under a key no rung spells, so the reach at the
|
|
134
|
-
* onion's
|
|
136
|
+
* onion's center keys the value by them rather than by what the gate made of them.
|
|
135
137
|
*/
|
|
136
138
|
const SENT: unique symbol = Symbol('abide.sent')
|
|
137
139
|
|
|
@@ -331,10 +333,10 @@ export function declare(method: Method): Declare {
|
|
|
331
333
|
args: argsOnce(gateOf(contract), args),
|
|
332
334
|
[SENT]: args,
|
|
333
335
|
}) as RpcContext
|
|
334
|
-
// 16.32 BEHIND RUNGS — A STREAM'S CHUNKS ARE FORWARDED, not collected. The
|
|
336
|
+
// 16.32 BEHIND RUNGS — A STREAM'S CHUNKS ARE FORWARDED, not collected. The center of the
|
|
335
337
|
// onion is an async function, and a `Reactive` is thenable (1.4), so handing a streaming value
|
|
336
338
|
// out through it would wait out the whole run and answer the accumulation once it closed. So
|
|
337
|
-
// the
|
|
339
|
+
// the center waits only until the value settles or turns out to stream (9.6), and hands a
|
|
338
340
|
// stream on as its chunks — a generator, which is not thenable. The value the call reaches
|
|
339
341
|
// resolves to it and is a stream of its own from then (9.6), so a second call joining it
|
|
340
342
|
// (1.8) reads the same accumulation. A refusal a rung answers with is the value's refusal.
|
|
@@ -369,8 +371,35 @@ export function declare(method: Method): Declare {
|
|
|
369
371
|
// this handler and these args is then answered from what this one reached, rather than by
|
|
370
372
|
// running the handler again. In a browser nothing fills the slot, so the report is one load
|
|
371
373
|
// of a `null`.
|
|
374
|
+
//
|
|
375
|
+
// 15.8 — AND REFUSED AHEAD OF THE HANDLER where the args leave a parameter of its route no
|
|
376
|
+
// spelling (30.3, 30.19), as the wire refuses them: a render and the browser's call then
|
|
377
|
+
// reach the same refusal, and there is no request for the render to report. The route is
|
|
378
|
+
// parsed once per address, which is stamped after the declaration (16.10).
|
|
379
|
+
let routed: string | undefined
|
|
380
|
+
let routeSegments: Segment[] | null = null
|
|
372
381
|
const call = (given?: unknown, options?: CallOptions): unknown => {
|
|
373
|
-
const args =
|
|
382
|
+
const args = normalized(contract, given)
|
|
383
|
+
const address = rpc.address
|
|
384
|
+
if (address !== routed) {
|
|
385
|
+
routed = address
|
|
386
|
+
routeSegments =
|
|
387
|
+
address?.includes('[') === true
|
|
388
|
+
? parseRoutePattern(address)
|
|
389
|
+
: null
|
|
390
|
+
}
|
|
391
|
+
if (routeSegments !== null) {
|
|
392
|
+
try {
|
|
393
|
+
filledSegments(
|
|
394
|
+
routeSegments,
|
|
395
|
+
(args ?? {}) as Record<string, unknown>,
|
|
396
|
+
routed as string,
|
|
397
|
+
pathRefusal,
|
|
398
|
+
)
|
|
399
|
+
} catch (refused) {
|
|
400
|
+
return reactive(() => refused)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
374
403
|
const reached = callLoad(
|
|
375
404
|
reachThrough,
|
|
376
405
|
args,
|
|
@@ -472,7 +501,7 @@ export const DELETE: Declare = declare('DELETE')
|
|
|
472
501
|
function addressOf(rpc: { address: string | undefined }): string {
|
|
473
502
|
if (rpc.address === undefined)
|
|
474
503
|
throw new Error(
|
|
475
|
-
'abide: this handler has no address yet. A handler is given one where it is mounted,
|
|
504
|
+
'abide: this handler has no address yet. A handler is given one where it is mounted, exported from a <script server> (16.2).',
|
|
476
505
|
)
|
|
477
506
|
return rpc.address
|
|
478
507
|
}
|
package/src/shared/rpcRequest.ts
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
// that agreement: method, URL and body, which is what makes two requests the same one.
|
|
5
5
|
//
|
|
6
6
|
// A `GET` or `DELETE` carries its args in the URL, in `queryText`'s spelling, and a mutation in its
|
|
7
|
-
// body; 19.7 is where text becomes the declared type again.
|
|
7
|
+
// body; 19.7 is where text becomes the declared type again. 16.9 — whatever the method, a parameter
|
|
8
|
+
// of the handler's route rides in the path, and the server puts it back among the args (16.65).
|
|
8
9
|
|
|
10
|
+
import { filledPath, parametersOf } from './filledPath.ts'
|
|
9
11
|
import { queryText } from './queryText.ts'
|
|
12
|
+
import { validationError } from './refuse.ts'
|
|
10
13
|
import type { Method } from './rpc.ts'
|
|
11
14
|
|
|
12
15
|
export type RpcRequest = {
|
|
@@ -32,10 +35,26 @@ export function argsInURL(method: Method): boolean {
|
|
|
32
35
|
export function rpcRequest(
|
|
33
36
|
method: Method,
|
|
34
37
|
address: string,
|
|
35
|
-
|
|
38
|
+
given: Record<string, unknown> | undefined,
|
|
36
39
|
): RpcRequest {
|
|
40
|
+
let args = given
|
|
37
41
|
let url = address
|
|
38
42
|
let body: string | undefined
|
|
43
|
+
// A route with parameters: they are written into the path and leave the args, so the query or
|
|
44
|
+
// body never carries one twice (16.66). 15.8 — one no address carries (30.3, 30.19) is refused as
|
|
45
|
+
// args a gate refused, so every caller answers it as the wire answers a 422.
|
|
46
|
+
if (address.includes('[')) {
|
|
47
|
+
const params = given ?? {}
|
|
48
|
+
url = filledPath(address, params, address, pathRefusal)
|
|
49
|
+
const inPath = parametersOf(address)
|
|
50
|
+
let rest: Record<string, unknown> | undefined
|
|
51
|
+
for (const name in params) {
|
|
52
|
+
if (inPath.has(name)) continue
|
|
53
|
+
rest ??= {}
|
|
54
|
+
rest[name] = params[name]
|
|
55
|
+
}
|
|
56
|
+
args = rest
|
|
57
|
+
}
|
|
39
58
|
if (argsInURL(method)) {
|
|
40
59
|
if (args !== undefined) {
|
|
41
60
|
let text = queryText(args)
|
|
@@ -47,7 +66,7 @@ export function rpcRequest(
|
|
|
47
66
|
const bare = bareName(name)
|
|
48
67
|
text = text === '' ? bare : `${text}&${bare}`
|
|
49
68
|
}
|
|
50
|
-
if (text !== '') url = `${
|
|
69
|
+
if (text !== '') url = `${url}?${text}`
|
|
51
70
|
}
|
|
52
71
|
} else if (args !== undefined) body = JSON.stringify(args)
|
|
53
72
|
// 18.14 — A COPY IS ITS OWN CACHE and keeps its own `max-age`. What it asks the server is asked
|
|
@@ -67,6 +86,11 @@ export function rpcRequest(
|
|
|
67
86
|
return { method, url, body, init, key: `${method} ${url} ${body ?? ''}` }
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
/** 15.8 — a parameter no address carries, refused as args a gate refused. */
|
|
90
|
+
export function pathRefusal(name: string, message: string): unknown {
|
|
91
|
+
return validationError<Record<string, unknown>>({ [name]: [message] })
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
/** A query member's name in the encoding `URLSearchParams` gives it, without its `=`. */
|
|
71
95
|
export function bareName(name: string): string {
|
|
72
96
|
return new URLSearchParams([[name, '']]).toString().slice(0, -1)
|
package/src/shared/url.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// 16.10 — RESOLVED AGAINST THE MOUNT at the call, so the same build serves at the root and below it.
|
|
6
6
|
|
|
7
|
+
import { filledPath } from './filledPath.ts'
|
|
7
8
|
import { mounted } from './mount.ts'
|
|
8
9
|
import { queryText } from './queryText.ts'
|
|
9
|
-
import { parseRoutePattern } from './routePattern.ts'
|
|
10
10
|
|
|
11
11
|
type Parts<P extends string> = P extends `${infer Head}/${infer Tail}`
|
|
12
12
|
? Head | Parts<Tail>
|
|
@@ -86,29 +86,9 @@ export function url<P extends RouteLiteral>(
|
|
|
86
86
|
): string {
|
|
87
87
|
const params = (rest[0] ?? {}) as Record<string, unknown>
|
|
88
88
|
const query = rest[1]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
address += `/${segment.name}`
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
const value = params[segment.name]
|
|
96
|
-
if (value === undefined || value === null || value === '') {
|
|
97
|
-
// 30.4 — an optional left out omits its segment, and so does an empty rest (30.5).
|
|
98
|
-
if (segment.kind !== 'required') continue
|
|
99
|
-
throw new Error(
|
|
100
|
-
`abide: url('${pattern}') was given no '${segment.name}', and the route requires it. RULEBOOK 30.3.`,
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
if (segment.kind === 'rest' && Array.isArray(value)) {
|
|
104
|
-
for (const each of value)
|
|
105
|
-
address += `/${encodedSegment(pattern, segment.name, each)}`
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
108
|
-
address += `/${encodedSegment(pattern, segment.name, value)}`
|
|
109
|
-
}
|
|
110
|
-
if (address === '') address = '/'
|
|
111
|
-
address = mounted(address)
|
|
89
|
+
const address = mounted(
|
|
90
|
+
filledPath(pattern ?? '/', params, `url('${pattern}')`),
|
|
91
|
+
)
|
|
112
92
|
// 30.6 — the query half, last, in the spelling `rpcRequest` writes.
|
|
113
93
|
const search =
|
|
114
94
|
query === undefined
|
|
@@ -118,23 +98,3 @@ export function url<P extends RouteLiteral>(
|
|
|
118
98
|
: queryText(query)
|
|
119
99
|
return search === '' ? address : `${address}?${search}`
|
|
120
100
|
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* A param value encoded as one path segment; `.` and `..` are refused.
|
|
124
|
-
*
|
|
125
|
-
* 30.19 — A VALUE OF `.` OR `..` HAS NO SPELLING AS A SEGMENT: the URL parser collapses it as a
|
|
126
|
-
* dot-segment, and `%2E` / `%2E%2E` with it, so the address would land on another route. Refused
|
|
127
|
-
* rather than written, as a missing required segment is (30.3).
|
|
128
|
-
*/
|
|
129
|
-
function encodedSegment(
|
|
130
|
-
pattern: string | undefined,
|
|
131
|
-
name: string,
|
|
132
|
-
value: unknown,
|
|
133
|
-
): string {
|
|
134
|
-
const text = String(value)
|
|
135
|
-
if (text === '.' || text === '..')
|
|
136
|
-
throw new Error(
|
|
137
|
-
`abide: url('${pattern}') was given '${text}' for '${name}', and no address carries it as a segment — the URL parser resolves it to another route. RULEBOOK 30.19.`,
|
|
138
|
-
)
|
|
139
|
-
return encodeURIComponent(text)
|
|
140
|
-
}
|
package/src/shared/writePath.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { putOwn } from './putOwn.ts'
|
|
|
4
4
|
* A copy of `value` with `next` written at `path`, each level down the path copied.
|
|
5
5
|
*
|
|
6
6
|
* 31.8 — a write through a member path COPIES DOWN THE PATH AND THEN WRITES, and the copy is not
|
|
7
|
-
*
|
|
7
|
+
* optimized away. The copy is the whole point: a `Reactive` wakes its readers on IDENTITY, so
|
|
8
8
|
* `user.name = 'x'` done as a mutation of the held object writes the right value and wakes nobody
|
|
9
9
|
* — the page renders correctly on load and then never moves again, with nothing about it saying
|
|
10
10
|
* so.
|
package/src/ui/LAYER_NAMES.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// code that EMITS the measure to the spec that reads it, and a string typed twice is a
|
|
3
3
|
// `shares()` call that throws "found no measure named" against a layer that ran.
|
|
4
4
|
//
|
|
5
|
-
// `.` RATHER THAN `:`. `abide:hydrate` and `abide:navigate` are WARNING channels (28.
|
|
5
|
+
// `.` RATHER THAN `:`. `abide:hydrate` and `abide:navigate` are WARNING channels (28.8) and these
|
|
6
6
|
// are performance entries — two namespaces that would otherwise be one grep.
|
|
7
7
|
//
|
|
8
8
|
// ONE NAME PER OP SOMEBODY WAITS ON, never per row. `shares()` sums every measure of a layer
|
package/src/ui/List.ts
CHANGED
|
@@ -18,7 +18,7 @@ export class List {
|
|
|
18
18
|
region: Region
|
|
19
19
|
rows: Row[]
|
|
20
20
|
/**
|
|
21
|
-
* The async iterable last started, or null — so a
|
|
21
|
+
* The async iterable last started, or null — so a rerun with the same iterable is not a new
|
|
22
22
|
* source.
|
|
23
23
|
*/
|
|
24
24
|
pumping: unknown
|
|
@@ -209,8 +209,8 @@ function settleList(
|
|
|
209
209
|
* async subject writes no rows inline and the region arrives empty. The pump fills it.
|
|
210
210
|
*/
|
|
211
211
|
function appendRows(list: List, source: AsyncIterable<unknown>): void {
|
|
212
|
-
// A
|
|
213
|
-
// the expression touches
|
|
212
|
+
// A RERUN IS NOT A NEW SOURCE. The subject is read inside a watch, so any other reactive
|
|
213
|
+
// the expression touches reruns it with the SAME iterable — and emptying the list to pump an
|
|
214
214
|
// async generator that has already run to completion left the region blank for good.
|
|
215
215
|
if (list.pumping === source) return
|
|
216
216
|
// A NEW SOURCE IS A NEW LIST. What the last one appended is stopped and removed, or its
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// 39.1, 39.6 — THE ROUTER'S LEVEL BEING MOUNTED, set by the router around a page's or a layout's
|
|
2
|
+
// setup and markup: a component mounted inside one adds its `<head>` to that level's
|
|
3
|
+
// (`contributeHead`), and one mounted later — a row a list grew by — finds none and leaves the
|
|
4
|
+
// document's head alone. `unapplied` is the head the components built on the client contributed
|
|
5
|
+
// while it was set, which the router applies once it is unset rather than once a component.
|
|
6
|
+
export const MOUNTING_LEVEL: {
|
|
7
|
+
current:
|
|
8
|
+
| {
|
|
9
|
+
head: (() => string) | undefined
|
|
10
|
+
/** Every component head joined to the level's own, in the order they mounted. */
|
|
11
|
+
contributed: (() => string)[] | undefined
|
|
12
|
+
}
|
|
13
|
+
| undefined
|
|
14
|
+
unapplied: string
|
|
15
|
+
} = {
|
|
16
|
+
current: undefined,
|
|
17
|
+
unapplied: '',
|
|
18
|
+
}
|
package/src/ui/Mismatch.ts
CHANGED
package/src/ui/RawRun.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { removeUntil } from './regions.ts'
|
|
|
11
11
|
*
|
|
12
12
|
* `html` IS UNDEFINED FOR A RUN THE SERVER PAINTED. What is between the anchors is what the server
|
|
13
13
|
* parsed from the same value, so the first write keeps it (`setMarkup`) the way a hydrated arm keeps
|
|
14
|
-
* the arm it was painted with — reading markup back out of nodes is a
|
|
14
|
+
* the arm it was painted with — reading markup back out of nodes is a serialization, and not the
|
|
15
15
|
* string the server wrote.
|
|
16
16
|
*/
|
|
17
17
|
export class RawRun {
|
package/src/ui/Router.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { arrive, route } from '#shared/route.ts'
|
|
|
8
8
|
import { rankRoutes, type Segment } from '#shared/routePattern.ts'
|
|
9
9
|
import { setUpLevels } from '#shared/setUpLevels.ts'
|
|
10
10
|
import { paintedCallsLanded } from '#shared/takeEarlyCall.ts'
|
|
11
|
-
import { withdrawHead } from './applyHead.ts'
|
|
11
|
+
import { applyHead, withdrawHead } from './applyHead.ts'
|
|
12
12
|
import { LAYER_MOUNT, LAYER_NAVIGATE } from './LAYER_NAMES.ts'
|
|
13
|
+
import { MOUNTING_LEVEL } from './MOUNTING_LEVEL.ts'
|
|
13
14
|
import { beginLayer, endLayer } from './measureLayer.ts'
|
|
14
15
|
import { menuBridge } from './menuBridge.ts'
|
|
15
16
|
import { fillRegion, pageRegion, type Region } from './regions.ts'
|
|
@@ -73,6 +74,8 @@ type Level = {
|
|
|
73
74
|
* wrote and the first navigation away from it withdraws that head like any other.
|
|
74
75
|
*/
|
|
75
76
|
head: (() => string) | undefined
|
|
77
|
+
/** The heads of the components mounted in this level, joined to `head` (`contributeHead`). */
|
|
78
|
+
contributed: (() => string)[] | undefined
|
|
76
79
|
/**
|
|
77
80
|
* 41.19 — this level's markup, left by its mount once its setup has run, for the router to run
|
|
78
81
|
* once every level below has been set up too; `undefined` once it has run, or where the module
|
|
@@ -144,20 +147,8 @@ export class Router {
|
|
|
144
147
|
* Read off the compiled `<style>` blocks. Nothing here looks at the document's stylesheets.
|
|
145
148
|
*/
|
|
146
149
|
transitions: boolean
|
|
147
|
-
/**
|
|
148
|
-
* The shell's keyed head entries, as the build read them off the shell (`keyedHead`).
|
|
149
|
-
*
|
|
150
|
-
* 39.4, 39.14 — THE OUTERMOST CONTRIBUTOR, and the one the document cannot say: the server
|
|
151
|
-
* merged out every entry of it a level overrode.
|
|
152
|
-
*/
|
|
153
|
-
shellHead: string
|
|
154
150
|
|
|
155
|
-
constructor(
|
|
156
|
-
region: Region,
|
|
157
|
-
routes: Compiled[],
|
|
158
|
-
transitions: boolean,
|
|
159
|
-
shellHead: string,
|
|
160
|
-
) {
|
|
151
|
+
constructor(region: Region, routes: Compiled[], transitions: boolean) {
|
|
161
152
|
this.region = region
|
|
162
153
|
this.routes = routes
|
|
163
154
|
this.levels = []
|
|
@@ -167,7 +158,6 @@ export class Router {
|
|
|
167
158
|
this.navigation = 0
|
|
168
159
|
this.prefetched = new Set()
|
|
169
160
|
this.transitions = transitions
|
|
170
|
-
this.shellHead = shellHead
|
|
171
161
|
}
|
|
172
162
|
|
|
173
163
|
/**
|
|
@@ -186,7 +176,7 @@ export class Router {
|
|
|
186
176
|
/**
|
|
187
177
|
* Loads a route's chunks ahead of a navigation the reader has signaled.
|
|
188
178
|
*
|
|
189
|
-
* 30.14 — A NAVIGATION THE READER HAS
|
|
179
|
+
* 30.14 — A NAVIGATION THE READER HAS SIGNALED, LOADED BEFORE IT HAPPENS: the route's chunks
|
|
190
180
|
* are asked for on intent, so the click finds them in the module cache and setup — and with it
|
|
191
181
|
* every call the page makes — starts at once. Only the CODE: a call run on a hover would be a
|
|
192
182
|
* call nobody asked for, and for anything but a `GET` one with an effect. A failed load is left
|
|
@@ -385,11 +375,9 @@ export class Router {
|
|
|
385
375
|
// outermost first; a kept layout is not mounted again, so neither is its head.
|
|
386
376
|
this.mountChain(target, found, arriving, shared, into)
|
|
387
377
|
// 39.14 — what the leaving levels keyed and the arriving ones did not goes back to the
|
|
388
|
-
// nearest kept layout's, else
|
|
389
|
-
withdrawHead(
|
|
390
|
-
|
|
391
|
-
this.heads(shared, this.levels.length),
|
|
392
|
-
() => this.shellHead + this.heads(0, shared),
|
|
378
|
+
// nearest kept layout's, else leaves.
|
|
379
|
+
withdrawHead(leaving, this.heads(shared, this.levels.length), () =>
|
|
380
|
+
this.heads(0, shared),
|
|
393
381
|
)
|
|
394
382
|
// The top of the page, or — as the browser does on a load — the fragment's element.
|
|
395
383
|
if (options.keepScroll !== true) scrollToFragment(target.hash)
|
|
@@ -480,6 +468,7 @@ export class Router {
|
|
|
480
468
|
scope: undefined,
|
|
481
469
|
slot: undefined,
|
|
482
470
|
head: undefined,
|
|
471
|
+
contributed: undefined,
|
|
483
472
|
markup: undefined,
|
|
484
473
|
share: null,
|
|
485
474
|
}
|
|
@@ -505,7 +494,9 @@ export class Router {
|
|
|
505
494
|
// runs; a module mounted by the router leaves its markup rather than using this one.
|
|
506
495
|
// A deferred level answers the scope its setup made, so the watches that setup opened are
|
|
507
496
|
// stopped with the level even where its markup never runs — a slot that never opens.
|
|
508
|
-
level.scope =
|
|
497
|
+
level.scope = mounting(level, () =>
|
|
498
|
+
module.default(region, props, level),
|
|
499
|
+
)
|
|
509
500
|
return level.share
|
|
510
501
|
}
|
|
511
502
|
|
|
@@ -515,7 +506,7 @@ export class Router {
|
|
|
515
506
|
const markup = level.markup
|
|
516
507
|
if (markup === undefined) return
|
|
517
508
|
level.markup = undefined
|
|
518
|
-
level.scope = markup(region)
|
|
509
|
+
level.scope = mounting(level, () => markup(region))
|
|
519
510
|
}
|
|
520
511
|
|
|
521
512
|
/**
|
|
@@ -577,7 +568,7 @@ export class Router {
|
|
|
577
568
|
): void {
|
|
578
569
|
arrive(target.href, found.route.pattern, found.params)
|
|
579
570
|
this.chain = modules
|
|
580
|
-
// 38.
|
|
571
|
+
// 38.88 — inside a desktop app, the window shows the menus this chain exports.
|
|
581
572
|
if (menuBridge() !== undefined)
|
|
582
573
|
void import('./reportMenu.ts').then((loaded) =>
|
|
583
574
|
loaded.reportMenu(modules as { menu?: unknown }[]),
|
|
@@ -685,14 +676,30 @@ export function startRouter(
|
|
|
685
676
|
routes: Route[],
|
|
686
677
|
host: Element = document.body,
|
|
687
678
|
transitions = false,
|
|
688
|
-
shellHead = '',
|
|
689
679
|
): Router {
|
|
690
|
-
const router = new Router(
|
|
691
|
-
pageRegion(host),
|
|
692
|
-
rankRoutes(routes),
|
|
693
|
-
transitions,
|
|
694
|
-
shellHead,
|
|
695
|
-
)
|
|
680
|
+
const router = new Router(pageRegion(host), rankRoutes(routes), transitions)
|
|
696
681
|
CURRENT_ROUTER.current = router
|
|
697
682
|
return router
|
|
698
683
|
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* `run`, with `level` the one its components add their `<head>` to (39.1) — and what those built on
|
|
687
|
+
* the client contributed applied once it returns, rather than once a component.
|
|
688
|
+
*/
|
|
689
|
+
function mounting<Result>(level: Level, run: () => Result): Result {
|
|
690
|
+
const back = MOUNTING_LEVEL.current
|
|
691
|
+
const backUnapplied = MOUNTING_LEVEL.unapplied
|
|
692
|
+
MOUNTING_LEVEL.current = level
|
|
693
|
+
MOUNTING_LEVEL.unapplied = ''
|
|
694
|
+
let unapplied: string
|
|
695
|
+
let result: Result
|
|
696
|
+
try {
|
|
697
|
+
result = run()
|
|
698
|
+
} finally {
|
|
699
|
+
unapplied = MOUNTING_LEVEL.unapplied
|
|
700
|
+
MOUNTING_LEVEL.current = back
|
|
701
|
+
MOUNTING_LEVEL.unapplied = backUnapplied
|
|
702
|
+
}
|
|
703
|
+
applyHead(unapplied)
|
|
704
|
+
return result
|
|
705
|
+
}
|