@abide/abide 0.43.1 → 0.44.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 (206) hide show
  1. package/AGENTS.md +293 -219
  2. package/CHANGELOG.md +63 -0
  3. package/README.md +191 -67
  4. package/package.json +2 -2
  5. package/src/abideLsp.ts +8 -4
  6. package/src/abideResolverPlugin.ts +17 -2
  7. package/src/build.ts +9 -0
  8. package/src/buildDisconnected.ts +2 -0
  9. package/src/devEntry.ts +44 -9
  10. package/src/lib/bundle/probeAbideServer.ts +1 -1
  11. package/src/lib/cli/parseArgvForRpc.ts +25 -26
  12. package/src/lib/cli/runCli.ts +9 -24
  13. package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
  14. package/src/lib/cli/types/CliManifest.ts +6 -4
  15. package/src/lib/mcp/annotationsForMethod.ts +1 -1
  16. package/src/lib/mcp/toolResultFromResponse.ts +1 -1
  17. package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
  18. package/src/lib/server/AppModule.ts +1 -1
  19. package/src/lib/server/cli/handleCliDownload.ts +7 -5
  20. package/src/lib/server/cli/installScript.ts +3 -3
  21. package/src/lib/server/prompts/promptRegistry.ts +1 -1
  22. package/src/lib/server/prompts/types/Prompt.ts +2 -1
  23. package/src/lib/server/rpc/defineRpc.ts +9 -1
  24. package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
  25. package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
  26. package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
  27. package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
  28. package/src/lib/server/runtime/createServer.ts +50 -12
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
  30. package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
  31. package/src/lib/server/runtime/crossOriginGate.ts +1 -1
  32. package/src/lib/server/runtime/devHotModuleResponse.ts +3 -3
  33. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
  34. package/src/lib/server/runtime/gzipResponse.ts +14 -16
  35. package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
  36. package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
  37. package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
  38. package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
  39. package/src/lib/server/runtime/streamFromIterator.ts +2 -2
  40. package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
  41. package/src/lib/server/runtime/types/RequestStore.ts +16 -0
  42. package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
  43. package/src/lib/server/socket.ts +1 -1
  44. package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
  45. package/src/lib/server/sockets/defineSocket.ts +5 -1
  46. package/src/lib/server/sockets/socketRegistry.ts +2 -2
  47. package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
  48. package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
  49. package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
  50. package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
  51. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
  52. package/src/lib/shared/activeCacheStore.ts +5 -14
  53. package/src/lib/shared/activePage.ts +5 -19
  54. package/src/lib/shared/augmentModule.ts +18 -0
  55. package/src/lib/shared/basePath.ts +6 -5
  56. package/src/lib/shared/baseResolver.ts +10 -0
  57. package/src/lib/shared/baseSlot.ts +6 -12
  58. package/src/lib/shared/bodyValueForKind.ts +35 -0
  59. package/src/lib/shared/cache.ts +22 -2
  60. package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
  61. package/src/lib/shared/cacheStoreResolver.ts +12 -0
  62. package/src/lib/shared/cacheStoreSlot.ts +5 -13
  63. package/src/lib/shared/changeAffectsClient.ts +35 -0
  64. package/src/lib/shared/createPushIterator.ts +1 -1
  65. package/src/lib/shared/createResolverSlot.ts +37 -0
  66. package/src/lib/shared/debugGate.ts +90 -0
  67. package/src/lib/shared/decodeResponse.ts +11 -8
  68. package/src/lib/shared/emitLogRecord.ts +18 -4
  69. package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
  70. package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
  71. package/src/lib/shared/hasReplayableRequest.ts +17 -0
  72. package/src/lib/shared/html.ts +7 -26
  73. package/src/lib/shared/isDebugEnabled.ts +3 -10
  74. package/src/lib/shared/isDebugNegated.ts +4 -6
  75. package/src/lib/shared/manifestModule.ts +2 -2
  76. package/src/lib/shared/online.ts +1 -1
  77. package/src/lib/shared/outboxProbeSlot.ts +1 -1
  78. package/src/lib/shared/pageResolver.ts +16 -0
  79. package/src/lib/shared/pageSlot.ts +5 -14
  80. package/src/lib/shared/pageUrlForFile.ts +1 -1
  81. package/src/lib/shared/requestScopeResolver.ts +12 -0
  82. package/src/lib/shared/requestScopeSlot.ts +7 -12
  83. package/src/lib/shared/responseBodyKind.ts +35 -0
  84. package/src/lib/shared/routeParamsShape.ts +20 -0
  85. package/src/lib/shared/setBaseResolver.ts +2 -4
  86. package/src/lib/shared/setCacheStoreResolver.ts +3 -5
  87. package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
  88. package/src/lib/shared/setPageResolver.ts +2 -5
  89. package/src/lib/shared/setRequestScopeResolver.ts +3 -5
  90. package/src/lib/shared/snapshotShippable.ts +22 -0
  91. package/src/lib/shared/types/CacheEntry.ts +1 -1
  92. package/src/lib/shared/types/CacheInvalidation.ts +3 -3
  93. package/src/lib/shared/types/ClientFlags.ts +5 -3
  94. package/src/lib/shared/types/DebugGate.ts +10 -0
  95. package/src/lib/shared/types/LastConnection.ts +1 -1
  96. package/src/lib/shared/types/ResolverSlot.ts +10 -0
  97. package/src/lib/shared/url.ts +4 -1
  98. package/src/lib/shared/withBase.ts +1 -2
  99. package/src/lib/shared/writeDts.ts +10 -3
  100. package/src/lib/shared/writePublicAssetsDts.ts +5 -9
  101. package/src/lib/shared/writeRoutesDts.ts +14 -46
  102. package/src/lib/shared/writeRpcDts.ts +13 -12
  103. package/src/lib/shared/writeTestRpcDts.ts +11 -11
  104. package/src/lib/shared/writeTestSocketsDts.ts +9 -9
  105. package/src/lib/test/createTestSocketChannel.ts +3 -3
  106. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
  107. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
  108. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
  109. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
  110. package/src/lib/ui/compile/abideUiPlugin.ts +10 -8
  111. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
  112. package/src/lib/ui/compile/awaitPlan.ts +14 -2
  113. package/src/lib/ui/compile/catchBinding.ts +8 -0
  114. package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
  115. package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
  116. package/src/lib/ui/compile/compileComponent.ts +4 -4
  117. package/src/lib/ui/compile/compileModule.ts +56 -21
  118. package/src/lib/ui/compile/compileSSR.ts +12 -10
  119. package/src/lib/ui/compile/compileShadow.ts +163 -12
  120. package/src/lib/ui/compile/composeProps.ts +3 -3
  121. package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
  122. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  123. package/src/lib/ui/compile/createShadowScope.ts +42 -0
  124. package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
  125. package/src/lib/ui/compile/desugarSignals.ts +12 -7
  126. package/src/lib/ui/compile/eachPlan.ts +61 -0
  127. package/src/lib/ui/compile/elementPlan.ts +55 -0
  128. package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
  129. package/src/lib/ui/compile/generateBuild.ts +156 -127
  130. package/src/lib/ui/compile/generateSSR.ts +112 -73
  131. package/src/lib/ui/compile/ifPlan.ts +7 -1
  132. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
  133. package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
  134. package/src/lib/ui/compile/isTextLeaf.ts +8 -7
  135. package/src/lib/ui/compile/lowerContext.ts +36 -46
  136. package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
  137. package/src/lib/ui/compile/lowerScript.ts +1 -1
  138. package/src/lib/ui/compile/markupTokens.ts +313 -0
  139. package/src/lib/ui/compile/pagePropsType.ts +23 -0
  140. package/src/lib/ui/compile/parseTemplate.ts +135 -62
  141. package/src/lib/ui/compile/reactiveBinding.ts +47 -0
  142. package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
  143. package/src/lib/ui/compile/skeletonable.ts +5 -5
  144. package/src/lib/ui/compile/snippetPlan.ts +28 -0
  145. package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
  146. package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
  147. package/src/lib/ui/compile/switchPlan.ts +7 -1
  148. package/src/lib/ui/compile/tryPlan.ts +8 -1
  149. package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
  150. package/src/lib/ui/compile/types/Binding.ts +24 -0
  151. package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
  152. package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
  153. package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
  154. package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
  155. package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
  156. package/src/lib/ui/compile/types/TemplateNode.ts +9 -9
  157. package/src/lib/ui/compile/withBindings.ts +45 -0
  158. package/src/lib/ui/createScope.ts +2 -2
  159. package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
  160. package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
  161. package/src/lib/ui/dom/appendText.ts +1 -1
  162. package/src/lib/ui/dom/applyResolved.ts +1 -1
  163. package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
  164. package/src/lib/ui/dom/cloneStatic.ts +3 -2
  165. package/src/lib/ui/dom/each.ts +26 -7
  166. package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
  167. package/src/lib/ui/dom/mountChild.ts +8 -1
  168. package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
  169. package/src/lib/ui/dom/readCall.ts +6 -4
  170. package/src/lib/ui/dom/scopeLabel.ts +3 -3
  171. package/src/lib/ui/dom/skeleton.ts +10 -1
  172. package/src/lib/ui/dom/switchBlock.ts +17 -63
  173. package/src/lib/ui/dom/types/EachRow.ts +4 -0
  174. package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
  175. package/src/lib/ui/dom/when.ts +11 -64
  176. package/src/lib/ui/history.ts +1 -2
  177. package/src/lib/ui/html.ts +28 -0
  178. package/src/lib/ui/installHotBridge.ts +0 -2
  179. package/src/lib/ui/renderChain.ts +16 -3
  180. package/src/lib/ui/router.ts +24 -3
  181. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
  182. package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
  183. package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
  184. package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
  185. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
  186. package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
  187. package/src/lib/ui/runtime/createDoc.ts +107 -13
  188. package/src/lib/ui/runtime/createEffectNode.ts +5 -5
  189. package/src/lib/ui/runtime/flushEffects.ts +22 -9
  190. package/src/lib/ui/runtime/historyEntries.ts +1 -1
  191. package/src/lib/ui/runtime/track.ts +4 -2
  192. package/src/lib/ui/runtime/trigger.ts +34 -20
  193. package/src/lib/ui/runtime/types/Route.ts +2 -2
  194. package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
  195. package/src/lib/ui/runtime/types/UiProps.ts +9 -12
  196. package/src/lib/ui/runtime/walkPath.ts +15 -3
  197. package/src/lib/ui/startClient.ts +41 -14
  198. package/src/lib/ui/state.ts +3 -3
  199. package/src/lib/ui/types/Scope.ts +3 -3
  200. package/src/serverBuildPlugins.ts +11 -5
  201. package/src/zodCjsPlugin.ts +51 -0
  202. package/template/.zed/settings.json +7 -1
  203. package/template/package.json +3 -0
  204. package/template/src/ui/pages/layout.abide +3 -5
  205. package/src/lib/shared/matchesDebugPattern.ts +0 -16
  206. package/src/lib/shared/parseDebugPatterns.ts +0 -21
@@ -10,7 +10,7 @@ Confirms a URL points at a abide server before the launcher navigates the app
10
10
  window there, by fetching its unauthenticated health endpoint. Returns the
11
11
  server's identity on success, or undefined when nothing abide answers — a network
12
12
  error, the wrong port, or a non-abide page (a bare 403/404, a different app, a
13
- captive portal's 200 — hence the `abide: true` body check, never response.ok
13
+ captive portal's 200 — hence the `abide: true` (or version string) body check, never response.ok
14
14
  alone). The endpoint bypasses the app's own middleware, so an auth-guarded abide
15
15
  app still verifies here even though its pages would later redirect to a login.
16
16
  Probes HEALTH_PATH first, falling back to the IDENTITY_PATH alias so a newer
@@ -1,3 +1,5 @@
1
+ import { tokenizeArgvFlags } from './tokenizeArgvFlags.ts'
2
+
1
3
  /*
2
4
  Parses an argv tail into the JSON args bag for an RPC. The JSON Schema
3
5
  on the manifest entry (when present) drives flag typing:
@@ -9,8 +11,10 @@ on the manifest entry (when present) drives flag typing:
9
11
 
10
12
  For complex shapes (nested objects, unions, anyOf) the CLI exposes
11
13
  `--json <stringified-args>` as an escape hatch that supplies the whole
12
- args bag verbatim. Stdin: if a JSON object arrives piped in, it's used
13
- as the full args bag (flags layer on top).
14
+ args bag verbatim. Stdin: if a JSON object arrives piped in with no argv flags, it is used
15
+ as the full args bag. Stdin and flags are mutually exclusive — stdin is
16
+ skipped when argv is non-empty to avoid hanging on a pipe that never
17
+ sends EOF.
14
18
 
15
19
  Unrecognised flags throw — early loud feedback is more useful than
16
20
  silent drops.
@@ -45,44 +49,39 @@ export async function parseArgvForRpc(
45
49
  }
46
50
  }
47
51
 
48
- for (let index = 0; index < argv.length; index++) {
49
- const token = argv[index] as string
50
- if (token === '--json') {
51
- const next = argv[++index]
52
- if (!next) {
52
+ /* The shared tokenizer owns the flag-consumption grammar (boolean / inline /
53
+ `--json` / `--no-` negation). This loop layers RPC value semantics on each
54
+ yielded token: JSON-blob merge, Number coercion, array accumulation. */
55
+ for (const token of tokenizeArgvFlags(argv, jsonSchema)) {
56
+ if (token.positional !== undefined) {
57
+ throw new Error(`unexpected positional argument: ${token.positional}`)
58
+ }
59
+ if (token.isJson) {
60
+ if (token.missingValue || token.value === undefined) {
53
61
  throw new Error('--json requires a value')
54
62
  }
55
- const parsed = JSON.parse(next)
63
+ const parsed = JSON.parse(token.value)
56
64
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
57
65
  throw new Error('--json value must be a JSON object')
58
66
  }
59
67
  Object.assign(args, parsed)
60
68
  continue
61
69
  }
62
- if (!token.startsWith('--')) {
63
- throw new Error(`unexpected positional argument: ${token}`)
70
+ // The tokenizer treats `--help` / `-h` as a help token; in RPC parsing
71
+ // they are an unrecognised flag, surfaced loudly like any other.
72
+ if (token.isHelp || token.name === undefined) {
73
+ throw new Error('unexpected --help flag')
64
74
  }
65
- const stripped = token.slice('--'.length)
66
- const [literalName, eqValue] = stripped.includes('=')
67
- ? [stripped.slice(0, stripped.indexOf('=')), stripped.slice(stripped.indexOf('=') + 1)]
68
- : [stripped, undefined]
69
- /* `--no-x` negates only a known boolean property x; otherwise the literal
70
- name wins, so a property legitimately named `no-…` stays reachable. */
71
- const negatedName = literalName.startsWith('no-')
72
- ? literalName.slice('no-'.length)
73
- : undefined
74
- const isNegated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
75
- const name = isNegated ? (negatedName as string) : literalName
76
- const prop = properties[name]
77
- const propType = prop?.type
75
+ const name = token.name
76
+ const propType = properties[name]?.type
78
77
  if (propType === 'boolean') {
79
- args[name] = !isNegated
78
+ args[name] = !token.negated
80
79
  continue
81
80
  }
82
- const value = eqValue ?? argv[++index]
83
- if (value === undefined) {
81
+ if (token.missingValue || token.value === undefined) {
84
82
  throw new Error(`--${name} requires a value`)
85
83
  }
84
+ const value = token.value
86
85
  if (propType === 'number' || propType === 'integer') {
87
86
  // Reject a blank value explicitly — `Number('')` / `Number(' ')` is 0,
88
87
  // not NaN, so the NaN guard alone would silently coerce it to zero.
@@ -9,6 +9,7 @@ import { printTrimmed } from './printTrimmed.ts'
9
9
  import { resolveCliTarget } from './resolveCliTarget.ts'
10
10
  import { runSession } from './runSession.ts'
11
11
  import { startLocalInstance } from './startLocalInstance.ts'
12
+ import { tokenizeArgvFlags } from './tokenizeArgvFlags.ts'
12
13
  import type { CliManifest } from './types/CliManifest.ts'
13
14
  import type { CliTarget } from './types/CliTarget.ts'
14
15
 
@@ -16,37 +17,21 @@ const isHelpFlag = (arg: string): boolean => arg === '--help' || arg === '-h'
16
17
 
17
18
  /*
18
19
  Whether a command's argv tail genuinely requests help, i.e. a help flag appears
19
- at a flag position rather than as the value of a value-expecting flag. Mirrors
20
- parseArgvForRpc's flag rule: boolean props (and `--name=…`) consume no following
21
- token, everything else consumes the next token as its value so `--title --help`
22
- passes `--help` as the title value, not a help request.
20
+ at a flag position rather than as the value of a value-expecting flag. Shares
21
+ tokenizeArgvFlags' grammar with parseArgvForRpc so the two can't drift: the
22
+ tokenizer applies the flag-consumption rule (boolean / `--name=…` / `--json`
23
+ consume their value), so a `--help` that was a flag's value is already swallowed
24
+ and never surfaces as a help token — `--title --help` passes `--help` as the
25
+ title value, not a help request.
23
26
  */
24
27
  function commandArgvRequestsHelp(
25
28
  argvTail: string[],
26
29
  jsonSchema: Record<string, unknown> | undefined,
27
30
  ): boolean {
28
- const properties =
29
- (jsonSchema?.properties as Record<string, { type?: string }> | undefined) ?? {}
30
- for (let index = 0; index < argvTail.length; index += 1) {
31
- const token = argvTail[index] as string
32
- if (isHelpFlag(token)) {
31
+ for (const token of tokenizeArgvFlags(argvTail, jsonSchema)) {
32
+ if (token.isHelp) {
33
33
  return true
34
34
  }
35
- if (!token.startsWith('--')) {
36
- continue
37
- }
38
- if (token === '--json') {
39
- index += 1 // consumes its value
40
- continue
41
- }
42
- const rawName = token.slice('--'.length)
43
- if (rawName.includes('=')) {
44
- continue // inline value, consumes no following token
45
- }
46
- const name = rawName.startsWith('no-') ? rawName.slice('no-'.length) : rawName
47
- if (properties[name]?.type !== 'boolean') {
48
- index += 1 // a value-expecting flag consumes the next token
49
- }
50
35
  }
51
36
  return false
52
37
  }
@@ -0,0 +1,84 @@
1
+ /*
2
+ The single argv-tail flag grammar shared by help-detection (runCli) and RPC arg
3
+ parsing (parseArgvForRpc) so the two can never drift. Walks the tail and yields
4
+ one token per flag occurrence, honouring the schema-driven consumption rule:
5
+
6
+ - `--help` / `-h` → { isHelp: true }, consumes no following token
7
+ - `--json <blob>` → { isJson: true, value }, consumes the next token
8
+ (the value is the verbatim JSON args bag)
9
+ - boolean props (and `--no-x`) → { name, value: undefined }, consumes no token
10
+ - `--name=value` → { name, value }, inline, consumes no token
11
+ - everything else → { name, value }, consumes the next token
12
+
13
+ `--no-x` negates only a known boolean property x; otherwise the literal `no-…`
14
+ name is kept so a property legitimately named `no-…` stays reachable. Reports
15
+ `negated` and `missingValue` so the parser can apply boolean/coercion semantics
16
+ and throw precise errors. A bare positional (no leading `--`) yields
17
+ { positional: token } so each consumer rejects or ignores it as it sees fit.
18
+
19
+ Pure; reads the schema's `properties` only to classify boolean props.
20
+ */
21
+ export function* tokenizeArgvFlags(
22
+ argvTail: string[],
23
+ jsonSchema: Record<string, unknown> | undefined,
24
+ ): Generator<{
25
+ isHelp?: boolean
26
+ isJson?: boolean
27
+ positional?: string
28
+ name?: string
29
+ value?: string
30
+ negated?: boolean
31
+ missingValue?: boolean
32
+ }> {
33
+ const properties =
34
+ (jsonSchema?.properties as Record<string, { type?: string }> | undefined) ?? {}
35
+ for (let index = 0; index < argvTail.length; index += 1) {
36
+ const token = argvTail[index] as string
37
+ if (token === '--help' || token === '-h') {
38
+ yield { isHelp: true }
39
+ continue
40
+ }
41
+ if (!token.startsWith('--')) {
42
+ yield { positional: token }
43
+ continue
44
+ }
45
+ // `--json <blob>`: the next token is the verbatim JSON args bag.
46
+ if (token === '--json') {
47
+ const value = argvTail[index + 1]
48
+ if (value === undefined) {
49
+ yield { isJson: true, missingValue: true }
50
+ } else {
51
+ index += 1
52
+ yield { isJson: true, value }
53
+ }
54
+ continue
55
+ }
56
+ const stripped = token.slice('--'.length)
57
+ const [literalName, inlineValue] = stripped.includes('=')
58
+ ? [stripped.slice(0, stripped.indexOf('=')), stripped.slice(stripped.indexOf('=') + 1)]
59
+ : [stripped, undefined]
60
+ /* `--no-x` negates only a known boolean property x; otherwise the literal
61
+ name wins, so a property legitimately named `no-…` stays reachable. */
62
+ const negatedName = literalName.startsWith('no-')
63
+ ? literalName.slice('no-'.length)
64
+ : undefined
65
+ const negated = negatedName !== undefined && properties[negatedName]?.type === 'boolean'
66
+ const name = negated ? (negatedName as string) : literalName
67
+ // Boolean props and inline `--name=value` consume no following token.
68
+ if (properties[name]?.type === 'boolean') {
69
+ yield { name, negated }
70
+ continue
71
+ }
72
+ if (inlineValue !== undefined) {
73
+ yield { name, value: inlineValue }
74
+ continue
75
+ }
76
+ const value = argvTail[index + 1]
77
+ if (value === undefined) {
78
+ yield { name, missingValue: true }
79
+ } else {
80
+ index += 1
81
+ yield { name, value }
82
+ }
83
+ }
84
+ }
@@ -1,9 +1,11 @@
1
1
  import type { CliManifestEntry } from './CliManifestEntry.ts'
2
2
 
3
3
  /*
4
- Map from rpc export-name (e.g. "getReport") to its manifest entry. Built
5
- by the bundler from the same rpcRegistry MCP consumes; entries are
6
- emitted only for rpcs with `clients.cli: true`. The CLI binary and any
7
- programmatic createClient caller read this to dispatch calls.
4
+ Map from command name (URL-derived via commandNameForUrl, e.g. "getReport"
5
+ or "users-list") to its manifest entry. Built by the bundler from
6
+ rpcRegistry and socketRegistry; entries are emitted for rpcs and sockets
7
+ with `clients.cli: true`. Sockets produce `<base>-tail` (and `-publish`
8
+ when `allowClientPublish` is set). The CLI binary and any programmatic
9
+ createClient caller read this to dispatch calls.
8
10
  */
9
11
  export type CliManifest = Record<string, CliManifestEntry>
@@ -2,7 +2,7 @@ import type { HttpMethod } from '../shared/types/HttpMethod.ts'
2
2
 
3
3
  /*
4
4
  Maps an HTTP method to MCP tool annotations so a model can tell a read from
5
- a write before calling. Abide derives these from the rpc the RPC was
5
+ a write before calling. Abide derives these from the HTTP method the RPC was
6
6
  declared with rather than asking the author to repeat the intent:
7
7
  - GET / HEAD → read-only, non-destructive
8
8
  - POST → creates; not idempotent, not (necessarily) destructive
@@ -15,7 +15,7 @@ function asText(value: unknown): string {
15
15
  }
16
16
 
17
17
  /*
18
- Turns an rpc/socket Response into an MCP `tools/call` result. Always
18
+ Turns an RPC Response into an MCP `tools/call` result. Always
19
19
  carries a `text` content block for backward compatibility; adds
20
20
  `structuredContent` (an object, per the MCP spec) so models that
21
21
  understand structured output get the typed value instead of a stringified
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  JSON-RPC 2.0 request frame as MCP delivers it over Streamable HTTP. The
3
- `id` is absent for notifications (which we don't currently receive from
4
- clients but accept silently). `method` is a string like "tools/list" or
5
- "resources/read".
3
+ `id` is absent for notifications; they are dispatched for their side effect
4
+ and acknowledged with a 202 (JSON-RPC forbids replying with an envelope).
5
+ `method` is a string like "tools/list" or "resources/read".
6
6
  */
7
7
  export type JsonRpcRequest = {
8
8
  jsonrpc: '2.0'
@@ -10,7 +10,7 @@ WebSockets are not exposed here — abide's only native WebSocket
10
10
  surface is the sockets hub (see `abide/server/socket`), multiplexed onto a
11
11
  single framework-owned connection per client at `/__abide/sockets`.
12
12
  Inside request scopes, the live Bun.Server is reachable via the
13
- exported `server()` function from `abide/server`; `init` receives it
13
+ exported `server()` function from `abide/server/server`; `init` receives it
14
14
  explicitly because it runs outside a request.
15
15
  */
16
16
  // @documentation plumbing
@@ -92,12 +92,14 @@ async function computeBinary(
92
92
  }
93
93
 
94
94
  /*
95
- Handles GET /__abide/cli/<platform> — streams a gzipped tarball
96
- containing the platform-specific thin binary + a `.env` carrying
97
- ABIDE_APP_URL (and ABIDE_APP_TOKEN if the inbound request was authenticated).
95
+ Handles GET /__abide/cli/<platform> — streams a gzipped tarball containing
96
+ the platform-specific thin binary, the sibling server binary, and a `.env`
97
+ carrying ABIDE_APP_URL (and ABIDE_APP_TOKEN if the inbound request was
98
+ authenticated).
98
99
 
99
- Thin binaries live at `dist/cli-thin/<platform>/<programName>`
100
- (produced by `abide cli` with ABIDE_APP_URL set). Missing platforms produce
100
+ Thin binaries live at `dist/cli-thin/<platform>/<programName>` (produced
101
+ lazily by `buildCli` on first request). The `.env` baking `ABIDE_APP_URL`
102
+ is generated at download time, not build time. Missing platforms produce
101
103
  404 — the install script reports it, doesn't try to fall back.
102
104
  */
103
105
  export async function handleCliDownload(
@@ -7,9 +7,9 @@ platform-specific tarball and extracts it into `$ABIDE_INSTALL_DIR`
7
7
  (default `~/.local/bin`). The tarball already contains the `.env` next
8
8
  to the binary — no separate config write step in the script.
9
9
 
10
- The script is rendered server-side so `<ABIDE_APP_URL>` is the request's own
11
- origin and the embedded curl URL needs no escaping or quoting beyond
12
- basic shell hygiene.
10
+ The script is rendered server-side so the request origin is baked into the
11
+ `URL` variable at generation time and the embedded curl URL needs no escaping
12
+ or quoting beyond basic shell hygiene.
13
13
  */
14
14
  export function installScript(appUrl: string, programName: string): string {
15
15
  return `#!/usr/bin/env sh
@@ -3,7 +3,7 @@ import type { PromptRegistryEntry } from './types/PromptRegistryEntry.ts'
3
3
  /*
4
4
  Process-wide registry of every prompt declared in the app. definePrompt
5
5
  inserts on first construction (eagerly when the registry loader walks the
6
- prompts manifest at MCP boot). The MCP server reads this to build its
6
+ prompts manifest on first enumeration request). The MCP server reads this to build its
7
7
  `prompts/list` + `prompts/get` responses.
8
8
  */
9
9
  export const promptRegistry = new Map<string, PromptRegistryEntry>()
@@ -2,7 +2,8 @@
2
2
  An MCP prompt declared by a markdown file under `src/mcp/prompts/`. The
3
3
  resolver plugin parses the file's frontmatter + body and generates a call
4
4
  to definePrompt, stamping in the `name` from the file path; `render(args)`
5
- interpolates the body's `{{name}}` placeholders into the single user
5
+ interpolates the body's `{{name}}` placeholders and returns the rendered
6
+ string; `renderPrompt` in mcpSurface.ts then wraps it as the single user
6
7
  message returned by `prompts/get`. Prompts are MCP-only — there is no
7
8
  client-side counterpart, so the shape carries no ClientFlags.
8
9
  */
@@ -100,7 +100,15 @@ export function defineRpc<Args, Return>(
100
100
  function buildRequest(args: Args | undefined): Request {
101
101
  const store = requestContext.getStore()
102
102
  const baseUrl = store ? store.url.href : 'http://localhost/'
103
- const headers = store ? forwardHeaders(store.req.headers) : new Headers()
103
+ /* The forwarded allowlist is request-invariant derive it once per scope
104
+ and clone per call (buildRpcRequest mutates the Headers). */
105
+ let headers: Headers
106
+ if (store) {
107
+ store.forwardedHeaders ??= forwardHeaders(store.req.headers)
108
+ headers = new Headers(store.forwardedHeaders)
109
+ } else {
110
+ headers = new Headers()
111
+ }
104
112
  return buildRpcRequest({ method, url, args, baseUrl, headers })
105
113
  }
106
114
 
@@ -7,8 +7,8 @@ import { runWithRequestScope } from '../runtime/runWithRequestScope.ts'
7
7
  Runs a rpc in-process: synthesizes the rpc Request from the remote's own
8
8
  method + url and pipes it through remote.fetch — the same handler/validation/
9
9
  error path the HTTP router uses, no network hop. The single in-process
10
- dispatch every consumer surface (the CLI client, the MCP tool dispatcher, and
11
- the test client) routes through, so they can't drift on how a rpc is invoked.
10
+ dispatch every consumer surface (the CLI client and the MCP tool dispatcher)
11
+ routes through, so they can't drift on how a rpc is invoked.
12
12
  Takes the RemoteFunction directly — invocation never reads the registry
13
13
  entry's schemas/clients (validation is closed over inside the remote), so the
14
14
  entry is not a dependency here. `baseUrl` gives the synthetic Request its
@@ -34,7 +34,7 @@ type MutatingRpcOpts = RpcBaseOpts & {
34
34
  }
35
35
 
36
36
  /*
37
- Shared signature for every rpc helper (GET / POST / …). Three overloads:
37
+ Shared signature for every rpc helper (GET / POST / …). Four overloads:
38
38
 
39
39
  - `Rpc(fn, { inputSchema, outputSchema?, clients? })` — `Args` infers
40
40
  from `InferInput<InputSchema>`, the handler receives
@@ -18,8 +18,8 @@ sources, picked at construction:
18
18
 
19
19
  Returns a server fn that resolves to `undefined` when no public file
20
20
  matches the request path, so the caller falls through to its own 404 /
21
- middleware path. The path-traversal guard mirrors serveStaticAsset's
22
- defence against encoded `..` segments in the raw URL.
21
+ middleware path. The path-traversal guard uses the same `containsTraversal`
22
+ check against encoded `..` segments in the raw URL.
23
23
 
24
24
  Async because disk mode globs `publicDir` once at construction to build a
25
25
  Set of the available paths: every page nav and RPC falls through here, so
@@ -36,7 +36,7 @@ function methodNotAllowed(allow: string): Response {
36
36
 
37
37
  /*
38
38
  Owns route dispatch: deciding, per registered URL, whether a request hits an
39
- rpc rpc, a page render, or nothing — and the method-matching that picks the
39
+ an rpc, a page render, or nothing — and the method-matching that picks the
40
40
  status. Page URLs (under src/ui/pages/) serve GET/HEAD by rendering; rpc
41
41
  URLs (under src/server/rpc/, `/rpc/...`) dispatch to the single declared rpc,
42
42
  405 on method mismatch; an unregistered URL is 404. Page and rpc URLs are
@@ -17,6 +17,7 @@ import { isDebugNegated } from '../../shared/isDebugNegated.ts'
17
17
  import { logClosingRecord } from '../../shared/logClosingRecord.ts'
18
18
  import { OFFLINE_HEADER } from '../../shared/OFFLINE_HEADER.ts'
19
19
  import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
20
+ import { responseBodyKind } from '../../shared/responseBodyKind.ts'
20
21
  import { SOCKETS_PATH } from '../../shared/SOCKETS_PATH.ts'
21
22
  import { setAppName } from '../../shared/setAppName.ts'
22
23
  import { setBaseResolver } from '../../shared/setBaseResolver.ts'
@@ -242,8 +243,11 @@ export async function createServer({
242
243
  isn't installed. Resolved at boot so the fetch route below can branch on it.
243
244
  */
244
245
  const inspectorHandler = await maybeMountInspector({ name: appName, version: appVersion })
245
- /* Built on first request, then reused — the rpc registry is frozen after load. */
246
- let openApiSpec: ReturnType<typeof buildOpenApiSpec> | undefined
246
+ /* Built on first request, then reused — the rpc registry is frozen after load.
247
+ Memoised as a promise so two concurrent cold requests share one build instead
248
+ of both building (the second otherwise clobbering the first). A rejected build
249
+ clears the memo so the next request retries rather than caching the failure. */
250
+ let openApiSpec: Promise<ReturnType<typeof buildOpenApiSpec>> | undefined
247
251
  const cliCwd = process.cwd()
248
252
 
249
253
  /* Request closing records are on by default — DEBUG=-abide is the off switch (negation, like the abide channel itself). */
@@ -362,12 +366,19 @@ export async function createServer({
362
366
  const response = app?.handle
363
367
  ? await app.handle(req, (next) => handler(next, pathParams, store))
364
368
  : await handler(req, pathParams, store)
369
+ /* Classify the body once (S2) and thread it into both downstream
370
+ steps — gzip + the closing-record stream monitor — instead of
371
+ each re-deriving from the Content-Type. */
372
+ const kind = responseBodyKind(response)
373
+ store.responseStreaming = kind === 'streaming'
374
+ // Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
375
+ if (kind === 'streaming') {
376
+ server.timeout(req, 0)
377
+ }
365
378
  /* Gzip compressible dynamic bodies (SSR HTML, rpc/json, 404) when the
366
379
  client accepts it; streaming frame protocols and static assets are
367
380
  passed through untouched (see gzipResponse). */
368
- const encoded = gzipResponse(req, response)
369
- // Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
370
- return disableIdleTimeoutForStream(server, req, encoded)
381
+ return gzipResponse(req, response, kind)
371
382
  })
372
383
  }
373
384
 
@@ -481,6 +492,27 @@ export async function createServer({
481
492
  /* Component hot module — the browser imports one edited `.abide`'s
482
493
  hot build here instead of reloading (dev component HMR). */
483
494
  if (clientFingerprint !== undefined && url.pathname.startsWith(DEV_HOT_PREFIX)) {
495
+ /* This endpoint serves `application/javascript` for the browser's
496
+ `import()`. A TOP-LEVEL NAVIGATION to it — clicking the module link in a
497
+ stack trace, or opening the URL — would DOWNLOAD the file, since browsers
498
+ can't render JS as a document. A navigation sends `Accept: text/html`;
499
+ `import()` sends a wildcard Accept. Redirect a navigation back to a real
500
+ page (the referring page when same-origin, else the mount root) so the
501
+ error surfaces in context as a normal render instead of saving a file. */
502
+ if ((req.headers.get('accept') ?? '').includes('text/html')) {
503
+ const referer = req.headers.get('referer')
504
+ const page =
505
+ referer !== null &&
506
+ URL.canParse(referer) &&
507
+ new URL(referer).origin === url.origin &&
508
+ !new URL(referer).pathname.startsWith(DEV_HOT_PREFIX)
509
+ ? referer
510
+ : base || '/'
511
+ return new Response(null, {
512
+ status: 302,
513
+ headers: { Location: page, 'Cache-Control': NO_STORE },
514
+ })
515
+ }
484
516
  return devHotModuleResponse(
485
517
  decodeURIComponent(url.pathname.slice(DEV_HOT_PREFIX.length)),
486
518
  )
@@ -562,14 +594,20 @@ export async function createServer({
562
594
  req,
563
595
  {},
564
596
  async () => {
565
- if (!openApiSpec) {
566
- await ensureRegistriesLoaded()
567
- openApiSpec = buildOpenApiSpec({
568
- title: appName,
569
- version: appVersion,
597
+ openApiSpec ??= ensureRegistriesLoaded()
598
+ .then(() =>
599
+ buildOpenApiSpec({
600
+ title: appName,
601
+ version: appVersion,
602
+ }),
603
+ )
604
+ .catch((error) => {
605
+ // Don't cache a failed build — clear the memo so a
606
+ // later request retries instead of 500-ing forever.
607
+ openApiSpec = undefined
608
+ throw error
570
609
  })
571
- }
572
- return Response.json(openApiSpec, {
610
+ return Response.json(await openApiSpec, {
573
611
  headers: { 'Cache-Control': NO_STORE },
574
612
  })
575
613
  },
@@ -1,10 +1,9 @@
1
1
  import { appNameSlot } from '../../shared/appNameSlot.ts'
2
2
  import { SSR_CACHE_CONTROL } from '../../shared/CACHE_CONTROL_VALUES.ts'
3
3
  import { formatTraceparent } from '../../shared/formatTraceparent.ts'
4
- import { isReplayableMethod } from '../../shared/isReplayableMethod.ts'
5
4
  import { layoutChainForRoute } from '../../shared/layoutChainForRoute.ts'
6
5
  import { safeJsonForScript } from '../../shared/safeJsonForScript.ts'
7
- import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
6
+ import { snapshotShippable } from '../../shared/snapshotShippable.ts'
8
7
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
9
8
  import type { StreamedResolution } from '../../shared/types/StreamedResolution.ts'
10
9
  import { renderChain } from '../../ui/renderChain.ts'
@@ -23,6 +22,9 @@ type LoadPage = () => Promise<{ default: UiComponent }>
23
22
 
24
23
  const SSR_MARKER = /<!--ssr:(head|body|state)-->/g
25
24
  const BODY_MARKER = '<!--ssr:body-->'
25
+ /* Compiled once — both fill the shell per page render. */
26
+ const HEAD_STATE_MARKER = /<!--ssr:(head|state)-->/g
27
+ const HEAD_CLOSE_TAG = /<\/head>/i
26
28
 
27
29
  function wantsJson(request: Request): boolean {
28
30
  return (request.headers.get('accept') ?? '').includes('application/json')
@@ -140,7 +142,7 @@ export function createUiPageRenderer({
140
142
  build-time injector). A no-op when there are none or the shell carries no </head>. */
141
143
  function injectRoutePreloads(html: string, routeUrl: string): string {
142
144
  const links = routePreloadLinks(routeUrl)
143
- return links === '' ? html : html.replace(/<\/head>/i, `${links}</head>`)
145
+ return links === '' ? html : html.replace(HEAD_CLOSE_TAG, `${links}</head>`)
144
146
  }
145
147
 
146
148
  async function renderPage(
@@ -212,9 +214,7 @@ export function createUiPageRenderer({
212
214
  `<script>${SSR_SWAP_SCRIPT}${CACHE_RESOLVE_SCRIPT}</script>` +
213
215
  `${await stateTag(routeUrl, params, store, inline)}`
214
216
  const filled = injectRoutePreloads(
215
- shell.replace(/<!--ssr:(head|state)-->/g, (_match, key: string) =>
216
- key === 'head' ? head : '',
217
- ),
217
+ shell.replace(HEAD_STATE_MARKER, (_match, key: string) => (key === 'head' ? head : '')),
218
218
  routeUrl,
219
219
  )
220
220
  const [before, after] = filled.split(BODY_MARKER)
@@ -245,11 +245,7 @@ export function createUiPageRenderer({
245
245
  bundle, so startClient seeds the store and the block's subscription read
246
246
  is warm. Skip keys already shipped inline in __SSR__. */
247
247
  const streamedEntries = Array.from(store.cache.entries.values()).filter(
248
- (entry: CacheEntry) =>
249
- entry.settled === true &&
250
- entry.request !== undefined &&
251
- isReplayableMethod(entry.request.method.toUpperCase()) &&
252
- !inlinedKeys.has(entry.key),
248
+ (entry) => snapshotShippable(entry) && !inlinedKeys.has(entry.key),
253
249
  )
254
250
  for await (const resolution of streamCacheResolutions(
255
251
  store.cache,
@@ -3,8 +3,8 @@ import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
3
3
 
4
4
  /*
5
5
  The framework's CSRF refusal, shared by every same-origin gate — the socket
6
- upgrade, the socket REST publish face, the MCP endpoint, and mutating rpc
7
- rpcs — so the 403 body can't drift between surfaces. `hint` appends a
6
+ upgrade, the socket REST publish face, the MCP endpoint, and mutating rpcs
7
+ — so the 403 body can't drift between surfaces. `hint` appends a
8
8
  surface-specific remedy; rpc names its `crossOrigin: true` opt-out so the
9
9
  first developer this 403s can self-serve.
10
10
  */
@@ -6,7 +6,7 @@ import { isCrossOriginRequest } from './isCrossOriginRequest.ts'
6
6
  /*
7
7
  The framework's CSRF/CSWSH posture in one place: a cross-origin browser
8
8
  request to a mutating framework endpoint is refused. Every endpoint that
9
- parses bodies ignoring Content-Type (rpc rpcs, socket publish, MCP JSON-RPC)
9
+ parses bodies ignoring Content-Type (rpcs, socket publish, MCP JSON-RPC)
10
10
  must gate here — a hostile page's text/plain form trick could otherwise
11
11
  smuggle a payload in with the visitor's ambient cookies; non-browser clients
12
12
  send no Origin and pass. Returns the 403 to send, or undefined to proceed.
@@ -11,8 +11,8 @@ mode — runtime sourced from `window.__abide`, self-invoking `hotReplace` — a
11
11
  transpiles the embedded author TypeScript to plain JS the browser can import
12
12
  directly (the normal pipeline relies on the bundler's `ts` loader for this). The
13
13
  browser imports this in place of a reload; on load it swaps the component's live
14
- instances. The id is guarded as a project-relative `.abide` under cwd; a bad or
15
- missing file 404s so the client falls back to a reload.
14
+ instances. The id is guarded as a project-relative `.abide` under cwd; a bad
15
+ module ID returns 400; a missing file 404s so the client falls back to a reload.
16
16
  */
17
17
  export async function devHotModuleResponse(moduleId: string): Promise<Response> {
18
18
  const root = process.cwd()
@@ -26,7 +26,7 @@ export async function devHotModuleResponse(moduleId: string): Promise<Response>
26
26
  if (source === undefined) {
27
27
  return new Response('not found', { status: 404 })
28
28
  }
29
- const code = compileModule(source, {
29
+ const { code } = compileModule(source, {
30
30
  isLayout: moduleId.endsWith('layout.abide'),
31
31
  moduleId,
32
32
  hot: true,
@@ -13,7 +13,7 @@ than `body instanceof ReadableStream`, since every bodied Response exposes one.
13
13
  A streamed SSR page (`text/html`) is deliberately not opted out: it inherits the
14
14
  configured `idleTimeout` as a bounded cap on how long it can hold the connection,
15
15
  and the client re-fetches any placeholder left unresolved by a cut stream (see
16
- flushUnresolvedPlaceholders). Non-stream responses pass through.
16
+ streamCacheResolutions, which yields { key, miss } markers the client re-fetches). Non-stream responses pass through.
17
17
  */
18
18
  export function disableIdleTimeoutForStream(
19
19
  server: Server<unknown>,