@abide/abide 0.103.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 +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- 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/projectShadow.ts +1 -1
- 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 +152 -146
- 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 +59 -73
- 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/generatedSurfaces.ts +12 -53
- 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/versionOf.ts +1 -2
- 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/RESERVED_DIRECTORIES.ts +6 -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 +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- 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 -110
- 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 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/src/server/commands.ts
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
7
7
|
import { parseArgs } from 'node:util' // bun exposes no argv parser of its own yet
|
|
8
8
|
import { MCP_ADDRESS, OPENAPI_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
9
|
-
import type { Mount } from './answerRpc.ts'
|
|
10
9
|
import { banner } from './banner.ts'
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import { installServer } from './installServer.ts'
|
|
10
|
+
import { type App, type AppHandle, enterApp } from './createApp.ts'
|
|
11
|
+
import { stopOnSignalOrFault } from './faultPolicy.ts'
|
|
14
12
|
import {
|
|
15
13
|
appUrl,
|
|
16
14
|
callHandler,
|
|
@@ -45,17 +43,10 @@ export type Host = {
|
|
|
45
43
|
/** What usage and every message speak as: `abide`, or the name the executable was run by. */
|
|
46
44
|
name: string
|
|
47
45
|
/**
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
* knows the table of. Empty where there is none to read, and never built to be read.
|
|
46
|
+
* The app the invocation means, found once off its flags (38.25): every command that serves,
|
|
47
|
+
* boots, lists or announces it derives from this one handle.
|
|
51
48
|
*/
|
|
52
|
-
|
|
53
|
-
/** The identity a script `run` resolves its configuration with. 27.3. */
|
|
54
|
-
appPackage: () => AppPackage
|
|
55
|
-
/** The app booted in this process: `openapi` and `mcp`'s answer where no `--url` is given. */
|
|
56
|
-
boot: () => Promise<App>
|
|
57
|
-
/** The app served, or the exit code of a refusal to. 38.6. */
|
|
58
|
-
serve: (given: Given) => Promise<App | number>
|
|
49
|
+
app: (given: Given) => AppHandle
|
|
59
50
|
source: SourceCommands | undefined
|
|
60
51
|
}
|
|
61
52
|
|
|
@@ -79,7 +70,8 @@ export async function runCommands(
|
|
|
79
70
|
console.log(usage(host, commands))
|
|
80
71
|
return 0
|
|
81
72
|
}
|
|
82
|
-
if (command === undefined)
|
|
73
|
+
if (command === undefined)
|
|
74
|
+
return await shellOn(host, host.app(given), given)
|
|
83
75
|
for (const [name, , body] of commands)
|
|
84
76
|
if (name.split(' ')[0] === command) return await body(given)
|
|
85
77
|
console.error(
|
|
@@ -111,7 +103,7 @@ function commandsOf(host: Host): Row[] {
|
|
|
111
103
|
[
|
|
112
104
|
'run <file> [args…]',
|
|
113
105
|
'run a script under the abide runtime',
|
|
114
|
-
(given) => run(host, given),
|
|
106
|
+
(given) => run(host, host.app(given), given),
|
|
115
107
|
false,
|
|
116
108
|
],
|
|
117
109
|
fromSource('dev', 'serve with the compiler in the loop', 'dev'),
|
|
@@ -120,23 +112,28 @@ function commandsOf(host: Host): Row[] {
|
|
|
120
112
|
'compile every .abide under the app into .abide/',
|
|
121
113
|
'build',
|
|
122
114
|
),
|
|
123
|
-
[
|
|
115
|
+
[
|
|
116
|
+
'start',
|
|
117
|
+
'serve the app',
|
|
118
|
+
(given) => start(host, host.app(given), given),
|
|
119
|
+
false,
|
|
120
|
+
],
|
|
124
121
|
[
|
|
125
122
|
'call <address> [args]',
|
|
126
123
|
'one call to one handler',
|
|
127
|
-
(given) => call(host, given),
|
|
124
|
+
(given) => call(host, host.app(given), given),
|
|
128
125
|
false,
|
|
129
126
|
],
|
|
130
127
|
[
|
|
131
128
|
'openapi',
|
|
132
129
|
'the OpenAPI document, written rather than served',
|
|
133
|
-
(given) => openapi(host, given),
|
|
130
|
+
(given) => openapi(host, host.app(given), given),
|
|
134
131
|
false,
|
|
135
132
|
],
|
|
136
133
|
[
|
|
137
134
|
'mcp',
|
|
138
135
|
"the app's MCP server over stdio",
|
|
139
|
-
(given) => mcp(host, given),
|
|
136
|
+
(given) => mcp(host, host.app(given), given),
|
|
140
137
|
false,
|
|
141
138
|
],
|
|
142
139
|
[
|
|
@@ -195,13 +192,12 @@ function usage(host: Host, commands: Row[]): string {
|
|
|
195
192
|
return `${text}
|
|
196
193
|
--port the listen port the app is served on
|
|
197
194
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
198
|
-
--out the file openapi writes
|
|
199
195
|
`
|
|
200
196
|
text += `
|
|
201
197
|
--root the app's source directory, where its pages sit —
|
|
202
198
|
src/ where you stand, or where you stand, whichever has one
|
|
203
|
-
--out where the build goes, beside the app by default; the file
|
|
204
|
-
|
|
199
|
+
--out where the build goes, beside the app by default; the file compile and
|
|
200
|
+
bundle write — named after the app, where you stand
|
|
205
201
|
--port the listen port for dev, start and the shell's app
|
|
206
202
|
--url the running app the shell, call, watch, openapi and mcp talk to
|
|
207
203
|
--target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
|
|
@@ -234,32 +230,63 @@ function parsed(argv: string[]) {
|
|
|
234
230
|
})
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* The app served and listening, with the address it answers at and the banner it is announced by —
|
|
235
|
+
* or the exit code of a refusal to. 38.6.
|
|
236
|
+
*
|
|
237
|
+
* Stopped on a signal or an unawaited fault ONCE SERVING, and not before: a boot that fails is
|
|
238
|
+
* 37.6's to fail hard.
|
|
239
|
+
*/
|
|
238
240
|
async function served(
|
|
239
241
|
host: Host,
|
|
242
|
+
app: AppHandle,
|
|
240
243
|
given: Given,
|
|
241
244
|
): Promise<{ app: App; url: string; heading: string } | number> {
|
|
242
245
|
// 36.3 — `dev`, `build` and `start` each name the environment where the process was started with
|
|
243
246
|
// none, and leave one it was started with exactly as given.
|
|
244
247
|
process.env.NODE_ENV ??= 'production'
|
|
245
|
-
const
|
|
246
|
-
if (typeof
|
|
248
|
+
const booted = await booting(host, app, given)
|
|
249
|
+
if (typeof booted === 'number') return booted
|
|
250
|
+
const port = given.values.port
|
|
251
|
+
await booted.listen(port === undefined ? {} : { port: Number(port) })
|
|
252
|
+
stopOnSignalOrFault(booted)
|
|
247
253
|
const server = LISTENING.server as Bun.Server<unknown>
|
|
248
254
|
return {
|
|
249
|
-
app,
|
|
255
|
+
app: booted,
|
|
250
256
|
url: withoutTrailingSlash(server.url.href),
|
|
251
|
-
heading: banner(host.name, 'start',
|
|
257
|
+
heading: banner(host.name, 'start', app.package, server),
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
260
|
|
|
261
|
+
/** The app booted in this process, or the exit code of a refusal naming why there is none. */
|
|
262
|
+
async function booting(
|
|
263
|
+
host: Host,
|
|
264
|
+
app: AppHandle,
|
|
265
|
+
given: Given,
|
|
266
|
+
): Promise<App | number> {
|
|
267
|
+
const booted = await app.boot()
|
|
268
|
+
if (typeof booted === 'string') {
|
|
269
|
+
const command = given.positionals[0]
|
|
270
|
+
console.error(
|
|
271
|
+
`${command === undefined ? host.name : `${host.name} ${command}`}: ${booted}`,
|
|
272
|
+
)
|
|
273
|
+
return 1
|
|
274
|
+
}
|
|
275
|
+
return booted
|
|
276
|
+
}
|
|
277
|
+
|
|
255
278
|
/**
|
|
256
279
|
* `start`: serves the app.
|
|
257
280
|
*
|
|
258
281
|
* 38.67 — READS NO INPUT, and hands back no code for the process to exit on: the process is the
|
|
259
282
|
* server, for a container or a supervisor to stop.
|
|
260
283
|
*/
|
|
261
|
-
async function start(
|
|
262
|
-
|
|
284
|
+
async function start(
|
|
285
|
+
host: Host,
|
|
286
|
+
app: AppHandle,
|
|
287
|
+
given: Given,
|
|
288
|
+
): Promise<number | undefined> {
|
|
289
|
+
const serving = await served(host, app, given)
|
|
263
290
|
if (typeof serving === 'number') return serving
|
|
264
291
|
console.log(serving.heading)
|
|
265
292
|
return undefined
|
|
@@ -273,24 +300,28 @@ async function start(host: Host, given: Given): Promise<number | undefined> {
|
|
|
273
300
|
* as a remote one's is. 38.54 — `exit` stops the app. 38.55 — input that ends leaves it serving, and
|
|
274
301
|
* hands back no code for the process to exit on.
|
|
275
302
|
*/
|
|
276
|
-
async function shellOn(
|
|
303
|
+
async function shellOn(
|
|
304
|
+
host: Host,
|
|
305
|
+
app: AppHandle,
|
|
306
|
+
given: Given,
|
|
307
|
+
): Promise<number | undefined> {
|
|
277
308
|
const remote = remoteUrl(given.values.url)
|
|
278
309
|
if (remote !== undefined) {
|
|
279
310
|
const url = appUrl(remote)
|
|
280
311
|
await shell({
|
|
281
312
|
url,
|
|
282
|
-
handlers: await
|
|
313
|
+
handlers: await app.handlers(),
|
|
283
314
|
heading: `${host.name}: ${url}`,
|
|
284
315
|
local: false,
|
|
285
316
|
})
|
|
286
317
|
return 0
|
|
287
318
|
}
|
|
288
|
-
const serving = await served(host, given)
|
|
319
|
+
const serving = await served(host, app, given)
|
|
289
320
|
if (typeof serving === 'number') return serving
|
|
290
321
|
// After the serve, which built the app where nothing had.
|
|
291
322
|
const ended = await shell({
|
|
292
323
|
url: serving.url,
|
|
293
|
-
handlers: await
|
|
324
|
+
handlers: await app.handlers(),
|
|
294
325
|
heading: serving.heading,
|
|
295
326
|
local: true,
|
|
296
327
|
})
|
|
@@ -312,6 +343,7 @@ async function shellOn(host: Host, given: Given): Promise<number | undefined> {
|
|
|
312
343
|
*/
|
|
313
344
|
async function run(
|
|
314
345
|
host: Host,
|
|
346
|
+
app: AppHandle,
|
|
315
347
|
{ positionals: [, file, ...rest] }: Given,
|
|
316
348
|
): Promise<number | undefined> {
|
|
317
349
|
if (file === undefined) {
|
|
@@ -322,11 +354,9 @@ async function run(
|
|
|
322
354
|
}
|
|
323
355
|
const path = resolve(file)
|
|
324
356
|
process.argv = [process.argv[0] as string, path, ...rest]
|
|
325
|
-
// 38.57 —
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
APP_PACKAGE.current = host.appPackage()
|
|
329
|
-
config.invalidate()
|
|
357
|
+
// 38.57 — entered as `bootApp` enters it, its server blocks aside: a script is run against the
|
|
358
|
+
// app's source, which nothing built.
|
|
359
|
+
await enterApp({ hooks: undefined, package: app.package })
|
|
330
360
|
await inScope(undefined, () => import(Bun.pathToFileURL(path).href))
|
|
331
361
|
return undefined
|
|
332
362
|
}
|
|
@@ -350,7 +380,7 @@ async function watch(host: Host, given: Given): Promise<number> {
|
|
|
350
380
|
})
|
|
351
381
|
}
|
|
352
382
|
|
|
353
|
-
async function call(host: Host, given: Given): Promise<number> {
|
|
383
|
+
async function call(host: Host, app: AppHandle, given: Given): Promise<number> {
|
|
354
384
|
const {
|
|
355
385
|
positionals: [, address, ...args],
|
|
356
386
|
values: { url, method },
|
|
@@ -366,7 +396,7 @@ async function call(host: Host, given: Given): Promise<number> {
|
|
|
366
396
|
address,
|
|
367
397
|
args,
|
|
368
398
|
method,
|
|
369
|
-
await
|
|
399
|
+
await app.handlers(),
|
|
370
400
|
).catch((thrown: Error) => ({ code: 1, text: thrown.message }))
|
|
371
401
|
if (answered.text !== '')
|
|
372
402
|
(answered.code === 0 ? console.log : console.error)(answered.text)
|
|
@@ -374,50 +404,55 @@ async function call(host: Host, given: Given): Promise<number> {
|
|
|
374
404
|
}
|
|
375
405
|
|
|
376
406
|
/**
|
|
377
|
-
* A fetch against a generated surface, on the app `--url` names or on one booted here
|
|
407
|
+
* A fetch against a generated surface, on the app `--url` names or on one booted here — or the exit
|
|
408
|
+
* code of a refusal to boot one.
|
|
378
409
|
*
|
|
379
410
|
* 38.9 — the app booted in this process derives it from the same route table the served one does.
|
|
380
411
|
*/
|
|
381
412
|
async function surfaceAt(
|
|
382
413
|
host: Host,
|
|
383
|
-
|
|
414
|
+
app: AppHandle,
|
|
415
|
+
given: Given,
|
|
384
416
|
address: string,
|
|
385
417
|
): Promise<
|
|
386
|
-
(
|
|
387
|
-
|
|
388
|
-
|
|
418
|
+
| ((
|
|
419
|
+
init?: RequestInit & { headers?: Record<string, string> },
|
|
420
|
+
) => Promise<Response>)
|
|
421
|
+
| number
|
|
389
422
|
> {
|
|
390
|
-
const remote = remoteUrl(url)
|
|
423
|
+
const remote = remoteUrl(given.values.url)
|
|
391
424
|
if (remote !== undefined)
|
|
392
425
|
return (init) =>
|
|
393
426
|
fetch(`${appUrl(remote)}${address}`, {
|
|
394
427
|
...init,
|
|
395
428
|
headers: { ...init?.headers, ...remoteHeaders() },
|
|
396
429
|
})
|
|
397
|
-
const
|
|
430
|
+
const booted = await booting(host, app, given)
|
|
431
|
+
if (typeof booted === 'number') return booted
|
|
398
432
|
return (init) =>
|
|
399
|
-
|
|
433
|
+
booted.fetch(new Request(`http://abide.local${address}`, init))
|
|
400
434
|
}
|
|
401
435
|
|
|
402
436
|
/**
|
|
403
|
-
* `openapi`:
|
|
437
|
+
* `openapi`: prints the OpenAPI document — redirected to a file, where one is wanted.
|
|
404
438
|
*
|
|
405
439
|
* 38.9 — from `--url` where one is given and from the app in this process where not.
|
|
406
440
|
*/
|
|
407
441
|
async function openapi(
|
|
408
442
|
host: Host,
|
|
409
|
-
|
|
443
|
+
app: AppHandle,
|
|
444
|
+
given: Given,
|
|
410
445
|
): Promise<number> {
|
|
411
|
-
const
|
|
446
|
+
const ask = await surfaceAt(host, app, given, OPENAPI_ADDRESS)
|
|
447
|
+
if (typeof ask === 'number') return ask
|
|
448
|
+
const answer = await ask()
|
|
412
449
|
if (!answer.ok) {
|
|
413
450
|
console.error(
|
|
414
451
|
`${host.name} openapi: the app answered ${answer.status}. The document lists every handler with openapi in its clients; none is.`,
|
|
415
452
|
)
|
|
416
453
|
return 1
|
|
417
454
|
}
|
|
418
|
-
|
|
419
|
-
if (out === undefined) console.log(text)
|
|
420
|
-
else await Bun.write(resolve(out), `${text}\n`)
|
|
455
|
+
console.log(JSON.stringify(await answer.json(), null, 4))
|
|
421
456
|
return 0
|
|
422
457
|
}
|
|
423
458
|
|
|
@@ -427,8 +462,9 @@ async function openapi(
|
|
|
427
462
|
* 38.9, 38.13 — a bridge a model client spawns and kills: each line in is a JSON-RPC message for
|
|
428
463
|
* `/__abide/mcp`, and each answer is a line out.
|
|
429
464
|
*/
|
|
430
|
-
async function mcp(host: Host,
|
|
431
|
-
const ask = await surfaceAt(host,
|
|
465
|
+
async function mcp(host: Host, app: AppHandle, given: Given): Promise<number> {
|
|
466
|
+
const ask = await surfaceAt(host, app, given, MCP_ADDRESS)
|
|
467
|
+
if (typeof ask === 'number') return ask
|
|
432
468
|
for await (const line of console) {
|
|
433
469
|
if (line.trim() === '') continue
|
|
434
470
|
const answer = await ask({
|
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
|
+
}
|