@lmjs/core 1.0.7 → 2.0.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.
package/src/walk.js ADDED
@@ -0,0 +1,749 @@
1
+ // walk.js — HTML/JS AST walker + reactive-variable rewrite compiler.
2
+ //
3
+ // Cleaned 2026-09-13: this file previously carried three competing, stacked
4
+ // implementations of the reactive-variable rewrite pass (see this repo's
5
+ // PROVENANCE.md / working-session history for the comparison). Two abandoned
6
+ // approaches were removed:
7
+ // 1. An early scope-tracking scheme (`getL1Vs_`/`digDeepL1Vs`/`transformL1Vs`
8
+ // and a large commented-out predecessor of `getL1Vs`) that rewrote
9
+ // identifiers by string-concatenating onto `node.name` directly
10
+ // (e.g. `node.name = "_vt." + node.name`) — this produces a syntactically
11
+ // invalid Identifier node (dotted names aren't valid JS identifiers) that
12
+ // only "worked" because the code generator doesn't validate names before
13
+ // printing them. Breaks any real AST-based tooling downstream (source
14
+ // maps included).
15
+ // 2. Two draft passes (both named `changeReactiveVarsOccurences`) that
16
+ // rewrote references to `x.value` — the Vue-ref/Solid-signal pattern.
17
+ // Structurally clean, but incompatible with this runtime's actual data
18
+ // model: `_re.js` stores all reactive state in one flat `_vt.View.vars`
19
+ // object behind a single Proxy, not as individually boxed values per
20
+ // variable, so `x.value` has nothing to resolve against.
21
+ // What's kept below is the third approach — rewriting references to
22
+ // `_vt.View.vars["<name>"]`, a real MemberExpression matching `_re.js`'s
23
+ // actual Proxy target — which is what's live in production today (verified
24
+ // against the actual `lmjs.beacdn.com` server, see PROVENANCE.md).
25
+ //
26
+ // 2026-09-13: two Phase 1 fixes on top of the cleanup above.
27
+ // - `transformTopLevelDeclarations` used to only rewrite a VariableDeclaration
28
+ // with exactly one declarator; `var x = 1, y = 2;` was silently left as a
29
+ // real `var` and never wired into `_vt.View.vars`. Now handles any number
30
+ // of declarators, splitting reactive and non-reactive ones into separate
31
+ // statements as needed.
32
+ // - `getWatcher` now runs `validateBeforeRewrite` first: it regenerates the
33
+ // *original*, unmodified script via `astring.generate` and parses it with
34
+ // `new Function(...)` (compiled, never executed) so a real JS engine — not
35
+ // hand-rolled logic — catches genuine errors (a duplicate `let`/`const`,
36
+ // for instance) using the developer's actual variable names, before the
37
+ // `_vt.View.vars[...]` rewrite would otherwise erase that error entirely.
38
+ // Reports through `reportLumenError` (see `_re.js`).
39
+
40
+ class WalkerBase {
41
+ constructor() {
42
+ this.should_skip = false;
43
+ this.should_remove = false;
44
+ this.replacement = null;
45
+ this.context = {
46
+ skip: () => (this.should_skip = true),
47
+ remove: () => (this.should_remove = true),
48
+ replace: (node) => (this.replacement = node)
49
+ };
50
+ }
51
+ replace(parent, prop, index, node) {
52
+ if (parent && prop) {
53
+ if (index != null) {
54
+ (parent[prop])[index] = node;
55
+ } else {
56
+ (parent[prop]) = node;
57
+ }
58
+ }
59
+ }
60
+
61
+ remove(parent, prop, index) {
62
+ if (parent && prop) {
63
+ if (index !== null && index !== undefined) {
64
+ (parent[prop]).splice(index, 1);
65
+ } else {
66
+ delete parent[prop];
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ class SyncWalker extends WalkerBase {
73
+
74
+ constructor(enter, leave) {
75
+ super();
76
+
77
+ this.should_skip = false;
78
+ this.should_remove = false;
79
+ this.replacement = null;
80
+
81
+ this.context = {
82
+ skip: () => (this.should_skip = true),
83
+ remove: () => (this.should_remove = true),
84
+ replace: (node) => (this.replacement = node)
85
+ };
86
+
87
+ this.enter = enter;
88
+ this.leave = leave;
89
+ }
90
+
91
+ visit(node, parent, prop, index) {
92
+ if (node) {
93
+ if (this.enter) {
94
+ const _should_skip = this.should_skip;
95
+ const _should_remove = this.should_remove;
96
+ const _replacement = this.replacement;
97
+ this.should_skip = false;
98
+ this.should_remove = false;
99
+ this.replacement = null;
100
+
101
+ this.enter.call(this.context, node, parent, prop, index);
102
+
103
+ if (this.replacement) {
104
+ if (Array.isArray(this.replacement)) {
105
+ var expressions = [];
106
+ for (let rp = 0; rp < this.replacement.length; rp++) {
107
+ expressions.push(this.replacement[rp]);
108
+ }
109
+ if (this.replacement.length > 1) {
110
+ node = {
111
+ "type": "VariableDeclaration",
112
+ "start": node.start,
113
+ "kind": "let",
114
+ "declarations": expressions,
115
+ "level": node.level,
116
+ "scope": node.scope
117
+ };
118
+ } else {
119
+ node = {
120
+ "type": "ExpressionStatement",
121
+ "expression": {
122
+ "type": "SequenceExpression",
123
+ "expressions": expressions,
124
+ "level": node.level,
125
+ "scope": node.scope
126
+ },
127
+ "level": node.level,
128
+ "scope": node.scope
129
+ };
130
+ }
131
+ this.replace(parent, prop, index, node);
132
+ } else {
133
+ node = this.replacement;
134
+ this.replace(parent, prop, index, node);
135
+ }
136
+ }
137
+
138
+ if (this.should_remove) {
139
+ this.remove(parent, prop, index);
140
+ }
141
+
142
+ const skipped = this.should_skip;
143
+ const removed = this.should_remove;
144
+
145
+ this.should_skip = _should_skip;
146
+ this.should_remove = _should_remove;
147
+ this.replacement = _replacement;
148
+
149
+ if (skipped) return node;
150
+ if (removed) return null;
151
+ }
152
+
153
+ let key;
154
+
155
+ for (key in node) {
156
+ const value = node[key];
157
+ if (value && typeof value === 'object') {
158
+ if (Array.isArray(value)) {
159
+ const nodes = (value);
160
+ for (let i = 0; i < nodes.length; i += 1) {
161
+ const item = nodes[i];
162
+ if (isNode(item)) {
163
+ if (!this.visit(item, node, key, i)) {
164
+ i--;
165
+ }
166
+ }
167
+ }
168
+ } else if (isNode(value)) {
169
+ this.visit(value, node, key, null);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (this.leave) {
175
+ const _replacement = this.replacement;
176
+ const _should_remove = this.should_remove;
177
+ this.replacement = null;
178
+ this.should_remove = false;
179
+
180
+ this.leave.call(this.context, node, parent, prop, index);
181
+
182
+ if (this.replacement) {
183
+ if (Array.isArray(this.replacement)) {
184
+ for (let rp = 0; rp < this.replacement.length; rp++) {
185
+ node = this.replacement[rp];
186
+ this.replace(parent, prop, index, node);
187
+ }
188
+ } else {
189
+ node = this.replacement;
190
+ this.replace(parent, prop, index, node);
191
+ }
192
+ }
193
+
194
+ if (this.should_remove) {
195
+ this.remove(parent, prop, index);
196
+ }
197
+
198
+ const removed = this.should_remove;
199
+
200
+ this.replacement = _replacement;
201
+ this.should_remove = _should_remove;
202
+
203
+ if (removed) return null;
204
+ }
205
+ }
206
+
207
+ return node;
208
+ }
209
+ }
210
+
211
+ function isNode(value) {
212
+ return (
213
+ value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string'
214
+ );
215
+ }
216
+
217
+ function walk(ast, { enter, leave }) {
218
+ const instance = new SyncWalker(enter, leave);
219
+ return instance.visit(ast, null);
220
+ }
221
+
222
+
223
+ function getProgramBody(node) {
224
+ if (node.type == 'Program') {
225
+ return node.body;
226
+ }
227
+ return node;
228
+ }
229
+
230
+ function parseNode(node) {
231
+ // console.log("Parsing", node);
232
+ }
233
+
234
+ function checkNodeL1(node, varz, vazzz) {
235
+ try {
236
+ if (node && typeof node === 'object') {
237
+ if (Array.isArray(node)) {
238
+ for (let i = 0; i < node.length; i++) {
239
+ const nd = node[i];
240
+ if (isNode(nd)) {
241
+ if (nd.type === "VariableDeclaration") {
242
+ let declarators = nd.declarations;
243
+ for (let x = 0; x < declarators.length; x++) {
244
+ let dec = declarators[x].id;
245
+
246
+ if (vazzz.includes(dec.name)) {
247
+ varz.push({
248
+ name: dec.name,
249
+ node: dec
250
+ });
251
+ dec.marked = true;
252
+ }
253
+ }
254
+ } else if (nd.type == "Identifier") {
255
+ }
256
+
257
+ parseNode(nd);
258
+ }
259
+ }
260
+ } else if (isNode(node)) {
261
+ }
262
+ }
263
+ } catch (e) {
264
+ }
265
+ }
266
+
267
+ function getL1Vs(AST, view, vazzz) {
268
+ var level = 0, block = [{ start: 0 }];
269
+ var varz = view?.varz ?? [];
270
+
271
+ let nodes = getProgramBody(AST);
272
+ checkNodeL1(nodes, varz, vazzz);
273
+
274
+ return {
275
+ "varz": varz,
276
+ "AST": AST
277
+ };
278
+ }
279
+
280
+ function getWatcher(AST, view, vazzz, targetKey = "View") {
281
+ let Vars = getL1Vs(AST, view, vazzz);
282
+ AST = Vars['AST'];
283
+ let varz = Vars['varz'];
284
+
285
+ validateBeforeRewrite(AST, view);
286
+
287
+ AST = changeReactiveVarsOccurences(AST, vazzz, targetKey);
288
+ AST = transformTopLevelDeclarations(AST, vazzz, targetKey);
289
+
290
+ return {
291
+ "code": astring.generate(AST),
292
+ "varz": varz
293
+ };
294
+ }
295
+
296
+ // Catches real JS errors (duplicate declarations, etc.) using the developer's
297
+ // original variable names, by handing the *unmodified* AST to a real engine
298
+ // before the `_vt.View.vars[...]` rewrite would otherwise make them invisible.
299
+ // `new Function(...)` only compiles this — it is never invoked, so nothing in
300
+ // the developer's script actually runs here.
301
+ function validateBeforeRewrite(AST, view) {
302
+ try {
303
+ new Function(astring.generate(AST));
304
+ } catch (e) {
305
+ if (typeof reportLumenError === 'function') {
306
+ reportLumenError({
307
+ stage: 'validate',
308
+ view: view?.name,
309
+ error: e,
310
+ hint: "This is a real JavaScript error in your <script> block (for example, a variable declared twice with let/const) — fix it in the .view file; it will not surface again once rewritten."
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ // The kept reactive-variable rewrite: turns a reference to a reactive
317
+ // variable into `_vt.View.vars["<name>"]`, matching `_re.js`'s Proxy target.
318
+ // `targetKey` (2026-09-14): which root of `_vt` to rewrite into — "View"
319
+ // (default, unchanged) for .view <script> blocks, "Global" for index.js's
320
+ // top-level vars, which need to outlive a single view (see _re.js's _vt.Global).
321
+ function changeReactiveVarsOccurences(AST, reactiveVariables, targetKey = "View") {
322
+ const reactive = new Set(reactiveVariables || []);
323
+
324
+ // ---- Scope Management ----
325
+ // Each scope entry: { isFunction: boolean, names: Set<string> }
326
+ const scopeStack = [];
327
+ const bindingIdNodes = new WeakSet(); // Identifier AST nodes that are binding positions
328
+
329
+ const pushScope = (isFunction) => scopeStack.push({ isFunction: !!isFunction, names: new Set() });
330
+ const popScope = () => scopeStack.pop();
331
+ const currentScope = () => scopeStack[scopeStack.length - 1];
332
+
333
+ // declare name in nearest function scope (for 'var'/'function') or current scope otherwise
334
+ function declare(name, kind) {
335
+ if (!name) return;
336
+ if (kind === "var" || kind === "function") {
337
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
338
+ if (scopeStack[i].isFunction || i === 0) {
339
+ scopeStack[i].names.add(name);
340
+ return;
341
+ }
342
+ }
343
+ } else {
344
+ // let/const/class/param etc -> current (block) scope
345
+ currentScope().names.add(name);
346
+ }
347
+ }
348
+
349
+ // Helper: root-level existence (declared in program scope)
350
+ function rootHas(name) {
351
+ return scopeStack.length > 0 && scopeStack[0].names.has(name);
352
+ }
353
+
354
+ // Helper: is this name shadowed by any inner scope (excluding root)
355
+ function isShadowedFromRoot(name) {
356
+ for (let i = scopeStack.length - 1; i >= 1; i--) {
357
+ if (scopeStack[i].names.has(name)) return true;
358
+ }
359
+ return false;
360
+ }
361
+
362
+ // ----- Pattern utilities (destructuring, rest, assignment patterns) -----
363
+ function visitPattern(node, onId) {
364
+ if (!node) return;
365
+ switch (node.type) {
366
+ case "Identifier":
367
+ onId(node);
368
+ break;
369
+ case "RestElement":
370
+ visitPattern(node.argument, onId);
371
+ break;
372
+ case "AssignmentPattern":
373
+ visitPattern(node.left, onId);
374
+ break;
375
+ case "ArrayPattern":
376
+ for (const el of node.elements) if (el) visitPattern(el, onId);
377
+ break;
378
+ case "ObjectPattern":
379
+ for (const p of node.properties) {
380
+ if (p.type === "Property") visitPattern(p.value, onId);
381
+ else if (p.type === "RestElement") visitPattern(p.argument, onId);
382
+ }
383
+ break;
384
+ default:
385
+ break;
386
+ }
387
+ }
388
+
389
+ // Mark pattern bindings (used for variable declarators, params, catch params)
390
+ function markPatternBindings(pattern, kind = "var") {
391
+ if (!pattern) return;
392
+ visitPattern(pattern, (idNode) => {
393
+ bindingIdNodes.add(idNode);
394
+ declare(idNode.name, kind);
395
+ });
396
+ }
397
+
398
+ // Predeclare top-level lexicals and hoisted declarations for Program
399
+ function predeclareProgram(programNode) {
400
+ if (!programNode || !Array.isArray(programNode.body)) return;
401
+ for (const stmt of programNode.body) {
402
+ if (stmt.type === "VariableDeclaration") {
403
+ // declare all declared names in program scope (var/let/const)
404
+ for (const d of stmt.declarations) {
405
+ visitPattern(d.id, (id) => {
406
+ // binding id node (declaration) should be marked
407
+ bindingIdNodes.add(id);
408
+ declare(id.name, stmt.kind);
409
+ });
410
+ }
411
+ } else if (stmt.type === "FunctionDeclaration" && stmt.id) {
412
+ bindingIdNodes.add(stmt.id);
413
+ declare(stmt.id.name, "function");
414
+ } else if (stmt.type === "ClassDeclaration" && stmt.id) {
415
+ bindingIdNodes.add(stmt.id);
416
+ declare(stmt.id.name, "let");
417
+ } else if (stmt.type === "ImportDeclaration") {
418
+ for (const spec of stmt.specifiers || []) {
419
+ if (spec.local) {
420
+ bindingIdNodes.add(spec.local);
421
+ declare(spec.local.name, "const");
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ // Predeclare block-scoped names at the start of a block (helps with TDZ)
429
+ function predeclareBlockLexicals(blockNode) {
430
+ if (!blockNode || !Array.isArray(blockNode.body)) return;
431
+ for (const stmt of blockNode.body) {
432
+ if (stmt.type === "VariableDeclaration" && stmt.kind !== "var") {
433
+ for (const d of stmt.declarations) {
434
+ visitPattern(d.id, (id) => {
435
+ bindingIdNodes.add(id);
436
+ // block-scoped: declareHere
437
+ currentScope().names.add(id.name);
438
+ });
439
+ }
440
+ } else if (stmt.type === "FunctionDeclaration" && stmt.id) {
441
+ // function declarations are block-scoped in modern JS
442
+ bindingIdNodes.add(stmt.id);
443
+ currentScope().names.add(stmt.id.name);
444
+ } else if (stmt.type === "ClassDeclaration" && stmt.id) {
445
+ bindingIdNodes.add(stmt.id);
446
+ currentScope().names.add(stmt.id.name);
447
+ }
448
+ }
449
+ }
450
+
451
+ // Predeclare for-loop header bindings (let/const in header shadow test/update/body)
452
+ function predeclareForHeader(node) {
453
+ const header = node.type === "ForStatement" ? node.init : node.left;
454
+ if (header && header.type === "VariableDeclaration" && header.kind !== "var") {
455
+ for (const d of header.declarations) {
456
+ visitPattern(d.id, (id) => {
457
+ bindingIdNodes.add(id);
458
+ currentScope().names.add(id.name);
459
+ });
460
+ }
461
+ }
462
+ }
463
+
464
+ // ----- Node-level checks to avoid rewriting keys/bindings/labels/imports -----
465
+ function shouldSkipIdentifier(node, parent, prop) {
466
+ if (!parent) return false;
467
+
468
+ // Label identifiers
469
+ if (
470
+ (parent.type === "LabeledStatement" && prop === "label") ||
471
+ ((parent.type === "BreakStatement" || parent.type === "ContinueStatement") && prop === "label")
472
+ ) return true;
473
+
474
+ // MemberExpression property (non-computed): obj.x -> don't rewrite 'x' as a property name
475
+ if (parent.type === "MemberExpression") {
476
+ if (prop === "property" && parent.computed === false) return true;
477
+ }
478
+
479
+ // Object literal property key (non-computed)
480
+ if (parent.type === "Property") {
481
+ if (prop === "key" && parent.computed === false) return true;
482
+ // If shorthand `{ x }` we will handle by turning shorthand=false when replacing the value
483
+ }
484
+
485
+ // MethodDefinition / ClassProperty keys (non-computed)
486
+ if ((parent.type === "MethodDefinition" || parent.type === "ClassProperty" || parent.type === "PropertyDefinition") &&
487
+ prop === "key" && parent.computed === false) return true;
488
+
489
+ // Import/Export specifiers: they are bindings and usually already marked, but double-guard
490
+ if (
491
+ parent.type === "ImportSpecifier" ||
492
+ parent.type === "ImportDefaultSpecifier" ||
493
+ parent.type === "ImportNamespaceSpecifier" ||
494
+ parent.type === "ExportSpecifier"
495
+ ) return true;
496
+
497
+ return false;
498
+ }
499
+
500
+ // ---- Recursive walker (post-order) ----
501
+ function walk(node, parent, prop, index) {
502
+ if (!node || typeof node !== "object") return;
503
+
504
+ // ENTER: handle scope creation & pre-declarations BEFORE visiting children
505
+ switch (node.type) {
506
+ case "Program":
507
+ pushScope(true);
508
+ predeclareProgram(node);
509
+ break;
510
+
511
+ case "BlockStatement":
512
+ case "StaticBlock":
513
+ pushScope(false);
514
+ predeclareBlockLexicals(node);
515
+ break;
516
+
517
+ case "FunctionDeclaration":
518
+ // function name is hoisted to the outer scope
519
+ if (node.id) {
520
+ bindingIdNodes.add(node.id);
521
+ declare(node.id.name, "function"); // hoist to nearest function/program (outer)
522
+ }
523
+ pushScope(true);
524
+ // params bind in the new function scope
525
+ for (const p of node.params) markPatternBindings(p, "param");
526
+ break;
527
+
528
+ case "FunctionExpression":
529
+ pushScope(true);
530
+ // named function expression: name binds in its own function scope
531
+ if (node.id) {
532
+ bindingIdNodes.add(node.id);
533
+ declare(node.id.name, "let"); // local to this function scope
534
+ }
535
+ for (const p of node.params) markPatternBindings(p, "param");
536
+ break;
537
+
538
+ case "ArrowFunctionExpression":
539
+ pushScope(true);
540
+ for (const p of node.params) markPatternBindings(p, "param");
541
+ break;
542
+
543
+ case "CatchClause":
544
+ pushScope(false);
545
+ if (node.param) markPatternBindings(node.param, "let");
546
+ break;
547
+
548
+ case "ForStatement":
549
+ case "ForInStatement":
550
+ case "ForOfStatement":
551
+ pushScope(false);
552
+ predeclareForHeader(node);
553
+ break;
554
+
555
+ case "VariableDeclaration":
556
+ // declare bindings for each declarator (var/let/const)
557
+ for (const decl of node.declarations) {
558
+ markPatternBindings(decl.id, node.kind || "var");
559
+ }
560
+ break;
561
+
562
+ case "ClassDeclaration":
563
+ if (node.id) {
564
+ bindingIdNodes.add(node.id);
565
+ declare(node.id.name, "let");
566
+ }
567
+ break;
568
+
569
+ case "ImportDeclaration":
570
+ for (const spec of node.specifiers || []) {
571
+ if (spec.local) {
572
+ bindingIdNodes.add(spec.local);
573
+ declare(spec.local.name, "const");
574
+ }
575
+ }
576
+ break;
577
+ }
578
+
579
+ // Recurse children (post-order traversal so declarations are marked before uses below)
580
+ for (const key in node) {
581
+ if (key === "parent") continue;
582
+ const child = node[key];
583
+ if (Array.isArray(child)) {
584
+ for (let i = 0; i < child.length; i++) {
585
+ if (child[i] && typeof child[i] === "object") {
586
+ walk(child[i], node, key, i);
587
+ }
588
+ }
589
+ } else if (child && typeof child === "object") {
590
+ walk(child, node, key, null);
591
+ }
592
+ }
593
+
594
+ // POST: rewrite Identifier references if they meet the criteria
595
+ if (node.type === "Identifier") {
596
+ const name = node.name;
597
+
598
+ // Only consider reactive names and only non-binding identifier nodes
599
+ if (!reactive.has(name) || bindingIdNodes.has(node)) {
600
+ // do nothing
601
+ } else if (!rootHas(name)) {
602
+ // Not declared at program root: we only rewrite variables declared at root
603
+ } else if (isShadowedFromRoot(name)) {
604
+ // An inner scope declared the same name -> the reference belongs to inner binding; skip
605
+ } else if (shouldSkipIdentifier(node, parent, prop)) {
606
+ // e.g., property key, non-computed member property, import/export, labels...
607
+ } else {
608
+ // Special-case object-property shorthand: if we're the value of a shorthand property,
609
+ // we must turn shorthand off so `{ x }` becomes `{ x: _vt.View.vars["x"] }`
610
+ if (parent && parent.type === "Property" && parent.shorthand && prop === "value") {
611
+ parent.shorthand = false;
612
+ }
613
+
614
+ // Build computed member expression: _vt.View.vars["<name>"]
615
+ const replacement = {
616
+ type: "MemberExpression",
617
+ object: {
618
+ type: "MemberExpression",
619
+ object: {
620
+ type: "MemberExpression",
621
+ object: { type: "Identifier", name: "_vt" },
622
+ property: { type: "Identifier", name: targetKey },
623
+ computed: false
624
+ },
625
+ property: { type: "Identifier", name: "vars" },
626
+ computed: false
627
+ },
628
+ property: { type: "Literal", value: name, raw: JSON.stringify(name) },
629
+ computed: true
630
+ };
631
+
632
+ // Replace in parent structure
633
+ if (parent) {
634
+ if (index !== null && Array.isArray(parent[prop])) {
635
+ parent[prop][index] = replacement;
636
+ } else {
637
+ parent[prop] = replacement;
638
+ }
639
+ } else {
640
+ // unlikely: top-level Identifier as AST root — replace the root reference
641
+ // (some tools expect AST to be an object with Program root, but be safe)
642
+ // mutate node in-place:
643
+ Object.keys(node).forEach(k => delete node[k]);
644
+ Object.assign(node, replacement);
645
+ }
646
+ }
647
+ }
648
+
649
+ // EXIT: pop scopes created on enter
650
+ switch (node.type) {
651
+ case "Program":
652
+ case "BlockStatement":
653
+ case "StaticBlock":
654
+ case "FunctionDeclaration":
655
+ case "FunctionExpression":
656
+ case "ArrowFunctionExpression":
657
+ case "CatchClause":
658
+ case "ForStatement":
659
+ case "ForInStatement":
660
+ case "ForOfStatement":
661
+ popScope();
662
+ break;
663
+ default:
664
+ break;
665
+ }
666
+ }
667
+
668
+ // Run walker
669
+ walk(AST, null, null, null);
670
+ return AST;
671
+ }
672
+
673
+ // KNOWN GAP (see file header): only rewrites a VariableDeclaration with
674
+ // exactly one declarator. `var x = 1, y = 2;` is left untouched.
675
+ function _reactiveAssignStatement(name, init, targetKey = "View") {
676
+ return {
677
+ type: "ExpressionStatement",
678
+ expression: {
679
+ type: "AssignmentExpression",
680
+ operator: "=",
681
+ left: {
682
+ type: "MemberExpression",
683
+ computed: true,
684
+ object: {
685
+ type: "MemberExpression",
686
+ computed: false,
687
+ object: {
688
+ type: "MemberExpression",
689
+ computed: false,
690
+ object: { type: "Identifier", name: "_vt" },
691
+ property: { type: "Identifier", name: targetKey }
692
+ },
693
+ property: { type: "Identifier", name: "vars" }
694
+ },
695
+ property: { type: "Literal", value: name }
696
+ },
697
+ right: init || { type: "Identifier", name: "undefined" }
698
+ }
699
+ };
700
+ }
701
+
702
+ // Handles any number of declarators in a single top-level statement, not just
703
+ // one. `var x = 1, y = 2;` with only `x` reactive becomes two statements:
704
+ // `_vt.View.vars["x"] = 1;` followed by a real `var y = 2;` for the rest —
705
+ // each declarator only needs its own rewrite, and non-reactive ones must stay
706
+ // real declarations to remain valid references elsewhere in the script.
707
+ function transformTopLevelDeclarations(AST, reactiveVariables, targetKey = "View") {
708
+ const reactive = new Set(reactiveVariables || []);
709
+ const newBody = [];
710
+
711
+ for (const stmt of AST.body) {
712
+ if (stmt.type !== "VariableDeclaration") {
713
+ newBody.push(stmt);
714
+ continue;
715
+ }
716
+
717
+ const reactiveDecls = stmt.declarations.filter(d => reactive.has(d.id.name));
718
+ const nonReactiveDecls = stmt.declarations.filter(d => !reactive.has(d.id.name));
719
+
720
+ if (reactiveDecls.length === 0) {
721
+ newBody.push(stmt);
722
+ continue;
723
+ }
724
+
725
+ for (const decl of reactiveDecls) {
726
+ newBody.push(_reactiveAssignStatement(decl.id.name, decl.init, targetKey));
727
+ }
728
+
729
+ if (nonReactiveDecls.length > 0) {
730
+ newBody.push({
731
+ type: "VariableDeclaration",
732
+ kind: stmt.kind,
733
+ declarations: nonReactiveDecls
734
+ });
735
+ }
736
+ }
737
+
738
+ AST.body = newBody;
739
+ return AST;
740
+ }
741
+
742
+ // Node-only export (2026-09-14) so the CLI's build/serve path (packages/cli's
743
+ // fcs.js) can reuse the exact same rewrite for index.js's top-level vars
744
+ // instead of re-implementing it. `module` doesn't exist in the browser
745
+ // bundle this file is concatenated into (see build/bundle.js), so this is a
746
+ // silent no-op there — it only ever runs in Node.
747
+ if (typeof module !== 'undefined' && module.exports) {
748
+ module.exports = { getWatcher, changeReactiveVarsOccurences, transformTopLevelDeclarations, validateBeforeRewrite };
749
+ }