@abide/abide 0.101.0 → 0.103.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 +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -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 +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- 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 +238 -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 +38 -37
- package/src/server/componentModules.d.ts +2 -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 +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -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 +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- 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 +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -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/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -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 +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- 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/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- 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 +37 -36
- 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/moveNode.ts +1 -1
- package/src/ui/reconcile.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 → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// HOW AN APP ANSWERS, given what it has. Two commands supply the parts — `abide start` off what
|
|
2
|
-
// the build wrote (38.6) and `abide dev` out of memory (38.
|
|
2
|
+
// the build wrote (38.6) and `abide dev` out of memory (38.16) — and the request path itself is
|
|
3
3
|
// this one, because a dev server that resolves an address differently from the served one is a
|
|
4
4
|
// bug a reader meets in production and never in development.
|
|
5
5
|
//
|
|
@@ -11,18 +11,20 @@ import {
|
|
|
11
11
|
BRIDGE_ADDRESS,
|
|
12
12
|
CLIENT_ADDRESS,
|
|
13
13
|
HEALTH_ADDRESS,
|
|
14
|
-
LOGS_ADDRESS,
|
|
15
14
|
MCP_ADDRESS,
|
|
16
15
|
OPENAPI_ADDRESS,
|
|
17
|
-
|
|
16
|
+
PRIVATE_HANDLERS_ADDRESS,
|
|
18
17
|
} from '#shared/ADDRESSES.ts'
|
|
18
|
+
import { APP_META } from '#shared/APP_META.ts'
|
|
19
19
|
import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
20
20
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
21
21
|
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
22
|
-
import { isFailed } from '#shared/Failed.ts'
|
|
22
|
+
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
23
23
|
import { health } from '#shared/health.ts'
|
|
24
|
+
import { appName } from '#shared/log.ts'
|
|
25
|
+
import { compose } from '#shared/Middleware.ts'
|
|
24
26
|
import { MOUNT_META } from '#shared/MOUNT_META.ts'
|
|
25
|
-
import {
|
|
27
|
+
import { matchOf, type Params, partsOf } from '#shared/matchRoute.ts'
|
|
26
28
|
import { MOUNT, mounted } from '#shared/mount.ts'
|
|
27
29
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
28
30
|
import { refuse, notFound as refusedNotFound } from '#shared/refuse.ts'
|
|
@@ -32,13 +34,13 @@ import {
|
|
|
32
34
|
rankRoutes,
|
|
33
35
|
type Segment,
|
|
34
36
|
} from '#shared/routePattern.ts'
|
|
35
|
-
import type { Rpc } from '#shared/rpc.ts'
|
|
37
|
+
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
36
38
|
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
37
|
-
import { answerFailure } from './answerFailure.ts'
|
|
39
|
+
import { answered, answerFailure } from './answerFailure.ts'
|
|
38
40
|
import {
|
|
39
41
|
answerRpc,
|
|
40
|
-
type Mount,
|
|
41
42
|
type Mounted,
|
|
43
|
+
type MountRow,
|
|
42
44
|
mountedOwn,
|
|
43
45
|
mountHandlers,
|
|
44
46
|
} from './answerRpc.ts'
|
|
@@ -48,24 +50,32 @@ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
|
48
50
|
import { callerOrMint } from './callerOf.ts'
|
|
49
51
|
import { compressAnswer } from './compressAnswer.ts'
|
|
50
52
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
51
|
-
import { answerMcp,
|
|
52
|
-
import { failureOf } from './hooks.ts'
|
|
53
|
+
import { answerMcp, openApi } from './generatedSurfaces.ts'
|
|
54
|
+
import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
53
55
|
import { chunksOf, type Manifest } from './manifestOf.ts'
|
|
54
56
|
import { muxAdmission } from './muxUpgrade.ts'
|
|
55
57
|
import { plainAnswer } from './plainAnswer.ts'
|
|
58
|
+
import { prefers } from './prefers.ts'
|
|
56
59
|
import { PRINCIPAL_MOUNT } from './principal.ts'
|
|
57
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
boundPage,
|
|
62
|
+
type Component,
|
|
63
|
+
DEFAULT_DOCUMENT,
|
|
64
|
+
intoHead,
|
|
65
|
+
renderDocument,
|
|
66
|
+
} from './render.ts'
|
|
58
67
|
import {
|
|
59
68
|
type AnsweringApp,
|
|
60
69
|
adoptRequest,
|
|
61
70
|
beginRender,
|
|
62
71
|
currentRequestScope,
|
|
72
|
+
type RequestScope,
|
|
63
73
|
within,
|
|
64
74
|
writtenStatus,
|
|
65
75
|
} from './requestScope.ts'
|
|
66
76
|
import { seedFor } from './seedBuffer.ts'
|
|
67
77
|
|
|
68
|
-
// One row of the table the compiler writes
|
|
78
|
+
// One row of the server's table the compiler writes (`SERVER_TABLE`). The loader is a dynamic import
|
|
69
79
|
// so a page's markup is off the boot path until an address asks for it, which is the same reason
|
|
70
80
|
// the client table's loaders are (30.11).
|
|
71
81
|
type ServerRoute = {
|
|
@@ -73,7 +83,7 @@ type ServerRoute = {
|
|
|
73
83
|
load: () => Promise<{ default: Component }>
|
|
74
84
|
// 35.1 — the layouts this page renders inside, outermost first, as the compiler found them.
|
|
75
85
|
layouts: (() => Promise<{ default: Component }>)[]
|
|
76
|
-
// 38.
|
|
86
|
+
// 38.28 — the modules a browser loads at this address, the page's then its layouts', as keys
|
|
77
87
|
// into the manifest `manifestOf` reads.
|
|
78
88
|
clientModules: string[]
|
|
79
89
|
}
|
|
@@ -85,29 +95,52 @@ type RankedError = ServerError & { segments: Segment[] }
|
|
|
85
95
|
|
|
86
96
|
type Ranked = RankedError & {
|
|
87
97
|
// The document this route is painted into, with the tags that load the app already in its
|
|
88
|
-
// head (38.
|
|
98
|
+
// head (38.22, 38.23) and the preloads for this route's own modules (38.28). One per route and
|
|
89
99
|
// built at boot, so a request pays for none of it.
|
|
90
100
|
document: string
|
|
91
101
|
}
|
|
92
102
|
|
|
103
|
+
/**
|
|
104
|
+
* One route of the app: the page at it, and the handler answering each method there.
|
|
105
|
+
*
|
|
106
|
+
* 19.1 — one file per method per route, so a route is a row and a method is a column, and 19.20's
|
|
107
|
+
* "the most specific route answering the method" is a walk down the rows asking each column.
|
|
108
|
+
*/
|
|
109
|
+
type RouteRow = {
|
|
110
|
+
pattern: string
|
|
111
|
+
segments: Segment[]
|
|
112
|
+
page: Ranked | undefined
|
|
113
|
+
handlers: Map<Method, Mounted>
|
|
114
|
+
/**
|
|
115
|
+
* The page and each handler, keyed by itself, inside the rungs its file's route filed (37.2).
|
|
116
|
+
*
|
|
117
|
+
* 16.14 — composed once with the table. Every server block is imported before it is built
|
|
118
|
+
* (37.1), so these are the rungs the boot found, as the root's are.
|
|
119
|
+
*/
|
|
120
|
+
answers: Map<
|
|
121
|
+
Ranked | Mounted,
|
|
122
|
+
(ctx: { request: Request }) => Promise<Response>
|
|
123
|
+
>
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
/** A public file, read once at boot: where the bytes are, the etag off them, and its type. */
|
|
94
127
|
type PublicFile = { path: string; etag: string; type: string }
|
|
95
128
|
|
|
96
129
|
/** Everything `answerRequest` answers from: the ranked routes, the lanes and the handlers. */
|
|
97
130
|
export type AppSources = {
|
|
98
|
-
/** The routes in 30.9's order, which `appSources` is the only way to get. */
|
|
99
|
-
routes:
|
|
131
|
+
/** The routes, pages and handlers both, in 30.9's order, which `appSources` is the only way to get. */
|
|
132
|
+
routes: RouteRow[]
|
|
100
133
|
/**
|
|
101
134
|
* Everything under `/__abide`: the bundle, and `abide dev`'s reload client beside it.
|
|
102
135
|
*
|
|
103
|
-
* The bundle is 38.
|
|
136
|
+
* The bundle is 38.20's and the reload client 38.17's. One lane rather than an asset table
|
|
104
137
|
* plus an exception, so what a command answers there is a fact about the command.
|
|
105
138
|
*/
|
|
106
139
|
framework: (pathname: string, request: Request) => Response | undefined
|
|
107
140
|
/**
|
|
108
141
|
* A file's address below `public`, to the file itself.
|
|
109
142
|
*
|
|
110
|
-
* 38.
|
|
143
|
+
* 38.18 and 38.19 — from the built output for `abide start` and from the source tree for
|
|
111
144
|
* `abide dev`.
|
|
112
145
|
*/
|
|
113
146
|
published: Map<string, PublicFile>
|
|
@@ -115,14 +148,8 @@ export type AppSources = {
|
|
|
115
148
|
errors: RankedError[]
|
|
116
149
|
/** The document the error pages are painted into. */
|
|
117
150
|
errorDocument: string
|
|
118
|
-
/** Every handler
|
|
151
|
+
/** Every handler, keyed by its method and address (`handlerKey`), to what answers it. 16.2. */
|
|
119
152
|
handlers: Map<string, Mounted>
|
|
120
|
-
/**
|
|
121
|
-
* The app's own route, asked where nothing of abide's answers.
|
|
122
|
-
*
|
|
123
|
-
* 37.2 — `bootApp` fills it from `src/server/app.ts`'s `default`.
|
|
124
|
-
*/
|
|
125
|
-
own: ((request: Request) => Promise<Response | undefined>) | undefined
|
|
126
153
|
/**
|
|
127
154
|
* What a page that threw, unrefused, is answered with in place of 16.41's bare 500.
|
|
128
155
|
*
|
|
@@ -132,9 +159,6 @@ export type AppSources = {
|
|
|
132
159
|
explained: ((thrown: unknown) => Response) | undefined
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
/** What a command reads or compiles, before `bootApp` adds what the `App` itself supplies. */
|
|
136
|
-
export type LoadedSources = Omit<AppSources, 'own'>
|
|
137
|
-
|
|
138
162
|
/** The `Response` to one request: a handler, a surface of abide's own, a file, or a page. */
|
|
139
163
|
export async function answerRequest(
|
|
140
164
|
app: AppSources,
|
|
@@ -145,15 +169,16 @@ export async function answerRequest(
|
|
|
145
169
|
adoptRequest(request)
|
|
146
170
|
const url = new URL(request.url)
|
|
147
171
|
const asked = url.pathname
|
|
172
|
+
const method = request.method === 'HEAD' ? 'GET' : request.method
|
|
148
173
|
|
|
149
|
-
if (asked.startsWith(`${RPC_ADDRESS}/`))
|
|
150
|
-
return answerRpc(app.handlers, url, request, app)
|
|
151
174
|
// 25.1, 25.3, 26.17 — the principal and the account of the app are handlers abide mounts,
|
|
152
175
|
// answered as the app's are.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
176
|
+
const own = OWN_HANDLERS.get(asked)
|
|
177
|
+
if (own !== undefined)
|
|
178
|
+
return own.method === method
|
|
179
|
+
? answerRpc(own, {}, url, request, app)
|
|
180
|
+
: methodRefused(app, asked, [own.method])
|
|
181
|
+
// 20.5, 20.9, 36.7 — a surface of abide's own, or a 404 where it answers nothing.
|
|
157
182
|
const surface = SURFACES.get(asked)
|
|
158
183
|
if (surface !== undefined) {
|
|
159
184
|
const answer = await surface(app, request)
|
|
@@ -172,10 +197,14 @@ export async function answerRequest(
|
|
|
172
197
|
// 37.3 — THE MUX'S UPGRADE PASSES THE APP LANE as every request does (`muxUpgrade`).
|
|
173
198
|
const admitted = muxAdmission(asked, request, app)
|
|
174
199
|
if (admitted !== undefined) return admitted
|
|
175
|
-
|
|
200
|
+
// 16.70 — a private handler is answered by the route table, below abide's own namespace.
|
|
201
|
+
if (
|
|
202
|
+
asked.startsWith(`${ABIDE_ADDRESS}/`) &&
|
|
203
|
+
!asked.startsWith(`${PRIVATE_HANDLERS_ADDRESS}/`)
|
|
204
|
+
) {
|
|
176
205
|
const answer = app.framework(asked, request)
|
|
177
206
|
if (answer === undefined) return notFound()
|
|
178
|
-
return readsOnly(request) ? answer : methodRefused(app, asked)
|
|
207
|
+
return readsOnly(request) ? answer : methodRefused(app, asked, READS)
|
|
179
208
|
}
|
|
180
209
|
|
|
181
210
|
// 36.11 — A PUBLIC FILE IS ITS PATH BELOW THE DIRECTORY, so `/favicon.ico` is answered
|
|
@@ -187,7 +216,7 @@ export async function answerRequest(
|
|
|
187
216
|
if (file !== undefined)
|
|
188
217
|
return readsOnly(request)
|
|
189
218
|
? publicResponse(request, file)
|
|
190
|
-
: methodRefused(app, asked)
|
|
219
|
+
: methodRefused(app, asked, READS)
|
|
191
220
|
|
|
192
221
|
// 15.16 — an address that will not percent-decode names nothing, and is the caller's fault.
|
|
193
222
|
const parts = partsOf(asked)
|
|
@@ -198,20 +227,79 @@ export async function answerRequest(
|
|
|
198
227
|
refuse(400, 'The address is not a valid path.'),
|
|
199
228
|
)
|
|
200
229
|
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
// 19.20 — THE MOST SPECIFIC ROUTE ANSWERING THE METHOD, an `OPTIONS` asking about the method
|
|
231
|
+
// its preflight names (16.24). A route that matches and answers some other method is held, so
|
|
232
|
+
// an address nothing answers the method at is a 405 naming what is answered there (15.17).
|
|
233
|
+
const preflight = request.method === 'OPTIONS'
|
|
234
|
+
// `null` for an `OPTIONS` that names no method, which is no preflight (Fetch's CORS-preflight
|
|
235
|
+
// request carries one) and so no handler's: the route answers 15.17's 405 with what it allows.
|
|
236
|
+
const asking = preflight
|
|
237
|
+
? request.headers.get('access-control-request-method')
|
|
238
|
+
: method
|
|
239
|
+
let allowed: Set<string> | undefined
|
|
240
|
+
let found:
|
|
241
|
+
| { route: RouteRow; params: Params; handler: Mounted | undefined }
|
|
242
|
+
| undefined
|
|
243
|
+
for (const route of app.routes) {
|
|
244
|
+
const params = matchOf(route.segments, parts)
|
|
245
|
+
if (params === undefined) continue
|
|
246
|
+
const handler =
|
|
247
|
+
asking === null ? undefined : route.handlers.get(asking as Method)
|
|
248
|
+
if (
|
|
249
|
+
handler !== undefined ||
|
|
250
|
+
(!preflight && asking === 'GET' && route.page !== undefined)
|
|
251
|
+
) {
|
|
252
|
+
found = { route, params, handler }
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
allowed ??= new Set()
|
|
256
|
+
if (route.page !== undefined) allowed.add('GET').add('HEAD')
|
|
257
|
+
for (const method of route.handlers.keys()) {
|
|
258
|
+
allowed.add(method)
|
|
259
|
+
if (method === 'GET') allowed.add('HEAD')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (found === undefined) {
|
|
263
|
+
if (allowed !== undefined)
|
|
264
|
+
return methodRefused(app, asked, [...allowed], request)
|
|
265
|
+
// 38.21. The client router leaves an unmatched address to the document (30.10), so this
|
|
266
|
+
// is where that address arrives. An app answering one itself does it in a root rung (37.3),
|
|
267
|
+
// which ran before this.
|
|
268
|
+
return laneFailure(
|
|
269
|
+
app,
|
|
270
|
+
asked,
|
|
271
|
+
refusedNotFound({ path: asked, method: request.method }),
|
|
272
|
+
request,
|
|
212
273
|
)
|
|
274
|
+
}
|
|
275
|
+
// 37.2, 37.5 — WHAT THE FILE'S ROUTE FILED: the request runs inside every rung filed at it or
|
|
276
|
+
// above it, the root's having run already, and an error in it reaches that route's `onError`.
|
|
277
|
+
const scope = currentRequestScope() as RequestScope
|
|
278
|
+
scope.route =
|
|
279
|
+
found.handler === undefined ? found.route.pattern : found.handler.scope
|
|
280
|
+
scope.params = found.params
|
|
281
|
+
const answer = found.route.answers.get(
|
|
282
|
+
found.handler ?? (found.route.page as Ranked),
|
|
283
|
+
) as (ctx: { request: Request }) => Promise<Response>
|
|
284
|
+
return answer({ request })
|
|
285
|
+
}
|
|
213
286
|
|
|
214
|
-
|
|
287
|
+
/**
|
|
288
|
+
* The answer of `found`, a page or a handler, once its middleware has admitted `request`, with the
|
|
289
|
+
* params routing put on the request's scope.
|
|
290
|
+
*/
|
|
291
|
+
async function answerMatched(
|
|
292
|
+
app: AppSources,
|
|
293
|
+
request: Request,
|
|
294
|
+
found: Ranked | Mounted,
|
|
295
|
+
): Promise<Response> {
|
|
296
|
+
// 37.2 — a route's rung may hand `next` a request of its own, adopted as a root rung's is.
|
|
297
|
+
adoptRequest(request)
|
|
298
|
+
const url = new URL(request.url)
|
|
299
|
+
const asked = url.pathname
|
|
300
|
+
const params = (currentRequestScope() as RequestScope).params as Params
|
|
301
|
+
if ('handler' in found) return answerRpc(found, params, url, request, app)
|
|
302
|
+
const page = found
|
|
215
303
|
|
|
216
304
|
// 30.16 — THE CLIENT ROUTER ASKING, not a reader loading the page. `middleware` has already run
|
|
217
305
|
// over this request by the time it gets here, and it is what the router came for: a refusal
|
|
@@ -226,8 +314,8 @@ export async function answerRequest(
|
|
|
226
314
|
// 33.4 — a prop is an ACCESSOR, which is the contract `renderComponent` renders a child under
|
|
227
315
|
// and the one the client router mounts a page under; the params are the record `firstMatch`
|
|
228
316
|
// hands the client for this same address.
|
|
229
|
-
const bound = boundPage(
|
|
230
|
-
// 38.
|
|
317
|
+
const bound = boundPage(page, { params: () => params })
|
|
318
|
+
// 38.29 — `vary` WHETHER OR NOT THIS ONE IS COMPRESSED: the document is negotiated, and a cache
|
|
231
319
|
// that stored the identity answer for a reader who sent no `accept-encoding` must not hand it
|
|
232
320
|
// to one who did, nor the reverse.
|
|
233
321
|
const headers = new Headers(DOCUMENT_HEADERS)
|
|
@@ -244,7 +332,7 @@ export async function answerRequest(
|
|
|
244
332
|
// would otherwise render one address and hydrate against another.
|
|
245
333
|
const at = new URL(request.url)
|
|
246
334
|
at.pathname = mounted(at.pathname)
|
|
247
|
-
arrive(at.href,
|
|
335
|
+
arrive(at.href, page.pattern, params)
|
|
248
336
|
// 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
|
|
249
337
|
// and 41.13, every call it makes is left for the browser's own call to be answered from.
|
|
250
338
|
beginRender({ seed: seedFor(caller), calls: [] })
|
|
@@ -253,7 +341,7 @@ export async function answerRequest(
|
|
|
253
341
|
// status rather than as a 200 the stream then abandons.
|
|
254
342
|
let document: Uint8Array<ArrayBuffer>
|
|
255
343
|
try {
|
|
256
|
-
document = await
|
|
344
|
+
document = await renderDocument(bound, page.document)
|
|
257
345
|
} catch (thrown) {
|
|
258
346
|
const failure = failureOf(thrown)
|
|
259
347
|
// Explained only where it was the server's fault — not a refusal, not a client that left.
|
|
@@ -277,11 +365,10 @@ export async function answerRequest(
|
|
|
277
365
|
const OWN_HANDLERS = new Map([
|
|
278
366
|
PRINCIPAL_MOUNT,
|
|
279
367
|
mountedOwn(HEALTH_ADDRESS, 'health', health),
|
|
280
|
-
mountedOwn(LOGS_ADDRESS, 'logs', LOGS_HANDLER),
|
|
281
368
|
])
|
|
282
369
|
|
|
283
370
|
// THE ADDRESSES ABIDE ANSWERS ITSELF, looked up once rather than asked in turn: each to its value,
|
|
284
|
-
// a `Response` of its own, or `undefined`
|
|
371
|
+
// a `Response` of its own, or `undefined` for a 404.
|
|
285
372
|
const SURFACES = new Map<
|
|
286
373
|
string,
|
|
287
374
|
(app: AppSources, request: Request) => unknown
|
|
@@ -321,9 +408,9 @@ export function reachedInRender(
|
|
|
321
408
|
}
|
|
322
409
|
|
|
323
410
|
/**
|
|
324
|
-
*
|
|
411
|
+
* The server's table the compiler writes (`SERVER_TABLE`): the routes and the error pages.
|
|
325
412
|
*
|
|
326
|
-
*
|
|
413
|
+
* Served out of the compilation (`abideModules`) — the one a build kept, or `abide dev`'s.
|
|
327
414
|
*/
|
|
328
415
|
export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
|
|
329
416
|
|
|
@@ -336,14 +423,12 @@ export type ServerTable = { routes: ServerRoute[]; errors: ServerError[] }
|
|
|
336
423
|
*/
|
|
337
424
|
export type SourceParts = {
|
|
338
425
|
table: ServerTable
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
handlerModule: (source: string) => Promise<Record<string, unknown>>
|
|
343
|
-
/** Every public file, by its name below the directory to its path. 38.19, 38.20. */
|
|
426
|
+
/** The handler table the build wrote, each row with its module's loader. 16.2. */
|
|
427
|
+
handlers: MountRow[]
|
|
428
|
+
/** Every public file, by its name below the directory to its path. 38.18, 38.19. */
|
|
344
429
|
published: Map<string, string>
|
|
345
430
|
/**
|
|
346
|
-
* The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.
|
|
431
|
+
* The bundle's manifest, its lane, and `head` — `abide dev`'s reload client (38.17), after the
|
|
347
432
|
* app's own tags.
|
|
348
433
|
*
|
|
349
434
|
* A PROMISE WHERE THE BUNDLE IS STILL BUILDING: `abide dev`'s handlers mount and its public files
|
|
@@ -363,33 +448,57 @@ type ClientLane = {
|
|
|
363
448
|
* The sources an `App` is booted over, off the parts a command supplies.
|
|
364
449
|
*
|
|
365
450
|
* 30.9 — MOST SPECIFIC FIRST, ranked off the table rather than in it. The ordering IS the client
|
|
366
|
-
* router's own: both seams compile through `rankRoutes` and
|
|
451
|
+
* router's own: both seams compile through `rankRoutes` and walk a route with `matchOf` — the
|
|
452
|
+
* server past a route that has no answer for the method asked, which the router never is — so
|
|
367
453
|
* "the two cannot resolve one address differently" is a grep rather than a promise. 35.2 — the
|
|
368
454
|
* error pages deepest directory first, so the nearest answers.
|
|
369
455
|
*/
|
|
370
|
-
export async function appSources(parts: SourceParts): Promise<
|
|
456
|
+
export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
371
457
|
const [mounted, published, client] = await Promise.all([
|
|
372
|
-
mountHandlers(parts.handlers
|
|
458
|
+
mountHandlers(parts.handlers),
|
|
373
459
|
publicFilesAt(parts.published),
|
|
374
460
|
parts.client,
|
|
375
461
|
])
|
|
376
|
-
|
|
462
|
+
// 35.6 — every page is served in abide's own document; what it carries beyond that, the root
|
|
463
|
+
// `layout.abide` writes (35.15).
|
|
377
464
|
const documentOf = (modules: string[]): string =>
|
|
378
|
-
intoHead(
|
|
379
|
-
|
|
465
|
+
intoHead(
|
|
466
|
+
DEFAULT_DOCUMENT,
|
|
467
|
+
clientTags(client.manifest, modules) + client.head,
|
|
468
|
+
)
|
|
469
|
+
const pages: Omit<Ranked, 'segments'>[] = []
|
|
380
470
|
for (const route of parts.table.routes)
|
|
381
|
-
|
|
471
|
+
pages.push({
|
|
382
472
|
pattern: route.pattern,
|
|
383
473
|
load: route.load,
|
|
384
474
|
layouts: route.layouts,
|
|
385
475
|
document: documentOf(route.clientModules),
|
|
386
476
|
})
|
|
477
|
+
// 19.1 — the build refused two files at one route and method, so a route is one row however
|
|
478
|
+
// many methods answer at it.
|
|
479
|
+
const rows = new Map<string, Omit<RouteRow, 'segments'>>()
|
|
480
|
+
const rowAt = (pattern: string): Omit<RouteRow, 'segments'> => {
|
|
481
|
+
let row = rows.get(pattern)
|
|
482
|
+
if (row === undefined) {
|
|
483
|
+
row = {
|
|
484
|
+
pattern,
|
|
485
|
+
page: undefined,
|
|
486
|
+
handlers: new Map(),
|
|
487
|
+
answers: new Map(),
|
|
488
|
+
}
|
|
489
|
+
rows.set(pattern, row)
|
|
490
|
+
}
|
|
491
|
+
return row
|
|
492
|
+
}
|
|
493
|
+
for (const page of rankRoutes(pages)) rowAt(page.pattern).page = page
|
|
494
|
+
for (const handler of mounted.handlers.values())
|
|
495
|
+
rowAt(handler.address).handlers.set(handler.method, handler)
|
|
387
496
|
const errors: RankedError[] = []
|
|
388
497
|
for (const error of parts.table.errors)
|
|
389
498
|
errors.push({ ...error, segments: parseRoutePattern(error.pattern) })
|
|
390
499
|
errors.sort((a, b) => b.segments.length - a.segments.length)
|
|
391
|
-
|
|
392
|
-
routes: rankRoutes(
|
|
500
|
+
const sources: AppSources = {
|
|
501
|
+
routes: rankRoutes([...rows.values()]),
|
|
393
502
|
errors,
|
|
394
503
|
errorDocument: documentOf([]),
|
|
395
504
|
framework: client.framework,
|
|
@@ -397,12 +506,34 @@ export async function appSources(parts: SourceParts): Promise<LoadedSources> {
|
|
|
397
506
|
explained: parts.explained,
|
|
398
507
|
...mounted,
|
|
399
508
|
}
|
|
509
|
+
for (const row of sources.routes) {
|
|
510
|
+
if (row.page !== undefined)
|
|
511
|
+
row.answers.set(
|
|
512
|
+
row.page,
|
|
513
|
+
answerInside(sources, row.page, rungsOver(row.pattern)),
|
|
514
|
+
)
|
|
515
|
+
for (const handler of row.handlers.values())
|
|
516
|
+
row.answers.set(
|
|
517
|
+
handler,
|
|
518
|
+
answerInside(sources, handler, handler.rungs),
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
return sources
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** `found`'s answer inside the rungs filed over its route. 16.14, 37.2. */
|
|
525
|
+
function answerInside(
|
|
526
|
+
app: AppSources,
|
|
527
|
+
found: Ranked | Mounted,
|
|
528
|
+
rungs: AppRung[],
|
|
529
|
+
): (ctx: { request: Request }) => Promise<Response> {
|
|
530
|
+
return compose(rungs, ({ request }) => answerMatched(app, request, found))
|
|
400
531
|
}
|
|
401
532
|
|
|
402
533
|
/**
|
|
403
534
|
* The head tags that load the app: its metas, stylesheet, entry script and the route's preloads.
|
|
404
535
|
*
|
|
405
|
-
* 38.
|
|
536
|
+
* 38.22, and 38.23 for the stylesheet beside it. An app does not write either: the bundle's
|
|
406
537
|
* address is the build's, and a hand-written one is a path that goes stale the first time the
|
|
407
538
|
* output layout moves.
|
|
408
539
|
*
|
|
@@ -417,23 +548,25 @@ export async function appSources(parts: SourceParts): Promise<LoadedSources> {
|
|
|
417
548
|
* component is not on it matches nothing. An imported stylesheet carries no token and applies as
|
|
418
549
|
* written, wherever it is served.
|
|
419
550
|
*
|
|
420
|
-
* 38.
|
|
551
|
+
* 38.28 — THE ROUTE'S CHUNKS ARE PRELOADED BESIDE THE ENTRY. Left to the client, the page's chunk
|
|
421
552
|
* and the shared runtime it imports are only asked for once the entry has arrived and run the
|
|
422
553
|
* router — a whole round trip after the document, for the largest file in the bundle. Named here,
|
|
423
554
|
* they are requested while the document is still being parsed.
|
|
424
555
|
*
|
|
425
556
|
* EVERY NAME IS THE MANIFEST'S, the entry's included: a build hashes it (38.5). A manifest with no
|
|
426
|
-
* entry is a bundle that failed under `abide dev` (38.
|
|
557
|
+
* entry is a bundle that failed under `abide dev` (38.17), and gets no tags rather than a
|
|
427
558
|
* `<script>` at a 404.
|
|
428
559
|
*/
|
|
429
560
|
export function clientTags(manifest: Manifest, modules: string[]): string {
|
|
430
561
|
if (manifest.entry === undefined) return ''
|
|
431
|
-
// 28.
|
|
562
|
+
// 28.10 — `DEBUG`, through the document rather than the bundle, where the server has one.
|
|
432
563
|
const debug = process.env.DEBUG
|
|
433
564
|
let tags =
|
|
434
565
|
debug === undefined || debug === ''
|
|
435
566
|
? ''
|
|
436
567
|
: `<meta name="${DEBUG_META}" content="${escapeAttribute(debug)}">`
|
|
568
|
+
// 28.4 — the app's name, so a browser's `log.named` prefixes the one the server's does.
|
|
569
|
+
tags += `<meta name="${APP_META}" content="${escapeAttribute(appName())}">`
|
|
437
570
|
// 16.10 — the mount the browser resolves every address against, the root's `''` included: a
|
|
438
571
|
// lookup that finds no tag has walked the whole document to say so.
|
|
439
572
|
tags += `<meta name="${MOUNT_META}" content="${escapeAttribute(MOUNT.current())}">`
|
|
@@ -448,38 +581,10 @@ export function clientTags(manifest: Manifest, modules: string[]): string {
|
|
|
448
581
|
return tags
|
|
449
582
|
}
|
|
450
583
|
|
|
451
|
-
/**
|
|
452
|
-
* The shell with every root-absolute `href` and `src` it writes put below the mount.
|
|
453
|
-
*
|
|
454
|
-
* 16.10, 36.11 — THE SHELL IS NOT A TEMPLATE, so it has no `url` to spell an address with, and the
|
|
455
|
-
* `/fonts/…` it preloads is a public file answered below the mount. The same shell is every page's
|
|
456
|
-
* at every depth, so a relative address is no spelling either. `//` is another host's.
|
|
457
|
-
*/
|
|
458
|
-
function shellUnderMount(shell: string): string {
|
|
459
|
-
const root = mounted('/')
|
|
460
|
-
return root === '/'
|
|
461
|
-
? shell
|
|
462
|
-
: shell.replace(
|
|
463
|
-
/\b(href|src)=(["'])\/(?!\/)/g,
|
|
464
|
-
(_whole, name: string, quote: string) =>
|
|
465
|
-
`${name}=${quote}${root}`,
|
|
466
|
-
)
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* The shell with `tags` at the head anchor `render` uses.
|
|
471
|
-
*
|
|
472
|
-
* So a shell with no `</head>` puts them where its head content goes rather than inside the body.
|
|
473
|
-
*/
|
|
474
|
-
function intoHead(shell: string, tags: string): string {
|
|
475
|
-
const at = headAnchor(shell)
|
|
476
|
-
return shell.slice(0, at) + tags + shell.slice(at)
|
|
477
|
-
}
|
|
478
|
-
|
|
479
584
|
/**
|
|
480
585
|
* The public files by address, off each file's name and where its bytes are.
|
|
481
586
|
*
|
|
482
|
-
* 38.
|
|
587
|
+
* 38.18 and 38.19's directory, as an address per file — or a compiled app's, whose bytes are
|
|
483
588
|
* inside itself. Read at boot rather than per request, and the ETAG IS READ WITH IT: 21.13
|
|
484
589
|
* derives it from the bytes, so there is no cheaper moment than the one read that is happening
|
|
485
590
|
* anyway. The bytes are hashed and dropped — `Bun.file` streams the body when somebody asks, so a
|
|
@@ -531,15 +636,44 @@ function readsOnly(request: Request): boolean {
|
|
|
531
636
|
return request.method === 'GET' || request.method === 'HEAD'
|
|
532
637
|
}
|
|
533
638
|
|
|
639
|
+
const READS = ['GET', 'HEAD']
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* 15.17 — a 405, naming what is answered at `pathname`.
|
|
643
|
+
*
|
|
644
|
+
* `request` where a route's methods were asked, which 15.22 answers in the asker's lane; a public
|
|
645
|
+
* file and the bundle are a document's alone.
|
|
646
|
+
*/
|
|
534
647
|
async function methodRefused(
|
|
535
648
|
app: AppSources,
|
|
536
649
|
pathname: string,
|
|
650
|
+
allowed: string[],
|
|
651
|
+
request?: Request,
|
|
537
652
|
): Promise<Response> {
|
|
538
|
-
const
|
|
539
|
-
answer
|
|
653
|
+
const failure = refuse(405)
|
|
654
|
+
const answer =
|
|
655
|
+
request === undefined
|
|
656
|
+
? await answerFailure(app, pathname, failure)
|
|
657
|
+
: await laneFailure(app, pathname, failure, request)
|
|
658
|
+
answer.headers.set('allow', allowed.join(', '))
|
|
540
659
|
return answer
|
|
541
660
|
}
|
|
542
661
|
|
|
662
|
+
/**
|
|
663
|
+
* A failure at an address whose lane only the request can say: a page's, painted, where it prefers
|
|
664
|
+
* a document, and a handler's otherwise. 15.22.
|
|
665
|
+
*/
|
|
666
|
+
function laneFailure(
|
|
667
|
+
app: AppSources,
|
|
668
|
+
pathname: string,
|
|
669
|
+
failure: Failed<string, unknown>,
|
|
670
|
+
request: Request,
|
|
671
|
+
): Response | Promise<Response> {
|
|
672
|
+
return prefers(request, 'text/html', 'application/json')
|
|
673
|
+
? answerFailure(app, pathname, failure)
|
|
674
|
+
: answered(failure)
|
|
675
|
+
}
|
|
676
|
+
|
|
543
677
|
/**
|
|
544
678
|
* A public file's answer, conditional and range-aware off the etag read at boot.
|
|
545
679
|
*
|