trickle-observe 0.2.84 → 0.2.86
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 +107 -25
- package/dist/trace-var.js +9 -1
- package/dist/vite-plugin.js +6 -1
- package/dist-esm/vite-plugin.js +6 -1
- package/package.json +1 -1
- package/src/observe-register.ts +114 -28
- package/src/trace-var.ts +9 -1
- package/src/vite-plugin.ts +8 -0
package/dist/observe-register.js
CHANGED
|
@@ -70,25 +70,75 @@ function decodeVLQ(encoded) {
|
|
|
70
70
|
}
|
|
71
71
|
return values;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a source map source path to a real filesystem path.
|
|
75
|
+
* Handles webpack:// URLs, file:// URLs, and relative paths.
|
|
76
|
+
*/
|
|
77
|
+
function resolveSourcePath(source, mapDir, sourceRoot, jsFilePath) {
|
|
78
|
+
// Handle webpack:// URLs: webpack://package-name/./src/file.ts
|
|
79
|
+
if (source.startsWith('webpack://')) {
|
|
80
|
+
// Strip webpack://package-name/ prefix
|
|
81
|
+
const withoutProtocol = source.replace(/^webpack:\/\/[^/]*\//, '');
|
|
82
|
+
// Resolve relative to the project root (parent of dist/ or the JS file's directory)
|
|
83
|
+
const projectRoot = findProjectRoot(jsFilePath);
|
|
84
|
+
return path_1.default.resolve(projectRoot, withoutProtocol);
|
|
85
|
+
}
|
|
86
|
+
// Handle file:// URLs
|
|
87
|
+
if (source.startsWith('file://')) {
|
|
88
|
+
return source.replace(/^file:\/\//, '');
|
|
89
|
+
}
|
|
90
|
+
// Regular relative path
|
|
91
|
+
return path_1.default.resolve(mapDir, sourceRoot, source);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Find the project root by looking for package.json or tsconfig.json
|
|
95
|
+
* starting from the JS file's directory and walking up.
|
|
96
|
+
*/
|
|
97
|
+
function findProjectRoot(jsFilePath) {
|
|
98
|
+
let dir = path_1.default.dirname(jsFilePath);
|
|
99
|
+
for (let i = 0; i < 10; i++) {
|
|
100
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) || fs_1.default.existsSync(path_1.default.join(dir, 'tsconfig.json'))) {
|
|
101
|
+
return dir;
|
|
102
|
+
}
|
|
103
|
+
const parent = path_1.default.dirname(dir);
|
|
104
|
+
if (parent === dir)
|
|
105
|
+
break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return path_1.default.dirname(jsFilePath);
|
|
109
|
+
}
|
|
73
110
|
/**
|
|
74
111
|
* Parse a source map JSON and build a compiled→original line mapping.
|
|
75
|
-
*
|
|
112
|
+
* Supports both single-source (tsc) and multi-source (webpack/rollup) maps.
|
|
76
113
|
*/
|
|
77
|
-
function parseSourceMap(mapJson, mapFilePath) {
|
|
114
|
+
function parseSourceMap(mapJson, mapFilePath, jsFilePath) {
|
|
78
115
|
try {
|
|
79
116
|
const map = JSON.parse(mapJson);
|
|
80
117
|
if (!map.mappings || !map.sources || map.sources.length === 0)
|
|
81
118
|
return null;
|
|
82
|
-
// Resolve original source path relative to the .map file location
|
|
83
119
|
const mapDir = path_1.default.dirname(mapFilePath);
|
|
84
120
|
const sourceRoot = map.sourceRoot || '';
|
|
85
|
-
|
|
121
|
+
// Resolve all source paths, filtering out non-user sources (webpack internals)
|
|
122
|
+
const resolvedSources = map.sources.map((s) => resolveSourcePath(s, mapDir, sourceRoot, jsFilePath));
|
|
123
|
+
// Find the first user source file (skip webpack bootstrap/runtime)
|
|
124
|
+
let primarySourceIdx = 0;
|
|
125
|
+
for (let i = 0; i < resolvedSources.length; i++) {
|
|
126
|
+
const s = map.sources[i];
|
|
127
|
+
if (!s.includes('webpack/bootstrap') && !s.includes('webpack/runtime') && fs_1.default.existsSync(resolvedSources[i])) {
|
|
128
|
+
primarySourceIdx = i;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const originalFile = resolvedSources[primarySourceIdx];
|
|
86
133
|
// Decode mappings: semicolons separate lines, commas separate segments
|
|
87
|
-
// VLQ values are cumulative across ALL segments (not just first per line)
|
|
88
|
-
//
|
|
134
|
+
// VLQ values are cumulative across ALL segments (not just first per line).
|
|
135
|
+
// For multi-source maps, also track the source index to map lines to different files.
|
|
89
136
|
const lineMap = new Map();
|
|
137
|
+
/** Map from generated line → resolved source file path */
|
|
138
|
+
const lineSourceMap = new Map();
|
|
90
139
|
const lines = map.mappings.split(';');
|
|
91
|
-
let sourceLine = 0;
|
|
140
|
+
let sourceLine = 0;
|
|
141
|
+
let sourceIdx = 0;
|
|
92
142
|
for (let genLine = 0; genLine < lines.length; genLine++) {
|
|
93
143
|
const line = lines[genLine];
|
|
94
144
|
if (!line)
|
|
@@ -99,18 +149,22 @@ function parseSourceMap(mapJson, mapFilePath) {
|
|
|
99
149
|
if (!seg)
|
|
100
150
|
continue;
|
|
101
151
|
const decoded = decodeVLQ(seg);
|
|
102
|
-
// decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
|
|
103
152
|
if (decoded.length >= 3) {
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
if (decoded.length >= 2)
|
|
154
|
+
sourceIdx += decoded[1]; // Update source index
|
|
155
|
+
sourceLine += decoded[2];
|
|
106
156
|
if (!firstSegmentMapped) {
|
|
107
|
-
lineMap.set(genLine + 1, sourceLine + 1);
|
|
157
|
+
lineMap.set(genLine + 1, sourceLine + 1);
|
|
158
|
+
// Track which source file this line belongs to
|
|
159
|
+
if (sourceIdx >= 0 && sourceIdx < resolvedSources.length) {
|
|
160
|
+
lineSourceMap.set(genLine + 1, resolvedSources[sourceIdx]);
|
|
161
|
+
}
|
|
108
162
|
firstSegmentMapped = true;
|
|
109
163
|
}
|
|
110
164
|
}
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
|
-
return { lineMap, originalFile, sources: map.sources };
|
|
167
|
+
return { lineMap, originalFile, sources: map.sources, lineSourceMap };
|
|
114
168
|
}
|
|
115
169
|
catch {
|
|
116
170
|
return null;
|
|
@@ -133,14 +187,14 @@ function loadSourceMap(jsFilePath, source) {
|
|
|
133
187
|
const mapPath = path_1.default.resolve(path_1.default.dirname(jsFilePath), url);
|
|
134
188
|
if (fs_1.default.existsSync(mapPath)) {
|
|
135
189
|
const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
|
|
136
|
-
return parseSourceMap(mapJson, mapPath);
|
|
190
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
137
191
|
}
|
|
138
192
|
}
|
|
139
193
|
// Fallback: check for .map file alongside .js
|
|
140
194
|
const mapPath = jsFilePath + '.map';
|
|
141
195
|
if (fs_1.default.existsSync(mapPath)) {
|
|
142
196
|
const mapJson = fs_1.default.readFileSync(mapPath, 'utf8');
|
|
143
|
-
return parseSourceMap(mapJson, mapPath);
|
|
197
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
144
198
|
}
|
|
145
199
|
return null;
|
|
146
200
|
}
|
|
@@ -322,6 +376,11 @@ function findVarDeclarations(source, lineOffset = 0) {
|
|
|
322
376
|
continue;
|
|
323
377
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
324
378
|
continue;
|
|
379
|
+
// Skip webpack internals
|
|
380
|
+
if (varName.startsWith('__webpack_'))
|
|
381
|
+
continue;
|
|
382
|
+
if (varName === '__unused_webpack_module')
|
|
383
|
+
continue;
|
|
325
384
|
// Skip React Refresh / HMR internals
|
|
326
385
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
327
386
|
continue;
|
|
@@ -619,6 +678,14 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
619
678
|
const remapLine = sourceMap
|
|
620
679
|
? (line) => mapLineToOriginal(sourceMap, line)
|
|
621
680
|
: (line) => line;
|
|
681
|
+
// Helper: get source file for a compiled line (for multi-source bundles)
|
|
682
|
+
const hasMultiSource = sourceMap?.lineSourceMap && sourceMap.lineSourceMap.size > 0;
|
|
683
|
+
const getSourceFile = hasMultiSource
|
|
684
|
+
? (compiledLine) => {
|
|
685
|
+
const file = sourceMap.lineSourceMap.get(compiledLine);
|
|
686
|
+
return file && !file.includes('webpack/bootstrap') && !file.includes('webpack/runtime') ? file : undefined;
|
|
687
|
+
}
|
|
688
|
+
: (_) => undefined;
|
|
622
689
|
if (varTraceEnabled) {
|
|
623
690
|
const isTsFile = /\.[mc]?tsx?$/.test(filename);
|
|
624
691
|
if (isTsFile && !sourceMap) {
|
|
@@ -682,10 +749,12 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
682
749
|
// Plain JS or source-map-assisted: parse compiled source, then remap lines
|
|
683
750
|
varInsertions = findVarDeclarations(source).map(ins => ({
|
|
684
751
|
...ins,
|
|
752
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
685
753
|
lineNo: remapLine(ins.lineNo),
|
|
686
754
|
}));
|
|
687
755
|
destructInsertions = findDestructuredDeclarations(source).map(ins => ({
|
|
688
756
|
...ins,
|
|
757
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
689
758
|
lineNo: remapLine(ins.lineNo),
|
|
690
759
|
}));
|
|
691
760
|
}
|
|
@@ -696,14 +765,17 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
696
765
|
// Apply source map remapping to these too.
|
|
697
766
|
const reassignInsertions = (0, vite_plugin_1.findReassignments)(source).map(ins => ({
|
|
698
767
|
...ins,
|
|
768
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
699
769
|
lineNo: remapLine(ins.lineNo),
|
|
700
770
|
}));
|
|
701
771
|
const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source).map(ins => ({
|
|
702
772
|
...ins,
|
|
773
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
703
774
|
lineNo: remapLine(ins.lineNo),
|
|
704
775
|
}));
|
|
705
776
|
const catchInsertions = (0, vite_plugin_1.findCatchVars)(source).map(ins => ({
|
|
706
777
|
...ins,
|
|
778
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
707
779
|
lineNo: remapLine(ins.lineNo),
|
|
708
780
|
}));
|
|
709
781
|
if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0)
|
|
@@ -740,7 +812,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
740
812
|
const traceModuleName = sourceMap
|
|
741
813
|
? path_1.default.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
|
|
742
814
|
: 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){} };`);
|
|
815
|
+
prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`);
|
|
744
816
|
}
|
|
745
817
|
prefixLines.push('');
|
|
746
818
|
const prefix = prefixLines.join('\n');
|
|
@@ -752,37 +824,47 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
752
824
|
code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
|
|
753
825
|
});
|
|
754
826
|
}
|
|
755
|
-
|
|
827
|
+
// Helper to generate source file args for __trickle_tv calls (for multi-source bundles)
|
|
828
|
+
const sfArgs = (sourceFile) => {
|
|
829
|
+
if (!sourceFile)
|
|
830
|
+
return '';
|
|
831
|
+
const mod = path_1.default.basename(sourceFile).replace(/\.[jt]sx?$/, '');
|
|
832
|
+
return `,${JSON.stringify(mod)},${JSON.stringify(sourceFile)}`;
|
|
833
|
+
};
|
|
834
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of varInsertions) {
|
|
756
835
|
allInsertions.push({
|
|
757
836
|
position: lineEnd,
|
|
758
|
-
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
837
|
+
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
759
838
|
});
|
|
760
839
|
}
|
|
761
|
-
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
762
|
-
const
|
|
840
|
+
for (const { lineEnd, varNames, lineNo, sourceFile } of destructInsertions) {
|
|
841
|
+
const sf = sfArgs(sourceFile);
|
|
842
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
763
843
|
allInsertions.push({
|
|
764
844
|
position: lineEnd,
|
|
765
845
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
766
846
|
});
|
|
767
847
|
}
|
|
768
848
|
// Reassignment insertions
|
|
769
|
-
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
849
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of reassignInsertions) {
|
|
770
850
|
allInsertions.push({
|
|
771
851
|
position: lineEnd,
|
|
772
|
-
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
852
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
773
853
|
});
|
|
774
854
|
}
|
|
775
855
|
// For-loop variable insertions
|
|
776
|
-
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
777
|
-
const
|
|
856
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of forLoopInsertions) {
|
|
857
|
+
const sf = sfArgs(sourceFile);
|
|
858
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
778
859
|
allInsertions.push({
|
|
779
860
|
position: bodyStart,
|
|
780
861
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
781
862
|
});
|
|
782
863
|
}
|
|
783
864
|
// Catch clause insertions
|
|
784
|
-
for (const { bodyStart, varNames, lineNo } of catchInsertions) {
|
|
785
|
-
const
|
|
865
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of catchInsertions) {
|
|
866
|
+
const sf = sfArgs(sourceFile);
|
|
867
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
786
868
|
allInsertions.push({
|
|
787
869
|
position: bodyStart,
|
|
788
870
|
code: `\ntry{${calls}}catch(__e2){}\n`,
|
package/dist/trace-var.js
CHANGED
|
@@ -59,6 +59,9 @@ let varsFilePath = '';
|
|
|
59
59
|
let debugMode = false;
|
|
60
60
|
/** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
|
|
61
61
|
const varCache = new Map();
|
|
62
|
+
/** Per-line sample count to avoid loop variable spam */
|
|
63
|
+
const sampleCount = new Map();
|
|
64
|
+
const MAX_SAMPLES_PER_LINE = 5;
|
|
62
65
|
/** Batch buffer for writing — avoids one fs.appendFileSync per variable */
|
|
63
66
|
let varBuffer = [];
|
|
64
67
|
let flushTimer = null;
|
|
@@ -105,8 +108,12 @@ function traceVar(value, varName, line, moduleName, filePath) {
|
|
|
105
108
|
// Create a stable hash for dedup
|
|
106
109
|
const dummyArgs = { kind: 'tuple', elements: [] };
|
|
107
110
|
const typeHash = (0, type_hash_1.hashType)(dummyArgs, type);
|
|
108
|
-
//
|
|
111
|
+
// Per-line sample count limit: stop after N samples to avoid loop spam
|
|
109
112
|
const cacheKey = `${filePath}:${line}:${varName}`;
|
|
113
|
+
const cnt = sampleCount.get(cacheKey) || 0;
|
|
114
|
+
if (cnt >= MAX_SAMPLES_PER_LINE)
|
|
115
|
+
return;
|
|
116
|
+
// Value-aware dedup: re-send if value changed or 10s elapsed
|
|
110
117
|
const t = typeof value;
|
|
111
118
|
const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
|
|
112
119
|
? String(value).substring(0, 60)
|
|
@@ -116,6 +123,7 @@ function traceVar(value, varName, line, moduleName, filePath) {
|
|
|
116
123
|
if (prev && prev.fp === fp && (now - prev.ts) < 10000)
|
|
117
124
|
return;
|
|
118
125
|
varCache.set(cacheKey, { fp, ts: now });
|
|
126
|
+
sampleCount.set(cacheKey, cnt + 1);
|
|
119
127
|
const sample = sanitizeVarSample(value);
|
|
120
128
|
const observation = {
|
|
121
129
|
kind: 'variable',
|
package/dist/vite-plugin.js
CHANGED
|
@@ -278,6 +278,11 @@ function findVarDeclarations(source) {
|
|
|
278
278
|
continue;
|
|
279
279
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
280
280
|
continue;
|
|
281
|
+
// Skip webpack internals
|
|
282
|
+
if (varName.startsWith('__webpack_'))
|
|
283
|
+
continue;
|
|
284
|
+
if (varName === '__unused_webpack_module')
|
|
285
|
+
continue;
|
|
281
286
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
282
287
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
283
288
|
continue;
|
|
@@ -1604,7 +1609,7 @@ ingestUrl) {
|
|
|
1604
1609
|
}
|
|
1605
1610
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1606
1611
|
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1607
|
-
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1612
|
+
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1608
1613
|
}
|
|
1609
1614
|
// Add React component render tracker if needed
|
|
1610
1615
|
if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -265,6 +265,11 @@ function findVarDeclarations(source) {
|
|
|
265
265
|
continue;
|
|
266
266
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps')
|
|
267
267
|
continue;
|
|
268
|
+
// Skip webpack internals
|
|
269
|
+
if (varName.startsWith('__webpack_'))
|
|
270
|
+
continue;
|
|
271
|
+
if (varName === '__unused_webpack_module')
|
|
272
|
+
continue;
|
|
268
273
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
269
274
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage')
|
|
270
275
|
continue;
|
|
@@ -1591,7 +1596,7 @@ ingestUrl) {
|
|
|
1591
1596
|
}
|
|
1592
1597
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1593
1598
|
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1594
|
-
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1599
|
+
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1595
1600
|
}
|
|
1596
1601
|
// Add React component render tracker if needed
|
|
1597
1602
|
if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
|
package/package.json
CHANGED
package/src/observe-register.ts
CHANGED
|
@@ -76,32 +76,87 @@ function decodeVLQ(encoded: string): number[] {
|
|
|
76
76
|
interface SourceMapData {
|
|
77
77
|
/** Map from compiled 1-based line → original 1-based line */
|
|
78
78
|
lineMap: Map<number, number>;
|
|
79
|
-
/** Original source file path (resolved to absolute) */
|
|
79
|
+
/** Original source file path (resolved to absolute) — primary/first user source */
|
|
80
80
|
originalFile: string;
|
|
81
81
|
/** All source files listed in the source map */
|
|
82
82
|
sources: string[];
|
|
83
|
+
/** Map from compiled line → resolved source file path (for multi-source bundles) */
|
|
84
|
+
lineSourceMap?: Map<number, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a source map source path to a real filesystem path.
|
|
89
|
+
* Handles webpack:// URLs, file:// URLs, and relative paths.
|
|
90
|
+
*/
|
|
91
|
+
function resolveSourcePath(source: string, mapDir: string, sourceRoot: string, jsFilePath: string): string {
|
|
92
|
+
// Handle webpack:// URLs: webpack://package-name/./src/file.ts
|
|
93
|
+
if (source.startsWith('webpack://')) {
|
|
94
|
+
// Strip webpack://package-name/ prefix
|
|
95
|
+
const withoutProtocol = source.replace(/^webpack:\/\/[^/]*\//, '');
|
|
96
|
+
// Resolve relative to the project root (parent of dist/ or the JS file's directory)
|
|
97
|
+
const projectRoot = findProjectRoot(jsFilePath);
|
|
98
|
+
return path.resolve(projectRoot, withoutProtocol);
|
|
99
|
+
}
|
|
100
|
+
// Handle file:// URLs
|
|
101
|
+
if (source.startsWith('file://')) {
|
|
102
|
+
return source.replace(/^file:\/\//, '');
|
|
103
|
+
}
|
|
104
|
+
// Regular relative path
|
|
105
|
+
return path.resolve(mapDir, sourceRoot, source);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find the project root by looking for package.json or tsconfig.json
|
|
110
|
+
* starting from the JS file's directory and walking up.
|
|
111
|
+
*/
|
|
112
|
+
function findProjectRoot(jsFilePath: string): string {
|
|
113
|
+
let dir = path.dirname(jsFilePath);
|
|
114
|
+
for (let i = 0; i < 10; i++) {
|
|
115
|
+
if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
118
|
+
const parent = path.dirname(dir);
|
|
119
|
+
if (parent === dir) break;
|
|
120
|
+
dir = parent;
|
|
121
|
+
}
|
|
122
|
+
return path.dirname(jsFilePath);
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
/**
|
|
86
126
|
* Parse a source map JSON and build a compiled→original line mapping.
|
|
87
|
-
*
|
|
127
|
+
* Supports both single-source (tsc) and multi-source (webpack/rollup) maps.
|
|
88
128
|
*/
|
|
89
|
-
function parseSourceMap(mapJson: string, mapFilePath: string): SourceMapData | null {
|
|
129
|
+
function parseSourceMap(mapJson: string, mapFilePath: string, jsFilePath: string): SourceMapData | null {
|
|
90
130
|
try {
|
|
91
131
|
const map = JSON.parse(mapJson);
|
|
92
132
|
if (!map.mappings || !map.sources || map.sources.length === 0) return null;
|
|
93
133
|
|
|
94
|
-
// Resolve original source path relative to the .map file location
|
|
95
134
|
const mapDir = path.dirname(mapFilePath);
|
|
96
135
|
const sourceRoot = map.sourceRoot || '';
|
|
97
|
-
|
|
136
|
+
|
|
137
|
+
// Resolve all source paths, filtering out non-user sources (webpack internals)
|
|
138
|
+
const resolvedSources = map.sources.map((s: string) => resolveSourcePath(s, mapDir, sourceRoot, jsFilePath));
|
|
139
|
+
|
|
140
|
+
// Find the first user source file (skip webpack bootstrap/runtime)
|
|
141
|
+
let primarySourceIdx = 0;
|
|
142
|
+
for (let i = 0; i < resolvedSources.length; i++) {
|
|
143
|
+
const s = map.sources[i] as string;
|
|
144
|
+
if (!s.includes('webpack/bootstrap') && !s.includes('webpack/runtime') && fs.existsSync(resolvedSources[i])) {
|
|
145
|
+
primarySourceIdx = i;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const originalFile = resolvedSources[primarySourceIdx];
|
|
98
150
|
|
|
99
151
|
// Decode mappings: semicolons separate lines, commas separate segments
|
|
100
|
-
// VLQ values are cumulative across ALL segments (not just first per line)
|
|
101
|
-
//
|
|
152
|
+
// VLQ values are cumulative across ALL segments (not just first per line).
|
|
153
|
+
// For multi-source maps, also track the source index to map lines to different files.
|
|
102
154
|
const lineMap = new Map<number, number>();
|
|
155
|
+
/** Map from generated line → resolved source file path */
|
|
156
|
+
const lineSourceMap = new Map<number, string>();
|
|
103
157
|
const lines = map.mappings.split(';');
|
|
104
|
-
let sourceLine = 0;
|
|
158
|
+
let sourceLine = 0;
|
|
159
|
+
let sourceIdx = 0;
|
|
105
160
|
|
|
106
161
|
for (let genLine = 0; genLine < lines.length; genLine++) {
|
|
107
162
|
const line = lines[genLine];
|
|
@@ -112,19 +167,22 @@ function parseSourceMap(mapJson: string, mapFilePath: string): SourceMapData | n
|
|
|
112
167
|
for (const seg of segments) {
|
|
113
168
|
if (!seg) continue;
|
|
114
169
|
const decoded = decodeVLQ(seg);
|
|
115
|
-
// decoded: [genCol, sourceIdx, sourceLine, sourceCol, ...]
|
|
116
170
|
if (decoded.length >= 3) {
|
|
117
|
-
|
|
118
|
-
|
|
171
|
+
if (decoded.length >= 2) sourceIdx += decoded[1]; // Update source index
|
|
172
|
+
sourceLine += decoded[2];
|
|
119
173
|
if (!firstSegmentMapped) {
|
|
120
|
-
lineMap.set(genLine + 1, sourceLine + 1);
|
|
174
|
+
lineMap.set(genLine + 1, sourceLine + 1);
|
|
175
|
+
// Track which source file this line belongs to
|
|
176
|
+
if (sourceIdx >= 0 && sourceIdx < resolvedSources.length) {
|
|
177
|
+
lineSourceMap.set(genLine + 1, resolvedSources[sourceIdx]);
|
|
178
|
+
}
|
|
121
179
|
firstSegmentMapped = true;
|
|
122
180
|
}
|
|
123
181
|
}
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
184
|
|
|
127
|
-
return { lineMap, originalFile, sources: map.sources };
|
|
185
|
+
return { lineMap, originalFile, sources: map.sources, lineSourceMap };
|
|
128
186
|
} catch {
|
|
129
187
|
return null;
|
|
130
188
|
}
|
|
@@ -146,7 +204,7 @@ function loadSourceMap(jsFilePath: string, source: string): SourceMapData | null
|
|
|
146
204
|
const mapPath = path.resolve(path.dirname(jsFilePath), url);
|
|
147
205
|
if (fs.existsSync(mapPath)) {
|
|
148
206
|
const mapJson = fs.readFileSync(mapPath, 'utf8');
|
|
149
|
-
return parseSourceMap(mapJson, mapPath);
|
|
207
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
|
|
@@ -154,7 +212,7 @@ function loadSourceMap(jsFilePath: string, source: string): SourceMapData | null
|
|
|
154
212
|
const mapPath = jsFilePath + '.map';
|
|
155
213
|
if (fs.existsSync(mapPath)) {
|
|
156
214
|
const mapJson = fs.readFileSync(mapPath, 'utf8');
|
|
157
|
-
return parseSourceMap(mapJson, mapPath);
|
|
215
|
+
return parseSourceMap(mapJson, mapPath, jsFilePath);
|
|
158
216
|
}
|
|
159
217
|
|
|
160
218
|
return null;
|
|
@@ -322,6 +380,9 @@ function findVarDeclarations(source: string, lineOffset: number = 0): Array<{ li
|
|
|
322
380
|
// Skip esbuild helpers
|
|
323
381
|
if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
|
|
324
382
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
|
|
383
|
+
// Skip webpack internals
|
|
384
|
+
if (varName.startsWith('__webpack_')) continue;
|
|
385
|
+
if (varName === '__unused_webpack_module') continue;
|
|
325
386
|
// Skip React Refresh / HMR internals
|
|
326
387
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
|
|
327
388
|
if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
|
|
@@ -594,14 +655,23 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
594
655
|
// are stripped from the compiled JS, shifting line numbers. The only accurate way to get correct
|
|
595
656
|
// line numbers is to read the original .ts source file and parse it directly.
|
|
596
657
|
// For plain JS files, we parse the source directly.
|
|
597
|
-
let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
|
|
598
|
-
let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number }> = [];
|
|
658
|
+
let varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number; sourceFile?: string }> = [];
|
|
659
|
+
let destructInsertions: Array<{ lineEnd: number; varNames: string[]; lineNo: number; sourceFile?: string }> = [];
|
|
599
660
|
|
|
600
661
|
// Helper: remap line numbers using source map if available
|
|
601
662
|
const remapLine = sourceMap
|
|
602
663
|
? (line: number) => mapLineToOriginal(sourceMap, line)
|
|
603
664
|
: (line: number) => line;
|
|
604
665
|
|
|
666
|
+
// Helper: get source file for a compiled line (for multi-source bundles)
|
|
667
|
+
const hasMultiSource = sourceMap?.lineSourceMap && sourceMap.lineSourceMap.size > 0;
|
|
668
|
+
const getSourceFile = hasMultiSource
|
|
669
|
+
? (compiledLine: number) => {
|
|
670
|
+
const file = sourceMap!.lineSourceMap!.get(compiledLine);
|
|
671
|
+
return file && !file.includes('webpack/bootstrap') && !file.includes('webpack/runtime') ? file : undefined;
|
|
672
|
+
}
|
|
673
|
+
: (_: number) => undefined as string | undefined;
|
|
674
|
+
|
|
605
675
|
if (varTraceEnabled) {
|
|
606
676
|
const isTsFile = /\.[mc]?tsx?$/.test(filename);
|
|
607
677
|
if (isTsFile && !sourceMap) {
|
|
@@ -663,10 +733,12 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
663
733
|
// Plain JS or source-map-assisted: parse compiled source, then remap lines
|
|
664
734
|
varInsertions = findVarDeclarations(source).map(ins => ({
|
|
665
735
|
...ins,
|
|
736
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
666
737
|
lineNo: remapLine(ins.lineNo),
|
|
667
738
|
}));
|
|
668
739
|
destructInsertions = findDestructuredDeclarations(source).map(ins => ({
|
|
669
740
|
...ins,
|
|
741
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
670
742
|
lineNo: remapLine(ins.lineNo),
|
|
671
743
|
}));
|
|
672
744
|
}
|
|
@@ -678,14 +750,17 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
678
750
|
// Apply source map remapping to these too.
|
|
679
751
|
const reassignInsertions = findReassignments(source).map(ins => ({
|
|
680
752
|
...ins,
|
|
753
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
681
754
|
lineNo: remapLine(ins.lineNo),
|
|
682
755
|
}));
|
|
683
756
|
const forLoopInsertions = findForLoopVars(source).map(ins => ({
|
|
684
757
|
...ins,
|
|
758
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
685
759
|
lineNo: remapLine(ins.lineNo),
|
|
686
760
|
}));
|
|
687
761
|
const catchInsertions = findCatchVars(source).map(ins => ({
|
|
688
762
|
...ins,
|
|
763
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
689
764
|
lineNo: remapLine(ins.lineNo),
|
|
690
765
|
}));
|
|
691
766
|
|
|
@@ -726,9 +801,10 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
726
801
|
const traceModuleName = sourceMap
|
|
727
802
|
? path.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
|
|
728
803
|
: moduleName;
|
|
804
|
+
|
|
729
805
|
prefixLines.push(
|
|
730
806
|
`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
|
|
731
|
-
`var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(traceModuleName)}, ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
|
|
807
|
+
`var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
|
|
732
808
|
);
|
|
733
809
|
}
|
|
734
810
|
|
|
@@ -747,15 +823,23 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
747
823
|
});
|
|
748
824
|
}
|
|
749
825
|
|
|
750
|
-
|
|
826
|
+
// Helper to generate source file args for __trickle_tv calls (for multi-source bundles)
|
|
827
|
+
const sfArgs = (sourceFile?: string) => {
|
|
828
|
+
if (!sourceFile) return '';
|
|
829
|
+
const mod = path.basename(sourceFile).replace(/\.[jt]sx?$/, '');
|
|
830
|
+
return `,${JSON.stringify(mod)},${JSON.stringify(sourceFile)}`;
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of varInsertions) {
|
|
751
834
|
allInsertions.push({
|
|
752
835
|
position: lineEnd,
|
|
753
|
-
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
836
|
+
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
754
837
|
});
|
|
755
838
|
}
|
|
756
839
|
|
|
757
|
-
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
758
|
-
const
|
|
840
|
+
for (const { lineEnd, varNames, lineNo, sourceFile } of destructInsertions) {
|
|
841
|
+
const sf = sfArgs(sourceFile);
|
|
842
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
759
843
|
allInsertions.push({
|
|
760
844
|
position: lineEnd,
|
|
761
845
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
@@ -763,16 +847,17 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
763
847
|
}
|
|
764
848
|
|
|
765
849
|
// Reassignment insertions
|
|
766
|
-
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
850
|
+
for (const { lineEnd, varName, lineNo, sourceFile } of reassignInsertions) {
|
|
767
851
|
allInsertions.push({
|
|
768
852
|
position: lineEnd,
|
|
769
|
-
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
853
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo}${sfArgs(sourceFile)})}catch(__e){}\n`,
|
|
770
854
|
});
|
|
771
855
|
}
|
|
772
856
|
|
|
773
857
|
// For-loop variable insertions
|
|
774
|
-
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
775
|
-
const
|
|
858
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of forLoopInsertions) {
|
|
859
|
+
const sf = sfArgs(sourceFile);
|
|
860
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
776
861
|
allInsertions.push({
|
|
777
862
|
position: bodyStart,
|
|
778
863
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
@@ -780,8 +865,9 @@ function transformCjsSource(source: string, filename: string, moduleName: string
|
|
|
780
865
|
}
|
|
781
866
|
|
|
782
867
|
// Catch clause insertions
|
|
783
|
-
for (const { bodyStart, varNames, lineNo } of catchInsertions) {
|
|
784
|
-
const
|
|
868
|
+
for (const { bodyStart, varNames, lineNo, sourceFile } of catchInsertions) {
|
|
869
|
+
const sf = sfArgs(sourceFile);
|
|
870
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
785
871
|
allInsertions.push({
|
|
786
872
|
position: bodyStart,
|
|
787
873
|
code: `\ntry{${calls}}catch(__e2){}\n`,
|
package/src/trace-var.ts
CHANGED
|
@@ -26,6 +26,9 @@ let debugMode = false;
|
|
|
26
26
|
|
|
27
27
|
/** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
|
|
28
28
|
const varCache = new Map<string, { fp: string; ts: number }>();
|
|
29
|
+
/** Per-line sample count to avoid loop variable spam */
|
|
30
|
+
const sampleCount = new Map<string, number>();
|
|
31
|
+
const MAX_SAMPLES_PER_LINE = 5;
|
|
29
32
|
|
|
30
33
|
/** Batch buffer for writing — avoids one fs.appendFileSync per variable */
|
|
31
34
|
let varBuffer: string[] = [];
|
|
@@ -92,8 +95,12 @@ export function traceVar(
|
|
|
92
95
|
const dummyArgs: TypeNode = { kind: 'tuple', elements: [] };
|
|
93
96
|
const typeHash = hashType(dummyArgs, type);
|
|
94
97
|
|
|
95
|
-
//
|
|
98
|
+
// Per-line sample count limit: stop after N samples to avoid loop spam
|
|
96
99
|
const cacheKey = `${filePath}:${line}:${varName}`;
|
|
100
|
+
const cnt = sampleCount.get(cacheKey) || 0;
|
|
101
|
+
if (cnt >= MAX_SAMPLES_PER_LINE) return;
|
|
102
|
+
|
|
103
|
+
// Value-aware dedup: re-send if value changed or 10s elapsed
|
|
97
104
|
const t = typeof value;
|
|
98
105
|
const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
|
|
99
106
|
? String(value).substring(0, 60)
|
|
@@ -102,6 +109,7 @@ export function traceVar(
|
|
|
102
109
|
const prev = varCache.get(cacheKey);
|
|
103
110
|
if (prev && prev.fp === fp && (now - prev.ts) < 10000) return;
|
|
104
111
|
varCache.set(cacheKey, { fp, ts: now });
|
|
112
|
+
sampleCount.set(cacheKey, cnt + 1);
|
|
105
113
|
|
|
106
114
|
const sample = sanitizeVarSample(value);
|
|
107
115
|
|
package/src/vite-plugin.ts
CHANGED
|
@@ -263,6 +263,9 @@ function findVarDeclarations(source: string): Array<{ lineEnd: number; varName:
|
|
|
263
263
|
// Skip esbuild helpers
|
|
264
264
|
if (varName === '__defProp' || varName === '__defNormalProp' || varName === '__publicField' || varName === '__getOwnPropNames') continue;
|
|
265
265
|
if (varName === '__commonJS' || varName === '__toCommonJS' || varName === '__export' || varName === '__copyProps') continue;
|
|
266
|
+
// Skip webpack internals
|
|
267
|
+
if (varName.startsWith('__webpack_')) continue;
|
|
268
|
+
if (varName === '__unused_webpack_module') continue;
|
|
266
269
|
// Skip React Refresh / HMR internals (Vite, webpack, Next.js inject these)
|
|
267
270
|
if (varName === 'prevRefreshReg' || varName === 'prevRefreshSig' || varName === 'inWebWorker' || varName === 'invalidateMessage') continue;
|
|
268
271
|
if (varName === '_s' || varName === '_c2' || varName === '_s2') continue;
|
|
@@ -1624,6 +1627,8 @@ export function transformEsmSource(
|
|
|
1624
1627
|
prefixLines.push(
|
|
1625
1628
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
1626
1629
|
` const _cache = new Map();`,
|
|
1630
|
+
` const _sampleCount = new Map();`,
|
|
1631
|
+
` const _MAX_SAMPLES = 5;`,
|
|
1627
1632
|
` function _inferType(v, d) {`,
|
|
1628
1633
|
` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`,
|
|
1629
1634
|
` if (v === null) return { kind: 'primitive', name: 'null' };`,
|
|
@@ -1660,10 +1665,13 @@ export function transformEsmSource(
|
|
|
1660
1665
|
` const sample = _sanitize(v, 2);`,
|
|
1661
1666
|
` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`,
|
|
1662
1667
|
` const ck = file + ':' + l + ':' + n;`,
|
|
1668
|
+
` const cnt = _sampleCount.get(ck) || 0;`,
|
|
1669
|
+
` if (cnt >= _MAX_SAMPLES) return;`,
|
|
1663
1670
|
` const prev = _cache.get(ck);`,
|
|
1664
1671
|
` const now = Date.now();`,
|
|
1665
1672
|
` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`,
|
|
1666
1673
|
` _cache.set(ck, { sv: sv, ts: now });`,
|
|
1674
|
+
` _sampleCount.set(ck, cnt + 1);`,
|
|
1667
1675
|
` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`,
|
|
1668
1676
|
` } catch(e) {}`,
|
|
1669
1677
|
` };`,
|