pulse-js-framework 1.2.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.
@@ -1,552 +1,1332 @@
1
- /**
2
- * Pulse Transformer - Code generator
3
- *
4
- * Transforms AST into JavaScript code
5
- */
6
-
7
- import { NodeType } from './parser.js';
8
-
9
- /**
10
- * Transformer class
11
- */
12
- export class Transformer {
13
- constructor(ast, options = {}) {
14
- this.ast = ast;
15
- this.options = {
16
- runtime: 'pulse-framework/runtime',
17
- minify: false,
18
- ...options
19
- };
20
- this.stateVars = new Set();
21
- this.actionNames = new Set();
22
- }
23
-
24
- /**
25
- * Transform AST to JavaScript code
26
- */
27
- transform() {
28
- const parts = [];
29
-
30
- // Imports
31
- parts.push(this.generateImports());
32
-
33
- // Extract state variables
34
- if (this.ast.state) {
35
- this.extractStateVars(this.ast.state);
36
- }
37
-
38
- // Extract action names
39
- if (this.ast.actions) {
40
- this.extractActionNames(this.ast.actions);
41
- }
42
-
43
- // State
44
- if (this.ast.state) {
45
- parts.push(this.transformState(this.ast.state));
46
- }
47
-
48
- // Actions
49
- if (this.ast.actions) {
50
- parts.push(this.transformActions(this.ast.actions));
51
- }
52
-
53
- // View
54
- if (this.ast.view) {
55
- parts.push(this.transformView(this.ast.view));
56
- }
57
-
58
- // Style
59
- if (this.ast.style) {
60
- parts.push(this.transformStyle(this.ast.style));
61
- }
62
-
63
- // Component export
64
- parts.push(this.generateExport());
65
-
66
- return parts.filter(Boolean).join('\n\n');
67
- }
68
-
69
- /**
70
- * Generate imports
71
- */
72
- generateImports() {
73
- const imports = [
74
- 'pulse',
75
- 'computed',
76
- 'effect',
77
- 'batch',
78
- 'el',
79
- 'text',
80
- 'on',
81
- 'list',
82
- 'when',
83
- 'mount',
84
- 'model'
85
- ];
86
-
87
- return `import { ${imports.join(', ')} } from '${this.options.runtime}';`;
88
- }
89
-
90
- /**
91
- * Extract state variable names
92
- */
93
- extractStateVars(stateBlock) {
94
- for (const prop of stateBlock.properties) {
95
- this.stateVars.add(prop.name);
96
- }
97
- }
98
-
99
- /**
100
- * Extract action names
101
- */
102
- extractActionNames(actionsBlock) {
103
- for (const fn of actionsBlock.functions) {
104
- this.actionNames.add(fn.name);
105
- }
106
- }
107
-
108
- /**
109
- * Transform state block
110
- */
111
- transformState(stateBlock) {
112
- const lines = ['// State'];
113
-
114
- for (const prop of stateBlock.properties) {
115
- const value = this.transformValue(prop.value);
116
- lines.push(`const ${prop.name} = pulse(${value});`);
117
- }
118
-
119
- return lines.join('\n');
120
- }
121
-
122
- /**
123
- * Transform value
124
- */
125
- transformValue(node) {
126
- if (!node) return 'undefined';
127
-
128
- switch (node.type) {
129
- case NodeType.Literal:
130
- if (typeof node.value === 'string') {
131
- return JSON.stringify(node.value);
132
- }
133
- return String(node.value);
134
-
135
- case NodeType.ObjectLiteral: {
136
- const props = node.properties.map(p =>
137
- `${p.name}: ${this.transformValue(p.value)}`
138
- );
139
- return `{ ${props.join(', ')} }`;
140
- }
141
-
142
- case NodeType.ArrayLiteral: {
143
- const elements = node.elements.map(e => this.transformValue(e));
144
- return `[${elements.join(', ')}]`;
145
- }
146
-
147
- case NodeType.Identifier:
148
- return node.name;
149
-
150
- default:
151
- return 'undefined';
152
- }
153
- }
154
-
155
- /**
156
- * Transform actions block
157
- */
158
- transformActions(actionsBlock) {
159
- const lines = ['// Actions'];
160
-
161
- for (const fn of actionsBlock.functions) {
162
- const asyncKeyword = fn.async ? 'async ' : '';
163
- const params = fn.params.join(', ');
164
- const body = this.transformFunctionBody(fn.body);
165
-
166
- lines.push(`${asyncKeyword}function ${fn.name}(${params}) {`);
167
- lines.push(` ${body}`);
168
- lines.push('}');
169
- lines.push('');
170
- }
171
-
172
- return lines.join('\n');
173
- }
174
-
175
- /**
176
- * Transform function body tokens back to code
177
- */
178
- transformFunctionBody(tokens) {
179
- let code = '';
180
- for (const token of tokens) {
181
- if (token.type === 'STRING') {
182
- code += token.raw || JSON.stringify(token.value);
183
- } else {
184
- code += token.value;
185
- }
186
- code += ' ';
187
- }
188
-
189
- // Transform state access
190
- for (const stateVar of this.stateVars) {
191
- // Replace standalone state var assignments
192
- code = code.replace(
193
- new RegExp(`\\b${stateVar}\\s*=\\s*`, 'g'),
194
- `${stateVar}.set(`
195
- );
196
- // Close the set call (simplified)
197
- code = code.replace(
198
- new RegExp(`${stateVar}\\.set\\(([^;\\n]+)([;\\n])`, 'g'),
199
- `${stateVar}.set($1)$2`
200
- );
201
- // Replace state var reads
202
- code = code.replace(
203
- new RegExp(`\\b${stateVar}\\b(?!\\s*[.=])`, 'g'),
204
- `${stateVar}.get()`
205
- );
206
- }
207
-
208
- return code.trim();
209
- }
210
-
211
- /**
212
- * Transform view block
213
- */
214
- transformView(viewBlock) {
215
- const lines = ['// View'];
216
- lines.push('function render() {');
217
- lines.push(' return (');
218
-
219
- const children = viewBlock.children.map(child =>
220
- this.transformViewNode(child, 4)
221
- );
222
-
223
- if (children.length === 1) {
224
- lines.push(children[0]);
225
- } else {
226
- lines.push(' [');
227
- lines.push(children.map(c => ' ' + c.trim()).join(',\n'));
228
- lines.push(' ]');
229
- }
230
-
231
- lines.push(' );');
232
- lines.push('}');
233
-
234
- return lines.join('\n');
235
- }
236
-
237
- /**
238
- * Transform a view node (element, directive, text)
239
- */
240
- transformViewNode(node, indent = 0) {
241
- const pad = ' '.repeat(indent);
242
-
243
- switch (node.type) {
244
- case NodeType.Element:
245
- return this.transformElement(node, indent);
246
-
247
- case NodeType.TextNode:
248
- return this.transformTextNode(node, indent);
249
-
250
- case NodeType.IfDirective:
251
- return this.transformIfDirective(node, indent);
252
-
253
- case NodeType.EachDirective:
254
- return this.transformEachDirective(node, indent);
255
-
256
- case NodeType.EventDirective:
257
- return this.transformEventDirective(node, indent);
258
-
259
- default:
260
- return `${pad}/* unknown node: ${node.type} */`;
261
- }
262
- }
263
-
264
- /**
265
- * Transform element
266
- */
267
- transformElement(node, indent) {
268
- const pad = ' '.repeat(indent);
269
- const parts = [];
270
-
271
- // Start with el() call
272
- parts.push(`${pad}el('${node.selector}'`);
273
-
274
- // Add event handlers as on() chain
275
- const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
276
-
277
- // Add text content
278
- if (node.textContent.length > 0) {
279
- for (const text of node.textContent) {
280
- const textCode = this.transformTextNode(text, 0);
281
- parts.push(`,\n${pad} ${textCode.trim()}`);
282
- }
283
- }
284
-
285
- // Add children
286
- if (node.children.length > 0) {
287
- for (const child of node.children) {
288
- const childCode = this.transformViewNode(child, indent + 2);
289
- parts.push(`,\n${childCode}`);
290
- }
291
- }
292
-
293
- parts.push(')');
294
-
295
- // Chain event handlers
296
- let result = parts.join('');
297
- for (const handler of eventHandlers) {
298
- const handlerCode = this.transformExpression(handler.handler);
299
- result = `on(${result}, '${handler.event}', () => { ${handlerCode}; })`;
300
- }
301
-
302
- return result;
303
- }
304
-
305
- /**
306
- * Transform text node
307
- */
308
- transformTextNode(node, indent) {
309
- const pad = ' '.repeat(indent);
310
- const parts = node.parts;
311
-
312
- if (parts.length === 1 && typeof parts[0] === 'string') {
313
- // Simple static text
314
- return `${pad}${JSON.stringify(parts[0])}`;
315
- }
316
-
317
- // Has interpolations - use text() with a function
318
- const textParts = parts.map(part => {
319
- if (typeof part === 'string') {
320
- return JSON.stringify(part);
321
- }
322
- // Interpolation
323
- const expr = this.transformExpressionString(part.expression);
324
- return `\${${expr}}`;
325
- });
326
-
327
- return `${pad}text(() => \`${textParts.join('')}\`)`;
328
- }
329
-
330
- /**
331
- * Transform @if directive
332
- */
333
- transformIfDirective(node, indent) {
334
- const pad = ' '.repeat(indent);
335
- const condition = this.transformExpression(node.condition);
336
-
337
- const consequent = node.consequent.map(c =>
338
- this.transformViewNode(c, indent + 2)
339
- ).join(',\n');
340
-
341
- let code = `${pad}when(\n`;
342
- code += `${pad} () => ${condition},\n`;
343
- code += `${pad} () => (\n${consequent}\n${pad} )`;
344
-
345
- if (node.alternate) {
346
- const alternate = node.alternate.map(c =>
347
- this.transformViewNode(c, indent + 2)
348
- ).join(',\n');
349
- code += `,\n${pad} () => (\n${alternate}\n${pad} )`;
350
- }
351
-
352
- code += `\n${pad})`;
353
- return code;
354
- }
355
-
356
- /**
357
- * Transform @each directive
358
- */
359
- transformEachDirective(node, indent) {
360
- const pad = ' '.repeat(indent);
361
- const iterable = this.transformExpression(node.iterable);
362
-
363
- const template = node.template.map(t =>
364
- this.transformViewNode(t, indent + 2)
365
- ).join(',\n');
366
-
367
- return `${pad}list(\n` +
368
- `${pad} () => ${iterable},\n` +
369
- `${pad} (${node.itemName}, _index) => (\n${template}\n${pad} )\n` +
370
- `${pad})`;
371
- }
372
-
373
- /**
374
- * Transform event directive
375
- */
376
- transformEventDirective(node, indent) {
377
- const pad = ' '.repeat(indent);
378
- const handler = this.transformExpression(node.handler);
379
-
380
- if (node.children && node.children.length > 0) {
381
- const children = node.children.map(c =>
382
- this.transformViewNode(c, indent + 2)
383
- ).join(',\n');
384
-
385
- return `${pad}on(el('div',\n${children}\n${pad}), '${node.event}', () => { ${handler}; })`;
386
- }
387
-
388
- return `/* event: ${node.event} -> ${handler} */`;
389
- }
390
-
391
- /**
392
- * Transform AST expression to JS code
393
- */
394
- transformExpression(node) {
395
- if (!node) return '';
396
-
397
- switch (node.type) {
398
- case NodeType.Identifier:
399
- if (this.stateVars.has(node.name)) {
400
- return `${node.name}.get()`;
401
- }
402
- return node.name;
403
-
404
- case NodeType.Literal:
405
- if (typeof node.value === 'string') {
406
- return JSON.stringify(node.value);
407
- }
408
- return String(node.value);
409
-
410
- case NodeType.MemberExpression: {
411
- const obj = this.transformExpression(node.object);
412
- if (node.computed) {
413
- const prop = this.transformExpression(node.property);
414
- return `${obj}[${prop}]`;
415
- }
416
- return `${obj}.${node.property}`;
417
- }
418
-
419
- case NodeType.CallExpression: {
420
- const callee = this.transformExpression(node.callee);
421
- const args = node.arguments.map(a => this.transformExpression(a)).join(', ');
422
- return `${callee}(${args})`;
423
- }
424
-
425
- case NodeType.BinaryExpression: {
426
- const left = this.transformExpression(node.left);
427
- const right = this.transformExpression(node.right);
428
- return `(${left} ${node.operator} ${right})`;
429
- }
430
-
431
- case NodeType.UnaryExpression: {
432
- const argument = this.transformExpression(node.argument);
433
- return `${node.operator}${argument}`;
434
- }
435
-
436
- case NodeType.UpdateExpression: {
437
- const argument = this.transformExpression(node.argument);
438
- // For state variables, convert x++ to x.set(x.get() + 1)
439
- if (node.argument.type === NodeType.Identifier &&
440
- this.stateVars.has(node.argument.name)) {
441
- const name = node.argument.name;
442
- const delta = node.operator === '++' ? 1 : -1;
443
- return `${name}.set(${name}.get() + ${delta})`;
444
- }
445
- return node.prefix
446
- ? `${node.operator}${argument}`
447
- : `${argument}${node.operator}`;
448
- }
449
-
450
- default:
451
- return '/* unknown expression */';
452
- }
453
- }
454
-
455
- /**
456
- * Transform expression string (from interpolation)
457
- */
458
- transformExpressionString(exprStr) {
459
- // Simple transformation: wrap state vars with .get()
460
- let result = exprStr;
461
- for (const stateVar of this.stateVars) {
462
- result = result.replace(
463
- new RegExp(`\\b${stateVar}\\b`, 'g'),
464
- `${stateVar}.get()`
465
- );
466
- }
467
- return result;
468
- }
469
-
470
- /**
471
- * Transform style block
472
- */
473
- transformStyle(styleBlock) {
474
- const lines = ['// Styles'];
475
- lines.push('const styles = `');
476
-
477
- for (const rule of styleBlock.rules) {
478
- lines.push(this.transformStyleRule(rule, 0));
479
- }
480
-
481
- lines.push('`;');
482
- lines.push('');
483
- lines.push('// Inject styles');
484
- lines.push('const styleEl = document.createElement("style");');
485
- lines.push('styleEl.textContent = styles;');
486
- lines.push('document.head.appendChild(styleEl);');
487
-
488
- return lines.join('\n');
489
- }
490
-
491
- /**
492
- * Transform style rule
493
- */
494
- transformStyleRule(rule, indent) {
495
- const pad = ' '.repeat(indent);
496
- const lines = [];
497
-
498
- lines.push(`${pad}${rule.selector} {`);
499
-
500
- for (const prop of rule.properties) {
501
- lines.push(`${pad} ${prop.name}: ${prop.value};`);
502
- }
503
-
504
- for (const nested of rule.nestedRules) {
505
- // For nested rules, combine selectors (simplified nesting)
506
- const nestedLines = this.transformStyleRule(nested, indent + 1);
507
- lines.push(nestedLines);
508
- }
509
-
510
- lines.push(`${pad}}`);
511
- return lines.join('\n');
512
- }
513
-
514
- /**
515
- * Generate component export
516
- */
517
- generateExport() {
518
- const pageName = this.ast.page?.name || 'Component';
519
- const routePath = this.ast.route?.path || null;
520
-
521
- const lines = ['// Export'];
522
- lines.push(`export const ${pageName} = {`);
523
- lines.push(' render,');
524
-
525
- if (routePath) {
526
- lines.push(` route: ${JSON.stringify(routePath)},`);
527
- }
528
-
529
- lines.push(' mount: (target) => {');
530
- lines.push(' const el = render();');
531
- lines.push(' return mount(target, el);');
532
- lines.push(' }');
533
- lines.push('};');
534
- lines.push('');
535
- lines.push(`export default ${pageName};`);
536
-
537
- return lines.join('\n');
538
- }
539
- }
540
-
541
- /**
542
- * Transform AST to JavaScript code
543
- */
544
- export function transform(ast, options = {}) {
545
- const transformer = new Transformer(ast, options);
546
- return transformer.transform();
547
- }
548
-
549
- export default {
550
- Transformer,
551
- transform
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
+ };