trickle-observe 0.2.54 → 0.2.55
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/vite-plugin.js +85 -4
- package/dist-esm/vite-plugin.js +85 -4
- package/package.json +1 -1
- package/src/vite-plugin.ts +97 -4
package/dist/vite-plugin.js
CHANGED
|
@@ -90,6 +90,54 @@ function tricklePlugin(options = {}) {
|
|
|
90
90
|
},
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Find the opening brace of a function body, skipping the parameter list.
|
|
95
|
+
* Starting from the character right after the opening `(` of the parameter list,
|
|
96
|
+
* scans forward matching parens to find the closing `)`, then finds the `{` after it.
|
|
97
|
+
* Returns -1 if not found.
|
|
98
|
+
*/
|
|
99
|
+
function findFunctionBodyBrace(source, afterOpenParen) {
|
|
100
|
+
let depth = 1;
|
|
101
|
+
let pos = afterOpenParen;
|
|
102
|
+
// Skip the parameter list (matching parens)
|
|
103
|
+
while (pos < source.length && depth > 0) {
|
|
104
|
+
const ch = source[pos];
|
|
105
|
+
if (ch === '(')
|
|
106
|
+
depth++;
|
|
107
|
+
else if (ch === ')') {
|
|
108
|
+
depth--;
|
|
109
|
+
if (depth === 0)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
113
|
+
const quote = ch;
|
|
114
|
+
pos++;
|
|
115
|
+
while (pos < source.length && source[pos] !== quote) {
|
|
116
|
+
if (source[pos] === '\\')
|
|
117
|
+
pos++;
|
|
118
|
+
pos++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
pos++;
|
|
122
|
+
}
|
|
123
|
+
// Now find the `{` after the closing `)`
|
|
124
|
+
while (pos < source.length) {
|
|
125
|
+
const ch = source[pos];
|
|
126
|
+
if (ch === '{')
|
|
127
|
+
return pos;
|
|
128
|
+
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r' && ch !== ':') {
|
|
129
|
+
// Hit something unexpected (like '=>' for arrows, or type annotation chars)
|
|
130
|
+
if (ch === '=' && pos + 1 < source.length && source[pos + 1] === '>') {
|
|
131
|
+
// Arrow — find { after =>
|
|
132
|
+
pos += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Type annotation — keep going (`: ReturnType`)
|
|
136
|
+
}
|
|
137
|
+
pos++;
|
|
138
|
+
}
|
|
139
|
+
return -1;
|
|
140
|
+
}
|
|
93
141
|
/**
|
|
94
142
|
* Find the closing brace position for a function body starting at `openBrace`.
|
|
95
143
|
*/
|
|
@@ -446,19 +494,24 @@ function escapeRegexStr(str) {
|
|
|
446
494
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
447
495
|
}
|
|
448
496
|
function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource) {
|
|
497
|
+
// Detect React files for component render tracking
|
|
498
|
+
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
449
499
|
// Match top-level and nested function declarations (including async, export)
|
|
450
500
|
const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
451
501
|
const funcInsertions = [];
|
|
502
|
+
// Body insertions: insert at start of function body (for React render tracking)
|
|
503
|
+
const bodyInsertions = [];
|
|
452
504
|
let match;
|
|
453
505
|
while ((match = funcRegex.exec(source)) !== null) {
|
|
454
506
|
const name = match[1];
|
|
455
507
|
if (name === 'require' || name === 'exports' || name === 'module')
|
|
456
508
|
continue;
|
|
457
509
|
const afterMatch = match.index + match[0].length;
|
|
458
|
-
|
|
510
|
+
// Use findFunctionBodyBrace to correctly skip destructured params like ({ a, b }) =>
|
|
511
|
+
const openBrace = findFunctionBodyBrace(source, afterMatch);
|
|
459
512
|
if (openBrace === -1)
|
|
460
513
|
continue;
|
|
461
|
-
// Extract parameter names
|
|
514
|
+
// Extract parameter names (between the opening ( and the body {)
|
|
462
515
|
const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
|
|
463
516
|
const paramNames = paramStr
|
|
464
517
|
? paramStr.split(',').map(p => {
|
|
@@ -472,6 +525,15 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
472
525
|
if (closeBrace === -1)
|
|
473
526
|
continue;
|
|
474
527
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
528
|
+
// React component render tracking: uppercase function name in .tsx/.jsx
|
|
529
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
530
|
+
let lineNo = 1;
|
|
531
|
+
for (let i = 0; i < match.index; i++) {
|
|
532
|
+
if (source[i] === '\n')
|
|
533
|
+
lineNo++;
|
|
534
|
+
}
|
|
535
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
536
|
+
}
|
|
475
537
|
}
|
|
476
538
|
// Also match arrow functions assigned to const/let/var
|
|
477
539
|
const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
|
|
@@ -498,12 +560,21 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
498
560
|
if (closeBrace === -1)
|
|
499
561
|
continue;
|
|
500
562
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
563
|
+
// React component render tracking: uppercase arrow function in .tsx/.jsx
|
|
564
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
565
|
+
let lineNo = 1;
|
|
566
|
+
for (let i = 0; i < match.index; i++) {
|
|
567
|
+
if (source[i] === '\n')
|
|
568
|
+
lineNo++;
|
|
569
|
+
}
|
|
570
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
571
|
+
}
|
|
501
572
|
}
|
|
502
573
|
// Find variable declarations for tracing
|
|
503
574
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
504
575
|
// Find destructured variable declarations for tracing
|
|
505
576
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
506
|
-
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0)
|
|
577
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && bodyInsertions.length === 0)
|
|
507
578
|
return source;
|
|
508
579
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
509
580
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -528,7 +599,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
528
599
|
const importLines = [
|
|
529
600
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
530
601
|
];
|
|
531
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
602
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0) {
|
|
532
603
|
importLines.push(`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`, `import { join as __trickle_join } from 'node:path';`);
|
|
533
604
|
}
|
|
534
605
|
const prefixLines = [
|
|
@@ -555,6 +626,10 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
555
626
|
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
556
627
|
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Set();`, ` let _varsFile = null;`, ` 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 {`, ` if (!_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` _varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const ck = file + ':' + l + ':' + n + ':' + th;`, ` if (_cache.has(ck)) return;`, ` _cache.add(ck);`, ` __trickle_appendFileSync(_varsFile, JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }) + '\\n');`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
557
628
|
}
|
|
629
|
+
// Add React component render tracker if needed
|
|
630
|
+
if (bodyInsertions.length > 0) {
|
|
631
|
+
prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `function __trickle_rc(name, line) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` __trickle_appendFileSync(f, JSON.stringify({ kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 }) + '\\n');`, ` } catch(e) {}`, `}`);
|
|
632
|
+
}
|
|
558
633
|
prefixLines.push('');
|
|
559
634
|
const prefix = prefixLines.join('\n');
|
|
560
635
|
const allInsertions = [];
|
|
@@ -578,6 +653,12 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
578
653
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
579
654
|
});
|
|
580
655
|
}
|
|
656
|
+
for (const { position, name, lineNo } of bodyInsertions) {
|
|
657
|
+
allInsertions.push({
|
|
658
|
+
position,
|
|
659
|
+
code: `\ntry{__trickle_rc(${JSON.stringify(name)},${lineNo})}catch(__e){}\n`,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
581
662
|
// Sort by position descending (insert from end to preserve earlier positions)
|
|
582
663
|
allInsertions.sort((a, b) => b.position - a.position);
|
|
583
664
|
let result = source;
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -84,6 +84,54 @@ export function tricklePlugin(options = {}) {
|
|
|
84
84
|
},
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Find the opening brace of a function body, skipping the parameter list.
|
|
89
|
+
* Starting from the character right after the opening `(` of the parameter list,
|
|
90
|
+
* scans forward matching parens to find the closing `)`, then finds the `{` after it.
|
|
91
|
+
* Returns -1 if not found.
|
|
92
|
+
*/
|
|
93
|
+
function findFunctionBodyBrace(source, afterOpenParen) {
|
|
94
|
+
let depth = 1;
|
|
95
|
+
let pos = afterOpenParen;
|
|
96
|
+
// Skip the parameter list (matching parens)
|
|
97
|
+
while (pos < source.length && depth > 0) {
|
|
98
|
+
const ch = source[pos];
|
|
99
|
+
if (ch === '(')
|
|
100
|
+
depth++;
|
|
101
|
+
else if (ch === ')') {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0)
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
107
|
+
const quote = ch;
|
|
108
|
+
pos++;
|
|
109
|
+
while (pos < source.length && source[pos] !== quote) {
|
|
110
|
+
if (source[pos] === '\\')
|
|
111
|
+
pos++;
|
|
112
|
+
pos++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
pos++;
|
|
116
|
+
}
|
|
117
|
+
// Now find the `{` after the closing `)`
|
|
118
|
+
while (pos < source.length) {
|
|
119
|
+
const ch = source[pos];
|
|
120
|
+
if (ch === '{')
|
|
121
|
+
return pos;
|
|
122
|
+
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r' && ch !== ':') {
|
|
123
|
+
// Hit something unexpected (like '=>' for arrows, or type annotation chars)
|
|
124
|
+
if (ch === '=' && pos + 1 < source.length && source[pos + 1] === '>') {
|
|
125
|
+
// Arrow — find { after =>
|
|
126
|
+
pos += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Type annotation — keep going (`: ReturnType`)
|
|
130
|
+
}
|
|
131
|
+
pos++;
|
|
132
|
+
}
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
87
135
|
/**
|
|
88
136
|
* Find the closing brace position for a function body starting at `openBrace`.
|
|
89
137
|
*/
|
|
@@ -440,19 +488,24 @@ function escapeRegexStr(str) {
|
|
|
440
488
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
441
489
|
}
|
|
442
490
|
function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource) {
|
|
491
|
+
// Detect React files for component render tracking
|
|
492
|
+
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
443
493
|
// Match top-level and nested function declarations (including async, export)
|
|
444
494
|
const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
445
495
|
const funcInsertions = [];
|
|
496
|
+
// Body insertions: insert at start of function body (for React render tracking)
|
|
497
|
+
const bodyInsertions = [];
|
|
446
498
|
let match;
|
|
447
499
|
while ((match = funcRegex.exec(source)) !== null) {
|
|
448
500
|
const name = match[1];
|
|
449
501
|
if (name === 'require' || name === 'exports' || name === 'module')
|
|
450
502
|
continue;
|
|
451
503
|
const afterMatch = match.index + match[0].length;
|
|
452
|
-
|
|
504
|
+
// Use findFunctionBodyBrace to correctly skip destructured params like ({ a, b }) =>
|
|
505
|
+
const openBrace = findFunctionBodyBrace(source, afterMatch);
|
|
453
506
|
if (openBrace === -1)
|
|
454
507
|
continue;
|
|
455
|
-
// Extract parameter names
|
|
508
|
+
// Extract parameter names (between the opening ( and the body {)
|
|
456
509
|
const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
|
|
457
510
|
const paramNames = paramStr
|
|
458
511
|
? paramStr.split(',').map(p => {
|
|
@@ -466,6 +519,15 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
466
519
|
if (closeBrace === -1)
|
|
467
520
|
continue;
|
|
468
521
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
522
|
+
// React component render tracking: uppercase function name in .tsx/.jsx
|
|
523
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
524
|
+
let lineNo = 1;
|
|
525
|
+
for (let i = 0; i < match.index; i++) {
|
|
526
|
+
if (source[i] === '\n')
|
|
527
|
+
lineNo++;
|
|
528
|
+
}
|
|
529
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
530
|
+
}
|
|
469
531
|
}
|
|
470
532
|
// Also match arrow functions assigned to const/let/var
|
|
471
533
|
const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
|
|
@@ -492,12 +554,21 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
492
554
|
if (closeBrace === -1)
|
|
493
555
|
continue;
|
|
494
556
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
557
|
+
// React component render tracking: uppercase arrow function in .tsx/.jsx
|
|
558
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
559
|
+
let lineNo = 1;
|
|
560
|
+
for (let i = 0; i < match.index; i++) {
|
|
561
|
+
if (source[i] === '\n')
|
|
562
|
+
lineNo++;
|
|
563
|
+
}
|
|
564
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
565
|
+
}
|
|
495
566
|
}
|
|
496
567
|
// Find variable declarations for tracing
|
|
497
568
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
498
569
|
// Find destructured variable declarations for tracing
|
|
499
570
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
500
|
-
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0)
|
|
571
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && bodyInsertions.length === 0)
|
|
501
572
|
return source;
|
|
502
573
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
503
574
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -522,7 +593,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
522
593
|
const importLines = [
|
|
523
594
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
524
595
|
];
|
|
525
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
596
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0) {
|
|
526
597
|
importLines.push(`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`, `import { join as __trickle_join } from 'node:path';`);
|
|
527
598
|
}
|
|
528
599
|
const prefixLines = [
|
|
@@ -549,6 +620,10 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
549
620
|
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
550
621
|
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Set();`, ` let _varsFile = null;`, ` 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 {`, ` if (!_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` _varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const ck = file + ':' + l + ':' + n + ':' + th;`, ` if (_cache.has(ck)) return;`, ` _cache.add(ck);`, ` __trickle_appendFileSync(_varsFile, JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }) + '\\n');`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
551
622
|
}
|
|
623
|
+
// Add React component render tracker if needed
|
|
624
|
+
if (bodyInsertions.length > 0) {
|
|
625
|
+
prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `function __trickle_rc(name, line) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` __trickle_appendFileSync(f, JSON.stringify({ kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 }) + '\\n');`, ` } catch(e) {}`, `}`);
|
|
626
|
+
}
|
|
552
627
|
prefixLines.push('');
|
|
553
628
|
const prefix = prefixLines.join('\n');
|
|
554
629
|
const allInsertions = [];
|
|
@@ -572,6 +647,12 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
572
647
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
573
648
|
});
|
|
574
649
|
}
|
|
650
|
+
for (const { position, name, lineNo } of bodyInsertions) {
|
|
651
|
+
allInsertions.push({
|
|
652
|
+
position,
|
|
653
|
+
code: `\ntry{__trickle_rc(${JSON.stringify(name)},${lineNo})}catch(__e){}\n`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
575
656
|
// Sort by position descending (insert from end to preserve earlier positions)
|
|
576
657
|
allInsertions.sort((a, b) => b.position - a.position);
|
|
577
658
|
let result = source;
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -104,6 +104,48 @@ export function tricklePlugin(options: TricklePluginOptions = {}) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Find the opening brace of a function body, skipping the parameter list.
|
|
109
|
+
* Starting from the character right after the opening `(` of the parameter list,
|
|
110
|
+
* scans forward matching parens to find the closing `)`, then finds the `{` after it.
|
|
111
|
+
* Returns -1 if not found.
|
|
112
|
+
*/
|
|
113
|
+
function findFunctionBodyBrace(source: string, afterOpenParen: number): number {
|
|
114
|
+
let depth = 1;
|
|
115
|
+
let pos = afterOpenParen;
|
|
116
|
+
// Skip the parameter list (matching parens)
|
|
117
|
+
while (pos < source.length && depth > 0) {
|
|
118
|
+
const ch = source[pos];
|
|
119
|
+
if (ch === '(') depth++;
|
|
120
|
+
else if (ch === ')') { depth--; if (depth === 0) break; }
|
|
121
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
122
|
+
const quote = ch;
|
|
123
|
+
pos++;
|
|
124
|
+
while (pos < source.length && source[pos] !== quote) {
|
|
125
|
+
if (source[pos] === '\\') pos++;
|
|
126
|
+
pos++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
pos++;
|
|
130
|
+
}
|
|
131
|
+
// Now find the `{` after the closing `)`
|
|
132
|
+
while (pos < source.length) {
|
|
133
|
+
const ch = source[pos];
|
|
134
|
+
if (ch === '{') return pos;
|
|
135
|
+
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r' && ch !== ':') {
|
|
136
|
+
// Hit something unexpected (like '=>' for arrows, or type annotation chars)
|
|
137
|
+
if (ch === '=' && pos + 1 < source.length && source[pos + 1] === '>') {
|
|
138
|
+
// Arrow — find { after =>
|
|
139
|
+
pos += 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Type annotation — keep going (`: ReturnType`)
|
|
143
|
+
}
|
|
144
|
+
pos++;
|
|
145
|
+
}
|
|
146
|
+
return -1;
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
/**
|
|
108
150
|
* Find the closing brace position for a function body starting at `openBrace`.
|
|
109
151
|
*/
|
|
@@ -448,9 +490,14 @@ function transformEsmSource(
|
|
|
448
490
|
traceVars: boolean,
|
|
449
491
|
originalSource?: string | null,
|
|
450
492
|
): string {
|
|
493
|
+
// Detect React files for component render tracking
|
|
494
|
+
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
495
|
+
|
|
451
496
|
// Match top-level and nested function declarations (including async, export)
|
|
452
497
|
const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
453
498
|
const funcInsertions: Array<{ position: number; name: string; paramNames: string[] }> = [];
|
|
499
|
+
// Body insertions: insert at start of function body (for React render tracking)
|
|
500
|
+
const bodyInsertions: Array<{ position: number; name: string; lineNo: number }> = [];
|
|
454
501
|
let match;
|
|
455
502
|
|
|
456
503
|
while ((match = funcRegex.exec(source)) !== null) {
|
|
@@ -458,10 +505,11 @@ function transformEsmSource(
|
|
|
458
505
|
if (name === 'require' || name === 'exports' || name === 'module') continue;
|
|
459
506
|
|
|
460
507
|
const afterMatch = match.index + match[0].length;
|
|
461
|
-
|
|
508
|
+
// Use findFunctionBodyBrace to correctly skip destructured params like ({ a, b }) =>
|
|
509
|
+
const openBrace = findFunctionBodyBrace(source, afterMatch);
|
|
462
510
|
if (openBrace === -1) continue;
|
|
463
511
|
|
|
464
|
-
// Extract parameter names
|
|
512
|
+
// Extract parameter names (between the opening ( and the body {)
|
|
465
513
|
const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
|
|
466
514
|
const paramNames = paramStr
|
|
467
515
|
? paramStr.split(',').map(p => {
|
|
@@ -475,6 +523,15 @@ function transformEsmSource(
|
|
|
475
523
|
if (closeBrace === -1) continue;
|
|
476
524
|
|
|
477
525
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
526
|
+
|
|
527
|
+
// React component render tracking: uppercase function name in .tsx/.jsx
|
|
528
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
529
|
+
let lineNo = 1;
|
|
530
|
+
for (let i = 0; i < match.index; i++) {
|
|
531
|
+
if (source[i] === '\n') lineNo++;
|
|
532
|
+
}
|
|
533
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
534
|
+
}
|
|
478
535
|
}
|
|
479
536
|
|
|
480
537
|
// Also match arrow functions assigned to const/let/var
|
|
@@ -503,15 +560,26 @@ function transformEsmSource(
|
|
|
503
560
|
if (closeBrace === -1) continue;
|
|
504
561
|
|
|
505
562
|
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
563
|
+
|
|
564
|
+
// React component render tracking: uppercase arrow function in .tsx/.jsx
|
|
565
|
+
if (isReactFile && /^[A-Z]/.test(name)) {
|
|
566
|
+
let lineNo = 1;
|
|
567
|
+
for (let i = 0; i < match.index; i++) {
|
|
568
|
+
if (source[i] === '\n') lineNo++;
|
|
569
|
+
}
|
|
570
|
+
bodyInsertions.push({ position: openBrace + 1, name, lineNo });
|
|
571
|
+
}
|
|
506
572
|
}
|
|
507
573
|
|
|
574
|
+
|
|
575
|
+
|
|
508
576
|
// Find variable declarations for tracing
|
|
509
577
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
510
578
|
|
|
511
579
|
// Find destructured variable declarations for tracing
|
|
512
580
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
513
581
|
|
|
514
|
-
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0) return source;
|
|
582
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && bodyInsertions.length === 0) return source;
|
|
515
583
|
|
|
516
584
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
517
585
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -536,7 +604,7 @@ function transformEsmSource(
|
|
|
536
604
|
const importLines: string[] = [
|
|
537
605
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
538
606
|
];
|
|
539
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
607
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0) {
|
|
540
608
|
importLines.push(
|
|
541
609
|
`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`,
|
|
542
610
|
`import { join as __trickle_join } from 'node:path';`,
|
|
@@ -619,6 +687,24 @@ function transformEsmSource(
|
|
|
619
687
|
);
|
|
620
688
|
}
|
|
621
689
|
|
|
690
|
+
// Add React component render tracker if needed
|
|
691
|
+
if (bodyInsertions.length > 0) {
|
|
692
|
+
prefixLines.push(
|
|
693
|
+
`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`,
|
|
694
|
+
`function __trickle_rc(name, line) {`,
|
|
695
|
+
` try {`,
|
|
696
|
+
` const key = ${JSON.stringify(filename)} + ':' + line;`,
|
|
697
|
+
` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`,
|
|
698
|
+
` globalThis.__trickle_react_renders.set(key, count);`,
|
|
699
|
+
` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`,
|
|
700
|
+
` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`,
|
|
701
|
+
` const f = __trickle_join(dir, 'variables.jsonl');`,
|
|
702
|
+
` __trickle_appendFileSync(f, JSON.stringify({ kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 }) + '\\n');`,
|
|
703
|
+
` } catch(e) {}`,
|
|
704
|
+
`}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
622
708
|
prefixLines.push('');
|
|
623
709
|
const prefix = prefixLines.join('\n');
|
|
624
710
|
|
|
@@ -649,6 +735,13 @@ function transformEsmSource(
|
|
|
649
735
|
});
|
|
650
736
|
}
|
|
651
737
|
|
|
738
|
+
for (const { position, name, lineNo } of bodyInsertions) {
|
|
739
|
+
allInsertions.push({
|
|
740
|
+
position,
|
|
741
|
+
code: `\ntry{__trickle_rc(${JSON.stringify(name)},${lineNo})}catch(__e){}\n`,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
652
745
|
// Sort by position descending (insert from end to preserve earlier positions)
|
|
653
746
|
allInsertions.sort((a, b) => b.position - a.position);
|
|
654
747
|
|