trickle-observe 0.2.82 → 0.2.83

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.
@@ -33,6 +33,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
34
  const module_1 = __importDefault(require("module"));
35
35
  const path_1 = __importDefault(require("path"));
36
+ const fs_1 = __importDefault(require("fs"));
36
37
  const transport_1 = require("./transport");
37
38
  const env_detect_1 = require("./env-detect");
38
39
  const wrap_1 = require("./wrap");
@@ -40,6 +41,133 @@ const fetch_observer_1 = require("./fetch-observer");
40
41
  const express_1 = require("./express");
41
42
  const trace_var_1 = require("./trace-var");
42
43
  const vite_plugin_1 = require("./vite-plugin");
44
+ // ── Source map support ──
45
+ // Lightweight VLQ decoder for mapping compiled JS lines back to original TS lines
46
+ const VLQ_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
47
+ const VLQ_LOOKUP = new Map();
48
+ for (let i = 0; i < VLQ_CHARS.length; i++)
49
+ VLQ_LOOKUP.set(VLQ_CHARS[i], i);
50
+ function decodeVLQ(encoded) {
51
+ const values = [];
52
+ let shift = 0;
53
+ let value = 0;
54
+ for (const c of encoded) {
55
+ const digit = VLQ_LOOKUP.get(c);
56
+ if (digit === undefined)
57
+ break;
58
+ const cont = digit & 32; // continuation bit
59
+ value += (digit & 31) << shift;
60
+ if (cont) {
61
+ shift += 5;
62
+ }
63
+ else {
64
+ const isNeg = value & 1;
65
+ value >>= 1;
66
+ values.push(isNeg ? -value : value);
67
+ shift = 0;
68
+ value = 0;
69
+ }
70
+ }
71
+ return values;
72
+ }
73
+ /**
74
+ * Parse a source map JSON and build a compiled→original line mapping.
75
+ * Only handles single-source maps (most common for tsc output).
76
+ */
77
+ function parseSourceMap(mapJson, mapFilePath) {
78
+ try {
79
+ const map = JSON.parse(mapJson);
80
+ if (!map.mappings || !map.sources || map.sources.length === 0)
81
+ return null;
82
+ // Resolve original source path relative to the .map file location
83
+ const mapDir = path_1.default.dirname(mapFilePath);
84
+ const sourceRoot = map.sourceRoot || '';
85
+ const originalFile = path_1.default.resolve(mapDir, sourceRoot, map.sources[0]);
86
+ // 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.
89
+ const lineMap = new Map();
90
+ const lines = map.mappings.split(';');
91
+ let sourceLine = 0; // Cumulative source line (0-based, relative)
92
+ for (let genLine = 0; genLine < lines.length; genLine++) {
93
+ const line = lines[genLine];
94
+ if (!line)
95
+ continue;
96
+ const segments = line.split(',');
97
+ let firstSegmentMapped = false;
98
+ for (const seg of segments) {
99
+ if (!seg)
100
+ continue;
101
+ const decoded = decodeVLQ(seg);
102
+ // decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
103
+ if (decoded.length >= 3) {
104
+ sourceLine += decoded[2]; // Update cumulative source line
105
+ // Only record the first segment's mapping for this generated line
106
+ if (!firstSegmentMapped) {
107
+ lineMap.set(genLine + 1, sourceLine + 1); // Convert to 1-based
108
+ firstSegmentMapped = true;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return { lineMap, originalFile, sources: map.sources };
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Try to load a source map for a compiled JS file.
121
+ * Checks: 1) sourceMappingURL comment, 2) .map file alongside .js
122
+ */
123
+ function loadSourceMap(jsFilePath, source) {
124
+ try {
125
+ // Check for inline sourceMappingURL
126
+ const urlMatch = source.match(/\/\/[#@]\s*sourceMappingURL=(.+?)(?:\s|$)/);
127
+ if (urlMatch) {
128
+ const url = urlMatch[1].trim();
129
+ // Skip data URIs (inline source maps) — too complex for now
130
+ if (url.startsWith('data:'))
131
+ return null;
132
+ // Resolve relative to the JS file
133
+ const mapPath = path_1.default.resolve(path_1.default.dirname(jsFilePath), url);
134
+ if (fs_1.default.existsSync(mapPath)) {
135
+ const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
136
+ return parseSourceMap(mapJson, mapPath);
137
+ }
138
+ }
139
+ // Fallback: check for .map file alongside .js
140
+ const mapPath = jsFilePath + '.map';
141
+ if (fs_1.default.existsSync(mapPath)) {
142
+ const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
143
+ return parseSourceMap(mapJson, mapPath);
144
+ }
145
+ return null;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * Map a compiled line number to original source line using source map data.
153
+ * If the exact line isn't in the map, find the nearest mapped line.
154
+ */
155
+ function mapLineToOriginal(sourceMap, compiledLine) {
156
+ const exact = sourceMap.lineMap.get(compiledLine);
157
+ if (exact !== undefined)
158
+ return exact;
159
+ // Find the nearest mapped line before this one
160
+ let bestLine = compiledLine;
161
+ let bestDist = Infinity;
162
+ for (const [genLine, origLine] of sourceMap.lineMap) {
163
+ const dist = compiledLine - genLine;
164
+ if (dist >= 0 && dist < bestDist) {
165
+ bestDist = dist;
166
+ bestLine = origLine + dist; // Assume roughly 1:1 mapping for nearby lines
167
+ }
168
+ }
169
+ return bestLine;
170
+ }
43
171
  const M = module_1.default;
44
172
  const originalLoad = M._load;
45
173
  const originalCompile = M.prototype._compile;
@@ -189,6 +317,11 @@ function findVarDeclarations(source, lineOffset = 0) {
189
317
  continue;
190
318
  if (varName === 'ownKeys' || varName === 'desc' || varName === '_')
191
319
  continue;
320
+ // Skip esbuild helpers
321
+ if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames')
322
+ continue;
323
+ if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
324
+ continue;
192
325
  // Skip React Refresh / HMR internals
193
326
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
194
327
  continue;
@@ -399,7 +532,7 @@ function extractDestructuredNames(pattern) {
399
532
  }
400
533
  return names;
401
534
  }
402
- function transformCjsSource(source, filename, moduleName, env) {
535
+ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
403
536
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
404
537
  const insertions = [];
405
538
  let match;
@@ -482,25 +615,24 @@ function transformCjsSource(source, filename, moduleName, env) {
482
615
  // For plain JS files, we parse the source directly.
483
616
  let varInsertions = [];
484
617
  let destructInsertions = [];
618
+ // Helper: remap line numbers using source map if available
619
+ const remapLine = sourceMap
620
+ ? (line) => mapLineToOriginal(sourceMap, line)
621
+ : (line) => line;
485
622
  if (varTraceEnabled) {
486
623
  const isTsFile = /\.[mc]?tsx?$/.test(filename);
487
- if (isTsFile) {
624
+ if (isTsFile && !sourceMap) {
625
+ // ts-node / tsx path: read original .ts file and match by occurrence
488
626
  try {
489
- // Read original TypeScript source to get correct line numbers
490
- const fs = require('fs');
491
- const originalSource = fs.readFileSync(filename, 'utf8');
627
+ const originalSource = fs_1.default.readFileSync(filename, 'utf8');
492
628
  const tsVarInsertions = findVarDeclarations(originalSource);
493
629
  const tsDestructInsertions = findDestructuredDeclarations(originalSource);
494
- // Map TypeScript variable names → correct line numbers (by occurrence order)
495
- // Use a counter map to handle duplicate variable names in different scopes
496
630
  const tsLineByVarAndOccurrence = new Map();
497
631
  for (const { varName, lineNo } of tsVarInsertions) {
498
632
  if (!tsLineByVarAndOccurrence.has(varName))
499
633
  tsLineByVarAndOccurrence.set(varName, []);
500
634
  tsLineByVarAndOccurrence.get(varName).push(lineNo);
501
635
  }
502
- // Find positions in compiled JS (for inserting trace calls),
503
- // but use line numbers from original TypeScript source
504
636
  const compiledInsertions = findVarDeclarations(source);
505
637
  const varOccurrenceCounter = new Map();
506
638
  for (const ins of compiledInsertions) {
@@ -511,8 +643,7 @@ function transformCjsSource(source, filename, moduleName, env) {
511
643
  varInsertions.push({ ...ins, lineNo: correctLineNo ?? ins.lineNo });
512
644
  }
513
645
  const compiledDestructInsertions = findDestructuredDeclarations(source);
514
- destructInsertions = compiledDestructInsertions; // Keep compiled line numbers as fallback
515
- // Try to fix destructured line numbers too
646
+ destructInsertions = compiledDestructInsertions;
516
647
  const tsDestructByKey = new Map();
517
648
  for (const { varNames, lineNo } of tsDestructInsertions) {
518
649
  const key = varNames.join(',');
@@ -548,27 +679,48 @@ function transformCjsSource(source, filename, moduleName, env) {
548
679
  }
549
680
  }
550
681
  else {
551
- varInsertions = findVarDeclarations(source);
552
- destructInsertions = findDestructuredDeclarations(source);
682
+ // Plain JS or source-map-assisted: parse compiled source, then remap lines
683
+ varInsertions = findVarDeclarations(source).map(ins => ({
684
+ ...ins,
685
+ lineNo: remapLine(ins.lineNo),
686
+ }));
687
+ destructInsertions = findDestructuredDeclarations(source).map(ins => ({
688
+ ...ins,
689
+ lineNo: remapLine(ins.lineNo),
690
+ }));
553
691
  }
554
692
  }
555
693
  // Additional variable patterns: reassignments, for-loops, catch clauses
556
694
  // Note: function params are NOT traced here because observe-register already
557
695
  // wraps functions with __trickle_wrap which captures param types via wrapFunction.
558
- const reassignInsertions = (0, vite_plugin_1.findReassignments)(source);
559
- const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source);
560
- const catchInsertions = (0, vite_plugin_1.findCatchVars)(source);
696
+ // Apply source map remapping to these too.
697
+ const reassignInsertions = (0, vite_plugin_1.findReassignments)(source).map(ins => ({
698
+ ...ins,
699
+ lineNo: remapLine(ins.lineNo),
700
+ }));
701
+ const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source).map(ins => ({
702
+ ...ins,
703
+ lineNo: remapLine(ins.lineNo),
704
+ }));
705
+ const catchInsertions = (0, vite_plugin_1.findCatchVars)(source).map(ins => ({
706
+ ...ins,
707
+ lineNo: remapLine(ins.lineNo),
708
+ }));
561
709
  if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0)
562
710
  return source;
563
711
  // Resolve the path to the wrap helper (compiled JS)
564
712
  const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
713
+ // When source map is available, use original module name for tracing
714
+ const effectiveModuleName = sourceMap
715
+ ? path_1.default.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
716
+ : moduleName;
565
717
  // Prepend: load the wrapper and create the wrap helper
566
718
  const prefixLines = [
567
719
  `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
568
720
  `var __trickle_wrap = function(fn, name, paramNames) {`,
569
721
  ` var opts = {`,
570
722
  ` functionName: name,`,
571
- ` module: ${JSON.stringify(moduleName)},`,
723
+ ` module: ${JSON.stringify(effectiveModuleName)},`,
572
724
  ` trackArgs: true,`,
573
725
  ` trackReturn: true,`,
574
726
  ` sampleRate: 1,`,
@@ -583,7 +735,12 @@ function transformCjsSource(source, filename, moduleName, env) {
583
735
  // Add variable tracing helper if we have var insertions
584
736
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0) {
585
737
  const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
586
- 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(moduleName)}, ${JSON.stringify(filename)}); } catch(e){} };`);
738
+ // When source map is available, trace variables against the original source file
739
+ const traceFilePath = sourceMap ? sourceMap.originalFile : filename;
740
+ const traceModuleName = sourceMap
741
+ ? path_1.default.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
742
+ : 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){} };`);
587
744
  }
588
745
  prefixLines.push('');
589
746
  const prefix = prefixLines.join('\n');
@@ -690,7 +847,12 @@ if (enabled) {
690
847
  if (shouldObserve(filename)) {
691
848
  const moduleName = path_1.default.basename(filename).replace(/\.[jt]sx?$/, '');
692
849
  try {
693
- const transformed = transformCjsSource(content, filename, moduleName, environment);
850
+ // Try to load source map for compiled JS files (e.g., tsc output)
851
+ const sourceMap = loadSourceMap(filename, content);
852
+ if (debug && sourceMap) {
853
+ console.log(`[trickle/observe] Source map found for ${filename} → ${sourceMap.originalFile}`);
854
+ }
855
+ const transformed = transformCjsSource(content, filename, moduleName, environment, sourceMap);
694
856
  if (transformed !== content) {
695
857
  // Count how many functions were wrapped (from insertions)
696
858
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
@@ -272,6 +272,11 @@ function findVarDeclarations(source) {
272
272
  continue;
273
273
  if (varName === 'ownKeys' || varName === 'desc')
274
274
  continue;
275
+ // Skip esbuild helpers
276
+ if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames')
277
+ continue;
278
+ if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
279
+ continue;
275
280
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
276
281
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
277
282
  continue;
@@ -260,6 +260,11 @@ function findVarDeclarations(source) {
260
260
  continue;
261
261
  if (varName === 'ownKeys' || varName === 'desc')
262
262
  continue;
263
+ // Skip esbuild helpers
264
+ if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames')
265
+ continue;
266
+ if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
267
+ continue;
263
268
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
264
269
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
265
270
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.82",
3
+ "version": "0.2.83",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,6 +29,7 @@
29
29
 
30
30
  import Module from 'module';
31
31
  import path from 'path';
32
+ import fs from 'fs';
32
33
  import { configure } from './transport';
33
34
  import { detectEnvironment } from './env-detect';
34
35
  import { wrapFunction } from './wrap';
@@ -43,6 +44,146 @@ import {
43
44
  findFunctionBodyBrace,
44
45
  } from './vite-plugin';
45
46
 
47
+ // ── Source map support ──
48
+ // Lightweight VLQ decoder for mapping compiled JS lines back to original TS lines
49
+
50
+ const VLQ_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
51
+ const VLQ_LOOKUP = new Map<string, number>();
52
+ for (let i = 0; i < VLQ_CHARS.length; i++) VLQ_LOOKUP.set(VLQ_CHARS[i], i);
53
+
54
+ function decodeVLQ(encoded: string): number[] {
55
+ const values: number[] = [];
56
+ let shift = 0;
57
+ let value = 0;
58
+ for (const c of encoded) {
59
+ const digit = VLQ_LOOKUP.get(c);
60
+ if (digit === undefined) break;
61
+ const cont = digit & 32; // continuation bit
62
+ value += (digit & 31) << shift;
63
+ if (cont) {
64
+ shift += 5;
65
+ } else {
66
+ const isNeg = value & 1;
67
+ value >>= 1;
68
+ values.push(isNeg ? -value : value);
69
+ shift = 0;
70
+ value = 0;
71
+ }
72
+ }
73
+ return values;
74
+ }
75
+
76
+ interface SourceMapData {
77
+ /** Map from compiled 1-based line → original 1-based line */
78
+ lineMap: Map<number, number>;
79
+ /** Original source file path (resolved to absolute) */
80
+ originalFile: string;
81
+ /** All source files listed in the source map */
82
+ sources: string[];
83
+ }
84
+
85
+ /**
86
+ * Parse a source map JSON and build a compiled→original line mapping.
87
+ * Only handles single-source maps (most common for tsc output).
88
+ */
89
+ function parseSourceMap(mapJson: string, mapFilePath: string): SourceMapData | null {
90
+ try {
91
+ const map = JSON.parse(mapJson);
92
+ if (!map.mappings || !map.sources || map.sources.length === 0) return null;
93
+
94
+ // Resolve original source path relative to the .map file location
95
+ const mapDir = path.dirname(mapFilePath);
96
+ const sourceRoot = map.sourceRoot || '';
97
+ const originalFile = path.resolve(mapDir, sourceRoot, map.sources[0]);
98
+
99
+ // 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.
102
+ const lineMap = new Map<number, number>();
103
+ const lines = map.mappings.split(';');
104
+ let sourceLine = 0; // Cumulative source line (0-based, relative)
105
+
106
+ for (let genLine = 0; genLine < lines.length; genLine++) {
107
+ const line = lines[genLine];
108
+ if (!line) continue;
109
+
110
+ const segments = line.split(',');
111
+ let firstSegmentMapped = false;
112
+ for (const seg of segments) {
113
+ if (!seg) continue;
114
+ const decoded = decodeVLQ(seg);
115
+ // decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
116
+ if (decoded.length >= 3) {
117
+ sourceLine += decoded[2]; // Update cumulative source line
118
+ // Only record the first segment's mapping for this generated line
119
+ if (!firstSegmentMapped) {
120
+ lineMap.set(genLine + 1, sourceLine + 1); // Convert to 1-based
121
+ firstSegmentMapped = true;
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ return { lineMap, originalFile, sources: map.sources };
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Try to load a source map for a compiled JS file.
135
+ * Checks: 1) sourceMappingURL comment, 2) .map file alongside .js
136
+ */
137
+ function loadSourceMap(jsFilePath: string, source: string): SourceMapData | null {
138
+ try {
139
+ // Check for inline sourceMappingURL
140
+ const urlMatch = source.match(/\/\/[#@]\s*sourceMappingURL=(.+?)(?:\s|$)/);
141
+ if (urlMatch) {
142
+ const url = urlMatch[1].trim();
143
+ // Skip data URIs (inline source maps) — too complex for now
144
+ if (url.startsWith('data:')) return null;
145
+ // Resolve relative to the JS file
146
+ const mapPath = path.resolve(path.dirname(jsFilePath), url);
147
+ if (fs.existsSync(mapPath)) {
148
+ const mapJson = fs.readFileSync(mapPath, 'utf8');
149
+ return parseSourceMap(mapJson, mapPath);
150
+ }
151
+ }
152
+
153
+ // Fallback: check for .map file alongside .js
154
+ const mapPath = jsFilePath + '.map';
155
+ if (fs.existsSync(mapPath)) {
156
+ const mapJson = fs.readFileSync(mapPath, 'utf8');
157
+ return parseSourceMap(mapJson, mapPath);
158
+ }
159
+
160
+ return null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Map a compiled line number to original source line using source map data.
168
+ * If the exact line isn't in the map, find the nearest mapped line.
169
+ */
170
+ function mapLineToOriginal(sourceMap: SourceMapData, compiledLine: number): number {
171
+ const exact = sourceMap.lineMap.get(compiledLine);
172
+ if (exact !== undefined) return exact;
173
+
174
+ // Find the nearest mapped line before this one
175
+ let bestLine = compiledLine;
176
+ let bestDist = Infinity;
177
+ for (const [genLine, origLine] of sourceMap.lineMap) {
178
+ const dist = compiledLine - genLine;
179
+ if (dist >= 0 && dist < bestDist) {
180
+ bestDist = dist;
181
+ bestLine = origLine + dist; // Assume roughly 1:1 mapping for nearby lines
182
+ }
183
+ }
184
+ return bestLine;
185
+ }
186
+
46
187
  const M = Module as any;
47
188
  const originalLoad = M._load;
48
189
  const originalCompile = M.prototype._compile;
@@ -178,6 +319,9 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
178
319
  if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault') continue;
179
320
  if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter') continue;
180
321
  if (varName === 'ownKeys' || varName === 'desc' || varName === '_') continue;
322
+ // Skip esbuild helpers
323
+ if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
324
+ if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
181
325
  // Skip React Refresh / HMR internals
182
326
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
183
327
  if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
@@ -368,7 +512,7 @@ function extractDestructuredNames(pattern: string): string[] {
368
512
  return names;
369
513
  }
370
514
 
371
- function transformCjsSource(source: string, filename: string, moduleName: string, env: string): string {
515
+ function transformCjsSource(source: string, filename: string, moduleName: string, env: string, sourceMap?: SourceMapData | null): string {
372
516
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
373
517
  const insertions: Array<{ position: number; name: string; paramNames: string[] }> = [];
374
518
  let match;
@@ -453,26 +597,26 @@ function transformCjsSource(source: string, filename: string, moduleName: string
453
597
  let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
454
598
  let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number }> = [];
455
599
 
600
+ // Helper: remap line numbers using source map if available
601
+ const remapLine = sourceMap
602
+ ? (line: number) => mapLineToOriginal(sourceMap, line)
603
+ : (line: number) => line;
604
+
456
605
  if (varTraceEnabled) {
457
606
  const isTsFile = /\.[mc]?tsx?$/.test(filename);
458
- if (isTsFile) {
607
+ if (isTsFile && !sourceMap) {
608
+ // ts-node / tsx path: read original .ts file and match by occurrence
459
609
  try {
460
- // Read original TypeScript source to get correct line numbers
461
- const fs = require('fs');
462
610
  const originalSource: string = fs.readFileSync(filename, 'utf8');
463
611
  const tsVarInsertions = findVarDeclarations(originalSource);
464
612
  const tsDestructInsertions = findDestructuredDeclarations(originalSource);
465
613
 
466
- // Map TypeScript variable names → correct line numbers (by occurrence order)
467
- // Use a counter map to handle duplicate variable names in different scopes
468
614
  const tsLineByVarAndOccurrence = new Map<string, number[]>();
469
615
  for (const { varName, lineNo } of tsVarInsertions) {
470
616
  if (!tsLineByVarAndOccurrence.has(varName)) tsLineByVarAndOccurrence.set(varName, []);
471
617
  tsLineByVarAndOccurrence.get(varName)!.push(lineNo);
472
618
  }
473
619
 
474
- // Find positions in compiled JS (for inserting trace calls),
475
- // but use line numbers from original TypeScript source
476
620
  const compiledInsertions = findVarDeclarations(source);
477
621
  const varOccurrenceCounter = new Map<string, number>();
478
622
  for (const ins of compiledInsertions) {
@@ -484,8 +628,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
484
628
  }
485
629
 
486
630
  const compiledDestructInsertions = findDestructuredDeclarations(source);
487
- destructInsertions = compiledDestructInsertions; // Keep compiled line numbers as fallback
488
- // Try to fix destructured line numbers too
631
+ destructInsertions = compiledDestructInsertions;
489
632
  const tsDestructByKey = new Map<string, number[]>();
490
633
  for (const { varNames, lineNo } of tsDestructInsertions) {
491
634
  const key = varNames.join(',');
@@ -517,30 +660,52 @@ function transformCjsSource(source: string, filename: string, moduleName: string
517
660
  destructInsertions = findDestructuredDeclarations(source, lineOffset);
518
661
  }
519
662
  } else {
520
- varInsertions = findVarDeclarations(source);
521
- destructInsertions = findDestructuredDeclarations(source);
663
+ // Plain JS or source-map-assisted: parse compiled source, then remap lines
664
+ varInsertions = findVarDeclarations(source).map(ins => ({
665
+ ...ins,
666
+ lineNo: remapLine(ins.lineNo),
667
+ }));
668
+ destructInsertions = findDestructuredDeclarations(source).map(ins => ({
669
+ ...ins,
670
+ lineNo: remapLine(ins.lineNo),
671
+ }));
522
672
  }
523
673
  }
524
674
 
525
675
  // Additional variable patterns: reassignments, for-loops, catch clauses
526
676
  // Note: function params are NOT traced here because observe-register already
527
677
  // wraps functions with __trickle_wrap which captures param types via wrapFunction.
528
- const reassignInsertions = findReassignments(source);
529
- const forLoopInsertions = findForLoopVars(source);
530
- const catchInsertions = findCatchVars(source);
678
+ // Apply source map remapping to these too.
679
+ const reassignInsertions = findReassignments(source).map(ins => ({
680
+ ...ins,
681
+ lineNo: remapLine(ins.lineNo),
682
+ }));
683
+ const forLoopInsertions = findForLoopVars(source).map(ins => ({
684
+ ...ins,
685
+ lineNo: remapLine(ins.lineNo),
686
+ }));
687
+ const catchInsertions = findCatchVars(source).map(ins => ({
688
+ ...ins,
689
+ lineNo: remapLine(ins.lineNo),
690
+ }));
531
691
 
532
692
  if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0) return source;
533
693
 
534
694
  // Resolve the path to the wrap helper (compiled JS)
535
695
  const wrapHelperPath = path.join(__dirname, 'wrap.js');
536
696
 
697
+ // When source map is available, use original module name for tracing
698
+ const effectiveModuleName = sourceMap
699
+ ? path.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
700
+ : moduleName;
701
+
537
702
  // Prepend: load the wrapper and create the wrap helper
538
703
  const prefixLines = [
539
704
  `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
540
705
  `var __trickle_wrap = function(fn, name, paramNames) {`,
541
706
  ` var opts = {`,
542
707
  ` functionName: name,`,
543
- ` module: ${JSON.stringify(moduleName)},`,
708
+ ` module: ${JSON.stringify(effectiveModuleName)},`,
544
709
  ` trackArgs: true,`,
545
710
  ` trackReturn: true,`,
546
711
  ` sampleRate: 1,`,
@@ -556,9 +721,14 @@ function transformCjsSource(source: string, filename: string, moduleName: string
556
721
  // Add variable tracing helper if we have var insertions
557
722
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0) {
558
723
  const traceVarPath = path.join(__dirname, 'trace-var.js');
724
+ // When source map is available, trace variables against the original source file
725
+ const traceFilePath = sourceMap ? sourceMap.originalFile : filename;
726
+ const traceModuleName = sourceMap
727
+ ? path.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
728
+ : moduleName;
559
729
  prefixLines.push(
560
730
  `var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
561
- `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e){} };`,
731
+ `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(traceModuleName)}, ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
562
732
  );
563
733
  }
564
734
 
@@ -684,7 +854,12 @@ if (enabled) {
684
854
  if (shouldObserve(filename)) {
685
855
  const moduleName = path.basename(filename).replace(/\.[jt]sx?$/, '');
686
856
  try {
687
- const transformed = transformCjsSource(content, filename, moduleName, environment);
857
+ // Try to load source map for compiled JS files (e.g., tsc output)
858
+ const sourceMap = loadSourceMap(filename, content);
859
+ if (debug && sourceMap) {
860
+ console.log(`[trickle/observe] Source map found for ${filename} → ${sourceMap.originalFile}`);
861
+ }
862
+ const transformed = transformCjsSource(content, filename, moduleName, environment, sourceMap);
688
863
  if (transformed !== content) {
689
864
  // Count how many functions were wrapped (from insertions)
690
865
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
@@ -260,6 +260,9 @@ function findVarDeclarations(source: string): Array<{ lineEnd: number; varName:
260
260
  if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault') continue;
261
261
  if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter') continue;
262
262
  if (varName === 'ownKeys' || varName === 'desc') continue;
263
+ // Skip esbuild helpers
264
+ if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
265
+ if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
263
266
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
264
267
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
265
268
  if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;