trickle-observe 0.2.2 → 0.2.4
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/auto-codegen.d.ts +1 -1
- package/dist/auto-codegen.js +234 -17
- package/dist/auto-register.js +1 -1
- package/dist/express.js +13 -0
- package/dist/observe-register.js +450 -41
- package/dist/trace-var.d.ts +44 -0
- package/dist/trace-var.js +219 -0
- package/dist/type-inference.js +9 -1
- package/dist/vite-plugin.d.ts +5 -2
- package/dist/vite-plugin.js +385 -25
- package/dist/wrap.js +4 -12
- package/package.json +10 -3
- package/src/auto-codegen.ts +226 -18
- package/src/auto-register.ts +1 -1
- package/src/express.d.ts +387 -0
- package/src/express.ts +14 -0
- package/src/observe-register.ts +420 -41
- package/src/trace-var.ts +202 -0
- package/src/type-inference.ts +11 -1
- package/src/vite-plugin.ts +444 -24
- package/src/wrap.ts +4 -12
package/src/vite-plugin.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Vite plugin for trickle observation.
|
|
3
3
|
*
|
|
4
4
|
* Integrates into Vite's (and Vitest's) transform pipeline to wrap
|
|
5
|
-
* user functions with trickle observation
|
|
6
|
-
* does for Node's Module._compile,
|
|
5
|
+
* user functions with trickle observation AND trace variable assignments —
|
|
6
|
+
* the same thing observe-register.ts does for Node's Module._compile,
|
|
7
|
+
* but for Vite/Vitest.
|
|
7
8
|
*
|
|
8
9
|
* Usage in vitest.config.ts:
|
|
9
10
|
*
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import path from 'path';
|
|
22
|
+
import fs from 'fs';
|
|
21
23
|
|
|
22
24
|
export interface TricklePluginOptions {
|
|
23
25
|
/** Substrings — only observe files whose paths contain one of these */
|
|
@@ -28,6 +30,8 @@ export interface TricklePluginOptions {
|
|
|
28
30
|
backendUrl?: string;
|
|
29
31
|
/** Enable debug logging */
|
|
30
32
|
debug?: boolean;
|
|
33
|
+
/** Disable variable tracing (default: enabled) */
|
|
34
|
+
traceVars?: boolean;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export function tricklePlugin(options: TricklePluginOptions = {}) {
|
|
@@ -44,6 +48,7 @@ export function tricklePlugin(options: TricklePluginOptions = {}) {
|
|
|
44
48
|
?? 'http://localhost:4888';
|
|
45
49
|
const debug = options.debug
|
|
46
50
|
?? (process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true');
|
|
51
|
+
const traceVars = options.traceVars ?? (process.env.TRICKLE_TRACE_VARS !== '0');
|
|
47
52
|
|
|
48
53
|
function shouldTransform(id: string): boolean {
|
|
49
54
|
// Only JS/TS files
|
|
@@ -76,8 +81,18 @@ export function tricklePlugin(options: TricklePluginOptions = {}) {
|
|
|
76
81
|
transform(code: string, id: string) {
|
|
77
82
|
if (!shouldTransform(id)) return null;
|
|
78
83
|
|
|
84
|
+
// Read the original source file to get accurate line numbers.
|
|
85
|
+
// Vite transforms the code before our plugin (enforce: 'post'),
|
|
86
|
+
// so line numbers from `code` don't match the original .ts file.
|
|
87
|
+
let originalSource: string | null = null;
|
|
88
|
+
try {
|
|
89
|
+
originalSource = fs.readFileSync(id, 'utf-8');
|
|
90
|
+
} catch {
|
|
91
|
+
// If we can't read the original, we'll use transformed line numbers
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
const moduleName = path.basename(id).replace(/\.[jt]sx?$/, '');
|
|
80
|
-
const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug);
|
|
95
|
+
const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug, traceVars, originalSource);
|
|
81
96
|
if (transformed === code) return null;
|
|
82
97
|
|
|
83
98
|
if (debug) {
|
|
@@ -133,22 +148,309 @@ function findClosingBrace(source: string, openBrace: number): number {
|
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
/**
|
|
136
|
-
*
|
|
151
|
+
* Find variable declarations in source and return insertions for tracing.
|
|
152
|
+
* Handles: const x = ...; let x = ...; var x = ...;
|
|
153
|
+
* Skips: destructuring, for-loop vars, require() calls, imports, type annotations.
|
|
154
|
+
*/
|
|
155
|
+
function findVarDeclarations(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
|
|
156
|
+
const varInsertions: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
|
|
157
|
+
|
|
158
|
+
// Match: const/let/var <identifier> = <something>
|
|
159
|
+
const varRegex = /^([ \t]*)(export\s+)?(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
|
|
160
|
+
let vmatch;
|
|
161
|
+
|
|
162
|
+
while ((vmatch = varRegex.exec(source)) !== null) {
|
|
163
|
+
const varName = vmatch[4];
|
|
164
|
+
|
|
165
|
+
// Skip trickle internals
|
|
166
|
+
if (varName.startsWith('__trickle')) continue;
|
|
167
|
+
// Skip TS compiled vars
|
|
168
|
+
if (varName === '_a' || varName === '_b' || varName === '_c') continue;
|
|
169
|
+
|
|
170
|
+
// Check if this is a require() call or import — skip those
|
|
171
|
+
const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
|
|
172
|
+
if (/^\s*require\s*\(/.test(restOfLine)) continue;
|
|
173
|
+
// Skip function/class assignments (those are handled by function wrapping)
|
|
174
|
+
if (/^\s*(?:async\s+)?(?:function\s|\([^)]*\)\s*(?::\s*[^=]+?)?\s*=>|\w+\s*=>)/.test(restOfLine)) continue;
|
|
175
|
+
|
|
176
|
+
// Calculate line number
|
|
177
|
+
let lineNo = 1;
|
|
178
|
+
for (let i = 0; i < vmatch.index; i++) {
|
|
179
|
+
if (source[i] === '\n') lineNo++;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Find the end of this statement
|
|
183
|
+
const startPos = vmatch.index + vmatch[0].length - 1;
|
|
184
|
+
let pos = startPos;
|
|
185
|
+
let depth = 0;
|
|
186
|
+
let foundEnd = -1;
|
|
187
|
+
|
|
188
|
+
while (pos < source.length) {
|
|
189
|
+
const ch = source[pos];
|
|
190
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
191
|
+
depth++;
|
|
192
|
+
} else if (ch === ')' || ch === ']' || ch === '}') {
|
|
193
|
+
depth--;
|
|
194
|
+
if (depth < 0) break;
|
|
195
|
+
} else if (ch === ';' && depth === 0) {
|
|
196
|
+
foundEnd = pos;
|
|
197
|
+
break;
|
|
198
|
+
} else if (ch === '\n' && depth === 0) {
|
|
199
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
200
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
201
|
+
foundEnd = pos;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
205
|
+
const quote = ch;
|
|
206
|
+
pos++;
|
|
207
|
+
while (pos < source.length) {
|
|
208
|
+
if (source[pos] === '\\') { pos++; }
|
|
209
|
+
else if (source[pos] === quote) break;
|
|
210
|
+
pos++;
|
|
211
|
+
}
|
|
212
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
213
|
+
while (pos < source.length && source[pos] !== '\n') pos++;
|
|
214
|
+
continue;
|
|
215
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
216
|
+
pos += 2;
|
|
217
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
|
|
218
|
+
pos++;
|
|
219
|
+
}
|
|
220
|
+
pos++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (foundEnd === -1) continue;
|
|
224
|
+
|
|
225
|
+
varInsertions.push({ lineEnd: foundEnd + 1, varName, lineNo });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return varInsertions;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Find destructured variable declarations: const { a, b } = ... and const [a, b] = ...
|
|
233
|
+
* Extracts the individual variable names from the destructuring pattern.
|
|
234
|
+
*/
|
|
235
|
+
function findDestructuredDeclarations(source: string): Array<{ lineEnd: number; varNames: string[]; lineNo: number }> {
|
|
236
|
+
const results: Array<{ lineEnd: number; varNames: string[]; lineNo: number }> = [];
|
|
237
|
+
|
|
238
|
+
// Match: const/let/var { ... } = ... or const/let/var [ ... ] = ...
|
|
239
|
+
const destructRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
|
|
240
|
+
let match;
|
|
241
|
+
|
|
242
|
+
while ((match = destructRegex.exec(source)) !== null) {
|
|
243
|
+
const pattern = match[1];
|
|
244
|
+
|
|
245
|
+
// Extract variable names from the destructuring pattern
|
|
246
|
+
const varNames = extractDestructuredNames(pattern);
|
|
247
|
+
if (varNames.length === 0) continue;
|
|
248
|
+
|
|
249
|
+
// Skip if it's a require() call
|
|
250
|
+
const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
|
|
251
|
+
if (/^\s*require\s*\(/.test(restOfLine)) continue;
|
|
252
|
+
|
|
253
|
+
// Calculate line number
|
|
254
|
+
let lineNo = 1;
|
|
255
|
+
for (let i = 0; i < match.index; i++) {
|
|
256
|
+
if (source[i] === '\n') lineNo++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Find the end of this statement (same logic as findVarDeclarations)
|
|
260
|
+
const startPos = match.index + match[0].length - 1;
|
|
261
|
+
let pos = startPos;
|
|
262
|
+
let depth = 0;
|
|
263
|
+
let foundEnd = -1;
|
|
264
|
+
|
|
265
|
+
while (pos < source.length) {
|
|
266
|
+
const ch = source[pos];
|
|
267
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
268
|
+
depth++;
|
|
269
|
+
} else if (ch === ')' || ch === ']' || ch === '}') {
|
|
270
|
+
depth--;
|
|
271
|
+
if (depth < 0) break;
|
|
272
|
+
} else if (ch === ';' && depth === 0) {
|
|
273
|
+
foundEnd = pos;
|
|
274
|
+
break;
|
|
275
|
+
} else if (ch === '\n' && depth === 0) {
|
|
276
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
277
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
278
|
+
foundEnd = pos;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
282
|
+
const quote = ch;
|
|
283
|
+
pos++;
|
|
284
|
+
while (pos < source.length) {
|
|
285
|
+
if (source[pos] === '\\') { pos++; }
|
|
286
|
+
else if (source[pos] === quote) break;
|
|
287
|
+
pos++;
|
|
288
|
+
}
|
|
289
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
290
|
+
while (pos < source.length && source[pos] !== '\n') pos++;
|
|
291
|
+
continue;
|
|
292
|
+
} else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
293
|
+
pos += 2;
|
|
294
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
|
|
295
|
+
pos++;
|
|
296
|
+
}
|
|
297
|
+
pos++;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (foundEnd === -1) continue;
|
|
301
|
+
results.push({ lineEnd: foundEnd + 1, varNames, lineNo });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return results;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Extract variable names from a destructuring pattern.
|
|
309
|
+
* Handles: { a, b, c: d } → ['a', 'b', 'd'] (renamed vars use the local name)
|
|
310
|
+
* Handles: [a, b, ...rest] → ['a', 'b', 'rest']
|
|
311
|
+
* Handles: { a: { b, c } } → ['b', 'c'] (nested destructuring)
|
|
312
|
+
*/
|
|
313
|
+
function extractDestructuredNames(pattern: string): string[] {
|
|
314
|
+
const names: string[] = [];
|
|
315
|
+
// Remove outer braces/brackets
|
|
316
|
+
const inner = pattern.slice(1, -1).trim();
|
|
317
|
+
if (!inner) return names;
|
|
318
|
+
|
|
319
|
+
// Split by commas at depth 0
|
|
320
|
+
const parts: string[] = [];
|
|
321
|
+
let depth = 0;
|
|
322
|
+
let current = '';
|
|
323
|
+
for (const ch of inner) {
|
|
324
|
+
if (ch === '{' || ch === '[') depth++;
|
|
325
|
+
else if (ch === '}' || ch === ']') depth--;
|
|
326
|
+
else if (ch === ',' && depth === 0) {
|
|
327
|
+
parts.push(current.trim());
|
|
328
|
+
current = '';
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
current += ch;
|
|
332
|
+
}
|
|
333
|
+
if (current.trim()) parts.push(current.trim());
|
|
334
|
+
|
|
335
|
+
for (let part of parts) {
|
|
336
|
+
// Remove type annotations: `a: Type` vs `a: b` (rename)
|
|
337
|
+
// Skip rest elements for now: ...rest → rest
|
|
338
|
+
if (part.startsWith('...')) {
|
|
339
|
+
const restName = part.slice(3).trim().split(/[\s:]/)[0];
|
|
340
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName)) {
|
|
341
|
+
names.push(restName);
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check for rename pattern: key: localName or key: { nested }
|
|
347
|
+
const colonIdx = part.indexOf(':');
|
|
348
|
+
if (colonIdx !== -1) {
|
|
349
|
+
const afterColon = part.slice(colonIdx + 1).trim();
|
|
350
|
+
// Nested destructuring: key: { a, b } or key: [a, b]
|
|
351
|
+
if (afterColon.startsWith('{') || afterColon.startsWith('[')) {
|
|
352
|
+
const nestedNames = extractDestructuredNames(afterColon);
|
|
353
|
+
names.push(...nestedNames);
|
|
354
|
+
} else {
|
|
355
|
+
// Rename: key: localName — extract localName (skip if it has another colon for type annotation)
|
|
356
|
+
const localName = afterColon.split(/[\s=]/)[0].trim();
|
|
357
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(localName)) {
|
|
358
|
+
names.push(localName);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Simple: just the identifier (possibly with default: `a = defaultVal`)
|
|
363
|
+
const name = part.split(/[\s=]/)[0].trim();
|
|
364
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) {
|
|
365
|
+
names.push(name);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return names;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Transform ESM source code to wrap function declarations and trace variables.
|
|
137
375
|
*
|
|
138
|
-
* Prepends
|
|
139
|
-
* each function declaration body
|
|
140
|
-
|
|
376
|
+
* Prepends imports of the wrap/trace helpers, then inserts wrapper calls after
|
|
377
|
+
* each function declaration body and trace calls after variable declarations.
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* Find the original line number for a simple variable declaration.
|
|
381
|
+
* Searches the original source lines for `const/let/var <varName>` near the expected position.
|
|
382
|
+
* Vite transforms typically remove lines (types, imports), so the original line is usually
|
|
383
|
+
* >= the transformed line. We search forward-biased (up to +80) but also a bit backward (-10).
|
|
384
|
+
*/
|
|
385
|
+
function findOriginalLine(origLines: string[], varName: string, transformedLine: number): number {
|
|
386
|
+
const pattern = new RegExp(`\\b(const|let|var)\\s+${escapeRegexStr(varName)}\\b`);
|
|
387
|
+
|
|
388
|
+
// Search: first try exact, then expand forward (more likely) and a bit backward
|
|
389
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
390
|
+
// Forward first (original line is usually after transformed line due to removed TS types)
|
|
391
|
+
const fwd = transformedLine - 1 + delta;
|
|
392
|
+
if (fwd >= 0 && fwd < origLines.length && pattern.test(origLines[fwd])) {
|
|
393
|
+
return fwd + 1;
|
|
394
|
+
}
|
|
395
|
+
// Also check backward (small range)
|
|
396
|
+
if (delta > 0 && delta <= 10) {
|
|
397
|
+
const bwd = transformedLine - 1 - delta;
|
|
398
|
+
if (bwd >= 0 && bwd < origLines.length && pattern.test(origLines[bwd])) {
|
|
399
|
+
return bwd + 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return -1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Find the original line number for a destructured declaration.
|
|
408
|
+
* Searches for const/let/var { or [ patterns containing at least one of the variable names.
|
|
141
409
|
*/
|
|
410
|
+
function findOriginalLineDestructured(origLines: string[], varNames: string[], transformedLine: number): number {
|
|
411
|
+
// Match names as actual bindings (not renamed property keys).
|
|
412
|
+
// In `{ data: customer }`, 'data' is a key (followed by ':'), 'customer' is the binding.
|
|
413
|
+
// In `{ data, error }`, 'data' is a binding (followed by ',' or '}').
|
|
414
|
+
// We check: name followed by comma, }, ], =, whitespace, or end — NOT followed by ':' (rename).
|
|
415
|
+
const namePatterns = varNames.map(n => new RegExp(`\\b${escapeRegexStr(n)}\\b(?!\\s*:)`));
|
|
416
|
+
|
|
417
|
+
for (let delta = 0; delta <= 80; delta++) {
|
|
418
|
+
const fwd = transformedLine - 1 + delta;
|
|
419
|
+
if (fwd >= 0 && fwd < origLines.length) {
|
|
420
|
+
const line = origLines[fwd];
|
|
421
|
+
if (/\b(const|let|var)\s+[\[{]/.test(line) && namePatterns.some(p => p.test(line))) {
|
|
422
|
+
return fwd + 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (delta > 0 && delta <= 10) {
|
|
426
|
+
const bwd = transformedLine - 1 - delta;
|
|
427
|
+
if (bwd >= 0 && bwd < origLines.length) {
|
|
428
|
+
const line = origLines[bwd];
|
|
429
|
+
if (/\b(const|let|var)\s+[\[{]/.test(line) && namePatterns.some(p => p.test(line))) {
|
|
430
|
+
return bwd + 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return -1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function escapeRegexStr(str: string): string {
|
|
439
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
440
|
+
}
|
|
441
|
+
|
|
142
442
|
function transformEsmSource(
|
|
143
443
|
source: string,
|
|
144
444
|
filename: string,
|
|
145
445
|
moduleName: string,
|
|
146
446
|
backendUrl: string,
|
|
147
447
|
debug: boolean,
|
|
448
|
+
traceVars: boolean,
|
|
449
|
+
originalSource?: string | null,
|
|
148
450
|
): string {
|
|
149
451
|
// Match top-level and nested function declarations (including async, export)
|
|
150
452
|
const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
151
|
-
const
|
|
453
|
+
const funcInsertions: Array<{ position: number; name: string; paramNames: string[] }> = [];
|
|
152
454
|
let match;
|
|
153
455
|
|
|
154
456
|
while ((match = funcRegex.exec(source)) !== null) {
|
|
@@ -172,7 +474,7 @@ function transformEsmSource(
|
|
|
172
474
|
const closeBrace = findClosingBrace(source, openBrace);
|
|
173
475
|
if (closeBrace === -1) continue;
|
|
174
476
|
|
|
175
|
-
|
|
477
|
+
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
176
478
|
}
|
|
177
479
|
|
|
178
480
|
// Also match arrow functions assigned to const/let/var
|
|
@@ -183,7 +485,6 @@ function transformEsmSource(
|
|
|
183
485
|
const openBrace = source.indexOf('{', match.index + match[0].length - 1);
|
|
184
486
|
if (openBrace === -1) continue;
|
|
185
487
|
|
|
186
|
-
// Extract param names from the arrow function
|
|
187
488
|
const arrowStr = match[0];
|
|
188
489
|
const arrowParamMatch = arrowStr.match(/=\s*(?:async\s+)?(?:\(([^)]*)\)|([a-zA-Z_$][a-zA-Z0-9_$]*))\s*(?::\s*[^=]+?)?\s*=>/);
|
|
189
490
|
let paramNames: string[] = [];
|
|
@@ -201,15 +502,49 @@ function transformEsmSource(
|
|
|
201
502
|
const closeBrace = findClosingBrace(source, openBrace);
|
|
202
503
|
if (closeBrace === -1) continue;
|
|
203
504
|
|
|
204
|
-
|
|
505
|
+
funcInsertions.push({ position: closeBrace + 1, name, paramNames });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Find variable declarations for tracing
|
|
509
|
+
const varInsertions = traceVars ? findVarDeclarations(source) : [];
|
|
510
|
+
|
|
511
|
+
// Find destructured variable declarations for tracing
|
|
512
|
+
const destructInsertions = traceVars ? findDestructuredDeclarations(source) : [];
|
|
513
|
+
|
|
514
|
+
if (funcInsertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0) return source;
|
|
515
|
+
|
|
516
|
+
// Fix line numbers: Vite transforms (TypeScript stripping) may change line numbers.
|
|
517
|
+
// Map transformed line numbers to original source line numbers.
|
|
518
|
+
if (originalSource && originalSource !== source) {
|
|
519
|
+
const origLines = originalSource.split('\n');
|
|
520
|
+
|
|
521
|
+
// For each variable insertion, find the declaration in the original source
|
|
522
|
+
for (const vi of varInsertions) {
|
|
523
|
+
const origLine = findOriginalLine(origLines, vi.varName, vi.lineNo);
|
|
524
|
+
if (origLine !== -1) vi.lineNo = origLine;
|
|
525
|
+
}
|
|
526
|
+
for (const di of destructInsertions) {
|
|
527
|
+
// Use the first variable name to locate the line
|
|
528
|
+
if (di.varNames.length > 0) {
|
|
529
|
+
const origLine = findOriginalLineDestructured(origLines, di.varNames, di.lineNo);
|
|
530
|
+
if (origLine !== -1) di.lineNo = origLine;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
205
533
|
}
|
|
206
534
|
|
|
207
|
-
|
|
535
|
+
// Build prefix — ALL imports first (ESM requires imports before any statements)
|
|
536
|
+
const importLines: string[] = [
|
|
537
|
+
`import { wrapFunction as __trickle_wrapFn, configure as __trickle_configure } from 'trickle-observe';`,
|
|
538
|
+
];
|
|
539
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
540
|
+
importLines.push(
|
|
541
|
+
`import { mkdirSync as __trickle_mkdirSync, appendFileSync as __trickle_appendFileSync } from 'node:fs';`,
|
|
542
|
+
`import { join as __trickle_join } from 'node:path';`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
208
545
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`import { wrapFunction as __trickle_wrapFn } from 'trickle-observe';`,
|
|
212
|
-
`import { configure as __trickle_configure } from 'trickle-observe';`,
|
|
546
|
+
const prefixLines = [
|
|
547
|
+
...importLines,
|
|
213
548
|
`__trickle_configure({ backendUrl: ${JSON.stringify(backendUrl)}, batchIntervalMs: 2000, debug: ${debug}, enabled: true, environment: 'node' });`,
|
|
214
549
|
`function __trickle_wrap(fn, name, paramNames) {`,
|
|
215
550
|
` const opts = {`,
|
|
@@ -225,16 +560,101 @@ function transformEsmSource(
|
|
|
225
560
|
` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
|
|
226
561
|
` return __trickle_wrapFn(fn, opts);`,
|
|
227
562
|
`}`,
|
|
228
|
-
|
|
229
|
-
].join('\n');
|
|
563
|
+
];
|
|
230
564
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
565
|
+
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
566
|
+
// Uses synchronous writes (appendFileSync) to guarantee data persists even if Vitest
|
|
567
|
+
// kills the worker abruptly without firing exit events.
|
|
568
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
569
|
+
prefixLines.push(
|
|
570
|
+
`if (!globalThis.__trickle_var_tracer) {`,
|
|
571
|
+
` const _cache = new Set();`,
|
|
572
|
+
` let _varsFile = null;`,
|
|
573
|
+
` function _inferType(v, d) {`,
|
|
574
|
+
` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`,
|
|
575
|
+
` if (v === null) return { kind: 'primitive', name: 'null' };`,
|
|
576
|
+
` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`,
|
|
577
|
+
` const t = typeof v;`,
|
|
578
|
+
` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`,
|
|
579
|
+
` if (t === 'function') return { kind: 'function' };`,
|
|
580
|
+
` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`,
|
|
581
|
+
` if (t === 'object') {`,
|
|
582
|
+
` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`,
|
|
583
|
+
` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`,
|
|
584
|
+
` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`,
|
|
585
|
+
` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`,
|
|
586
|
+
` const props = {}; const keys = Object.keys(v).slice(0, 20);`,
|
|
587
|
+
` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`,
|
|
588
|
+
` return { kind: 'object', properties: props };`,
|
|
589
|
+
` }`,
|
|
590
|
+
` return { kind: 'primitive', name: 'unknown' };`,
|
|
591
|
+
` }`,
|
|
592
|
+
` function _sanitize(v, d) {`,
|
|
593
|
+
` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`,
|
|
594
|
+
` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`,
|
|
595
|
+
` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`,
|
|
596
|
+
` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`,
|
|
597
|
+
` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`,
|
|
598
|
+
` 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]';`,
|
|
599
|
+
` 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; }`,
|
|
600
|
+
` return String(v);`,
|
|
601
|
+
` }`,
|
|
602
|
+
` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`,
|
|
603
|
+
` try {`,
|
|
604
|
+
` if (!_varsFile) {`,
|
|
605
|
+
` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`,
|
|
606
|
+
` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`,
|
|
607
|
+
` _varsFile = __trickle_join(dir, 'variables.jsonl');`,
|
|
608
|
+
` }`,
|
|
609
|
+
` const type = _inferType(v, 3);`,
|
|
610
|
+
` const th = JSON.stringify(type).substring(0, 32);`,
|
|
611
|
+
` const ck = file + ':' + l + ':' + n + ':' + th;`,
|
|
612
|
+
` if (_cache.has(ck)) return;`,
|
|
613
|
+
` _cache.add(ck);`,
|
|
614
|
+
` __trickle_appendFileSync(_varsFile, JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: _sanitize(v, 2) }) + '\\n');`,
|
|
615
|
+
` } catch(e) {}`,
|
|
616
|
+
` };`,
|
|
617
|
+
`}`,
|
|
618
|
+
`function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
prefixLines.push('');
|
|
623
|
+
const prefix = prefixLines.join('\n');
|
|
624
|
+
|
|
625
|
+
// Merge all insertions and sort by position descending
|
|
626
|
+
type Insertion = { position: number; code: string };
|
|
627
|
+
const allInsertions: Insertion[] = [];
|
|
628
|
+
|
|
629
|
+
for (const { position, name, paramNames } of funcInsertions) {
|
|
235
630
|
const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
|
|
236
|
-
|
|
237
|
-
|
|
631
|
+
allInsertions.push({
|
|
632
|
+
position,
|
|
633
|
+
code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for (const { lineEnd, varName, lineNo } of varInsertions) {
|
|
638
|
+
allInsertions.push({
|
|
639
|
+
position: lineEnd,
|
|
640
|
+
code: `\n;try{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
645
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
646
|
+
allInsertions.push({
|
|
647
|
+
position: lineEnd,
|
|
648
|
+
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Sort by position descending (insert from end to preserve earlier positions)
|
|
653
|
+
allInsertions.sort((a, b) => b.position - a.position);
|
|
654
|
+
|
|
655
|
+
let result = source;
|
|
656
|
+
for (const { position, code } of allInsertions) {
|
|
657
|
+
result = result.slice(0, position) + code + result.slice(position);
|
|
238
658
|
}
|
|
239
659
|
|
|
240
660
|
return prefix + result;
|
package/src/wrap.ts
CHANGED
|
@@ -30,23 +30,15 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
30
30
|
return fn.apply(this, args);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Set up arg tracking
|
|
34
|
-
const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
|
|
35
|
-
const proxiedArgs = args.map((arg, i) => {
|
|
36
|
-
if (arg !== null && arg !== undefined && typeof arg === 'object') {
|
|
37
|
-
const tracker = createTracker(arg);
|
|
38
|
-
trackers.push(tracker);
|
|
39
|
-
return tracker.proxy;
|
|
40
|
-
}
|
|
41
|
-
return arg;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
33
|
let result: any;
|
|
45
34
|
let threwError = false;
|
|
46
35
|
let caughtError: unknown;
|
|
36
|
+
const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
|
|
47
37
|
|
|
48
38
|
try {
|
|
49
|
-
|
|
39
|
+
// Always pass ORIGINAL args to the function — never proxied ones.
|
|
40
|
+
// Proxied args can break framework internals (Express Router, DI containers, etc.)
|
|
41
|
+
result = fn.apply(this, args);
|
|
50
42
|
} catch (err) {
|
|
51
43
|
threwError = true;
|
|
52
44
|
caughtError = err;
|