@abide/abide 0.123.2 → 0.124.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 (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -8,86 +8,47 @@
8
8
 
9
9
  import {
10
10
  ABIDE_ADDRESS,
11
- APP_MANIFEST_ADDRESS,
12
11
  BRIDGE_ADDRESS,
13
- CLIENT_ADDRESS,
14
12
  HEALTH_ADDRESS,
15
13
  MCP_ADDRESS,
16
14
  OPENAPI_ADDRESS,
17
15
  PRIVATE_HANDLERS_ADDRESS,
18
- WORKER_ADDRESS,
19
16
  } from '#shared/ADDRESSES.ts'
20
- import { APP_META } from '#shared/APP_META.ts'
21
- import { DEBUG_META } from '#shared/DEBUG_META.ts'
22
17
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
23
- import { ERROR_META } from '#shared/ERROR_META.ts'
24
- import { escapeAttribute } from '#shared/escapeText.ts'
25
18
  import { isFailed } from '#shared/Failed.ts'
26
- import { HandlerRoute } from '#shared/HandlerRoute.ts'
27
19
  import { health } from '#shared/health.ts'
28
- import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
29
- import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
30
- import { ICON_ADDRESSES } from '#shared/LAUNCHER_FILES.ts'
31
- import { appName } from '#shared/log.ts'
32
20
  import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
33
21
  import { compose } from '#shared/Middleware.ts'
34
- import { MOUNT, mounted } from '#shared/mount.ts'
35
- import { MOUNT_META } from '#shared/MOUNT_META.ts'
22
+ import { mounted } from '#shared/mount.ts'
36
23
  import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
37
- import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
38
24
  import { notFound as refusedNotFound, refuse } from '#shared/refuse.ts'
39
25
  import { arrive } from '#shared/route.ts'
40
- import {
41
- parseRoutePattern,
42
- rankRoutes,
43
- type Segment,
44
- } from '#shared/routePattern.ts'
45
- import type { Method, Rpc } from '#shared/rpc.ts'
46
- import { rpcRequest } from '#shared/rpcRequest.ts'
26
+ import type { Segment } from '#shared/routePattern.ts'
27
+ import type { Method } from '#shared/rpc.ts'
47
28
 
48
29
  import { answerFailure, laneFailure } from './answerFailure.ts'
49
- import {
50
- answerRpc,
51
- type Mounted,
52
- mountedOwn,
53
- mountHandlers,
54
- type MountRow,
55
- } from './answerRpc.ts'
56
- import { answerBlobTagged, answerValue, contentEtag } from './answerValue.ts'
57
- import { answerWorker } from './answerWorker.ts'
58
- import { appManifest } from './appManifest.ts'
30
+ import { answerRpc } from './answerRpc.ts'
31
+ import { answerBlobTagged, answerValue } from './answerValue.ts'
59
32
  import { answerBridge } from './bridgeRelay.ts'
60
- import { callerOrMint } from './callerOf.ts'
61
- import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
62
33
  import { compressAnswer } from './compressAnswer.ts'
63
- import { settingOn } from './config.ts'
64
- import { noteCallInRender } from './deferredBuilds.ts'
65
34
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
66
35
  import { answerMcp, openApi } from './generatedSurfaces.ts'
67
- import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
68
- import { chunksOf, type Manifest } from './manifestOf.ts'
36
+ import { failureOf, type LaneRung } from './hooks.ts'
37
+ import { type Mounted, mountedOwn } from './mountHandlers.ts'
69
38
  import { muxAdmission } from './muxUpgrade.ts'
70
39
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
40
+ import { beginPageRender } from './PageRender.ts'
71
41
  import { plainAnswer } from './plainAnswer.ts'
72
42
  import { PRINCIPAL_MOUNT } from './principal.ts'
73
- import {
74
- boundPage,
75
- type Component,
76
- DEFAULT_DOCUMENT,
77
- intoHead,
78
- renderDocument,
79
- } from './render.ts'
43
+ import { boundPage, type Component, renderDocument } from './render.ts'
80
44
  import {
81
45
  adoptRequest,
82
46
  type AnsweringApp,
83
- beginRender,
84
47
  currentRequestScope,
85
- type PageRender,
86
48
  type RequestScope,
87
49
  within,
88
50
  writtenStatus,
89
51
  } from './requestScope.ts'
90
- import { callsOnly, seedFor } from './seedBuffer.ts'
91
52
 
92
53
  // One row of the server's table the compiler writes (`SERVER_TABLE`). The loader is a dynamic import
93
54
  // so a page's markup is off the boot path until an address asks for it, which is the same reason
@@ -107,9 +68,9 @@ type ServerRoute = {
107
68
  // 35.2 — an error page, and the directory it answers for.
108
69
  type ServerError = Omit<ServerRoute, 'clientModules'>
109
70
 
110
- type RankedError = ServerError & { segments: Segment[] }
71
+ export type RankedError = ServerError & { segments: Segment[] }
111
72
 
112
- type Ranked = RankedError & {
73
+ export type Ranked = RankedError & {
113
74
  // The document this route is painted into, with the tags that load the app already in its
114
75
  // head (38.22, 38.23) and the preloads for this route's own modules (38.28). One per route and
115
76
  // built at boot, so a request pays for none of it.
@@ -122,7 +83,7 @@ type Ranked = RankedError & {
122
83
  * 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.20's
123
84
  * "the most specific route answering the method" is a walk down the rows asking each column.
124
85
  */
125
- type RouteRow = {
86
+ export type RouteRow = {
126
87
  pattern: string
127
88
  matchers: Record<string, string>
128
89
  segments: Segment[]
@@ -141,7 +102,7 @@ type RouteRow = {
141
102
  }
142
103
 
143
104
  /** A public file, read once at boot: where the bytes are, the etag off them, and its type. */
144
- type PublicFile = { path: string; etag: string; type: string }
105
+ export type PublicFile = { path: string; etag: string; type: string }
145
106
 
146
107
  /** Everything `answerRequest` answers from: the ranked routes, the lanes and the handlers. */
147
108
  export type AppSources = {
@@ -178,9 +139,6 @@ export type AppSources = {
178
139
  explained: ((thrown: unknown) => Response) | undefined
179
140
  }
180
141
 
181
- // 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
182
- const SEEDS_NOTHING: PageRender['seed'] = () => false
183
-
184
142
  // 35.19 — what fetch calls a navigation request's destinations, and a script's `fetch`.
185
143
  const PAGE_DESTINATIONS = new Set([
186
144
  'document',
@@ -368,9 +326,6 @@ async function answerMatched(
368
326
  // minted for a request that never becomes a page.
369
327
  if (request.method === 'HEAD')
370
328
  return new Response(null, { status: writtenStatus(), headers })
371
- // 26.5 — the caller the seed buffer is keyed by, minted where the browser carries none: the same
372
- // one `principal.caller` reads for the rest of the request.
373
- const caller = callerOrMint()
374
329
  // 23.1 — WHERE THIS PAGE IS, for its setup to read, in the request's own instances (11.32).
375
330
  // 16.10 — AT THE ADDRESS IT IS SERVED AT, which is the one the browser's router arrives at: the
376
331
  // request reached here with the mount taken off, and a page reading `route.url()` on both sides
@@ -379,23 +334,8 @@ async function answerMatched(
379
334
  at.pathname = mounted(at.pathname)
380
335
  arrive(at.href, page.pattern, params)
381
336
  // 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
382
- // and 41.13, every call it makes is left for the browser's own call to be answered from.
383
- // 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which nothing hydrates: its entries would answer the
384
- // next call this caller makes with what the render saw, a sign-in since included — save the one
385
- // the worker fetched for the navigation it answers, which is the page that hydrates (45.23).
386
- const keep = request.headers.get(KEEP_HEADER)
387
- const hydrated =
388
- keep === KEEP_OPENED ||
389
- request.headers.get('sec-fetch-dest') !== 'empty'
390
- // 41.30 — SAVE ONE THE WORKER KEEPS, whose early calls are what a later open of it starts.
391
- beginRender({
392
- seed: hydrated
393
- ? seedFor(caller)
394
- : keep !== null
395
- ? callsOnly()
396
- : SEEDS_NOTHING,
397
- calls: [],
398
- })
337
+ // and 41.13, every call it makes is recorded into its render.
338
+ beginPageRender(request)
399
339
  // 41.1, 35.2 — THE DOCUMENT IS THE BUILT PAGE, read whole before there is a `Response` to commit a
400
340
  // status to: a refusal the page read, or a throw, is answered by the nearest error page at its own
401
341
  // status rather than as a 200 the stream then abandons.
@@ -443,42 +383,6 @@ const SURFACES = new Map<
443
383
  [BRIDGE_ADDRESS, (_app, request) => answerBridge(request)],
444
384
  ])
445
385
 
446
- // Each handler's route, compiled once rather than per render that calls it — as its `Rpc` keeps its
447
- // own; compiled again only where it was mounted at another address since.
448
- const ROUTE_OF = new WeakMap<object, HandlerRoute>()
449
-
450
- // What a call made in a page's render reports into: the seed buffer (41.13), and — where the handler
451
- // is mounted — the request the browser will make for it, for the early-call script (41.20). Once per
452
- // render, as the browser's `Rpc` makes it once, and the seed buffer is what says which is the once.
453
- export function reachedInRender(
454
- rpc: object,
455
- args: unknown,
456
- reached: unknown,
457
- ): void {
458
- const page = currentRequestScope()?.page
459
- if (page === undefined) return
460
- noteCallInRender()
461
- if (!page.seed(rpc, args, reached)) return
462
- // 16.10 — the address the browser's own call is made at, stamped under the mount by
463
- // `mountHandlers` — and `undefined` on a handler nothing mounted.
464
- const { address, method } = rpc as Rpc<unknown, unknown>
465
- if (address === undefined) return
466
- let route = ROUTE_OF.get(rpc)
467
- if (route?.address !== address) {
468
- route = new HandlerRoute(address)
469
- ROUTE_OF.set(rpc, route)
470
- }
471
- page.calls.push({
472
- request: rpcRequest(
473
- method,
474
- route,
475
- args as Record<string, unknown> | undefined,
476
- ),
477
- reached,
478
- painted: undefined,
479
- })
480
- }
481
-
482
386
  /**
483
387
  * The server's table the compiler writes (`SERVER_TABLE`): the routes and the error pages.
484
388
  *
@@ -486,168 +390,8 @@ export function reachedInRender(
486
390
  */
487
391
  export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
488
392
 
489
- /**
490
- * What a command hands `appSources`: the parts it read, built or compiled, and the client lane.
491
- *
492
- * ONE SHAPE FOR EVERY SUPPLIER — `abide start` off disk, a compiled app out of itself, `abide dev` out
493
- * of memory, and a test handing over modules it already holds — so what differs between them is
494
- * where a part came from and never how the app is assembled from it.
495
- */
496
- export type SourceParts = {
497
- table: ServerTable
498
- /** The handler table the build wrote, each row with its module's loader. 16.2. */
499
- handlers: MountRow[]
500
- /** Every public file, by its name below the directory to its path. 38.18, 38.19. */
501
- published: Map<string, string>
502
- /**
503
- * The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.17), after the
504
- * app's own tags.
505
- *
506
- * A PROMISE WHERE THE BUNDLE IS STILL BUILDING: `abide dev`'s handlers mount and its public files
507
- * hash while the bundler runs, rather than after it — ~40 ms of a docs-app reload.
508
- */
509
- client: ClientLane | Promise<ClientLane>
510
- explained: AppSources['explained']
511
- }
512
-
513
- type ClientLane = {
514
- manifest: Manifest
515
- framework: AppSources['framework']
516
- head: string
517
- /**
518
- * 45.1, 45.9 — whether the worker saves `manifest`'s bundle and the pages register it: `abide
519
- * build`'s, unless `ABIDE_OFFLINE` is off. Where not, the worker address answers the removing one.
520
- */
521
- offline: boolean
522
- }
523
-
524
- /**
525
- * The sources an `App` is booted over, off the parts a command supplies.
526
- *
527
- * 30.9 — MOST SPECIFIC FIRST, ranked off the table rather than in it. The ordering IS the client
528
- * router's own: both seams compile through `rankRoutes` and walk a route with `matchOf` — the
529
- * server past a route that has no answer for the method asked, which the router never is — so
530
- * "the two cannot resolve one address differently" is a grep rather than a promise. 35.2 — the
531
- * error pages deepest directory first, so the nearest answers.
532
- */
533
- export async function appSources(parts: SourceParts): Promise<AppSources> {
534
- const [mounted, published, client] = await Promise.all([
535
- mountHandlers(parts.handlers),
536
- publicFilesAt(parts.published),
537
- parts.client,
538
- ])
539
- // 35.6 — every page is served in abide's own document; what it carries beyond that, the root
540
- // `layout.abide` writes (35.15).
541
- // 45.15 — installable where the pages are kept offline and the app has an icon to install by.
542
- const icon = published.get(ICON_ADDRESSES.png)
543
- const install =
544
- icon !== undefined && settingOn('ABIDE_OFFLINE', true)
545
- ? await appManifest(icon.path)
546
- : undefined
547
- const documentOf = (modules: string[]): string =>
548
- intoHead(
549
- DEFAULT_DOCUMENT,
550
- clientTags(client.manifest, modules) +
551
- (client.offline ? `<meta name="${OFFLINE_META}">` : '') +
552
- (install?.tags ?? '') +
553
- client.head,
554
- )
555
- const pages: Omit<Ranked, 'segments'>[] = []
556
- for (const route of parts.table.routes)
557
- pages.push({
558
- pattern: route.pattern,
559
- load: route.load,
560
- layouts: route.layouts,
561
- matchers: route.matchers,
562
- document: documentOf(route.clientModules),
563
- })
564
- // 19.1 — the build refused two files at one route and method, so a route is one row however
565
- // many methods answer at it — and 23.27 one matcher per position, so whichever file opens the
566
- // row brings what every file at it would.
567
- const rows = new Map<string, Omit<RouteRow, 'segments'>>()
568
- const rowAt = (
569
- pattern: string,
570
- matchers: Record<string, string>,
571
- ): Omit<RouteRow, 'segments'> => {
572
- let row = rows.get(pattern)
573
- if (row === undefined) {
574
- row = {
575
- pattern,
576
- matchers,
577
- page: undefined,
578
- handlers: new Map(),
579
- answers: new Map(),
580
- }
581
- rows.set(pattern, row)
582
- }
583
- return row
584
- }
585
- for (const page of rankRoutes(pages))
586
- rowAt(page.pattern, page.matchers).page = page
587
- for (const handler of mounted.handlers.values())
588
- rowAt(handler.address, handler.matchers).handlers.set(
589
- handler.method,
590
- handler,
591
- )
592
- const errors: RankedError[] = []
593
- for (const error of parts.table.errors)
594
- errors.push({
595
- ...error,
596
- segments: parseRoutePattern(error.pattern, error.matchers),
597
- })
598
- errors.sort((a, b) => b.segments.length - a.segments.length)
599
- const sources: AppSources = {
600
- routes: rankRoutes([...rows.values()]),
601
- errors,
602
- // 35.18 — marked, so the browser hydrates no page over what an error page painted.
603
- errorDocument: intoHead(documentOf([]), `<meta name="${ERROR_META}">`),
604
- framework: (pathname, request) => {
605
- if (pathname === WORKER_ADDRESS)
606
- return answerWorker(
607
- client.offline ? client.manifest : undefined,
608
- )
609
- if (pathname === APP_MANIFEST_ADDRESS && install !== undefined)
610
- return new Response(install.body, {
611
- headers: {
612
- 'content-type': 'application/manifest+json',
613
- 'cache-control': 'no-cache',
614
- },
615
- })
616
- return client.framework(pathname, request)
617
- },
618
- published,
619
- explained: parts.explained,
620
- offline: client.offline,
621
- ...mounted,
622
- }
623
- for (const row of sources.routes) {
624
- if (row.page !== undefined)
625
- row.answers.set(
626
- row.page,
627
- answerInside(
628
- sources,
629
- row.page,
630
- // 37.14 — a page's lane paints a refusal as its error page does (35.2).
631
- rungsOver(row.pattern, (failure, request) =>
632
- answerFailure(
633
- sources,
634
- new URL(request.url).pathname,
635
- failure,
636
- ),
637
- ),
638
- ),
639
- )
640
- for (const handler of row.handlers.values())
641
- row.answers.set(
642
- handler,
643
- answerInside(sources, handler, handler.rungs),
644
- )
645
- }
646
- return sources
647
- }
648
-
649
393
  /** `found`'s answer inside the rungs filed over its route. 16.14, 37.2. */
650
- function answerInside(
394
+ export function answerInside(
651
395
  app: AppSources,
652
396
  found: Ranked | Mounted,
653
397
  rungs: LaneRung[],
@@ -655,103 +399,6 @@ function answerInside(
655
399
  return compose(rungs, ({ request }) => answerMatched(app, request, found))
656
400
  }
657
401
 
658
- /**
659
- * The head tags that load the app: its metas, stylesheet, entry script and the route's preloads.
660
- *
661
- * 38.22, and 38.23 for the stylesheet beside it. An app does not write either: the bundle's
662
- * address is the build's, and a hand-written one is a path that goes stale the first time the
663
- * output layout moves.
664
- *
665
- * THE STYLESHEET GOES FIRST, and ahead of the page's own head contributions for the same reason:
666
- * a document that parses its script before its styles paints once unstyled. It is a plain `<link>`
667
- * rather than anything the client does, so a page the server painted is styled with no JS at all.
668
- *
669
- * ONE STYLESHEET FOR THE APP: the bundler collects every stylesheet reachable from the entry — each
670
- * component's scoped `<style>` (34.4) and each one a component imports (34.6) — into one file, so a
671
- * page that renders none of those components is served them too. For a `<style>` block that costs
672
- * bytes rather than correctness: every rule requires its component's scope token, so on a page the
673
- * component is not on it matches nothing. An imported stylesheet carries no token and applies as
674
- * written, wherever it is served.
675
- *
676
- * 38.28 — THE ROUTE'S CHUNKS ARE PRELOADED BESIDE THE ENTRY. Left to the client, the page's chunk
677
- * and the shared runtime it imports are only asked for once the entry has arrived and run the
678
- * router — a whole round trip after the document, for the largest file in the bundle. Named here,
679
- * they are requested while the document is still being parsed.
680
- *
681
- * EVERY NAME IS THE MANIFEST'S, the entry's included: a build hashes it (38.5). A manifest with no
682
- * entry is a bundle that failed under `abide dev` (38.17), and gets no tags rather than a
683
- * `<script>` at a 404.
684
- */
685
- export function clientTags(manifest: Manifest, modules: string[]): string {
686
- if (manifest.entry === undefined) return ''
687
- // 28.10 — `DEBUG`, through the document rather than the bundle, where the server has one.
688
- const debug = process.env.DEBUG
689
- let tags =
690
- debug === undefined || debug === ''
691
- ? ''
692
- : `<meta name="${DEBUG_META}" content="${escapeAttribute(debug)}">`
693
- // 28.4 — the app's name, so a browser's `log.named` prefixes the one the server's does.
694
- tags += `<meta name="${APP_META}" content="${escapeAttribute(appName())}">`
695
- // 16.10 — the mount the browser resolves every address against, the root's `''` included: a
696
- // lookup that finds no tag has walked the whole document to say so.
697
- tags += `<meta name="${MOUNT_META}" content="${escapeAttribute(MOUNT.current())}">`
698
- const client = mounted(CLIENT_ADDRESS)
699
- tags +=
700
- manifest.stylesheet === undefined
701
- ? ''
702
- : `<link rel="stylesheet" href="${client}/${manifest.stylesheet}">`
703
- tags += `<script type="module" src="${client}/${manifest.entry}"></script>`
704
- for (const chunk of chunksOf([`${CLIENT_ENTRY}.ts`, ...modules], manifest))
705
- tags += `<link rel="modulepreload" href="${client}/${chunk}">`
706
- return tags
707
- }
708
-
709
- /**
710
- * The public files by address, off each file's name and where its bytes are.
711
- *
712
- * 38.18 and 38.19's directory, as an address per file — or a compiled app's, whose bytes are
713
- * inside itself. Read at boot rather than per request, and the ETAG IS READ WITH IT: 21.13
714
- * derives it from the bytes, so there is no cheaper moment than the one read that is happening
715
- * anyway. The bytes are hashed and dropped — `Bun.file` streams the body when somebody asks, so a
716
- * large file is not held in memory for the life of the process.
717
- */
718
- async function publicFilesAt(
719
- paths: Map<string, string>,
720
- ): Promise<Map<string, PublicFile>> {
721
- const files = new Map<string, PublicFile>()
722
- // THE READS OVERLAP. They are independent, and `abide dev` runs this again on every
723
- // reload rather than once at boot — so it is reload latency an author waits on, not just
724
- // a boot cost. Each is hashed as it lands, so no more than the reads in flight are held.
725
- const names = [...paths.keys()]
726
- const etags = await Promise.all(
727
- names.map(async (name) =>
728
- contentEtag(await Bun.file(paths.get(name) as string).bytes()),
729
- ),
730
- )
731
- for (let index = 0; index < names.length; index += 1) {
732
- const name = names[index] as string
733
- files.set(`/${name}`, {
734
- path: paths.get(name) as string,
735
- etag: etags[index] as string,
736
- type: publicType(name),
737
- })
738
- }
739
- return files
740
- }
741
-
742
- /**
743
- * A public file's content type, off its name's extension.
744
- *
745
- * 36.14, 36.17 — a text type without a charset is given UTF-8's rather than whatever a browser
746
- * guesses.
747
- */
748
- function publicType(name: string): string {
749
- const type = Bun.file(name).type
750
- return type.startsWith('text/') && !type.includes('charset')
751
- ? `${type}; charset=utf-8`
752
- : type
753
- }
754
-
755
402
  function notFound(): Response {
756
403
  return plainAnswer('not found', 404)
757
404
  }