trickle-observe 0.2.76 → 0.2.78
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 +163 -3
- package/dist-esm/vite-plugin.js +163 -3
- package/package.json +1 -1
- package/src/vite-plugin.ts +153 -3
package/dist/vite-plugin.js
CHANGED
|
@@ -629,6 +629,150 @@ 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 `,` (function arguments, not JSX)
|
|
662
|
+
if (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 === ';')
|
|
679
|
+
break;
|
|
680
|
+
// `(` breaks scan in code context, but in JSX text `(` is normal
|
|
681
|
+
// Check: if `(` is preceded by `>` or text, it's JSX text
|
|
682
|
+
if (ch === '(') {
|
|
683
|
+
const before = source.slice(Math.max(0, scanPos - 20), scanPos).trim();
|
|
684
|
+
if (before.endsWith('>') || /[a-zA-Z0-9\s]$/.test(before)) {
|
|
685
|
+
// Could be JSX text like "Users ({count})" — keep scanning
|
|
686
|
+
scanPos--;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
692
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
693
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
694
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
695
|
+
if (/^[a-zA-Z]/.test(attrCheck))
|
|
696
|
+
break; // Likely an attribute
|
|
697
|
+
}
|
|
698
|
+
if (ch === '\n') {
|
|
699
|
+
// Check context of previous lines
|
|
700
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
701
|
+
if (/<|>|\}/.test(lineAbove)) {
|
|
702
|
+
inJsx = true;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
scanPos--;
|
|
708
|
+
}
|
|
709
|
+
if (!inJsx)
|
|
710
|
+
continue;
|
|
711
|
+
// Find the matching closing `}` for this expression
|
|
712
|
+
let depth = 1;
|
|
713
|
+
let pos = bracePos + 1;
|
|
714
|
+
while (pos < source.length && depth > 0) {
|
|
715
|
+
const ch = source[pos];
|
|
716
|
+
if (ch === '{')
|
|
717
|
+
depth++;
|
|
718
|
+
else if (ch === '}')
|
|
719
|
+
depth--;
|
|
720
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
721
|
+
const q = ch;
|
|
722
|
+
pos++;
|
|
723
|
+
while (pos < source.length && source[pos] !== q) {
|
|
724
|
+
if (source[pos] === '\\')
|
|
725
|
+
pos++;
|
|
726
|
+
pos++;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
pos++;
|
|
730
|
+
}
|
|
731
|
+
if (depth !== 0)
|
|
732
|
+
continue;
|
|
733
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
734
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
735
|
+
// Skip empty expressions
|
|
736
|
+
if (!exprText)
|
|
737
|
+
continue;
|
|
738
|
+
// Skip complex expressions that we don't want to trace:
|
|
739
|
+
// - JSX elements: contains `<` (could be a component)
|
|
740
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
741
|
+
// - Spread: starts with `...`
|
|
742
|
+
// - Arrow functions: contains `=>`
|
|
743
|
+
// - Already traced: starts with `__trickle`
|
|
744
|
+
if (exprText.includes('<') || exprText.includes('=>'))
|
|
745
|
+
continue;
|
|
746
|
+
if (exprText.startsWith('...'))
|
|
747
|
+
continue;
|
|
748
|
+
if (exprText.startsWith('__trickle'))
|
|
749
|
+
continue;
|
|
750
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText))
|
|
751
|
+
continue;
|
|
752
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
753
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
754
|
+
// Skip: fn(), obj.method(), Component()
|
|
755
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?'))
|
|
756
|
+
continue;
|
|
757
|
+
// Only trace if it's a "simple" expression:
|
|
758
|
+
// - Identifier: count, name
|
|
759
|
+
// - Property access: user.name, item.price.formatted
|
|
760
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
761
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
762
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
763
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
764
|
+
if (!isSimple)
|
|
765
|
+
continue;
|
|
766
|
+
// Calculate line number
|
|
767
|
+
let lineNo = 1;
|
|
768
|
+
for (let i = 0; i < bracePos; i++) {
|
|
769
|
+
if (source[i] === '\n')
|
|
770
|
+
lineNo++;
|
|
771
|
+
}
|
|
772
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
773
|
+
}
|
|
774
|
+
return results;
|
|
775
|
+
}
|
|
632
776
|
function findForLoopVars(source) {
|
|
633
777
|
const results = [];
|
|
634
778
|
// Match: for (const/let/var ...
|
|
@@ -1260,7 +1404,9 @@ ingestUrl) {
|
|
|
1260
1404
|
const catchInsertions = traceVars ? findCatchVars(source) : [];
|
|
1261
1405
|
// Find function parameter names for tracing
|
|
1262
1406
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1263
|
-
|
|
1407
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1408
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1409
|
+
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
1410
|
return source;
|
|
1265
1411
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1266
1412
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1329,7 +1475,7 @@ ingestUrl) {
|
|
|
1329
1475
|
}
|
|
1330
1476
|
}
|
|
1331
1477
|
// 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;
|
|
1478
|
+
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
1479
|
const importLines = [];
|
|
1334
1480
|
if (isSSR) {
|
|
1335
1481
|
// SSR/Node.js — import trickle-observe for function wrapping + file system for writing
|
|
@@ -1364,7 +1510,7 @@ ingestUrl) {
|
|
|
1364
1510
|
}
|
|
1365
1511
|
}
|
|
1366
1512
|
// 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) {
|
|
1513
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1368
1514
|
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
1515
|
}
|
|
1370
1516
|
// Add React component render tracker if needed
|
|
@@ -1433,6 +1579,20 @@ ingestUrl) {
|
|
|
1433
1579
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1434
1580
|
});
|
|
1435
1581
|
}
|
|
1582
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1583
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1584
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1585
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1586
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1587
|
+
allInsertions.push({
|
|
1588
|
+
position: exprStart,
|
|
1589
|
+
code: `(__trickle_tv(`,
|
|
1590
|
+
});
|
|
1591
|
+
allInsertions.push({
|
|
1592
|
+
position: exprEnd,
|
|
1593
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1436
1596
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1437
1597
|
allInsertions.push({
|
|
1438
1598
|
position,
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -622,6 +622,150 @@ 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 `,` (function arguments, not JSX)
|
|
655
|
+
if (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 === ';')
|
|
672
|
+
break;
|
|
673
|
+
// `(` breaks scan in code context, but in JSX text `(` is normal
|
|
674
|
+
// Check: if `(` is preceded by `>` or text, it's JSX text
|
|
675
|
+
if (ch === '(') {
|
|
676
|
+
const before = source.slice(Math.max(0, scanPos - 20), scanPos).trim();
|
|
677
|
+
if (before.endsWith('>') || /[a-zA-Z0-9\s]$/.test(before)) {
|
|
678
|
+
// Could be JSX text like "Users ({count})" — keep scanning
|
|
679
|
+
scanPos--;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
685
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
686
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
687
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
688
|
+
if (/^[a-zA-Z]/.test(attrCheck))
|
|
689
|
+
break; // Likely an attribute
|
|
690
|
+
}
|
|
691
|
+
if (ch === '\n') {
|
|
692
|
+
// Check context of previous lines
|
|
693
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
694
|
+
if (/<|>|\}/.test(lineAbove)) {
|
|
695
|
+
inJsx = true;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
scanPos--;
|
|
701
|
+
}
|
|
702
|
+
if (!inJsx)
|
|
703
|
+
continue;
|
|
704
|
+
// Find the matching closing `}` for this expression
|
|
705
|
+
let depth = 1;
|
|
706
|
+
let pos = bracePos + 1;
|
|
707
|
+
while (pos < source.length && depth > 0) {
|
|
708
|
+
const ch = source[pos];
|
|
709
|
+
if (ch === '{')
|
|
710
|
+
depth++;
|
|
711
|
+
else if (ch === '}')
|
|
712
|
+
depth--;
|
|
713
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
714
|
+
const q = ch;
|
|
715
|
+
pos++;
|
|
716
|
+
while (pos < source.length && source[pos] !== q) {
|
|
717
|
+
if (source[pos] === '\\')
|
|
718
|
+
pos++;
|
|
719
|
+
pos++;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
pos++;
|
|
723
|
+
}
|
|
724
|
+
if (depth !== 0)
|
|
725
|
+
continue;
|
|
726
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
727
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
728
|
+
// Skip empty expressions
|
|
729
|
+
if (!exprText)
|
|
730
|
+
continue;
|
|
731
|
+
// Skip complex expressions that we don't want to trace:
|
|
732
|
+
// - JSX elements: contains `<` (could be a component)
|
|
733
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
734
|
+
// - Spread: starts with `...`
|
|
735
|
+
// - Arrow functions: contains `=>`
|
|
736
|
+
// - Already traced: starts with `__trickle`
|
|
737
|
+
if (exprText.includes('<') || exprText.includes('=>'))
|
|
738
|
+
continue;
|
|
739
|
+
if (exprText.startsWith('...'))
|
|
740
|
+
continue;
|
|
741
|
+
if (exprText.startsWith('__trickle'))
|
|
742
|
+
continue;
|
|
743
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText))
|
|
744
|
+
continue;
|
|
745
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
746
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
747
|
+
// Skip: fn(), obj.method(), Component()
|
|
748
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?'))
|
|
749
|
+
continue;
|
|
750
|
+
// Only trace if it's a "simple" expression:
|
|
751
|
+
// - Identifier: count, name
|
|
752
|
+
// - Property access: user.name, item.price.formatted
|
|
753
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
754
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
755
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
756
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
757
|
+
if (!isSimple)
|
|
758
|
+
continue;
|
|
759
|
+
// Calculate line number
|
|
760
|
+
let lineNo = 1;
|
|
761
|
+
for (let i = 0; i < bracePos; i++) {
|
|
762
|
+
if (source[i] === '\n')
|
|
763
|
+
lineNo++;
|
|
764
|
+
}
|
|
765
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
766
|
+
}
|
|
767
|
+
return results;
|
|
768
|
+
}
|
|
625
769
|
function findForLoopVars(source) {
|
|
626
770
|
const results = [];
|
|
627
771
|
// Match: for (const/let/var ...
|
|
@@ -1253,7 +1397,9 @@ ingestUrl) {
|
|
|
1253
1397
|
const catchInsertions = traceVars ? findCatchVars(source) : [];
|
|
1254
1398
|
// Find function parameter names for tracing
|
|
1255
1399
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1256
|
-
|
|
1400
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1401
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1402
|
+
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
1403
|
return source;
|
|
1258
1404
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1259
1405
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1322,7 +1468,7 @@ ingestUrl) {
|
|
|
1322
1468
|
}
|
|
1323
1469
|
}
|
|
1324
1470
|
// 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;
|
|
1471
|
+
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
1472
|
const importLines = [];
|
|
1327
1473
|
if (isSSR) {
|
|
1328
1474
|
// SSR/Node.js — import trickle-observe for function wrapping + file system for writing
|
|
@@ -1357,7 +1503,7 @@ ingestUrl) {
|
|
|
1357
1503
|
}
|
|
1358
1504
|
}
|
|
1359
1505
|
// 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) {
|
|
1506
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1361
1507
|
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
1508
|
}
|
|
1363
1509
|
// Add React component render tracker if needed
|
|
@@ -1426,6 +1572,20 @@ ingestUrl) {
|
|
|
1426
1572
|
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1427
1573
|
});
|
|
1428
1574
|
}
|
|
1575
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1576
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1577
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1578
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1579
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1580
|
+
allInsertions.push({
|
|
1581
|
+
position: exprStart,
|
|
1582
|
+
code: `(__trickle_tv(`,
|
|
1583
|
+
});
|
|
1584
|
+
allInsertions.push({
|
|
1585
|
+
position: exprEnd,
|
|
1586
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1429
1589
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1430
1590
|
allInsertions.push({
|
|
1431
1591
|
position,
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -599,6 +599,138 @@ 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 `,` (function arguments, not JSX)
|
|
636
|
+
if (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 === ';') break;
|
|
647
|
+
// `(` breaks scan in code context, but in JSX text `(` is normal
|
|
648
|
+
// Check: if `(` is preceded by `>` or text, it's JSX text
|
|
649
|
+
if (ch === '(') {
|
|
650
|
+
const before = source.slice(Math.max(0, scanPos - 20), scanPos).trim();
|
|
651
|
+
if (before.endsWith('>') || /[a-zA-Z0-9\s]$/.test(before)) {
|
|
652
|
+
// Could be JSX text like "Users ({count})" — keep scanning
|
|
653
|
+
scanPos--;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
// `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
|
|
659
|
+
if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
|
|
660
|
+
// Check if this `=` is a JSX attribute assignment: look further back for tag
|
|
661
|
+
const attrCheck = source.slice(Math.max(0, scanPos - 30), scanPos).trim();
|
|
662
|
+
if (/^[a-zA-Z]/.test(attrCheck)) break; // Likely an attribute
|
|
663
|
+
}
|
|
664
|
+
if (ch === '\n') {
|
|
665
|
+
// Check context of previous lines
|
|
666
|
+
const lineAbove = source.slice(Math.max(0, scanPos - 100), scanPos);
|
|
667
|
+
if (/<|>|\}/.test(lineAbove)) { inJsx = true; break; }
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
scanPos--;
|
|
671
|
+
}
|
|
672
|
+
if (!inJsx) continue;
|
|
673
|
+
|
|
674
|
+
// Find the matching closing `}` for this expression
|
|
675
|
+
let depth = 1;
|
|
676
|
+
let pos = bracePos + 1;
|
|
677
|
+
while (pos < source.length && depth > 0) {
|
|
678
|
+
const ch = source[pos];
|
|
679
|
+
if (ch === '{') depth++;
|
|
680
|
+
else if (ch === '}') depth--;
|
|
681
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
682
|
+
const q = ch; pos++;
|
|
683
|
+
while (pos < source.length && source[pos] !== q) {
|
|
684
|
+
if (source[pos] === '\\') pos++;
|
|
685
|
+
pos++;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
pos++;
|
|
689
|
+
}
|
|
690
|
+
if (depth !== 0) continue;
|
|
691
|
+
|
|
692
|
+
const exprEnd = pos - 1; // position of closing `}`
|
|
693
|
+
const exprText = source.slice(bracePos + 1, exprEnd).trim();
|
|
694
|
+
|
|
695
|
+
// Skip empty expressions
|
|
696
|
+
if (!exprText) continue;
|
|
697
|
+
|
|
698
|
+
// Skip complex expressions that we don't want to trace:
|
|
699
|
+
// - JSX elements: contains `<` (could be a component)
|
|
700
|
+
// - .map/.filter/.reduce calls (return arrays of elements)
|
|
701
|
+
// - Spread: starts with `...`
|
|
702
|
+
// - Arrow functions: contains `=>`
|
|
703
|
+
// - Already traced: starts with `__trickle`
|
|
704
|
+
if (exprText.includes('<') || exprText.includes('=>')) continue;
|
|
705
|
+
if (exprText.startsWith('...')) continue;
|
|
706
|
+
if (exprText.startsWith('__trickle')) continue;
|
|
707
|
+
if (/\.(map|filter|reduce|forEach|flatMap)\s*\(/.test(exprText)) continue;
|
|
708
|
+
// Skip function calls with parens (complex expressions) — but allow property access
|
|
709
|
+
// Allow: user.name, count, x ? 'a' : 'b', a + b
|
|
710
|
+
// Skip: fn(), obj.method(), Component()
|
|
711
|
+
if (/\w\s*\(/.test(exprText) && !exprText.includes('?')) continue;
|
|
712
|
+
|
|
713
|
+
// Only trace if it's a "simple" expression:
|
|
714
|
+
// - Identifier: count, name
|
|
715
|
+
// - Property access: user.name, item.price.formatted
|
|
716
|
+
// - Simple ternary: x ? 'a' : 'b'
|
|
717
|
+
// - Simple arithmetic: count * 2, price + tax
|
|
718
|
+
const isSimple = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(exprText) || // identifier/property
|
|
719
|
+
/^[a-zA-Z_$]/.test(exprText); // starts with identifier (covers ternary, arithmetic)
|
|
720
|
+
if (!isSimple) continue;
|
|
721
|
+
|
|
722
|
+
// Calculate line number
|
|
723
|
+
let lineNo = 1;
|
|
724
|
+
for (let i = 0; i < bracePos; i++) {
|
|
725
|
+
if (source[i] === '\n') lineNo++;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
results.push({ exprStart: bracePos + 1, exprEnd, exprText, lineNo });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return results;
|
|
732
|
+
}
|
|
733
|
+
|
|
602
734
|
function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
|
|
603
735
|
const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
|
|
604
736
|
|
|
@@ -1250,7 +1382,10 @@ export function transformEsmSource(
|
|
|
1250
1382
|
// Find function parameter names for tracing
|
|
1251
1383
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1252
1384
|
|
|
1253
|
-
|
|
1385
|
+
// Find JSX text expressions for tracing (React files only)
|
|
1386
|
+
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1387
|
+
|
|
1388
|
+
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
1389
|
|
|
1255
1390
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1256
1391
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1318,7 +1453,7 @@ export function transformEsmSource(
|
|
|
1318
1453
|
}
|
|
1319
1454
|
|
|
1320
1455
|
// 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;
|
|
1456
|
+
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
1457
|
const importLines: string[] = [];
|
|
1323
1458
|
|
|
1324
1459
|
if (isSSR) {
|
|
@@ -1420,7 +1555,7 @@ export function transformEsmSource(
|
|
|
1420
1555
|
}
|
|
1421
1556
|
|
|
1422
1557
|
// 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) {
|
|
1558
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1424
1559
|
prefixLines.push(
|
|
1425
1560
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
1426
1561
|
` const _cache = new Map();`,
|
|
@@ -1630,6 +1765,21 @@ export function transformEsmSource(
|
|
|
1630
1765
|
});
|
|
1631
1766
|
}
|
|
1632
1767
|
|
|
1768
|
+
// JSX expression insertions: wrap with comma operator to trace without changing value
|
|
1769
|
+
// {expr} → {(__trickle_tv(expr, "expr", lineNo), expr)}
|
|
1770
|
+
for (const { exprStart, exprEnd, exprText, lineNo } of jsxExprInsertions) {
|
|
1771
|
+
// Use a display name: truncate long expressions, use the raw text
|
|
1772
|
+
const displayName = exprText.length > 30 ? exprText.slice(0, 27) + '...' : exprText;
|
|
1773
|
+
allInsertions.push({
|
|
1774
|
+
position: exprStart,
|
|
1775
|
+
code: `(__trickle_tv(`,
|
|
1776
|
+
});
|
|
1777
|
+
allInsertions.push({
|
|
1778
|
+
position: exprEnd,
|
|
1779
|
+
code: `,${JSON.stringify(displayName)},${lineNo}),${exprText})`,
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1633
1783
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1634
1784
|
allInsertions.push({
|
|
1635
1785
|
position,
|