@lmjs/core 1.0.7 → 2.0.2

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,837 @@
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
+ // 2026-09-17, real bug found and fixed: changeReactiveVarsOccurences()/
282
+ // transformTopLevelDeclarations() mutate the AST IN PLACE (they
283
+ // replace nodes via `parent[prop] = replacement`, not by building a
284
+ // new tree) — harmless as long as a given AST object is only ever
285
+ // rewritten once, which used to always be true. Per-instance subview
286
+ // scoping changed that: renderHST() (_re.js) is called once per
287
+ // subview MOUNT, but all mounts of the same .view file share the
288
+ // exact same parsed `nd._jst` object (parsed once, cached, reused) —
289
+ // so the first instance's rewrite (into e.g. _vt.View.views[0].vars)
290
+ // permanently consumed the raw identifiers, and every subsequent
291
+ // instance's "rewrite" silently found nothing left to rewrite,
292
+ // regenerating instance 0's already-rewritten code unchanged. Found
293
+ // testing 3 real mounted instances of the same subview on lumenjs.com's
294
+ // own V2 homepage — only the first ever got its own correct scope.
295
+ // Cloning here (not at each call site) makes getWatcher() safe to
296
+ // call repeatedly against the same input AST, which is now a real
297
+ // requirement, not just good hygiene. ASTs are plain, JSON-safe data
298
+ // (no functions/circular refs), so JSON round-tripping is a correct,
299
+ // dependency-free deep clone — walk.js has no access to the vendored
300
+ // recursive clone() helper (a different, browser-only bundle file).
301
+ AST = JSON.parse(JSON.stringify(AST));
302
+
303
+ let Vars = getL1Vs(AST, view, vazzz);
304
+ AST = Vars['AST'];
305
+ let varz = Vars['varz'];
306
+
307
+ validateBeforeRewrite(AST, view);
308
+
309
+ AST = changeReactiveVarsOccurences(AST, vazzz, targetKey);
310
+ AST = transformTopLevelDeclarations(AST, vazzz, targetKey);
311
+
312
+ return {
313
+ "code": astring.generate(AST),
314
+ "varz": varz
315
+ };
316
+ }
317
+
318
+ // Catches real JS errors (duplicate declarations, etc.) using the developer's
319
+ // original variable names, by handing the *unmodified* AST to a real engine
320
+ // before the `_vt.View.vars[...]` rewrite would otherwise make them invisible.
321
+ // `new Function(...)` only compiles this — it is never invoked, so nothing in
322
+ // the developer's script actually runs here.
323
+ function validateBeforeRewrite(AST, view) {
324
+ try {
325
+ new Function(astring.generate(AST));
326
+ } catch (e) {
327
+ if (typeof reportLumenError === 'function') {
328
+ reportLumenError({
329
+ stage: 'validate',
330
+ view: view?.name,
331
+ error: e,
332
+ 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."
333
+ });
334
+ }
335
+ }
336
+ }
337
+
338
+ // 2026-09-17: `targetKey` used to only ever be a plain string ("View" or
339
+ // "Global"), producing a fixed 2-level root (`_vt.View`/`_vt.Global`). Real
340
+ // per-instance subview scoping (`_vt.View.views[i].vars`, and its nested
341
+ // form `_vt.View.views[0].views[2].vars` for a subview hosting a subview)
342
+ // needs a *deeper* chain than a single Identifier can express. Accepts
343
+ // either form now: a bare string is treated as a one-element path (100%
344
+ // unchanged output for every existing caller — "View"/"Global" still build
345
+ // exactly `_vt.View`/`_vt.Global`); an array of segments builds the real
346
+ // chain, a string segment as a non-computed Identifier access, a number
347
+ // segment as a computed numeric-literal access (`.views[3]`).
348
+ function buildTargetRootExpr(targetKey) {
349
+ const segments = Array.isArray(targetKey) ? targetKey : [targetKey];
350
+ let expr = { type: "Identifier", name: "_vt" };
351
+ for (const seg of segments) {
352
+ if (typeof seg === "number") {
353
+ expr = {
354
+ type: "MemberExpression",
355
+ object: expr,
356
+ property: { type: "Literal", value: seg, raw: String(seg) },
357
+ computed: true
358
+ };
359
+ } else {
360
+ expr = {
361
+ type: "MemberExpression",
362
+ object: expr,
363
+ property: { type: "Identifier", name: seg },
364
+ computed: false
365
+ };
366
+ }
367
+ }
368
+ return expr;
369
+ }
370
+
371
+ // The kept reactive-variable rewrite: turns a reference to a reactive
372
+ // variable into `_vt.View.vars["<name>"]`, matching `_re.js`'s Proxy target.
373
+ // `targetKey` (2026-09-14): which root of `_vt` to rewrite into — "View"
374
+ // (default, unchanged) for .view <script> blocks, "Global" for index.js's
375
+ // top-level vars, which need to outlive a single view (see _re.js's _vt.Global).
376
+ // Can also be a path array (see buildTargetRootExpr above) for a subview
377
+ // instance's own scope.
378
+ function changeReactiveVarsOccurences(AST, reactiveVariables, targetKey = "View") {
379
+ const reactive = new Set(reactiveVariables || []);
380
+
381
+ // ---- Scope Management ----
382
+ // Each scope entry: { isFunction: boolean, names: Set<string> }
383
+ const scopeStack = [];
384
+ const bindingIdNodes = new WeakSet(); // Identifier AST nodes that are binding positions
385
+
386
+ const pushScope = (isFunction) => scopeStack.push({ isFunction: !!isFunction, names: new Set() });
387
+ const popScope = () => scopeStack.pop();
388
+ const currentScope = () => scopeStack[scopeStack.length - 1];
389
+
390
+ // declare name in nearest function scope (for 'var'/'function') or current scope otherwise
391
+ function declare(name, kind) {
392
+ if (!name) return;
393
+ if (kind === "var" || kind === "function") {
394
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
395
+ if (scopeStack[i].isFunction || i === 0) {
396
+ scopeStack[i].names.add(name);
397
+ return;
398
+ }
399
+ }
400
+ } else {
401
+ // let/const/class/param etc -> current (block) scope
402
+ currentScope().names.add(name);
403
+ }
404
+ }
405
+
406
+ // Helper: root-level existence (declared in program scope)
407
+ function rootHas(name) {
408
+ return scopeStack.length > 0 && scopeStack[0].names.has(name);
409
+ }
410
+
411
+ // Helper: is this name shadowed by any inner scope (excluding root)
412
+ function isShadowedFromRoot(name) {
413
+ for (let i = scopeStack.length - 1; i >= 1; i--) {
414
+ if (scopeStack[i].names.has(name)) return true;
415
+ }
416
+ return false;
417
+ }
418
+
419
+ // ----- Pattern utilities (destructuring, rest, assignment patterns) -----
420
+ function visitPattern(node, onId) {
421
+ if (!node) return;
422
+ switch (node.type) {
423
+ case "Identifier":
424
+ onId(node);
425
+ break;
426
+ case "RestElement":
427
+ visitPattern(node.argument, onId);
428
+ break;
429
+ case "AssignmentPattern":
430
+ visitPattern(node.left, onId);
431
+ break;
432
+ case "ArrayPattern":
433
+ for (const el of node.elements) if (el) visitPattern(el, onId);
434
+ break;
435
+ case "ObjectPattern":
436
+ for (const p of node.properties) {
437
+ if (p.type === "Property") visitPattern(p.value, onId);
438
+ else if (p.type === "RestElement") visitPattern(p.argument, onId);
439
+ }
440
+ break;
441
+ default:
442
+ break;
443
+ }
444
+ }
445
+
446
+ // Mark pattern bindings (used for variable declarators, params, catch params)
447
+ function markPatternBindings(pattern, kind = "var") {
448
+ if (!pattern) return;
449
+ visitPattern(pattern, (idNode) => {
450
+ bindingIdNodes.add(idNode);
451
+ declare(idNode.name, kind);
452
+ });
453
+ }
454
+
455
+ // Predeclare top-level lexicals and hoisted declarations for Program
456
+ function predeclareProgram(programNode) {
457
+ if (!programNode || !Array.isArray(programNode.body)) return;
458
+ for (const stmt of programNode.body) {
459
+ if (stmt.type === "VariableDeclaration") {
460
+ // declare all declared names in program scope (var/let/const)
461
+ for (const d of stmt.declarations) {
462
+ visitPattern(d.id, (id) => {
463
+ // binding id node (declaration) should be marked
464
+ bindingIdNodes.add(id);
465
+ declare(id.name, stmt.kind);
466
+ });
467
+ }
468
+ } else if (stmt.type === "FunctionDeclaration" && stmt.id) {
469
+ bindingIdNodes.add(stmt.id);
470
+ declare(stmt.id.name, "function");
471
+ } else if (stmt.type === "ClassDeclaration" && stmt.id) {
472
+ bindingIdNodes.add(stmt.id);
473
+ declare(stmt.id.name, "let");
474
+ } else if (stmt.type === "ImportDeclaration") {
475
+ for (const spec of stmt.specifiers || []) {
476
+ if (spec.local) {
477
+ bindingIdNodes.add(spec.local);
478
+ declare(spec.local.name, "const");
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+
485
+ // Predeclare block-scoped names at the start of a block (helps with TDZ)
486
+ function predeclareBlockLexicals(blockNode) {
487
+ if (!blockNode || !Array.isArray(blockNode.body)) return;
488
+ for (const stmt of blockNode.body) {
489
+ if (stmt.type === "VariableDeclaration" && stmt.kind !== "var") {
490
+ for (const d of stmt.declarations) {
491
+ visitPattern(d.id, (id) => {
492
+ bindingIdNodes.add(id);
493
+ // block-scoped: declareHere
494
+ currentScope().names.add(id.name);
495
+ });
496
+ }
497
+ } else if (stmt.type === "FunctionDeclaration" && stmt.id) {
498
+ // function declarations are block-scoped in modern JS
499
+ bindingIdNodes.add(stmt.id);
500
+ currentScope().names.add(stmt.id.name);
501
+ } else if (stmt.type === "ClassDeclaration" && stmt.id) {
502
+ bindingIdNodes.add(stmt.id);
503
+ currentScope().names.add(stmt.id.name);
504
+ }
505
+ }
506
+ }
507
+
508
+ // Predeclare for-loop header bindings (let/const in header shadow test/update/body)
509
+ function predeclareForHeader(node) {
510
+ const header = node.type === "ForStatement" ? node.init : node.left;
511
+ if (header && header.type === "VariableDeclaration" && header.kind !== "var") {
512
+ for (const d of header.declarations) {
513
+ visitPattern(d.id, (id) => {
514
+ bindingIdNodes.add(id);
515
+ currentScope().names.add(id.name);
516
+ });
517
+ }
518
+ }
519
+ }
520
+
521
+ // ----- Node-level checks to avoid rewriting keys/bindings/labels/imports -----
522
+ function shouldSkipIdentifier(node, parent, prop) {
523
+ if (!parent) return false;
524
+
525
+ // Label identifiers
526
+ if (
527
+ (parent.type === "LabeledStatement" && prop === "label") ||
528
+ ((parent.type === "BreakStatement" || parent.type === "ContinueStatement") && prop === "label")
529
+ ) return true;
530
+
531
+ // MemberExpression property (non-computed): obj.x -> don't rewrite 'x' as a property name
532
+ if (parent.type === "MemberExpression") {
533
+ if (prop === "property" && parent.computed === false) return true;
534
+ }
535
+
536
+ // Object literal property key (non-computed)
537
+ if (parent.type === "Property") {
538
+ if (prop === "key" && parent.computed === false) return true;
539
+ // If shorthand `{ x }` we will handle by turning shorthand=false when replacing the value
540
+ }
541
+
542
+ // MethodDefinition / ClassProperty keys (non-computed)
543
+ if ((parent.type === "MethodDefinition" || parent.type === "ClassProperty" || parent.type === "PropertyDefinition") &&
544
+ prop === "key" && parent.computed === false) return true;
545
+
546
+ // Import/Export specifiers: they are bindings and usually already marked, but double-guard
547
+ if (
548
+ parent.type === "ImportSpecifier" ||
549
+ parent.type === "ImportDefaultSpecifier" ||
550
+ parent.type === "ImportNamespaceSpecifier" ||
551
+ parent.type === "ExportSpecifier"
552
+ ) return true;
553
+
554
+ return false;
555
+ }
556
+
557
+ // ---- Recursive walker (post-order) ----
558
+ function walk(node, parent, prop, index) {
559
+ if (!node || typeof node !== "object") return;
560
+
561
+ // ENTER: handle scope creation & pre-declarations BEFORE visiting children
562
+ switch (node.type) {
563
+ case "Program":
564
+ pushScope(true);
565
+ predeclareProgram(node);
566
+ break;
567
+
568
+ case "BlockStatement":
569
+ case "StaticBlock":
570
+ pushScope(false);
571
+ predeclareBlockLexicals(node);
572
+ break;
573
+
574
+ case "FunctionDeclaration":
575
+ // function name is hoisted to the outer scope
576
+ if (node.id) {
577
+ bindingIdNodes.add(node.id);
578
+ declare(node.id.name, "function"); // hoist to nearest function/program (outer)
579
+ }
580
+ pushScope(true);
581
+ // params bind in the new function scope
582
+ for (const p of node.params) markPatternBindings(p, "param");
583
+ break;
584
+
585
+ case "FunctionExpression":
586
+ pushScope(true);
587
+ // named function expression: name binds in its own function scope
588
+ if (node.id) {
589
+ bindingIdNodes.add(node.id);
590
+ declare(node.id.name, "let"); // local to this function scope
591
+ }
592
+ for (const p of node.params) markPatternBindings(p, "param");
593
+ break;
594
+
595
+ case "ArrowFunctionExpression":
596
+ pushScope(true);
597
+ for (const p of node.params) markPatternBindings(p, "param");
598
+ break;
599
+
600
+ case "CatchClause":
601
+ pushScope(false);
602
+ if (node.param) markPatternBindings(node.param, "let");
603
+ break;
604
+
605
+ case "ForStatement":
606
+ case "ForInStatement":
607
+ case "ForOfStatement":
608
+ pushScope(false);
609
+ predeclareForHeader(node);
610
+ break;
611
+
612
+ case "VariableDeclaration":
613
+ // declare bindings for each declarator (var/let/const)
614
+ for (const decl of node.declarations) {
615
+ markPatternBindings(decl.id, node.kind || "var");
616
+ }
617
+ break;
618
+
619
+ case "ClassDeclaration":
620
+ if (node.id) {
621
+ bindingIdNodes.add(node.id);
622
+ declare(node.id.name, "let");
623
+ }
624
+ break;
625
+
626
+ case "ImportDeclaration":
627
+ for (const spec of node.specifiers || []) {
628
+ if (spec.local) {
629
+ bindingIdNodes.add(spec.local);
630
+ declare(spec.local.name, "const");
631
+ }
632
+ }
633
+ break;
634
+ }
635
+
636
+ // Recurse children (post-order traversal so declarations are marked before uses below)
637
+ for (const key in node) {
638
+ if (key === "parent") continue;
639
+ const child = node[key];
640
+ if (Array.isArray(child)) {
641
+ for (let i = 0; i < child.length; i++) {
642
+ if (child[i] && typeof child[i] === "object") {
643
+ walk(child[i], node, key, i);
644
+ }
645
+ }
646
+ } else if (child && typeof child === "object") {
647
+ walk(child, node, key, null);
648
+ }
649
+ }
650
+
651
+ // POST: rewrite Identifier references if they meet the criteria
652
+ if (node.type === "Identifier") {
653
+ const name = node.name;
654
+
655
+ // Only consider reactive names and only non-binding identifier nodes
656
+ if (!reactive.has(name) || bindingIdNodes.has(node)) {
657
+ // do nothing
658
+ } else if (!rootHas(name)) {
659
+ // Not declared at program root: we only rewrite variables declared at root
660
+ } else if (isShadowedFromRoot(name)) {
661
+ // An inner scope declared the same name -> the reference belongs to inner binding; skip
662
+ } else if (shouldSkipIdentifier(node, parent, prop)) {
663
+ // e.g., property key, non-computed member property, import/export, labels...
664
+ } else {
665
+ // Special-case object-property shorthand: if we're the value of a shorthand property,
666
+ // we must turn shorthand off so `{ x }` becomes `{ x: _vt.View.vars["x"] }`
667
+ if (parent && parent.type === "Property" && parent.shorthand && prop === "value") {
668
+ parent.shorthand = false;
669
+ }
670
+
671
+ // Build computed member expression: _vt.<targetKey>.vars["<name>"]
672
+ const replacement = {
673
+ type: "MemberExpression",
674
+ object: {
675
+ type: "MemberExpression",
676
+ object: buildTargetRootExpr(targetKey),
677
+ property: { type: "Identifier", name: "vars" },
678
+ computed: false
679
+ },
680
+ property: { type: "Literal", value: name, raw: JSON.stringify(name) },
681
+ computed: true
682
+ };
683
+
684
+ // Replace in parent structure
685
+ if (parent) {
686
+ if (index !== null && Array.isArray(parent[prop])) {
687
+ parent[prop][index] = replacement;
688
+ } else {
689
+ parent[prop] = replacement;
690
+ }
691
+ } else {
692
+ // unlikely: top-level Identifier as AST root — replace the root reference
693
+ // (some tools expect AST to be an object with Program root, but be safe)
694
+ // mutate node in-place:
695
+ Object.keys(node).forEach(k => delete node[k]);
696
+ Object.assign(node, replacement);
697
+ }
698
+ }
699
+ }
700
+
701
+ // EXIT: pop scopes created on enter
702
+ switch (node.type) {
703
+ case "Program":
704
+ case "BlockStatement":
705
+ case "StaticBlock":
706
+ case "FunctionDeclaration":
707
+ case "FunctionExpression":
708
+ case "ArrowFunctionExpression":
709
+ case "CatchClause":
710
+ case "ForStatement":
711
+ case "ForInStatement":
712
+ case "ForOfStatement":
713
+ popScope();
714
+ break;
715
+ default:
716
+ break;
717
+ }
718
+ }
719
+
720
+ // Run walker
721
+ walk(AST, null, null, null);
722
+ return AST;
723
+ }
724
+
725
+ // KNOWN GAP (see file header): only rewrites a VariableDeclaration with
726
+ // exactly one declarator. `var x = 1, y = 2;` is left untouched.
727
+ function _reactiveAssignStatement(name, init, targetKey = "View") {
728
+ return {
729
+ type: "ExpressionStatement",
730
+ expression: {
731
+ type: "AssignmentExpression",
732
+ operator: "=",
733
+ left: {
734
+ type: "MemberExpression",
735
+ computed: true,
736
+ object: {
737
+ type: "MemberExpression",
738
+ computed: false,
739
+ object: buildTargetRootExpr(targetKey),
740
+ property: { type: "Identifier", name: "vars" }
741
+ },
742
+ property: { type: "Literal", value: name }
743
+ },
744
+ right: init || { type: "Identifier", name: "undefined" }
745
+ }
746
+ };
747
+ }
748
+
749
+ // 2026-09-17: registers a top-level function declaration onto this
750
+ // render's own scope (`_vt.<targetKey>.fns["name"] = name`) — ADDITIVE,
751
+ // not a replacement: the function declaration itself is left completely
752
+ // untouched right above this (still a real `window.name` too, the
753
+ // existing, deliberate "functions stay real globals so onclick='fn()'
754
+ // keeps working" design — see compileProjectScript's matching comment).
755
+ // Needed once multiple instances of the same subview can be mounted at
756
+ // once (see _re.js's per-instance scoping work) — every instance
757
+ // declaring a same-named handler collided on the one global function
758
+ // (window.bump became whichever instance's script ran last), so every
759
+ // instance's @click ended up calling THAT one instance's handler
760
+ // regardless of which was actually clicked. lstnrs.js's evalEvAttr()
761
+ // checks this per-instance copy first, before falling back to window.
762
+ function _fnRegisterStatement(name, targetKey) {
763
+ return {
764
+ type: "ExpressionStatement",
765
+ expression: {
766
+ type: "AssignmentExpression",
767
+ operator: "=",
768
+ left: {
769
+ type: "MemberExpression",
770
+ computed: true,
771
+ object: {
772
+ type: "MemberExpression",
773
+ computed: false,
774
+ object: buildTargetRootExpr(targetKey),
775
+ property: { type: "Identifier", name: "fns" }
776
+ },
777
+ property: { type: "Literal", value: name }
778
+ },
779
+ right: { type: "Identifier", name: name }
780
+ }
781
+ };
782
+ }
783
+
784
+ // Handles any number of declarators in a single top-level statement, not just
785
+ // one. `var x = 1, y = 2;` with only `x` reactive becomes two statements:
786
+ // `_vt.View.vars["x"] = 1;` followed by a real `var y = 2;` for the rest —
787
+ // each declarator only needs its own rewrite, and non-reactive ones must stay
788
+ // real declarations to remain valid references elsewhere in the script.
789
+ function transformTopLevelDeclarations(AST, reactiveVariables, targetKey = "View") {
790
+ const reactive = new Set(reactiveVariables || []);
791
+ const newBody = [];
792
+
793
+ for (const stmt of AST.body) {
794
+ if (stmt.type === "FunctionDeclaration" && stmt.id) {
795
+ newBody.push(stmt);
796
+ newBody.push(_fnRegisterStatement(stmt.id.name, targetKey));
797
+ continue;
798
+ }
799
+
800
+ if (stmt.type !== "VariableDeclaration") {
801
+ newBody.push(stmt);
802
+ continue;
803
+ }
804
+
805
+ const reactiveDecls = stmt.declarations.filter(d => reactive.has(d.id.name));
806
+ const nonReactiveDecls = stmt.declarations.filter(d => !reactive.has(d.id.name));
807
+
808
+ if (reactiveDecls.length === 0) {
809
+ newBody.push(stmt);
810
+ continue;
811
+ }
812
+
813
+ for (const decl of reactiveDecls) {
814
+ newBody.push(_reactiveAssignStatement(decl.id.name, decl.init, targetKey));
815
+ }
816
+
817
+ if (nonReactiveDecls.length > 0) {
818
+ newBody.push({
819
+ type: "VariableDeclaration",
820
+ kind: stmt.kind,
821
+ declarations: nonReactiveDecls
822
+ });
823
+ }
824
+ }
825
+
826
+ AST.body = newBody;
827
+ return AST;
828
+ }
829
+
830
+ // Node-only export (2026-09-14) so the CLI's build/serve path (packages/cli's
831
+ // fcs.js) can reuse the exact same rewrite for index.js's top-level vars
832
+ // instead of re-implementing it. `module` doesn't exist in the browser
833
+ // bundle this file is concatenated into (see build/bundle.js), so this is a
834
+ // silent no-op there — it only ever runs in Node.
835
+ if (typeof module !== 'undefined' && module.exports) {
836
+ module.exports = { getWatcher, changeReactiveVarsOccurences, transformTopLevelDeclarations, validateBeforeRewrite };
837
+ }