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.
- package/dist/vite-plugin.js +85 -1
- package/dist-esm/vite-plugin.js +85 -1
- package/package.json +1 -1
- package/src/vite-plugin.test.ts +35 -0
- package/src/vite-plugin.ts +74 -1
package/dist/vite-plugin.js
CHANGED
|
@@ -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
|
|
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.
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -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
|
|
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
package/src/vite-plugin.test.ts
CHANGED
|
@@ -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);
|
package/src/vite-plugin.ts
CHANGED
|
@@ -537,7 +537,8 @@ export function transformEsmSource(
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
// Also match arrow functions assigned to const/let/var
|
|
540
|
-
const
|
|
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 `}`).
|