@lmjs/core 1.0.6 → 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/build/bundle.js +264 -0
- package/build/plugins-registry.js +216 -0
- package/dist/lumenjs-core-with-plugins.js +59258 -0
- package/dist/lumenjs-core.js +50 -0
- package/dist/lumenjs-plugins.css +2822 -0
- package/package.json +59 -28
- package/src/_re.js +2307 -0
- package/src/dom-shim.js +473 -0
- package/src/index-bootstrap.js +47 -0
- package/src/lstnrs.js +1391 -0
- package/src/walk.js +749 -0
- package/src/workers/css.js +1 -0
- package/src/workers/cssRaw.js +1173 -0
- package/src/workers/esp.js +1 -0
- package/src/workers/espRaw.js +78 -0
- package/src/workers/up.js +1 -0
- package/src/workers/upRaw.js +491 -0
- package/src/workers/work.js +1 -0
- package/src/workers/workRaw.js +1097 -0
- package/src/ws.js +1162 -0
- package/vendor/astring.js +3 -0
- package/vendor/bootstrap2-less-stubs/mixins.less +16 -0
- package/vendor/bootstrap2-less-stubs/variables.less +13 -0
- package/vendor/md5.js +1 -0
- package/vendor/reconnecting-websocket.js +4118 -0
- package/vendor/webworker-helper.js +1 -0
- package/LICENSE +0 -201
- package/README.md +0 -3
- package/index.js +0 -14
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
|
+
}
|