trickle-observe 0.2.70 → 0.2.72

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.
@@ -480,6 +480,110 @@ function extractDestructuredNames(pattern) {
480
480
  }
481
481
  return names;
482
482
  }
483
+ /**
484
+ * Find variable reassignments (not declarations) and return insertions for tracing.
485
+ * Handles: x = newValue; x += 1; x ||= fallback; etc.
486
+ * Only matches standalone reassignment statements at the start of a line.
487
+ * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
488
+ * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
489
+ */
490
+ function findReassignments(source) {
491
+ const results = [];
492
+ // Match: <identifier> <assignOp>= <value> at the start of a line
493
+ // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
494
+ // Plain: = (but not ==, ===, =>, !=)
495
+ const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
496
+ let match;
497
+ while ((match = reassignRegex.exec(source)) !== null) {
498
+ const varName = match[2];
499
+ // Skip trickle internals
500
+ if (varName.startsWith('__trickle') || varName.startsWith('_$'))
501
+ continue;
502
+ // Skip common non-variable patterns
503
+ if (varName === '_a' || varName === '_b' || varName === '_c')
504
+ continue;
505
+ // Skip 'this', 'self', 'super' (not reassignable in practice)
506
+ if (varName === 'this' || varName === 'super')
507
+ continue;
508
+ // Skip keywords that could look like identifiers
509
+ if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
510
+ 'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
511
+ 'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
512
+ 'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName))
513
+ continue;
514
+ // Check that this line doesn't start with const/let/var (would be a declaration, already handled)
515
+ const lineStart = source.lastIndexOf('\n', match.index) + 1;
516
+ const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
517
+ if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
518
+ continue;
519
+ // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
520
+ // or if it's a label (label: ...)
521
+ const beforeOnLine = source.slice(lineStart, match.index).trim();
522
+ if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
523
+ continue;
524
+ // Calculate line number
525
+ let lineNo = 1;
526
+ for (let i = 0; i < match.index; i++) {
527
+ if (source[i] === '\n')
528
+ lineNo++;
529
+ }
530
+ // Find end of statement (same logic as findVarDeclarations)
531
+ const startPos = match.index + match[0].length - 1;
532
+ let pos = startPos;
533
+ let depth = 0;
534
+ let foundEnd = -1;
535
+ while (pos < source.length) {
536
+ const ch = source[pos];
537
+ if (ch === '(' || ch === '[' || ch === '{') {
538
+ depth++;
539
+ }
540
+ else if (ch === ')' || ch === ']' || ch === '}') {
541
+ depth--;
542
+ if (depth < 0)
543
+ break;
544
+ }
545
+ else if (ch === ';' && depth === 0) {
546
+ foundEnd = pos;
547
+ break;
548
+ }
549
+ else if (ch === '\n' && depth === 0) {
550
+ const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
551
+ if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
552
+ foundEnd = pos;
553
+ break;
554
+ }
555
+ }
556
+ else if (ch === '"' || ch === "'" || ch === '`') {
557
+ const quote = ch;
558
+ pos++;
559
+ while (pos < source.length) {
560
+ if (source[pos] === '\\') {
561
+ pos++;
562
+ }
563
+ else if (source[pos] === quote)
564
+ break;
565
+ pos++;
566
+ }
567
+ }
568
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
569
+ while (pos < source.length && source[pos] !== '\n')
570
+ pos++;
571
+ continue;
572
+ }
573
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
574
+ pos += 2;
575
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
576
+ pos++;
577
+ pos++;
578
+ }
579
+ pos++;
580
+ }
581
+ if (foundEnd === -1)
582
+ continue;
583
+ results.push({ lineEnd: foundEnd + 1, varName, lineNo });
584
+ }
585
+ return results;
586
+ }
483
587
  /**
484
588
  * Find for-loop variable declarations and return insertions for tracing.
485
589
  * Handles:
@@ -490,6 +594,28 @@ function extractDestructuredNames(pattern) {
490
594
  * for (let i = 0; i < n; i++) { ... } → trace i
491
595
  * Inserts trace calls at the start of the loop body.
492
596
  */
597
+ /**
598
+ * Find catch clause variables and return insertions for tracing.
599
+ * Handles: catch (err) { ... } → trace err at start of catch body.
600
+ */
601
+ function findCatchVars(source) {
602
+ const results = [];
603
+ const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
604
+ let match;
605
+ while ((match = catchRegex.exec(source)) !== null) {
606
+ const varName = match[1];
607
+ if (varName.startsWith('__trickle'))
608
+ continue;
609
+ const bodyBrace = match.index + match[0].length - 1;
610
+ let lineNo = 1;
611
+ for (let i = 0; i < match.index; i++) {
612
+ if (source[i] === '\n')
613
+ lineNo++;
614
+ }
615
+ results.push({ bodyStart: bodyBrace + 1, varNames: [varName], lineNo });
616
+ }
617
+ return results;
618
+ }
493
619
  function findForLoopVars(source) {
494
620
  const results = [];
495
621
  // Match: for (const/let/var ...
@@ -1111,11 +1237,15 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
1111
1237
  const varInsertions = traceVars ? findVarDeclarations(source) : [];
1112
1238
  // Find destructured variable declarations for tracing
1113
1239
  const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
1240
+ // Find variable reassignments for tracing
1241
+ const reassignInsertions = traceVars ? findReassignments(source) : [];
1114
1242
  // Find for-loop variable declarations for tracing
1115
1243
  const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
1244
+ // Find catch clause variables for tracing
1245
+ const catchInsertions = traceVars ? findCatchVars(source) : [];
1116
1246
  // Find function parameter names for tracing
1117
1247
  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)
1248
+ if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
1119
1249
  return source;
1120
1250
  // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
1121
1251
  // Map transformed line numbers to original source line numbers.
@@ -1135,6 +1265,12 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
1135
1265
  di.lineNo = origLine;
1136
1266
  }
1137
1267
  }
1268
+ // Fix reassignment line numbers
1269
+ for (const ri of reassignInsertions) {
1270
+ const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
1271
+ if (origLine !== -1)
1272
+ ri.lineNo = origLine;
1273
+ }
1138
1274
  // Fix for-loop var line numbers
1139
1275
  for (const fi of forLoopInsertions) {
1140
1276
  if (fi.varNames.length > 0) {
@@ -1178,7 +1314,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
1178
1314
  }
1179
1315
  }
1180
1316
  // Build prefix — ALL imports first (ESM requires imports before any statements)
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;
1317
+ const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
1182
1318
  const importLines = [
1183
1319
  `import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
1184
1320
  ];
@@ -1216,7 +1352,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
1216
1352
  }
1217
1353
  }
1218
1354
  // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1219
- if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
1355
+ if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
1220
1356
  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) {} }`);
1221
1357
  }
1222
1358
  // Add React component render tracker if needed
@@ -1254,6 +1390,21 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
1254
1390
  code: `\n;try{${calls}}catch(__e){}\n`,
1255
1391
  });
1256
1392
  }
1393
+ // Reassignment insertions: trace after the reassignment statement
1394
+ for (const { lineEnd, varName, lineNo } of reassignInsertions) {
1395
+ allInsertions.push({
1396
+ position: lineEnd,
1397
+ code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
1398
+ });
1399
+ }
1400
+ // Catch clause insertions: insert trace at start of catch body
1401
+ for (const { bodyStart, varNames, lineNo } of catchInsertions) {
1402
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1403
+ allInsertions.push({
1404
+ position: bodyStart,
1405
+ code: `\ntry{${calls}}catch(__e2){}\n`,
1406
+ });
1407
+ }
1257
1408
  // For-loop variable insertions: insert trace at start of loop body
1258
1409
  for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
1259
1410
  const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
@@ -473,6 +473,110 @@ function extractDestructuredNames(pattern) {
473
473
  }
474
474
  return names;
475
475
  }
476
+ /**
477
+ * Find variable reassignments (not declarations) and return insertions for tracing.
478
+ * Handles: x = newValue; x += 1; x ||= fallback; etc.
479
+ * Only matches standalone reassignment statements at the start of a line.
480
+ * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
481
+ * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
482
+ */
483
+ function findReassignments(source) {
484
+ const results = [];
485
+ // Match: <identifier> <assignOp>= <value> at the start of a line
486
+ // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
487
+ // Plain: = (but not ==, ===, =>, !=)
488
+ const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
489
+ let match;
490
+ while ((match = reassignRegex.exec(source)) !== null) {
491
+ const varName = match[2];
492
+ // Skip trickle internals
493
+ if (varName.startsWith('__trickle') || varName.startsWith('_$'))
494
+ continue;
495
+ // Skip common non-variable patterns
496
+ if (varName === '_a' || varName === '_b' || varName === '_c')
497
+ continue;
498
+ // Skip 'this', 'self', 'super' (not reassignable in practice)
499
+ if (varName === 'this' || varName === 'super')
500
+ continue;
501
+ // Skip keywords that could look like identifiers
502
+ if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
503
+ 'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
504
+ 'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
505
+ 'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName))
506
+ continue;
507
+ // Check that this line doesn't start with const/let/var (would be a declaration, already handled)
508
+ const lineStart = source.lastIndexOf('\n', match.index) + 1;
509
+ const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
510
+ if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
511
+ continue;
512
+ // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
513
+ // or if it's a label (label: ...)
514
+ const beforeOnLine = source.slice(lineStart, match.index).trim();
515
+ if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(','))
516
+ continue;
517
+ // Calculate line number
518
+ let lineNo = 1;
519
+ for (let i = 0; i < match.index; i++) {
520
+ if (source[i] === '\n')
521
+ lineNo++;
522
+ }
523
+ // Find end of statement (same logic as findVarDeclarations)
524
+ const startPos = match.index + match[0].length - 1;
525
+ let pos = startPos;
526
+ let depth = 0;
527
+ let foundEnd = -1;
528
+ while (pos < source.length) {
529
+ const ch = source[pos];
530
+ if (ch === '(' || ch === '[' || ch === '{') {
531
+ depth++;
532
+ }
533
+ else if (ch === ')' || ch === ']' || ch === '}') {
534
+ depth--;
535
+ if (depth < 0)
536
+ break;
537
+ }
538
+ else if (ch === ';' && depth === 0) {
539
+ foundEnd = pos;
540
+ break;
541
+ }
542
+ else if (ch === '\n' && depth === 0) {
543
+ const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
544
+ if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
545
+ foundEnd = pos;
546
+ break;
547
+ }
548
+ }
549
+ else if (ch === '"' || ch === "'" || ch === '`') {
550
+ const quote = ch;
551
+ pos++;
552
+ while (pos < source.length) {
553
+ if (source[pos] === '\\') {
554
+ pos++;
555
+ }
556
+ else if (source[pos] === quote)
557
+ break;
558
+ pos++;
559
+ }
560
+ }
561
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
562
+ while (pos < source.length && source[pos] !== '\n')
563
+ pos++;
564
+ continue;
565
+ }
566
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
567
+ pos += 2;
568
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
569
+ pos++;
570
+ pos++;
571
+ }
572
+ pos++;
573
+ }
574
+ if (foundEnd === -1)
575
+ continue;
576
+ results.push({ lineEnd: foundEnd + 1, varName, lineNo });
577
+ }
578
+ return results;
579
+ }
476
580
  /**
477
581
  * Find for-loop variable declarations and return insertions for tracing.
478
582
  * Handles:
@@ -483,6 +587,28 @@ function extractDestructuredNames(pattern) {
483
587
  * for (let i = 0; i < n; i++) { ... } → trace i
484
588
  * Inserts trace calls at the start of the loop body.
485
589
  */
590
+ /**
591
+ * Find catch clause variables and return insertions for tracing.
592
+ * Handles: catch (err) { ... } → trace err at start of catch body.
593
+ */
594
+ function findCatchVars(source) {
595
+ const results = [];
596
+ const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
597
+ let match;
598
+ while ((match = catchRegex.exec(source)) !== null) {
599
+ const varName = match[1];
600
+ if (varName.startsWith('__trickle'))
601
+ continue;
602
+ const bodyBrace = match.index + match[0].length - 1;
603
+ let lineNo = 1;
604
+ for (let i = 0; i < match.index; i++) {
605
+ if (source[i] === '\n')
606
+ lineNo++;
607
+ }
608
+ results.push({ bodyStart: bodyBrace + 1, varNames: [varName], lineNo });
609
+ }
610
+ return results;
611
+ }
486
612
  function findForLoopVars(source) {
487
613
  const results = [];
488
614
  // Match: for (const/let/var ...
@@ -1104,11 +1230,15 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
1104
1230
  const varInsertions = traceVars ? findVarDeclarations(source) : [];
1105
1231
  // Find destructured variable declarations for tracing
1106
1232
  const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
1233
+ // Find variable reassignments for tracing
1234
+ const reassignInsertions = traceVars ? findReassignments(source) : [];
1107
1235
  // Find for-loop variable declarations for tracing
1108
1236
  const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
1237
+ // Find catch clause variables for tracing
1238
+ const catchInsertions = traceVars ? findCatchVars(source) : [];
1109
1239
  // Find function parameter names for tracing
1110
1240
  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)
1241
+ if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
1112
1242
  return source;
1113
1243
  // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
1114
1244
  // Map transformed line numbers to original source line numbers.
@@ -1128,6 +1258,12 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
1128
1258
  di.lineNo = origLine;
1129
1259
  }
1130
1260
  }
1261
+ // Fix reassignment line numbers
1262
+ for (const ri of reassignInsertions) {
1263
+ const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
1264
+ if (origLine !== -1)
1265
+ ri.lineNo = origLine;
1266
+ }
1131
1267
  // Fix for-loop var line numbers
1132
1268
  for (const fi of forLoopInsertions) {
1133
1269
  if (fi.varNames.length > 0) {
@@ -1171,7 +1307,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
1171
1307
  }
1172
1308
  }
1173
1309
  // 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;
1310
+ const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
1175
1311
  const importLines = [
1176
1312
  `import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
1177
1313
  ];
@@ -1209,7 +1345,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
1209
1345
  }
1210
1346
  }
1211
1347
  // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1212
- if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
1348
+ if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
1213
1349
  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) {} }`);
1214
1350
  }
1215
1351
  // Add React component render tracker if needed
@@ -1247,6 +1383,21 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
1247
1383
  code: `\n;try{${calls}}catch(__e){}\n`,
1248
1384
  });
1249
1385
  }
1386
+ // Reassignment insertions: trace after the reassignment statement
1387
+ for (const { lineEnd, varName, lineNo } of reassignInsertions) {
1388
+ allInsertions.push({
1389
+ position: lineEnd,
1390
+ code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
1391
+ });
1392
+ }
1393
+ // Catch clause insertions: insert trace at start of catch body
1394
+ for (const { bodyStart, varNames, lineNo } of catchInsertions) {
1395
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1396
+ allInsertions.push({
1397
+ position: bodyStart,
1398
+ code: `\ntry{${calls}}catch(__e2){}\n`,
1399
+ });
1400
+ }
1250
1401
  // For-loop variable insertions: insert trace at start of loop body
1251
1402
  for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
1252
1403
  const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.70",
3
+ "version": "0.2.72",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -461,6 +461,101 @@ function extractDestructuredNames(pattern: string): string[] {
461
461
  return names;
462
462
  }
463
463
 
464
+ /**
465
+ * Find variable reassignments (not declarations) and return insertions for tracing.
466
+ * Handles: x = newValue; x += 1; x ||= fallback; etc.
467
+ * Only matches standalone reassignment statements at the start of a line.
468
+ * Skips: property assignments (obj.x = ...), indexed (arr[i] = ...),
469
+ * comparisons (===, !==), arrow functions (=>), declarations (const/let/var).
470
+ */
471
+ function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
472
+ const results: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
473
+
474
+ // Match: <identifier> <assignOp>= <value> at the start of a line
475
+ // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
476
+ // Plain: = (but not ==, ===, =>, !=)
477
+ const reassignRegex = /^([ \t]*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\+|-|\*\*?|\/|%|&&|\|\||<<|>>>?|&|\||\^|\?\?)?=[^=>]/gm;
478
+ let match;
479
+
480
+ while ((match = reassignRegex.exec(source)) !== null) {
481
+ const varName = match[2];
482
+
483
+ // Skip trickle internals
484
+ if (varName.startsWith('__trickle') || varName.startsWith('_$')) continue;
485
+ // Skip common non-variable patterns
486
+ if (varName === '_a' || varName === '_b' || varName === '_c') continue;
487
+ // Skip 'this', 'self', 'super' (not reassignable in practice)
488
+ if (varName === 'this' || varName === 'super') continue;
489
+ // Skip keywords that could look like identifiers
490
+ if (['if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'return', 'throw',
491
+ 'break', 'continue', 'try', 'catch', 'finally', 'new', 'delete', 'typeof', 'void',
492
+ 'yield', 'await', 'class', 'extends', 'import', 'export', 'from', 'as', 'of', 'in',
493
+ 'const', 'let', 'var', 'function', 'true', 'false', 'null', 'undefined'].includes(varName)) continue;
494
+
495
+ // Check that this line doesn't start with const/let/var (would be a declaration, already handled)
496
+ const lineStart = source.lastIndexOf('\n', match.index) + 1;
497
+ const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
498
+ if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart())) continue;
499
+
500
+ // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
501
+ // or if it's a label (label: ...)
502
+ const beforeOnLine = source.slice(lineStart, match.index).trim();
503
+ if (beforeOnLine.endsWith(':') || beforeOnLine.endsWith(',')) continue;
504
+
505
+ // Calculate line number
506
+ let lineNo = 1;
507
+ for (let i = 0; i < match.index; i++) {
508
+ if (source[i] === '\n') lineNo++;
509
+ }
510
+
511
+ // Find end of statement (same logic as findVarDeclarations)
512
+ const startPos = match.index + match[0].length - 1;
513
+ let pos = startPos;
514
+ let depth = 0;
515
+ let foundEnd = -1;
516
+
517
+ while (pos < source.length) {
518
+ const ch = source[pos];
519
+ if (ch === '(' || ch === '[' || ch === '{') {
520
+ depth++;
521
+ } else if (ch === ')' || ch === ']' || ch === '}') {
522
+ depth--;
523
+ if (depth < 0) break;
524
+ } else if (ch === ';' && depth === 0) {
525
+ foundEnd = pos;
526
+ break;
527
+ } else if (ch === '\n' && depth === 0) {
528
+ const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
529
+ if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
530
+ foundEnd = pos;
531
+ break;
532
+ }
533
+ } else if (ch === '"' || ch === "'" || ch === '`') {
534
+ const quote = ch;
535
+ pos++;
536
+ while (pos < source.length) {
537
+ if (source[pos] === '\\') { pos++; }
538
+ else if (source[pos] === quote) break;
539
+ pos++;
540
+ }
541
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
542
+ while (pos < source.length && source[pos] !== '\n') pos++;
543
+ continue;
544
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
545
+ pos += 2;
546
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
547
+ pos++;
548
+ }
549
+ pos++;
550
+ }
551
+
552
+ if (foundEnd === -1) continue;
553
+ results.push({ lineEnd: foundEnd + 1, varName, lineNo });
554
+ }
555
+
556
+ return results;
557
+ }
558
+
464
559
  /**
465
560
  * Find for-loop variable declarations and return insertions for tracing.
466
561
  * Handles:
@@ -471,6 +566,31 @@ function extractDestructuredNames(pattern: string): string[] {
471
566
  * for (let i = 0; i < n; i++) { ... } → trace i
472
567
  * Inserts trace calls at the start of the loop body.
473
568
  */
569
+ /**
570
+ * Find catch clause variables and return insertions for tracing.
571
+ * Handles: catch (err) { ... } → trace err at start of catch body.
572
+ */
573
+ function findCatchVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
574
+ const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
575
+ const catchRegex = /\bcatch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^)]+?)?\s*\)\s*\{/g;
576
+ let match;
577
+
578
+ while ((match = catchRegex.exec(source)) !== null) {
579
+ const varName = match[1];
580
+ if (varName.startsWith('__trickle')) continue;
581
+
582
+ const bodyBrace = match.index + match[0].length - 1;
583
+ let lineNo = 1;
584
+ for (let i = 0; i < match.index; i++) {
585
+ if (source[i] === '\n') lineNo++;
586
+ }
587
+
588
+ results.push({ bodyStart: bodyBrace + 1, varNames: [varName], lineNo });
589
+ }
590
+
591
+ return results;
592
+ }
593
+
474
594
  function findForLoopVars(source: string): Array<{ bodyStart: number; varNames: string[]; lineNo: number }> {
475
595
  const results: Array<{ bodyStart: number; varNames: string[]; lineNo: number }> = [];
476
596
 
@@ -1108,13 +1228,19 @@ export function transformEsmSource(
1108
1228
  // Find destructured variable declarations for tracing
1109
1229
  const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
1110
1230
 
1231
+ // Find variable reassignments for tracing
1232
+ const reassignInsertions = traceVars ? findReassignments(source) : [];
1233
+
1111
1234
  // Find for-loop variable declarations for tracing
1112
1235
  const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
1113
1236
 
1237
+ // Find catch clause variables for tracing
1238
+ const catchInsertions = traceVars ? findCatchVars(source) : [];
1239
+
1114
1240
  // Find function parameter names for tracing
1115
1241
  const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
1116
1242
 
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;
1243
+ if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0) return source;
1118
1244
 
1119
1245
  // Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
1120
1246
  // Map transformed line numbers to original source line numbers.
@@ -1133,6 +1259,11 @@ export function transformEsmSource(
1133
1259
  if (origLine !== -1) di.lineNo = origLine;
1134
1260
  }
1135
1261
  }
1262
+ // Fix reassignment line numbers
1263
+ for (const ri of reassignInsertions) {
1264
+ const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
1265
+ if (origLine !== -1) ri.lineNo = origLine;
1266
+ }
1136
1267
  // Fix for-loop var line numbers
1137
1268
  for (const fi of forLoopInsertions) {
1138
1269
  if (fi.varNames.length > 0) {
@@ -1177,7 +1308,7 @@ export function transformEsmSource(
1177
1308
  }
1178
1309
 
1179
1310
  // Build prefix — ALL imports first (ESM requires imports before any statements)
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;
1311
+ const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
1181
1312
  const importLines: string[] = [
1182
1313
  `import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
1183
1314
  ];
@@ -1247,7 +1378,7 @@ export function transformEsmSource(
1247
1378
  }
1248
1379
 
1249
1380
  // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1250
- if (varInsertions.length > 0 || destructInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
1381
+ if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0) {
1251
1382
  prefixLines.push(
1252
1383
  `if (!globalThis.__trickle_var_tracer) {`,
1253
1384
  ` const _cache = new Set();`,
@@ -1418,6 +1549,23 @@ export function transformEsmSource(
1418
1549
  });
1419
1550
  }
1420
1551
 
1552
+ // Reassignment insertions: trace after the reassignment statement
1553
+ for (const { lineEnd, varName, lineNo } of reassignInsertions) {
1554
+ allInsertions.push({
1555
+ position: lineEnd,
1556
+ code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
1557
+ });
1558
+ }
1559
+
1560
+ // Catch clause insertions: insert trace at start of catch body
1561
+ for (const { bodyStart, varNames, lineNo } of catchInsertions) {
1562
+ const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
1563
+ allInsertions.push({
1564
+ position: bodyStart,
1565
+ code: `\ntry{${calls}}catch(__e2){}\n`,
1566
+ });
1567
+ }
1568
+
1421
1569
  // For-loop variable insertions: insert trace at start of loop body
1422
1570
  for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
1423
1571
  const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');