@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
@@ -9,7 +9,7 @@ import { isRecord } from '#shared/isRecord.ts'
9
9
  import { namedLogger } from '#shared/log.ts'
10
10
  import abidePackage from '../../package.json' with { type: 'json' }
11
11
  import { appIdentity, config } from './config.ts'
12
- import { HOOKS } from './hooks.ts'
12
+ import { HOOKS, inOrder } from './hooks.ts'
13
13
 
14
14
  const HEALTH_LOG = namedLogger('abide:health')
15
15
 
@@ -23,7 +23,7 @@ export async function reportHealth(): Promise<Health> {
23
23
  abide: abidePackage.version,
24
24
  startedAt: STARTED_AT,
25
25
  }
26
- for (const report of HOOKS.health) {
26
+ for (const report of inOrder(HOOKS.health)) {
27
27
  let answered: unknown
28
28
  try {
29
29
  answered = await report()
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { AsyncLocalStorage } from 'node:async_hooks' // bun implements AsyncLocalStorage and has no context-carrying api of its own
13
13
  import { Cookies } from '#shared/cookies.ts'
14
+ import type { Params } from '#shared/matchRoute.ts'
14
15
  import type { Principal } from '#shared/principal.ts'
15
16
  import { SCOPING, type Scope } from '#shared/reactive/scoping.ts'
16
17
  import type { RpcRequest } from '#shared/rpcRequest.ts'
@@ -51,6 +52,14 @@ export type RequestScope = Scope & {
51
52
  trace: { id: string; span: string; flags: string } | undefined
52
53
  /** `undefined` where the scope was opened without one — 42.8's scope, which 22.1 throws in. */
53
54
  request: Request | undefined
55
+ /**
56
+ * The route the request matched, once routing has run: the file route whose hooks answer it.
57
+ *
58
+ * 37.5 — which `onError` hooks an error in this request reaches.
59
+ */
60
+ route: string | undefined
61
+ /** What that route matched in the address, for the answer its rungs reach (16.14). */
62
+ params: Params | undefined
54
63
  /**
55
64
  * The app that opened this scope: the `App.fetch` `RPC_SERVER.raw` answers through, and its MCP
56
65
  * surface.
@@ -158,6 +167,8 @@ export function scopeFor(
158
167
  principal: undefined,
159
168
  applied: false,
160
169
  request: given,
170
+ route: undefined,
171
+ params: undefined,
161
172
  received,
162
173
  app,
163
174
  response: { status: 200, headers: new Headers() },
@@ -376,7 +387,7 @@ export function handlerTook(rpc: object, duration: number): void {
376
387
  /**
377
388
  * Adds `duration` to the time spent in rungs.
378
389
  *
379
- * 21.20 — or takes it back out where it was the centre's.
390
+ * 21.20 — or takes it back out where it was the center's.
380
391
  */
381
392
  export function rungsTook(duration: number): void {
382
393
  const timing = STORAGE.getStore()?.timing
@@ -0,0 +1,24 @@
1
+ // The CLI, spawned rather than imported: the compiler is not reachable from the server seam.
2
+ const ABIDE_BIN = Bun.fileURLToPath(
3
+ new URL('../../bin/abide.ts', import.meta.url),
4
+ )
5
+
6
+ /**
7
+ * `abide build` of the app under `root` into `out`, run as the CLI.
8
+ *
9
+ * SPAWNED RATHER THAN IMPORTED: the compiler is not reachable from the server seam.
10
+ */
11
+ export async function runBuild(root: string, out: string): Promise<void> {
12
+ const child = Bun.spawn(
13
+ [process.execPath, ABIDE_BIN, 'build', '--root', root, '--out', out],
14
+ { stdout: 'ignore', stderr: 'pipe' },
15
+ )
16
+ const [exitCode, said] = await Promise.all([
17
+ child.exited,
18
+ new Response(child.stderr).text(),
19
+ ])
20
+ if (exitCode !== 0)
21
+ throw new Error(
22
+ `abide: could not build the app under ${root}.\n${said}`,
23
+ )
24
+ }
@@ -1,4 +1,4 @@
1
- // 38.51 — A COMPILED APP'S COMMAND LINE: `abide`'s list (`commands.ts`), over the app the executable
1
+ // 38.50 — A COMPILED APP'S COMMAND LINE: `abide`'s list (`commands.ts`), over the app the executable
2
2
  // holds rather than one on disk, and spoken under the name it was started by — a `--platforms` build
3
3
  // is `ledger-linux-x64`, and that is what a reader types.
4
4
 
@@ -9,11 +9,11 @@ import { type BuildParts, builtSources, serveApp } from './serveBuild.ts'
9
9
  export async function runCompiled(
10
10
  origin: AppOrigin,
11
11
  parts: BuildParts,
12
- // 38.57 — every specifier a script `run` may import out of the executable: abide's own entries
13
- // and the app's handler modules, each to the module it holds.
12
+ // 38.56 — every specifier a script `run` may import out of the executable: abide's own entries
13
+ // and the app's `.abide` modules, each to the module it holds.
14
14
  specifiers: Record<string, () => Promise<Record<string, unknown>>>,
15
15
  ): Promise<void> {
16
- // REGISTERED AT START AND LOADED ON FIRST IMPORT: a handler module a script never names never
16
+ // REGISTERED AT START AND LOADED ON FIRST IMPORT: an app module a script never names never
17
17
  // runs its top level, as it would not under `abide run`.
18
18
  Bun.plugin({
19
19
  name: 'abide:compiled-specifiers',
@@ -30,7 +30,7 @@ export async function runCompiled(
30
30
  handlers: async () => parts.handlers,
31
31
  appPackage: () => origin.package,
32
32
  boot: () => bootApp(origin, () => builtSources(parts)),
33
- // 38.50.
33
+ // 38.49.
34
34
  serve: ({ values: { port } }) =>
35
35
  serveApp(
36
36
  origin,
@@ -1,4 +1,4 @@
1
- // 38.71 — WHAT `abide bundle` WRITES, STARTED WITH NO COMMAND: a window on the app. Given a command
1
+ // 38.70 — WHAT `abide bundle` WRITES, STARTED WITH NO COMMAND: a window on the app. Given a command
2
2
  // it is the executable `abide compile` writes (`runCompiled`), so a terminal still reaches every one.
3
3
  //
4
4
  // ONE PROCESS. The app, the launcher's own page and the window share it: AppKit's events are drained
@@ -25,18 +25,19 @@ import {
25
25
  import { APP_PACKAGE, configuredMount, dataDirectory } from './config.ts'
26
26
  import { configFields } from './configFields.ts'
27
27
  import { type App, type AppOrigin, bootApp } from './createApp.ts'
28
- import { reportUnexpected } from './hooks.ts'
28
+ import { executableName } from './executableName.ts'
29
+ import { HOOKS, inOrder, reportUnexpected } from './hooks.ts'
29
30
  import { installServer } from './installServer.ts'
30
- import type { LauncherOptions } from './launcherOptions.ts'
31
+ import { launcherOf } from './launcherOptions.ts'
31
32
  import { type Launch, launcherPage } from './launcherPage.ts'
32
33
  import { installMenu, menuBar, type NativeItem } from './nativeMenu.ts'
33
34
  import { randomToken } from './randomToken.ts'
34
- import { type Component, render } from './render.ts'
35
+ import { type Component, renderDocument } from './render.ts'
35
36
  import { LISTENING } from './requestScope.ts'
36
37
  import { runCompiled } from './runCompiled.ts'
37
38
  import { type BuildParts, builtSources } from './serveBuild.ts'
38
39
 
39
- /** What the launcher did last, and what its forms held, kept across launches. 38.74. */
40
+ /** What the launcher did last, and what its forms held, kept across launches. 38.73. */
40
41
  type Remembered = {
41
42
  last: { start: Record<string, string> } | { connect: string } | null
42
43
  values: Record<string, string>
@@ -50,15 +51,17 @@ type Failure = {
50
51
  issues: Record<string, string[]>
51
52
  }
52
53
 
53
- /** What `abide bundle` read out of `src/bundle/`, carried in the executable. */
54
+ /** What `abide bundle` read of the launcher's files, carried in the executable. */
54
55
  export type Desktop = {
55
- options: LauncherOptions
56
- /** 38.83 — `src/bundle/launch.abide`, where the app has one. */
56
+ /** 38.82 — `src/launcher.abide`'s page, where the app has one that paints. */
57
57
  launch:
58
58
  | (() => Promise<{ default: (props: Launch) => Component }>)
59
59
  | undefined
60
- /** The document the launch page renders into, its stylesheet already in its head. */
61
- shell: string
60
+ /** The document the launch page renders into, its stylesheet already in its head (38.84). */
61
+ document: string
62
+ /** 38.101 — the app's icon as a data URL, and the accent taken from it. */
63
+ icon: string | undefined
64
+ accent: string | undefined
62
65
  }
63
66
 
64
67
  const LOOPBACK = '127.0.0.1'
@@ -79,14 +82,24 @@ export async function runDesktop(
79
82
  async function launch(
80
83
  origin: AppOrigin,
81
84
  load: () => ReturnType<typeof builtSources>,
82
- { options, launch: launchPage, shell }: Desktop,
85
+ { launch: launchModule, document, icon, accent }: Desktop,
83
86
  ): Promise<void> {
84
87
  installServer()
85
88
  APP_PACKAGE.current = origin.package
86
- // 37.1 — imported for what it files, so the start form reads `onConfig`.
89
+ // 37.1 — imported for what it files, so the start form reads `onConfig` and the launcher's
90
+ // `onLauncher` is filed.
87
91
  await origin.hooks?.()
88
- const appName =
89
- options.name ?? (origin.package.name ?? 'app').replace(/^@[^/]+\//, '')
92
+ // 38.85 — a launcher page that fails to load leaves the built-in page, with its defaults.
93
+ const launchPage = await launchModule?.().catch((thrown: unknown) => {
94
+ reportUnexpected(thrown)
95
+ return undefined
96
+ })
97
+ // 38.80 — what its `onLauncher` answers.
98
+ const options = launcherOf(inOrder(HOOKS.launcher).at(-1)?.())
99
+ // 38.81 — an `onLauncher` answer the checker would have refused, written where nothing checked it.
100
+ if (typeof options === 'string')
101
+ throw new Error(`abide: ${options}. RULEBOOK 38.81.`)
102
+ const appName = executableName(origin.package.name)
90
103
  const directory = dataDirectory(origin.package.name ?? 'app')
91
104
  const stateFile = `${directory}/desktop.json`
92
105
  const remembered: Remembered = {
@@ -149,21 +162,24 @@ async function launch(
149
162
  )
150
163
  return connected === undefined ? onwards() : page('connect')
151
164
  }
165
+ // The Connect… menu item asks for the page with its connect form open.
152
166
  return page(
153
167
  url.searchParams.get('focus') === 'connect'
154
168
  ? 'connect'
155
- : 'start',
169
+ : undefined,
156
170
  )
157
171
  },
158
172
  })
159
173
  const base = `${launcher.url.origin}/${token}/`
160
- // 38.72 — the answer to a start or a connect that took: the app the window now shows.
174
+ // 38.71 — the answer to a start or a connect that took: the app the window now shows.
161
175
  const onwards = (): Response =>
162
176
  new Response(null, {
163
177
  status: 303,
164
178
  headers: { location: current?.url ?? base },
165
179
  })
166
- const page = async (focus: 'start' | 'connect'): Promise<Response> => {
180
+ const page = async (
181
+ focus: 'start' | 'connect' | undefined,
182
+ ): Promise<Response> => {
167
183
  const shown = failure
168
184
  failure = undefined
169
185
  const props: Launch = {
@@ -173,17 +189,21 @@ async function launch(
173
189
  fields: configFields(derived()),
174
190
  url: remembered.url,
175
191
  failure: shown,
176
- focus: shown?.form ?? (options.start ? focus : 'connect'),
192
+ focus:
193
+ shown?.form ??
194
+ (focus === 'connect' && options.connect ? focus : undefined),
195
+ icon,
196
+ accent,
177
197
  }
178
198
  const headers = { 'content-type': 'text/html; charset=utf-8' }
179
199
  if (launchPage !== undefined)
180
200
  try {
181
201
  return new Response(
182
- await render((await launchPage()).default(props), shell),
202
+ await renderDocument(launchPage.default(props), document),
183
203
  { headers },
184
204
  )
185
205
  } catch (thrown) {
186
- // 38.86 — the built-in page stands in for one that failed.
206
+ // 38.85 — the built-in page stands in for one that failed.
187
207
  reportUnexpected(thrown)
188
208
  }
189
209
  return new Response(launcherPage(props), { headers })
@@ -196,7 +216,7 @@ async function launch(
196
216
  await leaving.app.stop().catch(reportUnexpected)
197
217
  }
198
218
 
199
- // 38.73 — the form's values are the environment the app is booted under, so 27.8 coerces them
219
+ // 38.72 — the form's values are the environment the app is booted under, so 27.8 coerces them
200
220
  // and 27.7's schema refuses them as it would any other.
201
221
  async function start(
202
222
  values: Record<string, string>,
@@ -215,7 +235,7 @@ async function launch(
215
235
  }
216
236
  try {
217
237
  await app
218
- // 38.76.
238
+ // 38.75.
219
239
  .listen({ hostname: LOOPBACK, port: remembered.port })
220
240
  // The port the last launch served on, so the app's origin — and its storage with
221
241
  // it — is the one it had; any free one where that is taken.
@@ -296,7 +316,7 @@ async function launch(
296
316
  types: 'Q@:@',
297
317
  args: [FFIType.ptr],
298
318
  returns: FFIType.u64,
299
- // NSTerminateCancel: 38.77 — the app is stopped first, and the process ends after.
319
+ // NSTerminateCancel: 38.76 — the app is stopped first, and the process ends after.
300
320
  run: () => {
301
321
  quit()
302
322
  return 0
@@ -382,20 +402,20 @@ async function launch(
382
402
  ),
383
403
  null,
384
404
  )
385
- // 38.89 — THE MENU BAR IS THE PAGE'S: the defaults until the page the window loads tells it
405
+ // 38.88 — THE MENU BAR IS THE PAGE'S: the defaults until the page the window loads tells it
386
406
  // what its route exports, and again after every navigation it makes.
387
407
  const showMenu = (told: NativeItem[]): void =>
388
408
  installMenu(menuBar(appName, options.connect, told), (item) => {
389
409
  if (item.role === 'connect')
390
410
  return navigate(`${base}?focus=connect`)
391
- // 38.75.
411
+ // 38.74.
392
412
  if (item.role === 'disconnect') {
393
413
  remembered.last = null
394
414
  void remember()
395
415
  return void disconnect().then(() => navigate(base))
396
416
  }
397
417
  if (item.role === 'reload') return void send(view, 'reload')
398
- // 38.91, 38.92 — the page that numbered the item does what it means.
418
+ // 38.90, 38.91 — the page that numbered the item does what it means.
399
419
  if (item.id !== undefined)
400
420
  send(
401
421
  view,
@@ -433,7 +453,7 @@ async function launch(
433
453
  nsString('abideMenu'),
434
454
  )
435
455
  const navigate = (address: string): void => {
436
- // 38.93.
456
+ // 38.92.
437
457
  showMenu([])
438
458
  send(
439
459
  view,
@@ -485,7 +505,7 @@ async function launch(
485
505
  send(window, 'setTitle:', nsString(title))
486
506
  }, DRAIN_MILLISECONDS)
487
507
 
488
- // 38.74 — WHAT IT DID LAST, done again.
508
+ // 38.73 — WHAT IT DID LAST, done again.
489
509
  const last = remembered.last
490
510
  if (options.start && last !== null && 'start' in last)
491
511
  await start(last.start)
@@ -1,4 +1,4 @@
1
- // 38.6 and 38.11 — `abide start` and a compiled app: the built app, over http. The request path is
1
+ // 38.6 and 38.10 — `abide start` and a compiled app: the built app, over http. The request path is
2
2
  // `answerRequest`'s, shared with `abide dev`; what this module is, is the SUPPLIER — where each part
3
3
  // comes from when the parts are what `abide build` produced.
4
4
  //
@@ -8,19 +8,22 @@
8
8
 
9
9
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
10
10
  import {
11
+ type Compilation,
12
+ serveCompilation,
13
+ servedTable,
14
+ } from './abideModules.ts'
15
+ import {
16
+ type AppSources,
11
17
  appSources,
12
- type LoadedSources,
13
- type ServerTable,
14
18
  type SourceParts,
15
19
  } from './answerRequest.ts'
16
- import type { Mount } from './answerRpc.ts'
17
20
  import {
18
21
  CLIENT_DIRECTORY,
22
+ COMPILATION,
19
23
  ENCODING_SUFFIX,
20
24
  MANIFEST,
21
25
  PUBLIC_DIRECTORY,
22
26
  } from './CLIENT_BUILD.ts'
23
- import { componentImports } from './componentImports.ts'
24
27
  import { type App, type AppOrigin, bootApp } from './createApp.ts'
25
28
  import { stopOnSignalOrFault } from './faultPolicy.ts'
26
29
  import type { Manifest } from './manifestOf.ts'
@@ -30,13 +33,13 @@ import {
30
33
  type Encoding,
31
34
  negotiateEncoding,
32
35
  } from './negotiateEncoding.ts'
33
- import { shellUnder } from './render.ts'
36
+ import { runBuild } from './runBuild.ts'
34
37
 
35
38
  /**
36
39
  * What `abide build` produced, as the parts an `App` is booted over.
37
40
  *
38
41
  * Read off disk by `abide start` (`loadBuild`), and out of the executable itself by a compiled app
39
- * (38.11). ONE SHAPE, so the two are answered by one path and differ only in where a byte is read
42
+ * (38.10). ONE SHAPE, so the two are answered by one path and differ only in where a byte is read
40
43
  * from — a file is a PATH in both, and `Bun.file` reads the executable's own the same way it reads
41
44
  * the disk's.
42
45
  */
@@ -53,7 +56,7 @@ export type BuildParts = Omit<SourceParts, 'client' | 'explained'> & {
53
56
  */
54
57
  export async function serveApp(
55
58
  origin: AppOrigin,
56
- load: () => Promise<LoadedSources>,
59
+ load: () => Promise<AppSources>,
57
60
  port: number | undefined,
58
61
  ): Promise<App> {
59
62
  const app = await bootApp(origin, load)
@@ -64,63 +67,51 @@ export async function serveApp(
64
67
  }
65
68
 
66
69
  /**
67
- * A loader for a handler's module, out of the app's source tree.
70
+ * Serves the compilation `abide build` kept in `out` to this process's imports; `false` where there
71
+ * is none, because the app under `root` holds nothing to build.
68
72
  *
69
- * `compiled` is where the app's components were compiled to, which a `.abide` the module imports
70
- * lands on (`componentImports`).
73
+ * 38.6 — what `abide start` boots: no compiler is loaded, and every `.abide` the app imports is
74
+ * answered out of what the build compiled. 38.100 — AN APP NOBODY BUILT IS BUILT, by `build`, rather
75
+ * than refused, so every caller boots the same way whether or not a build came first. The CLI passes
76
+ * its own build, in-process; anything else spawns it.
71
77
  */
72
- export function sourceModuleLoader(
78
+ export async function serveBuild(
73
79
  root: string,
74
- compiled: string,
75
- ): (source: string) => Promise<Record<string, unknown>> {
76
- Bun.plugin(componentImports(root, compiled))
77
- return (source) => import(Bun.pathToFileURL(`${root}/${source}`).href)
78
- }
79
-
80
- /** The handler table a build wrote into `directory`. */
81
- export async function handlersIn(directory: string): Promise<Mount[]> {
82
- const rpc = (await import(
83
- Bun.pathToFileURL(`${directory}/rpc.ts`).href
84
- )) as {
85
- handlers: Mount[]
80
+ out: string,
81
+ build: (root: string, out: string) => Promise<unknown> = runBuild,
82
+ ): Promise<boolean> {
83
+ const path = `${out}/${COMPILATION}`
84
+ if (!(await Bun.file(path).exists())) {
85
+ await build(root, out)
86
+ // A FRESH HANDLE: one taken before the build wrote the file reads it as empty.
87
+ if (!(await Bun.file(path).exists())) return false
86
88
  }
87
- return rpc.handlers
89
+ serveCompilation(root, (await Bun.file(path).json()) as Compilation)
90
+ return true
88
91
  }
89
92
 
90
- /** What `abide build` left on disk, as the sources an `App` is booted over. */
93
+ /**
94
+ * What `abide build` left on disk, as the sources an `App` is booted over — its compilation served
95
+ * already (`serveBuild`).
96
+ */
91
97
  export async function loadBuild(options: {
92
98
  /**
93
- * The app's source root — the directory `ui/` and `server/` sit under, and the one 35.6's
94
- * shell is resolved against.
99
+ * The app's source root — the directory its pages sit under.
95
100
  */
96
101
  root: string
97
102
  /** Where `abide build` put its output. */
98
103
  out: string
99
- }): Promise<LoadedSources> {
100
- if (!(await Bun.file(`${options.out}/server.ts`).exists()))
101
- throw new Error(
102
- `abide: no build at ${options.out}. Run \`abide build\` first.`,
103
- )
104
- // THE BOOT READS OVERLAP: the route table, the handler table, the shell and the manifest are
105
- // independent of each other.
106
- //
107
- // THE SHELL IS READ ONCE, at boot. `render`'s own note prices the per-render read at 98% of the
108
- // op and leaves the caching to whatever server got written; this is that server. The manifest
109
- // names the stylesheet only where the bundler collected one, so an app that imports none links
110
- // none.
111
- const [table, handlers, shell, manifest] = await Promise.all([
112
- import(
113
- Bun.pathToFileURL(`${options.out}/server.ts`).href
114
- ) as Promise<ServerTable>,
115
- handlersIn(options.out),
116
- shellUnder(options.root),
104
+ }): Promise<AppSources> {
105
+ // THE BOOT READS OVERLAP: the tables and the manifest are independent of each other. The
106
+ // manifest names the stylesheet only where the bundler collected one, so an app that imports
107
+ // none links none.
108
+ const [table, manifest] = await Promise.all([
109
+ servedTable(options.root),
117
110
  Bun.file(`${options.out}/${MANIFEST}`).json() as Promise<Manifest>,
118
111
  ])
119
112
  return builtSources({
120
113
  table,
121
- handlers,
122
- handlerModule: sourceModuleLoader(options.root, options.out),
123
- shell,
114
+ handlers: table.handlers,
124
115
  manifest,
125
116
  client: pathsUnder(`${options.out}/${CLIENT_DIRECTORY}`),
126
117
  published: pathsUnder(`${options.out}/${PUBLIC_DIRECTORY}`),
@@ -136,9 +127,9 @@ export function pathsUnder(directory: string): Map<string, string> {
136
127
  }
137
128
 
138
129
  /** The sources an `App` is booted over, out of the parts `abide build` produced. */
139
- export function builtSources(parts: BuildParts): Promise<LoadedSources> {
130
+ export function builtSources(parts: BuildParts): Promise<AppSources> {
140
131
  // THE BUNDLE, ENUMERATED RATHER THAN RESOLVED. A request names a file in this set or it names
141
- // nothing — there is no path to normalise, so `..` is not a case that has to be got right,
132
+ // nothing — there is no path to normalize, so `..` is not a case that has to be got right,
142
133
  // and what is servable is exactly what the build produced.
143
134
  //
144
135
  // THE PRECOMPRESSED COPIES ARE NOT ADDRESSES OF THEIR OWN. They are the same file in another
@@ -173,7 +164,7 @@ export function builtSources(parts: BuildParts): Promise<LoadedSources> {
173
164
  'content-type': type,
174
165
  vary: 'accept-encoding',
175
166
  }
176
- // 38.30 — the precompressed copy where the request takes its encoding.
167
+ // 38.29 — the precompressed copy where the request takes its encoding.
177
168
  const encoding = negotiateEncoding(
178
169
  request.headers.get('accept-encoding'),
179
170
  encodings,