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.
- package/dist/observe-register.js +193 -19
- package/dist/vite-plugin.js +15 -1
- package/dist-esm/vite-plugin.js +15 -1
- package/package.json +1 -1
- package/src/observe-register.ts +200 -18
- package/src/vite-plugin.ts +9 -1
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;
|
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/vite-plugin.js
CHANGED
|
@@ -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;
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -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
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;
|
|
@@ -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;
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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;
|
package/src/vite-plugin.ts
CHANGED
|
@@ -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;
|