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