pulse-js-framework 1.4.10 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Pulse Transformer - Code generator
3
+ *
4
+ * Transforms AST into JavaScript code
5
+ *
6
+ * Features:
7
+ * - Import statement support
8
+ * - Slot-based component composition
9
+ * - CSS scoping with unique class prefixes
10
+ * - Source map generation
11
+ *
12
+ * @module pulse-js-framework/compiler/transformer
13
+ */
14
+
15
+ import { SourceMapGenerator } from '../sourcemap.js';
16
+ import { generateScopeId } from './constants.js';
17
+ import { extractImportedComponents, generateImports } from './imports.js';
18
+ import {
19
+ extractPropVars,
20
+ extractStateVars,
21
+ extractActionNames,
22
+ transformState,
23
+ transformActions,
24
+ transformValue
25
+ } from './state.js';
26
+ import { transformRouter } from './router.js';
27
+ import { transformStore } from './store.js';
28
+ import { transformExpression, transformExpressionString, transformFunctionBody } from './expressions.js';
29
+ import { transformView, transformViewNode, VIEW_NODE_HANDLERS, addScopeToSelector } from './view.js';
30
+ import { transformStyle, transformStyleRule, scopeStyleSelector } from './style.js';
31
+ import { generateExport } from './export.js';
32
+
33
+ /**
34
+ * Transformer class
35
+ */
36
+ export class Transformer {
37
+ constructor(ast, options = {}) {
38
+ this.ast = ast;
39
+ // Default to source maps enabled in development mode
40
+ const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
41
+ this.options = {
42
+ runtime: 'pulse-js-framework/runtime',
43
+ minify: false,
44
+ scopeStyles: true,
45
+ sourceMap: isDev, // Enable source map generation (default: true in dev)
46
+ sourceFileName: null, // Original .pulse file name
47
+ sourceContent: null, // Original source content (for inline source maps)
48
+ ...options
49
+ };
50
+ this.stateVars = new Set();
51
+ this.propVars = new Set();
52
+ this.propDefaults = new Map();
53
+ this.actionNames = new Set();
54
+ this.importedComponents = new Map();
55
+ this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
56
+
57
+ // Source map tracking
58
+ this.sourceMap = null;
59
+ this._currentLine = 0;
60
+ this._currentColumn = 0;
61
+
62
+ // Initialize source map generator if enabled
63
+ if (this.options.sourceMap) {
64
+ this.sourceMap = new SourceMapGenerator({
65
+ file: this.options.sourceFileName?.replace('.pulse', '.js') || 'output.js'
66
+ });
67
+ if (this.options.sourceFileName) {
68
+ this.sourceMap.addSource(
69
+ this.options.sourceFileName,
70
+ this.options.sourceContent
71
+ );
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Add a mapping to the source map
78
+ * @param {Object} original - Original position {line, column} (1-based)
79
+ * @param {string} name - Optional identifier name
80
+ */
81
+ _addMapping(original, name = null) {
82
+ if (!this.sourceMap || !original) return;
83
+
84
+ this.sourceMap.addMapping({
85
+ generated: {
86
+ line: this._currentLine,
87
+ column: this._currentColumn
88
+ },
89
+ original: {
90
+ line: original.line - 1, // Convert to 0-based
91
+ column: original.column - 1
92
+ },
93
+ source: this.options.sourceFileName,
94
+ name
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Track output position when writing code
100
+ * @param {string} code - Generated code
101
+ * @returns {string} The same code (for chaining)
102
+ */
103
+ _trackCode(code) {
104
+ for (const char of code) {
105
+ if (char === '\n') {
106
+ this._currentLine++;
107
+ this._currentColumn = 0;
108
+ } else {
109
+ this._currentColumn++;
110
+ }
111
+ }
112
+ return code;
113
+ }
114
+
115
+ /**
116
+ * Write code with optional source mapping
117
+ * @param {string} code - Code to write
118
+ * @param {Object} original - Original position {line, column}
119
+ * @param {string} name - Optional identifier name
120
+ * @returns {string} The code
121
+ */
122
+ _emit(code, original = null, name = null) {
123
+ if (original) {
124
+ this._addMapping(original, name);
125
+ }
126
+ return this._trackCode(code);
127
+ }
128
+
129
+ /**
130
+ * Transform AST to JavaScript code
131
+ */
132
+ transform() {
133
+ const parts = [];
134
+
135
+ // Extract imported components first
136
+ if (this.ast.imports) {
137
+ extractImportedComponents(this, this.ast.imports);
138
+ }
139
+
140
+ // Imports (runtime + user imports)
141
+ parts.push(generateImports(this));
142
+
143
+ // Extract prop variables
144
+ if (this.ast.props) {
145
+ extractPropVars(this, this.ast.props);
146
+ }
147
+
148
+ // Extract state variables
149
+ if (this.ast.state) {
150
+ extractStateVars(this, this.ast.state);
151
+ }
152
+
153
+ // Extract action names
154
+ if (this.ast.actions) {
155
+ extractActionNames(this, this.ast.actions);
156
+ }
157
+
158
+ // Store (must come before router so $store is available to guards)
159
+ if (this.ast.store) {
160
+ parts.push(transformStore(this, this.ast.store, transformValue));
161
+ }
162
+
163
+ // Router (after store so guards can access $store)
164
+ if (this.ast.router) {
165
+ parts.push(transformRouter(this, this.ast.router));
166
+ }
167
+
168
+ // State
169
+ if (this.ast.state) {
170
+ parts.push(transformState(this, this.ast.state));
171
+ }
172
+
173
+ // Actions
174
+ if (this.ast.actions) {
175
+ parts.push(transformActions(this, this.ast.actions, transformFunctionBody));
176
+ }
177
+
178
+ // View
179
+ if (this.ast.view) {
180
+ parts.push(transformView(this, this.ast.view));
181
+ }
182
+
183
+ // Style
184
+ if (this.ast.style) {
185
+ parts.push(transformStyle(this, this.ast.style));
186
+ }
187
+
188
+ // Component export
189
+ parts.push(generateExport(this));
190
+
191
+ const code = parts.filter(Boolean).join('\n\n');
192
+
193
+ // Track the generated code for source map positions
194
+ if (this.sourceMap) {
195
+ this._trackCode(code);
196
+ }
197
+
198
+ return code;
199
+ }
200
+
201
+ /**
202
+ * Transform AST and return result with optional source map
203
+ * @returns {Object} Result with code and optional sourceMap
204
+ */
205
+ transformWithSourceMap() {
206
+ const code = this.transform();
207
+
208
+ if (!this.sourceMap) {
209
+ return { code, sourceMap: null };
210
+ }
211
+
212
+ return {
213
+ code,
214
+ sourceMap: this.sourceMap.toJSON(),
215
+ sourceMapComment: this.sourceMap.toComment()
216
+ };
217
+ }
218
+
219
+ // =============================================================================
220
+ // Instance methods that delegate to module functions
221
+ // These are kept for backward compatibility
222
+ // =============================================================================
223
+
224
+ extractImportedComponents(imports) {
225
+ return extractImportedComponents(this, imports);
226
+ }
227
+
228
+ generateImports() {
229
+ return generateImports(this);
230
+ }
231
+
232
+ extractPropVars(propsBlock) {
233
+ return extractPropVars(this, propsBlock);
234
+ }
235
+
236
+ extractStateVars(stateBlock) {
237
+ return extractStateVars(this, stateBlock);
238
+ }
239
+
240
+ extractActionNames(actionsBlock) {
241
+ return extractActionNames(this, actionsBlock);
242
+ }
243
+
244
+ transformState(stateBlock) {
245
+ return transformState(this, stateBlock);
246
+ }
247
+
248
+ transformRouter(routerBlock) {
249
+ return transformRouter(this, routerBlock);
250
+ }
251
+
252
+ transformStore(storeBlock) {
253
+ return transformStore(this, storeBlock, transformValue);
254
+ }
255
+
256
+ transformValue(node) {
257
+ return transformValue(this, node);
258
+ }
259
+
260
+ transformActions(actionsBlock) {
261
+ return transformActions(this, actionsBlock, transformFunctionBody);
262
+ }
263
+
264
+ transformFunctionBody(tokens) {
265
+ return transformFunctionBody(this, tokens);
266
+ }
267
+
268
+ transformView(viewBlock) {
269
+ return transformView(this, viewBlock);
270
+ }
271
+
272
+ transformViewNode(node, indent = 0) {
273
+ return transformViewNode(this, node, indent);
274
+ }
275
+
276
+ transformExpression(node) {
277
+ return transformExpression(this, node);
278
+ }
279
+
280
+ transformExpressionString(exprStr) {
281
+ return transformExpressionString(this, exprStr);
282
+ }
283
+
284
+ transformStyle(styleBlock) {
285
+ return transformStyle(this, styleBlock);
286
+ }
287
+
288
+ transformStyleRule(rule, indent) {
289
+ return transformStyleRule(this, rule, indent);
290
+ }
291
+
292
+ scopeStyleSelector(selector) {
293
+ return scopeStyleSelector(this, selector);
294
+ }
295
+
296
+ addScopeToSelector(selector) {
297
+ return addScopeToSelector(this, selector);
298
+ }
299
+
300
+ generateExport() {
301
+ return generateExport(this);
302
+ }
303
+ }
304
+
305
+ // Static property for VIEW_NODE_HANDLERS (backward compatibility)
306
+ Transformer.VIEW_NODE_HANDLERS = VIEW_NODE_HANDLERS;
307
+
308
+ /**
309
+ * Transform AST to JavaScript code
310
+ */
311
+ export function transform(ast, options = {}) {
312
+ const transformer = new Transformer(ast, options);
313
+ return transformer.transform();
314
+ }
315
+
316
+ export default {
317
+ Transformer,
318
+ transform
319
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Transformer Router Module
3
+ * Handles router block transformation
4
+ * @module pulse-js-framework/compiler/transformer/router
5
+ */
6
+
7
+ import { PUNCT_NO_SPACE_BEFORE, PUNCT_NO_SPACE_AFTER } from './constants.js';
8
+
9
+ /**
10
+ * Helper to emit token value with proper string/template handling
11
+ * @param {Object} token - Token to emit
12
+ * @returns {string} Token value
13
+ */
14
+ export function emitToken(token) {
15
+ if (token.type === 'STRING') return token.raw || JSON.stringify(token.value);
16
+ if (token.type === 'TEMPLATE') return token.raw || ('`' + token.value + '`');
17
+ return token.value;
18
+ }
19
+
20
+ /**
21
+ * Helper to check if space needed between tokens
22
+ * @param {Object} token - Current token
23
+ * @param {Object} nextToken - Next token
24
+ * @returns {boolean} Whether space is needed
25
+ */
26
+ export function needsSpace(token, nextToken) {
27
+ if (!nextToken) return false;
28
+ return !PUNCT_NO_SPACE_BEFORE.includes(nextToken.type) &&
29
+ !PUNCT_NO_SPACE_AFTER.includes(token.type);
30
+ }
31
+
32
+ /**
33
+ * Transform router guard body - handles store references
34
+ * @param {Array} tokens - Body tokens
35
+ * @returns {string} JavaScript code
36
+ */
37
+ export function transformRouterGuardBody(tokens) {
38
+ let code = '';
39
+ for (let i = 0; i < tokens.length; i++) {
40
+ const token = tokens[i];
41
+ if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
42
+ code += '$store';
43
+ } else {
44
+ code += emitToken(token);
45
+ }
46
+ if (needsSpace(token, tokens[i + 1])) code += ' ';
47
+ }
48
+ return code.trim();
49
+ }
50
+
51
+ /**
52
+ * Transform router block to createRouter() call
53
+ * @param {Object} transformer - Transformer instance
54
+ * @param {Object} routerBlock - Router block from AST
55
+ * @returns {string} JavaScript code
56
+ */
57
+ export function transformRouter(transformer, routerBlock) {
58
+ const lines = ['// Router'];
59
+
60
+ // Build routes object
61
+ const routesCode = [];
62
+ for (const route of routerBlock.routes) {
63
+ routesCode.push(` '${route.path}': ${route.handler}`);
64
+ }
65
+
66
+ lines.push('const router = createRouter({');
67
+ lines.push(` mode: '${routerBlock.mode}',`);
68
+ if (routerBlock.base) {
69
+ lines.push(` base: '${routerBlock.base}',`);
70
+ }
71
+ lines.push(' routes: {');
72
+ lines.push(routesCode.join(',\n'));
73
+ lines.push(' }');
74
+ lines.push('});');
75
+ lines.push('');
76
+
77
+ // Add global guards
78
+ if (routerBlock.beforeEach) {
79
+ const params = routerBlock.beforeEach.params.join(', ');
80
+ const body = transformRouterGuardBody(routerBlock.beforeEach.body);
81
+ lines.push(`router.beforeEach((${params}) => { ${body} });`);
82
+ }
83
+
84
+ if (routerBlock.afterEach) {
85
+ const params = routerBlock.afterEach.params.join(', ');
86
+ const body = transformRouterGuardBody(routerBlock.afterEach.body);
87
+ lines.push(`router.afterEach((${params}) => { ${body} });`);
88
+ }
89
+
90
+ lines.push('');
91
+ lines.push('// Start router');
92
+ lines.push('router.start();');
93
+
94
+ return lines.join('\n');
95
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Transformer State Module
3
+ * Handles state, props, and actions extraction and transformation
4
+ * @module pulse-js-framework/compiler/transformer/state
5
+ */
6
+
7
+ import { NodeType } from '../parser.js';
8
+
9
+ /**
10
+ * Extract prop variable names and defaults
11
+ * @param {Object} transformer - Transformer instance
12
+ * @param {Object} propsBlock - Props block from AST
13
+ */
14
+ export function extractPropVars(transformer, propsBlock) {
15
+ for (const prop of propsBlock.properties) {
16
+ transformer.propVars.add(prop.name);
17
+ transformer.propDefaults.set(prop.name, prop.value);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Extract state variable names
23
+ * @param {Object} transformer - Transformer instance
24
+ * @param {Object} stateBlock - State block from AST
25
+ */
26
+ export function extractStateVars(transformer, stateBlock) {
27
+ for (const prop of stateBlock.properties) {
28
+ transformer.stateVars.add(prop.name);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Extract action names
34
+ * @param {Object} transformer - Transformer instance
35
+ * @param {Object} actionsBlock - Actions block from AST
36
+ */
37
+ export function extractActionNames(transformer, actionsBlock) {
38
+ for (const fn of actionsBlock.functions) {
39
+ transformer.actionNames.add(fn.name);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Transform a value node to JavaScript code
45
+ * @param {Object} transformer - Transformer instance
46
+ * @param {Object} node - AST node to transform
47
+ * @returns {string} JavaScript code
48
+ */
49
+ export function transformValue(transformer, node) {
50
+ if (!node) return 'undefined';
51
+
52
+ switch (node.type) {
53
+ case NodeType.Literal:
54
+ if (typeof node.value === 'string') {
55
+ return JSON.stringify(node.value);
56
+ }
57
+ return String(node.value);
58
+
59
+ case NodeType.ObjectLiteral: {
60
+ const props = node.properties.map(p =>
61
+ `${p.name}: ${transformValue(transformer, p.value)}`
62
+ );
63
+ return `{ ${props.join(', ')} }`;
64
+ }
65
+
66
+ case NodeType.ArrayLiteral: {
67
+ const elements = node.elements.map(e => transformValue(transformer, e));
68
+ return `[${elements.join(', ')}]`;
69
+ }
70
+
71
+ case NodeType.Identifier:
72
+ return node.name;
73
+
74
+ default:
75
+ return 'undefined';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Transform state block to pulse declarations
81
+ * @param {Object} transformer - Transformer instance
82
+ * @param {Object} stateBlock - State block from AST
83
+ * @returns {string} JavaScript code
84
+ */
85
+ export function transformState(transformer, stateBlock) {
86
+ const lines = ['// State'];
87
+
88
+ for (const prop of stateBlock.properties) {
89
+ const value = transformValue(transformer, prop.value);
90
+ lines.push(`const ${prop.name} = pulse(${value});`);
91
+ }
92
+
93
+ return lines.join('\n');
94
+ }
95
+
96
+ /**
97
+ * Transform actions block to function declarations
98
+ * @param {Object} transformer - Transformer instance
99
+ * @param {Object} actionsBlock - Actions block from AST
100
+ * @param {Function} transformFunctionBody - Function to transform body tokens
101
+ * @returns {string} JavaScript code
102
+ */
103
+ export function transformActions(transformer, actionsBlock, transformFunctionBody) {
104
+ const lines = ['// Actions'];
105
+
106
+ for (const fn of actionsBlock.functions) {
107
+ const asyncKeyword = fn.async ? 'async ' : '';
108
+ const params = fn.params.join(', ');
109
+ const body = transformFunctionBody(transformer, fn.body);
110
+
111
+ lines.push(`${asyncKeyword}function ${fn.name}(${params}) {`);
112
+ lines.push(` ${body}`);
113
+ lines.push('}');
114
+ lines.push('');
115
+ }
116
+
117
+ return lines.join('\n');
118
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Transformer Store Module
3
+ * Handles store block transformation
4
+ * @module pulse-js-framework/compiler/transformer/store
5
+ */
6
+
7
+ import { emitToken, needsSpace } from './router.js';
8
+
9
+ /**
10
+ * Transform store action body (this.x = y -> store.x.set(y))
11
+ * @param {Array} tokens - Body tokens
12
+ * @returns {string} JavaScript code
13
+ */
14
+ export function transformStoreActionBody(tokens) {
15
+ let code = '';
16
+ for (let i = 0; i < tokens.length; i++) {
17
+ const token = tokens[i];
18
+ if (token.value === 'this') code += 'store';
19
+ else if (token.type === 'COLON') code += ' : ';
20
+ else code += emitToken(token);
21
+ if (needsSpace(token, tokens[i + 1])) code += ' ';
22
+ }
23
+ return code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)').trim();
24
+ }
25
+
26
+ /**
27
+ * Transform store getter body (this.x -> store.x.get())
28
+ * @param {Array} tokens - Body tokens
29
+ * @returns {string} JavaScript code
30
+ */
31
+ export function transformStoreGetterBody(tokens) {
32
+ return transformStoreActionBody(tokens)
33
+ .replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
34
+ }
35
+
36
+ /**
37
+ * Transform store block to createStore(), createActions(), createGetters() calls
38
+ * @param {Object} transformer - Transformer instance
39
+ * @param {Object} storeBlock - Store block from AST
40
+ * @param {Function} transformValue - Function to transform values
41
+ * @returns {string} JavaScript code
42
+ */
43
+ export function transformStore(transformer, storeBlock, transformValue) {
44
+ const lines = ['// Store'];
45
+
46
+ // Transform state
47
+ if (storeBlock.state) {
48
+ const stateProps = storeBlock.state.properties.map(p =>
49
+ ` ${p.name}: ${transformValue(transformer, p.value)}`
50
+ ).join(',\n');
51
+
52
+ lines.push('const store = createStore({');
53
+ lines.push(stateProps);
54
+ lines.push('}, {');
55
+ lines.push(` persist: ${storeBlock.persist},`);
56
+ lines.push(` storageKey: '${storeBlock.storageKey}'`);
57
+ lines.push('});');
58
+ lines.push('');
59
+ }
60
+
61
+ // Transform actions
62
+ if (storeBlock.actions) {
63
+ lines.push('const storeActions = createActions(store, {');
64
+ for (const fn of storeBlock.actions.functions) {
65
+ const params = fn.params.length > 0 ? ', ' + fn.params.join(', ') : '';
66
+ const body = transformStoreActionBody(fn.body);
67
+ lines.push(` ${fn.name}: (store${params}) => { ${body} },`);
68
+ }
69
+ lines.push('});');
70
+ lines.push('');
71
+ }
72
+
73
+ // Transform getters
74
+ if (storeBlock.getters) {
75
+ lines.push('const storeGetters = createGetters(store, {');
76
+ for (const getter of storeBlock.getters.getters) {
77
+ const body = transformStoreGetterBody(getter.body);
78
+ lines.push(` ${getter.name}: (store) => { ${body} },`);
79
+ }
80
+ lines.push('});');
81
+ lines.push('');
82
+ }
83
+
84
+ // Create combined $store object for easy access
85
+ lines.push('// Combined store with actions and getters');
86
+ lines.push('const $store = {');
87
+ lines.push(' ...store,');
88
+ if (storeBlock.actions) {
89
+ lines.push(' ...storeActions,');
90
+ }
91
+ if (storeBlock.getters) {
92
+ lines.push(' ...storeGetters,');
93
+ }
94
+ lines.push('};');
95
+
96
+ return lines.join('\n');
97
+ }