@abide/abide 0.123.1 → 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 (98) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/abide.ts +36 -65
  3. package/docs/REGISTRY.md +2 -2
  4. package/docs/RULEBOOK.md +2 -2
  5. package/docs/abide.md +2 -2
  6. package/package.json +1 -1
  7. package/src/compiler/Diagnostics.ts +198 -0
  8. package/src/compiler/KeptChecker.ts +97 -0
  9. package/src/compiler/Lowering.ts +1 -1
  10. package/src/compiler/PositionedRefusal.ts +1 -1
  11. package/src/compiler/TypeSession.ts +62 -85
  12. package/src/compiler/appLayout.ts +28 -20
  13. package/src/compiler/argsDefaultsIn.ts +1 -1
  14. package/src/compiler/checkApp.ts +64 -68
  15. package/src/compiler/checkedShadow.ts +1 -1
  16. package/src/compiler/compileApp.ts +101 -70
  17. package/src/compiler/declaredExports.ts +2 -4
  18. package/src/compiler/emitClient.ts +1 -1
  19. package/src/compiler/formatAbide.ts +2 -2
  20. package/src/compiler/handlersUnder.ts +17 -29
  21. package/src/compiler/lineOf.ts +0 -19
  22. package/src/compiler/matchersOf.ts +3 -3
  23. package/src/compiler/mountsOf.ts +3 -3
  24. package/src/compiler/readSources.ts +16 -18
  25. package/src/compiler/readsIn.ts +1 -1
  26. package/src/compiler/serveLanguage.ts +23 -57
  27. package/src/compiler/sharedModuleWrites.ts +1 -1
  28. package/src/compiler/templateWarnings.ts +1 -2
  29. package/src/compiler/trackingWarnings.ts +1 -1
  30. package/src/server/CLIENT_BUILD.ts +4 -32
  31. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  32. package/src/server/answerRequest.ts +11 -300
  33. package/src/server/answerRpc.ts +12 -54
  34. package/src/server/answerSocket.ts +8 -35
  35. package/src/server/appBuild.ts +377 -0
  36. package/src/server/appPlugins.ts +15 -17
  37. package/src/server/appTree.ts +6 -5
  38. package/src/server/argsContract.ts +209 -0
  39. package/src/server/bridgeRelay.ts +153 -65
  40. package/src/server/claudeCode.ts +30 -127
  41. package/src/server/config.ts +6 -7
  42. package/src/server/createApp.ts +30 -64
  43. package/src/server/devWorker.ts +11 -15
  44. package/src/server/handlerFileMount.ts +40 -0
  45. package/src/server/index.ts +2 -2
  46. package/src/server/packageDirectory.ts +14 -0
  47. package/src/server/preload.ts +3 -2
  48. package/src/server/principal.ts +4 -3
  49. package/src/server/remote.ts +1 -1
  50. package/src/server/runCompiled.ts +1 -1
  51. package/src/server/runDesktop.ts +1 -1
  52. package/src/server/seedBuffer.ts +1 -1
  53. package/src/server/serveDev.ts +24 -58
  54. package/src/server/servedApp.ts +390 -0
  55. package/src/server/signInChanged.ts +20 -0
  56. package/src/server/surfaceOf.ts +2 -1
  57. package/src/server/versionOf.ts +6 -15
  58. package/src/shared/applyPatch.ts +1 -1
  59. package/src/shared/muxFrames.ts +1 -1
  60. package/src/shared/reactive/Keyed.ts +1 -1
  61. package/src/shared/reactive/Reactive.ts +21 -8
  62. package/src/shared/reactive/argsContract.ts +131 -4
  63. package/src/shared/reactive/index.ts +1 -0
  64. package/src/shared/reactive/patchLog.ts +79 -0
  65. package/src/shared/reactive/patchMarks.ts +34 -0
  66. package/src/shared/reactive/recordWrites.ts +11 -14
  67. package/src/shared/reactive/share.ts +4 -3
  68. package/src/shared/rpc.ts +12 -19
  69. package/src/shared/validateJson.ts +14 -3
  70. package/src/ui/ArmRegion.ts +10 -4
  71. package/src/ui/Level.ts +11 -20
  72. package/src/ui/LevelChain.ts +272 -0
  73. package/src/ui/Router.ts +54 -245
  74. package/src/ui/Scope.ts +17 -3
  75. package/src/ui/TryRegion.ts +22 -23
  76. package/src/ui/bindSpread.ts +2 -4
  77. package/src/ui/listenForRoutes.ts +3 -10
  78. package/src/ui/outboxQueue.ts +21 -3
  79. package/src/ui/reconcile.ts +4 -6
  80. package/src/ui/routing.ts +2 -1
  81. package/src/ui/rows.ts +2 -2
  82. package/src/ui/setAttribute.ts +12 -3
  83. package/src/ui/socketWire.ts +26 -11
  84. package/src/compiler/Finding.ts +0 -2
  85. package/src/compiler/Refusals.ts +0 -65
  86. package/src/compiler/formatDiagnosis.ts +0 -20
  87. package/src/server/appDirectories.ts +0 -9
  88. package/src/server/argsFor.ts +0 -39
  89. package/src/server/buildDirectory.ts +0 -10
  90. package/src/server/coerceArgs.ts +0 -109
  91. package/src/server/derivedArgs.ts +0 -95
  92. package/src/server/runBuild.ts +0 -24
  93. package/src/server/serveBuild.ts +0 -144
  94. package/src/server/testBuild.ts +0 -220
  95. package/src/shared/reactive/FOLLOWED.ts +0 -8
  96. package/src/shared/reactive/PATCHES.ts +0 -11
  97. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  98. package/src/shared/reactive/canonicalKey.ts +0 -95
package/src/ui/routing.ts CHANGED
@@ -16,4 +16,5 @@
16
16
 
17
17
  export { listenForRoutes } from './listenForRoutes.ts'
18
18
  export { registerWorker } from './registerWorker.ts'
19
- export { type PageMount, type Route, Router, startRouter } from './Router.ts'
19
+ export { type PageMount } from './LevelChain.ts'
20
+ export { type Route, Router, startRouter } from './Router.ts'
package/src/ui/rows.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { REGION_CLOSE, ROW_OPEN } from '#shared/ANCHORS.ts'
2
2
  import { moveItem, rowItem } from '#shared/reactive/index.ts'
3
- import { PATCHES } from '#shared/reactive/PATCHES.ts'
3
+ import { patchEpoch } from '#shared/reactive/patchMarks.ts'
4
4
 
5
5
  import { leave } from './leave.ts'
6
6
  import { moveNode } from './moveNode.ts'
@@ -291,7 +291,7 @@ function newRow(key: unknown, data: unknown, node: ChildNode, at: number): Row {
291
291
  // for the record's whole life.
292
292
  scope: EMPTY_SCOPE,
293
293
  at,
294
- seen: PATCHES.epoch,
294
+ seen: patchEpoch(),
295
295
  }
296
296
  }
297
297
 
@@ -35,10 +35,19 @@ export function setAttribute(
35
35
  if (element.hasAttribute(name)) element.removeAttribute(name)
36
36
  return
37
37
  }
38
+ writeAttribute(element, name, next)
39
+ }
40
+
41
+ /** Writes `text` to `element`'s `name` unless it already reads `text` — `setAttribute`'s compare, shared with a spread. */
42
+ export function writeAttribute(
43
+ element: Element,
44
+ name: string,
45
+ text: string,
46
+ ): void {
38
47
  const standing = element.getAttribute(name)
39
- if (standing === next) return
40
- if (standing === null) addAttribute(element, name, next)
41
- else element.setAttribute(name, next)
48
+ if (standing === text) return
49
+ if (standing === null) addAttribute(element, name, text)
50
+ else element.setAttribute(name, text)
42
51
  }
43
52
 
44
53
  /**
@@ -7,7 +7,7 @@ import { applyPatch } from '#shared/applyPatch.ts'
7
7
  import { mounted } from '#shared/mount.ts'
8
8
  import type { BrowserFrame, ServerFrame } from '#shared/muxFrames.ts'
9
9
  import { onPrincipalRotated } from '#shared/principalRotated.ts'
10
- import { canonicalKey } from '#shared/reactive/canonicalKey.ts'
10
+ import { canonicalKey } from '#shared/reactive/argsContract.ts'
11
11
  import {
12
12
  heldGenerationOf,
13
13
  loadOf,
@@ -16,7 +16,7 @@ import {
16
16
  type Reactive,
17
17
  writtenLocally,
18
18
  } from '#shared/reactive/index.ts'
19
- import { PATCHES } from '#shared/reactive/PATCHES.ts'
19
+ import { patchEpoch, writtenSince } from '#shared/reactive/patchMarks.ts'
20
20
 
21
21
  import { HYDRATE_LOG } from './Mismatch.ts'
22
22
  import { undelivered, whenDelivered } from './outboxQueue.ts'
@@ -64,18 +64,13 @@ class Following {
64
64
  inStep(): void {
65
65
  const copy = this.entry()
66
66
  this.generation = loadOf(copy) ?? heldGenerationOf(copy)
67
- this.epoch = PATCHES.epoch
67
+ this.epoch = patchEpoch()
68
68
  }
69
69
 
70
70
  /** Whether the copy was written to since it was last in step. 18.21. */
71
71
  diverged(copy: unknown): boolean {
72
72
  if (heldGenerationOf(copy) !== this.generation) return true
73
- const held = (copy as Reactive<unknown>).peek()
74
- return (
75
- held !== null &&
76
- typeof held === 'object' &&
77
- (PATCHES.written.get(held) ?? 0) > this.epoch
78
- )
73
+ return writtenSince((copy as Reactive<unknown>).peek(), this.epoch)
79
74
  }
80
75
 
81
76
  /**
@@ -241,10 +236,17 @@ class Mux {
241
236
  receive(frame: ServerFrame): void {
242
237
  this.subscriptions.get(frame.id)?.receive(frame)
243
238
  }
239
+
240
+ /** Lets go of every subscription and the socket, so a sign-in or a retry reaches nothing. */
241
+ dispose(): void {
242
+ clearTimeout(this.retrying)
243
+ this.retrying = undefined
244
+ this.subscriptions.clear()
245
+ this.reopen()
246
+ }
244
247
  }
245
248
 
246
- // Exported for a test to forget: a page never does, and a test process outlives one.
247
- export const MUXES = new Map<string, Mux>()
249
+ const MUXES = new Map<string, Mux>()
248
250
 
249
251
  /**
250
252
  * The mux a handler's address is carried over, made on first use.
@@ -276,6 +278,19 @@ function muxFor(address: string): Mux {
276
278
  */
277
279
  const FOLLOWING = new Map<string, Following>()
278
280
 
281
+ /**
282
+ * Forgets every mux and follow this page opened, their timers and sockets with them.
283
+ *
284
+ * A page never does; a test process outlives one, and a later file's sign-in (18.25) would reopen
285
+ * every mux an earlier file left standing.
286
+ */
287
+ export function resetSocketWire(): void {
288
+ for (const following of FOLLOWING.values()) clearTimeout(following.expiry)
289
+ FOLLOWING.clear()
290
+ for (const mux of MUXES.values()) mux.dispose()
291
+ MUXES.clear()
292
+ }
293
+
279
294
  /**
280
295
  * Reads again every followed copy written here, once the outbox holds no call waiting.
281
296
  *
@@ -1,2 +0,0 @@
1
- /** A warning or a refusal at a projection offset. */
2
- export type Finding = { at: number; message: string }
@@ -1,65 +0,0 @@
1
- import { resolve } from 'node:path' // bun exposes no path helpers of its own
2
-
3
- import { formatDiagnosis } from './formatDiagnosis.ts'
4
- import { diagnosisAt } from './lineOf.ts'
5
- import { PositionedRefusal } from './PositionedRefusal.ts'
6
- import type { Diagnosis } from './TypeSession.ts'
7
-
8
- /**
9
- * Where everything the compile refuses goes, each at the file it was refused in.
10
- *
11
- * TWO MODES AND ONE CALL: a build stops at a refusal, so a throwing sink throws it; `abide
12
- * check` reports every one at its file and goes on (38.33), so a collecting sink keeps each as a
13
- * finding at the file's first line. Every stage takes this and nothing else, so no stage decides for
14
- * itself what a refusal does, and none spells a path its own way: a file is named relative to `root`
15
- * or absolutely, and kept absolute.
16
- */
17
- export class Refusals {
18
- /** Every refusal kept, in the order met; empty in a throwing sink. */
19
- readonly found: Diagnosis[] = []
20
- /** What a file below the root is named relative to. */
21
- readonly root: string
22
- /** Whether a refusal is kept and the compile goes on, rather than thrown. */
23
- readonly collecting: boolean
24
-
25
- constructor(root: string, collecting: boolean) {
26
- this.root = root
27
- this.collecting = collecting
28
- }
29
-
30
- /**
31
- * Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. Handed the file's
32
- * `text`, a `PositionedRefusal` is placed on its line and column, and thrown as that line.
33
- */
34
- at(file: string, thrown: unknown, text?: string): void {
35
- const positioned =
36
- text !== undefined && thrown instanceof PositionedRefusal
37
- if (!this.collecting && !positioned) throw thrown
38
- const diagnosis = diagnosisAt(
39
- resolve(this.root, file),
40
- positioned ? text : undefined,
41
- positioned ? thrown.offset : 0,
42
- 'error',
43
- thrown instanceof Error ? thrown.message : String(thrown),
44
- )
45
- if (!this.collecting)
46
- throw new Error(formatDiagnosis(diagnosis, process.cwd()))
47
- this.found.push(diagnosis)
48
- }
49
-
50
- /**
51
- * What another stage kept, as this one takes it: every one kept, or every one thrown as one
52
- * error, each at its file — a build that stops names all it found rather than a first one and a
53
- * second run (11.37).
54
- */
55
- adopt(found: readonly Diagnosis[]): void {
56
- if (found.length === 0) return
57
- if (this.collecting) {
58
- this.found.push(...found)
59
- return
60
- }
61
- const lines: string[] = []
62
- for (const one of found) lines.push(formatDiagnosis(one, process.cwd()))
63
- throw new Error(lines.join('\n'))
64
- }
65
- }
@@ -1,20 +0,0 @@
1
- import { relative } from 'node:path' // bun exposes no path helpers of its own
2
-
3
- import type { Diagnosis } from './TypeSession.ts'
4
-
5
- /**
6
- * One finding as one `tsc`-style line.
7
- *
8
- * ONE LINE A FINDING, in the shape `tsc` and every editor's problem matcher already read:
9
- * `file:line:column - error TS2322: …`, the path relative to where the command was run. A finding
10
- * with no line — a whole-file or whole-app refusal — names the file alone.
11
- */
12
- export function formatDiagnosis(diagnosis: Diagnosis, from: string): string {
13
- const path = relative(from, diagnosis.path) || diagnosis.path
14
- const where =
15
- diagnosis.line === 0
16
- ? path
17
- : `${path}:${diagnosis.line}:${diagnosis.column}`
18
- const code = diagnosis.code === 0 ? '' : ` TS${diagnosis.code}`
19
- return `${where} - ${diagnosis.category}${code}: ${diagnosis.message}`
20
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * The directories an app's own files stand in: `root`, and the one above it.
3
- *
4
- * `root` is the app's root — `src/` — and the one above it is the package beside
5
- * it. The app's `package.json` and `bunfig.toml` are found in the first that has one.
6
- */
7
- export function appDirectories(root: string): string[] {
8
- return [root, `${root}/..`]
9
- }
@@ -1,39 +0,0 @@
1
- // WHAT A CALL'S ARGS ARE BEFORE ITS HANDLER SEES THEM, for every lane that lets a call to a handler in
2
- // off the wire: over http (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the
3
- // args off its own transport and answers a refusal in its own form.
4
-
5
- import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
6
- import { normalized } from '#shared/reactive/argsContract.ts'
7
- import { validationError } from '#shared/refuse.ts'
8
-
9
- import type { Mounted } from './answerRpc.ts'
10
- import { coerceArgs } from './coerceArgs.ts'
11
-
12
- /**
13
- * The args a call over the wire is answered with, out of the args it sent: the text a URL or a form
14
- * carried coerced where `query` says the args are text, the handler's parameter defaults laid in, and
15
- * the result held to the handler's annotation. Throws the refusal where it is not held.
16
- *
17
- * 19.7 — the text as the types the handler's `args` declares. 11.27 — then the defaults, ahead of
18
- * every key taken after it: the seed a render filed and the `global` entry standing were both filed
19
- * under them (11.29). 19.9 — THE WIRE HELD TO THE ANNOTATION, where no `args` schema is declared to
20
- * hold it — the check the handler carries (`derivedArgs`), which its in-process call makes too (15.8).
21
- */
22
- export function argsFor(
23
- found: Pick<Mounted, 'handler' | 'argsDocument'>,
24
- sent: unknown,
25
- query: string | undefined,
26
- ): unknown {
27
- const coerced =
28
- query === undefined
29
- ? sent
30
- : coerceArgs(
31
- sent as Record<string, string | string[]> | undefined,
32
- found.argsDocument,
33
- query,
34
- )
35
- const args = normalized(found.handler, coerced)
36
- const issues = found.handler[DERIVED_ARGS]?.(args) ?? null
37
- if (issues !== null) throw validationError(issues)
38
- return args
39
- }
@@ -1,10 +0,0 @@
1
- import { dirname } from 'node:path' // bun exposes no path helpers of its own
2
-
3
- /**
4
- * Where an app's build goes: `.abide` in the directory above its source root.
5
- *
6
- * BESIDE THE APP RATHER THAN BESIDE THE SHELL: an app's build belongs to the app.
7
- */
8
- export function buildDirectory(root: string): string {
9
- return `${dirname(root)}/.abide`
10
- }
@@ -1,109 +0,0 @@
1
- // 19.7 — A `GET` OR `DELETE`'s TEXT ARGS, COERCED FROM THE TYPE THE `JsonSchema` DECLARES, before
2
- // anything validates them. A URL carries text, so `?id=42` is `'42'` until the schema says the member
3
- // is a number. Coercion is the pipeline's and not the schema's (19.6): the schema is handed what this
4
- // made of the text, and refuses what would not coerce.
5
- //
6
- // Read off a `Schema` built by `jsonSchema`, where the document is exposed; a schema with no document
7
- // has declared no types, and its args stay the text they arrived as.
8
-
9
- import { numberFromText } from '#shared/numberFromText.ts'
10
- import { bareName } from '#shared/queryText.ts'
11
- import { type Gated, gateOf } from '#shared/reactive/argsContract.ts'
12
-
13
- export type Document = {
14
- type?: string | string[]
15
- properties?: Record<string, Document>
16
- items?: Document
17
- }
18
-
19
- /** The document a handler's `args` schema declares, or `undefined`. */
20
- export function argsDocumentOf(handler: Gated<unknown>): Document | undefined {
21
- const gate = gateOf(handler) as
22
- { '~standard'?: { jsonSchema?: { input?: () => unknown } } } | undefined
23
- const document = gate?.['~standard']?.jsonSchema?.input?.()
24
- return document !== null && typeof document === 'object'
25
- ? (document as Document)
26
- : undefined
27
- }
28
-
29
- /**
30
- * Name and value pairs as args: a name given more than once is a list, one given once its text.
31
- *
32
- * 16.18 — a query's and a form's members, and 38.61 — `abide call`'s `name=value` words, alike.
33
- */
34
- export function collapsed(
35
- entries: URLSearchParams | FormData,
36
- ): Record<string, string | string[]> {
37
- const args: Record<string, string | string[]> = {}
38
- for (const name of new Set(entries.keys())) {
39
- const values = entries.getAll(name) as string[]
40
- args[name] = values.length === 1 ? (values[0] as string) : values
41
- }
42
- return args
43
- }
44
-
45
- /**
46
- * The text args, each member coerced to the type `document` declares.
47
- *
48
- * `query` is the raw `?…` the args were read from: `URLSearchParams` reads `rpcRequest`'s empty
49
- * list, a bare name, as `''` — the same as `tags=`, the list `['']` — so a declared array tells the
50
- * two apart there. A text member keeps the `''`.
51
- */
52
- export function coerceArgs(
53
- args: Record<string, string | string[]> | undefined,
54
- document: Document | undefined,
55
- query: string,
56
- ): unknown {
57
- if (args === undefined || document?.properties === undefined) return args
58
- const coerced: Record<string, unknown> = {}
59
- for (const [name, text] of Object.entries(args)) {
60
- const member = document.properties[name]
61
- if (member === undefined) coerced[name] = text
62
- else if (
63
- text === '' &&
64
- typesOf(member).includes('array') &&
65
- `&${query.slice(1)}&`.includes(`&${bareName(name)}&`)
66
- )
67
- coerced[name] = []
68
- else coerced[name] = coerceMember(text, member)
69
- }
70
- return coerced
71
- }
72
-
73
- function typesOf(member: Document): string[] {
74
- if (member.type === undefined) return []
75
- return Array.isArray(member.type) ? member.type : [member.type]
76
- }
77
-
78
- function coerceMember(text: string | string[], member: Document): unknown {
79
- const types = typesOf(member)
80
- // A list is its member repeated, and one given once still answers a declared array.
81
- if (types.includes('array')) {
82
- const items = Array.isArray(text) ? text : [text]
83
- const item = member.items
84
- return item === undefined
85
- ? items
86
- : items.map((each) => coerceScalar(each, item))
87
- }
88
- if (Array.isArray(text)) return text
89
- return coerceScalar(text, member)
90
- }
91
-
92
- /**
93
- * The text as the first declared type it reads as, in the order the schema lists them.
94
- *
95
- * Text that reads as none stays text, for the schema to refuse.
96
- */
97
- function coerceScalar(text: string, member: Document): unknown {
98
- for (const type of typesOf(member)) {
99
- if (type === 'string') return text
100
- if (type === 'number' || type === 'integer') {
101
- const number = numberFromText(text)
102
- if (number !== undefined) return number
103
- }
104
- if (type === 'boolean' && (text === 'true' || text === 'false'))
105
- return text === 'true'
106
- if (type === 'null' && text === 'null') return null
107
- }
108
- return text
109
- }
@@ -1,95 +0,0 @@
1
- import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
2
- import type { JsonValue } from '#shared/JsonValue.ts'
3
- import type { Gated } from '#shared/reactive/argsContract.ts'
4
- import type { HeldToAnnotation } from '#shared/rpc.ts'
5
- import { validateJson } from '#shared/validateJson.ts'
6
-
7
- import { argsDocumentOf } from './coerceArgs.ts'
8
-
9
- /**
10
- * Holds `handler`'s args to `schema`, the one the build derived off its annotation, for every call
11
- * of it from now on: in process and over the wire (15.8, 19.9).
12
- *
13
- * What the module a handler is loaded through calls once its handler is built — a handler file's
14
- * face and a `<script server>`'s module — so the schema is there wherever the handler can be
15
- * called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
16
- * own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
17
- * schema is only derived where the handler's args are a required record, so a required member
18
- * missing is refused, not read off nothing. The args are held as SENT (`asSent`): a call in process
19
- * passing a member `undefined` is the call the wire carries without it.
20
- */
21
- export function derivedArgs(
22
- handler: HeldToAnnotation & Gated<unknown>,
23
- schema: JsonValue,
24
- ): void {
25
- if (argsDocumentOf(handler) !== undefined) return
26
- handler[DERIVED_ARGS] = (args) => validateJson(schema, asSent(args ?? {}))
27
- }
28
-
29
- /** The records `asSent` is inside of, so a cyclic value is walked once rather than forever. */
30
- const WALKING: object[] = []
31
-
32
- /**
33
- * `value` as the wire carries it, which drops a member left `undefined` (19.25): a URL omits it
34
- * (30.28) and JSON leaves it out, at any depth of a body. `value` itself wherever it holds none, so
35
- * the walk allocates nothing for the call that writes no `undefined` member.
36
- *
37
- * AN `undefined` IN A LIST IS KEPT. The two encodings part there — JSON writes `null`, a URL the
38
- * text `undefined` — so neither is the wire's, and the schema answers the value as given.
39
- */
40
- export function asSent(value: unknown): unknown {
41
- if (value === null || typeof value !== 'object' || WALKING.includes(value))
42
- return value
43
- WALKING.push(value)
44
- try {
45
- return Array.isArray(value)
46
- ? listAsSent(value)
47
- : recordAsSent(value as Record<string, unknown>)
48
- } finally {
49
- WALKING.pop()
50
- }
51
- }
52
-
53
- function listAsSent(list: unknown[]): unknown[] {
54
- let sent: unknown[] | null = null
55
- for (let index = 0; index < list.length; index += 1) {
56
- const member = list[index]
57
- const held = asSent(member)
58
- if (sent === null && held !== member) sent = list.slice(0, index)
59
- sent?.push(held)
60
- }
61
- return sent ?? list
62
- }
63
-
64
- function recordAsSent(
65
- record: Record<string, unknown>,
66
- ): Record<string, unknown> {
67
- let sent: Record<string, unknown> | null = null
68
- for (const key in record) {
69
- if (!Object.hasOwn(record, key)) continue
70
- const member = record[key]
71
- const held = member === undefined ? undefined : asSent(member)
72
- if (sent === null && (member === undefined || held !== member))
73
- sent = membersBefore(record, key)
74
- if (sent !== null && held !== undefined) sent[key] = held
75
- }
76
- return sent ?? record
77
- }
78
-
79
- /**
80
- * A copy of `record`'s own members ahead of `key`, in their order.
81
- *
82
- * NO PROTOTYPE, so an own `__proto__` member JSON parsed stays a member the schema reads rather
83
- * than becoming the copy's prototype.
84
- */
85
- function membersBefore(
86
- record: Record<string, unknown>,
87
- key: string,
88
- ): Record<string, unknown> {
89
- const copy: Record<string, unknown> = Object.create(null)
90
- for (const earlier in record) {
91
- if (earlier === key) break
92
- if (Object.hasOwn(record, earlier)) copy[earlier] = record[earlier]
93
- }
94
- return copy
95
- }
@@ -1,24 +0,0 @@
1
- // The CLI, spawned rather than imported: the compiler is not reachable from the server seam.
2
- const ABIDE_BIN = Bun.fileURLToPath(
3
- new URL('../../bin/abide.ts', import.meta.url),
4
- )
5
-
6
- /**
7
- * `abide build` of the app under `root` into `out`, run as the CLI.
8
- *
9
- * SPAWNED RATHER THAN IMPORTED: the compiler is not reachable from the server seam.
10
- */
11
- export async function runBuild(root: string, out: string): Promise<void> {
12
- const child = Bun.spawn(
13
- [process.execPath, ABIDE_BIN, 'build', '--root', root, '--out', out],
14
- { stdout: 'ignore', stderr: 'pipe' },
15
- )
16
- const [exitCode, said] = await Promise.all([
17
- child.exited,
18
- new Response(child.stderr).text(),
19
- ])
20
- if (exitCode !== 0)
21
- throw new Error(
22
- `abide: could not build the app under ${root}.\n${said}`,
23
- )
24
- }
@@ -1,144 +0,0 @@
1
- // 38.6 and 38.10 — `abide start` and a compiled app: the built app, over http. The request path is
2
- // `answerRequest`'s, shared with `abide dev`; what this module is, is the SUPPLIER — where each part
3
- // comes from when the parts are what `abide build` produced.
4
- //
5
- // THE ROUTE TABLE IS THE BUILD'S, not this module's. `abide build` walks the tree once and writes
6
- // what it found, so the addresses the server answers and the addresses the client router knows
7
- // are two readings of one walk rather than two walks that have to agree (30.8).
8
-
9
- import {
10
- type Compilation,
11
- serveCompilation,
12
- servedTable,
13
- } from './abideModules.ts'
14
- import {
15
- type AppSources,
16
- appSources,
17
- type SourceParts,
18
- } from './answerRequest.ts'
19
- import {
20
- CLIENT_DIRECTORY,
21
- COMPILATION,
22
- ENCODING_SUFFIX,
23
- MANIFEST,
24
- PUBLIC_DIRECTORY,
25
- } from './CLIENT_BUILD.ts'
26
- import { bundleServer, type ServedFile } from './clientBundle.ts'
27
- import { settingOn } from './config.ts'
28
- import type { Manifest } from './manifestOf.ts'
29
- import { namesUnder } from './namesUnder.ts'
30
- import { ENCODINGS } from './negotiateEncoding.ts'
31
- import { runBuild } from './runBuild.ts'
32
-
33
- /**
34
- * What `abide build` produced, as the parts an `App` is booted over.
35
- *
36
- * Read off disk by `abide start` (`loadBuild`), and out of the executable itself by a compiled app
37
- * (38.10). ONE SHAPE, so the two are answered by one path and differ only in where a byte is read
38
- * from — a file is a PATH in both, and `Bun.file` reads the executable's own the same way it reads
39
- * the disk's.
40
- */
41
- export type BuildParts = Omit<SourceParts, 'client' | 'explained'> & {
42
- manifest: Manifest
43
- /** Every file under `client/`, the precompressed copies among them, by name to its path. */
44
- client: Map<string, string>
45
- }
46
-
47
- /**
48
- * Serves the compilation `abide build` kept in `out` to this process's imports; `false` where there
49
- * is none, because the app under `root` holds nothing to build.
50
- *
51
- * 38.6 — what `abide start` boots: no compiler is loaded, and every `.abide` the app imports is
52
- * answered out of what the build compiled. 38.100 — AN APP NOBODY BUILT IS BUILT, by `build`, rather
53
- * than refused, so every caller boots the same way whether or not a build came first. The CLI passes
54
- * its own build, in-process; anything else spawns it.
55
- */
56
- export async function serveBuild(
57
- root: string,
58
- out: string,
59
- build: (root: string, out: string) => Promise<unknown> = runBuild,
60
- ): Promise<boolean> {
61
- const path = `${out}/${COMPILATION}`
62
- if (!(await Bun.file(path).exists())) {
63
- await build(root, out)
64
- // A FRESH HANDLE: one taken before the build wrote the file reads it as empty.
65
- if (!(await Bun.file(path).exists())) return false
66
- }
67
- serveCompilation(root, (await Bun.file(path).json()) as Compilation)
68
- return true
69
- }
70
-
71
- /**
72
- * What `abide build` left on disk, as the sources an `App` is booted over — its compilation served
73
- * already (`serveBuild`).
74
- */
75
- export async function loadBuild(options: {
76
- /**
77
- * The app's source root — the directory its pages sit under.
78
- */
79
- root: string
80
- /** Where `abide build` put its output. */
81
- out: string
82
- }): Promise<AppSources> {
83
- // THE BOOT READS OVERLAP: the tables and the manifest are independent of each other. The
84
- // manifest names the stylesheet only where the bundler collected one, so an app that imports
85
- // none links none.
86
- const [table, manifest] = await Promise.all([
87
- servedTable(options.root),
88
- Bun.file(`${options.out}/${MANIFEST}`).json() as Promise<Manifest>,
89
- ])
90
- return builtSources({
91
- table,
92
- handlers: table.handlers,
93
- manifest,
94
- client: pathsUnder(`${options.out}/${CLIENT_DIRECTORY}`),
95
- published: pathsUnder(`${options.out}/${PUBLIC_DIRECTORY}`),
96
- })
97
- }
98
-
99
- /** Every file under `directory`, by the name below it to its path. */
100
- export function pathsUnder(directory: string): Map<string, string> {
101
- const paths = new Map<string, string>()
102
- for (const name of namesUnder(directory))
103
- paths.set(name, `${directory}/${name}`)
104
- return paths
105
- }
106
-
107
- /** The sources an `App` is booted over, out of the parts `abide build` produced. */
108
- export function builtSources(parts: BuildParts): Promise<AppSources> {
109
- // THE PRECOMPRESSED COPIES ARE NOT ADDRESSES OF THEIR OWN. They are the same file in another
110
- // encoding (38.5), so they are found under the name they encode and served as that name.
111
- const suffixes = Object.values(ENCODING_SUFFIX) as string[]
112
- const files = new Map<string, ServedFile>()
113
- for (const [name, path] of parts.client) {
114
- if (suffixes.some((suffix) => name.endsWith(suffix))) continue
115
- const encoded: ServedFile['encoded'] = {}
116
- for (const encoding of ENCODINGS) {
117
- const copy = parts.client.get(name + ENCODING_SUFFIX[encoding])
118
- if (copy !== undefined) encoded[encoding] = () => Bun.file(copy)
119
- }
120
- files.set(name, {
121
- // THE TYPE IS THE NAME'S, not the path's: a compiled app's path carries a hash the
122
- // bundler put before the extension, and a `.br`'s extension names nothing.
123
- type: Bun.file(name).type,
124
- body: () => Bun.file(path),
125
- encoded,
126
- })
127
- }
128
- return appSources({
129
- ...parts,
130
- explained: undefined,
131
- client: {
132
- manifest: parts.manifest,
133
- head: '',
134
- // 21.5 — EVERY NAME IN THE BUNDLE CARRIES A HASH OF ITS BYTES, the entry's too (38.5), so
135
- // a name never answers two different files and a year is safe for all of them. A deploy
136
- // is new names in a new document, never new bytes at an old address.
137
- framework: bundleServer(
138
- files,
139
- 'public, max-age=31536000, immutable',
140
- ),
141
- offline: settingOn('ABIDE_OFFLINE', true),
142
- },
143
- })
144
- }