trickle-observe 0.2.77 → 0.2.79

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.
@@ -39,6 +39,7 @@ const wrap_1 = require("./wrap");
39
39
  const fetch_observer_1 = require("./fetch-observer");
40
40
  const express_1 = require("./express");
41
41
  const trace_var_1 = require("./trace-var");
42
+ const vite_plugin_1 = require("./vite-plugin");
42
43
  const M = module_1.default;
43
44
  const originalLoad = M._load;
44
45
  const originalCompile = M.prototype._compile;
@@ -538,7 +539,12 @@ function transformCjsSource(source, filename, moduleName, env) {
538
539
  destructInsertions = findDestructuredDeclarations(source);
539
540
  }
540
541
  }
541
- if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && classInsertions.length === 0)
542
+ // Additional variable patterns: reassignments, for-loops, catch clauses, function params
543
+ const reassignInsertions = (0, vite_plugin_1.findReassignments)(source);
544
+ const forLoopInsertions = (0, vite_plugin_1.findForLoopVars)(source);
545
+ const catchInsertions = (0, vite_plugin_1.findCatchVars)(source);
546
+ const funcParamInsertions = (0, vite_plugin_1.findFunctionParams)(source, false);
547
+ if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && classInsertions.length === 0)
542
548
  return source;
543
549
  // Resolve the path to the wrap helper (compiled JS)
544
550
  const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
@@ -561,7 +567,7 @@ function transformCjsSource(source, filename, moduleName, env) {
561
567
  `};`,
562
568
  ];
563
569
  // Add variable tracing helper if we have var insertions
564
- if (varInsertions.length > 0 || destructInsertions.length > 0) {
570
+ if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
565
571
  const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
566
572
  prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e){} };`);
567
573
  }
@@ -588,6 +594,37 @@ function transformCjsSource(source, filename, moduleName, env) {
588
594
  code: `\n;try{${calls}}catch(__e){}\n`,
589
595
  });
590
596
  }
597
+ // Reassignment insertions
598
+ for (const { lineEnd, varName, lineNo } of reassignInsertions) {
599
+ allInsertions.push({
600
+ position: lineEnd,
601
+ code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
602
+ });
603
+ }
604
+ // For-loop variable insertions
605
+ for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
606
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
607
+ allInsertions.push({
608
+ position: bodyStart,
609
+ code: `\ntry{${calls}}catch(__e){}\n`,
610
+ });
611
+ }
612
+ // Catch clause insertions
613
+ for (const { bodyStart, varNames, lineNo } of catchInsertions) {
614
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
615
+ allInsertions.push({
616
+ position: bodyStart,
617
+ code: `\ntry{${calls}}catch(__e2){}\n`,
618
+ });
619
+ }
620
+ // Function parameter insertions
621
+ for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
622
+ const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
623
+ allInsertions.push({
624
+ position: bodyStart,
625
+ code: `\ntry{${calls}}catch(__e){}\n`,
626
+ });
627
+ }
591
628
  // Add class method wrappings
592
629
  for (const ci of classInsertions) {
593
630
  allInsertions.push(ci);
package/dist/trace-var.js CHANGED
@@ -57,8 +57,8 @@ const type_hash_1 = require("./type-hash");
57
57
  /** Where to write variable observations */
58
58
  let varsFilePath = '';
59
59
  let debugMode = false;
60
- /** Cache: "file:line:varName:typeHash" → already written */
61
- const varCache = new Set();
60
+ /** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
61
+ const varCache = new Map();
62
62
  /** Batch buffer for writing — avoids one fs.appendFileSync per variable */
63
63
  let varBuffer = [];
64
64
  let flushTimer = null;
@@ -105,10 +105,17 @@ function traceVar(value, varName, line, moduleName, filePath) {
105
105
  // Create a stable hash for dedup
106
106
  const dummyArgs = { kind: 'tuple', elements: [] };
107
107
  const typeHash = (0, type_hash_1.hashType)(dummyArgs, type);
108
- const cacheKey = `${filePath}:${line}:${varName}:${typeHash}`;
109
- if (varCache.has(cacheKey))
108
+ // Value-aware dedup: re-send if value changed or 10s elapsed
109
+ const cacheKey = `${filePath}:${line}:${varName}`;
110
+ const t = typeof value;
111
+ const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
112
+ ? String(value).substring(0, 60)
113
+ : typeHash;
114
+ const now = Date.now();
115
+ const prev = varCache.get(cacheKey);
116
+ if (prev && prev.fp === fp && (now - prev.ts) < 10000)
110
117
  return;
111
- varCache.add(cacheKey);
118
+ varCache.set(cacheKey, { fp, ts: now });
112
119
  const sample = sanitizeVarSample(value);
113
120
  const observation = {
114
121
  kind: 'variable',
@@ -40,6 +40,53 @@ export declare function tricklePlugin(options?: TricklePluginOptions): {
40
40
  map: null;
41
41
  } | null;
42
42
  };
43
+ /**
44
+ * Find variable reassignments (not declarations) and return insertions for tracing.
45
+ * Handles: x = newValue; x += 1; x ||= fallback; etc.
46
+ * Only matches standalone reassignment statements at the start of a line.
47
+ * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
48
+ * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
49
+ */
50
+ export declare function findReassignments(source: string): Array<{
51
+ lineEnd: number;
52
+ varName: string;
53
+ lineNo: number;
54
+ }>;
55
+ /**
56
+ * Find for-loop variable declarations and return insertions for tracing.
57
+ * Handles:
58
+ * for (const item of items) { ... } → trace item
59
+ * for (const [key, val] of entries) { ... } → trace key, val
60
+ * for (const { a, b } of items) { ... } → trace a, b
61
+ * for (const key in obj) { ... } → trace key
62
+ * for (let i = 0; i < n; i++) { ... } → trace i
63
+ * Inserts trace calls at the start of the loop body.
64
+ */
65
+ /**
66
+ * Find catch clause variables and return insertions for tracing.
67
+ * Handles: catch (err) { ... } → trace err at start of catch body.
68
+ */
69
+ export declare function findCatchVars(source: string): Array<{
70
+ bodyStart: number;
71
+ varNames: string[];
72
+ lineNo: number;
73
+ }>;
74
+ export declare function findForLoopVars(source: string): Array<{
75
+ bodyStart: number;
76
+ varNames: string[];
77
+ lineNo: number;
78
+ }>;
79
+ /**
80
+ * Find function parameter names and return insertions for tracing at the start
81
+ * of function bodies. Traces the runtime values of all parameters.
82
+ * Handles: function declarations, arrow functions, method definitions.
83
+ * Skips: React components (already tracked via __trickle_rc with props).
84
+ */
85
+ export declare function findFunctionParams(source: string, isReactFile: boolean): Array<{
86
+ bodyStart: number;
87
+ paramNames: string[];
88
+ lineNo: number;
89
+ }>;
43
90
  export declare function transformEsmSource(source: string, filename: string, moduleName: string, backendUrl: string, debug: boolean, traceVars: boolean, originalSource?: string | null, isSSR?: boolean,
44
91
  /** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
45
92
  ingestUrl?: string | null): string;
@@ -23,6 +23,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.tricklePlugin = tricklePlugin;
26
+ exports.findReassignments = findReassignments;
27
+ exports.findCatchVars = findCatchVars;
28
+ exports.findForLoopVars = findForLoopVars;
29
+ exports.findFunctionParams = findFunctionParams;
26
30
  exports.transformEsmSource = transformEsmSource;
27
31
  const path_1 = __importDefault(require("path"));
28
32
  const fs_1 = __importDefault(require("fs"));
@@ -658,8 +662,8 @@ function findJsxExpressions(source) {
658
662
  // Skip if this looks like a template literal expression `${`
659
663
  if (charBefore === '$')
660
664
  continue;
661
- // Skip if preceded by `(` or `,` (function arguments, not JSX)
662
- if (charBefore === '(' || charBefore === ',')
665
+ // Skip if preceded by `,` (function arguments, not JSX)
666
+ if (charBefore === ',')
663
667
  continue;
664
668
  // Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
665
669
  // before hitting structural JS characters like `{`, `(`, `;`
@@ -675,8 +679,19 @@ function findJsxExpressions(source) {
675
679
  inJsx = true;
676
680
  break;
677
681
  } // After a previous JSX expression
678
- if (ch === '{' || ch === '(' || ch === ';')
682
+ if (ch === '{' || ch === ';')
679
683
  break;
684
+ // `(` breaks scan in code context, but in JSX text `(` is normal
685
+ // Check: if `(` is preceded by `>` or text, it's JSX text
686
+ if (ch === '(') {
687
+ const before = source.slice(Math.max(0, scanPos - 20), scanPos).trim();
688
+ if (before.endsWith('>') || /[a-zA-Z0-9\s]$/.test(before)) {
689
+ // Could be JSX text like "Users ({count})" — keep scanning
690
+ scanPos--;
691
+ continue;
692
+ }
693
+ break;
694
+ }
680
695
  // `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
681
696
  if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
682
697
  // Check if this `=` is a JSX attribute assignment: look further back for tag
@@ -488,7 +488,7 @@ function extractDestructuredNames(pattern) {
488
488
  * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
489
489
  * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
490
490
  */
491
- function findReassignments(source) {
491
+ export function findReassignments(source) {
492
492
  const results = [];
493
493
  // Match: <identifier> <assignOp>= <value> at the start of a line
494
494
  // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
@@ -604,7 +604,7 @@ function findReassignments(source) {
604
604
  * Find catch clause variables and return insertions for tracing.
605
605
  * Handles: catch (err) { ... } → trace err at start of catch body.
606
606
  */
607
- function findCatchVars(source) {
607
+ export function findCatchVars(source) {
608
608
  const results = [];
609
609
  const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
610
610
  let match;
@@ -651,8 +651,8 @@ function findJsxExpressions(source) {
651
651
  // Skip if this looks like a template literal expression `${`
652
652
  if (charBefore === '$')
653
653
  continue;
654
- // Skip if preceded by `(` or `,` (function arguments, not JSX)
655
- if (charBefore === '(' || charBefore === ',')
654
+ // Skip if preceded by `,` (function arguments, not JSX)
655
+ if (charBefore === ',')
656
656
  continue;
657
657
  // Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
658
658
  // before hitting structural JS characters like `{`, `(`, `;`
@@ -668,8 +668,19 @@ function findJsxExpressions(source) {
668
668
  inJsx = true;
669
669
  break;
670
670
  } // After a previous JSX expression
671
- if (ch === '{' || ch === '(' || ch === ';')
671
+ if (ch === '{' || ch === ';')
672
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
+ }
673
684
  // `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
674
685
  if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
675
686
  // Check if this `=` is a JSX attribute assignment: look further back for tag
@@ -755,7 +766,7 @@ function findJsxExpressions(source) {
755
766
  }
756
767
  return results;
757
768
  }
758
- function findForLoopVars(source) {
769
+ export function findForLoopVars(source) {
759
770
  const results = [];
760
771
  // Match: for (const/let/var ...
761
772
  const forRegex = /\bfor\s*\(/g;
@@ -847,7 +858,7 @@ function findForLoopVars(source) {
847
858
  * Handles: function declarations, arrow functions, method definitions.
848
859
  * Skips: React components (already tracked via __trickle_rc with props).
849
860
  */
850
- function findFunctionParams(source, isReactFile) {
861
+ export function findFunctionParams(source, isReactFile) {
851
862
  const results = [];
852
863
  // Match function declarations: function name(params) {
853
864
  const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.77",
3
+ "version": "0.2.79",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,6 +36,12 @@ import { WrapOptions } from './types';
36
36
  import { patchFetch } from './fetch-observer';
37
37
  import { instrumentExpress, trickleMiddleware } from './express';
38
38
  import { initVarTracer, traceVar } from './trace-var';
39
+ import {
40
+ findReassignments,
41
+ findForLoopVars,
42
+ findCatchVars,
43
+ findFunctionParams,
44
+ } from './vite-plugin';
39
45
 
40
46
  const M = Module as any;
41
47
  const originalLoad = M._load;
@@ -508,7 +514,13 @@ function transformCjsSource(source: string, filename: string, moduleName: string
508
514
  }
509
515
  }
510
516
 
511
- if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && classInsertions.length === 0) return source;
517
+ // Additional variable patterns: reassignments, for-loops, catch clauses, function params
518
+ const reassignInsertions = findReassignments(source);
519
+ const forLoopInsertions = findForLoopVars(source);
520
+ const catchInsertions = findCatchVars(source);
521
+ const funcParamInsertions = findFunctionParams(source, false);
522
+
523
+ if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && classInsertions.length === 0) return source;
512
524
 
513
525
  // Resolve the path to the wrap helper (compiled JS)
514
526
  const wrapHelperPath = path.join(__dirname, 'wrap.js');
@@ -533,7 +545,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
533
545
  ];
534
546
 
535
547
  // Add variable tracing helper if we have var insertions
536
- if (varInsertions.length > 0 || destructInsertions.length > 0) {
548
+ if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
537
549
  const traceVarPath = path.join(__dirname, 'trace-var.js');
538
550
  prefixLines.push(
539
551
  `var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
@@ -571,6 +583,41 @@ function transformCjsSource(source: string, filename: string, moduleName: string
571
583
  });
572
584
  }
573
585
 
586
+ // Reassignment insertions
587
+ for (const { lineEnd, varName, lineNo } of reassignInsertions) {
588
+ allInsertions.push({
589
+ position: lineEnd,
590
+ code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
591
+ });
592
+ }
593
+
594
+ // For-loop variable insertions
595
+ for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
596
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
597
+ allInsertions.push({
598
+ position: bodyStart,
599
+ code: `\ntry{${calls}}catch(__e){}\n`,
600
+ });
601
+ }
602
+
603
+ // Catch clause insertions
604
+ for (const { bodyStart, varNames, lineNo } of catchInsertions) {
605
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
606
+ allInsertions.push({
607
+ position: bodyStart,
608
+ code: `\ntry{${calls}}catch(__e2){}\n`,
609
+ });
610
+ }
611
+
612
+ // Function parameter insertions
613
+ for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
614
+ const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
615
+ allInsertions.push({
616
+ position: bodyStart,
617
+ code: `\ntry{${calls}}catch(__e){}\n`,
618
+ });
619
+ }
620
+
574
621
  // Add class method wrappings
575
622
  for (const ci of classInsertions) {
576
623
  allInsertions.push(ci);
package/src/trace-var.ts CHANGED
@@ -24,8 +24,8 @@ import { hashType } from './type-hash';
24
24
  let varsFilePath = '';
25
25
  let debugMode = false;
26
26
 
27
- /** Cache: "file:line:varName:typeHash" → already written */
28
- const varCache = new Set<string>();
27
+ /** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
28
+ const varCache = new Map<string, { fp: string; ts: number }>();
29
29
 
30
30
  /** Batch buffer for writing — avoids one fs.appendFileSync per variable */
31
31
  let varBuffer: string[] = [];
@@ -92,9 +92,16 @@ export function traceVar(
92
92
  const dummyArgs: TypeNode = { kind: 'tuple', elements: [] };
93
93
  const typeHash = hashType(dummyArgs, type);
94
94
 
95
- const cacheKey = `${filePath}:${line}:${varName}:${typeHash}`;
96
- if (varCache.has(cacheKey)) return;
97
- varCache.add(cacheKey);
95
+ // Value-aware dedup: re-send if value changed or 10s elapsed
96
+ const cacheKey = `${filePath}:${line}:${varName}`;
97
+ const t = typeof value;
98
+ const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
99
+ ? String(value).substring(0, 60)
100
+ : typeHash;
101
+ const now = Date.now();
102
+ const prev = varCache.get(cacheKey);
103
+ if (prev && prev.fp === fp && (now - prev.ts) < 10000) return;
104
+ varCache.set(cacheKey, { fp, ts: now });
98
105
 
99
106
  const sample = sanitizeVarSample(value);
100
107
 
@@ -473,7 +473,7 @@ function extractDestructuredNames(pattern: string): string[] {
473
473
  * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
474
474
  * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
475
475
  */
476
- function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
476
+ export function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
477
477
  const results: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
478
478
 
479
479
  // Match: <identifier> <assignOp>= <value> at the start of a line
@@ -578,7 +578,7 @@ function findReassignments(source: string): Array<{ lineEnd: number; varName: st
578
578
  * Find catch clause variables and return insertions for tracing.
579
579
  * Handles: catch (err) { ... } → trace err at start of catch body.
580
580
  */
581
- function findCatchVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
581
+ export function findCatchVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
582
582
  const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
583
583
  const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
584
584
  let match;
@@ -632,8 +632,8 @@ function findJsxExpressions(source: string): Array<{ exprStart: number; exprEnd:
632
632
  // Skip if this looks like a template literal expression `${`
633
633
  if (charBefore === '$') continue;
634
634
 
635
- // Skip if preceded by `(` or `,` (function arguments, not JSX)
636
- if (charBefore === '(' || charBefore === ',') continue;
635
+ // Skip if preceded by `,` (function arguments, not JSX)
636
+ if (charBefore === ',') continue;
637
637
 
638
638
  // Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
639
639
  // before hitting structural JS characters like `{`, `(`, `;`
@@ -643,7 +643,18 @@ function findJsxExpressions(source: string): Array<{ exprStart: number; exprEnd:
643
643
  const ch = source[scanPos];
644
644
  if (ch === '>') { inJsx = true; break; }
645
645
  if (ch === '}') { inJsx = true; break; } // After a previous JSX expression
646
- if (ch === '{' || ch === '(' || ch === ';') break;
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
+ }
647
658
  // `=` only breaks if NOT preceded by other text (could be JSX text like "count = 5")
648
659
  if (ch === '=' && scanPos > 0 && /\s/.test(source[scanPos - 1])) {
649
660
  // Check if this `=` is a JSX attribute assignment: look further back for tag
@@ -720,7 +731,7 @@ function findJsxExpressions(source: string): Array<{ exprStart: number; exprEnd:
720
731
  return results;
721
732
  }
722
733
 
723
- function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
734
+ export function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
724
735
  const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
725
736
 
726
737
  // Match: for (const/let/var ...
@@ -812,7 +823,7 @@ function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: s
812
823
  * Handles: function declarations, arrow functions, method definitions.
813
824
  * Skips: React components (already tracked via __trickle_rc with props).
814
825
  */
815
- function findFunctionParams(source: string, isReactFile: boolean): Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> {
826
+ export function findFunctionParams(source: string, isReactFile: boolean): Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> {
816
827
  const results: Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> = [];
817
828
 
818
829
  // Match function declarations: function name(params) {