@abide/abide 0.123.2 → 0.123.3

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 (94) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/abide.ts +36 -65
  3. package/package.json +1 -1
  4. package/src/compiler/Diagnostics.ts +198 -0
  5. package/src/compiler/KeptChecker.ts +97 -0
  6. package/src/compiler/Lowering.ts +1 -1
  7. package/src/compiler/PositionedRefusal.ts +1 -1
  8. package/src/compiler/TypeSession.ts +62 -85
  9. package/src/compiler/appLayout.ts +28 -20
  10. package/src/compiler/argsDefaultsIn.ts +1 -1
  11. package/src/compiler/checkApp.ts +64 -68
  12. package/src/compiler/checkedShadow.ts +1 -1
  13. package/src/compiler/compileApp.ts +101 -70
  14. package/src/compiler/declaredExports.ts +2 -4
  15. package/src/compiler/emitClient.ts +1 -1
  16. package/src/compiler/formatAbide.ts +2 -2
  17. package/src/compiler/handlersUnder.ts +17 -29
  18. package/src/compiler/lineOf.ts +0 -19
  19. package/src/compiler/matchersOf.ts +3 -3
  20. package/src/compiler/mountsOf.ts +3 -3
  21. package/src/compiler/readSources.ts +16 -18
  22. package/src/compiler/readsIn.ts +1 -1
  23. package/src/compiler/serveLanguage.ts +23 -57
  24. package/src/compiler/sharedModuleWrites.ts +1 -1
  25. package/src/compiler/templateWarnings.ts +1 -2
  26. package/src/compiler/trackingWarnings.ts +1 -1
  27. package/src/server/CLIENT_BUILD.ts +4 -32
  28. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  29. package/src/server/answerRequest.ts +11 -300
  30. package/src/server/answerRpc.ts +12 -54
  31. package/src/server/answerSocket.ts +8 -35
  32. package/src/server/appBuild.ts +377 -0
  33. package/src/server/appPlugins.ts +15 -17
  34. package/src/server/appTree.ts +6 -5
  35. package/src/server/argsContract.ts +209 -0
  36. package/src/server/bridgeRelay.ts +153 -65
  37. package/src/server/claudeCode.ts +30 -127
  38. package/src/server/config.ts +6 -7
  39. package/src/server/createApp.ts +30 -64
  40. package/src/server/devWorker.ts +11 -15
  41. package/src/server/handlerFileMount.ts +40 -0
  42. package/src/server/index.ts +2 -2
  43. package/src/server/packageDirectory.ts +14 -0
  44. package/src/server/preload.ts +3 -2
  45. package/src/server/principal.ts +4 -3
  46. package/src/server/remote.ts +1 -1
  47. package/src/server/runCompiled.ts +1 -1
  48. package/src/server/runDesktop.ts +1 -1
  49. package/src/server/seedBuffer.ts +1 -1
  50. package/src/server/serveDev.ts +24 -58
  51. package/src/server/servedApp.ts +390 -0
  52. package/src/server/signInChanged.ts +20 -0
  53. package/src/server/surfaceOf.ts +2 -1
  54. package/src/server/versionOf.ts +6 -15
  55. package/src/shared/applyPatch.ts +1 -1
  56. package/src/shared/muxFrames.ts +1 -1
  57. package/src/shared/reactive/Keyed.ts +1 -1
  58. package/src/shared/reactive/Reactive.ts +21 -8
  59. package/src/shared/reactive/argsContract.ts +131 -4
  60. package/src/shared/reactive/index.ts +1 -0
  61. package/src/shared/reactive/patchLog.ts +79 -0
  62. package/src/shared/reactive/patchMarks.ts +34 -0
  63. package/src/shared/reactive/recordWrites.ts +11 -14
  64. package/src/shared/reactive/share.ts +4 -3
  65. package/src/shared/rpc.ts +12 -19
  66. package/src/ui/ArmRegion.ts +10 -4
  67. package/src/ui/Level.ts +11 -20
  68. package/src/ui/LevelChain.ts +272 -0
  69. package/src/ui/Router.ts +54 -245
  70. package/src/ui/Scope.ts +17 -3
  71. package/src/ui/TryRegion.ts +22 -23
  72. package/src/ui/bindSpread.ts +2 -4
  73. package/src/ui/listenForRoutes.ts +3 -10
  74. package/src/ui/outboxQueue.ts +21 -3
  75. package/src/ui/reconcile.ts +4 -6
  76. package/src/ui/routing.ts +2 -1
  77. package/src/ui/rows.ts +2 -2
  78. package/src/ui/setAttribute.ts +12 -3
  79. package/src/ui/socketWire.ts +26 -11
  80. package/src/compiler/Finding.ts +0 -2
  81. package/src/compiler/Refusals.ts +0 -65
  82. package/src/compiler/formatDiagnosis.ts +0 -20
  83. package/src/server/appDirectories.ts +0 -9
  84. package/src/server/argsFor.ts +0 -39
  85. package/src/server/buildDirectory.ts +0 -10
  86. package/src/server/coerceArgs.ts +0 -109
  87. package/src/server/derivedArgs.ts +0 -26
  88. package/src/server/runBuild.ts +0 -24
  89. package/src/server/serveBuild.ts +0 -144
  90. package/src/server/testBuild.ts +0 -220
  91. package/src/shared/reactive/FOLLOWED.ts +0 -8
  92. package/src/shared/reactive/PATCHES.ts +0 -11
  93. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  94. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -14,24 +14,16 @@ import { refuse } from '#shared/refuse.ts'
14
14
 
15
15
  import {
16
16
  type AssistantContent,
17
- type AssistantEvent,
18
17
  type AssistantMessage,
19
18
  blocksOf,
20
19
  forwardOf,
21
20
  type Provider,
22
21
  } from './assistant.ts'
23
22
  import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
24
- import {
25
- followSignIn,
26
- mintCode,
27
- openTurn,
28
- pairedBridge,
29
- watchPairing,
30
- } from './bridgeRelay.ts'
23
+ import { mintCode, pairingStatus, runTurn } from './bridgeRelay.ts'
31
24
  import { callerOf } from './callerOf.ts'
32
25
  import { appOrigin } from './config.ts'
33
26
  import { mcpFailure } from './generatedSurfaces.ts'
34
- import { Inbox } from './Inbox.ts'
35
27
  import { signedInAs } from './principal.ts'
36
28
  import { request } from './requestScope.ts'
37
29
 
@@ -65,53 +57,19 @@ export type BridgeStatus = {
65
57
  function claudeCodeProvider(config: ClaudeCodeConfig = {}): Provider {
66
58
  return {
67
59
  async *run(messages, tools, { maxTurns, signal }) {
68
- const bridge = pairedBridge(askingCaller(), signedInAs())
69
- // 44.13, 44.33 — nothing paired by this sign-in is the page's cue to show the command.
70
- if (bridge === undefined)
71
- throw refuse(
72
- 409,
73
- 'No Claude Code is paired with this browser as the one signed in. claudeCode.bridgeCommand() is the command that pairs one.',
74
- )
75
- const turn = openTurn(bridge)
76
- bridge.frames.push({
77
- type: 'turn',
78
- id: turn.id,
60
+ yield* runTurn({
61
+ caller: askingCaller(),
62
+ signedIn: signedInAs(),
79
63
  prompt: foldedContent(messages),
80
64
  config,
81
65
  maxTurns,
66
+ // 44.17 — answered by the app's own MCP surface, through the turn's `Toolbox`.
67
+ answer: (method, params) =>
68
+ forwardOf(tools)(method, params).catch((thrown: unknown) =>
69
+ mcpFailure(-32603, String(thrown)),
70
+ ),
71
+ signal,
82
72
  })
83
- // 44.18 — a caller hanging up ends the run on the bridge.
84
- const cancel = (): void => {
85
- bridge.frames.push({ type: 'cancel', turn: turn.id })
86
- }
87
- signal.addEventListener('abort', cancel)
88
- try {
89
- for await (const item of turn.inbox) {
90
- if (item.kind === 'event')
91
- yield item.event as AssistantEvent
92
- else if (item.kind === 'mcp')
93
- // 44.17 — answered by the app's own MCP surface, through the turn's `Toolbox`.
94
- void forwardOf(tools)(item.method, item.params)
95
- .catch((thrown: unknown) =>
96
- mcpFailure(-32603, String(thrown)),
97
- )
98
- .then((reply) => {
99
- bridge.frames.push({
100
- type: 'reply',
101
- turn: turn.id,
102
- id: item.id,
103
- reply,
104
- })
105
- })
106
- else if (item.kind === 'end') return
107
- // 44.21.
108
- else
109
- throw refuse(502, `Claude Code failed: ${item.message}`)
110
- }
111
- } finally {
112
- signal.removeEventListener('abort', cancel)
113
- turn.close()
114
- }
115
73
  },
116
74
  }
117
75
  }
@@ -157,25 +115,25 @@ function askingCaller(): string {
157
115
  return caller
158
116
  }
159
117
 
160
- /** One pairing code, as the command a visitor runs and the link they open, and when it stops standing. */
161
- type Pairing = { command: string; link: string; expires: number }
162
-
163
118
  /**
164
119
  * The command and the link that pair a bridge with this browser, and when their code stops standing.
165
120
  * 44.14. The `tools` it is given are written after the command's `--` and in the link, so the visitor
166
121
  * running either grants them (44.29, 44.32, 44.40).
167
122
  */
168
- function bridgeCommand(options: PairingOptions = {}): Pairing {
169
- return commandFor(askingCaller(), signedInAs(), options)
123
+ function bridgeCommand(options: PairingOptions = {}): {
124
+ command: string
125
+ link: string
126
+ expires: number
127
+ } {
128
+ const { code, expires } = mintCode(askingCaller(), signedInAs())
129
+ return { ...commandFor(code, options), expires }
170
130
  }
171
131
 
172
- /** The command and the link pairing a bridge with `caller` signed in as `signedIn` (44.33). */
132
+ /** The command and the link that pair a bridge by `code`, granting `tools`. */
173
133
  function commandFor(
174
- caller: string,
175
- signedIn: string,
134
+ code: string,
176
135
  { tools }: PairingOptions,
177
- ): Pairing {
178
- const { code, expires } = mintCode(caller, signedIn)
136
+ ): { command: string; link: string } {
179
137
  const site = `${appOrigin() ?? new URL(request().url).origin}${mounted('')}`
180
138
  const granted = tools?.length ? tools.join(',') : undefined
181
139
  const query = new URLSearchParams({ site, code, version: BRIDGE_VERSION })
@@ -184,80 +142,25 @@ function commandFor(
184
142
  return {
185
143
  command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}${grant}`,
186
144
  link: `${BRIDGE_SCHEME}://pair?${query}`,
187
- expires,
188
145
  }
189
146
  }
190
147
 
191
148
  /**
192
149
  * This browser's pairing as it changes: unpaired with the command to run and the link to open, then
193
- * paired the moment a bridge connects, and unpaired again if it goes. 44.20.
194
- *
195
- * 44.34 — IT FOLLOWS WHOEVER SIGNS IN AT THE BROWSER, though its own request carries the cookies it
196
- * opened with: a sign-in or a sign-out made since is the pairing it watches from then on, and the
197
- * command it offers is that sign-in's.
150
+ * paired the moment a bridge connects, and unpaired again if it goes — following whoever signs in at
151
+ * the browser since. 44.20, 44.34.
198
152
  */
199
153
  async function* bridge(
200
154
  options: PairingOptions = {},
201
155
  ): AsyncGenerator<BridgeStatus> {
202
- let caller = askingCaller()
203
- let signedIn = signedInAs()
204
- const signal = request().signal
205
- // A change landing while the loop was not waiting — paused at its `yield` — stays queued, so it
206
- // looks again; a look that finds nothing new yields nothing.
207
- const changes = new Inbox<undefined>()
208
- const waiting = changes[Symbol.asyncIterator]()
209
- const rouse = (): void => changes.push(undefined)
210
- let watchingAs = ''
211
- let unwatch = (): void => {}
212
- let unfollow = (): void => {}
213
- const follow = (now: string, next: string): void => {
214
- caller = now
215
- signedIn = next
216
- rouse()
217
- }
218
- signal.addEventListener('abort', rouse)
219
- let pairing: Pairing | undefined
220
- let said = ''
221
- try {
222
- while (!signal.aborted) {
223
- const watching = `${caller} ${signedIn}`
224
- if (watching !== watchingAs) {
225
- unwatch()
226
- unfollow()
227
- unwatch = watchPairing(caller, signedIn, rouse)
228
- unfollow = followSignIn(caller, follow)
229
- watchingAs = watching
230
- pairing = undefined
231
- }
232
- const paired = pairedBridge(caller, signedIn) !== undefined
233
- if (
234
- !paired &&
235
- (pairing === undefined || pairing.expires <= Date.now())
236
- )
237
- pairing = commandFor(caller, signedIn, options)
238
- const status: BridgeStatus = {
239
- paired,
240
- command: paired ? undefined : pairing?.command,
241
- link: paired ? undefined : pairing?.link,
242
- }
243
- // The link is minted with the command, so the command alone tells a new pairing.
244
- const saying = `${status.paired} ${status.command}`
245
- if (saying !== said) {
246
- said = saying
247
- yield status
248
- }
249
- // A code that expires while nothing is paired is replaced with a fresh one.
250
- const renew = paired
251
- ? undefined
252
- : setTimeout(rouse, (pairing?.expires ?? 0) - Date.now())
253
- await waiting.next()
254
- clearTimeout(renew)
255
- }
256
- } finally {
257
- signal.removeEventListener('abort', rouse)
258
- unwatch()
259
- unfollow()
260
- }
156
+ for await (const code of pairingStatus(
157
+ askingCaller(),
158
+ signedInAs(),
159
+ request().signal,
160
+ ))
161
+ yield code === undefined
162
+ ? { paired: true, command: undefined, link: undefined }
163
+ : { paired: false, ...commandFor(code, options) }
261
164
  }
262
165
 
263
166
  /** The visitor's own Claude Code as a provider, with the pairing that reaches it. */
@@ -12,8 +12,8 @@ import { reactive } from '#shared/reactive/index.ts'
12
12
  import { validationError } from '#shared/refuse.ts'
13
13
  import { runSchema } from '#shared/Schema.ts'
14
14
 
15
- import { appDirectories } from './appDirectories.ts'
16
15
  import { type Env, HOOKS, inOrder } from './hooks.ts'
16
+ import { packageDirectory } from './packageDirectory.ts'
17
17
  import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
18
18
 
19
19
  /** The fields of an app's `package.json` 27.3 derives from. */
@@ -27,14 +27,13 @@ export type AppPackage = { name?: string; version?: string }
27
27
  */
28
28
  export const APP_PACKAGE: { current: AppPackage } = { current: {} }
29
29
 
30
- /** The `package.json` beside an app on disk, in the first of `appDirectories` that has one. */
30
+ /** The `package.json` of an app on disk (`packageDirectory`), or nothing where it has none. */
31
31
  export function packageUnder(root: string): AppPackage {
32
- for (const directory of appDirectories(root)) {
33
- try {
34
- return require(`${directory}/package.json`) as AppPackage
35
- } catch {}
32
+ try {
33
+ return require(`${packageDirectory(root)}/package.json`) as AppPackage
34
+ } catch {
35
+ return {}
36
36
  }
37
- return {}
38
37
  }
39
38
 
40
39
  /**
@@ -16,14 +16,17 @@ import { MOUNT, unmounted } from '#shared/mount.ts'
16
16
  import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
17
17
  import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
18
18
 
19
- import { servedTable } from './abideModules.ts'
20
19
  import { laneFailure } from './answerFailure.ts'
21
20
  import { answerRequest, type AppSources } from './answerRequest.ts'
22
21
  import type { Mount } from './answerRpc.ts'
23
22
  import { Mux, type MuxConnection } from './answerSocket.ts'
24
- import { appDirectories } from './appDirectories.ts'
23
+ import {
24
+ type Builder,
25
+ processBuild,
26
+ servedBuild,
27
+ spawnedBuild,
28
+ } from './appBuild.ts'
25
29
  import { holdsApp } from './appTree.ts'
26
- import { buildDirectory } from './buildDirectory.ts'
27
30
  import {
28
31
  APP_PACKAGE,
29
32
  type AppPackage,
@@ -36,6 +39,7 @@ import { HOOKS, inOrder, reportUnexpected, rootRungs } from './hooks.ts'
36
39
  import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
37
40
  import { installServer } from './installServer.ts'
38
41
  import { admitsMux, muxSocketOptions, upgraded } from './muxUpgrade.ts'
42
+ import { packageDirectory } from './packageDirectory.ts'
39
43
  import { plainAnswer } from './plainAnswer.ts'
40
44
  import { presumePrincipal } from './principal.ts'
41
45
  import { realOrAsGiven } from './realOrAsGiven.ts'
@@ -50,7 +54,6 @@ import {
50
54
  serverTiming,
51
55
  } from './requestScope.ts'
52
56
  import { heldBody } from './streamEvent.ts'
53
- import { testBuild } from './testBuild.ts'
54
57
  import { traceResponse } from './trace.ts'
55
58
 
56
59
  // 28.8 — one line per request.
@@ -80,13 +83,16 @@ export interface App {
80
83
  /**
81
84
  * The app this process was started in, booted off what `abide build` left beside it.
82
85
  *
83
- * 42.1 — found where the CLI finds it (38.25), and booted off the build (38.27).
86
+ * 42.1 — found where the CLI finds it (38.25), and booted off the build (38.27) — under `bun test`,
87
+ * the build of its sources as they stand (42.16), which the test preload served already (38.109).
84
88
  */
85
89
  export async function createApp(): Promise<App> {
86
90
  const root = appRoot(undefined)
87
91
  const app = await appOnDisk(
88
- root === undefined ? undefined : { root, out: buildDirectory(root) },
89
- (root) => servedBuild(root),
92
+ root === undefined
93
+ ? undefined
94
+ : { root, out: await processBuild(root) },
95
+ spawnedBuild,
90
96
  ).boot()
91
97
  if (typeof app === 'string')
92
98
  throw new Error(`abide: createApp found ${app}`)
@@ -117,13 +123,11 @@ export function noAppHere(): string {
117
123
 
118
124
  /**
119
125
  * The app on disk at `found` — its source root and build directory, both absolute — or the
120
- * refusal where nothing was found. `built` makes its build this process's (`serveBuild`, which
121
- * builds one nobody built — 38.100) and answers the directory it is in, or `undefined` where
122
- * nothing under the root builds.
126
+ * refusal where nothing was found. Its boot builds it with `build` where nobody has (38.100).
123
127
  */
124
128
  export function appOnDisk(
125
129
  found: { root: string; out: string } | undefined,
126
- built: (root: string, out: string) => Promise<string | undefined>,
130
+ build: Builder,
127
131
  ): AppHandle {
128
132
  if (found === undefined)
129
133
  return {
@@ -132,49 +136,25 @@ export function appOnDisk(
132
136
  boot: async () => noAppHere(),
133
137
  }
134
138
  const { root, out } = found
135
- const origin = originUnder(root)
136
139
  return {
137
- package: origin.package,
138
- handlers: async () => {
139
- const { serveBuild } = await import('./serveBuild.ts')
140
- if (!(await serveBuild(root, out, async () => undefined))) return []
141
- return (await servedTable(root)).handlers
142
- },
140
+ package: packageUnder(root),
141
+ handlers: async () =>
142
+ (await servedBuild(root, out, undefined))?.table.handlers ?? [],
143
143
  boot: async () => {
144
- const served = await built(root, out)
144
+ const served = await servedBuild(root, out, build)
145
145
  if (served === undefined) return `nothing under ${root} to build.`
146
- const { loadBuild } = await import('./serveBuild.ts')
147
- return bootApp(origin, () => loadBuild({ root, out: served }))
146
+ return bootApp(served.origin, served.load)
148
147
  },
149
148
  }
150
149
  }
151
150
 
152
- /**
153
- * The build of the app under `root`, served to this process's imports — or `undefined` where
154
- * nothing under it builds. `fresh` builds it for its sources as they stand.
155
- *
156
- * 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
157
- * whatever `abide build` last left, which a test run would otherwise need first and could find
158
- * stale. One answer for `createApp` and the test preload (38.109), so a test that boots the app
159
- * reuses the build its imports were served from.
160
- */
161
- export async function servedBuild(
162
- root: string,
163
- fresh = process.env.NODE_ENV === 'test',
164
- ): Promise<string | undefined> {
165
- const out = fresh ? await testBuild(root) : buildDirectory(root)
166
- const { serveBuild } = await import('./serveBuild.ts')
167
- return (await serveBuild(root, out)) ? out : undefined
168
- }
169
-
170
151
  /**
171
152
  * The app's root — `--root`, else `src` where the working directory holds one, else the working
172
153
  * directory — or `undefined` where it is no app's.
173
154
  *
174
- * 38.25 and 38.26 — `undefined` is a refusal and not a guess. An app's root is in a package — it or
175
- * the directory above it holds the `package.json` (`appDirectories`) — and holds a `.abide`
176
- * file; the package is asked first, so a command run from `/` answers at once rather than
177
- * walking the disk.
155
+ * 38.25 and 38.26 — `undefined` is a refusal and not a guess. An app's root is in a package
156
+ * (`packageDirectory`) and holds a `.abide` file; the package is asked first, so a command run from
157
+ * `/` answers at once rather than walking the disk.
178
158
  *
179
159
  * ITS REAL PATH, as the bundler and the resolver name every file under it: an app under `/tmp` is
180
160
  * `/private/tmp` to both, and one spelling here is one fewer at every reader downstream.
@@ -183,18 +163,18 @@ export function appRoot(given: string | undefined): string | undefined {
183
163
  if (given !== undefined) return realOrAsGiven(resolve(given))
184
164
  const under = resolve('src')
185
165
  const root = realOrAsGiven(existsSync(under) ? under : process.cwd())
186
- let packaged = false
187
- for (const directory of appDirectories(root))
188
- packaged ||= existsSync(`${directory}/package.json`)
189
- return packaged && holdsApp(root) ? root : undefined
166
+ return existsSync(`${packageDirectory(root)}/package.json`) &&
167
+ holdsApp(root)
168
+ ? root
169
+ : undefined
190
170
  }
191
171
 
192
172
  /**
193
173
  * What an app is booted as besides its sources: its hooks module and its `package.json`.
194
174
  *
195
- * The two things `bootApp` reads before the boot and not through it. Found under the source root
196
- * for an app on disk (`originUnder`), and handed over by a compiled app, which has no root to look
197
- * under.
175
+ * The two things `bootApp` reads before the boot and not through it — off the server's table of
176
+ * the compilation this process serves (`serveApp`), or handed over by a compiled app's entry, which
177
+ * holds the table itself.
198
178
  */
199
179
  export type AppOrigin = {
200
180
  /**
@@ -207,20 +187,6 @@ export type AppOrigin = {
207
187
  package: AppPackage
208
188
  }
209
189
 
210
- /**
211
- * The `AppOrigin` of an app on disk: its package under `root`, and its server blocks, out of the
212
- * compilation this process serves (`serveCompilation`) — the build's, or `abide dev`'s.
213
- */
214
- export function originUnder(root: string): AppOrigin {
215
- return {
216
- hooks: async () => {
217
- const table = await servedTable(root)
218
- await Promise.all(table.servers.map((load) => load()))
219
- },
220
- package: packageUnder(root),
221
- }
222
- }
223
-
224
190
  /**
225
191
  * An `App` as a command boots it: with the mux over its tables, which `listen` opens and `abide dev`'s
226
192
  * worker opens in its stead — one `AnsweringApp`, and one MCP surface, per app either way.
@@ -21,12 +21,8 @@
21
21
  import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
22
22
  import { escapeText } from '#shared/escapeText.ts'
23
23
 
24
- import {
25
- type Compilation,
26
- serveCompilation,
27
- servedTable,
28
- } from './abideModules.ts'
29
- import { type AppSources, appSources } from './answerRequest.ts'
24
+ import type { Compilation } from './abideModules.ts'
25
+ import type { AppSources } from './answerRequest.ts'
30
26
  import type { Mux, MuxConnection } from './answerSocket.ts'
31
27
  import { publishedUnder } from './appTree.ts'
32
28
  import {
@@ -35,7 +31,7 @@ import {
35
31
  clientBundle,
36
32
  type ServedFile,
37
33
  } from './clientBundle.ts'
38
- import { bootApp, type BootedApp, originUnder } from './createApp.ts'
34
+ import { bootApp, type BootedApp } from './createApp.ts'
39
35
  import { keepServing } from './faultPolicy.ts'
40
36
  import { type Manifest, NO_MANIFEST } from './manifestOf.ts'
41
37
  import { admitsMux, muxSocketOptions } from './muxUpgrade.ts'
@@ -46,6 +42,7 @@ import {
46
42
  serveOver,
47
43
  type ToRelay,
48
44
  } from './responseRelay.ts'
45
+ import { appSources, serveApp } from './servedApp.ts'
49
46
  import { withAbideSources } from './withAbideSources.ts'
50
47
 
51
48
  // The reload client, which the worker serves and every document it answers loads.
@@ -211,31 +208,30 @@ async function open(
211
208
  // the bundler resolves the browser's graph through it, so the two lanes cannot disagree
212
209
  // about what a specifier means. Registering it is process-wide — which is a worker rather
213
210
  // than the command, so a reload is a new registry rather than a cache to bust.
214
- serveCompilation(root, compilation)
211
+ const serving = serveApp(root, compilation)
215
212
 
216
213
  // ASKED FOR TOGETHER. The bundle, the public directory and the server's own
217
- // tables depend on none of each other — the plugin is registered above, before any of
218
- // them asks it anything — and on a reload every one of them is latency the author waits on,
219
- // so the bundle is handed over still building (`SourceParts.client`).
214
+ // tables depend on none of each other — `serveApp` registered the plugin as it was called,
215
+ // before any of them asks it anything — and on a reload every one of them is latency the
216
+ // author waits on, so the bundle is handed over still building (`SourceParts.client`).
220
217
  //
221
218
  // ONE LISTING of the public directories — the compile's — for the bundle's externals and for
222
219
  // what is served. 38.19 — OUT OF THE SOURCE TREE. `abide build` copies these into its output
223
220
  // and dev has no output, so the address answers where the author's file actually is.
224
221
  const published = publishedUnder(root, start.published)
225
222
  const bundling = bundleInMemory(root, compilation, start.published)
226
- const table = await servedTable(root)
227
- const handlers = table.handlers
223
+ const { origin, table } = await serving
228
224
 
229
225
  // THE SAME `App` `abide start` boots, so an app's hooks and middleware run in dev too — and its
230
226
  // sources are read INSIDE the boot, as `abide start` reads them: a document's head carries
231
227
  // what the resolved configuration says (28.4's app name), which is not resolved before it.
232
- app = await bootApp(originUnder(root), async () => {
228
+ app = await bootApp(origin, async () => {
233
229
  // 38.17 — THE RELOAD CLIENT IS THIS WORKER'S, not the bundle's, and it goes in whether or
234
230
  // not there is an entry beside it: a bundle that failed is exactly when a reader needs the
235
231
  // next save to reach them. `clientTags` writes nothing for a manifest with no entry.
236
232
  const sources = await appSources({
237
233
  table,
238
- handlers,
234
+ handlers: table.handlers,
239
235
  published,
240
236
  client: bundling.then((bundle) => ({
241
237
  manifest: bundle.manifest,
@@ -0,0 +1,40 @@
1
+ import type { Mount } from './answerRpc.ts'
2
+
3
+ /**
4
+ * The row of a handler file (16.73): its default, answered at the route the file is placed at and
5
+ * named for the file — what its declaration says of it laid over that.
6
+ */
7
+ export function handlerFileMount(
8
+ file: Pick<Mount, 'address' | 'source' | 'scope' | 'matchers'>,
9
+ declared: Pick<
10
+ Mount,
11
+ | 'method'
12
+ | 'ui'
13
+ | 'cli'
14
+ | 'mcp'
15
+ | 'description'
16
+ | 'copy'
17
+ | 'defaults'
18
+ | 'args'
19
+ | 'result'
20
+ >,
21
+ ): Mount {
22
+ return {
23
+ address: file.address,
24
+ method: declared.method,
25
+ source: file.source,
26
+ name: file.address.slice(file.address.lastIndexOf('/') + 1),
27
+ export: 'default',
28
+ scope: file.scope,
29
+ matchers: file.matchers,
30
+ private: false,
31
+ ui: declared.ui,
32
+ cli: declared.cli,
33
+ mcp: declared.mcp,
34
+ description: declared.description,
35
+ copy: declared.copy,
36
+ defaults: declared.defaults,
37
+ args: declared.args,
38
+ result: declared.result,
39
+ }
40
+ }
@@ -14,7 +14,7 @@ import { installServer } from './installServer.ts'
14
14
  // markup with, 32.20's `safeUrl`, and 31.8's `writePath`.
15
15
  export * from '#shared'
16
16
  // 18.11 — one write a patch made, as a followed `GET`'s `patch` carries it.
17
- export type { PatchOp } from '#shared/reactive/PATCH_SINKS.ts'
17
+ export type { PatchOp } from '#shared/reactive/patchLog.ts'
18
18
  export {
19
19
  type Clients,
20
20
  DELETE,
@@ -54,7 +54,7 @@ export { type App, createApp } from './createApp.ts'
54
54
  export { callsInRender } from './deferredBuilds.ts'
55
55
  // 19.9's run-time half: what the module a handler is loaded through calls to hold it to the schema
56
56
  // its annotation derives (15.8). Not an app's name.
57
- export { derivedArgs } from './derivedArgs.ts'
57
+ export { derivedArgs } from './argsContract.ts'
58
58
  // 37.1's run-time half: what a compiled server module calls around its `<script server>`, so the
59
59
  // hooks it makes are filed at its route.
60
60
  export {
@@ -0,0 +1,14 @@
1
+ import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
2
+ import { dirname } from 'node:path' // bun exposes no path helpers of its own
3
+
4
+ /**
5
+ * The package the app under `root` belongs to: `root` where it holds a `package.json`, else the
6
+ * directory above it.
7
+ *
8
+ * `root` is the app's root — `src/` beside its package, or the package itself where it has no
9
+ * `src`. What is the package's rather than the source's — its `package.json`, its `bunfig.toml`, its
10
+ * build (38.27) — is found here and nowhere else.
11
+ */
12
+ export function packageDirectory(root: string): string {
13
+ return existsSync(`${root}/package.json`) ? root : dirname(root)
14
+ }
@@ -5,8 +5,9 @@
5
5
  // one place that runs first. The build is `createApp`'s own under `bun test` (42.16), so a test that
6
6
  // boots the app afterwards reuses it rather than building twice.
7
7
 
8
+ import { processBuild, servedBuild, spawnedBuild } from './appBuild.ts'
8
9
  import { addDerived } from './config.ts'
9
- import { appRoot, servedBuild } from './createApp.ts'
10
+ import { appRoot } from './createApp.ts'
10
11
 
11
12
  // 38.120 — a sign-in seals under test where the app declared no secret: derived, so the app's
12
13
  // `onConfig` default and the env both still win (27.2), where an env value would beat the default.
@@ -17,7 +18,7 @@ const root = appRoot(undefined)
17
18
  // packages' `.abide` files are each their own app's rather than one app laid out across them.
18
19
  // Fresh whatever `NODE_ENV` says: `bun test` sets it only where nothing else has.
19
20
  if (root !== undefined && !(await isWorkspaceRoot(root)))
20
- await servedBuild(root, true)
21
+ await servedBuild(root, await processBuild(root, true), spawnedBuild)
21
22
 
22
23
  async function isWorkspaceRoot(directory: string): Promise<boolean> {
23
24
  const manifest = Bun.file(`${directory}/package.json`)
@@ -19,11 +19,11 @@ import { refuse } from '#shared/refuse.ts'
19
19
  import { declare } from '#shared/rpc.ts'
20
20
 
21
21
  import { mountedOwn } from './answerRpc.ts'
22
- import { signedInAt } from './bridgeRelay.ts'
23
22
  import { callerOf, callerOrMint, mintCaller } from './callerOf.ts'
24
23
  import { config, positiveSetting } from './config.ts'
25
24
  import { HOOKS, inOrder } from './hooks.ts'
26
25
  import { type RequestScope, type Sealed, within } from './requestScope.ts'
26
+ import { signInChanged } from './signInChanged.ts'
27
27
 
28
28
  // 26.18.
29
29
  const PRINCIPAL_COOKIE = 'abide-principal'
@@ -268,7 +268,7 @@ async function set(claims: Record<string, unknown>): Promise<void> {
268
268
  // 44.34 — a status stream opened before this sign-in follows it.
269
269
  const caller = callerOf()
270
270
  if (caller !== undefined)
271
- signedInAt(caller, caller, identityOf(scope.opened))
271
+ signInChanged(caller, caller, identityOf(scope.opened))
272
272
  principalMembers().document.set(await resolved)
273
273
  PRINCIPAL_LOG.debug('set')
274
274
  }
@@ -288,7 +288,8 @@ function clear(): void {
288
288
  scope.principal = Promise.resolve(ANONYMOUS)
289
289
  scope.opened = null
290
290
  // 44.34 — and one opened before this sign-out follows it to the caller it rotated to.
291
- if (signedOut !== undefined) signedInAt(signedOut, minted, identityOf(null))
291
+ if (signedOut !== undefined)
292
+ signInChanged(signedOut, minted, identityOf(null))
292
293
  principalMembers().document.set(ANONYMOUS)
293
294
  principalMembers().caller.set(minted)
294
295
  PRINCIPAL_LOG.debug('clear')
@@ -7,7 +7,7 @@ import { applyPatch } from '#shared/applyPatch.ts'
7
7
  import { eventsOf } from '#shared/framing.ts'
8
8
  import { HandlerRoute } from '#shared/HandlerRoute.ts'
9
9
  import { firstMatch, partsOf } from '#shared/matchRoute.ts'
10
- import type { PatchOp } from '#shared/reactive/PATCH_SINKS.ts'
10
+ import type { PatchOp } from '#shared/reactive/patchLog.ts'
11
11
  import { rankRoutes } from '#shared/routePattern.ts'
12
12
  import type { Method } from '#shared/rpc.ts'
13
13
  import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type Host, runCommands } from './commands.ts'
6
6
  import { type AppHandle, type AppOrigin, bootApp } from './createApp.ts'
7
- import { type BuildParts, builtSources } from './serveBuild.ts'
7
+ import { type BuildParts, builtSources } from './servedApp.ts'
8
8
 
9
9
  export async function runCompiled(
10
10
  origin: AppOrigin,
@@ -36,7 +36,7 @@ import { randomToken } from './randomToken.ts'
36
36
  import { type Component, renderDocument } from './render.ts'
37
37
  import { LISTENING } from './requestScope.ts'
38
38
  import { runCompiled } from './runCompiled.ts'
39
- import { type BuildParts, builtSources } from './serveBuild.ts'
39
+ import { type BuildParts, builtSources } from './servedApp.ts'
40
40
 
41
41
  /** What the launcher did last, and what its forms held, kept across launches. 38.73. */
42
42
  type Remembered = {
@@ -23,7 +23,7 @@
23
23
  // refresh, and says nothing.
24
24
 
25
25
  import { namedLogger } from '#shared/log.ts'
26
- import { canonicalKey } from '#shared/reactive/canonicalKey.ts'
26
+ import { canonicalKey } from '#shared/reactive/argsContract.ts'
27
27
  import {
28
28
  chunksOfStream,
29
29
  isReactive,