trickle-observe 0.2.68 → 0.2.70
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/metro-transformer.js +2 -1
- package/dist/next-loader.js +2 -1
- package/dist/vite-plugin.d.ts +5 -2
- package/dist/vite-plugin.js +333 -13
- package/dist-esm/vite-plugin.js +333 -13
- package/package.json +1 -1
- package/src/metro-transformer.ts +2 -1
- package/src/next-loader.ts +2 -1
- package/src/vite-plugin.ts +370 -28
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -60,9 +60,33 @@ export function tricklePlugin(options = {}) {
|
|
|
60
60
|
return {
|
|
61
61
|
name: 'trickle-observe',
|
|
62
62
|
enforce: 'post',
|
|
63
|
-
|
|
63
|
+
configureServer(server) {
|
|
64
|
+
// Listen for variable data from browser clients via Vite's HMR WebSocket
|
|
65
|
+
const hot = server.hot || server.ws;
|
|
66
|
+
if (hot && hot.on) {
|
|
67
|
+
const varDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(varDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
const varsFile = path.join(varDir, 'variables.jsonl');
|
|
73
|
+
hot.on('trickle:vars', (data, client) => {
|
|
74
|
+
try {
|
|
75
|
+
if (data && data.lines) {
|
|
76
|
+
fs.appendFileSync(varsFile, data.lines);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
});
|
|
81
|
+
if (debug) {
|
|
82
|
+
console.log(`[trickle/vite] WebSocket bridge active → ${varsFile}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
transform(code, id, options) {
|
|
64
87
|
if (!shouldTransform(id))
|
|
65
88
|
return null;
|
|
89
|
+
const isSSR = options?.ssr === true;
|
|
66
90
|
// Read the original source file to get accurate line numbers.
|
|
67
91
|
// Vite transforms the code before our plugin (enforce: 'post'),
|
|
68
92
|
// so line numbers from `code` don't match the original .ts file.
|
|
@@ -74,11 +98,11 @@ export function tricklePlugin(options = {}) {
|
|
|
74
98
|
// If we can't read the original, we'll use transformed line numbers
|
|
75
99
|
}
|
|
76
100
|
const moduleName = path.basename(id).replace(/\.[jt]sx?$/, '');
|
|
77
|
-
const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug, traceVars, originalSource);
|
|
101
|
+
const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug, traceVars, originalSource, isSSR);
|
|
78
102
|
if (transformed === code)
|
|
79
103
|
return null;
|
|
80
104
|
if (debug) {
|
|
81
|
-
console.log(`[trickle/vite] Transformed ${moduleName} (${id})`);
|
|
105
|
+
console.log(`[trickle/vite] Transformed ${moduleName} (${id}) [${isSSR ? 'SSR' : 'browser'}]`);
|
|
82
106
|
}
|
|
83
107
|
return { code: transformed, map: null };
|
|
84
108
|
},
|
|
@@ -449,6 +473,230 @@ function extractDestructuredNames(pattern) {
|
|
|
449
473
|
}
|
|
450
474
|
return names;
|
|
451
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
478
|
+
* Handles:
|
|
479
|
+
* for (const item of items) { ... } → trace item
|
|
480
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
481
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
482
|
+
* for (const key in obj) { ... } → trace key
|
|
483
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
484
|
+
* Inserts trace calls at the start of the loop body.
|
|
485
|
+
*/
|
|
486
|
+
function findForLoopVars(source) {
|
|
487
|
+
const results = [];
|
|
488
|
+
// Match: for (const/let/var ...
|
|
489
|
+
const forRegex = /\bfor\s*\(/g;
|
|
490
|
+
let match;
|
|
491
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
492
|
+
const afterParen = match.index + match[0].length;
|
|
493
|
+
// Skip whitespace
|
|
494
|
+
let pos = afterParen;
|
|
495
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
496
|
+
pos++;
|
|
497
|
+
// Expect const/let/var
|
|
498
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
499
|
+
if (!declMatch)
|
|
500
|
+
continue;
|
|
501
|
+
pos += declMatch[0].length;
|
|
502
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
503
|
+
const varNames = [];
|
|
504
|
+
const patternStart = pos;
|
|
505
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
506
|
+
// Destructured: find matching brace/bracket
|
|
507
|
+
const open = source[pos];
|
|
508
|
+
const close = open === '{' ? '}' : ']';
|
|
509
|
+
let depth = 1;
|
|
510
|
+
let end = pos + 1;
|
|
511
|
+
while (end < source.length && depth > 0) {
|
|
512
|
+
if (source[end] === open)
|
|
513
|
+
depth++;
|
|
514
|
+
else if (source[end] === close)
|
|
515
|
+
depth--;
|
|
516
|
+
end++;
|
|
517
|
+
}
|
|
518
|
+
const pattern = source.slice(pos, end);
|
|
519
|
+
const names = extractDestructuredNames(pattern);
|
|
520
|
+
varNames.push(...names);
|
|
521
|
+
pos = end;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Simple identifier
|
|
525
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
526
|
+
if (!idMatch)
|
|
527
|
+
continue;
|
|
528
|
+
varNames.push(idMatch[1]);
|
|
529
|
+
pos += idMatch[0].length;
|
|
530
|
+
}
|
|
531
|
+
if (varNames.length === 0)
|
|
532
|
+
continue;
|
|
533
|
+
// Skip trickle internals
|
|
534
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c'))
|
|
535
|
+
continue;
|
|
536
|
+
// Now find the opening `{` of the loop body
|
|
537
|
+
// Skip everything until the `)` that closes the for(...)
|
|
538
|
+
let parenDepth = 1; // We're inside the for(
|
|
539
|
+
while (pos < source.length && parenDepth > 0) {
|
|
540
|
+
const ch = source[pos];
|
|
541
|
+
if (ch === '(')
|
|
542
|
+
parenDepth++;
|
|
543
|
+
else if (ch === ')')
|
|
544
|
+
parenDepth--;
|
|
545
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
546
|
+
const q = ch;
|
|
547
|
+
pos++;
|
|
548
|
+
while (pos < source.length && source[pos] !== q) {
|
|
549
|
+
if (source[pos] === '\\')
|
|
550
|
+
pos++;
|
|
551
|
+
pos++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
pos++;
|
|
555
|
+
}
|
|
556
|
+
// Now find the `{` after the closing `)`
|
|
557
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
558
|
+
pos++;
|
|
559
|
+
if (pos >= source.length || source[pos] !== '{')
|
|
560
|
+
continue;
|
|
561
|
+
const bodyBrace = pos;
|
|
562
|
+
// Calculate line number
|
|
563
|
+
let lineNo = 1;
|
|
564
|
+
for (let i = 0; i < match.index; i++) {
|
|
565
|
+
if (source[i] === '\n')
|
|
566
|
+
lineNo++;
|
|
567
|
+
}
|
|
568
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
569
|
+
}
|
|
570
|
+
return results;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
574
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
575
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
576
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
577
|
+
*/
|
|
578
|
+
function findFunctionParams(source, isReactFile) {
|
|
579
|
+
const results = [];
|
|
580
|
+
// Match function declarations: function name(params) {
|
|
581
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
582
|
+
let match;
|
|
583
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
584
|
+
const name = match[1];
|
|
585
|
+
if (name === 'require' || name === 'exports' || name === 'module')
|
|
586
|
+
continue;
|
|
587
|
+
if (name.startsWith('__trickle'))
|
|
588
|
+
continue;
|
|
589
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
590
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
591
|
+
continue;
|
|
592
|
+
const afterParen = match.index + match[0].length;
|
|
593
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
594
|
+
if (bodyBrace === -1)
|
|
595
|
+
continue;
|
|
596
|
+
// Extract parameter names from between ( and )
|
|
597
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
598
|
+
const closeParen = paramStr.indexOf(')');
|
|
599
|
+
if (closeParen === -1)
|
|
600
|
+
continue;
|
|
601
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
602
|
+
const paramNames = extractParamNames(rawParams);
|
|
603
|
+
if (paramNames.length === 0)
|
|
604
|
+
continue;
|
|
605
|
+
let lineNo = 1;
|
|
606
|
+
for (let i = 0; i < match.index; i++) {
|
|
607
|
+
if (source[i] === '\n')
|
|
608
|
+
lineNo++;
|
|
609
|
+
}
|
|
610
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
611
|
+
}
|
|
612
|
+
// Match arrow functions: const name = (params) => {
|
|
613
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
614
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
615
|
+
const name = match[1];
|
|
616
|
+
if (name.startsWith('__trickle'))
|
|
617
|
+
continue;
|
|
618
|
+
// Skip React components in React files
|
|
619
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
620
|
+
continue;
|
|
621
|
+
const rawParams = match[2].trim();
|
|
622
|
+
const paramNames = extractParamNames(rawParams);
|
|
623
|
+
if (paramNames.length === 0)
|
|
624
|
+
continue;
|
|
625
|
+
// Find the { position
|
|
626
|
+
const bracePos = match.index + match[0].length - 1;
|
|
627
|
+
let lineNo = 1;
|
|
628
|
+
for (let i = 0; i < match.index; i++) {
|
|
629
|
+
if (source[i] === '\n')
|
|
630
|
+
lineNo++;
|
|
631
|
+
}
|
|
632
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
633
|
+
}
|
|
634
|
+
return results;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Extract parameter names from a function parameter string.
|
|
638
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
639
|
+
* Skips type annotations.
|
|
640
|
+
*/
|
|
641
|
+
function extractParamNames(rawParams) {
|
|
642
|
+
if (!rawParams)
|
|
643
|
+
return [];
|
|
644
|
+
const names = [];
|
|
645
|
+
// Split by commas at depth 0
|
|
646
|
+
const parts = [];
|
|
647
|
+
let depth = 0;
|
|
648
|
+
let current = '';
|
|
649
|
+
for (const ch of rawParams) {
|
|
650
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<')
|
|
651
|
+
depth++;
|
|
652
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>')
|
|
653
|
+
depth--;
|
|
654
|
+
else if (ch === ',' && depth === 0) {
|
|
655
|
+
parts.push(current.trim());
|
|
656
|
+
current = '';
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
current += ch;
|
|
660
|
+
}
|
|
661
|
+
if (current.trim())
|
|
662
|
+
parts.push(current.trim());
|
|
663
|
+
for (const part of parts) {
|
|
664
|
+
const trimmed = part.trim();
|
|
665
|
+
if (!trimmed)
|
|
666
|
+
continue;
|
|
667
|
+
// Destructured params — extract individual names
|
|
668
|
+
if (trimmed.startsWith('{')) {
|
|
669
|
+
const closeBrace = trimmed.indexOf('}');
|
|
670
|
+
if (closeBrace !== -1) {
|
|
671
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
672
|
+
names.push(...destructNames);
|
|
673
|
+
}
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (trimmed.startsWith('[')) {
|
|
677
|
+
const closeBracket = trimmed.indexOf(']');
|
|
678
|
+
if (closeBracket !== -1) {
|
|
679
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
680
|
+
names.push(...destructNames);
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
// Rest parameter: ...args
|
|
685
|
+
if (trimmed.startsWith('...')) {
|
|
686
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
687
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
688
|
+
names.push(restName);
|
|
689
|
+
}
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// Simple param: name or name: Type or name = default
|
|
693
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
694
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
695
|
+
names.push(paramName);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return names;
|
|
699
|
+
}
|
|
452
700
|
/**
|
|
453
701
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
454
702
|
*
|
|
@@ -513,7 +761,7 @@ function findOriginalLineDestructured(origLines, varNames, transformedLine) {
|
|
|
513
761
|
function escapeRegexStr(str) {
|
|
514
762
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
515
763
|
}
|
|
516
|
-
export function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource) {
|
|
764
|
+
export function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR) {
|
|
517
765
|
// Detect React files for component render tracking
|
|
518
766
|
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
519
767
|
// Match top-level and nested function declarations (including async, export, export default)
|
|
@@ -856,7 +1104,11 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
856
1104
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
857
1105
|
// Find destructured variable declarations for tracing
|
|
858
1106
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
859
|
-
|
|
1107
|
+
// Find for-loop variable declarations for tracing
|
|
1108
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1109
|
+
// Find function parameter names for tracing
|
|
1110
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1111
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
860
1112
|
return source;
|
|
861
1113
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
862
1114
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -876,12 +1128,55 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
876
1128
|
di.lineNo = origLine;
|
|
877
1129
|
}
|
|
878
1130
|
}
|
|
1131
|
+
// Fix for-loop var line numbers
|
|
1132
|
+
for (const fi of forLoopInsertions) {
|
|
1133
|
+
if (fi.varNames.length > 0) {
|
|
1134
|
+
// Search for 'for' keyword near the expected line
|
|
1135
|
+
const pattern = /\bfor\s*\(/;
|
|
1136
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1137
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1138
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1139
|
+
fi.lineNo = fwd + 1;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
if (delta > 0 && delta <= 10) {
|
|
1143
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1144
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1145
|
+
fi.lineNo = bwd + 1;
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// Fix function param line numbers
|
|
1153
|
+
for (const fp of funcParamInsertions) {
|
|
1154
|
+
if (fp.paramNames.length > 0) {
|
|
1155
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1156
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1157
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1158
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1159
|
+
fp.lineNo = fwd + 1;
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
if (delta > 0 && delta <= 10) {
|
|
1163
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1164
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1165
|
+
fp.lineNo = bwd + 1;
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
879
1172
|
}
|
|
880
1173
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
1174
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
881
1175
|
const importLines = [
|
|
882
1176
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
883
1177
|
];
|
|
884
|
-
if (
|
|
1178
|
+
if (needsTracing && isSSR) {
|
|
1179
|
+
// SSR/Node.js — use file system for writing
|
|
885
1180
|
importLines.push(`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`, `import { join as __trickle_join } from 'node:path';`);
|
|
886
1181
|
}
|
|
887
1182
|
const prefixLines = [
|
|
@@ -902,23 +1197,32 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
902
1197
|
` return __trickle_wrapFn(fn, opts);`,
|
|
903
1198
|
`}`,
|
|
904
1199
|
];
|
|
1200
|
+
// Add unified __trickle_send() transport — browser uses HMR WebSocket, SSR uses fs
|
|
1201
|
+
if (needsTracing) {
|
|
1202
|
+
if (isSSR) {
|
|
1203
|
+
// SSR/Node.js mode — write directly to file system
|
|
1204
|
+
prefixLines.push(`let __trickle_varsFile = null;`, `function __trickle_send(line) {`, ` try {`, ` if (!__trickle_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` __trickle_varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` __trickle_appendFileSync(__trickle_varsFile, line + '\\n');`, ` } catch(e) {}`, `}`);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
// Browser mode — buffer and send via Vite HMR WebSocket
|
|
1208
|
+
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(() => { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
905
1211
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
909
|
-
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Set();`, ` let _varsFile = null;`, ` 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 {`, ` if (!_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` _varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` 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_appendFileSync(_varsFile, JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }) + '\\n');`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1212
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1213
|
+
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) {} }`);
|
|
910
1214
|
}
|
|
911
1215
|
// Add React component render tracker if needed
|
|
912
1216
|
if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
|
|
913
|
-
prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `if (!globalThis.__trickle_react_prev_props) { globalThis.__trickle_react_prev_props = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const
|
|
1217
|
+
prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `if (!globalThis.__trickle_react_prev_props) { globalThis.__trickle_react_prev_props = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const rec = { kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 };`, ` if (props !== undefined && props !== null && typeof props === 'object') {`, ` try {`, ` const propKeys = Object.keys(props).filter(k => k !== 'children');`, ` const propSample = {};`, ` for (const k of propKeys.slice(0, 10)) {`, ` const v = props[k];`, ` const t = typeof v;`, ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`, ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`, ` else if (v === null || v === undefined) propSample[k] = v;`, ` else if (Array.isArray(v)) propSample[k] = '[arr:' + v.length + ']';`, ` else if (t === 'function') propSample[k] = '[fn]';`, ` else propSample[k] = '[object]';`, ` }`, ` rec.props = propSample;`, ` rec.propKeys = propKeys;`, ` const prevProps = globalThis.__trickle_react_prev_props.get(key);`, ` if (prevProps && count > 1) {`, ` const changedProps = [];`, ` const allKeys = new Set([...Object.keys(prevProps), ...Object.keys(propSample)]);`, ` for (const k of allKeys) {`, ` const prev = prevProps[k];`, ` const curr = propSample[k];`, ` if (String(prev) !== String(curr)) {`, ` changedProps.push({ key: k, from: prev, to: curr });`, ` }`, ` }`, ` if (changedProps.length > 0) rec.changedProps = changedProps;`, ` }`, ` globalThis.__trickle_react_prev_props.set(key, propSample);`, ` } catch(e2) {}`, ` }`, ` __trickle_send(JSON.stringify(rec));`, ` } catch(e) {}`, `}`);
|
|
914
1218
|
}
|
|
915
1219
|
// Add React hook tracker if needed
|
|
916
1220
|
if (hookInsertions.length > 0) {
|
|
917
|
-
prefixLines.push(`if (!globalThis.__trickle_hook_counts) { globalThis.__trickle_hook_counts = new Map(); }`, `function __trickle_hw(hookName, line, cb) {`, ` return function(...args) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + hookName;`, ` const n = (globalThis.__trickle_hook_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_hook_counts.set(key, n);`, `
|
|
1221
|
+
prefixLines.push(`if (!globalThis.__trickle_hook_counts) { globalThis.__trickle_hook_counts = new Map(); }`, `function __trickle_hw(hookName, line, cb) {`, ` return function(...args) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + hookName;`, ` const n = (globalThis.__trickle_hook_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_hook_counts.set(key, n);`, ` __trickle_send(JSON.stringify({ kind: 'react_hook', hookName, file: ${JSON.stringify(filename)}, line, invokeCount: n, timestamp: Date.now() / 1000 }));`, ` } catch(e) {}`, ` return cb(...args);`, ` };`, `}`);
|
|
918
1222
|
}
|
|
919
1223
|
// Add useState setter tracker if needed
|
|
920
1224
|
if (stateInsertions.length > 0) {
|
|
921
|
-
prefixLines.push(`if (!globalThis.__trickle_state_counts) { globalThis.__trickle_state_counts = new Map(); }`, `function __trickle_ss(stateName, line, origSetter) {`, ` return function(newVal) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + stateName;`, ` const n = (globalThis.__trickle_state_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_state_counts.set(key, n);`, ` const t = typeof newVal;`, ` let sample;`, ` if (t === 'function') sample = '[fn updater]';`, ` else if (t === 'string') sample = newVal.length > 40 ? newVal.slice(0,40)+'...' : newVal;`, ` else if (t === 'number' || t === 'boolean') sample = newVal;`, ` else if (newVal === null || newVal === undefined) sample = newVal;`, ` else if (Array.isArray(newVal)) sample = '[arr:'+newVal.length+']';`, ` else sample = '[object]';`, `
|
|
1225
|
+
prefixLines.push(`if (!globalThis.__trickle_state_counts) { globalThis.__trickle_state_counts = new Map(); }`, `function __trickle_ss(stateName, line, origSetter) {`, ` return function(newVal) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line + ':' + stateName;`, ` const n = (globalThis.__trickle_state_counts.get(key) || 0) + 1;`, ` globalThis.__trickle_state_counts.set(key, n);`, ` const t = typeof newVal;`, ` let sample;`, ` if (t === 'function') sample = '[fn updater]';`, ` else if (t === 'string') sample = newVal.length > 40 ? newVal.slice(0,40)+'...' : newVal;`, ` else if (t === 'number' || t === 'boolean') sample = newVal;`, ` else if (newVal === null || newVal === undefined) sample = newVal;`, ` else if (Array.isArray(newVal)) sample = '[arr:'+newVal.length+']';`, ` else sample = '[object]';`, ` __trickle_send(JSON.stringify({ kind: 'react_state', file: ${JSON.stringify(filename)}, line, stateName, updateCount: n, value: sample, timestamp: Date.now()/1000 }));`, ` } catch(e) {}`, ` return origSetter(newVal);`, ` };`, `}`);
|
|
922
1226
|
}
|
|
923
1227
|
prefixLines.push('');
|
|
924
1228
|
const prefix = prefixLines.join('\n');
|
|
@@ -943,6 +1247,22 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
943
1247
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
944
1248
|
});
|
|
945
1249
|
}
|
|
1250
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1251
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1252
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1253
|
+
allInsertions.push({
|
|
1254
|
+
position: bodyStart,
|
|
1255
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1259
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1260
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1261
|
+
allInsertions.push({
|
|
1262
|
+
position: bodyStart,
|
|
1263
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
946
1266
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
947
1267
|
allInsertions.push({
|
|
948
1268
|
position,
|
package/package.json
CHANGED
package/src/metro-transformer.ts
CHANGED
|
@@ -62,7 +62,8 @@ export async function transform({ src, filename, options }: MetroTransformArgs)
|
|
|
62
62
|
|
|
63
63
|
if ((isReactFile || isJsFile) && !filename.includes('node_modules') && !filename.includes('trickle-observe')) {
|
|
64
64
|
const moduleName = path.basename(filename).replace(/\.[jt]sx?$/, '');
|
|
65
|
-
|
|
65
|
+
// React Native has a JS runtime with fs access, use SSR mode
|
|
66
|
+
const transformed = transformEsmSource(src, filename, moduleName, backendUrl, debug, false, null, true);
|
|
66
67
|
if (transformed !== src) {
|
|
67
68
|
if (debug) {
|
|
68
69
|
console.log(`[trickle/metro] Instrumented ${filename}`);
|
package/src/next-loader.ts
CHANGED
|
@@ -43,7 +43,8 @@ export default function trickleNextLoader(this: { resourcePath: string; getOptio
|
|
|
43
43
|
const moduleName = path.basename(resourcePath).replace(/\.[jt]sx?$/, '');
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
// Next.js SSR renders all components on the server, so use SSR mode (node:fs)
|
|
47
|
+
const transformed = transformEsmSource(source, resourcePath, moduleName, backendUrl, debug, traceVars, source, true);
|
|
47
48
|
if (debug && transformed !== source) {
|
|
48
49
|
console.log(`[trickle/next] Instrumented ${resourcePath}`);
|
|
49
50
|
}
|