trickle-observe 0.2.81 → 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;
@@ -182,6 +310,23 @@ function findVarDeclarations(source, lineOffset = 0) {
182
310
  continue;
183
311
  if (varName === '_a' || varName === '_b' || varName === '_c')
184
312
  continue; // TS compiled vars
313
+ // Skip TS compiler helpers and module internals
314
+ if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault')
315
+ continue;
316
+ if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter')
317
+ continue;
318
+ if (varName === 'ownKeys' || varName === 'desc' || varName === '_')
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;
325
+ // Skip React Refresh / HMR internals
326
+ if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
327
+ continue;
328
+ if (varName === '_s' || varName === '_c2' || varName === '_s2')
329
+ continue;
185
330
  // Check if this is a require() call — skip those (they're imports, not interesting values)
186
331
  const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
187
332
  if (/^\s*require\s*\(/.test(restOfLine))
@@ -387,7 +532,7 @@ function extractDestructuredNames(pattern) {
387
532
  }
388
533
  return names;
389
534
  }
390
- function transformCjsSource(source, filename, moduleName, env) {
535
+ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
391
536
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
392
537
  const insertions = [];
393
538
  let match;
@@ -470,25 +615,24 @@ function transformCjsSource(source, filename, moduleName, env) {
470
615
  // For plain JS files, we parse the source directly.
471
616
  let varInsertions = [];
472
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;
473
622
  if (varTraceEnabled) {
474
623
  const isTsFile = /\.[mc]?tsx?$/.test(filename);
475
- if (isTsFile) {
624
+ if (isTsFile && !sourceMap) {
625
+ // ts-node / tsx path: read original .ts file and match by occurrence
476
626
  try {
477
- // Read original TypeScript source to get correct line numbers
478
- const fs = require('fs');
479
- const originalSource = fs.readFileSync(filename, 'utf8');
627
+ const originalSource = fs_1.default.readFileSync(filename, 'utf8');
480
628
  const tsVarInsertions = findVarDeclarations(originalSource);
481
629
  const tsDestructInsertions = findDestructuredDeclarations(originalSource);
482
- // Map TypeScript variable names → correct line numbers (by occurrence order)
483
- // Use a counter map to handle duplicate variable names in different scopes
484
630
  const tsLineByVarAndOccurrence = new Map();
485
631
  for (const { varName, lineNo } of tsVarInsertions) {
486
632
  if (!tsLineByVarAndOccurrence.has(varName))
487
633
  tsLineByVarAndOccurrence.set(varName, []);
488
634
  tsLineByVarAndOccurrence.get(varName).push(lineNo);
489
635
  }
490
- // Find positions in compiled JS (for inserting trace calls),
491
- // but use line numbers from original TypeScript source
492
636
  const compiledInsertions = findVarDeclarations(source);
493
637
  const varOccurrenceCounter = new Map();
494
638
  for (const ins of compiledInsertions) {
@@ -499,8 +643,7 @@ function transformCjsSource(source, filename, moduleName, env) {
499
643
  varInsertions.push({ ...ins, lineNo: correctLineNo ?? ins.lineNo });
500
644
  }
501
645
  const compiledDestructInsertions = findDestructuredDeclarations(source);
502
- destructInsertions = compiledDestructInsertions; // Keep compiled line numbers as fallback
503
- // Try to fix destructured line numbers too
646
+ destructInsertions = compiledDestructInsertions;
504
647
  const tsDestructByKey = new Map();
505
648
  for (const { varNames, lineNo } of tsDestructInsertions) {
506
649
  const key = varNames.join(',');
@@ -536,27 +679,48 @@ function transformCjsSource(source, filename, moduleName, env) {
536
679
  }
537
680
  }
538
681
  else {
539
- varInsertions = findVarDeclarations(source);
540
- 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
+ }));
541
691
  }
542
692
  }
543
693
  // Additional variable patterns: reassignments, for-loops, catch clauses
544
694
  // Note: function params are NOT traced here because observe-register already
545
695
  // wraps functions with __trickle_wrap which captures param types via wrapFunction.
546
- const reassignInsertions = (0, vite_plugin_1.findReassignments)(source);
547
- const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source);
548
- 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
+ }));
549
709
  if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0)
550
710
  return source;
551
711
  // Resolve the path to the wrap helper (compiled JS)
552
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;
553
717
  // Prepend: load the wrapper and create the wrap helper
554
718
  const prefixLines = [
555
719
  `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
556
720
  `var __trickle_wrap = function(fn, name, paramNames) {`,
557
721
  ` var opts = {`,
558
722
  ` functionName: name,`,
559
- ` module: ${JSON.stringify(moduleName)},`,
723
+ ` module: ${JSON.stringify(effectiveModuleName)},`,
560
724
  ` trackArgs: true,`,
561
725
  ` trackReturn: true,`,
562
726
  ` sampleRate: 1,`,
@@ -571,7 +735,12 @@ function transformCjsSource(source, filename, moduleName, env) {
571
735
  // Add variable tracing helper if we have var insertions
572
736
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0) {
573
737
  const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
574
- 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){} };`);
575
744
  }
576
745
  prefixLines.push('');
577
746
  const prefix = prefixLines.join('\n');
@@ -678,7 +847,12 @@ if (enabled) {
678
847
  if (shouldObserve(filename)) {
679
848
  const moduleName = path_1.default.basename(filename).replace(/\.[jt]sx?$/, '');
680
849
  try {
681
- 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);
682
856
  if (transformed !== content) {
683
857
  // Count how many functions were wrapped (from insertions)
684
858
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
@@ -263,9 +263,20 @@ function findVarDeclarations(source) {
263
263
  // Skip trickle internals
264
264
  if (varName.startsWith('__trickle'))
265
265
  continue;
266
- // Skip TS compiled vars
266
+ // Skip TS compiled vars and helpers
267
267
  if (varName === '_a' || varName === '_b' || varName === '_c')
268
268
  continue;
269
+ if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault')
270
+ continue;
271
+ if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter')
272
+ continue;
273
+ if (varName === 'ownKeys' || varName === 'desc')
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;
269
280
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
270
281
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
271
282
  continue;
@@ -518,6 +529,9 @@ function findReassignments(source) {
518
529
  // Skip 'this', 'self', 'super' (not reassignable in practice)
519
530
  if (varName === 'this' || varName === 'super')
520
531
  continue;
532
+ // Skip TS compiler helpers and module internals
533
+ if (varName === 'ownKeys' || varName === 'desc')
534
+ continue;
521
535
  // Skip React Refresh / HMR internals and discard variables
522
536
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker')
523
537
  continue;
@@ -251,9 +251,20 @@ function findVarDeclarations(source) {
251
251
  // Skip trickle internals
252
252
  if (varName.startsWith('__trickle'))
253
253
  continue;
254
- // Skip TS compiled vars
254
+ // Skip TS compiled vars and helpers
255
255
  if (varName === '_a' || varName === '_b' || varName === '_c')
256
256
  continue;
257
+ if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault')
258
+ continue;
259
+ if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter')
260
+ continue;
261
+ if (varName === 'ownKeys' || varName === 'desc')
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;
257
268
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
258
269
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
259
270
  continue;
@@ -506,6 +517,9 @@ export function findReassignments(source) {
506
517
  // Skip 'this', 'self', 'super' (not reassignable in practice)
507
518
  if (varName === 'this' || varName === 'super')
508
519
  continue;
520
+ // Skip TS compiler helpers and module internals
521
+ if (varName === 'ownKeys' || varName === 'desc')
522
+ continue;
509
523
  // Skip React Refresh / HMR internals and discard variables
510
524
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker')
511
525
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.81",
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;
@@ -174,6 +315,16 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
174
315
  if (varName === '__trickle_mod' || varName === '__trickle_wrap' || varName === '__trickle_tv') continue;
175
316
  if (varName.startsWith('__trickle')) continue;
176
317
  if (varName === '_a' || varName === '_b' || varName === '_c') continue; // TS compiled vars
318
+ // Skip TS compiler helpers and module internals
319
+ if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault') continue;
320
+ if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter') continue;
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;
325
+ // Skip React Refresh / HMR internals
326
+ if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
327
+ if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
177
328
 
178
329
  // Check if this is a require() call — skip those (they're imports, not interesting values)
179
330
  const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
@@ -361,7 +512,7 @@ function extractDestructuredNames(pattern: string): string[] {
361
512
  return names;
362
513
  }
363
514
 
364
- 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 {
365
516
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
366
517
  const insertions: Array<{ position: number; name: string; paramNames: string[] }> = [];
367
518
  let match;
@@ -446,26 +597,26 @@ function transformCjsSource(source: string, filename: string, moduleName: string
446
597
  let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
447
598
  let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number }> = [];
448
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
+
449
605
  if (varTraceEnabled) {
450
606
  const isTsFile = /\.[mc]?tsx?$/.test(filename);
451
- if (isTsFile) {
607
+ if (isTsFile && !sourceMap) {
608
+ // ts-node / tsx path: read original .ts file and match by occurrence
452
609
  try {
453
- // Read original TypeScript source to get correct line numbers
454
- const fs = require('fs');
455
610
  const originalSource: string = fs.readFileSync(filename, 'utf8');
456
611
  const tsVarInsertions = findVarDeclarations(originalSource);
457
612
  const tsDestructInsertions = findDestructuredDeclarations(originalSource);
458
613
 
459
- // Map TypeScript variable names → correct line numbers (by occurrence order)
460
- // Use a counter map to handle duplicate variable names in different scopes
461
614
  const tsLineByVarAndOccurrence = new Map<string, number[]>();
462
615
  for (const { varName, lineNo } of tsVarInsertions) {
463
616
  if (!tsLineByVarAndOccurrence.has(varName)) tsLineByVarAndOccurrence.set(varName, []);
464
617
  tsLineByVarAndOccurrence.get(varName)!.push(lineNo);
465
618
  }
466
619
 
467
- // Find positions in compiled JS (for inserting trace calls),
468
- // but use line numbers from original TypeScript source
469
620
  const compiledInsertions = findVarDeclarations(source);
470
621
  const varOccurrenceCounter = new Map<string, number>();
471
622
  for (const ins of compiledInsertions) {
@@ -477,8 +628,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
477
628
  }
478
629
 
479
630
  const compiledDestructInsertions = findDestructuredDeclarations(source);
480
- destructInsertions = compiledDestructInsertions; // Keep compiled line numbers as fallback
481
- // Try to fix destructured line numbers too
631
+ destructInsertions = compiledDestructInsertions;
482
632
  const tsDestructByKey = new Map<string, number[]>();
483
633
  for (const { varNames, lineNo } of tsDestructInsertions) {
484
634
  const key = varNames.join(',');
@@ -510,30 +660,52 @@ function transformCjsSource(source: string, filename: string, moduleName: string
510
660
  destructInsertions = findDestructuredDeclarations(source, lineOffset);
511
661
  }
512
662
  } else {
513
- varInsertions = findVarDeclarations(source);
514
- 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
+ }));
515
672
  }
516
673
  }
517
674
 
518
675
  // Additional variable patterns: reassignments, for-loops, catch clauses
519
676
  // Note: function params are NOT traced here because observe-register already
520
677
  // wraps functions with __trickle_wrap which captures param types via wrapFunction.
521
- const reassignInsertions = findReassignments(source);
522
- const forLoopInsertions = findForLoopVars(source);
523
- 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
+ }));
524
691
 
525
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;
526
693
 
527
694
  // Resolve the path to the wrap helper (compiled JS)
528
695
  const wrapHelperPath = path.join(__dirname, 'wrap.js');
529
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
+
530
702
  // Prepend: load the wrapper and create the wrap helper
531
703
  const prefixLines = [
532
704
  `var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
533
705
  `var __trickle_wrap = function(fn, name, paramNames) {`,
534
706
  ` var opts = {`,
535
707
  ` functionName: name,`,
536
- ` module: ${JSON.stringify(moduleName)},`,
708
+ ` module: ${JSON.stringify(effectiveModuleName)},`,
537
709
  ` trackArgs: true,`,
538
710
  ` trackReturn: true,`,
539
711
  ` sampleRate: 1,`,
@@ -549,9 +721,14 @@ function transformCjsSource(source: string, filename: string, moduleName: string
549
721
  // Add variable tracing helper if we have var insertions
550
722
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0) {
551
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;
552
729
  prefixLines.push(
553
730
  `var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
554
- `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){} };`,
555
732
  );
556
733
  }
557
734
 
@@ -677,7 +854,12 @@ if (enabled) {
677
854
  if (shouldObserve(filename)) {
678
855
  const moduleName = path.basename(filename).replace(/\.[jt]sx?$/, '');
679
856
  try {
680
- 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);
681
863
  if (transformed !== content) {
682
864
  // Count how many functions were wrapped (from insertions)
683
865
  const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
@@ -255,8 +255,14 @@ function findVarDeclarations(source: string): Array<{ lineEnd: number; varName:
255
255
 
256
256
  // Skip trickle internals
257
257
  if (varName.startsWith('__trickle')) continue;
258
- // Skip TS compiled vars
258
+ // Skip TS compiled vars and helpers
259
259
  if (varName === '_a' || varName === '_b' || varName === '_c') continue;
260
+ if (varName === '__createBinding' || varName === '__setModuleDefault' || varName === '__importStar' || varName === '__importDefault') continue;
261
+ if (varName === '__decorate' || varName === '__metadata' || varName === '__param' || varName === '__awaiter') continue;
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;
260
266
  // Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
261
267
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
262
268
  if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
@@ -491,6 +497,8 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
491
497
  if (varName === '_a' || varName === '_b' || varName === '_c') continue;
492
498
  // Skip 'this', 'self', 'super' (not reassignable in practice)
493
499
  if (varName === 'this' || varName === 'super') continue;
500
+ // Skip TS compiler helpers and module internals
501
+ if (varName === 'ownKeys' || varName === 'desc') continue;
494
502
  // Skip React Refresh / HMR internals and discard variables
495
503
  if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker') continue;
496
504
  if (varName === '_s' || varName === '_c2' || varName === '_s2' || varName === '_') continue;