@llui/compiler 0.3.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 (310) hide show
  1. package/LICENSE +21 -0
  2. package/dist/accessor-resolver.d.ts +58 -0
  3. package/dist/accessor-resolver.d.ts.map +1 -0
  4. package/dist/accessor-resolver.js +119 -0
  5. package/dist/accessor-resolver.js.map +1 -0
  6. package/dist/binding-descriptors.d.ts +105 -0
  7. package/dist/binding-descriptors.d.ts.map +1 -0
  8. package/dist/binding-descriptors.js +340 -0
  9. package/dist/binding-descriptors.js.map +1 -0
  10. package/dist/collect-deps.d.ts +49 -0
  11. package/dist/collect-deps.d.ts.map +1 -0
  12. package/dist/collect-deps.js +444 -0
  13. package/dist/collect-deps.js.map +1 -0
  14. package/dist/compiler-cache.d.ts +20 -0
  15. package/dist/compiler-cache.d.ts.map +1 -0
  16. package/dist/compiler-cache.js +20 -0
  17. package/dist/compiler-cache.js.map +1 -0
  18. package/dist/cross-file-resolver.d.ts +109 -0
  19. package/dist/cross-file-resolver.d.ts.map +1 -0
  20. package/dist/cross-file-resolver.js +530 -0
  21. package/dist/cross-file-resolver.js.map +1 -0
  22. package/dist/cross-file-walker.d.ts +63 -0
  23. package/dist/cross-file-walker.d.ts.map +1 -0
  24. package/dist/cross-file-walker.js +516 -0
  25. package/dist/cross-file-walker.js.map +1 -0
  26. package/dist/diagnostic.d.ts +76 -0
  27. package/dist/diagnostic.d.ts.map +1 -0
  28. package/dist/diagnostic.js +59 -0
  29. package/dist/diagnostic.js.map +1 -0
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +37 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/introspection-factory.d.ts +54 -0
  35. package/dist/introspection-factory.d.ts.map +1 -0
  36. package/dist/introspection-factory.js +46 -0
  37. package/dist/introspection-factory.js.map +1 -0
  38. package/dist/manifest.d.ts +144 -0
  39. package/dist/manifest.d.ts.map +1 -0
  40. package/dist/manifest.js +209 -0
  41. package/dist/manifest.js.map +1 -0
  42. package/dist/module.d.ts +222 -0
  43. package/dist/module.d.ts.map +1 -0
  44. package/dist/module.js +256 -0
  45. package/dist/module.js.map +1 -0
  46. package/dist/modules/_element-helpers.d.ts +4 -0
  47. package/dist/modules/_element-helpers.d.ts.map +1 -0
  48. package/dist/modules/_element-helpers.js +138 -0
  49. package/dist/modules/_element-helpers.js.map +1 -0
  50. package/dist/modules/_msg-variants.d.ts +10 -0
  51. package/dist/modules/_msg-variants.d.ts.map +1 -0
  52. package/dist/modules/_msg-variants.js +97 -0
  53. package/dist/modules/_msg-variants.js.map +1 -0
  54. package/dist/modules/_shared.d.ts +16 -0
  55. package/dist/modules/_shared.d.ts.map +1 -0
  56. package/dist/modules/_shared.js +30 -0
  57. package/dist/modules/_shared.js.map +1 -0
  58. package/dist/modules/accessibility.d.ts +3 -0
  59. package/dist/modules/accessibility.d.ts.map +1 -0
  60. package/dist/modules/accessibility.js +82 -0
  61. package/dist/modules/accessibility.js.map +1 -0
  62. package/dist/modules/accessor-side-effect.d.ts +3 -0
  63. package/dist/modules/accessor-side-effect.d.ts.map +1 -0
  64. package/dist/modules/accessor-side-effect.js +113 -0
  65. package/dist/modules/accessor-side-effect.js.map +1 -0
  66. package/dist/modules/agent-emits-drift.d.ts +3 -0
  67. package/dist/modules/agent-emits-drift.d.ts.map +1 -0
  68. package/dist/modules/agent-emits-drift.js +158 -0
  69. package/dist/modules/agent-emits-drift.js.map +1 -0
  70. package/dist/modules/agent-example-on-payload.d.ts +3 -0
  71. package/dist/modules/agent-example-on-payload.d.ts.map +1 -0
  72. package/dist/modules/agent-example-on-payload.js +53 -0
  73. package/dist/modules/agent-example-on-payload.js.map +1 -0
  74. package/dist/modules/agent-exclusive-annotations.d.ts +3 -0
  75. package/dist/modules/agent-exclusive-annotations.d.ts.map +1 -0
  76. package/dist/modules/agent-exclusive-annotations.js +68 -0
  77. package/dist/modules/agent-exclusive-annotations.js.map +1 -0
  78. package/dist/modules/agent-missing-intent.d.ts +3 -0
  79. package/dist/modules/agent-missing-intent.d.ts.map +1 -0
  80. package/dist/modules/agent-missing-intent.js +47 -0
  81. package/dist/modules/agent-missing-intent.js.map +1 -0
  82. package/dist/modules/agent-msg-resolvable.d.ts +3 -0
  83. package/dist/modules/agent-msg-resolvable.d.ts.map +1 -0
  84. package/dist/modules/agent-msg-resolvable.js +161 -0
  85. package/dist/modules/agent-msg-resolvable.js.map +1 -0
  86. package/dist/modules/agent-nonextractable-handler.d.ts +3 -0
  87. package/dist/modules/agent-nonextractable-handler.d.ts.map +1 -0
  88. package/dist/modules/agent-nonextractable-handler.js +127 -0
  89. package/dist/modules/agent-nonextractable-handler.js.map +1 -0
  90. package/dist/modules/agent-optional-field-undocumented.d.ts +3 -0
  91. package/dist/modules/agent-optional-field-undocumented.d.ts.map +1 -0
  92. package/dist/modules/agent-optional-field-undocumented.js +67 -0
  93. package/dist/modules/agent-optional-field-undocumented.js.map +1 -0
  94. package/dist/modules/agent-tagsend-translator-missing.d.ts +3 -0
  95. package/dist/modules/agent-tagsend-translator-missing.d.ts.map +1 -0
  96. package/dist/modules/agent-tagsend-translator-missing.js +58 -0
  97. package/dist/modules/agent-tagsend-translator-missing.js.map +1 -0
  98. package/dist/modules/agent-warning-on-confirm.d.ts +3 -0
  99. package/dist/modules/agent-warning-on-confirm.d.ts.map +1 -0
  100. package/dist/modules/agent-warning-on-confirm.js +46 -0
  101. package/dist/modules/agent-warning-on-confirm.js.map +1 -0
  102. package/dist/modules/async-update.d.ts +3 -0
  103. package/dist/modules/async-update.d.ts.map +1 -0
  104. package/dist/modules/async-update.js +86 -0
  105. package/dist/modules/async-update.js.map +1 -0
  106. package/dist/modules/binding-descriptors.d.ts +4 -0
  107. package/dist/modules/binding-descriptors.d.ts.map +1 -0
  108. package/dist/modules/binding-descriptors.js +48 -0
  109. package/dist/modules/binding-descriptors.js.map +1 -0
  110. package/dist/modules/bitmask-overflow.d.ts +3 -0
  111. package/dist/modules/bitmask-overflow.d.ts.map +1 -0
  112. package/dist/modules/bitmask-overflow.js +152 -0
  113. package/dist/modules/bitmask-overflow.js.map +1 -0
  114. package/dist/modules/compiler-stamp.d.ts +3 -0
  115. package/dist/modules/compiler-stamp.d.ts.map +1 -0
  116. package/dist/modules/compiler-stamp.js +44 -0
  117. package/dist/modules/compiler-stamp.js.map +1 -0
  118. package/dist/modules/component-meta.d.ts +3 -0
  119. package/dist/modules/component-meta.d.ts.map +1 -0
  120. package/dist/modules/component-meta.js +44 -0
  121. package/dist/modules/component-meta.js.map +1 -0
  122. package/dist/modules/controlled-input.d.ts +3 -0
  123. package/dist/modules/controlled-input.d.ts.map +1 -0
  124. package/dist/modules/controlled-input.js +68 -0
  125. package/dist/modules/controlled-input.js.map +1 -0
  126. package/dist/modules/core-synthesis.d.ts +18 -0
  127. package/dist/modules/core-synthesis.d.ts.map +1 -0
  128. package/dist/modules/core-synthesis.js +748 -0
  129. package/dist/modules/core-synthesis.js.map +1 -0
  130. package/dist/modules/direct-state-in-view.d.ts +3 -0
  131. package/dist/modules/direct-state-in-view.d.ts.map +1 -0
  132. package/dist/modules/direct-state-in-view.js +103 -0
  133. package/dist/modules/direct-state-in-view.js.map +1 -0
  134. package/dist/modules/each-closure-violation.d.ts +3 -0
  135. package/dist/modules/each-closure-violation.d.ts.map +1 -0
  136. package/dist/modules/each-closure-violation.js +255 -0
  137. package/dist/modules/each-closure-violation.js.map +1 -0
  138. package/dist/modules/each-memo.d.ts +15 -0
  139. package/dist/modules/each-memo.d.ts.map +1 -0
  140. package/dist/modules/each-memo.js +115 -0
  141. package/dist/modules/each-memo.js.map +1 -0
  142. package/dist/modules/effect-without-handler.d.ts +3 -0
  143. package/dist/modules/effect-without-handler.d.ts.map +1 -0
  144. package/dist/modules/effect-without-handler.js +92 -0
  145. package/dist/modules/effect-without-handler.js.map +1 -0
  146. package/dist/modules/element-rewrite.d.ts +22 -0
  147. package/dist/modules/element-rewrite.d.ts.map +1 -0
  148. package/dist/modules/element-rewrite.js +1017 -0
  149. package/dist/modules/element-rewrite.js.map +1 -0
  150. package/dist/modules/empty-props.d.ts +3 -0
  151. package/dist/modules/empty-props.d.ts.map +1 -0
  152. package/dist/modules/empty-props.js +50 -0
  153. package/dist/modules/empty-props.js.map +1 -0
  154. package/dist/modules/exhaustive-effect-handling.d.ts +3 -0
  155. package/dist/modules/exhaustive-effect-handling.d.ts.map +1 -0
  156. package/dist/modules/exhaustive-effect-handling.js +61 -0
  157. package/dist/modules/exhaustive-effect-handling.js.map +1 -0
  158. package/dist/modules/exhaustive-update.d.ts +3 -0
  159. package/dist/modules/exhaustive-update.d.ts.map +1 -0
  160. package/dist/modules/exhaustive-update.js +146 -0
  161. package/dist/modules/exhaustive-update.js.map +1 -0
  162. package/dist/modules/forgotten-spread.d.ts +3 -0
  163. package/dist/modules/forgotten-spread.d.ts.map +1 -0
  164. package/dist/modules/forgotten-spread.js +51 -0
  165. package/dist/modules/forgotten-spread.js.map +1 -0
  166. package/dist/modules/form-boilerplate.d.ts +3 -0
  167. package/dist/modules/form-boilerplate.d.ts.map +1 -0
  168. package/dist/modules/form-boilerplate.js +101 -0
  169. package/dist/modules/form-boilerplate.js.map +1 -0
  170. package/dist/modules/imperative-dom-in-view.d.ts +3 -0
  171. package/dist/modules/imperative-dom-in-view.d.ts.map +1 -0
  172. package/dist/modules/imperative-dom-in-view.js +123 -0
  173. package/dist/modules/imperative-dom-in-view.js.map +1 -0
  174. package/dist/modules/item-dedup.d.ts +7 -0
  175. package/dist/modules/item-dedup.d.ts.map +1 -0
  176. package/dist/modules/item-dedup.js +204 -0
  177. package/dist/modules/item-dedup.js.map +1 -0
  178. package/dist/modules/map-on-state-array.d.ts +3 -0
  179. package/dist/modules/map-on-state-array.d.ts.map +1 -0
  180. package/dist/modules/map-on-state-array.js +84 -0
  181. package/dist/modules/map-on-state-array.js.map +1 -0
  182. package/dist/modules/mask-legend.d.ts +10 -0
  183. package/dist/modules/mask-legend.d.ts.map +1 -0
  184. package/dist/modules/mask-legend.js +50 -0
  185. package/dist/modules/mask-legend.js.map +1 -0
  186. package/dist/modules/missing-memo.d.ts +3 -0
  187. package/dist/modules/missing-memo.d.ts.map +1 -0
  188. package/dist/modules/missing-memo.js +114 -0
  189. package/dist/modules/missing-memo.js.map +1 -0
  190. package/dist/modules/msg-annotations.d.ts +9 -0
  191. package/dist/modules/msg-annotations.d.ts.map +1 -0
  192. package/dist/modules/msg-annotations.js +54 -0
  193. package/dist/modules/msg-annotations.js.map +1 -0
  194. package/dist/modules/msg-schema.d.ts +10 -0
  195. package/dist/modules/msg-schema.d.ts.map +1 -0
  196. package/dist/modules/msg-schema.js +70 -0
  197. package/dist/modules/msg-schema.js.map +1 -0
  198. package/dist/modules/namespace-import.d.ts +3 -0
  199. package/dist/modules/namespace-import.d.ts.map +1 -0
  200. package/dist/modules/namespace-import.js +80 -0
  201. package/dist/modules/namespace-import.js.map +1 -0
  202. package/dist/modules/nested-send-in-update.d.ts +3 -0
  203. package/dist/modules/nested-send-in-update.d.ts.map +1 -0
  204. package/dist/modules/nested-send-in-update.js +77 -0
  205. package/dist/modules/nested-send-in-update.js.map +1 -0
  206. package/dist/modules/no-barrel-import-when-subpath-exists.d.ts +3 -0
  207. package/dist/modules/no-barrel-import-when-subpath-exists.d.ts.map +1 -0
  208. package/dist/modules/no-barrel-import-when-subpath-exists.js +100 -0
  209. package/dist/modules/no-barrel-import-when-subpath-exists.js.map +1 -0
  210. package/dist/modules/no-eager-item-accessor.d.ts +3 -0
  211. package/dist/modules/no-eager-item-accessor.d.ts.map +1 -0
  212. package/dist/modules/no-eager-item-accessor.js +74 -0
  213. package/dist/modules/no-eager-item-accessor.js.map +1 -0
  214. package/dist/modules/no-let-reactive-accessor.d.ts +3 -0
  215. package/dist/modules/no-let-reactive-accessor.d.ts.map +1 -0
  216. package/dist/modules/no-let-reactive-accessor.js +227 -0
  217. package/dist/modules/no-let-reactive-accessor.js.map +1 -0
  218. package/dist/modules/no-list-render-in-sample.d.ts +3 -0
  219. package/dist/modules/no-list-render-in-sample.d.ts.map +1 -0
  220. package/dist/modules/no-list-render-in-sample.js +89 -0
  221. package/dist/modules/no-list-render-in-sample.js.map +1 -0
  222. package/dist/modules/no-sample-in-accessor.d.ts +3 -0
  223. package/dist/modules/no-sample-in-accessor.d.ts.map +1 -0
  224. package/dist/modules/no-sample-in-accessor.js +141 -0
  225. package/dist/modules/no-sample-in-accessor.js.map +1 -0
  226. package/dist/modules/no-sample-in-reactive-position.d.ts +3 -0
  227. package/dist/modules/no-sample-in-reactive-position.d.ts.map +1 -0
  228. package/dist/modules/no-sample-in-reactive-position.js +72 -0
  229. package/dist/modules/no-sample-in-reactive-position.js.map +1 -0
  230. package/dist/modules/pure-update-function.d.ts +3 -0
  231. package/dist/modules/pure-update-function.d.ts.map +1 -0
  232. package/dist/modules/pure-update-function.js +127 -0
  233. package/dist/modules/pure-update-function.js.map +1 -0
  234. package/dist/modules/reactive-paths.d.ts +3 -0
  235. package/dist/modules/reactive-paths.d.ts.map +1 -0
  236. package/dist/modules/reactive-paths.js +77 -0
  237. package/dist/modules/reactive-paths.js.map +1 -0
  238. package/dist/modules/row-factory.d.ts +12 -0
  239. package/dist/modules/row-factory.d.ts.map +1 -0
  240. package/dist/modules/row-factory.js +385 -0
  241. package/dist/modules/row-factory.js.map +1 -0
  242. package/dist/modules/schema-hash.d.ts +15 -0
  243. package/dist/modules/schema-hash.d.ts.map +1 -0
  244. package/dist/modules/schema-hash.js +70 -0
  245. package/dist/modules/schema-hash.js.map +1 -0
  246. package/dist/modules/spread-in-children.d.ts +3 -0
  247. package/dist/modules/spread-in-children.d.ts.map +1 -0
  248. package/dist/modules/spread-in-children.js +144 -0
  249. package/dist/modules/spread-in-children.js.map +1 -0
  250. package/dist/modules/state-mutation.d.ts +3 -0
  251. package/dist/modules/state-mutation.d.ts.map +1 -0
  252. package/dist/modules/state-mutation.js +138 -0
  253. package/dist/modules/state-mutation.js.map +1 -0
  254. package/dist/modules/state-schema.d.ts +8 -0
  255. package/dist/modules/state-schema.d.ts.map +1 -0
  256. package/dist/modules/state-schema.js +55 -0
  257. package/dist/modules/state-schema.js.map +1 -0
  258. package/dist/modules/static-items.d.ts +3 -0
  259. package/dist/modules/static-items.d.ts.map +1 -0
  260. package/dist/modules/static-items.js +125 -0
  261. package/dist/modules/static-items.js.map +1 -0
  262. package/dist/modules/static-on.d.ts +3 -0
  263. package/dist/modules/static-on.d.ts.map +1 -0
  264. package/dist/modules/static-on.js +100 -0
  265. package/dist/modules/static-on.js.map +1 -0
  266. package/dist/modules/string-effect-callback.d.ts +3 -0
  267. package/dist/modules/string-effect-callback.d.ts.map +1 -0
  268. package/dist/modules/string-effect-callback.js +50 -0
  269. package/dist/modules/string-effect-callback.js.map +1 -0
  270. package/dist/modules/structural-mask.d.ts +8 -0
  271. package/dist/modules/structural-mask.d.ts.map +1 -0
  272. package/dist/modules/structural-mask.js +76 -0
  273. package/dist/modules/structural-mask.js.map +1 -0
  274. package/dist/modules/subapp-requires-reason.d.ts +3 -0
  275. package/dist/modules/subapp-requires-reason.d.ts.map +1 -0
  276. package/dist/modules/subapp-requires-reason.js +129 -0
  277. package/dist/modules/subapp-requires-reason.js.map +1 -0
  278. package/dist/modules/text-mask.d.ts +12 -0
  279. package/dist/modules/text-mask.d.ts.map +1 -0
  280. package/dist/modules/text-mask.js +63 -0
  281. package/dist/modules/text-mask.js.map +1 -0
  282. package/dist/modules/view-bag-import.d.ts +3 -0
  283. package/dist/modules/view-bag-import.d.ts.map +1 -0
  284. package/dist/modules/view-bag-import.js +80 -0
  285. package/dist/modules/view-bag-import.js.map +1 -0
  286. package/dist/msg-annotations.d.ts +104 -0
  287. package/dist/msg-annotations.d.ts.map +1 -0
  288. package/dist/msg-annotations.js +242 -0
  289. package/dist/msg-annotations.js.map +1 -0
  290. package/dist/msg-schema.d.ts +130 -0
  291. package/dist/msg-schema.d.ts.map +1 -0
  292. package/dist/msg-schema.js +770 -0
  293. package/dist/msg-schema.js.map +1 -0
  294. package/dist/schema-hash.d.ts +16 -0
  295. package/dist/schema-hash.d.ts.map +1 -0
  296. package/dist/schema-hash.js +31 -0
  297. package/dist/schema-hash.js.map +1 -0
  298. package/dist/state-schema.d.ts +41 -0
  299. package/dist/state-schema.d.ts.map +1 -0
  300. package/dist/state-schema.js +156 -0
  301. package/dist/state-schema.js.map +1 -0
  302. package/dist/transform.d.ts +109 -0
  303. package/dist/transform.d.ts.map +1 -0
  304. package/dist/transform.js +1390 -0
  305. package/dist/transform.js.map +1 -0
  306. package/dist/version.d.ts +11 -0
  307. package/dist/version.d.ts.map +1 -0
  308. package/dist/version.js +11 -0
  309. package/dist/version.js.map +1 -0
  310. package/package.json +47 -0
@@ -0,0 +1,1390 @@
1
+ import ts from 'typescript';
2
+ import { collectDeps } from './collect-deps.js';
3
+ import { resolveAccessorBody } from './accessor-resolver.js';
4
+ import { extractMsgSchema, extractEffectSchema } from './msg-schema.js';
5
+ import { extractMsgAnnotations } from './msg-annotations.js';
6
+ import { extractStateSchema } from './state-schema.js';
7
+ // `computeSchemaHash` no longer imported here — `schemaHashModule` owns
8
+ // the __schemaHash emission. See modules/schema-hash.ts.
9
+ // `tagDispatchHandlers` + `injectScopeVariantRegistrations` are now
10
+ // consumed by `bindingDescriptorsModule` via the registry's preTransform
11
+ // hook — no longer imported here. See modules/binding-descriptors.ts.
12
+ import { compilerCache } from './compiler-cache.js';
13
+ import { ModuleRegistry } from './module.js';
14
+ // Introspection modules (v2c/decomp-26) and devtools modules
15
+ // (v2c/decomp-27) moved to their sibling packages. Hosts register
16
+ // factories via `registerIntrospectionFactory` / `registerDevtoolsFactory`;
17
+ // transformLlui invokes them via the getters when their respective
18
+ // modes are active. BINDING_DESCRIPTORS_SLOT is re-exported from
19
+ // introspection-factory.ts as a string constant so the orchestrator
20
+ // can read the slot without importing the sibling package.
21
+ import { getIntrospectionFactory, getDevtoolsFactory, BINDING_DESCRIPTORS_SLOT, } from './introspection-factory.js';
22
+ import { maskLegendModule } from './modules/mask-legend.js';
23
+ import { compilerStampModule } from './modules/compiler-stamp.js';
24
+ import { eachMemoModule, EACH_MEMO_SLOT } from './modules/each-memo.js';
25
+ import { structuralMaskModule } from './modules/structural-mask.js';
26
+ import { textMaskModule } from './modules/text-mask.js';
27
+ import { itemDedupModule } from './modules/item-dedup.js';
28
+ import { elementRewriteModule, ELEMENT_REWRITE_SLOT, } from './modules/element-rewrite.js';
29
+ import { rowFactoryModule } from './modules/row-factory.js';
30
+ import { coreSynthesisModule, CORE_SYNTHESIS_SLOT, } from './modules/core-synthesis.js';
31
+ import { bitmaskOverflowModule } from './modules/bitmask-overflow.js';
32
+ import { asyncUpdateModule } from './modules/async-update.js';
33
+ import { mapOnStateArrayModule } from './modules/map-on-state-array.js';
34
+ import { nestedSendInUpdateModule } from './modules/nested-send-in-update.js';
35
+ import { directStateInViewModule } from './modules/direct-state-in-view.js';
36
+ import { imperativeDomInViewModule } from './modules/imperative-dom-in-view.js';
37
+ import { accessorSideEffectModule } from './modules/accessor-side-effect.js';
38
+ import { stateMutationModule } from './modules/state-mutation.js';
39
+ import { effectWithoutHandlerModule } from './modules/effect-without-handler.js';
40
+ import { exhaustiveEffectHandlingModule } from './modules/exhaustive-effect-handling.js';
41
+ import { noEagerItemAccessorModule } from './modules/no-eager-item-accessor.js';
42
+ import { pureUpdateFunctionModule } from './modules/pure-update-function.js';
43
+ import { exhaustiveUpdateModule } from './modules/exhaustive-update.js';
44
+ import { noLetReactiveAccessorModule } from './modules/no-let-reactive-accessor.js';
45
+ import { eachClosureViolationModule } from './modules/each-closure-violation.js';
46
+ import { stringEffectCallbackModule } from './modules/string-effect-callback.js';
47
+ import { agentMissingIntentModule } from './modules/agent-missing-intent.js';
48
+ import { agentWarningOnConfirmModule } from './modules/agent-warning-on-confirm.js';
49
+ import { agentExampleOnPayloadModule } from './modules/agent-example-on-payload.js';
50
+ import { agentExclusiveAnnotationsModule } from './modules/agent-exclusive-annotations.js';
51
+ import { agentOptionalFieldUndocumentedModule } from './modules/agent-optional-field-undocumented.js';
52
+ import { agentTagsendTranslatorMissingModule } from './modules/agent-tagsend-translator-missing.js';
53
+ import { agentNonextractableHandlerModule } from './modules/agent-nonextractable-handler.js';
54
+ import { subappRequiresReasonModule } from './modules/subapp-requires-reason.js';
55
+ import { emptyPropsModule } from './modules/empty-props.js';
56
+ import { forgottenSpreadModule } from './modules/forgotten-spread.js';
57
+ import { accessibilityModule } from './modules/accessibility.js';
58
+ import { viewBagImportModule } from './modules/view-bag-import.js';
59
+ import { controlledInputModule } from './modules/controlled-input.js';
60
+ import { missingMemoModule } from './modules/missing-memo.js';
61
+ import { namespaceImportModule } from './modules/namespace-import.js';
62
+ import { noBarrelImportWhenSubpathExistsModule } from './modules/no-barrel-import-when-subpath-exists.js';
63
+ import { formBoilerplateModule } from './modules/form-boilerplate.js';
64
+ import { spreadInChildrenModule } from './modules/spread-in-children.js';
65
+ import { staticItemsModule } from './modules/static-items.js';
66
+ import { staticOnModule } from './modules/static-on.js';
67
+ import { noListRenderInSampleModule } from './modules/no-list-render-in-sample.js';
68
+ import { noSampleInAccessorModule } from './modules/no-sample-in-accessor.js';
69
+ import { noSampleInReactivePositionModule } from './modules/no-sample-in-reactive-position.js';
70
+ import { agentEmitsDriftModule } from './modules/agent-emits-drift.js';
71
+ import { agentMsgResolvableModule } from './modules/agent-msg-resolvable.js';
72
+ export function createMaskLiteral(f, mask) {
73
+ if (mask >= 0)
74
+ return f.createNumericLiteral(mask);
75
+ // -1 (0xFFFFFFFF | 0) — emit as bitwise OR: 0xFFFFFFFF | 0
76
+ return f.createBinaryExpression(f.createNumericLiteral(0xffffffff), ts.SyntaxKind.BarToken, f.createNumericLiteral(0));
77
+ }
78
+ // HTML element helper names that the compiler can transform
79
+ const ELEMENT_HELPERS = new Set([
80
+ 'a',
81
+ 'abbr',
82
+ 'article',
83
+ 'aside',
84
+ 'b',
85
+ 'blockquote',
86
+ 'br',
87
+ 'button',
88
+ 'canvas',
89
+ 'code',
90
+ 'dd',
91
+ 'details',
92
+ 'dialog',
93
+ 'div',
94
+ 'dl',
95
+ 'dt',
96
+ 'em',
97
+ 'fieldset',
98
+ 'figcaption',
99
+ 'figure',
100
+ 'footer',
101
+ 'form',
102
+ 'h1',
103
+ 'h2',
104
+ 'h3',
105
+ 'h4',
106
+ 'h5',
107
+ 'h6',
108
+ 'header',
109
+ 'hr',
110
+ 'i',
111
+ 'iframe',
112
+ 'img',
113
+ 'input',
114
+ 'label',
115
+ 'legend',
116
+ 'li',
117
+ 'main',
118
+ 'mark',
119
+ 'nav',
120
+ 'ol',
121
+ 'optgroup',
122
+ 'option',
123
+ 'output',
124
+ 'p',
125
+ 'pre',
126
+ 'progress',
127
+ 'section',
128
+ 'select',
129
+ 'small',
130
+ 'span',
131
+ 'strong',
132
+ 'sub',
133
+ 'summary',
134
+ 'sup',
135
+ 'table',
136
+ 'tbody',
137
+ 'td',
138
+ 'textarea',
139
+ 'tfoot',
140
+ 'th',
141
+ 'thead',
142
+ 'time',
143
+ 'tr',
144
+ 'ul',
145
+ 'video',
146
+ ]);
147
+ export function transformLlui(source, _filename, devMode = false, emitAgentMetadata = false, mcpPort = 5200, verbose = false, typeSources, preExtracted, crossFilePaths) {
148
+ // Use the caller-provided filename so any module reading `sf.fileName`
149
+ // (e.g. `componentMetaModule` emitting `__componentMeta: { file }`)
150
+ // sees the real path instead of a placeholder. The monolith's inline
151
+ // `injectComponentMeta` used the `_filename` parameter directly; the
152
+ // bridge needs the same source via the AST node.
153
+ let sourceFile = ts.createSourceFile(_filename, source, ts.ScriptTarget.Latest, true);
154
+ // Find the @llui/dom import
155
+ const imp = findLluiImport(sourceFile);
156
+ if (!imp)
157
+ return null;
158
+ const lluiImport = imp;
159
+ // Collect imported element helper names (local → original)
160
+ const importedHelpers = getImportedHelpers(lluiImport);
161
+ // Previously: `if (importedHelpers.size === 0 && !hasReactiveAccessors(sourceFile)) return null`.
162
+ // Removed in lint-migration-8: lint-rule modules (namespace-import,
163
+ // form-boilerplate, etc.) need to fire on files whose only @llui/dom
164
+ // usage is a namespace import, a type-only import, or a Msg-union
165
+ // declaration — none of which produce element-helper or text/component
166
+ // call sites. The registry's per-file cost is small enough that running
167
+ // it on these files is fine; the late return-null still bails when
168
+ // there's no work to emit (no edits AND no diagnostics).
169
+ // Connect-pattern pass: detects `*.connect(get, sendFn, …)` call
170
+ // sites and inserts a runtime `__registerScopeVariants([...])`
171
+ // adjacent to each, with the variants statically extracted from the
172
+ // sendFn's body. Handles the dispatch-translation case at the
173
+ // syntactic level — handler propagation via `tagSend` covers the
174
+ // rest. Runs FIRST so its `collectLocalFns` resolver still sees raw
175
+ // arrow initializers in const declarations (the universal tagger
176
+ // below replaces those initializers with `Object.assign(...)`
177
+ // wrappers).
178
+ //
179
+ // Universal handler-tagger pass: walks every arrow/function
180
+ // expression and wraps any whose body contains literal
181
+ // `send({type:'X'})` / `dispatch({type:'X'})` calls with
182
+ // `Object.assign(arrow, {__lluiVariants: ['X']})`. The runtime
183
+ // (`@llui/dom` `elements.ts` / `el-split.ts`) reads the tag from
184
+ // event-handler bindings only — so tags placed on functions in
185
+ // non-handler positions are runtime-inert. This deliberately covers
186
+ // three patterns at once:
187
+ // • Inline event handlers (`onClick: () => send(...)`)
188
+ // • Const-bound translators (`const sendMenu = (m) => dispatch(...)`)
189
+ // • Positional-arg helpers (`navButton(label, () => dispatch(...))`)
190
+ //
191
+ // Both passes gated on dev/agent-metadata so production bundles
192
+ // without agent integration don't pay the per-handler `Object.assign`
193
+ // cost.
194
+ // Binding-descriptors pre-pass migrated to `bindingDescriptorsModule`
195
+ // via the registry's `preTransform` hook (v2c/decomp-7). The module
196
+ // wraps `injectScopeVariantRegistrations` + `tagDispatchHandlers` and
197
+ // runs inside `registry.run()` below — the resulting (post-transform)
198
+ // sourceFile is re-assigned from `registryResult.analysis.sourceFile`.
199
+ // `scopeRegistrationsInjected` reads from the module's slot after
200
+ // the registry run.
201
+ let scopeRegistrationsInjected = false;
202
+ // Pass 2 pre-scan: collect all state access paths
203
+ // Only use precise masks in files that define a component() — the __dirty
204
+ // function is generated per-component, so bit assignments in other files
205
+ // won't match. Files without component() get FULL_MASK on all bindings.
206
+ const fileHasComponent = hasComponentDef(sourceFile, lluiImport);
207
+ const { lo: fieldBits, hi: fieldBitsHi } = fileHasComponent
208
+ ? collectDeps(source, crossFilePaths)
209
+ : { lo: new Map(), hi: new Map() };
210
+ if (verbose && fileHasComponent) {
211
+ const pairs = [...fieldBits.entries()]
212
+ .map(([path, bit]) => `${path}=${bit === -1 ? 'FULL' : bit}`)
213
+ .join(', ');
214
+ console.info(`[llui] ${_filename}: ${fieldBits.size} reactive path${fieldBits.size === 1 ? '' : 's'}` +
215
+ (pairs.length > 0 ? ` — ${pairs}` : ''));
216
+ }
217
+ // Identifier names bound to the View<S,M> helpers parameter of a `view` callback.
218
+ // When the user writes `h.text(...)` / `h.show(...)` / `h.each(...)`, the
219
+ // compiler treats the call as if it were a bare import call.
220
+ const viewHelperNames = collectViewHelperNames(sourceFile, lluiImport);
221
+ // Destructured aliases: `view: (_, { show, text: t }) => [...]` → { show→show, t→text }.
222
+ const viewHelperAliases = collectViewHelperAliases(sourceFile, lluiImport, viewHelperNames);
223
+ // Track which helpers were compiled vs bailed out
224
+ const compiledHelpers = new Set();
225
+ const bailedHelpers = new Set();
226
+ let usesElTemplate = false;
227
+ let usesElSplit = false;
228
+ let usesMemo = false;
229
+ let usesApplyBinding = false;
230
+ let usesCloneStaticTemplate = false;
231
+ const f = ts.factory;
232
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
233
+ // Collect source positions of transformed nodes for source mapping
234
+ const edits = [];
235
+ // ── Module registry (v2c §2) ────────────────────────────────────
236
+ //
237
+ // Active CompilerModules run as a single AST pass over the source
238
+ // before the main visitor; their emissions are spliced into the
239
+ // matching `component()` call's config-arg by `applyRegistryEmissions`
240
+ // alongside the monolith's inline `inject*` helpers. As modules
241
+ // incrementally absorb inline-injector concerns (`__componentMeta`,
242
+ // `__stateSchema` today, `__msgSchema` / `__msgAnnotations` /
243
+ // `__schemaHash` / `__prefixes` next), the matching inline call
244
+ // sites delete and the registry becomes the sole emission path.
245
+ //
246
+ // Module activation rules:
247
+ // - `componentMetaModule` registers only in `devMode` (matches the
248
+ // monolith's `if (devMode) injectComponentMeta(...)` gate).
249
+ // - `stateSchemaModule` registers when `shouldEmitAgentMetadata`
250
+ // (devMode OR emitAgentMetadata flag) — matches the monolith's
251
+ // gating block at the inline-inject sites.
252
+ // - All other agent modules are dormant in this push.
253
+ //
254
+ // When the registry is empty the bridge collapses to a no-op — the
255
+ // monolith's existing emissions continue to dominate.
256
+ const shouldEmitAgentMetadataAtToplevel = devMode || emitAgentMetadata;
257
+ // Pre-compute the state schema once per file. The monolith previously
258
+ // computed this inside the visitor's component() block (re-running
259
+ // per call site); since the inputs are file-level (typeSources,
260
+ // preExtracted, source) the result is identical across calls, so we
261
+ // hoist for the module-registration. The inline computation below
262
+ // (in the visitor's `if (shouldEmitAgentMetadata)` block) gets its
263
+ // value from this same variable.
264
+ const hoistedStateSchema = shouldEmitAgentMetadataAtToplevel
265
+ ? preExtracted?.stateSchema !== undefined
266
+ ? preExtracted.stateSchema
267
+ : extractStateSchema(typeSources?.state?.source ?? source, typeSources?.state?.typeName ?? 'State')
268
+ : null;
269
+ const hoistedMsgAnnotations = shouldEmitAgentMetadataAtToplevel
270
+ ? preExtracted?.msgAnnotations !== undefined
271
+ ? preExtracted.msgAnnotations
272
+ : extractMsgAnnotations(typeSources?.msg?.source ?? source, typeSources?.msg?.typeName ?? 'Msg')
273
+ : null;
274
+ const hoistedMsgSchema = shouldEmitAgentMetadataAtToplevel
275
+ ? preExtracted?.msgSchema !== undefined
276
+ ? preExtracted.msgSchema
277
+ : extractMsgSchema(typeSources?.msg?.source ?? source, typeSources?.msg?.typeName ?? 'Msg')
278
+ : null;
279
+ const hoistedEffectSchema = shouldEmitAgentMetadataAtToplevel
280
+ ? preExtracted?.effectSchema !== undefined
281
+ ? preExtracted.effectSchema
282
+ : extractEffectSchema(typeSources?.effect?.source ?? source, typeSources?.effect?.typeName ?? 'Effect')
283
+ : null;
284
+ const activeModules = [];
285
+ // Devtools modules (componentMeta + future trace instrumentation)
286
+ // live in @llui/compiler-devtools. The host registers the factory
287
+ // once via `registerDevtoolsFactory`; the factory gates on devMode
288
+ // and any future per-module flags.
289
+ const devtools = getDevtoolsFactory();
290
+ if (devtools) {
291
+ activeModules.push(...devtools({ sourceFile, devMode }));
292
+ }
293
+ // Introspection modules (binding-descriptors, msg-schema,
294
+ // state-schema, msg-annotations, schema-hash) live in
295
+ // @llui/compiler-introspection. The host registers the factory
296
+ // once via `registerIntrospectionFactory`. The factory itself
297
+ // decides which modules to activate based on
298
+ // `shouldEmitAgentMetadata`: producer modules + binding-descriptors
299
+ // run only in agent mode, but schemaHashModule runs always (HMR
300
+ // re-send gating uses it even in prod builds — spec §7.4).
301
+ const introspection = getIntrospectionFactory();
302
+ if (introspection) {
303
+ activeModules.push(...introspection({
304
+ sourceFile,
305
+ msgSchema: hoistedMsgSchema,
306
+ effectSchema: hoistedEffectSchema,
307
+ stateSchema: hoistedStateSchema,
308
+ msgAnnotations: hoistedMsgAnnotations,
309
+ shouldEmitAgentMetadata: shouldEmitAgentMetadataAtToplevel,
310
+ }));
311
+ }
312
+ // maskLegendModule registers whenever the file has any reactive paths.
313
+ // The module's own emit() gates on empty bit maps so registering
314
+ // here when fieldBits/fieldBitsHi are non-empty is sufficient.
315
+ if (fieldBits.size > 0 || fieldBitsHi.size > 0) {
316
+ activeModules.push(maskLegendModule({ fieldBits, fieldBitsHi }));
317
+ }
318
+ // compilerStampModule is unconditional — the integrity marker fires
319
+ // on every compiled `component()` call regardless of mode. Replaces
320
+ // the umbrella's last remaining inline injector
321
+ // (`injectCompilerEmittedMarker`, deleted below).
322
+ activeModules.push(compilerStampModule);
323
+ // bitmaskOverflowModule is always-on. Emits `llui/bitmask-overflow`
324
+ // (severity: error) when a component reads more than 62 unique state
325
+ // paths. Migrated from @llui/eslint-plugin (v0.x); promoted from
326
+ // ESLint warning to compiler error because LLMs ignore warnings.
327
+ activeModules.push(bitmaskOverflowModule());
328
+ // Correctness rules migrated from @llui/eslint-plugin as compile-time
329
+ // errors. LLM-first authoring requires non-bypassable correctness
330
+ // signals; ESLint warnings are silently ignored by LLM agents and
331
+ // may not even be installed in a downstream project.
332
+ activeModules.push(asyncUpdateModule());
333
+ activeModules.push(mapOnStateArrayModule());
334
+ activeModules.push(nestedSendInUpdateModule());
335
+ activeModules.push(directStateInViewModule());
336
+ activeModules.push(imperativeDomInViewModule());
337
+ activeModules.push(accessorSideEffectModule());
338
+ activeModules.push(stateMutationModule());
339
+ activeModules.push(effectWithoutHandlerModule());
340
+ activeModules.push(exhaustiveEffectHandlingModule());
341
+ activeModules.push(noEagerItemAccessorModule());
342
+ activeModules.push(pureUpdateFunctionModule());
343
+ activeModules.push(exhaustiveUpdateModule());
344
+ activeModules.push(noLetReactiveAccessorModule());
345
+ activeModules.push(eachClosureViolationModule());
346
+ activeModules.push(stringEffectCallbackModule());
347
+ activeModules.push(agentMissingIntentModule());
348
+ activeModules.push(agentWarningOnConfirmModule());
349
+ activeModules.push(agentExampleOnPayloadModule());
350
+ activeModules.push(agentExclusiveAnnotationsModule());
351
+ activeModules.push(agentOptionalFieldUndocumentedModule());
352
+ activeModules.push(agentTagsendTranslatorMissingModule());
353
+ activeModules.push(agentNonextractableHandlerModule());
354
+ activeModules.push(subappRequiresReasonModule());
355
+ activeModules.push(emptyPropsModule());
356
+ activeModules.push(forgottenSpreadModule());
357
+ activeModules.push(accessibilityModule());
358
+ activeModules.push(viewBagImportModule());
359
+ activeModules.push(controlledInputModule());
360
+ activeModules.push(missingMemoModule());
361
+ activeModules.push(namespaceImportModule());
362
+ activeModules.push(noBarrelImportWhenSubpathExistsModule());
363
+ activeModules.push(formBoilerplateModule());
364
+ activeModules.push(spreadInChildrenModule());
365
+ activeModules.push(staticItemsModule());
366
+ activeModules.push(staticOnModule());
367
+ activeModules.push(noListRenderInSampleModule());
368
+ activeModules.push(noSampleInAccessorModule());
369
+ activeModules.push(noSampleInReactivePositionModule());
370
+ activeModules.push(agentEmitsDriftModule());
371
+ activeModules.push(agentMsgResolvableModule());
372
+ // eachMemoModule wraps allocating each() items accessors in
373
+ // `memo(...)` via `transformCallEnter`. Activated when the file
374
+ // has any reactive paths (mirrors the inline call's gating).
375
+ // The module sets a per-file slot when at least one wrap fired;
376
+ // the umbrella reads it to decide whether `memo` needs to enter
377
+ // the @llui/dom imports via `cleanupImports`.
378
+ if (fieldBits.size > 0 || fieldBitsHi.size > 0) {
379
+ activeModules.push(eachMemoModule({
380
+ fieldBits,
381
+ viewHelperNames,
382
+ viewHelperAliases,
383
+ }));
384
+ }
385
+ // itemDedupModule lifts repeated `item(selector)` / `item.field`
386
+ // accesses into hoisted `__sN` selectors + `__aN` accessors.
387
+ // Registered unconditionally — the original inline call ran on every
388
+ // each() regardless of reactive paths, since the optimization is a
389
+ // pure rewrite of the render body. Module fires top-down
390
+ // (`transformCallEnter`) so subsequent structural-mask + element
391
+ // rewrites see the hoisted form.
392
+ activeModules.push(itemDedupModule({
393
+ viewHelperNames,
394
+ viewHelperAliases,
395
+ }));
396
+ // elementRewriteModule transforms `div(...)` / `button(...)` /
397
+ // etc. into `elSplit(...)` / `elTemplate(...)` / `__cloneStaticTemplate(...)`.
398
+ // Activated unconditionally — the underlying `tryTransformElementCall`
399
+ // gates on the `importedHelpers` map (only rewrites helpers that
400
+ // were actually imported from @llui/dom). The module signals which
401
+ // helpers compiled / bailed and which runtime functions need
402
+ // imports via `ELEMENT_REWRITE_SLOT`; the umbrella reads the slot
403
+ // before `cleanupImports`.
404
+ activeModules.push(elementRewriteModule({
405
+ importedHelpers,
406
+ fieldBits,
407
+ fieldBitsHi,
408
+ }));
409
+ // rowFactoryModule fires bottom-up (`transformCall` not
410
+ // `transformCallEnter`) so it observes the each() call AFTER the
411
+ // render body's element children have been rewritten by
412
+ // `elementRewriteModule` into `elTemplate(...)` calls. Without that
413
+ // ordering it would always bail (`if (!templateCall) return null`).
414
+ activeModules.push(rowFactoryModule({
415
+ viewHelperNames,
416
+ viewHelperAliases,
417
+ filename: _filename,
418
+ source,
419
+ }));
420
+ // coreSynthesisModule injects `__update` / `__handlers` / `__prefixes`
421
+ // onto every `component()` call's config-arg literal. These three
422
+ // share `topLevelBits` / `structuralMask` intermediates and are
423
+ // co-emitted by `tryInjectDirty` (still inline in transform.ts;
424
+ // the module is a thin wrapper per v2c §7.9.2 decision (b)). Fires
425
+ // top-down so subsequent per-target emissions (componentMeta,
426
+ // compilerStamp, schemaHash, stateSchema, msgSchema, msgAnnotations)
427
+ // observe the synthesized config-arg via `findComponentCalls`.
428
+ activeModules.push(coreSynthesisModule({
429
+ fieldBits,
430
+ fieldBitsHi,
431
+ lluiImport,
432
+ }));
433
+ // structuralMaskModule injects `__mask` into each()/branch()/scope()/show()
434
+ // options. Activated when the file has any low-word reactive paths
435
+ // (matches the inline `fieldBits.size === 0` early-return). Module
436
+ // fires top-down (transformCallEnter) so subsequent passes — and
437
+ // the visitor-level inline `tryInjectDirty`'s structuralMask read —
438
+ // see the injected `__mask` prop on the options literal.
439
+ if (fieldBits.size > 0) {
440
+ activeModules.push(structuralMaskModule({
441
+ fieldBits,
442
+ viewHelperNames,
443
+ viewHelperAliases,
444
+ }));
445
+ }
446
+ // textMaskModule injects a `__mask` (precise or FULL_MASK) as
447
+ // text()'s second argument on every eligible text() call. Activated
448
+ // unconditionally — the inline `tryInjectTextMask` had no early
449
+ // return on empty fieldBits and always emitted FULL_MASK in the
450
+ // zero-path case so the runtime sees a uniform 2-arg shape.
451
+ activeModules.push(textMaskModule({
452
+ fieldBits,
453
+ viewHelperNames,
454
+ viewHelperAliases,
455
+ lluiImport,
456
+ }));
457
+ const registry = new ModuleRegistry(activeModules);
458
+ const registryResult = registry.run(sourceFile);
459
+ // The registry phases (preTransform v2c/decomp-7, transformCall
460
+ // v2c/decomp-11/12) may have mutated the source file — replace our
461
+ // local reference so all subsequent code (fieldBits, visitor,
462
+ // cleanupImports) sees the post-registry AST. When no rewriting
463
+ // module is active this is a no-op assignment.
464
+ sourceFile = registryResult.analysis.sourceFile;
465
+ // Read the binding-descriptors module's slot for the
466
+ // cleanupImports decision about the `__registerScopeVariants`
467
+ // runtime helper import. When the module didn't run (no agent
468
+ // metadata mode) the slot is absent and scopeRegistrationsInjected
469
+ // stays false.
470
+ const bdState = registryResult.analysis.perModule.get(BINDING_DESCRIPTORS_SLOT);
471
+ if (bdState)
472
+ scopeRegistrationsInjected = bdState.scopeRegistrationsInjected;
473
+ // each-memo module signals memo-usage via its slot — surfaces here
474
+ // so `cleanupImports` adds the `memo` runtime import. Mirrors the
475
+ // monolith's `usesMemo` flag that the inline `tryWrapEachItemsWithMemo`
476
+ // used to set.
477
+ const emState = registryResult.analysis.perModule.get(EACH_MEMO_SLOT);
478
+ if (emState?.usesMemo)
479
+ usesMemo = true;
480
+ // element-rewrite module signals compiled/bailed helpers and which
481
+ // runtime imports it referenced. Surfaces here so the umbrella's
482
+ // `cleanupImports` decision matches the inline path's behavior.
483
+ // Also pushes a sentinel edit when the module rewrote — the
484
+ // `edits.length === 0` short-circuit downstream would otherwise
485
+ // skip the per-statement diff that surfaces Phase 2b rewrites to
486
+ // the output.
487
+ const erState = registryResult.analysis.perModule.get(ELEMENT_REWRITE_SLOT);
488
+ if (erState) {
489
+ for (const h of erState.compiled)
490
+ compiledHelpers.add(h);
491
+ for (const h of erState.bailed)
492
+ bailedHelpers.add(h);
493
+ if (erState.usesElSplit)
494
+ usesElSplit = true;
495
+ if (erState.usesElTemplate)
496
+ usesElTemplate = true;
497
+ if (erState.usesCloneStaticTemplate)
498
+ usesCloneStaticTemplate = true;
499
+ if (erState.compiled.size > 0 ||
500
+ erState.usesElSplit ||
501
+ erState.usesElTemplate ||
502
+ erState.usesCloneStaticTemplate) {
503
+ edits.push({ start: 0, end: 0, replacement: '' });
504
+ }
505
+ }
506
+ // core-synthesis module signals __update/__handlers/__prefixes
507
+ // emission. Drives `__runPhase2` + `__handleMsg` runtime imports
508
+ // via `cleanupImports`. Also pushes a sentinel edit so the
509
+ // per-statement diff catches the synthesized config-arg.
510
+ const csState = registryResult.analysis.perModule.get(CORE_SYNTHESIS_SLOT);
511
+ if (csState?.usesApplyBinding) {
512
+ usesApplyBinding = true;
513
+ edits.push({ start: 0, end: 0, replacement: '' });
514
+ }
515
+ const emissionsByTarget = new Map();
516
+ const globalEmissions = [];
517
+ for (const emission of registryResult.emissions) {
518
+ if (emission.target) {
519
+ const list = emissionsByTarget.get(emission.target);
520
+ if (list)
521
+ list.push(emission);
522
+ else
523
+ emissionsByTarget.set(emission.target, [emission]);
524
+ }
525
+ else {
526
+ globalEmissions.push(emission);
527
+ }
528
+ }
529
+ /**
530
+ * Splice registry-collected emissions into a `component()` call's
531
+ * config-arg object literal. Idempotent: emissions whose `field`
532
+ * already exists on the config arg are skipped (matches the
533
+ * monolith's inline `inject*` helpers' "if-already-present-return"
534
+ * behaviour). The caller passes both the *original* call (the one
535
+ * the registry walked) and the current *transformed* call (the
536
+ * accumulator that previous `inject*` helpers built up).
537
+ */
538
+ function applyRegistryEmissions(transformedCall, originalCall) {
539
+ const targeted = emissionsByTarget.get(originalCall) ?? [];
540
+ const all = [...globalEmissions, ...targeted];
541
+ if (all.length === 0)
542
+ return transformedCall;
543
+ const configArg = transformedCall.arguments[0];
544
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
545
+ return transformedCall;
546
+ const existing = new Set();
547
+ for (const prop of configArg.properties) {
548
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
549
+ existing.add(prop.name.text);
550
+ }
551
+ }
552
+ const adds = [];
553
+ for (const emission of all) {
554
+ if (existing.has(emission.field))
555
+ continue;
556
+ adds.push(f.createPropertyAssignment(emission.field, emission.value));
557
+ }
558
+ if (adds.length === 0)
559
+ return transformedCall;
560
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, ...adds], true);
561
+ return f.createCallExpression(transformedCall.expression, transformedCall.typeArguments, [
562
+ newConfig,
563
+ ...transformedCall.arguments.slice(1),
564
+ ]);
565
+ }
566
+ // ── track() strip pass (v2b §3) ─────────────────────────────────
567
+ // `track({ deps: (s) => [...] })` is a compile-time declaration only.
568
+ // collectDeps already merged its paths into `fieldBits` because 'track'
569
+ // is in REACTIVE_API_NAMES. Now emit an edit deleting the statement
570
+ // and mark 'track' as compiled so cleanupImports strips the import.
571
+ //
572
+ // A track() call is stripped only when it's the entire ExpressionStatement
573
+ // (the documented form). track() inside a larger expression is left as a
574
+ // call to the runtime stub — which throws — so the developer notices
575
+ // they wrote an unsupported form. The llui/prefer-static-deps lint
576
+ // rule catches the unusual usages.
577
+ // `track` is imported from @llui/dom but is not an element helper, so
578
+ // `importedHelpers.has('track')` is always false. Check the named
579
+ // imports directly to gate the strip pass.
580
+ const lluiImportNames = new Set();
581
+ if (lluiImport.importClause?.namedBindings &&
582
+ ts.isNamedImports(lluiImport.importClause.namedBindings)) {
583
+ for (const spec of lluiImport.importClause.namedBindings.elements) {
584
+ lluiImportNames.add(spec.name.text);
585
+ }
586
+ }
587
+ // Track-strip recognition. The main visitor returns an EmptyStatement
588
+ // for any `track({ deps: ... })` ExpressionStatement — the per-statement
589
+ // diff then surfaces the deletion. Paths are already in fieldBits
590
+ // because 'track' is in REACTIVE_API_NAMES.
591
+ if (lluiImportNames.has('track')) {
592
+ // Pre-scan to detect track() use so cleanupImports strips the import.
593
+ let foundTrackCall = false;
594
+ const visitForTrackDetect = (node) => {
595
+ if (foundTrackCall)
596
+ return;
597
+ if (ts.isExpressionStatement(node) &&
598
+ ts.isCallExpression(node.expression) &&
599
+ ts.isIdentifier(node.expression.expression) &&
600
+ node.expression.expression.text === 'track') {
601
+ foundTrackCall = true;
602
+ return;
603
+ }
604
+ ts.forEachChild(node, visitForTrackDetect);
605
+ };
606
+ visitForTrackDetect(sourceFile);
607
+ if (foundTrackCall)
608
+ compiledHelpers.add('track');
609
+ }
610
+ function visitor(node) {
611
+ // Synthetic nodes (created by ts.factory) don't have real positions
612
+ const hasPos = node.pos >= 0 && node.end >= 0;
613
+ const origStart = hasPos ? node.getStart(sourceFile) : -1;
614
+ const origEnd = hasPos ? node.getEnd() : -1;
615
+ // track({ deps: (s) => [...] }) is a compile-time declaration — paths
616
+ // already in fieldBits, call expression is dead weight. Return an
617
+ // EmptyStatement so the per-statement diff strips it from the output
618
+ // (cleanupImports separately removes the `track` import via the
619
+ // compiledHelpers set populated above).
620
+ if (ts.isExpressionStatement(node) &&
621
+ ts.isCallExpression(node.expression) &&
622
+ ts.isIdentifier(node.expression.expression) &&
623
+ node.expression.expression.text === 'track' &&
624
+ lluiImportNames.has('track')) {
625
+ return f.createEmptyStatement();
626
+ }
627
+ // each() optimizations + row-factory all migrated to modules:
628
+ // - memo-wrap → eachMemoModule (v2c/decomp-13)
629
+ // - item-dedup → itemDedupModule (v2c/decomp-16)
630
+ // - structural-mask → structuralMaskModule (v2c/decomp-14)
631
+ // - row-factory → rowFactoryModule (v2c/decomp-18)
632
+ // Phase 2b chains the top-down hooks (memo, dedup, mask, element)
633
+ // then recurses, then fires the bottom-up exit hook (row-factory).
634
+ // The visitor's inline each() block is gone.
635
+ if (ts.isCallExpression(node)) {
636
+ // Pass 1: element rewrite (`div` → `elSplit`/`elTemplate`/
637
+ // `__cloneStaticTemplate`) moved to `elementRewriteModule`
638
+ // (v2c/decomp-17). Phase 2b ran before this visitor; the call
639
+ // is already rewritten on `node` when it was eligible.
640
+ // text() mask injection moved to `textMaskModule` (v2c/decomp-15).
641
+ // Structural-mask injection (each/branch/scope/show) moved to
642
+ // `structuralMaskModule` (v2c/decomp-14). Both ran in Phase 2b
643
+ // before this visitor; nothing to do here.
644
+ }
645
+ // Pass 2: component() metadata splicing. Core synthesis
646
+ // (__update/__handlers/__prefixes) moved to `coreSynthesisModule`
647
+ // (v2c/decomp-19) — fires top-down in Phase 2b before this
648
+ // visitor sees the call. The remaining work here is just the
649
+ // registry-emission splice (per-target __componentMeta,
650
+ // __compilerVersion, __lluiCompilerEmitted, __schemaHash,
651
+ // __stateSchema, __msgSchema, __effectSchema, __msgAnnotations).
652
+ if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
653
+ let result = null;
654
+ // Extract schema data once — used both for devMode injections and the
655
+ // unconditional __schemaHash (spec §7.4: hash ships in prod too).
656
+ //
657
+ // Resolution priority for each schema:
658
+ // 1. preExtracted.* — used when the plugin's async hook has already
659
+ // done cross-file + composition extraction (the production path).
660
+ // 2. typeSources.* — file-local extraction against an alternate
661
+ // source file (legacy path; covers cross-file but not composition).
662
+ // 3. file-local — the test path: extract from `source` itself.
663
+ //
664
+ // When `preExtracted` is provided, treat it as authoritative even
665
+ // when the value is `null` (the resolver was run and found
666
+ // nothing) — falling back to local extraction would mask the
667
+ // resolver's "not extractable" verdict.
668
+ // `msgSchema` previously computed here; uses the pre-hoisted value.
669
+ const msgSchema = hoistedMsgSchema;
670
+ // `msgAnnotations` previously computed here; uses the pre-hoisted
671
+ // value so the module + the inline `computeSchemaHash` call see
672
+ // the same input.
673
+ const msgAnnotations = hoistedMsgAnnotations;
674
+ // `stateSchema` was previously computed here; it now uses the
675
+ // pre-hoisted value (file-level inputs only). The `__stateSchema`
676
+ // emission migrated to `stateSchemaModule` via the registry
677
+ // bridge — the inline `injectStateSchema` call below deletes.
678
+ const stateSchema = hoistedStateSchema;
679
+ const shouldEmitAgentMetadata = devMode || emitAgentMetadata;
680
+ if (shouldEmitAgentMetadata) {
681
+ // __msgSchema: migrated to msgSchemaModule (v2c/decomp-5).
682
+ // __msgAnnotations: migrated to msgAnnotationsModule (v2c/decomp-4).
683
+ // __stateSchema: migrated to stateSchemaModule (v2c/decomp-3).
684
+ // __effectSchema: migrated to msgSchemaModule (handles both
685
+ // the Msg and Effect discriminated-union shapes since they
686
+ // share the same wire format).
687
+ const effectSchema = hoistedEffectSchema;
688
+ void effectSchema; // referenced by hoistedEffectSchema's narrow scope; the
689
+ // cache snapshot below reads msgSchema directly.
690
+ // Note: binding descriptors are no longer emitted on the
691
+ // component def. They're now collected at runtime by walking
692
+ // event-handler arrows that the `tagEventHandlerSends` pass
693
+ // wrapped with `__lluiVariants` metadata. See
694
+ // `binding-descriptors.ts` (compiler) and the matching
695
+ // `@llui/dom binding-descriptors.ts` (runtime registry).
696
+ // Populate compiler cache — preSource and msgMaskMap are known now;
697
+ // postSource is filled in after the full output is assembled.
698
+ const cachedComponentName = extractComponentNameFromConfig(node);
699
+ if (cachedComponentName) {
700
+ const preSource = extractViewBody(source) ?? '';
701
+ const msgMaskMap = {};
702
+ for (const [path, bit] of fieldBits) {
703
+ msgMaskMap[path] = bit;
704
+ }
705
+ compilerCache.set(cachedComponentName, {
706
+ preSource,
707
+ postSource: '',
708
+ msgMaskMap,
709
+ bindingSources: [],
710
+ });
711
+ }
712
+ }
713
+ // v2c §2 bridge: registry-emission splicing replaces the inline
714
+ // `injectComponentMeta` here. `componentMetaModule` is registered
715
+ // when `devMode` is true, so the dev-mode gating semantics match
716
+ // the prior `if (devMode)` guard. When `devMode` is false the
717
+ // registry is empty and this call is a no-op.
718
+ result = applyRegistryEmissions(result ?? node, node);
719
+ // __schemaHash: migrated to schemaHashModule (v2c/decomp-5).
720
+ // When shouldEmitAgentMetadata is true, schemaHashModule is in
721
+ // the active module list and produces the emission via the
722
+ // bridge. Out-of-agent-mode files don't emit __schemaHash today
723
+ // either (the monolith's inline path was always-on, but the
724
+ // hash only matters when the runtime is in agent mode — see
725
+ // agent spec §12.3). Aligned during the migration.
726
+ //
727
+ // The `msgSchema` / `stateSchema` / `msgAnnotations` variables
728
+ // remain in scope so they can feed the cache snapshot below.
729
+ void msgSchema;
730
+ void stateSchema;
731
+ void msgAnnotations;
732
+ // `__lluiCompilerEmitted` + `__compilerVersion` migrated to
733
+ // `compilerStampModule` (v2c/decomp-10). They flow through the
734
+ // same `applyRegistryEmissions` splice as every other registry
735
+ // contribution; the umbrella now contains zero inline injectors.
736
+ if (result) {
737
+ if (hasPos)
738
+ edits.push({ start: origStart, end: origEnd, replacement: '' });
739
+ return ts.visitEachChild(result, visitor, undefined);
740
+ }
741
+ }
742
+ return ts.visitEachChild(node, visitor, undefined);
743
+ }
744
+ let transformed = ts.visitNode(sourceFile, visitor);
745
+ // Pass 3: Clean up imports — use the old cleanupImports approach
746
+ // which operates on the transformed SourceFile safely
747
+ const safeToRemove = new Set([...compiledHelpers].filter((h) => !bailedHelpers.has(h)));
748
+ transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, scopeRegistrationsInjected, f);
749
+ if (edits.length === 0) {
750
+ // No element-helper rewrites — but registry may still have
751
+ // collected diagnostics (e.g. agent-rule errors on Msg variants
752
+ // in a Msg-declaration-only file). Surface them so the adapter
753
+ // can fail the build.
754
+ if (registryResult.analysis.diagnostics.length > 0) {
755
+ return { output: source, edits: [], diagnostics: registryResult.analysis.diagnostics };
756
+ }
757
+ return null;
758
+ }
759
+ // Find component declarations for HMR and agent metadata
760
+ const componentDecls = devMode || emitAgentMetadata ? findComponentDeclarations(sourceFile, lluiImport) : [];
761
+ // Build per-statement edits by comparing original vs transformed.
762
+ // Only emit edits for statements that actually changed.
763
+ // Untouched code keeps its original positions → accurate source maps.
764
+ const finalEdits = [];
765
+ const origStmts = sourceFile.statements;
766
+ const xfStmts = transformed.statements;
767
+ for (let i = 0; i < origStmts.length && i < xfStmts.length; i++) {
768
+ const origStart = origStmts[i].getStart(sourceFile);
769
+ const origEnd = origStmts[i].getEnd();
770
+ const origText = source.slice(origStart, origEnd);
771
+ let xfText;
772
+ try {
773
+ xfText = printer.printNode(ts.EmitHint.Unspecified, xfStmts[i], transformed);
774
+ }
775
+ catch {
776
+ // Synthetic nodes may fail to print individually — fall back to full reprint
777
+ const { top: _top, bottom: _bottom } = devMode
778
+ ? generateDevCode(componentDecls, mcpPort)
779
+ : { top: '', bottom: '' };
780
+ let output = (_top ? _top + '\n' : '') + printer.printFile(transformed) + (_bottom ? '\n' + _bottom : '');
781
+ if (devMode || emitAgentMetadata) {
782
+ output = appendCompilerCacheProps(output, componentDecls);
783
+ }
784
+ return {
785
+ output,
786
+ edits: [{ start: 0, end: source.length, replacement: output }],
787
+ diagnostics: registryResult.analysis.diagnostics,
788
+ };
789
+ }
790
+ // Compare ignoring trailing semicolons and whitespace (printer adds them)
791
+ const origNorm = origText.trim().replace(/;$/, '');
792
+ const xfNorm = xfText.trim().replace(/;$/, '');
793
+ if (origNorm !== xfNorm) {
794
+ // Match the original style: if the original didn't end with a semicolon,
795
+ // strip the one the printer added
796
+ const origHasSemi = origText.trimEnd().endsWith(';');
797
+ const replacement = origHasSemi ? xfText : xfText.replace(/;(\s*)$/, '$1');
798
+ finalEdits.push({ start: origStart, end: origEnd, replacement });
799
+ }
800
+ }
801
+ // Dev setup: enable* must run BEFORE user's mountApp (top of file),
802
+ // but import.meta.hot.accept needs to reference user's component vars
803
+ // (bottom of file). So split the injection.
804
+ if (devMode) {
805
+ const { top, bottom } = generateDevCode(componentDecls, mcpPort);
806
+ if (top)
807
+ finalEdits.push({ start: 0, end: 0, replacement: top + '\n' });
808
+ if (bottom)
809
+ finalEdits.push({ start: source.length, end: source.length, replacement: '\n' + bottom });
810
+ }
811
+ if (finalEdits.length === 0) {
812
+ // No rewrites — but registry may still have collected diagnostics
813
+ // (e.g. bitmask-overflow on an otherwise-clean file). Surface them
814
+ // so the adapter can fail the build.
815
+ if (registryResult.analysis.diagnostics.length > 0) {
816
+ return { output: source, edits: [], diagnostics: registryResult.analysis.diagnostics };
817
+ }
818
+ return null;
819
+ }
820
+ // Build the full output by applying edits (for backward compat)
821
+ const sorted = [...finalEdits].sort((a, b) => b.start - a.start);
822
+ let output = source;
823
+ for (const edit of sorted) {
824
+ output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
825
+ }
826
+ // After output is assembled, update postSource in cache and emit non-enumerable props
827
+ if ((devMode || emitAgentMetadata) && componentDecls.length > 0) {
828
+ const cacheProps = appendCompilerCacheProps(output, componentDecls);
829
+ if (cacheProps !== output) {
830
+ output = cacheProps;
831
+ }
832
+ }
833
+ return { output, edits: finalEdits, diagnostics: registryResult.analysis.diagnostics };
834
+ }
835
+ // ── HMR ──────────────────────────────────────────────────────────
836
+ function generateDevCode(components, mcpPort) {
837
+ if (components.length === 0) {
838
+ return {
839
+ top: '',
840
+ bottom: `if (import.meta.hot) {\n import.meta.hot.accept()\n}`,
841
+ };
842
+ }
843
+ const relayImport = mcpPort !== null ? ', startRelay as __startRelay' : '';
844
+ const relayCall = mcpPort !== null ? `\n__startRelay(${mcpPort})` : '';
845
+ const top = `
846
+ import { enableHmr as __enableHmr, replaceComponent as __replaceComponent } from '@llui/dom/hmr'
847
+ import { enableDevTools as __enableDevTools${relayImport} } from '@llui/dom/devtools'
848
+ __enableHmr()
849
+ __enableDevTools()${relayCall}
850
+ `.trim();
851
+ const replaceCalls = components
852
+ .map(({ varName, componentName }) => ` __replaceComponent("${componentName}", ${varName})`)
853
+ .join('\n');
854
+ // HMR auto-connect: when the Vite plugin detects that @llui/mcp's
855
+ // active marker file exists or appears, it sends `llui:mcp-ready`
856
+ // with the MCP bridge port. We forward that to __lluiConnect so the
857
+ // browser connects automatically — no console gymnastics, no retry
858
+ // spam, regardless of whether MCP or Vite started first.
859
+ const mcpHmrHandler = mcpPort !== null
860
+ ? `
861
+ import.meta.hot.on('llui:mcp-ready', (data) => {
862
+ if (typeof globalThis.__lluiConnect === 'function') {
863
+ globalThis.__lluiConnect(data?.port)
864
+ }
865
+ })`
866
+ : '';
867
+ const bottom = `
868
+ if (import.meta.hot) {
869
+ import.meta.hot.accept(() => {
870
+ ${replaceCalls}
871
+ })${mcpHmrHandler}
872
+ }
873
+ `.trim();
874
+ return { top, bottom };
875
+ }
876
+ /** Find all component() calls and extract the variable name and component name */
877
+ function findComponentDeclarations(sf, lluiImport) {
878
+ const result = [];
879
+ function visit(node) {
880
+ // Match: const Foo = component({ name: 'Foo', ... })
881
+ if (ts.isVariableDeclaration(node) &&
882
+ ts.isIdentifier(node.name) &&
883
+ node.initializer &&
884
+ ts.isCallExpression(node.initializer) &&
885
+ isComponentCall(node.initializer, lluiImport)) {
886
+ const varName = node.name.text;
887
+ const config = node.initializer.arguments[0];
888
+ if (config && ts.isObjectLiteralExpression(config)) {
889
+ for (const prop of config.properties) {
890
+ if (ts.isPropertyAssignment(prop) &&
891
+ ts.isIdentifier(prop.name) &&
892
+ prop.name.text === 'name' &&
893
+ ts.isStringLiteral(prop.initializer)) {
894
+ result.push({ varName, componentName: prop.initializer.text });
895
+ }
896
+ }
897
+ }
898
+ }
899
+ ts.forEachChild(node, visit);
900
+ }
901
+ visit(sf);
902
+ return result;
903
+ }
904
+ // ── Helpers ──────────────────────────────────────────────────────
905
+ function findLluiImport(sf) {
906
+ for (const stmt of sf.statements) {
907
+ if (ts.isImportDeclaration(stmt) &&
908
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
909
+ stmt.moduleSpecifier.text === '@llui/dom') {
910
+ return stmt;
911
+ }
912
+ }
913
+ return null;
914
+ }
915
+ function getImportedHelpers(imp) {
916
+ const map = new Map();
917
+ const clause = imp.importClause;
918
+ if (!clause || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
919
+ return map;
920
+ for (const spec of clause.namedBindings.elements) {
921
+ const original = (spec.propertyName ?? spec.name).text;
922
+ const local = spec.name.text;
923
+ if (ELEMENT_HELPERS.has(original)) {
924
+ map.set(local, original);
925
+ }
926
+ }
927
+ return map;
928
+ }
929
+ function hasComponentDef(sf, lluiImport) {
930
+ let found = false;
931
+ function visit(node) {
932
+ if (found)
933
+ return;
934
+ if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
935
+ found = true;
936
+ return;
937
+ }
938
+ ts.forEachChild(node, visit);
939
+ }
940
+ visit(sf);
941
+ return found;
942
+ }
943
+ /**
944
+ * Scan for `component({ view: (h) => ... })` arrow functions and collect
945
+ * the identifier name used as the View-bundle parameter. When the user
946
+ * writes `h.show(...)` / `h.text(...)` inside the view, the compiler treats
947
+ * it the same as bare `show(...)` / `text(...)` for mask injection.
948
+ */
949
+ function collectViewHelperNames(sf, lluiImport) {
950
+ const names = new Set();
951
+ function visit(node) {
952
+ if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
953
+ const arg = node.arguments[0];
954
+ if (arg && ts.isObjectLiteralExpression(arg)) {
955
+ for (const prop of arg.properties) {
956
+ if (ts.isPropertyAssignment(prop) &&
957
+ ts.isIdentifier(prop.name) &&
958
+ prop.name.text === 'view' &&
959
+ (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
960
+ const params = prop.initializer.parameters;
961
+ if (params.length >= 1) {
962
+ const first = params[0];
963
+ if (ts.isIdentifier(first.name)) {
964
+ names.add(first.name.text);
965
+ }
966
+ }
967
+ }
968
+ }
969
+ }
970
+ }
971
+ // Also: any function parameter annotated as `View<...>` — covers extracted
972
+ // view-functions like `function repoPage(h: View<State, Msg>, ...)`.
973
+ if (ts.isParameter(node) &&
974
+ node.type &&
975
+ isViewTypeReference(node.type) &&
976
+ ts.isIdentifier(node.name)) {
977
+ names.add(node.name.text);
978
+ }
979
+ ts.forEachChild(node, visit);
980
+ }
981
+ visit(sf);
982
+ return names;
983
+ }
984
+ function isViewTypeReference(t) {
985
+ return ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName) && t.typeName.text === 'View';
986
+ }
987
+ /**
988
+ * Scan for `component({ view: ({ show, each, text, ... }) => ... })`
989
+ * destructured parameters and return a map from the locally-bound name to
990
+ * the primitive name it aliases. This lets users write the bare `show(...)` /
991
+ * `text(...)` forms without importing them, while the compiler still
992
+ * applies mask injection etc.
993
+ *
994
+ * view: ({ show, text: t }) => [...]
995
+ * // returns { show → "show", t → "text" }
996
+ */
997
+ const VIEW_HELPER_PRIMITIVES = new Set([
998
+ 'show',
999
+ 'branch',
1000
+ 'scope',
1001
+ 'each',
1002
+ 'text',
1003
+ 'memo',
1004
+ 'sample',
1005
+ 'selector',
1006
+ 'ctx',
1007
+ 'slice',
1008
+ 'send',
1009
+ ]);
1010
+ function collectViewHelperAliases(sf, lluiImport, helperNames) {
1011
+ const aliases = new Map();
1012
+ function addFromBindingPattern(pattern) {
1013
+ for (const elem of pattern.elements) {
1014
+ // { show } → propertyName=undefined, name=show
1015
+ // { show: mySh } → propertyName=show, name=mySh
1016
+ const sourceName = elem.propertyName && ts.isIdentifier(elem.propertyName)
1017
+ ? elem.propertyName.text
1018
+ : ts.isIdentifier(elem.name)
1019
+ ? elem.name.text
1020
+ : null;
1021
+ const localName = ts.isIdentifier(elem.name) ? elem.name.text : null;
1022
+ if (sourceName && localName && VIEW_HELPER_PRIMITIVES.has(sourceName)) {
1023
+ aliases.set(localName, sourceName);
1024
+ }
1025
+ }
1026
+ }
1027
+ function visit(node) {
1028
+ if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
1029
+ const arg = node.arguments[0];
1030
+ if (arg && ts.isObjectLiteralExpression(arg)) {
1031
+ for (const prop of arg.properties) {
1032
+ if (ts.isPropertyAssignment(prop) &&
1033
+ ts.isIdentifier(prop.name) &&
1034
+ prop.name.text === 'view' &&
1035
+ (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
1036
+ const params = prop.initializer.parameters;
1037
+ if (params.length >= 1) {
1038
+ const first = params[0];
1039
+ if (ts.isObjectBindingPattern(first.name)) {
1040
+ addFromBindingPattern(first.name);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ // Also: function parameters like `(…, { show, text }: View<State, Msg>) => …`
1048
+ // on extracted helpers — allow the same destructuring ergonomics.
1049
+ if (ts.isParameter(node) &&
1050
+ node.type &&
1051
+ isViewTypeReference(node.type) &&
1052
+ ts.isObjectBindingPattern(node.name)) {
1053
+ addFromBindingPattern(node.name);
1054
+ }
1055
+ // Also: `const { show, text } = h` assignments where `h` is a known
1056
+ // helper binding — lets helpers destructure once at the top of the
1057
+ // function body.
1058
+ if (ts.isVariableDeclaration(node) &&
1059
+ ts.isObjectBindingPattern(node.name) &&
1060
+ node.initializer &&
1061
+ ts.isIdentifier(node.initializer) &&
1062
+ helperNames.has(node.initializer.text)) {
1063
+ addFromBindingPattern(node.name);
1064
+ }
1065
+ ts.forEachChild(node, visit);
1066
+ }
1067
+ visit(sf);
1068
+ return aliases;
1069
+ }
1070
+ export function isComponentCall(node, lluiImport) {
1071
+ if (!ts.isIdentifier(node.expression))
1072
+ return false;
1073
+ const name = node.expression.text;
1074
+ if (name !== 'component')
1075
+ return false;
1076
+ // Verify it's from the llui import
1077
+ const clause = lluiImport.importClause;
1078
+ if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
1079
+ return false;
1080
+ return clause.namedBindings.elements.some((s) => s.name.text === 'component' || (s.propertyName && s.propertyName.text === 'component'));
1081
+ }
1082
+ // ── Pass 2: Mask injection ───────────────────────────────────────
1083
+ /**
1084
+ * Match a call expression against a primitive name across all three binding
1085
+ * forms:
1086
+ * - bare imported identifier: `name(...)` where `name` was imported from @llui/dom
1087
+ * - destructured alias: `name(...)` where `name` is bound via
1088
+ * `view: (_, { name }) => ...` (or `{ name: alias }`)
1089
+ * - member call: `<h>.name(...)` where `<h>` is the 2nd view parameter
1090
+ *
1091
+ * The compiler treats all three identically for mask injection / each()
1092
+ * optimization purposes.
1093
+ */
1094
+ export function isHelperCall(expr, name, helperNames, aliases) {
1095
+ if (ts.isIdentifier(expr)) {
1096
+ if (expr.text === name)
1097
+ return true;
1098
+ if (aliases && aliases.get(expr.text) === name)
1099
+ return true;
1100
+ return false;
1101
+ }
1102
+ if (ts.isPropertyAccessExpression(expr) &&
1103
+ ts.isIdentifier(expr.expression) &&
1104
+ helperNames.has(expr.expression.text) &&
1105
+ ts.isIdentifier(expr.name) &&
1106
+ expr.name.text === name) {
1107
+ return true;
1108
+ }
1109
+ return false;
1110
+ }
1111
+ // text() `__mask` injection — migrated to `textMaskModule`
1112
+ // (v2c/decomp-15). Module fires top-down (transformCallEnter); the
1113
+ // visitor sees the masked form.
1114
+ // `__mask` injection on each()/branch()/scope()/show() — migrated to
1115
+ // `structuralMaskModule` (v2c/decomp-14). Module fires top-down
1116
+ // (transformCallEnter) so the visitor sees the masked options literal.
1117
+ // ── Pass 3: Import cleanup ───────────────────────────────────────
1118
+ function cleanupImports(sf, lluiImport, _helpers, compiled, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, usesRegisterScopeVariants, f) {
1119
+ if (compiled.size === 0 &&
1120
+ !usesElTemplate &&
1121
+ !usesElSplit &&
1122
+ !usesMemo &&
1123
+ !usesApplyBinding &&
1124
+ !usesCloneStaticTemplate &&
1125
+ !usesRegisterScopeVariants)
1126
+ return sf;
1127
+ const clause = lluiImport.importClause;
1128
+ if (!clause?.namedBindings || !ts.isNamedImports(clause.namedBindings))
1129
+ return sf;
1130
+ const remaining = clause.namedBindings.elements.filter((spec) => !compiled.has(spec.name.text));
1131
+ const hasElSplit = clause.namedBindings.elements.some((s) => s.name.text === 'elSplit');
1132
+ if (!hasElSplit && usesElSplit) {
1133
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elSplit')));
1134
+ }
1135
+ const hasElTemplate = clause.namedBindings.elements.some((s) => s.name.text === 'elTemplate');
1136
+ if (!hasElTemplate && usesElTemplate) {
1137
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('elTemplate')));
1138
+ }
1139
+ const hasCloneStaticTemplate = clause.namedBindings.elements.some((s) => s.name.text === '__cloneStaticTemplate');
1140
+ if (!hasCloneStaticTemplate && usesCloneStaticTemplate) {
1141
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__cloneStaticTemplate')));
1142
+ }
1143
+ const hasMemo = clause.namedBindings.elements.some((s) => s.name.text === 'memo');
1144
+ if (!hasMemo && usesMemo) {
1145
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('memo')));
1146
+ }
1147
+ if (usesApplyBinding) {
1148
+ if (!clause.namedBindings.elements.some((s) => s.name.text === '__runPhase2')) {
1149
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__runPhase2')));
1150
+ }
1151
+ if (!clause.namedBindings.elements.some((s) => s.name.text === '__handleMsg')) {
1152
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__handleMsg')));
1153
+ }
1154
+ }
1155
+ // The connect-pattern injector (binding-descriptors.ts) emits
1156
+ // `__registerScopeVariants([...])` calls; ensure the runtime
1157
+ // helper is imported when at least one was inserted.
1158
+ const hasRegisterScopeVariants = clause.namedBindings.elements.some((s) => s.name.text === '__registerScopeVariants');
1159
+ if (!hasRegisterScopeVariants && usesRegisterScopeVariants) {
1160
+ remaining.push(f.createImportSpecifier(false, undefined, f.createIdentifier('__registerScopeVariants')));
1161
+ }
1162
+ const newBindings = f.createNamedImports(remaining);
1163
+ const newClause = f.createImportClause(false, undefined, newBindings);
1164
+ const newImportDecl = f.createImportDeclaration(undefined, newClause, lluiImport.moduleSpecifier);
1165
+ let replaced = false;
1166
+ const statements = sf.statements.map((stmt) => {
1167
+ if (!replaced &&
1168
+ ts.isImportDeclaration(stmt) &&
1169
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
1170
+ stmt.moduleSpecifier.text === '@llui/dom' &&
1171
+ !stmt.importClause?.isTypeOnly) {
1172
+ replaced = true;
1173
+ return newImportDecl;
1174
+ }
1175
+ return stmt;
1176
+ });
1177
+ return f.updateSourceFile(sf, statements);
1178
+ }
1179
+ // ── __msgSchema injection ────────────────────────────────────────
1180
+ // `injectStateSchema` was the inline emitter for `__stateSchema`.
1181
+ // Migrated to `stateSchemaModule` + the registry bridge (v2c/decomp-3).
1182
+ // Behavior preserves end-to-end via the existing transform/HMR tests.
1183
+ // `stateTypeToLiteral` lives in `state-schema.ts` alongside `StateType`.
1184
+ // `injectComponentMeta` was the inline emitter for `__componentMeta`.
1185
+ // Migrated to `componentMetaModule` + the registry bridge in this
1186
+ // commit. Behavior preserves end-to-end via `registry-bridge.test.ts`.
1187
+ // `injectMsgSchema`, `injectEffectSchema`, `injectSchemaHash`,
1188
+ // `buildFieldDescriptorExpr`, `emitEnumValue` all migrated to
1189
+ // `msgSchemaModule` / `schemaHashModule` (v2c/decomp-5). The
1190
+ // literal builders live in `msg-schema.ts` alongside the schema
1191
+ // extraction logic.
1192
+ // `__lluiCompilerEmitted` + `__compilerVersion` integrity marker
1193
+ // migrated to `compilerStampModule` (v2c/decomp-10). Always-on through
1194
+ // the registry bridge — fires regardless of agent/dev mode.
1195
+ // ── Per-item accessor detection ──────────────────────────────────
1196
+ // Item selector deduplication — migrated to `itemDedupModule`
1197
+ // (v2c/decomp-16). Module fires top-down (transformCallEnter); the
1198
+ // visitor sees the post-dedup form.
1199
+ // Auto-memoize each() items accessor — migrated to `eachMemoModule`
1200
+ // (v2c/decomp-13). `tryWrapEachItemsWithMemo`, `accessorAllocatesArray`
1201
+ // + `ALLOCATING_METHODS` now live in `modules/each-memo.ts`.
1202
+ // ── Subtree collapse: nested elements → elTemplate ──────────────
1203
+ const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
1204
+ export function computeAccessorMask(accessor, fieldBits, visited = new Set(), fieldBitsHi) {
1205
+ if (visited.has(accessor))
1206
+ return { mask: 0, maskHi: 0, readsState: false };
1207
+ visited.add(accessor);
1208
+ if (accessor.parameters.length === 0)
1209
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
1210
+ const paramName = accessor.parameters[0].name;
1211
+ if (!ts.isIdentifier(paramName))
1212
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
1213
+ // FunctionDeclaration always has a body (we never resolve overloads here);
1214
+ // ArrowFunction's body may be a single expression. Both shapes are walked
1215
+ // identically by ts.forEachChild, so no special-casing is needed below.
1216
+ if (!accessor.body)
1217
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
1218
+ const stateParam = paramName.text;
1219
+ let mask = 0;
1220
+ let maskHi = 0;
1221
+ let readsState = false;
1222
+ // `inNestedFn` gates only the delegation-recursion. Property-access
1223
+ // path extraction happens everywhere — inner-arrow callbacks like
1224
+ // `s.items.filter((i) => i.includes(s.filter))` close over our
1225
+ // state, and their `s.filter` reads contribute to the mask.
1226
+ function walk(node, inNestedFn) {
1227
+ // `node.parent` can be undefined for synthetic nodes produced by
1228
+ // earlier AST-transform passes (the row-factory rewrite and the
1229
+ // per-item heuristic both build new sub-trees whose inner nodes
1230
+ // have no parent pointers). Guard every parent access below —
1231
+ // crashing the whole build on a perfectly valid reactive accessor
1232
+ // like `text((_s) => \`$${item.x.toLocaleString()}\`)` was how
1233
+ // this bug first surfaced in the persistent-layout example work.
1234
+ const parent = node.parent;
1235
+ if (ts.isIdentifier(node) && node.text === stateParam && (!parent || !ts.isParameter(parent))) {
1236
+ readsState = true;
1237
+ }
1238
+ if (ts.isPropertyAccessExpression(node)) {
1239
+ // When there's no parent we can't tell if this is the top of a
1240
+ // chain, so we resolve from here. That's still correct for mask
1241
+ // accounting: `resolveChain` on an inner PAE produces a prefix
1242
+ // of the outer chain, which maps to the same `fieldBits` bit
1243
+ // via the prefix-match loop below. Worst case we resolve the
1244
+ // same chain twice (`|=` is idempotent); best case we'd have
1245
+ // resolved once from the real top. Correctness unchanged.
1246
+ if (!parent || !ts.isPropertyAccessExpression(parent)) {
1247
+ const chain = resolveChain(node, stateParam);
1248
+ if (chain) {
1249
+ const bit = fieldBits.get(chain);
1250
+ const bitHi = fieldBitsHi?.get(chain);
1251
+ if (bit !== undefined) {
1252
+ mask |= bit;
1253
+ }
1254
+ else if (bitHi !== undefined) {
1255
+ maskHi |= bitHi;
1256
+ }
1257
+ else {
1258
+ // Match paths that overlap our chain in either direction:
1259
+ // - `path` extends `chain` — fieldBits has finer-grained paths
1260
+ // than we're reading (e.g. chain='user', fieldBits has
1261
+ // 'user.email').
1262
+ // - `chain` extends `path` — we're reading deeper than what
1263
+ // fieldBits tracks (e.g. chain='items.filter' from
1264
+ // `s.items.filter(...)`, fieldBits has 'items'). Both ends
1265
+ // must mask in: a change to `items` invalidates anything
1266
+ // downstream of it.
1267
+ for (const [path, b] of fieldBits) {
1268
+ if (path === chain || path.startsWith(chain + '.') || chain.startsWith(path + '.')) {
1269
+ mask |= b;
1270
+ }
1271
+ }
1272
+ if (fieldBitsHi) {
1273
+ for (const [path, b] of fieldBitsHi) {
1274
+ if (path === chain ||
1275
+ path.startsWith(chain + '.') ||
1276
+ chain.startsWith(path + '.')) {
1277
+ maskHi |= b;
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1285
+ // Delegation: `helper(s)` where `s` matches our state param.
1286
+ // Recurse into the helper's body so its state-path reads
1287
+ // contribute to our mask. Only at top level — inside a nested
1288
+ // function body, `s` may be shadowed and the call isn't
1289
+ // unambiguously handing our state in.
1290
+ if (!inNestedFn && ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1291
+ const calleeName = node.expression.text;
1292
+ if (!NON_DELEGATION_HELPERS.has(calleeName)) {
1293
+ const arg0 = node.arguments[0];
1294
+ if (arg0 && ts.isIdentifier(arg0) && arg0.text === stateParam) {
1295
+ const resolved = resolveAccessorBody(node.expression);
1296
+ if (resolved) {
1297
+ const inner = computeAccessorMask(resolved, fieldBits, visited, fieldBitsHi);
1298
+ mask |= inner.mask;
1299
+ maskHi |= inner.maskHi;
1300
+ if (inner.readsState)
1301
+ readsState = true;
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+ const enteringNested = ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node);
1307
+ ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
1308
+ }
1309
+ walk(accessor.body, false);
1310
+ if (mask === 0 && maskHi === 0 && readsState) {
1311
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: true };
1312
+ }
1313
+ return { mask, maskHi, readsState };
1314
+ }
1315
+ function resolveChain(node, paramName) {
1316
+ const parts = [];
1317
+ let current = node;
1318
+ while (ts.isPropertyAccessExpression(current)) {
1319
+ parts.unshift(current.name.text);
1320
+ current = current.expression;
1321
+ }
1322
+ if (!ts.isIdentifier(current) || current.text !== paramName)
1323
+ return null;
1324
+ if (parts.length > 2)
1325
+ return parts.slice(0, 2).join('.');
1326
+ return parts.join('.');
1327
+ }
1328
+ // ── Compiler cache helpers ────────────────────────────────────────
1329
+ /**
1330
+ * Extract the view function body (the value of the `view:` property) from
1331
+ * a component() config object literal. Uses a regex heuristic — good enough
1332
+ * for round-tripping source for dev/agent tools.
1333
+ */
1334
+ export function extractViewBody(code) {
1335
+ const match = /\bview\s*:\s*([\s\S]*?)(?=,\s*(?:onEffect|update|init|name|onMsg)\s*:|}\s*\))/m.exec(code);
1336
+ return match?.[1]?.trim() ?? null;
1337
+ }
1338
+ /**
1339
+ * Extract the component `name:` string literal from a component() call's
1340
+ * first argument object literal in the source text.
1341
+ */
1342
+ export function extractComponentNameFromConfig(node) {
1343
+ const configArg = node.arguments[0];
1344
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
1345
+ return null;
1346
+ for (const prop of configArg.properties) {
1347
+ if (ts.isPropertyAssignment(prop) &&
1348
+ ts.isIdentifier(prop.name) &&
1349
+ prop.name.text === 'name' &&
1350
+ ts.isStringLiteral(prop.initializer)) {
1351
+ return prop.initializer.text;
1352
+ }
1353
+ }
1354
+ return null;
1355
+ }
1356
+ /**
1357
+ * Generate Object.defineProperty calls for __preSource, __postSource,
1358
+ * __msgMaskMap, and __bindingSources on a component variable. These are
1359
+ * non-enumerable so they don't appear in JSON.stringify(componentDef) but are
1360
+ * visible to devtools.
1361
+ */
1362
+ export function generateCompilerCacheProps(varName, componentName) {
1363
+ const entry = compilerCache.get(componentName);
1364
+ if (!entry)
1365
+ return '';
1366
+ return (`\nObject.defineProperty(${varName}, '__preSource', { value: ${JSON.stringify(entry.preSource)}, enumerable: false, configurable: true })` +
1367
+ `\nObject.defineProperty(${varName}, '__postSource', { value: ${JSON.stringify(entry.postSource)}, enumerable: false, configurable: true })` +
1368
+ `\nObject.defineProperty(${varName}, '__msgMaskMap', { value: ${JSON.stringify(entry.msgMaskMap)}, enumerable: false, configurable: true })` +
1369
+ `\nObject.defineProperty(${varName}, '__bindingSources', { value: ${JSON.stringify(entry.bindingSources)}, enumerable: false, configurable: true })`);
1370
+ }
1371
+ /**
1372
+ * After the full output string is assembled, update each cached component's
1373
+ * postSource (extract view body from the transformed output), then append
1374
+ * Object.defineProperty calls for all four compiler-cache properties.
1375
+ */
1376
+ export function appendCompilerCacheProps(output, componentDecls) {
1377
+ let result = output;
1378
+ for (const { varName, componentName } of componentDecls) {
1379
+ const existing = compilerCache.get(componentName);
1380
+ if (!existing)
1381
+ continue;
1382
+ // Update the cache entry with the post-transform view body
1383
+ const postSource = extractViewBody(output) ?? '';
1384
+ compilerCache.set(componentName, { ...existing, postSource });
1385
+ // Append non-enumerable property definitions
1386
+ result += generateCompilerCacheProps(varName, componentName);
1387
+ }
1388
+ return result;
1389
+ }
1390
+ //# sourceMappingURL=transform.js.map