trickle-observe 0.2.58 → 0.2.59

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.
@@ -747,7 +747,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
747
747
  }
748
748
  // Add React component render tracker if needed
749
749
  if (bodyInsertions.length > 0) {
750
- prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` const rec = { kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 };`, ` if (props !== undefined && props !== null && typeof props === 'object') {`, ` try {`, ` const propKeys = Object.keys(props).filter(k => k !== 'children');`, ` const propSample = {};`, ` for (const k of propKeys.slice(0, 10)) {`, ` const v = props[k];`, ` const t = typeof v;`, ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`, ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`, ` else if (v === null || v === undefined) propSample[k] = v;`, ` else if (Array.isArray(v)) propSample[k] = '[' + t + '[' + v.length + ']]';`, ` else if (t === 'function') propSample[k] = '[fn]';`, ` else propSample[k] = '[object]';`, ` }`, ` rec.props = propSample;`, ` rec.propKeys = propKeys;`, ` } catch(e2) {}`, ` }`, ` __trickle_appendFileSync(f, JSON.stringify(rec) + '\\n');`, ` } catch(e) {}`, `}`);
750
+ prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `if (!globalThis.__trickle_react_prev_props) { globalThis.__trickle_react_prev_props = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` const rec = { kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 };`, ` if (props !== undefined && props !== null && typeof props === 'object') {`, ` try {`, ` const propKeys = Object.keys(props).filter(k => k !== 'children');`, ` const propSample = {};`, ` for (const k of propKeys.slice(0, 10)) {`, ` const v = props[k];`, ` const t = typeof v;`, ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`, ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`, ` else if (v === null || v === undefined) propSample[k] = v;`, ` else if (Array.isArray(v)) propSample[k] = '[arr:' + v.length + ']';`, ` else if (t === 'function') propSample[k] = '[fn]';`, ` else propSample[k] = '[object]';`, ` }`, ` rec.props = propSample;`, ` rec.propKeys = propKeys;`, ` // Detect which props changed vs previous render`, ` const prevProps = globalThis.__trickle_react_prev_props.get(key);`, ` if (prevProps && count > 1) {`, ` const changedProps = [];`, ` const allKeys = new Set([...Object.keys(prevProps), ...Object.keys(propSample)]);`, ` for (const k of allKeys) {`, ` const prev = prevProps[k];`, ` const curr = propSample[k];`, ` if (String(prev) !== String(curr)) {`, ` changedProps.push({ key: k, from: prev, to: curr });`, ` }`, ` }`, ` if (changedProps.length > 0) rec.changedProps = changedProps;`, ` }`, ` globalThis.__trickle_react_prev_props.set(key, propSample);`, ` } catch(e2) {}`, ` }`, ` __trickle_appendFileSync(f, JSON.stringify(rec) + '\\n');`, ` } catch(e) {}`, `}`);
751
751
  }
752
752
  // Add React hook tracker if needed
753
753
  if (hookInsertions.length > 0) {
@@ -741,7 +741,7 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
741
741
  }
742
742
  // Add React component render tracker if needed
743
743
  if (bodyInsertions.length > 0) {
744
- prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` const rec = { kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 };`, ` if (props !== undefined && props !== null && typeof props === 'object') {`, ` try {`, ` const propKeys = Object.keys(props).filter(k => k !== 'children');`, ` const propSample = {};`, ` for (const k of propKeys.slice(0, 10)) {`, ` const v = props[k];`, ` const t = typeof v;`, ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`, ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`, ` else if (v === null || v === undefined) propSample[k] = v;`, ` else if (Array.isArray(v)) propSample[k] = '[' + t + '[' + v.length + ']]';`, ` else if (t === 'function') propSample[k] = '[fn]';`, ` else propSample[k] = '[object]';`, ` }`, ` rec.props = propSample;`, ` rec.propKeys = propKeys;`, ` } catch(e2) {}`, ` }`, ` __trickle_appendFileSync(f, JSON.stringify(rec) + '\\n');`, ` } catch(e) {}`, `}`);
744
+ prefixLines.push(`if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`, `if (!globalThis.__trickle_react_prev_props) { globalThis.__trickle_react_prev_props = new Map(); }`, `function __trickle_rc(name, line, props) {`, ` try {`, ` const key = ${JSON.stringify(filename)} + ':' + line;`, ` const count = (globalThis.__trickle_react_renders.get(key) || 0) + 1;`, ` globalThis.__trickle_react_renders.set(key, count);`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` const f = __trickle_join(dir, 'variables.jsonl');`, ` const rec = { kind: 'react_render', file: ${JSON.stringify(filename)}, line: line, component: name, renderCount: count, timestamp: Date.now() / 1000 };`, ` if (props !== undefined && props !== null && typeof props === 'object') {`, ` try {`, ` const propKeys = Object.keys(props).filter(k => k !== 'children');`, ` const propSample = {};`, ` for (const k of propKeys.slice(0, 10)) {`, ` const v = props[k];`, ` const t = typeof v;`, ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`, ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`, ` else if (v === null || v === undefined) propSample[k] = v;`, ` else if (Array.isArray(v)) propSample[k] = '[arr:' + v.length + ']';`, ` else if (t === 'function') propSample[k] = '[fn]';`, ` else propSample[k] = '[object]';`, ` }`, ` rec.props = propSample;`, ` rec.propKeys = propKeys;`, ` // Detect which props changed vs previous render`, ` const prevProps = globalThis.__trickle_react_prev_props.get(key);`, ` if (prevProps && count > 1) {`, ` const changedProps = [];`, ` const allKeys = new Set([...Object.keys(prevProps), ...Object.keys(propSample)]);`, ` for (const k of allKeys) {`, ` const prev = prevProps[k];`, ` const curr = propSample[k];`, ` if (String(prev) !== String(curr)) {`, ` changedProps.push({ key: k, from: prev, to: curr });`, ` }`, ` }`, ` if (changedProps.length > 0) rec.changedProps = changedProps;`, ` }`, ` globalThis.__trickle_react_prev_props.set(key, propSample);`, ` } catch(e2) {}`, ` }`, ` __trickle_appendFileSync(f, JSON.stringify(rec) + '\\n');`, ` } catch(e) {}`, `}`);
745
745
  }
746
746
  // Add React hook tracker if needed
747
747
  if (hookInsertions.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -202,6 +202,43 @@ describe('Correct function body brace detection', () => {
202
202
  });
203
203
  });
204
204
 
205
+ // ── Re-render cause detection ─────────────────────────────────────────────────
206
+
207
+ describe('Re-render cause detection', () => {
208
+ it('emits changedProps tracking code in transformed output', () => {
209
+ const code = `function Card({ count, label }) { return null; }`;
210
+ const out = transformTsx(code);
211
+ assert.ok(out, 'should transform');
212
+ // Should include the prev_props map and comparison logic
213
+ assert.ok(out!.includes('__trickle_react_prev_props'), 'should initialize prev_props map');
214
+ assert.ok(out!.includes('changedProps'), 'should include changedProps detection');
215
+ });
216
+
217
+ it('includes prev props comparison logic in __trickle_rc', () => {
218
+ const code = `function UserCard({ name, age }) { return null; }`;
219
+ const out = transformTsx(code);
220
+ assert.ok(out, 'should transform');
221
+ assert.ok(out!.includes('prevProps'), 'should reference prevProps');
222
+ assert.ok(out!.includes('globalThis.__trickle_react_prev_props.set'), 'should store current props as prev');
223
+ });
224
+
225
+ it('stores previous props keyed by component file+line', () => {
226
+ const code = `function Button({ disabled, onClick }) { return null; }`;
227
+ const out = transformTsx(code);
228
+ assert.ok(out, 'should transform');
229
+ // The key used for prev_props storage should be the same as the render key
230
+ assert.ok(out!.includes('globalThis.__trickle_react_prev_props.get(key)'), 'should retrieve prev props by key');
231
+ });
232
+
233
+ it('does not emit prev_props tracking in .ts files', () => {
234
+ const code = `function helper({ x, y }) { return x + y; }`;
235
+ const out = transformTs(code);
236
+ if (out) {
237
+ assert.ok(!out.includes('__trickle_react_prev_props'), 'should NOT track prev props in .ts files');
238
+ }
239
+ });
240
+ });
241
+
205
242
  // ── React hook observability ──────────────────────────────────────────────────
206
243
 
207
244
  describe('React hook observability', () => {
@@ -799,6 +799,7 @@ function transformEsmSource(
799
799
  if (bodyInsertions.length > 0) {
800
800
  prefixLines.push(
801
801
  `if (!globalThis.__trickle_react_renders) { globalThis.__trickle_react_renders = new Map(); }`,
802
+ `if (!globalThis.__trickle_react_prev_props) { globalThis.__trickle_react_prev_props = new Map(); }`,
802
803
  `function __trickle_rc(name, line, props) {`,
803
804
  ` try {`,
804
805
  ` const key = ${JSON.stringify(filename)} + ':' + line;`,
@@ -818,12 +819,27 @@ function transformEsmSource(
818
819
  ` if (t === 'string') propSample[k] = v.length > 40 ? v.slice(0, 40) + '...' : v;`,
819
820
  ` else if (t === 'number' || t === 'boolean') propSample[k] = v;`,
820
821
  ` else if (v === null || v === undefined) propSample[k] = v;`,
821
- ` else if (Array.isArray(v)) propSample[k] = '[' + t + '[' + v.length + ']]';`,
822
+ ` else if (Array.isArray(v)) propSample[k] = '[arr:' + v.length + ']';`,
822
823
  ` else if (t === 'function') propSample[k] = '[fn]';`,
823
824
  ` else propSample[k] = '[object]';`,
824
825
  ` }`,
825
826
  ` rec.props = propSample;`,
826
827
  ` rec.propKeys = propKeys;`,
828
+ ` // Detect which props changed vs previous render`,
829
+ ` const prevProps = globalThis.__trickle_react_prev_props.get(key);`,
830
+ ` if (prevProps && count > 1) {`,
831
+ ` const changedProps = [];`,
832
+ ` const allKeys = new Set([...Object.keys(prevProps), ...Object.keys(propSample)]);`,
833
+ ` for (const k of allKeys) {`,
834
+ ` const prev = prevProps[k];`,
835
+ ` const curr = propSample[k];`,
836
+ ` if (String(prev) !== String(curr)) {`,
837
+ ` changedProps.push({ key: k, from: prev, to: curr });`,
838
+ ` }`,
839
+ ` }`,
840
+ ` if (changedProps.length > 0) rec.changedProps = changedProps;`,
841
+ ` }`,
842
+ ` globalThis.__trickle_react_prev_props.set(key, propSample);`,
827
843
  ` } catch(e2) {}`,
828
844
  ` }`,
829
845
  ` __trickle_appendFileSync(f, JSON.stringify(rec) + '\\n');`,