trickle-observe 0.2.85 → 0.2.87
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/dist/observe-register.js +127 -26
- package/dist/trace-var.js +5 -2
- package/dist/vite-plugin.js +5 -0
- package/dist-esm/vite-plugin.js +5 -0
- package/package.json +1 -1
- package/src/observe-register.ts +133 -29
- package/src/trace-var.ts +3 -1
- package/src/vite-plugin.ts +3 -0
package/dist/observe-register.js
CHANGED
|
@@ -70,25 +70,75 @@ function decodeVLQ(encoded) {
|
|
|
70
70
|
}
|
|
71
71
|
return values;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a source map source path to a real filesystem path.
|
|
75
|
+
* Handles webpack:// URLs, file:// URLs, and relative paths.
|
|
76
|
+
*/
|
|
77
|
+
function resolveSourcePath(source, mapDir, sourceRoot, jsFilePath) {
|
|
78
|
+
// Handle webpack:// URLs: webpack://package-name/./src/file.ts
|
|
79
|
+
if (source.startsWith('webpack://')) {
|
|
80
|
+
// Strip webpack://package-name/ prefix
|
|
81
|
+
const withoutProtocol = source.replace(/^webpack:\/\/[^/]*\//, '');
|
|
82
|
+
// Resolve relative to the project root (parent of dist/ or the JS file's directory)
|
|
83
|
+
const projectRoot = findProjectRoot(jsFilePath);
|
|
84
|
+
return path_1.default.resolve(projectRoot, withoutProtocol);
|
|
85
|
+
}
|
|
86
|
+
// Handle file:// URLs
|
|
87
|
+
if (source.startsWith('file://')) {
|
|
88
|
+
return source.replace(/^file:\/\//, '');
|
|
89
|
+
}
|
|
90
|
+
// Regular relative path
|
|
91
|
+
return path_1.default.resolve(mapDir, sourceRoot, source);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Find the project root by looking for package.json or tsconfig.json
|
|
95
|
+
* starting from the JS file's directory and walking up.
|
|
96
|
+
*/
|
|
97
|
+
function findProjectRoot(jsFilePath) {
|
|
98
|
+
let dir = path_1.default.dirname(jsFilePath);
|
|
99
|
+
for (let i = 0; i < 10; i++) {
|
|
100
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) || fs_1.default.existsSync(path_1.default.join(dir, 'tsconfig.json'))) {
|
|
101
|
+
return dir;
|
|
102
|
+
}
|
|
103
|
+
const parent = path_1.default.dirname(dir);
|
|
104
|
+
if (parent === dir)
|
|
105
|
+
break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return path_1.default.dirname(jsFilePath);
|
|
109
|
+
}
|
|
73
110
|
/**
|
|
74
111
|
* Parse a source map JSON and build a compiled→original line mapping.
|
|
75
|
-
*
|
|
112
|
+
* Supports both single-source (tsc) and multi-source (webpack/rollup) maps.
|
|
76
113
|
*/
|
|
77
|
-
function parseSourceMap(mapJson, mapFilePath) {
|
|
114
|
+
function parseSourceMap(mapJson, mapFilePath, jsFilePath) {
|
|
78
115
|
try {
|
|
79
116
|
const map = JSON.parse(mapJson);
|
|
80
117
|
if (!map.mappings || !map.sources || map.sources.length === 0)
|
|
81
118
|
return null;
|
|
82
|
-
// Resolve original source path relative to the .map file location
|
|
83
119
|
const mapDir = path_1.default.dirname(mapFilePath);
|
|
84
120
|
const sourceRoot = map.sourceRoot || '';
|
|
85
|
-
|
|
121
|
+
// Resolve all source paths, filtering out non-user sources (webpack internals)
|
|
122
|
+
const resolvedSources = map.sources.map((s) => resolveSourcePath(s, mapDir, sourceRoot, jsFilePath));
|
|
123
|
+
// Find the first user source file (skip webpack bootstrap/runtime)
|
|
124
|
+
let primarySourceIdx = 0;
|
|
125
|
+
for (let i = 0; i < resolvedSources.length; i++) {
|
|
126
|
+
const s = map.sources[i];
|
|
127
|
+
if (!s.includes('webpack/bootstrap') && !s.includes('webpack/runtime') && fs_1.default.existsSync(resolvedSources[i])) {
|
|
128
|
+
primarySourceIdx = i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const originalFile = resolvedSources[primarySourceIdx];
|
|
86
133
|
// Decode mappings: semicolons separate lines, commas separate segments
|
|
87
|
-
// VLQ values are cumulative across ALL segments (not just first per line)
|
|
88
|
-
//
|
|
134
|
+
// VLQ values are cumulative across ALL segments (not just first per line).
|
|
135
|
+
// For multi-source maps, also track the source index to map lines to different files.
|
|
89
136
|
const lineMap = new Map();
|
|
137
|
+
/** Map from generated line → resolved source file path */
|
|
138
|
+
const lineSourceMap = new Map();
|
|
90
139
|
const lines = map.mappings.split(';');
|
|
91
|
-
let sourceLine = 0;
|
|
140
|
+
let sourceLine = 0;
|
|
141
|
+
let sourceIdx = 0;
|
|
92
142
|
for (let genLine = 0; genLine < lines.length; genLine++) {
|
|
93
143
|
const line = lines[genLine];
|
|
94
144
|
if (!line)
|
|
@@ -99,18 +149,22 @@ function parseSourceMap(mapJson, mapFilePath) {
|
|
|
99
149
|
if (!seg)
|
|
100
150
|
continue;
|
|
101
151
|
const decoded = decodeVLQ(seg);
|
|
102
|
-
// decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
|
|
103
152
|
if (decoded.length >= 3) {
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
if (decoded.length >= 2)
|
|
154
|
+
sourceIdx += decoded[1]; // Update source index
|
|
155
|
+
sourceLine += decoded[2];
|
|
106
156
|
if (!firstSegmentMapped) {
|
|
107
|
-
lineMap.set(genLine + 1, sourceLine + 1);
|
|
157
|
+
lineMap.set(genLine + 1, sourceLine + 1);
|
|
158
|
+
// Track which source file this line belongs to
|
|
159
|
+
if (sourceIdx >= 0 && sourceIdx < resolvedSources.length) {
|
|
160
|
+
lineSourceMap.set(genLine + 1, resolvedSources[sourceIdx]);
|
|
161
|
+
}
|
|
108
162
|
firstSegmentMapped = true;
|
|
109
163
|
}
|
|
110
164
|
}
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
|
-
return { lineMap, originalFile, sources: map.sources };
|
|
167
|
+
return { lineMap, originalFile, sources: map.sources, lineSourceMap };
|
|
114
168
|
}
|
|
115
169
|
catch {
|
|
116
170
|
return null;
|
|
@@ -133,14 +187,14 @@ function loadSourceMap(jsFilePath, source) {
|
|
|
133
187
|
const mapPath = path_1.default.resolve(path_1.default.dirname(jsFilePath), url);
|
|
134
188
|
if (fs_1.default.existsSync(mapPath)) {
|
|
135
189
|
const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
|
|
136
|
-
return parseSourceMap(mapJson, mapPath);
|
|
190
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
137
191
|
}
|
|
138
192
|
}
|
|
139
193
|
// Fallback: check for .map file alongside .js
|
|
140
194
|
const mapPath = jsFilePath + '.map';
|
|
141
195
|
if (fs_1.default.existsSync(mapPath)) {
|
|
142
196
|
const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
|
|
143
|
-
return parseSourceMap(mapJson, mapPath);
|
|
197
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
144
198
|
}
|
|
145
199
|
return null;
|
|
146
200
|
}
|
|
@@ -322,6 +376,11 @@ function findVarDeclarations(source, lineOffset = 0) {
|
|
|
322
376
|
continue;
|
|
323
377
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
324
378
|
continue;
|
|
379
|
+
// Skip webpack internals
|
|
380
|
+
if (varName.startsWith('__webpack_'))
|
|
381
|
+
continue;
|
|
382
|
+
if (varName === '__unused_webpack_module')
|
|
383
|
+
continue;
|
|
325
384
|
// Skip React Refresh / HMR internals
|
|
326
385
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
327
386
|
continue;
|
|
@@ -331,6 +390,11 @@ function findVarDeclarations(source, lineOffset = 0) {
|
|
|
331
390
|
const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
|
|
332
391
|
if (/^\s*require\s*\(/.test(restOfLine))
|
|
333
392
|
continue;
|
|
393
|
+
// Skip variable declarations inside for-loop headers (for (let x = ...; ...; ...))
|
|
394
|
+
// The semicolon inside for(...) is NOT a statement end
|
|
395
|
+
const beforeDecl = source.slice(Math.max(0, vmatch.index - 50), vmatch.index);
|
|
396
|
+
if (/\bfor\s*\(\s*$/.test(beforeDecl))
|
|
397
|
+
continue;
|
|
334
398
|
// Calculate line number (count newlines before this position)
|
|
335
399
|
// Subtract lineOffset to map compiled line numbers back to original source lines
|
|
336
400
|
let lineNo = 1;
|
|
@@ -362,8 +426,17 @@ function findVarDeclarations(source, lineOffset = 0) {
|
|
|
362
426
|
else if (ch === '\n' && depth === 0) {
|
|
363
427
|
// For semicolon-free code, the newline is the end
|
|
364
428
|
// But only if the next non-whitespace isn't a continuation (., +, etc.)
|
|
429
|
+
// AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
|
|
365
430
|
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
366
431
|
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
432
|
+
// Also check if the line ends with an operator that expects a value on the next line
|
|
433
|
+
const prevOnLine = source.slice(source.lastIndexOf('\n', pos - 1) + 1, pos).trimEnd();
|
|
434
|
+
const lastChar = prevOnLine[prevOnLine.length - 1];
|
|
435
|
+
if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
|
|
436
|
+
// Line ends with operator — this is a continuation, don't end the statement
|
|
437
|
+
pos++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
367
440
|
foundEnd = pos;
|
|
368
441
|
break;
|
|
369
442
|
}
|
|
@@ -585,9 +658,14 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
585
658
|
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
586
659
|
const isStatic = !!methodMatch[1];
|
|
587
660
|
const methodName = methodMatch[2];
|
|
588
|
-
// Skip constructor and
|
|
661
|
+
// Skip constructor, private methods, and JS keywords that look like method calls
|
|
589
662
|
if (methodName === 'constructor' || methodName.startsWith('_'))
|
|
590
663
|
continue;
|
|
664
|
+
if (['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'return', 'throw',
|
|
665
|
+
'try', 'catch', 'finally', 'with', 'new', 'delete', 'typeof', 'void',
|
|
666
|
+
'yield', 'await', 'import', 'export', 'super', 'this', 'class',
|
|
667
|
+
'break', 'continue', 'debugger', 'in', 'of', 'instanceof'].includes(methodName))
|
|
668
|
+
continue;
|
|
591
669
|
// Extract param names
|
|
592
670
|
const mParamStr = methodMatch[3].trim();
|
|
593
671
|
const mParamNames = mParamStr
|
|
@@ -619,6 +697,14 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
619
697
|
const remapLine = sourceMap
|
|
620
698
|
? (line) => mapLineToOriginal(sourceMap, line)
|
|
621
699
|
: (line) => line;
|
|
700
|
+
// Helper: get source file for a compiled line (for multi-source bundles)
|
|
701
|
+
const hasMultiSource = sourceMap?.lineSourceMap && sourceMap.lineSourceMap.size > 0;
|
|
702
|
+
const getSourceFile = hasMultiSource
|
|
703
|
+
? (compiledLine) => {
|
|
704
|
+
const file = sourceMap.lineSourceMap.get(compiledLine);
|
|
705
|
+
return file && !file.includes('webpack/bootstrap') && !file.includes('webpack/runtime') ? file : undefined;
|
|
706
|
+
}
|
|
707
|
+
: (_) => undefined;
|
|
622
708
|
if (varTraceEnabled) {
|
|
623
709
|
const isTsFile = /\.[mc]?tsx?$/.test(filename);
|
|
624
710
|
if (isTsFile && !sourceMap) {
|
|
@@ -682,10 +768,12 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
682
768
|
// Plain JS or source-map-assisted: parse compiled source, then remap lines
|
|
683
769
|
varInsertions = findVarDeclarations(source).map(ins => ({
|
|
684
770
|
...ins,
|
|
771
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
685
772
|
lineNo: remapLine(ins.lineNo),
|
|
686
773
|
}));
|
|
687
774
|
destructInsertions = findDestructuredDeclarations(source).map(ins => ({
|
|
688
775
|
...ins,
|
|
776
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
689
777
|
lineNo: remapLine(ins.lineNo),
|
|
690
778
|
}));
|
|
691
779
|
}
|
|
@@ -696,14 +784,17 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
696
784
|
// Apply source map remapping to these too.
|
|
697
785
|
const reassignInsertions = (0, vite_plugin_1.findReassignments)(source).map(ins => ({
|
|
698
786
|
...ins,
|
|
787
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
699
788
|
lineNo: remapLine(ins.lineNo),
|
|
700
789
|
}));
|
|
701
790
|
const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source).map(ins => ({
|
|
702
791
|
...ins,
|
|
792
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
703
793
|
lineNo: remapLine(ins.lineNo),
|
|
704
794
|
}));
|
|
705
795
|
const catchInsertions = (0, vite_plugin_1.findCatchVars)(source).map(ins => ({
|
|
706
796
|
...ins,
|
|
797
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
707
798
|
lineNo: remapLine(ins.lineNo),
|
|
708
799
|
}));
|
|
709
800
|
if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0)
|
|
@@ -740,7 +831,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
740
831
|
const traceModuleName = sourceMap
|
|
741
832
|
? path_1.default.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
|
|
742
833
|
: moduleName;
|
|
743
|
-
prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(traceModuleName)}, ${JSON.stringify(traceFilePath)}); } catch(e){} };`);
|
|
834
|
+
prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`);
|
|
744
835
|
}
|
|
745
836
|
prefixLines.push('');
|
|
746
837
|
const prefix = prefixLines.join('\n');
|
|
@@ -752,37 +843,47 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
752
843
|
code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
|
|
753
844
|
});
|
|
754
845
|
}
|
|
755
|
-
|
|
846
|
+
// Helper to generate source file args for __trickle_tv calls (for multi-source bundles)
|
|
847
|
+
const sfArgs = (sourceFile) => {
|
|
848
|
+
if (!sourceFile)
|
|
849
|
+
return '';
|
|
850
|
+
const mod = path_1.default.basename(sourceFile).replace(/\.[jt]sx?$/, '');
|
|
851
|
+
return `,${JSON.stringify(mod)},${JSON.stringify(sourceFile)}`;
|
|
852
|
+
};
|
|
853
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of varInsertions) {
|
|
756
854
|
allInsertions.push({
|
|
757
855
|
position: lineEnd,
|
|
758
|
-
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
856
|
+
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
759
857
|
});
|
|
760
858
|
}
|
|
761
|
-
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
762
|
-
const
|
|
859
|
+
for (const { lineEnd, varNames, lineNo, sourceFile } of destructInsertions) {
|
|
860
|
+
const sf = sfArgs(sourceFile);
|
|
861
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
763
862
|
allInsertions.push({
|
|
764
863
|
position: lineEnd,
|
|
765
864
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
766
865
|
});
|
|
767
866
|
}
|
|
768
867
|
// Reassignment insertions
|
|
769
|
-
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
868
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of reassignInsertions) {
|
|
770
869
|
allInsertions.push({
|
|
771
870
|
position: lineEnd,
|
|
772
|
-
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
871
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
773
872
|
});
|
|
774
873
|
}
|
|
775
874
|
// For-loop variable insertions
|
|
776
|
-
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
777
|
-
const
|
|
875
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of forLoopInsertions) {
|
|
876
|
+
const sf = sfArgs(sourceFile);
|
|
877
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
778
878
|
allInsertions.push({
|
|
779
879
|
position: bodyStart,
|
|
780
880
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
781
881
|
});
|
|
782
882
|
}
|
|
783
883
|
// Catch clause insertions
|
|
784
|
-
for (const { bodyStart, varNames, lineNo } of catchInsertions) {
|
|
785
|
-
const
|
|
884
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of catchInsertions) {
|
|
885
|
+
const sf = sfArgs(sourceFile);
|
|
886
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
786
887
|
allInsertions.push({
|
|
787
888
|
position: bodyStart,
|
|
788
889
|
code: `\ntry{${calls}}catch(__e2){}\n`,
|
package/dist/trace-var.js
CHANGED
|
@@ -177,8 +177,11 @@ function flushVarBuffer() {
|
|
|
177
177
|
* More aggressive truncation than function samples since there are many more variables.
|
|
178
178
|
*/
|
|
179
179
|
function sanitizeVarSample(value, depth = 3) {
|
|
180
|
-
if (value === null
|
|
181
|
-
return
|
|
180
|
+
if (value === null)
|
|
181
|
+
return null;
|
|
182
|
+
// JSON.stringify drops undefined values, so use null to preserve the field
|
|
183
|
+
if (value === undefined)
|
|
184
|
+
return null;
|
|
182
185
|
const t = typeof value;
|
|
183
186
|
// Primitives are always safe to return at any depth
|
|
184
187
|
if (t === 'string') {
|
package/dist/vite-plugin.js
CHANGED
|
@@ -278,6 +278,11 @@ function findVarDeclarations(source) {
|
|
|
278
278
|
continue;
|
|
279
279
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
280
280
|
continue;
|
|
281
|
+
// Skip webpack internals
|
|
282
|
+
if (varName.startsWith('__webpack_'))
|
|
283
|
+
continue;
|
|
284
|
+
if (varName === '__unused_webpack_module')
|
|
285
|
+
continue;
|
|
281
286
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
282
287
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
283
288
|
continue;
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -265,6 +265,11 @@ function findVarDeclarations(source) {
|
|
|
265
265
|
continue;
|
|
266
266
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
267
267
|
continue;
|
|
268
|
+
// Skip webpack internals
|
|
269
|
+
if (varName.startsWith('__webpack_'))
|
|
270
|
+
continue;
|
|
271
|
+
if (varName === '__unused_webpack_module')
|
|
272
|
+
continue;
|
|
268
273
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
269
274
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
270
275
|
continue;
|
package/package.json
CHANGED
package/src/observe-register.ts
CHANGED
|
@@ -76,32 +76,87 @@ function decodeVLQ(encoded: string): number[] {
|
|
|
76
76
|
interface SourceMapData {
|
|
77
77
|
/** Map from compiled 1-based line → original 1-based line */
|
|
78
78
|
lineMap: Map<number, number>;
|
|
79
|
-
/** Original source file path (resolved to absolute) */
|
|
79
|
+
/** Original source file path (resolved to absolute) — primary/first user source */
|
|
80
80
|
originalFile: string;
|
|
81
81
|
/** All source files listed in the source map */
|
|
82
82
|
sources: string[];
|
|
83
|
+
/** Map from compiled line → resolved source file path (for multi-source bundles) */
|
|
84
|
+
lineSourceMap?: Map<number, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a source map source path to a real filesystem path.
|
|
89
|
+
* Handles webpack:// URLs, file:// URLs, and relative paths.
|
|
90
|
+
*/
|
|
91
|
+
function resolveSourcePath(source: string, mapDir: string, sourceRoot: string, jsFilePath: string): string {
|
|
92
|
+
// Handle webpack:// URLs: webpack://package-name/./src/file.ts
|
|
93
|
+
if (source.startsWith('webpack://')) {
|
|
94
|
+
// Strip webpack://package-name/ prefix
|
|
95
|
+
const withoutProtocol = source.replace(/^webpack:\/\/[^/]*\//, '');
|
|
96
|
+
// Resolve relative to the project root (parent of dist/ or the JS file's directory)
|
|
97
|
+
const projectRoot = findProjectRoot(jsFilePath);
|
|
98
|
+
return path.resolve(projectRoot, withoutProtocol);
|
|
99
|
+
}
|
|
100
|
+
// Handle file:// URLs
|
|
101
|
+
if (source.startsWith('file://')) {
|
|
102
|
+
return source.replace(/^file:\/\//, '');
|
|
103
|
+
}
|
|
104
|
+
// Regular relative path
|
|
105
|
+
return path.resolve(mapDir, sourceRoot, source);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find the project root by looking for package.json or tsconfig.json
|
|
110
|
+
* starting from the JS file's directory and walking up.
|
|
111
|
+
*/
|
|
112
|
+
function findProjectRoot(jsFilePath: string): string {
|
|
113
|
+
let dir = path.dirname(jsFilePath);
|
|
114
|
+
for (let i = 0; i < 10; i++) {
|
|
115
|
+
if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
118
|
+
const parent = path.dirname(dir);
|
|
119
|
+
if (parent === dir) break;
|
|
120
|
+
dir = parent;
|
|
121
|
+
}
|
|
122
|
+
return path.dirname(jsFilePath);
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
/**
|
|
86
126
|
* Parse a source map JSON and build a compiled→original line mapping.
|
|
87
|
-
*
|
|
127
|
+
* Supports both single-source (tsc) and multi-source (webpack/rollup) maps.
|
|
88
128
|
*/
|
|
89
|
-
function parseSourceMap(mapJson: string, mapFilePath: string): SourceMapData | null {
|
|
129
|
+
function parseSourceMap(mapJson: string, mapFilePath: string, jsFilePath: string): SourceMapData | null {
|
|
90
130
|
try {
|
|
91
131
|
const map = JSON.parse(mapJson);
|
|
92
132
|
if (!map.mappings || !map.sources || map.sources.length === 0) return null;
|
|
93
133
|
|
|
94
|
-
// Resolve original source path relative to the .map file location
|
|
95
134
|
const mapDir = path.dirname(mapFilePath);
|
|
96
135
|
const sourceRoot = map.sourceRoot || '';
|
|
97
|
-
|
|
136
|
+
|
|
137
|
+
// Resolve all source paths, filtering out non-user sources (webpack internals)
|
|
138
|
+
const resolvedSources = map.sources.map((s: string) => resolveSourcePath(s, mapDir, sourceRoot, jsFilePath));
|
|
139
|
+
|
|
140
|
+
// Find the first user source file (skip webpack bootstrap/runtime)
|
|
141
|
+
let primarySourceIdx = 0;
|
|
142
|
+
for (let i = 0; i < resolvedSources.length; i++) {
|
|
143
|
+
const s = map.sources[i] as string;
|
|
144
|
+
if (!s.includes('webpack/bootstrap') && !s.includes('webpack/runtime') && fs.existsSync(resolvedSources[i])) {
|
|
145
|
+
primarySourceIdx = i;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const originalFile = resolvedSources[primarySourceIdx];
|
|
98
150
|
|
|
99
151
|
// Decode mappings: semicolons separate lines, commas separate segments
|
|
100
|
-
// VLQ values are cumulative across ALL segments (not just first per line)
|
|
101
|
-
//
|
|
152
|
+
// VLQ values are cumulative across ALL segments (not just first per line).
|
|
153
|
+
// For multi-source maps, also track the source index to map lines to different files.
|
|
102
154
|
const lineMap = new Map<number, number>();
|
|
155
|
+
/** Map from generated line → resolved source file path */
|
|
156
|
+
const lineSourceMap = new Map<number, string>();
|
|
103
157
|
const lines = map.mappings.split(';');
|
|
104
|
-
let sourceLine = 0;
|
|
158
|
+
let sourceLine = 0;
|
|
159
|
+
let sourceIdx = 0;
|
|
105
160
|
|
|
106
161
|
for (let genLine = 0; genLine < lines.length; genLine++) {
|
|
107
162
|
const line = lines[genLine];
|
|
@@ -112,19 +167,22 @@ function parseSourceMap(mapJson: string, mapFilePath: string): SourceMapData | n
|
|
|
112
167
|
for (const seg of segments) {
|
|
113
168
|
if (!seg) continue;
|
|
114
169
|
const decoded = decodeVLQ(seg);
|
|
115
|
-
// decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
|
|
116
170
|
if (decoded.length >= 3) {
|
|
117
|
-
|
|
118
|
-
|
|
171
|
+
if (decoded.length >= 2) sourceIdx += decoded[1]; // Update source index
|
|
172
|
+
sourceLine += decoded[2];
|
|
119
173
|
if (!firstSegmentMapped) {
|
|
120
|
-
lineMap.set(genLine + 1, sourceLine + 1);
|
|
174
|
+
lineMap.set(genLine + 1, sourceLine + 1);
|
|
175
|
+
// Track which source file this line belongs to
|
|
176
|
+
if (sourceIdx >= 0 && sourceIdx < resolvedSources.length) {
|
|
177
|
+
lineSourceMap.set(genLine + 1, resolvedSources[sourceIdx]);
|
|
178
|
+
}
|
|
121
179
|
firstSegmentMapped = true;
|
|
122
180
|
}
|
|
123
181
|
}
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
184
|
|
|
127
|
-
return { lineMap, originalFile, sources: map.sources };
|
|
185
|
+
return { lineMap, originalFile, sources: map.sources, lineSourceMap };
|
|
128
186
|
} catch {
|
|
129
187
|
return null;
|
|
130
188
|
}
|
|
@@ -146,7 +204,7 @@ function loadSourceMap(jsFilePath: string, source: string): SourceMapData | null
|
|
|
146
204
|
const mapPath = path.resolve(path.dirname(jsFilePath), url);
|
|
147
205
|
if (fs.existsSync(mapPath)) {
|
|
148
206
|
const mapJson = fs.readFileSync(mapPath, 'utf8');
|
|
149
|
-
return parseSourceMap(mapJson, mapPath);
|
|
207
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
|
|
@@ -154,7 +212,7 @@ function loadSourceMap(jsFilePath: string, source: string): SourceMapData | null
|
|
|
154
212
|
const mapPath = jsFilePath + '.map';
|
|
155
213
|
if (fs.existsSync(mapPath)) {
|
|
156
214
|
const mapJson = fs.readFileSync(mapPath, 'utf8');
|
|
157
|
-
return parseSourceMap(mapJson, mapPath);
|
|
215
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
158
216
|
}
|
|
159
217
|
|
|
160
218
|
return null;
|
|
@@ -322,6 +380,9 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
|
|
|
322
380
|
// Skip esbuild helpers
|
|
323
381
|
if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
|
|
324
382
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
|
|
383
|
+
// Skip webpack internals
|
|
384
|
+
if (varName.startsWith('__webpack_')) continue;
|
|
385
|
+
if (varName === '__unused_webpack_module') continue;
|
|
325
386
|
// Skip React Refresh / HMR internals
|
|
326
387
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
|
|
327
388
|
if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
|
|
@@ -330,6 +391,11 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
|
|
|
330
391
|
const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
|
|
331
392
|
if (/^\s*require\s*\(/.test(restOfLine)) continue;
|
|
332
393
|
|
|
394
|
+
// Skip variable declarations inside for-loop headers (for (let x = ...; ...; ...))
|
|
395
|
+
// The semicolon inside for(...) is NOT a statement end
|
|
396
|
+
const beforeDecl = source.slice(Math.max(0, vmatch.index - 50), vmatch.index);
|
|
397
|
+
if (/\bfor\s*\(\s*$/.test(beforeDecl)) continue;
|
|
398
|
+
|
|
333
399
|
// Calculate line number (count newlines before this position)
|
|
334
400
|
// Subtract lineOffset to map compiled line numbers back to original source lines
|
|
335
401
|
let lineNo = 1;
|
|
@@ -358,8 +424,17 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
|
|
|
358
424
|
} else if (ch === '\n' && depth === 0) {
|
|
359
425
|
// For semicolon-free code, the newline is the end
|
|
360
426
|
// But only if the next non-whitespace isn't a continuation (., +, etc.)
|
|
427
|
+
// AND the previous non-whitespace isn't an operator expecting more (=, +, -, etc.)
|
|
361
428
|
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
362
429
|
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
430
|
+
// Also check if the line ends with an operator that expects a value on the next line
|
|
431
|
+
const prevOnLine = source.slice(source.lastIndexOf('\n', pos - 1) + 1, pos).trimEnd();
|
|
432
|
+
const lastChar = prevOnLine[prevOnLine.length - 1];
|
|
433
|
+
if (lastChar && '=+-*/%&|^~<>?:,({['.includes(lastChar)) {
|
|
434
|
+
// Line ends with operator — this is a continuation, don't end the statement
|
|
435
|
+
pos++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
363
438
|
foundEnd = pos;
|
|
364
439
|
break;
|
|
365
440
|
}
|
|
@@ -566,8 +641,12 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
566
641
|
while ((methodMatch = methodRegex.exec(classBody)) !== null) {
|
|
567
642
|
const isStatic = !!methodMatch[1];
|
|
568
643
|
const methodName = methodMatch[2];
|
|
569
|
-
// Skip constructor and
|
|
644
|
+
// Skip constructor, private methods, and JS keywords that look like method calls
|
|
570
645
|
if (methodName === 'constructor' || methodName.startsWith('_')) continue;
|
|
646
|
+
if (['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'return', 'throw',
|
|
647
|
+
'try', 'catch', 'finally', 'with', 'new', 'delete', 'typeof', 'void',
|
|
648
|
+
'yield', 'await', 'import', 'export', 'super', 'this', 'class',
|
|
649
|
+
'break', 'continue', 'debugger', 'in', 'of', 'instanceof'].includes(methodName)) continue;
|
|
571
650
|
// Extract param names
|
|
572
651
|
const mParamStr = methodMatch[3].trim();
|
|
573
652
|
const mParamNames = mParamStr
|
|
@@ -594,14 +673,23 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
594
673
|
// are stripped from the compiled JS, shifting line numbers. The only accurate way to get correct
|
|
595
674
|
// line numbers is to read the original .ts source file and parse it directly.
|
|
596
675
|
// For plain JS files, we parse the source directly.
|
|
597
|
-
let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
|
|
598
|
-
let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number }> = [];
|
|
676
|
+
let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number; sourceFile?: string }> = [];
|
|
677
|
+
let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number; sourceFile?: string }> = [];
|
|
599
678
|
|
|
600
679
|
// Helper: remap line numbers using source map if available
|
|
601
680
|
const remapLine = sourceMap
|
|
602
681
|
? (line: number) => mapLineToOriginal(sourceMap, line)
|
|
603
682
|
: (line: number) => line;
|
|
604
683
|
|
|
684
|
+
// Helper: get source file for a compiled line (for multi-source bundles)
|
|
685
|
+
const hasMultiSource = sourceMap?.lineSourceMap && sourceMap.lineSourceMap.size > 0;
|
|
686
|
+
const getSourceFile = hasMultiSource
|
|
687
|
+
? (compiledLine: number) => {
|
|
688
|
+
const file = sourceMap!.lineSourceMap!.get(compiledLine);
|
|
689
|
+
return file && !file.includes('webpack/bootstrap') && !file.includes('webpack/runtime') ? file : undefined;
|
|
690
|
+
}
|
|
691
|
+
: (_: number) => undefined as string | undefined;
|
|
692
|
+
|
|
605
693
|
if (varTraceEnabled) {
|
|
606
694
|
const isTsFile = /\.[mc]?tsx?$/.test(filename);
|
|
607
695
|
if (isTsFile && !sourceMap) {
|
|
@@ -663,10 +751,12 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
663
751
|
// Plain JS or source-map-assisted: parse compiled source, then remap lines
|
|
664
752
|
varInsertions = findVarDeclarations(source).map(ins => ({
|
|
665
753
|
...ins,
|
|
754
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
666
755
|
lineNo: remapLine(ins.lineNo),
|
|
667
756
|
}));
|
|
668
757
|
destructInsertions = findDestructuredDeclarations(source).map(ins => ({
|
|
669
758
|
...ins,
|
|
759
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
670
760
|
lineNo: remapLine(ins.lineNo),
|
|
671
761
|
}));
|
|
672
762
|
}
|
|
@@ -678,14 +768,17 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
678
768
|
// Apply source map remapping to these too.
|
|
679
769
|
const reassignInsertions = findReassignments(source).map(ins => ({
|
|
680
770
|
...ins,
|
|
771
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
681
772
|
lineNo: remapLine(ins.lineNo),
|
|
682
773
|
}));
|
|
683
774
|
const forLoopInsertions = findForLoopVars(source).map(ins => ({
|
|
684
775
|
...ins,
|
|
776
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
685
777
|
lineNo: remapLine(ins.lineNo),
|
|
686
778
|
}));
|
|
687
779
|
const catchInsertions = findCatchVars(source).map(ins => ({
|
|
688
780
|
...ins,
|
|
781
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
689
782
|
lineNo: remapLine(ins.lineNo),
|
|
690
783
|
}));
|
|
691
784
|
|
|
@@ -726,9 +819,10 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
726
819
|
const traceModuleName = sourceMap
|
|
727
820
|
? path.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
|
|
728
821
|
: moduleName;
|
|
822
|
+
|
|
729
823
|
prefixLines.push(
|
|
730
824
|
`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
|
|
731
|
-
`var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(traceModuleName)}, ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
|
|
825
|
+
`var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
|
|
732
826
|
);
|
|
733
827
|
}
|
|
734
828
|
|
|
@@ -747,15 +841,23 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
747
841
|
});
|
|
748
842
|
}
|
|
749
843
|
|
|
750
|
-
|
|
844
|
+
// Helper to generate source file args for __trickle_tv calls (for multi-source bundles)
|
|
845
|
+
const sfArgs = (sourceFile?: string) => {
|
|
846
|
+
if (!sourceFile) return '';
|
|
847
|
+
const mod = path.basename(sourceFile).replace(/\.[jt]sx?$/, '');
|
|
848
|
+
return `,${JSON.stringify(mod)},${JSON.stringify(sourceFile)}`;
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of varInsertions) {
|
|
751
852
|
allInsertions.push({
|
|
752
853
|
position: lineEnd,
|
|
753
|
-
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
854
|
+
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
754
855
|
});
|
|
755
856
|
}
|
|
756
857
|
|
|
757
|
-
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
758
|
-
const
|
|
858
|
+
for (const { lineEnd, varNames, lineNo, sourceFile } of destructInsertions) {
|
|
859
|
+
const sf = sfArgs(sourceFile);
|
|
860
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
759
861
|
allInsertions.push({
|
|
760
862
|
position: lineEnd,
|
|
761
863
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
@@ -763,16 +865,17 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
763
865
|
}
|
|
764
866
|
|
|
765
867
|
// Reassignment insertions
|
|
766
|
-
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
868
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of reassignInsertions) {
|
|
767
869
|
allInsertions.push({
|
|
768
870
|
position: lineEnd,
|
|
769
|
-
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
871
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
770
872
|
});
|
|
771
873
|
}
|
|
772
874
|
|
|
773
875
|
// For-loop variable insertions
|
|
774
|
-
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
775
|
-
const
|
|
876
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of forLoopInsertions) {
|
|
877
|
+
const sf = sfArgs(sourceFile);
|
|
878
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
776
879
|
allInsertions.push({
|
|
777
880
|
position: bodyStart,
|
|
778
881
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
@@ -780,8 +883,9 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
780
883
|
}
|
|
781
884
|
|
|
782
885
|
// Catch clause insertions
|
|
783
|
-
for (const { bodyStart, varNames, lineNo } of catchInsertions) {
|
|
784
|
-
const
|
|
886
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of catchInsertions) {
|
|
887
|
+
const sf = sfArgs(sourceFile);
|
|
888
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
785
889
|
allInsertions.push({
|
|
786
890
|
position: bodyStart,
|
|
787
891
|
code: `\ntry{${calls}}catch(__e2){}\n`,
|
package/src/trace-var.ts
CHANGED
|
@@ -167,7 +167,9 @@ function flushVarBuffer(): void {
|
|
|
167
167
|
* More aggressive truncation than function samples since there are many more variables.
|
|
168
168
|
*/
|
|
169
169
|
function sanitizeVarSample(value: unknown, depth: number = 3): unknown {
|
|
170
|
-
if (value === null
|
|
170
|
+
if (value === null) return null;
|
|
171
|
+
// JSON.stringify drops undefined values, so use null to preserve the field
|
|
172
|
+
if (value === undefined) return null;
|
|
171
173
|
|
|
172
174
|
const t = typeof value;
|
|
173
175
|
// Primitives are always safe to return at any depth
|
package/src/vite-plugin.ts
CHANGED
|
@@ -263,6 +263,9 @@ function findVarDeclarations(source: string): Array<{ lineEnd: number; varName:
|
|
|
263
263
|
// Skip esbuild helpers
|
|
264
264
|
if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
|
|
265
265
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
|
|
266
|
+
// Skip webpack internals
|
|
267
|
+
if (varName.startsWith('__webpack_')) continue;
|
|
268
|
+
if (varName === '__unused_webpack_module') continue;
|
|
266
269
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
267
270
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
|
|
268
271
|
if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
|