trickle-observe 0.2.109 → 0.2.111
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/observe-register.js +225 -5
- package/dist/vite-plugin.js +179 -4
- package/dist-esm/vite-plugin.js +179 -4
- package/package.json +1 -1
- package/src/observe-register.ts +224 -5
- package/src/vite-plugin.ts +181 -4
- package/dist/vite-plugin.test.d.ts +0 -1
- package/dist/vite-plugin.test.js +0 -160
package/dist/observe-register.js
CHANGED
|
@@ -677,6 +677,205 @@ function extractDestructuredNames(pattern) {
|
|
|
677
677
|
}
|
|
678
678
|
return names;
|
|
679
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Extract parameter names from a parameter string (the text between parens).
|
|
682
|
+
* Handles: simple names, defaults (x = 5), type annotations (x: string),
|
|
683
|
+
* rest params (...args), and destructured params ({a, b} or [a, b]).
|
|
684
|
+
* Skips params starting with _ (convention for unused params).
|
|
685
|
+
*/
|
|
686
|
+
function extractParamNamesFromStr(paramStr) {
|
|
687
|
+
if (!paramStr.trim())
|
|
688
|
+
return [];
|
|
689
|
+
// Split on commas at depth 0 (respecting nested parens, braces, brackets)
|
|
690
|
+
const params = [];
|
|
691
|
+
let depth = 0;
|
|
692
|
+
let current = '';
|
|
693
|
+
for (const ch of paramStr) {
|
|
694
|
+
if (ch === '(' || ch === '{' || ch === '[')
|
|
695
|
+
depth++;
|
|
696
|
+
else if (ch === ')' || ch === '}' || ch === ']')
|
|
697
|
+
depth--;
|
|
698
|
+
else if (ch === ',' && depth === 0) {
|
|
699
|
+
params.push(current.trim());
|
|
700
|
+
current = '';
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
current += ch;
|
|
704
|
+
}
|
|
705
|
+
if (current.trim())
|
|
706
|
+
params.push(current.trim());
|
|
707
|
+
const names = [];
|
|
708
|
+
for (const p of params) {
|
|
709
|
+
const trimmed = p.trim();
|
|
710
|
+
if (!trimmed)
|
|
711
|
+
continue;
|
|
712
|
+
// Rest params: ...args -> trace 'args'
|
|
713
|
+
if (trimmed.startsWith('...')) {
|
|
714
|
+
const restName = trimmed.slice(3).split(/[\s:=]/)[0].trim();
|
|
715
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName) && !restName.startsWith('_')) {
|
|
716
|
+
names.push(restName);
|
|
717
|
+
}
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
// Destructured params: { name, age } or [a, b] — skip (can't trace the whole object by a single name)
|
|
721
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('['))
|
|
722
|
+
continue;
|
|
723
|
+
// Simple param: strip default value and type annotation
|
|
724
|
+
const name = trimmed.split('=')[0].trim().split(':')[0].trim();
|
|
725
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !name.startsWith('_')) {
|
|
726
|
+
names.push(name);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return names;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Find all function bodies (declarations, expressions, arrow functions, methods)
|
|
733
|
+
* and return insertions to trace their parameters at the top of the body.
|
|
734
|
+
*
|
|
735
|
+
* Returns: Array of { bodyStart: position right after '{', paramNames: string[], lineNo: line of function }
|
|
736
|
+
*/
|
|
737
|
+
function findFunctionParamInsertions(source, lineOffset = 0) {
|
|
738
|
+
const results = [];
|
|
739
|
+
// Match all patterns that start a function with a parameter list followed by a body:
|
|
740
|
+
// 1. function foo(...) { ... }
|
|
741
|
+
// 2. function(...) { ... }
|
|
742
|
+
// 3. (...) => { ... }
|
|
743
|
+
// 4. async (...) => { ... }
|
|
744
|
+
// 5. methodName(...) { ... } (inside class or object)
|
|
745
|
+
// 6. single param arrow: x => { ... }
|
|
746
|
+
//
|
|
747
|
+
// Strategy: find every opening paren that is part of a function parameter list,
|
|
748
|
+
// then find the body brace. We use a regex to find candidate positions.
|
|
749
|
+
// Pattern A: function declarations and expressions
|
|
750
|
+
// Matches: [async] function [name] (
|
|
751
|
+
const funcPattern = /(?:async\s+)?function\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*\(/g;
|
|
752
|
+
let m;
|
|
753
|
+
while ((m = funcPattern.exec(source)) !== null) {
|
|
754
|
+
const openParenIdx = source.indexOf('(', m.index + 8); // skip past 'function'
|
|
755
|
+
if (openParenIdx === -1)
|
|
756
|
+
continue;
|
|
757
|
+
processFunction(source, openParenIdx, m.index, results, lineOffset);
|
|
758
|
+
}
|
|
759
|
+
// Pattern B: arrow functions with parens: (...) =>
|
|
760
|
+
// Look for ) followed by optional whitespace then =>
|
|
761
|
+
// We search for => and walk backwards to find the matching (
|
|
762
|
+
const arrowPattern = /\)\s*=>/g;
|
|
763
|
+
while ((m = arrowPattern.exec(source)) !== null) {
|
|
764
|
+
// Find the matching opening paren for the ) at m.index
|
|
765
|
+
const closeParenIdx = m.index;
|
|
766
|
+
const openParenIdx = findMatchingOpenParen(source, closeParenIdx);
|
|
767
|
+
if (openParenIdx === -1)
|
|
768
|
+
continue;
|
|
769
|
+
// Make sure this isn't inside a string or comment (basic heuristic: check if 'function' keyword precedes)
|
|
770
|
+
// If 'function' precedes, Pattern A already handled it
|
|
771
|
+
const before = source.slice(Math.max(0, openParenIdx - 30), openParenIdx).trimEnd();
|
|
772
|
+
if (/function\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*$/.test(before))
|
|
773
|
+
continue;
|
|
774
|
+
// Find the arrow and body
|
|
775
|
+
const arrowIdx = source.indexOf('=>', closeParenIdx + 1);
|
|
776
|
+
if (arrowIdx === -1)
|
|
777
|
+
continue;
|
|
778
|
+
const afterArrow = arrowIdx + 2;
|
|
779
|
+
// Skip whitespace after =>
|
|
780
|
+
let bodyPos = afterArrow;
|
|
781
|
+
while (bodyPos < source.length && (source[bodyPos] === ' ' || source[bodyPos] === '\t' || source[bodyPos] === '\n' || source[bodyPos] === '\r'))
|
|
782
|
+
bodyPos++;
|
|
783
|
+
// Only handle block bodies (with {), not expression bodies
|
|
784
|
+
if (bodyPos >= source.length || source[bodyPos] !== '{')
|
|
785
|
+
continue;
|
|
786
|
+
// Extract params
|
|
787
|
+
const paramStr = source.slice(openParenIdx + 1, closeParenIdx);
|
|
788
|
+
const paramNames = extractParamNamesFromStr(paramStr);
|
|
789
|
+
if (paramNames.length === 0)
|
|
790
|
+
continue;
|
|
791
|
+
// Calculate line number
|
|
792
|
+
let lineNo = 1;
|
|
793
|
+
for (let i = 0; i < openParenIdx; i++) {
|
|
794
|
+
if (source[i] === '\n')
|
|
795
|
+
lineNo++;
|
|
796
|
+
}
|
|
797
|
+
lineNo = Math.max(1, lineNo - lineOffset);
|
|
798
|
+
results.push({ bodyStart: bodyPos + 1, paramNames, lineNo });
|
|
799
|
+
}
|
|
800
|
+
return results;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Process a function whose opening paren is at openParenIdx.
|
|
804
|
+
* Finds params, body brace, and adds to results.
|
|
805
|
+
*/
|
|
806
|
+
function processFunction(source, openParenIdx, funcStartIdx, results, lineOffset) {
|
|
807
|
+
// Find closing paren using findFunctionBodyBrace approach: it expects the position after '('
|
|
808
|
+
const afterOpenParen = openParenIdx + 1;
|
|
809
|
+
const openBrace = (0, vite_plugin_1.findFunctionBodyBrace)(source, afterOpenParen);
|
|
810
|
+
if (openBrace === -1)
|
|
811
|
+
return;
|
|
812
|
+
// Extract param string between ( and the closing )
|
|
813
|
+
// findFunctionBodyBrace skips parens then finds {, so we need to find the closing ) ourselves
|
|
814
|
+
let parenDepth = 1;
|
|
815
|
+
let closeParenIdx = afterOpenParen;
|
|
816
|
+
while (closeParenIdx < source.length && parenDepth > 0) {
|
|
817
|
+
const ch = source[closeParenIdx];
|
|
818
|
+
if (ch === '(')
|
|
819
|
+
parenDepth++;
|
|
820
|
+
else if (ch === ')') {
|
|
821
|
+
parenDepth--;
|
|
822
|
+
if (parenDepth === 0)
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
826
|
+
const q = ch;
|
|
827
|
+
closeParenIdx++;
|
|
828
|
+
while (closeParenIdx < source.length && source[closeParenIdx] !== q) {
|
|
829
|
+
if (source[closeParenIdx] === '\\')
|
|
830
|
+
closeParenIdx++;
|
|
831
|
+
closeParenIdx++;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
closeParenIdx++;
|
|
835
|
+
}
|
|
836
|
+
const paramStr = source.slice(afterOpenParen, closeParenIdx);
|
|
837
|
+
const paramNames = extractParamNamesFromStr(paramStr);
|
|
838
|
+
if (paramNames.length === 0)
|
|
839
|
+
return;
|
|
840
|
+
// Calculate line number of the function
|
|
841
|
+
let lineNo = 1;
|
|
842
|
+
for (let i = 0; i < funcStartIdx; i++) {
|
|
843
|
+
if (source[i] === '\n')
|
|
844
|
+
lineNo++;
|
|
845
|
+
}
|
|
846
|
+
lineNo = Math.max(1, lineNo - lineOffset);
|
|
847
|
+
results.push({ bodyStart: openBrace + 1, paramNames, lineNo });
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Find the matching opening paren for a closing paren at closeIdx.
|
|
851
|
+
* Walks backward respecting nesting, strings, etc. (simplified).
|
|
852
|
+
*/
|
|
853
|
+
function findMatchingOpenParen(source, closeIdx) {
|
|
854
|
+
let depth = 1;
|
|
855
|
+
let pos = closeIdx - 1;
|
|
856
|
+
while (pos >= 0 && depth > 0) {
|
|
857
|
+
const ch = source[pos];
|
|
858
|
+
if (ch === ')')
|
|
859
|
+
depth++;
|
|
860
|
+
else if (ch === '(') {
|
|
861
|
+
depth--;
|
|
862
|
+
if (depth === 0)
|
|
863
|
+
return pos;
|
|
864
|
+
}
|
|
865
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
866
|
+
// Walk backward through string (simplified — won't handle all edge cases but good enough)
|
|
867
|
+
const q = ch;
|
|
868
|
+
pos--;
|
|
869
|
+
while (pos >= 0 && source[pos] !== q) {
|
|
870
|
+
if (pos > 0 && source[pos - 1] === '\\')
|
|
871
|
+
pos--;
|
|
872
|
+
pos--;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
pos--;
|
|
876
|
+
}
|
|
877
|
+
return -1;
|
|
878
|
+
}
|
|
680
879
|
function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
681
880
|
const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
682
881
|
const insertions = [];
|
|
@@ -854,8 +1053,6 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
854
1053
|
}
|
|
855
1054
|
}
|
|
856
1055
|
// Additional variable patterns: reassignments, for-loops, catch clauses
|
|
857
|
-
// Note: function params are NOT traced here because observe-register already
|
|
858
|
-
// wraps functions with __trickle_wrap which captures param types via wrapFunction.
|
|
859
1056
|
// Apply source map remapping to these too.
|
|
860
1057
|
const reassignInsertions = (0, vite_plugin_1.findReassignments)(source).map(ins => ({
|
|
861
1058
|
...ins,
|
|
@@ -872,7 +1069,21 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
872
1069
|
sourceFile: getSourceFile(ins.lineNo),
|
|
873
1070
|
lineNo: remapLine(ins.lineNo),
|
|
874
1071
|
}));
|
|
875
|
-
|
|
1072
|
+
// Function parameter tracing: inject __trickle_tv() calls at the top of function bodies
|
|
1073
|
+
// for each parameter. This covers function declarations, expressions, arrow functions,
|
|
1074
|
+
// and method definitions (including Express-style callbacks like (req, res) => {}).
|
|
1075
|
+
let funcParamInsertions = [];
|
|
1076
|
+
if (varTraceEnabled) {
|
|
1077
|
+
funcParamInsertions = findFunctionParamInsertions(source).map(ins => ({
|
|
1078
|
+
...ins,
|
|
1079
|
+
sourceFile: getSourceFile(ins.lineNo),
|
|
1080
|
+
lineNo: remapLine(ins.lineNo),
|
|
1081
|
+
}));
|
|
1082
|
+
if (debug && funcParamInsertions.length > 0) {
|
|
1083
|
+
console.log(`[trickle/observe] Tracing ${funcParamInsertions.length} function param sites in ${moduleName}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0 && funcParamInsertions.length === 0)
|
|
876
1087
|
return source;
|
|
877
1088
|
// Resolve the path to the wrap helper (compiled JS)
|
|
878
1089
|
const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
|
|
@@ -898,8 +1109,8 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
898
1109
|
` return __trickle_mod.wrapFunction(fn, opts);`,
|
|
899
1110
|
`};`,
|
|
900
1111
|
];
|
|
901
|
-
// Add variable tracing helper if we have var insertions
|
|
902
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0) {
|
|
1112
|
+
// Add variable tracing helper if we have var insertions or function param insertions
|
|
1113
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
903
1114
|
const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
|
|
904
1115
|
// When source map is available, trace variables against the original source file
|
|
905
1116
|
const traceFilePath = sourceMap ? sourceMap.originalFile : filename;
|
|
@@ -964,6 +1175,15 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
|
|
|
964
1175
|
code: `\ntry{${calls}}catch(__e2){}\n`,
|
|
965
1176
|
});
|
|
966
1177
|
}
|
|
1178
|
+
// Function parameter insertions — inject __trickle_tv() at top of function bodies
|
|
1179
|
+
for (const { bodyStart, paramNames, lineNo, sourceFile } of funcParamInsertions) {
|
|
1180
|
+
const sf = sfArgs(sourceFile);
|
|
1181
|
+
const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
|
|
1182
|
+
allInsertions.push({
|
|
1183
|
+
position: bodyStart,
|
|
1184
|
+
code: `\ntry{${calls}}catch(__e3){}\n`,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
967
1187
|
// Add class method wrappings
|
|
968
1188
|
for (const ci of classInsertions) {
|
|
969
1189
|
allInsertions.push(ci);
|
package/dist/vite-plugin.js
CHANGED
|
@@ -510,6 +510,156 @@ function extractDestructuredNames(pattern) {
|
|
|
510
510
|
}
|
|
511
511
|
return names;
|
|
512
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Find import declarations and extract imported bindings for tracing.
|
|
515
|
+
* Handles:
|
|
516
|
+
* import { a, b } from '...' → trace a, b
|
|
517
|
+
* import { a as b } from '...' → trace b (local name)
|
|
518
|
+
* import X from '...' → trace X (default import)
|
|
519
|
+
* import * as X from '...' → trace X (namespace import)
|
|
520
|
+
* import X, { a, b } from '...' → trace X, a, b
|
|
521
|
+
* Returns insertions to place AFTER the import statement.
|
|
522
|
+
*/
|
|
523
|
+
function findImportDeclarations(source) {
|
|
524
|
+
const results = [];
|
|
525
|
+
// Match import statements (potentially multiline)
|
|
526
|
+
// We scan for `import` at the start of a line (with optional whitespace)
|
|
527
|
+
const importRegex = /^[ \t]*import\s+/gm;
|
|
528
|
+
let match;
|
|
529
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
530
|
+
const importStart = match.index;
|
|
531
|
+
let pos = importStart + match[0].length;
|
|
532
|
+
const varNames = [];
|
|
533
|
+
// Skip type-only imports: `import type ...`
|
|
534
|
+
if (source.slice(pos).startsWith('type ') || source.slice(pos).startsWith('type{'))
|
|
535
|
+
continue;
|
|
536
|
+
// Skip bare imports: `import '...'` or `import "..."`
|
|
537
|
+
const afterImport = source[pos];
|
|
538
|
+
if (afterImport === '"' || afterImport === "'")
|
|
539
|
+
continue;
|
|
540
|
+
// Parse the import clause
|
|
541
|
+
// Could be:
|
|
542
|
+
// * as X
|
|
543
|
+
// X (default)
|
|
544
|
+
// { a, b, c as d }
|
|
545
|
+
// X, { a, b }
|
|
546
|
+
// X, * as Y
|
|
547
|
+
// Check for namespace import: * as X
|
|
548
|
+
if (source[pos] === '*') {
|
|
549
|
+
const nsMatch = source.slice(pos).match(/^\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
550
|
+
if (nsMatch) {
|
|
551
|
+
varNames.push(nsMatch[1]);
|
|
552
|
+
pos += nsMatch[0].length;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Check for default import or named imports
|
|
556
|
+
else if (source[pos] === '{') {
|
|
557
|
+
// Named imports only: { a, b, c as d }
|
|
558
|
+
const closeIdx = source.indexOf('}', pos);
|
|
559
|
+
if (closeIdx === -1)
|
|
560
|
+
continue;
|
|
561
|
+
const namedStr = source.slice(pos + 1, closeIdx);
|
|
562
|
+
const names = parseNamedImports(namedStr);
|
|
563
|
+
varNames.push(...names);
|
|
564
|
+
pos = closeIdx + 1;
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// Default import: X or X, { ... } or X, * as Y
|
|
568
|
+
const defaultMatch = source.slice(pos).match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
569
|
+
if (defaultMatch) {
|
|
570
|
+
varNames.push(defaultMatch[1]);
|
|
571
|
+
pos += defaultMatch[0].length;
|
|
572
|
+
// Skip whitespace and comma
|
|
573
|
+
while (pos < source.length && /[\s,]/.test(source[pos]))
|
|
574
|
+
pos++;
|
|
575
|
+
// Check for additional named imports: , { a, b }
|
|
576
|
+
if (source[pos] === '{') {
|
|
577
|
+
const closeIdx = source.indexOf('}', pos);
|
|
578
|
+
if (closeIdx !== -1) {
|
|
579
|
+
const namedStr = source.slice(pos + 1, closeIdx);
|
|
580
|
+
const names = parseNamedImports(namedStr);
|
|
581
|
+
varNames.push(...names);
|
|
582
|
+
pos = closeIdx + 1;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Check for namespace: , * as Y
|
|
586
|
+
else if (source[pos] === '*') {
|
|
587
|
+
const nsMatch = source.slice(pos).match(/^\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
588
|
+
if (nsMatch) {
|
|
589
|
+
varNames.push(nsMatch[1]);
|
|
590
|
+
pos += nsMatch[0].length;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (varNames.length === 0)
|
|
596
|
+
continue;
|
|
597
|
+
// Skip trickle internals
|
|
598
|
+
const filtered = varNames.filter(n => !n.startsWith('__trickle'));
|
|
599
|
+
if (filtered.length === 0)
|
|
600
|
+
continue;
|
|
601
|
+
// Find the end of the import statement (semicolon or newline after `from '...'`)
|
|
602
|
+
const fromIdx = source.indexOf('from', pos);
|
|
603
|
+
if (fromIdx === -1)
|
|
604
|
+
continue;
|
|
605
|
+
// Find the end: either `;` or end of line after the string literal
|
|
606
|
+
let endPos = fromIdx + 4;
|
|
607
|
+
// Skip whitespace
|
|
608
|
+
while (endPos < source.length && /\s/.test(source[endPos]))
|
|
609
|
+
endPos++;
|
|
610
|
+
// Skip the string literal
|
|
611
|
+
if (endPos < source.length && (source[endPos] === '"' || source[endPos] === "'")) {
|
|
612
|
+
const quote = source[endPos];
|
|
613
|
+
endPos++;
|
|
614
|
+
while (endPos < source.length && source[endPos] !== quote) {
|
|
615
|
+
if (source[endPos] === '\\')
|
|
616
|
+
endPos++;
|
|
617
|
+
endPos++;
|
|
618
|
+
}
|
|
619
|
+
endPos++; // skip closing quote
|
|
620
|
+
}
|
|
621
|
+
// Skip optional semicolon
|
|
622
|
+
while (endPos < source.length && (source[endPos] === ';' || source[endPos] === ' ' || source[endPos] === '\t'))
|
|
623
|
+
endPos++;
|
|
624
|
+
// Calculate line number
|
|
625
|
+
let lineNo = 1;
|
|
626
|
+
for (let i = 0; i < importStart; i++) {
|
|
627
|
+
if (source[i] === '\n')
|
|
628
|
+
lineNo++;
|
|
629
|
+
}
|
|
630
|
+
results.push({ lineEnd: endPos, varNames: filtered, lineNo });
|
|
631
|
+
}
|
|
632
|
+
return results;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Parse named imports from the content between { and }.
|
|
636
|
+
* Handles: a, b, c as d, type e (skips type-only imports)
|
|
637
|
+
* Returns local binding names.
|
|
638
|
+
*/
|
|
639
|
+
function parseNamedImports(namedStr) {
|
|
640
|
+
const names = [];
|
|
641
|
+
const parts = namedStr.split(',');
|
|
642
|
+
for (const part of parts) {
|
|
643
|
+
const trimmed = part.trim();
|
|
644
|
+
if (!trimmed)
|
|
645
|
+
continue;
|
|
646
|
+
// Skip type-only: `type Foo` or `type Foo as Bar`
|
|
647
|
+
if (/^type\s+/.test(trimmed))
|
|
648
|
+
continue;
|
|
649
|
+
// Check for alias: `original as local`
|
|
650
|
+
const asMatch = trimmed.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
651
|
+
if (asMatch) {
|
|
652
|
+
names.push(asMatch[1]);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
const name = trimmed.split(/[\s]/)[0];
|
|
656
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
657
|
+
names.push(name);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return names;
|
|
662
|
+
}
|
|
513
663
|
/**
|
|
514
664
|
* Find class body ranges in source code. Handles both:
|
|
515
665
|
* class Foo { ... }
|
|
@@ -803,6 +953,11 @@ function findJsxExpressions(source) {
|
|
|
803
953
|
let match;
|
|
804
954
|
while ((match = jsxExprRegex.exec(source)) !== null) {
|
|
805
955
|
const bracePos = match.index;
|
|
956
|
+
// Skip if this `{` is part of an import statement: `import { ... } from '...'`
|
|
957
|
+
const lineStart = source.lastIndexOf('\n', bracePos - 1) + 1;
|
|
958
|
+
const linePrefix = source.slice(lineStart, bracePos).trimStart();
|
|
959
|
+
if (/^import\s/.test(linePrefix) || /^import\s/.test(linePrefix.replace(/^export\s+/, '')))
|
|
960
|
+
continue;
|
|
806
961
|
// Skip if inside a string or comment
|
|
807
962
|
// Simple check: look at the character before `{`
|
|
808
963
|
const charBefore = bracePos > 0 ? source[bracePos - 1] : '';
|
|
@@ -813,9 +968,19 @@ function findJsxExpressions(source) {
|
|
|
813
968
|
// Skip if this looks like a template literal expression `${`
|
|
814
969
|
if (charBefore === '$')
|
|
815
970
|
continue;
|
|
816
|
-
// Skip if preceded by
|
|
971
|
+
// Skip if preceded (past whitespace) by `,`, `(`, or `:` — function arguments, object literals, attribute values
|
|
817
972
|
if (charBefore === ',')
|
|
818
973
|
continue;
|
|
974
|
+
{
|
|
975
|
+
let scanBack = bracePos - 1;
|
|
976
|
+
while (scanBack >= 0 && (source[scanBack] === ' ' || source[scanBack] === '\t' || source[scanBack] === '\n' || source[scanBack] === '\r'))
|
|
977
|
+
scanBack--;
|
|
978
|
+
if (scanBack >= 0 && (source[scanBack] === ',' || source[scanBack] === '(' || source[scanBack] === ':' || source[scanBack] === ')'))
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
// Skip if this `{` is part of a variable declaration destructuring: `const { ... }` or `let { ... }`
|
|
982
|
+
if (/(?:const|let|var)\s*$/.test(source.slice(Math.max(0, bracePos - 10), bracePos)))
|
|
983
|
+
continue;
|
|
819
984
|
// Must be in a JSX context: look backward for `>` or `}` (closing tag bracket or prev expression)
|
|
820
985
|
// before hitting structural JS characters like `{`, `(`, `;`
|
|
821
986
|
let inJsx = false;
|
|
@@ -1547,6 +1712,8 @@ ingestUrl) {
|
|
|
1547
1712
|
stateInsertions.push({ renamePos, afterLine, stateName, setterName, lineNo });
|
|
1548
1713
|
}
|
|
1549
1714
|
}
|
|
1715
|
+
// Find import declarations for tracing (trace imported bindings after import statement)
|
|
1716
|
+
const importInsertions = traceVars ? findImportDeclarations(source) : [];
|
|
1550
1717
|
// Find variable declarations for tracing
|
|
1551
1718
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
1552
1719
|
// Find destructured variable declarations for tracing
|
|
@@ -1561,7 +1728,7 @@ ingestUrl) {
|
|
|
1561
1728
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1562
1729
|
// Find JSX text expressions for tracing (React files only)
|
|
1563
1730
|
const jsxExprInsertions = (traceVars && isReactFile) ? findJsxExpressions(source) : [];
|
|
1564
|
-
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1731
|
+
if (funcInsertions.length === 0 && importInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && jsxExprInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1565
1732
|
return source;
|
|
1566
1733
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1567
1734
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1630,7 +1797,7 @@ ingestUrl) {
|
|
|
1630
1797
|
}
|
|
1631
1798
|
}
|
|
1632
1799
|
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
1633
|
-
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1800
|
+
const needsTracing = importInsertions.length > 0 || varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1634
1801
|
const importLines = [];
|
|
1635
1802
|
if (isSSR) {
|
|
1636
1803
|
// SSR/Node.js — import trickle-observe for function wrapping + file system for writing
|
|
@@ -1665,7 +1832,7 @@ ingestUrl) {
|
|
|
1665
1832
|
}
|
|
1666
1833
|
}
|
|
1667
1834
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
1668
|
-
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1835
|
+
if (importInsertions.length > 0 || varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
|
|
1669
1836
|
prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` 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 sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
|
|
1670
1837
|
}
|
|
1671
1838
|
// Add React component render tracker if needed
|
|
@@ -1690,6 +1857,14 @@ ingestUrl) {
|
|
|
1690
1857
|
code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
|
|
1691
1858
|
});
|
|
1692
1859
|
}
|
|
1860
|
+
// Import insertions: trace imported bindings after the import statement
|
|
1861
|
+
for (const { lineEnd, varNames, lineNo } of importInsertions) {
|
|
1862
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
1863
|
+
allInsertions.push({
|
|
1864
|
+
position: lineEnd,
|
|
1865
|
+
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1693
1868
|
for (const { lineEnd, varName, lineNo } of varInsertions) {
|
|
1694
1869
|
allInsertions.push({
|
|
1695
1870
|
position: lineEnd,
|