trickle-observe 0.1.0 → 0.2.0

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/index.d.ts CHANGED
@@ -62,3 +62,4 @@ export { flush } from './transport';
62
62
  export { instrumentExpress, trickleMiddleware } from './express';
63
63
  export { observe, observeFn } from './observe';
64
64
  export type { ObserveOpts } from './observe';
65
+ export { wrapFunction } from './wrap';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.observeFn = exports.observe = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
3
+ exports.wrapFunction = exports.observeFn = exports.observe = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
4
4
  exports.configure = configure;
5
5
  exports.trickle = trickle;
6
6
  exports.trickleHandler = trickleHandler;
@@ -170,3 +170,5 @@ Object.defineProperty(exports, "trickleMiddleware", { enumerable: true, get: fun
170
170
  var observe_1 = require("./observe");
171
171
  Object.defineProperty(exports, "observe", { enumerable: true, get: function () { return observe_1.observe; } });
172
172
  Object.defineProperty(exports, "observeFn", { enumerable: true, get: function () { return observe_1.observeFn; } });
173
+ var wrap_2 = require("./wrap");
174
+ Object.defineProperty(exports, "wrapFunction", { enumerable: true, get: function () { return wrap_2.wrapFunction; } });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Vite plugin for trickle observation.
3
+ *
4
+ * Integrates into Vite's (and Vitest's) transform pipeline to wrap
5
+ * user functions with trickle observation — the same thing observe-register.ts
6
+ * does for Node's Module._compile, but for Vite/Vitest.
7
+ *
8
+ * Usage in vitest.config.ts:
9
+ *
10
+ * import { tricklePlugin } from 'trickle-observe/vite-plugin';
11
+ * export default defineConfig({
12
+ * plugins: [tricklePlugin()],
13
+ * });
14
+ *
15
+ * Or via CLI:
16
+ *
17
+ * trickle run vitest run tests/
18
+ */
19
+ export interface TricklePluginOptions {
20
+ /** Substrings — only observe files whose paths contain one of these */
21
+ include?: string[];
22
+ /** Substrings — skip files whose paths contain one of these */
23
+ exclude?: string[];
24
+ /** Backend URL (default: http://localhost:4888) */
25
+ backendUrl?: string;
26
+ /** Enable debug logging */
27
+ debug?: boolean;
28
+ }
29
+ export declare function tricklePlugin(options?: TricklePluginOptions): {
30
+ name: string;
31
+ enforce: "pre";
32
+ transform(code: string, id: string): {
33
+ code: string;
34
+ map: null;
35
+ } | null;
36
+ };
37
+ export default tricklePlugin;
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ /**
3
+ * Vite plugin for trickle observation.
4
+ *
5
+ * Integrates into Vite's (and Vitest's) transform pipeline to wrap
6
+ * user functions with trickle observation — the same thing observe-register.ts
7
+ * does for Node's Module._compile, but for Vite/Vitest.
8
+ *
9
+ * Usage in vitest.config.ts:
10
+ *
11
+ * import { tricklePlugin } from 'trickle-observe/vite-plugin';
12
+ * export default defineConfig({
13
+ * plugins: [tricklePlugin()],
14
+ * });
15
+ *
16
+ * Or via CLI:
17
+ *
18
+ * trickle run vitest run tests/
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.tricklePlugin = tricklePlugin;
25
+ const path_1 = __importDefault(require("path"));
26
+ function tricklePlugin(options = {}) {
27
+ const include = options.include
28
+ ?? (process.env.TRICKLE_OBSERVE_INCLUDE
29
+ ? process.env.TRICKLE_OBSERVE_INCLUDE.split(',').map(s => s.trim())
30
+ : []);
31
+ const exclude = options.exclude
32
+ ?? (process.env.TRICKLE_OBSERVE_EXCLUDE
33
+ ? process.env.TRICKLE_OBSERVE_EXCLUDE.split(',').map(s => s.trim())
34
+ : []);
35
+ const backendUrl = options.backendUrl
36
+ ?? process.env.TRICKLE_BACKEND_URL
37
+ ?? 'http://localhost:4888';
38
+ const debug = options.debug
39
+ ?? (process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true');
40
+ function shouldTransform(id) {
41
+ // Only JS/TS files
42
+ const ext = path_1.default.extname(id).toLowerCase();
43
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts'].includes(ext))
44
+ return false;
45
+ // Skip node_modules
46
+ if (id.includes('node_modules'))
47
+ return false;
48
+ // Skip trickle internals
49
+ if (id.includes('trickle-observe') || id.includes('client-js/'))
50
+ return false;
51
+ // Include filter
52
+ if (include.length > 0) {
53
+ if (!include.some(p => id.includes(p)))
54
+ return false;
55
+ }
56
+ // Exclude filter
57
+ if (exclude.length > 0) {
58
+ if (exclude.some(p => id.includes(p)))
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+ return {
64
+ name: 'trickle-observe',
65
+ enforce: 'pre',
66
+ transform(code, id) {
67
+ if (!shouldTransform(id))
68
+ return null;
69
+ const moduleName = path_1.default.basename(id).replace(/\.[jt]sx?$/, '');
70
+ const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug);
71
+ if (transformed === code)
72
+ return null;
73
+ if (debug) {
74
+ console.log(`[trickle/vite] Transformed ${moduleName} (${id})`);
75
+ }
76
+ return { code: transformed, map: null };
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * Find the closing brace position for a function body starting at `openBrace`.
82
+ */
83
+ function findClosingBrace(source, openBrace) {
84
+ let depth = 1;
85
+ let pos = openBrace + 1;
86
+ while (pos < source.length && depth > 0) {
87
+ const ch = source[pos];
88
+ if (ch === '{') {
89
+ depth++;
90
+ }
91
+ else if (ch === '}') {
92
+ depth--;
93
+ if (depth === 0)
94
+ return pos;
95
+ }
96
+ else if (ch === '"' || ch === "'" || ch === '`') {
97
+ const quote = ch;
98
+ pos++;
99
+ while (pos < source.length) {
100
+ if (source[pos] === '\\') {
101
+ pos++;
102
+ }
103
+ else if (source[pos] === quote)
104
+ break;
105
+ else if (quote === '`' && source[pos] === '$' && pos + 1 < source.length && source[pos + 1] === '{') {
106
+ pos += 2;
107
+ let tDepth = 1;
108
+ while (pos < source.length && tDepth > 0) {
109
+ if (source[pos] === '{')
110
+ tDepth++;
111
+ else if (source[pos] === '}')
112
+ tDepth--;
113
+ pos++;
114
+ }
115
+ continue;
116
+ }
117
+ pos++;
118
+ }
119
+ }
120
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
121
+ while (pos < source.length && source[pos] !== '\n')
122
+ pos++;
123
+ }
124
+ else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
125
+ pos += 2;
126
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
127
+ pos++;
128
+ pos++;
129
+ }
130
+ pos++;
131
+ }
132
+ return -1;
133
+ }
134
+ /**
135
+ * Transform ESM source code to wrap function declarations with trickle observation.
136
+ *
137
+ * Prepends an import of the wrap helper, then inserts wrapper calls after
138
+ * each function declaration body — same approach as observe-register's
139
+ * transformCjsSource but using ESM imports.
140
+ */
141
+ function transformEsmSource(source, filename, moduleName, backendUrl, debug) {
142
+ // Match top-level and nested function declarations (including async, export)
143
+ const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
144
+ const insertions = [];
145
+ let match;
146
+ while ((match = funcRegex.exec(source)) !== null) {
147
+ const name = match[1];
148
+ if (name === 'require' || name === 'exports' || name === 'module')
149
+ continue;
150
+ const afterMatch = match.index + match[0].length;
151
+ const openBrace = source.indexOf('{', afterMatch);
152
+ if (openBrace === -1)
153
+ continue;
154
+ // Extract parameter names
155
+ const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
156
+ const paramNames = paramStr
157
+ ? paramStr.split(',').map(p => {
158
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
159
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
160
+ return '';
161
+ return trimmed;
162
+ }).filter(Boolean)
163
+ : [];
164
+ const closeBrace = findClosingBrace(source, openBrace);
165
+ if (closeBrace === -1)
166
+ continue;
167
+ insertions.push({ position: closeBrace + 1, name, paramNames });
168
+ }
169
+ // Also match arrow functions assigned to const/let/var
170
+ const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
171
+ while ((match = arrowRegex.exec(source)) !== null) {
172
+ const name = match[1];
173
+ const openBrace = source.indexOf('{', match.index + match[0].length - 1);
174
+ if (openBrace === -1)
175
+ continue;
176
+ // Extract param names from the arrow function
177
+ const arrowStr = match[0];
178
+ const arrowParamMatch = arrowStr.match(/=\s*(?:async\s+)?(?:\(([^)]*)\)|([a-zA-Z_$][a-zA-Z0-9_$]*))\s*(?::\s*[^=]+?)?\s*=>/);
179
+ let paramNames = [];
180
+ if (arrowParamMatch) {
181
+ const paramStr = (arrowParamMatch[1] || arrowParamMatch[2] || '').trim();
182
+ if (paramStr) {
183
+ paramNames = paramStr.split(',').map(p => {
184
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
185
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...'))
186
+ return '';
187
+ return trimmed;
188
+ }).filter(Boolean);
189
+ }
190
+ }
191
+ const closeBrace = findClosingBrace(source, openBrace);
192
+ if (closeBrace === -1)
193
+ continue;
194
+ insertions.push({ position: closeBrace + 1, name, paramNames });
195
+ }
196
+ if (insertions.length === 0)
197
+ return source;
198
+ // Prepend ESM import of the wrap helper + configure transport
199
+ const prefix = [
200
+ `import { wrapFunction as __trickle_wrapFn } from 'trickle-observe';`,
201
+ `import { configure as __trickle_configure } from 'trickle-observe';`,
202
+ `__trickle_configure({ backendUrl: ${JSON.stringify(backendUrl)}, batchIntervalMs: 2000, debug: ${debug}, enabled: true, environment: 'node' });`,
203
+ `function __trickle_wrap(fn, name, paramNames) {`,
204
+ ` const opts = {`,
205
+ ` functionName: name,`,
206
+ ` module: ${JSON.stringify(moduleName)},`,
207
+ ` trackArgs: true,`,
208
+ ` trackReturn: true,`,
209
+ ` sampleRate: 1,`,
210
+ ` maxDepth: 5,`,
211
+ ` environment: 'node',`,
212
+ ` enabled: true,`,
213
+ ` };`,
214
+ ` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
215
+ ` return __trickle_wrapFn(fn, opts);`,
216
+ `}`,
217
+ '',
218
+ ].join('\n');
219
+ // Insert wrapper calls after each function body (reverse order)
220
+ let result = source;
221
+ for (let i = insertions.length - 1; i >= 0; i--) {
222
+ const { position, name, paramNames } = insertions[i];
223
+ const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
224
+ const wrapperCall = `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`;
225
+ result = result.slice(0, position) + wrapperCall + result.slice(position);
226
+ }
227
+ return prefix + result;
228
+ }
229
+ exports.default = tricklePlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,8 @@
11
11
  "./observe-esm": "./observe-esm.mjs",
12
12
  "./auto": "./auto.js",
13
13
  "./auto-env": "./auto-env.js",
14
- "./auto-esm": "./auto-esm.mjs"
14
+ "./auto-esm": "./auto-esm.mjs",
15
+ "./vite-plugin": "./dist/vite-plugin.js"
15
16
  },
16
17
  "scripts": {
17
18
  "build": "tsc",
package/src/index.ts CHANGED
@@ -197,3 +197,4 @@ export { flush } from './transport';
197
197
  export { instrumentExpress, trickleMiddleware } from './express';
198
198
  export { observe, observeFn } from './observe';
199
199
  export type { ObserveOpts } from './observe';
200
+ export { wrapFunction } from './wrap';
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Vite plugin for trickle observation.
3
+ *
4
+ * Integrates into Vite's (and Vitest's) transform pipeline to wrap
5
+ * user functions with trickle observation — the same thing observe-register.ts
6
+ * does for Node's Module._compile, but for Vite/Vitest.
7
+ *
8
+ * Usage in vitest.config.ts:
9
+ *
10
+ * import { tricklePlugin } from 'trickle-observe/vite-plugin';
11
+ * export default defineConfig({
12
+ * plugins: [tricklePlugin()],
13
+ * });
14
+ *
15
+ * Or via CLI:
16
+ *
17
+ * trickle run vitest run tests/
18
+ */
19
+
20
+ import path from 'path';
21
+
22
+ export interface TricklePluginOptions {
23
+ /** Substrings — only observe files whose paths contain one of these */
24
+ include?: string[];
25
+ /** Substrings — skip files whose paths contain one of these */
26
+ exclude?: string[];
27
+ /** Backend URL (default: http://localhost:4888) */
28
+ backendUrl?: string;
29
+ /** Enable debug logging */
30
+ debug?: boolean;
31
+ }
32
+
33
+ export function tricklePlugin(options: TricklePluginOptions = {}) {
34
+ const include = options.include
35
+ ?? (process.env.TRICKLE_OBSERVE_INCLUDE
36
+ ? process.env.TRICKLE_OBSERVE_INCLUDE.split(',').map(s => s.trim())
37
+ : []);
38
+ const exclude = options.exclude
39
+ ?? (process.env.TRICKLE_OBSERVE_EXCLUDE
40
+ ? process.env.TRICKLE_OBSERVE_EXCLUDE.split(',').map(s => s.trim())
41
+ : []);
42
+ const backendUrl = options.backendUrl
43
+ ?? process.env.TRICKLE_BACKEND_URL
44
+ ?? 'http://localhost:4888';
45
+ const debug = options.debug
46
+ ?? (process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true');
47
+
48
+ function shouldTransform(id: string): boolean {
49
+ // Only JS/TS files
50
+ const ext = path.extname(id).toLowerCase();
51
+ if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts'].includes(ext)) return false;
52
+
53
+ // Skip node_modules
54
+ if (id.includes('node_modules')) return false;
55
+
56
+ // Skip trickle internals
57
+ if (id.includes('trickle-observe') || id.includes('client-js/')) return false;
58
+
59
+ // Include filter
60
+ if (include.length > 0) {
61
+ if (!include.some(p => id.includes(p))) return false;
62
+ }
63
+
64
+ // Exclude filter
65
+ if (exclude.length > 0) {
66
+ if (exclude.some(p => id.includes(p))) return false;
67
+ }
68
+
69
+ return true;
70
+ }
71
+
72
+ return {
73
+ name: 'trickle-observe',
74
+ enforce: 'pre' as const,
75
+
76
+ transform(code: string, id: string) {
77
+ if (!shouldTransform(id)) return null;
78
+
79
+ const moduleName = path.basename(id).replace(/\.[jt]sx?$/, '');
80
+ const transformed = transformEsmSource(code, id, moduleName, backendUrl, debug);
81
+ if (transformed === code) return null;
82
+
83
+ if (debug) {
84
+ console.log(`[trickle/vite] Transformed ${moduleName} (${id})`);
85
+ }
86
+
87
+ return { code: transformed, map: null };
88
+ },
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Find the closing brace position for a function body starting at `openBrace`.
94
+ */
95
+ function findClosingBrace(source: string, openBrace: number): number {
96
+ let depth = 1;
97
+ let pos = openBrace + 1;
98
+ while (pos < source.length && depth > 0) {
99
+ const ch = source[pos];
100
+ if (ch === '{') {
101
+ depth++;
102
+ } else if (ch === '}') {
103
+ depth--;
104
+ if (depth === 0) return pos;
105
+ } else if (ch === '"' || ch === "'" || ch === '`') {
106
+ const quote = ch;
107
+ pos++;
108
+ while (pos < source.length) {
109
+ if (source[pos] === '\\') { pos++; }
110
+ else if (source[pos] === quote) break;
111
+ else if (quote === '`' && source[pos] === '$' && pos + 1 < source.length && source[pos + 1] === '{') {
112
+ pos += 2;
113
+ let tDepth = 1;
114
+ while (pos < source.length && tDepth > 0) {
115
+ if (source[pos] === '{') tDepth++;
116
+ else if (source[pos] === '}') tDepth--;
117
+ pos++;
118
+ }
119
+ continue;
120
+ }
121
+ pos++;
122
+ }
123
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '/') {
124
+ while (pos < source.length && source[pos] !== '\n') pos++;
125
+ } else if (ch === '/' && pos + 1 < source.length && source[pos + 1] === '*') {
126
+ pos += 2;
127
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
128
+ pos++;
129
+ }
130
+ pos++;
131
+ }
132
+ return -1;
133
+ }
134
+
135
+ /**
136
+ * Transform ESM source code to wrap function declarations with trickle observation.
137
+ *
138
+ * Prepends an import of the wrap helper, then inserts wrapper calls after
139
+ * each function declaration body — same approach as observe-register's
140
+ * transformCjsSource but using ESM imports.
141
+ */
142
+ function transformEsmSource(
143
+ source: string,
144
+ filename: string,
145
+ moduleName: string,
146
+ backendUrl: string,
147
+ debug: boolean,
148
+ ): string {
149
+ // Match top-level and nested function declarations (including async, export)
150
+ const funcRegex = /^[ \t]*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm;
151
+ const insertions: Array<{ position: number; name: string; paramNames: string[] }> = [];
152
+ let match;
153
+
154
+ while ((match = funcRegex.exec(source)) !== null) {
155
+ const name = match[1];
156
+ if (name === 'require' || name === 'exports' || name === 'module') continue;
157
+
158
+ const afterMatch = match.index + match[0].length;
159
+ const openBrace = source.indexOf('{', afterMatch);
160
+ if (openBrace === -1) continue;
161
+
162
+ // Extract parameter names
163
+ const paramStr = source.slice(afterMatch, openBrace).replace(/[()]/g, '').trim();
164
+ const paramNames = paramStr
165
+ ? paramStr.split(',').map(p => {
166
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
167
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...')) return '';
168
+ return trimmed;
169
+ }).filter(Boolean)
170
+ : [];
171
+
172
+ const closeBrace = findClosingBrace(source, openBrace);
173
+ if (closeBrace === -1) continue;
174
+
175
+ insertions.push({ position: closeBrace + 1, name, paramNames });
176
+ }
177
+
178
+ // Also match arrow functions assigned to const/let/var
179
+ const arrowRegex = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+?)?\s*=>\s*\{/gm;
180
+
181
+ while ((match = arrowRegex.exec(source)) !== null) {
182
+ const name = match[1];
183
+ const openBrace = source.indexOf('{', match.index + match[0].length - 1);
184
+ if (openBrace === -1) continue;
185
+
186
+ // Extract param names from the arrow function
187
+ const arrowStr = match[0];
188
+ const arrowParamMatch = arrowStr.match(/=\s*(?:async\s+)?(?:\(([^)]*)\)|([a-zA-Z_$][a-zA-Z0-9_$]*))\s*(?::\s*[^=]+?)?\s*=>/);
189
+ let paramNames: string[] = [];
190
+ if (arrowParamMatch) {
191
+ const paramStr = (arrowParamMatch[1] || arrowParamMatch[2] || '').trim();
192
+ if (paramStr) {
193
+ paramNames = paramStr.split(',').map(p => {
194
+ const trimmed = p.trim().split('=')[0].trim().split(':')[0].trim();
195
+ if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('...')) return '';
196
+ return trimmed;
197
+ }).filter(Boolean);
198
+ }
199
+ }
200
+
201
+ const closeBrace = findClosingBrace(source, openBrace);
202
+ if (closeBrace === -1) continue;
203
+
204
+ insertions.push({ position: closeBrace + 1, name, paramNames });
205
+ }
206
+
207
+ if (insertions.length === 0) return source;
208
+
209
+ // Prepend ESM import of the wrap helper + configure transport
210
+ const prefix = [
211
+ `import { wrapFunction as __trickle_wrapFn } from 'trickle-observe';`,
212
+ `import { configure as __trickle_configure } from 'trickle-observe';`,
213
+ `__trickle_configure({ backendUrl: ${JSON.stringify(backendUrl)}, batchIntervalMs: 2000, debug: ${debug}, enabled: true, environment: 'node' });`,
214
+ `function __trickle_wrap(fn, name, paramNames) {`,
215
+ ` const opts = {`,
216
+ ` functionName: name,`,
217
+ ` module: ${JSON.stringify(moduleName)},`,
218
+ ` trackArgs: true,`,
219
+ ` trackReturn: true,`,
220
+ ` sampleRate: 1,`,
221
+ ` maxDepth: 5,`,
222
+ ` environment: 'node',`,
223
+ ` enabled: true,`,
224
+ ` };`,
225
+ ` if (paramNames && paramNames.length) opts.paramNames = paramNames;`,
226
+ ` return __trickle_wrapFn(fn, opts);`,
227
+ `}`,
228
+ '',
229
+ ].join('\n');
230
+
231
+ // Insert wrapper calls after each function body (reverse order)
232
+ let result = source;
233
+ for (let i = insertions.length - 1; i >= 0; i--) {
234
+ const { position, name, paramNames } = insertions[i];
235
+ const paramNamesArg = paramNames.length > 0 ? JSON.stringify(paramNames) : 'null';
236
+ const wrapperCall = `\ntry{${name}=__trickle_wrap(${name},'${name}',${paramNamesArg})}catch(__e){}\n`;
237
+ result = result.slice(0, position) + wrapperCall + result.slice(position);
238
+ }
239
+
240
+ return prefix + result;
241
+ }
242
+
243
+ export default tricklePlugin;