@abide/abide 0.117.2 → 0.119.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 (156) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +74 -11
  3. package/docs/REGISTRY.md +18 -15
  4. package/docs/RULEBOOK.md +71 -10
  5. package/docs/abide.md +584 -274
  6. package/package.json +1 -1
  7. package/scaffold/AGENTS.md +1 -1
  8. package/scaffold/src/getGreeting.test.ts +2 -1
  9. package/scaffold/src/index.abide +5 -2
  10. package/scaffold/src/index.test.ts +2 -0
  11. package/scaffold/src/layout.abide +6 -6
  12. package/src/compiler/Lowering.ts +20 -2
  13. package/src/compiler/PositionTable.ts +1 -0
  14. package/src/compiler/Refusals.ts +1 -0
  15. package/src/compiler/TypeSession.ts +49 -13
  16. package/src/compiler/aliasedSymbol.ts +1 -1
  17. package/src/compiler/ambientReadsIn.ts +1 -0
  18. package/src/compiler/appLayout.ts +55 -3
  19. package/src/compiler/appMap.ts +15 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -0
  21. package/src/compiler/argsSchemaOf.ts +1 -0
  22. package/src/compiler/checkApp.ts +3 -1
  23. package/src/compiler/checkedShadow.ts +2 -1
  24. package/src/compiler/classifyRead.ts +1 -0
  25. package/src/compiler/classifyTemplate.ts +1 -0
  26. package/src/compiler/compileApp.ts +5 -3
  27. package/src/compiler/compiledEntry.ts +2 -1
  28. package/src/compiler/componentGraph.ts +1 -0
  29. package/src/compiler/declaredExports.ts +4 -2
  30. package/src/compiler/emitClient.ts +19 -14
  31. package/src/compiler/emitComponent.ts +1 -0
  32. package/src/compiler/emitSource.ts +2 -1
  33. package/src/compiler/formatAbide.ts +857 -0
  34. package/src/compiler/formatDiagnosis.ts +1 -0
  35. package/src/compiler/handlersUnder.ts +7 -3
  36. package/src/compiler/hookCalls.ts +1 -0
  37. package/src/compiler/lineOf.ts +17 -0
  38. package/src/compiler/matchersOf.ts +157 -0
  39. package/src/compiler/mountsOf.ts +5 -0
  40. package/src/compiler/paramFile.ts +125 -0
  41. package/src/compiler/parseAbide.ts +48 -28
  42. package/src/compiler/projectShadow.ts +11 -4
  43. package/src/compiler/reactiveOptions.ts +1 -0
  44. package/src/compiler/reactiveTypes.ts +2 -1
  45. package/src/compiler/readsIn.ts +3 -2
  46. package/src/compiler/renderMarkup.ts +3 -2
  47. package/src/compiler/resolverIn.ts +2 -0
  48. package/src/compiler/sameTemplate.ts +150 -0
  49. package/src/compiler/scaffold.ts +4 -11
  50. package/src/compiler/serveLanguage.ts +48 -21
  51. package/src/compiler/sharedModuleWrites.ts +2 -1
  52. package/src/compiler/sourceMapOf.ts +1 -0
  53. package/src/compiler/splitImports.ts +2 -0
  54. package/src/compiler/templateWarnings.ts +2 -2
  55. package/src/compiler/trackingWarnings.ts +1 -0
  56. package/src/compiler/writeDefaultProject.ts +5 -1
  57. package/src/server/BRIDGE_VERSION.ts +1 -1
  58. package/src/server/abideModules.ts +2 -0
  59. package/src/server/answerFailure.ts +1 -0
  60. package/src/server/answerRequest.ts +57 -19
  61. package/src/server/answerRpc.ts +6 -2
  62. package/src/server/answerSocket.ts +2 -1
  63. package/src/server/answerValue.ts +1 -0
  64. package/src/server/answerWorker.ts +1 -0
  65. package/src/server/appTree.ts +18 -9
  66. package/src/server/argsFor.ts +1 -0
  67. package/src/server/assistant.ts +2 -1
  68. package/src/server/banner.ts +2 -0
  69. package/src/server/bridgeRelay.ts +1 -0
  70. package/src/server/callerOf.ts +1 -0
  71. package/src/server/claudeApi.ts +1 -0
  72. package/src/server/claudeCode.ts +1 -0
  73. package/src/server/clientBundle.ts +1 -0
  74. package/src/server/coerceArgs.ts +1 -2
  75. package/src/server/commands.ts +9 -0
  76. package/src/server/compressAnswer.ts +2 -1
  77. package/src/server/config.ts +2 -0
  78. package/src/server/createApp.ts +4 -2
  79. package/src/server/crossingOrigin.ts +1 -0
  80. package/src/server/csp.ts +1 -0
  81. package/src/server/deferredBuilds.ts +1 -0
  82. package/src/server/derivedArgs.ts +1 -0
  83. package/src/server/devWorker.ts +2 -1
  84. package/src/server/faceOf.ts +2 -7
  85. package/src/server/generatedSurfaces.ts +79 -7
  86. package/src/server/hooks.ts +1 -0
  87. package/src/server/iconAccent.ts +1 -1
  88. package/src/server/installServer.ts +1 -0
  89. package/src/server/installSourceMaps.ts +1 -0
  90. package/src/server/launcherPage.ts +2 -2
  91. package/src/server/manifestOf.ts +1 -0
  92. package/src/server/muxUpgrade.ts +1 -0
  93. package/src/server/nativeMenu.ts +2 -0
  94. package/src/server/placement.ts +12 -2
  95. package/src/server/principal.ts +2 -1
  96. package/src/server/remote.ts +7 -2
  97. package/src/server/render.ts +3 -3
  98. package/src/server/renderComponent.ts +1 -0
  99. package/src/server/reportHealth.ts +1 -0
  100. package/src/server/requestScope.ts +3 -1
  101. package/src/server/runDesktop.ts +3 -2
  102. package/src/server/seedBuffer.ts +1 -0
  103. package/src/server/serveDev.ts +3 -2
  104. package/src/server/shell.ts +2 -0
  105. package/src/server/streamEvent.ts +1 -0
  106. package/src/server/surfaceOf.ts +6 -1
  107. package/src/server/testBuild.ts +1 -0
  108. package/src/server/testMount.ts +1 -0
  109. package/src/server/trace.ts +2 -0
  110. package/src/server/withAbideSources.ts +1 -0
  111. package/src/shared/Attributes.ts +12 -8
  112. package/src/shared/cookies.ts +1 -1
  113. package/src/shared/escapeText.ts +1 -1
  114. package/src/shared/filledPath.ts +16 -10
  115. package/src/shared/index.ts +1 -5
  116. package/src/shared/jsonSchema.ts +1 -3
  117. package/src/shared/matchRoute.ts +16 -4
  118. package/src/shared/menuBridge.ts +1 -2
  119. package/src/shared/outbox.ts +1 -1
  120. package/src/shared/reactive/Keyed.ts +2 -2
  121. package/src/shared/reactive/Reactive.ts +12 -16
  122. package/src/shared/reactive/index.ts +3 -4
  123. package/src/shared/reactive/isReactive.ts +1 -1
  124. package/src/shared/reactive/scoped.ts +2 -2
  125. package/src/shared/reactive/share.ts +1 -1
  126. package/src/shared/reactive/watch.ts +4 -4
  127. package/src/shared/refuse.ts +1 -1
  128. package/src/shared/routePattern.ts +66 -14
  129. package/src/shared/rpc.ts +8 -11
  130. package/src/shared/url.ts +24 -7
  131. package/src/shared/validateJson.ts +1 -1
  132. package/src/ui/ArmRegion.ts +1 -0
  133. package/src/ui/Level.ts +1 -0
  134. package/src/ui/List.ts +1 -0
  135. package/src/ui/RawRun.ts +1 -0
  136. package/src/ui/Router.ts +1 -0
  137. package/src/ui/TryRegion.ts +1 -0
  138. package/src/ui/adoptWalk.ts +2 -1
  139. package/src/ui/bindElement.ts +1 -0
  140. package/src/ui/bindFiles.ts +1 -0
  141. package/src/ui/bindGroup.ts +1 -0
  142. package/src/ui/bindProperty.ts +1 -0
  143. package/src/ui/bindSpread.ts +1 -0
  144. package/src/ui/index.ts +1 -6
  145. package/src/ui/outboxQueue.ts +1 -1
  146. package/src/ui/parseMarkup.ts +2 -1
  147. package/src/ui/reconcile.ts +2 -1
  148. package/src/ui/reconcileHead.ts +1 -0
  149. package/src/ui/regions.ts +2 -1
  150. package/src/ui/registerWorker.ts +1 -0
  151. package/src/ui/rows.ts +1 -0
  152. package/src/ui/setMarkup.ts +1 -0
  153. package/src/ui/setText.ts +1 -0
  154. package/src/ui/socketWire.ts +4 -3
  155. package/src/ui/wire.ts +2 -1
  156. /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
@@ -23,6 +23,7 @@ import { PATCH_SINKS } from '#shared/reactive/PATCH_SINKS.ts'
23
23
  import { unowned } from '#shared/reactive/watch.ts'
24
24
  import { refuse } from '#shared/refuse.ts'
25
25
  import { rpcRequest } from '#shared/rpcRequest.ts'
26
+
26
27
  import { handlerKey, type Mounted } from './answerRpc.ts'
27
28
  import { argsFor } from './argsFor.ts'
28
29
  import { positiveSetting } from './config.ts'
@@ -32,8 +33,8 @@ import { sendOrClose } from './muxUpgrade.ts'
32
33
  import {
33
34
  heldByApp,
34
35
  inOpenScope,
35
- type RequestScope,
36
36
  releaseScope,
37
+ type RequestScope,
37
38
  scopeFor,
38
39
  } from './requestScope.ts'
39
40
  import { EVENT_STREAM_OPENER, streamEvent } from './streamEvent.ts'
@@ -4,6 +4,7 @@
4
4
  // `Values` a handler returned — framed one chunk per pull (16.32, 16.44–16.47).
5
5
 
6
6
  import { isFailed, problemOf } from '#shared/Failed.ts'
7
+
7
8
  import { compressAnswer } from './compressAnswer.ts'
8
9
  import { prefers } from './prefers.ts'
9
10
  import {
@@ -19,6 +19,7 @@ import {
19
19
  } from '#shared/OFFLINE_CACHES.ts'
20
20
  import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
21
21
  import { REACHED_MESSAGE } from '#shared/REACHED_MESSAGE.ts'
22
+
22
23
  import type { Manifest } from './manifestOf.ts'
23
24
 
24
25
  // What `framingOf` reads as a framed stream (16.32), and a binary's bytes (16.34), which may be one —
@@ -1,6 +1,6 @@
1
1
  // 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. A file directly in a directory
2
2
  // below the root is an address — a page, a layout, an error page or a handler file — and nothing
3
- // else is there but its tests (23.22); a fence is never one (23.15): `public` holds files answered as
3
+ // else is there but its tests and the param files its segments name (23.22, 23.24); a fence is never one (23.15): `public` holds files answered as
4
4
  // authored at the path of the directories above it, and `lib` holds the rest, a server file among it
5
5
  // (23.23). So listing a directory lists its routes.
6
6
  //
@@ -13,17 +13,21 @@
13
13
  // can drop them.
14
14
 
15
15
  import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
16
+
16
17
  import type { Refusals } from '#compiler/Refusals.ts'
17
18
  import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
18
19
  import {
19
20
  PUBLIC_DIRECTORY,
20
21
  RESERVED_DIRECTORIES,
21
22
  } from '#shared/RESERVED_DIRECTORIES.ts'
23
+
22
24
  import {
23
25
  DIRECTORY_FILES,
26
+ isBenchFile,
24
27
  isDeclarationFile,
28
+ isHandlerFile,
29
+ isParamFile,
25
30
  isServerFile,
26
- isBenchFile,
27
31
  isTestFile,
28
32
  placeOf,
29
33
  routeOf,
@@ -36,6 +40,8 @@ type AppFiles = {
36
40
  abide: string[]
37
41
  /** Every handler file, in path order. */
38
42
  handlers: string[]
43
+ /** 23.24 — every param file directly in an address directory, in path order. */
44
+ params: string[]
39
45
  /** Every module under a `lib` directory, in path order — read for 37.13. */
40
46
  modules: string[]
41
47
  /** Every file under a `public` directory, to the address it is answered at below the mount. */
@@ -52,6 +58,7 @@ export function appFiles(root: string, refusals: Refusals): AppFiles {
52
58
  const files: AppFiles = {
53
59
  abide: [],
54
60
  handlers: [],
61
+ params: [],
55
62
  modules: [],
56
63
  published: new Map(),
57
64
  }
@@ -61,6 +68,7 @@ export function appFiles(root: string, refusals: Refusals): AppFiles {
61
68
  walk(root, '', undefined, files, refusals, packaged)
62
69
  files.abide.sort()
63
70
  files.handlers.sort()
71
+ files.params.sort()
64
72
  files.modules.sort()
65
73
  return files
66
74
  }
@@ -126,13 +134,17 @@ function walk(
126
134
  isDeclarationFile(path)
127
135
  )
128
136
  continue
129
- if (!abide && !isModule(path)) {
137
+ if (isParamFile(path)) {
138
+ files.params.push(path)
139
+ continue
140
+ }
141
+ if (!abide && !isHandlerFile(path)) {
130
142
  if (packaged && relative === '' && !path.endsWith('.ts'))
131
143
  continue
132
144
  refusals.at(
133
145
  path,
134
146
  new Error(
135
- `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, test files, bench files and declaration files alone. Move it into a lib directory, beside it or in any directory above it. RULEBOOK 23.22.`,
147
+ `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, param files, test files, bench files and declaration files alone. ${path.endsWith('.ts') ? 'Name a handler file <name>.handler.ts, or move anything else' : 'Move it'} into a lib directory, beside it or in any directory above it. RULEBOOK 23.22.`,
136
148
  ),
137
149
  )
138
150
  continue
@@ -211,8 +223,7 @@ export function holdsApp(root: string): boolean {
211
223
  return !appSourcesUnder(root, '').next().done
212
224
  }
213
225
 
214
- // Every `.abide` under `directory`, and every handler file — a module whose place is an address, and
215
- // no server file (23.23).
226
+ // Every `.abide` under `directory`, and every handler file whose place is an address.
216
227
  function* appSourcesUnder(
217
228
  directory: string,
218
229
  relative: string,
@@ -230,9 +241,7 @@ function* appSourcesUnder(
230
241
  yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
231
242
  else if (
232
243
  path.endsWith('.abide') ||
233
- (isModule(path) &&
234
- !isServerFile(path) &&
235
- placeOf(path) === 'address')
244
+ (isHandlerFile(path) && placeOf(path) === 'address')
236
245
  )
237
246
  yield path
238
247
  }
@@ -5,6 +5,7 @@
5
5
  import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
6
6
  import { normalized } from '#shared/reactive/argsContract.ts'
7
7
  import { validationError } from '#shared/refuse.ts'
8
+
8
9
  import type { Mounted } from './answerRpc.ts'
9
10
  import { coerceArgs } from './coerceArgs.ts'
10
11
 
@@ -12,8 +12,9 @@
12
12
 
13
13
  import { namedLogger } from '#shared/log.ts'
14
14
  import { refuse } from '#shared/refuse.ts'
15
- import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
15
+
16
16
  import { type McpReply, McpSurface } from './generatedSurfaces.ts'
17
+ import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
17
18
  import { request, within } from './requestScope.ts'
18
19
 
19
20
  /** A piece of a turn: words, or an image as base64 with its type. */
@@ -1,6 +1,8 @@
1
1
  import { networkInterfaces } from 'node:os' // bun exposes no network interface list of its own
2
+
2
3
  import { useColor } from '#shared/log.ts'
3
4
  import { paint as painted } from '#shared/paint.ts'
5
+
4
6
  import type { AppPackage } from './config.ts'
5
7
 
6
8
  /**
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { isFailed } from '#shared/Failed.ts'
17
17
  import { refuse } from '#shared/refuse.ts'
18
+
18
19
  import { answered } from './answerFailure.ts'
19
20
  import { bodyWithin } from './answerRpc.ts'
20
21
  import { framed } from './answerValue.ts'
@@ -1,4 +1,5 @@
1
1
  import { cookies } from '#shared/cookies.ts'
2
+
2
3
  import { randomToken } from './randomToken.ts'
3
4
  import {
4
5
  currentRequestScope,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { framedStream } from '#shared/framing.ts'
10
10
  import { refuse } from '#shared/refuse.ts'
11
+
11
12
  import { blocksOf, type Provider, textOf } from './assistant.ts'
12
13
 
13
14
  /** A Messages API request body, less what abide writes, with the client's key and origin. */
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { mounted } from '#shared/mount.ts'
12
12
  import { refuse } from '#shared/refuse.ts'
13
+
13
14
  import {
14
15
  type AssistantContent,
15
16
  type AssistantEvent,
@@ -7,6 +7,7 @@
7
7
  // long a browser may keep it.
8
8
 
9
9
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
10
+
10
11
  import { abideModules, type Compilation, tablePath } from './abideModules.ts'
11
12
  import { appPlugins } from './appPlugins.ts'
12
13
  import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
@@ -19,8 +19,7 @@ export type Document = {
19
19
  /** The document a handler's `args` schema declares, or `undefined`. */
20
20
  export function argsDocumentOf(handler: Gated<unknown>): Document | undefined {
21
21
  const gate = gateOf(handler) as
22
- | { '~standard'?: { jsonSchema?: { input?: () => unknown } } }
23
- | undefined
22
+ { '~standard'?: { jsonSchema?: { input?: () => unknown } } } | undefined
24
23
  const document = gate?.['~standard']?.jsonSchema?.input?.()
25
24
  return document !== null && typeof document === 'object'
26
25
  ? (document as Document)
@@ -5,8 +5,10 @@
5
5
 
6
6
  import { resolve } from 'node:path' // bun exposes no path helpers of its own
7
7
  import { parseArgs } from 'node:util' // bun exposes no argv parser of its own yet
8
+
8
9
  import { MCP_ADDRESS, OPENAPI_ADDRESS } from '#shared/ADDRESSES.ts'
9
10
  import { IN_PROCESS_ORIGIN } from '#shared/IN_PROCESS_ORIGIN.ts'
11
+
10
12
  import { banner } from './banner.ts'
11
13
  import { type App, type AppHandle, enterApp } from './createApp.ts'
12
14
  import { stopOnSignalOrFault } from './faultPolicy.ts'
@@ -32,6 +34,7 @@ type SourceCommands = Record<
32
34
  | 'dev'
33
35
  | 'build'
34
36
  | 'check'
37
+ | 'format'
35
38
  | 'routes'
36
39
  | 'compile'
37
40
  | 'bundle'
@@ -194,6 +197,11 @@ function commandsOf(host: Host): Row[] {
194
197
  'type-check the templates and the TypeScript, on the line you wrote',
195
198
  'check',
196
199
  ),
200
+ fromSource(
201
+ 'format [--check] [dir…]',
202
+ 'format every .abide — its code by bun fmt, its markup by its nesting',
203
+ 'format',
204
+ ),
197
205
  fromSource(
198
206
  'routes [--json]',
199
207
  'every address the app answers, its file, and where each hook is filed',
@@ -288,6 +296,7 @@ function parseFlags(argv: string[]) {
288
296
  target: { type: 'string' },
289
297
  platforms: { type: 'string' },
290
298
  json: { type: 'boolean' },
299
+ check: { type: 'boolean' },
291
300
  help: { type: 'boolean', short: 'h' },
292
301
  version: { type: 'boolean', short: 'v' },
293
302
  },
@@ -10,9 +10,10 @@
10
10
  // BUN'S GZIP parts from zlib's past ~130 KB, which only the off-loop lane reaches — so that lane takes
11
11
  // zlib's, the loop keeps bun's, and the two make the same bytes.
12
12
  import { brotliCompress, brotliCompressSync, constants, gzip } from 'node:zlib' // bun has no brotli, and no compression off the loop
13
+
13
14
  import {
14
- ENCODINGS,
15
15
  type Encoding,
16
+ ENCODINGS,
16
17
  negotiateEncoding,
17
18
  } from './negotiateEncoding.ts'
18
19
 
@@ -5,11 +5,13 @@
5
5
  // and a throw out of `onConfig` is the value's standing error, which a read throws (27.1).
6
6
 
7
7
  import { homedir } from 'node:os' // bun exposes no home directory of its own
8
+
8
9
  import { isRecord } from '#shared/isRecord.ts'
9
10
  import { numberFromText } from '#shared/numberFromText.ts'
10
11
  import { reactive } from '#shared/reactive/index.ts'
11
12
  import { validationError } from '#shared/refuse.ts'
12
13
  import { runSchema } from '#shared/Schema.ts'
14
+
13
15
  import { appDirectories } from './appDirectories.ts'
14
16
  import { type Env, HOOKS, inOrder } from './hooks.ts'
15
17
  import { withoutTrailingSlash } from './withoutTrailingSlash.ts'
@@ -5,17 +5,19 @@
5
5
 
6
6
  import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
7
7
  import { resolve } from 'node:path' // bun exposes no path helpers of its own
8
+
8
9
  import { FAILURE_STATUS_TEXT } from '#shared/FAILURE_STATUS_TEXT.ts'
9
10
  import { framingOf } from '#shared/framing.ts'
10
11
  import { IN_PROCESS_ORIGIN } from '#shared/IN_PROCESS_ORIGIN.ts'
11
- import { LONGEST_DELAY } from '#shared/LONGEST_DELAY.ts'
12
12
  import { namedLogger } from '#shared/log.ts'
13
+ import { LONGEST_DELAY } from '#shared/LONGEST_DELAY.ts'
13
14
  import { compose } from '#shared/Middleware.ts'
14
15
  import { MOUNT, unmounted } from '#shared/mount.ts'
15
16
  import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
16
17
  import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
18
+
17
19
  import { servedTable } from './abideModules.ts'
18
- import { type AppSources, answerRequest } from './answerRequest.ts'
20
+ import { answerRequest, type AppSources } from './answerRequest.ts'
19
21
  import type { Mount } from './answerRpc.ts'
20
22
  import { Mux, type MuxConnection } from './answerSocket.ts'
21
23
  import { appDirectories } from './appDirectories.ts'
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { Failed } from '#shared/Failed.ts'
6
6
  import { refuse } from '#shared/refuse.ts'
7
+
7
8
  import { appOrigin } from './config.ts'
8
9
 
9
10
  /**
package/src/server/csp.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // rung writing a document's header — so the two agree, and an answer that is not a document mints none.
7
7
 
8
8
  import type { Middleware } from '#shared/Middleware.ts'
9
+
9
10
  import { randomToken } from './randomToken.ts'
10
11
  import { EARLY_CALL_HASH } from './render.ts'
11
12
  import { within } from './requestScope.ts'
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { regionMarkup } from '#shared/anchorMarkup.ts'
12
12
  import { isReactive, isStreaming, loadOf } from '#shared/reactive/index.ts'
13
+
13
14
  import type { Rendered } from './render.ts'
14
15
  import type { PageCall } from './requestScope.ts'
15
16
 
@@ -3,6 +3,7 @@ import type { JsonValue } from '#shared/JsonValue.ts'
3
3
  import type { Gated } from '#shared/reactive/argsContract.ts'
4
4
  import type { HeldToAnnotation } from '#shared/rpc.ts'
5
5
  import { validateJson } from '#shared/validateJson.ts'
6
+
6
7
  import { argsDocumentOf } from './coerceArgs.ts'
7
8
 
8
9
  /**
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
22
22
  import { escapeText } from '#shared/escapeText.ts'
23
+
23
24
  import {
24
25
  type Compilation,
25
26
  serveCompilation,
@@ -34,7 +35,7 @@ import {
34
35
  clientBundle,
35
36
  type ServedFile,
36
37
  } from './clientBundle.ts'
37
- import { type BootedApp, bootApp, originUnder } from './createApp.ts'
38
+ import { bootApp, type BootedApp, originUnder } from './createApp.ts'
38
39
  import { keepServing } from './faultPolicy.ts'
39
40
  import { type Manifest, NO_MANIFEST } from './manifestOf.ts'
40
41
  import { admitsMux, muxSocketOptions } from './muxUpgrade.ts'
@@ -4,6 +4,7 @@
4
4
  // specifier, an `onLoad` for a path), and this decides what the answer is.
5
5
 
6
6
  import type { JsonValue } from '#shared/JsonValue.ts'
7
+
7
8
  import { HANDLER_SUFFIX, SERVER_QUERY } from './MODULE_QUERIES.ts'
8
9
 
9
10
  // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
@@ -38,13 +39,7 @@ export type Face = { path: string; namespace: string }
38
39
  * module (`?server`), a `.abide` named bare, or a module that may be a handler file.
39
40
  */
40
41
  export type Asked =
41
- | 'text'
42
- | 'style'
43
- | 'component'
44
- | 'client'
45
- | 'server'
46
- | 'abide'
47
- | 'handler'
42
+ 'text' | 'style' | 'component' | 'client' | 'server' | 'abide' | 'handler'
48
43
 
49
44
  /**
50
45
  * One handler file, as `Compilation.handlers` holds it — and 19.9's schema its default's annotation
@@ -18,8 +18,9 @@ import { essenceOf } from '#shared/framing.ts'
18
18
  import { namedLogger } from '#shared/log.ts'
19
19
  import { mounted } from '#shared/mount.ts'
20
20
  import { type Reactive, reactive } from '#shared/reactive/index.ts'
21
- import { parseRoutePattern } from '#shared/routePattern.ts'
21
+ import { parseRoutePattern, type Segment } from '#shared/routePattern.ts'
22
22
  import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
23
+
23
24
  import { answered } from './answerFailure.ts'
24
25
  import { bodyWithin, type Mounted } from './answerRpc.ts'
25
26
  import type { ToolAnswer, ToolDefinition } from './assistant.ts'
@@ -134,13 +135,22 @@ function openApiOf(handlers: Map<string, Mounted>): {
134
135
  for (const segment of parseRoutePattern(each.address)) {
135
136
  if (segment.kind === 'static') continue
136
137
  inPath.add(segment.name)
138
+ const declared = schema?.properties?.[segment.name] ?? {
139
+ type: 'string',
140
+ }
141
+ // 20.23 — and what the segment takes, save a rest's: its value is the parts joined,
142
+ // which one part's pattern does not describe.
137
143
  const parameter: Record<string, unknown> = {
138
144
  name: segment.name,
139
145
  in: 'path',
140
146
  required: true,
141
- schema: schema?.properties?.[segment.name] ?? {
142
- type: 'string',
143
- },
147
+ schema:
148
+ segment.kind === 'rest'
149
+ ? declared
150
+ : patterned(
151
+ declared,
152
+ patternOf(segment, each.matchers),
153
+ ),
144
154
  }
145
155
  if (segment.kind === 'optional')
146
156
  parameter.description = 'May be left out, with its segment.'
@@ -172,8 +182,11 @@ function openApiOf(handlers: Map<string, Mounted>): {
172
182
  },
173
183
  },
174
184
  }
175
- // OpenAPI spells a parameter `{id}` where a route spells it `[id]`.
176
- const path = each.address.replace(/\[+(?:\.\.\.)?([^\]]+)\]+/g, '{$1}')
185
+ // OpenAPI spells a parameter `{id}` where a route spells it `[id]`, and names no matcher.
186
+ const path = each.address.replace(
187
+ /\[+(?:\.\.\.)?([^\]=]+)(?:=[^\]]*)?\]+/g,
188
+ '{$1}',
189
+ )
177
190
  paths[path] ??= {}
178
191
  ;(paths[path] as Record<string, unknown>)[each.method.toLowerCase()] =
179
192
  operation
@@ -291,7 +304,9 @@ export class McpSurface {
291
304
  listing.push({
292
305
  name: tool.name,
293
306
  description: tool.description ?? '',
294
- inputSchema: toolInput(tool.schema),
307
+ inputSchema: toolInput(
308
+ withPathPatterns(tool.schema, tool.address, tool.matchers),
309
+ ),
295
310
  // 20.14 — what the method promises: a `GET` is safe, and a `GET`, a `PUT` and a
296
311
  // `DELETE` idempotent. Any other write may destroy, and nothing says it does not.
297
312
  annotations: {
@@ -454,6 +469,63 @@ function logCall(
454
469
  )
455
470
  }
456
471
 
472
+ /** 20.23 — the anchored pattern a segment takes a part by: its matcher's, or 30.34's default. */
473
+ function patternOf(segment: Segment, matchers: Record<string, string>): string {
474
+ return segment.matcher === undefined
475
+ ? PERIODLESS_PATTERN
476
+ : (matchers[segment.matcher] ?? PERIODLESS_PATTERN)
477
+ }
478
+
479
+ const PERIODLESS_PATTERN = '^[^.]+$'
480
+
481
+ /** `declared` constrained to `pattern` too: beside a pattern it declares itself, not over it. */
482
+ function patterned(declared: unknown, pattern: string): unknown {
483
+ if (declared === null || typeof declared !== 'object') return { pattern }
484
+ return 'pattern' in declared
485
+ ? { allOf: [declared, { pattern }] }
486
+ : { ...declared, pattern }
487
+ }
488
+
489
+ /**
490
+ * 20.23 — `schema` with each route parameter it declares constrained to what its segment takes, a
491
+ * rest's on each of its items; `schema` itself where the route has no parameter it declares.
492
+ */
493
+ function withPathPatterns(
494
+ schema: unknown,
495
+ address: string,
496
+ matchers: Record<string, string>,
497
+ ): unknown {
498
+ if (schema === null || typeof schema !== 'object') return schema
499
+ const properties = (schema as { properties?: Record<string, unknown> })
500
+ .properties
501
+ if (properties === undefined) return schema
502
+ const constrained: Record<string, unknown> = { ...properties }
503
+ let changed = false
504
+ for (const segment of parseRoutePattern(address)) {
505
+ if (segment.kind === 'static') continue
506
+ const declared = properties[segment.name]
507
+ if (declared === undefined) continue
508
+ const pattern = patternOf(segment, matchers)
509
+ constrained[segment.name] =
510
+ segment.kind === 'rest' &&
511
+ declared !== null &&
512
+ typeof declared === 'object' &&
513
+ 'items' in declared
514
+ ? {
515
+ ...declared,
516
+ items: patterned(
517
+ (declared as { items: unknown }).items,
518
+ pattern,
519
+ ),
520
+ }
521
+ : segment.kind === 'rest'
522
+ ? declared
523
+ : patterned(declared, pattern)
524
+ changed = true
525
+ }
526
+ return changed ? { ...schema, properties: constrained } : schema
527
+ }
528
+
457
529
  /**
458
530
  * `schema` as a tool's `inputSchema`, its top-level `type` `object` whatever it declared there, or
459
531
  * `undefined` where no value it admits is an object. 20.19, 20.20.
@@ -21,6 +21,7 @@ import {
21
21
  type Segment,
22
22
  } from '#shared/routePattern.ts'
23
23
  import type { Schema } from '#shared/Schema.ts'
24
+
24
25
  import { config } from './config.ts'
25
26
  import type { LauncherOptions } from './launcherOptions.ts'
26
27
  import { currentRequestScope } from './requestScope.ts'
@@ -82,7 +82,7 @@ function decoded(
82
82
  let channels = 0
83
83
  const compressed: Uint8Array[] = []
84
84
  let length = 0
85
- for (let at = 8; at + 8 <= png.length; ) {
85
+ for (let at = 8; at + 8 <= png.length;) {
86
86
  const size = view.getUint32(at)
87
87
  const type = String.fromCharCode(...png.subarray(at + 4, at + 8))
88
88
  const body = png.subarray(at + 8, at + 8 + size)
@@ -14,6 +14,7 @@ import { MOUNT } from '#shared/mount.ts'
14
14
  import { PRINCIPAL } from '#shared/principal.ts'
15
15
  import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
16
16
  import { UNEXPECTED } from '#shared/UNEXPECTED.ts'
17
+
17
18
  import { reachedInRender } from './answerRequest.ts'
18
19
  import { environmentTimeout } from './answerRpc.ts'
19
20
  import { broadcast, feedFromServed, swept } from './answerSocket.ts'
@@ -7,6 +7,7 @@
7
7
  // rebuild, and a test boots several apps in one process.
8
8
 
9
9
  import { resolve } from 'node:path' // bun exposes no path helpers of its own
10
+
10
11
  import { HANDLER_FACE } from './MODULE_QUERIES.ts'
11
12
  import { decodeMappings, placeAt, type Segment } from './sourceMappings.ts'
12
13
 
@@ -7,6 +7,7 @@
7
7
  // asks something first, so a Start with no configuration to ask for has none and starts at once.
8
8
 
9
9
  import { escapeAttribute, escapeText } from '#shared/escapeText.ts'
10
+
10
11
  import type { ConfigField } from './configFields.ts'
11
12
  import { inkOn } from './iconAccent.ts'
12
13
 
@@ -77,8 +78,7 @@ export function launcherPage(page: Launch): string {
77
78
  // field shows them.
78
79
  const failureLine = (
79
80
  failure:
80
- | { message: string; issues: Record<string, string[]> }
81
- | undefined,
81
+ { message: string; issues: Record<string, string[]> } | undefined,
82
82
  ): string => {
83
83
  if (failure === undefined) return ''
84
84
  let line = `<p class="failure" role="alert">${escapeText(failure.message)}</p>`
@@ -16,6 +16,7 @@
16
16
  // a preload of the same address is a second request line for bytes already on their way.
17
17
 
18
18
  import { resolve } from 'node:path' // bun exposes no path helpers of its own
19
+
19
20
  import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
20
21
  import { VIRTUAL_DIRECTORY } from './VIRTUAL_DIRECTORY.ts'
21
22
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { SOCKET_ADDRESS } from '#shared/ADDRESSES.ts'
12
12
  import { unmounted } from '#shared/mount.ts'
13
+
13
14
  import { positiveSetting } from './config.ts'
14
15
  import { plainAnswer } from './plainAnswer.ts'
15
16
 
@@ -2,7 +2,9 @@
2
2
  // with `onMenu`, built as an `NSMenu`.
3
3
 
4
4
  import { FFIType } from 'bun:ffi'
5
+
5
6
  import type { MenuRole } from '#shared/menuItem.ts'
7
+
6
8
  import {
7
9
  definedClass,
8
10
  type Id,
@@ -104,6 +104,16 @@ export function isServerFile(path: string): boolean {
104
104
 
105
105
  const SERVER_FILE = /[^/]\.server\.[^./]+$/
106
106
 
107
+ /** 16.2 — whether `path` names a handler file, `<name>.handler.ts`, whose default export answers at its route. */
108
+ export function isHandlerFile(path: string): boolean {
109
+ return path.endsWith('.handler.ts')
110
+ }
111
+
112
+ /** 23.24 — whether `path` names a param file, `<NAME>.param.ts`, declaring the matcher `NAME`. */
113
+ export function isParamFile(path: string): boolean {
114
+ return path.endsWith('.param.ts')
115
+ }
116
+
107
117
  /** Whether `path` names a test file, which is never an address nor a module the app runs. */
108
118
  export function isTestFile(path: string): boolean {
109
119
  return path.endsWith('.test.ts')
@@ -127,7 +137,7 @@ export function skipped(name: string): boolean {
127
137
  /**
128
138
  * The route a file answers at — its path below the root without its extension.
129
139
  *
130
- * 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
140
+ * 23.14 — `media/[id].abide` and `media/getGrid.handler.ts` are `/media/[id]` and `/media/getGrid`; an
131
141
  * `index.abide`, a `layout.abide` and an `error.abide` are their directory's.
132
142
  */
133
143
  export function routeOf(path: string): string {
@@ -138,7 +148,7 @@ export function routeOf(path: string): string {
138
148
  return `/${parts.filter((part) => part !== '').join('/')}`
139
149
  }
140
150
 
141
- const EXTENSION = /\.(?:abide|ts)$/
151
+ const EXTENSION = /\.(?:abide|handler\.ts)$/
142
152
 
143
153
  /** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
144
154
  export const DIRECTORY_FILES = new Set([
@@ -9,14 +9,15 @@ import { cookies } from '#shared/cookies.ts'
9
9
  import { isFailed } from '#shared/Failed.ts'
10
10
  import { isRecord } from '#shared/isRecord.ts'
11
11
  import { namedLogger } from '#shared/log.ts'
12
- import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
13
12
  import {
14
13
  type PRINCIPAL,
15
14
  type Principal,
16
15
  principalMembers,
17
16
  } from '#shared/principal.ts'
17
+ import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
18
18
  import { refuse } from '#shared/refuse.ts'
19
19
  import { declare } from '#shared/rpc.ts'
20
+
20
21
  import { mountedOwn } from './answerRpc.ts'
21
22
  import { signedInAt } from './bridgeRelay.ts'
22
23
  import { callerOf, callerOrMint, mintCaller } from './callerOf.ts'