@abide/abide 0.104.0 → 0.105.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 +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
// BY POSITION, NOT BY PATH (23.19): a directory a file mounts places its files where the app says,
|
|
15
15
|
// so a `server` directory inside it is fenced as the app's own would be, wherever the package lies.
|
|
16
16
|
|
|
17
|
-
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
18
17
|
import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
|
|
19
|
-
import {
|
|
18
|
+
import type { Compilation } from './abideModules.ts'
|
|
20
19
|
import {
|
|
21
20
|
COMPONENT_NAMESPACE,
|
|
22
|
-
|
|
21
|
+
realFileOf,
|
|
23
22
|
STYLE_NAMESPACE,
|
|
24
23
|
TEXT_NAMESPACE,
|
|
25
|
-
} from './
|
|
24
|
+
} from './faceOf.ts'
|
|
25
|
+
import { placeOf, positionOf } from './placement.ts'
|
|
26
26
|
|
|
27
27
|
// Every namespace a browser bundle loads a file of the app's in: its modules, its components, a
|
|
28
28
|
// file's text and a component's stylesheet.
|
|
@@ -38,18 +38,6 @@ export function fenceServer(
|
|
|
38
38
|
root: string,
|
|
39
39
|
mounted: Compilation['mounted'],
|
|
40
40
|
): Bun.BunPlugin {
|
|
41
|
-
// Mounted directories first: one under the root's `node_modules` is the app's at its position.
|
|
42
|
-
const directories: [string, string][] = []
|
|
43
|
-
for (const [directory, at] of Object.entries(mounted))
|
|
44
|
-
directories.push([`${resolve(root, directory)}/`, at])
|
|
45
|
-
const positionOf = (path: string): string | undefined => {
|
|
46
|
-
for (const [directory, at] of directories)
|
|
47
|
-
if (path.startsWith(directory))
|
|
48
|
-
return `${at}${path.slice(directory.length)}`
|
|
49
|
-
return path.startsWith(`${root}/`)
|
|
50
|
-
? path.slice(root.length + 1)
|
|
51
|
-
: undefined
|
|
52
|
-
}
|
|
53
41
|
// Anything with a `server` directory in its path, and `placeOf` decides by its position — a
|
|
54
42
|
// load it passes on falls through to the next plugin's, or the bundler's own.
|
|
55
43
|
const filter = new RegExp(`/${SERVER_DIRECTORY}/`)
|
|
@@ -58,14 +46,18 @@ export function fenceServer(
|
|
|
58
46
|
setup(build) {
|
|
59
47
|
for (const namespace of FENCED)
|
|
60
48
|
build.onLoad({ filter, namespace }, (args) => {
|
|
61
|
-
const position = positionOf(
|
|
49
|
+
const position = positionOf(
|
|
50
|
+
root,
|
|
51
|
+
mounted,
|
|
52
|
+
realFileOf(args.path),
|
|
53
|
+
)
|
|
62
54
|
if (
|
|
63
55
|
position === undefined ||
|
|
64
56
|
placeOf(position) !== 'server'
|
|
65
57
|
)
|
|
66
58
|
return undefined
|
|
67
59
|
throw new Error(
|
|
68
|
-
`abide: ${position
|
|
60
|
+
`abide: ${position} is under a server directory, and the browser's bundle imports it. RULEBOOK 16.29.`,
|
|
69
61
|
)
|
|
70
62
|
})
|
|
71
63
|
},
|
package/src/server/index.ts
CHANGED
|
@@ -47,6 +47,9 @@ export { type Config, config } from './config.ts'
|
|
|
47
47
|
export type { ConfigField } from './configFields.ts'
|
|
48
48
|
// 42 — the app object, and 37's hooks a `<script server>` calls.
|
|
49
49
|
export { type App, createApp } from './createApp.ts'
|
|
50
|
+
// 41.28's run-time half: an emitted component reads it on either side of its setup, and one whose
|
|
51
|
+
// setup made a call answers as a page does. Not an app's name.
|
|
52
|
+
export { callsInRender } from './deferredBuilds.ts'
|
|
50
53
|
// 37.1's run-time half: what a compiled server module calls around its `<script server>`, so the
|
|
51
54
|
// hooks it makes are filed at its route.
|
|
52
55
|
export {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// WHERE A FILE SITS IN AN APP, AND SO WHAT IT IS — answered once per file, off the path it answers by
|
|
2
|
+
// (its POSITION, 23.19), and read by every walk, fence, emit and check rather than re-derived beside
|
|
3
|
+
// each. The route GRAMMAR is `routePattern`'s, which the browser's router reads too; this half is the
|
|
4
|
+
// build's and the server's alone, so it stays out of the client's closure.
|
|
5
|
+
|
|
6
|
+
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
7
|
+
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
8
|
+
import {
|
|
9
|
+
HOLDS_SERVER,
|
|
10
|
+
RESERVED_DIRECTORIES,
|
|
11
|
+
SERVER_DIRECTORY,
|
|
12
|
+
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
13
|
+
|
|
14
|
+
/** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
|
|
15
|
+
export type Place =
|
|
16
|
+
| 'address'
|
|
17
|
+
| 'server'
|
|
18
|
+
| 'public'
|
|
19
|
+
| 'lib'
|
|
20
|
+
| 'tests'
|
|
21
|
+
| 'dependency'
|
|
22
|
+
| 'skipped'
|
|
23
|
+
|
|
24
|
+
/** Whether a `.abide` is a page, a layout, an error page or a component. */
|
|
25
|
+
type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
26
|
+
|
|
27
|
+
/** One file placed in the app: where it is, where it answers by, and what that makes it. */
|
|
28
|
+
export type Placement = {
|
|
29
|
+
/** Below the root for the app's own, and above it — `../node_modules/…` — for a dependency's. */
|
|
30
|
+
relative: string
|
|
31
|
+
/** 23.19 — the path it answers by: its own below the root, or the one its mount gives it. */
|
|
32
|
+
position: string
|
|
33
|
+
place: Place
|
|
34
|
+
/** 23.21 — what a `.abide` at `position` is; a handler file is read as a page would be. */
|
|
35
|
+
kind: FileKind
|
|
36
|
+
/** 23.14 — the route of a page, a layout or an error page, and `undefined` for a component. */
|
|
37
|
+
route: string | undefined
|
|
38
|
+
/** 37.1 — the route its hooks and private handlers are filed at. */
|
|
39
|
+
scope: string
|
|
40
|
+
/**
|
|
41
|
+
* 34.23 — a package's file, by where it LIES rather than where it answers: compiled on the app's
|
|
42
|
+
* runtime (34.26), and reported on by its package rather than the app.
|
|
43
|
+
*/
|
|
44
|
+
dependency: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The file at `relative` placed at `position`.
|
|
49
|
+
*
|
|
50
|
+
* 23.21 — WHERE IT SITS, THEN ITS NAME. Every `.abide` directly in an address directory is a page
|
|
51
|
+
* but a `layout.abide`, an `error.abide` and the root's `launcher.abide`; one under a fence is a
|
|
52
|
+
* component whatever it is called. 37.1 — its hooks are filed at an address's own route; under a
|
|
53
|
+
* `lib` or a `tests` directory, at the route of the directory holding it, so a component an admin
|
|
54
|
+
* page renders runs under the admin route's rungs; under a `server` or a `public` directory, for a
|
|
55
|
+
* dependency's file, and for the launcher (38.82), at the root's.
|
|
56
|
+
*
|
|
57
|
+
* `position` is RELATIVE TO THE APP'S ROOT: its directories are its route, so an absolute path would
|
|
58
|
+
* read the machine's directories as the app's.
|
|
59
|
+
*/
|
|
60
|
+
export function placement(relative: string, position = relative): Placement {
|
|
61
|
+
const place = placeOf(position)
|
|
62
|
+
let kind: FileKind = 'component'
|
|
63
|
+
let scope = '/'
|
|
64
|
+
if (place === 'address' && position !== LAUNCHER_PAGE) {
|
|
65
|
+
const name = position.slice(position.lastIndexOf('/') + 1)
|
|
66
|
+
kind =
|
|
67
|
+
name === 'layout.abide'
|
|
68
|
+
? 'layout'
|
|
69
|
+
: name === 'error.abide'
|
|
70
|
+
? 'error'
|
|
71
|
+
: 'page'
|
|
72
|
+
scope = routeOf(position)
|
|
73
|
+
} else if (place === 'lib' || place === 'tests') {
|
|
74
|
+
const parts = position.split('/')
|
|
75
|
+
// The fence `placeOf` named is the first one, and its name is its place.
|
|
76
|
+
scope = `/${parts.slice(0, parts.indexOf(place)).join('/')}`
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
relative,
|
|
80
|
+
position,
|
|
81
|
+
place,
|
|
82
|
+
kind,
|
|
83
|
+
route: kind === 'component' ? undefined : routeOf(position),
|
|
84
|
+
scope,
|
|
85
|
+
dependency: placeOf(relative) === 'dependency',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Where the file at `path`, relative to the app's root, sits.
|
|
91
|
+
*
|
|
92
|
+
* - `dependency` — 34.24: a package's, outside the root or under `node_modules`, whose directories
|
|
93
|
+
* are its package's layout and none of the app's;
|
|
94
|
+
* - `skipped` — under a dot directory, which no walk of the app enters;
|
|
95
|
+
* - `server`, `public`, `lib` or `tests` — 23.15: under that fence, the nearest to the root deciding,
|
|
96
|
+
* save a `server` under a `lib` or a `tests`, which is `server` — the fence the browser's bundle
|
|
97
|
+
* is checked against (16.29) is never hidden by one above it (23.16);
|
|
98
|
+
* - `address` — 23.14: everywhere else, its directories the segments of its route.
|
|
99
|
+
*
|
|
100
|
+
* The last part is the file itself, so a path ending in `/` is a directory and every part is read.
|
|
101
|
+
*/
|
|
102
|
+
export function placeOf(path: string): Place {
|
|
103
|
+
if (path.startsWith('../') || path.startsWith('/')) return 'dependency'
|
|
104
|
+
const parts = path.split('/')
|
|
105
|
+
// A `lib` or a `tests` met first, which a `server` below it still overrides.
|
|
106
|
+
let fence: Place | undefined
|
|
107
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
108
|
+
const part = parts[index] as string
|
|
109
|
+
if (skipped(part))
|
|
110
|
+
return fence ?? (part === 'node_modules' ? 'dependency' : 'skipped')
|
|
111
|
+
if (fence !== undefined) {
|
|
112
|
+
if (part === SERVER_DIRECTORY) return 'server'
|
|
113
|
+
} else if (RESERVED_DIRECTORIES.has(part)) {
|
|
114
|
+
if (!HOLDS_SERVER.has(part)) return part as Place
|
|
115
|
+
fence = part as Place
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return fence ?? 'address'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether a walk of the app passes over the entry `name`: a dot file or directory, or `node_modules`. */
|
|
122
|
+
export function skipped(name: string): boolean {
|
|
123
|
+
return name.startsWith('.') || name === 'node_modules'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The route a file answers at — its path below the root without its extension.
|
|
128
|
+
*
|
|
129
|
+
* 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
|
|
130
|
+
* `index.abide`, a `layout.abide` and an `error.abide` are their directory's.
|
|
131
|
+
*/
|
|
132
|
+
export function routeOf(path: string): string {
|
|
133
|
+
const parts = path.split('/')
|
|
134
|
+
const file = parts.pop() as string
|
|
135
|
+
// The extension alone: `[...rest].abide` carries dots of its own.
|
|
136
|
+
if (!DIRECTORY_FILES.has(file)) parts.push(file.replace(EXTENSION, ''))
|
|
137
|
+
return `/${parts.filter((part) => part !== '').join('/')}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const EXTENSION = /\.(?:abide|ts)$/
|
|
141
|
+
|
|
142
|
+
/** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
|
|
143
|
+
export const DIRECTORY_FILES = new Set([
|
|
144
|
+
'index.abide',
|
|
145
|
+
'layout.abide',
|
|
146
|
+
'error.abide',
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The position of the file at the absolute `path`: the one a mount gives it where a directory in
|
|
151
|
+
* `mounted` holds it (23.19), its own below `root` where the root does, `undefined` otherwise.
|
|
152
|
+
*
|
|
153
|
+
* `mounted` is `Compilation.mounted`, a directory below the root to the position its files take —
|
|
154
|
+
* directory-shaped, because a module under a mounted `server` directory is placed by it too.
|
|
155
|
+
*/
|
|
156
|
+
export function positionOf(
|
|
157
|
+
root: string,
|
|
158
|
+
mounted: Readonly<Record<string, string>>,
|
|
159
|
+
path: string,
|
|
160
|
+
): string | undefined {
|
|
161
|
+
for (const directory in mounted) {
|
|
162
|
+
const prefix = `${resolve(root, directory)}/`
|
|
163
|
+
if (path.startsWith(prefix))
|
|
164
|
+
return `${mounted[directory]}${path.slice(prefix.length)}`
|
|
165
|
+
}
|
|
166
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : undefined
|
|
167
|
+
}
|
package/src/server/remote.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// sent (36.6), and neither variable is read anywhere else.
|
|
5
5
|
|
|
6
6
|
import { applyPatch } from '#shared/applyPatch.ts'
|
|
7
|
-
import {
|
|
7
|
+
import { eventsOf } from '#shared/framing.ts'
|
|
8
|
+
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
8
9
|
import { firstMatch, partsOf } from '#shared/matchRoute.ts'
|
|
9
10
|
import type { PatchOp } from '#shared/reactive/PATCH_SINKS.ts'
|
|
10
11
|
import { rankRoutes } from '#shared/routePattern.ts'
|
|
@@ -59,13 +60,14 @@ export async function callHandler(
|
|
|
59
60
|
const asked = method?.toUpperCase()
|
|
60
61
|
const handler = handlerAt(handlers, address, asked)
|
|
61
62
|
const args = callArgs(given)
|
|
63
|
+
const route = new HandlerRoute(address)
|
|
62
64
|
const chosen = (asked ??
|
|
63
65
|
(handler?.method as Method | undefined) ??
|
|
64
66
|
(args === undefined ? 'GET' : 'POST')) as Method
|
|
65
67
|
const request =
|
|
66
68
|
args instanceof URLSearchParams
|
|
67
|
-
? textRequest(chosen,
|
|
68
|
-
: rpcRequest(chosen,
|
|
69
|
+
? textRequest(chosen, route, args)
|
|
70
|
+
: rpcRequest(chosen, route, args)
|
|
69
71
|
const answer = await fetch(`${url}${request.url}`, {
|
|
70
72
|
...request.init,
|
|
71
73
|
headers: { ...remoteHeaders(), ...request.init.headers },
|
|
@@ -129,19 +131,19 @@ function handlerAt(
|
|
|
129
131
|
*/
|
|
130
132
|
function textRequest(
|
|
131
133
|
method: Method,
|
|
132
|
-
route:
|
|
134
|
+
route: HandlerRoute,
|
|
133
135
|
given: URLSearchParams,
|
|
134
136
|
): { url: string; init: RequestInit } {
|
|
135
|
-
let address = route
|
|
137
|
+
let address = route.address
|
|
136
138
|
let pairs = given
|
|
137
|
-
if (route.
|
|
139
|
+
if (route.segments !== undefined) {
|
|
138
140
|
const params: Record<string, string> = {}
|
|
139
141
|
pairs = new URLSearchParams()
|
|
140
|
-
const named = parametersOf(route)
|
|
141
142
|
for (const [name, value] of given)
|
|
142
|
-
if (
|
|
143
|
+
if (route.names.has(name)) params[name] = value
|
|
143
144
|
else pairs.append(name, value)
|
|
144
|
-
|
|
145
|
+
// Thrown as the message it is, which the command line prints.
|
|
146
|
+
address = route.path(params, (_name, message) => new Error(message))
|
|
145
147
|
}
|
|
146
148
|
if (argsInURL(method))
|
|
147
149
|
return { url: `${address}?${pairs}`, init: { method } }
|
|
@@ -169,12 +171,12 @@ export async function watch(
|
|
|
169
171
|
given: string[],
|
|
170
172
|
signal?: AbortSignal,
|
|
171
173
|
): Promise<number> {
|
|
172
|
-
const
|
|
174
|
+
const route = new HandlerRoute(`/${address.replace(/^\//, '')}`)
|
|
173
175
|
const args = callArgs(given)
|
|
174
176
|
const request =
|
|
175
177
|
args instanceof URLSearchParams
|
|
176
|
-
? textRequest('GET',
|
|
177
|
-
: rpcRequest('GET',
|
|
178
|
+
? textRequest('GET', route, args)
|
|
179
|
+
: rpcRequest('GET', route, args)
|
|
178
180
|
const answer = await fetch(`${url}${request.url}`, {
|
|
179
181
|
...request.init,
|
|
180
182
|
headers: {
|
|
@@ -207,33 +209,3 @@ export async function watch(
|
|
|
207
209
|
}
|
|
208
210
|
return 0
|
|
209
211
|
}
|
|
210
|
-
|
|
211
|
-
/** Each event of an event stream: its `event` name, `message` where it named none, and its `data`. */
|
|
212
|
-
async function* eventsOf(
|
|
213
|
-
body: ReadableStream<Uint8Array>,
|
|
214
|
-
): AsyncIterable<{ name: string; data: string }> {
|
|
215
|
-
const decoder = new TextDecoder()
|
|
216
|
-
let buffered = ''
|
|
217
|
-
for await (const chunk of body) {
|
|
218
|
-
buffered += decoder.decode(chunk, { stream: true })
|
|
219
|
-
for (
|
|
220
|
-
let end = buffered.indexOf('\n\n');
|
|
221
|
-
end !== -1;
|
|
222
|
-
end = buffered.indexOf('\n\n')
|
|
223
|
-
) {
|
|
224
|
-
const block = buffered.slice(0, end)
|
|
225
|
-
buffered = buffered.slice(end + 2)
|
|
226
|
-
let name = 'message'
|
|
227
|
-
let data: string | undefined
|
|
228
|
-
for (const line of block.split('\n')) {
|
|
229
|
-
if (line.startsWith('event:')) name = line.slice(6).trim()
|
|
230
|
-
else if (line.startsWith('data:')) {
|
|
231
|
-
const field = line.slice(5).replace(/^ /, '')
|
|
232
|
-
data = data === undefined ? field : `${data}\n${field}`
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// A comment alone — the stream's opener — is no event.
|
|
236
|
-
if (data !== undefined) yield { name, data }
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
package/src/server/render.ts
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
import { regionMarkup } from '#shared/anchorMarkup.ts'
|
|
7
7
|
import { EARLY_CALLS } from '#shared/EARLY_CALLS.ts'
|
|
8
8
|
import { headKeyOf } from '#shared/headKeyOf.ts'
|
|
9
|
-
import { isReactive, isStreaming, loadOf } from '#shared/reactive/index.ts'
|
|
10
9
|
import { isThenable } from '#shared/reactive/isThenable.ts'
|
|
11
10
|
import type { ShareScope } from '#shared/reactive/share.ts'
|
|
12
11
|
import { setUpLevels } from '#shared/setUpLevels.ts'
|
|
13
|
-
import {
|
|
12
|
+
import { buildDeferred, markPainted } from './deferredBuilds.ts'
|
|
14
13
|
import { currentRequestScope, type PageCall } from './requestScope.ts'
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -90,12 +89,19 @@ export async function composePage(
|
|
|
90
89
|
})
|
|
91
90
|
await new Promise((settled) => setImmediate(settled))
|
|
92
91
|
const outermost = levels[0] as Rendered
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const built = await buildDeferred(
|
|
93
|
+
currentRequestScope()?.page?.calls ?? [],
|
|
94
|
+
() => {
|
|
95
|
+
// THE OUTERMOST HEAD FIRST: reading it builds the chain, which is what pushes the heads
|
|
96
|
+
// below. Written end to end rather than merged: `render` merges it with the shell's in one
|
|
97
|
+
// pass (39.4).
|
|
98
|
+
const head = outermost.head
|
|
99
|
+
return { head: head + heads.join(''), body: outermost.body }
|
|
100
|
+
},
|
|
101
|
+
)
|
|
96
102
|
return {
|
|
97
|
-
head: head
|
|
98
|
-
body:
|
|
103
|
+
head: built.head,
|
|
104
|
+
body: built.body,
|
|
99
105
|
// 35.15 — the document's, which only the root layout writes, and it is outermost.
|
|
100
106
|
document: outermost.document,
|
|
101
107
|
}
|
|
@@ -317,6 +323,7 @@ export async function renderDocument(
|
|
|
317
323
|
// makes is known, and a script in the head starts each one as the request the browser's own
|
|
318
324
|
// `Rpc` would make — while the rest of the document and the bundle are still on their way.
|
|
319
325
|
const calls = currentRequestScope()?.page?.calls
|
|
326
|
+
if (calls !== undefined) markPainted(calls)
|
|
320
327
|
const early =
|
|
321
328
|
calls !== undefined && calls.length > 0 ? earlyCallScript(calls) : ''
|
|
322
329
|
// 39.4 — THE SHELL IS THE OUTERMOST CONTRIBUTOR, so its head is merged with the page's in the
|
|
@@ -335,15 +342,14 @@ export async function renderDocument(
|
|
|
335
342
|
* The head markup that starts each of the page's calls before the bundle arrives.
|
|
336
343
|
*
|
|
337
344
|
* 41.20 — THE SCRIPT ITSELF: each call started with `fetch`, and its answer left under the call's
|
|
338
|
-
* key in `EARLY_CALLS` for `wire` to take (41.21).
|
|
339
|
-
*
|
|
345
|
+
* key in `EARLY_CALLS` for `wire` to take (41.21). An answer's body is left UNREAD (16.32), so a
|
|
346
|
+
* streaming handler's copy takes its chunks as they land rather than after the last one.
|
|
340
347
|
* Inline because nothing else runs before the bundle has arrived, which is the wait it exists to
|
|
341
348
|
* overlap.
|
|
342
349
|
*
|
|
343
350
|
* 41.25 — A ROW ENDING IN `1` IS A CALL THE MARKUP WAS BUILT FROM SETTLED, a refusal included, and
|
|
344
|
-
* its entry is marked `painted` for hydration to wait on —
|
|
345
|
-
*
|
|
346
|
-
* have run since. 41.26 — A CALL THAT STREAMS IS NEVER MARKED: the
|
|
351
|
+
* its entry is marked `painted` for hydration to wait on — its body read to its bytes, so the wait
|
|
352
|
+
* covers it too. Marked as each pass of the markup is built (`markPainted`), not here. 41.26 — A CALL THAT STREAMS IS NEVER MARKED: the
|
|
347
353
|
* browser reads its chunks as they land (16.32), so no wait for its answer makes it settled where
|
|
348
354
|
* the page hydrates, and its region is 41.24's to put right.
|
|
349
355
|
*
|
|
@@ -358,18 +364,20 @@ function earlyCallScript(calls: PageCall[]): string {
|
|
|
358
364
|
| [string, string, RequestInit]
|
|
359
365
|
| [string, string, RequestInit, 1]
|
|
360
366
|
)[] = []
|
|
361
|
-
for (const { request,
|
|
367
|
+
for (const { request, painted } of calls)
|
|
362
368
|
rows.push(
|
|
363
|
-
|
|
364
|
-
loadOf(reached) === undefined &&
|
|
365
|
-
!isStreaming(reached)
|
|
369
|
+
painted === true
|
|
366
370
|
? [request.key, request.url, request.init, 1]
|
|
367
371
|
: [request.key, request.url, request.init],
|
|
368
372
|
)
|
|
369
373
|
return `<script type="application/json">${JSON.stringify(rows).replace(/</g, '\\u003c')}</script><script>${EARLY_CALL_RUNNER}</script>`
|
|
370
374
|
}
|
|
371
375
|
|
|
372
|
-
|
|
376
|
+
// THE RUNNER RECORDS, IT NEVER CLASSIFIES: the `Response`, and a painted call's body read to its end
|
|
377
|
+
// — the one decision is `painted`, which the server made. `answerOf` reads the record as it reads an
|
|
378
|
+
// answer it fetched, so no second reading of a content type lives in a string no typecheck reaches.
|
|
379
|
+
// The shape is `EarlyCall`'s.
|
|
380
|
+
export const EARLY_CALL_RUNNER = `(function(s,c){for(var i=0;i<c.length;i++)(function(k,u,n,h){var e=s[k]={painted:h,response:void 0,bytes:void 0};e.promise=fetch(u,n).then(function(r){return h?r.arrayBuffer().then(function(b){e.bytes=b;e.response=r}):e.response=r}).catch(function(){})})(c[i][0],c[i][1],c[i][2],c[i][3])})(self.${EARLY_CALLS}||(self.${EARLY_CALLS}={}),JSON.parse(document.currentScript.previousElementSibling.textContent))`
|
|
373
381
|
|
|
374
382
|
/** The early-call runner's hash, as `script-src` names it. 29.9. */
|
|
375
383
|
export const EARLY_CALL_HASH = `'sha256-${new Bun.CryptoHasher('sha256').update(EARLY_CALL_RUNNER).digest('base64')}'`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { regionMarkup } from '#shared/anchorMarkup.ts'
|
|
2
|
+
import { callsMadeSoFar, deferBuild, refuseDocument } from './deferredBuilds.ts'
|
|
2
3
|
import type { Component, Rendered } from './render.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -25,14 +26,17 @@ export function renderComponent(
|
|
|
25
26
|
heads: string[],
|
|
26
27
|
): string {
|
|
27
28
|
if (component == null) return regionMarkup('')
|
|
29
|
+
const callsFrom = callsMadeSoFar()
|
|
28
30
|
const rendered = (
|
|
29
31
|
component as (props: Record<string, () => unknown>) => Rendered
|
|
30
32
|
)(props)
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
33
|
+
// 41.28 — A CHILD WHOSE SETUP MADE A CALL ANSWERS AS A PAGE DOES, with a `scope` and its build
|
|
34
|
+
// left for the first read, and is built after the turn where a page is being built.
|
|
35
|
+
if ('scope' in rendered) {
|
|
36
|
+
const marker = deferBuild(rendered, heads, callsFrom)
|
|
37
|
+
if (marker !== undefined) return marker
|
|
38
|
+
}
|
|
39
|
+
refuseDocument(rendered)
|
|
36
40
|
if (rendered.head !== '') heads.push(rendered.head)
|
|
37
41
|
return regionMarkup(rendered.body)
|
|
38
42
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { AsyncLocalStorage } from 'node:async_hooks' // bun implements AsyncLocalStorage and has no context-carrying api of its own
|
|
13
13
|
import { Cookies } from '#shared/cookies.ts'
|
|
14
|
+
import type { Framed } from '#shared/framing.ts'
|
|
14
15
|
import type { Params } from '#shared/matchRoute.ts'
|
|
15
16
|
import type { Principal } from '#shared/principal.ts'
|
|
16
17
|
import { SCOPING, type Scope } from '#shared/reactive/scoping.ts'
|
|
@@ -18,7 +19,7 @@ import type { RpcRequest } from '#shared/rpcRequest.ts'
|
|
|
18
19
|
import type { McpSurface } from './generatedSurfaces.ts'
|
|
19
20
|
|
|
20
21
|
/** The three framings a stream is answered in. 17.6. */
|
|
21
|
-
export type Framing =
|
|
22
|
+
export type Framing = Framed | 'bytes'
|
|
22
23
|
|
|
23
24
|
/** What one request, or one `App.run`, carries across every `await` its work makes. */
|
|
24
25
|
export type RequestScope = Scope & {
|
|
@@ -123,10 +124,15 @@ export type AnsweringApp = {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
|
-
* One call a page's render made: the request the browser will make for it,
|
|
127
|
-
*
|
|
127
|
+
* One call a page's render made: the request the browser will make for it, what it reached, and
|
|
128
|
+
* whether the markup was built from it settled — `undefined` until the pass that built from it
|
|
129
|
+
* ends (41.25, `markPainted`).
|
|
128
130
|
*/
|
|
129
|
-
export type PageCall = {
|
|
131
|
+
export type PageCall = {
|
|
132
|
+
request: RpcRequest
|
|
133
|
+
reached: unknown
|
|
134
|
+
painted: boolean | undefined
|
|
135
|
+
}
|
|
130
136
|
|
|
131
137
|
// `undefined` IS A STORE, not the absence of one: a value built outside every request runs its body
|
|
132
138
|
// under it (`Cell.scope`), rather than under whichever request scheduled the drain.
|