@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
@@ -1,6 +1,4 @@
1
- import { globalCacheStoreSlot } from './globalCacheStoreSlot.ts'
2
- import type { CacheStore } from './types/CacheStore.ts'
1
+ import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
3
2
 
4
- export function setGlobalCacheStoreResolver(fn: () => CacheStore | undefined): void {
5
- globalCacheStoreSlot.resolver = fn
6
- }
3
+ // Registers the process-level cache store resolver. Called once per side at boot.
4
+ export const setGlobalCacheStoreResolver = globalCacheStoreResolver.set
@@ -1,7 +1,4 @@
1
- import { pageSlot } from './pageSlot.ts'
2
- import type { PageSnapshot } from './types/PageSnapshot.ts'
1
+ import { pageResolver } from './pageResolver.ts'
3
2
 
4
3
  // Registers the runtime's page resolver. Called once per side at boot.
5
- export function setPageResolver(fn: () => PageSnapshot | undefined): void {
6
- pageSlot.resolver = fn
7
- }
4
+ export const setPageResolver = pageResolver.set
@@ -1,6 +1,4 @@
1
- import { requestScopeSlot } from './requestScopeSlot.ts'
2
- import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
1
+ import { requestScopeResolver } from './requestScopeResolver.ts'
3
2
 
4
- export function setRequestScopeResolver(fn: () => RequestScopeInfo | undefined): void {
5
- requestScopeSlot.resolver = fn
6
- }
3
+ // Registers the runtime's request-scope resolver. Called once per side at boot.
4
+ export const setRequestScopeResolver = requestScopeResolver.set
@@ -0,0 +1,22 @@
1
+ import { hasReplayableRequest } from './hasReplayableRequest.ts'
2
+ import type { CacheEntry } from './types/CacheEntry.ts'
3
+
4
+ /*
5
+ The one store-filter predicate selecting which cache entries are candidates for a
6
+ warm snapshot: settled AND carrying a replayable (GET-only) wire request, so it
7
+ can ship without re-firing a write or losing a body. Every selection site
8
+ (serializeCacheSnapshot, the renderer's render-return and streaming-drain filters)
9
+ calls this instead of re-spelling the guard, so they cannot drift.
10
+
11
+ Composed from `hasReplayableRequest` — the request half snapshotEntryFromCache
12
+ also gates on. snapshotEntryFromCache itself uses ONLY the request half (not
13
+ `settled`): the streaming-drain path hands it still-pending entries and awaits
14
+ them, so it can't require `settled` up front.
15
+
16
+ This is the SYNC half of shippability — what's knowable from the entry without
17
+ awaiting its body. The async response-level half (non-streaming + warmable kind)
18
+ lives in snapshotEntryFromCache, reading the resolved Response.
19
+ */
20
+ export function snapshotShippable(entry: CacheEntry): boolean {
21
+ return entry.settled === true && hasReplayableRequest(entry)
22
+ }
@@ -75,7 +75,7 @@ type InvalidationState = {
75
75
  timer?: ReturnType<typeof setTimeout>
76
76
  /*
77
77
  An invalidation that arrived while a refetch was already in flight: fireRefetch
78
- can't run concurrently, so it records the request here and re-schedules once the
78
+ can't run concurrently, so it sets this flag and re-schedules once the
79
79
  in-flight refetch settles, rather than dropping the newer invalidation.
80
80
  */
81
81
  pending?: boolean
@@ -2,8 +2,8 @@
2
2
  Detail payload of the cache store's 'invalidate' event — the set of cache
3
3
  keys one invalidation touched. A Set so each subscriber's membership check
4
4
  is O(1) regardless of how many keys a single invalidate spans. Shared
5
- between both dispatchers (cache.invalidate + streamed-resolution settling)
6
- and the cache store's listener, so the event's shape has one definition
7
- instead of an inline cast repeated at every site.
5
+ between the dispatcher (cache.invalidate) and the cache store's listener,
6
+ so the event's shape has one definition instead of an inline cast repeated
7
+ at every site.
8
8
  */
9
9
  export type CacheInvalidation = Set<string>
@@ -1,8 +1,10 @@
1
1
  /*
2
2
  Which client surfaces a rpc or socket is exposed to. Browser is the
3
- historical default; MCP and CLI flip on automatically when the
4
- declaration carries a Standard Schema (the schema is what makes the
5
- non-browser surfaces safe to advertise). Explicit values always win.
3
+ historical default. CLI flips on for any rpc or socket with a Standard
4
+ Schema. MCP flips on automatically only for read-only rpcs (GET/HEAD)
5
+ with a schema, and for any socket with a schema — mutating rpcs require
6
+ an explicit clients.mcp even when a schema is present. Explicit values
7
+ always win.
6
8
  */
7
9
  export type ClientFlags = {
8
10
  browser: boolean
@@ -0,0 +1,10 @@
1
+ /*
2
+ The two questions a DEBUG string answers for one channel name. `enabled` gates a
3
+ diagnostic channel (inclusion, with exclusions winning); `negated` is the off
4
+ switch for always-on channels (a `-name` pattern silencing the app's own or
5
+ abide's framework voice). Returned by debugGate, already bound to one env value.
6
+ */
7
+ export type DebugGate = {
8
+ enabled(name: string): boolean
9
+ negated(name: string): boolean
10
+ }
@@ -4,6 +4,6 @@ survives relaunch and is readable before any window or session opens. It records
4
4
  the *intent*, not a concrete embedded URL — an embedded server picks a fresh port
5
5
  each launch, so only `{ kind: 'embedded' }` is durable; a remote connection keeps
6
6
  its url. resolveLaunchTarget / resolveCliTarget read it; /connect and /start write
7
- it; /disconnect clears it.
7
+ it; /__abide/disconnect clears it.
8
8
  */
9
9
  export type LastConnection = { kind: 'embedded' } | { kind: 'url'; url: string }
@@ -0,0 +1,10 @@
1
+ /*
2
+ The mutable cell a runtime entry registers its resolver into, plus the single
3
+ lazy/value fallback used when no resolver is registered. createServer and
4
+ startClient install side-specific resolvers; isolated tests poke `.resolver` /
5
+ `.fallback` directly, so both fields stay public and mutable.
6
+ */
7
+ export type ResolverSlot<T> = {
8
+ resolver: (() => T | undefined) | undefined
9
+ fallback: T | undefined
10
+ }
@@ -8,6 +8,9 @@ import { queryStringFromArgs } from './queryStringFromArgs.ts'
8
8
  type QueryValue = string | number | boolean | undefined
9
9
  type Query = Record<string, QueryValue>
10
10
 
11
+ /* Compiled once — a scheme-qualified URL (`http:`, `mailto:`). */
12
+ const SCHEME_QUALIFIED = /^[a-z][a-z0-9+.-]*:/i
13
+
11
14
  /*
12
15
  Augmentable rpc map. Codegen emits a `declare module '<importName>/shared/url'`
13
16
  block filling this with `'/rpc/<file>': args` for each query-carrying (GET)
@@ -77,7 +80,7 @@ export function url<P extends KnownPath | (string & {})>(
77
80
  ): string
78
81
  export function url(path: string, first?: Query, second?: Query): string {
79
82
  // Scheme-qualified (http:, mailto:) or protocol-relative URLs are external — leave them whole.
80
- if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith('//')) {
83
+ if (SCHEME_QUALIFIED.test(path) || path.startsWith('//')) {
81
84
  return appendQuery(path, first)
82
85
  }
83
86
  const segments = parseRouteSegments(path)
@@ -3,8 +3,7 @@ import { basePath } from './basePath.ts'
3
3
  /*
4
4
  Prefixes a rooted internal path (`/rpc/…`, `/__abide/…`) with the mount base
5
5
  so the client's framework requests route through the proxy (`/v2/rpc/…`). The
6
- single rule the runtime fetch sites (remoteProxy, openResolveStream,
7
- socketChannel, the streaming placeholders) share — '' at root is a no-op.
6
+ single rule the runtime fetch sites (remoteProxy, socketChannel, health) share — '' at root is a no-op.
8
7
  */
9
8
  export function withBase(path: string): string {
10
9
  return `${basePath()}${path}`
@@ -4,9 +4,16 @@ const DTS_BANNER = '// Generated by abide. Do not edit by hand.'
4
4
  /*
5
5
  Writes a generated `.d.ts` to `src/.abide/<name>.d.ts` (the path the consumer's
6
6
  src tsconfig include picks up), wrapping `body` in the shared banner + the
7
- `export {}` footer that makes the file a module. The three codegen writers
8
- (routes, rpc, public assets) differ only in `body`, so the envelope lives here.
7
+ `export {}` footer that makes the file a module. All codegen writers (routes, rpc, publicAssets, testRpc, testSockets, health)
8
+ differ only in `body`, so the envelope lives here.
9
9
  */
10
10
  export async function writeDts(cwd: string, name: string, body: string): Promise<void> {
11
- await Bun.write(`${cwd}/src/.abide/${name}.d.ts`, `${DTS_BANNER}\n${body}\n\nexport {}\n`)
11
+ const next = `${DTS_BANNER}\n${body}\n\nexport {}\n`
12
+ const file = Bun.file(`${cwd}/src/.abide/${name}.d.ts`)
13
+ // Skip the write when byte-identical — an unchanged rebuild (the common case)
14
+ // otherwise bumps mtime every save, forcing the editor's TS server to re-typecheck.
15
+ if ((await file.exists()) && (await file.text()) === next) {
16
+ return
17
+ }
18
+ await Bun.write(file, next)
12
19
  }
@@ -1,3 +1,4 @@
1
+ import { augmentModule } from './augmentModule.ts'
1
2
  import { writeDts } from './writeDts.ts'
2
3
 
3
4
  /*
@@ -19,13 +20,8 @@ export async function writePublicAssetsDts({
19
20
  importName: string
20
21
  }): Promise<void> {
21
22
  const entries = publicFiles
22
- .map((file) => ` ${JSON.stringify(`/${file}`)}: true`)
23
- .toSorted()
24
- .join('\n')
25
- const body = `declare module '${importName}/shared/url' {
26
- interface PublicAssets {
27
- ${entries}
28
- }
29
- }`
30
- await writeDts(cwd, 'publicAssets', body)
23
+ .map((file): [string, string] => [`/${file}`, 'true'])
24
+ .toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
25
+ const module = augmentModule(`${importName}/shared/url`, 'PublicAssets', entries)
26
+ await writeDts(cwd, 'publicAssets', module)
31
27
  }
@@ -1,30 +1,8 @@
1
+ import { augmentModule } from './augmentModule.ts'
1
2
  import { pageUrlForFile } from './pageUrlForFile.ts'
2
- import { parseRouteSegments } from './parseRouteSegments.ts'
3
+ import { routeParamsShape } from './routeParamsShape.ts'
3
4
  import { writeDts } from './writeDts.ts'
4
5
 
5
- /*
6
- Walks a `[name]` / `[...rest]` route URL and returns the param shape it
7
- declares. Catch-all segments map to `string` under their declared name —
8
- the server's toBunRoutePattern renames Bun's `*` key back to that name
9
- when dispatching, so the page component sees `params.rest`, not
10
- `params['*']`.
11
- */
12
- function paramsForRoute(routeUrl: string): Record<string, 'string'> {
13
- return Object.fromEntries(
14
- parseRouteSegments(routeUrl)
15
- .filter((segment) => segment.kind === 'param')
16
- .map((segment) => [segment.name, 'string'] as const),
17
- )
18
- }
19
-
20
- function renderParamsShape(shape: Record<string, 'string'>): string {
21
- const keys = Object.keys(shape)
22
- if (keys.length === 0) {
23
- return 'Record<string, never>'
24
- }
25
- return `{ ${keys.map((key) => `${JSON.stringify(key)}: string`).join('; ')} }`
26
- }
27
-
28
6
  /*
29
7
  Emits a `.d.ts` that augments abide's `Routes` interface with one entry per
30
8
  page file in the project. Page picks this up as a discriminated union keyed
@@ -46,28 +24,18 @@ export async function writeRoutesDts({
46
24
  importName: string
47
25
  }): Promise<void> {
48
26
  const routes = pageFiles
49
- .map((file) => ({
50
- route: pageUrlForFile(file),
51
- params: paramsForRoute(pageUrlForFile(file)),
52
- }))
27
+ .map((file) => ({ route: pageUrlForFile(file) }))
53
28
  .toSorted((a, b) => a.route.localeCompare(b.route))
54
- const entries = routes
55
- .map(
56
- ({ route, params }) => ` ${JSON.stringify(route)}: ${renderParamsShape(params)}`,
57
- )
58
- .join('\n')
29
+ const routesModule = augmentModule(
30
+ `${importName}/shared/page`,
31
+ 'Routes',
32
+ routes.map(({ route }): [string, string] => [route, routeParamsShape(route)]),
33
+ )
59
34
  /* Keys-only mirror for url()'s autocomplete (values unused — PathParams derives the shape). */
60
- const urlKeys = routes.map(({ route }) => ` ${JSON.stringify(route)}: true`).join('\n')
61
- const body = `declare module '${importName}/shared/page' {
62
- interface Routes {
63
- ${entries}
64
- }
65
- }
66
-
67
- declare module '${importName}/shared/url' {
68
- interface PageRoutes {
69
- ${urlKeys}
70
- }
71
- }`
72
- await writeDts(cwd, 'routes', body)
35
+ const urlModule = augmentModule(
36
+ `${importName}/shared/url`,
37
+ 'PageRoutes',
38
+ routes.map(({ route }): [string, string] => [route, 'true']),
39
+ )
40
+ await writeDts(cwd, 'routes', `${routesModule}\n\n${urlModule}`)
73
41
  }
@@ -1,6 +1,8 @@
1
+ import { augmentModule } from './augmentModule.ts'
1
2
  import { carriesBodyArgs } from './carriesBodyArgs.ts'
2
3
  import { detectRpcMethod } from './detectRpcMethod.ts'
3
4
  import { fileStem } from './fileStem.ts'
5
+ import { RPC_ARGS_TYPE } from './RPC_ARGS_TYPE.ts'
4
6
  import { rpcUrlForFile } from './rpcUrlForFile.ts'
5
7
  import { writeDts } from './writeDts.ts'
6
8
 
@@ -26,24 +28,23 @@ export async function writeRpcDts({
26
28
  rpcFiles: string[]
27
29
  importName: string
28
30
  }): Promise<void> {
29
- const lines = await Promise.all(
30
- rpcFiles.map(async (file) => {
31
+ const pairs = await Promise.all(
32
+ rpcFiles.map(async (file): Promise<[string, string] | undefined> => {
31
33
  const method = detectRpcMethod(await Bun.file(`${rpcDir}/${file}`).text())
32
34
  // A body rpc's args can't ride a URL — leave it out of the url() rpc map.
33
35
  if (!method || carriesBodyArgs(method)) {
34
36
  return undefined
35
37
  }
36
38
  const importPath = `../server/rpc/${file}`
37
- return ` ${JSON.stringify(rpcUrlForFile(file))}: RpcArgs<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`
39
+ return [
40
+ rpcUrlForFile(file),
41
+ `RpcArgs<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`,
42
+ ]
38
43
  }),
39
44
  )
40
- const entries = lines.filter((line) => line !== undefined).toSorted()
41
- const body = `type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never
42
-
43
- declare module '${importName}/shared/url' {
44
- interface RpcRoutes {
45
- ${entries.join('\n')}
46
- }
47
- }`
48
- await writeDts(cwd, 'rpc', body)
45
+ const entries = pairs
46
+ .filter((pair) => pair !== undefined)
47
+ .toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
48
+ const module = augmentModule(`${importName}/shared/url`, 'RpcRoutes', entries)
49
+ await writeDts(cwd, 'rpc', `${RPC_ARGS_TYPE}\n\n${module}`)
49
50
  }
@@ -1,5 +1,7 @@
1
+ import { augmentModule } from './augmentModule.ts'
1
2
  import { commandNameForUrl } from './commandNameForUrl.ts'
2
3
  import { fileStem } from './fileStem.ts'
4
+ import { RPC_ARGS_TYPE } from './RPC_ARGS_TYPE.ts'
3
5
  import { rpcUrlForFile } from './rpcUrlForFile.ts'
4
6
  import { writeDts } from './writeDts.ts'
5
7
 
@@ -24,22 +26,20 @@ export async function writeTestRpcDts({
24
26
  importName: string
25
27
  }): Promise<void> {
26
28
  const entries = rpcFiles
27
- .map((file) => {
29
+ .map((file): [string, string] => {
28
30
  const name = commandNameForUrl(rpcUrlForFile(file))
29
31
  const importPath = `../server/rpc/${file}`
30
- return ` ${JSON.stringify(name)}: RpcInvoker<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`
32
+ return [
33
+ name,
34
+ `RpcInvoker<typeof import(${JSON.stringify(importPath)}).${fileStem(file)}>`,
35
+ ]
31
36
  })
32
- .toSorted()
33
- const body = `type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never
37
+ .toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
38
+ const helperTypes = `${RPC_ARGS_TYPE}
34
39
  type RpcReturn<Fn> = Fn extends (...args: never[]) => Promise<infer Return> ? Return : never
35
40
  type RpcInvoker<Fn> = ((args?: RpcArgs<Fn>) => Promise<RpcReturn<Fn>>) & {
36
41
  raw: (args?: RpcArgs<Fn>) => Promise<Response>
37
- }
38
-
39
- declare module '${importName}/test/createTestApp' {
40
- interface RpcClient {
41
- ${entries.join('\n')}
42
- }
43
42
  }`
44
- await writeDts(cwd, 'testRpc', body)
43
+ const module = augmentModule(`${importName}/test/createTestApp`, 'RpcClient', entries)
44
+ await writeDts(cwd, 'testRpc', `${helperTypes}\n\n${module}`)
45
45
  }
@@ -1,3 +1,4 @@
1
+ import { augmentModule } from './augmentModule.ts'
1
2
  import { fileStem } from './fileStem.ts'
2
3
  import { socketNameForFile } from './socketNameForFile.ts'
3
4
  import { writeDts } from './writeDts.ts'
@@ -20,15 +21,14 @@ export async function writeTestSocketsDts({
20
21
  importName: string
21
22
  }): Promise<void> {
22
23
  const entries = socketFiles
23
- .map((file) => {
24
+ .map((file): [string, string] => {
24
25
  const importPath = `../server/sockets/${file}`
25
- return ` ${JSON.stringify(socketNameForFile(file))}: typeof import(${JSON.stringify(importPath)}).${fileStem(file)}`
26
+ return [
27
+ socketNameForFile(file),
28
+ `typeof import(${JSON.stringify(importPath)}).${fileStem(file)}`,
29
+ ]
26
30
  })
27
- .toSorted()
28
- const body = `declare module '${importName}/test/createTestApp' {
29
- interface SocketClient {
30
- ${entries.join('\n')}
31
- }
32
- }`
33
- await writeDts(cwd, 'testSockets', body)
31
+ .toSorted(([a], [b]) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1))
32
+ const module = augmentModule(`${importName}/test/createTestApp`, 'SocketClient', entries)
33
+ await writeDts(cwd, 'testSockets', module)
34
34
  }
@@ -14,9 +14,9 @@ machinery — a test owns the connection lifecycle through `close()`, so a drop
14
14
  is teardown, not something to recover from. Frames sent before the ws opens
15
15
  queue and flush on open, the one piece of timing a test can't sidestep.
16
16
 
17
- Implements SocketChannel (subscribe/unsubscribe/publish), so `socket(name)`
18
- hands its sockets to the same buildSocketOverChannel the browser socketProxy
19
- uses — the Socket<T> surface can't drift between the test path and production.
17
+ Uses the same createSocketSubRegistry-backed SocketChannel the browser channel
18
+ uses, so `socket(name)` passes it to the same buildSocketOverChannel the browser
19
+ socketProxy uses — the Socket<T> surface can't drift between the test path and production.
20
20
  */
21
21
  export function createTestSocketChannel(wsUrl: string): {
22
22
  socket: <T>(name: string) => Socket<T>
@@ -1,5 +1,5 @@
1
1
  /* The `abide-` tag prefix on framework-owned custom elements (the router's
2
2
  `abide-outlet`, the streaming `abide-resolve`/`abide-cache` fragments). `scopeLabel`
3
- strips it to read the outlet host's bare name. Child components no longer mount into
3
+ strips it to derive the bare name from a framework-owned host tag (e.g. `abide-resolve` → `resolve`). Child components no longer mount into
4
4
  an `abide-<name>` wrapper — they build as marker ranges (see `mountRange`). */
5
5
  export const COMPONENT_WRAPPER_PREFIX = 'abide-'
@@ -1,9 +1,16 @@
1
+ import ts from 'typescript'
2
+
1
3
  /*
2
- The LSP semantic-tokens legend abide lsp advertises, and the decoder from
4
+ The LSP semantic-tokens legend abide lsp advertises, and the decoders from
3
5
  TypeScript's encoded classifications to legend names. TypeScript encodes a
4
- classification as `((tokenType + 1) << 8) + modifierBitset`; its TokenType and
5
- TokenModifier enums fix the orders below. `keyword`/`operator` carry the `{#…}`
6
- block framing the structural tokenizer emits.
6
+ *semantic* classification as `((tokenType + 1) << 8) + modifierBitset`; its
7
+ TokenType and TokenModifier enums fix the orders below. `keyword`/`operator` carry
8
+ the `{#…}` block framing the structural tokenizer emits; `string`/`number`/`regexp`
9
+ carry the literal *syntactic* classifications the semantic classifier never reports
10
+ (so a template-literal string inside `{…}` gets string coloring); `tag`/`attribute`/
11
+ `comment` carry the HTML markup structure the markup tokenizer emits, so the LSP
12
+ owns element/attribute/comment coloring instead of leaning on tree-sitter (which
13
+ desyncs on abide's `attr={expr}` values).
7
14
  */
8
15
 
9
16
  /* TS TokenType order (class=0 … member=11) → LSP token-type name. */
@@ -25,12 +32,32 @@ const TS_TYPE_TO_LSP = [
25
32
  /* TS TokenModifier order (declaration=bit 0 … local=bit 5). */
26
33
  const TS_MODIFIERS = ['declaration', 'static', 'async', 'readonly', 'defaultLibrary', 'local']
27
34
 
35
+ /* TS syntactic `ClassificationType` (legacy direct enum value) → literal legend
36
+ type. Only the literals the semantic classifier omits; identifiers, keywords, and
37
+ punctuation are left to the semantic pass (or untouched). */
38
+ const SYNTACTIC_LITERAL_TO_LSP: Partial<Record<ts.ClassificationType, string>> = {
39
+ [ts.ClassificationType.stringLiteral]: 'string',
40
+ [ts.ClassificationType.numericLiteral]: 'number',
41
+ [ts.ClassificationType.bigintLiteral]: 'number',
42
+ [ts.ClassificationType.regularExpressionLiteral]: 'regexp',
43
+ }
44
+
28
45
  export const ABIDE_SEMANTIC_TOKENS_LEGEND = {
29
- tokenTypes: [...TS_TYPE_TO_LSP, 'keyword', 'operator'],
46
+ tokenTypes: [
47
+ ...TS_TYPE_TO_LSP,
48
+ 'keyword',
49
+ 'operator',
50
+ 'string',
51
+ 'number',
52
+ 'regexp',
53
+ 'tag',
54
+ 'attribute',
55
+ 'comment',
56
+ ],
30
57
  tokenModifiers: TS_MODIFIERS,
31
58
  }
32
59
 
33
- /* Decodes one TypeScript encoded classification into legend names. */
60
+ /* Decodes one TypeScript encoded *semantic* classification into legend names. */
34
61
  export function mapTsClassification(
35
62
  classification: number,
36
63
  ): { type: string; modifiers: string[] } | undefined {
@@ -43,3 +70,10 @@ export function mapTsClassification(
43
70
  const modifiers = TS_MODIFIERS.filter((_, bit) => (modifierSet & (1 << bit)) !== 0)
44
71
  return { type, modifiers }
45
72
  }
73
+
74
+ /* Decodes one TypeScript *syntactic* classification (the legacy direct enum value,
75
+ not the bit-shifted semantic form) into a literal legend type, or undefined for
76
+ classifications the semantic pass already covers. */
77
+ export function mapSyntacticClassification(classification: number): string | undefined {
78
+ return SYNTACTIC_LITERAL_TO_LSP[classification as ts.ClassificationType]
79
+ }
@@ -1,5 +1,5 @@
1
1
  /* The callee names the `.abide` compiler recognises as reactive declarations
2
- (`let x = state(...)`, `linked(...)`, `computed(...)`, and the destructuring
2
+ (`scope().state(...)`, `scope().linked(...)`, `scope().computed(...)`, and the destructuring
3
3
  `const {…} = props()`): the shared "is this a reactive binding" allowlist read by
4
4
  the desugarer, the nested-script scoper, and the type-checking shadow. How each
5
5
  lowers — a serializable doc slot vs a `.value` cell — is decided per-site; this is
@@ -4,42 +4,54 @@ paired with its package subpath (after the package name). One source of truth so
4
4
  three things can't drift: the normal module's import block (`compileModule`), the
5
5
  hot module's `window.__abide` destructure (dev component HMR), and the dev bridge
6
6
  that populates `window.__abide`. Order is the emit order.
7
+
8
+ `alias` (when set) is the LOCAL name codegen emits — a `$$`-prefixed form reserved
9
+ for the compiler so a user variable of the helper's bare name can never collide
10
+ (`each as $$each`). It defaults to `name`; the dev bridge keys stay bare (the import
11
+ source), so a hot module destructures `{ name: alias }`. As each helper's emit sites
12
+ flip to its `$$` alias, set `alias` here in lockstep.
7
13
  */
8
- export const UI_RUNTIME_IMPORTS: { name: string; specifier: string }[] = [
9
- { name: 'html', specifier: 'shared/html' },
10
- { name: 'snippet', specifier: 'shared/snippet' },
11
- { name: 'scope', specifier: 'ui/scope' },
12
- { name: 'enterScope', specifier: 'ui/enterScope' },
13
- { name: 'exitScope', specifier: 'ui/exitScope' },
14
- { name: 'effect', specifier: 'ui/effect' },
15
- { name: 'mount', specifier: 'ui/dom/mount' },
16
- { name: 'appendText', specifier: 'ui/dom/appendText' },
17
- { name: 'appendTextAt', specifier: 'ui/dom/appendTextAt' },
18
- { name: 'appendSnippet', specifier: 'ui/dom/appendSnippet' },
19
- { name: 'appendStatic', specifier: 'ui/dom/appendStatic' },
20
- { name: 'cloneStatic', specifier: 'ui/dom/cloneStatic' },
21
- { name: 'skeleton', specifier: 'ui/dom/skeleton' },
22
- { name: 'anchorCursor', specifier: 'ui/dom/anchorCursor' },
23
- { name: 'attr', specifier: 'ui/dom/attr' },
24
- { name: 'on', specifier: 'ui/dom/on' },
25
- { name: 'attach', specifier: 'ui/dom/attach' },
26
- { name: 'each', specifier: 'ui/dom/each' },
27
- { name: 'eachAsync', specifier: 'ui/dom/eachAsync' },
28
- { name: 'when', specifier: 'ui/dom/when' },
29
- { name: 'awaitBlock', specifier: 'ui/dom/awaitBlock' },
30
- { name: 'tryBlock', specifier: 'ui/dom/tryBlock' },
31
- { name: 'switchBlock', specifier: 'ui/dom/switchBlock' },
32
- { name: 'mountSlot', specifier: 'ui/dom/mountSlot' },
33
- { name: 'outlet', specifier: 'ui/dom/outlet' },
34
- { name: 'mountChild', specifier: 'ui/dom/mountChild' },
35
- { name: 'mergeProps', specifier: 'ui/dom/mergeProps' },
36
- { name: 'spreadProps', specifier: 'ui/dom/spreadProps' },
37
- { name: 'restProps', specifier: 'ui/dom/restProps' },
38
- { name: 'spreadAttrs', specifier: 'ui/dom/spreadAttrs' },
39
- { name: 'readCall', specifier: 'ui/dom/readCall' },
40
- { name: 'hydrate', specifier: 'ui/dom/hydrate' },
41
- { name: 'escapeKey', specifier: 'ui/runtime/escapeKey' },
42
- { name: 'nextBlockId', specifier: 'ui/runtime/nextBlockId' },
43
- { name: 'enterRenderPass', specifier: 'ui/runtime/enterRenderPass' },
44
- { name: 'exitRenderPass', specifier: 'ui/runtime/exitRenderPass' },
14
+ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string; alias?: string }[] = [
15
+ { name: 'snippet', specifier: 'shared/snippet', alias: '$$snippet' },
16
+ /* `scope` is author-facing (`scope().state(...)`) but lowers to `$$scope` shadowably:
17
+ `referenceFor` rewrites a bare `scope` read to `$$scope` only when not lexically
18
+ shadowed, so the aliased import is the binding that emitted code resolves to. */
19
+ { name: 'scope', specifier: 'ui/scope', alias: '$$scope' },
20
+ { name: 'enterScope', specifier: 'ui/enterScope', alias: '$$enterScope' },
21
+ { name: 'exitScope', specifier: 'ui/exitScope', alias: '$$exitScope' },
22
+ { name: 'effect', specifier: 'ui/effect', alias: '$$effect' },
23
+ { name: 'mount', specifier: 'ui/dom/mount', alias: '$$mount' },
24
+ { name: 'appendText', specifier: 'ui/dom/appendText', alias: '$$appendText' },
25
+ { name: 'appendTextAt', specifier: 'ui/dom/appendTextAt', alias: '$$appendTextAt' },
26
+ { name: 'appendSnippet', specifier: 'ui/dom/appendSnippet', alias: '$$appendSnippet' },
27
+ { name: 'appendStatic', specifier: 'ui/dom/appendStatic', alias: '$$appendStatic' },
28
+ { name: 'cloneStatic', specifier: 'ui/dom/cloneStatic', alias: '$$cloneStatic' },
29
+ { name: 'skeleton', specifier: 'ui/dom/skeleton', alias: '$$skeleton' },
30
+ { name: 'anchorCursor', specifier: 'ui/dom/anchorCursor', alias: '$$anchorCursor' },
31
+ { name: 'attr', specifier: 'ui/dom/attr', alias: '$$attr' },
32
+ { name: 'on', specifier: 'ui/dom/on', alias: '$$on' },
33
+ { name: 'attach', specifier: 'ui/dom/attach', alias: '$$attach' },
34
+ { name: 'each', specifier: 'ui/dom/each', alias: '$$each' },
35
+ { name: 'eachAsync', specifier: 'ui/dom/eachAsync', alias: '$$eachAsync' },
36
+ { name: 'when', specifier: 'ui/dom/when', alias: '$$when' },
37
+ { name: 'awaitBlock', specifier: 'ui/dom/awaitBlock', alias: '$$awaitBlock' },
38
+ { name: 'tryBlock', specifier: 'ui/dom/tryBlock', alias: '$$tryBlock' },
39
+ { name: 'switchBlock', specifier: 'ui/dom/switchBlock', alias: '$$switchBlock' },
40
+ { name: 'mountSlot', specifier: 'ui/dom/mountSlot', alias: '$$mountSlot' },
41
+ { name: 'outlet', specifier: 'ui/dom/outlet', alias: '$$outlet' },
42
+ { name: 'mountChild', specifier: 'ui/dom/mountChild', alias: '$$mountChild' },
43
+ { name: 'mergeProps', specifier: 'ui/dom/mergeProps', alias: '$$mergeProps' },
44
+ { name: 'spreadProps', specifier: 'ui/dom/spreadProps', alias: '$$spreadProps' },
45
+ { name: 'restProps', specifier: 'ui/dom/restProps', alias: '$$restProps' },
46
+ { name: 'spreadAttrs', specifier: 'ui/dom/spreadAttrs', alias: '$$spreadAttrs' },
47
+ { name: 'readCall', specifier: 'ui/dom/readCall', alias: '$$readCall' },
48
+ { name: 'hydrate', specifier: 'ui/dom/hydrate', alias: '$$hydrate' },
49
+ { name: 'escapeKey', specifier: 'ui/runtime/escapeKey', alias: '$$escapeKey' },
50
+ { name: 'nextBlockId', specifier: 'ui/runtime/nextBlockId', alias: '$$nextBlockId' },
51
+ {
52
+ name: 'enterRenderPass',
53
+ specifier: 'ui/runtime/enterRenderPass',
54
+ alias: '$$enterRenderPass',
55
+ },
56
+ { name: 'exitRenderPass', specifier: 'ui/runtime/exitRenderPass', alias: '$$exitRenderPass' },
45
57
  ]