pulse-js-framework 1.4.10 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Transformer Export Module
3
+ * Handles component export generation
4
+ * @module pulse-js-framework/compiler/transformer/export
5
+ */
6
+
7
+ /**
8
+ * Generate component export
9
+ * @param {Object} transformer - Transformer instance
10
+ * @returns {string} JavaScript code
11
+ */
12
+ export function generateExport(transformer) {
13
+ const pageName = transformer.ast.page?.name || 'Component';
14
+ const routePath = transformer.ast.route?.path || null;
15
+
16
+ const lines = ['// Export'];
17
+ lines.push(`export const ${pageName} = {`);
18
+ lines.push(' render,');
19
+
20
+ if (routePath) {
21
+ lines.push(` route: ${JSON.stringify(routePath)},`);
22
+ }
23
+
24
+ lines.push(' mount: (target) => {');
25
+ lines.push(' const el = render();');
26
+ lines.push(' return mount(target, el);');
27
+ lines.push(' }');
28
+ lines.push('};');
29
+ lines.push('');
30
+ lines.push(`export default ${pageName};`);
31
+
32
+ return lines.join('\n');
33
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Transformer Expressions Module
3
+ * Handles AST expression transformation to JavaScript code
4
+ * @module pulse-js-framework/compiler/transformer/expressions
5
+ */
6
+
7
+ import { NodeType } from '../parser.js';
8
+ import {
9
+ NO_SPACE_AFTER,
10
+ NO_SPACE_BEFORE,
11
+ STATEMENT_KEYWORDS,
12
+ BUILTIN_FUNCTIONS,
13
+ STATEMENT_TOKEN_TYPES,
14
+ STATEMENT_END_TYPES
15
+ } from './constants.js';
16
+
17
+ /**
18
+ * Transform AST expression to JS code
19
+ * @param {Object} transformer - Transformer instance
20
+ * @param {Object} node - AST node to transform
21
+ * @returns {string} JavaScript code
22
+ */
23
+ export function transformExpression(transformer, node) {
24
+ if (!node) return '';
25
+
26
+ switch (node.type) {
27
+ case NodeType.Identifier:
28
+ if (transformer.stateVars.has(node.name)) {
29
+ return `${node.name}.get()`;
30
+ }
31
+ // Props are accessed directly (already destructured)
32
+ return node.name;
33
+
34
+ case NodeType.Literal:
35
+ if (typeof node.value === 'string') {
36
+ return JSON.stringify(node.value);
37
+ }
38
+ return String(node.value);
39
+
40
+ case NodeType.TemplateLiteral:
41
+ // Transform state vars in template literal
42
+ return '`' + transformExpressionString(transformer, node.value) + '`';
43
+
44
+ case NodeType.MemberExpression: {
45
+ const obj = transformExpression(transformer, node.object);
46
+ // Use optional chaining when accessing properties on function call results
47
+ const isCallResult = node.object.type === NodeType.CallExpression;
48
+ const accessor = isCallResult ? '?.' : '.';
49
+ if (node.computed) {
50
+ const prop = transformExpression(transformer, node.property);
51
+ return isCallResult ? `${obj}?.[${prop}]` : `${obj}[${prop}]`;
52
+ }
53
+ return `${obj}${accessor}${node.property}`;
54
+ }
55
+
56
+ case NodeType.CallExpression: {
57
+ const callee = transformExpression(transformer, node.callee);
58
+ const args = node.arguments.map(a => transformExpression(transformer, a)).join(', ');
59
+ return `${callee}(${args})`;
60
+ }
61
+
62
+ case NodeType.BinaryExpression: {
63
+ const left = transformExpression(transformer, node.left);
64
+ const right = transformExpression(transformer, node.right);
65
+ return `(${left} ${node.operator} ${right})`;
66
+ }
67
+
68
+ case NodeType.UnaryExpression: {
69
+ const argument = transformExpression(transformer, node.argument);
70
+ return `${node.operator}${argument}`;
71
+ }
72
+
73
+ case NodeType.UpdateExpression: {
74
+ const argument = transformExpression(transformer, node.argument);
75
+ // For state variables, convert x++ to x.set(x.get() + 1)
76
+ if (node.argument.type === NodeType.Identifier &&
77
+ transformer.stateVars.has(node.argument.name)) {
78
+ const name = node.argument.name;
79
+ const delta = node.operator === '++' ? 1 : -1;
80
+ return `${name}.set(${name}.get() + ${delta})`;
81
+ }
82
+ return node.prefix
83
+ ? `${node.operator}${argument}`
84
+ : `${argument}${node.operator}`;
85
+ }
86
+
87
+ case NodeType.ConditionalExpression: {
88
+ const test = transformExpression(transformer, node.test);
89
+ const consequent = transformExpression(transformer, node.consequent);
90
+ const alternate = transformExpression(transformer, node.alternate);
91
+ return `(${test} ? ${consequent} : ${alternate})`;
92
+ }
93
+
94
+ case NodeType.ArrowFunction: {
95
+ const params = node.params.join(', ');
96
+ if (node.block) {
97
+ // Block body - transform tokens
98
+ const body = transformFunctionBody(transformer, node.body);
99
+ return `(${params}) => { ${body} }`;
100
+ } else {
101
+ // Expression body
102
+ const body = transformExpression(transformer, node.body);
103
+ return `(${params}) => ${body}`;
104
+ }
105
+ }
106
+
107
+ case NodeType.AssignmentExpression: {
108
+ const left = transformExpression(transformer, node.left);
109
+ const right = transformExpression(transformer, node.right);
110
+ // For state variables, convert to .set()
111
+ if (node.left.type === NodeType.Identifier &&
112
+ transformer.stateVars.has(node.left.name)) {
113
+ return `${node.left.name}.set(${right})`;
114
+ }
115
+ return `(${left} = ${right})`;
116
+ }
117
+
118
+ case NodeType.ArrayLiteral: {
119
+ const elements = node.elements.map(e => transformExpression(transformer, e)).join(', ');
120
+ return `[${elements}]`;
121
+ }
122
+
123
+ case NodeType.ObjectLiteral: {
124
+ const props = node.properties.map(p => {
125
+ if (p.type === NodeType.SpreadElement) {
126
+ return `...${transformExpression(transformer, p.argument)}`;
127
+ }
128
+ if (p.shorthand) {
129
+ // Check if it's a state var
130
+ if (transformer.stateVars.has(p.name)) {
131
+ return `${p.name}: ${p.name}.get()`;
132
+ }
133
+ return p.name;
134
+ }
135
+ return `${p.name}: ${transformExpression(transformer, p.value)}`;
136
+ }).join(', ');
137
+ return `{ ${props} }`;
138
+ }
139
+
140
+ case NodeType.SpreadElement:
141
+ return `...${transformExpression(transformer, node.argument)}`;
142
+
143
+ default:
144
+ return '/* unknown expression */';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Transform expression string (from interpolation)
150
+ * @param {Object} transformer - Transformer instance
151
+ * @param {string} exprStr - Expression string
152
+ * @returns {string} Transformed expression string
153
+ */
154
+ export function transformExpressionString(transformer, exprStr) {
155
+ // Simple transformation: wrap state vars with .get()
156
+ let result = exprStr;
157
+ for (const stateVar of transformer.stateVars) {
158
+ result = result.replace(
159
+ new RegExp(`\\b${stateVar}\\b`, 'g'),
160
+ `${stateVar}.get()`
161
+ );
162
+ }
163
+ // Add optional chaining after function calls followed by property access
164
+ result = result.replace(/(\w+\([^)]*\))\.(\w)/g, '$1?.$2');
165
+ return result;
166
+ }
167
+
168
+ /**
169
+ * Transform function body tokens back to code
170
+ * @param {Object} transformer - Transformer instance
171
+ * @param {Array} tokens - Body tokens
172
+ * @returns {string} JavaScript code
173
+ */
174
+ export function transformFunctionBody(transformer, tokens) {
175
+ const { stateVars, actionNames } = transformer;
176
+ let code = '';
177
+ let lastToken = null;
178
+ let lastNonSpaceToken = null;
179
+
180
+ const needsManualSemicolon = (token, nextToken, lastNonSpace) => {
181
+ if (!token || lastNonSpace?.value === 'new') return false;
182
+ if (STATEMENT_TOKEN_TYPES.has(token.type)) return true;
183
+ if (token.type !== 'IDENT') return false;
184
+ if (STATEMENT_KEYWORDS.has(token.value)) return true;
185
+ if (stateVars.has(token.value) && nextToken?.type === 'EQ') return true;
186
+ if (nextToken?.type === 'LPAREN' &&
187
+ (BUILTIN_FUNCTIONS.has(token.value) || actionNames.has(token.value))) return true;
188
+ if (nextToken?.type === 'DOT' && BUILTIN_FUNCTIONS.has(token.value)) return true;
189
+ return false;
190
+ };
191
+
192
+ const afterStatementEnd = (t) => t && STATEMENT_END_TYPES.has(t.type);
193
+
194
+ let afterIfCondition = false;
195
+
196
+ for (let i = 0; i < tokens.length; i++) {
197
+ const token = tokens[i];
198
+ const nextToken = tokens[i + 1];
199
+
200
+ // Detect if we just closed an if/for/while condition
201
+ if (token.type === 'RPAREN') {
202
+ let parenDepth = 1;
203
+ for (let j = i - 1; j >= 0 && parenDepth > 0; j--) {
204
+ if (tokens[j].type === 'RPAREN') parenDepth++;
205
+ else if (tokens[j].type === 'LPAREN') parenDepth--;
206
+ if (parenDepth === 0) {
207
+ if (j > 0 && (tokens[j - 1].type === 'IF' || tokens[j - 1].type === 'FOR' ||
208
+ tokens[j - 1].type === 'EACH' || tokens[j - 1].value === 'while')) {
209
+ afterIfCondition = true;
210
+ }
211
+ break;
212
+ }
213
+ }
214
+ }
215
+
216
+ // Add semicolon before statement starters
217
+ if (needsManualSemicolon(token, nextToken, lastNonSpaceToken) &&
218
+ afterStatementEnd(lastNonSpaceToken)) {
219
+ if (!afterIfCondition && lastToken && lastToken.value !== ';' && lastToken.value !== '{') {
220
+ code += '; ';
221
+ }
222
+ }
223
+
224
+ // Reset afterIfCondition after processing the token following the condition
225
+ if (afterIfCondition && token.type !== 'RPAREN') {
226
+ afterIfCondition = false;
227
+ }
228
+
229
+ // Emit the token value
230
+ if (token.type === 'STRING') {
231
+ code += token.raw || JSON.stringify(token.value);
232
+ } else if (token.type === 'TEMPLATE') {
233
+ code += token.raw || ('`' + token.value + '`');
234
+ } else {
235
+ code += token.value;
236
+ }
237
+
238
+ // Decide whether to add space after this token
239
+ const noSpaceAfter = NO_SPACE_AFTER.has(token.type) || NO_SPACE_AFTER.has(token.value);
240
+ const noSpaceBefore = nextToken &&
241
+ (NO_SPACE_BEFORE.has(nextToken.type) || NO_SPACE_BEFORE.has(nextToken.value));
242
+ if (!noSpaceAfter && !noSpaceBefore && nextToken) code += ' ';
243
+
244
+ lastToken = token;
245
+ lastNonSpaceToken = token;
246
+ }
247
+
248
+ // Build patterns for state variable transformation
249
+ const stateVarPattern = [...stateVars].join('|');
250
+ const funcPattern = [...actionNames, ...BUILTIN_FUNCTIONS].join('|');
251
+ const keywordsPattern = [...STATEMENT_KEYWORDS].join('|');
252
+
253
+ // Transform state var assignments: stateVar = value -> stateVar.set(value)
254
+ for (const stateVar of stateVars) {
255
+ const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${keywordsPattern})\\b|;|$`;
256
+ const assignPattern = new RegExp(`\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`, 'g');
257
+ code = code.replace(assignPattern, (_, value) => `${stateVar}.set(${value.trim()});`);
258
+ }
259
+
260
+ // Clean up any double semicolons
261
+ code = code.replace(/;+/g, ';');
262
+ code = code.replace(/; ;/g, ';');
263
+
264
+ // Replace state var reads
265
+ for (const stateVar of stateVars) {
266
+ code = code.replace(
267
+ new RegExp(`(?<!\\.\\s*)\\b${stateVar}\\b(?!\\s*=(?!=)|\\s*\\(|\\s*\\.(?:get|set))`, 'g'),
268
+ `${stateVar}.get()`
269
+ );
270
+ }
271
+
272
+ return code.trim();
273
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Transformer Imports Module
3
+ * Handles import generation for compiled Pulse components
4
+ * @module pulse-js-framework/compiler/transformer/imports
5
+ */
6
+
7
+ /**
8
+ * Extract imported component names from AST imports
9
+ * @param {Object} transformer - Transformer instance
10
+ * @param {Array} imports - Array of import statements from AST
11
+ */
12
+ export function extractImportedComponents(transformer, imports) {
13
+ for (const imp of imports) {
14
+ for (const spec of imp.specifiers) {
15
+ transformer.importedComponents.set(spec.local, {
16
+ source: imp.source,
17
+ type: spec.type,
18
+ imported: spec.imported
19
+ });
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Generate import statements (runtime + user imports)
26
+ * @param {Object} transformer - Transformer instance
27
+ * @returns {string} Generated import statements
28
+ */
29
+ export function generateImports(transformer) {
30
+ const lines = [];
31
+ const { ast, options } = transformer;
32
+
33
+ // Runtime imports
34
+ const runtimeImports = [
35
+ 'pulse',
36
+ 'computed',
37
+ 'effect',
38
+ 'batch',
39
+ 'el',
40
+ 'text',
41
+ 'on',
42
+ 'list',
43
+ 'when',
44
+ 'mount',
45
+ 'model'
46
+ ];
47
+
48
+ lines.push(`import { ${runtimeImports.join(', ')} } from '${options.runtime}';`);
49
+
50
+ // Router imports (if router block exists)
51
+ if (ast.router) {
52
+ lines.push(`import { createRouter } from '${options.runtime}/router';`);
53
+ }
54
+
55
+ // Store imports (if store block exists)
56
+ if (ast.store) {
57
+ const storeImports = ['createStore', 'createActions', 'createGetters'];
58
+ lines.push(`import { ${storeImports.join(', ')} } from '${options.runtime}/store';`);
59
+ }
60
+
61
+ // User imports from .pulse files
62
+ if (ast.imports && ast.imports.length > 0) {
63
+ lines.push('');
64
+ lines.push('// Component imports');
65
+
66
+ for (const imp of ast.imports) {
67
+ // Handle default + named imports
68
+ const defaultSpec = imp.specifiers.find(s => s.type === 'default');
69
+ const namedSpecs = imp.specifiers.filter(s => s.type === 'named');
70
+ const namespaceSpec = imp.specifiers.find(s => s.type === 'namespace');
71
+
72
+ let importStr = 'import ';
73
+ if (defaultSpec) {
74
+ importStr += defaultSpec.local;
75
+ if (namedSpecs.length > 0) {
76
+ importStr += ', ';
77
+ }
78
+ }
79
+ if (namespaceSpec) {
80
+ importStr += `* as ${namespaceSpec.local}`;
81
+ }
82
+ if (namedSpecs.length > 0) {
83
+ const named = namedSpecs.map(s =>
84
+ s.local !== s.imported ? `${s.imported} as ${s.local}` : s.local
85
+ );
86
+ importStr += `{ ${named.join(', ')} }`;
87
+ }
88
+
89
+ // Convert .pulse extension to .js
90
+ let source = imp.source;
91
+ if (source.endsWith('.pulse')) {
92
+ source = source.replace('.pulse', '.js');
93
+ }
94
+
95
+ importStr += ` from '${source}';`;
96
+ lines.push(importStr);
97
+ }
98
+ }
99
+
100
+ return lines.join('\n');
101
+ }