pulse-js-framework 1.0.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +414 -182
- package/cli/analyze.js +499 -0
- package/cli/build.js +341 -199
- package/cli/format.js +704 -0
- package/cli/index.js +398 -324
- package/cli/lint.js +642 -0
- package/cli/mobile.js +1473 -0
- package/cli/utils/file-utils.js +298 -0
- package/compiler/lexer.js +766 -581
- package/compiler/parser.js +1797 -900
- package/compiler/transformer.js +1332 -552
- package/index.js +1 -1
- package/mobile/bridge/pulse-native.js +420 -0
- package/package.json +68 -58
- package/runtime/dom.js +363 -33
- package/runtime/index.js +2 -0
- package/runtime/native.js +368 -0
- package/runtime/pulse.js +247 -13
- package/runtime/router.js +596 -392
package/compiler/transformer.js
CHANGED
|
@@ -1,552 +1,1332 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse Transformer - Code generator
|
|
3
|
-
*
|
|
4
|
-
* Transforms AST into JavaScript code
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
default:
|
|
451
|
-
return '
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Transform
|
|
457
|
-
*/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
for (const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Transformer - Code generator
|
|
3
|
+
*
|
|
4
|
+
* Transforms AST into JavaScript code
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Import statement support
|
|
8
|
+
* - Slot-based component composition
|
|
9
|
+
* - CSS scoping with unique class prefixes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NodeType } from './parser.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a unique scope ID for CSS scoping
|
|
16
|
+
*/
|
|
17
|
+
function generateScopeId() {
|
|
18
|
+
return 'p' + Math.random().toString(36).substring(2, 8);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Transformer class
|
|
23
|
+
*/
|
|
24
|
+
export class Transformer {
|
|
25
|
+
constructor(ast, options = {}) {
|
|
26
|
+
this.ast = ast;
|
|
27
|
+
this.options = {
|
|
28
|
+
runtime: 'pulse-framework/runtime',
|
|
29
|
+
minify: false,
|
|
30
|
+
scopeStyles: true, // Enable CSS scoping by default
|
|
31
|
+
...options
|
|
32
|
+
};
|
|
33
|
+
this.stateVars = new Set();
|
|
34
|
+
this.propVars = new Set(); // Track prop names
|
|
35
|
+
this.propDefaults = new Map(); // Track prop default values
|
|
36
|
+
this.actionNames = new Set();
|
|
37
|
+
this.importedComponents = new Map(); // Map of local name -> import info
|
|
38
|
+
this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Transform AST to JavaScript code
|
|
43
|
+
*/
|
|
44
|
+
transform() {
|
|
45
|
+
const parts = [];
|
|
46
|
+
|
|
47
|
+
// Extract imported components first
|
|
48
|
+
if (this.ast.imports) {
|
|
49
|
+
this.extractImportedComponents(this.ast.imports);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Imports (runtime + user imports)
|
|
53
|
+
parts.push(this.generateImports());
|
|
54
|
+
|
|
55
|
+
// Extract prop variables
|
|
56
|
+
if (this.ast.props) {
|
|
57
|
+
this.extractPropVars(this.ast.props);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract state variables
|
|
61
|
+
if (this.ast.state) {
|
|
62
|
+
this.extractStateVars(this.ast.state);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract action names
|
|
66
|
+
if (this.ast.actions) {
|
|
67
|
+
this.extractActionNames(this.ast.actions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Store (must come before router so $store is available to guards)
|
|
71
|
+
if (this.ast.store) {
|
|
72
|
+
parts.push(this.transformStore(this.ast.store));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Router (after store so guards can access $store)
|
|
76
|
+
if (this.ast.router) {
|
|
77
|
+
parts.push(this.transformRouter(this.ast.router));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// State
|
|
81
|
+
if (this.ast.state) {
|
|
82
|
+
parts.push(this.transformState(this.ast.state));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Actions
|
|
86
|
+
if (this.ast.actions) {
|
|
87
|
+
parts.push(this.transformActions(this.ast.actions));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// View
|
|
91
|
+
if (this.ast.view) {
|
|
92
|
+
parts.push(this.transformView(this.ast.view));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Style
|
|
96
|
+
if (this.ast.style) {
|
|
97
|
+
parts.push(this.transformStyle(this.ast.style));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Component export
|
|
101
|
+
parts.push(this.generateExport());
|
|
102
|
+
|
|
103
|
+
return parts.filter(Boolean).join('\n\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract imported component names
|
|
108
|
+
*/
|
|
109
|
+
extractImportedComponents(imports) {
|
|
110
|
+
for (const imp of imports) {
|
|
111
|
+
for (const spec of imp.specifiers) {
|
|
112
|
+
this.importedComponents.set(spec.local, {
|
|
113
|
+
source: imp.source,
|
|
114
|
+
type: spec.type,
|
|
115
|
+
imported: spec.imported
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate imports (runtime + user imports)
|
|
123
|
+
*/
|
|
124
|
+
generateImports() {
|
|
125
|
+
const lines = [];
|
|
126
|
+
|
|
127
|
+
// Runtime imports
|
|
128
|
+
const runtimeImports = [
|
|
129
|
+
'pulse',
|
|
130
|
+
'computed',
|
|
131
|
+
'effect',
|
|
132
|
+
'batch',
|
|
133
|
+
'el',
|
|
134
|
+
'text',
|
|
135
|
+
'on',
|
|
136
|
+
'list',
|
|
137
|
+
'when',
|
|
138
|
+
'mount',
|
|
139
|
+
'model'
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
lines.push(`import { ${runtimeImports.join(', ')} } from '${this.options.runtime}';`);
|
|
143
|
+
|
|
144
|
+
// Router imports (if router block exists)
|
|
145
|
+
if (this.ast.router) {
|
|
146
|
+
lines.push(`import { createRouter } from '${this.options.runtime}/router';`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Store imports (if store block exists)
|
|
150
|
+
if (this.ast.store) {
|
|
151
|
+
const storeImports = ['createStore', 'createActions', 'createGetters'];
|
|
152
|
+
lines.push(`import { ${storeImports.join(', ')} } from '${this.options.runtime}/store';`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// User imports from .pulse files
|
|
156
|
+
if (this.ast.imports && this.ast.imports.length > 0) {
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('// Component imports');
|
|
159
|
+
|
|
160
|
+
for (const imp of this.ast.imports) {
|
|
161
|
+
// Handle default + named imports
|
|
162
|
+
const defaultSpec = imp.specifiers.find(s => s.type === 'default');
|
|
163
|
+
const namedSpecs = imp.specifiers.filter(s => s.type === 'named');
|
|
164
|
+
const namespaceSpec = imp.specifiers.find(s => s.type === 'namespace');
|
|
165
|
+
|
|
166
|
+
let importStr = 'import ';
|
|
167
|
+
if (defaultSpec) {
|
|
168
|
+
importStr += defaultSpec.local;
|
|
169
|
+
if (namedSpecs.length > 0) {
|
|
170
|
+
importStr += ', ';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (namespaceSpec) {
|
|
174
|
+
importStr += `* as ${namespaceSpec.local}`;
|
|
175
|
+
}
|
|
176
|
+
if (namedSpecs.length > 0) {
|
|
177
|
+
const named = namedSpecs.map(s =>
|
|
178
|
+
s.local !== s.imported ? `${s.imported} as ${s.local}` : s.local
|
|
179
|
+
);
|
|
180
|
+
importStr += `{ ${named.join(', ')} }`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Convert .pulse extension to .js
|
|
184
|
+
let source = imp.source;
|
|
185
|
+
if (source.endsWith('.pulse')) {
|
|
186
|
+
source = source.replace('.pulse', '.js');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
importStr += ` from '${source}';`;
|
|
190
|
+
lines.push(importStr);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Extract prop variable names and defaults
|
|
199
|
+
*/
|
|
200
|
+
extractPropVars(propsBlock) {
|
|
201
|
+
for (const prop of propsBlock.properties) {
|
|
202
|
+
this.propVars.add(prop.name);
|
|
203
|
+
this.propDefaults.set(prop.name, prop.value);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract state variable names
|
|
209
|
+
*/
|
|
210
|
+
extractStateVars(stateBlock) {
|
|
211
|
+
for (const prop of stateBlock.properties) {
|
|
212
|
+
this.stateVars.add(prop.name);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract action names
|
|
218
|
+
*/
|
|
219
|
+
extractActionNames(actionsBlock) {
|
|
220
|
+
for (const fn of actionsBlock.functions) {
|
|
221
|
+
this.actionNames.add(fn.name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Transform state block
|
|
227
|
+
*/
|
|
228
|
+
transformState(stateBlock) {
|
|
229
|
+
const lines = ['// State'];
|
|
230
|
+
|
|
231
|
+
for (const prop of stateBlock.properties) {
|
|
232
|
+
const value = this.transformValue(prop.value);
|
|
233
|
+
lines.push(`const ${prop.name} = pulse(${value});`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// Router Transformation
|
|
241
|
+
// =============================================================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Transform router block to createRouter() call
|
|
245
|
+
*/
|
|
246
|
+
transformRouter(routerBlock) {
|
|
247
|
+
const lines = ['// Router'];
|
|
248
|
+
|
|
249
|
+
// Build routes object
|
|
250
|
+
const routesCode = [];
|
|
251
|
+
for (const route of routerBlock.routes) {
|
|
252
|
+
routesCode.push(` '${route.path}': ${route.handler}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
lines.push('const router = createRouter({');
|
|
256
|
+
lines.push(` mode: '${routerBlock.mode}',`);
|
|
257
|
+
if (routerBlock.base) {
|
|
258
|
+
lines.push(` base: '${routerBlock.base}',`);
|
|
259
|
+
}
|
|
260
|
+
lines.push(' routes: {');
|
|
261
|
+
lines.push(routesCode.join(',\n'));
|
|
262
|
+
lines.push(' }');
|
|
263
|
+
lines.push('});');
|
|
264
|
+
lines.push('');
|
|
265
|
+
|
|
266
|
+
// Add global guards
|
|
267
|
+
if (routerBlock.beforeEach) {
|
|
268
|
+
const params = routerBlock.beforeEach.params.join(', ');
|
|
269
|
+
const body = this.transformRouterGuardBody(routerBlock.beforeEach.body);
|
|
270
|
+
lines.push(`router.beforeEach((${params}) => { ${body} });`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (routerBlock.afterEach) {
|
|
274
|
+
const params = routerBlock.afterEach.params.join(', ');
|
|
275
|
+
const body = this.transformRouterGuardBody(routerBlock.afterEach.body);
|
|
276
|
+
lines.push(`router.afterEach((${params}) => { ${body} });`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push('// Start router');
|
|
281
|
+
lines.push('router.start();');
|
|
282
|
+
|
|
283
|
+
return lines.join('\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Transform router guard body - handles store references
|
|
288
|
+
*/
|
|
289
|
+
transformRouterGuardBody(tokens) {
|
|
290
|
+
let code = '';
|
|
291
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
292
|
+
const token = tokens[i];
|
|
293
|
+
if (token.type === 'STRING') {
|
|
294
|
+
code += token.raw || JSON.stringify(token.value);
|
|
295
|
+
} else if (token.type === 'TEMPLATE') {
|
|
296
|
+
code += token.raw || ('`' + token.value + '`');
|
|
297
|
+
} else if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
|
|
298
|
+
// Transform store.xxx to $store.xxx for accessing combined store
|
|
299
|
+
code += '$store';
|
|
300
|
+
} else {
|
|
301
|
+
code += token.value;
|
|
302
|
+
}
|
|
303
|
+
// Add space between tokens unless it's punctuation (before or after)
|
|
304
|
+
const nextToken = tokens[i + 1];
|
|
305
|
+
const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
|
|
306
|
+
const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
|
|
307
|
+
if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
|
|
308
|
+
code += ' ';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return code.trim();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// =============================================================================
|
|
315
|
+
// Store Transformation
|
|
316
|
+
// =============================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Transform store block to createStore(), createActions(), createGetters() calls
|
|
320
|
+
*/
|
|
321
|
+
transformStore(storeBlock) {
|
|
322
|
+
const lines = ['// Store'];
|
|
323
|
+
|
|
324
|
+
// Transform state
|
|
325
|
+
if (storeBlock.state) {
|
|
326
|
+
const stateProps = storeBlock.state.properties.map(p =>
|
|
327
|
+
` ${p.name}: ${this.transformValue(p.value)}`
|
|
328
|
+
).join(',\n');
|
|
329
|
+
|
|
330
|
+
lines.push('const store = createStore({');
|
|
331
|
+
lines.push(stateProps);
|
|
332
|
+
lines.push('}, {');
|
|
333
|
+
lines.push(` persist: ${storeBlock.persist},`);
|
|
334
|
+
lines.push(` storageKey: '${storeBlock.storageKey}'`);
|
|
335
|
+
lines.push('});');
|
|
336
|
+
lines.push('');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Transform actions
|
|
340
|
+
if (storeBlock.actions) {
|
|
341
|
+
lines.push('const storeActions = createActions(store, {');
|
|
342
|
+
for (const fn of storeBlock.actions.functions) {
|
|
343
|
+
const params = fn.params.length > 0 ? ', ' + fn.params.join(', ') : '';
|
|
344
|
+
const body = this.transformStoreActionBody(fn.body);
|
|
345
|
+
lines.push(` ${fn.name}: (store${params}) => { ${body} },`);
|
|
346
|
+
}
|
|
347
|
+
lines.push('});');
|
|
348
|
+
lines.push('');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Transform getters
|
|
352
|
+
if (storeBlock.getters) {
|
|
353
|
+
lines.push('const storeGetters = createGetters(store, {');
|
|
354
|
+
for (const getter of storeBlock.getters.getters) {
|
|
355
|
+
const body = this.transformStoreGetterBody(getter.body);
|
|
356
|
+
lines.push(` ${getter.name}: (store) => { ${body} },`);
|
|
357
|
+
}
|
|
358
|
+
lines.push('});');
|
|
359
|
+
lines.push('');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Create combined $store object for easy access
|
|
363
|
+
lines.push('// Combined store with actions and getters');
|
|
364
|
+
lines.push('const $store = {');
|
|
365
|
+
lines.push(' ...store,');
|
|
366
|
+
if (storeBlock.actions) {
|
|
367
|
+
lines.push(' ...storeActions,');
|
|
368
|
+
}
|
|
369
|
+
if (storeBlock.getters) {
|
|
370
|
+
lines.push(' ...storeGetters,');
|
|
371
|
+
}
|
|
372
|
+
lines.push('};');
|
|
373
|
+
|
|
374
|
+
return lines.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Transform store action body (this.x = y -> store.x.set(y))
|
|
379
|
+
*/
|
|
380
|
+
transformStoreActionBody(tokens) {
|
|
381
|
+
let code = '';
|
|
382
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
383
|
+
const token = tokens[i];
|
|
384
|
+
// Transform 'this' to 'store'
|
|
385
|
+
if (token.value === 'this') {
|
|
386
|
+
code += 'store';
|
|
387
|
+
} else if (token.type === 'STRING') {
|
|
388
|
+
code += token.raw || JSON.stringify(token.value);
|
|
389
|
+
} else if (token.type === 'TEMPLATE') {
|
|
390
|
+
code += token.raw || ('`' + token.value + '`');
|
|
391
|
+
} else if (token.type === 'COLON') {
|
|
392
|
+
// Handle colon with proper spacing (ternary operator)
|
|
393
|
+
code += ' : ';
|
|
394
|
+
} else {
|
|
395
|
+
code += token.value;
|
|
396
|
+
}
|
|
397
|
+
// Add space between tokens unless it's punctuation (before or after)
|
|
398
|
+
const nextToken = tokens[i + 1];
|
|
399
|
+
const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
|
|
400
|
+
const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
|
|
401
|
+
if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
|
|
402
|
+
code += ' ';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Post-process: store.x = y -> store.x.set(y)
|
|
407
|
+
code = code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)');
|
|
408
|
+
|
|
409
|
+
return code.trim();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Transform store getter body (this.x -> store.x.get())
|
|
414
|
+
*/
|
|
415
|
+
transformStoreGetterBody(tokens) {
|
|
416
|
+
let code = this.transformStoreActionBody(tokens);
|
|
417
|
+
// Transform store.x reads to store.x.get() (but not store.x.set or store.x.get)
|
|
418
|
+
code = code.replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
|
|
419
|
+
return code;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Transform value
|
|
424
|
+
*/
|
|
425
|
+
transformValue(node) {
|
|
426
|
+
if (!node) return 'undefined';
|
|
427
|
+
|
|
428
|
+
switch (node.type) {
|
|
429
|
+
case NodeType.Literal:
|
|
430
|
+
if (typeof node.value === 'string') {
|
|
431
|
+
return JSON.stringify(node.value);
|
|
432
|
+
}
|
|
433
|
+
return String(node.value);
|
|
434
|
+
|
|
435
|
+
case NodeType.ObjectLiteral: {
|
|
436
|
+
const props = node.properties.map(p =>
|
|
437
|
+
`${p.name}: ${this.transformValue(p.value)}`
|
|
438
|
+
);
|
|
439
|
+
return `{ ${props.join(', ')} }`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
case NodeType.ArrayLiteral: {
|
|
443
|
+
const elements = node.elements.map(e => this.transformValue(e));
|
|
444
|
+
return `[${elements.join(', ')}]`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
case NodeType.Identifier:
|
|
448
|
+
return node.name;
|
|
449
|
+
|
|
450
|
+
default:
|
|
451
|
+
return 'undefined';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Transform actions block
|
|
457
|
+
*/
|
|
458
|
+
transformActions(actionsBlock) {
|
|
459
|
+
const lines = ['// Actions'];
|
|
460
|
+
|
|
461
|
+
for (const fn of actionsBlock.functions) {
|
|
462
|
+
const asyncKeyword = fn.async ? 'async ' : '';
|
|
463
|
+
const params = fn.params.join(', ');
|
|
464
|
+
const body = this.transformFunctionBody(fn.body);
|
|
465
|
+
|
|
466
|
+
lines.push(`${asyncKeyword}function ${fn.name}(${params}) {`);
|
|
467
|
+
lines.push(` ${body}`);
|
|
468
|
+
lines.push('}');
|
|
469
|
+
lines.push('');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return lines.join('\n');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Transform function body tokens back to code
|
|
477
|
+
*/
|
|
478
|
+
transformFunctionBody(tokens) {
|
|
479
|
+
let code = '';
|
|
480
|
+
let lastToken = null;
|
|
481
|
+
let lastNonSpaceToken = null;
|
|
482
|
+
const statementKeywords = ['let', 'const', 'var', 'return', 'if', 'else', 'for', 'while', 'switch', 'throw', 'try', 'catch', 'finally'];
|
|
483
|
+
const builtinFunctions = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'alert', 'confirm', 'prompt', 'console', 'document', 'window', 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'fetch'];
|
|
484
|
+
|
|
485
|
+
// Tokens that should not have space after them
|
|
486
|
+
const noSpaceAfterTypes = new Set(['DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD']);
|
|
487
|
+
const noSpaceAfterValues = new Set(['.', '(', '[', '{', '!', '~', '...']);
|
|
488
|
+
|
|
489
|
+
// Tokens that should not have space before them
|
|
490
|
+
const noSpaceBeforeTypes = new Set(['DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA', 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET']);
|
|
491
|
+
const noSpaceBeforeValues = new Set(['.', ')', ']', '}', ';', ',', '++', '--', '(', '[']);
|
|
492
|
+
|
|
493
|
+
// Check if token is a statement starter that the regex won't handle
|
|
494
|
+
// (i.e., not a state variable assignment - those are handled by regex)
|
|
495
|
+
// Statement keywords that have their own token types
|
|
496
|
+
// Note: ELSE is excluded because it follows IF and should not have semicolon before it
|
|
497
|
+
const statementTokenTypes = new Set(['IF', 'FOR', 'EACH']);
|
|
498
|
+
|
|
499
|
+
const needsManualSemicolon = (token, nextToken, lastNonSpace) => {
|
|
500
|
+
if (!token) return false;
|
|
501
|
+
|
|
502
|
+
// Don't add semicolon after 'new' keyword (e.g., new Date())
|
|
503
|
+
if (lastNonSpace?.value === 'new') return false;
|
|
504
|
+
|
|
505
|
+
// Statement keywords with dedicated token types (if, else, for, etc.)
|
|
506
|
+
if (statementTokenTypes.has(token.type)) return true;
|
|
507
|
+
|
|
508
|
+
// Only process IDENT tokens from here
|
|
509
|
+
if (token.type !== 'IDENT') return false;
|
|
510
|
+
|
|
511
|
+
// Statement keywords (let, const, var, return, etc.)
|
|
512
|
+
if (statementKeywords.includes(token.value)) return true;
|
|
513
|
+
|
|
514
|
+
// State variable assignment (stateVar = value)
|
|
515
|
+
// These need semicolons BEFORE them when following a statement end
|
|
516
|
+
// The regex adds semicolons AFTER, but not before
|
|
517
|
+
if (this.stateVars.has(token.value) && nextToken?.type === 'EQ') {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Builtin function call or action call (not state var assignment)
|
|
522
|
+
if (nextToken?.type === 'LPAREN') {
|
|
523
|
+
if (builtinFunctions.includes(token.value)) return true;
|
|
524
|
+
if (this.actionNames.has(token.value)) return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Builtin method chain (e.g., document.body.classList.toggle(...))
|
|
528
|
+
if (nextToken?.type === 'DOT' && builtinFunctions.includes(token.value)) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return false;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Check if previous context indicates end of statement
|
|
536
|
+
const afterStatementEnd = (lastNonSpace) => {
|
|
537
|
+
if (!lastNonSpace) return false;
|
|
538
|
+
return lastNonSpace.type === 'RBRACE' ||
|
|
539
|
+
lastNonSpace.type === 'RPAREN' ||
|
|
540
|
+
lastNonSpace.type === 'RBRACKET' ||
|
|
541
|
+
lastNonSpace.type === 'SEMICOLON' ||
|
|
542
|
+
lastNonSpace.type === 'STRING' ||
|
|
543
|
+
lastNonSpace.type === 'NUMBER' ||
|
|
544
|
+
lastNonSpace.type === 'TRUE' ||
|
|
545
|
+
lastNonSpace.type === 'FALSE' ||
|
|
546
|
+
lastNonSpace.type === 'NULL' ||
|
|
547
|
+
lastNonSpace.value === 'null' ||
|
|
548
|
+
lastNonSpace.value === 'true' ||
|
|
549
|
+
lastNonSpace.value === 'false' ||
|
|
550
|
+
lastNonSpace.type === 'IDENT'; // Any identifier can end a statement (variables, function results, etc.)
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
let afterIfCondition = false; // Track if we just closed an if(...) condition
|
|
554
|
+
|
|
555
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
556
|
+
const token = tokens[i];
|
|
557
|
+
const nextToken = tokens[i + 1];
|
|
558
|
+
|
|
559
|
+
// Track if we're exiting an if condition (if followed by ( ... ))
|
|
560
|
+
if (token.type === 'RPAREN' && lastNonSpaceToken?.type === 'IF') {
|
|
561
|
+
// This isn't quite right - we need to track the if keyword before the paren
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Detect if we just closed an if/for/while condition
|
|
565
|
+
// Look back to find if there was an if/for/while before this )
|
|
566
|
+
if (token.type === 'RPAREN') {
|
|
567
|
+
// Check if this ) closes an if/for/while condition
|
|
568
|
+
// by looking for the matching ( and what's before it
|
|
569
|
+
let parenDepth = 1;
|
|
570
|
+
for (let j = i - 1; j >= 0 && parenDepth > 0; j--) {
|
|
571
|
+
if (tokens[j].type === 'RPAREN') parenDepth++;
|
|
572
|
+
else if (tokens[j].type === 'LPAREN') parenDepth--;
|
|
573
|
+
if (parenDepth === 0) {
|
|
574
|
+
// Found matching (, check what's before it
|
|
575
|
+
if (j > 0 && (tokens[j - 1].type === 'IF' || tokens[j - 1].type === 'FOR' ||
|
|
576
|
+
tokens[j - 1].type === 'EACH' || tokens[j - 1].value === 'while')) {
|
|
577
|
+
afterIfCondition = true;
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Add semicolon before statement starters (only for non-state-var cases)
|
|
585
|
+
// But NOT immediately after an if/for/while condition (the next statement is the body)
|
|
586
|
+
if (needsManualSemicolon(token, nextToken, lastNonSpaceToken) && afterStatementEnd(lastNonSpaceToken)) {
|
|
587
|
+
if (!afterIfCondition && lastToken && lastToken.value !== ';' && lastToken.value !== '{') {
|
|
588
|
+
code += '; ';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Reset afterIfCondition after processing the token following the condition
|
|
593
|
+
if (afterIfCondition && token.type !== 'RPAREN') {
|
|
594
|
+
afterIfCondition = false;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Emit the token value
|
|
598
|
+
if (token.type === 'STRING') {
|
|
599
|
+
code += token.raw || JSON.stringify(token.value);
|
|
600
|
+
} else if (token.type === 'TEMPLATE') {
|
|
601
|
+
code += token.raw || ('`' + token.value + '`');
|
|
602
|
+
} else {
|
|
603
|
+
code += token.value;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Decide whether to add space after this token
|
|
607
|
+
let addSpace = true;
|
|
608
|
+
|
|
609
|
+
// No space after certain tokens
|
|
610
|
+
if (noSpaceAfterTypes.has(token.type) || noSpaceAfterValues.has(token.value)) {
|
|
611
|
+
addSpace = false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// No space before certain tokens (look ahead)
|
|
615
|
+
if (nextToken && (noSpaceBeforeTypes.has(nextToken.type) || noSpaceBeforeValues.has(nextToken.value))) {
|
|
616
|
+
addSpace = false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (addSpace && nextToken) {
|
|
620
|
+
code += ' ';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
lastToken = token;
|
|
624
|
+
lastNonSpaceToken = token;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Build patterns for boundaries
|
|
628
|
+
const stateVarPattern = [...this.stateVars].join('|');
|
|
629
|
+
const funcPattern = [...this.actionNames, ...builtinFunctions].join('|');
|
|
630
|
+
|
|
631
|
+
// Transform state access - order matters!
|
|
632
|
+
// First, replace state var assignments with boundary detection
|
|
633
|
+
// Use multiple passes to handle the case where replacements change the boundaries
|
|
634
|
+
for (const stateVar of this.stateVars) {
|
|
635
|
+
// Replace standalone state var assignments: stateVar = value -> stateVar.set(value)
|
|
636
|
+
// Use negative lookahead (?!=) to avoid matching === or ==
|
|
637
|
+
// Stop at: next state var assignment (original or already replaced), function call, statement keyword, or end
|
|
638
|
+
const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${statementKeywords.join('|')})\\b|;|$`;
|
|
639
|
+
const assignPattern = new RegExp(
|
|
640
|
+
`\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`,
|
|
641
|
+
'g'
|
|
642
|
+
);
|
|
643
|
+
code = code.replace(assignPattern, (_match, value) => `${stateVar}.set(${value.trim()});`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Clean up any double semicolons
|
|
647
|
+
code = code.replace(/;+/g, ';');
|
|
648
|
+
code = code.replace(/; ;/g, ';');
|
|
649
|
+
|
|
650
|
+
// Then, replace state var reads:
|
|
651
|
+
// - Not preceded by . (avoid transforming obj.stateVar property access)
|
|
652
|
+
// - Not followed by = (assignment), ( (function call), .get/.set (already transformed)
|
|
653
|
+
// - Allow method calls like stateVar.toLowerCase() -> stateVar.get().toLowerCase()
|
|
654
|
+
for (const stateVar of this.stateVars) {
|
|
655
|
+
code = code.replace(
|
|
656
|
+
new RegExp(`(?<!\\.\\s*)\\b${stateVar}\\b(?!\\s*=(?!=)|\\s*\\(|\\s*\\.(?:get|set))`, 'g'),
|
|
657
|
+
`${stateVar}.get()`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return code.trim();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Transform view block
|
|
666
|
+
*/
|
|
667
|
+
transformView(viewBlock) {
|
|
668
|
+
const lines = ['// View'];
|
|
669
|
+
|
|
670
|
+
// Generate render function with props parameter
|
|
671
|
+
lines.push('function render({ props = {}, slots = {} } = {}) {');
|
|
672
|
+
|
|
673
|
+
// Destructure props with defaults if component has props
|
|
674
|
+
if (this.propVars.size > 0) {
|
|
675
|
+
const propsDestructure = [...this.propVars].map(name => {
|
|
676
|
+
const defaultValue = this.propDefaults.get(name);
|
|
677
|
+
const defaultCode = defaultValue ? this.transformValue(defaultValue) : 'undefined';
|
|
678
|
+
return `${name} = ${defaultCode}`;
|
|
679
|
+
}).join(', ');
|
|
680
|
+
lines.push(` const { ${propsDestructure} } = props;`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
lines.push(' return (');
|
|
684
|
+
|
|
685
|
+
const children = viewBlock.children.map(child =>
|
|
686
|
+
this.transformViewNode(child, 4)
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
if (children.length === 1) {
|
|
690
|
+
lines.push(children[0]);
|
|
691
|
+
} else {
|
|
692
|
+
lines.push(' [');
|
|
693
|
+
lines.push(children.map(c => ' ' + c.trim()).join(',\n'));
|
|
694
|
+
lines.push(' ]');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
lines.push(' );');
|
|
698
|
+
lines.push('}');
|
|
699
|
+
|
|
700
|
+
return lines.join('\n');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Transform a view node (element, directive, slot, text)
|
|
705
|
+
*/
|
|
706
|
+
transformViewNode(node, indent = 0) {
|
|
707
|
+
const pad = ' '.repeat(indent);
|
|
708
|
+
|
|
709
|
+
switch (node.type) {
|
|
710
|
+
case NodeType.Element:
|
|
711
|
+
return this.transformElement(node, indent);
|
|
712
|
+
|
|
713
|
+
case NodeType.TextNode:
|
|
714
|
+
return this.transformTextNode(node, indent);
|
|
715
|
+
|
|
716
|
+
case NodeType.IfDirective:
|
|
717
|
+
return this.transformIfDirective(node, indent);
|
|
718
|
+
|
|
719
|
+
case NodeType.EachDirective:
|
|
720
|
+
return this.transformEachDirective(node, indent);
|
|
721
|
+
|
|
722
|
+
case NodeType.EventDirective:
|
|
723
|
+
return this.transformEventDirective(node, indent);
|
|
724
|
+
|
|
725
|
+
case NodeType.SlotElement:
|
|
726
|
+
return this.transformSlot(node, indent);
|
|
727
|
+
|
|
728
|
+
// Router directives
|
|
729
|
+
case NodeType.LinkDirective:
|
|
730
|
+
return this.transformLinkDirective(node, indent);
|
|
731
|
+
|
|
732
|
+
case NodeType.OutletDirective:
|
|
733
|
+
return this.transformOutletDirective(node, indent);
|
|
734
|
+
|
|
735
|
+
case NodeType.NavigateDirective:
|
|
736
|
+
return this.transformNavigateDirective(node, indent);
|
|
737
|
+
|
|
738
|
+
default:
|
|
739
|
+
return `${pad}/* unknown node: ${node.type} */`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Transform slot element
|
|
745
|
+
*/
|
|
746
|
+
transformSlot(node, indent) {
|
|
747
|
+
const pad = ' '.repeat(indent);
|
|
748
|
+
const slotName = node.name || 'default';
|
|
749
|
+
|
|
750
|
+
// If there's fallback content
|
|
751
|
+
if (node.fallback && node.fallback.length > 0) {
|
|
752
|
+
const fallbackCode = node.fallback.map(child =>
|
|
753
|
+
this.transformViewNode(child, indent + 2)
|
|
754
|
+
).join(',\n');
|
|
755
|
+
|
|
756
|
+
return `${pad}(slots?.${slotName} ? slots.${slotName}() : (\n${fallbackCode}\n${pad}))`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Simple slot reference
|
|
760
|
+
return `${pad}(slots?.${slotName} ? slots.${slotName}() : null)`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// =============================================================================
|
|
764
|
+
// Router Directive Transformations
|
|
765
|
+
// =============================================================================
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Transform @link directive
|
|
769
|
+
*/
|
|
770
|
+
transformLinkDirective(node, indent) {
|
|
771
|
+
const pad = ' '.repeat(indent);
|
|
772
|
+
const path = this.transformExpression(node.path);
|
|
773
|
+
|
|
774
|
+
let content;
|
|
775
|
+
if (Array.isArray(node.content)) {
|
|
776
|
+
content = node.content.map(c => this.transformViewNode(c, 0)).join(', ');
|
|
777
|
+
} else if (node.content) {
|
|
778
|
+
content = this.transformTextNode(node.content, 0).trim();
|
|
779
|
+
} else {
|
|
780
|
+
content = '""';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let options = '{}';
|
|
784
|
+
if (node.options) {
|
|
785
|
+
options = this.transformExpression(node.options);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return `${pad}router.link(${path}, ${content}, ${options})`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Transform @outlet directive
|
|
793
|
+
*/
|
|
794
|
+
transformOutletDirective(node, indent) {
|
|
795
|
+
const pad = ' '.repeat(indent);
|
|
796
|
+
const container = node.container ? `'${node.container}'` : "'#app'";
|
|
797
|
+
return `${pad}router.outlet(${container})`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Transform @navigate directive (used in event handlers)
|
|
802
|
+
*/
|
|
803
|
+
transformNavigateDirective(node, indent) {
|
|
804
|
+
const pad = ' '.repeat(indent);
|
|
805
|
+
|
|
806
|
+
// Handle @back and @forward
|
|
807
|
+
if (node.action === 'back') {
|
|
808
|
+
return `${pad}router.back()`;
|
|
809
|
+
}
|
|
810
|
+
if (node.action === 'forward') {
|
|
811
|
+
return `${pad}router.forward()`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Regular @navigate(path)
|
|
815
|
+
const path = this.transformExpression(node.path);
|
|
816
|
+
let options = '';
|
|
817
|
+
if (node.options) {
|
|
818
|
+
options = ', ' + this.transformExpression(node.options);
|
|
819
|
+
}
|
|
820
|
+
return `${pad}router.navigate(${path}${options})`;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Transform element
|
|
825
|
+
*/
|
|
826
|
+
transformElement(node, indent) {
|
|
827
|
+
const pad = ' '.repeat(indent);
|
|
828
|
+
const parts = [];
|
|
829
|
+
|
|
830
|
+
// Check if this is a component (starts with uppercase)
|
|
831
|
+
const selectorParts = node.selector.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
|
|
832
|
+
const tagName = selectorParts ? selectorParts[1] : '';
|
|
833
|
+
const isComponent = tagName && /^[A-Z]/.test(tagName) && this.importedComponents.has(tagName);
|
|
834
|
+
|
|
835
|
+
if (isComponent) {
|
|
836
|
+
// Render as component call
|
|
837
|
+
return this.transformComponentCall(node, indent);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Add scoped class to selector if CSS scoping is enabled
|
|
841
|
+
let selector = node.selector;
|
|
842
|
+
if (this.scopeId && selector) {
|
|
843
|
+
// Add scope class to the selector
|
|
844
|
+
selector = this.addScopeToSelector(selector);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Start with el() call
|
|
848
|
+
parts.push(`${pad}el('${selector}'`);
|
|
849
|
+
|
|
850
|
+
// Add event handlers as on() chain
|
|
851
|
+
const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
|
|
852
|
+
|
|
853
|
+
// Add text content
|
|
854
|
+
if (node.textContent.length > 0) {
|
|
855
|
+
for (const text of node.textContent) {
|
|
856
|
+
const textCode = this.transformTextNode(text, 0);
|
|
857
|
+
parts.push(`,\n${pad} ${textCode.trim()}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Add children
|
|
862
|
+
if (node.children.length > 0) {
|
|
863
|
+
for (const child of node.children) {
|
|
864
|
+
const childCode = this.transformViewNode(child, indent + 2);
|
|
865
|
+
parts.push(`,\n${childCode}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
parts.push(')');
|
|
870
|
+
|
|
871
|
+
// Chain event handlers
|
|
872
|
+
let result = parts.join('');
|
|
873
|
+
for (const handler of eventHandlers) {
|
|
874
|
+
const handlerCode = this.transformExpression(handler.handler);
|
|
875
|
+
result = `on(${result}, '${handler.event}', () => { ${handlerCode}; })`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return result;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Add scope class to a CSS selector
|
|
883
|
+
*/
|
|
884
|
+
addScopeToSelector(selector) {
|
|
885
|
+
// If selector has classes, add scope class after the first class
|
|
886
|
+
// Otherwise add it at the end
|
|
887
|
+
if (selector.includes('.')) {
|
|
888
|
+
// Add scope after tag name and before first class
|
|
889
|
+
return selector.replace(/^([a-zA-Z0-9-]*)/, `$1.${this.scopeId}`);
|
|
890
|
+
}
|
|
891
|
+
// Just a tag name, add scope class
|
|
892
|
+
return `${selector}.${this.scopeId}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Transform a component call (imported component)
|
|
897
|
+
*/
|
|
898
|
+
transformComponentCall(node, indent) {
|
|
899
|
+
const pad = ' '.repeat(indent);
|
|
900
|
+
const selectorParts = node.selector.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
|
|
901
|
+
const componentName = selectorParts[1];
|
|
902
|
+
|
|
903
|
+
// Extract slots from children
|
|
904
|
+
const slots = {};
|
|
905
|
+
|
|
906
|
+
// Children become the default slot
|
|
907
|
+
if (node.children.length > 0 || node.textContent.length > 0) {
|
|
908
|
+
const slotContent = [];
|
|
909
|
+
for (const text of node.textContent) {
|
|
910
|
+
slotContent.push(this.transformTextNode(text, 0).trim());
|
|
911
|
+
}
|
|
912
|
+
for (const child of node.children) {
|
|
913
|
+
slotContent.push(this.transformViewNode(child, 0).trim());
|
|
914
|
+
}
|
|
915
|
+
slots['default'] = slotContent;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Build component call
|
|
919
|
+
let code = `${pad}${componentName}.render({ `;
|
|
920
|
+
|
|
921
|
+
const renderArgs = [];
|
|
922
|
+
|
|
923
|
+
// Add props if any
|
|
924
|
+
if (node.props && node.props.length > 0) {
|
|
925
|
+
const propsCode = node.props.map(prop => {
|
|
926
|
+
const valueCode = this.transformExpression(prop.value);
|
|
927
|
+
return `${prop.name}: ${valueCode}`;
|
|
928
|
+
}).join(', ');
|
|
929
|
+
renderArgs.push(`props: { ${propsCode} }`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Add slots if any
|
|
933
|
+
if (Object.keys(slots).length > 0) {
|
|
934
|
+
const slotCode = Object.entries(slots).map(([name, content]) => {
|
|
935
|
+
return `${name}: () => ${content.length === 1 ? content[0] : `[${content.join(', ')}]`}`;
|
|
936
|
+
}).join(', ');
|
|
937
|
+
renderArgs.push(`slots: { ${slotCode} }`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
code += renderArgs.join(', ');
|
|
941
|
+
code += ' })';
|
|
942
|
+
return code;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Transform text node
|
|
947
|
+
*/
|
|
948
|
+
transformTextNode(node, indent) {
|
|
949
|
+
const pad = ' '.repeat(indent);
|
|
950
|
+
const parts = node.parts;
|
|
951
|
+
|
|
952
|
+
if (parts.length === 1 && typeof parts[0] === 'string') {
|
|
953
|
+
// Simple static text
|
|
954
|
+
return `${pad}${JSON.stringify(parts[0])}`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Has interpolations - use text() with a function
|
|
958
|
+
const textParts = parts.map(part => {
|
|
959
|
+
if (typeof part === 'string') {
|
|
960
|
+
return JSON.stringify(part);
|
|
961
|
+
}
|
|
962
|
+
// Interpolation
|
|
963
|
+
const expr = this.transformExpressionString(part.expression);
|
|
964
|
+
return `\${${expr}}`;
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
return `${pad}text(() => \`${textParts.join('')}\`)`;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Transform @if directive
|
|
972
|
+
*/
|
|
973
|
+
transformIfDirective(node, indent) {
|
|
974
|
+
const pad = ' '.repeat(indent);
|
|
975
|
+
const condition = this.transformExpression(node.condition);
|
|
976
|
+
|
|
977
|
+
const consequent = node.consequent.map(c =>
|
|
978
|
+
this.transformViewNode(c, indent + 2)
|
|
979
|
+
).join(',\n');
|
|
980
|
+
|
|
981
|
+
let code = `${pad}when(\n`;
|
|
982
|
+
code += `${pad} () => ${condition},\n`;
|
|
983
|
+
code += `${pad} () => (\n${consequent}\n${pad} )`;
|
|
984
|
+
|
|
985
|
+
if (node.alternate) {
|
|
986
|
+
const alternate = node.alternate.map(c =>
|
|
987
|
+
this.transformViewNode(c, indent + 2)
|
|
988
|
+
).join(',\n');
|
|
989
|
+
code += `,\n${pad} () => (\n${alternate}\n${pad} )`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
code += `\n${pad})`;
|
|
993
|
+
return code;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Transform @each directive
|
|
998
|
+
*/
|
|
999
|
+
transformEachDirective(node, indent) {
|
|
1000
|
+
const pad = ' '.repeat(indent);
|
|
1001
|
+
const iterable = this.transformExpression(node.iterable);
|
|
1002
|
+
|
|
1003
|
+
const template = node.template.map(t =>
|
|
1004
|
+
this.transformViewNode(t, indent + 2)
|
|
1005
|
+
).join(',\n');
|
|
1006
|
+
|
|
1007
|
+
return `${pad}list(\n` +
|
|
1008
|
+
`${pad} () => ${iterable},\n` +
|
|
1009
|
+
`${pad} (${node.itemName}, _index) => (\n${template}\n${pad} )\n` +
|
|
1010
|
+
`${pad})`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Transform event directive
|
|
1015
|
+
*/
|
|
1016
|
+
transformEventDirective(node, indent) {
|
|
1017
|
+
const pad = ' '.repeat(indent);
|
|
1018
|
+
const handler = this.transformExpression(node.handler);
|
|
1019
|
+
|
|
1020
|
+
if (node.children && node.children.length > 0) {
|
|
1021
|
+
const children = node.children.map(c =>
|
|
1022
|
+
this.transformViewNode(c, indent + 2)
|
|
1023
|
+
).join(',\n');
|
|
1024
|
+
|
|
1025
|
+
return `${pad}on(el('div',\n${children}\n${pad}), '${node.event}', () => { ${handler}; })`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return `/* event: ${node.event} -> ${handler} */`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Transform AST expression to JS code
|
|
1033
|
+
*/
|
|
1034
|
+
transformExpression(node) {
|
|
1035
|
+
if (!node) return '';
|
|
1036
|
+
|
|
1037
|
+
switch (node.type) {
|
|
1038
|
+
case NodeType.Identifier:
|
|
1039
|
+
if (this.stateVars.has(node.name)) {
|
|
1040
|
+
return `${node.name}.get()`;
|
|
1041
|
+
}
|
|
1042
|
+
// Props are accessed directly (already destructured)
|
|
1043
|
+
return node.name;
|
|
1044
|
+
|
|
1045
|
+
case NodeType.Literal:
|
|
1046
|
+
if (typeof node.value === 'string') {
|
|
1047
|
+
return JSON.stringify(node.value);
|
|
1048
|
+
}
|
|
1049
|
+
return String(node.value);
|
|
1050
|
+
|
|
1051
|
+
case NodeType.TemplateLiteral:
|
|
1052
|
+
// Transform state vars in template literal
|
|
1053
|
+
return '`' + this.transformExpressionString(node.value) + '`';
|
|
1054
|
+
|
|
1055
|
+
case NodeType.MemberExpression: {
|
|
1056
|
+
const obj = this.transformExpression(node.object);
|
|
1057
|
+
// Use optional chaining when accessing properties on function call results
|
|
1058
|
+
// This prevents "Cannot read property X of null" when the function returns null
|
|
1059
|
+
const isCallResult = node.object.type === NodeType.CallExpression;
|
|
1060
|
+
const accessor = isCallResult ? '?.' : '.';
|
|
1061
|
+
if (node.computed) {
|
|
1062
|
+
const prop = this.transformExpression(node.property);
|
|
1063
|
+
return isCallResult ? `${obj}?.[${prop}]` : `${obj}[${prop}]`;
|
|
1064
|
+
}
|
|
1065
|
+
return `${obj}${accessor}${node.property}`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
case NodeType.CallExpression: {
|
|
1069
|
+
const callee = this.transformExpression(node.callee);
|
|
1070
|
+
const args = node.arguments.map(a => this.transformExpression(a)).join(', ');
|
|
1071
|
+
return `${callee}(${args})`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
case NodeType.BinaryExpression: {
|
|
1075
|
+
const left = this.transformExpression(node.left);
|
|
1076
|
+
const right = this.transformExpression(node.right);
|
|
1077
|
+
return `(${left} ${node.operator} ${right})`;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
case NodeType.UnaryExpression: {
|
|
1081
|
+
const argument = this.transformExpression(node.argument);
|
|
1082
|
+
return `${node.operator}${argument}`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
case NodeType.UpdateExpression: {
|
|
1086
|
+
const argument = this.transformExpression(node.argument);
|
|
1087
|
+
// For state variables, convert x++ to x.set(x.get() + 1)
|
|
1088
|
+
if (node.argument.type === NodeType.Identifier &&
|
|
1089
|
+
this.stateVars.has(node.argument.name)) {
|
|
1090
|
+
const name = node.argument.name;
|
|
1091
|
+
const delta = node.operator === '++' ? 1 : -1;
|
|
1092
|
+
return `${name}.set(${name}.get() + ${delta})`;
|
|
1093
|
+
}
|
|
1094
|
+
return node.prefix
|
|
1095
|
+
? `${node.operator}${argument}`
|
|
1096
|
+
: `${argument}${node.operator}`;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
case NodeType.ConditionalExpression: {
|
|
1100
|
+
const test = this.transformExpression(node.test);
|
|
1101
|
+
const consequent = this.transformExpression(node.consequent);
|
|
1102
|
+
const alternate = this.transformExpression(node.alternate);
|
|
1103
|
+
return `(${test} ? ${consequent} : ${alternate})`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
case NodeType.ArrowFunction: {
|
|
1107
|
+
const params = node.params.join(', ');
|
|
1108
|
+
if (node.block) {
|
|
1109
|
+
// Block body - transform tokens
|
|
1110
|
+
const body = this.transformFunctionBody(node.body);
|
|
1111
|
+
return `(${params}) => { ${body} }`;
|
|
1112
|
+
} else {
|
|
1113
|
+
// Expression body
|
|
1114
|
+
const body = this.transformExpression(node.body);
|
|
1115
|
+
return `(${params}) => ${body}`;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
case NodeType.AssignmentExpression: {
|
|
1120
|
+
const left = this.transformExpression(node.left);
|
|
1121
|
+
const right = this.transformExpression(node.right);
|
|
1122
|
+
// For state variables, convert to .set()
|
|
1123
|
+
if (node.left.type === NodeType.Identifier &&
|
|
1124
|
+
this.stateVars.has(node.left.name)) {
|
|
1125
|
+
return `${node.left.name}.set(${right})`;
|
|
1126
|
+
}
|
|
1127
|
+
return `(${left} = ${right})`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
case NodeType.ArrayLiteral: {
|
|
1131
|
+
const elements = node.elements.map(e => this.transformExpression(e)).join(', ');
|
|
1132
|
+
return `[${elements}]`;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
case NodeType.ObjectLiteral: {
|
|
1136
|
+
const props = node.properties.map(p => {
|
|
1137
|
+
if (p.type === NodeType.SpreadElement) {
|
|
1138
|
+
return `...${this.transformExpression(p.argument)}`;
|
|
1139
|
+
}
|
|
1140
|
+
if (p.shorthand) {
|
|
1141
|
+
// Check if it's a state var
|
|
1142
|
+
if (this.stateVars.has(p.name)) {
|
|
1143
|
+
return `${p.name}: ${p.name}.get()`;
|
|
1144
|
+
}
|
|
1145
|
+
return p.name;
|
|
1146
|
+
}
|
|
1147
|
+
return `${p.name}: ${this.transformExpression(p.value)}`;
|
|
1148
|
+
}).join(', ');
|
|
1149
|
+
return `{ ${props} }`;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
case NodeType.SpreadElement:
|
|
1153
|
+
return `...${this.transformExpression(node.argument)}`;
|
|
1154
|
+
|
|
1155
|
+
default:
|
|
1156
|
+
return '/* unknown expression */';
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Transform expression string (from interpolation)
|
|
1162
|
+
*/
|
|
1163
|
+
transformExpressionString(exprStr) {
|
|
1164
|
+
// Simple transformation: wrap state vars with .get()
|
|
1165
|
+
let result = exprStr;
|
|
1166
|
+
for (const stateVar of this.stateVars) {
|
|
1167
|
+
result = result.replace(
|
|
1168
|
+
new RegExp(`\\b${stateVar}\\b`, 'g'),
|
|
1169
|
+
`${stateVar}.get()`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
// Add optional chaining after function calls followed by property access
|
|
1173
|
+
// This prevents "Cannot read property X of null" errors
|
|
1174
|
+
// Pattern: functionName(...).property -> functionName(...)?.property
|
|
1175
|
+
result = result.replace(/(\w+\([^)]*\))\.(\w)/g, '$1?.$2');
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Transform style block with optional scoping
|
|
1181
|
+
*/
|
|
1182
|
+
transformStyle(styleBlock) {
|
|
1183
|
+
const lines = ['// Styles'];
|
|
1184
|
+
|
|
1185
|
+
if (this.scopeId) {
|
|
1186
|
+
lines.push(`const SCOPE_ID = '${this.scopeId}';`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
lines.push('const styles = `');
|
|
1190
|
+
|
|
1191
|
+
for (const rule of styleBlock.rules) {
|
|
1192
|
+
lines.push(this.transformStyleRule(rule, 0));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
lines.push('`;');
|
|
1196
|
+
lines.push('');
|
|
1197
|
+
lines.push('// Inject styles');
|
|
1198
|
+
lines.push('const styleEl = document.createElement("style");');
|
|
1199
|
+
|
|
1200
|
+
if (this.scopeId) {
|
|
1201
|
+
lines.push(`styleEl.setAttribute('data-p-scope', SCOPE_ID);`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
lines.push('styleEl.textContent = styles;');
|
|
1205
|
+
lines.push('document.head.appendChild(styleEl);');
|
|
1206
|
+
|
|
1207
|
+
return lines.join('\n');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Transform style rule with optional scoping
|
|
1212
|
+
*/
|
|
1213
|
+
transformStyleRule(rule, indent) {
|
|
1214
|
+
const pad = ' '.repeat(indent);
|
|
1215
|
+
const lines = [];
|
|
1216
|
+
|
|
1217
|
+
// Apply scope to selector if enabled
|
|
1218
|
+
let selector = rule.selector;
|
|
1219
|
+
if (this.scopeId) {
|
|
1220
|
+
selector = this.scopeStyleSelector(selector);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
lines.push(`${pad}${selector} {`);
|
|
1224
|
+
|
|
1225
|
+
for (const prop of rule.properties) {
|
|
1226
|
+
lines.push(`${pad} ${prop.name}: ${prop.value};`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
for (const nested of rule.nestedRules) {
|
|
1230
|
+
// For nested rules, combine selectors (simplified nesting)
|
|
1231
|
+
const nestedLines = this.transformStyleRule(nested, indent + 1);
|
|
1232
|
+
lines.push(nestedLines);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
lines.push(`${pad}}`);
|
|
1236
|
+
return lines.join('\n');
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* Add scope to CSS selector
|
|
1241
|
+
* .container -> .container.p123abc
|
|
1242
|
+
* div -> div.p123abc
|
|
1243
|
+
* .a .b -> .a.p123abc .b.p123abc
|
|
1244
|
+
* @media (max-width: 900px) -> @media (max-width: 900px) (unchanged)
|
|
1245
|
+
* :root, body, *, html -> unchanged (global selectors)
|
|
1246
|
+
*/
|
|
1247
|
+
scopeStyleSelector(selector) {
|
|
1248
|
+
if (!this.scopeId) return selector;
|
|
1249
|
+
|
|
1250
|
+
// Don't scope at-rules (media queries, keyframes, etc.)
|
|
1251
|
+
if (selector.startsWith('@')) {
|
|
1252
|
+
return selector;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Global selectors that should not be scoped
|
|
1256
|
+
const globalSelectors = new Set([':root', 'body', 'html', '*']);
|
|
1257
|
+
|
|
1258
|
+
// Check if entire selector is a global selector (possibly with classes like body.dark)
|
|
1259
|
+
const trimmed = selector.trim();
|
|
1260
|
+
const baseSelector = trimmed.split(/[.#\[:\s]/)[0];
|
|
1261
|
+
if (globalSelectors.has(baseSelector) || globalSelectors.has(trimmed)) {
|
|
1262
|
+
return selector;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Split by comma for multiple selectors
|
|
1266
|
+
return selector.split(',').map(part => {
|
|
1267
|
+
part = part.trim();
|
|
1268
|
+
|
|
1269
|
+
// Split by space for descendant selectors
|
|
1270
|
+
return part.split(/\s+/).map(segment => {
|
|
1271
|
+
// Check if this segment is a global selector
|
|
1272
|
+
const segmentBase = segment.split(/[.#\[]/)[0];
|
|
1273
|
+
if (globalSelectors.has(segmentBase) || globalSelectors.has(segment)) {
|
|
1274
|
+
return segment;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Skip pseudo-elements and pseudo-classes at the end
|
|
1278
|
+
const pseudoMatch = segment.match(/^([^:]+)(:.+)?$/);
|
|
1279
|
+
if (pseudoMatch) {
|
|
1280
|
+
const base = pseudoMatch[1];
|
|
1281
|
+
const pseudo = pseudoMatch[2] || '';
|
|
1282
|
+
|
|
1283
|
+
// Skip if it's just a pseudo selector (like :root)
|
|
1284
|
+
if (!base || globalSelectors.has(`:${pseudo.slice(1)}`)) return segment;
|
|
1285
|
+
|
|
1286
|
+
// Add scope class
|
|
1287
|
+
return `${base}.${this.scopeId}${pseudo}`;
|
|
1288
|
+
}
|
|
1289
|
+
return `${segment}.${this.scopeId}`;
|
|
1290
|
+
}).join(' ');
|
|
1291
|
+
}).join(', ');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Generate component export
|
|
1296
|
+
*/
|
|
1297
|
+
generateExport() {
|
|
1298
|
+
const pageName = this.ast.page?.name || 'Component';
|
|
1299
|
+
const routePath = this.ast.route?.path || null;
|
|
1300
|
+
|
|
1301
|
+
const lines = ['// Export'];
|
|
1302
|
+
lines.push(`export const ${pageName} = {`);
|
|
1303
|
+
lines.push(' render,');
|
|
1304
|
+
|
|
1305
|
+
if (routePath) {
|
|
1306
|
+
lines.push(` route: ${JSON.stringify(routePath)},`);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
lines.push(' mount: (target) => {');
|
|
1310
|
+
lines.push(' const el = render();');
|
|
1311
|
+
lines.push(' return mount(target, el);');
|
|
1312
|
+
lines.push(' }');
|
|
1313
|
+
lines.push('};');
|
|
1314
|
+
lines.push('');
|
|
1315
|
+
lines.push(`export default ${pageName};`);
|
|
1316
|
+
|
|
1317
|
+
return lines.join('\n');
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Transform AST to JavaScript code
|
|
1323
|
+
*/
|
|
1324
|
+
export function transform(ast, options = {}) {
|
|
1325
|
+
const transformer = new Transformer(ast, options);
|
|
1326
|
+
return transformer.transform();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
export default {
|
|
1330
|
+
Transformer,
|
|
1331
|
+
transform
|
|
1332
|
+
};
|