@abide/abide 0.101.0 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +238 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +38 -37
  65. package/src/server/componentModules.d.ts +2 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -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 re-runs all of it (27.5),
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 defaults = hooked?.defaults?.({ ...derivedValues, ...env }) ?? {}
66
- if (defaults 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
- const document: Record<string, unknown> = {
71
- // 36.7 — CLOSED BY DEFAULT, and a boolean, so 27.8 reads the environment's spelling of one.
72
- ABIDE_LOGS: false,
73
- ABIDE_OPENAPI: false,
74
- ABIDE_MCP: false,
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 schema = hooked?.options?.schema
88
- if (schema === undefined) return document as Config
89
- const checked = runSchema(schema, document)
90
- if (checked.issues !== null)
91
- return validationError(checked.issues as never) as never
92
- return checked.value as Config
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.5 — THE APP'S NAME IS `log`'s ROOT NAMESPACE, and the config is where the name is decided.
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.73 — THE FIELDS THE DESKTOP LAUNCHER'S START FORM ASKS FOR: the members of the `onConfig`
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
- const hooked = HOOKS.config
34
- let defaults: Record<string, unknown> = {}
35
- try {
36
- const given = hooked?.defaults?.({ ...env })
37
- if (given !== null && typeof given === 'object')
38
- defaults = given as Record<string, unknown>
39
- } catch {}
40
- const schema = hooked?.options?.schema as
41
- | {
42
- '~standard'?: {
43
- jsonSchema?: { input?: (options: unknown) => unknown }
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
- | undefined
47
- let document:
48
- | { properties?: Record<string, Property>; required?: string[] }
49
- | undefined
50
- try {
51
- document = schema?.['~standard']?.jsonSchema?.input?.({
52
- target: 'draft-2020-12',
53
- }) as typeof document
54
- } catch {}
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
- document?.properties ??
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
@@ -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
- type AppSources,
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 { holdsApp } 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.9 — one line per request.
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,52 +54,64 @@ 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.26), and booted off the build (38.28).
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 ui/ in ${process.cwd()} and in ${resolve('src')}.`,
66
+ `abide: createApp found no app. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`,
67
+ )
68
+ const out = await servedBuild(root)
69
+ if (out === undefined)
70
+ throw new Error(
71
+ `abide: createApp found nothing under ${root} to build.`,
80
72
  )
81
- // 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
82
- // whatever `abide build` last left, which a test run would otherwise need first and could find
83
- // stale.
84
- const out =
85
- process.env.NODE_ENV === 'test'
86
- ? await testBuild(root)
87
- : buildDirectory(root)
88
73
  const { loadBuild } = await import('./serveBuild.ts')
89
74
  return bootApp(originUnder(root), () => loadBuild({ root, out }))
90
75
  }
91
76
 
92
77
  /**
93
- * The directory `ui/` sits under, or `undefined` where none is found.
78
+ * The build of the app under `root`, served to this process's imports — or `undefined` where
79
+ * nothing under it builds. `fresh` builds it for its sources as they stand.
80
+ *
81
+ * 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
82
+ * whatever `abide build` last left, which a test run would otherwise need first and could find
83
+ * stale. One answer for `createApp` and the test preload (38.109), so a test that boots the app
84
+ * reuses the build its imports were served from.
85
+ */
86
+ export async function servedBuild(
87
+ root: string,
88
+ fresh = process.env.NODE_ENV === 'test',
89
+ ): Promise<string | undefined> {
90
+ const out = fresh ? await testBuild(root) : buildDirectory(root)
91
+ const { serveBuild } = await import('./serveBuild.ts')
92
+ return (await serveBuild(root, out)) ? out : undefined
93
+ }
94
+
95
+ /**
96
+ * The app's root — `--root`, else `src` where the working directory holds one, else the working
97
+ * directory — or `undefined` where it is no app's.
98
+ *
99
+ * 38.25 and 38.26 — `undefined` is a refusal and not a guess. An app's root is in a package — it or
100
+ * the directory above it holds the `package.json` (`appDirectories`) — and holds a `.abide`
101
+ * file; the package is asked first, so a command run from `/` answers at once rather than
102
+ * walking the disk.
94
103
  *
95
- * 38.26 and 38.27 — `undefined` is a refusal and not a guess.
104
+ * ITS REAL PATH, as the bundler and the resolver name every file under it: an app under `/tmp` is
105
+ * `/private/tmp` to both, and one spelling here is one fewer at every reader downstream.
96
106
  */
97
107
  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
108
+ if (given !== undefined) return realOrAsGiven(resolve(given))
101
109
  const under = resolve('src')
102
- return existsSync(`${under}/ui`) ? under : undefined
110
+ const root = realOrAsGiven(existsSync(under) ? under : process.cwd())
111
+ let packaged = false
112
+ for (const directory of appDirectories(root))
113
+ packaged ||= existsSync(`${directory}/package.json`)
114
+ return packaged && holdsApp(root) ? root : undefined
103
115
  }
104
116
 
105
117
  /**
@@ -110,26 +122,26 @@ export function appRoot(given: string | undefined): string | undefined {
110
122
  * under.
111
123
  */
112
124
  export type AppOrigin = {
113
- /** The import of `src/server/app.ts`, where the app has one. 37.1. */
114
- hooks: (() => Promise<{ default?: OwnRoute }>) | undefined
125
+ /**
126
+ * Imports every module holding a `<script server>`, where the app has one. 37.1.
127
+ *
128
+ * Imported for what each CALLS: every hook it runs is filed by the time the import settles.
129
+ */
130
+ hooks: (() => Promise<unknown>) | undefined
115
131
  /** 27.3's name and version. */
116
132
  package: AppPackage
117
133
  }
118
134
 
119
- /** The path of the app's hooks module under its source root, where it has one. 37.1. */
120
- export function hooksModuleUnder(root: string): string | undefined {
121
- const hooksModule = `${root}/server/app.ts`
122
- return existsSync(hooksModule) ? hooksModule : undefined
123
- }
124
-
125
- /** The `AppOrigin` of an app on disk, found under its source root. */
135
+ /**
136
+ * The `AppOrigin` of an app on disk: its package under `root`, and its server blocks, out of the
137
+ * compilation this process serves (`serveCompilation`) — the build's, or `abide dev`'s.
138
+ */
126
139
  export function originUnder(root: string): AppOrigin {
127
- const hooksModule = hooksModuleUnder(root)
128
140
  return {
129
- hooks:
130
- hooksModule === undefined
131
- ? undefined
132
- : () => import(Bun.pathToFileURL(hooksModule).href),
141
+ hooks: async () => {
142
+ const table = await servedTable(root)
143
+ await Promise.all(table.servers.map((load) => load()))
144
+ },
133
145
  package: packageUnder(root),
134
146
  }
135
147
  }
@@ -143,15 +155,12 @@ export type BootedApp = App & { openMux(): Mux }
143
155
  /** An `App` booted over the sources `load` supplies, inside the app's `onStart` hooks. */
144
156
  export async function bootApp(
145
157
  origin: AppOrigin,
146
- load: () => Promise<LoadedSources>,
158
+ load: () => Promise<AppSources>,
147
159
  ): Promise<BootedApp> {
148
160
  installServer()
149
- // 37.1 — OPTIONAL, and imported for what it CALLS: every hook it runs is filed by the time the
150
- // import settles.
151
- const own =
152
- origin.hooks === undefined
153
- ? undefined
154
- : ((await origin.hooks()).default ?? undefined)
161
+ // 37.1 — EVERY SERVER BLOCK, imported for what it CALLS: every hook it runs is filed by the
162
+ // time the import settles, which is before `onStart` is composed around the boot.
163
+ if (origin.hooks !== undefined) await origin.hooks()
155
164
 
156
165
  // 37.4 — THE BOOT, INSIDE `onStart`. 37.6 — a throw out of a hook or out of the boot rejects
157
166
  // `createApp`, and nothing is served.
@@ -159,8 +168,8 @@ export async function bootApp(
159
168
  // 42.2 — THE CONFIGURATION IS READ AS PART OF THE BOOT, so a configuration that does not resolve
160
169
  // is a boot that fails rather than a first request that does.
161
170
  APP_PACKAGE.current = origin.package
162
- let sources: LoadedSources | undefined
163
- await compose(HOOKS.starts, async () => {
171
+ let sources: AppSources | undefined
172
+ await compose(inOrder(HOOKS.starts), async () => {
164
173
  config.invalidate()
165
174
  await config
166
175
  sources = await load()
@@ -169,17 +178,16 @@ export async function bootApp(
169
178
  throw new Error(
170
179
  'abide: an onStart hook returned without calling start(), so the app never booted. RULEBOOK 37.4.',
171
180
  )
172
- const booted: AppSources = {
173
- ...sources,
174
- own: own === undefined ? undefined : ownRoute(own),
175
- }
181
+ const booted = sources
176
182
 
177
- // 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path. 21.20 — the
178
- // centre takes its own time back out of what `fetch` counts for the whole onion.
179
- const answer = compose(HOOKS.rungs, async ({ request }) => {
183
+ // 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path: the root
184
+ // route's rungs, the ones below it running after routing (37.2, `answerRequest`). 21.20 — the
185
+ // center takes its own time back out of what `fetch` counts for the whole onion.
186
+ const rungs = rootRungs()
187
+ const answer = compose(rungs, async ({ request }) => {
180
188
  const began = performance.now()
181
189
  const answered = await answerRequest(booted, request)
182
- if (HOOKS.rungs.length > 0) rungsTook(began - performance.now())
190
+ if (rungs.length > 0) rungsTook(began - performance.now())
183
191
  return answered
184
192
  })
185
193
 
@@ -213,14 +221,14 @@ export async function bootApp(
213
221
  let answered: Response
214
222
  try {
215
223
  answered = await answer({ request })
216
- if (HOOKS.rungs.length > 0)
224
+ if (rungs.length > 0)
217
225
  rungsTook(performance.now() - started)
218
226
  } catch (thrown) {
219
227
  reportUnexpected(thrown)
220
228
  answered = plainAnswer('internal server error', 500)
221
229
  }
222
230
  const stamped = stamp(answered)
223
- // 28.8 — a closed namespace does not pay for the line it would have written.
231
+ // 28.7 — a closed namespace does not pay for the line it would have written.
224
232
  if (REQUEST_LOG.enabled())
225
233
  REQUEST_LOG.debug(
226
234
  request.method,
@@ -289,7 +297,7 @@ export async function bootApp(
289
297
  // 16.42 — a stopped app answers no `raw` call made outside every request.
290
298
  if (RPC_SERVER.raw === answerRaw) RPC_SERVER.raw = null
291
299
  bound = undefined
292
- await compose(HOOKS.stops, async () => {})(undefined)
300
+ await compose(inOrder(HOOKS.stops), async () => {})(undefined)
293
301
  }
294
302
 
295
303
  return {
@@ -416,13 +424,3 @@ function belowMount(request: Request): Request | Response {
416
424
  url.pathname = below
417
425
  return new Request(url, request)
418
426
  }
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
- }