@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.
- package/CHANGELOG.md +33 -0
- package/bin/abide.ts +36 -65
- package/docs/REGISTRY.md +2 -2
- package/docs/RULEBOOK.md +2 -2
- package/docs/abide.md +2 -2
- package/package.json +1 -1
- package/src/compiler/Diagnostics.ts +198 -0
- package/src/compiler/KeptChecker.ts +97 -0
- package/src/compiler/Lowering.ts +1 -1
- package/src/compiler/PositionedRefusal.ts +1 -1
- package/src/compiler/TypeSession.ts +62 -85
- package/src/compiler/appLayout.ts +28 -20
- package/src/compiler/argsDefaultsIn.ts +1 -1
- package/src/compiler/checkApp.ts +64 -68
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +101 -70
- package/src/compiler/declaredExports.ts +2 -4
- package/src/compiler/emitClient.ts +1 -1
- package/src/compiler/formatAbide.ts +2 -2
- package/src/compiler/handlersUnder.ts +17 -29
- package/src/compiler/lineOf.ts +0 -19
- package/src/compiler/matchersOf.ts +3 -3
- package/src/compiler/mountsOf.ts +3 -3
- package/src/compiler/readSources.ts +16 -18
- package/src/compiler/readsIn.ts +1 -1
- package/src/compiler/serveLanguage.ts +23 -57
- package/src/compiler/sharedModuleWrites.ts +1 -1
- package/src/compiler/templateWarnings.ts +1 -2
- package/src/compiler/trackingWarnings.ts +1 -1
- package/src/server/CLIENT_BUILD.ts +4 -32
- package/src/server/RESUME_MILLISECONDS.ts +2 -0
- package/src/server/answerRequest.ts +11 -300
- package/src/server/answerRpc.ts +12 -54
- package/src/server/answerSocket.ts +8 -35
- package/src/server/appBuild.ts +377 -0
- package/src/server/appPlugins.ts +15 -17
- package/src/server/appTree.ts +6 -5
- package/src/server/argsContract.ts +209 -0
- package/src/server/bridgeRelay.ts +153 -65
- package/src/server/claudeCode.ts +30 -127
- package/src/server/config.ts +6 -7
- package/src/server/createApp.ts +30 -64
- package/src/server/devWorker.ts +11 -15
- package/src/server/handlerFileMount.ts +40 -0
- package/src/server/index.ts +2 -2
- package/src/server/packageDirectory.ts +14 -0
- package/src/server/preload.ts +3 -2
- package/src/server/principal.ts +4 -3
- package/src/server/remote.ts +1 -1
- package/src/server/runCompiled.ts +1 -1
- package/src/server/runDesktop.ts +1 -1
- package/src/server/seedBuffer.ts +1 -1
- package/src/server/serveDev.ts +24 -58
- package/src/server/servedApp.ts +390 -0
- package/src/server/signInChanged.ts +20 -0
- package/src/server/surfaceOf.ts +2 -1
- package/src/server/versionOf.ts +6 -15
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/muxFrames.ts +1 -1
- package/src/shared/reactive/Keyed.ts +1 -1
- package/src/shared/reactive/Reactive.ts +21 -8
- package/src/shared/reactive/argsContract.ts +131 -4
- package/src/shared/reactive/index.ts +1 -0
- package/src/shared/reactive/patchLog.ts +79 -0
- package/src/shared/reactive/patchMarks.ts +34 -0
- package/src/shared/reactive/recordWrites.ts +11 -14
- package/src/shared/reactive/share.ts +4 -3
- package/src/shared/rpc.ts +12 -19
- package/src/shared/validateJson.ts +14 -3
- package/src/ui/ArmRegion.ts +10 -4
- package/src/ui/Level.ts +11 -20
- package/src/ui/LevelChain.ts +272 -0
- package/src/ui/Router.ts +54 -245
- package/src/ui/Scope.ts +17 -3
- package/src/ui/TryRegion.ts +22 -23
- package/src/ui/bindSpread.ts +2 -4
- package/src/ui/listenForRoutes.ts +3 -10
- package/src/ui/outboxQueue.ts +21 -3
- package/src/ui/reconcile.ts +4 -6
- package/src/ui/routing.ts +2 -1
- package/src/ui/rows.ts +2 -2
- package/src/ui/setAttribute.ts +12 -3
- package/src/ui/socketWire.ts +26 -11
- package/src/compiler/Finding.ts +0 -2
- package/src/compiler/Refusals.ts +0 -65
- package/src/compiler/formatDiagnosis.ts +0 -20
- package/src/server/appDirectories.ts +0 -9
- package/src/server/argsFor.ts +0 -39
- package/src/server/buildDirectory.ts +0 -10
- package/src/server/coerceArgs.ts +0 -109
- package/src/server/derivedArgs.ts +0 -95
- package/src/server/runBuild.ts +0 -24
- package/src/server/serveBuild.ts +0 -144
- package/src/server/testBuild.ts +0 -220
- package/src/shared/reactive/FOLLOWED.ts +0 -8
- package/src/shared/reactive/PATCHES.ts +0 -11
- package/src/shared/reactive/PATCH_SINKS.ts +0 -26
- 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
|
|
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 {
|
|
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:
|
|
294
|
+
seen: patchEpoch(),
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
package/src/ui/setAttribute.ts
CHANGED
|
@@ -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 ===
|
|
40
|
-
if (standing === null) addAttribute(element, name,
|
|
41
|
-
else element.setAttribute(name,
|
|
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
|
/**
|
package/src/ui/socketWire.ts
CHANGED
|
@@ -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/
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*
|
package/src/compiler/Finding.ts
DELETED
package/src/compiler/Refusals.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/argsFor.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/coerceArgs.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/runBuild.ts
DELETED
|
@@ -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
|
-
}
|
package/src/server/serveBuild.ts
DELETED
|
@@ -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
|
-
}
|