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,1217 @@
|
|
|
1
|
+
import MagicString from "magic-string";
|
|
2
|
+
import { isJSXElement, isJSXFragment } from "./parser.js";
|
|
3
|
+
import { processBindings, findGetCalls } from "./transforms/bind-detector.js";
|
|
4
|
+
import { processEvents, generateEventAssignment } from "./transforms/events.js";
|
|
5
|
+
import { extractForInfo, getCallbackPreamble } from "./transforms/for-transform.js";
|
|
6
|
+
import {
|
|
7
|
+
TemplateRegistry,
|
|
8
|
+
VariableNameGenerator,
|
|
9
|
+
extractTemplate,
|
|
10
|
+
} from "./transforms/jsx-to-template.js";
|
|
11
|
+
import { extractShowInfo } from "./transforms/show-transform.js";
|
|
12
|
+
import { CONSTANTS, traverse, escapeStringLiteral } from "./utils.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Code generator using magic-string for efficient string manipulation
|
|
16
|
+
* and source map generation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate compiled output from analyzed JSX
|
|
21
|
+
* @param {string} code - Original source code
|
|
22
|
+
* @param {import("@babel/types").File} ast - Babel AST
|
|
23
|
+
* @param {string} filename - Source filename
|
|
24
|
+
* @returns {{ code: string, map: object }}
|
|
25
|
+
*/
|
|
26
|
+
export function generateOutput(code, ast, filename) {
|
|
27
|
+
const s = new MagicString(code);
|
|
28
|
+
const templateRegistry = new TemplateRegistry();
|
|
29
|
+
const nameGen = new VariableNameGenerator();
|
|
30
|
+
const allEventTypes = new Set();
|
|
31
|
+
|
|
32
|
+
// Track what framework imports are needed
|
|
33
|
+
const usedImports = new Set(["defineComponent"]);
|
|
34
|
+
|
|
35
|
+
// Find all Roqa-defined component tag names (from defineComponent calls)
|
|
36
|
+
const roqaComponentTags = findRoqaComponentTags(ast);
|
|
37
|
+
|
|
38
|
+
// Find all component functions with JSX returns
|
|
39
|
+
const componentInfos = [];
|
|
40
|
+
|
|
41
|
+
traverse(ast, {
|
|
42
|
+
FunctionDeclaration(path) {
|
|
43
|
+
const jsxReturn = findJSXReturn(path.node.body);
|
|
44
|
+
if (jsxReturn) {
|
|
45
|
+
componentInfos.push({
|
|
46
|
+
type: "declaration",
|
|
47
|
+
node: path.node,
|
|
48
|
+
name: path.node.id.name,
|
|
49
|
+
jsxReturn,
|
|
50
|
+
bodyStart: path.node.body.start,
|
|
51
|
+
bodyEnd: path.node.body.end,
|
|
52
|
+
params: path.node.params,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
FunctionExpression(path) {
|
|
57
|
+
const jsxReturn = findJSXReturn(path.node.body);
|
|
58
|
+
if (jsxReturn) {
|
|
59
|
+
componentInfos.push({
|
|
60
|
+
type: "expression",
|
|
61
|
+
node: path.node,
|
|
62
|
+
jsxReturn,
|
|
63
|
+
bodyStart: path.node.body.start,
|
|
64
|
+
bodyEnd: path.node.body.end,
|
|
65
|
+
params: path.node.params,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
noScope: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (componentInfos.length === 0) {
|
|
73
|
+
// No JSX found, return as-is
|
|
74
|
+
return {
|
|
75
|
+
code: s.toString(),
|
|
76
|
+
map: s.generateMap({ source: filename, includeContent: true }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Transform all components (process in reverse order to preserve positions)
|
|
81
|
+
for (const componentInfo of componentInfos.slice().reverse()) {
|
|
82
|
+
transformComponent(
|
|
83
|
+
code,
|
|
84
|
+
s,
|
|
85
|
+
ast,
|
|
86
|
+
componentInfo,
|
|
87
|
+
templateRegistry,
|
|
88
|
+
nameGen,
|
|
89
|
+
usedImports,
|
|
90
|
+
allEventTypes,
|
|
91
|
+
roqaComponentTags,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Collect all needed imports BEFORE calling updateImports
|
|
96
|
+
// Add delegate if we have events
|
|
97
|
+
if (allEventTypes.size > 0) {
|
|
98
|
+
usedImports.add("delegate");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add template if we have templates
|
|
102
|
+
const templateDecls = templateRegistry.getDeclarations();
|
|
103
|
+
if (templateDecls.length > 0) {
|
|
104
|
+
usedImports.add("template");
|
|
105
|
+
// Also add svgTemplate if any SVG templates are used
|
|
106
|
+
if (templateRegistry.hasSvgTemplates()) {
|
|
107
|
+
usedImports.add("svgTemplate");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// NOW update imports with the complete set
|
|
112
|
+
updateImports(s, ast, usedImports);
|
|
113
|
+
|
|
114
|
+
// Add delegate call at the end if we have events
|
|
115
|
+
if (allEventTypes.size > 0) {
|
|
116
|
+
const eventTypesArray = Array.from(allEventTypes)
|
|
117
|
+
.map((e) => `"${e}"`)
|
|
118
|
+
.join(", ");
|
|
119
|
+
s.append(`\n\ndelegate([${eventTypesArray}]);`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Prepend template declarations after imports
|
|
123
|
+
if (templateDecls.length > 0) {
|
|
124
|
+
const importEndPos = findImportEndPosition(ast);
|
|
125
|
+
s.appendLeft(importEndPos, "\n\n" + templateDecls.join("\n"));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
code: s.toString(),
|
|
130
|
+
map: s.generateMap({
|
|
131
|
+
source: filename,
|
|
132
|
+
file: filename + ".map",
|
|
133
|
+
includeContent: true,
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if a node is JSX (element or fragment)
|
|
140
|
+
*/
|
|
141
|
+
function isJSX(node) {
|
|
142
|
+
return isJSXElement(node) || isJSXFragment(node);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find the JSX return statement in a function body
|
|
147
|
+
*/
|
|
148
|
+
function findJSXReturn(body) {
|
|
149
|
+
if (body.type !== "BlockStatement") return null;
|
|
150
|
+
|
|
151
|
+
for (const stmt of body.body) {
|
|
152
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
153
|
+
if (isJSX(stmt.argument)) {
|
|
154
|
+
return stmt;
|
|
155
|
+
}
|
|
156
|
+
// Handle parenthesized: return (<div>...</div>)
|
|
157
|
+
if (stmt.argument.type === "ParenthesizedExpression") {
|
|
158
|
+
if (isJSX(stmt.argument.expression)) {
|
|
159
|
+
return stmt;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find all Roqa-defined component tag names from defineComponent calls
|
|
170
|
+
* @param {import("@babel/types").File} ast - Babel AST
|
|
171
|
+
* @returns {Set<string>} Set of tag names defined via defineComponent
|
|
172
|
+
*/
|
|
173
|
+
function findRoqaComponentTags(ast) {
|
|
174
|
+
const tags = new Set();
|
|
175
|
+
|
|
176
|
+
traverse(ast, {
|
|
177
|
+
CallExpression(path) {
|
|
178
|
+
const node = path.node;
|
|
179
|
+
// Check if this is a defineComponent call
|
|
180
|
+
if (
|
|
181
|
+
node.callee &&
|
|
182
|
+
node.callee.type === "Identifier" &&
|
|
183
|
+
node.callee.name === "defineComponent" &&
|
|
184
|
+
node.arguments.length >= 1
|
|
185
|
+
) {
|
|
186
|
+
const firstArg = node.arguments[0];
|
|
187
|
+
// Extract the tag name from the first argument (should be a string literal)
|
|
188
|
+
if (firstArg.type === "StringLiteral") {
|
|
189
|
+
tags.add(firstArg.value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
noScope: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return tags;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Transform a component function
|
|
201
|
+
*/
|
|
202
|
+
function transformComponent(
|
|
203
|
+
code,
|
|
204
|
+
s,
|
|
205
|
+
ast,
|
|
206
|
+
componentInfo,
|
|
207
|
+
templateRegistry,
|
|
208
|
+
nameGen,
|
|
209
|
+
usedImports,
|
|
210
|
+
allEventTypes,
|
|
211
|
+
roqaComponentTags,
|
|
212
|
+
) {
|
|
213
|
+
const { jsxReturn, bodyStart, bodyEnd, name } = componentInfo;
|
|
214
|
+
|
|
215
|
+
// Get the JSX element or fragment
|
|
216
|
+
let jsxNode = isJSX(jsxReturn.argument) ? jsxReturn.argument : jsxReturn.argument.expression;
|
|
217
|
+
|
|
218
|
+
// Extract template info (isComponentRoot = true for main component)
|
|
219
|
+
// Pass the fragment flag so extractTemplate can handle it properly
|
|
220
|
+
const isFragment = isJSXFragment(jsxNode);
|
|
221
|
+
const templateResult = extractTemplate(
|
|
222
|
+
jsxNode,
|
|
223
|
+
templateRegistry,
|
|
224
|
+
nameGen,
|
|
225
|
+
true,
|
|
226
|
+
isFragment,
|
|
227
|
+
roqaComponentTags,
|
|
228
|
+
);
|
|
229
|
+
const { templateVar, rootVar, traversal, bindings, events, forBlocks, showBlocks } =
|
|
230
|
+
templateResult;
|
|
231
|
+
|
|
232
|
+
// Process bindings for bind() calls
|
|
233
|
+
const processedBindings = processBindings(bindings, code);
|
|
234
|
+
for (const b of processedBindings) {
|
|
235
|
+
if (!b.isStatic) {
|
|
236
|
+
usedImports.add("bind");
|
|
237
|
+
usedImports.add("get");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Process events
|
|
242
|
+
const processedEvents = processEvents(events);
|
|
243
|
+
for (const e of processedEvents) {
|
|
244
|
+
allEventTypes.add(e.eventName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Extract forBlock cell names for variable hoisting
|
|
248
|
+
const forBlockVars = extractForBlockVars(code, forBlocks);
|
|
249
|
+
|
|
250
|
+
// Build the connected callback body
|
|
251
|
+
const connectedBody = buildConnectedBody(
|
|
252
|
+
code,
|
|
253
|
+
templateVar,
|
|
254
|
+
rootVar,
|
|
255
|
+
traversal,
|
|
256
|
+
processedBindings,
|
|
257
|
+
processedEvents,
|
|
258
|
+
forBlocks,
|
|
259
|
+
showBlocks,
|
|
260
|
+
nameGen,
|
|
261
|
+
templateRegistry,
|
|
262
|
+
usedImports,
|
|
263
|
+
allEventTypes,
|
|
264
|
+
forBlockVars,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Replace the return statement with this.connected()
|
|
268
|
+
const returnStart = jsxReturn.start;
|
|
269
|
+
const returnEnd = jsxReturn.end;
|
|
270
|
+
|
|
271
|
+
// Generate variable declarations for forBlock captures (hoisted before connected)
|
|
272
|
+
const forBlockDecls = forBlockVars.map((fb) => `let ${fb.varName};`).join("\n ");
|
|
273
|
+
const forBlockDeclsCode = forBlockDecls ? `${forBlockDecls}\n ` : "";
|
|
274
|
+
|
|
275
|
+
const connectedCode = `${forBlockDeclsCode}this.connected(() => {
|
|
276
|
+
${connectedBody}
|
|
277
|
+
});`;
|
|
278
|
+
|
|
279
|
+
// Ensure the generated this.connected (which appends the root template)
|
|
280
|
+
// is the first this.connected() call inside the component body. If the
|
|
281
|
+
// original source already contained one or more this.connected() calls
|
|
282
|
+
// earlier in the function, move those after our generated connected.
|
|
283
|
+
const existingConnected = [];
|
|
284
|
+
|
|
285
|
+
traverse(ast, {
|
|
286
|
+
CallExpression(path) {
|
|
287
|
+
const node = path.node;
|
|
288
|
+
if (
|
|
289
|
+
node.callee &&
|
|
290
|
+
node.callee.type === "MemberExpression" &&
|
|
291
|
+
node.callee.object &&
|
|
292
|
+
node.callee.object.type === "ThisExpression" &&
|
|
293
|
+
node.callee.property &&
|
|
294
|
+
node.callee.property.type === "Identifier" &&
|
|
295
|
+
node.callee.property.name === "connected" &&
|
|
296
|
+
node.start >= bodyStart &&
|
|
297
|
+
node.end <= bodyEnd
|
|
298
|
+
) {
|
|
299
|
+
existingConnected.push({ start: node.start, end: node.end });
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
noScope: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
existingConnected.sort((a, b) => a.start - b.start);
|
|
306
|
+
|
|
307
|
+
const leadingConnected = existingConnected.filter((c) => c.start < returnStart);
|
|
308
|
+
|
|
309
|
+
if (leadingConnected.length > 0) {
|
|
310
|
+
const snippets = leadingConnected.map((c) => code.slice(c.start, c.end));
|
|
311
|
+
|
|
312
|
+
for (let i = leadingConnected.length - 1; i >= 0; i--) {
|
|
313
|
+
const c = leadingConnected[i];
|
|
314
|
+
s.remove(c.start, c.end);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
s.overwrite(returnStart, returnEnd, "");
|
|
318
|
+
|
|
319
|
+
const insertPos = leadingConnected[0].start;
|
|
320
|
+
const combined = `${connectedCode}\n\n${snippets.join("\n\n")}`;
|
|
321
|
+
s.appendLeft(insertPos, combined);
|
|
322
|
+
} else {
|
|
323
|
+
s.overwrite(returnStart, returnEnd, connectedCode);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { componentName: name };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Extract forBlock cell names and generate variable names for capturing returns
|
|
331
|
+
* @param {string} code - Original source code
|
|
332
|
+
* @param {Array} forBlocks - For blocks from template extraction
|
|
333
|
+
* @returns {Array<{cellName: string, varName: string, cellCode: string}>}
|
|
334
|
+
*/
|
|
335
|
+
function extractForBlockVars(code, forBlocks) {
|
|
336
|
+
const result = [];
|
|
337
|
+
for (const forBlock of forBlocks) {
|
|
338
|
+
const forInfo = extractForInfo(forBlock.node, forBlock.containerVarName);
|
|
339
|
+
const cellCode = code.slice(forInfo.itemsExpression.start, forInfo.itemsExpression.end);
|
|
340
|
+
// Use simple identifier if possible, otherwise generate a name
|
|
341
|
+
const cellName =
|
|
342
|
+
forInfo.itemsExpression.type === "Identifier"
|
|
343
|
+
? forInfo.itemsExpression.name
|
|
344
|
+
: `forBlock_${result.length + 1}`;
|
|
345
|
+
result.push({
|
|
346
|
+
cellName,
|
|
347
|
+
varName: `${cellName}_forBlock`,
|
|
348
|
+
cellCode,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Collect all variable names that are actually used by bindings, events, for blocks, or show blocks
|
|
356
|
+
*/
|
|
357
|
+
function collectUsedVars(bindings, events, forBlocks, showBlocks = []) {
|
|
358
|
+
const used = new Set();
|
|
359
|
+
|
|
360
|
+
for (const b of bindings) {
|
|
361
|
+
used.add(b.targetVar);
|
|
362
|
+
// If this binding uses a marker, also track the marker variable
|
|
363
|
+
if (b.usesMarker) {
|
|
364
|
+
used.add(`${b.targetVar}_marker`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const e of events) {
|
|
369
|
+
used.add(e.varName);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const f of forBlocks) {
|
|
373
|
+
used.add(f.containerVarName);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const s of showBlocks) {
|
|
377
|
+
used.add(s.containerVarName);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return used;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Filter traversal steps to only include those that are needed
|
|
385
|
+
* A step is needed if:
|
|
386
|
+
* 1. Its variable is directly used by a binding/event/forBlock
|
|
387
|
+
* 2. Its variable is referenced by another needed step's traversal code
|
|
388
|
+
* @param {Array} traversal - Traversal steps
|
|
389
|
+
* @param {Set} usedVars - Variables that are directly used
|
|
390
|
+
* @param {Set} alreadyDeclared - Variables that have already been declared (to avoid duplicates)
|
|
391
|
+
*/
|
|
392
|
+
function filterTraversalSteps(traversal, usedVars, alreadyDeclared = new Set()) {
|
|
393
|
+
// Start with directly used vars
|
|
394
|
+
const needed = new Set(usedVars);
|
|
395
|
+
|
|
396
|
+
// Build a map of varName -> step for quick lookup
|
|
397
|
+
const stepMap = new Map();
|
|
398
|
+
for (const step of traversal) {
|
|
399
|
+
stepMap.set(step.varName, step);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Iteratively expand needed set based on dependencies
|
|
403
|
+
// A step depends on another if its code references that var
|
|
404
|
+
let changed = true;
|
|
405
|
+
while (changed) {
|
|
406
|
+
changed = false;
|
|
407
|
+
for (const step of traversal) {
|
|
408
|
+
if (needed.has(step.varName)) {
|
|
409
|
+
// Check what this step depends on
|
|
410
|
+
// e.g., "div_2.firstChild" depends on "div_2"
|
|
411
|
+
for (const [varName] of stepMap) {
|
|
412
|
+
if (step.code.startsWith(varName + ".") && !needed.has(varName)) {
|
|
413
|
+
needed.add(varName);
|
|
414
|
+
changed = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Filter traversal to only needed steps, excluding already declared vars
|
|
422
|
+
return traversal.filter((step) => needed.has(step.varName) && !alreadyDeclared.has(step.varName));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Build the body of the connected() callback
|
|
427
|
+
*/
|
|
428
|
+
function buildConnectedBody(
|
|
429
|
+
code,
|
|
430
|
+
templateVar,
|
|
431
|
+
rootVar,
|
|
432
|
+
traversal,
|
|
433
|
+
bindings,
|
|
434
|
+
events,
|
|
435
|
+
forBlocks,
|
|
436
|
+
showBlocks,
|
|
437
|
+
nameGen,
|
|
438
|
+
templateRegistry,
|
|
439
|
+
usedImports,
|
|
440
|
+
allEventTypes,
|
|
441
|
+
forBlockVars = [],
|
|
442
|
+
) {
|
|
443
|
+
const lines = [];
|
|
444
|
+
|
|
445
|
+
// Separate prop bindings (need to be set before appendChild)
|
|
446
|
+
const propBindings = bindings.filter((b) => b.type === "prop");
|
|
447
|
+
const otherBindings = bindings.filter((b) => b.type !== "prop");
|
|
448
|
+
|
|
449
|
+
// Template instantiation
|
|
450
|
+
lines.push(` const ${rootVar} = ${templateVar}();`);
|
|
451
|
+
|
|
452
|
+
// If we have prop bindings, we need to set them BEFORE appendChild
|
|
453
|
+
// This requires getting element references from the template fragment
|
|
454
|
+
if (propBindings.length > 0) {
|
|
455
|
+
// Collect used vars for props
|
|
456
|
+
const propUsedVars = new Set(propBindings.map((b) => b.targetVar));
|
|
457
|
+
// Generate traversal that works on the template fragment (before appendChild)
|
|
458
|
+
const propTraversal = filterTraversalSteps(traversal, propUsedVars);
|
|
459
|
+
|
|
460
|
+
// Generate traversal code using rootVar instead of this.firstChild
|
|
461
|
+
for (const step of propTraversal) {
|
|
462
|
+
// Replace 'this.firstChild' with rootVar.firstChild for props
|
|
463
|
+
const propCode = step.code.replace("this.firstChild", `${rootVar}.firstChild`);
|
|
464
|
+
lines.push(` const ${step.varName} = ${propCode};`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
lines.push("");
|
|
468
|
+
|
|
469
|
+
// Set props before appendChild (using WeakMap-based setProp)
|
|
470
|
+
for (const binding of propBindings) {
|
|
471
|
+
const bindCode = generateBinding(code, binding, usedImports, null, false, null);
|
|
472
|
+
lines.push(` ${bindCode}`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
lines.push("");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
lines.push(` this.appendChild(${rootVar});`);
|
|
479
|
+
lines.push("");
|
|
480
|
+
|
|
481
|
+
// Track which vars have already been declared (from prop bindings)
|
|
482
|
+
const alreadyDeclared = new Set();
|
|
483
|
+
if (propBindings.length > 0) {
|
|
484
|
+
// Collect all vars that were declared for prop bindings
|
|
485
|
+
const propUsedVars = new Set(propBindings.map((b) => b.targetVar));
|
|
486
|
+
const propTraversalVars = filterTraversalSteps(traversal, propUsedVars);
|
|
487
|
+
for (const step of propTraversalVars) {
|
|
488
|
+
alreadyDeclared.add(step.varName);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Filter traversal to only include steps that are actually needed (excluding prop vars already declared)
|
|
493
|
+
const usedVars = collectUsedVars(otherBindings, events, forBlocks, showBlocks);
|
|
494
|
+
const filteredTraversal = filterTraversalSteps(traversal, usedVars, alreadyDeclared);
|
|
495
|
+
|
|
496
|
+
// DOM traversal - text nodes are now regular nodes (space placeholders), not markers
|
|
497
|
+
for (const step of filteredTraversal) {
|
|
498
|
+
lines.push(` const ${step.varName} = ${step.code};`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (filteredTraversal.length > 0) {
|
|
502
|
+
lines.push("");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Event handler assignments
|
|
506
|
+
for (const event of events) {
|
|
507
|
+
const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
|
|
508
|
+
lines.push(` ${assignment}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (events.length > 0) {
|
|
512
|
+
lines.push("");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Process for blocks
|
|
516
|
+
for (let i = 0; i < forBlocks.length; i++) {
|
|
517
|
+
const forBlock = forBlocks[i];
|
|
518
|
+
const forBlockVar = forBlockVars[i];
|
|
519
|
+
usedImports.add("forBlock");
|
|
520
|
+
const forCode = generateForBlock(
|
|
521
|
+
code,
|
|
522
|
+
forBlock,
|
|
523
|
+
nameGen,
|
|
524
|
+
templateRegistry,
|
|
525
|
+
usedImports,
|
|
526
|
+
allEventTypes,
|
|
527
|
+
forBlockVar,
|
|
528
|
+
);
|
|
529
|
+
lines.push(forCode);
|
|
530
|
+
lines.push("");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Process show blocks
|
|
534
|
+
for (const showBlock of showBlocks) {
|
|
535
|
+
usedImports.add("showBlock");
|
|
536
|
+
const showCode = generateShowBlock(
|
|
537
|
+
code,
|
|
538
|
+
showBlock,
|
|
539
|
+
nameGen,
|
|
540
|
+
templateRegistry,
|
|
541
|
+
usedImports,
|
|
542
|
+
allEventTypes,
|
|
543
|
+
);
|
|
544
|
+
lines.push(showCode);
|
|
545
|
+
lines.push("");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
|
|
549
|
+
const cellRefCounts = new Map();
|
|
550
|
+
|
|
551
|
+
// Other bindings (non-prop)
|
|
552
|
+
for (const binding of otherBindings) {
|
|
553
|
+
const bindCode = generateBinding(code, binding, usedImports, null, false, cellRefCounts);
|
|
554
|
+
lines.push(` ${bindCode}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return lines.join("\n");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Generate code for a forBlock
|
|
562
|
+
*/
|
|
563
|
+
function generateForBlock(
|
|
564
|
+
code,
|
|
565
|
+
forBlock,
|
|
566
|
+
nameGen,
|
|
567
|
+
templateRegistry,
|
|
568
|
+
usedImports,
|
|
569
|
+
allEventTypes,
|
|
570
|
+
forBlockVar = null,
|
|
571
|
+
) {
|
|
572
|
+
const { containerVarName, node } = forBlock;
|
|
573
|
+
|
|
574
|
+
// Extract <For> component info
|
|
575
|
+
const forInfo = extractForInfo(node, containerVarName);
|
|
576
|
+
const { itemsExpression, itemParam, indexParam, bodyJSX, originalCallback } = forInfo;
|
|
577
|
+
|
|
578
|
+
// Get preamble code from original callback (variable declarations, etc.)
|
|
579
|
+
const preamble = getCallbackPreamble(originalCallback);
|
|
580
|
+
const preambleCode = preamble
|
|
581
|
+
.map((stmt) => " " + code.slice(stmt.start, stmt.end))
|
|
582
|
+
.join("\n");
|
|
583
|
+
|
|
584
|
+
// Extract template for the loop body (including nested forBlocks and showBlocks)
|
|
585
|
+
const innerTemplate = extractTemplate(bodyJSX, templateRegistry, nameGen);
|
|
586
|
+
const {
|
|
587
|
+
templateVar,
|
|
588
|
+
rootVar,
|
|
589
|
+
traversal,
|
|
590
|
+
bindings,
|
|
591
|
+
events,
|
|
592
|
+
forBlocks: innerForBlocks,
|
|
593
|
+
showBlocks: innerShowBlocks,
|
|
594
|
+
} = innerTemplate;
|
|
595
|
+
|
|
596
|
+
// Process inner bindings (these need to reference the item parameter)
|
|
597
|
+
const processedBindings = processBindings(bindings, code);
|
|
598
|
+
for (const b of processedBindings) {
|
|
599
|
+
if (!b.isStatic) {
|
|
600
|
+
usedImports.add("bind");
|
|
601
|
+
usedImports.add("get");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Process inner events (may reference item)
|
|
606
|
+
const processedEvents = processEvents(events, itemParam);
|
|
607
|
+
for (const e of processedEvents) {
|
|
608
|
+
allEventTypes.add(e.eventName);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Build the forBlock callback body
|
|
612
|
+
const indexParamName = indexParam || "index";
|
|
613
|
+
const lines = [];
|
|
614
|
+
|
|
615
|
+
// Capture the forBlock return value if we have a variable for it
|
|
616
|
+
const forBlockAssignment = forBlockVar ? `${forBlockVar.varName} = ` : "";
|
|
617
|
+
|
|
618
|
+
lines.push(
|
|
619
|
+
` ${forBlockAssignment}forBlock(${containerVarName}, ${generateExpr(
|
|
620
|
+
code,
|
|
621
|
+
itemsExpression,
|
|
622
|
+
)}, (anchor, ${itemParam}, ${indexParamName}) => {`,
|
|
623
|
+
);
|
|
624
|
+
lines.push(` const ${rootVar} = ${templateVar}();`);
|
|
625
|
+
lines.push("");
|
|
626
|
+
|
|
627
|
+
// Filter traversal to only include steps that are actually needed
|
|
628
|
+
const usedVars = collectUsedVars(
|
|
629
|
+
processedBindings,
|
|
630
|
+
processedEvents,
|
|
631
|
+
innerForBlocks || [],
|
|
632
|
+
innerShowBlocks || [],
|
|
633
|
+
);
|
|
634
|
+
const filteredTraversal = filterTraversalSteps(traversal, usedVars);
|
|
635
|
+
|
|
636
|
+
// Traversal - text nodes are now regular nodes (space placeholders), not markers
|
|
637
|
+
for (const step of filteredTraversal) {
|
|
638
|
+
lines.push(` const ${step.varName} = ${step.code};`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Get the first element for start/end tracking
|
|
642
|
+
const firstElementVar = filteredTraversal.length > 0 ? filteredTraversal[0].varName : rootVar;
|
|
643
|
+
|
|
644
|
+
if (filteredTraversal.length > 0) {
|
|
645
|
+
lines.push("");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Events inside for block
|
|
649
|
+
for (const event of processedEvents) {
|
|
650
|
+
const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
|
|
651
|
+
lines.push(` ${assignment}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (processedEvents.length > 0) {
|
|
655
|
+
lines.push("");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Process nested for blocks inside for block
|
|
659
|
+
if (innerForBlocks && innerForBlocks.length > 0) {
|
|
660
|
+
for (const nestedForBlock of innerForBlocks) {
|
|
661
|
+
usedImports.add("forBlock");
|
|
662
|
+
const nestedForCode = generateForBlock(
|
|
663
|
+
code,
|
|
664
|
+
nestedForBlock,
|
|
665
|
+
nameGen,
|
|
666
|
+
templateRegistry,
|
|
667
|
+
usedImports,
|
|
668
|
+
allEventTypes,
|
|
669
|
+
null, // no variable capture needed inside nested forBlock
|
|
670
|
+
);
|
|
671
|
+
// Indent the forBlock code by 2 extra spaces (it's inside another forBlock callback)
|
|
672
|
+
lines.push(nestedForCode.replace(/^ /gm, " "));
|
|
673
|
+
lines.push("");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Process nested show blocks inside for block
|
|
678
|
+
if (innerShowBlocks && innerShowBlocks.length > 0) {
|
|
679
|
+
for (const nestedShowBlock of innerShowBlocks) {
|
|
680
|
+
usedImports.add("showBlock");
|
|
681
|
+
const showCode = generateShowBlock(
|
|
682
|
+
code,
|
|
683
|
+
nestedShowBlock,
|
|
684
|
+
nameGen,
|
|
685
|
+
templateRegistry,
|
|
686
|
+
usedImports,
|
|
687
|
+
allEventTypes,
|
|
688
|
+
);
|
|
689
|
+
// Indent the showBlock code by 2 extra spaces (it's inside forBlock callback)
|
|
690
|
+
lines.push(showCode.replace(/^ /gm, " "));
|
|
691
|
+
lines.push("");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Preamble (local variables from original callback)
|
|
696
|
+
if (preambleCode) {
|
|
697
|
+
lines.push(preambleCode);
|
|
698
|
+
lines.push("");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
|
|
702
|
+
const cellRefCounts = new Map();
|
|
703
|
+
|
|
704
|
+
// Bindings inside for block
|
|
705
|
+
for (const binding of processedBindings) {
|
|
706
|
+
const bindCode = generateBinding(code, binding, usedImports, itemParam, true, cellRefCounts);
|
|
707
|
+
lines.push(` ${bindCode}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Insert before anchor and return range
|
|
711
|
+
lines.push("");
|
|
712
|
+
lines.push(` anchor.before(${firstElementVar});`);
|
|
713
|
+
lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
|
|
714
|
+
lines.push(` });`);
|
|
715
|
+
|
|
716
|
+
return lines.join("\n");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Generate code for a showBlock
|
|
721
|
+
*/
|
|
722
|
+
function generateShowBlock(code, showBlock, nameGen, templateRegistry, usedImports, allEventTypes) {
|
|
723
|
+
const { containerVarName, node } = showBlock;
|
|
724
|
+
|
|
725
|
+
// Extract <Show> component info
|
|
726
|
+
const showInfo = extractShowInfo(node, containerVarName);
|
|
727
|
+
const { conditionExpression, bodyJSX } = showInfo;
|
|
728
|
+
|
|
729
|
+
// Detect get() calls in the condition to determine if it's simple or complex
|
|
730
|
+
const getCalls = findGetCalls(conditionExpression);
|
|
731
|
+
const isSimpleCell = getCalls.length === 1 && getCalls[0].isOnlyExpression;
|
|
732
|
+
|
|
733
|
+
// Extract template for the show body (including nested forBlocks and showBlocks)
|
|
734
|
+
const innerTemplate = extractTemplate(bodyJSX, templateRegistry, nameGen);
|
|
735
|
+
const {
|
|
736
|
+
templateVar,
|
|
737
|
+
rootVar,
|
|
738
|
+
traversal,
|
|
739
|
+
bindings,
|
|
740
|
+
events,
|
|
741
|
+
forBlocks: innerForBlocks,
|
|
742
|
+
showBlocks: innerShowBlocks,
|
|
743
|
+
} = innerTemplate;
|
|
744
|
+
|
|
745
|
+
// Process inner bindings
|
|
746
|
+
const processedBindings = processBindings(bindings, code);
|
|
747
|
+
for (const b of processedBindings) {
|
|
748
|
+
if (!b.isStatic) {
|
|
749
|
+
usedImports.add("bind");
|
|
750
|
+
usedImports.add("get");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Process inner events
|
|
755
|
+
const processedEvents = processEvents(events);
|
|
756
|
+
for (const e of processedEvents) {
|
|
757
|
+
allEventTypes.add(e.eventName);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Build the showBlock callback body
|
|
761
|
+
const lines = [];
|
|
762
|
+
|
|
763
|
+
// Generate the condition and dependencies based on complexity
|
|
764
|
+
let conditionCode;
|
|
765
|
+
let depsCode = "";
|
|
766
|
+
|
|
767
|
+
if (isSimpleCell) {
|
|
768
|
+
// Simple case: just pass the cell directly
|
|
769
|
+
conditionCode = generateExpr(code, getCalls[0].cellArg);
|
|
770
|
+
} else if (getCalls.length > 0) {
|
|
771
|
+
// Complex expression with get() calls - pass a getter function and deps array
|
|
772
|
+
conditionCode = `() => ${generateExpr(code, conditionExpression)}`;
|
|
773
|
+
const deps = getCalls.map((gc) => generateExpr(code, gc.cellArg));
|
|
774
|
+
depsCode = `, [${deps.join(", ")}]`;
|
|
775
|
+
usedImports.add("get");
|
|
776
|
+
} else {
|
|
777
|
+
// Static expression (no get() calls) - pass the value directly
|
|
778
|
+
conditionCode = generateExpr(code, conditionExpression);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
lines.push(` showBlock(${containerVarName}, ${conditionCode}, (anchor) => {`);
|
|
782
|
+
lines.push(` const ${rootVar} = ${templateVar}();`);
|
|
783
|
+
|
|
784
|
+
// Filter traversal to only include steps that are actually needed
|
|
785
|
+
const usedVars = collectUsedVars(
|
|
786
|
+
processedBindings,
|
|
787
|
+
processedEvents,
|
|
788
|
+
innerForBlocks || [],
|
|
789
|
+
innerShowBlocks || [],
|
|
790
|
+
);
|
|
791
|
+
const filteredTraversal = filterTraversalSteps(traversal, usedVars);
|
|
792
|
+
|
|
793
|
+
// For start/end tracking, we need the actual first DOM element, not the fragment
|
|
794
|
+
// If there's no traversal, we need to grab firstChild before inserting
|
|
795
|
+
let firstElementVar;
|
|
796
|
+
if (filteredTraversal.length > 0) {
|
|
797
|
+
firstElementVar = filteredTraversal[0].varName;
|
|
798
|
+
} else {
|
|
799
|
+
// No traversal - get firstChild from the fragment before it's emptied by insertion
|
|
800
|
+
firstElementVar = `${rootVar}_first`;
|
|
801
|
+
lines.push(` const ${firstElementVar} = ${rootVar}.firstChild;`);
|
|
802
|
+
}
|
|
803
|
+
lines.push("");
|
|
804
|
+
|
|
805
|
+
// Traversal
|
|
806
|
+
for (const step of filteredTraversal) {
|
|
807
|
+
lines.push(` const ${step.varName} = ${step.code};`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (filteredTraversal.length > 0) {
|
|
811
|
+
lines.push("");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Events inside show block
|
|
815
|
+
for (const event of processedEvents) {
|
|
816
|
+
const assignment = generateEventAssignment(event, (node) => generateExpr(code, node));
|
|
817
|
+
lines.push(` ${assignment}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (processedEvents.length > 0) {
|
|
821
|
+
lines.push("");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Process nested for blocks inside show block
|
|
825
|
+
if (innerForBlocks && innerForBlocks.length > 0) {
|
|
826
|
+
for (const forBlock of innerForBlocks) {
|
|
827
|
+
usedImports.add("forBlock");
|
|
828
|
+
const forCode = generateForBlock(
|
|
829
|
+
code,
|
|
830
|
+
forBlock,
|
|
831
|
+
nameGen,
|
|
832
|
+
templateRegistry,
|
|
833
|
+
usedImports,
|
|
834
|
+
allEventTypes,
|
|
835
|
+
null, // no variable capture needed inside showBlock
|
|
836
|
+
);
|
|
837
|
+
// Indent the forBlock code by 2 extra spaces (it's inside showBlock callback)
|
|
838
|
+
lines.push(forCode.replace(/^ /gm, " "));
|
|
839
|
+
lines.push("");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Process nested show blocks inside show block
|
|
844
|
+
if (innerShowBlocks && innerShowBlocks.length > 0) {
|
|
845
|
+
for (const nestedShowBlock of innerShowBlocks) {
|
|
846
|
+
usedImports.add("showBlock");
|
|
847
|
+
const showCode = generateShowBlock(
|
|
848
|
+
code,
|
|
849
|
+
nestedShowBlock,
|
|
850
|
+
nameGen,
|
|
851
|
+
templateRegistry,
|
|
852
|
+
usedImports,
|
|
853
|
+
allEventTypes,
|
|
854
|
+
);
|
|
855
|
+
// Indent the showBlock code by 2 extra spaces (it's inside showBlock callback)
|
|
856
|
+
lines.push(showCode.replace(/^ /gm, " "));
|
|
857
|
+
lines.push("");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Track ref counts per cell for numbered refs (ref_1, ref_2, etc.)
|
|
862
|
+
const cellRefCounts = new Map();
|
|
863
|
+
|
|
864
|
+
// Bindings inside show block
|
|
865
|
+
for (const binding of processedBindings) {
|
|
866
|
+
const bindCode = generateBinding(code, binding, usedImports, null, true, cellRefCounts);
|
|
867
|
+
lines.push(` ${bindCode}`);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Insert before anchor and return range
|
|
871
|
+
lines.push("");
|
|
872
|
+
lines.push(` anchor.before(${firstElementVar});`);
|
|
873
|
+
lines.push(` return { start: ${firstElementVar}, end: ${firstElementVar} };`);
|
|
874
|
+
lines.push(` }${depsCode});`);
|
|
875
|
+
|
|
876
|
+
return lines.join("\n");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Check if a binding is a simple direct mapping (v maps directly to property)
|
|
881
|
+
* Simple: get(cell) with no transform -> element.property = v
|
|
882
|
+
* Not simple: get(cell) ? 'a' : 'b' -> requires transform
|
|
883
|
+
*/
|
|
884
|
+
function isSimpleDirectBinding(binding) {
|
|
885
|
+
const { needsTransform, staticPrefix, fullExpression, getCallNode } = binding;
|
|
886
|
+
|
|
887
|
+
// If there's a static prefix, it's not simple
|
|
888
|
+
if (staticPrefix) return false;
|
|
889
|
+
|
|
890
|
+
// If no transform needed, the entire expression is get(cell)
|
|
891
|
+
if (!needsTransform) return true;
|
|
892
|
+
|
|
893
|
+
// If transform is needed, check if it's just get(cell) with no surrounding expression
|
|
894
|
+
// The fullExpression should be the same as the getCallNode
|
|
895
|
+
if (
|
|
896
|
+
getCallNode &&
|
|
897
|
+
fullExpression.start === getCallNode.start &&
|
|
898
|
+
fullExpression.end === getCallNode.end
|
|
899
|
+
) {
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Generate code for a binding
|
|
908
|
+
* @param {string} code - Original source code
|
|
909
|
+
* @param {object} binding - Binding info
|
|
910
|
+
* @param {Set} usedImports - Set of imports to track
|
|
911
|
+
* @param {string} itemParam - Item parameter name (for forBlock context)
|
|
912
|
+
* @param {boolean} insideForBlock - Whether we're inside a forBlock callback
|
|
913
|
+
* @param {Map} cellRefCounts - Map to track ref counts per cell (for numbered refs)
|
|
914
|
+
*/
|
|
915
|
+
function generateBinding(
|
|
916
|
+
code,
|
|
917
|
+
binding,
|
|
918
|
+
usedImports,
|
|
919
|
+
itemParam = null,
|
|
920
|
+
insideForBlock = false,
|
|
921
|
+
cellRefCounts = null,
|
|
922
|
+
) {
|
|
923
|
+
// Handle prop bindings (for custom elements)
|
|
924
|
+
if (binding.type === "prop") {
|
|
925
|
+
return generatePropBinding(code, binding, usedImports, insideForBlock);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const {
|
|
929
|
+
targetVar,
|
|
930
|
+
targetProperty,
|
|
931
|
+
cellArg,
|
|
932
|
+
fullExpression,
|
|
933
|
+
isStatic,
|
|
934
|
+
needsTransform,
|
|
935
|
+
getCallNode,
|
|
936
|
+
staticPrefix,
|
|
937
|
+
contentParts,
|
|
938
|
+
isSvg,
|
|
939
|
+
} = binding;
|
|
940
|
+
|
|
941
|
+
// Handle new contentParts format (concatenated text content)
|
|
942
|
+
if (contentParts) {
|
|
943
|
+
return generateContentPartsBinding(code, binding, usedImports, insideForBlock, cellRefCounts);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Build prefix string if we have static text before the dynamic expression
|
|
947
|
+
const prefixCode = staticPrefix ? `"${escapeStringLiteral(staticPrefix)}" + ` : "";
|
|
948
|
+
|
|
949
|
+
// For SVG elements or attributes with hyphens, use setAttribute instead of property assignment
|
|
950
|
+
// (except for className and nodeValue which work as properties)
|
|
951
|
+
const needsSetAttribute =
|
|
952
|
+
(isSvg || targetProperty.includes("-")) &&
|
|
953
|
+
targetProperty !== "className" &&
|
|
954
|
+
targetProperty !== "nodeValue";
|
|
955
|
+
|
|
956
|
+
if (isStatic) {
|
|
957
|
+
// Static assignment
|
|
958
|
+
const exprCode = generateExpr(code, fullExpression);
|
|
959
|
+
if (needsSetAttribute) {
|
|
960
|
+
return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${exprCode});`;
|
|
961
|
+
}
|
|
962
|
+
return `${targetVar}.${targetProperty} = ${prefixCode}${exprCode};`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Generate the cell argument code
|
|
966
|
+
const cellCode = generateExpr(code, cellArg);
|
|
967
|
+
|
|
968
|
+
// Generate the expression code for initial value (using original get() calls)
|
|
969
|
+
const initialExprCode = generateExpr(code, fullExpression);
|
|
970
|
+
|
|
971
|
+
// Check if this is a simple direct binding (works for both forBlock and component level)
|
|
972
|
+
// If so, we can use ref-based direct DOM updates instead of bind()
|
|
973
|
+
// Note: For SVG or hyphenated attributes we skip ref optimization since setAttribute needs different handling
|
|
974
|
+
if (isSimpleDirectBinding(binding) && cellRefCounts && !needsSetAttribute) {
|
|
975
|
+
// Get or initialize the ref count for this cell
|
|
976
|
+
const currentCount = cellRefCounts.get(cellCode) || 0;
|
|
977
|
+
const refNum = currentCount + 1;
|
|
978
|
+
cellRefCounts.set(cellCode, refNum);
|
|
979
|
+
|
|
980
|
+
// Determine indentation based on context
|
|
981
|
+
const indent = insideForBlock ? " " : " ";
|
|
982
|
+
|
|
983
|
+
// Emit: initial value assignment + ref storage on cell
|
|
984
|
+
// cell.ref_N = element;
|
|
985
|
+
return `${targetVar}.${targetProperty} = ${initialExprCode};
|
|
986
|
+
${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Fall back to bind() for complex bindings (or SVG attributes)
|
|
990
|
+
usedImports.add("bind");
|
|
991
|
+
usedImports.add("get");
|
|
992
|
+
|
|
993
|
+
// Generate the expression code for bind callback, replacing get(cell) with v
|
|
994
|
+
let bindExprCode;
|
|
995
|
+
if (needsTransform && getCallNode) {
|
|
996
|
+
// Replace the specific get() call with 'v'
|
|
997
|
+
bindExprCode = generateExprWithReplacement(code, fullExpression, getCallNode, "v");
|
|
998
|
+
} else {
|
|
999
|
+
// Simple case: entire expression is get(cell), so just use v
|
|
1000
|
+
bindExprCode = "v";
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Set initial value AND bind for updates
|
|
1004
|
+
if (needsSetAttribute) {
|
|
1005
|
+
return `${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${initialExprCode});
|
|
1006
|
+
bind(${cellCode}, (v) => {
|
|
1007
|
+
${targetVar}.setAttribute("${targetProperty}", ${prefixCode}${bindExprCode});
|
|
1008
|
+
});`;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return `${targetVar}.${targetProperty} = ${prefixCode}${initialExprCode};
|
|
1012
|
+
bind(${cellCode}, (v) => {
|
|
1013
|
+
${targetVar}.${targetProperty} = ${prefixCode}${bindExprCode};
|
|
1014
|
+
});`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Generate binding code for contentParts format (concatenated text)
|
|
1019
|
+
*/
|
|
1020
|
+
function generateContentPartsBinding(
|
|
1021
|
+
code,
|
|
1022
|
+
binding,
|
|
1023
|
+
usedImports,
|
|
1024
|
+
insideForBlock = false,
|
|
1025
|
+
cellRefCounts = null,
|
|
1026
|
+
) {
|
|
1027
|
+
const { targetVar, targetProperty, cellArg, contentParts, isStatic } = binding;
|
|
1028
|
+
|
|
1029
|
+
// Build the concatenated expression from content parts
|
|
1030
|
+
const buildConcatExpr = (useOriginalCode = true) => {
|
|
1031
|
+
const parts = [];
|
|
1032
|
+
for (const part of contentParts) {
|
|
1033
|
+
if (part.type === "static") {
|
|
1034
|
+
// Keep whitespace as-is to preserve spacing between static/dynamic parts
|
|
1035
|
+
const text = part.value;
|
|
1036
|
+
// Only skip if completely empty
|
|
1037
|
+
if (text) {
|
|
1038
|
+
parts.push(`"${escapeStringLiteral(text)}"`);
|
|
1039
|
+
}
|
|
1040
|
+
} else if (part.type === "dynamic") {
|
|
1041
|
+
const exprCode = generateExpr(code, part.expression);
|
|
1042
|
+
// Wrap in parentheses if the expression contains binary operators that could
|
|
1043
|
+
// cause precedence issues when concatenated with strings (e.g., "a + b = " + (get(a) + get(b)))
|
|
1044
|
+
// We check for common arithmetic operators: +, -, *, /, %
|
|
1045
|
+
const needsParens =
|
|
1046
|
+
/[+\-*/%]/.test(exprCode) && !/^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)*$/.test(exprCode);
|
|
1047
|
+
if (useOriginalCode) {
|
|
1048
|
+
parts.push(needsParens ? `(${exprCode})` : exprCode);
|
|
1049
|
+
} else {
|
|
1050
|
+
// For bind callback, we keep the original expression
|
|
1051
|
+
// (the bind will just re-evaluate the whole thing)
|
|
1052
|
+
parts.push(needsParens ? `(${exprCode})` : exprCode);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return parts.join(" + ");
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const initialExpr = buildConcatExpr(true);
|
|
1060
|
+
|
|
1061
|
+
if (isStatic) {
|
|
1062
|
+
return `${targetVar}.${targetProperty} = ${initialExpr};`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// For reactive bindings, use ref-based updates
|
|
1066
|
+
const cellCode = generateExpr(code, cellArg);
|
|
1067
|
+
|
|
1068
|
+
if (cellRefCounts) {
|
|
1069
|
+
// Get or initialize the ref count for this cell
|
|
1070
|
+
const currentCount = cellRefCounts.get(cellCode) || 0;
|
|
1071
|
+
const refNum = currentCount + 1;
|
|
1072
|
+
cellRefCounts.set(cellCode, refNum);
|
|
1073
|
+
|
|
1074
|
+
// Determine indentation based on context
|
|
1075
|
+
const indent = insideForBlock ? " " : " ";
|
|
1076
|
+
|
|
1077
|
+
// Store ref on cell for direct DOM updates
|
|
1078
|
+
// The setter will recompute the full expression using the ref
|
|
1079
|
+
return `${targetVar}.${targetProperty} = ${initialExpr};
|
|
1080
|
+
${indent}${cellCode}.${CONSTANTS.REF_PREFIX}${refNum} = ${targetVar};`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Fallback if no cellRefCounts provided (shouldn't happen in practice)
|
|
1084
|
+
usedImports.add("bind");
|
|
1085
|
+
usedImports.add("get");
|
|
1086
|
+
|
|
1087
|
+
const indent = insideForBlock ? " " : " ";
|
|
1088
|
+
|
|
1089
|
+
// Set initial value AND bind for updates
|
|
1090
|
+
return `${targetVar}.${targetProperty} = ${initialExpr};
|
|
1091
|
+
${indent}bind(${cellCode}, (v) => {
|
|
1092
|
+
${indent} ${targetVar}.${targetProperty} = ${initialExpr};
|
|
1093
|
+
${indent}});`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Generate code for a prop binding (for custom elements)
|
|
1098
|
+
*/
|
|
1099
|
+
function generatePropBinding(code, binding, usedImports) {
|
|
1100
|
+
const { targetVar, propName, expression, fullExpression, isStatic, isThirdParty } = binding;
|
|
1101
|
+
|
|
1102
|
+
// For third-party web components, set property directly on the element
|
|
1103
|
+
// For Roqa components, use setProp() with WeakMap for pre-upgrade storage
|
|
1104
|
+
if (isThirdParty) {
|
|
1105
|
+
// Handle string literal props
|
|
1106
|
+
if (expression && expression.type === "StringLiteral") {
|
|
1107
|
+
return `${targetVar}.${propName} = "${escapeStringLiteral(expression.value)}";`;
|
|
1108
|
+
}
|
|
1109
|
+
const expr = fullExpression || expression;
|
|
1110
|
+
const exprCode = generateExpr(code, expr);
|
|
1111
|
+
return `${targetVar}.${propName} = ${exprCode};`;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Mark that we need setProp import
|
|
1115
|
+
usedImports.add("setProp");
|
|
1116
|
+
|
|
1117
|
+
// Handle string literal props
|
|
1118
|
+
if (expression && expression.type === "StringLiteral") {
|
|
1119
|
+
return `setProp(${targetVar}, "${propName}", "${escapeStringLiteral(expression.value)}");`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Get the expression to use
|
|
1123
|
+
const expr = fullExpression || expression;
|
|
1124
|
+
|
|
1125
|
+
if (isStatic) {
|
|
1126
|
+
// Static expression (no get() calls)
|
|
1127
|
+
const exprCode = generateExpr(code, expr);
|
|
1128
|
+
return `setProp(${targetVar}, "${propName}", ${exprCode});`;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Reactive prop - set initial value
|
|
1132
|
+
// Note: Props are passed at connection time, so we just need the initial value
|
|
1133
|
+
const exprCode = generateExpr(code, expr);
|
|
1134
|
+
return `setProp(${targetVar}, "${propName}", ${exprCode});`;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Generate expression code from AST node using original source
|
|
1139
|
+
*/
|
|
1140
|
+
function generateExpr(code, node) {
|
|
1141
|
+
if (!node) return "undefined";
|
|
1142
|
+
return code.slice(node.start, node.end);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Generate expression code with a specific node replaced
|
|
1147
|
+
*/
|
|
1148
|
+
function generateExprWithReplacement(code, expr, nodeToReplace, replacement) {
|
|
1149
|
+
// Get the full expression code
|
|
1150
|
+
const fullCode = code.slice(expr.start, expr.end);
|
|
1151
|
+
|
|
1152
|
+
// Calculate relative positions
|
|
1153
|
+
const replaceStart = nodeToReplace.start - expr.start;
|
|
1154
|
+
const replaceEnd = nodeToReplace.end - expr.start;
|
|
1155
|
+
|
|
1156
|
+
// Replace
|
|
1157
|
+
return fullCode.slice(0, replaceStart) + replacement + fullCode.slice(replaceEnd);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Update the imports at the top of the file
|
|
1162
|
+
*/
|
|
1163
|
+
function updateImports(s, ast, usedImports) {
|
|
1164
|
+
// Find existing import from roqa
|
|
1165
|
+
let roqaImport = null;
|
|
1166
|
+
|
|
1167
|
+
traverse(ast, {
|
|
1168
|
+
ImportDeclaration(path) {
|
|
1169
|
+
const source = path.node.source.value;
|
|
1170
|
+
if (source === "roqa") {
|
|
1171
|
+
roqaImport = path.node;
|
|
1172
|
+
path.stop();
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
noScope: true,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Preserve existing imports from the original import statement
|
|
1179
|
+
if (roqaImport) {
|
|
1180
|
+
for (const specifier of roqaImport.specifiers) {
|
|
1181
|
+
if (specifier.type === "ImportSpecifier") {
|
|
1182
|
+
const importedName = specifier.imported.name;
|
|
1183
|
+
usedImports.add(importedName);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Build the new import statement
|
|
1189
|
+
const imports = Array.from(usedImports).sort();
|
|
1190
|
+
const newImport = `import { ${imports.join(", ")} } from "roqa";`;
|
|
1191
|
+
|
|
1192
|
+
if (roqaImport) {
|
|
1193
|
+
// Replace existing import
|
|
1194
|
+
s.overwrite(roqaImport.start, roqaImport.end, newImport);
|
|
1195
|
+
} else {
|
|
1196
|
+
// No existing import found, prepend import from roqa
|
|
1197
|
+
s.prepend(`${newImport}\n\n`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Find the position after all imports
|
|
1203
|
+
*/
|
|
1204
|
+
function findImportEndPosition(ast) {
|
|
1205
|
+
let lastImportEnd = 0;
|
|
1206
|
+
|
|
1207
|
+
traverse(ast, {
|
|
1208
|
+
ImportDeclaration(path) {
|
|
1209
|
+
if (path.node.end > lastImportEnd) {
|
|
1210
|
+
lastImportEnd = path.node.end;
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
noScope: true,
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
return lastImportEnd;
|
|
1217
|
+
}
|