trickle-observe 0.2.110 → 0.2.112

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/express.js CHANGED
@@ -181,11 +181,6 @@ function wrapExpressHandler(handler, routeName, opts) {
181
181
  return next(err);
182
182
  }
183
183
  };
184
- const debug = process.env.TRICKLE_DEBUG === '1';
185
- if (debug) {
186
- const hs = handler.toString().substring(0, 200);
187
- process.stderr.write(`[trickle/express-wrap] Calling handler: ${hs.replace(/\n/g, '\\n')}\n`);
188
- }
189
184
  try {
190
185
  const result = handler.call(this, req, res, wrappedNext);
191
186
  // Handle async handlers that return a promise
@@ -1080,10 +1080,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
1080
1080
  lineNo: remapLine(ins.lineNo),
1081
1081
  }));
1082
1082
  if (debug && funcParamInsertions.length > 0) {
1083
- console.log(`[trickle/observe] Found ${funcParamInsertions.length} function param insertions in ${moduleName}`);
1084
- for (const ins of funcParamInsertions) {
1085
- console.log(` Line ${ins.lineNo}: params=[${ins.paramNames.join(', ')}]`);
1086
- }
1083
+ console.log(`[trickle/observe] Tracing ${funcParamInsertions.length} function param sites in ${moduleName}`);
1087
1084
  }
1088
1085
  }
1089
1086
  if (insertions.length === 0 && varInsertions.length === 0 && destructInsertions.length === 0 && reassignInsertions.length === 0 && forLoopInsertions.length === 0 && catchInsertions.length === 0 && classInsertions.length === 0 && funcParamInsertions.length === 0)
@@ -1120,7 +1117,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
1120
1117
  const traceModuleName = sourceMap
1121
1118
  ? path_1.default.basename(sourceMap.originalFile).replace(/\.[jt]sx?$/, '')
1122
1119
  : moduleName;
1123
- prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){ if (process.env.TRICKLE_DEBUG === '1') console.error('[trickle/tv] Error tracing', n, ':', e && e.message); } };`);
1120
+ prefixLines.push(`var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`, `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`);
1124
1121
  }
1125
1122
  prefixLines.push('');
1126
1123
  const prefix = prefixLines.join('\n');
@@ -1184,7 +1181,7 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
1184
1181
  const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
1185
1182
  allInsertions.push({
1186
1183
  position: bodyStart,
1187
- code: `\nprocess.stderr.write("[trickle/params] Entering L${lineNo}\\n");try{${calls}}catch(__e3){process.stderr.write('[trickle/params] Error:'+(__e3&&__e3.message)+'\\n')}\n`,
1184
+ code: `\ntry{${calls}}catch(__e3){}\n`,
1188
1185
  });
1189
1186
  }
1190
1187
  // Add class method wrappings
@@ -1197,14 +1194,6 @@ function transformCjsSource(source, filename, moduleName, env, sourceMap) {
1197
1194
  for (const { position, code } of allInsertions) {
1198
1195
  result = result.slice(0, position) + code + result.slice(position);
1199
1196
  }
1200
- // Debug: dump transformed source
1201
- if (debug && funcParamInsertions.length > 0) {
1202
- try {
1203
- const debugDir = process.env.TRICKLE_LOCAL_DIR || path_1.default.join(process.cwd(), '.trickle');
1204
- fs_1.default.writeFileSync(path_1.default.join(debugDir, 'transformed-' + moduleName + '.js'), prefix + result);
1205
- }
1206
- catch { }
1207
- }
1208
1197
  return prefix + result;
1209
1198
  }
1210
1199
  /**
package/dist/trace-var.js CHANGED
@@ -145,9 +145,6 @@ function traceVar(value, varName, line, moduleName, filePath) {
145
145
  return;
146
146
  }
147
147
  try {
148
- if (debugMode) {
149
- console.log(`[trickle/vars] traceVar called: ${varName} at ${filePath}:${line} (typeof=${typeof value})`);
150
- }
151
148
  const type = (0, type_inference_1.inferType)(value, 3);
152
149
  // Create a stable hash for dedup
153
150
  const dummyArgs = { kind: 'tuple', elements: [] };
@@ -193,11 +190,8 @@ function traceVar(value, varName, line, moduleName, filePath) {
193
190
  }
194
191
  }
195
192
  }
196
- catch (err) {
193
+ catch {
197
194
  // Never crash user's app
198
- if (debugMode) {
199
- console.error(`[trickle/vars] Error tracing ${varName}: ${err instanceof Error ? err.message : String(err)}`);
200
- }
201
195
  }
202
196
  }
203
197
  /**
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Unit tests for the Vite plugin transform (React component tracking).
8
+ *
9
+ * Run with: node --experimental-strip-types --test src/vite-plugin.test.ts
10
+ * Or after build: node --test dist/vite-plugin.test.js
11
+ */
12
+ const node_test_1 = require("node:test");
13
+ const strict_1 = __importDefault(require("node:assert/strict"));
14
+ const vite_plugin_js_1 = require("../dist/vite-plugin.js");
15
+ // Helper: transform code as if it came from a .tsx file
16
+ function transformTsx(code) {
17
+ const plugin = (0, vite_plugin_js_1.tricklePlugin)({ debug: false, traceVars: false });
18
+ const result = plugin.transform(code, '/test/App.tsx');
19
+ return result ? result.code : null;
20
+ }
21
+ function transformTs(code) {
22
+ const plugin = (0, vite_plugin_js_1.tricklePlugin)({ debug: false, traceVars: false });
23
+ const result = plugin.transform(code, '/test/util.ts');
24
+ return result ? result.code : null;
25
+ }
26
+ // ── React file detection ─────────────────────────────────────────────────────
27
+ (0, node_test_1.describe)('React file detection', () => {
28
+ (0, node_test_1.it)('tracks uppercase components in .tsx files', () => {
29
+ const code = `function UserCard(props) { return null; }`;
30
+ const out = transformTsx(code);
31
+ strict_1.default.ok(out, 'should transform');
32
+ strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
33
+ });
34
+ (0, node_test_1.it)('does not inject render tracker for .ts files', () => {
35
+ const code = `function UserCard(props) { return null; }`;
36
+ const out = transformTs(code);
37
+ // May still transform for function wrapping, but not for render tracking
38
+ if (out) {
39
+ strict_1.default.ok(!out.includes('__trickle_rc'), 'should NOT inject render tracker in .ts files');
40
+ }
41
+ });
42
+ (0, node_test_1.it)('does not track lowercase functions as components', () => {
43
+ const code = `function helper(x) { return x + 1; }`;
44
+ const out = transformTsx(code);
45
+ if (out) {
46
+ strict_1.default.ok(!out.includes('__trickle_rc'), 'lowercase function should not be tracked');
47
+ }
48
+ });
49
+ });
50
+ // ── Props capture: function declarations ─────────────────────────────────────
51
+ (0, node_test_1.describe)('Props capture — function declarations', () => {
52
+ (0, node_test_1.it)('uses arguments[0] for simple param: function Component(props)', () => {
53
+ const code = `function MyComponent(props) { return null; }`;
54
+ const out = transformTsx(code);
55
+ strict_1.default.ok(out, 'should transform');
56
+ strict_1.default.ok(out.includes('arguments[0]'), 'should pass arguments[0] as props');
57
+ });
58
+ (0, node_test_1.it)('uses arguments[0] for destructured param: function Component({ name })', () => {
59
+ const code = `function UserCard({ name, age }) { return null; }`;
60
+ const out = transformTsx(code);
61
+ strict_1.default.ok(out, 'should transform');
62
+ strict_1.default.ok(out.includes('arguments[0]'), 'should pass arguments[0] for destructured params');
63
+ });
64
+ (0, node_test_1.it)('injects __trickle_rc call at start of function body', () => {
65
+ const code = `function MyComponent(props) {\n const x = 1;\n return null;\n}`;
66
+ const out = transformTsx(code);
67
+ strict_1.default.ok(out, 'should transform');
68
+ // __trickle_rc should appear before body statements
69
+ const rcIdx = out.indexOf('__trickle_rc');
70
+ const bodyIdx = out.indexOf('const x = 1');
71
+ strict_1.default.ok(rcIdx !== -1, '__trickle_rc should be present');
72
+ strict_1.default.ok(bodyIdx !== -1, 'body code should be present');
73
+ strict_1.default.ok(rcIdx < bodyIdx, '__trickle_rc should come before body statements');
74
+ });
75
+ (0, node_test_1.it)('includes correct component name and line in __trickle_rc call', () => {
76
+ const code = `function UserCard(props) { return null; }`;
77
+ const out = transformTsx(code);
78
+ strict_1.default.ok(out, 'should transform');
79
+ strict_1.default.ok(out.includes('"UserCard"'), 'should include component name');
80
+ strict_1.default.ok(out.includes('__trickle_rc("UserCard"'), 'should call with component name');
81
+ });
82
+ });
83
+ // ── Props capture: arrow function components ──────────────────────────────────
84
+ (0, node_test_1.describe)('Props capture — arrow function components', () => {
85
+ (0, node_test_1.it)('uses single param name for simple arrow: const C = (props) => {}', () => {
86
+ const code = `const Dashboard = (props) => { return null; };`;
87
+ const out = transformTsx(code);
88
+ strict_1.default.ok(out, 'should transform');
89
+ strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
90
+ // props should be the param variable, not arguments[0]
91
+ strict_1.default.ok(out.includes('__trickle_rc("Dashboard"'), 'should use component name');
92
+ // should NOT use arguments[0] for arrow functions
93
+ const rcCall = out.match(/__trickle_rc\("Dashboard",[^)]+\)/);
94
+ strict_1.default.ok(rcCall, 'should have __trickle_rc call');
95
+ strict_1.default.ok(!rcCall[0].includes('arguments[0]'), 'arrow functions should not use arguments[0]');
96
+ });
97
+ (0, node_test_1.it)('reconstructs object for destructured arrow: const C = ({ a, b }) => {}', () => {
98
+ const code = `const Counter = ({ count, label }) => { return null; };`;
99
+ const out = transformTsx(code);
100
+ strict_1.default.ok(out, 'should transform');
101
+ strict_1.default.ok(out.includes('__trickle_rc'), 'should inject render tracker');
102
+ // Should reconstruct { count, label }
103
+ const rcCall = out.match(/__trickle_rc\("Counter",[^,]+,([^)]+)\)/);
104
+ if (rcCall) {
105
+ strict_1.default.ok(rcCall[1].includes('count') && rcCall[1].includes('label'), 'should reconstruct props object from destructured fields');
106
+ }
107
+ });
108
+ (0, node_test_1.it)('passes undefined for no-param arrow: const C = () => {}', () => {
109
+ const code = `const NoProps = () => { return null; };`;
110
+ const out = transformTsx(code);
111
+ if (out && out.includes('__trickle_rc')) {
112
+ strict_1.default.ok(out.includes('undefined'), 'should pass undefined for no-param component');
113
+ }
114
+ });
115
+ });
116
+ // ── render count tracking ─────────────────────────────────────────────────────
117
+ (0, node_test_1.describe)('Render count tracking', () => {
118
+ (0, node_test_1.it)('includes react_render kind in emitted record code', () => {
119
+ const code = `function Card(props) { return null; }`;
120
+ const out = transformTsx(code);
121
+ strict_1.default.ok(out, 'should transform');
122
+ strict_1.default.ok(out.includes("'react_render'"), 'emitted record should have kind react_render');
123
+ });
124
+ (0, node_test_1.it)('includes props data in emitted record', () => {
125
+ const code = `function Card(props) { return null; }`;
126
+ const out = transformTsx(code);
127
+ strict_1.default.ok(out, 'should transform');
128
+ strict_1.default.ok(out.includes('rec.props'), 'should capture props onto the record');
129
+ strict_1.default.ok(out.includes('propKeys'), 'should include propKeys');
130
+ });
131
+ (0, node_test_1.it)('tracks multiple components in one file', () => {
132
+ const code = [
133
+ `function Header(props) { return null; }`,
134
+ `function Footer(props) { return null; }`,
135
+ `function helper(x) { return x; }`,
136
+ ].join('\n');
137
+ const out = transformTsx(code);
138
+ strict_1.default.ok(out, 'should transform');
139
+ strict_1.default.ok(out.includes('"Header"'), 'should track Header');
140
+ strict_1.default.ok(out.includes('"Footer"'), 'should track Footer');
141
+ // helper should not be tracked as a component
142
+ const rcCalls = out.match(/__trickle_rc\("helper"/g);
143
+ strict_1.default.ok(!rcCalls, 'lowercase helper should not be tracked');
144
+ });
145
+ });
146
+ // ── findFunctionBodyBrace — destructured params don't confuse brace finding ───
147
+ (0, node_test_1.describe)('Correct function body brace detection', () => {
148
+ (0, node_test_1.it)('finds body brace even with destructured object params', () => {
149
+ const code = `function Form({ onSubmit, title }) {\n const x = 1;\n return null;\n}`;
150
+ const out = transformTsx(code);
151
+ strict_1.default.ok(out, 'should transform');
152
+ // __trickle_rc should be INSIDE the function body (before 'const x = 1')
153
+ const rcIdx = out.indexOf('__trickle_rc');
154
+ const bodyIdx = out.indexOf('const x = 1');
155
+ strict_1.default.ok(rcIdx < bodyIdx, 'render tracker must be inside the function body, before first statement');
156
+ // The wrap insertion should be AFTER the closing brace of the function
157
+ const wrapIdx = out.indexOf('__trickle_wrap');
158
+ strict_1.default.ok(wrapIdx > bodyIdx, 'function wrap should be after the function body');
159
+ });
160
+ });
package/dist/wrap.js CHANGED
@@ -94,6 +94,34 @@ function wrapFunction(fn, opts) {
94
94
  // Preserve function name and length
95
95
  Object.defineProperty(wrapper, 'name', { value: fn.name || opts.functionName, configurable: true });
96
96
  Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
97
+ // Copy all own properties from original function to wrapper.
98
+ // This is critical for Express apps (app.get, app.listen, etc.),
99
+ // class constructors with static methods, and other functions with properties.
100
+ for (const key of Object.getOwnPropertyNames(fn)) {
101
+ if (key === 'name' || key === 'length' || key === 'prototype' || key === 'caller' || key === 'arguments')
102
+ continue;
103
+ try {
104
+ const desc = Object.getOwnPropertyDescriptor(fn, key);
105
+ if (desc)
106
+ Object.defineProperty(wrapper, key, desc);
107
+ }
108
+ catch {
109
+ // Some properties may not be configurable
110
+ }
111
+ }
112
+ // Also copy symbol properties
113
+ for (const sym of Object.getOwnPropertySymbols(fn)) {
114
+ try {
115
+ const desc = Object.getOwnPropertyDescriptor(fn, sym);
116
+ if (desc)
117
+ Object.defineProperty(wrapper, sym, desc);
118
+ }
119
+ catch { }
120
+ }
121
+ // Copy prototype for constructor functions
122
+ if (fn.prototype && fn.prototype !== Object.prototype) {
123
+ wrapper.prototype = fn.prototype;
124
+ }
97
125
  // Mark as wrapped to prevent double-wrapping
98
126
  wrapper[TRICKLE_WRAPPED] = true;
99
127
  return wrapper;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.110",
3
+ "version": "0.2.112",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/express.ts CHANGED
@@ -205,11 +205,6 @@ function wrapExpressHandler(
205
205
  }
206
206
  };
207
207
 
208
- const debug = process.env.TRICKLE_DEBUG === '1';
209
- if (debug) {
210
- const hs = handler.toString().substring(0, 200);
211
- process.stderr.write(`[trickle/express-wrap] Calling handler: ${hs.replace(/\n/g, '\\n')}\n`);
212
- }
213
208
  try {
214
209
  const result = handler.call(this, req, res, wrappedNext);
215
210
 
@@ -1041,10 +1041,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
1041
1041
  lineNo: remapLine(ins.lineNo),
1042
1042
  }));
1043
1043
  if (debug && funcParamInsertions.length > 0) {
1044
- console.log(`[trickle/observe] Found ${funcParamInsertions.length} function param insertions in ${moduleName}`);
1045
- for (const ins of funcParamInsertions) {
1046
- console.log(` Line ${ins.lineNo}: params=[${ins.paramNames.join(', ')}]`);
1047
- }
1044
+ console.log(`[trickle/observe] Tracing ${funcParamInsertions.length} function param sites in ${moduleName}`);
1048
1045
  }
1049
1046
  }
1050
1047
 
@@ -1088,7 +1085,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
1088
1085
 
1089
1086
  prefixLines.push(
1090
1087
  `var __trickle_tv_mod = require(${JSON.stringify(traceVarPath)});`,
1091
- `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){ if (process.env.TRICKLE_DEBUG === '1') console.error('[trickle/tv] Error tracing', n, ':', e && e.message); } };`,
1088
+ `var __trickle_tv = function(v, n, l, m, f) { try { __trickle_tv_mod.traceVar(v, n, l, m || ${JSON.stringify(traceModuleName)}, f || ${JSON.stringify(traceFilePath)}); } catch(e){} };`,
1092
1089
  );
1093
1090
  }
1094
1091
 
@@ -1164,7 +1161,7 @@ function transformCjsSource(source: string, filename: string, moduleName: string
1164
1161
  const calls = paramNames.map(n => `__trickle_tv(${n},${JSON.stringify(n)},${lineNo}${sf})`).join(';');
1165
1162
  allInsertions.push({
1166
1163
  position: bodyStart,
1167
- code: `\nprocess.stderr.write("[trickle/params] Entering L${lineNo}\\n");try{${calls}}catch(__e3){process.stderr.write('[trickle/params] Error:'+(__e3&&__e3.message)+'\\n')}\n`,
1164
+ code: `\ntry{${calls}}catch(__e3){}\n`,
1168
1165
  });
1169
1166
  }
1170
1167
 
@@ -1181,14 +1178,6 @@ function transformCjsSource(source: string, filename: string, moduleName: string
1181
1178
  result = result.slice(0, position) + code + result.slice(position);
1182
1179
  }
1183
1180
 
1184
- // Debug: dump transformed source
1185
- if (debug && funcParamInsertions.length > 0) {
1186
- try {
1187
- const debugDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
1188
- fs.writeFileSync(path.join(debugDir, 'transformed-' + moduleName + '.js'), prefix + result);
1189
- } catch {}
1190
- }
1191
-
1192
1181
  return prefix + result;
1193
1182
  }
1194
1183
 
package/src/trace-var.ts CHANGED
@@ -130,9 +130,6 @@ export function traceVar(
130
130
  }
131
131
 
132
132
  try {
133
- if (debugMode) {
134
- console.log(`[trickle/vars] traceVar called: ${varName} at ${filePath}:${line} (typeof=${typeof value})`);
135
- }
136
133
  const type = inferType(value, 3);
137
134
 
138
135
  // Create a stable hash for dedup
@@ -181,11 +178,8 @@ export function traceVar(
181
178
  flushTimer.unref();
182
179
  }
183
180
  }
184
- } catch (err: unknown) {
181
+ } catch {
185
182
  // Never crash user's app
186
- if (debugMode) {
187
- console.error(`[trickle/vars] Error tracing ${varName}: ${err instanceof Error ? err.message : String(err)}`);
188
- }
189
183
  }
190
184
  }
191
185
 
package/src/wrap.ts CHANGED
@@ -102,6 +102,30 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
102
102
  Object.defineProperty(wrapper, 'name', { value: fn.name || opts.functionName, configurable: true });
103
103
  Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
104
104
 
105
+ // Copy all own properties from original function to wrapper.
106
+ // This is critical for Express apps (app.get, app.listen, etc.),
107
+ // class constructors with static methods, and other functions with properties.
108
+ for (const key of Object.getOwnPropertyNames(fn)) {
109
+ if (key === 'name' || key === 'length' || key === 'prototype' || key === 'caller' || key === 'arguments') continue;
110
+ try {
111
+ const desc = Object.getOwnPropertyDescriptor(fn, key);
112
+ if (desc) Object.defineProperty(wrapper, key, desc);
113
+ } catch {
114
+ // Some properties may not be configurable
115
+ }
116
+ }
117
+ // Also copy symbol properties
118
+ for (const sym of Object.getOwnPropertySymbols(fn)) {
119
+ try {
120
+ const desc = Object.getOwnPropertyDescriptor(fn, sym);
121
+ if (desc) Object.defineProperty(wrapper, sym, desc);
122
+ } catch {}
123
+ }
124
+ // Copy prototype for constructor functions
125
+ if (fn.prototype && fn.prototype !== Object.prototype) {
126
+ wrapper.prototype = fn.prototype;
127
+ }
128
+
105
129
  // Mark as wrapped to prevent double-wrapping
106
130
  (wrapper as any)[TRICKLE_WRAPPED] = true;
107
131