roqa 0.0.1
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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +77 -0
- package/src/compiler/codegen.js +1217 -0
- package/src/compiler/index.js +47 -0
- package/src/compiler/parser.js +197 -0
- package/src/compiler/transforms/bind-detector.js +264 -0
- package/src/compiler/transforms/events.js +246 -0
- package/src/compiler/transforms/for-transform.js +164 -0
- package/src/compiler/transforms/inline-get.js +1049 -0
- package/src/compiler/transforms/jsx-to-template.js +871 -0
- package/src/compiler/transforms/show-transform.js +78 -0
- package/src/compiler/transforms/validate.js +80 -0
- package/src/compiler/utils.js +69 -0
- package/src/jsx-runtime.d.ts +640 -0
- package/src/jsx-runtime.js +73 -0
- package/src/runtime/cell.js +37 -0
- package/src/runtime/component.js +241 -0
- package/src/runtime/events.js +156 -0
- package/src/runtime/for-block.js +374 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/show-block.js +115 -0
- package/src/runtime/template.js +32 -0
- package/types/compiler.d.ts +9 -0
- package/types/index.d.ts +433 -0
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import { CONSTANTS, traverse } from "../utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inline get(), cell(), put(), set(), and bind() calls
|
|
7
|
+
*
|
|
8
|
+
* The key optimization is wholesale inlining of bind() callback bodies at set() locations.
|
|
9
|
+
* Instead of using effect loops, we:
|
|
10
|
+
* 1. Find all bind(cell, callback) calls
|
|
11
|
+
* 2. Store refs to DOM elements: cell.ref_N = element
|
|
12
|
+
* 3. At set() locations, inline the callback body with element vars replaced by cell.ref_N
|
|
13
|
+
*
|
|
14
|
+
* Transforms:
|
|
15
|
+
* get(cell) -> cell.v (or inlined function body for derived cells)
|
|
16
|
+
* cell(value) -> { v: value, e: [] }
|
|
17
|
+
* cell(() => expr) -> { v: () => expr, e: [] } (derived cell - function stored)
|
|
18
|
+
* put(cell, value) -> cell.v = value
|
|
19
|
+
* set(cell, value) -> { cell.v = value; <inlined callback bodies> }
|
|
20
|
+
* bind(cell, callback) -> cell.ref_N = element; (callback inlined at set() locations)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-compiled regex for property pattern extraction
|
|
25
|
+
* Matches patterns like "row.isSelected" -> prefix="row", pattern="isSelected"
|
|
26
|
+
*/
|
|
27
|
+
const PROPERTY_PATTERN_REGEX = /^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context class that encapsulates mutable state for a single compilation.
|
|
31
|
+
* This eliminates global state and makes the compiler safe for parallel execution.
|
|
32
|
+
*/
|
|
33
|
+
class InlineContext {
|
|
34
|
+
constructor() {
|
|
35
|
+
/**
|
|
36
|
+
* Map to track derived cells: cellName -> { body: string, dependencies: string[] }
|
|
37
|
+
* body is the function body code (with get() already transformed to .v)
|
|
38
|
+
* dependencies is an array of cell names this derived cell depends on (direct dependencies only)
|
|
39
|
+
* @type {Map<string, {body: string, dependencies: string[]}>}
|
|
40
|
+
*/
|
|
41
|
+
this.derivedCells = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cache for extractPropertyPattern to avoid repeated regex matching
|
|
45
|
+
* @type {Map<string, {prefix: string, pattern: string} | null>}
|
|
46
|
+
*/
|
|
47
|
+
this.propertyPatternCache = new Map();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a derived cell
|
|
52
|
+
* @param {string} cellName - The cell variable name
|
|
53
|
+
* @param {string} body - The transformed function body
|
|
54
|
+
* @param {string[]} dependencies - Direct dependencies
|
|
55
|
+
*/
|
|
56
|
+
registerDerivedCell(cellName, body, dependencies) {
|
|
57
|
+
this.derivedCells.set(cellName, { body, dependencies });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if a cell is a derived cell
|
|
62
|
+
* @param {string} cellName
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
isDerivedCell(cellName) {
|
|
66
|
+
return this.derivedCells.has(cellName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the body of a derived cell
|
|
71
|
+
* @param {string} cellName
|
|
72
|
+
* @returns {{body: string, dependencies: string[]} | undefined}
|
|
73
|
+
*/
|
|
74
|
+
getDerivedCellInfo(cellName) {
|
|
75
|
+
return this.derivedCells.get(cellName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the fully expanded body for a derived cell, recursively resolving any
|
|
80
|
+
* references to other derived cells.
|
|
81
|
+
* @param {string} cellName - The name of the derived cell
|
|
82
|
+
* @param {Set<string>} visited - Set of already visited cells (to prevent infinite loops)
|
|
83
|
+
* @returns {string|null} - The fully expanded body, or null if not a derived cell
|
|
84
|
+
*/
|
|
85
|
+
getExpandedDerivedBody(cellName, visited = new Set()) {
|
|
86
|
+
const info = this.derivedCells.get(cellName);
|
|
87
|
+
if (!info) return null;
|
|
88
|
+
|
|
89
|
+
// Prevent infinite loops from circular dependencies
|
|
90
|
+
if (visited.has(cellName)) {
|
|
91
|
+
return info.body;
|
|
92
|
+
}
|
|
93
|
+
visited.add(cellName);
|
|
94
|
+
|
|
95
|
+
let expandedBody = info.body;
|
|
96
|
+
|
|
97
|
+
// Replace any references to other derived cells with their expanded bodies
|
|
98
|
+
for (const otherCellName of this.derivedCells.keys()) {
|
|
99
|
+
if (otherCellName === cellName) continue;
|
|
100
|
+
|
|
101
|
+
// Check if this body references the other derived cell
|
|
102
|
+
const cellRefRegex = new RegExp(`\\b${otherCellName}\\.v\\b`, "g");
|
|
103
|
+
if (cellRefRegex.test(expandedBody)) {
|
|
104
|
+
// Recursively get the expanded body for the other cell
|
|
105
|
+
const otherExpandedBody = this.getExpandedDerivedBody(otherCellName, visited);
|
|
106
|
+
if (otherExpandedBody) {
|
|
107
|
+
// Replace references with the expanded body (wrapped in parens for safety)
|
|
108
|
+
expandedBody = expandedBody.replace(
|
|
109
|
+
new RegExp(`\\b${otherCellName}\\.v\\b`, "g"),
|
|
110
|
+
`(${otherExpandedBody})`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return expandedBody;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get all cells that transitively depend on a given cell.
|
|
121
|
+
* This includes direct dependencies and dependencies of dependencies.
|
|
122
|
+
* @param {string} cellName - The source cell name
|
|
123
|
+
* @returns {Set<string>} - Set of all derived cell names that depend on this cell
|
|
124
|
+
*/
|
|
125
|
+
getTransitiveDependents(cellName) {
|
|
126
|
+
const dependents = new Set();
|
|
127
|
+
|
|
128
|
+
// Find direct dependents
|
|
129
|
+
for (const [derivedCellName, derivedInfo] of this.derivedCells) {
|
|
130
|
+
if (derivedInfo.dependencies.includes(cellName)) {
|
|
131
|
+
dependents.add(derivedCellName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Find transitive dependents (cells that depend on our direct dependents)
|
|
136
|
+
let changed = true;
|
|
137
|
+
while (changed) {
|
|
138
|
+
changed = false;
|
|
139
|
+
for (const [derivedCellName, derivedInfo] of this.derivedCells) {
|
|
140
|
+
if (dependents.has(derivedCellName)) continue;
|
|
141
|
+
|
|
142
|
+
// Check if this cell depends on any of our current dependents
|
|
143
|
+
for (const dep of derivedInfo.dependencies) {
|
|
144
|
+
if (dependents.has(dep)) {
|
|
145
|
+
dependents.add(derivedCellName);
|
|
146
|
+
changed = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return dependents;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract the property pattern from a cell code (with caching)
|
|
158
|
+
* e.g., "row.isSelected" -> { pattern: "isSelected", prefix: "row" }
|
|
159
|
+
* e.g., "count" -> null (simple identifier, no pattern)
|
|
160
|
+
* @param {string} cellCode
|
|
161
|
+
* @returns {{prefix: string, pattern: string} | null}
|
|
162
|
+
*/
|
|
163
|
+
extractPropertyPattern(cellCode) {
|
|
164
|
+
if (this.propertyPatternCache.has(cellCode)) {
|
|
165
|
+
return this.propertyPatternCache.get(cellCode);
|
|
166
|
+
}
|
|
167
|
+
const match = cellCode.match(PROPERTY_PATTERN_REGEX);
|
|
168
|
+
const result = match ? { prefix: match[1], pattern: match[2] } : null;
|
|
169
|
+
this.propertyPatternCache.set(cellCode, result);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a node is a get() call
|
|
176
|
+
*/
|
|
177
|
+
function isGetCall(node) {
|
|
178
|
+
return (
|
|
179
|
+
node?.type === "CallExpression" &&
|
|
180
|
+
node.callee?.type === "Identifier" &&
|
|
181
|
+
node.callee.name === "get"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if a node is a cell() call
|
|
187
|
+
*/
|
|
188
|
+
function isCellCall(node) {
|
|
189
|
+
return (
|
|
190
|
+
node?.type === "CallExpression" &&
|
|
191
|
+
node.callee?.type === "Identifier" &&
|
|
192
|
+
node.callee.name === "cell"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if a node is a put() call
|
|
198
|
+
*/
|
|
199
|
+
function isPutCall(node) {
|
|
200
|
+
return (
|
|
201
|
+
node?.type === "CallExpression" &&
|
|
202
|
+
node.callee?.type === "Identifier" &&
|
|
203
|
+
node.callee.name === "put"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if a node is a set() call
|
|
209
|
+
*/
|
|
210
|
+
function isSetCall(node) {
|
|
211
|
+
return (
|
|
212
|
+
node?.type === "CallExpression" &&
|
|
213
|
+
node.callee?.type === "Identifier" &&
|
|
214
|
+
node.callee.name === "set"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if a node is a bind() call
|
|
220
|
+
*/
|
|
221
|
+
function isBindCall(node) {
|
|
222
|
+
return (
|
|
223
|
+
node?.type === "CallExpression" &&
|
|
224
|
+
node.callee?.type === "Identifier" &&
|
|
225
|
+
node.callee.name === "bind"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Find ref assignments at the component level (NOT inside bind() callbacks).
|
|
231
|
+
* These are generated by codegen for simple bindings without bind():
|
|
232
|
+
* element.property = expression; (where expression contains get(cell))
|
|
233
|
+
* cell.ref_N = element;
|
|
234
|
+
*
|
|
235
|
+
* Returns a map: cellCode -> [{ refNum, property, updateExpr }]
|
|
236
|
+
*/
|
|
237
|
+
function findRefAssignmentsWithoutBind(ast, code) {
|
|
238
|
+
// Map: cellCode -> [{ refNum, property, updateExpr }]
|
|
239
|
+
const refMappings = new Map();
|
|
240
|
+
|
|
241
|
+
// Helper to process statements from a callback body
|
|
242
|
+
function processStatements(statements) {
|
|
243
|
+
// Build a map: elementVar -> { property, updateExpr, cellCodes }
|
|
244
|
+
// from element.property = expression assignments
|
|
245
|
+
const elementAssignments = new Map();
|
|
246
|
+
|
|
247
|
+
// First pass: find element.property = expression (with get(cell) calls)
|
|
248
|
+
for (const stmt of statements) {
|
|
249
|
+
if (
|
|
250
|
+
stmt.type === "ExpressionStatement" &&
|
|
251
|
+
stmt.expression.type === "AssignmentExpression" &&
|
|
252
|
+
stmt.expression.operator === "=" &&
|
|
253
|
+
stmt.expression.left.type === "MemberExpression" &&
|
|
254
|
+
stmt.expression.left.object.type === "Identifier" &&
|
|
255
|
+
stmt.expression.left.property.type === "Identifier"
|
|
256
|
+
) {
|
|
257
|
+
const elementVar = stmt.expression.left.object.name;
|
|
258
|
+
const property = stmt.expression.left.property.name;
|
|
259
|
+
const rightExpr = stmt.expression.right;
|
|
260
|
+
|
|
261
|
+
// Check if the expression contains get() calls
|
|
262
|
+
const cellCodes = findGetCallCells(rightExpr, code);
|
|
263
|
+
if (cellCodes.length > 0) {
|
|
264
|
+
// Get the full expression and transform get(x) to x.v
|
|
265
|
+
let updateExpr = code.slice(rightExpr.start, rightExpr.end);
|
|
266
|
+
updateExpr = updateExpr.replace(/\bget\(([^)]+)\)/g, "$1.v");
|
|
267
|
+
|
|
268
|
+
elementAssignments.set(elementVar, {
|
|
269
|
+
property,
|
|
270
|
+
updateExpr,
|
|
271
|
+
cellCodes,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Second pass: find cell.ref_N = element assignments
|
|
278
|
+
// Handle both simple identifiers (count.ref_1) and member expressions (row.label.ref_1)
|
|
279
|
+
for (const stmt of statements) {
|
|
280
|
+
if (
|
|
281
|
+
stmt.type === "ExpressionStatement" &&
|
|
282
|
+
stmt.expression.type === "AssignmentExpression" &&
|
|
283
|
+
stmt.expression.operator === "=" &&
|
|
284
|
+
stmt.expression.right.type === "Identifier"
|
|
285
|
+
) {
|
|
286
|
+
const left = stmt.expression.left;
|
|
287
|
+
let cellCode = null;
|
|
288
|
+
let refProp = null;
|
|
289
|
+
|
|
290
|
+
// Pattern 1: cell.ref_N (simple identifier)
|
|
291
|
+
if (
|
|
292
|
+
left.type === "MemberExpression" &&
|
|
293
|
+
left.object.type === "Identifier" &&
|
|
294
|
+
left.property.type === "Identifier" &&
|
|
295
|
+
left.property.name.startsWith(CONSTANTS.REF_PREFIX)
|
|
296
|
+
) {
|
|
297
|
+
cellCode = left.object.name;
|
|
298
|
+
refProp = left.property.name;
|
|
299
|
+
}
|
|
300
|
+
// Pattern 2: obj.cell.ref_N (member expression like row.label.ref_1)
|
|
301
|
+
else if (
|
|
302
|
+
left.type === "MemberExpression" &&
|
|
303
|
+
left.object.type === "MemberExpression" &&
|
|
304
|
+
left.property.type === "Identifier" &&
|
|
305
|
+
left.property.name.startsWith(CONSTANTS.REF_PREFIX)
|
|
306
|
+
) {
|
|
307
|
+
cellCode = code.slice(left.object.start, left.object.end);
|
|
308
|
+
refProp = left.property.name;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (cellCode && refProp) {
|
|
312
|
+
const refNum = parseInt(refProp.replace(CONSTANTS.REF_PREFIX, ""), 10);
|
|
313
|
+
const elementVar = stmt.expression.right.name;
|
|
314
|
+
|
|
315
|
+
// Look up the element assignment
|
|
316
|
+
const elemInfo = elementAssignments.get(elementVar);
|
|
317
|
+
if (elemInfo && elemInfo.cellCodes.includes(cellCode)) {
|
|
318
|
+
if (!refMappings.has(cellCode)) {
|
|
319
|
+
refMappings.set(cellCode, []);
|
|
320
|
+
}
|
|
321
|
+
refMappings.get(cellCode).push({
|
|
322
|
+
refNum,
|
|
323
|
+
property: elemInfo.property,
|
|
324
|
+
updateExpr: elemInfo.updateExpr,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
traverse(ast, {
|
|
333
|
+
CallExpression(path) {
|
|
334
|
+
const node = path.node;
|
|
335
|
+
|
|
336
|
+
// Find this.connected() calls
|
|
337
|
+
if (
|
|
338
|
+
node.callee?.type === "MemberExpression" &&
|
|
339
|
+
node.callee.object?.type === "ThisExpression" &&
|
|
340
|
+
node.callee.property?.type === "Identifier" &&
|
|
341
|
+
node.callee.property.name === "connected" &&
|
|
342
|
+
node.arguments.length >= 1
|
|
343
|
+
) {
|
|
344
|
+
const callbackArg = node.arguments[0];
|
|
345
|
+
if (
|
|
346
|
+
callbackArg.type !== "ArrowFunctionExpression" &&
|
|
347
|
+
callbackArg.type !== "FunctionExpression"
|
|
348
|
+
) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const body = callbackArg.body;
|
|
353
|
+
const statements = body.type === "BlockStatement" ? body.body : [body];
|
|
354
|
+
processStatements(statements);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Find forBlock() calls and process their render callbacks
|
|
358
|
+
if (
|
|
359
|
+
node.callee?.type === "Identifier" &&
|
|
360
|
+
node.callee.name === "forBlock" &&
|
|
361
|
+
node.arguments.length >= 3
|
|
362
|
+
) {
|
|
363
|
+
const callbackArg = node.arguments[2];
|
|
364
|
+
if (
|
|
365
|
+
callbackArg.type !== "ArrowFunctionExpression" &&
|
|
366
|
+
callbackArg.type !== "FunctionExpression"
|
|
367
|
+
) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const body = callbackArg.body;
|
|
372
|
+
const statements = body.type === "BlockStatement" ? body.body : [body];
|
|
373
|
+
processStatements(statements);
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
noScope: true,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return refMappings;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Find all cell codes referenced by get() calls in an expression
|
|
384
|
+
*/
|
|
385
|
+
function findGetCallCells(node, code) {
|
|
386
|
+
const cellCodes = [];
|
|
387
|
+
|
|
388
|
+
function visit(n) {
|
|
389
|
+
if (!n) return;
|
|
390
|
+
if (isGetCall(n) && n.arguments[0]) {
|
|
391
|
+
cellCodes.push(code.slice(n.arguments[0].start, n.arguments[0].end));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
for (const key of Object.keys(n)) {
|
|
395
|
+
const child = n[key];
|
|
396
|
+
if (child && typeof child === "object") {
|
|
397
|
+
if (Array.isArray(child)) {
|
|
398
|
+
child.forEach(visit);
|
|
399
|
+
} else if (child.type) {
|
|
400
|
+
visit(child);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
visit(node);
|
|
407
|
+
return cellCodes;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Find all forBlock and showBlock calls and collect:
|
|
412
|
+
* 1. Cells that are sources for forBlock/showBlock (need effect loop at set() if no explicit .update())
|
|
413
|
+
* 2. Variable assignments for forBlock/showBlock calls (for explicit .update() calls)
|
|
414
|
+
*
|
|
415
|
+
* Returns { sourceCells: Set<string>, variableMappings: Map<string, string> }
|
|
416
|
+
*/
|
|
417
|
+
function findBlockInfo(ast, code) {
|
|
418
|
+
const sourceCells = new Set();
|
|
419
|
+
const variableMappings = new Map();
|
|
420
|
+
|
|
421
|
+
traverse(ast, {
|
|
422
|
+
CallExpression(path) {
|
|
423
|
+
const node = path.node;
|
|
424
|
+
const isForBlock =
|
|
425
|
+
node.callee?.type === "Identifier" &&
|
|
426
|
+
node.callee.name === "forBlock" &&
|
|
427
|
+
node.arguments.length >= 2;
|
|
428
|
+
const isShowBlock =
|
|
429
|
+
node.callee?.type === "Identifier" &&
|
|
430
|
+
node.callee.name === "showBlock" &&
|
|
431
|
+
node.arguments.length >= 2;
|
|
432
|
+
|
|
433
|
+
if (isForBlock || isShowBlock) {
|
|
434
|
+
// Always track the source cell (argument index 1 for both)
|
|
435
|
+
const cellArg = node.arguments[1];
|
|
436
|
+
const cellCode = code.slice(cellArg.start, cellArg.end);
|
|
437
|
+
sourceCells.add(cellCode);
|
|
438
|
+
|
|
439
|
+
// Check if this is an assignment: xxx_forBlock = forBlock(...) or xxx_showBlock = showBlock(...)
|
|
440
|
+
const parent = path.parent;
|
|
441
|
+
if (
|
|
442
|
+
parent?.type === "AssignmentExpression" &&
|
|
443
|
+
parent.left?.type === "Identifier" &&
|
|
444
|
+
(parent.left.name.endsWith("_forBlock") || parent.left.name.endsWith("_showBlock"))
|
|
445
|
+
) {
|
|
446
|
+
variableMappings.set(cellCode, parent.left.name);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
noScope: true,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return { sourceCells, variableMappings };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Find all bind() calls and extract callback info for inlining
|
|
458
|
+
* Returns a map from cell code -> array of callback info
|
|
459
|
+
*/
|
|
460
|
+
function findBindCallbacks(ast, code) {
|
|
461
|
+
// Map: cellCode -> [{ callbackBody, elementVars, refNum, paramName, statementStart, statementEnd }]
|
|
462
|
+
const bindCallbacks = new Map();
|
|
463
|
+
// Track ref numbers per cell
|
|
464
|
+
const refCounters = new Map();
|
|
465
|
+
|
|
466
|
+
traverse(ast, {
|
|
467
|
+
ExpressionStatement(path) {
|
|
468
|
+
const expr = path.node.expression;
|
|
469
|
+
if (!isBindCall(expr)) return;
|
|
470
|
+
|
|
471
|
+
const cellArg = expr.arguments[0];
|
|
472
|
+
const callbackArg = expr.arguments[1];
|
|
473
|
+
if (!cellArg || !callbackArg) return;
|
|
474
|
+
|
|
475
|
+
// Get callback info
|
|
476
|
+
if (
|
|
477
|
+
callbackArg.type !== "ArrowFunctionExpression" &&
|
|
478
|
+
callbackArg.type !== "FunctionExpression"
|
|
479
|
+
) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const cellCode = code.slice(cellArg.start, cellArg.end);
|
|
484
|
+
const paramName = callbackArg.params[0]?.name || "v";
|
|
485
|
+
|
|
486
|
+
// Get the callback body
|
|
487
|
+
const body = callbackArg.body;
|
|
488
|
+
let bodyCode;
|
|
489
|
+
if (body.type === "BlockStatement") {
|
|
490
|
+
// Extract statements from block, removing braces
|
|
491
|
+
bodyCode = code.slice(body.start + 1, body.end - 1).trim();
|
|
492
|
+
} else {
|
|
493
|
+
// Expression body
|
|
494
|
+
bodyCode = code.slice(body.start, body.end);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Find element variables used in the callback (e.g., p_1_text, tr_1)
|
|
498
|
+
const elementVars = findElementVariables(body);
|
|
499
|
+
|
|
500
|
+
// Get or create ref number for this cell
|
|
501
|
+
const currentRef = refCounters.get(cellCode) || 0;
|
|
502
|
+
const refNum = currentRef + 1;
|
|
503
|
+
refCounters.set(cellCode, refNum);
|
|
504
|
+
|
|
505
|
+
// Store bind callback info
|
|
506
|
+
if (!bindCallbacks.has(cellCode)) {
|
|
507
|
+
bindCallbacks.set(cellCode, []);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
bindCallbacks.get(cellCode).push({
|
|
511
|
+
callbackBody: bodyCode,
|
|
512
|
+
elementVars,
|
|
513
|
+
refNum,
|
|
514
|
+
paramName,
|
|
515
|
+
statementStart: path.node.start,
|
|
516
|
+
statementEnd: path.node.end,
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
noScope: true,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return bindCallbacks;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Find element variables used in a callback body
|
|
527
|
+
* Looks for element.property = ... patterns
|
|
528
|
+
*/
|
|
529
|
+
function findElementVariables(body) {
|
|
530
|
+
const elementVars = [];
|
|
531
|
+
const seen = new Set();
|
|
532
|
+
|
|
533
|
+
function visit(node) {
|
|
534
|
+
if (!node) return;
|
|
535
|
+
|
|
536
|
+
// Look for element.property = ... patterns
|
|
537
|
+
if (
|
|
538
|
+
node.type === "AssignmentExpression" &&
|
|
539
|
+
node.left.type === "MemberExpression" &&
|
|
540
|
+
node.left.object.type === "Identifier"
|
|
541
|
+
) {
|
|
542
|
+
const varName = node.left.object.name;
|
|
543
|
+
if (!seen.has(varName)) {
|
|
544
|
+
seen.add(varName);
|
|
545
|
+
elementVars.push({ varName });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Recurse into child nodes
|
|
550
|
+
for (const key of Object.keys(node)) {
|
|
551
|
+
const child = node[key];
|
|
552
|
+
if (child && typeof child === "object") {
|
|
553
|
+
if (Array.isArray(child)) {
|
|
554
|
+
child.forEach(visit);
|
|
555
|
+
} else if (child.type) {
|
|
556
|
+
visit(child);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
visit(body);
|
|
563
|
+
return elementVars;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Transform a callback body for inlining at a set() location
|
|
568
|
+
* - Replace element variables with cell.ref_N
|
|
569
|
+
* - Replace callback parameter (v) with cell.v
|
|
570
|
+
* - Transform get() calls to .v access
|
|
571
|
+
*/
|
|
572
|
+
function transformCallbackBody(bodyCode, cellCode, elementVars, paramName, refNum) {
|
|
573
|
+
let transformed = bodyCode;
|
|
574
|
+
|
|
575
|
+
// Replace the callback parameter (v) with cell.v
|
|
576
|
+
const paramRegex = new RegExp(`\\b${paramName}\\b`, "g");
|
|
577
|
+
transformed = transformed.replace(paramRegex, `${cellCode}.v`);
|
|
578
|
+
|
|
579
|
+
// Replace element variables with cell.ref_N
|
|
580
|
+
for (const { varName } of elementVars) {
|
|
581
|
+
const varRegex = new RegExp(`\\b${varName}\\b`, "g");
|
|
582
|
+
transformed = transformed.replace(varRegex, `${cellCode}.${CONSTANTS.REF_PREFIX}${refNum}`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Transform any remaining get() calls to .v access
|
|
586
|
+
transformed = transformed.replace(/\bget\(([^)]+)\)/g, "$1.v");
|
|
587
|
+
|
|
588
|
+
return transformed;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Generate ref assignment code: cell.ref_N = elementVar
|
|
593
|
+
*/
|
|
594
|
+
function generateRefAssignment(cellCode, elementVar, refNum) {
|
|
595
|
+
return `${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${elementVar};`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Transform all get(), cell(), put(), set(), and bind() calls
|
|
600
|
+
*/
|
|
601
|
+
export function inlineGetCalls(code, filename) {
|
|
602
|
+
// Create a fresh context for this compilation
|
|
603
|
+
const ctx = new InlineContext();
|
|
604
|
+
|
|
605
|
+
const isTypeScript = filename && (filename.endsWith(".tsx") || filename.endsWith(".ts"));
|
|
606
|
+
const plugins = isTypeScript ? ["jsx", "typescript"] : ["jsx"];
|
|
607
|
+
|
|
608
|
+
const ast = parse(code, {
|
|
609
|
+
sourceType: "module",
|
|
610
|
+
plugins,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const s = new MagicString(code);
|
|
614
|
+
|
|
615
|
+
// Find forBlock and showBlock info (source cells and variable mappings)
|
|
616
|
+
const { sourceCells: blockSourceCells, variableMappings: blockMappings } = findBlockInfo(
|
|
617
|
+
ast,
|
|
618
|
+
code,
|
|
619
|
+
ctx,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Find all bind() callbacks for inlining
|
|
623
|
+
const bindCallbacks = findBindCallbacks(ast, code);
|
|
624
|
+
|
|
625
|
+
// Find ref assignments without bind() (from codegen's direct ref approach)
|
|
626
|
+
const refWithoutBind = findRefAssignmentsWithoutBind(ast, code, ctx);
|
|
627
|
+
|
|
628
|
+
// Track all calls to transform
|
|
629
|
+
const getCalls = [];
|
|
630
|
+
const cellCalls = [];
|
|
631
|
+
const putCalls = [];
|
|
632
|
+
const setCalls = [];
|
|
633
|
+
|
|
634
|
+
// Track bind statements to remove
|
|
635
|
+
const bindStatementsToRemove = [];
|
|
636
|
+
|
|
637
|
+
// Track roqa imports for removal
|
|
638
|
+
const importsToRemove = [];
|
|
639
|
+
|
|
640
|
+
// Collect bind statements to remove
|
|
641
|
+
for (const [cellCode, callbacks] of bindCallbacks) {
|
|
642
|
+
for (const cb of callbacks) {
|
|
643
|
+
bindStatementsToRemove.push({
|
|
644
|
+
start: cb.statementStart,
|
|
645
|
+
end: cb.statementEnd,
|
|
646
|
+
cellCode,
|
|
647
|
+
callback: cb,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// First pass: identify derived cells (cells with arrow function arguments)
|
|
653
|
+
traverse(ast, {
|
|
654
|
+
VariableDeclarator(path) {
|
|
655
|
+
if (path.node.id.type === "Identifier" && path.node.init && isCellCall(path.node.init)) {
|
|
656
|
+
const cellName = path.node.id.name;
|
|
657
|
+
const arg = path.node.init.arguments[0];
|
|
658
|
+
|
|
659
|
+
// Check if the argument is an arrow function or function expression
|
|
660
|
+
if (arg && (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression")) {
|
|
661
|
+
// Extract the function body and transform get() calls to .v
|
|
662
|
+
const body = arg.body;
|
|
663
|
+
let bodyCode;
|
|
664
|
+
if (body.type === "BlockStatement") {
|
|
665
|
+
// For block bodies, we'd need to handle return statements
|
|
666
|
+
// For now, skip these complex cases
|
|
667
|
+
return;
|
|
668
|
+
} else {
|
|
669
|
+
// Expression body - inline it directly
|
|
670
|
+
bodyCode = code.slice(body.start, body.end);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Extract dependencies from get() calls
|
|
674
|
+
const dependencies = [];
|
|
675
|
+
const getCallRegex = /\bget\(([^)]+)\)/g;
|
|
676
|
+
let match;
|
|
677
|
+
while ((match = getCallRegex.exec(bodyCode)) !== null) {
|
|
678
|
+
dependencies.push(match[1].trim());
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Transform get() calls in the body to .v access
|
|
682
|
+
const transformedBody = bodyCode.replace(/\bget\(([^)]+)\)/g, "$1.v");
|
|
683
|
+
ctx.registerDerivedCell(cellName, transformedBody, dependencies);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
noScope: true,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Second pass: collect all calls to transform
|
|
691
|
+
traverse(ast, {
|
|
692
|
+
CallExpression(path) {
|
|
693
|
+
if (isGetCall(path.node)) {
|
|
694
|
+
const arg = path.node.arguments[0];
|
|
695
|
+
if (arg) {
|
|
696
|
+
const argCode = code.slice(arg.start, arg.end);
|
|
697
|
+
const derivedInfo = ctx.getDerivedCellInfo(argCode);
|
|
698
|
+
getCalls.push({
|
|
699
|
+
start: path.node.start,
|
|
700
|
+
end: path.node.end,
|
|
701
|
+
argCode,
|
|
702
|
+
// Check if this is a derived cell
|
|
703
|
+
isDerived: !!derivedInfo,
|
|
704
|
+
derivedBody: derivedInfo?.body || null,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
} else if (isCellCall(path.node)) {
|
|
708
|
+
const arg = path.node.arguments[0];
|
|
709
|
+
cellCalls.push({
|
|
710
|
+
start: path.node.start,
|
|
711
|
+
end: path.node.end,
|
|
712
|
+
argStart: arg?.start,
|
|
713
|
+
argEnd: arg?.end,
|
|
714
|
+
});
|
|
715
|
+
} else if (isPutCall(path.node)) {
|
|
716
|
+
const cellArg = path.node.arguments[0];
|
|
717
|
+
const valueArg = path.node.arguments[1];
|
|
718
|
+
if (cellArg) {
|
|
719
|
+
putCalls.push({
|
|
720
|
+
start: path.node.start,
|
|
721
|
+
end: path.node.end,
|
|
722
|
+
cellStart: cellArg.start,
|
|
723
|
+
cellEnd: cellArg.end,
|
|
724
|
+
valueStart: valueArg?.start,
|
|
725
|
+
valueEnd: valueArg?.end,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
} else if (isSetCall(path.node)) {
|
|
729
|
+
const cellArg = path.node.arguments[0];
|
|
730
|
+
const valueArg = path.node.arguments[1];
|
|
731
|
+
if (cellArg) {
|
|
732
|
+
const cellCode = code.slice(cellArg.start, cellArg.end);
|
|
733
|
+
setCalls.push({
|
|
734
|
+
start: path.node.start,
|
|
735
|
+
end: path.node.end,
|
|
736
|
+
cellStart: cellArg.start,
|
|
737
|
+
cellEnd: cellArg.end,
|
|
738
|
+
cellCode,
|
|
739
|
+
valueStart: valueArg?.start,
|
|
740
|
+
valueEnd: valueArg?.end,
|
|
741
|
+
blockVar: blockMappings.get(cellCode) || null,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
ImportDeclaration(path) {
|
|
747
|
+
const source = path.node.source.value;
|
|
748
|
+
if (source === "roqa") {
|
|
749
|
+
for (const specifier of path.node.specifiers) {
|
|
750
|
+
if (specifier.type === "ImportSpecifier") {
|
|
751
|
+
const name = specifier.imported.name;
|
|
752
|
+
if (["get", "cell", "put", "set", "bind"].includes(name)) {
|
|
753
|
+
importsToRemove.push({ name, specifier });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
noScope: true,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Sort all calls by position descending (process from end to start)
|
|
763
|
+
const allCalls = [
|
|
764
|
+
...getCalls.map((c) => ({ ...c, type: "get" })),
|
|
765
|
+
...cellCalls.map((c) => ({ ...c, type: "cell" })),
|
|
766
|
+
...putCalls.map((c) => ({ ...c, type: "put" })),
|
|
767
|
+
...setCalls.map((c) => ({ ...c, type: "set" })),
|
|
768
|
+
].sort((a, b) => b.start - a.start);
|
|
769
|
+
|
|
770
|
+
// Process calls
|
|
771
|
+
for (const call of allCalls) {
|
|
772
|
+
if (call.type === "get") {
|
|
773
|
+
// If this is a derived cell, inline the fully expanded function body
|
|
774
|
+
if (call.isDerived) {
|
|
775
|
+
const expandedBody = ctx.getExpandedDerivedBody(call.argCode);
|
|
776
|
+
s.overwrite(call.start, call.end, `(${expandedBody})`);
|
|
777
|
+
} else {
|
|
778
|
+
s.overwrite(call.start, call.end, `${call.argCode}.v`);
|
|
779
|
+
}
|
|
780
|
+
} else if (call.type === "cell") {
|
|
781
|
+
const argCode = call.argStart != null ? s.slice(call.argStart, call.argEnd) : "undefined";
|
|
782
|
+
s.overwrite(call.start, call.end, `{ v: ${argCode}, e: [] }`);
|
|
783
|
+
} else if (call.type === "put") {
|
|
784
|
+
const cellCode = s.slice(call.cellStart, call.cellEnd);
|
|
785
|
+
const valueCode =
|
|
786
|
+
call.valueStart != null ? s.slice(call.valueStart, call.valueEnd) : "undefined";
|
|
787
|
+
s.overwrite(call.start, call.end, `(${cellCode}.v = ${valueCode})`);
|
|
788
|
+
} else if (call.type === "set") {
|
|
789
|
+
const c = s.slice(call.cellStart, call.cellEnd);
|
|
790
|
+
const v = call.valueStart != null ? s.slice(call.valueStart, call.valueEnd) : "undefined";
|
|
791
|
+
|
|
792
|
+
// Helper to generate inlined updates from bind callbacks
|
|
793
|
+
// Only include callbacks that were fully inlined (have element vars)
|
|
794
|
+
const generateBindCallbackUpdates = () => {
|
|
795
|
+
let callbacks = bindCallbacks.get(call.cellCode);
|
|
796
|
+
let callbackCellCode = call.cellCode;
|
|
797
|
+
|
|
798
|
+
// If no exact match, try pattern match (e.g., "prev.isSelected" matches "row.isSelected")
|
|
799
|
+
if ((!callbacks || callbacks.length === 0) && call.cellCode.includes(".")) {
|
|
800
|
+
const patternInfo = ctx.extractPropertyPattern(call.cellCode);
|
|
801
|
+
if (patternInfo) {
|
|
802
|
+
for (const [existingCellCode, existingCallbacks] of bindCallbacks) {
|
|
803
|
+
const existingPattern = ctx.extractPropertyPattern(existingCellCode);
|
|
804
|
+
if (existingPattern && existingPattern.pattern === patternInfo.pattern) {
|
|
805
|
+
callbacks = existingCallbacks;
|
|
806
|
+
callbackCellCode = existingCellCode;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (callbacks && callbacks.length > 0) {
|
|
814
|
+
// Filter to only include callbacks that have element vars (were fully inlined)
|
|
815
|
+
const inlinableCallbacks = callbacks.filter((cb) => cb.elementVars.length > 0);
|
|
816
|
+
return inlinableCallbacks
|
|
817
|
+
.map((cb) => {
|
|
818
|
+
let body = transformCallbackBody(
|
|
819
|
+
cb.callbackBody,
|
|
820
|
+
callbackCellCode,
|
|
821
|
+
cb.elementVars,
|
|
822
|
+
cb.paramName,
|
|
823
|
+
cb.refNum,
|
|
824
|
+
);
|
|
825
|
+
if (callbackCellCode !== call.cellCode) {
|
|
826
|
+
const originalPattern = ctx.extractPropertyPattern(callbackCellCode);
|
|
827
|
+
const actualPattern = ctx.extractPropertyPattern(call.cellCode);
|
|
828
|
+
if (originalPattern && actualPattern) {
|
|
829
|
+
const regex = new RegExp(
|
|
830
|
+
`\\b${originalPattern.prefix}\\.${originalPattern.pattern}\\b`,
|
|
831
|
+
"g",
|
|
832
|
+
);
|
|
833
|
+
body = body.replace(regex, `${actualPattern.prefix}.${actualPattern.pattern}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return body;
|
|
837
|
+
})
|
|
838
|
+
.join(" ");
|
|
839
|
+
}
|
|
840
|
+
return "";
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// Helper to generate updates from ref assignments without bind()
|
|
844
|
+
const generateRefUpdates = () => {
|
|
845
|
+
let refInfos = refWithoutBind.get(call.cellCode);
|
|
846
|
+
let refCellCode = call.cellCode;
|
|
847
|
+
|
|
848
|
+
if ((!refInfos || refInfos.length === 0) && call.cellCode.includes(".")) {
|
|
849
|
+
const patternInfo = ctx.extractPropertyPattern(call.cellCode);
|
|
850
|
+
if (patternInfo) {
|
|
851
|
+
for (const [existingCellCode, existingRefInfos] of refWithoutBind) {
|
|
852
|
+
const existingPattern = ctx.extractPropertyPattern(existingCellCode);
|
|
853
|
+
if (existingPattern && existingPattern.pattern === patternInfo.pattern) {
|
|
854
|
+
refInfos = existingRefInfos;
|
|
855
|
+
refCellCode = existingCellCode;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (refInfos && refInfos.length > 0) {
|
|
863
|
+
return refInfos
|
|
864
|
+
.map((info) => {
|
|
865
|
+
let update = `${refCellCode}.${CONSTANTS.REF_PREFIX}${info.refNum}.${info.property} = ${info.updateExpr};`;
|
|
866
|
+
if (refCellCode !== call.cellCode) {
|
|
867
|
+
const originalPattern = ctx.extractPropertyPattern(refCellCode);
|
|
868
|
+
const actualPattern = ctx.extractPropertyPattern(call.cellCode);
|
|
869
|
+
if (originalPattern && actualPattern) {
|
|
870
|
+
const regex = new RegExp(
|
|
871
|
+
`\\b${originalPattern.prefix}\\.${originalPattern.pattern}\\b`,
|
|
872
|
+
"g",
|
|
873
|
+
);
|
|
874
|
+
update = update.replace(
|
|
875
|
+
regex,
|
|
876
|
+
`${actualPattern.prefix}.${actualPattern.pattern}`,
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return update;
|
|
881
|
+
})
|
|
882
|
+
.join(" ");
|
|
883
|
+
}
|
|
884
|
+
return "";
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
// Helper to generate updates for derived cells that depend on this cell (transitively)
|
|
888
|
+
const generateDerivedCellUpdates = () => {
|
|
889
|
+
const updates = [];
|
|
890
|
+
const seenUpdates = new Set(); // Deduplicate updates
|
|
891
|
+
|
|
892
|
+
// Get ALL cells that transitively depend on the cell being set
|
|
893
|
+
const transitiveDependents = ctx.getTransitiveDependents(call.cellCode);
|
|
894
|
+
|
|
895
|
+
for (const derivedCellName of transitiveDependents) {
|
|
896
|
+
// Find refs for the derived cell to update them
|
|
897
|
+
const derivedRefInfos = refWithoutBind.get(derivedCellName);
|
|
898
|
+
if (derivedRefInfos && derivedRefInfos.length > 0) {
|
|
899
|
+
// Get the fully expanded body for this derived cell
|
|
900
|
+
const expandedBody = ctx.getExpandedDerivedBody(derivedCellName);
|
|
901
|
+
|
|
902
|
+
for (const info of derivedRefInfos) {
|
|
903
|
+
// Replace references to the derived cell with its fully expanded body
|
|
904
|
+
// e.g., "Doubled: " + doubled.v -> "Doubled: " + count.v * 2
|
|
905
|
+
// e.g., "Quadrupled: " + quadrupled.v -> "Quadrupled: " + count.v * 2 * 2
|
|
906
|
+
let updateExpr = info.updateExpr.replace(
|
|
907
|
+
new RegExp(`\\b${derivedCellName}\\.v\\b`, "g"),
|
|
908
|
+
`(${expandedBody})`,
|
|
909
|
+
);
|
|
910
|
+
const updateCode = `${derivedCellName}.${CONSTANTS.REF_PREFIX}${info.refNum}.${info.property} = ${updateExpr};`;
|
|
911
|
+
|
|
912
|
+
// Deduplicate
|
|
913
|
+
if (!seenUpdates.has(updateCode)) {
|
|
914
|
+
seenUpdates.add(updateCode);
|
|
915
|
+
updates.push(updateCode);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return updates.join(" ");
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// Collect all updates
|
|
924
|
+
const bindUpdates = generateBindCallbackUpdates();
|
|
925
|
+
const refUpdates = generateRefUpdates();
|
|
926
|
+
const derivedUpdates = generateDerivedCellUpdates();
|
|
927
|
+
const blockUpdate = call.blockVar ? `${call.blockVar}.update();` : "";
|
|
928
|
+
|
|
929
|
+
// Check if there are non-inlined bind callbacks for this cell
|
|
930
|
+
// These are callbacks without element vars that were kept as runtime bind() calls
|
|
931
|
+
let hasNonInlinedBinds = false;
|
|
932
|
+
const cellCallbacks = bindCallbacks.get(call.cellCode);
|
|
933
|
+
if (cellCallbacks) {
|
|
934
|
+
hasNonInlinedBinds = cellCallbacks.some((cb) => cb.elementVars.length === 0);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Check if this cell is a source for forBlock/showBlock
|
|
938
|
+
// First check exact match
|
|
939
|
+
let isBlockSource = blockSourceCells.has(call.cellCode);
|
|
940
|
+
|
|
941
|
+
// If not an exact match and this is a member expression (foo.bar), check if any
|
|
942
|
+
// block source has the same property name (pattern matching)
|
|
943
|
+
// e.g., todoColumn.tasks should match column.tasks
|
|
944
|
+
if (!isBlockSource && call.cellCode.includes(".")) {
|
|
945
|
+
const callPattern = ctx.extractPropertyPattern(call.cellCode);
|
|
946
|
+
if (callPattern) {
|
|
947
|
+
for (const sourceCell of blockSourceCells) {
|
|
948
|
+
const sourcePattern = ctx.extractPropertyPattern(sourceCell);
|
|
949
|
+
if (sourcePattern && sourcePattern.pattern === callPattern.pattern) {
|
|
950
|
+
isBlockSource = true;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Effect loop needed for:
|
|
958
|
+
// 1. Non-inlined bind() callbacks
|
|
959
|
+
// 2. Cells that are sources for forBlock/showBlock WITHOUT an explicit .update() call
|
|
960
|
+
// (if we have blockVar, we call .update() directly, so no need for effect loop)
|
|
961
|
+
const needsEffectLoop = hasNonInlinedBinds || (isBlockSource && !call.blockVar);
|
|
962
|
+
const effectLoop = needsEffectLoop
|
|
963
|
+
? `for (let i = 0; i < ${c}.e.length; i++) ${c}.e[i](${c}.v);`
|
|
964
|
+
: "";
|
|
965
|
+
|
|
966
|
+
// Combine all updates
|
|
967
|
+
const allUpdates = [blockUpdate, bindUpdates, refUpdates, derivedUpdates, effectLoop]
|
|
968
|
+
.filter(Boolean)
|
|
969
|
+
.join(" ");
|
|
970
|
+
|
|
971
|
+
// Always wrap in block with effect loop to support runtime bindings
|
|
972
|
+
s.overwrite(call.start, call.end, `{ ${c}.v = ${v}; ${allUpdates} }`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Remove bind statements and add ref assignments in their place
|
|
977
|
+
// Sort by position descending
|
|
978
|
+
bindStatementsToRemove.sort((a, b) => b.start - a.start);
|
|
979
|
+
|
|
980
|
+
for (const { start, end, cellCode, callback } of bindStatementsToRemove) {
|
|
981
|
+
// Generate ref assignment for each element variable
|
|
982
|
+
let refAssignment = "";
|
|
983
|
+
for (const { varName } of callback.elementVars) {
|
|
984
|
+
refAssignment += generateRefAssignment(cellCode, varName, callback.refNum);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Replace bind() call with ref assignment(s)
|
|
988
|
+
if (refAssignment) {
|
|
989
|
+
s.overwrite(start, end, refAssignment);
|
|
990
|
+
} else {
|
|
991
|
+
// No element vars found - bind() can't be fully inlined
|
|
992
|
+
// Keep the bind() call but wrap it to run immediately AND register for updates
|
|
993
|
+
// This handles complex callbacks like d3.select().call() patterns
|
|
994
|
+
// Don't remove - leave bind() in place (runtime handles immediate execution)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Remove imports that are no longer needed
|
|
999
|
+
// Collect all imports to remove first
|
|
1000
|
+
// Only remove bind import if ALL bind calls were fully inlined (had element vars)
|
|
1001
|
+
const allBindsInlined = bindStatementsToRemove.every((b) => b.callback.elementVars.length > 0);
|
|
1002
|
+
const shouldRemoveBind = bindStatementsToRemove.length > 0 && allBindsInlined;
|
|
1003
|
+
const importsToActuallyRemove = importsToRemove.filter(({ name }) => {
|
|
1004
|
+
return (
|
|
1005
|
+
(name === "get" && getCalls.length > 0) ||
|
|
1006
|
+
(name === "cell" && cellCalls.length > 0) ||
|
|
1007
|
+
(name === "put" && putCalls.length > 0) ||
|
|
1008
|
+
(name === "set" && setCalls.length > 0) ||
|
|
1009
|
+
(name === "bind" && shouldRemoveBind)
|
|
1010
|
+
);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Sort by position
|
|
1014
|
+
importsToActuallyRemove.sort((a, b) => a.specifier.start - b.specifier.start);
|
|
1015
|
+
|
|
1016
|
+
// Remove from right to left to preserve positions
|
|
1017
|
+
for (let i = importsToActuallyRemove.length - 1; i >= 0; i--) {
|
|
1018
|
+
const { specifier } = importsToActuallyRemove[i];
|
|
1019
|
+
|
|
1020
|
+
// Check if this is the first import (after opening brace)
|
|
1021
|
+
const beforeSpecifier = code.slice(Math.max(0, specifier.start - 2), specifier.start);
|
|
1022
|
+
const afterSpecifier = code.slice(specifier.end, specifier.end + 2);
|
|
1023
|
+
|
|
1024
|
+
let startPos = specifier.start;
|
|
1025
|
+
let endPos = specifier.end;
|
|
1026
|
+
|
|
1027
|
+
if (afterSpecifier.startsWith(", ")) {
|
|
1028
|
+
// Remove trailing ", "
|
|
1029
|
+
endPos = specifier.end + 2;
|
|
1030
|
+
} else if (afterSpecifier.startsWith(",")) {
|
|
1031
|
+
// Remove trailing ","
|
|
1032
|
+
endPos = specifier.end + 1;
|
|
1033
|
+
} else if (beforeSpecifier.endsWith(", ")) {
|
|
1034
|
+
// No trailing comma - remove leading ", "
|
|
1035
|
+
startPos = specifier.start - 2;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
s.remove(startPos, endPos);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
code: s.toString(),
|
|
1043
|
+
map: s.generateMap({
|
|
1044
|
+
source: filename,
|
|
1045
|
+
file: filename + ".map",
|
|
1046
|
+
includeContent: true,
|
|
1047
|
+
}),
|
|
1048
|
+
};
|
|
1049
|
+
}
|