@abide/abide 0.42.0 → 0.43.1

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 (146) hide show
  1. package/AGENTS.md +234 -300
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +50 -93
  4. package/package.json +3 -2
  5. package/src/abideResolverPlugin.ts +104 -14
  6. package/src/buildCli.ts +1 -1
  7. package/src/discoveryEntry.ts +3 -3
  8. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  9. package/src/lib/cli/createClient.ts +12 -12
  10. package/src/lib/cli/dispatchCommand.ts +1 -1
  11. package/src/lib/cli/printSessionStatus.ts +1 -1
  12. package/src/lib/cli/resolveCliTarget.ts +1 -1
  13. package/src/lib/cli/runCli.ts +1 -1
  14. package/src/lib/cli/types/CliManifest.ts +1 -1
  15. package/src/lib/cli/types/CliManifestEntry.ts +2 -2
  16. package/src/lib/mcp/annotationsForMethod.ts +5 -5
  17. package/src/lib/mcp/createMcpServer.ts +3 -3
  18. package/src/lib/mcp/mcpSurface.ts +14 -14
  19. package/src/lib/mcp/types/McpServerOptions.ts +1 -1
  20. package/src/lib/server/DELETE.ts +4 -4
  21. package/src/lib/server/GET.ts +4 -4
  22. package/src/lib/server/HEAD.ts +4 -4
  23. package/src/lib/server/PATCH.ts +4 -4
  24. package/src/lib/server/POST.ts +4 -4
  25. package/src/lib/server/PUT.ts +4 -4
  26. package/src/lib/server/agent.ts +4 -4
  27. package/src/lib/server/env.ts +1 -1
  28. package/src/lib/server/error.ts +49 -7
  29. package/src/lib/server/json.ts +1 -1
  30. package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
  31. package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
  32. package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
  33. package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
  34. package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
  35. package/src/lib/server/rpc/parseArgs.ts +6 -6
  36. package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
  37. package/src/lib/server/rpc/registerRpc.ts +6 -0
  38. package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
  39. package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
  40. package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
  41. package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
  42. package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
  43. package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
  44. package/src/lib/server/rpc/unprocessed.ts +6 -7
  45. package/src/lib/server/rpc/validationError.ts +17 -0
  46. package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
  47. package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
  48. package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
  49. package/src/lib/server/runtime/createServer.ts +4 -4
  50. package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
  51. package/src/lib/server/runtime/crossOriginGate.ts +4 -4
  52. package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
  53. package/src/lib/server/runtime/registryManifests.ts +2 -2
  54. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  55. package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
  56. package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
  57. package/src/lib/server/runtime/types/RequestStore.ts +1 -1
  58. package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
  59. package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
  60. package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
  61. package/src/lib/shared/HttpError.ts +15 -1
  62. package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
  63. package/src/lib/shared/buildRpcRequest.ts +5 -5
  64. package/src/lib/shared/cache.ts +50 -168
  65. package/src/lib/shared/carriesBodyArgs.ts +4 -4
  66. package/src/lib/shared/createCacheStore.ts +12 -0
  67. package/src/lib/shared/createRemoteFunction.ts +11 -6
  68. package/src/lib/shared/decodeResponse.ts +2 -2
  69. package/src/lib/shared/detectRpcMethod.ts +17 -0
  70. package/src/lib/shared/emitLogRecord.ts +3 -3
  71. package/src/lib/shared/extraForwardHeaders.ts +1 -1
  72. package/src/lib/shared/findExportCallSite.ts +30 -273
  73. package/src/lib/shared/forwardHeaders.ts +2 -2
  74. package/src/lib/shared/httpErrorFor.ts +26 -0
  75. package/src/lib/shared/isAsciiWhitespace.ts +5 -0
  76. package/src/lib/shared/isIdentPart.ts +9 -0
  77. package/src/lib/shared/isIdentStart.ts +8 -0
  78. package/src/lib/shared/isReadOnlyMethod.ts +4 -4
  79. package/src/lib/shared/keyForRemoteCall.ts +4 -4
  80. package/src/lib/shared/log.ts +1 -1
  81. package/src/lib/shared/outboxProbeSlot.ts +20 -0
  82. package/src/lib/shared/pending.ts +4 -1
  83. package/src/lib/shared/prepareRpcModule.ts +96 -14
  84. package/src/lib/shared/probeRegistries.ts +17 -4
  85. package/src/lib/shared/queryStringFromArgs.ts +1 -1
  86. package/src/lib/shared/remoteMetaStore.ts +2 -2
  87. package/src/lib/shared/resolveClientFlags.ts +1 -1
  88. package/src/lib/shared/skipNonCode.ts +230 -0
  89. package/src/lib/shared/streamResponse.ts +9 -6
  90. package/src/lib/shared/stripImport.ts +3 -3
  91. package/src/lib/shared/types/CacheEntry.ts +2 -4
  92. package/src/lib/shared/types/CacheOnContext.ts +1 -11
  93. package/src/lib/shared/types/CacheStore.ts +13 -7
  94. package/src/lib/shared/types/ClientFlags.ts +1 -1
  95. package/src/lib/shared/types/ErrorConstructors.ts +17 -0
  96. package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
  97. package/src/lib/shared/types/ErrorSpec.ts +11 -0
  98. package/src/lib/shared/types/HttpMethod.ts +1 -0
  99. package/src/lib/shared/types/Outbox.ts +9 -0
  100. package/src/lib/shared/types/OutboxEntry.ts +27 -0
  101. package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
  102. package/src/lib/shared/types/RemoteCallable.ts +2 -2
  103. package/src/lib/shared/types/RemoteFunction.ts +21 -6
  104. package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
  105. package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
  106. package/src/lib/shared/types/RpcInvoker.ts +1 -1
  107. package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
  108. package/src/lib/shared/types/ValidationErrorData.ts +20 -0
  109. package/src/lib/shared/url.ts +3 -3
  110. package/src/lib/shared/writeRpcDts.ts +7 -7
  111. package/src/lib/shared/writeTestRpcDts.ts +3 -3
  112. package/src/lib/test/createTestApp.ts +10 -10
  113. package/src/lib/ui/compile/awaitPlan.ts +48 -0
  114. package/src/lib/ui/compile/compileShadow.ts +89 -21
  115. package/src/lib/ui/compile/desugarSignals.ts +42 -6
  116. package/src/lib/ui/compile/generateBuild.ts +43 -60
  117. package/src/lib/ui/compile/generateSSR.ts +84 -54
  118. package/src/lib/ui/compile/ifPlan.ts +30 -0
  119. package/src/lib/ui/compile/parseTemplate.ts +41 -11
  120. package/src/lib/ui/compile/stripEffects.ts +17 -9
  121. package/src/lib/ui/compile/switchPlan.ts +22 -0
  122. package/src/lib/ui/compile/tryPlan.ts +29 -0
  123. package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
  124. package/src/lib/ui/compile/types/TemplateNode.ts +6 -2
  125. package/src/lib/ui/createScope.ts +20 -2
  126. package/src/lib/ui/dom/disposeRange.ts +6 -10
  127. package/src/lib/ui/dom/hydrate.ts +2 -5
  128. package/src/lib/ui/dom/mount.ts +1 -2
  129. package/src/lib/ui/dom/withScope.ts +6 -9
  130. package/src/lib/ui/effect.ts +4 -2
  131. package/src/lib/ui/navigate.ts +40 -13
  132. package/src/lib/ui/outbox.ts +30 -110
  133. package/src/lib/ui/remoteProxy.ts +160 -10
  134. package/src/lib/ui/router.ts +45 -15
  135. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
  136. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
  137. package/src/lib/ui/socketChannel.ts +7 -1
  138. package/src/lib/ui/types/Scope.ts +20 -5
  139. package/template/CLAUDE.md +1 -1
  140. package/template/src/server/rpc/getHello.ts +3 -3
  141. package/template/test/app.test.ts +2 -2
  142. package/src/lib/server/rpc/registerVerb.ts +0 -6
  143. package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
  144. package/src/lib/shared/detectVerbMethod.ts +0 -17
  145. package/src/lib/shared/types/HttpVerb.ts +0 -1
  146. package/src/lib/ui/types/Outbox.ts +0 -14
@@ -0,0 +1,11 @@
1
+ import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
2
+
3
+ /*
4
+ A rpc's declared error set, keyed by error NAME (not status, so two errors can
5
+ share a status). Each entry names its HTTP `status` and an optional `data`
6
+ schema whose inferred input the error constructor requires. Passed as the rpc's
7
+ `errors` opt; the client derives a typed `Result` union from it (see
8
+ `RpcErrorUnion`), and the handler receives matching constructors (see
9
+ `ErrorConstructors`).
10
+ */
11
+ export type ErrorSpec = Record<string, { status: number; data?: StandardSchemaV1 }>
@@ -0,0 +1 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'
@@ -0,0 +1,9 @@
1
+ import type { OutboxEntry } from './OutboxEntry.ts'
2
+
3
+ /*
4
+ The `.outbox` face on a durable RPC's client proxy: callable to read the reactive list
5
+ of undelivered entries (`rpc.outbox()`), with an awaitable `retry()` that drains the whole
6
+ queue and resolves when the replay settles. Server-side there is no client queue, so the
7
+ face is absent.
8
+ */
9
+ export type Outbox<Args> = (() => OutboxEntry<Args>[]) & { retry: () => Promise<void> }
@@ -0,0 +1,27 @@
1
+ /* The lifecycle status of a parked mutation: waiting to (re)send, or currently sending.
2
+ A write lives in the outbox ONLY while it can't reach the server; the instant the server
3
+ responds at all — a 2xx OR a real 4xx/500 — the entry leaves the queue, so there is no
4
+ terminal `error` state. Only the unreachable family (transport failure / 502/503/504/52x)
5
+ keeps it `queued`. */
6
+ export type OutboxStatus = 'queued' | 'sending'
7
+
8
+ /* One durable, replayable mutation in an RPC's outbox — parked because the original
9
+ call couldn't reach the server (transport failure, or a 502/503/504/52x). `controller`
10
+ is the entry's own abort handle (cancel = `controller.abort()`); `request` is the
11
+ synthesized, persisted Request the drain re-sends; `args` is the typed input (for
12
+ rendering); `error` is why it's parked (the `kind: 'queued'` HttpError from the
13
+ unreachable attempt); `retry()` kicks a FIFO drain. `settled` is the eventual outcome
14
+ of THIS write as if the original call had reached the server — it resolves with the
15
+ decoded result on delivery and rejects with the real HttpError on a server refusal (or
16
+ an AbortError on cancel). It may stay pending indefinitely if the write is never
17
+ replayed; reading it is what arms it (a never-read `settled` never rejects). */
18
+ export type OutboxEntry<Args> = {
19
+ id: string
20
+ controller: AbortController
21
+ request: Request
22
+ args: Args
23
+ status: OutboxStatus
24
+ error?: unknown
25
+ retry: () => Promise<void>
26
+ settled: Promise<unknown>
27
+ }
@@ -1,4 +1,4 @@
1
- import type { HttpVerb } from './HttpVerb.ts'
1
+ import type { HttpMethod } from './HttpMethod.ts'
2
2
  import type { RemoteCallable } from './RemoteCallable.ts'
3
3
 
4
4
  /*
@@ -9,6 +9,6 @@ so callers that need status / headers / body streaming or want to
9
9
  implement custom error handling can opt out of the decode.
10
10
  */
11
11
  export type RawRemoteFunction<Args> = RemoteCallable<Args, Response> & {
12
- readonly method: HttpVerb
12
+ readonly method: HttpMethod
13
13
  readonly url: string
14
14
  }
@@ -2,8 +2,8 @@ import type { RpcOptions } from './RpcOptions.ts'
2
2
 
3
3
  /*
4
4
  Call signature shared by RemoteFunction and RawRemoteFunction. The base
5
- signature keeps `args` required so a schema'd verb can't silently drop its
6
- input; when `Args` admits undefined (no-input verbs) an intersected
5
+ signature keeps `args` required so a schema'd rpc can't silently drop its
6
+ input; when `Args` admits undefined (no-input rpcs) an intersected
7
7
  optional-arg signature lets call sites write `fn()` instead of
8
8
  `fn(undefined)`. Intersection rather than a bare conditional so the type
9
9
  stays callable while `Args` is still generic (cache() invokes producers
@@ -1,7 +1,10 @@
1
1
  import type { ClientFlags } from './ClientFlags.ts'
2
- import type { HttpVerb } from './HttpVerb.ts'
2
+ import type { ErrorSpec } from './ErrorSpec.ts'
3
+ import type { HttpMethod } from './HttpMethod.ts'
4
+ import type { Outbox } from './Outbox.ts'
3
5
  import type { RawRemoteFunction } from './RawRemoteFunction.ts'
4
6
  import type { RemoteCallable } from './RemoteCallable.ts'
7
+ import type { RpcErrorGuard } from './RpcErrorGuard.ts'
5
8
  import type { Subscribable } from './Subscribable.ts'
6
9
 
7
10
  /*
@@ -25,23 +28,35 @@ HTTP rpc isn't the place for long-lived multi-publisher subscriptions.
25
28
  `.fetch(req)` is the framework's request-dispatch entry point — used by
26
29
  the router to invoke the handler from an incoming HTTP request, not
27
30
  for user code.
28
- `crossOrigin` (server-side only, set via the verb's opts) exempts a
29
- mutating verb from the router's same-origin CSRF gate, accepting browser
31
+ `crossOrigin` (server-side only, set via the rpc's opts) exempts a
32
+ mutating rpc from the router's same-origin CSRF gate, accepting browser
30
33
  requests whose Origin doesn't match the app's own host.
31
34
  */
32
35
  /*
33
- A body verb (POST/PUT/PATCH) also accepts a FormData in place of typed args:
36
+ A body rpc (POST/PUT/PATCH) also accepts a FormData in place of typed args:
34
37
  buildRpcRequest ships it as a multipart body and the server splits text fields
35
38
  into args (still schema-validated) and File parts into files(). FormData is
36
39
  stringly-typed, so this is the upload escape hatch — typed object args remain
37
40
  the default for everything else.
38
41
  */
39
- export type RemoteFunction<Args, Return> = RemoteCallable<Args, Return> & {
40
- readonly method: HttpVerb
42
+ export type RemoteFunction<
43
+ Args,
44
+ Return,
45
+ Errors extends ErrorSpec = Record<never, never>,
46
+ > = RemoteCallable<Args, Return> & {
47
+ readonly method: HttpMethod
41
48
  readonly url: string
42
49
  readonly clients: ClientFlags
43
50
  readonly crossOrigin?: boolean
44
51
  readonly raw: RawRemoteFunction<Args>
45
52
  stream(args?: Args | FormData): Subscribable<Return>
46
53
  fetch(request: Request): Promise<Response>
54
+ /* Type-guard a caught error against this rpc's declared `errors` (plus the framework
55
+ `'validation'` / `'queued'`): narrows `.kind` and, for a known kind, `.data` — the
56
+ per-rpc replacement for a global guard, since the error name → data type mapping
57
+ lives in the rpc's own spec. */
58
+ readonly isError: RpcErrorGuard<Errors>
59
+ /* Present only on a durable (`outbox`) RPC's client proxy: callable for the reactive
60
+ list of undelivered entries, with `retry()` to drain the queue. Undefined otherwise. */
61
+ readonly outbox?: Outbox<Args>
47
62
  }
@@ -3,7 +3,7 @@ import type { TraceContext } from './TraceContext.ts'
3
3
  /*
4
4
  The request-scope facts the shared layer reads through requestScopeSlot:
5
5
  trace position, elapsed ms since the scope opened (request start on the
6
- server, navigation start in the browser), and the verb+path that anchors log
6
+ server, navigation start in the browser), and the method+path that anchors log
7
7
  lines. Resolved fresh per read so `elapsedMs` is current at the call.
8
8
  */
9
9
  export type RequestScopeInfo = {
@@ -0,0 +1,40 @@
1
+ import type { HttpError } from '../HttpError.ts'
2
+ import type { ErrorSpec } from './ErrorSpec.ts'
3
+ import type { OutboxEntry } from './OutboxEntry.ts'
4
+ import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
5
+ import type { ValidationErrorData } from './ValidationErrorData.ts'
6
+
7
+ /* The payload a declared error name carries: its data schema's inferred input, or
8
+ `unknown` for a nullary error (no data schema). */
9
+ type DeclaredErrorData<
10
+ Errors extends ErrorSpec,
11
+ Name extends keyof Errors,
12
+ > = Errors[Name]['data'] extends StandardSchemaV1
13
+ ? StandardSchemaV1.InferInput<Errors[Name]['data']>
14
+ : unknown
15
+
16
+ /*
17
+ A rpc's `isError` guard — branch a caught error on a kind THIS rpc knows, with the
18
+ data type narrowed from the rpc's own `errors` spec. `rpc.isError(err, 'outOfStock')`
19
+ narrows `.data` to that error's payload; the framework-reserved `'validation'` /
20
+ `'queued'` to their fixed shapes; any other string narrows `.kind` only (`.data`
21
+ stays `unknown` — it belongs to a different rpc, whose type isn't in scope here).
22
+
23
+ The declared-name overload comes first so an app error shadows the reserved ones on a
24
+ name collision, and so the kind argument autocompletes to the rpc's declared names.
25
+ */
26
+ export interface RpcErrorGuard<Errors extends ErrorSpec> {
27
+ <Name extends keyof Errors & string>(
28
+ error: unknown,
29
+ kind: Name,
30
+ ): error is HttpError & { kind: Name; data: DeclaredErrorData<Errors, Name> }
31
+ (
32
+ error: unknown,
33
+ kind: 'validation',
34
+ ): error is HttpError & { kind: 'validation'; data: ValidationErrorData }
35
+ (
36
+ error: unknown,
37
+ kind: 'queued',
38
+ ): error is HttpError & { kind: 'queued'; data: OutboxEntry<unknown> }
39
+ <Kind extends string>(error: unknown, kind: Kind): error is HttpError & { kind: Kind }
40
+ }
@@ -1,4 +1,4 @@
1
- /* A verb invoker mirrors the verb's own call: a plain call decodes the body
1
+ /* A rpc invoker mirrors the rpc's own call: a plain call decodes the body
2
2
  (throws HttpError on non-2xx), `.raw` returns the Response untouched. Shared
3
3
  by every name→callable RPC proxy (the CLI client, the test harness). */
4
4
  export type RpcInvoker = ((args?: unknown) => Promise<unknown>) & {
@@ -2,7 +2,7 @@
2
2
  Mirror of the Standard Schema v1 spec interface (standardschema.dev). Any
3
3
  library that implements the spec — zod, valibot, arktype, etc. — produces
4
4
  values whose `~standard` property structurally matches this shape, so users
5
- can pass their existing schemas to verb helpers without an adapter.
5
+ can pass their existing schemas to rpc helpers without an adapter.
6
6
 
7
7
  Kept inline (no `@standard-schema/spec` dep) because the spec is type-only
8
8
  and tiny; adding a package for ~30 lines of interface would be churn. The
@@ -0,0 +1,20 @@
1
+ import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
2
+
3
+ /*
4
+ The `data` payload an input/file validation failure carries on its HttpError —
5
+ `kind: 'validation'`, status 422. `issues` is the raw Standard Schema issue list
6
+ (every message + full path); `fields` is the form-friendly top-level-field →
7
+ first-message map (fieldErrorsFromIssues). Because a throw can't carry the rpc's
8
+ per-kind type to the catch, `HttpError.data` is typed `unknown` — narrow it with
9
+ this when `err.kind === 'validation'`:
10
+
11
+ if (err instanceof HttpError && err.kind === 'validation') {
12
+ const { fields } = err.data as ValidationErrorData
13
+ // fields.email === 'Required', …
14
+ }
15
+ */
16
+ // @documentation response
17
+ export type ValidationErrorData = {
18
+ readonly issues: readonly StandardSchemaV1.Issue[]
19
+ readonly fields: Record<string, string>
20
+ }
@@ -11,7 +11,7 @@ type Query = Record<string, QueryValue>
11
11
  /*
12
12
  Augmentable rpc map. Codegen emits a `declare module '<importName>/shared/url'`
13
13
  block filling this with `'/rpc/<file>': args` for each query-carrying (GET)
14
- verb, so url('/rpc/search', { q }) types its args against the verb's own
14
+ rpc, so url('/rpc/search', { q }) types its args against the rpc's own
15
15
  signature. Empty by default — an absent entry falls through to the page/asset
16
16
  branch, so the helper works before the generated d.ts lands.
17
17
  */
@@ -47,7 +47,7 @@ no bracket segments yields {}, which collapses the params slot away. The
47
47
  catch-all branch recurses on its head too, so `[name]` segments before a
48
48
  `[...rest]` are kept.
49
49
  */
50
- type PathParams<P extends string> = P extends `${infer Head}[...${infer Rest}]${infer Tail}`
50
+ export type PathParams<P extends string> = P extends `${infer Head}[...${infer Rest}]${infer Tail}`
51
51
  ? PathParams<Head> & { [K in Rest]: string | number } & PathParams<Tail>
52
52
  : P extends `${string}[${infer Name}]${infer Tail}`
53
53
  ? { [K in Name]: string | number } & PathParams<Tail>
@@ -59,7 +59,7 @@ Resolves any in-app URL to its base-correct, typed form — the single chokepoin
59
59
  so a project mounted under APP_URL's subpath (e.g. /v2) generates links, asset
60
60
  refs, and rpc hrefs that stay within the mount. Three disjoint URL kinds, keyed
61
61
  off the path:
62
- - rpc (flat /rpc/*, present in RpcRoutes): the verb's args, serialised to query.
62
+ - rpc (flat /rpc/*, present in RpcRoutes): the rpc's args, serialised to query.
63
63
  - page route (has [name] segments): path params, then optional query.
64
64
  - asset / paramless / raw: bare path, then optional query.
65
65
  External URLs (scheme-qualified or protocol-relative) skip the base untouched,
@@ -1,15 +1,15 @@
1
1
  import { carriesBodyArgs } from './carriesBodyArgs.ts'
2
- import { detectVerbMethod } from './detectVerbMethod.ts'
2
+ import { detectRpcMethod } from './detectRpcMethod.ts'
3
3
  import { fileStem } from './fileStem.ts'
4
4
  import { rpcUrlForFile } from './rpcUrlForFile.ts'
5
5
  import { writeDts } from './writeDts.ts'
6
6
 
7
7
  /*
8
8
  Emits a `.d.ts` that augments abide's `RpcRoutes` interface with one entry per
9
- query-carrying $rpc verb, so `url('/rpc/search', { q })` types its args against
10
- the verb's own signature. Only GET/DELETE/HEAD (non-body) verbs are included —
11
- a url() can carry a query string but not a request body, so a POST verb has no
12
- URL form. `RpcArgs` lifts the args type out of the verb's RemoteFunction
9
+ query-carrying $rpc rpc, so `url('/rpc/search', { q })` types its args against
10
+ the rpc's own signature. Only GET/DELETE/HEAD (non-body) rpcs are included —
11
+ a url() can carry a query string but not a request body, so a POST rpc has no
12
+ URL form. `RpcArgs` lifts the args type out of the rpc's RemoteFunction
13
13
  (dropping the FormData upload variant); the file path resolves the export by
14
14
  its filename, the abide one-export-per-file convention. Written to
15
15
  `src/.abide/rpc.d.ts` so the consumer's src tsconfig include picks it up, keyed
@@ -28,8 +28,8 @@ export async function writeRpcDts({
28
28
  }): Promise<void> {
29
29
  const lines = await Promise.all(
30
30
  rpcFiles.map(async (file) => {
31
- const method = detectVerbMethod(await Bun.file(`${rpcDir}/${file}`).text())
32
- // A body verb's args can't ride a URL — leave it out of the url() rpc map.
31
+ const method = detectRpcMethod(await Bun.file(`${rpcDir}/${file}`).text())
32
+ // A body rpc's args can't ride a URL — leave it out of the url() rpc map.
33
33
  if (!method || carriesBodyArgs(method)) {
34
34
  return undefined
35
35
  }
@@ -5,9 +5,9 @@ import { writeDts } from './writeDts.ts'
5
5
 
6
6
  /*
7
7
  Emits a `.d.ts` that augments createTestApp's `RpcClient` interface with one
8
- entry per $rpc verb, keyed by command name (the same key `app.rpc.<name>`
9
- resolves at runtime). Each entry lifts the verb's args + resolved return out of
10
- its RemoteFunction so `app.rpc.getProduct({ id })` types against the verb's own
8
+ entry per $rpc rpc, keyed by command name (the same key `app.rpc.<name>`
9
+ resolves at runtime). Each entry lifts the rpc's args + resolved return out of
10
+ its RemoteFunction so `app.rpc.getProduct({ id })` types against the rpc's own
11
11
  signature — args in, decoded body out, plus `.raw` for the Response. `RpcArgs`
12
12
  drops the FormData upload variant exactly as writeRpcDts does; `RpcReturn`
13
13
  reads the resolved body type. Written to `src/.abide/testRpc.d.ts` so the
@@ -25,7 +25,7 @@ import { rpc } from '../../_virtual/rpc.ts'
25
25
  import { shell } from '../../_virtual/shell.ts'
26
26
  // @ts-expect-error virtual module resolved by abideResolverPlugin
27
27
  import { sockets } from '../../_virtual/sockets.ts'
28
- import { verbRegistry } from '../server/rpc/verbRegistry.ts'
28
+ import { rpcRegistry } from '../server/rpc/rpcRegistry.ts'
29
29
  import { createServer } from '../server/runtime/createServer.ts'
30
30
  import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
31
31
  import { requestContext } from '../server/runtime/requestContext.ts'
@@ -46,8 +46,8 @@ import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
46
46
  import { createTestSocketChannel } from './createTestSocketChannel.ts'
47
47
 
48
48
  /*
49
- Augmentable verb/socket maps for `app.rpc.<verb>` / `app.sockets.<name>`. The
50
- build's writeTestRpcDts / writeTestSocketsDts emit one entry per verb/socket
49
+ Augmentable rpc/socket maps for `app.rpc.<rpc>` / `app.sockets.<name>`. The
50
+ build's writeTestRpcDts / writeTestSocketsDts emit one entry per rpc/socket
51
51
  (into src/.abide/), so the keys + signatures are the project's real surface
52
52
  with no imports. Empty here; types arrive once the app has been built. Mirrors
53
53
  url's RpcRoutes / health's AppHealthMap.
@@ -58,7 +58,7 @@ export interface RpcClient {}
58
58
  // biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testSockets.d.ts
59
59
  export interface SocketClient {}
60
60
 
61
- /* The booted app under test. Every named subsystem is reachable as the verb
61
+ /* The booted app under test. Every named subsystem is reachable as the rpc
62
62
  you call, the socket you iterate, or a path you fetch — over the real
63
63
  server, so the full pipeline (CSRF, cookies, base path) runs. */
64
64
  export type TestApp = {
@@ -69,7 +69,7 @@ export type TestApp = {
69
69
  matches routes at raw paths — the APP_URL mount base is stripped by an
70
70
  external proxy in production, absent here — so paths carry no base. */
71
71
  fetch: (path: string, init?: RequestInit) => Promise<Response>
72
- /* Verb calls over HTTP, keyed by command name: `app.rpc.getProduct({ id })`. */
72
+ /* Rpc calls over HTTP, keyed by command name: `app.rpc.getProduct({ id })`. */
73
73
  rpc: RpcClient
74
74
  /* Sockets keyed by name: `app.sockets.ticker` is the Socket — iterate it for
75
75
  the live stream, `.tail(n)` to seed, `.publish(m)` to send. */
@@ -88,7 +88,7 @@ export type TestApp = {
88
88
  Boots the real app on an ephemeral port — the same wiring serverEntry performs,
89
89
  minus the standalone-binary env layers. Imports the framework's virtual
90
90
  manifests (resolved by abideResolverPlugin, registered via `abide/preload`
91
- in the consumer's bunfig), so the routes, verbs, and sockets are the project's
91
+ in the consumer's bunfig), so the routes, rpcs, and sockets are the project's
92
92
  real surface, not a fixture. Pass nothing: `await createTestApp()` is the app
93
93
  exactly as `bun start` would serve it.
94
94
 
@@ -138,19 +138,19 @@ export async function createTestApp(): Promise<TestApp> {
138
138
  return fetch(`${origin}${path}`, init)
139
139
  }
140
140
 
141
- /* Verb modules loaded once so the registry holds every RemoteFunction; the
141
+ /* Rpc modules loaded once so the registry holds every RemoteFunction; the
142
142
  proxy maps command name → an HTTP call against the booted server. */
143
143
  await ensureRegistriesLoaded()
144
144
  const remotes = new Map<string, RemoteFunction<unknown, unknown>>(
145
- Array.from(verbRegistry.values()).map((entry) => [
145
+ Array.from(rpcRegistry.values()).map((entry) => [
146
146
  commandNameForUrl(entry.remote.url),
147
147
  entry.remote,
148
148
  ]),
149
149
  )
150
150
 
151
151
  function send(remote: RemoteFunction<unknown, unknown>, args: unknown): Promise<Response> {
152
- /* Same-origin Origin header so the CSRF gate admits mutating verbs; the
153
- server serves verbs at their raw url (no mount base applied here). */
152
+ /* Same-origin Origin header so the CSRF gate admits mutating rpcs; the
153
+ server serves rpcs at their raw url (no mount base applied here). */
154
154
  const request = buildRpcRequest({
155
155
  method: remote.method,
156
156
  url: remote.url,
@@ -0,0 +1,48 @@
1
+ import { resolveBranches } from './resolveBranches.ts'
2
+ import type { TemplateNode } from './types/TemplateNode.ts'
3
+
4
+ /* The structural shape of an `await` block, resolved ONCE from the node so the build and
5
+ SSR back-ends share one reading of it and only own emission. Both previously recomputed
6
+ branch resolution, the `finallyChildren` default, the blocking-vs-streaming split of the
7
+ resolved content/binding, and the surface-the-rejection rule — the parts that can silently
8
+ diverge. Same single-source-of-truth model the positional walks already use. */
9
+ export type AwaitPlan = {
10
+ /* `then` on the tag → blocking: no pending branch, the children ARE the resolved content
11
+ (settled before first flush). Absent → streaming: pending flushes, `then` resolves later. */
12
+ blocking: boolean
13
+ /* The pending content (streaming) — `[]` when blocking. */
14
+ pending: TemplateNode[]
15
+ /* The resolved content + its bound name: the children-minus-branch bound to `node.as`
16
+ when blocking, the `then` branch bound to its `as` when streaming. `_value` default. */
17
+ resolvedChildren: TemplateNode[]
18
+ resolvedAs: string
19
+ /* The catch content + its bound name (`_error` default); empty when no catch branch. */
20
+ catchChildren: TemplateNode[]
21
+ catchAs: string
22
+ finallyChildren: TemplateNode[]
23
+ /* Neither catch nor finally → a rejection must surface (re-throw / `undefined` catch thunk)
24
+ instead of rendering an empty branch. */
25
+ surfaceRejection: boolean
26
+ }
27
+
28
+ /* Resolves an `await` node's branches into the shared structural plan. */
29
+ export function awaitPlan(node: Extract<TemplateNode, { kind: 'await' }>): AwaitPlan {
30
+ const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
31
+ node,
32
+ 'then',
33
+ 'catch',
34
+ 'finally',
35
+ )
36
+ const finallyChildren = finallyBranch?.children ?? []
37
+ const nonBranch = node.children.filter((child) => child.kind !== 'branch')
38
+ return {
39
+ blocking: node.blocking,
40
+ pending: node.blocking ? [] : nonBranch,
41
+ resolvedChildren: node.blocking ? nonBranch : (thenBranch?.children ?? []),
42
+ resolvedAs: (node.blocking ? node.as : thenBranch?.as) ?? '_value',
43
+ catchChildren: catchBranch?.children ?? [],
44
+ catchAs: catchBranch?.as ?? '_error',
45
+ finallyChildren,
46
+ surfaceRejection: catchBranch === undefined && finallyChildren.length === 0,
47
+ }
48
+ }
@@ -282,6 +282,14 @@ function scopeLineFor(
282
282
  propsShapes.push(shape === undefined ? 'Record<string, any>' : verbatim(shape))
283
283
  return { text: `const ${verbatim(declaration)};`, segments: [span(declaration, 6)] }
284
284
  }
285
+ /* The rewrite drops the callee from the line, so hovering `state`/`computed`/
286
+ `linked` at its call site has nothing to resolve. Append the callee as a
287
+ trailing reference statement (`…; state;`), mapped back to its source span,
288
+ so it resolves to the same primitive the destructure does. */
289
+ const withCalleeRef = (line: ScopeLine): ScopeLine => ({
290
+ text: `${line.text} ${verbatim(call.expression)};`,
291
+ segments: [...line.segments, span(call.expression, line.text.length + 1)],
292
+ })
285
293
  if (callee === 'state') {
286
294
  /* state<T>(initial): T is the value type — carry it onto the `let` so an
287
295
  explicit annotation isn't lost to `any`/`any[]` inference of the initial. */
@@ -297,13 +305,16 @@ function scopeLineFor(
297
305
  then correctly flagged possibly-undefined; a guard narrows cleanly. */
298
306
  const valueType = annotation === '' ? ': unknown' : `${annotation} | undefined`
299
307
  /* map the binding name (offset 4, past `let `) so hover/go-to resolve on it */
300
- return { text: `let ${name}!${valueType};`, segments: [span(declaration.name, 4)] }
308
+ return withCalleeRef({
309
+ text: `let ${name}!${valueType};`,
310
+ segments: [span(declaration.name, 4)],
311
+ })
301
312
  }
302
313
  const prefix = `let ${name}${annotation} = (`
303
- return {
314
+ return withCalleeRef({
304
315
  text: `${prefix}${verbatim(init)});`,
305
316
  segments: [span(declaration.name, 4), span(init, prefix.length)],
306
- }
317
+ })
307
318
  }
308
319
  /* computed<T>(compute) / linked<T>(seed) — the only callees left: T is the value
309
320
  type — the call's first arg is a thunk, so invoking it yields the value. Annotate
@@ -317,16 +328,16 @@ function scopeLineFor(
317
328
  /* binding-name map offset = past the keyword + space (`let ` = 4, `const ` = 6) */
318
329
  const keywordOffset = keyword.length + 1
319
330
  if (fn === undefined) {
320
- return {
331
+ return withCalleeRef({
321
332
  text: `${keyword} ${name} = undefined;`,
322
333
  segments: [span(declaration.name, keywordOffset)],
323
- }
334
+ })
324
335
  }
325
336
  const prefix = `${keyword} ${name}${annotation} = (`
326
- return {
337
+ return withCalleeRef({
327
338
  text: `${prefix}${verbatim(fn)})();`,
328
339
  segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
329
- }
340
+ })
330
341
  }
331
342
 
332
343
  /* Emits a sibling list — each node standalone via `emitNode`. */
@@ -359,23 +370,32 @@ function emitNode(node: TemplateNode, builder: Builder): void {
359
370
  emitNodes(node.children, builder)
360
371
  return
361
372
  case 'component': {
362
- /* Check each prop against the child's declared type. The imported tag
363
- resolves (via the shadow host) to the child's `(props: Props) => …`
364
- default, so `Parameters<typeof Child>[0]["name"]` is that prop's type;
365
- assigning the mapped value to it lands a mismatch diagnostic on the
366
- offending expression (an annotated target reports the error on the RHS,
367
- unlike an object literal which reports it on the key). */
368
- for (const prop of node.props) {
369
- /* Lead with a defensive `;`: this IIFE is the one shadow emission that
370
- starts with `(`, so without it a preceding scope statement left
371
- unterminated (a script ending in a call with no trailing semicolon,
372
- e.g. `effect(() => …)`) merges across the newline into `effect(…)(…)`
373
- — a spurious "not callable" on the author's last statement. */
373
+ /* The imported tag resolves (via the shadow host) to the child's
374
+ `(props: Props) => …` default, so `Parameters<typeof Child>[0]` is its prop
375
+ shape. `bind:` / `class:` / `style:` / `attach` and `{...spread}` are
376
+ framework directives (not part of the declared shape), so they are checked
377
+ leniently each value against its own declared key type — never flagged as
378
+ excess. `on*` callbacks ARE ordinary declared props on a component (abide does
379
+ not auto-forward events to the DOM), so they go through the completeness check
380
+ with the rest otherwise a required `onsave`/`oncancel` reads as missing even
381
+ when passed, and an undeclared handler slips by unflagged. */
382
+ const handled = (prop: { name: string; spread?: boolean }): boolean =>
383
+ prop.spread === true ||
384
+ prop.name.startsWith('bind:') ||
385
+ prop.name.startsWith('class:') ||
386
+ prop.name.startsWith('style:') ||
387
+ prop.name === 'attach'
388
+ const hasSpread = node.props.some((prop) => prop.spread)
389
+ for (const prop of node.props.filter(handled)) {
390
+ /* Lead with a defensive `;`: an IIFE/object-literal arg starts with `(` or
391
+ `{`, so without it a preceding scope statement left unterminated (a script
392
+ ending in a call with no trailing semicolon, e.g. `effect(() => …)`)
393
+ merges across the newline into `effect(…)(…)` — a spurious "not callable". */
374
394
  if (prop.spread) {
375
395
  /* A `{...expr}` spread contributes a SUBSET of the props (required ones
376
396
  may come from another spread/explicit prop), so check it against
377
- `Partial<Props>` — every key it does carry must match the child's
378
- declared type, without demanding completeness. */
397
+ `Partial<Props>` — every key it does carry must match, without
398
+ demanding completeness. */
379
399
  builder.raw(`;((__spread: Partial<Parameters<typeof ${node.name}>[0]>) => {})(`)
380
400
  } else {
381
401
  builder.raw(
@@ -385,6 +405,54 @@ function emitNode(node: TemplateNode, builder: Builder): void {
385
405
  builder.expr(prop.code, prop.loc)
386
406
  builder.raw(');\n')
387
407
  }
408
+ /* The plain data props as one object-literal argument typed to the child's whole
409
+ prop shape: a missing required prop errors on the literal (anchored at the tag
410
+ via the mapped `{`), an unknown prop errors on its key, a wrong type on its
411
+ value. Skipped when a spread is present — a spread may supply required props,
412
+ so completeness can't be demanded; the data props fall back to lenient per-key
413
+ checks instead. */
414
+ const dataProps = node.props.filter((prop) => !handled(prop))
415
+ if (hasSpread) {
416
+ for (const prop of dataProps) {
417
+ builder.raw(
418
+ `;((__prop: Parameters<typeof ${node.name}>[0][${JSON.stringify(prop.name)}]) => {})(`,
419
+ )
420
+ builder.expr(prop.code, prop.loc)
421
+ builder.raw(');\n')
422
+ }
423
+ } else {
424
+ builder.raw(`;((__c: Parameters<typeof ${node.name}>[0]): void => { void __c })({`)
425
+ /* A zero-length anchor right after `{`, pointing at the tag: a missing-
426
+ required-prop error spans the literal from `{`, so it overlaps this and
427
+ maps to the tag (an empty span trivially satisfies the source-text ==
428
+ shadow-text invariant). */
429
+ builder.mapped('', node.loc)
430
+ builder.raw('\n')
431
+ for (const prop of dataProps) {
432
+ /* The key mapped to its source name (excess-prop errors land on the key);
433
+ the value verbatim-mapped (wrong-type errors land on the value).
434
+ Hyphenated names (`aria-label`, `data-*`) aren't valid identifiers, so
435
+ they're wrapped in raw quotes — a string-literal key the parser accepts.
436
+ The quotes are unmapped; an excess-prop error on the literal starts at the
437
+ opening quote and overlaps the mapped name, clamping back onto it. */
438
+ const needsQuoting = !/^[A-Za-z_$][\w$]*$/.test(prop.name)
439
+ if (needsQuoting) {
440
+ builder.raw('"')
441
+ }
442
+ if (prop.nameLoc !== undefined) {
443
+ builder.mapped(prop.name, prop.nameLoc)
444
+ } else {
445
+ builder.raw(prop.name)
446
+ }
447
+ if (needsQuoting) {
448
+ builder.raw('"')
449
+ }
450
+ builder.raw(': ')
451
+ builder.expr(prop.code, prop.loc)
452
+ builder.raw(',\n')
453
+ }
454
+ builder.raw('});\n')
455
+ }
388
456
  emitNodes(node.children, builder)
389
457
  return
390
458
  }