@abide/abide 0.101.0 → 0.102.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/abide.ts +223 -164
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +5 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -59
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +102 -30
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +114 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +73 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
package/src/shared/ADDRESSES.ts
CHANGED
|
@@ -7,20 +7,19 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* The namespace every framework address lives under.
|
|
9
9
|
*
|
|
10
|
-
* UNDER `/__abide`, which is the namespace the framework's own addresses live in (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 38.21 is the clause.
|
|
10
|
+
* UNDER `/__abide`, which is the namespace the framework's own addresses live in (20.5, 25.1).
|
|
11
|
+
* That is what keeps the app's routes and abide's own addresses from colliding: a route whose first
|
|
12
|
+
* segment is `__abide` is refused at build (23.17), so nothing can be shadowed at run time. 38.20
|
|
13
|
+
* is the clause.
|
|
15
14
|
*/
|
|
16
15
|
export const ABIDE_ADDRESS = '/__abide'
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Where the browser bundle is served.
|
|
20
19
|
*
|
|
21
|
-
* The bundle's own address below `ABIDE_ADDRESS` (38.
|
|
20
|
+
* The bundle's own address below `ABIDE_ADDRESS` (38.16, 38.20). One request path answers
|
|
22
21
|
* everything under `ABIDE_ADDRESS` and the command supplying the app decides what is there, so a
|
|
23
|
-
* command with a second address of its own — the reload client `abide dev` serves (38.
|
|
22
|
+
* command with a second address of its own — the reload client `abide dev` serves (38.17) — adds
|
|
24
23
|
* one rather than a lane.
|
|
25
24
|
*/
|
|
26
25
|
export const CLIENT_ADDRESS = `${ABIDE_ADDRESS}/client`
|
|
@@ -31,8 +30,6 @@ export const CLIENT_ADDRESS = `${ABIDE_ADDRESS}/client`
|
|
|
31
30
|
export const OPENAPI_ADDRESS = `${ABIDE_ADDRESS}/openapi.json`
|
|
32
31
|
/** The MCP endpoint. */
|
|
33
32
|
export const MCP_ADDRESS = `${ABIDE_ADDRESS}/mcp`
|
|
34
|
-
/** The stream of log records. */
|
|
35
|
-
export const LOGS_ADDRESS = `${ABIDE_ADDRESS}/logs`
|
|
36
33
|
/** The health check. */
|
|
37
34
|
export const HEALTH_ADDRESS = `${ABIDE_ADDRESS}/health`
|
|
38
35
|
/** Where a visitor's Claude Code bridge pairs, and is relayed turns (45.13). */
|
|
@@ -41,19 +38,11 @@ export const BRIDGE_ADDRESS = `${ABIDE_ADDRESS}/bridge`
|
|
|
41
38
|
/**
|
|
42
39
|
* The socket the dev reload client holds open.
|
|
43
40
|
*
|
|
44
|
-
* 38.
|
|
41
|
+
* 38.17 — the LISTENER upgrades it on the main thread and the dev WORKER serves the script that
|
|
45
42
|
* opens it.
|
|
46
43
|
*/
|
|
47
44
|
export const DEV_ADDRESS = `${ABIDE_ADDRESS}/dev`
|
|
48
45
|
|
|
49
|
-
/**
|
|
50
|
-
* Where a `server/rpc` handler is answered over http.
|
|
51
|
-
*
|
|
52
|
-
* 16.2 — its path below that directory, then its export name (19.1). The build writes the address
|
|
53
|
-
* into the browser's wrapper and the server answers at it.
|
|
54
|
-
*/
|
|
55
|
-
export const RPC_ADDRESS = `${ABIDE_ADDRESS}/rpc`
|
|
56
|
-
|
|
57
46
|
/**
|
|
58
47
|
* Where the one web socket a page follows its `GET`s over is served, below the app's root.
|
|
59
48
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 18.4 — WHAT A `GET`'s VALUE DECLARES THAT ITS BROWSER COPY HAS TO BEHAVE BY, as the build reads
|
|
5
5
|
* it off the declaration and writes it into the wrapper: data, because it crosses as text.
|
|
6
6
|
* `structural` is named rather than carried, being abide's own comparator on both sides; an app's
|
|
7
|
-
* own has no
|
|
7
|
+
* own has no serialization and stays the server's, as a function `by` or `tags` does.
|
|
8
8
|
*/
|
|
9
9
|
export type CopyOptions = {
|
|
10
10
|
by?: string
|
package/src/shared/DEBUG_META.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// THE TWO DIRECTORY NAMES THAT ARE NEVER AN ADDRESS (23.15): `server` fences a graph (16.29) and
|
|
2
|
+
// `public` is answered as authored (36.11). Every other directory under the root is a segment of the
|
|
3
|
+
// address of the `page.abide`, `layout.abide` and `error.abide` in it (23.14).
|
|
4
|
+
|
|
5
|
+
/** A module under one only the server loads. */
|
|
6
|
+
export const SERVER_DIRECTORY = 'server'
|
|
7
|
+
|
|
8
|
+
/** A file under one is answered as authored, at the path above it. */
|
|
9
|
+
export const PUBLIC_DIRECTORY = 'public'
|
|
10
|
+
|
|
11
|
+
/** Both, as the walk of the app tests a directory against them. */
|
|
12
|
+
export const RESERVED_DIRECTORIES: ReadonlySet<string> = new Set([
|
|
13
|
+
SERVER_DIRECTORY,
|
|
14
|
+
PUBLIC_DIRECTORY,
|
|
15
|
+
])
|
package/src/shared/cookies.ts
CHANGED
|
@@ -185,12 +185,12 @@ export function serializeCookie(init: CookieInit, inBrowser: boolean): string {
|
|
|
185
185
|
// A page's own script cannot set an `HttpOnly` cookie, and a browser drops the whole line that
|
|
186
186
|
// asks to — so the attribute is a server's alone.
|
|
187
187
|
if (init.httpOnly === true && !inBrowser) line += '; HttpOnly'
|
|
188
|
-
line += `; SameSite=${
|
|
188
|
+
line += `; SameSite=${capitalized(init.sameSite ?? 'lax')}`
|
|
189
189
|
if (init.partitioned === true) line += '; Partitioned'
|
|
190
190
|
return line
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function
|
|
193
|
+
function capitalized(sameSite: string): string {
|
|
194
194
|
return sameSite.charAt(0).toUpperCase() + sameSite.slice(1)
|
|
195
195
|
}
|
|
196
196
|
|
package/src/shared/csp.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function documentNonce(): string {
|
|
|
41
41
|
* A middleware rung that sets the content security policy, and `csp.nonce()` for the current one.
|
|
42
42
|
*
|
|
43
43
|
* THE RUNG IS BUILT ON ITS FIRST REQUEST, because an app's `middleware` can be read before the
|
|
44
|
-
* server half has filled `CSP`: `
|
|
44
|
+
* server half has filled `CSP`: the root `layout.abide` is one more module, and nothing orders it after that one.
|
|
45
45
|
*/
|
|
46
46
|
export const csp = Object.assign(
|
|
47
47
|
(sources?: Record<string, string[]>): Rung => {
|
package/src/shared/escapeText.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { RAW_WRITES, Raw, textOf } from './Raw.ts'
|
|
|
10
10
|
// the fallback: a clean string is returned as it arrived, with no allocation.
|
|
11
11
|
//
|
|
12
12
|
// THE DETECTION IS THE ENGINE'S, not a `charCodeAt` loop. The hand-written scan is one JS
|
|
13
|
-
// iteration per character where this is
|
|
13
|
+
// iteration per character where this is vectorized, and it lost on the case it was written for:
|
|
14
14
|
// 5x on a clean 74-character slot and 14x on a clean 320-character one, over two runs that did
|
|
15
15
|
// not swap places. The share is named — `escapeText` is 74-86% of a 500-row x 4-slot markup
|
|
16
16
|
// build, and the whole build moves 1.7x.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A ROUTE'S PATH WITH ITS PARAMETERS WRITTEN IN, for the two things that build an address from a
|
|
2
|
+
// route: `url`, from a page's literal (30.1), and a call, from its handler's route (16.9).
|
|
3
|
+
|
|
4
|
+
import { parseRoutePattern, type Segment } from './routePattern.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `pattern` with each parameter `params` names written in as a path segment, or `/` for none.
|
|
8
|
+
*
|
|
9
|
+
* `spelled` is how the caller is named where a parameter is refused — `url('/x/[id]')`, or a
|
|
10
|
+
* handler's route — and `refused` what is thrown for it: an `Error` naming the clause, by default.
|
|
11
|
+
*/
|
|
12
|
+
export function filledPath(
|
|
13
|
+
pattern: string,
|
|
14
|
+
params: Record<string, unknown>,
|
|
15
|
+
spelled: string,
|
|
16
|
+
refused: (name: string, message: string) => unknown = thrownError,
|
|
17
|
+
): string {
|
|
18
|
+
return filledSegments(parseRoutePattern(pattern), params, spelled, refused)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `filledPath` over a route already parsed, for a caller that fills the same route per call. */
|
|
22
|
+
export function filledSegments(
|
|
23
|
+
segments: Segment[],
|
|
24
|
+
params: Record<string, unknown>,
|
|
25
|
+
spelled: string,
|
|
26
|
+
refused: (name: string, message: string) => unknown,
|
|
27
|
+
): string {
|
|
28
|
+
let address = ''
|
|
29
|
+
for (const segment of segments) {
|
|
30
|
+
if (segment.kind === 'static') {
|
|
31
|
+
address += `/${segment.name}`
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
const value = params[segment.name]
|
|
35
|
+
if (value === undefined || value === null || value === '') {
|
|
36
|
+
// 30.4 — an optional left out omits its segment, and so does an empty rest (30.5).
|
|
37
|
+
if (segment.kind !== 'required') continue
|
|
38
|
+
throw refused(
|
|
39
|
+
segment.name,
|
|
40
|
+
`abide: ${spelled} was given no '${segment.name}', and the route requires it. RULEBOOK 30.3.`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
if (segment.kind === 'rest' && Array.isArray(value)) {
|
|
44
|
+
for (const each of value)
|
|
45
|
+
address += `/${encodedSegment(spelled, segment.name, each, refused)}`
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
address += `/${encodedSegment(spelled, segment.name, value, refused)}`
|
|
49
|
+
}
|
|
50
|
+
return address === '' ? '/' : address
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The names of `pattern`'s parameters — what a call takes out of its args and into the path (16.66). */
|
|
54
|
+
export function parametersOf(pattern: string): Set<string> {
|
|
55
|
+
const names = new Set<string>()
|
|
56
|
+
for (const segment of parseRoutePattern(pattern))
|
|
57
|
+
if (segment.kind !== 'static') names.add(segment.name)
|
|
58
|
+
return names
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A param value encoded as one path segment; `.` and `..` are refused.
|
|
63
|
+
*
|
|
64
|
+
* 30.19 — A VALUE OF `.` OR `..` HAS NO SPELLING AS A SEGMENT: the URL parser collapses it as a
|
|
65
|
+
* dot-segment, and `%2E` / `%2E%2E` with it, so the address would land on another route. Refused
|
|
66
|
+
* rather than written, as a missing required segment is (30.3).
|
|
67
|
+
*/
|
|
68
|
+
function encodedSegment(
|
|
69
|
+
spelled: string,
|
|
70
|
+
name: string,
|
|
71
|
+
value: unknown,
|
|
72
|
+
refused: (name: string, message: string) => unknown,
|
|
73
|
+
): string {
|
|
74
|
+
const text = String(value)
|
|
75
|
+
if (text === '.' || text === '..')
|
|
76
|
+
throw refused(
|
|
77
|
+
name,
|
|
78
|
+
`abide: ${spelled} was given '${text}' for '${name}', and no address carries it as a segment — the URL parser resolves it to another route. RULEBOOK 30.19.`,
|
|
79
|
+
)
|
|
80
|
+
return encodeURIComponent(text)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function thrownError(_name: string, message: string): Error {
|
|
84
|
+
return new Error(message)
|
|
85
|
+
}
|
package/src/shared/framing.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
// 16.32 — A FRAMED ANSWER, read one JSON value per line: the whole line for jsonl, the `data:` line of
|
|
2
2
|
// each event for sse. A line that does not carry the prefix — an sse `event:` line, a blank line — is
|
|
3
3
|
// skipped. One line reader behind both a body read to the end and one read as it lands.
|
|
4
|
+
//
|
|
5
|
+
// 16.68 — AND ITS LAST FRAME MAY BE A REFUSAL: one that reached the stream after it had answered, so
|
|
6
|
+
// after its status was sent. An sse `event: refusal` and the Problem Details on its `data:` line, or a
|
|
7
|
+
// jsonl line `{"$refusal": <Problem Details>}` — read here as the refusal, thrown where it lands.
|
|
8
|
+
|
|
9
|
+
import { failedFrom, isProblem, type Problem } from './Failed.ts'
|
|
10
|
+
|
|
11
|
+
/** The key a jsonl line carries a refusal under (16.68). */
|
|
12
|
+
const REFUSAL_KEY = '$refusal'
|
|
13
|
+
|
|
14
|
+
/** A framed stream's last frame, as the server writes it for a refusal (16.68). */
|
|
15
|
+
export function refusalFrame(problem: Problem, prefix: string): string {
|
|
16
|
+
return prefix === ''
|
|
17
|
+
? `${JSON.stringify({ [REFUSAL_KEY]: problem })}\n`
|
|
18
|
+
: `event: refusal\n${prefix}${JSON.stringify(problem)}\n\n`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// What a read of one framed body carries from one line to the next: whether an sse event named the
|
|
22
|
+
// data line after it a refusal.
|
|
23
|
+
type Reading = { refusal: boolean }
|
|
24
|
+
|
|
25
|
+
// A refusal's frame, read: thrown where it lands among the chunks.
|
|
26
|
+
class Refused {
|
|
27
|
+
problem: Problem
|
|
28
|
+
constructor(problem: Problem) {
|
|
29
|
+
this.problem = problem
|
|
30
|
+
}
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
/**
|
|
6
34
|
* What precedes a chunk's JSON on its line: nothing for jsonl, `data: ` for sse, and `null` where
|
|
@@ -67,14 +95,16 @@ async function* bytesStream(
|
|
|
67
95
|
/** The chunks a framed text carried, every line of it complete. */
|
|
68
96
|
export function framedChunks(text: string, prefix: string): unknown[] {
|
|
69
97
|
const chunks: unknown[] = []
|
|
70
|
-
readLines(text, prefix, true, chunks)
|
|
98
|
+
readLines(text, prefix, true, chunks, { refusal: false })
|
|
99
|
+
for (const chunk of chunks)
|
|
100
|
+
if (chunk instanceof Refused) throw failedFrom(chunk.problem)
|
|
71
101
|
return chunks
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
/**
|
|
75
105
|
* A framed body's chunks, yielded as each line completes.
|
|
76
106
|
*
|
|
77
|
-
*
|
|
107
|
+
* Canceled when the reader stops reading — a refresh starts a new run and the old one's reader has
|
|
78
108
|
* nothing left to hand anything.
|
|
79
109
|
*/
|
|
80
110
|
export async function* framedStream(
|
|
@@ -84,6 +114,7 @@ export async function* framedStream(
|
|
|
84
114
|
const reader = body.getReader()
|
|
85
115
|
const decoder = new TextDecoder()
|
|
86
116
|
const chunks: unknown[] = []
|
|
117
|
+
const reading: Reading = { refusal: false }
|
|
87
118
|
let buffered = ''
|
|
88
119
|
try {
|
|
89
120
|
for (;;) {
|
|
@@ -97,8 +128,17 @@ export async function* framedStream(
|
|
|
97
128
|
buffered += decoded
|
|
98
129
|
continue
|
|
99
130
|
}
|
|
100
|
-
buffered = readLines(
|
|
101
|
-
|
|
131
|
+
buffered = readLines(
|
|
132
|
+
buffered + decoded,
|
|
133
|
+
prefix,
|
|
134
|
+
done,
|
|
135
|
+
chunks,
|
|
136
|
+
reading,
|
|
137
|
+
)
|
|
138
|
+
for (const chunk of chunks) {
|
|
139
|
+
if (chunk instanceof Refused) throw failedFrom(chunk.problem)
|
|
140
|
+
yield chunk
|
|
141
|
+
}
|
|
102
142
|
chunks.length = 0
|
|
103
143
|
if (done) return
|
|
104
144
|
}
|
|
@@ -117,15 +157,48 @@ function readLines(
|
|
|
117
157
|
prefix: string,
|
|
118
158
|
ended: boolean,
|
|
119
159
|
chunks: unknown[],
|
|
160
|
+
reading: Reading,
|
|
120
161
|
): string {
|
|
121
162
|
let start = 0
|
|
122
163
|
for (;;) {
|
|
123
164
|
const newline = text.indexOf('\n', start)
|
|
124
165
|
if (newline === -1 && !ended) return text.slice(start)
|
|
125
166
|
const end = newline === -1 ? text.length : newline
|
|
126
|
-
if (
|
|
127
|
-
|
|
167
|
+
if (prefix !== '' && text.startsWith('event: refusal', start))
|
|
168
|
+
reading.refusal = true
|
|
169
|
+
else if (end > start && text.startsWith(prefix, start)) {
|
|
170
|
+
const value: unknown = JSON.parse(
|
|
171
|
+
text.slice(start + prefix.length, end),
|
|
172
|
+
)
|
|
173
|
+
chunks.push(refusalIn(value, prefix, reading) ?? value)
|
|
174
|
+
reading.refusal = false
|
|
175
|
+
}
|
|
128
176
|
if (newline === -1) return ''
|
|
129
177
|
start = newline + 1
|
|
130
178
|
}
|
|
131
179
|
}
|
|
180
|
+
|
|
181
|
+
// The refusal a value carries as its frame: the data line an sse event named one, or a jsonl line
|
|
182
|
+
// holding the one reserved key and Problem Details under it.
|
|
183
|
+
function refusalIn(
|
|
184
|
+
value: unknown,
|
|
185
|
+
prefix: string,
|
|
186
|
+
reading: Reading,
|
|
187
|
+
): Refused | undefined {
|
|
188
|
+
if (prefix !== '')
|
|
189
|
+
return reading.refusal && isProblem(value)
|
|
190
|
+
? new Refused(value)
|
|
191
|
+
: undefined
|
|
192
|
+
// Per chunk: the reserved key is tested before any key list is built.
|
|
193
|
+
if (
|
|
194
|
+
value === null ||
|
|
195
|
+
typeof value !== 'object' ||
|
|
196
|
+
!Object.hasOwn(value, REFUSAL_KEY)
|
|
197
|
+
)
|
|
198
|
+
return undefined
|
|
199
|
+
const keys = Object.keys(value)
|
|
200
|
+
const problem = (value as Record<string, unknown>)[REFUSAL_KEY]
|
|
201
|
+
return keys.length === 1 && isProblem(problem)
|
|
202
|
+
? new Refused(problem)
|
|
203
|
+
: undefined
|
|
204
|
+
}
|
package/src/shared/headKeyOf.ts
CHANGED
|
@@ -20,6 +20,8 @@ export function headKeyOf(
|
|
|
20
20
|
): string | undefined {
|
|
21
21
|
if (lowerTag === 'title' || lowerTag === 'base') return lowerTag
|
|
22
22
|
if (lowerTag === 'meta') {
|
|
23
|
+
// One encoding a document, so a layout's replaces the default document's.
|
|
24
|
+
if (attribute('charset') != null) return 'meta charset'
|
|
23
25
|
const named = attribute('name')
|
|
24
26
|
const key = named === null ? attribute('property') : named
|
|
25
27
|
return key == null ? undefined : `meta ${key}`
|
package/src/shared/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ export { listOf } from './listOf.ts'
|
|
|
36
36
|
export { type Logger, type LogRecord, log } from './log.ts'
|
|
37
37
|
export type { Middleware } from './Middleware.ts'
|
|
38
38
|
export type { Params } from './matchRoute.ts'
|
|
39
|
-
// 38.
|
|
39
|
+
// 38.88 — a desktop app's menu items, exported by the route's layouts and page.
|
|
40
40
|
export {
|
|
41
41
|
type MenuEntry,
|
|
42
42
|
type MenuItem,
|
package/src/shared/log.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// SECTION 28 — the logger, on both sides. A line is written to the console AND published to
|
|
2
|
-
// `log.records` (28.1), so a subscriber —
|
|
2
|
+
// `log.records` (28.1), so a subscriber — a handler, a test, a page — sees exactly what the
|
|
3
3
|
// terminal saw.
|
|
4
4
|
//
|
|
5
5
|
// THE RECORDS ARE BUILT ON FIRST ASK. The reactive core warns through this module, so a
|
|
6
6
|
// `reactive` built at this module's evaluation would be a cycle evaluated in the wrong order;
|
|
7
7
|
// built on the first line or the first read of `log.records`, the core has long since loaded.
|
|
8
8
|
//
|
|
9
|
-
// 28.
|
|
9
|
+
// 28.10, 28.11 — IN A BROWSER, `DEBUG` IS THE DOCUMENT'S: the server names it in a `<meta>` it writes
|
|
10
10
|
// into the head, and the browser's own stored `debug` answers only where the document carries none.
|
|
11
11
|
|
|
12
|
+
import { APP_META } from './APP_META.ts'
|
|
12
13
|
import { DEBUG_META } from './DEBUG_META.ts'
|
|
13
14
|
import { paint } from './paint.ts'
|
|
14
15
|
import { type Reactive, reactive } from './reactive/index.ts'
|
|
@@ -53,7 +54,7 @@ function recordsValue(): Reactive<LogRecord[]> {
|
|
|
53
54
|
Number.isInteger(configured) && configured >= 0
|
|
54
55
|
? configured
|
|
55
56
|
: DEFAULT_BUFFER
|
|
56
|
-
// 28.1 — one array for the process
|
|
57
|
+
// 28.1 — one array for the process.
|
|
57
58
|
records = reactive<LogRecord[]>([], { global: true })
|
|
58
59
|
}
|
|
59
60
|
return records
|
|
@@ -62,7 +63,7 @@ function recordsValue(): Reactive<LogRecord[]> {
|
|
|
62
63
|
/**
|
|
63
64
|
* Appends `record` to `log.records`, trimming to the buffer it keeps.
|
|
64
65
|
*
|
|
65
|
-
* THE TRIM IS
|
|
66
|
+
* THE TRIM IS AMORTIZED: grown to twice the buffer and cut back in one move, rather than a `shift`
|
|
66
67
|
* per line that moves every record still held.
|
|
67
68
|
*/
|
|
68
69
|
function keep(list: LogRecord[], record: LogRecord): void {
|
|
@@ -81,14 +82,26 @@ function keep(list: LogRecord[], record: LogRecord): void {
|
|
|
81
82
|
*/
|
|
82
83
|
let documentDebugRead: string | undefined
|
|
83
84
|
|
|
85
|
+
let documentAppRead: string | undefined
|
|
86
|
+
|
|
84
87
|
function environment(name: string): string | undefined {
|
|
85
88
|
if (typeof process !== 'undefined') return process.env[name]
|
|
89
|
+
// 28.4 — the app's name, as the server wrote it into the document.
|
|
90
|
+
if (name === 'APP_NAME') {
|
|
91
|
+
documentAppRead ??=
|
|
92
|
+
(typeof document === 'undefined'
|
|
93
|
+
? undefined
|
|
94
|
+
: document
|
|
95
|
+
.querySelector(`meta[name="${APP_META}"]`)
|
|
96
|
+
?.getAttribute('content')) ?? ''
|
|
97
|
+
return documentAppRead === '' ? undefined : documentAppRead
|
|
98
|
+
}
|
|
86
99
|
if (name !== 'DEBUG') return undefined
|
|
87
100
|
documentDebugRead ??= documentDebug() ?? ''
|
|
88
101
|
return documentDebugRead
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
/** The document's `DEBUG`, or the browser's own stored one where it carries none (28.
|
|
104
|
+
/** The document's `DEBUG`, or the browser's own stored one where it carries none (28.10, 28.11). */
|
|
92
105
|
export function documentDebug(): string | undefined {
|
|
93
106
|
if (typeof document === 'undefined') return undefined
|
|
94
107
|
const named = document
|
|
@@ -106,7 +119,7 @@ export function documentDebug(): string | undefined {
|
|
|
106
119
|
/**
|
|
107
120
|
* The compiled `DEBUG` patterns: namespaces included, and namespaces excluded.
|
|
108
121
|
*
|
|
109
|
-
* 28.
|
|
122
|
+
* 28.9 — THE debug-npm GRAMMAR: a list split on commas or whitespace, `*` any run of characters,
|
|
110
123
|
* and a leading `-` an exclusion that beats every inclusion. Compiled once per distinct `DEBUG`.
|
|
111
124
|
*/
|
|
112
125
|
type Gate = { include: RegExp[]; exclude: RegExp[] }
|
|
@@ -131,7 +144,7 @@ function currentGate(): Gate {
|
|
|
131
144
|
return gate
|
|
132
145
|
}
|
|
133
146
|
|
|
134
|
-
/** Whether `DEBUG` enables a namespace's gated lines (28.
|
|
147
|
+
/** Whether `DEBUG` enables a namespace's gated lines (28.6). */
|
|
135
148
|
function isEnabled(namespace: string): boolean {
|
|
136
149
|
const { include, exclude } = currentGate()
|
|
137
150
|
for (const pattern of exclude) if (pattern.test(namespace)) return false
|
|
@@ -215,12 +228,12 @@ function write(
|
|
|
215
228
|
recordsValue().patch((list) => keep(list, record))
|
|
216
229
|
}
|
|
217
230
|
|
|
218
|
-
/** A logger under `namespace`, which 28.
|
|
231
|
+
/** A logger under `namespace`, which 28.4 builds from the app's name and 28.8 from `abide:`. */
|
|
219
232
|
export function namedLogger(namespace: string): Logger {
|
|
220
233
|
return loggerOver(() => namespace)
|
|
221
234
|
}
|
|
222
235
|
|
|
223
|
-
/** Both lanes' render logger (28.
|
|
236
|
+
/** Both lanes' render logger (28.8): the server's escalations and 32.20's dropped URLs. */
|
|
224
237
|
export const RENDER_LOG = namedLogger('abide:render')
|
|
225
238
|
|
|
226
239
|
/**
|
|
@@ -229,7 +242,7 @@ export const RENDER_LOG = namedLogger('abide:render')
|
|
|
229
242
|
* The namespace is ASKED per line rather than held, which is all `log` needs over a named logger.
|
|
230
243
|
*/
|
|
231
244
|
function loggerOver(namespaceOf: () => string): Logger {
|
|
232
|
-
// 28.
|
|
245
|
+
// 28.2 — the bare call always writes. 28.3 — so do warnings and errors.
|
|
233
246
|
const logger = ((...args: unknown[]): void => {
|
|
234
247
|
write(namespaceOf(), 'info', args)
|
|
235
248
|
}) as Logger
|
|
@@ -243,7 +256,7 @@ function loggerOver(namespaceOf: () => string): Logger {
|
|
|
243
256
|
}
|
|
244
257
|
logger.warning = (...args) => write(namespaceOf(), 'warning', args)
|
|
245
258
|
logger.error = (...args) => write(namespaceOf(), 'error', args)
|
|
246
|
-
// 28.
|
|
259
|
+
// 28.4 — another segment, below this one. 28.12 — asked per line too, so a logger named at module
|
|
247
260
|
// load, before the app's name resolved, writes under the name once it has.
|
|
248
261
|
logger.named = (name) => loggerOver(() => `${namespaceOf()}:${name}`)
|
|
249
262
|
logger.enabled = () => isEnabled(namespaceOf())
|
|
@@ -254,14 +267,15 @@ function loggerOver(namespaceOf: () => string): Logger {
|
|
|
254
267
|
/**
|
|
255
268
|
* The slot the derived app name is read through, filled by the server's `config`.
|
|
256
269
|
*
|
|
257
|
-
* 28.
|
|
270
|
+
* 28.4 — the app's name is the root every `log.named` hangs off: the environment's, or what 27.3
|
|
258
271
|
* derived, which the server's `config` fills in here.
|
|
259
272
|
*/
|
|
260
273
|
export const APP_NAME: { derived: () => string | undefined } = {
|
|
261
274
|
derived: () => undefined,
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
|
|
277
|
+
/** The name every `log.named` hangs off: the environment's, or the one the app derived (28.4). */
|
|
278
|
+
export function appName(): string {
|
|
265
279
|
return environment('APP_NAME') ?? APP_NAME.derived() ?? 'app'
|
|
266
280
|
}
|
|
267
281
|
|
package/src/shared/matchRoute.ts
CHANGED
|
@@ -55,8 +55,18 @@ function walk(
|
|
|
55
55
|
if (index === segments.length)
|
|
56
56
|
return prefix || at === parts.length ? {} : null
|
|
57
57
|
const segment = segments[index] as Segment
|
|
58
|
-
// 30.5 — A REST TAKES
|
|
59
|
-
|
|
58
|
+
// 30.5 — A REST TAKES THE MOST THE SEGMENTS AFTER IT LEAVE, nothing included, so a handler's
|
|
59
|
+
// name after one (`/docs/[...path]/save`) is reached rather than spent on the rest.
|
|
60
|
+
if (segment.kind === 'rest') {
|
|
61
|
+
for (let end = parts.length; end >= at; end -= 1) {
|
|
62
|
+
const params = walk(segments, index + 1, parts, end, prefix)
|
|
63
|
+
if (params !== null) {
|
|
64
|
+
params[segment.name] = parts.slice(at, end)
|
|
65
|
+
return params
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
60
70
|
const part = parts[at]
|
|
61
71
|
if (segment.kind === 'optional') {
|
|
62
72
|
if (part !== undefined) {
|
|
@@ -80,8 +90,9 @@ function walk(
|
|
|
80
90
|
* The first route whose pattern matches `parts`, with its params, or `undefined`.
|
|
81
91
|
*
|
|
82
92
|
* 30.9 — THE FIRST PATTERN THAT MATCHES WINS, over a table already ranked most-specific-first.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
93
|
+
* The client router resolves through this, and the server walks the same ranked table with
|
|
94
|
+
* `matchOf`, passing a route with no answer for the method asked. The two resolving one address
|
|
95
|
+
* alike is the isomorphism 30.8 rests on.
|
|
85
96
|
*
|
|
86
97
|
* Walked rather than compiled to a regex: a route table is matched once per navigation, so the
|
|
87
98
|
* walk is never the cost, and a regex would have to encode three segment kinds in a grammar nobody
|
|
@@ -97,3 +108,11 @@ export function firstMatch<T extends { segments: Segment[] }>(
|
|
|
97
108
|
}
|
|
98
109
|
return undefined
|
|
99
110
|
}
|
|
111
|
+
|
|
112
|
+
/** The params `segments` match the whole of `parts` with, or `undefined` where they do not. */
|
|
113
|
+
export function matchOf(
|
|
114
|
+
segments: Segment[],
|
|
115
|
+
parts: string[],
|
|
116
|
+
): Params | undefined {
|
|
117
|
+
return walk(segments, 0, parts, 0, false) ?? undefined
|
|
118
|
+
}
|
package/src/shared/menuItem.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.88 — THE DESKTOP APP'S MENU BAR IS THE ROUTE'S: a layout or a page exports `menu` from its
|
|
2
2
|
// `<script module>`, and the window shows what the route it stands on exports. An item made by
|
|
3
3
|
// `menuItem` is one a page answers; the rest are an address or a role, which need no page.
|
|
4
4
|
|
|
5
|
-
/** What a menu item does natively, where an address or a page's own answer could not. 38.
|
|
5
|
+
/** What a menu item does natively, where an address or a page's own answer could not. 38.89. */
|
|
6
6
|
export type MenuRole =
|
|
7
7
|
| 'about'
|
|
8
8
|
| 'hide'
|
|
@@ -37,7 +37,7 @@ export type MenuItem =
|
|
|
37
37
|
items?: MenuItem[]
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** An item a page answers: each function `on` filed runs when it is picked. 38.
|
|
40
|
+
/** An item a page answers: each function `on` filed runs when it is picked. 38.91. */
|
|
41
41
|
export class MenuEntry {
|
|
42
42
|
label: string
|
|
43
43
|
key: string | undefined
|
|
@@ -12,7 +12,7 @@ import type { Schema } from '../Schema.ts'
|
|
|
12
12
|
import {
|
|
13
13
|
ARGS_GATE,
|
|
14
14
|
type Gated,
|
|
15
|
-
|
|
15
|
+
normalized,
|
|
16
16
|
PARAMETER_DEFAULTS,
|
|
17
17
|
} from './argsContract.ts'
|
|
18
18
|
import { CELL } from './CELL.ts'
|
|
@@ -352,7 +352,7 @@ export function createKeyed<
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
function reach(given: Args): Reactive<Stored, Accepted, Failures> {
|
|
355
|
-
const args =
|
|
355
|
+
const args = normalized(self, given)
|
|
356
356
|
const key = canonicalKey(args)
|
|
357
357
|
const standing = entries.get(key)
|
|
358
358
|
// 1.8: reaching an instance that exists joins the standing work — 15.19, unless that work
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* What a read throws where nothing has landed.
|
|
3
3
|
*
|
|
4
4
|
* RULEBOOK 2.2. It carries the value it was thrown FOR, and that is the whole mechanism behind the
|
|
5
|
-
* abort: a tracked body that meets one subscribes to `source` and
|
|
5
|
+
* abort: a tracked body that meets one subscribes to `source` and reruns when it lands, so the
|
|
6
6
|
* body never computes against an absent value and never takes the wrong branch. The
|
|
7
7
|
* privileged-call hazard is what that buys — a body run to completion against an unlanded read
|
|
8
8
|
* issues the else arm's call for a caller nothing has authenticated.
|