@abide/abide 0.100.0 → 0.102.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.29 — AN ANSWER, COMPRESSED WHOLE: a document, and a JSON value at least `COMPRESSION_FLOOR`
|
|
2
2
|
// bytes long. Both are in hand before a byte goes out — `render` sends nothing until the page is
|
|
3
3
|
// built (41.1), and a value is stringified once — so one compression is the whole answer, with its
|
|
4
4
|
// length known, where a stream compressed as it arrived had to be flushed at every chunk.
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// WHAT EACH LANE SENDS BESIDE THE BYTES is here too, so the two cannot drift. A document names
|
|
7
7
|
// `accept-encoding` in `vary` through `DOCUMENT_HEADERS`, which its `HEAD` and error answers carry as
|
|
8
8
|
// well; a JSON answer names it here. Both say their length: `abide dev` relays a body chunk by chunk
|
|
9
|
-
// from its worker (38.
|
|
10
|
-
|
|
9
|
+
// from its worker (38.35), and a relayed body with no declared length goes out chunked.
|
|
10
|
+
// BUN'S GZIP parts from zlib's past ~130 KB, which only the off-loop lane reaches — so that lane takes
|
|
11
|
+
// zlib's, the loop keeps bun's, and the two make the same bytes.
|
|
12
|
+
import { brotliCompress, brotliCompressSync, constants, gzip } from 'node:zlib' // bun has no brotli, and no compression off the loop
|
|
11
13
|
import {
|
|
12
14
|
ENCODINGS,
|
|
13
15
|
type Encoding,
|
|
@@ -17,13 +19,18 @@ import {
|
|
|
17
19
|
// QUALITY 5, NOT 11. This runs per request, and the top brotli level is priced for a build that
|
|
18
20
|
// runs once: on an 18 KB docs page 11 took 15,800 µs against 5's 150, for 15% fewer bytes. 5 is
|
|
19
21
|
// already under gzip's size at twice gzip's 70 µs.
|
|
20
|
-
const
|
|
22
|
+
const BROTLI_OPTIONS = { params: { [constants.BROTLI_PARAM_QUALITY]: 5 } }
|
|
21
23
|
|
|
22
|
-
// 38.
|
|
24
|
+
// 38.68 — the bytes under which a JSON answer goes out as it is. Under it an answer saves tens of
|
|
23
25
|
// bytes that leave in the same packet anyway, for 3–8 µs of a ~57 µs request; at 8 KB the same CPU
|
|
24
26
|
// saves seven kilobytes.
|
|
25
27
|
const COMPRESSION_FLOOR = 1024
|
|
26
28
|
|
|
29
|
+
// 38.95 — the bytes from which an answer is compressed off the event loop. Brotli 5 holds the loop
|
|
30
|
+
// ~360 µs at 64 KB and ~7 ms at 700 KB — every other request and socket waits that long — where
|
|
31
|
+
// the thread pool's hop adds ~20 µs to the answer it carries: 6% at 64 KB, 27% at 16 KB.
|
|
32
|
+
const OFF_LOOP_FLOOR = 64 * 1024
|
|
33
|
+
|
|
27
34
|
const ENCODER = new TextEncoder()
|
|
28
35
|
|
|
29
36
|
function compressed(
|
|
@@ -31,24 +38,48 @@ function compressed(
|
|
|
31
38
|
encoding: Encoding,
|
|
32
39
|
): Uint8Array<ArrayBuffer> {
|
|
33
40
|
return encoding === 'br'
|
|
34
|
-
? (brotliCompressSync(bytes,
|
|
35
|
-
params: { [constants.BROTLI_PARAM_QUALITY]: QUALITY },
|
|
36
|
-
}) as Uint8Array<ArrayBuffer>)
|
|
41
|
+
? (brotliCompressSync(bytes, BROTLI_OPTIONS) as Uint8Array<ArrayBuffer>)
|
|
37
42
|
: Bun.gzipSync(bytes)
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
function compressedOffLoop(
|
|
46
|
+
bytes: Uint8Array<ArrayBuffer>,
|
|
47
|
+
encoding: Encoding,
|
|
48
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const settle = (error: Error | null, result: Uint8Array): void => {
|
|
51
|
+
if (error === null) resolve(result as Uint8Array<ArrayBuffer>)
|
|
52
|
+
else reject(error)
|
|
53
|
+
}
|
|
54
|
+
if (encoding === 'br') brotliCompress(bytes, BROTLI_OPTIONS, settle)
|
|
55
|
+
else gzip(bytes, settle)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function answered(
|
|
60
|
+
body: Uint8Array<ArrayBuffer>,
|
|
61
|
+
encoding: Encoding | undefined,
|
|
62
|
+
headers: Headers,
|
|
63
|
+
status: number,
|
|
64
|
+
): Response {
|
|
65
|
+
if (encoding !== undefined) headers.set('content-encoding', encoding)
|
|
66
|
+
headers.set('content-length', String(body.byteLength))
|
|
67
|
+
return new Response(body, { status, headers })
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
/**
|
|
41
71
|
* `answer` as the `Response` to `request`, in the encoding the request takes where it takes one.
|
|
42
72
|
*
|
|
43
73
|
* THE LANE IS THE BODY'S TYPE: a built document arrives as its bytes, and a JSON value as its text,
|
|
44
|
-
* encoded once here and floored by its length.
|
|
74
|
+
* encoded once here and floored by its length. A `Promise` only where the compression runs off the
|
|
75
|
+
* loop, so an answer under that floor costs no await.
|
|
45
76
|
*/
|
|
46
77
|
export function compressAnswer(
|
|
47
78
|
answer: Uint8Array<ArrayBuffer> | string,
|
|
48
79
|
request: Request,
|
|
49
80
|
headers: Headers,
|
|
50
81
|
status: number,
|
|
51
|
-
): Response {
|
|
82
|
+
): Response | Promise<Response> {
|
|
52
83
|
const json = typeof answer === 'string'
|
|
53
84
|
const bytes = json ? ENCODER.encode(answer) : answer
|
|
54
85
|
const encoding =
|
|
@@ -59,8 +90,11 @@ export function compressAnswer(
|
|
|
59
90
|
ENCODINGS,
|
|
60
91
|
)
|
|
61
92
|
if (json) headers.append('vary', 'accept-encoding')
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
if (encoding === undefined)
|
|
94
|
+
return answered(bytes, encoding, headers, status)
|
|
95
|
+
if (bytes.byteLength >= OFF_LOOP_FLOOR)
|
|
96
|
+
return compressedOffLoop(bytes, encoding).then((body) =>
|
|
97
|
+
answered(body, encoding, headers, status),
|
|
98
|
+
)
|
|
99
|
+
return answered(compressed(bytes, encoding), encoding, headers, status)
|
|
66
100
|
}
|
package/src/server/config.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
// SECTION 27 — THE RESOLVED CONFIGURATION. One process-wide value (27.9) over three layers, the last
|
|
2
2
|
// winning (27.2): what abide derives, what the app's `onConfig` defaulted, and the environment.
|
|
3
3
|
//
|
|
4
|
-
// A `reactive` BODY, so `config.invalidate` is the core's own trigger and
|
|
4
|
+
// A `reactive` BODY, so `config.invalidate` is the core's own trigger and reruns all of it (27.5),
|
|
5
5
|
// and a throw out of `onConfig` is the value's standing error, which a read throws (27.1).
|
|
6
6
|
|
|
7
7
|
import { homedir } from 'node:os' // bun exposes no home directory of its own
|
|
8
|
+
import { isRecord } from '#shared/isRecord.ts'
|
|
8
9
|
import { numberFromText } from '#shared/numberFromText.ts'
|
|
9
10
|
import { reactive } from '#shared/reactive/index.ts'
|
|
10
11
|
import { validationError } from '#shared/refuse.ts'
|
|
11
12
|
import { runSchema } from '#shared/Schema.ts'
|
|
12
13
|
import { appDirectories } from './appDirectories.ts'
|
|
13
|
-
import { type Env, HOOKS } from './hooks.ts'
|
|
14
|
+
import { type Env, HOOKS, inOrder } from './hooks.ts'
|
|
14
15
|
import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
|
|
15
16
|
|
|
16
17
|
/** The fields of an app's `package.json` 27.3 derives from. */
|
|
@@ -46,10 +47,6 @@ export interface Config {
|
|
|
46
47
|
APP_NAME: string
|
|
47
48
|
APP_VERSION: string
|
|
48
49
|
APP_DATA_DIR: string
|
|
49
|
-
ABIDE_LOGS: boolean
|
|
50
|
-
ABIDE_OPENAPI: boolean
|
|
51
|
-
ABIDE_MCP: boolean
|
|
52
|
-
ABIDE_BRIDGE: boolean
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/** The resolved configuration: abide's derived values, then `onConfig`'s defaults, then the env. */
|
|
@@ -57,24 +54,20 @@ export const config = reactive(resolve, { global: true })
|
|
|
57
54
|
|
|
58
55
|
function resolve(): Config {
|
|
59
56
|
const env: Env = { ...process.env }
|
|
60
|
-
const hooked = HOOKS.config
|
|
61
|
-
// 27.4 — SYNCHRONOUS BY CONTRACT; a promise handed back is a contract broken, not a value.
|
|
57
|
+
const hooked = inOrder(HOOKS.config)
|
|
62
58
|
// 27.11 — handed what 27.3 derives under the environment, so a default can be built on
|
|
63
59
|
// `APP_DATA_DIR` without deriving it a second time.
|
|
64
60
|
const derivedValues = derived()
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
ABIDE_BRIDGE: false,
|
|
76
|
-
...derivedValues,
|
|
77
|
-
...(defaults as Record<string, unknown>),
|
|
61
|
+
const document: Record<string, unknown> = { ...derivedValues }
|
|
62
|
+
// 27.6, 37.7 — EVERY `onConfig`'S DEFAULTS, merged over the ones before it.
|
|
63
|
+
for (const { defaults } of hooked) {
|
|
64
|
+
const given = defaults?.({ ...derivedValues, ...env }) ?? {}
|
|
65
|
+
// 27.4 — SYNCHRONOUS BY CONTRACT; a promise handed back is a contract broken, not a value.
|
|
66
|
+
if (given instanceof Promise)
|
|
67
|
+
throw new Error(
|
|
68
|
+
'abide: onConfig returned a promise. ConfigDefaults is synchronous; read what the defaults need before registering them. RULEBOOK 27.4.',
|
|
69
|
+
)
|
|
70
|
+
Object.assign(document, given as Record<string, unknown>)
|
|
78
71
|
}
|
|
79
72
|
// 27.8 — THE ENVIRONMENT, COERCED TO THE DEFAULT IT OVERRIDES; a value that will not coerce leaves
|
|
80
73
|
// the default standing.
|
|
@@ -83,13 +76,16 @@ function resolve(): Config {
|
|
|
83
76
|
const coerced = coerce(text, document[name])
|
|
84
77
|
if (coerced !== undefined) document[name] = coerced
|
|
85
78
|
}
|
|
86
|
-
// 27.7 — LAST, over the whole document.
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
// 27.7 — LAST, each over the whole document, and what each accepts merged back over it.
|
|
80
|
+
for (const { options } of hooked) {
|
|
81
|
+
const schema = options?.schema
|
|
82
|
+
if (schema === undefined) continue
|
|
83
|
+
const checked = runSchema(schema, document)
|
|
84
|
+
if (checked.issues !== null)
|
|
85
|
+
return validationError(checked.issues as never) as never
|
|
86
|
+
if (isRecord(checked.value)) Object.assign(document, checked.value)
|
|
87
|
+
}
|
|
88
|
+
return document as Config
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
function coerce(text: string, standing: unknown): unknown {
|
|
@@ -129,7 +125,7 @@ export function dataDirectory(name: string): string {
|
|
|
129
125
|
return `${process.env.XDG_DATA_HOME ?? `${home}/.local/share`}/${name}`
|
|
130
126
|
}
|
|
131
127
|
|
|
132
|
-
// 28.
|
|
128
|
+
// 28.4 — THE APP'S NAME IS `log`'s ROOT NAMESPACE, and the config is where the name is decided.
|
|
133
129
|
// PEEKED, so a log line written inside an effect does not subscribe it to the configuration.
|
|
134
130
|
export function derivedAppName(): string | undefined {
|
|
135
131
|
const resolved = config.peek()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.72 — THE FIELDS THE DESKTOP LAUNCHER'S START FORM ASKS FOR: the members of the `onConfig`
|
|
2
2
|
// schema's JSON Schema, probed as 20.11 probes a handler's, and the fields `onConfig`'s defaults
|
|
3
3
|
// return where the schema has no document to read.
|
|
4
4
|
|
|
5
|
-
import { HOOKS } from './hooks.ts'
|
|
5
|
+
import { HOOKS, inOrder } from './hooks.ts'
|
|
6
6
|
|
|
7
7
|
type Property = {
|
|
8
8
|
type?: string | string[]
|
|
@@ -30,32 +30,38 @@ export type ConfigField = {
|
|
|
30
30
|
export function configFields(
|
|
31
31
|
env: Record<string, string | undefined>,
|
|
32
32
|
): ConfigField[] {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
// 27.6, 37.7 — EVERY `onConfig`, its defaults and its schema's fields merged in route order.
|
|
34
|
+
const defaults: Record<string, unknown> = {}
|
|
35
|
+
let declared: Record<string, Property> | undefined
|
|
36
|
+
const required = new Set<string>()
|
|
37
|
+
for (const { defaults: given, options } of inOrder(HOOKS.config)) {
|
|
38
|
+
try {
|
|
39
|
+
const answered = given?.({ ...env })
|
|
40
|
+
if (answered !== null && typeof answered === 'object')
|
|
41
|
+
Object.assign(defaults, answered)
|
|
42
|
+
} catch {}
|
|
43
|
+
const schema = options?.schema as
|
|
44
|
+
| {
|
|
45
|
+
'~standard'?: {
|
|
46
|
+
jsonSchema?: { input?: (options: unknown) => unknown }
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
49
|
+
| undefined
|
|
50
|
+
let document:
|
|
51
|
+
| { properties?: Record<string, Property>; required?: string[] }
|
|
52
|
+
| undefined
|
|
53
|
+
try {
|
|
54
|
+
document = schema?.['~standard']?.jsonSchema?.input?.({
|
|
55
|
+
target: 'draft-2020-12',
|
|
56
|
+
}) as typeof document
|
|
57
|
+
} catch {}
|
|
58
|
+
if (document?.properties !== undefined)
|
|
59
|
+
declared = { ...declared, ...document.properties }
|
|
60
|
+
for (const name of document?.required ?? []) required.add(name)
|
|
61
|
+
}
|
|
55
62
|
const properties: Record<string, Property> =
|
|
56
|
-
|
|
63
|
+
declared ??
|
|
57
64
|
Object.fromEntries(Object.keys(defaults).map((name) => [name, {}]))
|
|
58
|
-
const required = new Set(document?.required ?? [])
|
|
59
65
|
const fields: ConfigField[] = []
|
|
60
66
|
for (const [name, property] of Object.entries(properties)) {
|
|
61
67
|
const fallback = defaults[name] ?? property.default
|
package/src/server/createApp.ts
CHANGED
|
@@ -10,19 +10,19 @@ import { namedLogger } from '#shared/log.ts'
|
|
|
10
10
|
import { compose } from '#shared/Middleware.ts'
|
|
11
11
|
import { MOUNT, unmounted } from '#shared/mount.ts'
|
|
12
12
|
import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
answerRequest,
|
|
16
|
-
type LoadedSources,
|
|
17
|
-
} from './answerRequest.ts'
|
|
13
|
+
import { servedTable } from './abideModules.ts'
|
|
14
|
+
import { type AppSources, answerRequest } from './answerRequest.ts'
|
|
18
15
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
16
|
+
import { appDirectories } from './appDirectories.ts'
|
|
17
|
+
import { holdsAbide } from './appTree.ts'
|
|
19
18
|
import { buildDirectory } from './buildDirectory.ts'
|
|
20
19
|
import { APP_PACKAGE, type AppPackage, config, packageUnder } from './config.ts'
|
|
21
20
|
import { McpSurface } from './generatedSurfaces.ts'
|
|
22
|
-
import { HOOKS, reportUnexpected } from './hooks.ts'
|
|
21
|
+
import { HOOKS, inOrder, reportUnexpected, rootRungs } from './hooks.ts'
|
|
23
22
|
import { installServer } from './installServer.ts'
|
|
24
23
|
import { admitsMux, upgraded } from './muxUpgrade.ts'
|
|
25
24
|
import { plainAnswer } from './plainAnswer.ts'
|
|
25
|
+
import { realOrAsGiven } from './realOrAsGiven.ts'
|
|
26
26
|
import {
|
|
27
27
|
type AnsweringApp,
|
|
28
28
|
applyWritten,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { testBuild } from './testBuild.ts'
|
|
36
36
|
import { traceResponse } from './trace.ts'
|
|
37
37
|
|
|
38
|
-
// 28.
|
|
38
|
+
// 28.8 — one line per request.
|
|
39
39
|
const REQUEST_LOG = namedLogger('abide:request')
|
|
40
40
|
|
|
41
41
|
/** The app object: what a host, a test or a script holds the app as. */
|
|
@@ -54,29 +54,16 @@ export interface App {
|
|
|
54
54
|
[Symbol.asyncDispose](): Promise<void>
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// 37.2 — the two shapes `default` may take.
|
|
58
|
-
type OwnRoute =
|
|
59
|
-
| ((
|
|
60
|
-
request: Request,
|
|
61
|
-
server: Bun.Server<unknown> | undefined,
|
|
62
|
-
) => Response | undefined | Promise<Response | undefined>)
|
|
63
|
-
| {
|
|
64
|
-
fetch: (
|
|
65
|
-
request: Request,
|
|
66
|
-
server: Bun.Server<unknown> | undefined,
|
|
67
|
-
) => unknown
|
|
68
|
-
}
|
|
69
|
-
|
|
70
57
|
/**
|
|
71
58
|
* The app this process was started in, booted off what `abide build` left beside it.
|
|
72
59
|
*
|
|
73
|
-
* 42.1 — found where the CLI finds it (38.
|
|
60
|
+
* 42.1 — found where the CLI finds it (38.25), and booted off the build (38.27).
|
|
74
61
|
*/
|
|
75
62
|
export async function createApp(): Promise<App> {
|
|
76
63
|
const root = appRoot(undefined)
|
|
77
64
|
if (root === undefined)
|
|
78
65
|
throw new Error(
|
|
79
|
-
`abide: createApp found no app. Looked for
|
|
66
|
+
`abide: createApp found no app. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`,
|
|
80
67
|
)
|
|
81
68
|
// 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
|
|
82
69
|
// whatever `abide build` last left, which a test run would otherwise need first and could find
|
|
@@ -85,21 +72,34 @@ export async function createApp(): Promise<App> {
|
|
|
85
72
|
process.env.NODE_ENV === 'test'
|
|
86
73
|
? await testBuild(root)
|
|
87
74
|
: buildDirectory(root)
|
|
88
|
-
const { loadBuild } = await import('./serveBuild.ts')
|
|
75
|
+
const { loadBuild, serveBuild } = await import('./serveBuild.ts')
|
|
76
|
+
if (!(await serveBuild(root, out)))
|
|
77
|
+
throw new Error(
|
|
78
|
+
`abide: createApp found nothing under ${root} to build.`,
|
|
79
|
+
)
|
|
89
80
|
return bootApp(originUnder(root), () => loadBuild({ root, out }))
|
|
90
81
|
}
|
|
91
82
|
|
|
92
83
|
/**
|
|
93
|
-
* The
|
|
84
|
+
* The app's root — `--root`, else `src` where the working directory holds one, else the working
|
|
85
|
+
* directory — or `undefined` where it is no app's.
|
|
94
86
|
*
|
|
95
|
-
* 38.
|
|
87
|
+
* 38.25 and 38.26 — `undefined` is a refusal and not a guess. An app's root is in a package — it or
|
|
88
|
+
* the directory above it holds the `package.json` (`appDirectories`) — and holds a `.abide`
|
|
89
|
+
* file; the package is asked first, so a command run from `/` answers at once rather than
|
|
90
|
+
* walking the disk.
|
|
91
|
+
*
|
|
92
|
+
* ITS REAL PATH, as the bundler and the resolver name every file under it: an app under `/tmp` is
|
|
93
|
+
* `/private/tmp` to both, and one spelling here is one fewer at every reader downstream.
|
|
96
94
|
*/
|
|
97
95
|
export function appRoot(given: string | undefined): string | undefined {
|
|
98
|
-
if (given !== undefined) return resolve(given)
|
|
99
|
-
const here = process.cwd()
|
|
100
|
-
if (existsSync(`${here}/ui`)) return here
|
|
96
|
+
if (given !== undefined) return realOrAsGiven(resolve(given))
|
|
101
97
|
const under = resolve('src')
|
|
102
|
-
|
|
98
|
+
const root = realOrAsGiven(existsSync(under) ? under : process.cwd())
|
|
99
|
+
let packaged = false
|
|
100
|
+
for (const directory of appDirectories(root))
|
|
101
|
+
packaged ||= existsSync(`${directory}/package.json`)
|
|
102
|
+
return packaged && holdsAbide(root) ? root : undefined
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
@@ -110,26 +110,26 @@ export function appRoot(given: string | undefined): string | undefined {
|
|
|
110
110
|
* under.
|
|
111
111
|
*/
|
|
112
112
|
export type AppOrigin = {
|
|
113
|
-
/**
|
|
114
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Imports every module holding a `<script server>`, where the app has one. 37.1.
|
|
115
|
+
*
|
|
116
|
+
* Imported for what each CALLS: every hook it runs is filed by the time the import settles.
|
|
117
|
+
*/
|
|
118
|
+
hooks: (() => Promise<unknown>) | undefined
|
|
115
119
|
/** 27.3's name and version. */
|
|
116
120
|
package: AppPackage
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** The `AppOrigin` of an app on disk, found under its source root. */
|
|
123
|
+
/**
|
|
124
|
+
* The `AppOrigin` of an app on disk: its package under `root`, and its server blocks, out of the
|
|
125
|
+
* compilation this process serves (`serveCompilation`) — the build's, or `abide dev`'s.
|
|
126
|
+
*/
|
|
126
127
|
export function originUnder(root: string): AppOrigin {
|
|
127
|
-
const hooksModule = hooksModuleUnder(root)
|
|
128
128
|
return {
|
|
129
|
-
hooks:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
hooks: async () => {
|
|
130
|
+
const table = await servedTable(root)
|
|
131
|
+
await Promise.all(table.servers.map((load) => load()))
|
|
132
|
+
},
|
|
133
133
|
package: packageUnder(root),
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -143,15 +143,12 @@ export type BootedApp = App & { openMux(): Mux }
|
|
|
143
143
|
/** An `App` booted over the sources `load` supplies, inside the app's `onStart` hooks. */
|
|
144
144
|
export async function bootApp(
|
|
145
145
|
origin: AppOrigin,
|
|
146
|
-
load: () => Promise<
|
|
146
|
+
load: () => Promise<AppSources>,
|
|
147
147
|
): Promise<BootedApp> {
|
|
148
148
|
installServer()
|
|
149
|
-
// 37.1 —
|
|
150
|
-
// import settles.
|
|
151
|
-
|
|
152
|
-
origin.hooks === undefined
|
|
153
|
-
? undefined
|
|
154
|
-
: ((await origin.hooks()).default ?? undefined)
|
|
149
|
+
// 37.1 — EVERY SERVER BLOCK, imported for what it CALLS: every hook it runs is filed by the
|
|
150
|
+
// time the import settles, which is before `onStart` is composed around the boot.
|
|
151
|
+
if (origin.hooks !== undefined) await origin.hooks()
|
|
155
152
|
|
|
156
153
|
// 37.4 — THE BOOT, INSIDE `onStart`. 37.6 — a throw out of a hook or out of the boot rejects
|
|
157
154
|
// `createApp`, and nothing is served.
|
|
@@ -159,8 +156,8 @@ export async function bootApp(
|
|
|
159
156
|
// 42.2 — THE CONFIGURATION IS READ AS PART OF THE BOOT, so a configuration that does not resolve
|
|
160
157
|
// is a boot that fails rather than a first request that does.
|
|
161
158
|
APP_PACKAGE.current = origin.package
|
|
162
|
-
let sources:
|
|
163
|
-
await compose(HOOKS.starts, async () => {
|
|
159
|
+
let sources: AppSources | undefined
|
|
160
|
+
await compose(inOrder(HOOKS.starts), async () => {
|
|
164
161
|
config.invalidate()
|
|
165
162
|
await config
|
|
166
163
|
sources = await load()
|
|
@@ -169,17 +166,16 @@ export async function bootApp(
|
|
|
169
166
|
throw new Error(
|
|
170
167
|
'abide: an onStart hook returned without calling start(), so the app never booted. RULEBOOK 37.4.',
|
|
171
168
|
)
|
|
172
|
-
const booted
|
|
173
|
-
...sources,
|
|
174
|
-
own: own === undefined ? undefined : ownRoute(own),
|
|
175
|
-
}
|
|
169
|
+
const booted = sources
|
|
176
170
|
|
|
177
|
-
// 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path
|
|
178
|
-
//
|
|
179
|
-
|
|
171
|
+
// 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path: the root
|
|
172
|
+
// route's rungs, the ones below it running after routing (37.2, `answerRequest`). 21.20 — the
|
|
173
|
+
// center takes its own time back out of what `fetch` counts for the whole onion.
|
|
174
|
+
const rungs = rootRungs()
|
|
175
|
+
const answer = compose(rungs, async ({ request }) => {
|
|
180
176
|
const began = performance.now()
|
|
181
177
|
const answered = await answerRequest(booted, request)
|
|
182
|
-
if (
|
|
178
|
+
if (rungs.length > 0) rungsTook(began - performance.now())
|
|
183
179
|
return answered
|
|
184
180
|
})
|
|
185
181
|
|
|
@@ -213,14 +209,14 @@ export async function bootApp(
|
|
|
213
209
|
let answered: Response
|
|
214
210
|
try {
|
|
215
211
|
answered = await answer({ request })
|
|
216
|
-
if (
|
|
212
|
+
if (rungs.length > 0)
|
|
217
213
|
rungsTook(performance.now() - started)
|
|
218
214
|
} catch (thrown) {
|
|
219
215
|
reportUnexpected(thrown)
|
|
220
216
|
answered = plainAnswer('internal server error', 500)
|
|
221
217
|
}
|
|
222
218
|
const stamped = stamp(answered)
|
|
223
|
-
// 28.
|
|
219
|
+
// 28.7 — a closed namespace does not pay for the line it would have written.
|
|
224
220
|
if (REQUEST_LOG.enabled())
|
|
225
221
|
REQUEST_LOG.debug(
|
|
226
222
|
request.method,
|
|
@@ -289,7 +285,7 @@ export async function bootApp(
|
|
|
289
285
|
// 16.42 — a stopped app answers no `raw` call made outside every request.
|
|
290
286
|
if (RPC_SERVER.raw === answerRaw) RPC_SERVER.raw = null
|
|
291
287
|
bound = undefined
|
|
292
|
-
await compose(HOOKS.stops, async () => {})(undefined)
|
|
288
|
+
await compose(inOrder(HOOKS.stops), async () => {})(undefined)
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
return {
|
|
@@ -416,13 +412,3 @@ function belowMount(request: Request): Request | Response {
|
|
|
416
412
|
url.pathname = below
|
|
417
413
|
return new Request(url, request)
|
|
418
414
|
}
|
|
419
|
-
|
|
420
|
-
function ownRoute(
|
|
421
|
-
own: OwnRoute,
|
|
422
|
-
): (request: Request) => Promise<Response | undefined> {
|
|
423
|
-
const answer = typeof own === 'function' ? own : own.fetch.bind(own)
|
|
424
|
-
return async (request) => {
|
|
425
|
-
const answered = await answer(request, LISTENING.server)
|
|
426
|
-
return answered instanceof Response ? answered : undefined
|
|
427
|
-
}
|
|
428
|
-
}
|