@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
@@ -1,5 +1,5 @@
1
1
  // HOW AN APP ANSWERS, given what it has. Two commands supply the parts — `abide start` off what
2
- // the build wrote (38.6) and `abide dev` out of memory (38.17) — and the request path itself is
2
+ // the build wrote (38.6) and `abide dev` out of memory (38.16) — and the request path itself is
3
3
  // this one, because a dev server that resolves an address differently from the served one is a
4
4
  // bug a reader meets in production and never in development.
5
5
  //
@@ -11,18 +11,19 @@ import {
11
11
  BRIDGE_ADDRESS,
12
12
  CLIENT_ADDRESS,
13
13
  HEALTH_ADDRESS,
14
- LOGS_ADDRESS,
15
14
  MCP_ADDRESS,
16
15
  OPENAPI_ADDRESS,
17
- RPC_ADDRESS,
18
16
  } from '#shared/ADDRESSES.ts'
17
+ import { APP_META } from '#shared/APP_META.ts'
19
18
  import { DEBUG_META } from '#shared/DEBUG_META.ts'
20
19
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
21
20
  import { escapeAttribute } from '#shared/escapeText.ts'
22
- import { isFailed } from '#shared/Failed.ts'
21
+ import { type Failed, isFailed } from '#shared/Failed.ts'
23
22
  import { health } from '#shared/health.ts'
23
+ import { appName } from '#shared/log.ts'
24
+ import { compose } from '#shared/Middleware.ts'
24
25
  import { MOUNT_META } from '#shared/MOUNT_META.ts'
25
- import { firstMatch, partsOf } from '#shared/matchRoute.ts'
26
+ import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
26
27
  import { MOUNT, mounted } from '#shared/mount.ts'
27
28
  import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
28
29
  import { refuse, notFound as refusedNotFound } from '#shared/refuse.ts'
@@ -32,13 +33,13 @@ import {
32
33
  rankRoutes,
33
34
  type Segment,
34
35
  } from '#shared/routePattern.ts'
35
- import type { Rpc } from '#shared/rpc.ts'
36
+ import type { Method, Rpc } from '#shared/rpc.ts'
36
37
  import { rpcRequest } from '#shared/rpcRequest.ts'
37
- import { answerFailure } from './answerFailure.ts'
38
+ import { answered, answerFailure } from './answerFailure.ts'
38
39
  import {
39
40
  answerRpc,
40
- type Mount,
41
41
  type Mounted,
42
+ type MountRow,
42
43
  mountedOwn,
43
44
  mountHandlers,
44
45
  } from './answerRpc.ts'
@@ -48,24 +49,32 @@ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
48
49
  import { callerOrMint } from './callerOf.ts'
49
50
  import { compressAnswer } from './compressAnswer.ts'
50
51
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
51
- import { answerMcp, LOGS_HANDLER, openApi } from './generatedSurfaces.ts'
52
- import { failureOf } from './hooks.ts'
52
+ import { answerMcp, openApi } from './generatedSurfaces.ts'
53
+ import { type AppRung, failureOf, rungsOver } from './hooks.ts'
53
54
  import { chunksOf, type Manifest } from './manifestOf.ts'
54
55
  import { muxAdmission } from './muxUpgrade.ts'
55
56
  import { plainAnswer } from './plainAnswer.ts'
57
+ import { prefers } from './prefers.ts'
56
58
  import { PRINCIPAL_MOUNT } from './principal.ts'
57
- import { boundPage, type Component, headAnchor, render } from './render.ts'
59
+ import {
60
+ boundPage,
61
+ type Component,
62
+ DEFAULT_DOCUMENT,
63
+ intoHead,
64
+ renderDocument,
65
+ } from './render.ts'
58
66
  import {
59
67
  type AnsweringApp,
60
68
  adoptRequest,
61
69
  beginRender,
62
70
  currentRequestScope,
71
+ type RequestScope,
63
72
  within,
64
73
  writtenStatus,
65
74
  } from './requestScope.ts'
66
75
  import { seedFor } from './seedBuffer.ts'
67
76
 
68
- // One row of the table the compiler writes to `<out>/server.ts`. The loader is a dynamic import
77
+ // One row of the server's table the compiler writes (`SERVER_TABLE`). The loader is a dynamic import
69
78
  // so a page's markup is off the boot path until an address asks for it, which is the same reason
70
79
  // the client table's loaders are (30.11).
71
80
  type ServerRoute = {
@@ -73,7 +82,7 @@ type ServerRoute = {
73
82
  load: () => Promise<{ default: Component }>
74
83
  // 35.1 — the layouts this page renders inside, outermost first, as the compiler found them.
75
84
  layouts: (() => Promise<{ default: Component }>)[]
76
- // 38.29 — the modules a browser loads at this address, the page's then its layouts', as keys
85
+ // 38.28 — the modules a browser loads at this address, the page's then its layouts', as keys
77
86
  // into the manifest `manifestOf` reads.
78
87
  clientModules: string[]
79
88
  }
@@ -85,29 +94,52 @@ type RankedError = ServerError & { segments: Segment[] }
85
94
 
86
95
  type Ranked = RankedError & {
87
96
  // The document this route is painted into, with the tags that load the app already in its
88
- // head (38.23, 38.24) and the preloads for this route's own modules (38.29). One per route and
97
+ // head (38.22, 38.23) and the preloads for this route's own modules (38.28). One per route and
89
98
  // built at boot, so a request pays for none of it.
90
99
  document: string
91
100
  }
92
101
 
102
+ /**
103
+ * One route of the app: the page at it, and the handler answering each method there.
104
+ *
105
+ * 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.21's
106
+ * "the most specific route answering the method" is a walk down the rows asking each column.
107
+ */
108
+ type RouteRow = {
109
+ pattern: string
110
+ segments: Segment[]
111
+ page: Ranked | undefined
112
+ handlers: Map<Method, Mounted>
113
+ /**
114
+ * The page and each handler, keyed by itself, inside the rungs its file's route filed (37.2).
115
+ *
116
+ * 16.14 — composed once with the table. Every server block is imported before it is built
117
+ * (37.1), so these are the rungs the boot found, as the root's are.
118
+ */
119
+ answers: Map<
120
+ Ranked | Mounted,
121
+ (ctx: { request: Request }) => Promise<Response>
122
+ >
123
+ }
124
+
93
125
  /** A public file, read once at boot: where the bytes are, the etag off them, and its type. */
94
126
  type PublicFile = { path: string; etag: string; type: string }
95
127
 
96
128
  /** Everything `answerRequest` answers from: the ranked routes, the lanes and the handlers. */
97
129
  export type AppSources = {
98
- /** The routes in 30.9's order, which `appSources` is the only way to get. */
99
- routes: Ranked[]
130
+ /** The routes, pages and handlers both, in 30.9's order, which `appSources` is the only way to get. */
131
+ routes: RouteRow[]
100
132
  /**
101
133
  * Everything under `/__abide`: the bundle, and `abide dev`'s reload client beside it.
102
134
  *
103
- * The bundle is 38.21's and the reload client 38.18's. One lane rather than an asset table
135
+ * The bundle is 38.20's and the reload client 38.17's. One lane rather than an asset table
104
136
  * plus an exception, so what a command answers there is a fact about the command.
105
137
  */
106
138
  framework: (pathname: string, request: Request) => Response | undefined
107
139
  /**
108
140
  * A file's address below `public`, to the file itself.
109
141
  *
110
- * 38.19 and 38.20 — from the built output for `abide start` and from the source tree for
142
+ * 38.18 and 38.19 — from the built output for `abide start` and from the source tree for
111
143
  * `abide dev`.
112
144
  */
113
145
  published: Map<string, PublicFile>
@@ -115,14 +147,8 @@ export type AppSources = {
115
147
  errors: RankedError[]
116
148
  /** The document the error pages are painted into. */
117
149
  errorDocument: string
118
- /** Every handler's address, to what answers it (`mountHandlers`). 16.2. */
150
+ /** Every handler, keyed by its method and address (`handlerKey`), to what answers it. 16.2. */
119
151
  handlers: Map<string, Mounted>
120
- /**
121
- * The app's own route, asked where nothing of abide's answers.
122
- *
123
- * 37.2 — `bootApp` fills it from `src/server/app.ts`'s `default`.
124
- */
125
- own: ((request: Request) => Promise<Response | undefined>) | undefined
126
152
  /**
127
153
  * What a page that threw, unrefused, is answered with in place of 16.41's bare 500.
128
154
  *
@@ -132,9 +158,6 @@ export type AppSources = {
132
158
  explained: ((thrown: unknown) => Response) | undefined
133
159
  }
134
160
 
135
- /** What a command reads or compiles, before `bootApp` adds what the `App` itself supplies. */
136
- export type LoadedSources = Omit<AppSources, 'own'>
137
-
138
161
  /** The `Response` to one request: a handler, a surface of abide's own, a file, or a page. */
139
162
  export async function answerRequest(
140
163
  app: AppSources,
@@ -145,15 +168,16 @@ export async function answerRequest(
145
168
  adoptRequest(request)
146
169
  const url = new URL(request.url)
147
170
  const asked = url.pathname
171
+ const method = request.method === 'HEAD' ? 'GET' : request.method
148
172
 
149
- if (asked.startsWith(`${RPC_ADDRESS}/`))
150
- return answerRpc(app.handlers, url, request, app)
151
173
  // 25.1, 25.3, 26.17 — the principal and the account of the app are handlers abide mounts,
152
174
  // answered as the app's are.
153
- if (OWN_HANDLERS.has(asked))
154
- return answerRpc(OWN_HANDLERS, url, request, app)
155
- // 20.5, 20.9, 36.7 — a surface of abide's own, or a 404 where its `ABIDE_*` has not
156
- // opened it.
175
+ const own = OWN_HANDLERS.get(asked)
176
+ if (own !== undefined)
177
+ return own.method === method
178
+ ? answerRpc(own, {}, url, request, app)
179
+ : methodRefused(app, asked, [own.method])
180
+ // 20.5, 20.9, 36.7 — a surface of abide's own, or a 404 where it answers nothing.
157
181
  const surface = SURFACES.get(asked)
158
182
  if (surface !== undefined) {
159
183
  const answer = await surface(app, request)
@@ -175,7 +199,7 @@ export async function answerRequest(
175
199
  if (asked.startsWith(`${ABIDE_ADDRESS}/`)) {
176
200
  const answer = app.framework(asked, request)
177
201
  if (answer === undefined) return notFound()
178
- return readsOnly(request) ? answer : methodRefused(app, asked)
202
+ return readsOnly(request) ? answer : methodRefused(app, asked, READS)
179
203
  }
180
204
 
181
205
  // 36.11 — A PUBLIC FILE IS ITS PATH BELOW THE DIRECTORY, so `/favicon.ico` is answered
@@ -187,7 +211,7 @@ export async function answerRequest(
187
211
  if (file !== undefined)
188
212
  return readsOnly(request)
189
213
  ? publicResponse(request, file)
190
- : methodRefused(app, asked)
214
+ : methodRefused(app, asked, READS)
191
215
 
192
216
  // 15.16 — an address that will not percent-decode names nothing, and is the caller's fault.
193
217
  const parts = partsOf(asked)
@@ -198,20 +222,79 @@ export async function answerRequest(
198
222
  refuse(400, 'The address is not a valid path.'),
199
223
  )
200
224
 
201
- const found = firstMatch(app.routes, parts)
202
- // 38.22. The client router leaves an unmatched address to the document (30.10), so this
203
- // is where that address arrives — and where the app's own route is asked before the 404.
204
- if (found === undefined)
205
- return (
206
- (await app.own?.(request)) ??
207
- (await answerFailure(
208
- app,
209
- asked,
210
- refusedNotFound({ path: asked, method: request.method }),
211
- ))
225
+ // 19.21 — THE MOST SPECIFIC ROUTE ANSWERING THE METHOD, an `OPTIONS` asking about the method
226
+ // its preflight names (16.24). A route that matches and answers some other method is held, so
227
+ // an address nothing answers the method at is a 405 naming what is answered there (15.17).
228
+ const preflight = request.method === 'OPTIONS'
229
+ // `null` for an `OPTIONS` that names no method, which is no preflight (Fetch's CORS-preflight
230
+ // request carries one) and so no handler's: the route answers 15.17's 405 with what it allows.
231
+ const asking = preflight
232
+ ? request.headers.get('access-control-request-method')
233
+ : method
234
+ let allowed: Set<string> | undefined
235
+ let found:
236
+ | { route: RouteRow; params: Params; handler: Mounted | undefined }
237
+ | undefined
238
+ for (const route of app.routes) {
239
+ const params = matchOf(route.segments, parts)
240
+ if (params === undefined) continue
241
+ const handler =
242
+ asking === null ? undefined : route.handlers.get(asking as Method)
243
+ if (
244
+ handler !== undefined ||
245
+ (!preflight && asking === 'GET' && route.page !== undefined)
246
+ ) {
247
+ found = { route, params, handler }
248
+ break
249
+ }
250
+ allowed ??= new Set()
251
+ if (route.page !== undefined) allowed.add('GET').add('HEAD')
252
+ for (const method of route.handlers.keys()) {
253
+ allowed.add(method)
254
+ if (method === 'GET') allowed.add('HEAD')
255
+ }
256
+ }
257
+ if (found === undefined) {
258
+ if (allowed !== undefined)
259
+ return methodRefused(app, asked, [...allowed], request)
260
+ // 38.21. The client router leaves an unmatched address to the document (30.10), so this
261
+ // is where that address arrives. An app answering one itself does it in a root rung (37.3),
262
+ // which ran before this.
263
+ return laneFailure(
264
+ app,
265
+ asked,
266
+ refusedNotFound({ path: asked, method: request.method }),
267
+ request,
212
268
  )
269
+ }
270
+ // 37.2, 37.5 — WHAT THE FILE'S ROUTE FILED: the request runs inside every rung filed at it or
271
+ // above it, the root's having run already, and an error in it reaches that route's `onError`.
272
+ const scope = currentRequestScope() as RequestScope
273
+ scope.route =
274
+ found.handler === undefined ? found.route.pattern : found.handler.route
275
+ scope.params = found.params
276
+ const answer = found.route.answers.get(
277
+ found.handler ?? (found.route.page as Ranked),
278
+ ) as (ctx: { request: Request }) => Promise<Response>
279
+ return answer({ request })
280
+ }
213
281
 
214
- if (!readsOnly(request)) return methodRefused(app, asked)
282
+ /**
283
+ * The answer of `found`, a page or a handler, once its middleware has admitted `request`, with the
284
+ * params routing put on the request's scope.
285
+ */
286
+ async function answerMatched(
287
+ app: AppSources,
288
+ request: Request,
289
+ found: Ranked | Mounted,
290
+ ): Promise<Response> {
291
+ // 37.2 — a route's rung may hand `next` a request of its own, adopted as a root rung's is.
292
+ adoptRequest(request)
293
+ const url = new URL(request.url)
294
+ const asked = url.pathname
295
+ const params = (currentRequestScope() as RequestScope).params as Params
296
+ if ('handler' in found) return answerRpc(found, params, url, request, app)
297
+ const page = found
215
298
 
216
299
  // 30.16 — THE CLIENT ROUTER ASKING, not a reader loading the page. `middleware` has already run
217
300
  // over this request by the time it gets here, and it is what the router came for: a refusal
@@ -226,8 +309,8 @@ export async function answerRequest(
226
309
  // 33.4 — a prop is an ACCESSOR, which is the contract `renderComponent` renders a child under
227
310
  // and the one the client router mounts a page under; the params are the record `firstMatch`
228
311
  // hands the client for this same address.
229
- const bound = boundPage(found.route, { params: () => found.params })
230
- // 38.30 — `vary` WHETHER OR NOT THIS ONE IS COMPRESSED: the document is negotiated, and a cache
312
+ const bound = boundPage(page, { params: () => params })
313
+ // 38.29 — `vary` WHETHER OR NOT THIS ONE IS COMPRESSED: the document is negotiated, and a cache
231
314
  // that stored the identity answer for a reader who sent no `accept-encoding` must not hand it
232
315
  // to one who did, nor the reverse.
233
316
  const headers = new Headers(DOCUMENT_HEADERS)
@@ -244,7 +327,7 @@ export async function answerRequest(
244
327
  // would otherwise render one address and hydrate against another.
245
328
  const at = new URL(request.url)
246
329
  at.pathname = mounted(at.pathname)
247
- arrive(at.href, found.route.pattern, found.params)
330
+ arrive(at.href, page.pattern, params)
248
331
  // 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
249
332
  // and 41.13, every call it makes is left for the browser's own call to be answered from.
250
333
  beginRender({ seed: seedFor(caller), calls: [] })
@@ -253,7 +336,7 @@ export async function answerRequest(
253
336
  // status rather than as a 200 the stream then abandons.
254
337
  let document: Uint8Array<ArrayBuffer>
255
338
  try {
256
- document = await render(bound, found.route.document)
339
+ document = await renderDocument(bound, page.document)
257
340
  } catch (thrown) {
258
341
  const failure = failureOf(thrown)
259
342
  // Explained only where it was the server's fault — not a refusal, not a client that left.
@@ -277,11 +360,10 @@ export async function answerRequest(
277
360
  const OWN_HANDLERS = new Map([
278
361
  PRINCIPAL_MOUNT,
279
362
  mountedOwn(HEALTH_ADDRESS, 'health', health),
280
- mountedOwn(LOGS_ADDRESS, 'logs', LOGS_HANDLER),
281
363
  ])
282
364
 
283
365
  // THE ADDRESSES ABIDE ANSWERS ITSELF, looked up once rather than asked in turn: each to its value,
284
- // a `Response` of its own, or `undefined` where its `ABIDE_*` has not opened it.
366
+ // a `Response` of its own, or `undefined` for a 404.
285
367
  const SURFACES = new Map<
286
368
  string,
287
369
  (app: AppSources, request: Request) => unknown
@@ -321,9 +403,9 @@ export function reachedInRender(
321
403
  }
322
404
 
323
405
  /**
324
- * What the compiler writes to `<out>/server.ts`: the routes and the error pages.
406
+ * The server's table the compiler writes (`SERVER_TABLE`): the routes and the error pages.
325
407
  *
326
- * Read off disk by `abide start` and out of memory by `abide dev`.
408
+ * Served out of the compilation (`abideModules`) — the one a build kept, or `abide dev`'s.
327
409
  */
328
410
  export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
329
411
 
@@ -336,14 +418,12 @@ export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
336
418
  */
337
419
  export type SourceParts = {
338
420
  table: ServerTable
339
- shell: string
340
- /** The handler table the build wrote, and where each row's module is loaded from. 16.2. */
341
- handlers: Mount[]
342
- handlerModule: (source: string) => Promise<Record<string, unknown>>
343
- /** Every public file, by its name below the directory to its path. 38.19, 38.20. */
421
+ /** The handler table the build wrote, each row with its module's loader. 16.2. */
422
+ handlers: MountRow[]
423
+ /** Every public file, by its name below the directory to its path. 38.18, 38.19. */
344
424
  published: Map<string, string>
345
425
  /**
346
- * The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.18), after the
426
+ * The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.17), after the
347
427
  * app's own tags.
348
428
  *
349
429
  * A PROMISE WHERE THE BUNDLE IS STILL BUILDING: `abide dev`'s handlers mount and its public files
@@ -363,33 +443,57 @@ type ClientLane = {
363
443
  * The sources an `App` is booted over, off the parts a command supplies.
364
444
  *
365
445
  * 30.9 — MOST SPECIFIC FIRST, ranked off the table rather than in it. The ordering IS the client
366
- * router's own: both seams compile through `rankRoutes` and resolve through `firstMatch`, so
446
+ * router's own: both seams compile through `rankRoutes` and walk a route with `matchOf` — the
447
+ * server past a route that has no answer for the method asked, which the router never is — so
367
448
  * "the two cannot resolve one address differently" is a grep rather than a promise. 35.2 — the
368
449
  * error pages deepest directory first, so the nearest answers.
369
450
  */
370
- export async function appSources(parts: SourceParts): Promise<LoadedSources> {
451
+ export async function appSources(parts: SourceParts): Promise<AppSources> {
371
452
  const [mounted, published, client] = await Promise.all([
372
- mountHandlers(parts.handlers, parts.handlerModule),
453
+ mountHandlers(parts.handlers),
373
454
  publicFilesAt(parts.published),
374
455
  parts.client,
375
456
  ])
376
- const underMount = shellUnderMount(parts.shell)
457
+ // 35.6 — every page is served in abide's own document; what it carries beyond that, the root
458
+ // `layout.abide` writes (35.15).
377
459
  const documentOf = (modules: string[]): string =>
378
- intoHead(underMount, clientTags(client.manifest, modules) + client.head)
379
- const routes: Omit<Ranked, 'segments'>[] = []
460
+ intoHead(
461
+ DEFAULT_DOCUMENT,
462
+ clientTags(client.manifest, modules) + client.head,
463
+ )
464
+ const pages: Omit<Ranked, 'segments'>[] = []
380
465
  for (const route of parts.table.routes)
381
- routes.push({
466
+ pages.push({
382
467
  pattern: route.pattern,
383
468
  load: route.load,
384
469
  layouts: route.layouts,
385
470
  document: documentOf(route.clientModules),
386
471
  })
472
+ // 19.1 — the build refused two files at one route and method, so a route is one row however
473
+ // many methods answer at it.
474
+ const rows = new Map<string, Omit<RouteRow, 'segments'>>()
475
+ const rowAt = (pattern: string): Omit<RouteRow, 'segments'> => {
476
+ let row = rows.get(pattern)
477
+ if (row === undefined) {
478
+ row = {
479
+ pattern,
480
+ page: undefined,
481
+ handlers: new Map(),
482
+ answers: new Map(),
483
+ }
484
+ rows.set(pattern, row)
485
+ }
486
+ return row
487
+ }
488
+ for (const page of rankRoutes(pages)) rowAt(page.pattern).page = page
489
+ for (const handler of mounted.handlers.values())
490
+ rowAt(handler.address).handlers.set(handler.method, handler)
387
491
  const errors: RankedError[] = []
388
492
  for (const error of parts.table.errors)
389
493
  errors.push({ ...error, segments: parseRoutePattern(error.pattern) })
390
494
  errors.sort((a, b) => b.segments.length - a.segments.length)
391
- return {
392
- routes: rankRoutes(routes),
495
+ const sources: AppSources = {
496
+ routes: rankRoutes([...rows.values()]),
393
497
  errors,
394
498
  errorDocument: documentOf([]),
395
499
  framework: client.framework,
@@ -397,12 +501,34 @@ export async function appSources(parts: SourceParts): Promise<LoadedSources> {
397
501
  explained: parts.explained,
398
502
  ...mounted,
399
503
  }
504
+ for (const row of sources.routes) {
505
+ if (row.page !== undefined)
506
+ row.answers.set(
507
+ row.page,
508
+ answerInside(sources, row.page, rungsOver(row.pattern)),
509
+ )
510
+ for (const handler of row.handlers.values())
511
+ row.answers.set(
512
+ handler,
513
+ answerInside(sources, handler, handler.rungs),
514
+ )
515
+ }
516
+ return sources
517
+ }
518
+
519
+ /** `found`'s answer inside the rungs filed over its route. 16.14, 37.2. */
520
+ function answerInside(
521
+ app: AppSources,
522
+ found: Ranked | Mounted,
523
+ rungs: AppRung[],
524
+ ): (ctx: { request: Request }) => Promise<Response> {
525
+ return compose(rungs, ({ request }) => answerMatched(app, request, found))
400
526
  }
401
527
 
402
528
  /**
403
529
  * The head tags that load the app: its metas, stylesheet, entry script and the route's preloads.
404
530
  *
405
- * 38.23, and 38.24 for the stylesheet beside it. An app does not write either: the bundle's
531
+ * 38.22, and 38.23 for the stylesheet beside it. An app does not write either: the bundle's
406
532
  * address is the build's, and a hand-written one is a path that goes stale the first time the
407
533
  * output layout moves.
408
534
  *
@@ -417,23 +543,25 @@ export async function appSources(parts: SourceParts): Promise<LoadedSources> {
417
543
  * component is not on it matches nothing. An imported stylesheet carries no token and applies as
418
544
  * written, wherever it is served.
419
545
  *
420
- * 38.29 — THE ROUTE'S CHUNKS ARE PRELOADED BESIDE THE ENTRY. Left to the client, the page's chunk
546
+ * 38.28 — THE ROUTE'S CHUNKS ARE PRELOADED BESIDE THE ENTRY. Left to the client, the page's chunk
421
547
  * and the shared runtime it imports are only asked for once the entry has arrived and run the
422
548
  * router — a whole round trip after the document, for the largest file in the bundle. Named here,
423
549
  * they are requested while the document is still being parsed.
424
550
  *
425
551
  * EVERY NAME IS THE MANIFEST'S, the entry's included: a build hashes it (38.5). A manifest with no
426
- * entry is a bundle that failed under `abide dev` (38.18), and gets no tags rather than a
552
+ * entry is a bundle that failed under `abide dev` (38.17), and gets no tags rather than a
427
553
  * `<script>` at a 404.
428
554
  */
429
555
  export function clientTags(manifest: Manifest, modules: string[]): string {
430
556
  if (manifest.entry === undefined) return ''
431
- // 28.11 — `DEBUG`, through the document rather than the bundle, where the server has one.
557
+ // 28.10 — `DEBUG`, through the document rather than the bundle, where the server has one.
432
558
  const debug = process.env.DEBUG
433
559
  let tags =
434
560
  debug === undefined || debug === ''
435
561
  ? ''
436
562
  : `<meta name="${DEBUG_META}" content="${escapeAttribute(debug)}">`
563
+ // 28.4 — the app's name, so a browser's `log.named` prefixes the one the server's does.
564
+ tags += `<meta name="${APP_META}" content="${escapeAttribute(appName())}">`
437
565
  // 16.10 — the mount the browser resolves every address against, the root's `''` included: a
438
566
  // lookup that finds no tag has walked the whole document to say so.
439
567
  tags += `<meta name="${MOUNT_META}" content="${escapeAttribute(MOUNT.current())}">`
@@ -448,38 +576,10 @@ export function clientTags(manifest: Manifest, modules: string[]): string {
448
576
  return tags
449
577
  }
450
578
 
451
- /**
452
- * The shell with every root-absolute `href` and `src` it writes put below the mount.
453
- *
454
- * 16.10, 36.11 — THE SHELL IS NOT A TEMPLATE, so it has no `url` to spell an address with, and the
455
- * `/fonts/…` it preloads is a public file answered below the mount. The same shell is every page's
456
- * at every depth, so a relative address is no spelling either. `//` is another host's.
457
- */
458
- function shellUnderMount(shell: string): string {
459
- const root = mounted('/')
460
- return root === '/'
461
- ? shell
462
- : shell.replace(
463
- /\b(href|src)=(["'])\/(?!\/)/g,
464
- (_whole, name: string, quote: string) =>
465
- `${name}=${quote}${root}`,
466
- )
467
- }
468
-
469
- /**
470
- * The shell with `tags` at the head anchor `render` uses.
471
- *
472
- * So a shell with no `</head>` puts them where its head content goes rather than inside the body.
473
- */
474
- function intoHead(shell: string, tags: string): string {
475
- const at = headAnchor(shell)
476
- return shell.slice(0, at) + tags + shell.slice(at)
477
- }
478
-
479
579
  /**
480
580
  * The public files by address, off each file's name and where its bytes are.
481
581
  *
482
- * 38.19 and 38.20's directory, as an address per file — or a compiled app's, whose bytes are
582
+ * 38.18 and 38.19's directory, as an address per file — or a compiled app's, whose bytes are
483
583
  * inside itself. Read at boot rather than per request, and the ETAG IS READ WITH IT: 21.13
484
584
  * derives it from the bytes, so there is no cheaper moment than the one read that is happening
485
585
  * anyway. The bytes are hashed and dropped — `Bun.file` streams the body when somebody asks, so a
@@ -531,15 +631,44 @@ function readsOnly(request: Request): boolean {
531
631
  return request.method === 'GET' || request.method === 'HEAD'
532
632
  }
533
633
 
634
+ const READS = ['GET', 'HEAD']
635
+
636
+ /**
637
+ * 15.17 — a 405, naming what is answered at `pathname`.
638
+ *
639
+ * `request` where a route's methods were asked, which 15.22 answers in the asker's lane; a public
640
+ * file and the bundle are a document's alone.
641
+ */
534
642
  async function methodRefused(
535
643
  app: AppSources,
536
644
  pathname: string,
645
+ allowed: string[],
646
+ request?: Request,
537
647
  ): Promise<Response> {
538
- const answer = await answerFailure(app, pathname, refuse(405))
539
- answer.headers.set('allow', 'GET, HEAD')
648
+ const failure = refuse(405)
649
+ const answer =
650
+ request === undefined
651
+ ? await answerFailure(app, pathname, failure)
652
+ : await laneFailure(app, pathname, failure, request)
653
+ answer.headers.set('allow', allowed.join(', '))
540
654
  return answer
541
655
  }
542
656
 
657
+ /**
658
+ * A failure at an address whose lane only the request can say: a page's, painted, where it prefers
659
+ * a document, and a handler's otherwise. 15.22.
660
+ */
661
+ function laneFailure(
662
+ app: AppSources,
663
+ pathname: string,
664
+ failure: Failed<string, unknown>,
665
+ request: Request,
666
+ ): Response | Promise<Response> {
667
+ return prefers(request, 'text/html', 'application/json')
668
+ ? answerFailure(app, pathname, failure)
669
+ : answered(failure)
670
+ }
671
+
543
672
  /**
544
673
  * A public file's answer, conditional and range-aware off the etag read at boot.
545
674
  *