trickle-observe 0.2.66 → 0.2.68

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.
@@ -418,6 +418,48 @@ function transformCjsSource(source, filename, moduleName, env) {
418
418
  continue;
419
419
  insertions.push({ position: closeBrace + 1, name, paramNames });
420
420
  }
421
+ // Find class declarations and wrap their methods
422
+ const classRegex = /^[ \t]*class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:extends\s+[a-zA-Z_$.]+\s*)?\{/gm;
423
+ const classInsertions = [];
424
+ let classMatch;
425
+ while ((classMatch = classRegex.exec(source)) !== null) {
426
+ const className = classMatch[1];
427
+ const classOpenBrace = source.indexOf('{', classMatch.index + classMatch[0].length - 1);
428
+ if (classOpenBrace === -1)
429
+ continue;
430
+ const classCloseBrace = findClosingBrace(source, classOpenBrace);
431
+ if (classCloseBrace === -1)
432
+ continue;
433
+ // Extract methods from the class body
434
+ const classBody = source.slice(classOpenBrace + 1, classCloseBrace);
435
+ // Match: methodName(params) { or async methodName(params) { or static methodName(params) {
436
+ const methodRegex = /^[ \t]*(static\s+)?(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)\s*\{/gm;
437
+ let methodMatch;
438
+ while ((methodMatch = methodRegex.exec(classBody)) !== null) {
439
+ const isStatic = !!methodMatch[1];
440
+ const methodName = methodMatch[2];
441
+ // Skip constructor and private methods
442
+ if (methodName === 'constructor' || methodName.startsWith('_'))
443
+ continue;
444
+ // Extract param names
445
+ const mParamStr = methodMatch[3].trim();
446
+ const mParamNames = mParamStr
447
+ ? mParamStr.split(',').map((p) => {
448
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
449
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
450
+ return '';
451
+ return trimmed;
452
+ }).filter(Boolean)
453
+ : [];
454
+ const target = isStatic ? className : `${className}.prototype`;
455
+ const obsName = `${className}.${methodName}`;
456
+ const paramNamesArg = mParamNames.length > 0 ? JSON.stringify(mParamNames) : 'null';
457
+ classInsertions.push({
458
+ position: classCloseBrace + 1,
459
+ code: `\ntry{${target}.${methodName}=__trickle_wrap(${target}.${methodName},'${obsName}',${paramNamesArg})}catch(__e){}\n`,
460
+ });
461
+ }
462
+ }
421
463
  // Also find variable declarations for tracing
422
464
  const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0';
423
465
  // For TypeScript files (compiled by ts-node/tsc), type declarations (interfaces, type aliases)
@@ -496,7 +538,7 @@ function transformCjsSource(source, filename, moduleName, env) {
496
538
  destructInsertions = findDestructuredDeclarations(source);
497
539
  }
498
540
  }
499
- if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0)
541
+ if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && classInsertions.length === 0)
500
542
  return source;
501
543
  // Resolve the path to the wrap helper (compiled JS)
502
544
  const wrapHelperPath = path_1.default.join(__dirname, 'wrap.js');
@@ -546,6 +588,10 @@ function transformCjsSource(source, filename, moduleName, env) {
546
588
  code: `\n;try{${calls}}catch(__e){}\n`,
547
589
  });
548
590
  }
591
+ // Add class method wrappings
592
+ for (const ci of classInsertions) {
593
+ allInsertions.push(ci);
594
+ }
549
595
  // Sort by position descending (insert from end to preserve earlier positions)
550
596
  allInsertions.sort((a, b) => b.position - a.position);
551
597
  let result = source;
@@ -523,7 +523,21 @@ function transformSource(source, url, originalSource) {
523
523
  const paramNames = parenPos >= 0 ? extractParamNamesFromSource(source, parenPos) : [];
524
524
  // Remove 'export ' prefix, keep the function
525
525
  result.push(line.replace(/^(\s*)export\s+/, '$1'));
526
- exportedFunctions.push({ name, paramNames });
526
+ exportedFunctions.push({ name, paramNames, wrapInPlace: true });
527
+ continue;
528
+ }
529
+
530
+ // Non-exported function declarations — wrap them too for entry module coverage
531
+ const plainFuncMatch = trimmed.match(/^(async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(/);
532
+ if (plainFuncMatch && !trimmed.startsWith('export')) {
533
+ const name = plainFuncMatch[2];
534
+ if (name !== 'require' && name !== 'exports' && name !== 'module' && !name.startsWith('__trickle')) {
535
+ const srcOffset = lineOffset(source, i) + (line.length - trimmed.length);
536
+ const parenPos = source.indexOf('(', srcOffset + plainFuncMatch[0].indexOf('('));
537
+ const paramNames = parenPos >= 0 ? extractParamNamesFromSource(source, parenPos) : [];
538
+ exportedFunctions.push({ name, paramNames, wrapInPlace: true, noExport: true });
539
+ }
540
+ result.push(line);
527
541
  continue;
528
542
  }
529
543
 
@@ -607,23 +621,58 @@ function transformSource(source, url, originalSource) {
607
621
  return source;
608
622
  }
609
623
 
610
- // Add wrapper import and wrapping code at the bottom
624
+ // Add wrapper import and wrapping code
611
625
  const wrapperPathEscaped = config.wrapperPath.replace(/\\/g, '\\\\');
612
626
 
613
- result.push('');
614
- result.push('// [trickle] Auto-observation wrappers');
615
- result.push(`import { createRequire as __cr } from 'node:module';`);
616
- result.push(`const __require = __cr(import.meta.url);`);
617
- result.push(`const { wrapFunction: __tw } = __require('${wrapperPathEscaped}');`);
618
- result.push(`const __twOpts = (name, paramNames) => { const o = { functionName: name, module: '${moduleName}', trackArgs: true, trackReturn: true, sampleRate: 1, maxDepth: 5, environment: process.env.TRICKLE_ENV || 'development', enabled: true }; if (paramNames && paramNames.length) o.paramNames = paramNames; return o; };`);
627
+ // Insert wrapper setup at the top (after imports, before user code)
628
+ // Using import + createRequire to load CJS wrapper from ESM context
629
+ const wrapSetup = [
630
+ '',
631
+ '// [trickle] Auto-observation wrappers',
632
+ `import { createRequire as __cr } from 'node:module';`,
633
+ `const __require = __cr(import.meta.url);`,
634
+ `const { wrapFunction: __tw } = __require('${wrapperPathEscaped}');`,
635
+ `const __twOpts = (name, paramNames) => { const o = { functionName: name, module: '${moduleName}', trackArgs: true, trackReturn: true, sampleRate: 1, maxDepth: 5, environment: process.env.TRICKLE_ENV || 'development', enabled: true }; if (paramNames && paramNames.length) o.paramNames = paramNames; return o; };`,
636
+ ];
637
+
638
+ // For in-place wrapping: insert wrapper calls right after each function declaration
639
+ // This ensures functions are wrapped BEFORE any top-level code calls them
640
+ const inPlaceWraps = [];
641
+ for (const fn of exportedFunctions) {
642
+ if (fn.wrapInPlace) {
643
+ const pn = fn.paramNames.length > 0 ? JSON.stringify(fn.paramNames) : 'null';
644
+ inPlaceWraps.push(`try{${fn.name}=__tw(${fn.name},__twOpts('${fn.name}',${pn}))}catch(__e){}`);
645
+ }
646
+ }
619
647
 
620
- // Wrap and re-export named functions
648
+ // Find the right position to insert the wrap setup + in-place wraps
649
+ // Insert after the last import/function-declaration block, before top-level code
650
+ let insertIdx = 0;
651
+ for (let j = 0; j < result.length; j++) {
652
+ const t = result[j].trimStart();
653
+ if (t.startsWith('import ') || t.startsWith('// [trickle]') || t === '' ||
654
+ t.startsWith('function ') || t.startsWith('async function ') ||
655
+ t.startsWith('const __trickle_tv') || t.startsWith('const __require_tv') ||
656
+ t.startsWith('const __tv_mod') || t.startsWith('if (typeof __tv_mod')) {
657
+ insertIdx = j + 1;
658
+ }
659
+ }
660
+ // But make sure we're after any function body closing braces too
661
+ // Simple heuristic: find the last line that's just '}' before any assignment/call
662
+ for (let j = insertIdx; j < result.length; j++) {
663
+ const t = result[j].trim();
664
+ if (t === '}') insertIdx = j + 1;
665
+ else if (t && !t.startsWith('//') && !t.startsWith('function') && !t.startsWith('async function')) break;
666
+ }
667
+
668
+ result.splice(insertIdx, 0, ...wrapSetup, ...inPlaceWraps, '');
669
+
670
+ // Re-export wrapped functions (for importers of this module)
621
671
  const reExports = [];
622
- for (const { name, paramNames } of exportedFunctions) {
623
- const wrappedName = `__trickle_${name}`;
624
- const pn = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
625
- result.push(`const ${wrappedName} = typeof ${name} === 'function' ? __tw(${name}, __twOpts('${name}', ${pn})) : ${name};`);
626
- reExports.push(`${wrappedName} as ${name}`);
672
+ for (const fn of exportedFunctions) {
673
+ if (!fn.noExport) {
674
+ reExports.push(`${fn.name}`);
675
+ }
627
676
  }
628
677
 
629
678
  // Handle named exports from export { } statements
@@ -703,10 +752,11 @@ export async function load(url, context, nextLoad) {
703
752
  ? source
704
753
  : Buffer.from(source).toString('utf-8');
705
754
 
706
- // Only transform if the module has exports or variable declarations to trace
755
+ // Only transform if the module has exports, function declarations, or variable declarations to trace
707
756
  const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0' && config.traceVarPath;
708
757
  const hasVarDecls = varTraceEnabled && /^[ \t]*(?:export\s+)?(?:const|let|var)\s+[a-zA-Z_$]/m.test(sourceStr);
709
- if (!sourceStr.includes('export ') && !hasVarDecls) return result;
758
+ const hasFuncDecls = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+\w/m.test(sourceStr);
759
+ if (!sourceStr.includes('export ') && !hasVarDecls && !hasFuncDecls) return result;
710
760
 
711
761
  try {
712
762
  const transformed = transformSource(sourceStr, url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.66",
3
+ "version": "0.2.68",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -389,6 +389,46 @@ function transformCjsSource(source: string, filename: string, moduleName: string
389
389
  insertions.push({ position: closeBrace + 1, name, paramNames });
390
390
  }
391
391
 
392
+ // Find class declarations and wrap their methods
393
+ const classRegex = /^[ \t]*class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:extends\s+[a-zA-Z_$.]+\s*)?\{/gm;
394
+ const classInsertions: Array<{ position: number; code: string }> = [];
395
+ let classMatch;
396
+ while ((classMatch = classRegex.exec(source)) !== null) {
397
+ const className = classMatch[1];
398
+ const classOpenBrace = source.indexOf('{', classMatch.index + classMatch[0].length - 1);
399
+ if (classOpenBrace === -1) continue;
400
+ const classCloseBrace = findClosingBrace(source, classOpenBrace);
401
+ if (classCloseBrace === -1) continue;
402
+
403
+ // Extract methods from the class body
404
+ const classBody = source.slice(classOpenBrace + 1, classCloseBrace);
405
+ // Match: methodName(params) { or async methodName(params) { or static methodName(params) {
406
+ const methodRegex = /^[ \t]*(static\s+)?(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)\s*\{/gm;
407
+ let methodMatch;
408
+ while ((methodMatch = methodRegex.exec(classBody)) !== null) {
409
+ const isStatic = !!methodMatch[1];
410
+ const methodName = methodMatch[2];
411
+ // Skip constructor and private methods
412
+ if (methodName === 'constructor' || methodName.startsWith('_')) continue;
413
+ // Extract param names
414
+ const mParamStr = methodMatch[3].trim();
415
+ const mParamNames = mParamStr
416
+ ? mParamStr.split(',').map((p: string) => {
417
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
418
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...')) return '';
419
+ return trimmed;
420
+ }).filter(Boolean)
421
+ : [];
422
+ const target = isStatic ? className : `${className}.prototype`;
423
+ const obsName = `${className}.${methodName}`;
424
+ const paramNamesArg = mParamNames.length > 0 ? JSON.stringify(mParamNames) : 'null';
425
+ classInsertions.push({
426
+ position: classCloseBrace + 1,
427
+ code: `\ntry{${target}.${methodName}=__trickle_wrap(${target}.${methodName},'${obsName}',${paramNamesArg})}catch(__e){}\n`,
428
+ });
429
+ }
430
+ }
431
+
392
432
  // Also find variable declarations for tracing
393
433
  const varTraceEnabled = process.env.TRICKLE_TRACE_VARS !== '0';
394
434
 
@@ -468,7 +508,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
468
508
  }
469
509
  }
470
510
 
471
- if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0) return source;
511
+ if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && classInsertions.length === 0) return source;
472
512
 
473
513
  // Resolve the path to the wrap helper (compiled JS)
474
514
  const wrapHelperPath = path.join(__dirname, 'wrap.js');
@@ -531,6 +571,11 @@ function transformCjsSource(source: string, filename: string, moduleName: string
531
571
  });
532
572
  }
533
573
 
574
+ // Add class method wrappings
575
+ for (const ci of classInsertions) {
576
+ allInsertions.push(ci);
577
+ }
578
+
534
579
  // Sort by position descending (insert from end to preserve earlier positions)
535
580
  allInsertions.sort((a, b) => b.position - a.position);
536
581