trickle-observe 0.2.69 → 0.2.71
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 +407 -3
- package/dist-esm/vite-plugin.js +407 -3
- package/package.json +1 -1
- package/src/vite-plugin.ts +409 -3
package/dist/vite-plugin.js
CHANGED
|
@@ -480,6 +480,334 @@ function extractDestructuredNames(pattern) {
|
|
|
480
480
|
}
|
|
481
481
|
return names;
|
|
482
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
485
|
+
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
486
|
+
* Only matches standalone reassignment statements at the start of a line.
|
|
487
|
+
* Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
|
|
488
|
+
* comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
|
|
489
|
+
*/
|
|
490
|
+
function findReassignments(source) {
|
|
491
|
+
const results = [];
|
|
492
|
+
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
493
|
+
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
494
|
+
// Plain: = (but not ==, ===, =>, !=)
|
|
495
|
+
const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
|
|
496
|
+
let match;
|
|
497
|
+
while ((match = reassignRegex.exec(source)) !== null) {
|
|
498
|
+
const varName = match[2];
|
|
499
|
+
// Skip trickle internals
|
|
500
|
+
if (varName.startsWith('__trickle') || varName.startsWith('_$'))
|
|
501
|
+
continue;
|
|
502
|
+
// Skip common non-variable patterns
|
|
503
|
+
if (varName === '_a' || varName === '_b' || varName === '_c')
|
|
504
|
+
continue;
|
|
505
|
+
// Skip 'this', 'self', 'super' (not reassignable in practice)
|
|
506
|
+
if (varName === 'this' || varName === 'super')
|
|
507
|
+
continue;
|
|
508
|
+
// Skip keywords that could look like identifiers
|
|
509
|
+
if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
|
|
510
|
+
'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
|
|
511
|
+
'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
|
|
512
|
+
'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName))
|
|
513
|
+
continue;
|
|
514
|
+
// Check that this line doesn't start with const/let/var (would be a declaration, already handled)
|
|
515
|
+
const lineStart = source.lastIndexOf('\n', match.index) + 1;
|
|
516
|
+
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
517
|
+
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
|
|
518
|
+
continue;
|
|
519
|
+
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
520
|
+
// or if it's a label (label: ...)
|
|
521
|
+
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
522
|
+
if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
|
|
523
|
+
continue;
|
|
524
|
+
// Calculate line number
|
|
525
|
+
let lineNo = 1;
|
|
526
|
+
for (let i = 0; i < match.index; i++) {
|
|
527
|
+
if (source[i] === '\n')
|
|
528
|
+
lineNo++;
|
|
529
|
+
}
|
|
530
|
+
// Find end of statement (same logic as findVarDeclarations)
|
|
531
|
+
const startPos = match.index + match[0].length - 1;
|
|
532
|
+
let pos = startPos;
|
|
533
|
+
let depth = 0;
|
|
534
|
+
let foundEnd = -1;
|
|
535
|
+
while (pos < source.length) {
|
|
536
|
+
const ch = source[pos];
|
|
537
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
538
|
+
depth++;
|
|
539
|
+
}
|
|
540
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
541
|
+
depth--;
|
|
542
|
+
if (depth < 0)
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
else if (ch === ';' && depth === 0) {
|
|
546
|
+
foundEnd = pos;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
else if (ch === '\n' && depth === 0) {
|
|
550
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
551
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
552
|
+
foundEnd = pos;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
557
|
+
const quote = ch;
|
|
558
|
+
pos++;
|
|
559
|
+
while (pos < source.length) {
|
|
560
|
+
if (source[pos] === '\\') {
|
|
561
|
+
pos++;
|
|
562
|
+
}
|
|
563
|
+
else if (source[pos] === quote)
|
|
564
|
+
break;
|
|
565
|
+
pos++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
569
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
570
|
+
pos++;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
574
|
+
pos += 2;
|
|
575
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
576
|
+
pos++;
|
|
577
|
+
pos++;
|
|
578
|
+
}
|
|
579
|
+
pos++;
|
|
580
|
+
}
|
|
581
|
+
if (foundEnd === -1)
|
|
582
|
+
continue;
|
|
583
|
+
results.push({ lineEnd: foundEnd + 1, varName, lineNo });
|
|
584
|
+
}
|
|
585
|
+
return results;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
589
|
+
* Handles:
|
|
590
|
+
* for (const item of items) { ... } → trace item
|
|
591
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
592
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
593
|
+
* for (const key in obj) { ... } → trace key
|
|
594
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
595
|
+
* Inserts trace calls at the start of the loop body.
|
|
596
|
+
*/
|
|
597
|
+
function findForLoopVars(source) {
|
|
598
|
+
const results = [];
|
|
599
|
+
// Match: for (const/let/var ...
|
|
600
|
+
const forRegex = /\bfor\s*\(/g;
|
|
601
|
+
let match;
|
|
602
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
603
|
+
const afterParen = match.index + match[0].length;
|
|
604
|
+
// Skip whitespace
|
|
605
|
+
let pos = afterParen;
|
|
606
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
607
|
+
pos++;
|
|
608
|
+
// Expect const/let/var
|
|
609
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
610
|
+
if (!declMatch)
|
|
611
|
+
continue;
|
|
612
|
+
pos += declMatch[0].length;
|
|
613
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
614
|
+
const varNames = [];
|
|
615
|
+
const patternStart = pos;
|
|
616
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
617
|
+
// Destructured: find matching brace/bracket
|
|
618
|
+
const open = source[pos];
|
|
619
|
+
const close = open === '{' ? '}' : ']';
|
|
620
|
+
let depth = 1;
|
|
621
|
+
let end = pos + 1;
|
|
622
|
+
while (end < source.length && depth > 0) {
|
|
623
|
+
if (source[end] === open)
|
|
624
|
+
depth++;
|
|
625
|
+
else if (source[end] === close)
|
|
626
|
+
depth--;
|
|
627
|
+
end++;
|
|
628
|
+
}
|
|
629
|
+
const pattern = source.slice(pos, end);
|
|
630
|
+
const names = extractDestructuredNames(pattern);
|
|
631
|
+
varNames.push(...names);
|
|
632
|
+
pos = end;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
// Simple identifier
|
|
636
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
637
|
+
if (!idMatch)
|
|
638
|
+
continue;
|
|
639
|
+
varNames.push(idMatch[1]);
|
|
640
|
+
pos += idMatch[0].length;
|
|
641
|
+
}
|
|
642
|
+
if (varNames.length === 0)
|
|
643
|
+
continue;
|
|
644
|
+
// Skip trickle internals
|
|
645
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c'))
|
|
646
|
+
continue;
|
|
647
|
+
// Now find the opening `{` of the loop body
|
|
648
|
+
// Skip everything until the `)` that closes the for(...)
|
|
649
|
+
let parenDepth = 1; // We're inside the for(
|
|
650
|
+
while (pos < source.length && parenDepth > 0) {
|
|
651
|
+
const ch = source[pos];
|
|
652
|
+
if (ch === '(')
|
|
653
|
+
parenDepth++;
|
|
654
|
+
else if (ch === ')')
|
|
655
|
+
parenDepth--;
|
|
656
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
657
|
+
const q = ch;
|
|
658
|
+
pos++;
|
|
659
|
+
while (pos < source.length && source[pos] !== q) {
|
|
660
|
+
if (source[pos] === '\\')
|
|
661
|
+
pos++;
|
|
662
|
+
pos++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
pos++;
|
|
666
|
+
}
|
|
667
|
+
// Now find the `{` after the closing `)`
|
|
668
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
669
|
+
pos++;
|
|
670
|
+
if (pos >= source.length || source[pos] !== '{')
|
|
671
|
+
continue;
|
|
672
|
+
const bodyBrace = pos;
|
|
673
|
+
// Calculate line number
|
|
674
|
+
let lineNo = 1;
|
|
675
|
+
for (let i = 0; i < match.index; i++) {
|
|
676
|
+
if (source[i] === '\n')
|
|
677
|
+
lineNo++;
|
|
678
|
+
}
|
|
679
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
680
|
+
}
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
685
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
686
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
687
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
688
|
+
*/
|
|
689
|
+
function findFunctionParams(source, isReactFile) {
|
|
690
|
+
const results = [];
|
|
691
|
+
// Match function declarations: function name(params) {
|
|
692
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
693
|
+
let match;
|
|
694
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
695
|
+
const name = match[1];
|
|
696
|
+
if (name === 'require' || name === 'exports' || name === 'module')
|
|
697
|
+
continue;
|
|
698
|
+
if (name.startsWith('__trickle'))
|
|
699
|
+
continue;
|
|
700
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
701
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
702
|
+
continue;
|
|
703
|
+
const afterParen = match.index + match[0].length;
|
|
704
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
705
|
+
if (bodyBrace === -1)
|
|
706
|
+
continue;
|
|
707
|
+
// Extract parameter names from between ( and )
|
|
708
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
709
|
+
const closeParen = paramStr.indexOf(')');
|
|
710
|
+
if (closeParen === -1)
|
|
711
|
+
continue;
|
|
712
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
713
|
+
const paramNames = extractParamNames(rawParams);
|
|
714
|
+
if (paramNames.length === 0)
|
|
715
|
+
continue;
|
|
716
|
+
let lineNo = 1;
|
|
717
|
+
for (let i = 0; i < match.index; i++) {
|
|
718
|
+
if (source[i] === '\n')
|
|
719
|
+
lineNo++;
|
|
720
|
+
}
|
|
721
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
722
|
+
}
|
|
723
|
+
// Match arrow functions: const name = (params) => {
|
|
724
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
725
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
726
|
+
const name = match[1];
|
|
727
|
+
if (name.startsWith('__trickle'))
|
|
728
|
+
continue;
|
|
729
|
+
// Skip React components in React files
|
|
730
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
731
|
+
continue;
|
|
732
|
+
const rawParams = match[2].trim();
|
|
733
|
+
const paramNames = extractParamNames(rawParams);
|
|
734
|
+
if (paramNames.length === 0)
|
|
735
|
+
continue;
|
|
736
|
+
// Find the { position
|
|
737
|
+
const bracePos = match.index + match[0].length - 1;
|
|
738
|
+
let lineNo = 1;
|
|
739
|
+
for (let i = 0; i < match.index; i++) {
|
|
740
|
+
if (source[i] === '\n')
|
|
741
|
+
lineNo++;
|
|
742
|
+
}
|
|
743
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
744
|
+
}
|
|
745
|
+
return results;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Extract parameter names from a function parameter string.
|
|
749
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
750
|
+
* Skips type annotations.
|
|
751
|
+
*/
|
|
752
|
+
function extractParamNames(rawParams) {
|
|
753
|
+
if (!rawParams)
|
|
754
|
+
return [];
|
|
755
|
+
const names = [];
|
|
756
|
+
// Split by commas at depth 0
|
|
757
|
+
const parts = [];
|
|
758
|
+
let depth = 0;
|
|
759
|
+
let current = '';
|
|
760
|
+
for (const ch of rawParams) {
|
|
761
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<')
|
|
762
|
+
depth++;
|
|
763
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>')
|
|
764
|
+
depth--;
|
|
765
|
+
else if (ch === ',' && depth === 0) {
|
|
766
|
+
parts.push(current.trim());
|
|
767
|
+
current = '';
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
current += ch;
|
|
771
|
+
}
|
|
772
|
+
if (current.trim())
|
|
773
|
+
parts.push(current.trim());
|
|
774
|
+
for (const part of parts) {
|
|
775
|
+
const trimmed = part.trim();
|
|
776
|
+
if (!trimmed)
|
|
777
|
+
continue;
|
|
778
|
+
// Destructured params — extract individual names
|
|
779
|
+
if (trimmed.startsWith('{')) {
|
|
780
|
+
const closeBrace = trimmed.indexOf('}');
|
|
781
|
+
if (closeBrace !== -1) {
|
|
782
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
783
|
+
names.push(...destructNames);
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (trimmed.startsWith('[')) {
|
|
788
|
+
const closeBracket = trimmed.indexOf(']');
|
|
789
|
+
if (closeBracket !== -1) {
|
|
790
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
791
|
+
names.push(...destructNames);
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
// Rest parameter: ...args
|
|
796
|
+
if (trimmed.startsWith('...')) {
|
|
797
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
798
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
799
|
+
names.push(restName);
|
|
800
|
+
}
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// Simple param: name or name: Type or name = default
|
|
804
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
805
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
806
|
+
names.push(paramName);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return names;
|
|
810
|
+
}
|
|
483
811
|
/**
|
|
484
812
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
485
813
|
*
|
|
@@ -887,7 +1215,13 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
887
1215
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
888
1216
|
// Find destructured variable declarations for tracing
|
|
889
1217
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
890
|
-
|
|
1218
|
+
// Find variable reassignments for tracing
|
|
1219
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1220
|
+
// Find for-loop variable declarations for tracing
|
|
1221
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1222
|
+
// Find function parameter names for tracing
|
|
1223
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1224
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
891
1225
|
return source;
|
|
892
1226
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
893
1227
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -907,9 +1241,56 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
907
1241
|
di.lineNo = origLine;
|
|
908
1242
|
}
|
|
909
1243
|
}
|
|
1244
|
+
// Fix reassignment line numbers
|
|
1245
|
+
for (const ri of reassignInsertions) {
|
|
1246
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1247
|
+
if (origLine !== -1)
|
|
1248
|
+
ri.lineNo = origLine;
|
|
1249
|
+
}
|
|
1250
|
+
// Fix for-loop var line numbers
|
|
1251
|
+
for (const fi of forLoopInsertions) {
|
|
1252
|
+
if (fi.varNames.length > 0) {
|
|
1253
|
+
// Search for 'for' keyword near the expected line
|
|
1254
|
+
const pattern = /\bfor\s*\(/;
|
|
1255
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1256
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1257
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1258
|
+
fi.lineNo = fwd + 1;
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
if (delta > 0 && delta <= 10) {
|
|
1262
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1263
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1264
|
+
fi.lineNo = bwd + 1;
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Fix function param line numbers
|
|
1272
|
+
for (const fp of funcParamInsertions) {
|
|
1273
|
+
if (fp.paramNames.length > 0) {
|
|
1274
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1275
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1276
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1277
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1278
|
+
fp.lineNo = fwd + 1;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
if (delta > 0 && delta <= 10) {
|
|
1282
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1283
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1284
|
+
fp.lineNo = bwd + 1;
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
910
1291
|
}
|
|
911
1292
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
912
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1293
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
913
1294
|
const importLines = [
|
|
914
1295
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
915
1296
|
];
|
|
@@ -947,7 +1328,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
947
1328
|
}
|
|
948
1329
|
}
|
|
949
1330
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
950
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1331
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
951
1332
|
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Set();`, ` 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 ck = file + ':' + l + ':' + n + ':' + th;`, ` if (_cache.has(ck)) return;`, ` _cache.add(ck);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
952
1333
|
}
|
|
953
1334
|
// Add React component render tracker if needed
|
|
@@ -985,6 +1366,29 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
985
1366
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
986
1367
|
});
|
|
987
1368
|
}
|
|
1369
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1370
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1371
|
+
allInsertions.push({
|
|
1372
|
+
position: lineEnd,
|
|
1373
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1377
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1378
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1379
|
+
allInsertions.push({
|
|
1380
|
+
position: bodyStart,
|
|
1381
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1385
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1386
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1387
|
+
allInsertions.push({
|
|
1388
|
+
position: bodyStart,
|
|
1389
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
988
1392
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
989
1393
|
allInsertions.push({
|
|
990
1394
|
position,
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -473,6 +473,334 @@ function extractDestructuredNames(pattern) {
|
|
|
473
473
|
}
|
|
474
474
|
return names;
|
|
475
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
478
|
+
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
479
|
+
* Only matches standalone reassignment statements at the start of a line.
|
|
480
|
+
* Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
|
|
481
|
+
* comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
|
|
482
|
+
*/
|
|
483
|
+
function findReassignments(source) {
|
|
484
|
+
const results = [];
|
|
485
|
+
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
486
|
+
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
487
|
+
// Plain: = (but not ==, ===, =>, !=)
|
|
488
|
+
const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
|
|
489
|
+
let match;
|
|
490
|
+
while ((match = reassignRegex.exec(source)) !== null) {
|
|
491
|
+
const varName = match[2];
|
|
492
|
+
// Skip trickle internals
|
|
493
|
+
if (varName.startsWith('__trickle') || varName.startsWith('_$'))
|
|
494
|
+
continue;
|
|
495
|
+
// Skip common non-variable patterns
|
|
496
|
+
if (varName === '_a' || varName === '_b' || varName === '_c')
|
|
497
|
+
continue;
|
|
498
|
+
// Skip 'this', 'self', 'super' (not reassignable in practice)
|
|
499
|
+
if (varName === 'this' || varName === 'super')
|
|
500
|
+
continue;
|
|
501
|
+
// Skip keywords that could look like identifiers
|
|
502
|
+
if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
|
|
503
|
+
'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
|
|
504
|
+
'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
|
|
505
|
+
'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName))
|
|
506
|
+
continue;
|
|
507
|
+
// Check that this line doesn't start with const/let/var (would be a declaration, already handled)
|
|
508
|
+
const lineStart = source.lastIndexOf('\n', match.index) + 1;
|
|
509
|
+
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
510
|
+
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
|
|
511
|
+
continue;
|
|
512
|
+
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
513
|
+
// or if it's a label (label: ...)
|
|
514
|
+
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
515
|
+
if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
|
|
516
|
+
continue;
|
|
517
|
+
// Calculate line number
|
|
518
|
+
let lineNo = 1;
|
|
519
|
+
for (let i = 0; i < match.index; i++) {
|
|
520
|
+
if (source[i] === '\n')
|
|
521
|
+
lineNo++;
|
|
522
|
+
}
|
|
523
|
+
// Find end of statement (same logic as findVarDeclarations)
|
|
524
|
+
const startPos = match.index + match[0].length - 1;
|
|
525
|
+
let pos = startPos;
|
|
526
|
+
let depth = 0;
|
|
527
|
+
let foundEnd = -1;
|
|
528
|
+
while (pos < source.length) {
|
|
529
|
+
const ch = source[pos];
|
|
530
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
531
|
+
depth++;
|
|
532
|
+
}
|
|
533
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
534
|
+
depth--;
|
|
535
|
+
if (depth < 0)
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
else if (ch === ';' && depth === 0) {
|
|
539
|
+
foundEnd = pos;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
else if (ch === '\n' && depth === 0) {
|
|
543
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
544
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
545
|
+
foundEnd = pos;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
550
|
+
const quote = ch;
|
|
551
|
+
pos++;
|
|
552
|
+
while (pos < source.length) {
|
|
553
|
+
if (source[pos] === '\\') {
|
|
554
|
+
pos++;
|
|
555
|
+
}
|
|
556
|
+
else if (source[pos] === quote)
|
|
557
|
+
break;
|
|
558
|
+
pos++;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
562
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
563
|
+
pos++;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
567
|
+
pos += 2;
|
|
568
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
569
|
+
pos++;
|
|
570
|
+
pos++;
|
|
571
|
+
}
|
|
572
|
+
pos++;
|
|
573
|
+
}
|
|
574
|
+
if (foundEnd === -1)
|
|
575
|
+
continue;
|
|
576
|
+
results.push({ lineEnd: foundEnd + 1, varName, lineNo });
|
|
577
|
+
}
|
|
578
|
+
return results;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
582
|
+
* Handles:
|
|
583
|
+
* for (const item of items) { ... } → trace item
|
|
584
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
585
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
586
|
+
* for (const key in obj) { ... } → trace key
|
|
587
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
588
|
+
* Inserts trace calls at the start of the loop body.
|
|
589
|
+
*/
|
|
590
|
+
function findForLoopVars(source) {
|
|
591
|
+
const results = [];
|
|
592
|
+
// Match: for (const/let/var ...
|
|
593
|
+
const forRegex = /\bfor\s*\(/g;
|
|
594
|
+
let match;
|
|
595
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
596
|
+
const afterParen = match.index + match[0].length;
|
|
597
|
+
// Skip whitespace
|
|
598
|
+
let pos = afterParen;
|
|
599
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
600
|
+
pos++;
|
|
601
|
+
// Expect const/let/var
|
|
602
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
603
|
+
if (!declMatch)
|
|
604
|
+
continue;
|
|
605
|
+
pos += declMatch[0].length;
|
|
606
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
607
|
+
const varNames = [];
|
|
608
|
+
const patternStart = pos;
|
|
609
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
610
|
+
// Destructured: find matching brace/bracket
|
|
611
|
+
const open = source[pos];
|
|
612
|
+
const close = open === '{' ? '}' : ']';
|
|
613
|
+
let depth = 1;
|
|
614
|
+
let end = pos + 1;
|
|
615
|
+
while (end < source.length && depth > 0) {
|
|
616
|
+
if (source[end] === open)
|
|
617
|
+
depth++;
|
|
618
|
+
else if (source[end] === close)
|
|
619
|
+
depth--;
|
|
620
|
+
end++;
|
|
621
|
+
}
|
|
622
|
+
const pattern = source.slice(pos, end);
|
|
623
|
+
const names = extractDestructuredNames(pattern);
|
|
624
|
+
varNames.push(...names);
|
|
625
|
+
pos = end;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
// Simple identifier
|
|
629
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
630
|
+
if (!idMatch)
|
|
631
|
+
continue;
|
|
632
|
+
varNames.push(idMatch[1]);
|
|
633
|
+
pos += idMatch[0].length;
|
|
634
|
+
}
|
|
635
|
+
if (varNames.length === 0)
|
|
636
|
+
continue;
|
|
637
|
+
// Skip trickle internals
|
|
638
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c'))
|
|
639
|
+
continue;
|
|
640
|
+
// Now find the opening `{` of the loop body
|
|
641
|
+
// Skip everything until the `)` that closes the for(...)
|
|
642
|
+
let parenDepth = 1; // We're inside the for(
|
|
643
|
+
while (pos < source.length && parenDepth > 0) {
|
|
644
|
+
const ch = source[pos];
|
|
645
|
+
if (ch === '(')
|
|
646
|
+
parenDepth++;
|
|
647
|
+
else if (ch === ')')
|
|
648
|
+
parenDepth--;
|
|
649
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
650
|
+
const q = ch;
|
|
651
|
+
pos++;
|
|
652
|
+
while (pos < source.length && source[pos] !== q) {
|
|
653
|
+
if (source[pos] === '\\')
|
|
654
|
+
pos++;
|
|
655
|
+
pos++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
pos++;
|
|
659
|
+
}
|
|
660
|
+
// Now find the `{` after the closing `)`
|
|
661
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
662
|
+
pos++;
|
|
663
|
+
if (pos >= source.length || source[pos] !== '{')
|
|
664
|
+
continue;
|
|
665
|
+
const bodyBrace = pos;
|
|
666
|
+
// Calculate line number
|
|
667
|
+
let lineNo = 1;
|
|
668
|
+
for (let i = 0; i < match.index; i++) {
|
|
669
|
+
if (source[i] === '\n')
|
|
670
|
+
lineNo++;
|
|
671
|
+
}
|
|
672
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
673
|
+
}
|
|
674
|
+
return results;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
678
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
679
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
680
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
681
|
+
*/
|
|
682
|
+
function findFunctionParams(source, isReactFile) {
|
|
683
|
+
const results = [];
|
|
684
|
+
// Match function declarations: function name(params) {
|
|
685
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
686
|
+
let match;
|
|
687
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
688
|
+
const name = match[1];
|
|
689
|
+
if (name === 'require' || name === 'exports' || name === 'module')
|
|
690
|
+
continue;
|
|
691
|
+
if (name.startsWith('__trickle'))
|
|
692
|
+
continue;
|
|
693
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
694
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
695
|
+
continue;
|
|
696
|
+
const afterParen = match.index + match[0].length;
|
|
697
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
698
|
+
if (bodyBrace === -1)
|
|
699
|
+
continue;
|
|
700
|
+
// Extract parameter names from between ( and )
|
|
701
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
702
|
+
const closeParen = paramStr.indexOf(')');
|
|
703
|
+
if (closeParen === -1)
|
|
704
|
+
continue;
|
|
705
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
706
|
+
const paramNames = extractParamNames(rawParams);
|
|
707
|
+
if (paramNames.length === 0)
|
|
708
|
+
continue;
|
|
709
|
+
let lineNo = 1;
|
|
710
|
+
for (let i = 0; i < match.index; i++) {
|
|
711
|
+
if (source[i] === '\n')
|
|
712
|
+
lineNo++;
|
|
713
|
+
}
|
|
714
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
715
|
+
}
|
|
716
|
+
// Match arrow functions: const name = (params) => {
|
|
717
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
718
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
719
|
+
const name = match[1];
|
|
720
|
+
if (name.startsWith('__trickle'))
|
|
721
|
+
continue;
|
|
722
|
+
// Skip React components in React files
|
|
723
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
724
|
+
continue;
|
|
725
|
+
const rawParams = match[2].trim();
|
|
726
|
+
const paramNames = extractParamNames(rawParams);
|
|
727
|
+
if (paramNames.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
// Find the { position
|
|
730
|
+
const bracePos = match.index + match[0].length - 1;
|
|
731
|
+
let lineNo = 1;
|
|
732
|
+
for (let i = 0; i < match.index; i++) {
|
|
733
|
+
if (source[i] === '\n')
|
|
734
|
+
lineNo++;
|
|
735
|
+
}
|
|
736
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
737
|
+
}
|
|
738
|
+
return results;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Extract parameter names from a function parameter string.
|
|
742
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
743
|
+
* Skips type annotations.
|
|
744
|
+
*/
|
|
745
|
+
function extractParamNames(rawParams) {
|
|
746
|
+
if (!rawParams)
|
|
747
|
+
return [];
|
|
748
|
+
const names = [];
|
|
749
|
+
// Split by commas at depth 0
|
|
750
|
+
const parts = [];
|
|
751
|
+
let depth = 0;
|
|
752
|
+
let current = '';
|
|
753
|
+
for (const ch of rawParams) {
|
|
754
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<')
|
|
755
|
+
depth++;
|
|
756
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>')
|
|
757
|
+
depth--;
|
|
758
|
+
else if (ch === ',' && depth === 0) {
|
|
759
|
+
parts.push(current.trim());
|
|
760
|
+
current = '';
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
current += ch;
|
|
764
|
+
}
|
|
765
|
+
if (current.trim())
|
|
766
|
+
parts.push(current.trim());
|
|
767
|
+
for (const part of parts) {
|
|
768
|
+
const trimmed = part.trim();
|
|
769
|
+
if (!trimmed)
|
|
770
|
+
continue;
|
|
771
|
+
// Destructured params — extract individual names
|
|
772
|
+
if (trimmed.startsWith('{')) {
|
|
773
|
+
const closeBrace = trimmed.indexOf('}');
|
|
774
|
+
if (closeBrace !== -1) {
|
|
775
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
776
|
+
names.push(...destructNames);
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (trimmed.startsWith('[')) {
|
|
781
|
+
const closeBracket = trimmed.indexOf(']');
|
|
782
|
+
if (closeBracket !== -1) {
|
|
783
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
784
|
+
names.push(...destructNames);
|
|
785
|
+
}
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// Rest parameter: ...args
|
|
789
|
+
if (trimmed.startsWith('...')) {
|
|
790
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
791
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
792
|
+
names.push(restName);
|
|
793
|
+
}
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
// Simple param: name or name: Type or name = default
|
|
797
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
798
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
799
|
+
names.push(paramName);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return names;
|
|
803
|
+
}
|
|
476
804
|
/**
|
|
477
805
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
478
806
|
*
|
|
@@ -880,7 +1208,13 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
880
1208
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
881
1209
|
// Find destructured variable declarations for tracing
|
|
882
1210
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
883
|
-
|
|
1211
|
+
// Find variable reassignments for tracing
|
|
1212
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1213
|
+
// Find for-loop variable declarations for tracing
|
|
1214
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1215
|
+
// Find function parameter names for tracing
|
|
1216
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1217
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
884
1218
|
return source;
|
|
885
1219
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
886
1220
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -900,9 +1234,56 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
900
1234
|
di.lineNo = origLine;
|
|
901
1235
|
}
|
|
902
1236
|
}
|
|
1237
|
+
// Fix reassignment line numbers
|
|
1238
|
+
for (const ri of reassignInsertions) {
|
|
1239
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1240
|
+
if (origLine !== -1)
|
|
1241
|
+
ri.lineNo = origLine;
|
|
1242
|
+
}
|
|
1243
|
+
// Fix for-loop var line numbers
|
|
1244
|
+
for (const fi of forLoopInsertions) {
|
|
1245
|
+
if (fi.varNames.length > 0) {
|
|
1246
|
+
// Search for 'for' keyword near the expected line
|
|
1247
|
+
const pattern = /\bfor\s*\(/;
|
|
1248
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1249
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1250
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1251
|
+
fi.lineNo = fwd + 1;
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
if (delta > 0 && delta <= 10) {
|
|
1255
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1256
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1257
|
+
fi.lineNo = bwd + 1;
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// Fix function param line numbers
|
|
1265
|
+
for (const fp of funcParamInsertions) {
|
|
1266
|
+
if (fp.paramNames.length > 0) {
|
|
1267
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1268
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1269
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1270
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1271
|
+
fp.lineNo = fwd + 1;
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
if (delta > 0 && delta <= 10) {
|
|
1275
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1276
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1277
|
+
fp.lineNo = bwd + 1;
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
903
1284
|
}
|
|
904
1285
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
905
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1286
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
906
1287
|
const importLines = [
|
|
907
1288
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
908
1289
|
];
|
|
@@ -940,7 +1321,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
940
1321
|
}
|
|
941
1322
|
}
|
|
942
1323
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
943
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1324
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
944
1325
|
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Set();`, ` 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 ck = file + ':' + l + ':' + n + ':' + th;`, ` if (_cache.has(ck)) return;`, ` _cache.add(ck);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
945
1326
|
}
|
|
946
1327
|
// Add React component render tracker if needed
|
|
@@ -978,6 +1359,29 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
978
1359
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
979
1360
|
});
|
|
980
1361
|
}
|
|
1362
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1363
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1364
|
+
allInsertions.push({
|
|
1365
|
+
position: lineEnd,
|
|
1366
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1370
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1371
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1372
|
+
allInsertions.push({
|
|
1373
|
+
position: bodyStart,
|
|
1374
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1378
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1379
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1380
|
+
allInsertions.push({
|
|
1381
|
+
position: bodyStart,
|
|
1382
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
981
1385
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
982
1386
|
allInsertions.push({
|
|
983
1387
|
position,
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -461,6 +461,331 @@ function extractDestructuredNames(pattern: string): string[] {
|
|
|
461
461
|
return names;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Find variable reassignments (not declarations) and return insertions for tracing.
|
|
466
|
+
* Handles: x = newValue; x += 1; x ||= fallback; etc.
|
|
467
|
+
* Only matches standalone reassignment statements at the start of a line.
|
|
468
|
+
* Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
|
|
469
|
+
* comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
|
|
470
|
+
*/
|
|
471
|
+
function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
|
|
472
|
+
const results: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
|
|
473
|
+
|
|
474
|
+
// Match: <identifier> <assignOp>= <value> at the start of a line
|
|
475
|
+
// Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
|
|
476
|
+
// Plain: = (but not ==, ===, =>, !=)
|
|
477
|
+
const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
|
|
478
|
+
let match;
|
|
479
|
+
|
|
480
|
+
while ((match = reassignRegex.exec(source)) !== null) {
|
|
481
|
+
const varName = match[2];
|
|
482
|
+
|
|
483
|
+
// Skip trickle internals
|
|
484
|
+
if (varName.startsWith('__trickle') || varName.startsWith('_$')) continue;
|
|
485
|
+
// Skip common non-variable patterns
|
|
486
|
+
if (varName === '_a' || varName === '_b' || varName === '_c') continue;
|
|
487
|
+
// Skip 'this', 'self', 'super' (not reassignable in practice)
|
|
488
|
+
if (varName === 'this' || varName === 'super') continue;
|
|
489
|
+
// Skip keywords that could look like identifiers
|
|
490
|
+
if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
|
|
491
|
+
'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
|
|
492
|
+
'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
|
|
493
|
+
'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName)) continue;
|
|
494
|
+
|
|
495
|
+
// Check that this line doesn't start with const/let/var (would be a declaration, already handled)
|
|
496
|
+
const lineStart = source.lastIndexOf('\n', match.index) + 1;
|
|
497
|
+
const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
|
|
498
|
+
if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart())) continue;
|
|
499
|
+
|
|
500
|
+
// Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
|
|
501
|
+
// or if it's a label (label: ...)
|
|
502
|
+
const beforeOnLine = source.slice(lineStart, match.index).trim();
|
|
503
|
+
if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(',')) continue;
|
|
504
|
+
|
|
505
|
+
// Calculate line number
|
|
506
|
+
let lineNo = 1;
|
|
507
|
+
for (let i = 0; i < match.index; i++) {
|
|
508
|
+
if (source[i] === '\n') lineNo++;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Find end of statement (same logic as findVarDeclarations)
|
|
512
|
+
const startPos = match.index + match[0].length - 1;
|
|
513
|
+
let pos = startPos;
|
|
514
|
+
let depth = 0;
|
|
515
|
+
let foundEnd = -1;
|
|
516
|
+
|
|
517
|
+
while (pos < source.length) {
|
|
518
|
+
const ch = source[pos];
|
|
519
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
520
|
+
depth++;
|
|
521
|
+
} else if (ch === ')' || ch === ']' || ch === '}') {
|
|
522
|
+
depth--;
|
|
523
|
+
if (depth < 0) break;
|
|
524
|
+
} else if (ch === ';' && depth === 0) {
|
|
525
|
+
foundEnd = pos;
|
|
526
|
+
break;
|
|
527
|
+
} else if (ch === '\n' && depth === 0) {
|
|
528
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
529
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
530
|
+
foundEnd = pos;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
534
|
+
const quote = ch;
|
|
535
|
+
pos++;
|
|
536
|
+
while (pos < source.length) {
|
|
537
|
+
if (source[pos] === '\\') { pos++; }
|
|
538
|
+
else if (source[pos] === quote) break;
|
|
539
|
+
pos++;
|
|
540
|
+
}
|
|
541
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
542
|
+
while (pos < source.length && source[pos] !== '\n') pos++;
|
|
543
|
+
continue;
|
|
544
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
545
|
+
pos += 2;
|
|
546
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
|
|
547
|
+
pos++;
|
|
548
|
+
}
|
|
549
|
+
pos++;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (foundEnd === -1) continue;
|
|
553
|
+
results.push({ lineEnd: foundEnd + 1, varName, lineNo });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return results;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
561
|
+
* Handles:
|
|
562
|
+
* for (const item of items) { ... } → trace item
|
|
563
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
564
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
565
|
+
* for (const key in obj) { ... } → trace key
|
|
566
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
567
|
+
* Inserts trace calls at the start of the loop body.
|
|
568
|
+
*/
|
|
569
|
+
function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
|
|
570
|
+
const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
|
|
571
|
+
|
|
572
|
+
// Match: for (const/let/var ...
|
|
573
|
+
const forRegex = /\bfor\s*\(/g;
|
|
574
|
+
let match;
|
|
575
|
+
|
|
576
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
577
|
+
const afterParen = match.index + match[0].length;
|
|
578
|
+
|
|
579
|
+
// Skip whitespace
|
|
580
|
+
let pos = afterParen;
|
|
581
|
+
while (pos < source.length && /\s/.test(source[pos])) pos++;
|
|
582
|
+
|
|
583
|
+
// Expect const/let/var
|
|
584
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
585
|
+
if (!declMatch) continue;
|
|
586
|
+
pos += declMatch[0].length;
|
|
587
|
+
|
|
588
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
589
|
+
const varNames: string[] = [];
|
|
590
|
+
const patternStart = pos;
|
|
591
|
+
|
|
592
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
593
|
+
// Destructured: find matching brace/bracket
|
|
594
|
+
const open = source[pos];
|
|
595
|
+
const close = open === '{' ? '}' : ']';
|
|
596
|
+
let depth = 1;
|
|
597
|
+
let end = pos + 1;
|
|
598
|
+
while (end < source.length && depth > 0) {
|
|
599
|
+
if (source[end] === open) depth++;
|
|
600
|
+
else if (source[end] === close) depth--;
|
|
601
|
+
end++;
|
|
602
|
+
}
|
|
603
|
+
const pattern = source.slice(pos, end);
|
|
604
|
+
const names = extractDestructuredNames(pattern);
|
|
605
|
+
varNames.push(...names);
|
|
606
|
+
pos = end;
|
|
607
|
+
} else {
|
|
608
|
+
// Simple identifier
|
|
609
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
610
|
+
if (!idMatch) continue;
|
|
611
|
+
varNames.push(idMatch[1]);
|
|
612
|
+
pos += idMatch[0].length;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (varNames.length === 0) continue;
|
|
616
|
+
|
|
617
|
+
// Skip trickle internals
|
|
618
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c')) continue;
|
|
619
|
+
|
|
620
|
+
// Now find the opening `{` of the loop body
|
|
621
|
+
// Skip everything until the `)` that closes the for(...)
|
|
622
|
+
let parenDepth = 1; // We're inside the for(
|
|
623
|
+
while (pos < source.length && parenDepth > 0) {
|
|
624
|
+
const ch = source[pos];
|
|
625
|
+
if (ch === '(') parenDepth++;
|
|
626
|
+
else if (ch === ')') parenDepth--;
|
|
627
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
628
|
+
const q = ch; pos++;
|
|
629
|
+
while (pos < source.length && source[pos] !== q) {
|
|
630
|
+
if (source[pos] === '\\') pos++;
|
|
631
|
+
pos++;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
pos++;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Now find the `{` after the closing `)`
|
|
638
|
+
while (pos < source.length && /\s/.test(source[pos])) pos++;
|
|
639
|
+
if (pos >= source.length || source[pos] !== '{') continue;
|
|
640
|
+
|
|
641
|
+
const bodyBrace = pos;
|
|
642
|
+
|
|
643
|
+
// Calculate line number
|
|
644
|
+
let lineNo = 1;
|
|
645
|
+
for (let i = 0; i < match.index; i++) {
|
|
646
|
+
if (source[i] === '\n') lineNo++;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return results;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
657
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
658
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
659
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
660
|
+
*/
|
|
661
|
+
function findFunctionParams(source: string, isReactFile: boolean): Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> {
|
|
662
|
+
const results: Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> = [];
|
|
663
|
+
|
|
664
|
+
// Match function declarations: function name(params) {
|
|
665
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
666
|
+
let match;
|
|
667
|
+
|
|
668
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
669
|
+
const name = match[1];
|
|
670
|
+
if (name === 'require' || name === 'exports' || name === 'module') continue;
|
|
671
|
+
if (name.startsWith('__trickle')) continue;
|
|
672
|
+
|
|
673
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
674
|
+
if (isReactFile && /^[A-Z]/.test(name)) continue;
|
|
675
|
+
|
|
676
|
+
const afterParen = match.index + match[0].length;
|
|
677
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
678
|
+
if (bodyBrace === -1) continue;
|
|
679
|
+
|
|
680
|
+
// Extract parameter names from between ( and )
|
|
681
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
682
|
+
const closeParen = paramStr.indexOf(')');
|
|
683
|
+
if (closeParen === -1) continue;
|
|
684
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
685
|
+
const paramNames = extractParamNames(rawParams);
|
|
686
|
+
if (paramNames.length === 0) continue;
|
|
687
|
+
|
|
688
|
+
let lineNo = 1;
|
|
689
|
+
for (let i = 0; i < match.index; i++) {
|
|
690
|
+
if (source[i] === '\n') lineNo++;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Match arrow functions: const name = (params) => {
|
|
697
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
698
|
+
|
|
699
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
700
|
+
const name = match[1];
|
|
701
|
+
if (name.startsWith('__trickle')) continue;
|
|
702
|
+
// Skip React components in React files
|
|
703
|
+
if (isReactFile && /^[A-Z]/.test(name)) continue;
|
|
704
|
+
|
|
705
|
+
const rawParams = match[2].trim();
|
|
706
|
+
const paramNames = extractParamNames(rawParams);
|
|
707
|
+
if (paramNames.length === 0) continue;
|
|
708
|
+
|
|
709
|
+
// Find the { position
|
|
710
|
+
const bracePos = match.index + match[0].length - 1;
|
|
711
|
+
|
|
712
|
+
let lineNo = 1;
|
|
713
|
+
for (let i = 0; i < match.index; i++) {
|
|
714
|
+
if (source[i] === '\n') lineNo++;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return results;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Extract parameter names from a function parameter string.
|
|
725
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
726
|
+
* Skips type annotations.
|
|
727
|
+
*/
|
|
728
|
+
function extractParamNames(rawParams: string): string[] {
|
|
729
|
+
if (!rawParams) return [];
|
|
730
|
+
const names: string[] = [];
|
|
731
|
+
|
|
732
|
+
// Split by commas at depth 0
|
|
733
|
+
const parts: string[] = [];
|
|
734
|
+
let depth = 0;
|
|
735
|
+
let current = '';
|
|
736
|
+
for (const ch of rawParams) {
|
|
737
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<') depth++;
|
|
738
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>') depth--;
|
|
739
|
+
else if (ch === ',' && depth === 0) {
|
|
740
|
+
parts.push(current.trim());
|
|
741
|
+
current = '';
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
current += ch;
|
|
745
|
+
}
|
|
746
|
+
if (current.trim()) parts.push(current.trim());
|
|
747
|
+
|
|
748
|
+
for (const part of parts) {
|
|
749
|
+
const trimmed = part.trim();
|
|
750
|
+
if (!trimmed) continue;
|
|
751
|
+
|
|
752
|
+
// Destructured params — extract individual names
|
|
753
|
+
if (trimmed.startsWith('{')) {
|
|
754
|
+
const closeBrace = trimmed.indexOf('}');
|
|
755
|
+
if (closeBrace !== -1) {
|
|
756
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
757
|
+
names.push(...destructNames);
|
|
758
|
+
}
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (trimmed.startsWith('[')) {
|
|
762
|
+
const closeBracket = trimmed.indexOf(']');
|
|
763
|
+
if (closeBracket !== -1) {
|
|
764
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
765
|
+
names.push(...destructNames);
|
|
766
|
+
}
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Rest parameter: ...args
|
|
771
|
+
if (trimmed.startsWith('...')) {
|
|
772
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
773
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
774
|
+
names.push(restName);
|
|
775
|
+
}
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Simple param: name or name: Type or name = default
|
|
780
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
781
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
782
|
+
names.push(paramName);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return names;
|
|
787
|
+
}
|
|
788
|
+
|
|
464
789
|
/**
|
|
465
790
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
466
791
|
*
|
|
@@ -878,7 +1203,16 @@ export function transformEsmSource(
|
|
|
878
1203
|
// Find destructured variable declarations for tracing
|
|
879
1204
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
880
1205
|
|
|
881
|
-
|
|
1206
|
+
// Find variable reassignments for tracing
|
|
1207
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1208
|
+
|
|
1209
|
+
// Find for-loop variable declarations for tracing
|
|
1210
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1211
|
+
|
|
1212
|
+
// Find function parameter names for tracing
|
|
1213
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1214
|
+
|
|
1215
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0) return source;
|
|
882
1216
|
|
|
883
1217
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
884
1218
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -897,10 +1231,56 @@ export function transformEsmSource(
|
|
|
897
1231
|
if (origLine !== -1) di.lineNo = origLine;
|
|
898
1232
|
}
|
|
899
1233
|
}
|
|
1234
|
+
// Fix reassignment line numbers
|
|
1235
|
+
for (const ri of reassignInsertions) {
|
|
1236
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1237
|
+
if (origLine !== -1) ri.lineNo = origLine;
|
|
1238
|
+
}
|
|
1239
|
+
// Fix for-loop var line numbers
|
|
1240
|
+
for (const fi of forLoopInsertions) {
|
|
1241
|
+
if (fi.varNames.length > 0) {
|
|
1242
|
+
// Search for 'for' keyword near the expected line
|
|
1243
|
+
const pattern = /\bfor\s*\(/;
|
|
1244
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1245
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1246
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1247
|
+
fi.lineNo = fwd + 1;
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
if (delta > 0 && delta <= 10) {
|
|
1251
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1252
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1253
|
+
fi.lineNo = bwd + 1;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
// Fix function param line numbers
|
|
1261
|
+
for (const fp of funcParamInsertions) {
|
|
1262
|
+
if (fp.paramNames.length > 0) {
|
|
1263
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1264
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1265
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1266
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1267
|
+
fp.lineNo = fwd + 1;
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
if (delta > 0 && delta <= 10) {
|
|
1271
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1272
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1273
|
+
fp.lineNo = bwd + 1;
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
900
1280
|
}
|
|
901
1281
|
|
|
902
1282
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
903
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1283
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
904
1284
|
const importLines: string[] = [
|
|
905
1285
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
906
1286
|
];
|
|
@@ -970,7 +1350,7 @@ export function transformEsmSource(
|
|
|
970
1350
|
}
|
|
971
1351
|
|
|
972
1352
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
973
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1353
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
974
1354
|
prefixLines.push(
|
|
975
1355
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
976
1356
|
` const _cache = new Set();`,
|
|
@@ -1141,6 +1521,32 @@ export function transformEsmSource(
|
|
|
1141
1521
|
});
|
|
1142
1522
|
}
|
|
1143
1523
|
|
|
1524
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1525
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1526
|
+
allInsertions.push({
|
|
1527
|
+
position: lineEnd,
|
|
1528
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1533
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1534
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1535
|
+
allInsertions.push({
|
|
1536
|
+
position: bodyStart,
|
|
1537
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1542
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1543
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1544
|
+
allInsertions.push({
|
|
1545
|
+
position: bodyStart,
|
|
1546
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1144
1550
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1145
1551
|
allInsertions.push({
|
|
1146
1552
|
position,
|