trickle-observe 0.2.69 → 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/vite-plugin.js +288 -3
- package/dist-esm/vite-plugin.js +288 -3
- package/package.json +1 -1
- package/src/vite-plugin.ts +298 -3
package/dist/vite-plugin.js
CHANGED
|
@@ -480,6 +480,230 @@ function extractDestructuredNames(pattern) {
|
|
|
480
480
|
}
|
|
481
481
|
return names;
|
|
482
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
485
|
+
* Handles:
|
|
486
|
+
* for (const item of items) { ... } → trace item
|
|
487
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
488
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
489
|
+
* for (const key in obj) { ... } → trace key
|
|
490
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
491
|
+
* Inserts trace calls at the start of the loop body.
|
|
492
|
+
*/
|
|
493
|
+
function findForLoopVars(source) {
|
|
494
|
+
const results = [];
|
|
495
|
+
// Match: for (const/let/var ...
|
|
496
|
+
const forRegex = /\bfor\s*\(/g;
|
|
497
|
+
let match;
|
|
498
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
499
|
+
const afterParen = match.index + match[0].length;
|
|
500
|
+
// Skip whitespace
|
|
501
|
+
let pos = afterParen;
|
|
502
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
503
|
+
pos++;
|
|
504
|
+
// Expect const/let/var
|
|
505
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
506
|
+
if (!declMatch)
|
|
507
|
+
continue;
|
|
508
|
+
pos += declMatch[0].length;
|
|
509
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
510
|
+
const varNames = [];
|
|
511
|
+
const patternStart = pos;
|
|
512
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
513
|
+
// Destructured: find matching brace/bracket
|
|
514
|
+
const open = source[pos];
|
|
515
|
+
const close = open === '{' ? '}' : ']';
|
|
516
|
+
let depth = 1;
|
|
517
|
+
let end = pos + 1;
|
|
518
|
+
while (end < source.length && depth > 0) {
|
|
519
|
+
if (source[end] === open)
|
|
520
|
+
depth++;
|
|
521
|
+
else if (source[end] === close)
|
|
522
|
+
depth--;
|
|
523
|
+
end++;
|
|
524
|
+
}
|
|
525
|
+
const pattern = source.slice(pos, end);
|
|
526
|
+
const names = extractDestructuredNames(pattern);
|
|
527
|
+
varNames.push(...names);
|
|
528
|
+
pos = end;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Simple identifier
|
|
532
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
533
|
+
if (!idMatch)
|
|
534
|
+
continue;
|
|
535
|
+
varNames.push(idMatch[1]);
|
|
536
|
+
pos += idMatch[0].length;
|
|
537
|
+
}
|
|
538
|
+
if (varNames.length === 0)
|
|
539
|
+
continue;
|
|
540
|
+
// Skip trickle internals
|
|
541
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c'))
|
|
542
|
+
continue;
|
|
543
|
+
// Now find the opening `{` of the loop body
|
|
544
|
+
// Skip everything until the `)` that closes the for(...)
|
|
545
|
+
let parenDepth = 1; // We're inside the for(
|
|
546
|
+
while (pos < source.length && parenDepth > 0) {
|
|
547
|
+
const ch = source[pos];
|
|
548
|
+
if (ch === '(')
|
|
549
|
+
parenDepth++;
|
|
550
|
+
else if (ch === ')')
|
|
551
|
+
parenDepth--;
|
|
552
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
553
|
+
const q = ch;
|
|
554
|
+
pos++;
|
|
555
|
+
while (pos < source.length && source[pos] !== q) {
|
|
556
|
+
if (source[pos] === '\\')
|
|
557
|
+
pos++;
|
|
558
|
+
pos++;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
pos++;
|
|
562
|
+
}
|
|
563
|
+
// Now find the `{` after the closing `)`
|
|
564
|
+
while (pos < source.length && /\s/.test(source[pos]))
|
|
565
|
+
pos++;
|
|
566
|
+
if (pos >= source.length || source[pos] !== '{')
|
|
567
|
+
continue;
|
|
568
|
+
const bodyBrace = pos;
|
|
569
|
+
// Calculate line number
|
|
570
|
+
let lineNo = 1;
|
|
571
|
+
for (let i = 0; i < match.index; i++) {
|
|
572
|
+
if (source[i] === '\n')
|
|
573
|
+
lineNo++;
|
|
574
|
+
}
|
|
575
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
576
|
+
}
|
|
577
|
+
return results;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
581
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
582
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
583
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
584
|
+
*/
|
|
585
|
+
function findFunctionParams(source, isReactFile) {
|
|
586
|
+
const results = [];
|
|
587
|
+
// Match function declarations: function name(params) {
|
|
588
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
589
|
+
let match;
|
|
590
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
591
|
+
const name = match[1];
|
|
592
|
+
if (name === 'require' || name === 'exports' || name === 'module')
|
|
593
|
+
continue;
|
|
594
|
+
if (name.startsWith('__trickle'))
|
|
595
|
+
continue;
|
|
596
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
597
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
598
|
+
continue;
|
|
599
|
+
const afterParen = match.index + match[0].length;
|
|
600
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
601
|
+
if (bodyBrace === -1)
|
|
602
|
+
continue;
|
|
603
|
+
// Extract parameter names from between ( and )
|
|
604
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
605
|
+
const closeParen = paramStr.indexOf(')');
|
|
606
|
+
if (closeParen === -1)
|
|
607
|
+
continue;
|
|
608
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
609
|
+
const paramNames = extractParamNames(rawParams);
|
|
610
|
+
if (paramNames.length === 0)
|
|
611
|
+
continue;
|
|
612
|
+
let lineNo = 1;
|
|
613
|
+
for (let i = 0; i < match.index; i++) {
|
|
614
|
+
if (source[i] === '\n')
|
|
615
|
+
lineNo++;
|
|
616
|
+
}
|
|
617
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
618
|
+
}
|
|
619
|
+
// Match arrow functions: const name = (params) => {
|
|
620
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
621
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
622
|
+
const name = match[1];
|
|
623
|
+
if (name.startsWith('__trickle'))
|
|
624
|
+
continue;
|
|
625
|
+
// Skip React components in React files
|
|
626
|
+
if (isReactFile && /^[A-Z]/.test(name))
|
|
627
|
+
continue;
|
|
628
|
+
const rawParams = match[2].trim();
|
|
629
|
+
const paramNames = extractParamNames(rawParams);
|
|
630
|
+
if (paramNames.length === 0)
|
|
631
|
+
continue;
|
|
632
|
+
// Find the { position
|
|
633
|
+
const bracePos = match.index + match[0].length - 1;
|
|
634
|
+
let lineNo = 1;
|
|
635
|
+
for (let i = 0; i < match.index; i++) {
|
|
636
|
+
if (source[i] === '\n')
|
|
637
|
+
lineNo++;
|
|
638
|
+
}
|
|
639
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
640
|
+
}
|
|
641
|
+
return results;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Extract parameter names from a function parameter string.
|
|
645
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
646
|
+
* Skips type annotations.
|
|
647
|
+
*/
|
|
648
|
+
function extractParamNames(rawParams) {
|
|
649
|
+
if (!rawParams)
|
|
650
|
+
return [];
|
|
651
|
+
const names = [];
|
|
652
|
+
// Split by commas at depth 0
|
|
653
|
+
const parts = [];
|
|
654
|
+
let depth = 0;
|
|
655
|
+
let current = '';
|
|
656
|
+
for (const ch of rawParams) {
|
|
657
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<')
|
|
658
|
+
depth++;
|
|
659
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>')
|
|
660
|
+
depth--;
|
|
661
|
+
else if (ch === ',' && depth === 0) {
|
|
662
|
+
parts.push(current.trim());
|
|
663
|
+
current = '';
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
current += ch;
|
|
667
|
+
}
|
|
668
|
+
if (current.trim())
|
|
669
|
+
parts.push(current.trim());
|
|
670
|
+
for (const part of parts) {
|
|
671
|
+
const trimmed = part.trim();
|
|
672
|
+
if (!trimmed)
|
|
673
|
+
continue;
|
|
674
|
+
// Destructured params — extract individual names
|
|
675
|
+
if (trimmed.startsWith('{')) {
|
|
676
|
+
const closeBrace = trimmed.indexOf('}');
|
|
677
|
+
if (closeBrace !== -1) {
|
|
678
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
679
|
+
names.push(...destructNames);
|
|
680
|
+
}
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (trimmed.startsWith('[')) {
|
|
684
|
+
const closeBracket = trimmed.indexOf(']');
|
|
685
|
+
if (closeBracket !== -1) {
|
|
686
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
687
|
+
names.push(...destructNames);
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
// Rest parameter: ...args
|
|
692
|
+
if (trimmed.startsWith('...')) {
|
|
693
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
694
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
695
|
+
names.push(restName);
|
|
696
|
+
}
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
// Simple param: name or name: Type or name = default
|
|
700
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
701
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
702
|
+
names.push(paramName);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return names;
|
|
706
|
+
}
|
|
483
707
|
/**
|
|
484
708
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
485
709
|
*
|
|
@@ -887,7 +1111,11 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
887
1111
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
888
1112
|
// Find destructured variable declarations for tracing
|
|
889
1113
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
890
|
-
|
|
1114
|
+
// Find for-loop variable declarations for tracing
|
|
1115
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1116
|
+
// Find function parameter names for tracing
|
|
1117
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1118
|
+
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)
|
|
891
1119
|
return source;
|
|
892
1120
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
893
1121
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -907,9 +1135,50 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
907
1135
|
di.lineNo = origLine;
|
|
908
1136
|
}
|
|
909
1137
|
}
|
|
1138
|
+
// Fix for-loop var line numbers
|
|
1139
|
+
for (const fi of forLoopInsertions) {
|
|
1140
|
+
if (fi.varNames.length > 0) {
|
|
1141
|
+
// Search for 'for' keyword near the expected line
|
|
1142
|
+
const pattern = /\bfor\s*\(/;
|
|
1143
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1144
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1145
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1146
|
+
fi.lineNo = fwd + 1;
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
if (delta > 0 && delta <= 10) {
|
|
1150
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1151
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1152
|
+
fi.lineNo = bwd + 1;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
// Fix function param line numbers
|
|
1160
|
+
for (const fp of funcParamInsertions) {
|
|
1161
|
+
if (fp.paramNames.length > 0) {
|
|
1162
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1163
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1164
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1165
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1166
|
+
fp.lineNo = fwd + 1;
|
|
1167
|
+
break;
|
|
1168
|
+
}
|
|
1169
|
+
if (delta > 0 && delta <= 10) {
|
|
1170
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1171
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1172
|
+
fp.lineNo = bwd + 1;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
910
1179
|
}
|
|
911
1180
|
// 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;
|
|
1181
|
+
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;
|
|
913
1182
|
const importLines = [
|
|
914
1183
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
915
1184
|
];
|
|
@@ -947,7 +1216,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
947
1216
|
}
|
|
948
1217
|
}
|
|
949
1218
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
950
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1219
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
951
1220
|
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
1221
|
}
|
|
953
1222
|
// Add React component render tracker if needed
|
|
@@ -985,6 +1254,22 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
985
1254
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
986
1255
|
});
|
|
987
1256
|
}
|
|
1257
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1258
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1259
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1260
|
+
allInsertions.push({
|
|
1261
|
+
position: bodyStart,
|
|
1262
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1266
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1267
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1268
|
+
allInsertions.push({
|
|
1269
|
+
position: bodyStart,
|
|
1270
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
988
1273
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
989
1274
|
allInsertions.push({
|
|
990
1275
|
position,
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -473,6 +473,230 @@ function extractDestructuredNames(pattern) {
|
|
|
473
473
|
}
|
|
474
474
|
return names;
|
|
475
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
|
+
}
|
|
476
700
|
/**
|
|
477
701
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
478
702
|
*
|
|
@@ -880,7 +1104,11 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
880
1104
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
881
1105
|
// Find destructured variable declarations for tracing
|
|
882
1106
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
883
|
-
|
|
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)
|
|
884
1112
|
return source;
|
|
885
1113
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
886
1114
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -900,9 +1128,50 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
900
1128
|
di.lineNo = origLine;
|
|
901
1129
|
}
|
|
902
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
|
+
}
|
|
903
1172
|
}
|
|
904
1173
|
// 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;
|
|
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;
|
|
906
1175
|
const importLines = [
|
|
907
1176
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
908
1177
|
];
|
|
@@ -940,7 +1209,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
940
1209
|
}
|
|
941
1210
|
}
|
|
942
1211
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
943
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1212
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
944
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) {} }`);
|
|
945
1214
|
}
|
|
946
1215
|
// Add React component render tracker if needed
|
|
@@ -978,6 +1247,22 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
978
1247
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
979
1248
|
});
|
|
980
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
|
+
}
|
|
981
1266
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
982
1267
|
allInsertions.push({
|
|
983
1268
|
position,
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -461,6 +461,236 @@ function extractDestructuredNames(pattern: string): string[] {
|
|
|
461
461
|
return names;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Find for-loop variable declarations and return insertions for tracing.
|
|
466
|
+
* Handles:
|
|
467
|
+
* for (const item of items) { ... } → trace item
|
|
468
|
+
* for (const [key, val] of entries) { ... } → trace key, val
|
|
469
|
+
* for (const { a, b } of items) { ... } → trace a, b
|
|
470
|
+
* for (const key in obj) { ... } → trace key
|
|
471
|
+
* for (let i = 0; i < n; i++) { ... } → trace i
|
|
472
|
+
* Inserts trace calls at the start of the loop body.
|
|
473
|
+
*/
|
|
474
|
+
function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
|
|
475
|
+
const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
|
|
476
|
+
|
|
477
|
+
// Match: for (const/let/var ...
|
|
478
|
+
const forRegex = /\bfor\s*\(/g;
|
|
479
|
+
let match;
|
|
480
|
+
|
|
481
|
+
while ((match = forRegex.exec(source)) !== null) {
|
|
482
|
+
const afterParen = match.index + match[0].length;
|
|
483
|
+
|
|
484
|
+
// Skip whitespace
|
|
485
|
+
let pos = afterParen;
|
|
486
|
+
while (pos < source.length && /\s/.test(source[pos])) pos++;
|
|
487
|
+
|
|
488
|
+
// Expect const/let/var
|
|
489
|
+
const declMatch = source.slice(pos).match(/^(const|let|var)\s+/);
|
|
490
|
+
if (!declMatch) continue;
|
|
491
|
+
pos += declMatch[0].length;
|
|
492
|
+
|
|
493
|
+
// Now we have the variable pattern — could be identifier, {destructure}, or [destructure]
|
|
494
|
+
const varNames: string[] = [];
|
|
495
|
+
const patternStart = pos;
|
|
496
|
+
|
|
497
|
+
if (source[pos] === '{' || source[pos] === '[') {
|
|
498
|
+
// Destructured: find matching brace/bracket
|
|
499
|
+
const open = source[pos];
|
|
500
|
+
const close = open === '{' ? '}' : ']';
|
|
501
|
+
let depth = 1;
|
|
502
|
+
let end = pos + 1;
|
|
503
|
+
while (end < source.length && depth > 0) {
|
|
504
|
+
if (source[end] === open) depth++;
|
|
505
|
+
else if (source[end] === close) depth--;
|
|
506
|
+
end++;
|
|
507
|
+
}
|
|
508
|
+
const pattern = source.slice(pos, end);
|
|
509
|
+
const names = extractDestructuredNames(pattern);
|
|
510
|
+
varNames.push(...names);
|
|
511
|
+
pos = end;
|
|
512
|
+
} else {
|
|
513
|
+
// Simple identifier
|
|
514
|
+
const idMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
515
|
+
if (!idMatch) continue;
|
|
516
|
+
varNames.push(idMatch[1]);
|
|
517
|
+
pos += idMatch[0].length;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (varNames.length === 0) continue;
|
|
521
|
+
|
|
522
|
+
// Skip trickle internals
|
|
523
|
+
if (varNames.every(n => n.startsWith('__trickle') || n === '_a' || n === '_b' || n === '_c')) continue;
|
|
524
|
+
|
|
525
|
+
// Now find the opening `{` of the loop body
|
|
526
|
+
// Skip everything until the `)` that closes the for(...)
|
|
527
|
+
let parenDepth = 1; // We're inside the for(
|
|
528
|
+
while (pos < source.length && parenDepth > 0) {
|
|
529
|
+
const ch = source[pos];
|
|
530
|
+
if (ch === '(') parenDepth++;
|
|
531
|
+
else if (ch === ')') parenDepth--;
|
|
532
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
533
|
+
const q = ch; pos++;
|
|
534
|
+
while (pos < source.length && source[pos] !== q) {
|
|
535
|
+
if (source[pos] === '\\') pos++;
|
|
536
|
+
pos++;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
pos++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Now find the `{` after the closing `)`
|
|
543
|
+
while (pos < source.length && /\s/.test(source[pos])) pos++;
|
|
544
|
+
if (pos >= source.length || source[pos] !== '{') continue;
|
|
545
|
+
|
|
546
|
+
const bodyBrace = pos;
|
|
547
|
+
|
|
548
|
+
// Calculate line number
|
|
549
|
+
let lineNo = 1;
|
|
550
|
+
for (let i = 0; i < match.index; i++) {
|
|
551
|
+
if (source[i] === '\n') lineNo++;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
results.push({ bodyStart: bodyBrace + 1, varNames: varNames.filter(n => !n.startsWith('__trickle')), lineNo });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return results;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Find function parameter names and return insertions for tracing at the start
|
|
562
|
+
* of function bodies. Traces the runtime values of all parameters.
|
|
563
|
+
* Handles: function declarations, arrow functions, method definitions.
|
|
564
|
+
* Skips: React components (already tracked via __trickle_rc with props).
|
|
565
|
+
*/
|
|
566
|
+
function findFunctionParams(source: string, isReactFile: boolean): Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> {
|
|
567
|
+
const results: Array<{ bodyStart: number; paramNames: string[]; lineNo: number }> = [];
|
|
568
|
+
|
|
569
|
+
// Match function declarations: function name(params) {
|
|
570
|
+
const funcDeclRegex = /\b(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
571
|
+
let match;
|
|
572
|
+
|
|
573
|
+
while ((match = funcDeclRegex.exec(source)) !== null) {
|
|
574
|
+
const name = match[1];
|
|
575
|
+
if (name === 'require' || name === 'exports' || name === 'module') continue;
|
|
576
|
+
if (name.startsWith('__trickle')) continue;
|
|
577
|
+
|
|
578
|
+
// Skip React components (uppercase) in React files — already tracked
|
|
579
|
+
if (isReactFile && /^[A-Z]/.test(name)) continue;
|
|
580
|
+
|
|
581
|
+
const afterParen = match.index + match[0].length;
|
|
582
|
+
const bodyBrace = findFunctionBodyBrace(source, afterParen);
|
|
583
|
+
if (bodyBrace === -1) continue;
|
|
584
|
+
|
|
585
|
+
// Extract parameter names from between ( and )
|
|
586
|
+
const paramStr = source.slice(afterParen, bodyBrace);
|
|
587
|
+
const closeParen = paramStr.indexOf(')');
|
|
588
|
+
if (closeParen === -1) continue;
|
|
589
|
+
const rawParams = paramStr.slice(0, closeParen).trim();
|
|
590
|
+
const paramNames = extractParamNames(rawParams);
|
|
591
|
+
if (paramNames.length === 0) continue;
|
|
592
|
+
|
|
593
|
+
let lineNo = 1;
|
|
594
|
+
for (let i = 0; i < match.index; i++) {
|
|
595
|
+
if (source[i] === '\n') lineNo++;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
results.push({ bodyStart: bodyBrace + 1, paramNames, lineNo });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Match arrow functions: const name = (params) => {
|
|
602
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*:\s*[^=]+?)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+?)?\s*=>\s*\{/g;
|
|
603
|
+
|
|
604
|
+
while ((match = arrowFuncRegex.exec(source)) !== null) {
|
|
605
|
+
const name = match[1];
|
|
606
|
+
if (name.startsWith('__trickle')) continue;
|
|
607
|
+
// Skip React components in React files
|
|
608
|
+
if (isReactFile && /^[A-Z]/.test(name)) continue;
|
|
609
|
+
|
|
610
|
+
const rawParams = match[2].trim();
|
|
611
|
+
const paramNames = extractParamNames(rawParams);
|
|
612
|
+
if (paramNames.length === 0) continue;
|
|
613
|
+
|
|
614
|
+
// Find the { position
|
|
615
|
+
const bracePos = match.index + match[0].length - 1;
|
|
616
|
+
|
|
617
|
+
let lineNo = 1;
|
|
618
|
+
for (let i = 0; i < match.index; i++) {
|
|
619
|
+
if (source[i] === '\n') lineNo++;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
results.push({ bodyStart: bracePos + 1, paramNames, lineNo });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return results;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Extract parameter names from a function parameter string.
|
|
630
|
+
* Handles: simple names, destructured { a, b }, defaults (a = 1), rest (...args).
|
|
631
|
+
* Skips type annotations.
|
|
632
|
+
*/
|
|
633
|
+
function extractParamNames(rawParams: string): string[] {
|
|
634
|
+
if (!rawParams) return [];
|
|
635
|
+
const names: string[] = [];
|
|
636
|
+
|
|
637
|
+
// Split by commas at depth 0
|
|
638
|
+
const parts: string[] = [];
|
|
639
|
+
let depth = 0;
|
|
640
|
+
let current = '';
|
|
641
|
+
for (const ch of rawParams) {
|
|
642
|
+
if (ch === '{' || ch === '[' || ch === '(' || ch === '<') depth++;
|
|
643
|
+
else if (ch === '}' || ch === ']' || ch === ')' || ch === '>') depth--;
|
|
644
|
+
else if (ch === ',' && depth === 0) {
|
|
645
|
+
parts.push(current.trim());
|
|
646
|
+
current = '';
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
current += ch;
|
|
650
|
+
}
|
|
651
|
+
if (current.trim()) parts.push(current.trim());
|
|
652
|
+
|
|
653
|
+
for (const part of parts) {
|
|
654
|
+
const trimmed = part.trim();
|
|
655
|
+
if (!trimmed) continue;
|
|
656
|
+
|
|
657
|
+
// Destructured params — extract individual names
|
|
658
|
+
if (trimmed.startsWith('{')) {
|
|
659
|
+
const closeBrace = trimmed.indexOf('}');
|
|
660
|
+
if (closeBrace !== -1) {
|
|
661
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBrace + 1));
|
|
662
|
+
names.push(...destructNames);
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (trimmed.startsWith('[')) {
|
|
667
|
+
const closeBracket = trimmed.indexOf(']');
|
|
668
|
+
if (closeBracket !== -1) {
|
|
669
|
+
const destructNames = extractDestructuredNames(trimmed.slice(0, closeBracket + 1));
|
|
670
|
+
names.push(...destructNames);
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Rest parameter: ...args
|
|
676
|
+
if (trimmed.startsWith('...')) {
|
|
677
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
678
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
679
|
+
names.push(restName);
|
|
680
|
+
}
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Simple param: name or name: Type or name = default
|
|
685
|
+
const paramName = trimmed.split(/[\s:=]/)[0].trim();
|
|
686
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(paramName)) {
|
|
687
|
+
names.push(paramName);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return names;
|
|
692
|
+
}
|
|
693
|
+
|
|
464
694
|
/**
|
|
465
695
|
* Transform ESM source code to wrap function declarations and trace variables.
|
|
466
696
|
*
|
|
@@ -878,7 +1108,13 @@ export function transformEsmSource(
|
|
|
878
1108
|
// Find destructured variable declarations for tracing
|
|
879
1109
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
880
1110
|
|
|
881
|
-
|
|
1111
|
+
// Find for-loop variable declarations for tracing
|
|
1112
|
+
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1113
|
+
|
|
1114
|
+
// Find function parameter names for tracing
|
|
1115
|
+
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1116
|
+
|
|
1117
|
+
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) return source;
|
|
882
1118
|
|
|
883
1119
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
884
1120
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -897,10 +1133,51 @@ export function transformEsmSource(
|
|
|
897
1133
|
if (origLine !== -1) di.lineNo = origLine;
|
|
898
1134
|
}
|
|
899
1135
|
}
|
|
1136
|
+
// Fix for-loop var line numbers
|
|
1137
|
+
for (const fi of forLoopInsertions) {
|
|
1138
|
+
if (fi.varNames.length > 0) {
|
|
1139
|
+
// Search for 'for' keyword near the expected line
|
|
1140
|
+
const pattern = /\bfor\s*\(/;
|
|
1141
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1142
|
+
const fwd = fi.lineNo - 1 + delta;
|
|
1143
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1144
|
+
fi.lineNo = fwd + 1;
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
if (delta > 0 && delta <= 10) {
|
|
1148
|
+
const bwd = fi.lineNo - 1 - delta;
|
|
1149
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1150
|
+
fi.lineNo = bwd + 1;
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Fix function param line numbers
|
|
1158
|
+
for (const fp of funcParamInsertions) {
|
|
1159
|
+
if (fp.paramNames.length > 0) {
|
|
1160
|
+
const pattern = /\bfunction\s+\w+\s*\(|=>\s*\{/;
|
|
1161
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
1162
|
+
const fwd = fp.lineNo - 1 + delta;
|
|
1163
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
1164
|
+
fp.lineNo = fwd + 1;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
if (delta > 0 && delta <= 10) {
|
|
1168
|
+
const bwd = fp.lineNo - 1 - delta;
|
|
1169
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
1170
|
+
fp.lineNo = bwd + 1;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
900
1177
|
}
|
|
901
1178
|
|
|
902
1179
|
// 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;
|
|
1180
|
+
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;
|
|
904
1181
|
const importLines: string[] = [
|
|
905
1182
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
906
1183
|
];
|
|
@@ -970,7 +1247,7 @@ export function transformEsmSource(
|
|
|
970
1247
|
}
|
|
971
1248
|
|
|
972
1249
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
973
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
1250
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
974
1251
|
prefixLines.push(
|
|
975
1252
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
976
1253
|
` const _cache = new Set();`,
|
|
@@ -1141,6 +1418,24 @@ export function transformEsmSource(
|
|
|
1141
1418
|
});
|
|
1142
1419
|
}
|
|
1143
1420
|
|
|
1421
|
+
// For-loop variable insertions: insert trace at start of loop body
|
|
1422
|
+
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1423
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1424
|
+
allInsertions.push({
|
|
1425
|
+
position: bodyStart,
|
|
1426
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Function parameter insertions: insert trace at start of function body
|
|
1431
|
+
for (const { bodyStart, paramNames, lineNo } of funcParamInsertions) {
|
|
1432
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1433
|
+
allInsertions.push({
|
|
1434
|
+
position: bodyStart,
|
|
1435
|
+
code: `\ntry{${calls}}catch(__e){}\n`,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1144
1439
|
for (const { position, name, lineNo, propsExpr } of bodyInsertions) {
|
|
1145
1440
|
allInsertions.push({
|
|
1146
1441
|
position,
|