@abide/abide 0.123.2 → 0.123.3

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 (94) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/abide.ts +36 -65
  3. package/package.json +1 -1
  4. package/src/compiler/Diagnostics.ts +198 -0
  5. package/src/compiler/KeptChecker.ts +97 -0
  6. package/src/compiler/Lowering.ts +1 -1
  7. package/src/compiler/PositionedRefusal.ts +1 -1
  8. package/src/compiler/TypeSession.ts +62 -85
  9. package/src/compiler/appLayout.ts +28 -20
  10. package/src/compiler/argsDefaultsIn.ts +1 -1
  11. package/src/compiler/checkApp.ts +64 -68
  12. package/src/compiler/checkedShadow.ts +1 -1
  13. package/src/compiler/compileApp.ts +101 -70
  14. package/src/compiler/declaredExports.ts +2 -4
  15. package/src/compiler/emitClient.ts +1 -1
  16. package/src/compiler/formatAbide.ts +2 -2
  17. package/src/compiler/handlersUnder.ts +17 -29
  18. package/src/compiler/lineOf.ts +0 -19
  19. package/src/compiler/matchersOf.ts +3 -3
  20. package/src/compiler/mountsOf.ts +3 -3
  21. package/src/compiler/readSources.ts +16 -18
  22. package/src/compiler/readsIn.ts +1 -1
  23. package/src/compiler/serveLanguage.ts +23 -57
  24. package/src/compiler/sharedModuleWrites.ts +1 -1
  25. package/src/compiler/templateWarnings.ts +1 -2
  26. package/src/compiler/trackingWarnings.ts +1 -1
  27. package/src/server/CLIENT_BUILD.ts +4 -32
  28. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  29. package/src/server/answerRequest.ts +11 -300
  30. package/src/server/answerRpc.ts +12 -54
  31. package/src/server/answerSocket.ts +8 -35
  32. package/src/server/appBuild.ts +377 -0
  33. package/src/server/appPlugins.ts +15 -17
  34. package/src/server/appTree.ts +6 -5
  35. package/src/server/argsContract.ts +209 -0
  36. package/src/server/bridgeRelay.ts +153 -65
  37. package/src/server/claudeCode.ts +30 -127
  38. package/src/server/config.ts +6 -7
  39. package/src/server/createApp.ts +30 -64
  40. package/src/server/devWorker.ts +11 -15
  41. package/src/server/handlerFileMount.ts +40 -0
  42. package/src/server/index.ts +2 -2
  43. package/src/server/packageDirectory.ts +14 -0
  44. package/src/server/preload.ts +3 -2
  45. package/src/server/principal.ts +4 -3
  46. package/src/server/remote.ts +1 -1
  47. package/src/server/runCompiled.ts +1 -1
  48. package/src/server/runDesktop.ts +1 -1
  49. package/src/server/seedBuffer.ts +1 -1
  50. package/src/server/serveDev.ts +24 -58
  51. package/src/server/servedApp.ts +390 -0
  52. package/src/server/signInChanged.ts +20 -0
  53. package/src/server/surfaceOf.ts +2 -1
  54. package/src/server/versionOf.ts +6 -15
  55. package/src/shared/applyPatch.ts +1 -1
  56. package/src/shared/muxFrames.ts +1 -1
  57. package/src/shared/reactive/Keyed.ts +1 -1
  58. package/src/shared/reactive/Reactive.ts +21 -8
  59. package/src/shared/reactive/argsContract.ts +131 -4
  60. package/src/shared/reactive/index.ts +1 -0
  61. package/src/shared/reactive/patchLog.ts +79 -0
  62. package/src/shared/reactive/patchMarks.ts +34 -0
  63. package/src/shared/reactive/recordWrites.ts +11 -14
  64. package/src/shared/reactive/share.ts +4 -3
  65. package/src/shared/rpc.ts +12 -19
  66. package/src/ui/ArmRegion.ts +10 -4
  67. package/src/ui/Level.ts +11 -20
  68. package/src/ui/LevelChain.ts +272 -0
  69. package/src/ui/Router.ts +54 -245
  70. package/src/ui/Scope.ts +17 -3
  71. package/src/ui/TryRegion.ts +22 -23
  72. package/src/ui/bindSpread.ts +2 -4
  73. package/src/ui/listenForRoutes.ts +3 -10
  74. package/src/ui/outboxQueue.ts +21 -3
  75. package/src/ui/reconcile.ts +4 -6
  76. package/src/ui/routing.ts +2 -1
  77. package/src/ui/rows.ts +2 -2
  78. package/src/ui/setAttribute.ts +12 -3
  79. package/src/ui/socketWire.ts +26 -11
  80. package/src/compiler/Finding.ts +0 -2
  81. package/src/compiler/Refusals.ts +0 -65
  82. package/src/compiler/formatDiagnosis.ts +0 -20
  83. package/src/server/appDirectories.ts +0 -9
  84. package/src/server/argsFor.ts +0 -39
  85. package/src/server/buildDirectory.ts +0 -10
  86. package/src/server/coerceArgs.ts +0 -109
  87. package/src/server/derivedArgs.ts +0 -26
  88. package/src/server/runBuild.ts +0 -24
  89. package/src/server/serveBuild.ts +0 -144
  90. package/src/server/testBuild.ts +0 -220
  91. package/src/shared/reactive/FOLLOWED.ts +0 -8
  92. package/src/shared/reactive/PATCHES.ts +0 -11
  93. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  94. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -1,18 +1,19 @@
1
- import { existsSync, type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher or sync existence check of its own
1
+ import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
2
2
  import { dirname } from 'node:path' // bun exposes no path helpers of its own
3
3
 
4
4
  import { realOrAsGiven } from '#server/realOrAsGiven.ts'
5
5
 
6
6
  import { checkSource, lowerApp } from './checkApp.ts'
7
7
  import { citedClauses } from './citedClauses.ts'
8
- import { type AppChecker, openChecker } from './compileApp.ts'
8
+ import type { AppChecker } from './compileApp.ts'
9
+ import { type Diagnosis, formatDiagnosis } from './Diagnostics.ts'
9
10
  import { DOCS_SITE } from './DOCS_SITE.ts'
10
11
  import { formatAbide } from './formatAbide.ts'
11
- import { formatDiagnosis } from './formatDiagnosis.ts'
12
12
  import { isNamedAlone } from './isNamedAlone.ts'
13
+ import { KeptChecker } from './KeptChecker.ts'
13
14
  import { lineOf, offsetOf } from './lineOf.ts'
14
15
  import { nearest } from './nearest.ts'
15
- import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
16
+ import type { Inspection, Place } from './TypeSession.ts'
16
17
 
17
18
  // 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
18
19
  // about one open document at a time, so the squiggle an editor draws and the line CI fails on are
@@ -64,8 +65,7 @@ export async function serveLanguage(
64
65
  ): Promise<void> {
65
66
  const documents = new Map<string, Document>()
66
67
  const timers = new Map<string, ReturnType<typeof setTimeout>>()
67
- const checkers = new Map<string, AppChecker>()
68
- const watchers = new Map<string, FSWatcher>()
68
+ const checkers = new Map<string, KeptChecker>()
69
69
  // Each app's real root, to the root the editor reached it by.
70
70
  const links = new Map<string, string>()
71
71
  let queue: Promise<void> = Promise.resolve()
@@ -133,37 +133,19 @@ export async function serveLanguage(
133
133
  const held = (path: string): string | undefined =>
134
134
  documents.get(Bun.pathToFileURL(spelled(path)).href)?.text
135
135
 
136
- // An app's checker, opened on first use and kept for the session — and TOLD WHAT MOVED ON DISK,
137
- // or it answers every check from the text it read when it opened: an export added to a `lib`
138
- // module the page imports is "no exported member" until the editor restarts this. A project whose
139
- // tsconfig or package.json moved is opened again.
140
- const checkerOf = async (app: string): Promise<AppChecker> => {
136
+ // An app's checker, kept for the session — and TOLD WHAT MOVED ON DISK, or it answers every
137
+ // check from the text it read when it opened: an export added to a `lib` module the page
138
+ // imports is "no exported member" until the editor restarts this (`KeptChecker`).
139
+ const checkerOf = (app: string): KeptChecker => {
141
140
  let checker = checkers.get(app)
142
- if (checker !== undefined && !checker.current()) {
143
- checkers.delete(app)
144
- await checker.close().catch(() => {})
145
- checker = undefined
146
- }
147
141
  if (checker === undefined) {
148
- checker = await openChecker(app)
142
+ checker = new KeptChecker(app, () => {
143
+ // What an open page says may be decided by the file that moved.
144
+ for (const uri of documents.keys())
145
+ if (placed(uri)?.app === app) check(uri)
146
+ })
149
147
  checkers.set(app, checker)
150
148
  }
151
- if (!watchers.has(app))
152
- watchers.set(
153
- app,
154
- watch(app, { recursive: true }, (_event, filename) => {
155
- checkers
156
- .get(app)
157
- ?.saw(
158
- filename === null
159
- ? undefined
160
- : `${app}/${filename}`,
161
- )
162
- // What an open page says may be decided by the file that moved.
163
- for (const uri of documents.keys())
164
- if (placed(uri)?.app === app) check(uri)
165
- }),
166
- )
167
149
  return checker
168
150
  }
169
151
 
@@ -172,11 +154,9 @@ export async function serveLanguage(
172
154
  // keeps one for the session. Queued behind the checks, so none is in flight when it closes.
173
155
  const closeIfUnopened = async (app: string): Promise<void> => {
174
156
  for (const uri of documents.keys()) if (placed(uri)?.app === app) return
175
- watchers.get(app)?.close()
176
- watchers.delete(app)
177
157
  const checker = checkers.get(app)
178
158
  checkers.delete(app)
179
- await checker?.close().catch(() => {})
159
+ await checker?.close()
180
160
  }
181
161
 
182
162
  const run = async (uri: string): Promise<void> => {
@@ -189,17 +169,11 @@ export async function serveLanguage(
189
169
  const { app, path } = at
190
170
  let found: Diagnosis[]
191
171
  try {
192
- found = await checkSource(
193
- await checkerOf(app),
194
- path,
195
- document.text,
196
- held,
172
+ found = await checkerOf(app).run((checker) =>
173
+ checkSource(checker, path, document.text, held),
197
174
  )
198
175
  } catch (thrown) {
199
176
  // The checker's own failure, not the page's: the next check opens another.
200
- const failed = checkers.get(app)
201
- checkers.delete(app)
202
- await failed?.close().catch(() => {})
203
177
  found = [
204
178
  {
205
179
  path,
@@ -269,7 +243,7 @@ export async function serveLanguage(
269
243
  const at = placed(uri)
270
244
  if (document === undefined || at === undefined) return undefined
271
245
  return question(
272
- await checkerOf(at.app),
246
+ await checkerOf(at.app).live(),
273
247
  at.path,
274
248
  document.text,
275
249
  offsetOf(document.text, position),
@@ -544,24 +518,16 @@ export async function serveLanguage(
544
518
  ) as Message
545
519
  buffer = buffer.slice(start + length)
546
520
  if (!(await handle(message))) {
547
- await closeAll(checkers, watchers)
521
+ await closeAll(checkers)
548
522
  return
549
523
  }
550
524
  }
551
525
  }
552
- await closeAll(checkers, watchers)
526
+ await closeAll(checkers)
553
527
  }
554
528
 
555
- async function closeAll(
556
- checkers: Map<string, AppChecker>,
557
- watchers: Map<string, FSWatcher>,
558
- ): Promise<void> {
559
- for (const watcher of watchers.values()) watcher.close()
560
- await Promise.all(
561
- [...checkers.values()].map((checker) =>
562
- checker.close().catch(() => {}),
563
- ),
564
- )
529
+ async function closeAll(checkers: Map<string, KeptChecker>): Promise<void> {
530
+ await Promise.all([...checkers.values()].map((checker) => checker.close()))
565
531
  }
566
532
 
567
533
  /**
@@ -9,7 +9,7 @@ import type {
9
9
  } from 'typescript/unstable/async'
10
10
 
11
11
  import { classifyRead, FUNCTIONS } from './classifyRead.ts'
12
- import type { Finding } from './Finding.ts'
12
+ import type { Finding } from './Diagnostics.ts'
13
13
  import type { Span } from './projectShadow.ts'
14
14
 
15
15
  // 34.18 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
@@ -1,8 +1,8 @@
1
1
  import type { CheckerAnswer } from './CheckerAnswer.ts'
2
2
  import type { Read } from './classifyRead.ts'
3
3
  import { isComponentTag } from './classifyTemplate.ts'
4
+ import { type Diagnosis, diagnosisAt } from './Diagnostics.ts'
4
5
  import { isNamedAlone } from './isNamedAlone.ts'
5
- import { diagnosisAt } from './lineOf.ts'
6
6
  import { Lowering } from './Lowering.ts'
7
7
  import type { AbideFile } from './parseAbide.ts'
8
8
  import {
@@ -12,7 +12,6 @@ import {
12
12
  type TemplateNode,
13
13
  visitBodies,
14
14
  } from './TemplateNode.ts'
15
- import type { Diagnosis } from './TypeSession.ts'
16
15
 
17
16
  // 32.14 — A `{#for}` WITH NO KEY OVER A BODY THAT HOLDS STATE. Unkeyed, a row is matched to the
18
17
  // item at its index, so a reorder hands one row's state — a component's own values, a field's text,
@@ -16,7 +16,7 @@ import {
16
16
  } from 'typescript/unstable/ast/is'
17
17
 
18
18
  import { FUNCTIONS, TRANSPARENT } from './classifyRead.ts'
19
- import type { Finding } from './Finding.ts'
19
+ import type { Finding } from './Diagnostics.ts'
20
20
  import type { PositionKind, PositionTable } from './PositionTable.ts'
21
21
 
22
22
  // 14.14, 14.15, 14.16 — WHERE A READ STANDS IN A BODY THAT TRACKS, and the three ways it can stand
@@ -1,13 +1,9 @@
1
- // WHERE THE BROWSER BUNDLE LANDS, named once for the commands that have to agree about it:
2
- // `abide build` writes it, `abide start` answers it, and `abide dev` builds it into memory
3
- // (38.16, 38.20). A leaf, with no imports of its own, for the reason every constant crossing a
4
- // seam is one — the build is in `bin/` and the server is in `src/server/`, and a string spelled out
5
- // in both is a build that silently serves nothing. The addresses it is answered at are
1
+ // WHAT THE BROWSER BUNDLE IS NAMED, once for the seams that have to agree about it: the compiler
2
+ // writes the entry, `abide build` precompresses every file, and the server answers both (38.5,
3
+ // 38.20). A leaf, with no imports of its own, for the reason every constant crossing a seam is one.
4
+ // Where the bundle lands in a build is `appBuild.ts`'s; the addresses it is answered at are
6
5
  // `#shared/ADDRESSES.ts`.
7
6
 
8
- /** The directory the bundle lands in, below the build's own output directory. */
9
- export const CLIENT_DIRECTORY = 'client'
10
-
11
7
  /**
12
8
  * The module that mounts the app — the one bundle file whose name the shell writes out.
13
9
  *
@@ -19,21 +15,6 @@ export const CLIENT_DIRECTORY = 'client'
19
15
  */
20
16
  export const CLIENT_ENTRY = 'entry'
21
17
 
22
- /**
23
- * The manifest file `abide build` writes beside the client directory.
24
- *
25
- * 38.5 — the manifest `manifestOf` reads off the bundle, below the build's own output directory
26
- * and BESIDE the client directory rather than in it: everything in there is answered under
27
- * `CLIENT_ADDRESS` (38.20), and the manifest is the server's, not the browser's.
28
- */
29
- export const MANIFEST = 'manifest.json'
30
-
31
- /**
32
- * The compilation `abide build` keeps beside the client directory: every `.abide` compiled, and
33
- * the build's own tables (`Compilation`), which `abide start` serves without the compiler.
34
- */
35
- export const COMPILATION = 'compilation.json'
36
-
37
18
  /**
38
19
  * The encodings a response can be compressed in, each with its precompressed file's suffix.
39
20
  *
@@ -43,12 +24,3 @@ export const COMPILATION = 'compilation.json'
43
24
  * a build that precompresses into files nothing ever serves.
44
25
  */
45
26
  export const ENCODING_SUFFIX = { br: '.br', gzip: '.gz' } as const
46
-
47
- /**
48
- * Where `abide build` copies every `public` directory, each file at its address, below its own
49
- * output directory. The source's own name for one is `#shared/RESERVED_DIRECTORIES.ts`'s.
50
- *
51
- * 38.18 — a directory rather than the source tree: `abide start` boots what the build produced and
52
- * reaches for nothing an app might have moved since.
53
- */
54
- export const PUBLIC_DIRECTORY = 'public'
@@ -0,0 +1,2 @@
1
+ /** How long a pairing whose stream dropped waits to be resumed. 44.26. */
2
+ export const RESUME_MILLISECONDS = 60_000
@@ -8,75 +8,43 @@
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
19
  import { HandlerRoute } from '#shared/HandlerRoute.ts'
27
20
  import { health } from '#shared/health.ts'
28
21
  import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
29
22
  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
23
  import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
33
24
  import { compose } from '#shared/Middleware.ts'
34
- import { MOUNT, mounted } from '#shared/mount.ts'
35
- import { MOUNT_META } from '#shared/MOUNT_META.ts'
25
+ import { mounted } from '#shared/mount.ts'
36
26
  import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
37
- import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
38
27
  import { notFound as refusedNotFound, refuse } from '#shared/refuse.ts'
39
28
  import { arrive } from '#shared/route.ts'
40
- import {
41
- parseRoutePattern,
42
- rankRoutes,
43
- type Segment,
44
- } from '#shared/routePattern.ts'
29
+ import type { Segment } from '#shared/routePattern.ts'
45
30
  import type { Method, Rpc } from '#shared/rpc.ts'
46
31
  import { rpcRequest } from '#shared/rpcRequest.ts'
47
32
 
48
33
  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'
34
+ import { answerRpc, type Mounted, mountedOwn } from './answerRpc.ts'
35
+ import { answerBlobTagged, answerValue } from './answerValue.ts'
59
36
  import { answerBridge } from './bridgeRelay.ts'
60
37
  import { callerOrMint } from './callerOf.ts'
61
- import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
62
38
  import { compressAnswer } from './compressAnswer.ts'
63
- import { settingOn } from './config.ts'
64
39
  import { noteCallInRender } from './deferredBuilds.ts'
65
40
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
66
41
  import { answerMcp, openApi } from './generatedSurfaces.ts'
67
- import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
68
- import { chunksOf, type Manifest } from './manifestOf.ts'
42
+ import { failureOf, type LaneRung } from './hooks.ts'
69
43
  import { muxAdmission } from './muxUpgrade.ts'
70
44
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
71
45
  import { plainAnswer } from './plainAnswer.ts'
72
46
  import { PRINCIPAL_MOUNT } from './principal.ts'
73
- import {
74
- boundPage,
75
- type Component,
76
- DEFAULT_DOCUMENT,
77
- intoHead,
78
- renderDocument,
79
- } from './render.ts'
47
+ import { boundPage, type Component, renderDocument } from './render.ts'
80
48
  import {
81
49
  adoptRequest,
82
50
  type AnsweringApp,
@@ -107,9 +75,9 @@ type ServerRoute = {
107
75
  // 35.2 — an error page, and the directory it answers for.
108
76
  type ServerError = Omit<ServerRoute, 'clientModules'>
109
77
 
110
- type RankedError = ServerError & { segments: Segment[] }
78
+ export type RankedError = ServerError & { segments: Segment[] }
111
79
 
112
- type Ranked = RankedError & {
80
+ export type Ranked = RankedError & {
113
81
  // The document this route is painted into, with the tags that load the app already in its
114
82
  // head (38.22, 38.23) and the preloads for this route's own modules (38.28). One per route and
115
83
  // built at boot, so a request pays for none of it.
@@ -122,7 +90,7 @@ type Ranked = RankedError & {
122
90
  * 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
91
  * "the most specific route answering the method" is a walk down the rows asking each column.
124
92
  */
125
- type RouteRow = {
93
+ export type RouteRow = {
126
94
  pattern: string
127
95
  matchers: Record<string, string>
128
96
  segments: Segment[]
@@ -141,7 +109,7 @@ type RouteRow = {
141
109
  }
142
110
 
143
111
  /** 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 }
112
+ export type PublicFile = { path: string; etag: string; type: string }
145
113
 
146
114
  /** Everything `answerRequest` answers from: the ranked routes, the lanes and the handlers. */
147
115
  export type AppSources = {
@@ -486,168 +454,8 @@ export function reachedInRender(
486
454
  */
487
455
  export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
488
456
 
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
457
  /** `found`'s answer inside the rungs filed over its route. 16.14, 37.2. */
650
- function answerInside(
458
+ export function answerInside(
651
459
  app: AppSources,
652
460
  found: Ranked | Mounted,
653
461
  rungs: LaneRung[],
@@ -655,103 +463,6 @@ function answerInside(
655
463
  return compose(rungs, ({ request }) => answerMatched(app, request, found))
656
464
  }
657
465
 
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
466
  function notFound(): Response {
756
467
  return plainAnswer('not found', 404)
757
468
  }