@abide/abide 0.100.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 +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -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 -56
- 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 +124 -39
- package/src/server/answerSocket.ts +141 -48
- 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 +48 -14
- 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 +144 -183
- 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 +81 -39
- 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/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- 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/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- 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 +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- 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/server/manifestOf.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
// 38.5 AND 38.
|
|
1
|
+
// 38.5 AND 38.28 — THE MANIFEST: the files the entry was written under, and each module a route
|
|
2
2
|
// loads in a browser to every chunk that module needs before it can run. Read off the bundler's
|
|
3
3
|
// own metafile rather than off the emitted code, so a chunk the bundler hoisted into a shared file
|
|
4
4
|
// is in the list because the bundler says it is imported, not because a pattern over its output
|
|
5
5
|
// happened to match.
|
|
6
6
|
//
|
|
7
7
|
// THE ENTRY'S NAME IS HERE BECAUSE A BUILD HASHES IT (38.5). `abide dev` names it by its source
|
|
8
|
-
// (38.
|
|
8
|
+
// (38.16) and reads it off this all the same, so the tags the document carries come from one
|
|
9
9
|
// place in both lanes rather than from a constant in one and a lookup in the other.
|
|
10
10
|
//
|
|
11
11
|
// THE STATIC CLOSURE AND NOTHING ELSE. An `import-statement` edge is a chunk the module cannot
|
|
12
12
|
// run without; a `dynamic-import` edge is a destination somebody may never navigate to, and
|
|
13
13
|
// preloading it is the whole route table on every page.
|
|
14
14
|
//
|
|
15
|
-
// THE ENTRY IS LEFT OUT of every list, because the document already loads it by name (38.
|
|
15
|
+
// THE ENTRY IS LEFT OUT of every list, because the document already loads it by name (38.22) and
|
|
16
16
|
// a preload of the same address is a second request line for bytes already on their way.
|
|
17
17
|
|
|
18
18
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
19
19
|
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
20
|
+
import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
|
|
21
|
+
|
|
22
|
+
// The entry's key: the table it is served as, below the root.
|
|
23
|
+
const ENTRY_MODULE = `${VIRTUAL_DIRECTORY}/${CLIENT_ENTRY}.ts`
|
|
20
24
|
|
|
21
25
|
export type Manifest = {
|
|
22
26
|
/** The entry's file name, or undefined where the bundle has none — a dev build that failed. */
|
|
@@ -24,10 +28,10 @@ export type Manifest = {
|
|
|
24
28
|
/**
|
|
25
29
|
* The stylesheet the bundler collected under the entry, or undefined where none was imported.
|
|
26
30
|
*
|
|
27
|
-
* 38.
|
|
31
|
+
* 38.23.
|
|
28
32
|
*/
|
|
29
33
|
stylesheet: string | undefined
|
|
30
|
-
/** A module's path below `
|
|
34
|
+
/** A module's path below the root — a route's `.abide` — to the chunk file names it needs, its own first. */
|
|
31
35
|
chunks: Record<string, string[]>
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -40,19 +44,18 @@ export const NO_MANIFEST: Manifest = {
|
|
|
40
44
|
/**
|
|
41
45
|
* The manifest read off a bundle's metafile: the entry, its stylesheet, and each module's chunks.
|
|
42
46
|
*
|
|
43
|
-
* `
|
|
44
|
-
* relative to the working directory with symlinks resolved, and a
|
|
45
|
-
* namespace as the plugin spelled it
|
|
46
|
-
* `abide dev` passes the virtual directory its plugin resolves into.
|
|
47
|
+
* `root` IS THE TREE THE ROUTE TABLE NAMES MODULES BELOW, as its real path: the metafile names a file
|
|
48
|
+
* relative to the working directory with symlinks resolved, and a table the plugin served under its
|
|
49
|
+
* namespace as the plugin spelled it (`abideModules`).
|
|
47
50
|
*/
|
|
48
51
|
export function manifestOf(
|
|
49
52
|
metafile: Bun.BuildMetafile,
|
|
50
|
-
|
|
53
|
+
root: string,
|
|
51
54
|
): Manifest {
|
|
52
55
|
const manifest: Manifest = { ...NO_MANIFEST, chunks: {} }
|
|
53
56
|
const imports = new Map<string, string[]>()
|
|
54
57
|
const modules = new Map<string, string>()
|
|
55
|
-
const prefix = `${
|
|
58
|
+
const prefix = `${root}/`
|
|
56
59
|
for (const [output, described] of Object.entries(metafile.outputs)) {
|
|
57
60
|
if (!isModule(output)) continue
|
|
58
61
|
const statically: string[] = []
|
|
@@ -66,7 +69,7 @@ export function manifestOf(
|
|
|
66
69
|
if (!path.startsWith(prefix)) continue
|
|
67
70
|
const module = path.slice(prefix.length)
|
|
68
71
|
modules.set(module, chunkName(output))
|
|
69
|
-
if (module !==
|
|
72
|
+
if (module !== ENTRY_MODULE) continue
|
|
70
73
|
manifest.entry = chunkName(output)
|
|
71
74
|
// NOT IN bun-types, and in the metafile: the stylesheet an entry's CSS was collected into.
|
|
72
75
|
const sheet = (described as { cssBundle?: string }).cssBundle
|
|
@@ -103,7 +106,7 @@ export function chunksOf(modules: string[], manifest: Manifest): string[] {
|
|
|
103
106
|
*
|
|
104
107
|
* A MODULE, NOT A STYLESHEET. The bundler reports the CSS it collected both as a static import of
|
|
105
108
|
* the entry and as an output whose entry point IS the entry — so unfiltered it is fetched as a
|
|
106
|
-
* script, and it takes the entry's own place in the manifest. 38.
|
|
109
|
+
* script, and it takes the entry's own place in the manifest. 38.23 links it already.
|
|
107
110
|
*/
|
|
108
111
|
function isModule(output: string): boolean {
|
|
109
112
|
return output.endsWith('.js')
|
package/src/server/namesUnder.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The paths below `directory` that `pattern` matches, or none for a directory that is not there.
|
|
3
3
|
*
|
|
4
|
-
* A missing directory is the common case rather than an error:
|
|
5
|
-
* `
|
|
4
|
+
* A missing directory is the common case rather than an error: a build has no `client/` or
|
|
5
|
+
* `public/` until it writes one. Narrowed to that one code because a
|
|
6
6
|
* directory that exists and cannot be read is a different fault and must not read as an empty one.
|
|
7
7
|
*/
|
|
8
8
|
export function namesUnder(directory: string, pattern = '**/*'): string[] {
|
package/src/server/nativeMenu.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// THE DESKTOP APP'S MENU BAR (38.
|
|
1
|
+
// THE DESKTOP APP'S MENU BAR (38.88): the default, with what the page the window shows exports,
|
|
2
2
|
// built as an `NSMenu`.
|
|
3
3
|
|
|
4
4
|
import { FFIType } from 'bun:ffi'
|
|
@@ -27,8 +27,8 @@ export type NativeItem =
|
|
|
27
27
|
items?: NativeItem[] | undefined
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// 38.
|
|
31
|
-
// holds the selection and is
|
|
30
|
+
// 38.89 — A ROLE AppKit ALREADY ANSWERS is sent down the responder chain, so `copy` copies whatever
|
|
31
|
+
// holds the selection and is grayed where nothing does. The rest are the launcher's own.
|
|
32
32
|
const RESPONDER_ROLES: Partial<Record<MenuRole, string>> = {
|
|
33
33
|
about: 'orderFrontStandardAboutPanel:',
|
|
34
34
|
hide: 'hide:',
|
|
@@ -76,8 +76,8 @@ const NAMED_KEYS: Record<string, string> = {
|
|
|
76
76
|
/**
|
|
77
77
|
* The menu bar the window shows: the application, File, Edit, View and Window menus, and `added`.
|
|
78
78
|
*
|
|
79
|
-
* 38.
|
|
80
|
-
* the platform puts an app's own. 38.
|
|
79
|
+
* 38.88 — AN ADDED MENU NAMED AS A DEFAULT ONE GROWS IT, and any other stands before Window, where
|
|
80
|
+
* the platform puts an app's own. 38.83 — no Connect… where the `launcher` export turns connecting off.
|
|
81
81
|
*/
|
|
82
82
|
export function menuBar(
|
|
83
83
|
appName: string,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.29 — WHICH OF THE ENCODINGS ON OFFER A REQUEST TAKES, or `undefined` for the bytes as they
|
|
2
2
|
// are. What is on offer is asked rather than assumed: a request preferring brotli for a file with
|
|
3
3
|
// only a gzip copy takes the gzip, where picking first and checking after would send it neither.
|
|
4
4
|
//
|
package/src/server/principal.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { declare } from '#shared/rpc.ts'
|
|
|
20
20
|
import { mountedOwn } from './answerRpc.ts'
|
|
21
21
|
import { callerOrMint, mintCaller } from './callerOf.ts'
|
|
22
22
|
import { config, positiveSetting } from './config.ts'
|
|
23
|
-
import { HOOKS } from './hooks.ts'
|
|
23
|
+
import { HOOKS, inOrder } from './hooks.ts'
|
|
24
24
|
import { within } from './requestScope.ts'
|
|
25
25
|
|
|
26
26
|
// 26.18.
|
|
@@ -150,12 +150,17 @@ async function documentOf(
|
|
|
150
150
|
expiresAt: new Date(expires).toISOString(),
|
|
151
151
|
error: undefined,
|
|
152
152
|
}
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
153
|
+
const resolvers = inOrder(HOOKS.principal)
|
|
154
|
+
if (resolvers.length === 0)
|
|
155
155
|
return { ...(isRecord(claims) ? claims : {}), ...baseline }
|
|
156
156
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
// 37.7 — EVERY RESOLVER, handed the claims, its answer merged over the ones before it.
|
|
158
|
+
const resolved: Record<string, unknown> = {}
|
|
159
|
+
for (const resolve of resolvers) {
|
|
160
|
+
const answered = await resolve(claims)
|
|
161
|
+
if (isRecord(answered)) Object.assign(resolved, answered)
|
|
162
|
+
}
|
|
163
|
+
return { ...resolved, ...baseline }
|
|
159
164
|
} catch (thrown) {
|
|
160
165
|
// 26.3 — a resolver that failed leaves the caller unauthenticated, and says why.
|
|
161
166
|
PRINCIPAL_LOG.warning('onPrincipal threw', thrown)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs' // bun exposes no realpath of its own
|
|
2
|
+
|
|
3
|
+
// A root that is not there is refused by the command that reads it, at the path it was given.
|
|
4
|
+
export function realOrAsGiven(path: string): string {
|
|
5
|
+
try {
|
|
6
|
+
return realpathSync(path)
|
|
7
|
+
} catch {
|
|
8
|
+
return path
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
import { PUBLIC_DIRECTORY } from './CLIENT_BUILD.ts'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Throws, naming both, when a public file
|
|
2
|
+
* Throws, naming both, when a public file claims a route's address or another public file's.
|
|
5
3
|
*
|
|
6
|
-
* 36.13 —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 36.13 — the walks that find routes and public files cannot see each other's collisions from
|
|
5
|
+
* inside either one; this is the one place they meet, and naming BOTH is the clause — naming one
|
|
6
|
+
* leaves the reader to find the other. Two `public` directories can claim one address too, since
|
|
7
|
+
* each mounts at the path above it (36.11).
|
|
9
8
|
*
|
|
10
9
|
* THE BUILD AND DEV, and `abide start` trusts the build: `answerRequest` answers a public file
|
|
11
10
|
* AHEAD of any route on the ground that 36.13 makes a collision impossible at run time, and 30.8
|
|
12
11
|
* says dev must resolve an address the way the built app does.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
* globbed name (36.11 — the path below the directory IS the address) and the dev worker's
|
|
16
|
-
* `published` map is keyed by them already.
|
|
13
|
+
* `published` is each public file's source path to its address, as `appTree` reads them.
|
|
17
14
|
*/
|
|
18
15
|
export function refuseAddressCollision(
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
pages: readonly { pattern: string }[],
|
|
17
|
+
handlers: readonly { address: string }[],
|
|
18
|
+
published: Map<string, string>,
|
|
21
19
|
command: string,
|
|
22
20
|
): void {
|
|
23
21
|
const patterns = new Set<string>()
|
|
24
|
-
for (const
|
|
25
|
-
for (const
|
|
22
|
+
for (const page of pages) patterns.add(page.pattern)
|
|
23
|
+
for (const handler of handlers) patterns.add(handler.address)
|
|
24
|
+
const claimed = new Map<string, string>()
|
|
25
|
+
for (const [source, address] of published) {
|
|
26
26
|
if (patterns.has(address))
|
|
27
27
|
throw new Error(
|
|
28
|
-
`${command}:
|
|
28
|
+
`${command}: ${source} and the route ${address} claim the same address. RULEBOOK 36.13.`,
|
|
29
29
|
)
|
|
30
|
+
const other = claimed.get(address)
|
|
31
|
+
if (other !== undefined)
|
|
32
|
+
throw new Error(
|
|
33
|
+
`${command}: ${other} and ${source} are both answered at ${address}. RULEBOOK 36.13.`,
|
|
34
|
+
)
|
|
35
|
+
claimed.set(address, source)
|
|
36
|
+
}
|
|
30
37
|
}
|
package/src/server/remote.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// THE CLIENT FOR A RUNNING APP — what the shell (`shell.ts`), `call`, `
|
|
1
|
+
// THE CLIENT FOR A RUNNING APP — what the shell (`shell.ts`), `call`, `watch`, `openapi` and `mcp` talk to
|
|
2
2
|
// it with, the shell on the app served here included.
|
|
3
3
|
// Which app is `--url`, then `ABIDE_APP_URL`, then the local one; `ABIDE_APP_TOKEN` is the bearer
|
|
4
4
|
// sent (36.6), and neither variable is read anywhere else.
|
|
5
5
|
|
|
6
|
-
import { LOGS_ADDRESS, RPC_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
7
6
|
import { applyPatch } from '#shared/applyPatch.ts'
|
|
8
|
-
import
|
|
7
|
+
import { filledPath, parametersOf } from '#shared/filledPath.ts'
|
|
8
|
+
import { firstMatch, partsOf } from '#shared/matchRoute.ts'
|
|
9
9
|
import type { PatchOp } from '#shared/reactive/PATCH_SINKS.ts'
|
|
10
|
+
import { rankRoutes } from '#shared/routePattern.ts'
|
|
10
11
|
import type { Method } from '#shared/rpc.ts'
|
|
11
12
|
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
12
13
|
import type { Mount } from './answerRpc.ts'
|
|
@@ -43,8 +44,8 @@ const FAULTED = 8
|
|
|
43
44
|
/**
|
|
44
45
|
* Calls one handler on a running app; returns the CLI's exit code and the answer's text.
|
|
45
46
|
*
|
|
46
|
-
* 38.8 — ONE CALL TO ONE HANDLER, spelled `invoices/getInvoice id=4310`
|
|
47
|
-
* alike, and 38.
|
|
47
|
+
* 38.8 — ONE CALL TO ONE HANDLER, spelled `invoices/getInvoice id=4310` — or `invoices/4310` where
|
|
48
|
+
* the route carries the parameter — in argv and in the shell alike, and 38.61's args.
|
|
48
49
|
*/
|
|
49
50
|
export async function callHandler(
|
|
50
51
|
url: string,
|
|
@@ -54,10 +55,11 @@ export async function callHandler(
|
|
|
54
55
|
handlers: Mount[],
|
|
55
56
|
signal?: AbortSignal,
|
|
56
57
|
): Promise<{ code: number; text: string }> {
|
|
57
|
-
const address =
|
|
58
|
-
const
|
|
58
|
+
const address = `/${path.replace(/^\//, '')}`
|
|
59
|
+
const asked = method?.toUpperCase()
|
|
60
|
+
const handler = handlerAt(handlers, address, asked)
|
|
59
61
|
const args = callArgs(given)
|
|
60
|
-
const chosen = (
|
|
62
|
+
const chosen = (asked ??
|
|
61
63
|
(handler?.method as Method | undefined) ??
|
|
62
64
|
(args === undefined ? 'GET' : 'POST')) as Method
|
|
63
65
|
const request =
|
|
@@ -78,7 +80,7 @@ export async function callHandler(
|
|
|
78
80
|
/**
|
|
79
81
|
* A call's args out of the words after its address, or `undefined` where there are none.
|
|
80
82
|
*
|
|
81
|
-
* 38.
|
|
83
|
+
* 38.61 — `name=value` words, kept as the text they are for the app to coerce as it coerces a query's
|
|
82
84
|
* (19.7), against the schema it holds the call to; or, where the first word opens one, a JSON object.
|
|
83
85
|
*/
|
|
84
86
|
function callArgs(
|
|
@@ -102,12 +104,45 @@ function callArgs(
|
|
|
102
104
|
return pairs
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* The handler `address` names: its route as written (`media/[id]`), or an address its route matches
|
|
109
|
+
* (`media/4310`) — the most specific, of `method` where one is asked for.
|
|
110
|
+
*/
|
|
111
|
+
function handlerAt(
|
|
112
|
+
handlers: Mount[],
|
|
113
|
+
address: string,
|
|
114
|
+
method: string | undefined,
|
|
115
|
+
): Mount | undefined {
|
|
116
|
+
const rows: { pattern: string; mount: Mount }[] = []
|
|
117
|
+
for (const mount of handlers) {
|
|
118
|
+
if (method !== undefined && mount.method !== method) continue
|
|
119
|
+
if (mount.address === address) return mount
|
|
120
|
+
rows.push({ pattern: mount.address, mount })
|
|
121
|
+
}
|
|
122
|
+
return firstMatch(rankRoutes(rows), partsOf(address) ?? [])?.route.mount
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The request a call with text args is: in the URL where its method carries them, a form where not.
|
|
127
|
+
*
|
|
128
|
+
* 16.9 — a route written with its parameters takes them out of the args and into the path.
|
|
129
|
+
*/
|
|
106
130
|
function textRequest(
|
|
107
131
|
method: Method,
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
route: string,
|
|
133
|
+
given: URLSearchParams,
|
|
110
134
|
): { url: string; init: RequestInit } {
|
|
135
|
+
let address = route
|
|
136
|
+
let pairs = given
|
|
137
|
+
if (route.includes('[')) {
|
|
138
|
+
const params: Record<string, string> = {}
|
|
139
|
+
pairs = new URLSearchParams()
|
|
140
|
+
const named = parametersOf(route)
|
|
141
|
+
for (const [name, value] of given)
|
|
142
|
+
if (named.has(name)) params[name] = value
|
|
143
|
+
else pairs.append(name, value)
|
|
144
|
+
address = filledPath(route, params, route)
|
|
145
|
+
}
|
|
111
146
|
if (argsInURL(method))
|
|
112
147
|
return { url: `${address}?${pairs}`, init: { method } }
|
|
113
148
|
return {
|
|
@@ -120,49 +155,11 @@ function textRequest(
|
|
|
120
155
|
}
|
|
121
156
|
}
|
|
122
157
|
|
|
123
|
-
/**
|
|
124
|
-
* Prints every line `log.records` publishes, as it is written; returns the exit code.
|
|
125
|
-
*
|
|
126
|
-
* 38.10.
|
|
127
|
-
*/
|
|
128
|
-
export async function followLogs(url: string): Promise<number> {
|
|
129
|
-
const answer = await fetch(`${url}${LOGS_ADDRESS}`, {
|
|
130
|
-
headers: { ...remoteHeaders(), accept: 'text/event-stream' },
|
|
131
|
-
})
|
|
132
|
-
if (!answer.ok || answer.body === null) {
|
|
133
|
-
console.error(
|
|
134
|
-
`logs: the app answered ${answer.status}. ABIDE_LOGS opens the subscription.`,
|
|
135
|
-
)
|
|
136
|
-
return 1
|
|
137
|
-
}
|
|
138
|
-
// 18.20 — the lines held arrive as the value, and each line after as an `add` at `/-`; a trim's
|
|
139
|
-
// `remove`s are the buffer letting old lines go, which a tail has already printed.
|
|
140
|
-
for await (const event of eventsOf(answer.body)) {
|
|
141
|
-
if (event.name === 'error') console.error(`logs: ${event.data}`)
|
|
142
|
-
const lines =
|
|
143
|
-
event.name === 'value'
|
|
144
|
-
? ((JSON.parse(event.data) as LogRecord[] | null) ?? [])
|
|
145
|
-
: event.name === 'patch'
|
|
146
|
-
? addedBy(JSON.parse(event.data) as PatchOp[])
|
|
147
|
-
: []
|
|
148
|
-
for (const { time, level, namespace, message } of lines)
|
|
149
|
-
console.log(`${time} ${level} ${namespace} ${message}`)
|
|
150
|
-
}
|
|
151
|
-
return 0
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** The records a patch appended: the value of each `add`. */
|
|
155
|
-
function addedBy(ops: PatchOp[]): LogRecord[] {
|
|
156
|
-
const added: LogRecord[] = []
|
|
157
|
-
for (const op of ops) if (op.op === 'add') added.push(op.value as LogRecord)
|
|
158
|
-
return added
|
|
159
|
-
}
|
|
160
|
-
|
|
161
158
|
/**
|
|
162
159
|
* Follows a `GET` of the app at `url` as an event stream, printing its value and its value after each
|
|
163
160
|
* patch; returns the exit code.
|
|
164
161
|
*
|
|
165
|
-
* 38.
|
|
162
|
+
* 38.69, 18.20 — THE EVENT STREAM ANY CLIENT MAY ASK A FOLLOWED `GET` FOR, read by one: `event: value`
|
|
166
163
|
* replaces what is held and `event: patch` is JSON Patch applied over it. A handler the server does
|
|
167
164
|
* not follow — one built per request — answers once, and is printed once.
|
|
168
165
|
*/
|
|
@@ -172,7 +169,7 @@ export async function watch(
|
|
|
172
169
|
given: string[],
|
|
173
170
|
signal?: AbortSignal,
|
|
174
171
|
): Promise<number> {
|
|
175
|
-
const path =
|
|
172
|
+
const path = `/${address.replace(/^\//, '')}`
|
|
176
173
|
const args = callArgs(given)
|
|
177
174
|
const request =
|
|
178
175
|
args instanceof URLSearchParams
|
package/src/server/render.ts
CHANGED
|
@@ -27,17 +27,25 @@ import { currentRequestScope, type PageCall } from './requestScope.ts'
|
|
|
27
27
|
// biome-ignore lint/suspicious/noEmptyInterface: REGISTRY declares `interface Component {}`.
|
|
28
28
|
export interface Component {}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
30
|
+
/**
|
|
31
|
+
* 35.6 — THE DOCUMENT EVERY RENDER WRITES INTO: an encoding and a viewport, each keyed, so the
|
|
32
|
+
* component's own replace them (35.15), and nothing else — what a document carries beyond that is
|
|
33
|
+
* the component's to write.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_DOCUMENT =
|
|
36
|
+
'<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body></body></html>'
|
|
36
37
|
|
|
37
38
|
const ENCODER = new TextEncoder()
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* What a compiled component answers: its head and its body markup, and the document's `<html>`
|
|
42
|
+
* and `<body>` it writes, rendered empty (35.15), or `undefined`.
|
|
43
|
+
*/
|
|
44
|
+
export type Rendered = {
|
|
45
|
+
head: string
|
|
46
|
+
body: string
|
|
47
|
+
document: { html: string; body: string } | undefined
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
43
51
|
* The page inside its layouts, every level set up first and then built.
|
|
@@ -85,7 +93,12 @@ export async function composePage(
|
|
|
85
93
|
// THE OUTERMOST HEAD FIRST: reading it builds the chain, which is what pushes the heads below.
|
|
86
94
|
// Written end to end rather than merged: `render` merges it with the shell's in one pass (39.4).
|
|
87
95
|
const head = outermost.head
|
|
88
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
head: head + heads.join(''),
|
|
98
|
+
body: outermost.body,
|
|
99
|
+
// 35.15 — the document's, which only the root layout writes, and it is outermost.
|
|
100
|
+
document: outermost.document,
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
/**
|
|
@@ -185,46 +198,6 @@ function dropShadowed(markup: string, total: Map<string, number>): string {
|
|
|
185
198
|
})
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
/**
|
|
189
|
-
* The keyed elements at the top of a shell's head, end to end, and nothing else.
|
|
190
|
-
*
|
|
191
|
-
* 39.14 — WHAT A CLIENT NAVIGATION PUTS BACK. `mergeHead` drops every shell entry a level
|
|
192
|
-
* overrides, so once the server has painted the document the shell's `<title>` is nowhere a
|
|
193
|
-
* browser can read it — and a navigation to a page with no title owes it back. The build hands
|
|
194
|
-
* these to the router rather than the server writing them into each page: the shell is one per
|
|
195
|
-
* app, so it rides in the route table once instead of in every document. KEYED ONLY, because an
|
|
196
|
-
* entry with no key is never withdrawn (39.5) and a shell's inline `<style>` would be the route
|
|
197
|
-
* table's largest string for nothing. Top level only, as `mergeHead` keys it.
|
|
198
|
-
*/
|
|
199
|
-
export function keyedHead(document: string): string {
|
|
200
|
-
const unwrap = {
|
|
201
|
-
element(element: HTMLRewriterTypes.Element) {
|
|
202
|
-
element.removeAndKeepContent()
|
|
203
|
-
},
|
|
204
|
-
}
|
|
205
|
-
const top = { depth: 0 }
|
|
206
|
-
const rewriter = new HTMLRewriter()
|
|
207
|
-
.onDocument({
|
|
208
|
-
doctype(doctype) {
|
|
209
|
-
doctype.remove()
|
|
210
|
-
},
|
|
211
|
-
comments(comment) {
|
|
212
|
-
comment.remove()
|
|
213
|
-
},
|
|
214
|
-
text(text) {
|
|
215
|
-
if (top.depth === 0) text.remove()
|
|
216
|
-
},
|
|
217
|
-
})
|
|
218
|
-
.on('html', unwrap)
|
|
219
|
-
.on('head', unwrap)
|
|
220
|
-
return dropAtHeadTop(
|
|
221
|
-
rewriter,
|
|
222
|
-
document.slice(0, headAnchor(document)),
|
|
223
|
-
(key) => key === undefined,
|
|
224
|
-
top,
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
201
|
/**
|
|
229
202
|
* `markup` through `rewriter`, with each element at the top of the head removed where `drop`
|
|
230
203
|
* answers true for its key — `undefined` for an element with none. `top` is the depth the walk is
|
|
@@ -300,23 +273,25 @@ export function boundPage(
|
|
|
300
273
|
}
|
|
301
274
|
|
|
302
275
|
/**
|
|
303
|
-
* The document's bytes: `component` rendered into
|
|
276
|
+
* The document's bytes: `component` rendered into abide's own document (35.6).
|
|
304
277
|
*
|
|
305
278
|
* 35.4, 35.5 — the component is already bound to its props.
|
|
306
279
|
*/
|
|
307
|
-
export
|
|
280
|
+
export function render(component: Component): Promise<Uint8Array<ArrayBuffer>> {
|
|
281
|
+
return renderDocument(component, DEFAULT_DOCUMENT)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* `render` into `given`: abide's own document with what the server adds to it — a page's client
|
|
286
|
+
* tags, the launcher page's stylesheet. NOT PUBLIC SURFACE — `server/index.ts` exports `render`.
|
|
287
|
+
*/
|
|
288
|
+
export async function renderDocument(
|
|
308
289
|
component: Component,
|
|
309
|
-
|
|
290
|
+
given: string,
|
|
310
291
|
): Promise<Uint8Array<ArrayBuffer>> {
|
|
311
|
-
// GUARDED, because the string form is already settled: `shellText` is `async`, so awaiting it
|
|
312
|
-
// unconditionally wrapped a promise and spent a microtask turn per request for nothing.
|
|
313
|
-
const document = typeof shell === 'string' ? shell : await shellText(shell)
|
|
314
|
-
|
|
315
292
|
// TWO INSERTION POINTS, CLAMPED AND ORDERED. `</head>` and `</body>` are both OPTIONAL end
|
|
316
293
|
// tags, so a valid shell can carry neither: an absent anchor means "the end of the document",
|
|
317
294
|
// and the clamp is what keeps the head ahead of the body whatever order the shell wrote them in.
|
|
318
|
-
const bodyAt = bodyAnchor(document)
|
|
319
|
-
const headAt = headAnchor(document)
|
|
320
295
|
// 41.1 — NOTHING GOES OUT UNTIL THE PAGE IS BUILT. The status line goes out with the first byte,
|
|
321
296
|
// and a page reading a record that is not there is a 404 only while no byte has: so the page and
|
|
322
297
|
// its layouts are set up (41.19) and built before the shell's head is sent, and a refusal or a
|
|
@@ -331,6 +306,13 @@ export async function render(
|
|
|
331
306
|
const rendered = isThenable(answered) ? await answered : answered
|
|
332
307
|
const head = rendered.head
|
|
333
308
|
const body = rendered.body
|
|
309
|
+
// 35.15 — the root layout's `<html>` and `<body>` attributes, on the document's own two.
|
|
310
|
+
const document =
|
|
311
|
+
rendered.document === undefined
|
|
312
|
+
? given
|
|
313
|
+
: withDocumentAttributes(given, rendered.document)
|
|
314
|
+
const bodyAt = bodyAnchor(document)
|
|
315
|
+
const headAt = headAnchor(document)
|
|
334
316
|
// 41.20 — THE PAGE'S CALLS LEAVE AHEAD OF ITS MARKUP. Setup has run, so every call the page
|
|
335
317
|
// makes is known, and a script in the head starts each one as the request the browser's own
|
|
336
318
|
// `Rpc` would make — while the rest of the document and the bundle are still on their way.
|
|
@@ -399,13 +381,20 @@ function bodyAnchor(document: string): number {
|
|
|
399
381
|
}
|
|
400
382
|
|
|
401
383
|
/**
|
|
402
|
-
*
|
|
384
|
+
* The shell with `tags` at the head anchor `render` uses.
|
|
403
385
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* but the head
|
|
386
|
+
* So a shell with no `</head>` puts them where its head content goes rather than inside the body.
|
|
387
|
+
* NOT PUBLIC SURFACE: `server/index.ts` does not re-export it. A shell has to carry the client
|
|
388
|
+
* entry's script tag (38.22), and putting that anywhere but the head `render` finds is the same
|
|
389
|
+
* clamped-and-ordered question over again.
|
|
407
390
|
*/
|
|
408
|
-
export function
|
|
391
|
+
export function intoHead(shell: string, tags: string): string {
|
|
392
|
+
const at = headAnchor(shell)
|
|
393
|
+
return shell.slice(0, at) + tags + shell.slice(at)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Where a shell's head ends, clamped ahead of its body. */
|
|
397
|
+
function headAnchor(document: string): number {
|
|
409
398
|
const bodyAt = bodyAnchor(document)
|
|
410
399
|
// THE OPENING `<body` IS THE FALLBACK, not the body anchor. Collapsed onto `bodyAt`, a shell
|
|
411
400
|
// with no `</head>` put the page's `<title>`, `<meta>` and stylesheet links after the mount
|
|
@@ -428,37 +417,19 @@ function bodyOpen(document: string): number {
|
|
|
428
417
|
}
|
|
429
418
|
|
|
430
419
|
/**
|
|
431
|
-
*
|
|
420
|
+
* `document` with the attributes `written` carries — an `<html …></html>` and a `<body …></body>`,
|
|
421
|
+
* each rendered empty — written onto its own `<html>` and `<body>` tags.
|
|
432
422
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* owner of the question.
|
|
423
|
+
* 35.15, 35.17 — RENDERED AS ANY ELEMENT IS, so an attribute is escaped, absent or live exactly as
|
|
424
|
+
* one on a `<div>` is, and lifted off the rendered pair by its fixed frame rather than parsed.
|
|
436
425
|
*/
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
* is called from wherever the app is, so `src` is the root it passes; `loadBuild` is handed the
|
|
447
|
-
* root `abide build` compiled and the two need not be the same directory. NOT PUBLIC SURFACE —
|
|
448
|
-
* `server/index.ts` does not re-export it.
|
|
449
|
-
*/
|
|
450
|
-
export async function shellUnder(root: string): Promise<string> {
|
|
451
|
-
// `.text()` already fails when the file is not there, so no `.exists()` asks first. The read
|
|
452
|
-
// happens per render for a bare `render` call — 20 µs and 98% of the op — and `loadBuild` is the caller that hoists
|
|
453
|
-
// it, reading the shell once at boot rather than once a request.
|
|
454
|
-
try {
|
|
455
|
-
return await Bun.file(`${root}/ui/app.html`).text()
|
|
456
|
-
} catch (thrown) {
|
|
457
|
-
// NARROWED TO THE ONE CASE THE LINE ABOVE ARGUES FOR. A bare catch made every failure look
|
|
458
|
-
// like an absent file, so a project that HAS a shell which is momentarily unreadable —
|
|
459
|
-
// EACCES, EISDIR, EMFILE under load — was served the bare shell with no stylesheet, no
|
|
460
|
-
// client script and nothing said, on one request in a thousand.
|
|
461
|
-
if ((thrown as { code?: unknown }).code !== 'ENOENT') throw thrown
|
|
462
|
-
return BARE_SHELL
|
|
463
|
-
}
|
|
426
|
+
function withDocumentAttributes(
|
|
427
|
+
document: string,
|
|
428
|
+
written: { html: string; body: string },
|
|
429
|
+
): string {
|
|
430
|
+
const html = written.html.slice('<html'.length, -'></html>'.length)
|
|
431
|
+
const body = written.body.slice('<body'.length, -'></body>'.length)
|
|
432
|
+
return document
|
|
433
|
+
.replace(/<html(?=[\s>])/i, `<html${html}`)
|
|
434
|
+
.replace(/<body(?=[\s>])/i, `<body${body}`)
|
|
464
435
|
}
|
|
@@ -28,6 +28,11 @@ export function renderComponent(
|
|
|
28
28
|
const rendered = (
|
|
29
29
|
component as (props: Record<string, () => unknown>) => Rendered
|
|
30
30
|
)(props)
|
|
31
|
+
// 35.16 — a document is written by the outermost component alone, and this one is placed.
|
|
32
|
+
if (rendered.document !== undefined)
|
|
33
|
+
throw new Error(
|
|
34
|
+
'abide: a component that writes <html> is placed inside another, and only the outermost component writes the document. RULEBOOK 35.16.',
|
|
35
|
+
)
|
|
31
36
|
if (rendered.head !== '') heads.push(rendered.head)
|
|
32
37
|
return regionMarkup(rendered.body)
|
|
33
38
|
}
|
|
@@ -9,7 +9,7 @@ import { isRecord } from '#shared/isRecord.ts'
|
|
|
9
9
|
import { namedLogger } from '#shared/log.ts'
|
|
10
10
|
import abidePackage from '../../package.json' with { type: 'json' }
|
|
11
11
|
import { appIdentity, config } from './config.ts'
|
|
12
|
-
import { HOOKS } from './hooks.ts'
|
|
12
|
+
import { HOOKS, inOrder } from './hooks.ts'
|
|
13
13
|
|
|
14
14
|
const HEALTH_LOG = namedLogger('abide:health')
|
|
15
15
|
|
|
@@ -23,7 +23,7 @@ export async function reportHealth(): Promise<Health> {
|
|
|
23
23
|
abide: abidePackage.version,
|
|
24
24
|
startedAt: STARTED_AT,
|
|
25
25
|
}
|
|
26
|
-
for (const report of HOOKS.health) {
|
|
26
|
+
for (const report of inOrder(HOOKS.health)) {
|
|
27
27
|
let answered: unknown
|
|
28
28
|
try {
|
|
29
29
|
answered = await report()
|