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.
- package/cli/analyze.js +8 -7
- package/cli/build.js +14 -13
- package/cli/dev.js +28 -7
- package/cli/format.js +13 -12
- package/cli/index.js +20 -1
- package/cli/lint.js +8 -7
- package/cli/release.js +493 -0
- package/compiler/parser.js +41 -20
- package/compiler/transformer/constants.js +54 -0
- package/compiler/transformer/export.js +33 -0
- package/compiler/transformer/expressions.js +273 -0
- package/compiler/transformer/imports.js +101 -0
- package/compiler/transformer/index.js +319 -0
- package/compiler/transformer/router.js +95 -0
- package/compiler/transformer/state.js +118 -0
- package/compiler/transformer/store.js +97 -0
- package/compiler/transformer/style.js +130 -0
- package/compiler/transformer/view.js +428 -0
- package/compiler/transformer.js +17 -1310
- package/core/errors.js +300 -0
- package/package.json +7 -2
- package/runtime/dom.js +61 -10
- package/runtime/lru-cache.js +145 -0
- package/runtime/native.js +6 -1
- package/runtime/pulse.js +46 -2
- package/runtime/router.js +4 -1
- package/runtime/store.js +35 -1
- package/runtime/utils.js +348 -0
- package/types/index.d.ts +19 -0
- package/types/lru-cache.d.ts +118 -0
- package/types/utils.d.ts +255 -0
package/compiler/transformer.js
CHANGED
|
@@ -1,1316 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pulse Transformer - Code generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
};
|
|
21
|
+
// Re-export everything from the modular implementation
|
|
22
|
+
export { Transformer, transform } from './transformer/index.js';
|
|
23
|
+
export { default } from './transformer/index.js';
|