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.
- package/dist/observe-register.js +181 -19
- package/dist/vite-plugin.js +5 -0
- package/dist-esm/vite-plugin.js +5 -0
- package/package.json +1 -1
- package/src/observe-register.ts +193 -18
- package/src/vite-plugin.ts +3 -0
package/dist/observe-register.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/vite-plugin.js
CHANGED
|
@@ -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;
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -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
package/src/observe-register.ts
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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;
|
package/src/vite-plugin.ts
CHANGED
|
@@ -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;
|