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.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/cli/build.js +199 -0
- package/cli/dev.js +225 -0
- package/cli/index.js +324 -0
- package/compiler/index.js +65 -0
- package/compiler/lexer.js +581 -0
- package/compiler/parser.js +900 -0
- package/compiler/transformer.js +552 -0
- package/index.js +19 -0
- package/loader/vite-plugin.js +160 -0
- package/package.json +58 -0
- package/runtime/dom.js +484 -0
- package/runtime/index.js +13 -0
- package/runtime/pulse.js +339 -0
- package/runtime/router.js +392 -0
- package/runtime/store.js +301 -0
|
@@ -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
|
+
};
|