trickle-observe 0.2.76 → 0.2.77
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 +152 -3
- package/dist-esm/vite-plugin.js +152 -3
- package/package.json +1 -1
- package/src/vite-plugin.ts +142 -3
package/dist/vite-plugin.js
CHANGED
|
@@ -629,6 +629,139 @@ function findCatchVars(source) {
|
|
|
629
629
|
}
|
|
630
630
|
return results;
|
|
631
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Find simple JSX text expressions and return insertions for tracing.
|
|
634
|
+
* Only traces simple expressions in text content positions (after > or between tags):
|
|
635
|
+
* <p>{count}</p> → trace count
|
|
636
|
+
* <span>{user.name}</span> → trace user.name
|
|
637
|
+
* <div>{a + b}</div> → trace a + b (simple binary)
|
|
638
|
+
* <p>{x ? 'a' : 'b'}</p> → trace ternary
|
|
639
|
+
* Skips: attribute expressions, .map() calls, JSX elements, spread, complex calls.
|
|
640
|
+
* Uses comma operator: {(__trickle_tv(expr, name, line), expr)} — safe, returns original value.
|
|
641
|
+
*/
|
|
642
|
+
function findJsxExpressions(source) {
|
|
643
|
+
const results = [];
|
|
644
|
+
// Find JSX text expressions: characters `>` followed (with optional whitespace/text) by `{expr}`
|
|
645
|
+
// We look for `{` that follows `>` (possibly with whitespace or text between)
|
|
646
|
+
// and is NOT preceded by `=` (which would be an attribute value)
|
|
647
|
+
const jsxExprRegex = /\{/g;
|
|
648
|
+
let match;
|
|
649
|
+
while ((match = jsxExprRegex.exec(source)) !== null) {
|
|
650
|
+
const bracePos = match.index;
|
|
651
|
+
// Skip if inside a string or comment
|
|
652
|
+
// Simple check: look at the character before `{`
|
|
653
|
+
const charBefore = bracePos > 0 ? source[bracePos - 1] : '';
|
|
654
|
+
// Skip if this is an attribute value: preceded by `=` (with optional whitespace)
|
|
655
|
+
const beforeSlice = source.slice(Math.max(0, bracePos - 5), bracePos).trimEnd();
|
|
656
|
+
if (beforeSlice.endsWith('='))
|
|
657
|
+
continue;
|
|
658
|
+
// Skip if this looks like a template literal expression `${`
|
|
659
|
+
if (charBefore === '$')
|
|
660
|
+
continue;
|
|
661
|
+
// Skip if preceded by `(` or `,` (function arguments, not JSX)
|
|
662
|
+
if (charBefore === '(' || charBefore === ',')
|
|
663
|
+
continue;
|
|
664
|
+
// Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
|
|
665
|
+
// before hitting structural JS characters like `{`, `(`, `;`
|
|
666
|
+
let inJsx = false;
|
|
667
|
+
let scanPos = bracePos - 1;
|
|
668
|
+
while (scanPos >= 0) {
|
|
669
|
+
const ch = source[scanPos];
|
|
670
|
+
if (ch === '>') {
|
|
671
|
+
inJsx = true;
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
if (ch === '}') {
|
|
675
|
+
inJsx = true;
|
|
676
|
+
break;
|
|
677
|
+
} // After a previous JSX expression
|
|
678
|
+
if (ch === '{' || ch === '(' || ch === ';')
|
|
679
|
+
break;
|
|
680
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
681
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
682
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
683
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
684
|
+
if (/^[a-zA-Z]/.test(attrCheck))
|
|
685
|
+
break; // Likely an attribute
|
|
686
|
+
}
|
|
687
|
+
if (ch === '\n') {
|
|
688
|
+
// Check context of previous lines
|
|
689
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
690
|
+
if (/<|>|\}/.test(lineAbove)) {
|
|
691
|
+
inJsx = true;
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
scanPos--;
|
|
697
|
+
}
|
|
698
|
+
if (!inJsx)
|
|
699
|
+
continue;
|
|
700
|
+
// Find the matching closing `}` for this expression
|
|
701
|
+
let depth = 1;
|
|
702
|
+
let pos = bracePos + 1;
|
|
703
|
+
while (pos < source.length && depth > 0) {
|
|
704
|
+
const ch = source[pos];
|
|
705
|
+
if (ch === '{')
|
|
706
|
+
depth++;
|
|
707
|
+
else if (ch === '}')
|
|
708
|
+
depth--;
|
|
709
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
710
|
+
const q = ch;
|
|
711
|
+
pos++;
|
|
712
|
+
while (pos < source.length && source[pos] !== q) {
|
|
713
|
+
if (source[pos] === '\\')
|
|
714
|
+
pos++;
|
|
715
|
+
pos++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
pos++;
|
|
719
|
+
}
|
|
720
|
+
if (depth !== 0)
|
|
721
|
+
continue;
|
|
722
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
723
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
724
|
+
// Skip empty expressions
|
|
725
|
+
if (!exprText)
|
|
726
|
+
continue;
|
|
727
|
+
// Skip complex expressions that we don't want to trace:
|
|
728
|
+
// - JSX elements: contains `<` (could be a component)
|
|
729
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
730
|
+
// - Spread: starts with `...`
|
|
731
|
+
// - Arrow functions: contains `=>`
|
|
732
|
+
// - Already traced: starts with `__trickle`
|
|
733
|
+
if (exprText.includes('<') || exprText.includes('=>'))
|
|
734
|
+
continue;
|
|
735
|
+
if (exprText.startsWith('...'))
|
|
736
|
+
continue;
|
|
737
|
+
if (exprText.startsWith('__trickle'))
|
|
738
|
+
continue;
|
|
739
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText))
|
|
740
|
+
continue;
|
|
741
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
742
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
743
|
+
// Skip: fn(), obj.method(), Component()
|
|
744
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?'))
|
|
745
|
+
continue;
|
|
746
|
+
// Only trace if it's a "simple" expression:
|
|
747
|
+
// - Identifier: count, name
|
|
748
|
+
// - Property access: user.name, item.price.formatted
|
|
749
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
750
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
751
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
752
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
753
|
+
if (!isSimple)
|
|
754
|
+
continue;
|
|
755
|
+
// Calculate line number
|
|
756
|
+
let lineNo = 1;
|
|
757
|
+
for (let i = 0; i < bracePos; i++) {
|
|
758
|
+
if (source[i] === '\n')
|
|
759
|
+
lineNo++;
|
|
760
|
+
}
|
|
761
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
762
|
+
}
|
|
763
|
+
return results;
|
|
764
|
+
}
|
|
632
765
|
function findForLoopVars(source) {
|
|
633
766
|
const results = [];
|
|
634
767
|
// Match: for (const/let/var ...
|
|
@@ -1260,7 +1393,9 @@ ingestUrl) {
|
|
|
1260
1393
|
const catchInsertions = traceVars ? findCatchVars(source) : [];
|
|
1261
1394
|
// Find function parameter names for tracing
|
|
1262
1395
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1263
|
-
|
|
1396
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1397
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1398
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1264
1399
|
return source;
|
|
1265
1400
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1266
1401
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1329,7 +1464,7 @@ ingestUrl) {
|
|
|
1329
1464
|
}
|
|
1330
1465
|
}
|
|
1331
1466
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
1332
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1467
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1333
1468
|
const importLines = [];
|
|
1334
1469
|
if (isSSR) {
|
|
1335
1470
|
// SSR/Node.js — import trickle-observe for function wrapping + file system for writing
|
|
@@ -1364,7 +1499,7 @@ ingestUrl) {
|
|
|
1364
1499
|
}
|
|
1365
1500
|
}
|
|
1366
1501
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1367
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1502
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1368
1503
|
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) {} }`);
|
|
1369
1504
|
}
|
|
1370
1505
|
// Add React component render tracker if needed
|
|
@@ -1433,6 +1568,20 @@ ingestUrl) {
|
|
|
1433
1568
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1434
1569
|
});
|
|
1435
1570
|
}
|
|
1571
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1572
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1573
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1574
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1575
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1576
|
+
allInsertions.push({
|
|
1577
|
+
position: exprStart,
|
|
1578
|
+
code: `(__trickle_tv(`,
|
|
1579
|
+
});
|
|
1580
|
+
allInsertions.push({
|
|
1581
|
+
position: exprEnd,
|
|
1582
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1436
1585
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1437
1586
|
allInsertions.push({
|
|
1438
1587
|
position,
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -622,6 +622,139 @@ function findCatchVars(source) {
|
|
|
622
622
|
}
|
|
623
623
|
return results;
|
|
624
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Find simple JSX text expressions and return insertions for tracing.
|
|
627
|
+
* Only traces simple expressions in text content positions (after > or between tags):
|
|
628
|
+
* <p>{count}</p> → trace count
|
|
629
|
+
* <span>{user.name}</span> → trace user.name
|
|
630
|
+
* <div>{a + b}</div> → trace a + b (simple binary)
|
|
631
|
+
* <p>{x ? 'a' : 'b'}</p> → trace ternary
|
|
632
|
+
* Skips: attribute expressions, .map() calls, JSX elements, spread, complex calls.
|
|
633
|
+
* Uses comma operator: {(__trickle_tv(expr, name, line), expr)} — safe, returns original value.
|
|
634
|
+
*/
|
|
635
|
+
function findJsxExpressions(source) {
|
|
636
|
+
const results = [];
|
|
637
|
+
// Find JSX text expressions: characters `>` followed (with optional whitespace/text) by `{expr}`
|
|
638
|
+
// We look for `{` that follows `>` (possibly with whitespace or text between)
|
|
639
|
+
// and is NOT preceded by `=` (which would be an attribute value)
|
|
640
|
+
const jsxExprRegex = /\{/g;
|
|
641
|
+
let match;
|
|
642
|
+
while ((match = jsxExprRegex.exec(source)) !== null) {
|
|
643
|
+
const bracePos = match.index;
|
|
644
|
+
// Skip if inside a string or comment
|
|
645
|
+
// Simple check: look at the character before `{`
|
|
646
|
+
const charBefore = bracePos > 0 ? source[bracePos - 1] : '';
|
|
647
|
+
// Skip if this is an attribute value: preceded by `=` (with optional whitespace)
|
|
648
|
+
const beforeSlice = source.slice(Math.max(0, bracePos - 5), bracePos).trimEnd();
|
|
649
|
+
if (beforeSlice.endsWith('='))
|
|
650
|
+
continue;
|
|
651
|
+
// Skip if this looks like a template literal expression `${`
|
|
652
|
+
if (charBefore === '$')
|
|
653
|
+
continue;
|
|
654
|
+
// Skip if preceded by `(` or `,` (function arguments, not JSX)
|
|
655
|
+
if (charBefore === '(' || charBefore === ',')
|
|
656
|
+
continue;
|
|
657
|
+
// Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
|
|
658
|
+
// before hitting structural JS characters like `{`, `(`, `;`
|
|
659
|
+
let inJsx = false;
|
|
660
|
+
let scanPos = bracePos - 1;
|
|
661
|
+
while (scanPos >= 0) {
|
|
662
|
+
const ch = source[scanPos];
|
|
663
|
+
if (ch === '>') {
|
|
664
|
+
inJsx = true;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
if (ch === '}') {
|
|
668
|
+
inJsx = true;
|
|
669
|
+
break;
|
|
670
|
+
} // After a previous JSX expression
|
|
671
|
+
if (ch === '{' || ch === '(' || ch === ';')
|
|
672
|
+
break;
|
|
673
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
674
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
675
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
676
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
677
|
+
if (/^[a-zA-Z]/.test(attrCheck))
|
|
678
|
+
break; // Likely an attribute
|
|
679
|
+
}
|
|
680
|
+
if (ch === '\n') {
|
|
681
|
+
// Check context of previous lines
|
|
682
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
683
|
+
if (/<|>|\}/.test(lineAbove)) {
|
|
684
|
+
inJsx = true;
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
scanPos--;
|
|
690
|
+
}
|
|
691
|
+
if (!inJsx)
|
|
692
|
+
continue;
|
|
693
|
+
// Find the matching closing `}` for this expression
|
|
694
|
+
let depth = 1;
|
|
695
|
+
let pos = bracePos + 1;
|
|
696
|
+
while (pos < source.length && depth > 0) {
|
|
697
|
+
const ch = source[pos];
|
|
698
|
+
if (ch === '{')
|
|
699
|
+
depth++;
|
|
700
|
+
else if (ch === '}')
|
|
701
|
+
depth--;
|
|
702
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
703
|
+
const q = ch;
|
|
704
|
+
pos++;
|
|
705
|
+
while (pos < source.length && source[pos] !== q) {
|
|
706
|
+
if (source[pos] === '\\')
|
|
707
|
+
pos++;
|
|
708
|
+
pos++;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
pos++;
|
|
712
|
+
}
|
|
713
|
+
if (depth !== 0)
|
|
714
|
+
continue;
|
|
715
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
716
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
717
|
+
// Skip empty expressions
|
|
718
|
+
if (!exprText)
|
|
719
|
+
continue;
|
|
720
|
+
// Skip complex expressions that we don't want to trace:
|
|
721
|
+
// - JSX elements: contains `<` (could be a component)
|
|
722
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
723
|
+
// - Spread: starts with `...`
|
|
724
|
+
// - Arrow functions: contains `=>`
|
|
725
|
+
// - Already traced: starts with `__trickle`
|
|
726
|
+
if (exprText.includes('<') || exprText.includes('=>'))
|
|
727
|
+
continue;
|
|
728
|
+
if (exprText.startsWith('...'))
|
|
729
|
+
continue;
|
|
730
|
+
if (exprText.startsWith('__trickle'))
|
|
731
|
+
continue;
|
|
732
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText))
|
|
733
|
+
continue;
|
|
734
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
735
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
736
|
+
// Skip: fn(), obj.method(), Component()
|
|
737
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?'))
|
|
738
|
+
continue;
|
|
739
|
+
// Only trace if it's a "simple" expression:
|
|
740
|
+
// - Identifier: count, name
|
|
741
|
+
// - Property access: user.name, item.price.formatted
|
|
742
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
743
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
744
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
745
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
746
|
+
if (!isSimple)
|
|
747
|
+
continue;
|
|
748
|
+
// Calculate line number
|
|
749
|
+
let lineNo = 1;
|
|
750
|
+
for (let i = 0; i < bracePos; i++) {
|
|
751
|
+
if (source[i] === '\n')
|
|
752
|
+
lineNo++;
|
|
753
|
+
}
|
|
754
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
755
|
+
}
|
|
756
|
+
return results;
|
|
757
|
+
}
|
|
625
758
|
function findForLoopVars(source) {
|
|
626
759
|
const results = [];
|
|
627
760
|
// Match: for (const/let/var ...
|
|
@@ -1253,7 +1386,9 @@ ingestUrl) {
|
|
|
1253
1386
|
const catchInsertions = traceVars ? findCatchVars(source) : [];
|
|
1254
1387
|
// Find function parameter names for tracing
|
|
1255
1388
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1256
|
-
|
|
1389
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1390
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1391
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1257
1392
|
return source;
|
|
1258
1393
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1259
1394
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1322,7 +1457,7 @@ ingestUrl) {
|
|
|
1322
1457
|
}
|
|
1323
1458
|
}
|
|
1324
1459
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
1325
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1460
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1326
1461
|
const importLines = [];
|
|
1327
1462
|
if (isSSR) {
|
|
1328
1463
|
// SSR/Node.js — import trickle-observe for function wrapping + file system for writing
|
|
@@ -1357,7 +1492,7 @@ ingestUrl) {
|
|
|
1357
1492
|
}
|
|
1358
1493
|
}
|
|
1359
1494
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1360
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1495
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1361
1496
|
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) {} }`);
|
|
1362
1497
|
}
|
|
1363
1498
|
// Add React component render tracker if needed
|
|
@@ -1426,6 +1561,20 @@ ingestUrl) {
|
|
|
1426
1561
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1427
1562
|
});
|
|
1428
1563
|
}
|
|
1564
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1565
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1566
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1567
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1568
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1569
|
+
allInsertions.push({
|
|
1570
|
+
position: exprStart,
|
|
1571
|
+
code: `(__trickle_tv(`,
|
|
1572
|
+
});
|
|
1573
|
+
allInsertions.push({
|
|
1574
|
+
position: exprEnd,
|
|
1575
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1429
1578
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1430
1579
|
allInsertions.push({
|
|
1431
1580
|
position,
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -599,6 +599,127 @@ function findCatchVars(source: string): Array<{ bodyStart: number; varNames: str
|
|
|
599
599
|
return results;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Find simple JSX text expressions and return insertions for tracing.
|
|
604
|
+
* Only traces simple expressions in text content positions (after > or between tags):
|
|
605
|
+
* <p>{count}</p> → trace count
|
|
606
|
+
* <span>{user.name}</span> → trace user.name
|
|
607
|
+
* <div>{a + b}</div> → trace a + b (simple binary)
|
|
608
|
+
* <p>{x ? 'a' : 'b'}</p> → trace ternary
|
|
609
|
+
* Skips: attribute expressions, .map() calls, JSX elements, spread, complex calls.
|
|
610
|
+
* Uses comma operator: {(__trickle_tv(expr, name, line), expr)} — safe, returns original value.
|
|
611
|
+
*/
|
|
612
|
+
function findJsxExpressions(source: string): Array<{ exprStart: number; exprEnd: number; exprText: string; lineNo: number }> {
|
|
613
|
+
const results: Array<{ exprStart: number; exprEnd: number; exprText: string; lineNo: number }> = [];
|
|
614
|
+
|
|
615
|
+
// Find JSX text expressions: characters `>` followed (with optional whitespace/text) by `{expr}`
|
|
616
|
+
// We look for `{` that follows `>` (possibly with whitespace or text between)
|
|
617
|
+
// and is NOT preceded by `=` (which would be an attribute value)
|
|
618
|
+
const jsxExprRegex = /\{/g;
|
|
619
|
+
let match;
|
|
620
|
+
|
|
621
|
+
while ((match = jsxExprRegex.exec(source)) !== null) {
|
|
622
|
+
const bracePos = match.index;
|
|
623
|
+
|
|
624
|
+
// Skip if inside a string or comment
|
|
625
|
+
// Simple check: look at the character before `{`
|
|
626
|
+
const charBefore = bracePos > 0 ? source[bracePos - 1] : '';
|
|
627
|
+
|
|
628
|
+
// Skip if this is an attribute value: preceded by `=` (with optional whitespace)
|
|
629
|
+
const beforeSlice = source.slice(Math.max(0, bracePos - 5), bracePos).trimEnd();
|
|
630
|
+
if (beforeSlice.endsWith('=')) continue;
|
|
631
|
+
|
|
632
|
+
// Skip if this looks like a template literal expression `${`
|
|
633
|
+
if (charBefore === '$') continue;
|
|
634
|
+
|
|
635
|
+
// Skip if preceded by `(` or `,` (function arguments, not JSX)
|
|
636
|
+
if (charBefore === '(' || charBefore === ',') continue;
|
|
637
|
+
|
|
638
|
+
// Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
|
|
639
|
+
// before hitting structural JS characters like `{`, `(`, `;`
|
|
640
|
+
let inJsx = false;
|
|
641
|
+
let scanPos = bracePos - 1;
|
|
642
|
+
while (scanPos >= 0) {
|
|
643
|
+
const ch = source[scanPos];
|
|
644
|
+
if (ch === '>') { inJsx = true; break; }
|
|
645
|
+
if (ch === '}') { inJsx = true; break; } // After a previous JSX expression
|
|
646
|
+
if (ch === '{' || ch === '(' || ch === ';') break;
|
|
647
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
648
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
649
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
650
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
651
|
+
if (/^[a-zA-Z]/.test(attrCheck)) break; // Likely an attribute
|
|
652
|
+
}
|
|
653
|
+
if (ch === '\n') {
|
|
654
|
+
// Check context of previous lines
|
|
655
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
656
|
+
if (/<|>|\}/.test(lineAbove)) { inJsx = true; break; }
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
scanPos--;
|
|
660
|
+
}
|
|
661
|
+
if (!inJsx) continue;
|
|
662
|
+
|
|
663
|
+
// Find the matching closing `}` for this expression
|
|
664
|
+
let depth = 1;
|
|
665
|
+
let pos = bracePos + 1;
|
|
666
|
+
while (pos < source.length && depth > 0) {
|
|
667
|
+
const ch = source[pos];
|
|
668
|
+
if (ch === '{') depth++;
|
|
669
|
+
else if (ch === '}') depth--;
|
|
670
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
671
|
+
const q = ch; pos++;
|
|
672
|
+
while (pos < source.length && source[pos] !== q) {
|
|
673
|
+
if (source[pos] === '\\') pos++;
|
|
674
|
+
pos++;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
pos++;
|
|
678
|
+
}
|
|
679
|
+
if (depth !== 0) continue;
|
|
680
|
+
|
|
681
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
682
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
683
|
+
|
|
684
|
+
// Skip empty expressions
|
|
685
|
+
if (!exprText) continue;
|
|
686
|
+
|
|
687
|
+
// Skip complex expressions that we don't want to trace:
|
|
688
|
+
// - JSX elements: contains `<` (could be a component)
|
|
689
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
690
|
+
// - Spread: starts with `...`
|
|
691
|
+
// - Arrow functions: contains `=>`
|
|
692
|
+
// - Already traced: starts with `__trickle`
|
|
693
|
+
if (exprText.includes('<') || exprText.includes('=>')) continue;
|
|
694
|
+
if (exprText.startsWith('...')) continue;
|
|
695
|
+
if (exprText.startsWith('__trickle')) continue;
|
|
696
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText)) continue;
|
|
697
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
698
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
699
|
+
// Skip: fn(), obj.method(), Component()
|
|
700
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?')) continue;
|
|
701
|
+
|
|
702
|
+
// Only trace if it's a "simple" expression:
|
|
703
|
+
// - Identifier: count, name
|
|
704
|
+
// - Property access: user.name, item.price.formatted
|
|
705
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
706
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
707
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
708
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
709
|
+
if (!isSimple) continue;
|
|
710
|
+
|
|
711
|
+
// Calculate line number
|
|
712
|
+
let lineNo = 1;
|
|
713
|
+
for (let i = 0; i < bracePos; i++) {
|
|
714
|
+
if (source[i] === '\n') lineNo++;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return results;
|
|
721
|
+
}
|
|
722
|
+
|
|
602
723
|
function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
|
|
603
724
|
const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
|
|
604
725
|
|
|
@@ -1250,7 +1371,10 @@ export function transformEsmSource(
|
|
|
1250
1371
|
// Find function parameter names for tracing
|
|
1251
1372
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1252
1373
|
|
|
1253
|
-
|
|
1374
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1375
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1376
|
+
|
|
1377
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0) return source;
|
|
1254
1378
|
|
|
1255
1379
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1256
1380
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1318,7 +1442,7 @@ export function transformEsmSource(
|
|
|
1318
1442
|
}
|
|
1319
1443
|
|
|
1320
1444
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
1321
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1445
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1322
1446
|
const importLines: string[] = [];
|
|
1323
1447
|
|
|
1324
1448
|
if (isSSR) {
|
|
@@ -1420,7 +1544,7 @@ export function transformEsmSource(
|
|
|
1420
1544
|
}
|
|
1421
1545
|
|
|
1422
1546
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1423
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1547
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1424
1548
|
prefixLines.push(
|
|
1425
1549
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
1426
1550
|
` const _cache = new Map();`,
|
|
@@ -1630,6 +1754,21 @@ export function transformEsmSource(
|
|
|
1630
1754
|
});
|
|
1631
1755
|
}
|
|
1632
1756
|
|
|
1757
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1758
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1759
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1760
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1761
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1762
|
+
allInsertions.push({
|
|
1763
|
+
position: exprStart,
|
|
1764
|
+
code: `(__trickle_tv(`,
|
|
1765
|
+
});
|
|
1766
|
+
allInsertions.push({
|
|
1767
|
+
position: exprEnd,
|
|
1768
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1633
1772
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1634
1773
|
allInsertions.push({
|
|
1635
1774
|
position,
|