trickle-observe 0.2.70 → 0.2.71
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 +122 -3
- package/dist-esm/vite-plugin.js +122 -3
- package/package.json +1 -1
- package/src/vite-plugin.ts +114 -3
package/dist/vite-plugin.js
CHANGED
|
@@ -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:
|
|
@@ -1111,11 +1215,13 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1111
1215
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
1112
1216
|
// Find destructured variable declarations for tracing
|
|
1113
1217
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
1218
|
+
// Find variable reassignments for tracing
|
|
1219
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1114
1220
|
// Find for-loop variable declarations for tracing
|
|
1115
1221
|
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1116
1222
|
// Find function parameter names for tracing
|
|
1117
1223
|
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)
|
|
1224
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1119
1225
|
return source;
|
|
1120
1226
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1121
1227
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1135,6 +1241,12 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1135
1241
|
di.lineNo = origLine;
|
|
1136
1242
|
}
|
|
1137
1243
|
}
|
|
1244
|
+
// Fix reassignment line numbers
|
|
1245
|
+
for (const ri of reassignInsertions) {
|
|
1246
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1247
|
+
if (origLine !== -1)
|
|
1248
|
+
ri.lineNo = origLine;
|
|
1249
|
+
}
|
|
1138
1250
|
// Fix for-loop var line numbers
|
|
1139
1251
|
for (const fi of forLoopInsertions) {
|
|
1140
1252
|
if (fi.varNames.length > 0) {
|
|
@@ -1178,7 +1290,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1178
1290
|
}
|
|
1179
1291
|
}
|
|
1180
1292
|
// 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;
|
|
1293
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1182
1294
|
const importLines = [
|
|
1183
1295
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
1184
1296
|
];
|
|
@@ -1216,7 +1328,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1216
1328
|
}
|
|
1217
1329
|
}
|
|
1218
1330
|
// 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) {
|
|
1331
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1220
1332
|
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
1333
|
}
|
|
1222
1334
|
// Add React component render tracker if needed
|
|
@@ -1254,6 +1366,13 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1254
1366
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
1255
1367
|
});
|
|
1256
1368
|
}
|
|
1369
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1370
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1371
|
+
allInsertions.push({
|
|
1372
|
+
position: lineEnd,
|
|
1373
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1257
1376
|
// For-loop variable insertions: insert trace at start of loop body
|
|
1258
1377
|
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1259
1378
|
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -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:
|
|
@@ -1104,11 +1208,13 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1104
1208
|
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
1105
1209
|
// Find destructured variable declarations for tracing
|
|
1106
1210
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
1211
|
+
// Find variable reassignments for tracing
|
|
1212
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1107
1213
|
// Find for-loop variable declarations for tracing
|
|
1108
1214
|
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1109
1215
|
// Find function parameter names for tracing
|
|
1110
1216
|
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)
|
|
1217
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0)
|
|
1112
1218
|
return source;
|
|
1113
1219
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1114
1220
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1128,6 +1234,12 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1128
1234
|
di.lineNo = origLine;
|
|
1129
1235
|
}
|
|
1130
1236
|
}
|
|
1237
|
+
// Fix reassignment line numbers
|
|
1238
|
+
for (const ri of reassignInsertions) {
|
|
1239
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1240
|
+
if (origLine !== -1)
|
|
1241
|
+
ri.lineNo = origLine;
|
|
1242
|
+
}
|
|
1131
1243
|
// Fix for-loop var line numbers
|
|
1132
1244
|
for (const fi of forLoopInsertions) {
|
|
1133
1245
|
if (fi.varNames.length > 0) {
|
|
@@ -1171,7 +1283,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1171
1283
|
}
|
|
1172
1284
|
}
|
|
1173
1285
|
// 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;
|
|
1286
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1175
1287
|
const importLines = [
|
|
1176
1288
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
1177
1289
|
];
|
|
@@ -1209,7 +1321,7 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1209
1321
|
}
|
|
1210
1322
|
}
|
|
1211
1323
|
// 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) {
|
|
1324
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1213
1325
|
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
1326
|
}
|
|
1215
1327
|
// Add React component render tracker if needed
|
|
@@ -1247,6 +1359,13 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1247
1359
|
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
1248
1360
|
});
|
|
1249
1361
|
}
|
|
1362
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1363
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1364
|
+
allInsertions.push({
|
|
1365
|
+
position: lineEnd,
|
|
1366
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1250
1369
|
// For-loop variable insertions: insert trace at start of loop body
|
|
1251
1370
|
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1252
1371
|
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
package/package.json
CHANGED
package/src/vite-plugin.ts
CHANGED
|
@@ -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:
|
|
@@ -1108,13 +1203,16 @@ export function transformEsmSource(
|
|
|
1108
1203
|
// Find destructured variable declarations for tracing
|
|
1109
1204
|
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
1110
1205
|
|
|
1206
|
+
// Find variable reassignments for tracing
|
|
1207
|
+
const reassignInsertions = traceVars ? findReassignments(source) : [];
|
|
1208
|
+
|
|
1111
1209
|
// Find for-loop variable declarations for tracing
|
|
1112
1210
|
const forLoopInsertions = traceVars ? findForLoopVars(source) : [];
|
|
1113
1211
|
|
|
1114
1212
|
// Find function parameter names for tracing
|
|
1115
1213
|
const funcParamInsertions = traceVars ? findFunctionParams(source, isReactFile) : [];
|
|
1116
1214
|
|
|
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;
|
|
1215
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && funcParamInsertions.length === 0 && bodyInsertions.length === 0 && hookInsertions.length === 0 && stateInsertions.length === 0 && conciseBodyInsertions.length === 0) return source;
|
|
1118
1216
|
|
|
1119
1217
|
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
1120
1218
|
// Map transformed line numbers to original source line numbers.
|
|
@@ -1133,6 +1231,11 @@ export function transformEsmSource(
|
|
|
1133
1231
|
if (origLine !== -1) di.lineNo = origLine;
|
|
1134
1232
|
}
|
|
1135
1233
|
}
|
|
1234
|
+
// Fix reassignment line numbers
|
|
1235
|
+
for (const ri of reassignInsertions) {
|
|
1236
|
+
const origLine = findOriginalLine(origLines, ri.varName, ri.lineNo);
|
|
1237
|
+
if (origLine !== -1) ri.lineNo = origLine;
|
|
1238
|
+
}
|
|
1136
1239
|
// Fix for-loop var line numbers
|
|
1137
1240
|
for (const fi of forLoopInsertions) {
|
|
1138
1241
|
if (fi.varNames.length > 0) {
|
|
@@ -1177,7 +1280,7 @@ export function transformEsmSource(
|
|
|
1177
1280
|
}
|
|
1178
1281
|
|
|
1179
1282
|
// 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;
|
|
1283
|
+
const needsTracing = varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0 || bodyInsertions.length > 0 || hookInsertions.length > 0 || stateInsertions.length > 0 || conciseBodyInsertions.length > 0;
|
|
1181
1284
|
const importLines: string[] = [
|
|
1182
1285
|
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
1183
1286
|
];
|
|
@@ -1247,7 +1350,7 @@ export function transformEsmSource(
|
|
|
1247
1350
|
}
|
|
1248
1351
|
|
|
1249
1352
|
// 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) {
|
|
1353
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || funcParamInsertions.length > 0) {
|
|
1251
1354
|
prefixLines.push(
|
|
1252
1355
|
`if (!globalThis.__trickle_var_tracer) {`,
|
|
1253
1356
|
` const _cache = new Set();`,
|
|
@@ -1418,6 +1521,14 @@ export function transformEsmSource(
|
|
|
1418
1521
|
});
|
|
1419
1522
|
}
|
|
1420
1523
|
|
|
1524
|
+
// Reassignment insertions: trace after the reassignment statement
|
|
1525
|
+
for (const { lineEnd, varName, lineNo } of reassignInsertions) {
|
|
1526
|
+
allInsertions.push({
|
|
1527
|
+
position: lineEnd,
|
|
1528
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1421
1532
|
// For-loop variable insertions: insert trace at start of loop body
|
|
1422
1533
|
for (const { bodyStart, varNames, lineNo } of forLoopInsertions) {
|
|
1423
1534
|
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|