trickle-observe 0.2.62 → 0.2.63

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.
@@ -539,7 +539,8 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
539
539
  }
540
540
  }
541
541
  // Also match arrow functions assigned to const/let/var
542
- const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
542
+ // Handles: const X = () => {}, const X: React.FC = () => {}, const X: React.FC<Props> = ({ a }) => {}
543
+ const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
543
544
  while ((match = arrowRegex.exec(source)) !== null) {
544
545
  const name = match[1];
545
546
  const openBrace = source.indexOf('{', match.index + match[0].length - 1);
@@ -613,6 +614,89 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
613
614
  bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
614
615
  }
615
616
  }
617
+ // Match React.memo() and React.forwardRef() wrapped components
618
+ // Pattern: const Name = (React.)?memo( or const Name = (React.)?forwardRef<T,P>(
619
+ // Then scan forward to find the inner arrow => { body
620
+ if (isReactFile) {
621
+ const memoRefRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:React\.)?(?:memo|forwardRef)\s*(?:<[^(]*>)?\s*\(/gm;
622
+ let memoMatch;
623
+ while ((memoMatch = memoRefRegex.exec(source)) !== null) {
624
+ const name = memoMatch[1];
625
+ // Position after the opening `(` of memo/forwardRef call
626
+ const afterMemoOpen = memoMatch.index + memoMatch[0].length;
627
+ // Scan forward to find `=> {` — the arrow body of the inner function.
628
+ // We need to skip over the inner function's parameter list (which may contain nested parens).
629
+ // Strategy: find the next `=>` that is followed by optional whitespace and `{`.
630
+ let pos = afterMemoOpen;
631
+ let arrowPos = -1;
632
+ let parenDepth = 0;
633
+ while (pos < source.length - 1) {
634
+ const ch = source[pos];
635
+ if (ch === '(')
636
+ parenDepth++;
637
+ else if (ch === ')')
638
+ parenDepth--;
639
+ else if (ch === '=' && source[pos + 1] === '>' && parenDepth <= 0) {
640
+ arrowPos = pos;
641
+ break;
642
+ }
643
+ pos++;
644
+ }
645
+ if (arrowPos === -1)
646
+ continue;
647
+ // Skip `=>` and whitespace to find `{`
648
+ let bracePos = arrowPos + 2;
649
+ while (bracePos < source.length && /[\s]/.test(source[bracePos]))
650
+ bracePos++;
651
+ if (source[bracePos] !== '{')
652
+ continue;
653
+ const openBrace = bracePos;
654
+ const closeBrace = findClosingBrace(source, openBrace);
655
+ if (closeBrace === -1)
656
+ continue;
657
+ // Extract the param list: everything between memo( and arrowPos
658
+ const innerParamStr = source.slice(afterMemoOpen, arrowPos).trim();
659
+ // innerParamStr is like `({ item, onSelect })` or `(props, ref)` or `props`
660
+ let propsExpr = 'undefined';
661
+ if (innerParamStr.startsWith('(')) {
662
+ // Peel outer parens
663
+ const inner = innerParamStr.slice(1, innerParamStr.lastIndexOf(')')).trim();
664
+ if (inner.startsWith('{')) {
665
+ // Find the matching `}` of the destructuring pattern, ignoring any type annotation after it
666
+ let depth3 = 0, destructEnd = -1;
667
+ for (let i = 0; i < inner.length; i++) {
668
+ if (inner[i] === '{')
669
+ depth3++;
670
+ else if (inner[i] === '}') {
671
+ depth3--;
672
+ if (depth3 === 0) {
673
+ destructEnd = i;
674
+ break;
675
+ }
676
+ }
677
+ }
678
+ const destructPart = destructEnd !== -1 ? inner.slice(0, destructEnd + 1) : inner;
679
+ const fields = extractDestructuredNames(destructPart);
680
+ if (fields.length > 0)
681
+ propsExpr = `{ ${fields.join(', ')} }`;
682
+ }
683
+ else {
684
+ const firstParam = inner.split(',')[0].trim().split(':')[0].trim();
685
+ if (firstParam)
686
+ propsExpr = firstParam;
687
+ }
688
+ }
689
+ else if (innerParamStr && /^[a-zA-Z_$]/.test(innerParamStr)) {
690
+ propsExpr = innerParamStr.split(/[\s,:(]/)[0];
691
+ }
692
+ let lineNo = 1;
693
+ for (let i = 0; i < memoMatch.index; i++) {
694
+ if (source[i] === '\n')
695
+ lineNo++;
696
+ }
697
+ bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
698
+ }
699
+ }
616
700
  const hookInsertions = [];
617
701
  if (isReactFile) {
618
702
  // Match useEffect(, useMemo(, useCallback( — also handles React.useEffect(, etc.
@@ -532,7 +532,8 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
532
532
  }
533
533
  }
534
534
  // Also match arrow functions assigned to const/let/var
535
- const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
535
+ // Handles: const X = () => {}, const X: React.FC = () => {}, const X: React.FC<Props> = ({ a }) => {}
536
+ const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
536
537
  while ((match = arrowRegex.exec(source)) !== null) {
537
538
  const name = match[1];
538
539
  const openBrace = source.indexOf('{', match.index + match[0].length - 1);
@@ -606,6 +607,89 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
606
607
  bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
607
608
  }
608
609
  }
610
+ // Match React.memo() and React.forwardRef() wrapped components
611
+ // Pattern: const Name = (React.)?memo( or const Name = (React.)?forwardRef<T,P>(
612
+ // Then scan forward to find the inner arrow => { body
613
+ if (isReactFile) {
614
+ const memoRefRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:React\.)?(?:memo|forwardRef)\s*(?:<[^(]*>)?\s*\(/gm;
615
+ let memoMatch;
616
+ while ((memoMatch = memoRefRegex.exec(source)) !== null) {
617
+ const name = memoMatch[1];
618
+ // Position after the opening `(` of memo/forwardRef call
619
+ const afterMemoOpen = memoMatch.index + memoMatch[0].length;
620
+ // Scan forward to find `=> {` — the arrow body of the inner function.
621
+ // We need to skip over the inner function's parameter list (which may contain nested parens).
622
+ // Strategy: find the next `=>` that is followed by optional whitespace and `{`.
623
+ let pos = afterMemoOpen;
624
+ let arrowPos = -1;
625
+ let parenDepth = 0;
626
+ while (pos < source.length - 1) {
627
+ const ch = source[pos];
628
+ if (ch === '(')
629
+ parenDepth++;
630
+ else if (ch === ')')
631
+ parenDepth--;
632
+ else if (ch === '=' && source[pos + 1] === '>' && parenDepth <= 0) {
633
+ arrowPos = pos;
634
+ break;
635
+ }
636
+ pos++;
637
+ }
638
+ if (arrowPos === -1)
639
+ continue;
640
+ // Skip `=>` and whitespace to find `{`
641
+ let bracePos = arrowPos + 2;
642
+ while (bracePos < source.length && /[\s]/.test(source[bracePos]))
643
+ bracePos++;
644
+ if (source[bracePos] !== '{')
645
+ continue;
646
+ const openBrace = bracePos;
647
+ const closeBrace = findClosingBrace(source, openBrace);
648
+ if (closeBrace === -1)
649
+ continue;
650
+ // Extract the param list: everything between memo( and arrowPos
651
+ const innerParamStr = source.slice(afterMemoOpen, arrowPos).trim();
652
+ // innerParamStr is like `({ item, onSelect })` or `(props, ref)` or `props`
653
+ let propsExpr = 'undefined';
654
+ if (innerParamStr.startsWith('(')) {
655
+ // Peel outer parens
656
+ const inner = innerParamStr.slice(1, innerParamStr.lastIndexOf(')')).trim();
657
+ if (inner.startsWith('{')) {
658
+ // Find the matching `}` of the destructuring pattern, ignoring any type annotation after it
659
+ let depth3 = 0, destructEnd = -1;
660
+ for (let i = 0; i < inner.length; i++) {
661
+ if (inner[i] === '{')
662
+ depth3++;
663
+ else if (inner[i] === '}') {
664
+ depth3--;
665
+ if (depth3 === 0) {
666
+ destructEnd = i;
667
+ break;
668
+ }
669
+ }
670
+ }
671
+ const destructPart = destructEnd !== -1 ? inner.slice(0, destructEnd + 1) : inner;
672
+ const fields = extractDestructuredNames(destructPart);
673
+ if (fields.length > 0)
674
+ propsExpr = `{ ${fields.join(', ')} }`;
675
+ }
676
+ else {
677
+ const firstParam = inner.split(',')[0].trim().split(':')[0].trim();
678
+ if (firstParam)
679
+ propsExpr = firstParam;
680
+ }
681
+ }
682
+ else if (innerParamStr && /^[a-zA-Z_$]/.test(innerParamStr)) {
683
+ propsExpr = innerParamStr.split(/[\s,:(]/)[0];
684
+ }
685
+ let lineNo = 1;
686
+ for (let i = 0; i < memoMatch.index; i++) {
687
+ if (source[i] === '\n')
688
+ lineNo++;
689
+ }
690
+ bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
691
+ }
692
+ }
609
693
  const hookInsertions = [];
610
694
  if (isReactFile) {
611
695
  // Match useEffect(, useMemo(, useCallback( — also handles React.useEffect(, etc.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.62",
3
+ "version": "0.2.63",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,6 +47,41 @@ describe('React file detection', () => {
47
47
  assert.ok(out!.includes('__trickle_rc'), 'export default function should be tracked as component');
48
48
  });
49
49
 
50
+ it('tracks React.FC typed arrow components', () => {
51
+ const code = `const UserCard: React.FC = () => { return null; }`;
52
+ const out = transformTsx(code);
53
+ assert.ok(out, 'should transform');
54
+ assert.ok(out!.includes('__trickle_rc'), 'React.FC arrow should be tracked');
55
+ });
56
+
57
+ it('tracks React.FC<Props> typed arrow components', () => {
58
+ const code = `const UserCard: React.FC<Props> = ({ name }) => { return null; }`;
59
+ const out = transformTsx(code);
60
+ assert.ok(out, 'should transform');
61
+ assert.ok(out!.includes('__trickle_rc'), 'React.FC<Props> arrow should be tracked');
62
+ });
63
+
64
+ it('tracks React.memo() wrapped components', () => {
65
+ const code = `const UserCard = React.memo(() => { return null; });`;
66
+ const out = transformTsx(code);
67
+ assert.ok(out, 'should transform');
68
+ assert.ok(out!.includes('__trickle_rc'), 'React.memo component should be tracked');
69
+ });
70
+
71
+ it('tracks memo() wrapped components with destructured props', () => {
72
+ const code = `const UserCard = memo(({ name, age }) => { return null; });`;
73
+ const out = transformTsx(code);
74
+ assert.ok(out, 'should transform');
75
+ assert.ok(out!.includes('__trickle_rc'), 'memo() component should be tracked');
76
+ });
77
+
78
+ it('tracks React.forwardRef() wrapped components', () => {
79
+ const code = `const UserCard = React.forwardRef<View, Props>((props, ref) => { return null; });`;
80
+ const out = transformTsx(code);
81
+ assert.ok(out, 'should transform');
82
+ assert.ok(out!.includes('__trickle_rc'), 'forwardRef component should be tracked');
83
+ });
84
+
50
85
  it('does not track lowercase functions as components', () => {
51
86
  const code = `function helper(x) { return x + 1; }`;
52
87
  const out = transformTsx(code);
@@ -537,7 +537,8 @@ export function transformEsmSource(
537
537
  }
538
538
 
539
539
  // Also match arrow functions assigned to const/let/var
540
- const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
540
+ // Handles: const X = () => {}, const X: React.FC = () => {}, const X: React.FC<Props> = ({ a }) => {}
541
+ const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
541
542
 
542
543
  while ((match = arrowRegex.exec(source)) !== null) {
543
544
  const name = match[1];
@@ -607,6 +608,78 @@ export function transformEsmSource(
607
608
 
608
609
 
609
610
 
611
+ // Match React.memo() and React.forwardRef() wrapped components
612
+ // Pattern: const Name = (React.)?memo( or const Name = (React.)?forwardRef<T,P>(
613
+ // Then scan forward to find the inner arrow => { body
614
+ if (isReactFile) {
615
+ const memoRefRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:React\.)?(?:memo|forwardRef)\s*(?:<[^(]*>)?\s*\(/gm;
616
+ let memoMatch;
617
+ while ((memoMatch = memoRefRegex.exec(source)) !== null) {
618
+ const name = memoMatch[1];
619
+ // Position after the opening `(` of memo/forwardRef call
620
+ const afterMemoOpen = memoMatch.index + memoMatch[0].length;
621
+
622
+ // Scan forward to find `=> {` — the arrow body of the inner function.
623
+ // We need to skip over the inner function's parameter list (which may contain nested parens).
624
+ // Strategy: find the next `=>` that is followed by optional whitespace and `{`.
625
+ let pos = afterMemoOpen;
626
+ let arrowPos = -1;
627
+ let parenDepth = 0;
628
+ while (pos < source.length - 1) {
629
+ const ch = source[pos];
630
+ if (ch === '(') parenDepth++;
631
+ else if (ch === ')') parenDepth--;
632
+ else if (ch === '=' && source[pos + 1] === '>' && parenDepth <= 0) {
633
+ arrowPos = pos;
634
+ break;
635
+ }
636
+ pos++;
637
+ }
638
+ if (arrowPos === -1) continue;
639
+
640
+ // Skip `=>` and whitespace to find `{`
641
+ let bracePos = arrowPos + 2;
642
+ while (bracePos < source.length && /[\s]/.test(source[bracePos])) bracePos++;
643
+ if (source[bracePos] !== '{') continue;
644
+ const openBrace = bracePos;
645
+
646
+ const closeBrace = findClosingBrace(source, openBrace);
647
+ if (closeBrace === -1) continue;
648
+
649
+ // Extract the param list: everything between memo( and arrowPos
650
+ const innerParamStr = source.slice(afterMemoOpen, arrowPos).trim();
651
+ // innerParamStr is like `({ item, onSelect })` or `(props, ref)` or `props`
652
+ let propsExpr = 'undefined';
653
+ if (innerParamStr.startsWith('(')) {
654
+ // Peel outer parens
655
+ const inner = innerParamStr.slice(1, innerParamStr.lastIndexOf(')')).trim();
656
+ if (inner.startsWith('{')) {
657
+ // Find the matching `}` of the destructuring pattern, ignoring any type annotation after it
658
+ let depth3 = 0, destructEnd = -1;
659
+ for (let i = 0; i < inner.length; i++) {
660
+ if (inner[i] === '{') depth3++;
661
+ else if (inner[i] === '}') { depth3--; if (depth3 === 0) { destructEnd = i; break; } }
662
+ }
663
+ const destructPart = destructEnd !== -1 ? inner.slice(0, destructEnd + 1) : inner;
664
+ const fields = extractDestructuredNames(destructPart);
665
+ if (fields.length > 0) propsExpr = `{ ${fields.join(', ')} }`;
666
+ } else {
667
+ const firstParam = inner.split(',')[0].trim().split(':')[0].trim();
668
+ if (firstParam) propsExpr = firstParam;
669
+ }
670
+ } else if (innerParamStr && /^[a-zA-Z_$]/.test(innerParamStr)) {
671
+ propsExpr = innerParamStr.split(/[\s,:(]/)[0];
672
+ }
673
+
674
+ let lineNo = 1;
675
+ for (let i = 0; i < memoMatch.index; i++) {
676
+ if (source[i] === '\n') lineNo++;
677
+ }
678
+
679
+ bodyInsertions.push({ position: openBrace + 1, name, lineNo, propsExpr });
680
+ }
681
+ }
682
+
610
683
  // React hook tracking — wrap the callback arg of useEffect/useMemo/useCallback
611
684
  // to count how many times each hook fires (effect ran, memo recomputed, callback invoked).
612
685
  // Each hook produces TWO insertions: wrapStart (before callback) and wrapEnd (after callback `}`).