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