@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
package/src/server/createApp.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { MOUNT, unmounted } from '#shared/mount.ts'
|
|
|
12
12
|
import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
|
|
13
13
|
import { servedTable } from './abideModules.ts'
|
|
14
14
|
import { type AppSources, answerRequest } from './answerRequest.ts'
|
|
15
|
+
import type { Mount } from './answerRpc.ts'
|
|
15
16
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
16
17
|
import { appDirectories } from './appDirectories.ts'
|
|
17
18
|
import { holdsApp } from './appTree.ts'
|
|
@@ -61,17 +62,69 @@ export interface App {
|
|
|
61
62
|
*/
|
|
62
63
|
export async function createApp(): Promise<App> {
|
|
63
64
|
const root = appRoot(undefined)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
const app = await appOnDisk(
|
|
66
|
+
root === undefined ? undefined : { root, out: buildDirectory(root) },
|
|
67
|
+
(root) => servedBuild(root),
|
|
68
|
+
).boot()
|
|
69
|
+
if (typeof app === 'string')
|
|
70
|
+
throw new Error(`abide: createApp found ${app}`)
|
|
71
|
+
return app
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One app, as one invocation means it: FOUND ONCE, and everything a command derives — the banner's
|
|
76
|
+
* identity, the shell's handler table, the boot — read off this rather than found again from where
|
|
77
|
+
* the process happens to stand. An app on disk (`appOnDisk`), or the one an executable holds.
|
|
78
|
+
*/
|
|
79
|
+
export type AppHandle = {
|
|
80
|
+
/** 27.3's name and version: the banner's, and what a script `run` resolves its configuration with. */
|
|
81
|
+
package: AppPackage
|
|
82
|
+
/**
|
|
83
|
+
* The handler table of a build already there, NEVER ONE MADE FOR THE ASKING: `call` and the
|
|
84
|
+
* shell are aimed at a running app (38.7), which this tree may not even be. Empty where none.
|
|
85
|
+
*/
|
|
86
|
+
handlers: () => Promise<Mount[]>
|
|
87
|
+
/** The app's build made this process's, and booted — or why there is nothing to boot. */
|
|
88
|
+
boot: () => Promise<App | string>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Why there is no app where none was named: where 38.25 looked. 38.26. */
|
|
92
|
+
export function noAppHere(): string {
|
|
93
|
+
return `no app here. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The app on disk at `found` — its source root and build directory, both absolute — or the
|
|
98
|
+
* refusal where nothing was found. `built` makes its build this process's (`serveBuild`, which
|
|
99
|
+
* builds one nobody built — 38.100) and answers the directory it is in, or `undefined` where
|
|
100
|
+
* nothing under the root builds.
|
|
101
|
+
*/
|
|
102
|
+
export function appOnDisk(
|
|
103
|
+
found: { root: string; out: string } | undefined,
|
|
104
|
+
built: (root: string, out: string) => Promise<string | undefined>,
|
|
105
|
+
): AppHandle {
|
|
106
|
+
if (found === undefined)
|
|
107
|
+
return {
|
|
108
|
+
package: packageUnder(process.cwd()),
|
|
109
|
+
handlers: async () => [],
|
|
110
|
+
boot: async () => noAppHere(),
|
|
111
|
+
}
|
|
112
|
+
const { root, out } = found
|
|
113
|
+
const origin = originUnder(root)
|
|
114
|
+
return {
|
|
115
|
+
package: origin.package,
|
|
116
|
+
handlers: async () => {
|
|
117
|
+
const { serveBuild } = await import('./serveBuild.ts')
|
|
118
|
+
if (!(await serveBuild(root, out, async () => undefined))) return []
|
|
119
|
+
return (await servedTable(root)).handlers
|
|
120
|
+
},
|
|
121
|
+
boot: async () => {
|
|
122
|
+
const served = await built(root, out)
|
|
123
|
+
if (served === undefined) return `nothing under ${root} to build.`
|
|
124
|
+
const { loadBuild } = await import('./serveBuild.ts')
|
|
125
|
+
return bootApp(origin, () => loadBuild({ root, out: served }))
|
|
126
|
+
},
|
|
127
|
+
}
|
|
75
128
|
}
|
|
76
129
|
|
|
77
130
|
/**
|
|
@@ -152,22 +205,43 @@ export function originUnder(root: string): AppOrigin {
|
|
|
152
205
|
*/
|
|
153
206
|
export type BootedApp = App & { openMux(): Mux }
|
|
154
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Enters the app as this process's server — every slot `#shared` leaves for one filled, the app's
|
|
210
|
+
* identity set, every server block imported — and re-resolves the configuration: the one preamble
|
|
211
|
+
* `bootApp`, a script `run` and the desktop launcher each start with.
|
|
212
|
+
*
|
|
213
|
+
* 37.1 — EVERY SERVER BLOCK, imported for what it CALLS: every hook it runs is filed by the time the
|
|
214
|
+
* import settles, which is before `onStart` is composed around a boot. 38.57 — RE-RESOLVED: a module
|
|
215
|
+
* this process loaded first may already have read the configuration, under no identity.
|
|
216
|
+
*/
|
|
217
|
+
export async function enterApp(origin: AppOrigin): Promise<void> {
|
|
218
|
+
installServer()
|
|
219
|
+
APP_PACKAGE.current = origin.package
|
|
220
|
+
if (origin.hooks !== undefined) await origin.hooks()
|
|
221
|
+
config.invalidate()
|
|
222
|
+
}
|
|
223
|
+
|
|
155
224
|
/** An `App` booted over the sources `load` supplies, inside the app's `onStart` hooks. */
|
|
156
225
|
export async function bootApp(
|
|
157
226
|
origin: AppOrigin,
|
|
158
227
|
load: () => Promise<AppSources>,
|
|
159
228
|
): Promise<BootedApp> {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (origin.hooks !== undefined) await origin.hooks()
|
|
229
|
+
await enterApp(origin)
|
|
230
|
+
return bootEntered(load)
|
|
231
|
+
}
|
|
164
232
|
|
|
233
|
+
/**
|
|
234
|
+
* `bootApp` in a process `enterApp` already entered the app in: the desktop launcher's, which enters
|
|
235
|
+
* once for the hooks its own page reads and boots again on every start.
|
|
236
|
+
*/
|
|
237
|
+
export async function bootEntered(
|
|
238
|
+
load: () => Promise<AppSources>,
|
|
239
|
+
): Promise<BootedApp> {
|
|
165
240
|
// 37.4 — THE BOOT, INSIDE `onStart`. 37.6 — a throw out of a hook or out of the boot rejects
|
|
166
241
|
// `createApp`, and nothing is served.
|
|
167
242
|
//
|
|
168
243
|
// 42.2 — THE CONFIGURATION IS READ AS PART OF THE BOOT, so a configuration that does not resolve
|
|
169
244
|
// is a boot that fails rather than a first request that does.
|
|
170
|
-
APP_PACKAGE.current = origin.package
|
|
171
245
|
let sources: AppSources | undefined
|
|
172
246
|
await compose(inOrder(HOOKS.starts), async () => {
|
|
173
247
|
config.invalidate()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn.
|
|
2
|
+
// A component is set up where its parent's markup invokes it, so its build cannot wait inline; it is
|
|
3
|
+
// left as a marker in the parent's head and body, built once the turn has run, and written in where
|
|
4
|
+
// its marker stands. A child whose setup made no call builds where it stands, as before, and pays a
|
|
5
|
+
// counter read on either side of its setup for the difference.
|
|
6
|
+
//
|
|
7
|
+
// THE MARKERS ARE NEVER IN WHAT IS SENT. Every one is replaced before `composePage` answers, and the
|
|
8
|
+
// nonce is never written anywhere, so no text an author or a caller supplies can stand in for one.
|
|
9
|
+
|
|
10
|
+
import { regionMarkup } from '#shared/anchorMarkup.ts'
|
|
11
|
+
import { isReactive, isStreaming, loadOf } from '#shared/reactive/index.ts'
|
|
12
|
+
import type { Rendered } from './render.ts'
|
|
13
|
+
import type { PageCall } from './requestScope.ts'
|
|
14
|
+
|
|
15
|
+
// SYNCHRONOUS SETUP IS WHAT MAKES ONE COUNTER ENOUGH. 34.3 keeps a setup from awaiting, so no other
|
|
16
|
+
// request's render runs between a component's two reads, however many renders are in flight.
|
|
17
|
+
let callsInRenders = 0
|
|
18
|
+
|
|
19
|
+
/** How many calls renders have made, read on either side of a component's setup. */
|
|
20
|
+
export function callsInRender(): number {
|
|
21
|
+
return callsInRenders
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Counts a call a page's render made (`reachedInRender`). */
|
|
25
|
+
export function noteCallInRender(): void {
|
|
26
|
+
callsInRenders += 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A deferred child: its record, and the page's calls its setup made, marked with its build. */
|
|
30
|
+
type Deferred = { rendered: Rendered; callsFrom: number; callsTo: number }
|
|
31
|
+
|
|
32
|
+
type Build = { deferred: Deferred[]; calls: PageCall[] }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The render whose markup is being built. Set only while `pass` runs, which is synchronous, so
|
|
36
|
+
* renders in flight never see each other's.
|
|
37
|
+
*/
|
|
38
|
+
let building: Build | undefined
|
|
39
|
+
|
|
40
|
+
const NONCE = crypto.randomUUID()
|
|
41
|
+
const MARKER = new RegExp(`\u0000${NONCE}([hb])(\\d+)\u0000`, 'g')
|
|
42
|
+
|
|
43
|
+
/** The page's calls so far, for a child to note where its setup's begin; `-1` outside a build. */
|
|
44
|
+
export function callsMadeSoFar(): number {
|
|
45
|
+
return building === undefined ? -1 : building.calls.length
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Where a child that deferred stands in its parent's markup: a marker in the head and in the body,
|
|
50
|
+
* or `undefined` where no page is being built and the child builds where it stands.
|
|
51
|
+
*/
|
|
52
|
+
export function deferBuild(
|
|
53
|
+
rendered: Rendered,
|
|
54
|
+
heads: string[],
|
|
55
|
+
callsFrom: number,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
if (building === undefined) return undefined
|
|
58
|
+
const at = building.deferred.length
|
|
59
|
+
building.deferred.push({
|
|
60
|
+
rendered,
|
|
61
|
+
callsFrom,
|
|
62
|
+
callsTo: building.calls.length,
|
|
63
|
+
})
|
|
64
|
+
heads.push(`\u0000${NONCE}h${at}\u0000`)
|
|
65
|
+
return regionMarkup(`\u0000${NONCE}b${at}\u0000`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `build`'s markup, with every child it deferred built after the turn — and the children those
|
|
70
|
+
* defer after another — each written in where its marker stands.
|
|
71
|
+
*/
|
|
72
|
+
export async function buildDeferred(
|
|
73
|
+
calls: PageCall[],
|
|
74
|
+
build: () => { head: string; body: string },
|
|
75
|
+
): Promise<{ head: string; body: string }> {
|
|
76
|
+
const state: Build = { deferred: [], calls }
|
|
77
|
+
let built = { head: '', body: '' }
|
|
78
|
+
pass(state, () => {
|
|
79
|
+
built = build()
|
|
80
|
+
})
|
|
81
|
+
if (state.deferred.length === 0) return built
|
|
82
|
+
const fills: { head: string; body: string }[] = []
|
|
83
|
+
let from = 0
|
|
84
|
+
while (from < state.deferred.length) {
|
|
85
|
+
await new Promise((settled) => setImmediate(settled))
|
|
86
|
+
const to = state.deferred.length
|
|
87
|
+
pass(state, () => {
|
|
88
|
+
for (let at = from; at < to; at++)
|
|
89
|
+
fills.push(filled((state.deferred[at] as Deferred).rendered))
|
|
90
|
+
})
|
|
91
|
+
from = to
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
head: expanded(built.head, fills),
|
|
95
|
+
body: expanded(built.body, fills),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Marks every call not yet marked: settled or not, as the markup just built read it.
|
|
101
|
+
*
|
|
102
|
+
* 41.25 — READ WHERE THE MARKUP HAS JUST BEEN BUILT, before anything has run since: a call that
|
|
103
|
+
* settles a microtask later was painted in flight, and marked settled it has hydration choose an
|
|
104
|
+
* arm the server never painted.
|
|
105
|
+
*/
|
|
106
|
+
export function markPainted(calls: PageCall[]): void {
|
|
107
|
+
for (const call of calls)
|
|
108
|
+
if (call.painted === undefined) call.painted = paintedSettled(call)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// One synchronous build pass, and the calls it built from marked at its end — all but those a
|
|
112
|
+
// child it deferred made, which its own pass marks.
|
|
113
|
+
function pass(state: Build, run: () => void): void {
|
|
114
|
+
const deferredFrom = state.deferred.length
|
|
115
|
+
building = state
|
|
116
|
+
try {
|
|
117
|
+
run()
|
|
118
|
+
} finally {
|
|
119
|
+
building = undefined
|
|
120
|
+
}
|
|
121
|
+
const { calls, deferred } = state
|
|
122
|
+
for (let at = 0; at < calls.length; at++) {
|
|
123
|
+
const call = calls[at] as PageCall
|
|
124
|
+
if (
|
|
125
|
+
call.painted !== undefined ||
|
|
126
|
+
ownedByDeferred(at, deferred, deferredFrom)
|
|
127
|
+
)
|
|
128
|
+
continue
|
|
129
|
+
call.painted = paintedSettled(call)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ownedByDeferred(
|
|
134
|
+
call: number,
|
|
135
|
+
deferred: Deferred[],
|
|
136
|
+
from: number,
|
|
137
|
+
): boolean {
|
|
138
|
+
for (let at = from; at < deferred.length; at++) {
|
|
139
|
+
const child = deferred[at] as Deferred
|
|
140
|
+
if (call >= child.callsFrom && call < child.callsTo) return true
|
|
141
|
+
}
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 41.26 — a call that streams is never marked.
|
|
146
|
+
function paintedSettled({ reached }: PageCall): boolean {
|
|
147
|
+
return (
|
|
148
|
+
isReactive(reached) &&
|
|
149
|
+
loadOf(reached) === undefined &&
|
|
150
|
+
!isStreaming(reached)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 35.16 — a document is written by the outermost component alone, and `rendered` is placed. */
|
|
155
|
+
export function refuseDocument(rendered: Rendered): void {
|
|
156
|
+
if (rendered.document !== undefined)
|
|
157
|
+
throw new Error(
|
|
158
|
+
'abide: a component that writes <html> is placed inside another, and only the outermost component writes the document. RULEBOOK 35.16.',
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function filled(rendered: Rendered): { head: string; body: string } {
|
|
163
|
+
refuseDocument(rendered)
|
|
164
|
+
return { head: rendered.head, body: rendered.body }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function expanded(
|
|
168
|
+
markup: string,
|
|
169
|
+
fills: { head: string; body: string }[],
|
|
170
|
+
): string {
|
|
171
|
+
return markup.replace(MARKER, (_, kind: string, at: string) => {
|
|
172
|
+
const fill = fills[Number(at)] as { head: string; body: string }
|
|
173
|
+
return expanded(kind === 'h' ? fill.head : fill.body, fills)
|
|
174
|
+
})
|
|
175
|
+
}
|