@abide/abide 0.43.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/AGENTS.md +293 -219
  2. package/CHANGELOG.md +39 -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 +11 -1
  7. package/src/build.ts +9 -0
  8. package/src/buildDisconnected.ts +2 -0
  9. package/src/devEntry.ts +1 -0
  10. package/src/lib/bundle/probeAbideServer.ts +1 -1
  11. package/src/lib/cli/parseArgvForRpc.ts +4 -2
  12. package/src/lib/cli/types/CliManifest.ts +6 -4
  13. package/src/lib/mcp/annotationsForMethod.ts +1 -1
  14. package/src/lib/mcp/toolResultFromResponse.ts +1 -1
  15. package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
  16. package/src/lib/server/AppModule.ts +1 -1
  17. package/src/lib/server/cli/handleCliDownload.ts +7 -5
  18. package/src/lib/server/cli/installScript.ts +3 -3
  19. package/src/lib/server/prompts/promptRegistry.ts +1 -1
  20. package/src/lib/server/prompts/types/Prompt.ts +2 -1
  21. package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
  22. package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
  23. package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
  24. package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
  25. package/src/lib/server/runtime/createServer.ts +21 -0
  26. package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
  27. package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
  28. package/src/lib/server/runtime/crossOriginGate.ts +1 -1
  29. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -2
  30. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
  31. package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
  32. package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
  33. package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
  34. package/src/lib/server/runtime/streamFromIterator.ts +2 -2
  35. package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
  36. package/src/lib/server/socket.ts +1 -1
  37. package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
  38. package/src/lib/server/sockets/socketRegistry.ts +2 -2
  39. package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
  40. package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
  41. package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
  42. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
  43. package/src/lib/shared/bodyValueForKind.ts +35 -0
  44. package/src/lib/shared/cache.ts +1 -1
  45. package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
  46. package/src/lib/shared/createPushIterator.ts +1 -1
  47. package/src/lib/shared/decodeResponse.ts +11 -8
  48. package/src/lib/shared/hasReplayableRequest.ts +17 -0
  49. package/src/lib/shared/html.ts +7 -26
  50. package/src/lib/shared/manifestModule.ts +2 -2
  51. package/src/lib/shared/online.ts +1 -1
  52. package/src/lib/shared/outboxProbeSlot.ts +1 -1
  53. package/src/lib/shared/pageUrlForFile.ts +1 -1
  54. package/src/lib/shared/routeParamsShape.ts +20 -0
  55. package/src/lib/shared/snapshotShippable.ts +22 -0
  56. package/src/lib/shared/types/CacheEntry.ts +1 -1
  57. package/src/lib/shared/types/CacheInvalidation.ts +3 -3
  58. package/src/lib/shared/types/ClientFlags.ts +5 -3
  59. package/src/lib/shared/types/LastConnection.ts +1 -1
  60. package/src/lib/shared/url.ts +4 -1
  61. package/src/lib/shared/withBase.ts +1 -2
  62. package/src/lib/shared/writeDts.ts +2 -2
  63. package/src/lib/shared/writeRoutesDts.ts +3 -31
  64. package/src/lib/test/createTestSocketChannel.ts +3 -3
  65. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
  66. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
  67. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
  68. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
  69. package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
  70. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +3 -1
  71. package/src/lib/ui/compile/awaitPlan.ts +14 -2
  72. package/src/lib/ui/compile/catchBinding.ts +8 -0
  73. package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
  74. package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
  75. package/src/lib/ui/compile/compileComponent.ts +4 -4
  76. package/src/lib/ui/compile/compileModule.ts +20 -6
  77. package/src/lib/ui/compile/compileSSR.ts +12 -10
  78. package/src/lib/ui/compile/compileShadow.ts +88 -11
  79. package/src/lib/ui/compile/composeProps.ts +3 -3
  80. package/src/lib/ui/compile/createShadowLanguageService.ts +30 -2
  81. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  82. package/src/lib/ui/compile/createShadowScope.ts +42 -0
  83. package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
  84. package/src/lib/ui/compile/desugarSignals.ts +12 -7
  85. package/src/lib/ui/compile/eachPlan.ts +61 -0
  86. package/src/lib/ui/compile/elementPlan.ts +55 -0
  87. package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
  88. package/src/lib/ui/compile/generateBuild.ts +150 -127
  89. package/src/lib/ui/compile/generateSSR.ts +112 -73
  90. package/src/lib/ui/compile/ifPlan.ts +7 -1
  91. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
  92. package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
  93. package/src/lib/ui/compile/isTextLeaf.ts +8 -7
  94. package/src/lib/ui/compile/lowerContext.ts +36 -46
  95. package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
  96. package/src/lib/ui/compile/lowerScript.ts +1 -1
  97. package/src/lib/ui/compile/markupTokens.ts +313 -0
  98. package/src/lib/ui/compile/pagePropsType.ts +23 -0
  99. package/src/lib/ui/compile/parseTemplate.ts +131 -61
  100. package/src/lib/ui/compile/reactiveBinding.ts +47 -0
  101. package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
  102. package/src/lib/ui/compile/skeletonable.ts +5 -5
  103. package/src/lib/ui/compile/snippetPlan.ts +28 -0
  104. package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
  105. package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
  106. package/src/lib/ui/compile/switchPlan.ts +7 -1
  107. package/src/lib/ui/compile/tryPlan.ts +8 -1
  108. package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
  109. package/src/lib/ui/compile/types/Binding.ts +24 -0
  110. package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
  111. package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
  112. package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
  113. package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
  114. package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
  115. package/src/lib/ui/compile/types/TemplateNode.ts +8 -8
  116. package/src/lib/ui/compile/withBindings.ts +45 -0
  117. package/src/lib/ui/createScope.ts +2 -2
  118. package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
  119. package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
  120. package/src/lib/ui/dom/appendText.ts +1 -1
  121. package/src/lib/ui/dom/applyResolved.ts +1 -1
  122. package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
  123. package/src/lib/ui/dom/cloneStatic.ts +3 -2
  124. package/src/lib/ui/dom/each.ts +26 -7
  125. package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
  126. package/src/lib/ui/dom/mountChild.ts +8 -1
  127. package/src/lib/ui/dom/readCall.ts +6 -4
  128. package/src/lib/ui/dom/scopeLabel.ts +3 -3
  129. package/src/lib/ui/dom/switchBlock.ts +3 -1
  130. package/src/lib/ui/dom/types/EachRow.ts +4 -0
  131. package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
  132. package/src/lib/ui/history.ts +1 -2
  133. package/src/lib/ui/html.ts +28 -0
  134. package/src/lib/ui/installHotBridge.ts +0 -2
  135. package/src/lib/ui/renderChain.ts +16 -3
  136. package/src/lib/ui/router.ts +24 -3
  137. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
  138. package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
  139. package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
  140. package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
  141. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
  142. package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
  143. package/src/lib/ui/runtime/createDoc.ts +5 -1
  144. package/src/lib/ui/runtime/createEffectNode.ts +5 -5
  145. package/src/lib/ui/runtime/flushEffects.ts +22 -9
  146. package/src/lib/ui/runtime/historyEntries.ts +1 -1
  147. package/src/lib/ui/runtime/track.ts +4 -2
  148. package/src/lib/ui/runtime/trigger.ts +34 -20
  149. package/src/lib/ui/runtime/types/Route.ts +2 -2
  150. package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
  151. package/src/lib/ui/runtime/types/UiProps.ts +9 -12
  152. package/src/lib/ui/runtime/walkPath.ts +15 -3
  153. package/src/lib/ui/startClient.ts +41 -14
  154. package/src/lib/ui/state.ts +3 -3
  155. package/src/lib/ui/types/Scope.ts +3 -3
  156. package/src/serverBuildPlugins.ts +11 -5
  157. package/src/zodCjsPlugin.ts +36 -0
  158. package/template/.zed/settings.json +7 -1
  159. package/template/package.json +3 -0
  160. package/template/src/ui/pages/layout.abide +3 -5
@@ -3,7 +3,7 @@ import { requestScopeSlot } from './requestScopeSlot.ts'
3
3
 
4
4
  /*
5
5
  Reactive network-connectivity probe in the pending()/refreshing() family:
6
- reading it from a $derived/$effect re-runs that scope when the browser's
6
+ reading it from a scope().computed() / scope().effect() re-runs that scope when the browser's
7
7
  online/offline events fire. navigator.onLine's *offline* signal is reliable
8
8
  (a true network loss always reports); its online value can false-positive
9
9
  behind captive portals — verified backend reachability is a separate,
@@ -2,7 +2,7 @@
2
2
  Internal slot the browser outbox registry registers its prober into, so the
3
3
  shared pending() probe can count parked durable-rpc writes without shared/
4
4
  importing browser/. Mirrors tailProbeSlot. The prober reads the doc-backed
5
- queue entries (reactive inside $derived / $effect) and reports whether any
5
+ queue entries (reactive inside scope().computed() / scope().effect()) and reports whether any
6
6
  undelivered entry matches the selector: a durable rpc selector (carries a
7
7
  `url`) narrows to its own queue, the bare form spans every registered queue,
8
8
  optional args narrow to one parked call by structural compare. Outbox state is
@@ -4,7 +4,7 @@ folder-based: every leaf is `page.abide` or `layout.abide`, and the URL
4
4
  is the directory path. Pages mount at the directory path; layouts mount at
5
5
  the directory prefix. Dynamic segments keep their `[name]` / `[...rest]`
6
6
  shape — translation to Bun's `:name` / `*` happens at server registration
7
- via toBunRoutePattern; consumers see the readable form in `nav.route`.
7
+ via toBunRoutePattern; consumers see the readable form in `page.route`.
8
8
  */
9
9
  export function pageUrlForFile(relPath: string): string {
10
10
  const segments = relPath.split('/')
@@ -0,0 +1,20 @@
1
+ import { parseRouteSegments } from './parseRouteSegments.ts'
2
+
3
+ /*
4
+ The TypeScript type literal for a route's path params — `{ "id": string }` for
5
+ `/post/[id]`, `Record<string, never>` for a param-less route. Walks the `[name]` /
6
+ `[...rest]` segments; catch-all segments map to `string` under their declared name
7
+ (the server's toBunRoutePattern renames Bun's `*` key back to that name when
8
+ dispatching, so the page sees `params.rest`, not `params['*']`). The single source of
9
+ truth shared by `writeRoutesDts` (page.params typing) and the `.abide` check shadow
10
+ (page `props()` typing).
11
+ */
12
+ export function routeParamsShape(routeUrl: string): string {
13
+ const keys = parseRouteSegments(routeUrl)
14
+ .filter((segment) => segment.kind === 'param')
15
+ .map((segment) => segment.name)
16
+ if (keys.length === 0) {
17
+ return 'Record<string, never>'
18
+ }
19
+ return `{ ${keys.map((key) => `${JSON.stringify(key)}: string`).join('; ')} }`
20
+ }
@@ -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
@@ -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 }
@@ -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,8 +4,8 @@ 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
11
  await Bun.write(`${cwd}/src/.abide/${name}.d.ts`, `${DTS_BANNER}\n${body}\n\nexport {}\n`)
@@ -1,30 +1,7 @@
1
1
  import { pageUrlForFile } from './pageUrlForFile.ts'
2
- import { parseRouteSegments } from './parseRouteSegments.ts'
2
+ import { routeParamsShape } from './routeParamsShape.ts'
3
3
  import { writeDts } from './writeDts.ts'
4
4
 
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
5
  /*
29
6
  Emits a `.d.ts` that augments abide's `Routes` interface with one entry per
30
7
  page file in the project. Page picks this up as a discriminated union keyed
@@ -46,15 +23,10 @@ export async function writeRoutesDts({
46
23
  importName: string
47
24
  }): Promise<void> {
48
25
  const routes = pageFiles
49
- .map((file) => ({
50
- route: pageUrlForFile(file),
51
- params: paramsForRoute(pageUrlForFile(file)),
52
- }))
26
+ .map((file) => ({ route: pageUrlForFile(file) }))
53
27
  .toSorted((a, b) => a.route.localeCompare(b.route))
54
28
  const entries = routes
55
- .map(
56
- ({ route, params }) => ` ${JSON.stringify(route)}: ${renderParamsShape(params)}`,
57
- )
29
+ .map(({ route }) => ` ${JSON.stringify(route)}: ${routeParamsShape(route)}`)
58
30
  .join('\n')
59
31
  /* Keys-only mirror for url()'s autocomplete (values unused — PathParams derives the shape). */
60
32
  const urlKeys = routes.map(({ route }) => ` ${JSON.stringify(route)}: true`).join('\n')
@@ -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
  ]
@@ -15,7 +15,7 @@ only UI loader in the dev/build/preload pipeline; the emitted module's
15
15
  `abide/ui/*` imports resolve through the package exports.
16
16
 
17
17
  A `layout.abide` compiles as a layout: its `<slot/>` lowers to the router's page
18
- outlet (`<abide-outlet>`) rather than a passed-children slot — the file's role is
18
+ outlet (a `<!--abide:outlet-->…<!--/abide:outlet-->` comment boundary the router fills) rather than a passed-children slot — the file's role is
19
19
  its name, so the loader flags it from the path stem.
20
20
 
21
21
  The emitted module embeds the component's `<script>` and `{expr}` bodies verbatim,
@@ -16,7 +16,9 @@ comment is not a `CallExpression` callee, so a docs component quoting framework
16
16
  (`mount(...)` in a snippet) never false-positives. Compile-time only; never on the hot path.
17
17
  */
18
18
  export function assertRuntimeHelpersBound(module: string, context: string): void {
19
- const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.name))
19
+ /* The EMITTED local names (the `$$` alias when set) codegen calls those, and the
20
+ aliased import binds them, so the bound/called check must use the same form. */
21
+ const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.alias ?? entry.name))
20
22
  const source = ts.createSourceFile(
21
23
  'module.ts',
22
24
  module,
@@ -1,4 +1,6 @@
1
+ import { catchBinding } from './catchBinding.ts'
1
2
  import { resolveBranches } from './resolveBranches.ts'
3
+ import type { Binding } from './types/Binding.ts'
2
4
  import type { TemplateNode } from './types/TemplateNode.ts'
3
5
 
4
6
  /* The structural shape of an `await` block, resolved ONCE from the node so the build and
@@ -23,6 +25,12 @@ export type AwaitPlan = {
23
25
  /* Neither catch nor finally → a rejection must surface (re-throw / `undefined` catch thunk)
24
26
  instead of rendering an empty branch. */
25
27
  surfaceRejection: boolean
28
+ /* The resolved branch's binding (`resolvedAs`, `reactive` — a `.value` cell on the
29
+ client, a re-settle updates it in place). One element. */
30
+ resolvedBindings: Binding[]
31
+ /* The catch branch's binding (`catchAs`, `plain`); empty when no catch branch. The
32
+ `finally` branch binds nothing, so it registers no binding. */
33
+ catchBindings: Binding[]
26
34
  }
27
35
 
28
36
  /* Resolves an `await` node's branches into the shared structural plan. */
@@ -35,14 +43,18 @@ export function awaitPlan(node: Extract<TemplateNode, { kind: 'await' }>): Await
35
43
  )
36
44
  const finallyChildren = finallyBranch?.children ?? []
37
45
  const nonBranch = node.children.filter((child) => child.kind !== 'branch')
46
+ const resolvedAs = (node.blocking ? node.as : thenBranch?.as) ?? '_value'
47
+ const catchAs = catchBranch?.as ?? '_error'
38
48
  return {
39
49
  blocking: node.blocking,
40
50
  pending: node.blocking ? [] : nonBranch,
41
51
  resolvedChildren: node.blocking ? nonBranch : (thenBranch?.children ?? []),
42
- resolvedAs: (node.blocking ? node.as : thenBranch?.as) ?? '_value',
52
+ resolvedAs,
43
53
  catchChildren: catchBranch?.children ?? [],
44
- catchAs: catchBranch?.as ?? '_error',
54
+ catchAs,
45
55
  finallyChildren,
46
56
  surfaceRejection: catchBranch === undefined && finallyChildren.length === 0,
57
+ resolvedBindings: [{ name: resolvedAs, classification: 'reactive' }],
58
+ catchBindings: catchBinding(catchAs, catchBranch !== undefined),
47
59
  }
48
60
  }
@@ -0,0 +1,8 @@
1
+ import type { Binding } from './types/Binding.ts'
2
+
3
+ /* A block's `catch` branch binding — the error name bound `plain`, or none when the block
4
+ has no catch branch. Shared by the await/each/try plans so the one catch-binding shape
5
+ stays single-source. */
6
+ export function catchBinding(catchAs: string, hasCatch: boolean): Binding[] {
7
+ return hasCatch ? [{ name: catchAs, classification: 'plain' }] : []
8
+ }
@@ -0,0 +1,108 @@
1
+ import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
2
+ import type { TemplateAttr } from './types/TemplateAttr.ts'
3
+
4
+ /*
5
+ The one decision site for how an element's `class`/`style` base attribute composes
6
+ with its `class:`/`style:` directives — lifted out of the two code-gen back-ends
7
+ (`generateBuild`, `generateSSR`) so the load-bearing congruence (which attrs fold
8
+ into a merged value, and HOW that value's parts concatenate) lives once. Each
9
+ back-end RENDERS the returned plan differently — the build into a reactive `effect`,
10
+ SSR into a pushed attribute string — but consults the same decision, so the markup
11
+ the client clones and the server emits can never drift on the merge logic.
12
+
13
+ The two back-ends differ ONLY in their merge TRIGGER, because their non-merge
14
+ fallbacks differ: the build leaves a STATIC base in the cloned skeleton and toggles
15
+ directives surgically, so it merges only when the base is INTERPOLATED (re-set on
16
+ every update, which would wipe additive toggles) — `mergeClassBuild`/`mergeStyleBuild`.
17
+ SSR must always emit ONE merged attribute string (a duplicate `class`/`style` is
18
+ invalid), so it merges whenever a directive exists — `mergeClass`/`mergeStyle`. The
19
+ PARTS that compose the value (`classParts`/`styleParts`) are identical either way, so
20
+ they are built here once.
21
+
22
+ `attrs` classified up front; `lower` rewrites the signal surface to `model` (the same
23
+ transformer each back-end already threads through its expressions).
24
+ */
25
+
26
+ /* The base class/style attribute (static or interpolated) found on the element. */
27
+ type MergeBase =
28
+ | Extract<TemplateAttr, { kind: 'static' }>
29
+ | Extract<TemplateAttr, { kind: 'interpolated' }>
30
+ | undefined
31
+
32
+ export type ClassStyleMergePlan = {
33
+ classBase: MergeBase
34
+ styleBase: MergeBase
35
+ classDirectives: Extract<TemplateAttr, { kind: 'class' }>[]
36
+ styleDirectives: Extract<TemplateAttr, { kind: 'style' }>[]
37
+ /* SSR trigger: merge whenever a directive exists (must emit one attribute). */
38
+ mergeClass: boolean
39
+ mergeStyle: boolean
40
+ /* Build trigger: merge only when the base is interpolated (a static base stays in
41
+ the skeleton + surgical toggles). */
42
+ mergeClassBuild: boolean
43
+ mergeStyleBuild: boolean
44
+ /* The JS expression list a merged value joins — base (if any) + directive parts.
45
+ `class` joins on a space (directive name when truthy); `style` joins on `;`
46
+ (`property:value`, String-coerced). Identical across back-ends. */
47
+ classParts: string[]
48
+ styleParts: string[]
49
+ }
50
+
51
+ /* The base class/style attribute (static or interpolated) for one property name. */
52
+ function findBase(attrs: TemplateAttr[], name: 'class' | 'style'): MergeBase {
53
+ return attrs.find(
54
+ (attr): attr is NonNullable<MergeBase> =>
55
+ (attr.kind === 'static' || attr.kind === 'interpolated') && attr.name === name,
56
+ )
57
+ }
58
+
59
+ /* The base value as a lowered JS string expression, or undefined when there is no
60
+ base. A static base is a JSON string literal; an interpolated base is its
61
+ template-literal source run through `lower` (embedded signals → `model`). */
62
+ function baseExpr(base: MergeBase, lower: (code: string) => string): string | undefined {
63
+ if (base === undefined) {
64
+ return undefined
65
+ }
66
+ return base.kind === 'static'
67
+ ? JSON.stringify(base.value)
68
+ : lower(interpolatedTemplateLiteral(base.parts))
69
+ }
70
+
71
+ /* Classifies an element's attrs into the class/style merge plan. */
72
+ export function classStyleMergePlan(
73
+ attrs: TemplateAttr[],
74
+ lower: (code: string) => string,
75
+ ): ClassStyleMergePlan {
76
+ const classBase = findBase(attrs, 'class')
77
+ const styleBase = findBase(attrs, 'style')
78
+ const classDirectives = attrs.filter(
79
+ (attr): attr is Extract<TemplateAttr, { kind: 'class' }> => attr.kind === 'class',
80
+ )
81
+ const styleDirectives = attrs.filter(
82
+ (attr): attr is Extract<TemplateAttr, { kind: 'style' }> => attr.kind === 'style',
83
+ )
84
+ const classBaseExpr = baseExpr(classBase, lower)
85
+ const styleBaseExpr = baseExpr(styleBase, lower)
86
+ return {
87
+ classBase,
88
+ styleBase,
89
+ classDirectives,
90
+ styleDirectives,
91
+ mergeClass: classDirectives.length > 0,
92
+ mergeStyle: styleDirectives.length > 0,
93
+ mergeClassBuild: classBase?.kind === 'interpolated' && classDirectives.length > 0,
94
+ mergeStyleBuild: styleBase?.kind === 'interpolated' && styleDirectives.length > 0,
95
+ classParts: [
96
+ ...(classBaseExpr === undefined ? [] : [classBaseExpr]),
97
+ ...classDirectives.map(
98
+ (dir) => `((${lower(dir.code)}) ? ${JSON.stringify(dir.name)} : "")`,
99
+ ),
100
+ ],
101
+ styleParts: [
102
+ ...(styleBaseExpr === undefined ? [] : [styleBaseExpr]),
103
+ ...styleDirectives.map(
104
+ (dir) => `(${JSON.stringify(`${dir.property}:`)} + String(${lower(dir.code)}))`,
105
+ ),
106
+ ],
107
+ }
108
+ }
@@ -25,6 +25,17 @@ export function collectAbideDiagnostics(shadow: ShadowProgram): AbideDiagnostic[
25
25
  })
26
26
  continue
27
27
  }
28
+ /* Shadow-raised author rules (e.g. importing a compiler-internal helper) are
29
+ already in source coordinates — emit them directly, no segment remap. */
30
+ for (const diagnostic of shadows.get(abidePath)?.diagnostics ?? []) {
31
+ diagnostics.push({
32
+ file: abidePath,
33
+ start: diagnostic.start,
34
+ length: diagnostic.length,
35
+ message: diagnostic.message,
36
+ category: ts.DiagnosticCategory.Error,
37
+ })
38
+ }
28
39
  const sourceFile = program.getSourceFile(`${abidePath}.ts`)
29
40
  const mappings = shadows.get(abidePath)?.mappings
30
41
  if (sourceFile === undefined || mappings === undefined) {