trickle-observe 0.2.2 → 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/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/trace-var.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { TypeNode } from './types';
|
|
20
|
+
import { inferType } from './type-inference';
|
|
21
|
+
import { hashType } from './type-hash';
|
|
22
|
+
|
|
23
|
+
/** Where to write variable observations */
|
|
24
|
+
let varsFilePath = '';
|
|
25
|
+
let debugMode = false;
|
|
26
|
+
|
|
27
|
+
/** Cache: "file:line:varName:typeHash" → already written */
|
|
28
|
+
const varCache = new Set<string>();
|
|
29
|
+
|
|
30
|
+
/** Batch buffer for writing — avoids one fs.appendFileSync per variable */
|
|
31
|
+
let varBuffer: string[] = [];
|
|
32
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
33
|
+
const FLUSH_INTERVAL_MS = 1000;
|
|
34
|
+
const MAX_BUFFER_SIZE = 100;
|
|
35
|
+
|
|
36
|
+
export interface VariableObservation {
|
|
37
|
+
kind: 'variable';
|
|
38
|
+
varName: string;
|
|
39
|
+
line: number;
|
|
40
|
+
module: string;
|
|
41
|
+
file: string;
|
|
42
|
+
type: TypeNode;
|
|
43
|
+
typeHash: string;
|
|
44
|
+
sample: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the variable tracer.
|
|
49
|
+
* Called once during observe-register setup.
|
|
50
|
+
*/
|
|
51
|
+
export function initVarTracer(opts: { debug?: boolean } = {}): void {
|
|
52
|
+
debugMode = opts.debug === true;
|
|
53
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
54
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
55
|
+
varsFilePath = path.join(dir, 'variables.jsonl');
|
|
56
|
+
|
|
57
|
+
if (debugMode) {
|
|
58
|
+
console.log(`[trickle/vars] Variable tracing enabled → ${varsFilePath}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Trace a variable's runtime value.
|
|
64
|
+
* Called by injected code after each variable declaration.
|
|
65
|
+
*
|
|
66
|
+
* @param value - The variable's current value (already computed)
|
|
67
|
+
* @param varName - The variable name in source
|
|
68
|
+
* @param line - Line number in source file
|
|
69
|
+
* @param moduleName - Module name (derived from filename)
|
|
70
|
+
* @param filePath - Absolute path to source file
|
|
71
|
+
*/
|
|
72
|
+
export function traceVar(
|
|
73
|
+
value: unknown,
|
|
74
|
+
varName: string,
|
|
75
|
+
line: number,
|
|
76
|
+
moduleName: string,
|
|
77
|
+
filePath: string,
|
|
78
|
+
): void {
|
|
79
|
+
// Auto-initialize if not yet done (needed for Vite/Vitest worker processes)
|
|
80
|
+
if (!varsFilePath) {
|
|
81
|
+
initVarTracer();
|
|
82
|
+
if (!varsFilePath) return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const type = inferType(value, 3);
|
|
87
|
+
|
|
88
|
+
// Create a stable hash for dedup
|
|
89
|
+
const dummyArgs: TypeNode = { kind: 'tuple', elements: [] };
|
|
90
|
+
const typeHash = hashType(dummyArgs, type);
|
|
91
|
+
|
|
92
|
+
const cacheKey = `${filePath}:${line}:${varName}:${typeHash}`;
|
|
93
|
+
if (varCache.has(cacheKey)) return;
|
|
94
|
+
varCache.add(cacheKey);
|
|
95
|
+
|
|
96
|
+
const sample = sanitizeVarSample(value);
|
|
97
|
+
|
|
98
|
+
const observation: VariableObservation = {
|
|
99
|
+
kind: 'variable',
|
|
100
|
+
varName,
|
|
101
|
+
line,
|
|
102
|
+
module: moduleName,
|
|
103
|
+
file: filePath,
|
|
104
|
+
type,
|
|
105
|
+
typeHash,
|
|
106
|
+
sample,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Buffer the write
|
|
110
|
+
varBuffer.push(JSON.stringify(observation));
|
|
111
|
+
|
|
112
|
+
if (varBuffer.length >= MAX_BUFFER_SIZE) {
|
|
113
|
+
flushVarBuffer();
|
|
114
|
+
} else if (!flushTimer) {
|
|
115
|
+
flushTimer = setTimeout(() => {
|
|
116
|
+
flushVarBuffer();
|
|
117
|
+
}, FLUSH_INTERVAL_MS);
|
|
118
|
+
if (flushTimer && typeof flushTimer === 'object' && 'unref' in flushTimer) {
|
|
119
|
+
flushTimer.unref();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Never crash user's app
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Flush buffered variable observations to disk.
|
|
129
|
+
*/
|
|
130
|
+
function flushVarBuffer(): void {
|
|
131
|
+
if (flushTimer) {
|
|
132
|
+
clearTimeout(flushTimer);
|
|
133
|
+
flushTimer = null;
|
|
134
|
+
}
|
|
135
|
+
if (varBuffer.length === 0) return;
|
|
136
|
+
|
|
137
|
+
const lines = varBuffer.join('\n') + '\n';
|
|
138
|
+
varBuffer = [];
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
fs.appendFileSync(varsFilePath, lines);
|
|
142
|
+
} catch {
|
|
143
|
+
// Never crash user's app
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sanitize a variable value for safe serialization.
|
|
149
|
+
* More aggressive truncation than function samples since there are many more variables.
|
|
150
|
+
*/
|
|
151
|
+
function sanitizeVarSample(value: unknown, depth: number = 2): unknown {
|
|
152
|
+
if (depth <= 0) return '[truncated]';
|
|
153
|
+
if (value === null || value === undefined) return value;
|
|
154
|
+
|
|
155
|
+
const t = typeof value;
|
|
156
|
+
if (t === 'string') {
|
|
157
|
+
const s = value as string;
|
|
158
|
+
return s.length > 100 ? s.substring(0, 100) + '...' : s;
|
|
159
|
+
}
|
|
160
|
+
if (t === 'number' || t === 'boolean') return value;
|
|
161
|
+
if (t === 'bigint') return String(value);
|
|
162
|
+
if (t === 'symbol') return String(value);
|
|
163
|
+
if (t === 'function') return `[Function: ${(value as Function).name || 'anonymous'}]`;
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
return value.slice(0, 3).map(item => sanitizeVarSample(item, depth - 1));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (t === 'object') {
|
|
170
|
+
if (value instanceof Date) return value.toISOString();
|
|
171
|
+
if (value instanceof RegExp) return String(value);
|
|
172
|
+
if (value instanceof Error) return { error: value.message };
|
|
173
|
+
if (value instanceof Map) return `[Map: ${value.size} entries]`;
|
|
174
|
+
if (value instanceof Set) return `[Set: ${value.size} items]`;
|
|
175
|
+
if (value instanceof Promise) return '[Promise]';
|
|
176
|
+
|
|
177
|
+
const obj = value as Record<string, unknown>;
|
|
178
|
+
const result: Record<string, unknown> = {};
|
|
179
|
+
const keys = Object.keys(obj).slice(0, 10);
|
|
180
|
+
for (const key of keys) {
|
|
181
|
+
try {
|
|
182
|
+
result[key] = sanitizeVarSample(obj[key], depth - 1);
|
|
183
|
+
} catch {
|
|
184
|
+
result[key] = '[unreadable]';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return String(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Flush on process exit — use 'exit' event (synchronous, fires even on process.exit())
|
|
194
|
+
// because Vitest workers and forked processes may exit without 'beforeExit'.
|
|
195
|
+
// flushVarBuffer uses fs.appendFileSync so it's safe in the 'exit' handler.
|
|
196
|
+
if (typeof process !== 'undefined' && process.on) {
|
|
197
|
+
const exitFlush = () => { flushVarBuffer(); };
|
|
198
|
+
process.on('exit', exitFlush);
|
|
199
|
+
process.on('beforeExit', exitFlush);
|
|
200
|
+
process.on('SIGTERM', exitFlush);
|
|
201
|
+
process.on('SIGINT', exitFlush);
|
|
202
|
+
}
|
package/src/type-inference.ts
CHANGED
|
@@ -187,11 +187,21 @@ function inferArray(arr: unknown[], depth: number, seen: WeakSet<object>): TypeN
|
|
|
187
187
|
return { kind: 'array', element: unifyTypes(elementTypes) };
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
const MAX_PROPERTIES = 20;
|
|
191
|
+
|
|
190
192
|
function inferPlainObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
|
|
191
193
|
const properties: Record<string, TypeNode> = {};
|
|
192
194
|
const keys = Object.keys(obj);
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
// Skip internal/private properties (common in Node.js built-in objects like
|
|
197
|
+
// http.Server, streams, etc.) — they make generated types unusably verbose.
|
|
198
|
+
const publicKeys = keys.filter(k => !k.startsWith('_'));
|
|
199
|
+
// If filtering removed everything, use all keys (it's a plain data object with _ keys)
|
|
200
|
+
const effectiveKeys = publicKeys.length > 0 ? publicKeys : keys;
|
|
201
|
+
// Cap the number of properties to keep types manageable
|
|
202
|
+
const cappedKeys = effectiveKeys.slice(0, MAX_PROPERTIES);
|
|
203
|
+
|
|
204
|
+
for (const key of cappedKeys) {
|
|
195
205
|
try {
|
|
196
206
|
properties[key] = infer((obj as Record<string, unknown>)[key], depth - 1, seen);
|
|
197
207
|
} catch {
|