trickle-observe 0.2.1 → 0.2.3
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/transport.js +5 -1
- 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/transport.ts +4 -1
- package/src/type-inference.ts +11 -1
- package/src/vite-plugin.ts +444 -24
- package/src/wrap.ts +4 -12
package/dist/observe-register.js
CHANGED
|
@@ -37,6 +37,8 @@ const transport_1 = require("./transport");
|
|
|
37
37
|
const env_detect_1 = require("./env-detect");
|
|
38
38
|
const wrap_1 = require("./wrap");
|
|
39
39
|
const fetch_observer_1 = require("./fetch-observer");
|
|
40
|
+
const express_1 = require("./express");
|
|
41
|
+
const trace_var_1 = require("./trace-var");
|
|
40
42
|
const M = module_1.default;
|
|
41
43
|
const originalLoad = M._load;
|
|
42
44
|
const originalCompile = M.prototype._compile;
|
|
@@ -159,6 +161,228 @@ function findClosingBrace(source, openBrace) {
|
|
|
159
161
|
* which is critical for entry files where functions are defined and used
|
|
160
162
|
* in the same top-level scope.
|
|
161
163
|
*/
|
|
164
|
+
/**
|
|
165
|
+
* Find variable declarations in source and return insertions for tracing.
|
|
166
|
+
* Handles: const x = ...; let x = ...; var x = ...;
|
|
167
|
+
* Skips: destructuring, for-loop vars, require() calls, imports.
|
|
168
|
+
*/
|
|
169
|
+
function findVarDeclarations(source) {
|
|
170
|
+
const varInsertions = [];
|
|
171
|
+
// Match: const/let/var <identifier> = <something>
|
|
172
|
+
// We look for lines containing a simple variable declaration with an assignment
|
|
173
|
+
const varRegex = /^([ \t]*)(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=[^=]/gm;
|
|
174
|
+
let vmatch;
|
|
175
|
+
while ((vmatch = varRegex.exec(source)) !== null) {
|
|
176
|
+
const varName = vmatch[3];
|
|
177
|
+
// Skip common noise
|
|
178
|
+
if (varName === '__trickle_mod' || varName === '__trickle_wrap' || varName === '__trickle_tv')
|
|
179
|
+
continue;
|
|
180
|
+
if (varName.startsWith('__trickle'))
|
|
181
|
+
continue;
|
|
182
|
+
if (varName === '_a' || varName === '_b' || varName === '_c')
|
|
183
|
+
continue; // TS compiled vars
|
|
184
|
+
// Check if this is a require() call — skip those (they're imports, not interesting values)
|
|
185
|
+
const restOfLine = source.slice(vmatch.index + vmatch[0].length - 1, vmatch.index + vmatch[0].length + 200);
|
|
186
|
+
if (/^\s*require\s*\(/.test(restOfLine))
|
|
187
|
+
continue;
|
|
188
|
+
// Calculate line number (count newlines before this position)
|
|
189
|
+
let lineNo = 1;
|
|
190
|
+
for (let i = 0; i < vmatch.index; i++) {
|
|
191
|
+
if (source[i] === '\n')
|
|
192
|
+
lineNo++;
|
|
193
|
+
}
|
|
194
|
+
// Find the end of this statement — look for the semicolon at depth 0
|
|
195
|
+
// or the end of the line for semicolon-free code
|
|
196
|
+
const startPos = vmatch.index + vmatch[0].length - 1; // position of the '='
|
|
197
|
+
let pos = startPos;
|
|
198
|
+
let depth = 0;
|
|
199
|
+
let foundEnd = -1;
|
|
200
|
+
while (pos < source.length) {
|
|
201
|
+
const ch = source[pos];
|
|
202
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
203
|
+
depth++;
|
|
204
|
+
}
|
|
205
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
206
|
+
depth--;
|
|
207
|
+
if (depth < 0)
|
|
208
|
+
break; // we've gone past our scope
|
|
209
|
+
}
|
|
210
|
+
else if (ch === ';' && depth === 0) {
|
|
211
|
+
foundEnd = pos;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
else if (ch === '\n' && depth === 0) {
|
|
215
|
+
// For semicolon-free code, the newline is the end
|
|
216
|
+
// But only if the next non-whitespace isn't a continuation (., +, etc.)
|
|
217
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
218
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
219
|
+
foundEnd = pos;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
224
|
+
// Skip strings
|
|
225
|
+
const quote = ch;
|
|
226
|
+
pos++;
|
|
227
|
+
while (pos < source.length) {
|
|
228
|
+
if (source[pos] === '\\') {
|
|
229
|
+
pos++;
|
|
230
|
+
}
|
|
231
|
+
else if (source[pos] === quote)
|
|
232
|
+
break;
|
|
233
|
+
pos++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
237
|
+
// Skip line comment
|
|
238
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
239
|
+
pos++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
243
|
+
// Skip block comment
|
|
244
|
+
pos += 2;
|
|
245
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
246
|
+
pos++;
|
|
247
|
+
pos++;
|
|
248
|
+
}
|
|
249
|
+
pos++;
|
|
250
|
+
}
|
|
251
|
+
if (foundEnd === -1)
|
|
252
|
+
continue; // couldn't find statement end
|
|
253
|
+
varInsertions.push({ lineEnd: foundEnd + 1, varName, lineNo });
|
|
254
|
+
}
|
|
255
|
+
return varInsertions;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Find destructured variable declarations: const { a, b } = ... and const [a, b] = ...
|
|
259
|
+
*/
|
|
260
|
+
function findDestructuredDeclarations(source) {
|
|
261
|
+
const results = [];
|
|
262
|
+
const destructRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\{[^}]*\}|\[[^\]]*\])\s*(?::\s*[^=]+?)?\s*=[^=]/gm;
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = destructRegex.exec(source)) !== null) {
|
|
265
|
+
const pattern = match[1];
|
|
266
|
+
const varNames = extractDestructuredNames(pattern);
|
|
267
|
+
if (varNames.length === 0)
|
|
268
|
+
continue;
|
|
269
|
+
const restOfLine = source.slice(match.index + match[0].length - 1, match.index + match[0].length + 200);
|
|
270
|
+
if (/^\s*require\s*\(/.test(restOfLine))
|
|
271
|
+
continue;
|
|
272
|
+
let lineNo = 1;
|
|
273
|
+
for (let i = 0; i < match.index; i++) {
|
|
274
|
+
if (source[i] === '\n')
|
|
275
|
+
lineNo++;
|
|
276
|
+
}
|
|
277
|
+
const startPos = match.index + match[0].length - 1;
|
|
278
|
+
let pos = startPos;
|
|
279
|
+
let depth = 0;
|
|
280
|
+
let foundEnd = -1;
|
|
281
|
+
while (pos < source.length) {
|
|
282
|
+
const ch = source[pos];
|
|
283
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
284
|
+
depth++;
|
|
285
|
+
}
|
|
286
|
+
else if (ch === ')' || ch === ']' || ch === '}') {
|
|
287
|
+
depth--;
|
|
288
|
+
if (depth < 0)
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
else if (ch === ';' && depth === 0) {
|
|
292
|
+
foundEnd = pos;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
else if (ch === '\n' && depth === 0) {
|
|
296
|
+
const nextNonWs = source.slice(pos + 1).match(/^\s*(\S)/);
|
|
297
|
+
if (nextNonWs && !'.+=-|&?:,'.includes(nextNonWs[1])) {
|
|
298
|
+
foundEnd = pos;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
303
|
+
const quote = ch;
|
|
304
|
+
pos++;
|
|
305
|
+
while (pos < source.length) {
|
|
306
|
+
if (source[pos] === '\\') {
|
|
307
|
+
pos++;
|
|
308
|
+
}
|
|
309
|
+
else if (source[pos] === quote)
|
|
310
|
+
break;
|
|
311
|
+
pos++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
|
|
315
|
+
while (pos < source.length && source[pos] !== '\n')
|
|
316
|
+
pos++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
|
|
320
|
+
pos += 2;
|
|
321
|
+
while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
|
|
322
|
+
pos++;
|
|
323
|
+
pos++;
|
|
324
|
+
}
|
|
325
|
+
pos++;
|
|
326
|
+
}
|
|
327
|
+
if (foundEnd === -1)
|
|
328
|
+
continue;
|
|
329
|
+
results.push({ lineEnd: foundEnd + 1, varNames, lineNo });
|
|
330
|
+
}
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Extract variable names from a destructuring pattern.
|
|
335
|
+
* { a, b, c: d } → ['a', 'b', 'd'], [a, b, ...rest] → ['a', 'b', 'rest']
|
|
336
|
+
*/
|
|
337
|
+
function extractDestructuredNames(pattern) {
|
|
338
|
+
const names = [];
|
|
339
|
+
const inner = pattern.slice(1, -1).trim();
|
|
340
|
+
if (!inner)
|
|
341
|
+
return names;
|
|
342
|
+
const parts = [];
|
|
343
|
+
let depth = 0;
|
|
344
|
+
let current = '';
|
|
345
|
+
for (const ch of inner) {
|
|
346
|
+
if (ch === '{' || ch === '[')
|
|
347
|
+
depth++;
|
|
348
|
+
else if (ch === '}' || ch === ']')
|
|
349
|
+
depth--;
|
|
350
|
+
else if (ch === ',' && depth === 0) {
|
|
351
|
+
parts.push(current.trim());
|
|
352
|
+
current = '';
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
current += ch;
|
|
356
|
+
}
|
|
357
|
+
if (current.trim())
|
|
358
|
+
parts.push(current.trim());
|
|
359
|
+
for (const part of parts) {
|
|
360
|
+
if (part.startsWith('...')) {
|
|
361
|
+
const restName = part.slice(3).trim().split(/[\s:]/)[0];
|
|
362
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(restName))
|
|
363
|
+
names.push(restName);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const colonIdx = part.indexOf(':');
|
|
367
|
+
if (colonIdx !== -1) {
|
|
368
|
+
const afterColon = part.slice(colonIdx + 1).trim();
|
|
369
|
+
if (afterColon.startsWith('{') || afterColon.startsWith('[')) {
|
|
370
|
+
names.push(...extractDestructuredNames(afterColon));
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const localName = afterColon.split(/[\s=]/)[0].trim();
|
|
374
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(localName))
|
|
375
|
+
names.push(localName);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const name = part.split(/[\s=]/)[0].trim();
|
|
380
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
|
|
381
|
+
names.push(name);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return names;
|
|
385
|
+
}
|
|
162
386
|
function transformCjsSource(source, filename, moduleName, env) {
|
|
163
387
|
const funcRegex = /^[ \t]*(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
|
|
164
388
|
const insertions = [];
|
|
@@ -191,12 +415,16 @@ function transformCjsSource(source, filename, moduleName, env) {
|
|
|
191
415
|
continue;
|
|
192
416
|
insertions.push({ position: closeBrace + 1, name, paramNames });
|
|
193
417
|
}
|
|
194
|
-
|
|
418
|
+
// Also find variable declarations for tracing
|
|
419
|
+
const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0';
|
|
420
|
+
const varInsertions = varTraceEnabled ? findVarDeclarations(source) : [];
|
|
421
|
+
const destructInsertions = varTraceEnabled ? findDestructuredDeclarations(source) : [];
|
|
422
|
+
if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0)
|
|
195
423
|
return source;
|
|
196
424
|
// Resolve the path to the wrap helper (compiled JS)
|
|
197
425
|
const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
|
|
198
426
|
// Prepend: load the wrapper and create the wrap helper
|
|
199
|
-
const
|
|
427
|
+
const prefixLines = [
|
|
200
428
|
`var __trickle_mod = require(${JSON.stringify(wrapHelperPath)});`,
|
|
201
429
|
`var __trickle_wrap = function(fn, name, paramNames) {`,
|
|
202
430
|
` var opts = {`,
|
|
@@ -205,22 +433,47 @@ function transformCjsSource(source, filename, moduleName, env) {
|
|
|
205
433
|
` trackArgs: true,`,
|
|
206
434
|
` trackReturn: true,`,
|
|
207
435
|
` sampleRate: 1,`,
|
|
208
|
-
` maxDepth:
|
|
436
|
+
` maxDepth: 3,`,
|
|
209
437
|
` environment: ${JSON.stringify(env)},`,
|
|
210
438
|
` enabled: true,`,
|
|
211
439
|
` };`,
|
|
212
440
|
` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
|
|
213
441
|
` return __trickle_mod.wrapFunction(fn, opts);`,
|
|
214
442
|
`};`,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
443
|
+
];
|
|
444
|
+
// Add variable tracing helper if we have var insertions
|
|
445
|
+
if (varInsertions.length > 0 || destructInsertions.length > 0) {
|
|
446
|
+
const traceVarPath = path_1.default.join(__dirname, 'trace-var.js');
|
|
447
|
+
prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l) { try { __trickle_tv_mod.traceVar(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e){} };`);
|
|
448
|
+
}
|
|
449
|
+
prefixLines.push('');
|
|
450
|
+
const prefix = prefixLines.join('\n');
|
|
451
|
+
const allInsertions = [];
|
|
452
|
+
for (const { position, name, paramNames } of insertions) {
|
|
221
453
|
const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
|
|
222
|
-
|
|
223
|
-
|
|
454
|
+
allInsertions.push({
|
|
455
|
+
position,
|
|
456
|
+
code: `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
for (const { lineEnd, varName, lineNo } of varInsertions) {
|
|
460
|
+
allInsertions.push({
|
|
461
|
+
position: lineEnd,
|
|
462
|
+
code: `\ntry{__trickle_tv(${varName},${JSON.stringify(varName)},${lineNo})}catch(__e){}\n`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
for (const { lineEnd, varNames, lineNo } of destructInsertions) {
|
|
466
|
+
const calls = varNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo})`).join(';');
|
|
467
|
+
allInsertions.push({
|
|
468
|
+
position: lineEnd,
|
|
469
|
+
code: `\n;try{${calls}}catch(__e){}\n`,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
// Sort by position descending (insert from end to preserve earlier positions)
|
|
473
|
+
allInsertions.sort((a, b) => b.position - a.position);
|
|
474
|
+
let result = source;
|
|
475
|
+
for (const { position, code } of allInsertions) {
|
|
476
|
+
result = result.slice(0, position) + code + result.slice(position);
|
|
224
477
|
}
|
|
225
478
|
return prefix + result;
|
|
226
479
|
}
|
|
@@ -259,8 +512,12 @@ if (enabled) {
|
|
|
259
512
|
if (debug) {
|
|
260
513
|
console.log(`[trickle/observe] Auto-observation enabled (backend: ${backendUrl})`);
|
|
261
514
|
}
|
|
262
|
-
// ── Hook
|
|
515
|
+
// ── Hook 0a: Patch global.fetch to capture HTTP response types ──
|
|
263
516
|
(0, fetch_observer_1.patchFetch)(environment, debug);
|
|
517
|
+
// ── Hook 0b: Initialize variable tracer ──
|
|
518
|
+
if (process.env.TRICKLE_TRACE_VARS !== '0') {
|
|
519
|
+
(0, trace_var_1.initVarTracer)({ debug });
|
|
520
|
+
}
|
|
264
521
|
// ── Hook 1: Module._compile — transform source to wrap function declarations ──
|
|
265
522
|
// This catches ALL functions including entry file and non-exported helpers.
|
|
266
523
|
M.prototype._compile = function hookedCompile(content, filename) {
|
|
@@ -296,13 +553,79 @@ if (enabled) {
|
|
|
296
553
|
// Still useful for wrapping exports that use inline function expressions
|
|
297
554
|
// (e.g. module.exports = { foo: function() {} }).
|
|
298
555
|
// The double-wrap guard in wrapFunction prevents redundant wrapping.
|
|
556
|
+
// Track whether we've already patched Express to avoid double-patching
|
|
557
|
+
const expressPatched = new Set();
|
|
299
558
|
M._load = function hookedLoad(request, parent, isMain) {
|
|
300
559
|
const exports = originalLoad.apply(this, arguments);
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
560
|
+
// ── Express auto-detection: wrap Express factory to capture route types ──
|
|
561
|
+
// When someone requires 'express', wrap the factory so every app.get/post/etc
|
|
562
|
+
// automatically captures { body, params, query } → res.json() type data.
|
|
563
|
+
if (request === 'express' && !expressPatched.has('express')) {
|
|
564
|
+
expressPatched.add('express');
|
|
565
|
+
try {
|
|
566
|
+
const origExpress = exports;
|
|
567
|
+
const wrappedExpress = function (...args) {
|
|
568
|
+
const app = origExpress.apply(this, args);
|
|
569
|
+
// Tag the app so we can verify the middleware was injected
|
|
570
|
+
app.__trickle_instrumented = true;
|
|
571
|
+
try {
|
|
572
|
+
// Wrap route methods for future route definitions
|
|
573
|
+
(0, express_1.instrumentExpress)(app, { environment });
|
|
574
|
+
// Also inject middleware to capture routes defined BEFORE instrumentation
|
|
575
|
+
// (common in DI/class-based architectures where routes are defined in constructors)
|
|
576
|
+
if (typeof app.use === 'function') {
|
|
577
|
+
app.use((0, express_1.trickleMiddleware)({ environment }));
|
|
578
|
+
if (debug) {
|
|
579
|
+
console.log('[trickle/observe] Injected trickleMiddleware into Express app');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (debug) {
|
|
583
|
+
console.log('[trickle/observe] Auto-instrumented Express app (route types will be captured)');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (e) {
|
|
587
|
+
if (debug) {
|
|
588
|
+
console.log('[trickle/observe] Express instrumentation error:', e.message);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return app;
|
|
592
|
+
};
|
|
593
|
+
// Copy all static properties (express.json, express.static, express.Router, etc.)
|
|
594
|
+
for (const key of Object.keys(origExpress)) {
|
|
595
|
+
wrappedExpress[key] = origExpress[key];
|
|
596
|
+
}
|
|
597
|
+
Object.setPrototypeOf(wrappedExpress, Object.getPrototypeOf(origExpress));
|
|
598
|
+
// Also wrap express.Router() to instrument route handlers on Router instances.
|
|
599
|
+
// Most real Express apps define routes on Routers, not directly on the app.
|
|
600
|
+
if (typeof origExpress.Router === 'function') {
|
|
601
|
+
const origRouter = origExpress.Router;
|
|
602
|
+
wrappedExpress.Router = function (...rArgs) {
|
|
603
|
+
const router = origRouter.apply(this, rArgs);
|
|
604
|
+
try {
|
|
605
|
+
(0, express_1.instrumentExpress)(router, { environment });
|
|
606
|
+
if (debug) {
|
|
607
|
+
console.log('[trickle/observe] Auto-instrumented Express Router');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch { /* don't crash */ }
|
|
611
|
+
return router;
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// Update require cache
|
|
615
|
+
try {
|
|
616
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
617
|
+
if (require.cache[resolvedPath]) {
|
|
618
|
+
require.cache[resolvedPath].exports = wrappedExpress;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch { /* non-critical */ }
|
|
622
|
+
return wrappedExpress;
|
|
623
|
+
}
|
|
624
|
+
catch { /* fall through to normal processing */ }
|
|
304
625
|
}
|
|
305
|
-
// Resolve to absolute path for dedup
|
|
626
|
+
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
627
|
+
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
628
|
+
// with './' or '/'. We need the resolved path to decide if it's user code.
|
|
306
629
|
let resolvedPath;
|
|
307
630
|
try {
|
|
308
631
|
resolvedPath = M._resolveFilename(request, parent);
|
|
@@ -310,6 +633,9 @@ if (enabled) {
|
|
|
310
633
|
catch {
|
|
311
634
|
return exports;
|
|
312
635
|
}
|
|
636
|
+
// Skip built-in modules (they resolve to names like 'fs', 'path', not absolute paths)
|
|
637
|
+
if (!resolvedPath.startsWith('/'))
|
|
638
|
+
return exports;
|
|
313
639
|
// Skip node_modules and trickle internals
|
|
314
640
|
if (resolvedPath.includes('node_modules'))
|
|
315
641
|
return exports;
|
|
@@ -338,9 +664,26 @@ if (enabled) {
|
|
|
338
664
|
for (const key of Object.keys(exports)) {
|
|
339
665
|
if (typeof exports[key] === 'function' && key !== 'default') {
|
|
340
666
|
const fn = exports[key];
|
|
341
|
-
// Skip classes —
|
|
342
|
-
|
|
343
|
-
|
|
667
|
+
// Skip classes — wrapping them breaks DI containers (tsyringe, inversify, etc.)
|
|
668
|
+
// and decorator metadata. Detect via toString() for ES2015+ classes,
|
|
669
|
+
// and via prototype check for classes with prototype methods.
|
|
670
|
+
// Wrapped in try-catch because some prototype property access can throw
|
|
671
|
+
// (e.g., Node.js stream classes with getter-only properties).
|
|
672
|
+
let isClass = false;
|
|
673
|
+
try {
|
|
674
|
+
const fnStr = Function.prototype.toString.call(fn);
|
|
675
|
+
isClass = fnStr.startsWith('class ') ||
|
|
676
|
+
(fn.prototype && fn.prototype.constructor === fn &&
|
|
677
|
+
Object.getOwnPropertyNames(fn.prototype).some(m => {
|
|
678
|
+
try {
|
|
679
|
+
return m !== 'constructor' && typeof fn.prototype[m] === 'function';
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
catch { /* assume not a class */ }
|
|
344
687
|
if (isClass)
|
|
345
688
|
continue;
|
|
346
689
|
const paramNames = extractParamNames(fn);
|
|
@@ -350,39 +693,98 @@ if (enabled) {
|
|
|
350
693
|
trackArgs: true,
|
|
351
694
|
trackReturn: true,
|
|
352
695
|
sampleRate: 1,
|
|
353
|
-
maxDepth:
|
|
696
|
+
maxDepth: 3,
|
|
354
697
|
environment,
|
|
355
698
|
enabled: true,
|
|
356
699
|
paramNames: paramNames.length > 0 ? paramNames : undefined,
|
|
357
700
|
};
|
|
358
|
-
|
|
701
|
+
const wrapped = (0, wrap_1.wrapFunction)(fn, wrapOpts);
|
|
702
|
+
try {
|
|
703
|
+
exports[key] = wrapped;
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
// Property might be getter-only (tsx/esbuild uses getter exports).
|
|
707
|
+
// Redefine with Object.defineProperty.
|
|
708
|
+
try {
|
|
709
|
+
Object.defineProperty(exports, key, {
|
|
710
|
+
value: wrapped,
|
|
711
|
+
enumerable: true,
|
|
712
|
+
configurable: true,
|
|
713
|
+
writable: true,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
continue; /* truly read-only, skip */
|
|
718
|
+
}
|
|
719
|
+
}
|
|
359
720
|
count++;
|
|
360
721
|
}
|
|
361
722
|
}
|
|
362
|
-
// Handle default export if it's a function
|
|
723
|
+
// Handle default export if it's a function (but not a class)
|
|
363
724
|
if (typeof exports.default === 'function') {
|
|
364
725
|
const fn = exports.default;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
726
|
+
let defaultIsClass = false;
|
|
727
|
+
try {
|
|
728
|
+
const defaultFnStr = Function.prototype.toString.call(fn);
|
|
729
|
+
defaultIsClass = defaultFnStr.startsWith('class ') ||
|
|
730
|
+
(fn.prototype && fn.prototype.constructor === fn &&
|
|
731
|
+
Object.getOwnPropertyNames(fn.prototype).some(m => {
|
|
732
|
+
try {
|
|
733
|
+
return m !== 'constructor' && typeof fn.prototype[m] === 'function';
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
catch { /* assume not a class */ }
|
|
741
|
+
if (!defaultIsClass) {
|
|
742
|
+
const paramNames = extractParamNames(fn);
|
|
743
|
+
const wrapOpts = {
|
|
744
|
+
functionName: fn.name || 'default',
|
|
745
|
+
module: moduleName,
|
|
746
|
+
trackArgs: true,
|
|
747
|
+
trackReturn: true,
|
|
748
|
+
sampleRate: 1,
|
|
749
|
+
maxDepth: 3,
|
|
750
|
+
environment,
|
|
751
|
+
enabled: true,
|
|
752
|
+
paramNames: paramNames.length > 0 ? paramNames : undefined,
|
|
753
|
+
};
|
|
754
|
+
const wrapped = (0, wrap_1.wrapFunction)(fn, wrapOpts);
|
|
755
|
+
try {
|
|
756
|
+
exports.default = wrapped;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
try {
|
|
760
|
+
Object.defineProperty(exports, 'default', {
|
|
761
|
+
value: wrapped, enumerable: true, configurable: true, writable: true,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch { /* skip */ }
|
|
765
|
+
}
|
|
766
|
+
count++;
|
|
767
|
+
}
|
|
379
768
|
}
|
|
380
769
|
// Wrap class prototype methods for exported classes
|
|
381
770
|
for (const key of Object.keys(exports)) {
|
|
382
771
|
const val = exports[key];
|
|
383
772
|
if (typeof val === 'function' && val.prototype && val.prototype.constructor === val) {
|
|
384
|
-
|
|
385
|
-
|
|
773
|
+
let protoNames;
|
|
774
|
+
try {
|
|
775
|
+
protoNames = Object.getOwnPropertyNames(val.prototype)
|
|
776
|
+
.filter(m => { try {
|
|
777
|
+
return m !== 'constructor' && typeof val.prototype[m] === 'function';
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return false;
|
|
781
|
+
} });
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
// Use the class's actual name, not the export key (avoids "default.method")
|
|
787
|
+
const className = val.name || key;
|
|
386
788
|
for (const method of protoNames) {
|
|
387
789
|
if (method.startsWith('_'))
|
|
388
790
|
continue;
|
|
@@ -392,12 +794,12 @@ if (enabled) {
|
|
|
392
794
|
continue;
|
|
393
795
|
const methodParamNames = extractParamNames(origMethod);
|
|
394
796
|
const methodOpts = {
|
|
395
|
-
functionName: `${
|
|
797
|
+
functionName: `${className}.${method}`,
|
|
396
798
|
module: moduleName,
|
|
397
799
|
trackArgs: true,
|
|
398
800
|
trackReturn: true,
|
|
399
801
|
sampleRate: 1,
|
|
400
|
-
maxDepth:
|
|
802
|
+
maxDepth: 3,
|
|
401
803
|
environment,
|
|
402
804
|
enabled: true,
|
|
403
805
|
paramNames: methodParamNames.length > 0 ? methodParamNames : undefined,
|
|
@@ -415,7 +817,14 @@ if (enabled) {
|
|
|
415
817
|
}
|
|
416
818
|
else if (typeof exports === 'function') {
|
|
417
819
|
// Module exports a single function (e.g. module.exports = fn)
|
|
820
|
+
// But skip classes — wrapping them breaks DI, decorators, and instanceof
|
|
418
821
|
const fn = exports;
|
|
822
|
+
const singleFnStr = Function.prototype.toString.call(fn);
|
|
823
|
+
const singleIsClass = singleFnStr.startsWith('class ') ||
|
|
824
|
+
(fn.prototype && fn.prototype.constructor === fn &&
|
|
825
|
+
Object.getOwnPropertyNames(fn.prototype).some(m => m !== 'constructor' && typeof fn.prototype[m] === 'function'));
|
|
826
|
+
if (singleIsClass)
|
|
827
|
+
return exports;
|
|
419
828
|
const fnParamNames = extractParamNames(fn);
|
|
420
829
|
const wrapOpts = {
|
|
421
830
|
functionName: fn.name || moduleName,
|
|
@@ -423,7 +832,7 @@ if (enabled) {
|
|
|
423
832
|
trackArgs: true,
|
|
424
833
|
trackReturn: true,
|
|
425
834
|
sampleRate: 1,
|
|
426
|
-
maxDepth:
|
|
835
|
+
maxDepth: 3,
|
|
427
836
|
environment,
|
|
428
837
|
enabled: true,
|
|
429
838
|
paramNames: fnParamNames.length > 0 ? fnParamNames : undefined,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable-level tracing — captures the runtime type and sample value
|
|
3
|
+
* of variable assignments within function bodies.
|
|
4
|
+
*
|
|
5
|
+
* This is injected by the Module._compile source transform. After each
|
|
6
|
+
* `const/let/var x = expr;` statement, the transform inserts:
|
|
7
|
+
*
|
|
8
|
+
* __trickle_tv(x, 'x', 42, 'my-module', '/path/to/file.ts');
|
|
9
|
+
*
|
|
10
|
+
* The traceVar function:
|
|
11
|
+
* 1. Infers the TypeNode from the runtime value
|
|
12
|
+
* 2. Captures a sanitized sample value
|
|
13
|
+
* 3. Appends to .trickle/variables.jsonl
|
|
14
|
+
* 4. Caches by (file:line:varName + typeHash) to avoid duplicates
|
|
15
|
+
*/
|
|
16
|
+
import { TypeNode } from './types';
|
|
17
|
+
export interface VariableObservation {
|
|
18
|
+
kind: 'variable';
|
|
19
|
+
varName: string;
|
|
20
|
+
line: number;
|
|
21
|
+
module: string;
|
|
22
|
+
file: string;
|
|
23
|
+
type: TypeNode;
|
|
24
|
+
typeHash: string;
|
|
25
|
+
sample: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the variable tracer.
|
|
29
|
+
* Called once during observe-register setup.
|
|
30
|
+
*/
|
|
31
|
+
export declare function initVarTracer(opts?: {
|
|
32
|
+
debug?: boolean;
|
|
33
|
+
}): void;
|
|
34
|
+
/**
|
|
35
|
+
* Trace a variable's runtime value.
|
|
36
|
+
* Called by injected code after each variable declaration.
|
|
37
|
+
*
|
|
38
|
+
* @param value - The variable's current value (already computed)
|
|
39
|
+
* @param varName - The variable name in source
|
|
40
|
+
* @param line - Line number in source file
|
|
41
|
+
* @param moduleName - Module name (derived from filename)
|
|
42
|
+
* @param filePath - Absolute path to source file
|
|
43
|
+
*/
|
|
44
|
+
export declare function traceVar(value: unknown, varName: string, line: number, moduleName: string, filePath: string): void;
|