pulse-js-framework 1.0.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.
@@ -0,0 +1,552 @@
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
+ };
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pulse Framework
3
+ *
4
+ * A declarative DOM framework with CSS selector-based structure
5
+ */
6
+
7
+ // Runtime exports
8
+ export * from './runtime/index.js';
9
+
10
+ // Compiler exports
11
+ export { compile, parse, tokenize } from './compiler/index.js';
12
+
13
+ // Version
14
+ export const VERSION = '1.0.0';
15
+
16
+ // Default export
17
+ export default {
18
+ VERSION
19
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Pulse Vite Plugin
3
+ *
4
+ * Enables .pulse file support in Vite projects
5
+ */
6
+
7
+ import { compile } from '../compiler/index.js';
8
+
9
+ /**
10
+ * Create Pulse Vite plugin
11
+ */
12
+ export default function pulsePlugin(options = {}) {
13
+ const {
14
+ include = /\.pulse$/,
15
+ exclude = /node_modules/,
16
+ sourceMap = true
17
+ } = options;
18
+
19
+ return {
20
+ name: 'vite-plugin-pulse',
21
+
22
+ /**
23
+ * Resolve .pulse files
24
+ */
25
+ resolveId(id) {
26
+ if (id.endsWith('.pulse')) {
27
+ return id;
28
+ }
29
+ return null;
30
+ },
31
+
32
+ /**
33
+ * Transform .pulse files to JavaScript
34
+ */
35
+ transform(code, id) {
36
+ if (!id.endsWith('.pulse')) {
37
+ return null;
38
+ }
39
+
40
+ if (exclude && exclude.test(id)) {
41
+ return null;
42
+ }
43
+
44
+ try {
45
+ const result = compile(code, {
46
+ runtime: 'pulse-framework/runtime',
47
+ sourceMap
48
+ });
49
+
50
+ if (!result.success) {
51
+ const errors = result.errors.map(e =>
52
+ `${e.message}${e.line ? ` at line ${e.line}` : ''}`
53
+ ).join('\n');
54
+
55
+ this.error(`Pulse compilation failed:\n${errors}`);
56
+ return null;
57
+ }
58
+
59
+ return {
60
+ code: result.code,
61
+ map: result.map || null
62
+ };
63
+ } catch (error) {
64
+ this.error(`Pulse compilation error: ${error.message}`);
65
+ return null;
66
+ }
67
+ },
68
+
69
+ /**
70
+ * Handle hot module replacement
71
+ */
72
+ handleHotUpdate({ file, server }) {
73
+ if (file.endsWith('.pulse')) {
74
+ console.log(`[Pulse] HMR update: ${file}`);
75
+
76
+ // Invalidate the module
77
+ const module = server.moduleGraph.getModuleById(file);
78
+ if (module) {
79
+ server.moduleGraph.invalidateModule(module);
80
+ }
81
+
82
+ // Send full reload for now
83
+ // In a more advanced implementation, we could do partial updates
84
+ server.ws.send({
85
+ type: 'full-reload',
86
+ path: '*'
87
+ });
88
+ }
89
+ },
90
+
91
+ /**
92
+ * Configure dev server
93
+ */
94
+ configureServer(server) {
95
+ server.middlewares.use((req, res, next) => {
96
+ // Add any custom middleware here
97
+ next();
98
+ });
99
+ },
100
+
101
+ /**
102
+ * Build hooks
103
+ */
104
+ buildStart() {
105
+ console.log('[Pulse] Build started');
106
+ },
107
+
108
+ buildEnd() {
109
+ console.log('[Pulse] Build completed');
110
+ }
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Pulse HMR runtime
116
+ * Injected into the client for hot module replacement
117
+ */
118
+ export const hmrRuntime = `
119
+ if (import.meta.hot) {
120
+ import.meta.hot.accept((newModule) => {
121
+ if (newModule) {
122
+ // Re-render with new module
123
+ if (newModule.default && newModule.default.mount) {
124
+ // Find and replace the current view
125
+ const app = document.querySelector('#app');
126
+ if (app) {
127
+ app.innerHTML = '';
128
+ newModule.default.mount(app);
129
+ }
130
+ }
131
+ }
132
+ });
133
+ }
134
+ `;
135
+
136
+ /**
137
+ * Additional utilities for Vite integration
138
+ */
139
+ export const utils = {
140
+ /**
141
+ * Check if a file is a Pulse file
142
+ */
143
+ isPulseFile(id) {
144
+ return id.endsWith('.pulse');
145
+ },
146
+
147
+ /**
148
+ * Get the output filename for a Pulse file
149
+ */
150
+ getOutputFilename(id) {
151
+ return id.replace(/\.pulse$/, '.js');
152
+ },
153
+
154
+ /**
155
+ * Create a virtual module ID
156
+ */
157
+ createVirtualId(id) {
158
+ return `\0${id}`;
159
+ }
160
+ };