@abide/abide 0.100.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/abide.ts +227 -163
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +10 -3
  7. package/scaffold/AGENTS.md +31 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -56
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +124 -39
  53. package/src/server/answerSocket.ts +141 -48
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +179 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +48 -14
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +144 -183
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +81 -39
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/GLOBAL_GETS.ts +9 -3
  107. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  108. package/src/shared/RPC_SERVER.ts +2 -2
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +1 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/shared/menuItem.ts +3 -3
  119. package/src/shared/reactive/Cursor.ts +3 -9
  120. package/src/shared/reactive/Keyed.ts +4 -3
  121. package/src/shared/reactive/NotLanded.ts +1 -1
  122. package/src/shared/reactive/Reactive.ts +81 -60
  123. package/src/shared/reactive/argsContract.ts +4 -4
  124. package/src/shared/reactive/canonicalKey.ts +4 -4
  125. package/src/shared/reactive/chunksOfStream.ts +6 -0
  126. package/src/shared/reactive/index.ts +1 -1
  127. package/src/shared/reactive/recordWrites.ts +103 -5
  128. package/src/shared/reactive/structural.ts +1 -1
  129. package/src/shared/reactive/tracking.ts +155 -21
  130. package/src/shared/reactive/watch.ts +10 -6
  131. package/src/shared/refuse.ts +1 -1
  132. package/src/shared/routePattern.ts +82 -46
  133. package/src/shared/rpc.ts +47 -13
  134. package/src/shared/rpcRequest.ts +27 -3
  135. package/src/shared/url.ts +4 -44
  136. package/src/shared/writePath.ts +1 -1
  137. package/src/ui/LAYER_NAMES.ts +1 -1
  138. package/src/ui/List.ts +3 -3
  139. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  140. package/src/ui/Mismatch.ts +1 -1
  141. package/src/ui/RawRun.ts +1 -1
  142. package/src/ui/Router.ts +38 -31
  143. package/src/ui/Scope.ts +1 -1
  144. package/src/ui/TryRegion.ts +2 -2
  145. package/src/ui/applyHead.ts +33 -0
  146. package/src/ui/index.ts +1 -1
  147. package/src/ui/menuBridge.ts +1 -1
  148. package/src/ui/moveNode.ts +1 -1
  149. package/src/ui/reconcile.ts +2 -2
  150. package/src/ui/reportMenu.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /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 directory a
2
- // page is filed under (`ui/pages/invoices/[id]/page.abide`) and the literal an author writes
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: no imports, because the compiler, the client router and a server router all read it and
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. Both halves are load-bearing: the file name is the address and
52
- * the directory is what says the address is a ROUTE rather than a component filed anywhere. One
53
- * test for every name (35.1 reads `layout.abide` the same way), so the directory half cannot be
54
- * got right for one and wrong for another — and one test for every reader: the build's routes,
55
- * the checker's `Routes` and the `route.params` a page is typed by.
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
- * 23.3 — under `ui/pages/` and nowhere else: a `page.abide` filed in some other `pages/`
58
- * directory is a component, as it is to the `Routes` the checker declares. `path` may be relative
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 (routesAt(path) === -1) return 'component'
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
- * The route literal a page file is filed under, read off its path below `ui/pages/`.
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
- * 23.3 — BELOW THE FIRST `ui/pages/`, which is the routes directory, and not below the last `pages/`:
77
- * a section an app files as `ui/pages/pages/` is the address `/pages`, and cutting at the last one
78
- * served `ui/pages/pages/add-a-page` at `/add-a-page` and gave `/` two pages.
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 routeOf(pagePath: string): string {
81
- const at = routesAt(pagePath)
82
- const tail = at === -1 ? pagePath : pagePath.slice(at + ROUTES.length)
83
- const parts: string[] = []
84
- for (const part of tail.split('/'))
85
- if (part !== '' && !part.endsWith('.abide')) parts.push(part)
86
- return `/${parts.join('/')}`
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
- /** Where the first `ui/pages/` in `path` begins, as whole directories, or -1. */
90
- function routesAt(path: string): number {
91
- if (path.startsWith(ROUTES)) return 0
92
- const at = path.indexOf(`/${ROUTES}`)
93
- return at === -1 ? -1 : at + 1
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
- * THE SHORTER PATTERN WINS A TIE, and only an exhausted prefix can produce one: every segment the
111
- * two share is the same kind, so what is left is the extra segments of the longer, which can only
112
- * be optional or rest or the two would not match one path. `/a` admits one address and
113
- * `/a/[[b]]` admits two, so the shorter is the narrower claim.
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 shared = a.length < b.length ? a.length : b.length
117
- for (let at = 0; at < shared; at += 1) {
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 a.length - b.length
140
+ return 0
123
141
  }
124
142
 
125
- function specificity(segment: Segment): number {
126
- if (segment.kind === 'static') return 3
127
- if (segment.kind === 'required') return 2
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 normalising — a plain function or a value becomes 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
- normalised,
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 { rpcRequest } from './rpcRequest.ts'
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 centre keys the value by them rather than by what the gate made of them.
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 centre of 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 centre waits only until the value settles or turns out to stream (9.6), and hands a
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 = normalised(contract, given)
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,
@@ -446,12 +475,17 @@ export function declare(method: Method): Declare {
446
475
  // value produced, and holds no way back to it. A keyed value is looked up without a reach,
447
476
  // so asking starts nothing (11.55).
448
477
  if (method === 'GET' && !isForwarder(value))
449
- GLOBAL_GETS.set(rpc, (args: unknown): unknown =>
478
+ GLOBAL_GETS.set(
479
+ rpc,
450
480
  keyed
451
- ? (value as { entries: Map<string, unknown> }).entries.get(
452
- keyOf(contract, args),
453
- )
454
- : value,
481
+ ? {
482
+ key: (args) => keyOf(contract, args),
483
+ at: (key) =>
484
+ (
485
+ value as { entries: Map<string, unknown> }
486
+ ).entries.get(key),
487
+ }
488
+ : { key: () => '', at: () => value },
455
489
  )
456
490
  return rpc
457
491
  }) as Declare
@@ -467,7 +501,7 @@ export const DELETE: Declare = declare('DELETE')
467
501
  function addressOf(rpc: { address: string | undefined }): string {
468
502
  if (rpc.address === undefined)
469
503
  throw new Error(
470
- 'abide: this handler has no address yet. A handler is given one where it is mounted, under src/server/rpc (16.2).',
504
+ 'abide: this handler has no address yet. A handler is given one where it is mounted, exported from a <script server> (16.2).',
471
505
  )
472
506
  return rpc.address
473
507
  }
@@ -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
- args: Record<string, unknown> | undefined,
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 = `${address}?${text}`
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
- let address = ''
90
- for (const segment of parseRoutePattern(pattern ?? '/')) {
91
- if (segment.kind === 'static') {
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
- }
@@ -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
- * optimised away. The copy is the whole point: a `Reactive` wakes its readers on IDENTITY, so
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.
@@ -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.9) and these
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 re-run with the same iterable is not a new
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 RE-RUN IS NOT A NEW SOURCE. The subject is read inside a watch, so any other reactive
213
- // the expression touches re-runs it with the SAME iterable — and emptying the list to pump an
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { namedLogger } from '#shared/log.ts'
2
2
 
3
- /** The browser lane's hydration logger (28.9). */
3
+ /** The browser lane's hydration logger (28.8). */
4
4
  export const HYDRATE_LOG = namedLogger('abide:hydrate')
5
5
 
6
6
  /**
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 serialisation, and not the
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 {