@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,141 @@
1
+ // `no-sample-in-accessor` — errors when `sample()` appears inside an
2
+ // accessor passed to a structural primitive (each.items/key, branch.on,
3
+ // show.when, scope.on, child.props, foreign.props) or a binding helper
4
+ // (text, unsafeHtml). The sampled read is invisible to the compiler's
5
+ // mask analysis and breaks reconciliation. Migrated from
6
+ // `@llui/eslint-plugin/src/rules/no-sample-in-accessor.ts`.
7
+ import ts from 'typescript';
8
+ import { rangeFromOffsets } from '../diagnostic.js';
9
+ const ACCESSOR_PROPS_BY_PRIMITIVE = {
10
+ each: new Set(['items', 'key']),
11
+ branch: new Set(['on']),
12
+ show: new Set(['when']),
13
+ scope: new Set(['on']),
14
+ child: new Set(['props']),
15
+ foreign: new Set(['props']),
16
+ };
17
+ const BINDING_HELPERS = new Set(['text', 'unsafeHtml']);
18
+ function isSampleCall(n) {
19
+ if (!ts.isCallExpression(n))
20
+ return false;
21
+ if (ts.isIdentifier(n.expression) && n.expression.text === 'sample')
22
+ return true;
23
+ if (ts.isPropertyAccessExpression(n.expression) &&
24
+ ts.isIdentifier(n.expression.name) &&
25
+ n.expression.name.text === 'sample')
26
+ return true;
27
+ return false;
28
+ }
29
+ function findFirstSampleInside(body) {
30
+ let found;
31
+ const walk = (n) => {
32
+ if (found)
33
+ return;
34
+ if (isSampleCall(n)) {
35
+ found = n;
36
+ return;
37
+ }
38
+ // Don't descend into nested functions — `sample` inside a handler
39
+ // callback isn't running in the accessor's reactive position.
40
+ if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) &&
41
+ n !== body) {
42
+ return;
43
+ }
44
+ ts.forEachChild(n, walk);
45
+ };
46
+ walk(body);
47
+ return found;
48
+ }
49
+ function getCalleeName(callee) {
50
+ if (ts.isIdentifier(callee))
51
+ return callee.text;
52
+ if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name))
53
+ return callee.name.text;
54
+ return null;
55
+ }
56
+ export function noSampleInAccessorModule() {
57
+ return {
58
+ name: 'no-sample-in-accessor',
59
+ compilerVersion: '^0.3.0',
60
+ diagnostics: [
61
+ {
62
+ id: 'llui/no-sample-in-accessor',
63
+ description: '`sample()` inside an accessor — invisible to mask analysis, breaks reconciliation.',
64
+ },
65
+ ],
66
+ visitors: {
67
+ [ts.SyntaxKind.SourceFile]: (ctx, node) => {
68
+ const visited = node;
69
+ const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
70
+ const walk = (n) => {
71
+ if (ts.isCallExpression(n)) {
72
+ const name = getCalleeName(n.expression);
73
+ if (name) {
74
+ const accessorProps = ACCESSOR_PROPS_BY_PRIMITIVE[name];
75
+ if (accessorProps !== undefined) {
76
+ const opts = n.arguments[0];
77
+ if (opts && ts.isObjectLiteralExpression(opts)) {
78
+ for (const prop of opts.properties) {
79
+ if (!ts.isPropertyAssignment(prop))
80
+ continue;
81
+ if (!ts.isIdentifier(prop.name))
82
+ continue;
83
+ if (!accessorProps.has(prop.name.text))
84
+ continue;
85
+ const v = prop.initializer;
86
+ if (!ts.isArrowFunction(v) && !ts.isFunctionExpression(v))
87
+ continue;
88
+ if (!v.body)
89
+ continue;
90
+ const sample = findFirstSampleInside(v.body);
91
+ if (sample) {
92
+ ctx.reportDiagnostic({
93
+ id: 'llui/no-sample-in-accessor',
94
+ severity: 'error',
95
+ category: 'reactivity',
96
+ message: `\`sample()\` inside \`${name}({ ${prop.name.text}: … })\` reads state ` +
97
+ `outside the accessor's parameter — invisible to the compiler's mask ` +
98
+ `analysis. The accessor must be a pure function of its parameter. Lift ` +
99
+ `the outer state into the parameter (e.g. for \`each.key\`, bake the dep ` +
100
+ `into \`items\`: \`items: (s) => s.rows.map(it => ({ it, rev: s.rev }))\`, ` +
101
+ `then \`key: (r) => \`\${r.it.id}|\${r.rev}\`\`).`,
102
+ location: {
103
+ file: sf.fileName,
104
+ range: rangeFromOffsets(sf.text, sample.getStart(sf), sample.getEnd()),
105
+ },
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ else if (BINDING_HELPERS.has(name)) {
112
+ const arg = n.arguments[0];
113
+ if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) && arg.body) {
114
+ const sample = findFirstSampleInside(arg.body);
115
+ if (sample) {
116
+ ctx.reportDiagnostic({
117
+ id: 'llui/no-sample-in-accessor',
118
+ severity: 'error',
119
+ category: 'reactivity',
120
+ message: `\`sample()\` inside \`${name}((s) => …)\` is redundant and invisible to ` +
121
+ `mask analysis. Read the state directly via the accessor's parameter: ` +
122
+ `\`${name}((s) => s.field)\` re-runs reactively on every commit; the ` +
123
+ `\`sample()\` wrapper bypasses that.`,
124
+ location: {
125
+ file: sf.fileName,
126
+ range: rangeFromOffsets(sf.text, sample.getStart(sf), sample.getEnd()),
127
+ },
128
+ });
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ts.forEachChild(n, walk);
135
+ };
136
+ walk(sf);
137
+ },
138
+ },
139
+ };
140
+ }
141
+ //# sourceMappingURL=no-sample-in-accessor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-sample-in-accessor.js","sourceRoot":"","sources":["../../src/modules/no-sample-in-accessor.ts"],"names":[],"mappings":"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","sourcesContent":["// `no-sample-in-accessor` — errors when `sample()` appears inside an\n// accessor passed to a structural primitive (each.items/key, branch.on,\n// show.when, scope.on, child.props, foreign.props) or a binding helper\n// (text, unsafeHtml). The sampled read is invisible to the compiler's\n// mask analysis and breaks reconciliation. Migrated from\n// `@llui/eslint-plugin/src/rules/no-sample-in-accessor.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\n\nconst ACCESSOR_PROPS_BY_PRIMITIVE: Record<string, Set<string>> = {\n each: new Set(['items', 'key']),\n branch: new Set(['on']),\n show: new Set(['when']),\n scope: new Set(['on']),\n child: new Set(['props']),\n foreign: new Set(['props']),\n}\n\nconst BINDING_HELPERS = new Set(['text', 'unsafeHtml'])\n\nfunction isSampleCall(n: ts.Node): boolean {\n if (!ts.isCallExpression(n)) return false\n if (ts.isIdentifier(n.expression) && n.expression.text === 'sample') return true\n if (\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n n.expression.name.text === 'sample'\n )\n return true\n return false\n}\n\nfunction findFirstSampleInside(body: ts.Node): ts.CallExpression | undefined {\n let found: ts.CallExpression | undefined\n const walk = (n: ts.Node): void => {\n if (found) return\n if (isSampleCall(n)) {\n found = n as ts.CallExpression\n return\n }\n // Don't descend into nested functions — `sample` inside a handler\n // callback isn't running in the accessor's reactive position.\n if (\n (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) &&\n n !== body\n ) {\n return\n }\n ts.forEachChild(n, walk)\n }\n walk(body)\n return found\n}\n\nfunction getCalleeName(callee: ts.Expression): string | null {\n if (ts.isIdentifier(callee)) return callee.text\n if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) return callee.name.text\n return null\n}\n\nexport function noSampleInAccessorModule(): CompilerModule {\n return {\n name: 'no-sample-in-accessor',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-sample-in-accessor',\n description:\n '`sample()` inside an accessor — invisible to mask analysis, breaks reconciliation.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n const walk = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n const name = getCalleeName(n.expression)\n if (name) {\n const accessorProps = ACCESSOR_PROPS_BY_PRIMITIVE[name]\n if (accessorProps !== undefined) {\n const opts = n.arguments[0]\n if (opts && ts.isObjectLiteralExpression(opts)) {\n for (const prop of opts.properties) {\n if (!ts.isPropertyAssignment(prop)) continue\n if (!ts.isIdentifier(prop.name)) continue\n if (!accessorProps.has(prop.name.text)) continue\n const v = prop.initializer\n if (!ts.isArrowFunction(v) && !ts.isFunctionExpression(v)) continue\n if (!v.body) continue\n const sample = findFirstSampleInside(v.body)\n if (sample) {\n ctx.reportDiagnostic({\n id: 'llui/no-sample-in-accessor',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`sample()\\` inside \\`${name}({ ${prop.name.text}: … })\\` reads state ` +\n `outside the accessor's parameter — invisible to the compiler's mask ` +\n `analysis. The accessor must be a pure function of its parameter. Lift ` +\n `the outer state into the parameter (e.g. for \\`each.key\\`, bake the dep ` +\n `into \\`items\\`: \\`items: (s) => s.rows.map(it => ({ it, rev: s.rev }))\\`, ` +\n `then \\`key: (r) => \\`\\${r.it.id}|\\${r.rev}\\`\\`).`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, sample.getStart(sf), sample.getEnd()),\n },\n })\n }\n }\n }\n } else if (BINDING_HELPERS.has(name)) {\n const arg = n.arguments[0]\n if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) && arg.body) {\n const sample = findFirstSampleInside(arg.body)\n if (sample) {\n ctx.reportDiagnostic({\n id: 'llui/no-sample-in-accessor',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`sample()\\` inside \\`${name}((s) => …)\\` is redundant and invisible to ` +\n `mask analysis. Read the state directly via the accessor's parameter: ` +\n `\\`${name}((s) => s.field)\\` re-runs reactively on every commit; the ` +\n `\\`sample()\\` wrapper bypasses that.`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, sample.getStart(sf), sample.getEnd()),\n },\n })\n }\n }\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { CompilerModule } from '../module.js';
2
+ export declare function noSampleInReactivePositionModule(): CompilerModule;
3
+ //# sourceMappingURL=no-sample-in-reactive-position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-sample-in-reactive-position.d.ts","sourceRoot":"","sources":["../../src/modules/no-sample-in-reactive-position.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgBlD,wBAAgB,gCAAgC,IAAI,cAAc,CAsDjE"}
@@ -0,0 +1,72 @@
1
+ // `no-sample-in-reactive-position` — errors when `sample(...)` results
2
+ // are passed directly to a reactive-accessor-taking position like
3
+ // `text(sample(...))` or `unsafeHtml(sample(...))`. The result is
4
+ // static; the rendered DOM never updates. Migrated from
5
+ // `@llui/eslint-plugin/src/rules/no-sample-in-reactive-position.ts`.
6
+ import ts from 'typescript';
7
+ import { rangeFromOffsets } from '../diagnostic.js';
8
+ const REACTIVE_TARGETS = new Set(['text', 'unsafeHtml']);
9
+ function isSampleCall(n) {
10
+ if (!ts.isCallExpression(n))
11
+ return false;
12
+ if (ts.isIdentifier(n.expression) && n.expression.text === 'sample')
13
+ return true;
14
+ if (ts.isPropertyAccessExpression(n.expression) &&
15
+ ts.isIdentifier(n.expression.name) &&
16
+ n.expression.name.text === 'sample')
17
+ return true;
18
+ return false;
19
+ }
20
+ export function noSampleInReactivePositionModule() {
21
+ return {
22
+ name: 'no-sample-in-reactive-position',
23
+ compilerVersion: '^0.3.0',
24
+ diagnostics: [
25
+ {
26
+ id: 'llui/no-sample-in-reactive-position',
27
+ description: '`text(sample(…))` / `unsafeHtml(sample(…))` reads once at view-construction — DOM never updates.',
28
+ },
29
+ ],
30
+ visitors: {
31
+ [ts.SyntaxKind.SourceFile]: (ctx, node) => {
32
+ const visited = node;
33
+ const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
34
+ const walk = (n) => {
35
+ if (ts.isCallExpression(n)) {
36
+ let calleeName = null;
37
+ if (ts.isIdentifier(n.expression) && REACTIVE_TARGETS.has(n.expression.text)) {
38
+ calleeName = n.expression.text;
39
+ }
40
+ else if (ts.isPropertyAccessExpression(n.expression) &&
41
+ ts.isIdentifier(n.expression.name) &&
42
+ REACTIVE_TARGETS.has(n.expression.name.text)) {
43
+ calleeName = n.expression.name.text;
44
+ }
45
+ if (calleeName) {
46
+ const arg = n.arguments[0];
47
+ if (arg && isSampleCall(arg)) {
48
+ ctx.reportDiagnostic({
49
+ id: 'llui/no-sample-in-reactive-position',
50
+ severity: 'error',
51
+ category: 'reactivity',
52
+ message: `\`${calleeName}(sample(…))\` reads the value once at view-construction and ` +
53
+ `the resulting node never updates. \`sample\` is an opt-out of reactivity — ` +
54
+ `drop the wrapper to make \`${calleeName}\` reactive: \`${calleeName}((s) => …)\` ` +
55
+ `reads on every commit, or \`${calleeName}(item.field)\` reads from an \`each\` ` +
56
+ `ItemAccessor.`,
57
+ location: {
58
+ file: sf.fileName,
59
+ range: rangeFromOffsets(sf.text, arg.getStart(sf), arg.getEnd()),
60
+ },
61
+ });
62
+ }
63
+ }
64
+ }
65
+ ts.forEachChild(n, walk);
66
+ };
67
+ walk(sf);
68
+ },
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=no-sample-in-reactive-position.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-sample-in-reactive-position.js","sourceRoot":"","sources":["../../src/modules/no-sample-in-reactive-position.ts"],"names":[],"mappings":"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","sourcesContent":["// `no-sample-in-reactive-position` — errors when `sample(...)` results\n// are passed directly to a reactive-accessor-taking position like\n// `text(sample(...))` or `unsafeHtml(sample(...))`. The result is\n// static; the rendered DOM never updates. Migrated from\n// `@llui/eslint-plugin/src/rules/no-sample-in-reactive-position.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\n\nconst REACTIVE_TARGETS = new Set(['text', 'unsafeHtml'])\n\nfunction isSampleCall(n: ts.Node): boolean {\n if (!ts.isCallExpression(n)) return false\n if (ts.isIdentifier(n.expression) && n.expression.text === 'sample') return true\n if (\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n n.expression.name.text === 'sample'\n )\n return true\n return false\n}\n\nexport function noSampleInReactivePositionModule(): CompilerModule {\n return {\n name: 'no-sample-in-reactive-position',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-sample-in-reactive-position',\n description:\n '`text(sample(…))` / `unsafeHtml(sample(…))` reads once at view-construction — DOM never updates.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n const walk = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n let calleeName: string | null = null\n if (ts.isIdentifier(n.expression) && REACTIVE_TARGETS.has(n.expression.text)) {\n calleeName = n.expression.text\n } else if (\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n REACTIVE_TARGETS.has(n.expression.name.text)\n ) {\n calleeName = n.expression.name.text\n }\n if (calleeName) {\n const arg = n.arguments[0]\n if (arg && isSampleCall(arg)) {\n ctx.reportDiagnostic({\n id: 'llui/no-sample-in-reactive-position',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`${calleeName}(sample(…))\\` reads the value once at view-construction and ` +\n `the resulting node never updates. \\`sample\\` is an opt-out of reactivity — ` +\n `drop the wrapper to make \\`${calleeName}\\` reactive: \\`${calleeName}((s) => …)\\` ` +\n `reads on every commit, or \\`${calleeName}(item.field)\\` reads from an \\`each\\` ` +\n `ItemAccessor.`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, arg.getStart(sf), arg.getEnd()),\n },\n })\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { CompilerModule } from '../module.js';
2
+ export declare function pureUpdateFunctionModule(): CompilerModule;
3
+ //# sourceMappingURL=pure-update-function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pure-update-function.d.ts","sourceRoot":"","sources":["../../src/modules/pure-update-function.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA8BlD,wBAAgB,wBAAwB,IAAI,cAAc,CAmGzD"}
@@ -0,0 +1,127 @@
1
+ // `pure-update-function` — errors on side-effecting calls inside a
2
+ // component's update function. update() must be a pure reducer;
3
+ // non-determinism (Math.random, Date.now), I/O (fetch, document,
4
+ // localStorage), and scheduling (setTimeout) break replay,
5
+ // time-travel debugging, and reproducibility. Return an Effect
6
+ // instead. Migrated from
7
+ // `@llui/eslint-plugin/src/rules/pure-update-function.ts`.
8
+ import ts from 'typescript';
9
+ import { rangeFromOffsets } from '../diagnostic.js';
10
+ import { findComponentCalls } from './_shared.js';
11
+ const BANNED_GLOBAL_CALLS = new Set([
12
+ 'fetch',
13
+ 'setTimeout',
14
+ 'setInterval',
15
+ 'clearTimeout',
16
+ 'clearInterval',
17
+ 'requestAnimationFrame',
18
+ 'cancelAnimationFrame',
19
+ ]);
20
+ const IMPURE_ROOT_OBJECTS = new Set(['document', 'window', 'localStorage', 'sessionStorage']);
21
+ function findUpdateProperty(call) {
22
+ const arg = call.arguments[0];
23
+ if (!arg || !ts.isObjectLiteralExpression(arg))
24
+ return undefined;
25
+ for (const prop of arg.properties) {
26
+ if (ts.isPropertyAssignment(prop) &&
27
+ ts.isIdentifier(prop.name) &&
28
+ prop.name.text === 'update') {
29
+ return prop;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+ export function pureUpdateFunctionModule() {
35
+ return {
36
+ name: 'pure-update-function',
37
+ compilerVersion: '^0.3.0',
38
+ diagnostics: [
39
+ {
40
+ id: 'llui/pure-update-function',
41
+ description: 'Side effect inside update() — return an Effect instead, keep update() pure.',
42
+ },
43
+ ],
44
+ visitors: {
45
+ [ts.SyntaxKind.SourceFile]: (ctx, node) => {
46
+ const visited = node;
47
+ const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
48
+ for (const call of findComponentCalls(sf)) {
49
+ const updateProp = findUpdateProperty(call);
50
+ if (!updateProp)
51
+ continue;
52
+ const fn = updateProp.initializer;
53
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
54
+ continue;
55
+ if (!fn.body)
56
+ continue;
57
+ const report = (n, name) => {
58
+ ctx.reportDiagnostic({
59
+ id: 'llui/pure-update-function',
60
+ severity: 'error',
61
+ category: 'reactivity',
62
+ message: `update() must be a pure reducer; \`${name}\` is a side effect. ` +
63
+ `Return an Effect from update() (e.g. \`return [state, [{ type: 'fetch', ... }]]\`) ` +
64
+ `and run the side effect in the corresponding effect handler.`,
65
+ location: {
66
+ file: sf.fileName,
67
+ range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),
68
+ },
69
+ });
70
+ };
71
+ const walk = (n) => {
72
+ // Boundary check — do walk into nested arrows here. The
73
+ // ESLint rule used parent-chain "is inside update" without
74
+ // a nested-fn cutoff; deferred callbacks would still trigger
75
+ // (e.g. nested arrow that does `setTimeout` inside update).
76
+ // We preserve that semantics: the rule flags any sync
77
+ // *or* deferred side-effect declared in the update body,
78
+ // because the reducer should not even orchestrate them.
79
+ // Direct calls: fetch(), setTimeout(), …
80
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
81
+ if (BANNED_GLOBAL_CALLS.has(n.expression.text)) {
82
+ report(n, n.expression.text);
83
+ }
84
+ }
85
+ // Member calls: window.fetch, Math.random, Date.now, document.*, localStorage.*, sessionStorage.*
86
+ if (ts.isCallExpression(n) &&
87
+ ts.isPropertyAccessExpression(n.expression) &&
88
+ ts.isIdentifier(n.expression.name)) {
89
+ const propName = n.expression.name.text;
90
+ const root = n.expression.expression;
91
+ const objName = ts.isIdentifier(root) ? root.text : '';
92
+ const isBanned = (objName === 'window' && BANNED_GLOBAL_CALLS.has(propName)) ||
93
+ (objName === 'Math' && propName === 'random') ||
94
+ (objName === 'Date' && propName === 'now') ||
95
+ objName === 'document' ||
96
+ objName === 'localStorage' ||
97
+ objName === 'sessionStorage';
98
+ if (isBanned)
99
+ report(n, `${objName}.${propName}`);
100
+ }
101
+ // Bare property reads on impure root objects (not as a call).
102
+ if (ts.isPropertyAccessExpression(n) &&
103
+ ts.isIdentifier(n.expression) &&
104
+ IMPURE_ROOT_OBJECTS.has(n.expression.text)) {
105
+ // Skip if this access is the callee of an enclosing CallExpression
106
+ // (handled by the call branch above).
107
+ const parent = n.parent;
108
+ const isCallee = parent && ts.isCallExpression(parent) && parent.expression === n;
109
+ if (!isCallee) {
110
+ report(n, n.expression.text);
111
+ }
112
+ }
113
+ // `new Date()` — non-deterministic time read.
114
+ if (ts.isNewExpression(n) &&
115
+ ts.isIdentifier(n.expression) &&
116
+ n.expression.text === 'Date') {
117
+ report(n, 'new Date()');
118
+ }
119
+ ts.forEachChild(n, walk);
120
+ };
121
+ walk(fn.body);
122
+ }
123
+ },
124
+ },
125
+ };
126
+ }
127
+ //# sourceMappingURL=pure-update-function.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pure-update-function.js","sourceRoot":"","sources":["../../src/modules/pure-update-function.ts"],"names":[],"mappings":"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","sourcesContent":["// `pure-update-function` — errors on side-effecting calls inside a\n// component's update function. update() must be a pure reducer;\n// non-determinism (Math.random, Date.now), I/O (fetch, document,\n// localStorage), and scheduling (setTimeout) break replay,\n// time-travel debugging, and reproducibility. Return an Effect\n// instead. Migrated from\n// `@llui/eslint-plugin/src/rules/pure-update-function.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nconst BANNED_GLOBAL_CALLS = new Set([\n 'fetch',\n 'setTimeout',\n 'setInterval',\n 'clearTimeout',\n 'clearInterval',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n])\n\nconst IMPURE_ROOT_OBJECTS = new Set(['document', 'window', 'localStorage', 'sessionStorage'])\n\nfunction findUpdateProperty(call: ts.CallExpression): ts.PropertyAssignment | undefined {\n const arg = call.arguments[0]\n if (!arg || !ts.isObjectLiteralExpression(arg)) return undefined\n for (const prop of arg.properties) {\n if (\n ts.isPropertyAssignment(prop) &&\n ts.isIdentifier(prop.name) &&\n prop.name.text === 'update'\n ) {\n return prop\n }\n }\n return undefined\n}\n\nexport function pureUpdateFunctionModule(): CompilerModule {\n return {\n name: 'pure-update-function',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/pure-update-function',\n description: 'Side effect inside update() — return an Effect instead, keep update() pure.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n for (const call of findComponentCalls(sf)) {\n const updateProp = findUpdateProperty(call)\n if (!updateProp) continue\n const fn = updateProp.initializer\n if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) continue\n if (!fn.body) continue\n\n const report = (n: ts.Node, name: string): void => {\n ctx.reportDiagnostic({\n id: 'llui/pure-update-function',\n severity: 'error',\n category: 'reactivity',\n message:\n `update() must be a pure reducer; \\`${name}\\` is a side effect. ` +\n `Return an Effect from update() (e.g. \\`return [state, [{ type: 'fetch', ... }]]\\`) ` +\n `and run the side effect in the corresponding effect handler.`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),\n },\n })\n }\n\n const walk = (n: ts.Node): void => {\n // Boundary check — do walk into nested arrows here. The\n // ESLint rule used parent-chain \"is inside update\" without\n // a nested-fn cutoff; deferred callbacks would still trigger\n // (e.g. nested arrow that does `setTimeout` inside update).\n // We preserve that semantics: the rule flags any sync\n // *or* deferred side-effect declared in the update body,\n // because the reducer should not even orchestrate them.\n\n // Direct calls: fetch(), setTimeout(), …\n if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {\n if (BANNED_GLOBAL_CALLS.has(n.expression.text)) {\n report(n, n.expression.text)\n }\n }\n // Member calls: window.fetch, Math.random, Date.now, document.*, localStorage.*, sessionStorage.*\n if (\n ts.isCallExpression(n) &&\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name)\n ) {\n const propName = n.expression.name.text\n const root = n.expression.expression\n const objName = ts.isIdentifier(root) ? root.text : ''\n const isBanned =\n (objName === 'window' && BANNED_GLOBAL_CALLS.has(propName)) ||\n (objName === 'Math' && propName === 'random') ||\n (objName === 'Date' && propName === 'now') ||\n objName === 'document' ||\n objName === 'localStorage' ||\n objName === 'sessionStorage'\n if (isBanned) report(n, `${objName}.${propName}`)\n }\n // Bare property reads on impure root objects (not as a call).\n if (\n ts.isPropertyAccessExpression(n) &&\n ts.isIdentifier(n.expression) &&\n IMPURE_ROOT_OBJECTS.has(n.expression.text)\n ) {\n // Skip if this access is the callee of an enclosing CallExpression\n // (handled by the call branch above).\n const parent = n.parent\n const isCallee = parent && ts.isCallExpression(parent) && parent.expression === n\n if (!isCallee) {\n report(n, n.expression.text)\n }\n }\n // `new Date()` — non-deterministic time read.\n if (\n ts.isNewExpression(n) &&\n ts.isIdentifier(n.expression) &&\n n.expression.text === 'Date'\n ) {\n report(n, 'new Date()')\n }\n ts.forEachChild(n, walk)\n }\n walk(fn.body)\n }\n },\n },\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { CompilerModule } from '../module.js';
2
+ export declare const reactivePathsModule: CompilerModule;
3
+ //# sourceMappingURL=reactive-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactive-paths.d.ts","sourceRoot":"","sources":["../../src/modules/reactive-paths.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,cAAc,CAAA;AAOxE,eAAO,MAAM,mBAAmB,EAAE,cAqCjC,CAAA"}
@@ -0,0 +1,77 @@
1
+ // `reactive-paths` — proof-of-concept compiler module (v2c §2 / v2c/2.3).
2
+ //
3
+ // Walks the file's reactive accessors via the same path-collection logic
4
+ // the monolithic `transform.ts` uses (`collectStatePathsFromSource`) and
5
+ // emits a `__prefixes: [...]` array — one stable closure per minimal
6
+ // reference-stable prefix, in sorted order.
7
+ //
8
+ // The POC's job is to prove the visitor-registry primitive can produce
9
+ // an emission whose shape matches what today's monolith produces. It
10
+ // does NOT replace `transform.ts`'s `__prefixes` emission yet — that's
11
+ // the v2c decomposition push proper. The two paths run side-by-side
12
+ // during the validation pass (`test/poc-module-prefixes.test.ts`),
13
+ // and the test asserts the path sets match.
14
+ //
15
+ // Once the monolith decomposes, this module owns `__prefixes` and the
16
+ // legacy injection path in `transform.ts` deletes.
17
+ import ts from 'typescript';
18
+ import { collectStatePathsFromSource } from '../collect-deps.js';
19
+ export const reactivePathsModule = {
20
+ name: 'reactive-paths',
21
+ compilerVersion: '^0.3.0',
22
+ diagnostics: [],
23
+ visitors: {
24
+ [ts.SyntaxKind.SourceFile]: (ctx, node) => {
25
+ const slot = ctx.getSlot('reactive-paths', () => ({
26
+ sourceFile: null,
27
+ }));
28
+ slot.sourceFile = node;
29
+ },
30
+ },
31
+ emit(ctx, analysis) {
32
+ const slot = analysis.perModule.get('reactive-paths');
33
+ if (!slot?.sourceFile)
34
+ return [];
35
+ const paths = collectStatePathsFromSource(slot.sourceFile);
36
+ if (paths.size === 0)
37
+ return [];
38
+ // Build `__prefixes: [s => s.foo, s => s.bar.baz, ...]` as an
39
+ // ArrayLiteralExpression. The accessor functions are stable
40
+ // closures; the runtime's `computeDirtyFromPrefixes` reference-
41
+ // compares each prefix(prev) vs prefix(next) per bit.
42
+ const arrows = [...paths].sort().map((path) => buildPrefixAccessor(ctx.factory, path));
43
+ const arrayLit = ctx.factory.createArrayLiteralExpression(arrows, false);
44
+ const contribution = {
45
+ module: 'reactive-paths',
46
+ field: '__prefixes',
47
+ value: arrayLit,
48
+ };
49
+ return [contribution];
50
+ },
51
+ };
52
+ /**
53
+ * Build a `(s) => s?.<path>?.<leaf>` arrow expression for a dotted path.
54
+ * `path` is depth-2 normalised by the collector (e.g. `user.name`).
55
+ *
56
+ * Multi-segment paths use optional chaining (`?.`) on every segment so
57
+ * the prefix function stays well-defined under structural-sharing
58
+ * reducers where an intermediate slice may be undefined transiently.
59
+ * Single-segment paths (`s.theme`) use plain `.` since there's no
60
+ * intermediate. The monolith's `buildAccess` in `transform.ts` uses the
61
+ * exact same shape — produces byte-equivalent emission when the path
62
+ * sets match.
63
+ */
64
+ function buildPrefixAccessor(f, path) {
65
+ const parts = path.split('.');
66
+ const useChain = parts.length > 1;
67
+ let expr = f.createIdentifier('s');
68
+ for (const part of parts) {
69
+ expr = useChain
70
+ ? f.createPropertyAccessChain(expr, f.createToken(ts.SyntaxKind.QuestionDotToken), f.createIdentifier(part))
71
+ : f.createPropertyAccessExpression(expr, f.createIdentifier(part));
72
+ }
73
+ return f.createArrowFunction(undefined, undefined, [
74
+ f.createParameterDeclaration(undefined, undefined, f.createIdentifier('s'), undefined, undefined, undefined),
75
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), expr);
76
+ }
77
+ //# sourceMappingURL=reactive-paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactive-paths.js","sourceRoot":"","sources":["../../src/modules/reactive-paths.ts"],"names":[],"mappings":"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","sourcesContent":["// `reactive-paths` — proof-of-concept compiler module (v2c §2 / v2c/2.3).\n//\n// Walks the file's reactive accessors via the same path-collection logic\n// the monolithic `transform.ts` uses (`collectStatePathsFromSource`) and\n// emits a `__prefixes: [...]` array — one stable closure per minimal\n// reference-stable prefix, in sorted order.\n//\n// The POC's job is to prove the visitor-registry primitive can produce\n// an emission whose shape matches what today's monolith produces. It\n// does NOT replace `transform.ts`'s `__prefixes` emission yet — that's\n// the v2c decomposition push proper. The two paths run side-by-side\n// during the validation pass (`test/poc-module-prefixes.test.ts`),\n// and the test asserts the path sets match.\n//\n// Once the monolith decomposes, this module owns `__prefixes` and the\n// legacy injection path in `transform.ts` deletes.\n\nimport ts from 'typescript'\nimport { collectStatePathsFromSource } from '../collect-deps.js'\nimport type { CompilerModule, EmissionContribution } from '../module.js'\n\ninterface ReactivePathsSlot {\n /** Source file the module was applied to — for emission-time access. */\n sourceFile: ts.SourceFile | null\n}\n\nexport const reactivePathsModule: CompilerModule = {\n name: 'reactive-paths',\n compilerVersion: '^0.3.0',\n diagnostics: [],\n\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const slot = ctx.getSlot(\n 'reactive-paths',\n (): ReactivePathsSlot => ({\n sourceFile: null,\n }),\n )\n slot.sourceFile = node as ts.SourceFile\n },\n },\n\n emit(ctx, analysis) {\n const slot = analysis.perModule.get('reactive-paths') as ReactivePathsSlot | undefined\n if (!slot?.sourceFile) return []\n const paths = collectStatePathsFromSource(slot.sourceFile)\n if (paths.size === 0) return []\n\n // Build `__prefixes: [s => s.foo, s => s.bar.baz, ...]` as an\n // ArrayLiteralExpression. The accessor functions are stable\n // closures; the runtime's `computeDirtyFromPrefixes` reference-\n // compares each prefix(prev) vs prefix(next) per bit.\n const arrows = [...paths].sort().map((path) => buildPrefixAccessor(ctx.factory, path))\n const arrayLit = ctx.factory.createArrayLiteralExpression(arrows, false)\n\n const contribution: EmissionContribution = {\n module: 'reactive-paths',\n field: '__prefixes',\n value: arrayLit,\n }\n return [contribution]\n },\n}\n\n/**\n * Build a `(s) => s?.<path>?.<leaf>` arrow expression for a dotted path.\n * `path` is depth-2 normalised by the collector (e.g. `user.name`).\n *\n * Multi-segment paths use optional chaining (`?.`) on every segment so\n * the prefix function stays well-defined under structural-sharing\n * reducers where an intermediate slice may be undefined transiently.\n * Single-segment paths (`s.theme`) use plain `.` since there's no\n * intermediate. The monolith's `buildAccess` in `transform.ts` uses the\n * exact same shape — produces byte-equivalent emission when the path\n * sets match.\n */\nfunction buildPrefixAccessor(f: ts.NodeFactory, path: string): ts.ArrowFunction {\n const parts = path.split('.')\n const useChain = parts.length > 1\n let expr: ts.Expression = f.createIdentifier('s')\n for (const part of parts) {\n expr = useChain\n ? f.createPropertyAccessChain(\n expr,\n f.createToken(ts.SyntaxKind.QuestionDotToken),\n f.createIdentifier(part),\n )\n : f.createPropertyAccessExpression(expr, f.createIdentifier(part))\n }\n return f.createArrowFunction(\n undefined,\n undefined,\n [\n f.createParameterDeclaration(\n undefined,\n undefined,\n f.createIdentifier('s'),\n undefined,\n undefined,\n undefined,\n ),\n ],\n undefined,\n f.createToken(ts.SyntaxKind.EqualsGreaterThanToken),\n expr,\n )\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { CompilerModule } from '../module.js';
2
+ export interface RowFactoryModuleOptions {
3
+ viewHelperNames: Set<string>;
4
+ viewHelperAliases: Map<string, string>;
5
+ /** Filename for the warn message. */
6
+ filename: string;
7
+ /** Original source text — passed through to the rewrite (unused
8
+ * by the function, but the signature requires it). */
9
+ source: string;
10
+ }
11
+ export declare function rowFactoryModule(options: RowFactoryModuleOptions): CompilerModule;
12
+ //# sourceMappingURL=row-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-factory.d.ts","sourceRoot":"","sources":["../../src/modules/row-factory.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC5B,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB;2DACuD;IACvD,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,cAAc,CAqBjF"}