pulse-js-framework 1.4.8 → 1.4.10
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/README.md +48 -1
- package/cli/lint.js +117 -0
- package/compiler/index.js +33 -3
- package/compiler/sourcemap.js +360 -0
- package/compiler/transformer.js +108 -2
- package/package.json +5 -5
- package/runtime/router.js +374 -8
- package/types/hmr.d.ts +112 -0
- package/types/index.d.ts +25 -1
- package/types/router.d.ts +89 -0
- package/types/sourcemap.d.ts +126 -0
package/README.md
CHANGED
|
@@ -136,6 +136,7 @@ style {
|
|
|
136
136
|
- CSS scoping (styles are automatically scoped to component)
|
|
137
137
|
- Native `router {}` and `store {}` blocks
|
|
138
138
|
- Detailed error messages with line/column info
|
|
139
|
+
- Source map generation (v1.4.9) for debugging original `.pulse` code
|
|
139
140
|
|
|
140
141
|
### Router & Store DSL (v1.4.0)
|
|
141
142
|
|
|
@@ -266,7 +267,53 @@ router.start();
|
|
|
266
267
|
- Per-route guards (`beforeEnter`)
|
|
267
268
|
- Global guards (`beforeEach`, `beforeResolve`, `afterEach`)
|
|
268
269
|
- Scroll restoration
|
|
269
|
-
- Lazy-loaded routes (
|
|
270
|
+
- Lazy-loaded routes with `lazy()` and `preload()`
|
|
271
|
+
- Middleware pipeline (Koa-style)
|
|
272
|
+
|
|
273
|
+
#### Lazy Loading & Middleware (v1.4.9)
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
import { createRouter, lazy, preload } from 'pulse-js-framework/runtime/router';
|
|
277
|
+
|
|
278
|
+
// Lazy load components
|
|
279
|
+
const routes = {
|
|
280
|
+
'/': HomePage,
|
|
281
|
+
'/dashboard': lazy(() => import('./Dashboard.js')),
|
|
282
|
+
'/settings': lazy(() => import('./Settings.js'), {
|
|
283
|
+
loading: () => el('div.spinner', 'Loading...'),
|
|
284
|
+
error: (err) => el('div.error', `Failed: ${err.message}`),
|
|
285
|
+
timeout: 5000
|
|
286
|
+
})
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Preload on hover
|
|
290
|
+
link.addEventListener('mouseenter', () => preload(routes['/dashboard']));
|
|
291
|
+
|
|
292
|
+
// Middleware
|
|
293
|
+
const router = createRouter({
|
|
294
|
+
routes,
|
|
295
|
+
middleware: [
|
|
296
|
+
// Logger middleware
|
|
297
|
+
async (ctx, next) => {
|
|
298
|
+
console.log('Navigating to:', ctx.to.path);
|
|
299
|
+
await next();
|
|
300
|
+
},
|
|
301
|
+
// Auth middleware
|
|
302
|
+
async (ctx, next) => {
|
|
303
|
+
if (ctx.to.meta.requiresAuth && !isLoggedIn()) {
|
|
304
|
+
return ctx.redirect('/login');
|
|
305
|
+
}
|
|
306
|
+
await next();
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Add middleware dynamically
|
|
312
|
+
const unsubscribe = router.use(async (ctx, next) => {
|
|
313
|
+
ctx.meta.startTime = Date.now();
|
|
314
|
+
await next();
|
|
315
|
+
});
|
|
316
|
+
```
|
|
270
317
|
|
|
271
318
|
### Store
|
|
272
319
|
|
package/cli/lint.js
CHANGED
|
@@ -14,6 +14,9 @@ export const LintRules = {
|
|
|
14
14
|
'undefined-reference': { severity: 'error', fixable: false },
|
|
15
15
|
'duplicate-declaration': { severity: 'error', fixable: false },
|
|
16
16
|
|
|
17
|
+
// Security rules (warnings)
|
|
18
|
+
'xss-vulnerability': { severity: 'warning', fixable: false },
|
|
19
|
+
|
|
17
20
|
// Usage rules (warnings)
|
|
18
21
|
'unused-import': { severity: 'warning', fixable: true },
|
|
19
22
|
'unused-state': { severity: 'warning', fixable: false },
|
|
@@ -127,6 +130,9 @@ export class SemanticAnalyzer {
|
|
|
127
130
|
// Phase 4: Style checks
|
|
128
131
|
this.checkStyle();
|
|
129
132
|
|
|
133
|
+
// Phase 5: Security checks (XSS detection)
|
|
134
|
+
this.checkSecurity();
|
|
135
|
+
|
|
130
136
|
return this.diagnostics;
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -519,6 +525,117 @@ export class SemanticAnalyzer {
|
|
|
519
525
|
}
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Check for security vulnerabilities (XSS patterns)
|
|
530
|
+
*/
|
|
531
|
+
checkSecurity() {
|
|
532
|
+
// Check actions for dangerous DOM manipulation
|
|
533
|
+
if (this.ast.actions && this.ast.actions.functions) {
|
|
534
|
+
for (const fn of this.ast.actions.functions) {
|
|
535
|
+
if (fn.body) {
|
|
536
|
+
this.checkForXSS(fn.body, fn.name, fn.line, fn.column);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check view for dangerous patterns in directives
|
|
542
|
+
if (this.ast.view) {
|
|
543
|
+
this.checkViewForXSS(this.ast.view);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Check code string for XSS vulnerabilities
|
|
549
|
+
*/
|
|
550
|
+
checkForXSS(code, context, line, column) {
|
|
551
|
+
if (typeof code !== 'string') return;
|
|
552
|
+
|
|
553
|
+
// Dangerous DOM manipulation patterns
|
|
554
|
+
const xssPatterns = [
|
|
555
|
+
{
|
|
556
|
+
pattern: /\.innerHTML\s*=\s*(?!['"]\s*['"])/,
|
|
557
|
+
message: 'Assigning dynamic content to innerHTML can lead to XSS. Use textContent or sanitize input.'
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
pattern: /\.outerHTML\s*=\s*(?!['"]\s*['"])/,
|
|
561
|
+
message: 'Assigning dynamic content to outerHTML can lead to XSS. Consider safer alternatives.'
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
pattern: /document\.write\s*\(/,
|
|
565
|
+
message: 'document.write() can execute scripts and lead to XSS. Use DOM methods instead.'
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
pattern: /\.insertAdjacentHTML\s*\(/,
|
|
569
|
+
message: 'insertAdjacentHTML with unsanitized input can lead to XSS. Sanitize HTML or use DOM methods.'
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
pattern: /eval\s*\(/,
|
|
573
|
+
message: 'eval() executes arbitrary code and is a security risk. Avoid using eval().'
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
pattern: /new\s+Function\s*\(/,
|
|
577
|
+
message: 'new Function() can execute arbitrary code like eval(). Avoid dynamic function creation.'
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
pattern: /setTimeout\s*\(\s*['"`]/,
|
|
581
|
+
message: 'setTimeout with string argument executes code like eval(). Use a function instead.'
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
pattern: /setInterval\s*\(\s*['"`]/,
|
|
585
|
+
message: 'setInterval with string argument executes code like eval(). Use a function instead.'
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
for (const { pattern, message } of xssPatterns) {
|
|
590
|
+
if (pattern.test(code)) {
|
|
591
|
+
this.addDiagnostic('warning', 'xss-vulnerability',
|
|
592
|
+
`Potential XSS in action '${context}': ${message}`,
|
|
593
|
+
line || 1, column || 1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Check view nodes for XSS vulnerabilities
|
|
600
|
+
*/
|
|
601
|
+
checkViewForXSS(node) {
|
|
602
|
+
if (!node) return;
|
|
603
|
+
|
|
604
|
+
const children = node.children || [];
|
|
605
|
+
for (const child of children) {
|
|
606
|
+
if (!child) continue;
|
|
607
|
+
|
|
608
|
+
// Check for @html directive (if exists in DSL)
|
|
609
|
+
if (child.directives) {
|
|
610
|
+
for (const directive of child.directives) {
|
|
611
|
+
if (directive.name === 'html') {
|
|
612
|
+
this.addDiagnostic('warning', 'xss-vulnerability',
|
|
613
|
+
'@html directive renders raw HTML and can lead to XSS if used with user input. Ensure content is sanitized.',
|
|
614
|
+
directive.line || child.line || 1, directive.column || child.column || 1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check directive expressions for dangerous patterns
|
|
620
|
+
if (child.directives) {
|
|
621
|
+
for (const directive of child.directives) {
|
|
622
|
+
const expr = directive.handler || directive.expression || '';
|
|
623
|
+
if (typeof expr === 'string') {
|
|
624
|
+
// Check for innerHTML in expressions
|
|
625
|
+
if (/innerHTML|outerHTML/.test(expr)) {
|
|
626
|
+
this.addDiagnostic('warning', 'xss-vulnerability',
|
|
627
|
+
`Using innerHTML/outerHTML in directive expression can lead to XSS. Use safer alternatives.`,
|
|
628
|
+
directive.line || child.line || 1, directive.column || child.column || 1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Recurse into children
|
|
635
|
+
this.checkViewForXSS(child);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
522
639
|
/**
|
|
523
640
|
* Add a diagnostic message
|
|
524
641
|
*/
|
package/compiler/index.js
CHANGED
|
@@ -5,26 +5,51 @@
|
|
|
5
5
|
export * from './lexer.js';
|
|
6
6
|
export * from './parser.js';
|
|
7
7
|
export * from './transformer.js';
|
|
8
|
+
export * from './sourcemap.js';
|
|
8
9
|
|
|
9
10
|
import { tokenize } from './lexer.js';
|
|
10
11
|
import { parse } from './parser.js';
|
|
11
|
-
import { transform } from './transformer.js';
|
|
12
|
+
import { transform, Transformer } from './transformer.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Compile Pulse source code to JavaScript
|
|
16
|
+
*
|
|
17
|
+
* @param {string} source - Pulse source code
|
|
18
|
+
* @param {Object} options - Compiler options
|
|
19
|
+
* @param {string} options.runtime - Runtime import path
|
|
20
|
+
* @param {boolean} options.minify - Minify output
|
|
21
|
+
* @param {boolean} options.scopeStyles - Scope CSS with unique prefixes
|
|
22
|
+
* @param {boolean} options.sourceMap - Generate source map
|
|
23
|
+
* @param {string} options.sourceFileName - Original file name (for source maps)
|
|
24
|
+
* @param {boolean} options.inlineSourceMap - Include source map as inline comment
|
|
25
|
+
* @returns {Object} Compilation result
|
|
15
26
|
*/
|
|
16
27
|
export function compile(source, options = {}) {
|
|
17
28
|
try {
|
|
18
29
|
// Parse source to AST
|
|
19
30
|
const ast = parse(source);
|
|
20
31
|
|
|
32
|
+
// Prepare transformer options
|
|
33
|
+
const transformerOptions = {
|
|
34
|
+
...options,
|
|
35
|
+
sourceContent: options.sourceMap ? source : null
|
|
36
|
+
};
|
|
37
|
+
|
|
21
38
|
// Transform AST to JavaScript
|
|
22
|
-
const
|
|
39
|
+
const transformer = new Transformer(ast, transformerOptions);
|
|
40
|
+
const result = transformer.transformWithSourceMap();
|
|
41
|
+
|
|
42
|
+
// Add inline source map if requested
|
|
43
|
+
let code = result.code;
|
|
44
|
+
if (options.sourceMap && options.inlineSourceMap && result.sourceMapComment) {
|
|
45
|
+
code = code + '\n' + result.sourceMapComment;
|
|
46
|
+
}
|
|
23
47
|
|
|
24
48
|
return {
|
|
25
49
|
success: true,
|
|
26
50
|
code,
|
|
27
51
|
ast,
|
|
52
|
+
sourceMap: result.sourceMap,
|
|
28
53
|
errors: []
|
|
29
54
|
};
|
|
30
55
|
} catch (error) {
|
|
@@ -32,6 +57,7 @@ export function compile(source, options = {}) {
|
|
|
32
57
|
success: false,
|
|
33
58
|
code: null,
|
|
34
59
|
ast: null,
|
|
60
|
+
sourceMap: null,
|
|
35
61
|
errors: [{
|
|
36
62
|
message: error.message,
|
|
37
63
|
line: error.line,
|
|
@@ -55,11 +81,15 @@ export function tokenizeOnly(source) {
|
|
|
55
81
|
return tokenize(source);
|
|
56
82
|
}
|
|
57
83
|
|
|
84
|
+
import { SourceMapGenerator, SourceMapConsumer } from './sourcemap.js';
|
|
85
|
+
|
|
58
86
|
export default {
|
|
59
87
|
compile,
|
|
60
88
|
parseOnly,
|
|
61
89
|
tokenizeOnly,
|
|
62
90
|
tokenize,
|
|
63
91
|
parse,
|
|
64
|
-
transform
|
|
92
|
+
transform,
|
|
93
|
+
SourceMapGenerator,
|
|
94
|
+
SourceMapConsumer
|
|
65
95
|
};
|
|
@@ -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
|
+
};
|