@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +223 -164
  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 +5 -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 -59
  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 +102 -30
  53. package/src/server/answerSocket.ts +65 -20
  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 +4 -4
  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 +114 -181
  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 +73 -37
  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/RESERVED_DIRECTORIES.ts +15 -0
  107. package/src/shared/cookies.ts +2 -2
  108. package/src/shared/csp.ts +1 -1
  109. package/src/shared/escapeText.ts +1 -1
  110. package/src/shared/filledPath.ts +85 -0
  111. package/src/shared/framing.ts +79 -6
  112. package/src/shared/headKeyOf.ts +2 -0
  113. package/src/shared/index.ts +1 -1
  114. package/src/shared/log.ts +27 -13
  115. package/src/shared/matchRoute.ts +23 -4
  116. package/src/shared/menuItem.ts +3 -3
  117. package/src/shared/reactive/Keyed.ts +2 -2
  118. package/src/shared/reactive/NotLanded.ts +1 -1
  119. package/src/shared/reactive/Reactive.ts +43 -43
  120. package/src/shared/reactive/argsContract.ts +4 -4
  121. package/src/shared/reactive/canonicalKey.ts +4 -4
  122. package/src/shared/reactive/chunksOfStream.ts +6 -0
  123. package/src/shared/reactive/index.ts +1 -1
  124. package/src/shared/reactive/recordWrites.ts +103 -5
  125. package/src/shared/reactive/structural.ts +1 -1
  126. package/src/shared/reactive/tracking.ts +1 -1
  127. package/src/shared/reactive/watch.ts +3 -3
  128. package/src/shared/refuse.ts +1 -1
  129. package/src/shared/routePattern.ts +82 -46
  130. package/src/shared/rpc.ts +37 -8
  131. package/src/shared/rpcRequest.ts +27 -3
  132. package/src/shared/url.ts +4 -44
  133. package/src/shared/writePath.ts +1 -1
  134. package/src/ui/LAYER_NAMES.ts +1 -1
  135. package/src/ui/List.ts +3 -3
  136. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  137. package/src/ui/Mismatch.ts +1 -1
  138. package/src/ui/RawRun.ts +1 -1
  139. package/src/ui/Router.ts +38 -31
  140. package/src/ui/Scope.ts +1 -1
  141. package/src/ui/TryRegion.ts +2 -2
  142. package/src/ui/applyHead.ts +33 -0
  143. package/src/ui/index.ts +1 -1
  144. package/src/ui/menuBridge.ts +1 -1
  145. package/src/ui/moveNode.ts +1 -1
  146. package/src/ui/reconcile.ts +2 -2
  147. package/src/ui/reportMenu.ts +2 -2
  148. package/src/ui/routing.ts +2 -2
  149. package/src/ui/setAttribute.ts +1 -1
  150. package/src/ui/setText.ts +1 -1
  151. package/src/ui/socketWire.ts +9 -7
  152. package/src/ui/wire.ts +1 -1
  153. package/scaffold/src/server/app.ts +0 -6
  154. package/scaffold/src/shared/.gitkeep +0 -0
  155. package/scaffold/src/ui/app.html +0 -8
  156. package/src/server/componentImports.ts +0 -56
  157. /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
@@ -7,20 +7,19 @@
7
7
  /**
8
8
  * The namespace every framework address lives under.
9
9
  *
10
- * UNDER `/__abide`, which is the namespace the framework's own addresses live in (19.1, 20.5,
11
- * 25.1). That is what makes a route table and an asset table incapable of colliding: a page is a
12
- * directory under `pages/` and no directory there can be named `__abide` without an author
13
- * writing it, so nothing has to be checked at build time and nothing can be shadowed at run time.
14
- * 38.21 is the clause.
10
+ * UNDER `/__abide`, which is the namespace the framework's own addresses live in (20.5, 25.1).
11
+ * That is what keeps the app's routes and abide's own addresses from colliding: a route whose first
12
+ * segment is `__abide` is refused at build (23.17), so nothing can be shadowed at run time. 38.20
13
+ * is the clause.
15
14
  */
16
15
  export const ABIDE_ADDRESS = '/__abide'
17
16
 
18
17
  /**
19
18
  * Where the browser bundle is served.
20
19
  *
21
- * The bundle's own address below `ABIDE_ADDRESS` (38.17, 38.21). One request path answers
20
+ * The bundle's own address below `ABIDE_ADDRESS` (38.16, 38.20). One request path answers
22
21
  * everything under `ABIDE_ADDRESS` and the command supplying the app decides what is there, so a
23
- * command with a second address of its own — the reload client `abide dev` serves (38.18) — adds
22
+ * command with a second address of its own — the reload client `abide dev` serves (38.17) — adds
24
23
  * one rather than a lane.
25
24
  */
26
25
  export const CLIENT_ADDRESS = `${ABIDE_ADDRESS}/client`
@@ -31,8 +30,6 @@ export const CLIENT_ADDRESS = `${ABIDE_ADDRESS}/client`
31
30
  export const OPENAPI_ADDRESS = `${ABIDE_ADDRESS}/openapi.json`
32
31
  /** The MCP endpoint. */
33
32
  export const MCP_ADDRESS = `${ABIDE_ADDRESS}/mcp`
34
- /** The stream of log records. */
35
- export const LOGS_ADDRESS = `${ABIDE_ADDRESS}/logs`
36
33
  /** The health check. */
37
34
  export const HEALTH_ADDRESS = `${ABIDE_ADDRESS}/health`
38
35
  /** Where a visitor's Claude Code bridge pairs, and is relayed turns (45.13). */
@@ -41,19 +38,11 @@ export const BRIDGE_ADDRESS = `${ABIDE_ADDRESS}/bridge`
41
38
  /**
42
39
  * The socket the dev reload client holds open.
43
40
  *
44
- * 38.18 — the LISTENER upgrades it on the main thread and the dev WORKER serves the script that
41
+ * 38.17 — the LISTENER upgrades it on the main thread and the dev WORKER serves the script that
45
42
  * opens it.
46
43
  */
47
44
  export const DEV_ADDRESS = `${ABIDE_ADDRESS}/dev`
48
45
 
49
- /**
50
- * Where a `server/rpc` handler is answered over http.
51
- *
52
- * 16.2 — its path below that directory, then its export name (19.1). The build writes the address
53
- * into the browser's wrapper and the server answers at it.
54
- */
55
- export const RPC_ADDRESS = `${ABIDE_ADDRESS}/rpc`
56
-
57
46
  /**
58
47
  * Where the one web socket a page follows its `GET`s over is served, below the app's root.
59
48
  *
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The `<meta>` the server writes the app's name into, and a browser's logger reads it back from, so
3
+ * `log.named` prefixes the same name on both sides.
4
+ *
5
+ * 28.4.
6
+ */
7
+ export const APP_META = 'abide-app'
@@ -4,7 +4,7 @@
4
4
  * 18.4 — WHAT A `GET`'s VALUE DECLARES THAT ITS BROWSER COPY HAS TO BEHAVE BY, as the build reads
5
5
  * it off the declaration and writes it into the wrapper: data, because it crosses as text.
6
6
  * `structural` is named rather than carried, being abide's own comparator on both sides; an app's
7
- * own has no serialisation and stays the server's, as a function `by` or `tags` does.
7
+ * own has no serialization and stays the server's, as a function `by` or `tags` does.
8
8
  */
9
9
  export type CopyOptions = {
10
10
  by?: string
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The `<meta>` the server writes `DEBUG` into, and a browser reads it back from.
3
3
  *
4
- * 28.11.
4
+ * 28.10.
5
5
  */
6
6
  export const DEBUG_META = 'abide-debug'
@@ -0,0 +1,15 @@
1
+ // THE TWO DIRECTORY NAMES THAT ARE NEVER AN ADDRESS (23.15): `server` fences a graph (16.29) and
2
+ // `public` is answered as authored (36.11). Every other directory under the root is a segment of the
3
+ // address of the `page.abide`, `layout.abide` and `error.abide` in it (23.14).
4
+
5
+ /** A module under one only the server loads. */
6
+ export const SERVER_DIRECTORY = 'server'
7
+
8
+ /** A file under one is answered as authored, at the path above it. */
9
+ export const PUBLIC_DIRECTORY = 'public'
10
+
11
+ /** Both, as the walk of the app tests a directory against them. */
12
+ export const RESERVED_DIRECTORIES: ReadonlySet<string> = new Set([
13
+ SERVER_DIRECTORY,
14
+ PUBLIC_DIRECTORY,
15
+ ])
@@ -185,12 +185,12 @@ export function serializeCookie(init: CookieInit, inBrowser: boolean): string {
185
185
  // A page's own script cannot set an `HttpOnly` cookie, and a browser drops the whole line that
186
186
  // asks to — so the attribute is a server's alone.
187
187
  if (init.httpOnly === true && !inBrowser) line += '; HttpOnly'
188
- line += `; SameSite=${capitalised(init.sameSite ?? 'lax')}`
188
+ line += `; SameSite=${capitalized(init.sameSite ?? 'lax')}`
189
189
  if (init.partitioned === true) line += '; Partitioned'
190
190
  return line
191
191
  }
192
192
 
193
- function capitalised(sameSite: string): string {
193
+ function capitalized(sameSite: string): string {
194
194
  return sameSite.charAt(0).toUpperCase() + sameSite.slice(1)
195
195
  }
196
196
 
package/src/shared/csp.ts CHANGED
@@ -41,7 +41,7 @@ export function documentNonce(): string {
41
41
  * A middleware rung that sets the content security policy, and `csp.nonce()` for the current one.
42
42
  *
43
43
  * THE RUNG IS BUILT ON ITS FIRST REQUEST, because an app's `middleware` can be read before the
44
- * server half has filled `CSP`: `app.ts` is one more module, and nothing orders it after that one.
44
+ * server half has filled `CSP`: the root `layout.abide` is one more module, and nothing orders it after that one.
45
45
  */
46
46
  export const csp = Object.assign(
47
47
  (sources?: Record<string, string[]>): Rung => {
@@ -10,7 +10,7 @@ import { RAW_WRITES, Raw, textOf } from './Raw.ts'
10
10
  // the fallback: a clean string is returned as it arrived, with no allocation.
11
11
  //
12
12
  // THE DETECTION IS THE ENGINE'S, not a `charCodeAt` loop. The hand-written scan is one JS
13
- // iteration per character where this is vectorised, and it lost on the case it was written for:
13
+ // iteration per character where this is vectorized, and it lost on the case it was written for:
14
14
  // 5x on a clean 74-character slot and 14x on a clean 320-character one, over two runs that did
15
15
  // not swap places. The share is named — `escapeText` is 74-86% of a 500-row x 4-slot markup
16
16
  // build, and the whole build moves 1.7x.
@@ -0,0 +1,85 @@
1
+ // A ROUTE'S PATH WITH ITS PARAMETERS WRITTEN IN, for the two things that build an address from a
2
+ // route: `url`, from a page's literal (30.1), and a call, from its handler's route (16.9).
3
+
4
+ import { parseRoutePattern, type Segment } from './routePattern.ts'
5
+
6
+ /**
7
+ * `pattern` with each parameter `params` names written in as a path segment, or `/` for none.
8
+ *
9
+ * `spelled` is how the caller is named where a parameter is refused — `url('/x/[id]')`, or a
10
+ * handler's route — and `refused` what is thrown for it: an `Error` naming the clause, by default.
11
+ */
12
+ export function filledPath(
13
+ pattern: string,
14
+ params: Record<string, unknown>,
15
+ spelled: string,
16
+ refused: (name: string, message: string) => unknown = thrownError,
17
+ ): string {
18
+ return filledSegments(parseRoutePattern(pattern), params, spelled, refused)
19
+ }
20
+
21
+ /** `filledPath` over a route already parsed, for a caller that fills the same route per call. */
22
+ export function filledSegments(
23
+ segments: Segment[],
24
+ params: Record<string, unknown>,
25
+ spelled: string,
26
+ refused: (name: string, message: string) => unknown,
27
+ ): string {
28
+ let address = ''
29
+ for (const segment of segments) {
30
+ if (segment.kind === 'static') {
31
+ address += `/${segment.name}`
32
+ continue
33
+ }
34
+ const value = params[segment.name]
35
+ if (value === undefined || value === null || value === '') {
36
+ // 30.4 — an optional left out omits its segment, and so does an empty rest (30.5).
37
+ if (segment.kind !== 'required') continue
38
+ throw refused(
39
+ segment.name,
40
+ `abide: ${spelled} was given no '${segment.name}', and the route requires it. RULEBOOK 30.3.`,
41
+ )
42
+ }
43
+ if (segment.kind === 'rest' && Array.isArray(value)) {
44
+ for (const each of value)
45
+ address += `/${encodedSegment(spelled, segment.name, each, refused)}`
46
+ continue
47
+ }
48
+ address += `/${encodedSegment(spelled, segment.name, value, refused)}`
49
+ }
50
+ return address === '' ? '/' : address
51
+ }
52
+
53
+ /** The names of `pattern`'s parameters — what a call takes out of its args and into the path (16.66). */
54
+ export function parametersOf(pattern: string): Set<string> {
55
+ const names = new Set<string>()
56
+ for (const segment of parseRoutePattern(pattern))
57
+ if (segment.kind !== 'static') names.add(segment.name)
58
+ return names
59
+ }
60
+
61
+ /**
62
+ * A param value encoded as one path segment; `.` and `..` are refused.
63
+ *
64
+ * 30.19 — A VALUE OF `.` OR `..` HAS NO SPELLING AS A SEGMENT: the URL parser collapses it as a
65
+ * dot-segment, and `%2E` / `%2E%2E` with it, so the address would land on another route. Refused
66
+ * rather than written, as a missing required segment is (30.3).
67
+ */
68
+ function encodedSegment(
69
+ spelled: string,
70
+ name: string,
71
+ value: unknown,
72
+ refused: (name: string, message: string) => unknown,
73
+ ): string {
74
+ const text = String(value)
75
+ if (text === '.' || text === '..')
76
+ throw refused(
77
+ name,
78
+ `abide: ${spelled} was given '${text}' for '${name}', and no address carries it as a segment — the URL parser resolves it to another route. RULEBOOK 30.19.`,
79
+ )
80
+ return encodeURIComponent(text)
81
+ }
82
+
83
+ function thrownError(_name: string, message: string): Error {
84
+ return new Error(message)
85
+ }
@@ -1,6 +1,34 @@
1
1
  // 16.32 — A FRAMED ANSWER, read one JSON value per line: the whole line for jsonl, the `data:` line of
2
2
  // each event for sse. A line that does not carry the prefix — an sse `event:` line, a blank line — is
3
3
  // skipped. One line reader behind both a body read to the end and one read as it lands.
4
+ //
5
+ // 16.68 — AND ITS LAST FRAME MAY BE A REFUSAL: one that reached the stream after it had answered, so
6
+ // after its status was sent. An sse `event: refusal` and the Problem Details on its `data:` line, or a
7
+ // jsonl line `{"$refusal": <Problem Details>}` — read here as the refusal, thrown where it lands.
8
+
9
+ import { failedFrom, isProblem, type Problem } from './Failed.ts'
10
+
11
+ /** The key a jsonl line carries a refusal under (16.68). */
12
+ const REFUSAL_KEY = '$refusal'
13
+
14
+ /** A framed stream's last frame, as the server writes it for a refusal (16.68). */
15
+ export function refusalFrame(problem: Problem, prefix: string): string {
16
+ return prefix === ''
17
+ ? `${JSON.stringify({ [REFUSAL_KEY]: problem })}\n`
18
+ : `event: refusal\n${prefix}${JSON.stringify(problem)}\n\n`
19
+ }
20
+
21
+ // What a read of one framed body carries from one line to the next: whether an sse event named the
22
+ // data line after it a refusal.
23
+ type Reading = { refusal: boolean }
24
+
25
+ // A refusal's frame, read: thrown where it lands among the chunks.
26
+ class Refused {
27
+ problem: Problem
28
+ constructor(problem: Problem) {
29
+ this.problem = problem
30
+ }
31
+ }
4
32
 
5
33
  /**
6
34
  * What precedes a chunk's JSON on its line: nothing for jsonl, `data: ` for sse, and `null` where
@@ -67,14 +95,16 @@ async function* bytesStream(
67
95
  /** The chunks a framed text carried, every line of it complete. */
68
96
  export function framedChunks(text: string, prefix: string): unknown[] {
69
97
  const chunks: unknown[] = []
70
- readLines(text, prefix, true, chunks)
98
+ readLines(text, prefix, true, chunks, { refusal: false })
99
+ for (const chunk of chunks)
100
+ if (chunk instanceof Refused) throw failedFrom(chunk.problem)
71
101
  return chunks
72
102
  }
73
103
 
74
104
  /**
75
105
  * A framed body's chunks, yielded as each line completes.
76
106
  *
77
- * Cancelled when the reader stops reading — a refresh starts a new run and the old one's reader has
107
+ * Canceled when the reader stops reading — a refresh starts a new run and the old one's reader has
78
108
  * nothing left to hand anything.
79
109
  */
80
110
  export async function* framedStream(
@@ -84,6 +114,7 @@ export async function* framedStream(
84
114
  const reader = body.getReader()
85
115
  const decoder = new TextDecoder()
86
116
  const chunks: unknown[] = []
117
+ const reading: Reading = { refusal: false }
87
118
  let buffered = ''
88
119
  try {
89
120
  for (;;) {
@@ -97,8 +128,17 @@ export async function* framedStream(
97
128
  buffered += decoded
98
129
  continue
99
130
  }
100
- buffered = readLines(buffered + decoded, prefix, done, chunks)
101
- yield* chunks
131
+ buffered = readLines(
132
+ buffered + decoded,
133
+ prefix,
134
+ done,
135
+ chunks,
136
+ reading,
137
+ )
138
+ for (const chunk of chunks) {
139
+ if (chunk instanceof Refused) throw failedFrom(chunk.problem)
140
+ yield chunk
141
+ }
102
142
  chunks.length = 0
103
143
  if (done) return
104
144
  }
@@ -117,15 +157,48 @@ function readLines(
117
157
  prefix: string,
118
158
  ended: boolean,
119
159
  chunks: unknown[],
160
+ reading: Reading,
120
161
  ): string {
121
162
  let start = 0
122
163
  for (;;) {
123
164
  const newline = text.indexOf('\n', start)
124
165
  if (newline === -1 && !ended) return text.slice(start)
125
166
  const end = newline === -1 ? text.length : newline
126
- if (end > start && text.startsWith(prefix, start))
127
- chunks.push(JSON.parse(text.slice(start + prefix.length, end)))
167
+ if (prefix !== '' && text.startsWith('event: refusal', start))
168
+ reading.refusal = true
169
+ else if (end > start && text.startsWith(prefix, start)) {
170
+ const value: unknown = JSON.parse(
171
+ text.slice(start + prefix.length, end),
172
+ )
173
+ chunks.push(refusalIn(value, prefix, reading) ?? value)
174
+ reading.refusal = false
175
+ }
128
176
  if (newline === -1) return ''
129
177
  start = newline + 1
130
178
  }
131
179
  }
180
+
181
+ // The refusal a value carries as its frame: the data line an sse event named one, or a jsonl line
182
+ // holding the one reserved key and Problem Details under it.
183
+ function refusalIn(
184
+ value: unknown,
185
+ prefix: string,
186
+ reading: Reading,
187
+ ): Refused | undefined {
188
+ if (prefix !== '')
189
+ return reading.refusal && isProblem(value)
190
+ ? new Refused(value)
191
+ : undefined
192
+ // Per chunk: the reserved key is tested before any key list is built.
193
+ if (
194
+ value === null ||
195
+ typeof value !== 'object' ||
196
+ !Object.hasOwn(value, REFUSAL_KEY)
197
+ )
198
+ return undefined
199
+ const keys = Object.keys(value)
200
+ const problem = (value as Record<string, unknown>)[REFUSAL_KEY]
201
+ return keys.length === 1 && isProblem(problem)
202
+ ? new Refused(problem)
203
+ : undefined
204
+ }
@@ -20,6 +20,8 @@ export function headKeyOf(
20
20
  ): string | undefined {
21
21
  if (lowerTag === 'title' || lowerTag === 'base') return lowerTag
22
22
  if (lowerTag === 'meta') {
23
+ // One encoding a document, so a layout's replaces the default document's.
24
+ if (attribute('charset') != null) return 'meta charset'
23
25
  const named = attribute('name')
24
26
  const key = named === null ? attribute('property') : named
25
27
  return key == null ? undefined : `meta ${key}`
@@ -36,7 +36,7 @@ export { listOf } from './listOf.ts'
36
36
  export { type Logger, type LogRecord, log } from './log.ts'
37
37
  export type { Middleware } from './Middleware.ts'
38
38
  export type { Params } from './matchRoute.ts'
39
- // 38.89 — a desktop app's menu items, exported by the route's layouts and page.
39
+ // 38.88 — a desktop app's menu items, exported by the route's layouts and page.
40
40
  export {
41
41
  type MenuEntry,
42
42
  type MenuItem,
package/src/shared/log.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  // SECTION 28 — the logger, on both sides. A line is written to the console AND published to
2
- // `log.records` (28.1), so a subscriber — `abide logs`, a test, a page — sees exactly what the
2
+ // `log.records` (28.1), so a subscriber — a handler, a test, a page — sees exactly what the
3
3
  // terminal saw.
4
4
  //
5
5
  // THE RECORDS ARE BUILT ON FIRST ASK. The reactive core warns through this module, so a
6
6
  // `reactive` built at this module's evaluation would be a cycle evaluated in the wrong order;
7
7
  // built on the first line or the first read of `log.records`, the core has long since loaded.
8
8
  //
9
- // 28.11, 28.12 — IN A BROWSER, `DEBUG` IS THE DOCUMENT'S: the server names it in a `<meta>` it writes
9
+ // 28.10, 28.11 — IN A BROWSER, `DEBUG` IS THE DOCUMENT'S: the server names it in a `<meta>` it writes
10
10
  // into the head, and the browser's own stored `debug` answers only where the document carries none.
11
11
 
12
+ import { APP_META } from './APP_META.ts'
12
13
  import { DEBUG_META } from './DEBUG_META.ts'
13
14
  import { paint } from './paint.ts'
14
15
  import { type Reactive, reactive } from './reactive/index.ts'
@@ -53,7 +54,7 @@ function recordsValue(): Reactive<LogRecord[]> {
53
54
  Number.isInteger(configured) && configured >= 0
54
55
  ? configured
55
56
  : DEFAULT_BUFFER
56
- // 28.1 — one array for the process, which `/__abide/logs` follows (28.2).
57
+ // 28.1 — one array for the process.
57
58
  records = reactive<LogRecord[]>([], { global: true })
58
59
  }
59
60
  return records
@@ -62,7 +63,7 @@ function recordsValue(): Reactive<LogRecord[]> {
62
63
  /**
63
64
  * Appends `record` to `log.records`, trimming to the buffer it keeps.
64
65
  *
65
- * THE TRIM IS AMORTISED: grown to twice the buffer and cut back in one move, rather than a `shift`
66
+ * THE TRIM IS AMORTIZED: grown to twice the buffer and cut back in one move, rather than a `shift`
66
67
  * per line that moves every record still held.
67
68
  */
68
69
  function keep(list: LogRecord[], record: LogRecord): void {
@@ -81,14 +82,26 @@ function keep(list: LogRecord[], record: LogRecord): void {
81
82
  */
82
83
  let documentDebugRead: string | undefined
83
84
 
85
+ let documentAppRead: string | undefined
86
+
84
87
  function environment(name: string): string | undefined {
85
88
  if (typeof process !== 'undefined') return process.env[name]
89
+ // 28.4 — the app's name, as the server wrote it into the document.
90
+ if (name === 'APP_NAME') {
91
+ documentAppRead ??=
92
+ (typeof document === 'undefined'
93
+ ? undefined
94
+ : document
95
+ .querySelector(`meta[name="${APP_META}"]`)
96
+ ?.getAttribute('content')) ?? ''
97
+ return documentAppRead === '' ? undefined : documentAppRead
98
+ }
86
99
  if (name !== 'DEBUG') return undefined
87
100
  documentDebugRead ??= documentDebug() ?? ''
88
101
  return documentDebugRead
89
102
  }
90
103
 
91
- /** The document's `DEBUG`, or the browser's own stored one where it carries none (28.11, 28.12). */
104
+ /** The document's `DEBUG`, or the browser's own stored one where it carries none (28.10, 28.11). */
92
105
  export function documentDebug(): string | undefined {
93
106
  if (typeof document === 'undefined') return undefined
94
107
  const named = document
@@ -106,7 +119,7 @@ export function documentDebug(): string | undefined {
106
119
  /**
107
120
  * The compiled `DEBUG` patterns: namespaces included, and namespaces excluded.
108
121
  *
109
- * 28.10 — THE debug-npm GRAMMAR: a list split on commas or whitespace, `*` any run of characters,
122
+ * 28.9 — THE debug-npm GRAMMAR: a list split on commas or whitespace, `*` any run of characters,
110
123
  * and a leading `-` an exclusion that beats every inclusion. Compiled once per distinct `DEBUG`.
111
124
  */
112
125
  type Gate = { include: RegExp[]; exclude: RegExp[] }
@@ -131,7 +144,7 @@ function currentGate(): Gate {
131
144
  return gate
132
145
  }
133
146
 
134
- /** Whether `DEBUG` enables a namespace's gated lines (28.7). */
147
+ /** Whether `DEBUG` enables a namespace's gated lines (28.6). */
135
148
  function isEnabled(namespace: string): boolean {
136
149
  const { include, exclude } = currentGate()
137
150
  for (const pattern of exclude) if (pattern.test(namespace)) return false
@@ -215,12 +228,12 @@ function write(
215
228
  recordsValue().patch((list) => keep(list, record))
216
229
  }
217
230
 
218
- /** A logger under `namespace`, which 28.5 builds from the app's name and 28.9 from `abide:`. */
231
+ /** A logger under `namespace`, which 28.4 builds from the app's name and 28.8 from `abide:`. */
219
232
  export function namedLogger(namespace: string): Logger {
220
233
  return loggerOver(() => namespace)
221
234
  }
222
235
 
223
- /** Both lanes' render logger (28.9): the server's escalations and 32.20's dropped URLs. */
236
+ /** Both lanes' render logger (28.8): the server's escalations and 32.20's dropped URLs. */
224
237
  export const RENDER_LOG = namedLogger('abide:render')
225
238
 
226
239
  /**
@@ -229,7 +242,7 @@ export const RENDER_LOG = namedLogger('abide:render')
229
242
  * The namespace is ASKED per line rather than held, which is all `log` needs over a named logger.
230
243
  */
231
244
  function loggerOver(namespaceOf: () => string): Logger {
232
- // 28.3 — the bare call always writes. 28.4 — so do warnings and errors.
245
+ // 28.2 — the bare call always writes. 28.3 — so do warnings and errors.
233
246
  const logger = ((...args: unknown[]): void => {
234
247
  write(namespaceOf(), 'info', args)
235
248
  }) as Logger
@@ -243,7 +256,7 @@ function loggerOver(namespaceOf: () => string): Logger {
243
256
  }
244
257
  logger.warning = (...args) => write(namespaceOf(), 'warning', args)
245
258
  logger.error = (...args) => write(namespaceOf(), 'error', args)
246
- // 28.5 — another segment, below this one. 28.13 — asked per line too, so a logger named at module
259
+ // 28.4 — another segment, below this one. 28.12 — asked per line too, so a logger named at module
247
260
  // load, before the app's name resolved, writes under the name once it has.
248
261
  logger.named = (name) => loggerOver(() => `${namespaceOf()}:${name}`)
249
262
  logger.enabled = () => isEnabled(namespaceOf())
@@ -254,14 +267,15 @@ function loggerOver(namespaceOf: () => string): Logger {
254
267
  /**
255
268
  * The slot the derived app name is read through, filled by the server's `config`.
256
269
  *
257
- * 28.5 — the app's name is the root every `log.named` hangs off: the environment's, or what 27.3
270
+ * 28.4 — the app's name is the root every `log.named` hangs off: the environment's, or what 27.3
258
271
  * derived, which the server's `config` fills in here.
259
272
  */
260
273
  export const APP_NAME: { derived: () => string | undefined } = {
261
274
  derived: () => undefined,
262
275
  }
263
276
 
264
- function appName(): string {
277
+ /** The name every `log.named` hangs off: the environment's, or the one the app derived (28.4). */
278
+ export function appName(): string {
265
279
  return environment('APP_NAME') ?? APP_NAME.derived() ?? 'app'
266
280
  }
267
281
 
@@ -55,8 +55,18 @@ function walk(
55
55
  if (index === segments.length)
56
56
  return prefix || at === parts.length ? {} : null
57
57
  const segment = segments[index] as Segment
58
- // 30.5 — A REST TAKES WHAT IS LEFT, including nothing, and it is last by construction.
59
- if (segment.kind === 'rest') return { [segment.name]: parts.slice(at) }
58
+ // 30.5 — A REST TAKES THE MOST THE SEGMENTS AFTER IT LEAVE, nothing included, so a handler's
59
+ // name after one (`/docs/[...path]/save`) is reached rather than spent on the rest.
60
+ if (segment.kind === 'rest') {
61
+ for (let end = parts.length; end >= at; end -= 1) {
62
+ const params = walk(segments, index + 1, parts, end, prefix)
63
+ if (params !== null) {
64
+ params[segment.name] = parts.slice(at, end)
65
+ return params
66
+ }
67
+ }
68
+ return null
69
+ }
60
70
  const part = parts[at]
61
71
  if (segment.kind === 'optional') {
62
72
  if (part !== undefined) {
@@ -80,8 +90,9 @@ function walk(
80
90
  * The first route whose pattern matches `parts`, with its params, or `undefined`.
81
91
  *
82
92
  * 30.9 — THE FIRST PATTERN THAT MATCHES WINS, over a table already ranked most-specific-first.
83
- * BOTH SEAMS WALK THIS ONE. The server resolving an address and the client router resolving the
84
- * same one is the isomorphism 30.8 rests on.
93
+ * The client router resolves through this, and the server walks the same ranked table with
94
+ * `matchOf`, passing a route with no answer for the method asked. The two resolving one address
95
+ * alike is the isomorphism 30.8 rests on.
85
96
  *
86
97
  * Walked rather than compiled to a regex: a route table is matched once per navigation, so the
87
98
  * walk is never the cost, and a regex would have to encode three segment kinds in a grammar nobody
@@ -97,3 +108,11 @@ export function firstMatch<T extends { segments: Segment[] }>(
97
108
  }
98
109
  return undefined
99
110
  }
111
+
112
+ /** The params `segments` match the whole of `parts` with, or `undefined` where they do not. */
113
+ export function matchOf(
114
+ segments: Segment[],
115
+ parts: string[],
116
+ ): Params | undefined {
117
+ return walk(segments, 0, parts, 0, false) ?? undefined
118
+ }
@@ -1,8 +1,8 @@
1
- // 38.89 — THE DESKTOP APP'S MENU BAR IS THE ROUTE'S: a layout or a page exports `menu` from its
1
+ // 38.88 — THE DESKTOP APP'S MENU BAR IS THE ROUTE'S: a layout or a page exports `menu` from its
2
2
  // `<script module>`, and the window shows what the route it stands on exports. An item made by
3
3
  // `menuItem` is one a page answers; the rest are an address or a role, which need no page.
4
4
 
5
- /** What a menu item does natively, where an address or a page's own answer could not. 38.90. */
5
+ /** What a menu item does natively, where an address or a page's own answer could not. 38.89. */
6
6
  export type MenuRole =
7
7
  | 'about'
8
8
  | 'hide'
@@ -37,7 +37,7 @@ export type MenuItem =
37
37
  items?: MenuItem[]
38
38
  }
39
39
 
40
- /** An item a page answers: each function `on` filed runs when it is picked. 38.92. */
40
+ /** An item a page answers: each function `on` filed runs when it is picked. 38.91. */
41
41
  export class MenuEntry {
42
42
  label: string
43
43
  key: string | undefined
@@ -12,7 +12,7 @@ import type { Schema } from '../Schema.ts'
12
12
  import {
13
13
  ARGS_GATE,
14
14
  type Gated,
15
- normalised,
15
+ normalized,
16
16
  PARAMETER_DEFAULTS,
17
17
  } from './argsContract.ts'
18
18
  import { CELL } from './CELL.ts'
@@ -352,7 +352,7 @@ export function createKeyed<
352
352
  }
353
353
 
354
354
  function reach(given: Args): Reactive<Stored, Accepted, Failures> {
355
- const args = normalised(self, given)
355
+ const args = normalized(self, given)
356
356
  const key = canonicalKey(args)
357
357
  const standing = entries.get(key)
358
358
  // 1.8: reaching an instance that exists joins the standing work — 15.19, unless that work
@@ -2,7 +2,7 @@
2
2
  * What a read throws where nothing has landed.
3
3
  *
4
4
  * RULEBOOK 2.2. It carries the value it was thrown FOR, and that is the whole mechanism behind the
5
- * abort: a tracked body that meets one subscribes to `source` and re-runs when it lands, so the
5
+ * abort: a tracked body that meets one subscribes to `source` and reruns when it lands, so the
6
6
  * body never computes against an absent value and never takes the wrong branch. The
7
7
  * privileged-call hazard is what that buys — a body run to completion against an unlanded read
8
8
  * issues the else arm's call for a caller nothing has authenticated.