@abide/abide 0.100.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/abide.ts +227 -163
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +10 -3
  7. package/scaffold/AGENTS.md +31 -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 +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -56
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +124 -39
  53. package/src/server/answerSocket.ts +141 -48
  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 +179 -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 +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +48 -14
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +144 -183
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -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 +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +81 -39
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/GLOBAL_GETS.ts +9 -3
  107. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  108. package/src/shared/RPC_SERVER.ts +2 -2
  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 +1 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/shared/menuItem.ts +3 -3
  119. package/src/shared/reactive/Cursor.ts +3 -9
  120. package/src/shared/reactive/Keyed.ts +4 -3
  121. package/src/shared/reactive/NotLanded.ts +1 -1
  122. package/src/shared/reactive/Reactive.ts +81 -60
  123. package/src/shared/reactive/argsContract.ts +4 -4
  124. package/src/shared/reactive/canonicalKey.ts +4 -4
  125. package/src/shared/reactive/chunksOfStream.ts +6 -0
  126. package/src/shared/reactive/index.ts +1 -1
  127. package/src/shared/reactive/recordWrites.ts +103 -5
  128. package/src/shared/reactive/structural.ts +1 -1
  129. package/src/shared/reactive/tracking.ts +155 -21
  130. package/src/shared/reactive/watch.ts +10 -6
  131. package/src/shared/refuse.ts +1 -1
  132. package/src/shared/routePattern.ts +82 -46
  133. package/src/shared/rpc.ts +47 -13
  134. package/src/shared/rpcRequest.ts +27 -3
  135. package/src/shared/url.ts +4 -44
  136. package/src/shared/writePath.ts +1 -1
  137. package/src/ui/LAYER_NAMES.ts +1 -1
  138. package/src/ui/List.ts +3 -3
  139. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  140. package/src/ui/Mismatch.ts +1 -1
  141. package/src/ui/RawRun.ts +1 -1
  142. package/src/ui/Router.ts +38 -31
  143. package/src/ui/Scope.ts +1 -1
  144. package/src/ui/TryRegion.ts +2 -2
  145. package/src/ui/applyHead.ts +33 -0
  146. package/src/ui/index.ts +1 -1
  147. package/src/ui/menuBridge.ts +1 -1
  148. package/src/ui/moveNode.ts +1 -1
  149. package/src/ui/reconcile.ts +2 -2
  150. package/src/ui/reportMenu.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 → page.abide} +0 -0
@@ -1,5 +1,5 @@
1
1
  // 38.4 — `abide dev`: the project, watched, with the app kept up across every save. What is here
2
- // is the LISTENER and nothing else (38.15): the socket is bound once in this process and survives
2
+ // is the LISTENER and nothing else (38.14): the socket is bound once in this process and survives
3
3
  // every reload, so a browser holding a connection open meets a server that is still there when
4
4
  // the app behind it has been thrown away and rebuilt.
5
5
  //
@@ -12,13 +12,14 @@
12
12
  // reload, where a compiler imported into each new app cost 4.5 s a save against the 80 ms the
13
13
  // compile takes — and the clause asks for the APP to be in the child, which the compiler is not.
14
14
  //
15
- // 38.16 — A RELOAD IS A NEW PROCESS, not a module cache to invalidate. An app leaves things behind
15
+ // 38.15 — A RELOAD IS A NEW PROCESS, not a module cache to invalidate. An app leaves things behind
16
16
  // that no invalidation reaches — a timer, a `share`, a module that ran once, and bun's own memory of
17
17
  // a file it failed to find — and a process is the one boundary that takes all of them with it. Every live socket is force-closed with it, which
18
18
  // is also the whole of the reload protocol: the reload client reconnects and reloads, so nothing
19
19
  // here has a message to send.
20
20
 
21
21
  import { watch } from 'node:fs' // bun exposes no file watcher of its own
22
+ import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
22
23
  import {
23
24
  type AppChecker,
24
25
  compileApp,
@@ -26,23 +27,27 @@ import {
26
27
  } from '#compiler/compileApp.ts'
27
28
  import { formatDiagnosis } from '#compiler/formatDiagnosis.ts'
28
29
  import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
30
+ import { placeOf } from '#shared/routePattern.ts'
31
+ import type { Compilation } from './abideModules.ts'
29
32
  import { DEFAULT_PORT } from './DEFAULT_PORT.ts'
30
33
  import type { Ask, FromWorker, ToWorker } from './devWorker.ts'
31
34
  import { upgraded } from './muxUpgrade.ts'
32
35
  import { plainAnswer } from './plainAnswer.ts'
33
36
  import { SETTLE_MILLISECONDS } from './SETTLE_MILLISECONDS.ts'
34
- import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
35
37
 
36
38
  /** What `abide dev` serves, and where. */
37
39
  export type DevOptions = {
38
- /** The app's source root — the directory `ui/` sits under, and the tree that is watched. */
40
+ /**
41
+ * The app's source root — where its pages sit, and the tree that is watched — as its real path
42
+ * (`appRoot`), which is how the bundler names what it built.
43
+ */
39
44
  root: string
40
45
  port?: number | undefined
41
46
  }
42
47
 
43
48
  // A request in flight: the request itself — because a worker replaced before it answered answers
44
- // nothing, and the ask has to be put to the app that exists (38.25) — and, once its head has come
45
- // back, the stream the body is fed into and the pull waiting on the worker's next chunk (38.36).
49
+ // nothing, and the ask has to be put to the app that exists (38.24) — and, once its head has come
50
+ // back, the stream the body is fed into and the pull waiting on the worker's next chunk (38.35).
46
51
  type InFlight = Omit<Ask, 'id' | 'method' | 'url'> & {
47
52
  // 16.22 — the request bun handed the listener, which is the one its idle timeout is raised on.
48
53
  request: Request
@@ -60,7 +65,12 @@ type DevWorker = {
60
65
  terminate(): void
61
66
  }
62
67
 
63
- type Compiled = { modules: Map<string, string>; failure: string }
68
+ type Compiled = {
69
+ compilation: Compilation
70
+ /** The public files the compile's walk found (`appFiles`), which the worker answers. */
71
+ published: Map<string, string>
72
+ failure: string
73
+ }
64
74
 
65
75
  const LAST_DEV_PORT = 3099
66
76
 
@@ -128,21 +138,26 @@ export async function serveDev(
128
138
  if (checker !== undefined && !checker.current())
129
139
  await closeChecker()
130
140
  checker ??= await openChecker(options.root)
131
- const compiled = await compileApp(
132
- options.root,
133
- `${options.root}/${VIRTUAL_DIRECTORY}`,
134
- checker,
135
- )
141
+ const compiled = await compileApp(options.root, checker)
136
142
  // 14 and 32.14 — what the compiler has to say, on every compile.
137
143
  for (const warning of compiled.warnings)
138
144
  console.warn(formatDiagnosis(warning, process.cwd()))
139
- return { modules: compiled.modules, failure: '' }
145
+ watchDependencies(compiled.files)
146
+ return {
147
+ compilation: { files: compiled.files, tables: compiled.tables },
148
+ published: compiled.published,
149
+ failure: '',
150
+ }
140
151
  } catch (thrown) {
141
152
  await closeChecker()
142
153
  const failure =
143
154
  thrown instanceof Error ? thrown.message : String(thrown)
144
155
  console.error(`abide dev: ${failure}`)
145
- return { modules: new Map(), failure }
156
+ return {
157
+ compilation: { files: {}, tables: {} },
158
+ published: new Map(),
159
+ failure,
160
+ }
146
161
  }
147
162
  }
148
163
 
@@ -156,9 +171,34 @@ export async function serveDev(
156
171
  }
157
172
  }
158
173
 
174
+ // A path a watcher saw move — or a move nobody could name — and the replacement it schedules.
175
+ let settling: ReturnType<typeof setTimeout> | undefined
176
+ function moved(path: string | undefined): void {
177
+ checker?.saw(path)
178
+ if (settling !== undefined) clearTimeout(settling)
179
+ settling = setTimeout(replace, SETTLE_MILLISECONDS)
180
+ }
181
+
182
+ // 38.99 — A DEPENDENCY'S `.abide` IS THE APP'S TOO, and a linked package's lies outside the root's
183
+ // watcher: each directory one was compiled from is watched once, for the session.
184
+ const watchedDependencies = new Set<string>()
185
+ function watchDependencies(files: Compilation['files']): void {
186
+ for (const relative of Object.keys(files)) {
187
+ if (placeOf(relative) !== 'dependency') continue
188
+ const directory = dirname(resolve(options.root, relative))
189
+ if (watchedDependencies.has(directory)) continue
190
+ watchedDependencies.add(directory)
191
+ watch(directory, (_event, filename) =>
192
+ moved(
193
+ filename === null ? undefined : `${directory}/${filename}`,
194
+ ),
195
+ )
196
+ }
197
+ }
198
+
159
199
  // The worker, compiled and answering.
160
- function open({ modules, failure }: Compiled): void {
161
- // 38.15 — A PROCESS, NOT A THREAD, for the header's reason: bun remembers a file it failed
200
+ function open({ compilation, published, failure }: Compiled): void {
201
+ // 38.14 — A PROCESS, NOT A THREAD, for the header's reason: bun remembers a file it failed
162
202
  // to find for the life of the process, and a thread inherits the miss.
163
203
  //
164
204
  // THE ENVIRONMENT AS IT STANDS, `NODE_ENV` included (36.3): the child is handed what this
@@ -182,7 +222,7 @@ export async function serveDev(
182
222
  server.timeout(asked, message.seconds)
183
223
  } else answer(started, message)
184
224
  },
185
- // 38.42 — AN APP THAT EXITS ON ITS OWN is replaced, and says so: a listener still
225
+ // 38.41 — AN APP THAT EXITS ON ITS OWN is replaced, and says so: a listener still
186
226
  // forwarding to it leaves every tab hanging.
187
227
  onExit() {
188
228
  if (worker !== started) return
@@ -200,15 +240,19 @@ export async function serveDev(
200
240
  if (!child.killed && child.exitCode === null)
201
241
  child.send(message)
202
242
  },
243
+ // 38.96 — SIGTERM, which the worker answers by hanging up what it is answering; a worker
244
+ // wedged in a loop never reads it, so a second later it is killed outright.
203
245
  terminate() {
204
246
  child.kill()
247
+ setTimeout(() => child.kill('SIGKILL'), 1000).unref()
205
248
  },
206
249
  }
207
250
  worker = started
208
251
  started.postMessage({
209
252
  kind: 'start',
210
253
  root: options.root,
211
- modules,
254
+ compilation,
255
+ published,
212
256
  failure,
213
257
  })
214
258
  // The worker holds every request behind its own compile, so what waited here only had
@@ -218,8 +262,8 @@ export async function serveDev(
218
262
  }
219
263
 
220
264
  // WHAT AN APP THAT IS GONE WAS HOLDING. A request whose head has not gone out is re-asked of the
221
- // replacement (38.25), or answered where the app ended itself — the request in hand may be what
222
- // ended it (38.42). 38.38 — one whose head has gone out cannot be answered again, and ends as
265
+ // replacement (38.24), or answered where the app ended itself — the request in hand may be what
266
+ // ended it (38.41). 38.37 — one whose head has gone out cannot be answered again, and ends as
223
267
  // `abide start`'s does.
224
268
  function endInFlight(reask: boolean, why: string): void {
225
269
  for (const held of inFlight.values()) {
@@ -232,11 +276,11 @@ export async function serveDev(
232
276
  inFlight.clear()
233
277
  }
234
278
 
235
- // 38.16 — THE REPLACEMENT. The old worker is terminated, every socket it could still be
279
+ // 38.15 — THE REPLACEMENT. The old worker is terminated, every socket it could still be
236
280
  // answering is force-closed, and the new one is opened. The order matters: a socket closed
237
281
  // after the new worker is up is a reload client that reconnects to a server still compiling.
238
282
  //
239
- // 38.25 — A REQUEST IN FLIGHT IS RE-ASKED rather than failed. Failing it serves a reader a
283
+ // 38.24 — A REQUEST IN FLIGHT IS RE-ASKED rather than failed. Failing it serves a reader a
240
284
  // network error for asking while somebody saved, and during a regeneration of a whole docs
241
285
  // tree that is every request; re-asked, they wait and get the app the source now says.
242
286
  function replace(): void {
@@ -255,24 +299,19 @@ export async function serveDev(
255
299
  await compiling
256
300
 
257
301
  // 38.4 — THE PROJECT, WATCHED. One recursive watcher over the source root: the pages, the
258
- // shell, the stylesheets and `ui/public` are all under it, and every one of them is something
302
+ // layouts, the stylesheets and every `public` directory are all under it, and every one of them is something
259
303
  // the worker read at boot.
260
304
  //
261
305
  // IT IS NOT UNREFFED. It holds the process open, which is what a command that IS a server
262
306
  // wants: the only ways out of `abide dev` are a signal and a throw, exactly as for
263
307
  // `abide start`.
264
- let settling: ReturnType<typeof setTimeout> | undefined
265
- watch(options.root, { recursive: true }, (_event, filename) => {
266
- checker?.saw(
267
- filename === null ? undefined : `${options.root}/${filename}`,
268
- )
269
- if (settling !== undefined) clearTimeout(settling)
270
- settling = setTimeout(replace, SETTLE_MILLISECONDS)
271
- })
308
+ watch(options.root, { recursive: true }, (_event, filename) =>
309
+ moved(filename === null ? undefined : `${options.root}/${filename}`),
310
+ )
272
311
 
273
312
  // A REQUEST AS A MESSAGE. `Request` does not survive a structured clone, so what crosses is
274
313
  // the four things the worker rebuilds one from — and the request's body is collected here, which
275
- // is what lets it be asked again of a replacement (38.25). The answer's body streams (38.36).
314
+ // is what lets it be asked again of a replacement (38.24). The answer's body streams (38.35).
276
315
  //
277
316
  // COLLECTED ONCE, HERE, rather than inside `forward`: a body can only be read off a
278
317
  // `Request` once, and a re-asked request is the second read that is not there.
@@ -291,7 +330,7 @@ export async function serveDev(
291
330
  })
292
331
  }
293
332
 
294
- // 38.39 — ASKED WITHOUT `accept-encoding`, so the app answers uncompressed. A failure after the
333
+ // 38.38 — ASKED WITHOUT `accept-encoding`, so the app answers uncompressed. A failure after the
295
334
  // head is shown as markup appended to the stream (`pull` in the worker), and markup appended to a
296
335
  // compressed stream is a stream no browser can decode. Localhost buys nothing from the bytes saved.
297
336
  function identityOnly(headers: Headers): [string, string][] {
@@ -301,7 +340,7 @@ export async function serveDev(
301
340
  return kept
302
341
  }
303
342
 
304
- // 38.36, 38.37 — THE HEAD SETTLES THE RESPONSE, and its body pulls one chunk from the worker per
343
+ // 38.35, 38.36 — THE HEAD SETTLES THE RESPONSE, and its body pulls one chunk from the worker per
305
344
  // read the connection makes: a high-water mark of 0 asks for nothing ahead of the reader.
306
345
  function answer(
307
346
  from: DevWorker,
@@ -391,11 +430,14 @@ export async function serveDev(
391
430
  })
392
431
  }
393
432
 
394
- // THE CHECKER GOES WITH THE COMMAND. It is a process of its own, and an app with no tsconfig
395
- // holds a temporary project on disk for as long as it is open. The signal is raised again once
396
- // it is closed, so the command still ends the way that signal ends a process.
433
+ // THE CHECKER AND THE WORKER GO WITH THE COMMAND. Each is a process of its own, and an app with
434
+ // no tsconfig holds a temporary project on disk for as long as the checker is open. The worker
435
+ // is told directly rather than left to its `disconnect`, which a worker busy inside a build
436
+ // reads only when the build lets go — orphaned until then. The signal is raised again once the
437
+ // checker is closed, so the command still ends the way that signal ends a process.
397
438
  for (const signal of ['SIGINT', 'SIGTERM'] as const)
398
439
  process.once(signal, () => {
440
+ worker?.terminate()
399
441
  void closeChecker().finally(() => process.kill(process.pid, signal))
400
442
  })
401
443
 
@@ -472,7 +514,7 @@ export async function serveDev(
472
514
  },
473
515
  })
474
516
 
475
- // 38.65 — A PORT SOMEONE NAMED IS THE PORT, and one taken is the failure. `Bun.serve` given
517
+ // 38.64 — A PORT SOMEONE NAMED IS THE PORT, and one taken is the failure. `Bun.serve` given
476
518
  // none reads `PORT` itself.
477
519
  const server =
478
520
  options.port !== undefined || process.env.PORT
@@ -481,7 +523,7 @@ export async function serveDev(
481
523
  return server as unknown as Bun.Server<undefined>
482
524
  }
483
525
 
484
- // 38.64 — NAMED BY NOBODY, the first free port from 3000. A port taken is the next one tried, and
526
+ // 38.63 — NAMED BY NOBODY, the first free port from 3000. A port taken is the next one tried, and
485
527
  // any other failure is the failure.
486
528
  function listenOnFirstFree<Listener>(
487
529
  listen: (port: number) => Listener,
@@ -9,7 +9,7 @@ import { useColor } from '#shared/log.ts'
9
9
  import { paint as painted } from '#shared/paint.ts'
10
10
  import type { Mount } from './answerRpc.ts'
11
11
  import { LogPanel } from './LogPanel.ts'
12
- import { callHandler, followLogs, watch } from './remote.ts'
12
+ import { callHandler, watch } from './remote.ts'
13
13
  import { type Listed, surfaceOf } from './surfaceOf.ts'
14
14
 
15
15
  /**
@@ -48,8 +48,7 @@ function wordsOf(text: string): string[] {
48
48
  * is in argv (38.8). Answers `exit` where the line said so and `ended` where the input did, so a
49
49
  * host that owns the app can stop it on the one and keep serving on the other.
50
50
  *
51
- * `local` is the app in this process — the one `abide` alone serves — whose console already tails into the panel, so
52
- * `logs` has nothing to add, and whose `exit` stops it.
51
+ * `local` is the app in this process — the one `abide` alone serves — whose `exit` stops it.
53
52
  */
54
53
  export async function shell(options: {
55
54
  url: string
@@ -78,7 +77,7 @@ export async function shell(options: {
78
77
  ),
79
78
  })
80
79
  // CTRL-C AS A SHELL TAKES IT — raw mode takes the key from the terminal, so no signal is sent for
81
- // it: a call running is cancelled, then every watch standing, a line typed is dropped, and on
80
+ // it: a call running is canceled, then every watch standing, a line typed is dropped, and on
82
81
  // an empty line it asks once and leaves on the second, as `node`'s REPL does.
83
82
  let exited = false
84
83
  let closed = false
@@ -169,7 +168,6 @@ export async function shell(options: {
169
168
  )
170
169
  : undefined
171
170
  if (panel === undefined) console.log(options.heading)
172
- let tailing = false
173
171
  try {
174
172
  for await (const line of lines) {
175
173
  const [word, address] = line.trim().split(/\s+/)
@@ -199,7 +197,7 @@ export async function shell(options: {
199
197
  ).catch((thrown: Error) => ({
200
198
  code: 1,
201
199
  text: running?.signal.aborted
202
- ? '(cancelled)'
200
+ ? '(canceled)'
203
201
  : thrown.message,
204
202
  }))
205
203
  running = undefined
@@ -210,14 +208,8 @@ export async function shell(options: {
210
208
  )
211
209
  } else if (word === 'call')
212
210
  console.log('call: name the handler — call <address> [args]')
213
- // TAILED ALONGSIDE THE PROMPT rather than in place of it, so the shell still answers.
214
- else if (word === 'logs' && !local && !tailing) {
215
- tailing = true
216
- void followLogs(url).then(() => {
217
- tailing = false
218
- })
219
- } else if (word === 'watch' && address !== undefined) {
220
- // Followed alongside the prompt, as `logs` is.
211
+ // FOLLOWED ALONGSIDE THE PROMPT rather than in place of it, so the shell still answers.
212
+ else if (word === 'watch' && address !== undefined) {
221
213
  const following = new AbortController()
222
214
  watching.add(following)
223
215
  void watch(
@@ -235,12 +227,6 @@ export async function shell(options: {
235
227
  .finally(() => watching.delete(following))
236
228
  } else if (word === 'watch')
237
229
  console.log('watch: name a GET — watch <address> [args]')
238
- else if (word === 'logs')
239
- console.log(
240
- local
241
- ? "logs: the app's log already tails here."
242
- : 'logs: already tailing.',
243
- )
244
230
  else
245
231
  console.log(
246
232
  `unknown: ${word} — help lists what the shell takes`,
@@ -456,7 +442,6 @@ export function shellWords(local: boolean): [usage: string, meaning: string][] {
456
442
  ['handlers', 'every handler call can reach, with its args'],
457
443
  ['watch <address> [args]', 'follow a GET — watch chat/room room=lobby'],
458
444
  ]
459
- if (!local) words.push(['logs', "tail the app's log.records here"])
460
445
  words.push(
461
446
  ['help', 'this list — Ctrl+C clears the line, Ctrl+L the panel'],
462
447
  ['exit', local ? 'stop the app and leave' : 'leave the shell'],
@@ -488,7 +473,7 @@ export function cliListing(handlers: Mount[]): Listed[] {
488
473
 
489
474
  /**
490
475
  * The handlers the shell lists: each address with the args it is typed with, and its description on
491
- * the line under it where it declares one. 38.63.
476
+ * the line under it where it declares one. 38.62.
492
477
  */
493
478
  export function handlersText(
494
479
  listed: Listed[],
@@ -0,0 +1,139 @@
1
+ // THE `mappings` FIELD OF A SOURCE MAP (revision 3), read and written. The compiler writes one per
2
+ // module it emits from a `.abide` (`sourceMapOf`), the server reads them to place a stack frame in
3
+ // the `.abide` (`installSourceMaps`), and the client build reads bun's to put the two together —
4
+ // bun reads no map it is handed, at run time or in the bundler, so every one of those is ours.
5
+
6
+ /** Where one generated column came from: `[column, source, line, sourceColumn]`, all from 0. */
7
+ export type Segment = [number, number, number, number]
8
+
9
+ const DIGITS =
10
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
11
+ const VALUES = new Map<string, number>()
12
+ for (let index = 0; index < DIGITS.length; index += 1)
13
+ VALUES.set(DIGITS[index] as string, index)
14
+
15
+ function vlq(value: number): string {
16
+ let rest = value < 0 ? (-value << 1) | 1 : value << 1
17
+ let out = ''
18
+ do {
19
+ let digit = rest & 31
20
+ rest >>>= 5
21
+ if (rest > 0) digit |= 32
22
+ out += DIGITS[digit]
23
+ } while (rest > 0)
24
+ return out
25
+ }
26
+
27
+ /** Each generated line's segments, ascending by column, as the `mappings` string. */
28
+ export function encodeMappings(lines: readonly (readonly Segment[])[]): string {
29
+ let out = ''
30
+ let source = 0
31
+ let line = 0
32
+ let sourceColumn = 0
33
+ for (let index = 0; index < lines.length; index += 1) {
34
+ if (index > 0) out += ';'
35
+ let column = 0
36
+ const segments = lines[index] as readonly Segment[]
37
+ for (let at = 0; at < segments.length; at += 1) {
38
+ const segment = segments[at] as Segment
39
+ if (at > 0) out += ','
40
+ out +=
41
+ vlq(segment[0] - column) +
42
+ vlq(segment[1] - source) +
43
+ vlq(segment[2] - line) +
44
+ vlq(segment[3] - sourceColumn)
45
+ column = segment[0]
46
+ source = segment[1]
47
+ line = segment[2]
48
+ sourceColumn = segment[3]
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ /**
55
+ * A `mappings` string as each generated line's segments. A segment naming no source — one field
56
+ * long — places nothing, and is dropped.
57
+ */
58
+ export function decodeMappings(mappings: string): Segment[][] {
59
+ const lines: Segment[][] = [[]]
60
+ const fields: number[] = []
61
+ let source = 0
62
+ let line = 0
63
+ let sourceColumn = 0
64
+ let column = 0
65
+ let value = 0
66
+ let shift = 0
67
+ const close = (): void => {
68
+ if (fields.length >= 4) {
69
+ column += fields[0] as number
70
+ source += fields[1] as number
71
+ line += fields[2] as number
72
+ sourceColumn += fields[3] as number
73
+ ;(lines[lines.length - 1] as Segment[]).push([
74
+ column,
75
+ source,
76
+ line,
77
+ sourceColumn,
78
+ ])
79
+ } else if (fields.length > 0) column += fields[0] as number
80
+ fields.length = 0
81
+ }
82
+ for (const character of mappings) {
83
+ if (character === ';') {
84
+ close()
85
+ lines.push([])
86
+ column = 0
87
+ continue
88
+ }
89
+ if (character === ',') {
90
+ close()
91
+ continue
92
+ }
93
+ const digit = VALUES.get(character) ?? 0
94
+ value |= (digit & 31) << shift
95
+ if (digit & 32) {
96
+ shift += 5
97
+ continue
98
+ }
99
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1)
100
+ value = 0
101
+ shift = 0
102
+ }
103
+ close()
104
+ return lines
105
+ }
106
+
107
+ /**
108
+ * Where `column` on generated `line` came from: the last segment at or before it, or the line's
109
+ * first where the column is before every one — a frame's column points into an expression, and
110
+ * the segment is its start.
111
+ */
112
+ export function segmentAt(
113
+ lines: readonly (readonly Segment[])[],
114
+ line: number,
115
+ column: number,
116
+ ): Segment | undefined {
117
+ const segments = lines[line]
118
+ if (segments === undefined || segments.length === 0) return undefined
119
+ let found = segments[0] as Segment
120
+ for (const segment of segments) {
121
+ if (segment[0] > column) break
122
+ found = segment
123
+ }
124
+ return found
125
+ }
126
+
127
+ /**
128
+ * The `[line, column]` generated `line` and `column` came from. Within a segment the run was copied
129
+ * as written, so the column moves with it.
130
+ */
131
+ export function placeAt(
132
+ lines: readonly (readonly Segment[])[],
133
+ line: number,
134
+ column: number,
135
+ ): [number, number] | undefined {
136
+ const segment = segmentAt(lines, line, column)
137
+ if (segment === undefined) return undefined
138
+ return [segment[2], segment[3] + Math.max(column - segment[0], 0)]
139
+ }
@@ -1,4 +1,3 @@
1
- import { RPC_ADDRESS } from '#shared/ADDRESSES.ts'
2
1
  import type { Method } from '#shared/rpc.ts'
3
2
  import type { Mount, Mounted } from './answerRpc.ts'
4
3
 
@@ -9,8 +8,10 @@ export type Surface = 'mcp' | 'openapi' | 'cli'
9
8
  export type Listed = {
10
9
  address: string
11
10
  method: Method
12
- /** Its address below `/__abide/rpc` — what `call` spells it (38.8), and every surface names it by. */
11
+ /** Its route without the leading `/` — what `call` spells it (38.8). */
13
12
  path: string
13
+ /** What MCP and OpenAPI name it by. 20.13. */
14
+ name: string
14
15
  description: string | undefined
15
16
  /** Its args' JSON Schema, where it has one. 19.9, 20.11. */
16
17
  schema: unknown
@@ -39,9 +40,23 @@ export function surfaceOf(
39
40
  return {
40
41
  address: mount.address,
41
42
  method: mount.method,
42
- path: mount.address.slice(RPC_ADDRESS.length + 1),
43
+ path: mount.address.slice(1),
44
+ name: surfaceName(mount.method, mount.address),
43
45
  description:
44
46
  live === undefined ? mount.description : live.handler.description,
45
47
  schema: live === undefined ? mount.args : live.argsDocument,
46
48
  }
47
49
  }
50
+
51
+ /**
52
+ * A handler's name on MCP and OpenAPI: its method and its route, in the letters a tool's name admits.
53
+ *
54
+ * 20.13 — the method first, so two handlers at one route are two names, and a segment's brackets
55
+ * and dots dropped, so `/media/[id]` reads `media_id` rather than `media__id_`.
56
+ */
57
+ export function surfaceName(method: Method, route: string): string {
58
+ return `${method.toLowerCase()}${route === '/' ? '' : route.replace(/[[\].]/g, '')}`.replace(
59
+ /[^a-zA-Z0-9-]/g,
60
+ '_',
61
+ )
62
+ }
@@ -7,11 +7,8 @@
7
7
  import { mkdir, rename, rm, utimes, writeFile } from 'node:fs/promises' // bun exposes no directory creation, rename, removal, mtime write or exclusive create of its own
8
8
  import { dirname } from 'node:path' // bun exposes no path helpers of its own
9
9
  import { namesUnder } from './namesUnder.ts'
10
+ import { runBuild } from './runBuild.ts'
10
11
 
11
- // The CLI, spawned rather than imported: the compiler is not reachable from the server seam.
12
- const ABIDE_BIN = Bun.fileURLToPath(
13
- new URL('../../bin/abide.ts', import.meta.url),
14
- )
15
12
  // The framework's own source is part of what made a build: a compiler change is a new build.
16
13
  const FRAMEWORK = Bun.fileURLToPath(new URL('../..', import.meta.url))
17
14
  const POLL_MS = 50
@@ -95,21 +92,6 @@ async function buildInto(
95
92
  }
96
93
  }
97
94
 
98
- async function runBuild(root: string, out: string): Promise<void> {
99
- const child = Bun.spawn(
100
- [process.execPath, ABIDE_BIN, 'build', '--root', root, '--out', out],
101
- { stdout: 'ignore', stderr: 'pipe' },
102
- )
103
- const [exitCode, said] = await Promise.all([
104
- child.exited,
105
- new Response(child.stderr).text(),
106
- ])
107
- if (exitCode !== 0)
108
- throw new Error(
109
- `abide: createApp could not build the app under ${root} for this test run.\n${said}`,
110
- )
111
- }
112
-
113
95
  /**
114
96
  * A hash of everything a build of the app under `root` is made from.
115
97
  *