@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
@@ -3,7 +3,6 @@ import type { BunPlugin } from 'bun'
3
3
  import { fileName } from '../../shared/fileName.ts'
4
4
  import { messageFromError } from '../../shared/messageFromError.ts'
5
5
  import { AbideCompileError } from './AbideCompileError.ts'
6
- import { analyzeComponent } from './analyzeComponent.ts'
7
6
  import { compileModule } from './compileModule.ts'
8
7
  import { nearestProjectRoot } from './nearestProjectRoot.ts'
9
8
  import { offsetToLineColumn } from './offsetToLineColumn.ts'
@@ -15,7 +14,7 @@ only UI loader in the dev/build/preload pipeline; the emitted module's
15
14
  `abide/ui/*` imports resolve through the package exports.
16
15
 
17
16
  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
17
+ outlet (a `<!--abide:outlet-->…<!--/abide:outlet-->` comment boundary the router fills) rather than a passed-children slot — the file's role is
19
18
  its name, so the loader flags it from the path stem.
20
19
 
21
20
  The emitted module embeds the component's `<script>` and `{expr}` bodies verbatim,
@@ -63,14 +62,17 @@ export const abideUiPlugin: BunPlugin = {
63
62
  throw new Error(`${message.replace(/^\[abide\]\s*/, `[abide] ${at} — `)}`)
64
63
  }
65
64
  }
66
- const code = compileAbide(() => compileModule(source, { isLayout, moduleId }))
65
+ /* One compile pass yields both the module code and its scoped `<style>` blocks —
66
+ the styles come from the analysis `compileModule` already ran, so the loader no
67
+ longer re-analyzes the source just to recover them. */
68
+ const { code, styles } = compileAbide(() =>
69
+ compileModule(source, { isLayout, moduleId }),
70
+ )
67
71
  /* Browser build with `<style>`(s): concatenate every scoped block's CSS and
68
72
  pull it into the bundle via one virtual import, keyed by `moduleId` so the
69
- registry id and the CSS id agree. */
70
- const styles = compileAbide(() =>
71
- toBrowser ? analyzeComponent(source, moduleId).styles : [],
72
- )
73
- if (styles.length === 0) {
73
+ registry id and the CSS id agree. Server builds skip the import (SSR styling
74
+ comes from the already-linked sheet). */
75
+ if (!toBrowser || styles.length === 0) {
74
76
  return { contents: code, loader: 'ts' }
75
77
  }
76
78
  const virtual = `abide-style:${moduleId}`
@@ -15,18 +15,32 @@ and require that name to be imported or locally bound. A helper name inside a st
15
15
  comment is not a `CallExpression` callee, so a docs component quoting framework code
16
16
  (`mount(...)` in a snippet) never false-positives. Compile-time only; never on the hot path.
17
17
  */
18
- export function assertRuntimeHelpersBound(module: string, context: string): void {
19
- const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.name))
20
- const source = ts.createSourceFile(
21
- 'module.ts',
22
- module,
23
- ts.ScriptTarget.Latest,
24
- /* setParentNodes */ true,
25
- )
26
- /* Names a bare call can resolve to: every import binding plus every declared name.
27
- Collected generously (any identifier in a binding position) — over-approximating the
28
- bound set only risks missing a defect, never raising a false alarm on valid output. */
29
- const bound = new Set<string>()
18
+ /* `module` is either the source string (standalone use — parsed here) or the already-parsed
19
+ tree of the bodies WITHOUT the prepended import block (compile pipeline — shares the one
20
+ parse the dead-import filter made). In the latter case the import block's bindings aren't in
21
+ the tree, so `importedHelpers` supplies the helper names that block will bind. */
22
+ export function assertRuntimeHelpersBound(
23
+ module: string | ts.SourceFile,
24
+ importedHelpers: Set<string>,
25
+ context: string,
26
+ ): void {
27
+ /* The EMITTED local names (the `$$` alias when set) — codegen calls those, and the
28
+ aliased import binds them, so the bound/called check must use the same form. */
29
+ const helperNames = new Set(UI_RUNTIME_IMPORTS.map((entry) => entry.alias ?? entry.name))
30
+ const source =
31
+ typeof module === 'string'
32
+ ? ts.createSourceFile(
33
+ 'module.ts',
34
+ module,
35
+ ts.ScriptTarget.Latest,
36
+ /* setParentNodes */ true,
37
+ )
38
+ : module
39
+ /* Names a bare call can resolve to: the import block's helper bindings (supplied when the
40
+ tree omits them) plus every import binding and declared name in the tree. Collected
41
+ generously (any identifier in a binding position) — over-approximating the bound set only
42
+ risks missing a defect, never raising a false alarm on valid output. */
43
+ const bound = new Set<string>(importedHelpers)
30
44
  const calledHelpers: { name: string; position: number }[] = []
31
45
  const visit = (node: ts.Node): void => {
32
46
  if (
@@ -60,7 +74,7 @@ export function assertRuntimeHelpersBound(module: string, context: string): void
60
74
  if (unbound !== undefined) {
61
75
  const { line, character } = source.getLineAndCharacterOfPosition(unbound.position)
62
76
  throw new Error(
63
- `[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${module}`,
77
+ `[abide] ${context} calls runtime helper \`${unbound.name}\` at line ${line + 1}:${character + 1} but never imports it — the dead-import filter dropped it. Please report this with the component source.\nOutput:\n${source.text}`,
64
78
  )
65
79
  }
66
80
  }
@@ -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) {
@@ -6,9 +6,9 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
6
6
  /*
7
7
  Compiles a single-file abide component into the body of a client build function.
8
8
  Runs the shared front-end (`analyzeComponent`), generates the DOM build from the
9
- template, and hoists static paths to cells. The returned body runs against a
10
- `host` element with `doc`/`state`/`computed`/`effect` and the dom bindings in
11
- scope and defines `model` itself. `compileModule` wraps it (and the SSR body) into
9
+ template, and hoists static paths to cells. The returned body operates on `host`
10
+ with the `$$`-prefixed dom bindings and `$$model` in scope (the latter emitted by
11
+ `desugarSignals`). `compileModule` wraps it (and the SSR body) into
12
12
  a real module; tests wrap it with `new Function`.
13
13
 
14
14
  `analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
@@ -26,5 +26,5 @@ export function compileComponent(
26
26
  /* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
27
27
  injected at runtime; the build only needs the `data-a-…` scope attributes on
28
28
  elements, which `generateBuild` reads from each node's annotated `scopes`. */
29
- return `${script}\n${hoistCells(build, 'model')}`
29
+ return `${script}\n${hoistCells(build, '$$model')}`
30
30
  }
@@ -5,6 +5,7 @@ import { assertRuntimeHelpersBound } from './assertRuntimeHelpersBound.ts'
5
5
  import { assertTranspiles } from './assertTranspiles.ts'
6
6
  import { compileComponent } from './compileComponent.ts'
7
7
  import { compileSSR } from './compileSSR.ts'
8
+ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
8
9
  import { UI_RUNTIME_IMPORTS } from './UI_RUNTIME_IMPORTS.ts'
9
10
 
10
11
  /*
@@ -26,7 +27,7 @@ what the `.abide` bundler loader emits.
26
27
  export function compileModule(
27
28
  source: string,
28
29
  options: { isLayout?: boolean; moduleId?: string; hot?: boolean } = {},
29
- ): string {
30
+ ): { code: string; styles: AnalyzedComponent['styles'] } {
30
31
  const isLayout = options.isLayout ?? false
31
32
  /* Run the shared front-end once and feed it to both back-ends — the analysis is
32
33
  pure over (source, moduleId), so the client and SSR builds reuse one parse
@@ -44,20 +45,31 @@ export function compileModule(
44
45
  leaf components today; one importing children falls back to a reload (the dev
45
46
  layer decides what to hot-swap). */
46
47
  if (options.hot) {
47
- const names = UI_RUNTIME_IMPORTS.map((entry) => entry.name).join(', ')
48
+ /* Bridge keys are the bare source names; bind each to its emitted local (`$$`
49
+ alias when set), so the hot body's `$$mountChild(...)` resolves. */
50
+ const names = UI_RUNTIME_IMPORTS.map((entry) =>
51
+ entry.alias === undefined || entry.alias === entry.name
52
+ ? entry.name
53
+ : `${entry.name}: ${entry.alias}`,
54
+ ).join(', ')
48
55
  const id = JSON.stringify(options.moduleId)
49
- return `const { ${names}, hotReplace } = window.__abide
56
+ /* Hot mode never imports the scoped CSS (it reuses the live bundle's sheet), so
57
+ styles are empty here regardless of the analyzed blocks. */
58
+ return {
59
+ code: `const { ${names}, hotReplace } = window.__abide
50
60
  ${userImports}
51
61
  function build(host, $props) {
52
62
  ${body}
53
63
  }
54
64
  function component(host, $props) {
55
- return mount(host, build, $props)
65
+ return $$mount(host, build, $props)
56
66
  }
57
67
  component.build = build
58
68
  component.__abideId = ${id}
59
69
  if (!hotReplace(${id}, component)) location.reload()
60
- `
70
+ `,
71
+ styles: [],
72
+ }
61
73
  }
62
74
 
63
75
  const ssrBody = indent(compileSSR(source, isLayout, options.moduleId, analyzed))
@@ -66,12 +78,12 @@ ${body}
66
78
  }
67
79
 
68
80
  export default function component(host, $props) {
69
- return mount(host, build, $props)
81
+ return $$mount(host, build, $props)
70
82
  }
71
83
 
72
84
  /* Adopt the server-rendered DOM in place instead of rebuilding it. */
73
85
  export function hydrateInto(host, $props) {
74
- return hydrate(host, build, $props)
86
+ return $$hydrate(host, build, $props)
75
87
  }
76
88
 
77
89
  export function render($props, $ctx) {
@@ -95,9 +107,31 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
95
107
  string/comment/HTML literal (e.g. an `on`-attribute in static markup) no longer
96
108
  forces a spurious import, so no per-surface scoping is needed: a client-only helper
97
109
  simply doesn't appear as an identifier in the SSR body. */
98
- const referenced = collectIdentifiers(`${userImports}\n${body}\n${ssrBody}\n${moduleBody}`)
99
- const importBlock = UI_RUNTIME_IMPORTS.filter((entry) => referenced.has(entry.name))
100
- .map((entry) => `import { ${entry.name} } from '${ABIDE_PACKAGE_NAME}/${entry.specifier}'`)
110
+ /* Parse the generated bodies ONCE and feed the single tree to both AST passes: the
111
+ dead-import filter (`collectIdentifiers`) and the binding backstop
112
+ (`assertRuntimeHelpersBound`). The import block isn't in this tree yet — it is derived
113
+ from the filter's result — so the backstop is told the names that block will bind
114
+ (`importedHelpers`), which is exactly what it would have read from the prepended imports
115
+ had it re-parsed the whole module. `setParentNodes` is required by the backstop's
116
+ `getStart`/line lookups. */
117
+ const bodySource = ts.createSourceFile(
118
+ 'module.ts',
119
+ `${userImports}\n${moduleBody}`,
120
+ ts.ScriptTarget.Latest,
121
+ /* setParentNodes */ true,
122
+ )
123
+ const referenced = collectIdentifiers(bodySource)
124
+ const keptImports = UI_RUNTIME_IMPORTS.filter((entry) =>
125
+ referenced.has(entry.alias ?? entry.name),
126
+ )
127
+ const importBlock = keptImports
128
+ .map((entry) => {
129
+ const local =
130
+ entry.alias === undefined || entry.alias === entry.name
131
+ ? entry.name
132
+ : `${entry.name} as ${entry.alias}`
133
+ return `import { ${local} } from '${ABIDE_PACKAGE_NAME}/${entry.specifier}'`
134
+ })
101
135
  .join('\n')
102
136
  const module = `${importBlock}
103
137
  ${userImports}
@@ -112,9 +146,15 @@ ${moduleBody}`
112
146
  /* `assertTranspiles` only proves the output PARSES — a call to an un-imported helper is
113
147
  valid syntax, so it slips through. This second guard proves the output is BOUND: every
114
148
  runtime helper it calls is actually imported (an independent check of the dead-import
115
- filter above), turning a runtime `ReferenceError` into a located compile error. */
116
- assertRuntimeHelpersBound(module, 'component module generation')
117
- return module
149
+ filter above), turning a runtime `ReferenceError` into a located compile error. It walks
150
+ the SAME tree the filter just parsed, with the kept helper imports supplied as the bound
151
+ set the prepended import block provides. */
152
+ assertRuntimeHelpersBound(
153
+ bodySource,
154
+ new Set(keptImports.map((entry) => entry.alias ?? entry.name)),
155
+ 'component module generation',
156
+ )
157
+ return { code: module, styles: analyzed.styles }
118
158
  }
119
159
 
120
160
  /* Indents a body block for embedding inside a wrapper function. Lines whose start
@@ -156,14 +196,9 @@ function unescapedBacktickCount(line: string): number {
156
196
  in a handler — leaves the scanner unable to find the substitution's closing `}` without
157
197
  the parser's re-scan, so a token-only pass mis-reads the rest of the module as template
158
198
  text and drops every helper referenced after it (an `import`-less `effect`/`mountChild`
159
- → a `ReferenceError` at mount → the router's reload fallback → a refresh loop). */
160
- function collectIdentifiers(code: string): Set<string> {
161
- const source = ts.createSourceFile(
162
- 'module.ts',
163
- code,
164
- ts.ScriptTarget.Latest,
165
- /* setParentNodes */ false,
166
- )
199
+ → a `ReferenceError` at mount → the router's reload fallback → a refresh loop). Takes the
200
+ already-parsed source so the one tree feeds this pass and the binding backstop both. */
201
+ function collectIdentifiers(source: ts.SourceFile): Set<string> {
167
202
  const names = new Set<string>()
168
203
  const visit = (node: ts.Node): void => {
169
204
  if (ts.isIdentifier(node)) {
@@ -5,17 +5,20 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
5
5
 
6
6
  /*
7
7
  Compiles a component into the body of a server render function. Runs the shared
8
- front-end, then the SSR back-end, and returns `{ html, state, awaits }`:
8
+ front-end, then the SSR back-end, and returns `{ html, state, awaits, resume }`:
9
9
 
10
10
  - `html` — server-rendered markup (await blocks render their pending shell);
11
11
  - `state` — the document snapshot the client adopts on resume;
12
12
  - `awaits` — pending await blocks (id + promise + resolved/error renderers) that
13
- `renderToStream` flushes out of order; empty for a fully synchronous component.
13
+ `renderToStream` flushes out of order; empty for a fully synchronous component;
14
+ - `resume` — blocking await values rendered inline, keyed by boundary id, seeded
15
+ into the hydration manifest.
14
16
 
15
17
  Effects are stripped — they are client lifecycle and emit no HTML, so the server
16
18
  render is a snapshot of the markup before any effect runs.
17
19
 
18
- Defines `model` with `doc`/`state`/`computed`/`effect` in scope. The block-id counter is
20
+ Defines `$$model` via the lowered script; the reactive scope is entered via `$$enterScope`
21
+ and exited in a `finally` block. The block-id counter is
19
22
  the request-local `$ctx` (threaded into child renders), not a module global — a blocking
20
23
  `await` block awaits its promise at its structural position and renders inline, so ids
21
24
  allocate depth-first like the client; render yields at that `await`, and a shared global
@@ -29,10 +32,9 @@ purely synchronous / streaming-await / try-only component returns `SsrRender` di
29
32
  framework always `await`s `render()`, so production is uniform either way; the sync return
30
33
  keeps static pages off the microtask queue and leaves the bulk of the SSR tests synchronous.
31
34
 
32
- The reactive scope brackets the body; it is entered synchronously before any inline `await`,
33
- so `const model = scope()` resolves correctly, and `model` is captured for the rest of the
34
- render (its reads are object-method calls, not scope lookups, so they stay correct across
35
- the awaits).
35
+ The reactive scope brackets the body: `$$enterScope()` runs synchronously before any inline
36
+ `await`, and `$$model` is captured for the rest of the render (its reads are object-method
37
+ calls, not scope lookups, so they stay correct across the awaits).
36
38
 
37
39
  `analyzed` is a lazy default: a direct caller (tests) omits it and the front-end
38
40
  runs here, but `compileModule` shares one analysis across both back-ends.
@@ -51,9 +53,9 @@ export function compileSSR(
51
53
  /* `typeof model` guards a component with no reactive state (a pure-async or
52
54
  static component declares no `model`); its snapshot is then empty. */
53
55
  const body =
54
- `const $scope = enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
55
- `return { html: $out.join(''), state: (typeof model !== 'undefined' ? model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
56
- `} finally { exitScope($scope); }`
56
+ `const $scope = $$enterScope();\ntry {\n${lowered}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\nconst $resume = {};\n${ssr}` +
57
+ `return { html: $out.join(''), state: (typeof $$model !== 'undefined' ? $$model.snapshot() : {}), awaits: $awaits, resume: $resume };\n` +
58
+ `} finally { $$exitScope($scope); }`
57
59
  /* An inline `await` — a blocking await block, a child render, a slot read, or a
58
60
  top-level `await` in the author script — forces an async wrapper. Match `await` as a
59
61
  standalone token (operator), so a no-space form (`await(x)`, `await[i]`, `` await`t` ``)