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.
@@ -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
- * Only handles single-source maps (most common for tsc output).
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
- const originalFile = path_1.default.resolve(mapDir, sourceRoot, map.sources[0]);
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
- // so we must process every segment to maintain correct state.
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; // Cumulative source line (0-based, relative)
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
- sourceLine += decoded[2]; // Update cumulative source line
105
- // Only record the first segment's mapping for this generated line
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); // Convert to 1-based
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 private methods
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
- for (const { lineEnd, varName, lineNo } of varInsertions) {
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 || value === undefined)
181
- return value;
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') {
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
- * Only handles single-source maps (most common for tsc output).
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
- const originalFile = path.resolve(mapDir, sourceRoot, map.sources[0]);
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
- // so we must process every segment to maintain correct state.
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; // Cumulative source line (0-based, relative)
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
- sourceLine += decoded[2]; // Update cumulative source line
118
- // Only record the first segment's mapping for this generated line
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); // Convert to 1-based
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 private methods
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
- for (const { lineEnd, varName, lineNo } of varInsertions) {
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
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 || value === undefined) return value;
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
@@ -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;