pulse-js-framework 1.4.7 → 1.4.9

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,360 @@
1
+ /**
2
+ * Pulse Source Map Generator
3
+ *
4
+ * Generates V3 source maps for .pulse files compiled to JavaScript.
5
+ * Enables debugging of original .pulse code in browser devtools.
6
+ *
7
+ * @module pulse-js-framework/compiler/sourcemap
8
+ */
9
+
10
+ /**
11
+ * Base64 VLQ encoding for source map mappings
12
+ * VLQ (Variable Length Quantity) is used to encode position data compactly
13
+ */
14
+ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
15
+
16
+ /**
17
+ * Encode a number as VLQ base64
18
+ * @param {number} value - Number to encode (can be negative)
19
+ * @returns {string} VLQ encoded string
20
+ */
21
+ export function encodeVLQ(value) {
22
+ let encoded = '';
23
+ // Convert to unsigned and add sign bit
24
+ let vlq = value < 0 ? ((-value) << 1) + 1 : (value << 1);
25
+
26
+ do {
27
+ let digit = vlq & 0x1F; // 5 bits
28
+ vlq >>>= 5;
29
+ if (vlq > 0) {
30
+ digit |= 0x20; // Set continuation bit
31
+ }
32
+ encoded += BASE64_CHARS[digit];
33
+ } while (vlq > 0);
34
+
35
+ return encoded;
36
+ }
37
+
38
+ /**
39
+ * Source Map Generator class
40
+ * Tracks mappings between generated and original source positions
41
+ */
42
+ export class SourceMapGenerator {
43
+ /**
44
+ * @param {Object} options
45
+ * @param {string} options.file - Generated file name
46
+ * @param {string} options.sourceRoot - Root URL for source files
47
+ */
48
+ constructor(options = {}) {
49
+ this.file = options.file || '';
50
+ this.sourceRoot = options.sourceRoot || '';
51
+ this.sources = [];
52
+ this.sourcesContent = [];
53
+ this.names = [];
54
+ this.mappings = [];
55
+
56
+ // Current state for relative encoding
57
+ this._lastGeneratedLine = 0;
58
+ this._lastGeneratedColumn = 0;
59
+ this._lastSourceIndex = 0;
60
+ this._lastSourceLine = 0;
61
+ this._lastSourceColumn = 0;
62
+ this._lastNameIndex = 0;
63
+ }
64
+
65
+ /**
66
+ * Add a source file
67
+ * @param {string} source - Source file path
68
+ * @param {string} content - Source file content (optional)
69
+ * @returns {number} Source index
70
+ */
71
+ addSource(source, content = null) {
72
+ let index = this.sources.indexOf(source);
73
+ if (index === -1) {
74
+ index = this.sources.length;
75
+ this.sources.push(source);
76
+ this.sourcesContent.push(content);
77
+ }
78
+ return index;
79
+ }
80
+
81
+ /**
82
+ * Add a name (identifier) to the names array
83
+ * @param {string} name - Identifier name
84
+ * @returns {number} Name index
85
+ */
86
+ addName(name) {
87
+ let index = this.names.indexOf(name);
88
+ if (index === -1) {
89
+ index = this.names.length;
90
+ this.names.push(name);
91
+ }
92
+ return index;
93
+ }
94
+
95
+ /**
96
+ * Add a mapping between generated and original positions
97
+ * @param {Object} mapping
98
+ * @param {Object} mapping.generated - Generated position {line, column}
99
+ * @param {Object} mapping.original - Original position {line, column} (optional)
100
+ * @param {string} mapping.source - Source file path (required if original provided)
101
+ * @param {string} mapping.name - Original identifier name (optional)
102
+ */
103
+ addMapping(mapping) {
104
+ const { generated, original, source, name } = mapping;
105
+
106
+ // Ensure mappings array has enough lines
107
+ while (this.mappings.length <= generated.line) {
108
+ this.mappings.push([]);
109
+ }
110
+
111
+ const segment = {
112
+ generatedColumn: generated.column,
113
+ sourceIndex: source ? this.addSource(source) : null,
114
+ originalLine: original?.line ?? null,
115
+ originalColumn: original?.column ?? null,
116
+ nameIndex: name ? this.addName(name) : null
117
+ };
118
+
119
+ this.mappings[generated.line].push(segment);
120
+ }
121
+
122
+ /**
123
+ * Encode all mappings as VLQ string
124
+ * @returns {string} Encoded mappings
125
+ */
126
+ _encodeMappings() {
127
+ let previousGeneratedColumn = 0;
128
+ let previousSourceIndex = 0;
129
+ let previousSourceLine = 0;
130
+ let previousSourceColumn = 0;
131
+ let previousNameIndex = 0;
132
+
133
+ const lines = [];
134
+
135
+ for (const line of this.mappings) {
136
+ if (line.length === 0) {
137
+ lines.push('');
138
+ continue;
139
+ }
140
+
141
+ // Sort segments by generated column
142
+ line.sort((a, b) => a.generatedColumn - b.generatedColumn);
143
+
144
+ const segments = [];
145
+ previousGeneratedColumn = 0; // Reset column for each line
146
+
147
+ for (const segment of line) {
148
+ let encoded = '';
149
+
150
+ // 1. Generated column (relative to previous segment in same line)
151
+ encoded += encodeVLQ(segment.generatedColumn - previousGeneratedColumn);
152
+ previousGeneratedColumn = segment.generatedColumn;
153
+
154
+ // If we have source info
155
+ if (segment.sourceIndex !== null) {
156
+ // 2. Source file index (relative)
157
+ encoded += encodeVLQ(segment.sourceIndex - previousSourceIndex);
158
+ previousSourceIndex = segment.sourceIndex;
159
+
160
+ // 3. Original line (relative, 0-based in source maps)
161
+ encoded += encodeVLQ(segment.originalLine - previousSourceLine);
162
+ previousSourceLine = segment.originalLine;
163
+
164
+ // 4. Original column (relative)
165
+ encoded += encodeVLQ(segment.originalColumn - previousSourceColumn);
166
+ previousSourceColumn = segment.originalColumn;
167
+
168
+ // 5. Optional: name index (relative)
169
+ if (segment.nameIndex !== null) {
170
+ encoded += encodeVLQ(segment.nameIndex - previousNameIndex);
171
+ previousNameIndex = segment.nameIndex;
172
+ }
173
+ }
174
+
175
+ segments.push(encoded);
176
+ }
177
+
178
+ lines.push(segments.join(','));
179
+ }
180
+
181
+ return lines.join(';');
182
+ }
183
+
184
+ /**
185
+ * Generate the source map object
186
+ * @returns {Object} Source map object (V3 format)
187
+ */
188
+ toJSON() {
189
+ return {
190
+ version: 3,
191
+ file: this.file,
192
+ sourceRoot: this.sourceRoot,
193
+ sources: this.sources,
194
+ sourcesContent: this.sourcesContent,
195
+ names: this.names,
196
+ mappings: this._encodeMappings()
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Generate source map as JSON string
202
+ * @returns {string} JSON string
203
+ */
204
+ toString() {
205
+ return JSON.stringify(this.toJSON());
206
+ }
207
+
208
+ /**
209
+ * Generate inline source map comment
210
+ * @returns {string} Comment with base64 encoded source map
211
+ */
212
+ toComment() {
213
+ const base64 = typeof btoa === 'function'
214
+ ? btoa(this.toString())
215
+ : Buffer.from(this.toString()).toString('base64');
216
+ return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64}`;
217
+ }
218
+
219
+ /**
220
+ * Generate external source map URL comment
221
+ * @param {string} url - URL to source map file
222
+ * @returns {string} Comment with source map URL
223
+ */
224
+ static toURLComment(url) {
225
+ return `//# sourceMappingURL=${url}`;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Source Map Consumer - parse and query source maps
231
+ * Useful for error stack trace translation
232
+ */
233
+ export class SourceMapConsumer {
234
+ /**
235
+ * @param {Object|string} sourceMap - Source map object or JSON string
236
+ */
237
+ constructor(sourceMap) {
238
+ this.map = typeof sourceMap === 'string' ? JSON.parse(sourceMap) : sourceMap;
239
+ this._decodedMappings = null;
240
+ }
241
+
242
+ /**
243
+ * Decode VLQ string to numbers
244
+ * @param {string} str - VLQ encoded string
245
+ * @returns {number[]} Decoded numbers
246
+ */
247
+ static decodeVLQ(str) {
248
+ const values = [];
249
+ let value = 0;
250
+ let shift = 0;
251
+
252
+ for (const char of str) {
253
+ const digit = BASE64_CHARS.indexOf(char);
254
+ if (digit === -1) continue;
255
+
256
+ value += (digit & 0x1F) << shift;
257
+
258
+ if (digit & 0x20) {
259
+ shift += 5;
260
+ } else {
261
+ // Check sign bit
262
+ const negative = value & 1;
263
+ value >>= 1;
264
+ values.push(negative ? -value : value);
265
+ value = 0;
266
+ shift = 0;
267
+ }
268
+ }
269
+
270
+ return values;
271
+ }
272
+
273
+ /**
274
+ * Get original position for a generated position
275
+ * @param {Object} position - Generated position {line, column}
276
+ * @returns {Object|null} Original position or null
277
+ */
278
+ originalPositionFor(position) {
279
+ this._ensureDecoded();
280
+
281
+ const { line, column } = position;
282
+ const lineData = this._decodedMappings[line];
283
+
284
+ if (!lineData) return null;
285
+
286
+ // Find the closest mapping at or before this column
287
+ let closest = null;
288
+ for (const mapping of lineData) {
289
+ if (mapping.generatedColumn <= column) {
290
+ closest = mapping;
291
+ } else {
292
+ break;
293
+ }
294
+ }
295
+
296
+ if (!closest || closest.sourceIndex === null) return null;
297
+
298
+ return {
299
+ source: this.map.sources[closest.sourceIndex],
300
+ line: closest.originalLine,
301
+ column: closest.originalColumn,
302
+ name: closest.nameIndex !== null ? this.map.names[closest.nameIndex] : null
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Decode mappings lazily
308
+ */
309
+ _ensureDecoded() {
310
+ if (this._decodedMappings) return;
311
+
312
+ this._decodedMappings = [];
313
+ const lines = this.map.mappings.split(';');
314
+
315
+ let sourceIndex = 0;
316
+ let sourceLine = 0;
317
+ let sourceColumn = 0;
318
+ let nameIndex = 0;
319
+
320
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
321
+ const line = lines[lineIndex];
322
+ const segments = line.split(',').filter(s => s);
323
+ const lineData = [];
324
+ let generatedColumn = 0;
325
+
326
+ for (const segment of segments) {
327
+ const values = SourceMapConsumer.decodeVLQ(segment);
328
+
329
+ generatedColumn += values[0];
330
+
331
+ const mapping = { generatedColumn };
332
+
333
+ if (values.length >= 4) {
334
+ sourceIndex += values[1];
335
+ sourceLine += values[2];
336
+ sourceColumn += values[3];
337
+
338
+ mapping.sourceIndex = sourceIndex;
339
+ mapping.originalLine = sourceLine;
340
+ mapping.originalColumn = sourceColumn;
341
+ }
342
+
343
+ if (values.length >= 5) {
344
+ nameIndex += values[4];
345
+ mapping.nameIndex = nameIndex;
346
+ }
347
+
348
+ lineData.push(mapping);
349
+ }
350
+
351
+ this._decodedMappings.push(lineData);
352
+ }
353
+ }
354
+ }
355
+
356
+ export default {
357
+ SourceMapGenerator,
358
+ SourceMapConsumer,
359
+ encodeVLQ
360
+ };
@@ -7,9 +7,11 @@
7
7
  * - Import statement support
8
8
  * - Slot-based component composition
9
9
  * - CSS scoping with unique class prefixes
10
+ * - Source map generation
10
11
  */
11
12
 
12
13
  import { NodeType } from './parser.js';
14
+ import { SourceMapGenerator } from './sourcemap.js';
13
15
 
14
16
  /** Generate a unique scope ID for CSS scoping */
15
17
  const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 8);
@@ -29,13 +31,92 @@ const STATEMENT_TOKEN_TYPES = new Set(['IF', 'FOR', 'EACH']);
29
31
  export class Transformer {
30
32
  constructor(ast, options = {}) {
31
33
  this.ast = ast;
32
- this.options = { runtime: 'pulse-js-framework/runtime', minify: false, scopeStyles: true, ...options };
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
+ };
33
43
  this.stateVars = new Set();
34
44
  this.propVars = new Set();
35
45
  this.propDefaults = new Map();
36
46
  this.actionNames = new Set();
37
47
  this.importedComponents = new Map();
38
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);
39
120
  }
40
121
 
41
122
  /**
@@ -100,7 +181,32 @@ export class Transformer {
100
181
  // Component export
101
182
  parts.push(this.generateExport());
102
183
 
103
- return parts.filter(Boolean).join('\n\n');
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
+ };
104
210
  }
105
211
 
106
212
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.4.7",
3
+ "version": "1.4.9",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -42,6 +42,7 @@
42
42
  "default": "./runtime/logger.js"
43
43
  },
44
44
  "./runtime/hmr": {
45
+ "types": "./types/hmr.d.ts",
45
46
  "default": "./runtime/hmr.js"
46
47
  },
47
48
  "./compiler": {
@@ -70,8 +71,9 @@
70
71
  "LICENSE"
71
72
  ],
72
73
  "scripts": {
73
- "test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze",
74
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze",
74
75
  "test:compiler": "node test/compiler.test.js",
76
+ "test:sourcemap": "node test/sourcemap.test.js",
75
77
  "test:pulse": "node test/pulse.test.js",
76
78
  "test:dom": "node test/dom.test.js",
77
79
  "test:router": "node test/router.test.js",