trickle-observe 0.2.83 → 0.2.85
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/trace-var.js +9 -1
- package/dist/vite-plugin.d.ts +7 -0
- package/dist/vite-plugin.js +75 -1
- package/dist-esm/vite-plugin.js +74 -1
- package/package.json +1 -1
- package/src/trace-var.ts +9 -1
- package/src/vite-plugin.ts +62 -0
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.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare function tricklePlugin(options?: TricklePluginOptions): {
|
|
|
47
47
|
* Returns -1 if not found.
|
|
48
48
|
*/
|
|
49
49
|
export declare function findFunctionBodyBrace(source: string, afterOpenParen: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Find class body ranges in source code. Handles both:
|
|
52
|
+
* class Foo { ... }
|
|
53
|
+
* var Foo = class { ... }
|
|
54
|
+
* Returns an array of [start, end] positions (inclusive of braces).
|
|
55
|
+
*/
|
|
56
|
+
export declare function findClassBodyRanges(source: string): Array<[number, number]>;
|
|
50
57
|
/**
|
|
51
58
|
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
52
59
|
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
package/dist/vite-plugin.js
CHANGED
|
@@ -24,6 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.tricklePlugin = tricklePlugin;
|
|
26
26
|
exports.findFunctionBodyBrace = findFunctionBodyBrace;
|
|
27
|
+
exports.findClassBodyRanges = findClassBodyRanges;
|
|
27
28
|
exports.findReassignments = findReassignments;
|
|
28
29
|
exports.findCatchVars = findCatchVars;
|
|
29
30
|
exports.findForLoopVars = findForLoopVars;
|
|
@@ -504,6 +505,73 @@ function extractDestructuredNames(pattern) {
|
|
|
504
505
|
}
|
|
505
506
|
return names;
|
|
506
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Find class body ranges in source code. Handles both:
|
|
510
|
+
* class Foo { ... }
|
|
511
|
+
* var Foo = class { ... }
|
|
512
|
+
* Returns an array of [start, end] positions (inclusive of braces).
|
|
513
|
+
*/
|
|
514
|
+
function findClassBodyRanges(source) {
|
|
515
|
+
const ranges = [];
|
|
516
|
+
// Match both class declarations and class expressions
|
|
517
|
+
const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
|
|
518
|
+
let m;
|
|
519
|
+
while ((m = classRegex.exec(source)) !== null) {
|
|
520
|
+
const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
|
|
521
|
+
if (openBrace === -1)
|
|
522
|
+
continue;
|
|
523
|
+
// Find matching close brace
|
|
524
|
+
let depth = 1;
|
|
525
|
+
let pos = openBrace + 1;
|
|
526
|
+
while (pos < source.length && depth > 0) {
|
|
527
|
+
const ch = source[pos];
|
|
528
|
+
if (ch === '{')
|
|
529
|
+
depth++;
|
|
530
|
+
else if (ch === '}') {
|
|
531
|
+
depth--;
|
|
532
|
+
if (depth === 0)
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
536
|
+
const q = ch;
|
|
537
|
+
pos++;
|
|
538
|
+
while (pos < source.length) {
|
|
539
|
+
if (source[pos] === '\\')
|
|
540
|
+
pos++;
|
|
541
|
+
else if (source[pos] === q)
|
|
542
|
+
break;
|
|
543
|
+
else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
|
|
544
|
+
pos += 2;
|
|
545
|
+
let td = 1;
|
|
546
|
+
while (pos < source.length && td > 0) {
|
|
547
|
+
if (source[pos] === '{')
|
|
548
|
+
td++;
|
|
549
|
+
else if (source[pos] === '}')
|
|
550
|
+
td--;
|
|
551
|
+
pos++;
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
pos++;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (ch === '/' && source[pos + 1] === '/') {
|
|
559
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
560
|
+
pos++;
|
|
561
|
+
}
|
|
562
|
+
else if (ch === '/' && source[pos + 1] === '*') {
|
|
563
|
+
pos += 2;
|
|
564
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
565
|
+
pos++;
|
|
566
|
+
pos++;
|
|
567
|
+
}
|
|
568
|
+
pos++;
|
|
569
|
+
}
|
|
570
|
+
if (depth === 0)
|
|
571
|
+
ranges.push([openBrace, pos]);
|
|
572
|
+
}
|
|
573
|
+
return ranges;
|
|
574
|
+
}
|
|
507
575
|
/**
|
|
508
576
|
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
509
577
|
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
@@ -513,6 +581,8 @@ function extractDestructuredNames(pattern) {
|
|
|
513
581
|
*/
|
|
514
582
|
function findReassignments(source) {
|
|
515
583
|
const results = [];
|
|
584
|
+
// Pre-compute class body ranges to skip class field declarations
|
|
585
|
+
const classRanges = findClassBodyRanges(source);
|
|
516
586
|
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
517
587
|
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
518
588
|
// Plain: = (but not ==, ===, =>, !=)
|
|
@@ -548,6 +618,10 @@ function findReassignments(source) {
|
|
|
548
618
|
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
549
619
|
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
|
|
550
620
|
continue;
|
|
621
|
+
// Skip class field declarations (e.g., `tasks = []` inside a class body)
|
|
622
|
+
// Inserting trace calls inside class bodies causes SyntaxError
|
|
623
|
+
if (classRanges.some(([start, end]) => match.index > start && match.index < end))
|
|
624
|
+
continue;
|
|
551
625
|
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
552
626
|
// or if it's a label (label: ...)
|
|
553
627
|
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
@@ -1530,7 +1604,7 @@ ingestUrl) {
|
|
|
1530
1604
|
}
|
|
1531
1605
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1532
1606
|
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1533
|
-
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) {} }`);
|
|
1607
|
+
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) {} }`);
|
|
1534
1608
|
}
|
|
1535
1609
|
// Add React component render tracker if needed
|
|
1536
1610
|
if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -492,6 +492,73 @@ function extractDestructuredNames(pattern) {
|
|
|
492
492
|
}
|
|
493
493
|
return names;
|
|
494
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Find class body ranges in source code. Handles both:
|
|
497
|
+
* class Foo { ... }
|
|
498
|
+
* var Foo = class { ... }
|
|
499
|
+
* Returns an array of [start, end] positions (inclusive of braces).
|
|
500
|
+
*/
|
|
501
|
+
export function findClassBodyRanges(source) {
|
|
502
|
+
const ranges = [];
|
|
503
|
+
// Match both class declarations and class expressions
|
|
504
|
+
const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
|
|
505
|
+
let m;
|
|
506
|
+
while ((m = classRegex.exec(source)) !== null) {
|
|
507
|
+
const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
|
|
508
|
+
if (openBrace === -1)
|
|
509
|
+
continue;
|
|
510
|
+
// Find matching close brace
|
|
511
|
+
let depth = 1;
|
|
512
|
+
let pos = openBrace + 1;
|
|
513
|
+
while (pos < source.length && depth > 0) {
|
|
514
|
+
const ch = source[pos];
|
|
515
|
+
if (ch === '{')
|
|
516
|
+
depth++;
|
|
517
|
+
else if (ch === '}') {
|
|
518
|
+
depth--;
|
|
519
|
+
if (depth === 0)
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
523
|
+
const q = ch;
|
|
524
|
+
pos++;
|
|
525
|
+
while (pos < source.length) {
|
|
526
|
+
if (source[pos] === '\\')
|
|
527
|
+
pos++;
|
|
528
|
+
else if (source[pos] === q)
|
|
529
|
+
break;
|
|
530
|
+
else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
|
|
531
|
+
pos += 2;
|
|
532
|
+
let td = 1;
|
|
533
|
+
while (pos < source.length && td > 0) {
|
|
534
|
+
if (source[pos] === '{')
|
|
535
|
+
td++;
|
|
536
|
+
else if (source[pos] === '}')
|
|
537
|
+
td--;
|
|
538
|
+
pos++;
|
|
539
|
+
}
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
pos++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (ch === '/' && source[pos + 1] === '/') {
|
|
546
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
547
|
+
pos++;
|
|
548
|
+
}
|
|
549
|
+
else if (ch === '/' && source[pos + 1] === '*') {
|
|
550
|
+
pos += 2;
|
|
551
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
552
|
+
pos++;
|
|
553
|
+
pos++;
|
|
554
|
+
}
|
|
555
|
+
pos++;
|
|
556
|
+
}
|
|
557
|
+
if (depth === 0)
|
|
558
|
+
ranges.push([openBrace, pos]);
|
|
559
|
+
}
|
|
560
|
+
return ranges;
|
|
561
|
+
}
|
|
495
562
|
/**
|
|
496
563
|
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
497
564
|
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
@@ -501,6 +568,8 @@ function extractDestructuredNames(pattern) {
|
|
|
501
568
|
*/
|
|
502
569
|
export function findReassignments(source) {
|
|
503
570
|
const results = [];
|
|
571
|
+
// Pre-compute class body ranges to skip class field declarations
|
|
572
|
+
const classRanges = findClassBodyRanges(source);
|
|
504
573
|
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
505
574
|
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
506
575
|
// Plain: = (but not ==, ===, =>, !=)
|
|
@@ -536,6 +605,10 @@ export function findReassignments(source) {
|
|
|
536
605
|
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
537
606
|
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
|
|
538
607
|
continue;
|
|
608
|
+
// Skip class field declarations (e.g., `tasks = []` inside a class body)
|
|
609
|
+
// Inserting trace calls inside class bodies causes SyntaxError
|
|
610
|
+
if (classRanges.some(([start, end]) => match.index > start && match.index < end))
|
|
611
|
+
continue;
|
|
539
612
|
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
540
613
|
// or if it's a label (label: ...)
|
|
541
614
|
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
@@ -1518,7 +1591,7 @@ ingestUrl) {
|
|
|
1518
1591
|
}
|
|
1519
1592
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1520
1593
|
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1521
|
-
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) {} }`);
|
|
1594
|
+
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) {} }`);
|
|
1522
1595
|
}
|
|
1523
1596
|
// Add React component render tracker if needed
|
|
1524
1597
|
if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
|
package/package.json
CHANGED
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
|
@@ -472,6 +472,57 @@ function extractDestructuredNames(pattern: string): string[] {
|
|
|
472
472
|
return names;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Find class body ranges in source code. Handles both:
|
|
477
|
+
* class Foo { ... }
|
|
478
|
+
* var Foo = class { ... }
|
|
479
|
+
* Returns an array of [start, end] positions (inclusive of braces).
|
|
480
|
+
*/
|
|
481
|
+
export function findClassBodyRanges(source: string): Array<[number, number]> {
|
|
482
|
+
const ranges: Array<[number, number]> = [];
|
|
483
|
+
// Match both class declarations and class expressions
|
|
484
|
+
const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
|
|
485
|
+
let m;
|
|
486
|
+
while ((m = classRegex.exec(source)) !== null) {
|
|
487
|
+
const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
|
|
488
|
+
if (openBrace === -1) continue;
|
|
489
|
+
// Find matching close brace
|
|
490
|
+
let depth = 1;
|
|
491
|
+
let pos = openBrace + 1;
|
|
492
|
+
while (pos < source.length && depth > 0) {
|
|
493
|
+
const ch = source[pos];
|
|
494
|
+
if (ch === '{') depth++;
|
|
495
|
+
else if (ch === '}') { depth--; if (depth === 0) break; }
|
|
496
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
497
|
+
const q = ch; pos++;
|
|
498
|
+
while (pos < source.length) {
|
|
499
|
+
if (source[pos] === '\\') pos++;
|
|
500
|
+
else if (source[pos] === q) break;
|
|
501
|
+
else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
|
|
502
|
+
pos += 2; let td = 1;
|
|
503
|
+
while (pos < source.length && td > 0) {
|
|
504
|
+
if (source[pos] === '{') td++;
|
|
505
|
+
else if (source[pos] === '}') td--;
|
|
506
|
+
pos++;
|
|
507
|
+
}
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
pos++;
|
|
511
|
+
}
|
|
512
|
+
} else if (ch === '/' && source[pos + 1] === '/') {
|
|
513
|
+
while (pos < source.length && source[pos] !== '\n') pos++;
|
|
514
|
+
} else if (ch === '/' && source[pos + 1] === '*') {
|
|
515
|
+
pos += 2;
|
|
516
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
|
|
517
|
+
pos++;
|
|
518
|
+
}
|
|
519
|
+
pos++;
|
|
520
|
+
}
|
|
521
|
+
if (depth === 0) ranges.push([openBrace, pos]);
|
|
522
|
+
}
|
|
523
|
+
return ranges;
|
|
524
|
+
}
|
|
525
|
+
|
|
475
526
|
/**
|
|
476
527
|
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
477
528
|
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
@@ -481,6 +532,8 @@ function extractDestructuredNames(pattern: string): string[] {
|
|
|
481
532
|
*/
|
|
482
533
|
export function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
|
|
483
534
|
const results: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
|
|
535
|
+
// Pre-compute class body ranges to skip class field declarations
|
|
536
|
+
const classRanges = findClassBodyRanges(source);
|
|
484
537
|
|
|
485
538
|
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
486
539
|
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
@@ -513,6 +566,10 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
|
|
|
513
566
|
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
514
567
|
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart())) continue;
|
|
515
568
|
|
|
569
|
+
// Skip class field declarations (e.g., `tasks = []` inside a class body)
|
|
570
|
+
// Inserting trace calls inside class bodies causes SyntaxError
|
|
571
|
+
if (classRanges.some(([start, end]) => match!.index > start && match!.index < end)) continue;
|
|
572
|
+
|
|
516
573
|
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
517
574
|
// or if it's a label (label: ...)
|
|
518
575
|
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
@@ -1567,6 +1624,8 @@ export function transformEsmSource(
|
|
|
1567
1624
|
prefixLines.push(
|
|
1568
1625
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
1569
1626
|
` const _cache = new Map();`,
|
|
1627
|
+
` const _sampleCount = new Map();`,
|
|
1628
|
+
` const _MAX_SAMPLES = 5;`,
|
|
1570
1629
|
` function _inferType(v, d) {`,
|
|
1571
1630
|
` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`,
|
|
1572
1631
|
` if (v === null) return { kind: 'primitive', name: 'null' };`,
|
|
@@ -1603,10 +1662,13 @@ export function transformEsmSource(
|
|
|
1603
1662
|
` const sample = _sanitize(v, 2);`,
|
|
1604
1663
|
` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`,
|
|
1605
1664
|
` const ck = file + ':' + l + ':' + n;`,
|
|
1665
|
+
` const cnt = _sampleCount.get(ck) || 0;`,
|
|
1666
|
+
` if (cnt >= _MAX_SAMPLES) return;`,
|
|
1606
1667
|
` const prev = _cache.get(ck);`,
|
|
1607
1668
|
` const now = Date.now();`,
|
|
1608
1669
|
` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`,
|
|
1609
1670
|
` _cache.set(ck, { sv: sv, ts: now });`,
|
|
1671
|
+
` _sampleCount.set(ck, cnt + 1);`,
|
|
1610
1672
|
` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`,
|
|
1611
1673
|
` } catch(e) {}`,
|
|
1612
1674
|
` };`,
|